Native tvOS 17+ SwiftUI app that turns Plex libraries into always-on "live TV" channels.
Channels are computed client-side with deterministic schedule offsets, so no backend is required.
MVP demo recording: https://youtu.be/FkOh5230XSM
- PIN-based Plex linking via
plex.tv/link(PlexLinkService,LinkLoginView). - Keychain token persistence (
KeychainStorage) with automatic session bootstrap (AuthState). - Smart server selection from Plex resources:
- prioritizes HTTPS
- prefers non-relay connections
- includes fallback URLs
- Session refresh flow triggered by
.plexSessionShouldRefresh.
Channelmodel stores:- channel metadata
- source libraries
- compiled media lineup
- schedule anchor for 24/7 looping
- Wall-clock schedule computation:
playbackPosition(at:)nowPlaying,nextUp,timeRemaining
- Persistent storage using JSON files (atomic writes) in
ChannelStore. - Channel ordering persisted separately (
channel_order.json).
- Multi-library channel draft model (
ChannelDraft) with per-library specs (LibraryFilterSpec). - Nested boolean filter trees (
FilterGroup+FilterRule) with Match All / Match Any semantics. - Live count updates and preview compilation in
ChannelBuilderViewModel. - Sort support via
PlexSortCatalogandSortDescriptor. - Deterministic randomization with seeded RNG (
SeededRandomNumberGenerator) for stable shuffle behavior.
- Actor-based query engine (
PlexQueryBuilder) for thread-safe, concurrent media workflows. - Supports:
- server-side / local hybrid filtering
- filter matching for movie and TV metadata
- two-step TV handling (show-level + episode-level workflows)
- Disk + memory cache for library snapshots (
LibraryMediaCacheStore) to reduce repeated Plex pagination/fetch cost.
ChannelsCoordinatorcentralizes playback presentation state.ChannelPlayerViewsupports:- direct play attempts first
- HLS transcoding fallback
- adaptive bitrate downshift logic
- forced new transcoder sessions after stalls/failures
- session start/stop reporting back to Plex
PlexServicestream planning includes remux/transcode control and MKV-specific strategy.
- Candidate-based artwork selection (background/poster/logo) on
Channel.Media. - Cached image pipeline (
PlexImageCache,CachedAsyncImage) with memory/cost limits. - tvOS-focused interaction patterns:
- focus-aware channel rows
- play/pause command menus
- full-screen modal flows
PlexChannelsTV/PlexChannelsTVApp.swift: app composition and dependency wiringPlexChannelsTV/Application/AuthState.swift: auth/session lifecycle orchestrationPlexChannelsTV/Networking/PlexLinkService.swift: Plex PIN/link + resources/account callsPlexChannelsTV/Services/PlexService.swift: Plex API/session/stream/artwork servicePlexChannelsTV/Services/PlexQueryBuilder.swift: actor-based filter/query compilerPlexChannelsTV/Services/ChannelStore.swift: channel CRUD + persistencePlexChannelsTV/Models/Channel.swift: channel/schedule/media modelPlexChannelsTV/Views/ChannelBuilder/*: channel wizard UI + statePlexChannelsTV/Views/ChannelPlayerView.swift: playback runtime
PlexKit(SPM):https://github.com/lcharlick/PlexKit(resolved at1.7.2in this repo).
This codebase is easiest to integrate as a "channel engine" layer, then bind it to your existing UI/navigation.
- Add
PlexKitto your existing app target. - Ensure your app supports async/await and SwiftUI state propagation (
ObservableObject/ environment or DI container).
Copy or refactor these modules into your app:
Models:Channel, filter models,ChannelPlaybackRequestServices:PlexService,PlexQueryBuilder,PlexFilterCatalog,PlexSortCatalog,ChannelStore,LibraryMediaCacheStore,ChannelsCoordinatorNetworking:PlexLinkService, link models/parser/headersUtilities:KeychainStorage, image cache, loggers, seeded RNG
If you already have equivalents, keep your APIs and adapt these implementations behind protocols.
- Create a single shared
PlexService. - Create
PlexLinkServiceusing the same Plex client identity values you use elsewhere. - Initialize
AuthState(plexService:linkService:). - Use your existing login entrypoint to trigger
requestPin->pollPin->completeLink.
Minimal composition pattern:
let plexService = PlexService()
let linkService = PlexLinkService(
clientIdentifier: plexService.clientIdentifier,
product: "Your App Name",
version: appVersion,
device: "Apple TV",
platform: "tvOS",
deviceName: "Apple TV"
)
let authState = AuthState(plexService: plexService, linkService: linkService)
let channelStore = ChannelStore()- Use
ChannelBuilderViewModel+ChannelBuilderFlowView(or your own UI) to collect filters/sort/options. - For each selected library, call
PlexQueryBuilder.buildChannelMedia(...). - Merge and dedupe by media ID, then create
Channel. - Persist via
channelStore.addChannel(_:).
- Use
ChannelsCoordinatoras the single source of truth for playback requests. - Present
ChannelPlayerViewfrom coordinator state. - Feed
ChannelPlayerViewwithPlexService+ChannelStoreso it can refresh channels and recover streams.
- Preserve
scheduleAnchorand channel lineup order. - Compute playback from wall clock using
Channel.playbackPosition(at:). - If you implement random order, keep seeded shuffle to avoid user-visible reorder drift between app launches.
ChannelStorewrites JSON under documents.LibraryMediaCacheStorewrites library snapshots under app support.- Keep these stores on-device; no server needed for schedule state.
- Treat Plex auth and channel engine as independent layers:
- auth layer yields active server/token context
- channel layer compiles and persists channel state
- playback layer resolves direct/transcode stream plans
- Avoid duplicate Plex sessions across modules. Share one
PlexServiceper signed-in profile. - If your app is multi-profile, namespace persisted channel/cache files by profile/server identifier.
- If you are iOS-first, keep the service/model layer and replace tvOS-specific view/focus behaviors.
- Open
PlexChannelsTV.xcodeprojin Xcode. - Build target:
PlexChannelsTV(tvOS). - Example test command:
xcodebuild test \
-project PlexChannelsTV.xcodeproj \
-scheme PlexChannelsTV \
-destination 'platform=tvOS Simulator,name=Apple TV'No license file is currently included in this repository. Add one before redistributing.