Files
plezy/lib/media/live_tv_support.dart
T

249 lines
12 KiB
Dart

import '../models/livetv_capture_buffer.dart';
import '../models/livetv_channel.dart';
import '../models/livetv_dvr.dart';
import '../models/livetv_lineup.dart';
import '../models/livetv_program.dart';
import '../models/livetv_server_status.dart';
import '../models/livetv_session.dart';
import '../models/media_grab_operation.dart';
import '../models/media_grabber_device.dart';
import '../models/media_provider_info.dart';
import '../models/media_subscription.dart';
class LiveTvActivityResult<T> {
final T value;
final String? activityUuid;
const LiveTvActivityResult({required this.value, this.activityUuid});
}
/// Program info captured when a live session starts. Plex's tune response
/// carries the airing program; Jellyfin streams the channel without a
/// program-scoped session, so its sessions report [none].
class LiveProgramInfo {
/// Program identifier for timeline reporting (Plex program ratingKey).
final String? id;
final int? durationMs;
/// Program start, epoch seconds.
final int? beginsAt;
const LiveProgramInfo({this.id, this.durationMs, this.beginsAt});
static const none = LiveProgramInfo();
}
/// One live-TV playback session, produced by [LiveTvSupport.startPlayback].
///
/// This is the backend-neutral handle the player drives; the
/// `client is PlexClient` branches that used to live in the player's live
/// methods are the per-backend implementations of this interface:
///
/// - **Plex** tunes a DVR transcode session ([captureBuffer] non-null when
/// the server has seekable history) and rebuilds its stream URL for
/// time-shift; heartbeats go to `/:/timeline` and return capture-buffer
/// updates.
/// - **Jellyfin** negotiates one direct stream URL up front; no time-shift,
/// heartbeats go through `/Sessions/Playing*`, and [recover] re-uses the
/// same URL.
///
/// Sessions are immutable handles: every operation that changes the playable
/// stream returns a URL or a fresh session for the caller to adopt, so the
/// player's runtime state has a single adoption point.
abstract class LiveTvPlaybackSession {
LiveProgramInfo get program;
/// Seekable-history snapshot from session start. Heartbeats may return
/// fresher ones ([reportTimeline]); the caller owns tracking the current
/// value.
CaptureBuffer? get captureBuffer;
/// Whether [streamUrlAt] supports a non-null offset.
bool get canTimeShift;
/// Build the playable stream URL. [offsetSeconds] positions the stream
/// that many seconds from the capture-buffer origin — watch-from-start and
/// time-shift seek are the same operation; `null` plays the live edge.
/// Returns `null` on failure, or when an offset is requested but
/// unsupported.
Future<String?> streamUrlAt({int? offsetSeconds});
/// Send a playback heartbeat (`'playing'` / `'paused'` / `'stopped'`).
/// [positionMs] is elapsed playback time; [durationMs] the program
/// duration when known. Returns an updated capture buffer when the backend
/// supplies one, null otherwise.
Future<CaptureBuffer?> reportTimeline({required String state, required int positionMs, required int durationMs});
/// Re-establish playback after stream death. Plex re-tunes (the previous
/// capture session expires while the player exhausts its reconnect
/// attempts) applying the degradation flags; Jellyfin returns itself —
/// the session-less URL is simply re-opened. Returns `null` on failure.
Future<LiveTvPlaybackSession?> recover({required bool directStream, required bool directStreamAudio});
}
enum FavoriteChannelPersistenceMode {
/// A single write replaces the full backend account's favorite list.
sharedFullList,
/// Writes must only include the favorites owned by this server/source.
serverSlice,
}
class LiveTvStreamResolution {
final String url;
final String? playSessionId;
const LiveTvStreamResolution({required this.url, this.playSessionId});
}
/// Backend-neutral live-TV operations. Implementations are obtained via
/// [MediaServerClient.liveTv]; the getter returns `null` when the server has no
/// live-TV support configured.
///
/// Plex servers expose multiple per-DVR lineups (`/livetv/dvrs`), Jellyfin
/// servers expose a single flat channel list. The interface flattens both:
/// callers that need DVR identity for Plex's per-lineup channel fetch use
/// [fetchDvrs]; callers that only need the channel list pass the optional
/// [lineup] (Plex provider identifier) to [fetchChannels].
///
/// Stream URL resolution differs sharply by backend: Plex's DVR allocates a
/// transcode session and returns a session-scoped path that requires
/// follow-up calls (`tuneChannel` + `buildLiveStreamPath`). Jellyfin returns a
/// direct-play URL. [resolveStreamUrl] returns the Jellyfin URL directly;
/// Plex callers use the existing `client + dvrKey` plumbing inside the player.
abstract class LiveTvSupport {
/// Fast probe — `true` when this server has live-TV configured. Plex calls
/// `/livetv/dvrs` and returns true when any DVR exists; Jellyfin probes
/// `/LiveTv/Channels?limit=1`.
Future<bool> isAvailable();
/// Plex returns one entry per configured DVR; Jellyfin returns an empty
/// list (it has no per-DVR partitioning).
Future<List<LiveTvDvr>> fetchDvrs();
/// Channel list. Plex callers may pass [lineup] (the EPG provider
/// identifier from a DVR's lineup) to scope to a specific provider's
/// channels. Jellyfin ignores [lineup] and returns the flat list.
Future<List<LiveTvChannel>> fetchChannels({String? lineup});
/// EPG / programs grid covering [from]..[to]. Plex queries
/// `/livetv/dvrs/{dvrKey}/grid`; Jellyfin queries `/LiveTv/Programs`.
Future<List<LiveTvProgram>> fetchSchedule({DateTime? from, DateTime? to});
/// Resolve a playable stream URL for [channelKey].
///
/// Jellyfin returns a negotiated stream URL plus the play session id. Plex
/// returns `null` because its stream URL is only valid after a tune;
/// playback callers use [startPlayback], which owns that difference.
Future<LiveTvStreamResolution?> resolveStreamUrl(String channelKey, {String? dvrKey});
/// Start a playback session for [channelKey] — the single entry the player
/// uses for initial launch and channel switching. Plex requires [dvrKey]
/// (tune + transcode-session setup); Jellyfin ignores it and negotiates a
/// direct stream URL. Returns `null` when the channel can't be started.
Future<LiveTvPlaybackSession?> startPlayback(String channelKey, {String? dvrKey});
/// Source URI to stamp into [FavoriteChannel] entries. Plex uses
/// `server://{machineId}/{providerId}` so its cloud-synced favorites are
/// keyed per EPG provider. Jellyfin uses `server://{serverId}/jellyfin`
/// (no provider concept).
Future<String> buildFavoriteChannelSource({String? lineup});
/// Runtime store identity used to avoid fetching/writing a shared favorite
/// backend more than once. Plex is cloud/account-scoped; Jellyfin is
/// server-user scoped.
String get favoriteStoreKey;
FavoriteChannelPersistenceMode get favoritePersistenceMode;
/// Read the user's favorite channels for this server. Plex pulls from the
/// cloud-synced list; Jellyfin queries `IsFavorite=true` with locally
/// stored ordering.
Future<List<FavoriteChannel>> fetchFavoriteChannels();
/// Persist the favorites list (and order, where supported). Plex pushes
/// to its cloud sync endpoint; Jellyfin POSTs/DELETEs the
/// `/Users/{userId}/FavoriteItems/{channelId}` flag and saves the order
/// locally.
Future<void> setFavoriteChannels(List<FavoriteChannel> channels);
Future<LiveTvServerStatus> fetchLiveTvServerStatus();
Future<LiveTvDvr?> fetchDvr(String dvrId);
Future<LiveTvActivityResult<LiveTvDvr?>> createDvr({
required List<String> devices,
required List<String> lineups,
String? language,
String? country,
String? postalCode,
});
Future<void> deleteDvr(String dvrId);
Future<void> updateDvrPrefs(String dvrId, Map<String, Object?> prefs);
Future<void> attachDeviceToDvr(String dvrId, String deviceId);
Future<void> detachDeviceFromDvr(String dvrId, String deviceId);
Future<void> addLineupToDvr(String dvrId, String lineupUri);
Future<void> removeLineupFromDvr(String dvrId, String lineupUri);
Future<LiveTvActivityResult<void>> reloadGuide(String dvrId);
Future<void> cancelGuideReload(String dvrId);
Future<List<MediaGrabber>> fetchGrabbers({String? protocol});
Future<List<MediaGrabberDevice>> fetchGrabberDevices();
Future<LiveTvActivityResult<List<MediaGrabberDevice>>> discoverGrabberDevices();
Future<MediaGrabberDevice?> fetchGrabberDevice(String deviceId);
Future<MediaGrabberDevice?> addGrabberDevice(String uri, {String? grabberId});
Future<void> updateGrabberDevice(String deviceId, {bool? enabled, String? title});
Future<void> deleteGrabberDevice(String deviceId);
Future<List<MediaGrabberDeviceChannel>> fetchGrabberDeviceChannels(String deviceId);
Future<LiveTvActivityResult<MediaGrabberDevice?>> scanGrabberDevice(
String deviceId, {
String? source,
Map<String, Object?> prefs = const {},
String? network,
String? country,
});
Future<MediaGrabberDevice?> cancelGrabberDeviceScan(String deviceId);
Future<MediaGrabberDevice?> saveGrabberDeviceChannelMap(String deviceId, MediaGrabberChannelMapRequest request);
Future<void> updateGrabberDevicePrefs(String deviceId, Map<String, Object?> prefs);
String buildGrabberDeviceThumbUrl(String deviceId, int version);
Future<List<LiveTvCountry>> fetchEpgCountries();
Future<List<LiveTvLanguage>> fetchEpgLanguages();
Future<List<LiveTvRegion>> fetchEpgRegions(String country, String epgId);
Future<LiveTvLineupResult> fetchEpgLineups(String country, String epgId, {String? postalCode, String? region});
Future<List<LiveTvChannel>> fetchEpgChannelsForLineup(String lineupUri);
Future<List<LiveTvLineup>> fetchEpgChannelsForLineups(List<String> lineupUris);
Future<List<ChannelMapping>> computeEpgChannelMap({required String deviceUri, required String lineupUri});
Future<LiveTvActivityResult<Map<String, dynamic>?>> findBestLineup({
required String deviceUri,
required String lineupGroupUri,
});
Future<List<SubscriptionTemplate>> getSubscriptionTemplate(String guid);
Future<List<MediaSubscription>> fetchRecordingRules({bool includeGrabs = true, bool includeStorage = true});
Future<MediaSubscription?> fetchRecordingRule(
String subscriptionId, {
bool includeGrabs = true,
bool includeStorage = true,
});
Future<MediaSubscription?> createRecordingRule(MediaSubscriptionCreateRequest request);
Future<MediaSubscription?> updateRecordingRule(String subscriptionId, Map<String, Object?> prefs);
Future<void> deleteRecordingRule(String subscriptionId);
Future<MediaSubscription?> moveRecordingRule(String subscriptionId, {String? afterSubscriptionId});
Future<void> processRecordingRules();
Future<List<MediaGrabOperation>> fetchScheduledRecordings();
Future<void> cancelGrab(String operationId);
Future<List<MediaSubscription>> fetchSubscriptionMapping({
required String providerId,
required List<String> ratingKeys,
bool includeStorage = true,
});
Future<List<MediaProviderInfo>> fetchMediaProviders();
Future<void> registerMediaProvider(String url);
Future<void> refreshMediaProviders();
Future<void> unregisterMediaProvider(String providerId);
Future<List<LiveTvSession>> fetchLiveTvSessionsDetailed();
Future<LiveTvSession?> fetchLiveTvSession(String sessionId);
Uri buildNotificationWebSocketUri({List<String>? filters});
Uri buildNotificationEventSourceUri({List<String>? filters});
}