Files
plezy/lib/services/play_queue_launcher.dart
T

295 lines
8.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/play_queue_response.dart';
import '../models/plex_metadata.dart';
import '../models/plex_playlist.dart';
import '../providers/playback_state_provider.dart';
import '../utils/app_logger.dart';
import '../utils/video_player_navigation.dart';
import '../i18n/strings.g.dart';
import 'plex_client.dart';
/// Result type for play queue operations
sealed class PlayQueueResult {
const PlayQueueResult();
}
class PlayQueueSuccess extends PlayQueueResult {
const PlayQueueSuccess();
}
class PlayQueueEmpty extends PlayQueueResult {
const PlayQueueEmpty();
}
class PlayQueueError extends PlayQueueResult {
final Object error;
const PlayQueueError(this.error);
}
/// Service to handle play queue creation and navigation.
///
/// Centralizes the common pattern of:
/// 1. Creating a play queue via various methods
/// 2. Setting up PlaybackStateProvider
/// 3. Navigating to the video player
/// 4. Handling errors with appropriate feedback
class PlayQueueLauncher {
final BuildContext context;
final PlexClient client;
final String? serverId;
final String? serverName;
PlayQueueLauncher({
required this.context,
required this.client,
this.serverId,
this.serverName,
});
/// Launch playback from a collection or playlist.
Future<PlayQueueResult> launchFromCollectionOrPlaylist({
required dynamic item, // PlexMetadata (collection) or PlexPlaylist
required bool shuffle,
bool showLoadingIndicator = true,
}) async {
final isCollection = item is PlexMetadata;
final isPlaylist = item is PlexPlaylist;
if (!isCollection && !isPlaylist) {
return PlayQueueError(
Exception('Item must be either a collection or playlist'),
);
}
return _executeWithLoading(
showLoading: showLoadingIndicator,
action: t.common.shuffle,
execute: () async {
final String ratingKey = item.ratingKey;
final String? itemServerId = item.serverId ?? serverId;
final String? itemServerName = item.serverName ?? serverName;
PlayQueueResponse? playQueue;
if (isCollection) {
// Get machine identifier (fetch if not cached in config)
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/${item.ratingKey}';
playQueue = await client.createPlayQueue(
uri: collectionUri,
type: 'video',
shuffle: shuffle ? 1 : 0,
);
} else {
// For playlists, use playlistID parameter
playQueue = await client.createPlayQueue(
playlistID: int.parse(item.ratingKey),
type: 'video',
shuffle: shuffle ? 1 : 0,
);
}
// 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;
}
}
return _launchFromQueue(
playQueue: playQueue,
ratingKey: ratingKey,
serverId: itemServerId,
serverName: itemServerName,
);
},
);
}
/// Launch playback from a playlist starting at a specific item.
Future<PlayQueueResult> launchFromPlaylistItem({
required PlexPlaylist playlist,
required PlexMetadata selectedItem,
bool showLoadingIndicator = true,
}) async {
return _executeWithLoading(
showLoading: showLoadingIndicator,
action: t.discover.play,
execute: () async {
final playQueue = await client.createPlayQueue(
playlistID: int.parse(playlist.ratingKey),
type: 'video',
key: selectedItem.key,
);
return _launchFromQueue(
playQueue: playQueue,
ratingKey: playlist.ratingKey,
serverId: serverId,
serverName: serverName,
selectedItem: playQueue?.selectedItem,
);
},
);
}
/// Launch shuffled playback for a show or season.
Future<PlayQueueResult> launchShuffledShow({
required PlexMetadata metadata,
bool showLoadingIndicator = true,
}) async {
final itemType = metadata.type.toLowerCase();
if (itemType != 'show' && itemType != 'season') {
return PlayQueueError(
Exception('Shuffle play only works for shows and seasons'),
);
}
return _executeWithLoading(
showLoading: showLoadingIndicator,
action: t.common.shuffle,
execute: () async {
// Determine the rating key for the play queue
String showRatingKey;
if (itemType == 'show') {
showRatingKey = metadata.ratingKey;
} else {
// For seasons, we need the show's rating key
if (metadata.parentRatingKey == null) {
throw Exception('Season is missing parentRatingKey');
}
showRatingKey = metadata.parentRatingKey!;
}
final playQueue = await client.createShowPlayQueue(
showRatingKey: showRatingKey,
shuffle: 1,
);
return _launchFromQueue(
playQueue: playQueue,
ratingKey: showRatingKey,
serverId: metadata.serverId ?? serverId,
serverName: metadata.serverName ?? serverName,
copyServerInfo: true,
);
},
);
}
/// Core method to launch playback from a play queue.
Future<PlayQueueResult> _launchFromQueue({
required PlayQueueResponse? playQueue,
required String ratingKey,
String? serverId,
String? serverName,
PlexMetadata? 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');
// Set up playback state
final playbackState = context.read<PlaybackStateProvider>();
playbackState.setClient(client);
await playbackState.setPlaybackFromPlayQueue(
playQueue,
ratingKey,
serverId: serverId,
serverName: serverName,
);
if (!context.mounted) return const PlayQueueError('Context not mounted');
// Determine which item to navigate to
var itemToPlay = selectedItem ?? playQueue.items!.first;
// Copy server info if needed
if (copyServerInfo && serverId != null) {
itemToPlay = itemToPlay.copyWith(
serverId: serverId,
serverName: serverName,
);
}
// Navigate to video player
await navigateToVideoPlayer(context, metadata: itemToPlay);
return const PlayQueueSuccess();
}
/// Execute an action with optional loading indicator and error handling.
Future<PlayQueueResult> _executeWithLoading({
required bool showLoading,
required String action,
required Future<PlayQueueResult> Function() execute,
}) async {
try {
// Show loading indicator
if (showLoading && context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) =>
const Center(child: CircularProgressIndicator()),
);
}
final result = await execute();
// Close loading indicator
if (showLoading && context.mounted && Navigator.canPop(context)) {
Navigator.pop(context);
}
// Handle empty queue result
if (result is PlayQueueEmpty && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.messages.failedToCreatePlayQueueNoItems)),
);
}
return result;
} catch (e) {
appLogger.e('Failed to $action', error: e);
// Close loading indicator if it's still open
if (showLoading && context.mounted && Navigator.canPop(context)) {
Navigator.pop(context);
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
t.messages.failedPlayback(action: action, error: e.toString()),
),
),
);
}
return PlayQueueError(e);
}
}
}