303 lines
11 KiB
Dart
303 lines
11 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../media/media_item.dart';
|
|
import '../media/media_kind.dart';
|
|
import '../media/media_playlist.dart';
|
|
import '../models/plex/play_queue_response.dart';
|
|
import '../providers/multi_server_provider.dart';
|
|
import '../providers/playback_state_provider.dart';
|
|
import '../utils/video_player_navigation.dart';
|
|
import '../i18n/strings.g.dart';
|
|
import 'media_list_playback_launcher.dart';
|
|
import 'plex_client.dart';
|
|
|
|
// Re-export the result types so existing imports of this file keep working.
|
|
export 'media_list_playback_launcher.dart' show PlayQueueResult, PlayQueueSuccess, PlayQueueEmpty, PlayQueueError;
|
|
|
|
/// Plex-specific play queue launcher.
|
|
///
|
|
/// Centralizes the common pattern of:
|
|
/// 1. Creating a play queue via Plex's server-side `/playQueues` resource
|
|
/// 2. Setting up [PlaybackStateProvider]
|
|
/// 3. Navigating to the video player
|
|
/// 4. Handling errors with appropriate feedback
|
|
///
|
|
/// Implements [MediaListPlaybackLauncher.launchFromCollectionOrPlaylist] for
|
|
/// the backend-neutral entry point. Plex-only flows
|
|
/// ([launchFromPlaylistItem], [launchShuffledShow], [launchFromFolder]) live
|
|
/// directly on this class because they have no Jellyfin equivalent.
|
|
class PlexPlayQueueLauncher extends MediaListPlaybackLauncher {
|
|
final BuildContext context;
|
|
final PlexClient client;
|
|
final String? serverId;
|
|
final String? serverName;
|
|
|
|
PlexPlayQueueLauncher({required this.context, required this.client, this.serverId, this.serverName});
|
|
|
|
/// Resolve the right [PlexClient] for [item]'s server and build a launcher.
|
|
/// Falls back to the first available Plex client when [item] doesn't carry
|
|
/// a `serverId`.
|
|
factory PlexPlayQueueLauncher.forContext(BuildContext context, Object item) {
|
|
final String? itemServerId;
|
|
final String? itemServerName;
|
|
if (item is MediaItem) {
|
|
itemServerId = item.serverId;
|
|
itemServerName = item.serverName;
|
|
} else if (item is MediaPlaylist) {
|
|
itemServerId = item.serverId;
|
|
itemServerName = item.serverName;
|
|
} else {
|
|
itemServerId = null;
|
|
itemServerName = null;
|
|
}
|
|
|
|
final provider = Provider.of<MultiServerProvider>(context, listen: false);
|
|
PlexClient? plexClient;
|
|
if (itemServerId != null) {
|
|
// Plex-only: server-side `/playQueues` resource has no Jellyfin equivalent.
|
|
plexClient = provider.getPlexClientForServer(itemServerId);
|
|
}
|
|
if (plexClient == null) {
|
|
// Fall back to the first online Plex client.
|
|
for (final id in provider.onlineServerIds) {
|
|
final c = provider.getPlexClientForServer(id);
|
|
if (c != null) {
|
|
plexClient = c;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (plexClient == null) {
|
|
throw Exception(t.errors.noClientAvailable);
|
|
}
|
|
return PlexPlayQueueLauncher(
|
|
context: context,
|
|
client: plexClient,
|
|
serverId: itemServerId,
|
|
serverName: itemServerName,
|
|
);
|
|
}
|
|
|
|
/// Launch playback from a collection or playlist.
|
|
///
|
|
/// Accepts a [MediaItem] (collection) or [MediaPlaylist]. Typed as [Object]
|
|
/// because Dart has no nominal union type.
|
|
@override
|
|
Future<PlayQueueResult> launchFromCollectionOrPlaylist({
|
|
required Object item,
|
|
required bool shuffle,
|
|
MediaItem? startItem,
|
|
bool showLoadingIndicator = true,
|
|
}) async {
|
|
final facts = MediaListPlaybackLauncher.classifyItem(item);
|
|
if (facts == null) {
|
|
return PlayQueueError(Exception('Item must be either a collection or playlist'));
|
|
}
|
|
final ratingKey = facts.id;
|
|
final itemServerId = facts.serverId ?? serverId;
|
|
final itemServerName = facts.serverName ?? serverName;
|
|
|
|
return executeWithLoading(
|
|
context: context,
|
|
showLoading: showLoadingIndicator,
|
|
actionLabel: t.common.shuffle,
|
|
execute: (dismissLoading) async {
|
|
PlayQueueResponse? playQueue;
|
|
// Plex's `key` param positions the queue's selected item — passed
|
|
// through when the caller wants playback to start at a specific
|
|
// entry. Ignored on shuffle (the server picks a random head).
|
|
final selectedKey = (!shuffle && startItem != null) ? '/library/metadata/${startItem.id}' : null;
|
|
|
|
if (facts.isCollection) {
|
|
final machineId = client.config.machineIdentifier ?? await client.getMachineIdentifier();
|
|
|
|
if (machineId == null) {
|
|
throw Exception('Could not get server machine identifier');
|
|
}
|
|
|
|
final collectionUri = 'server://$machineId/com.plexapp.plugins.library/library/collections/$ratingKey';
|
|
playQueue = await client.createPlayQueue(
|
|
uri: collectionUri,
|
|
type: 'video',
|
|
shuffle: shuffle ? 1 : 0,
|
|
key: selectedKey,
|
|
);
|
|
} else {
|
|
// For playlists, use playlistID parameter
|
|
playQueue = await client.createPlayQueue(
|
|
playlistID: int.parse(ratingKey),
|
|
type: 'video',
|
|
shuffle: shuffle ? 1 : 0,
|
|
key: selectedKey,
|
|
);
|
|
}
|
|
|
|
// If the queue is empty, try fetching it again with getPlayQueue
|
|
if (playQueue != null && (playQueue.items == null || playQueue.items!.isEmpty)) {
|
|
final fetchedQueue = await client.getPlayQueue(playQueue.playQueueID);
|
|
if (fetchedQueue != null && fetchedQueue.items != null && fetchedQueue.items!.isNotEmpty) {
|
|
playQueue = fetchedQueue;
|
|
}
|
|
}
|
|
|
|
// Close loading dialog before navigating to the player
|
|
await dismissLoading();
|
|
|
|
return _launchFromQueue(
|
|
playQueue: playQueue,
|
|
ratingKey: ratingKey,
|
|
serverId: itemServerId,
|
|
serverName: itemServerName,
|
|
selectedItem: selectedKey != null ? _resolveSelectedMediaItem(playQueue) : null,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Launch playback from a playlist starting at a specific item.
|
|
Future<PlayQueueResult> launchFromPlaylistItem({
|
|
required MediaPlaylist playlist,
|
|
required MediaItem selectedItem,
|
|
bool showLoadingIndicator = true,
|
|
}) async {
|
|
return executeWithLoading(
|
|
context: context,
|
|
showLoading: showLoadingIndicator,
|
|
actionLabel: t.common.play,
|
|
execute: (dismissLoading) async {
|
|
// Plex's createPlayQueue takes the metadata `key` (`/library/metadata/{id}`),
|
|
// not the bare ratingKey. Construct it from the MediaItem id.
|
|
final selectedKey = '/library/metadata/${selectedItem.id}';
|
|
final playQueue = await client.createPlayQueue(
|
|
playlistID: int.parse(playlist.id),
|
|
type: 'video',
|
|
key: selectedKey,
|
|
);
|
|
|
|
// Close loading dialog before navigating to the player
|
|
await dismissLoading();
|
|
|
|
return _launchFromQueue(
|
|
playQueue: playQueue,
|
|
ratingKey: playlist.id,
|
|
serverId: serverId,
|
|
serverName: serverName,
|
|
selectedItem: _resolveSelectedMediaItem(playQueue),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Launch shuffled playback for a show or season.
|
|
@override
|
|
Future<PlayQueueResult> launchShuffledShow({required MediaItem metadata, bool showLoadingIndicator = true}) async {
|
|
final kind = metadata.kind;
|
|
|
|
if (kind != MediaKind.show && kind != MediaKind.season) {
|
|
return PlayQueueError(Exception('Shuffle play only works for shows and seasons'));
|
|
}
|
|
|
|
return executeWithLoading(
|
|
context: context,
|
|
showLoading: showLoadingIndicator,
|
|
actionLabel: t.common.shuffle,
|
|
execute: (dismissLoading) async {
|
|
// Determine the rating key for the play queue
|
|
String showRatingKey;
|
|
if (kind == MediaKind.show) {
|
|
showRatingKey = metadata.id;
|
|
} else {
|
|
// For seasons, we need the show's rating key
|
|
if (metadata.parentId == null) {
|
|
throw Exception('Season is missing parentRatingKey');
|
|
}
|
|
showRatingKey = metadata.parentId!;
|
|
}
|
|
|
|
final playQueue = await client.createShowPlayQueue(showRatingKey: showRatingKey, shuffle: 1);
|
|
|
|
// Close loading dialog before navigating to the player
|
|
await dismissLoading();
|
|
|
|
return _launchFromQueue(
|
|
playQueue: playQueue,
|
|
ratingKey: showRatingKey,
|
|
serverId: metadata.serverId ?? serverId,
|
|
serverName: metadata.serverName ?? serverName,
|
|
copyServerInfo: true,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Launch playback from a folder's contents.
|
|
Future<PlayQueueResult> launchFromFolder({
|
|
required String folderKey,
|
|
required bool shuffle,
|
|
bool showLoadingIndicator = true,
|
|
}) async {
|
|
return executeWithLoading(
|
|
context: context,
|
|
showLoading: showLoadingIndicator,
|
|
actionLabel: shuffle ? t.common.shuffle : t.common.play,
|
|
execute: (dismissLoading) async {
|
|
final folderUri = await client.buildFolderUri(folderKey);
|
|
|
|
var playQueue = await client.createPlayQueue(uri: folderUri, type: 'video', shuffle: shuffle ? 1 : 0);
|
|
|
|
if (playQueue != null && (playQueue.items == null || playQueue.items!.isEmpty)) {
|
|
final fetchedQueue = await client.getPlayQueue(playQueue.playQueueID);
|
|
if (fetchedQueue != null && fetchedQueue.items != null && fetchedQueue.items!.isNotEmpty) {
|
|
playQueue = fetchedQueue;
|
|
}
|
|
}
|
|
|
|
await dismissLoading();
|
|
|
|
return _launchFromQueue(playQueue: playQueue, ratingKey: folderKey, serverId: serverId, serverName: serverName);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Core method to launch playback from a play queue.
|
|
Future<PlayQueueResult> _launchFromQueue({
|
|
required PlayQueueResponse? playQueue,
|
|
required String ratingKey,
|
|
String? serverId,
|
|
String? serverName,
|
|
MediaItem? selectedItem,
|
|
bool copyServerInfo = false,
|
|
}) async {
|
|
if (playQueue == null || playQueue.items == null || playQueue.items!.isEmpty) {
|
|
return const PlayQueueEmpty();
|
|
}
|
|
|
|
if (!context.mounted) return const PlayQueueError('Context not mounted');
|
|
|
|
final playbackState = context.read<PlaybackStateProvider>();
|
|
playbackState.setPlayQueueWindowFetcher(client.getPlayQueue);
|
|
await playbackState.setPlaybackFromPlayQueue(playQueue, ratingKey);
|
|
|
|
if (!context.mounted) return const PlayQueueError('Context not mounted');
|
|
|
|
var itemToPlay = selectedItem ?? playQueue.items!.first;
|
|
|
|
if (copyServerInfo && serverId != null) {
|
|
itemToPlay = itemToPlay.copyWith(serverId: serverId, serverName: serverName);
|
|
}
|
|
|
|
await navigateToVideoPlayer(context, metadata: itemToPlay);
|
|
|
|
return const PlayQueueSuccess();
|
|
}
|
|
}
|
|
|
|
/// Pull the selected item from a play queue. The selected item is identified
|
|
/// by `playQueueSelectedItemID`; returns null if the queue has no selection.
|
|
MediaItem? _resolveSelectedMediaItem(PlayQueueResponse? playQueue) {
|
|
return playQueue?.selectedItem;
|
|
}
|