-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
A Playa Named Gus is one multiplatform SwiftUI app target with five supported destinations (iOS, iPadOS, tvOS, visionOS, macOS) plus a second watchOS application target (GusWatch). It uses the pure SwiftUI lifecycle (@main struct GusApp: App, no AppDelegate).
The core engineering rule is: use Apple/system frameworks before writing custom code or adding dependencies. The only runtime dependencies are jellyfin-sdk-swift and — on iOS/iPadOS only — the Readium toolkit for in-app EPUB reading (ADR 0009). XcodeGen is a build-time tool, not a shipped dependency.
| Concern | Project choice |
|---|---|
| Navigation |
NavigationStack, NavigationSplitView, TabView
|
| State and dependency injection | Observation @Observable plus SwiftUI @Environment
|
| Video playback | AVKit VideoPlayer and AVPlayerViewController
|
| Audio playback |
AVPlayer queue engine over the Jellyfin universal audio endpoint |
| Images |
AsyncImage plus shared URLCache
|
| Secrets | Security framework SecItem*
|
| Persistence |
Codable, FileManager, UserDefaults
|
| Downloads | Background URLSessionDownloadTask
|
| Now Playing | MediaPlayer |
| System integration | Core Spotlight, NSUserActivity (Handoff), App Intents, CarPlay templates |
| Diagnostics | OSLog, OSSignposter, MetricKit (no third-party analytics) |
| Logging | OSLog Logger (subsystem dev.ericslutz.gus) |
| Strings | String Catalog |
| Immersive visionOS UI | RealityKit and ImmersiveSpace
|
| EPUB rendering (iOS/iPadOS) | Readium toolkit (ADR 0009 dependency exception) |
Sources/
App/ @main entry point + RootView (signed-out/signed-in switch), App Intents
Models/ Codable value types (ServerConnection, StoredUser, ContentLink, ...)
Providers/ Media-server provider boundary (Jellyfin impl behind a protocol)
Services/ Stateless helpers and persistence: client factory, Keychain, URL builders,
downloads, diagnostics, content-rating gate, sockets, Spotlight, watch relay
Stores/ @Observable state objects (the "view models")
Features/ Connect, Home, Item, Player, Search, Settings, Music, Photos, LiveTV, Books
SharedUI/ Reusable SwiftUI views, display helpers, GlassStyle (Liquid Glass)
Platform/ ALL #if os(...) divergence: routing, modifiers, commands, availability,
deep links, Handoff user activities
Immersive/ visionOS Gus Cinema and stereoscopic rendering
TopShelf/ tvOS Top Shelf extension (content-aware Continue Watching)
CarPlay/ iOS-only CarPlay audio templates
Watch/ watchOS companion app UI (GusWatch target)
Jellyfin-specific API and DTO assumptions are isolated behind a provider abstraction (Sources/Providers, ADR 0008). Feature stores talk to a MediaProviderSession rather than calling the Jellyfin SDK directly, so A Playa Named Gus stays Jellyfin-only at launch while preserving a clean route to future backends such as Emby. The Jellyfin implementation lives under Providers/Jellyfin.
GusApp creates AppModel and injects it into the environment. AppModel owns known servers, stored users, and the optional current session.
RootView switches on AppModel.currentSession:
-
nilshowsConnectFlowView. - a signed-in session creates and injects
SessionStore.
Feature stores such as HomeStore, LibraryStore, SearchStore, ItemDetailStore, PlaybackStore, and AudioPlayerStore are @Observable @MainActor classes created inside a view's .task. They use async/await through the provider session. State is Observation-framework based (no Combine @Published, no DI container).
The connect flow normalizes a server URL, creates a tokenless JellyfinClient, fetches public system info (following redirects), and persists a ServerConnection.
Sign-in calls Jellyfin authentication APIs. Access tokens are stored in Keychain using an account key of serverID:userID. Token-free StoredUser records are persisted through ServerStore (Codable JSON in Application Support).
On launch, AppModel.restoreLastSession() rebuilds an authenticated client from the stored token when available.
Platform navigation divergence is centralized in Sources/Platform/RootContainer.swift.
- iPhone and tvOS use tab-style navigation; iPad, macOS, and visionOS use split navigation.
- Users can customize which sections appear and their order via
NavigationPreferencesStore(Settings). - Item/library navigation uses typed routes registered once per
NavigationStackroot. - Fixed app destinations (
gus://home,gus://search,gus://settings) flow throughAppRoute+AppNavigationModeland are shared by URL opens, menu commands, and the tvOS Top Shelf.
Content deep links are the shared entry point for system integration (Models/ContentLink.swift, Platform/ContentLinkHandler.swift): gus://item/<id> and gus://play/<id> ride AppNavigationModel via a consume-once pending-link request, resolve through the session's provider, and present the detail sheet or player.
-
Handoff —
Platform/UserActivities.swiftpublishesNSUserActivityfrom detail/player surfaces using item ids only, never tokens. -
Core Spotlight —
Services/SpotlightIndexer.swiftdonates browsed items withserver|user|itemidentifiers, refuses other-account continuations, and deindexes on sign-out (no watchOS/tvOS). -
Siri / App Intents —
App/GusAppIntents.swiftprovides a "Play media" intent and entity search. -
tvOS Top Shelf — the
GusTopShelfextension reads a Continue Watching snapshot from thegroup.dev.ericslutz.gusApp Group, written byHomeStore. -
CarPlay —
Sources/CarPlayprovides iOS-only audio templates (inert until thecarplay-audioentitlement is granted).
Video playback is pure AVKit. StreamURLBuilder POSTs Paths.getPostedPlaybackInfo with a DeviceProfile that leans direct play for everything the device's hardware genuinely decodes (DevicePlaybackCapabilities gates HEVC/AV1 on VTIsHardwareDecodeSupported and HDR ranges on AVPlayer.eligibleForHDRPlayback), falling back to HEVC-preferred fMP4 HLS transcoding with multichannel audio and manifest-delivered text subtitles only when the source needs it. PlaybackStore owns the active AVPlayer, playback state, stream selections, reporting, and teardown.
- iOS/iPadOS use
AVPlayerViewController(PiP); macOS usesAVPlayerViewin a dedicated resizable player window (GusPlayerWindowscene — not a sheet); tvOS uses the focus-engine controller via a representable; visionOS uses SwiftUIVideoPlayer. -
AVPlayerItem.externalMetadatafeeds the system title/info chrome (except macOS, which lacks the API). -
NowPlayingControllerfeedsMPNowPlayingInfoCenter/MPRemoteCommandCenter; on iOS, AVKit's automatic publishing is disabled so there is a single Now Playing writer. - Track switching is in place via
AVMediaSelectionon direct-played files (PlaybackMediaSelectionMatcher, ordinal + language matching) with a server-side rebuild fallback for transcoded streams. - At natural end, playback auto-plays the next episode (Settings toggle, default on) or dismisses the player; pause/resume report to the server immediately.
- A Settings-backed Streaming Quality picker (
PlaybackQuality: Maximum/High/Medium/Low) caps the negotiated bitrate.
Songs and audiobooks play through a separate AudioPlayerStore + AudioPlayerView (queue, shuffle/repeat, playback speed, chapters) over the Jellyfin universal audio endpoint. playerPresentation routes by MediaItem.isAudioPlayable.
Stereo playback is visionOS-only and requires direct play because server transcoding flattens stereo. Media3DDetector maps Jellyfin SBS/TAB/MVC metadata and conservative MV-HEVC hints into a Stereo3DPresentation.
- MV-HEVC spatial video uses the normal AVKit
VideoPlayerpath with a Spatial badge. - Side-by-side and top-and-bottom sources use the Gus Cinema
ImmersiveSpace: aStereoFrameRenderersplits packed frames into per-eye buffers tagged viaCMTaggedBufferGroupand rendered throughVideoMaterialwith.stereoviewing mode. - MVC is unsupported and falls back to 2D with a notice.
- Non-visionOS platforms always resolve to 2D.
Downloads are available on iOS, iPadOS, macOS, and visionOS. tvOS is excluded because app storage is purgeable and not suitable for persistent local media without a separate design. The watchOS companion has its own on-watch audio downloads under a separate budget.
The download stack uses:
-
OfflineDownloadStorefor user-facing state (status/progress/resume, 20 GB soft cap, low-space guard). -
DownloadSessionCoordinatorfor backgroundURLSessiontransfers. -
DownloadSourceResolverto choose AVPlayer-native originals or Jellyfin progressive MP4 transcodes. -
FileManagerandCodablerecords under Application Support, scoped per server/user and excluded from backup.
Platform availability is routed through Platform/DownloadsAvailability.swift so feature views stay free of #ifs.
GusWatch is a second application target (platform watchOS), embedded into the iOS app's Watch/ directory and standalone-capable (WKRunsIndependentlyOfCompanionApp). It compiles the verified shared subset — Models/, Providers/, Services/, and the non-video Stores/ (no PlaybackStore/SyncPlayStore) — plus the watch UI in Sources/Watch. Remote control rides RemoteSessionsStore + SessionsSocket (Jellyfin Sessions API); credentials hand off from the iPhone via WatchSessionRelay / WatchCredentialReceiver. See Documentation/watchos-brief.md and ADR 0010.
-
Diagnostics —
DiagnosticsHubrecords privacy-safe lifecycle markers (numeric/boolean payloads only) and OSSignposter intervals;MetricKitCollectornormalizes MetricKit payloads into localDiagnosticSummaryrecords. No third-party analytics (Documentation/AppStore/diagnostics-reliability.md). -
Family safety —
ContentRatingGate+RestrictedContentViewgate detail and playback against the Declared Age Range (Documentation/family-safety-brief.md).
Accepted ADRs live in Documentation/adr/:
- 0001 — AVKit-only playback.
- 0002 — Observation for state.
- 0003 — XcodeGen as project source of truth.
- 0004 — Swift Testing for unit tests.
- 0005 — Offline downloads background and transcode scope.
- 0006 — Stereoscopic video playback.
- 0007 — HEVC/HLS transcoding evidence.
- 0008 — Media provider boundary.
- 0009 — Readium EPUB reader (dependency exception).
- 0010 — watchOS companion.
A Playa Named Gus - an Apple-first, multiplatform Jellyfin client for iOS, iPadOS, tvOS, visionOS, and macOS with a watchOS companion - SwiftUI - © 2026 Eric Slutz - PolyForm Noncommercial 1.0.0. The repository, AGENTS.md, and Documentation/ROADMAP.md remain the canonical source of truth.
Project
Operations