refactor: simplify metadata server tagging

This commit is contained in:
edde746
2025-11-23 05:10:42 +01:00
parent 8d0d220d78
commit 7ab8dfa3c3
25 changed files with 287 additions and 270 deletions
+97 -30
View File
@@ -223,11 +223,15 @@ class PlexClient {
}
/// Extract list of PlexMetadata from response
/// Automatically tags all items with this client's serverId and serverName
List<PlexMetadata> _extractMetadataList(Response response) {
final container = _getMediaContainer(response);
if (container != null && container['Metadata'] != null) {
return (container['Metadata'] as List)
.map((json) => PlexMetadata.fromJson(json))
.map((json) => PlexMetadata.fromJson(json).copyWith(
serverId: serverId,
serverName: serverName,
))
.toList();
}
return [];
@@ -245,9 +249,15 @@ class PlexClient {
}
/// Extract single PlexMetadata from response (returns first item or null)
/// Automatically tags the item with this client's serverId and serverName
PlexMetadata? _extractSingleMetadata(Response response) {
final metadataJson = _getFirstMetadataJson(response);
return metadataJson != null ? PlexMetadata.fromJson(metadataJson) : null;
return metadataJson != null
? PlexMetadata.fromJson(metadataJson).copyWith(
serverId: serverId,
serverName: serverName,
)
: null;
}
/// Generic helper to extract and map Directory list from response
@@ -264,6 +274,30 @@ class PlexClient {
return [];
}
/// Extract PlexLibrary list from response with auto-tagging
List<PlexLibrary> _extractLibraryList(Response response) {
final container = _getMediaContainer(response);
if (container != null && container['Directory'] != null) {
return (container['Directory'] as List)
.map((json) => PlexLibrary.fromJson(json as Map<String, dynamic>)
.copyWith(serverId: serverId, serverName: serverName))
.toList();
}
return [];
}
/// Extract PlexPlaylist list from response with auto-tagging
List<PlexPlaylist> _extractPlaylistList(Response response) {
final container = _getMediaContainer(response);
if (container != null && container['Metadata'] != null) {
return (container['Metadata'] as List)
.map((json) => PlexPlaylist.fromJson(json as Map<String, dynamic>)
.copyWith(serverId: serverId, serverName: serverName))
.toList();
}
return [];
}
// ============================================================================
// API Methods
// ============================================================================
@@ -275,9 +309,10 @@ class PlexClient {
}
/// Get library sections
/// Returns libraries automatically tagged with this client's serverId and serverName
Future<List<PlexLibrary>> getLibraries() async {
final response = await _dio.get('/library/sections');
return _extractDirectoryList(response, PlexLibrary.fromJson);
return _extractLibraryList(response);
}
/// Get library content by section ID
@@ -352,7 +387,10 @@ class PlexClient {
// Log the parsed metadata JSON
if (metadataJson != null) {
metadata = PlexMetadata.fromJsonWithImages(metadataJson);
metadata = PlexMetadata.fromJsonWithImages(metadataJson).copyWith(
serverId: serverId,
serverName: serverName,
);
// Check if OnDeck is nested inside Metadata
if (metadataJson.containsKey('OnDeck') &&
@@ -363,7 +401,10 @@ class PlexClient {
if (onDeckData is Map && onDeckData.containsKey('Metadata')) {
final onDeckMetadata = onDeckData['Metadata'];
if (onDeckMetadata != null) {
onDeckEpisode = PlexMetadata.fromJson(onDeckMetadata);
onDeckEpisode = PlexMetadata.fromJson(onDeckMetadata).copyWith(
serverId: serverId,
serverName: serverName,
);
}
}
}
@@ -377,7 +418,10 @@ class PlexClient {
final response = await _dio.get('/library/metadata/$ratingKey');
final metadataJson = _getFirstMetadataJson(response);
return metadataJson != null
? PlexMetadata.fromJsonWithImages(metadataJson)
? PlexMetadata.fromJsonWithImages(metadataJson).copyWith(
serverId: serverId,
serverName: serverName,
)
: null;
}
@@ -445,7 +489,10 @@ class PlexClient {
if (hub['Metadata'] != null) {
for (final json in hub['Metadata'] as List) {
try {
results.add(PlexMetadata.fromJson(json));
results.add(PlexMetadata.fromJson(json).copyWith(
serverId: serverId,
serverName: serverName,
));
} catch (e) {
// Skip items that fail to parse
appLogger.w('Failed to parse search result', error: e);
@@ -456,7 +503,10 @@ class PlexClient {
if (hub['Directory'] != null) {
for (final json in hub['Directory'] as List) {
try {
results.add(PlexMetadata.fromJson(json));
results.add(PlexMetadata.fromJson(json).copyWith(
serverId: serverId,
serverName: serverName,
));
} catch (e) {
// Skip items that fail to parse
appLogger.w('Failed to parse search result', error: e);
@@ -492,7 +542,10 @@ class PlexClient {
final container = _getMediaContainer(response);
if (container != null && container['Metadata'] != null) {
final allItems = (container['Metadata'] as List)
.map((json) => PlexMetadata.fromJsonWithImages(json))
.map((json) => PlexMetadata.fromJsonWithImages(json).copyWith(
serverId: serverId,
serverName: serverName,
))
.toList();
// Filter out music content (artists, albums, tracks)
@@ -1130,11 +1183,17 @@ class PlexClient {
final hub = PlexHub.fromJson(hubJson);
// Only include hubs that have items and are movie/show content
if (hub.items.isNotEmpty) {
// Filter out non-video content types
final videoItems = hub.items.where((item) {
// Filter out non-video content types and tag with server info
final videoItems = hub.items
.where((item) {
final type = item.type.toLowerCase();
return type == 'movie' || type == 'show';
}).toList();
})
.map((item) => item.copyWith(
serverId: serverId,
serverName: serverName,
))
.toList();
if (videoItems.isNotEmpty) {
hubs.add(
@@ -1146,6 +1205,8 @@ class PlexClient {
size: hub.size,
more: hub.more,
items: videoItems,
serverId: serverId,
serverName: serverName,
),
);
}
@@ -1209,21 +1270,8 @@ class PlexClient {
'/playlists',
queryParameters: queryParams,
);
final container = _getMediaContainer(response);
if (container == null || container['Metadata'] == null) {
return [];
}
final List<dynamic> metadata = container['Metadata'] as List;
if (metadata.isEmpty) {
return [];
}
return metadata
.map((item) => PlexPlaylist.fromJson(item as Map<String, dynamic>))
.toList();
return _extractPlaylistList(response);
} catch (e) {
appLogger.e('Failed to get playlists: $e');
return [];
@@ -1596,7 +1644,11 @@ class PlexClient {
queryParameters: queryParams,
);
return PlayQueueResponse.fromJson(response.data);
return PlayQueueResponse.fromJson(
response.data,
serverId: serverId,
serverName: serverName,
);
} catch (e) {
appLogger.e('Failed to create play queue', error: e);
return null;
@@ -1628,7 +1680,11 @@ class PlexClient {
queryParameters: queryParams,
);
return PlayQueueResponse.fromJson(response.data);
return PlayQueueResponse.fromJson(
response.data,
serverId: serverId,
serverName: serverName,
);
} catch (e) {
appLogger.e('Failed to get play queue: $e');
return null;
@@ -1703,6 +1759,7 @@ class PlexClient {
/// Extract both Metadata and Directory entries from response
/// Folders can come back as either type
/// Automatically tags all items with this client's serverId and serverName
List<PlexMetadata> _extractMetadataAndDirectories(Response response) {
final List<PlexMetadata> items = [];
final container = _getMediaContainer(response);
@@ -1713,7 +1770,10 @@ class PlexClient {
for (final json in container['Metadata'] as List) {
try {
// Try to parse with full PlexMetadata.fromJson first
items.add(PlexMetadata.fromJson(json));
items.add(PlexMetadata.fromJson(json).copyWith(
serverId: serverId,
serverName: serverName,
));
} catch (e) {
// If full parsing fails, use minimal safe parsing
appLogger.d('Using minimal parsing for metadata item: $e');
@@ -1727,6 +1787,8 @@ class PlexClient {
thumb: json['thumb'],
art: json['art'],
year: json['year'],
serverId: serverId,
serverName: serverName,
),
);
} catch (e2) {
@@ -1741,7 +1803,10 @@ class PlexClient {
for (final json in container['Directory'] as List) {
try {
// Try to parse as PlexMetadata first
items.add(PlexMetadata.fromJson(json));
items.add(PlexMetadata.fromJson(json).copyWith(
serverId: serverId,
serverName: serverName,
));
} catch (e) {
// If that fails, use minimal folder representation
try {
@@ -1753,6 +1818,8 @@ class PlexClient {
title: json['title'] ?? 'Untitled',
thumb: json['thumb'],
art: json['art'],
serverId: serverId,
serverName: serverName,
),
);
} catch (e2) {
+31
View File
@@ -47,4 +47,35 @@ class PlexLibrary with MultiServerFields {
_$PlexLibraryFromJson(json);
Map<String, dynamic> toJson() => _$PlexLibraryToJson(this);
/// Create a copy of this library with optional field overrides
PlexLibrary copyWith({
String? key,
String? title,
String? type,
String? agent,
String? scanner,
String? language,
String? uuid,
int? updatedAt,
int? createdAt,
int? hidden,
String? serverId,
String? serverName,
}) {
return PlexLibrary(
key: key ?? this.key,
title: title ?? this.title,
type: type ?? this.type,
agent: agent ?? this.agent,
scanner: scanner ?? this.scanner,
language: language ?? this.language,
uuid: uuid ?? this.uuid,
updatedAt: updatedAt ?? this.updatedAt,
createdAt: createdAt ?? this.createdAt,
hidden: hidden ?? this.hidden,
serverId: serverId ?? this.serverId,
serverName: serverName ?? this.serverName,
);
}
}
+7 -28
View File
@@ -105,10 +105,8 @@ class PlaybackStateProvider with ChangeNotifier {
_playQueueShuffled = playQueue.playQueueShuffled;
_currentPlayQueueItemID = playQueue.playQueueSelectedItemID;
// Preserve serverId/serverName on all items
_loadedItems = (playQueue.items ?? []).map((item) {
return item.copyWith(serverId: serverId, serverName: serverName);
}).toList();
// Items are already tagged with server info by PlexClient
_loadedItems = playQueue.items ?? [];
_contextKey = contextKey;
_playbackMode = PlaybackMode.playQueue;
@@ -136,17 +134,8 @@ class PlaybackStateProvider with ChangeNotifier {
);
if (response != null && response.items != null) {
// Preserve serverId from existing items
final serverId = _loadedItems.isNotEmpty
? _loadedItems.first.serverId
: null;
final serverName = _loadedItems.isNotEmpty
? _loadedItems.first.serverName
: null;
_loadedItems = response.items!.map((item) {
return item.copyWith(serverId: serverId, serverName: serverName);
}).toList();
// Items are already tagged with server info by PlexClient
_loadedItems = response.items!;
// Use size or items length as fallback if totalCount is null
_playQueueTotalCount =
response.playQueueTotalCount ??
@@ -238,25 +227,15 @@ class PlaybackStateProvider with ChangeNotifier {
if (response != null &&
response.items != null &&
response.items!.isNotEmpty) {
// Preserve serverId when looping
final serverId = _loadedItems.isNotEmpty
? _loadedItems.first.serverId
: null;
final serverName = _loadedItems.isNotEmpty
? _loadedItems.first.serverName
: null;
_loadedItems = response.items!.map((item) {
return item.copyWith(serverId: serverId, serverName: serverName);
}).toList();
// Items are already tagged with server info by PlexClient
_loadedItems = response.items!;
final firstItem = _loadedItems.first;
// Don't update _currentPlayQueueItemID here - let setCurrentItem do it when playback starts
return firstItem;
}
}
}
// Queue has ended - clear it and let sequential playback take over
clearShuffle();
// At end of queue - return null but keep queue active so user can still go back
return null;
}
+16 -11
View File
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../client/plex_client.dart';
import '../models/plex_metadata.dart';
import '../providers/multi_server_provider.dart';
import '../utils/provider_extensions.dart';
import '../utils/collection_playlist_play_helper.dart';
import '../utils/app_logger.dart';
@@ -51,6 +53,18 @@ abstract class BaseMediaListDetailScreen<T extends StatefulWidget>
}
}
// If serverId is null, fall back to first available server
if (serverId == null) {
final multiServerProvider = Provider.of<MultiServerProvider>(
context,
listen: false,
);
if (!multiServerProvider.hasConnectedServers) {
throw Exception(t.errors.noClientAvailable);
}
serverId = multiServerProvider.onlineServerIds.first;
}
return context.getClientForServer(serverId);
}
@@ -231,21 +245,12 @@ mixin StandardItemLoader<T extends StatefulWidget>
}
try {
// Items are automatically tagged with server info by PlexClient
final newItems = await fetchItems();
// Tag items with server info for correct client resolution
final serverId = (mediaItem as dynamic).serverId as String?;
final serverName = (mediaItem as dynamic).serverName as String?;
final taggedItems = newItems
.map(
(item) => item.copyWith(serverId: serverId, serverName: serverName),
)
.toList();
if (mounted) {
setState(() {
items = taggedItems;
items = newItems;
isLoading = false;
});
}
+27 -2
View File
@@ -37,7 +37,18 @@ class _DiscoverScreenState extends State<DiscoverScreen>
static const Duration _heroAutoScrollDuration = Duration(seconds: 8);
@override
PlexClient get client => _getClientForItem(null);
PlexClient get client {
final multiServerProvider = Provider.of<MultiServerProvider>(
context,
listen: false,
);
if (!multiServerProvider.hasConnectedServers) {
throw Exception('No servers available');
}
return context.getClientForServer(
multiServerProvider.onlineServerIds.first,
);
}
List<PlexMetadata> _onDeck = [];
List<PlexHub> _hubs = [];
@@ -52,7 +63,21 @@ class _DiscoverScreenState extends State<DiscoverScreen>
/// Get the correct PlexClient for an item's server
PlexClient _getClientForItem(PlexMetadata? item) {
return context.getClientForServer(item?.serverId);
// Items should always have a serverId, but if not, fall back to first available server
final serverId = item?.serverId;
if (serverId == null) {
final multiServerProvider = Provider.of<MultiServerProvider>(
context,
listen: false,
);
if (!multiServerProvider.hasConnectedServers) {
throw Exception('No servers available');
}
return context.getClientForServer(
multiServerProvider.onlineServerIds.first,
);
}
return context.getClientForServer(serverId);
}
@override
+1 -1
View File
@@ -37,7 +37,7 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable {
/// Get the correct PlexClient for this hub's server
PlexClient _getClientForHub() {
return context.getClientForServer(widget.hub.serverId);
return context.getClientForServer(widget.hub.serverId!);
}
@override
+12 -1
View File
@@ -33,7 +33,18 @@ class LibrariesScreen extends StatefulWidget {
class _LibrariesScreenState extends State<LibrariesScreen>
with Refreshable, ItemUpdatable, SingleTickerProviderStateMixin {
@override
PlexClient get client => context.getClientForServer(null);
PlexClient get client {
final multiServerProvider = Provider.of<MultiServerProvider>(
context,
listen: false,
);
if (!multiServerProvider.hasConnectedServers) {
throw Exception(t.errors.noClientAvailable);
}
return context.getClientForServer(
multiServerProvider.onlineServerIds.first,
);
}
late TabController _tabController;
@@ -9,7 +9,6 @@ import '../../models/plex_sort.dart';
import '../../providers/settings_provider.dart';
import '../../utils/error_message_utils.dart';
import '../../utils/grid_size_calculator.dart';
import '../../utils/server_tagging_extensions.dart';
import '../../widgets/media_card.dart';
import '../../widgets/folder_tree_view.dart';
import '../../widgets/filters_bottom_sheet.dart';
@@ -219,6 +218,7 @@ class _LibraryBrowseTabState extends State<LibraryBrowseTab>
);
}
// Items are automatically tagged with server info by PlexClient
final items = await client.getLibraryContent(
widget.library.key,
start: _currentPage * _pageSize,
@@ -227,18 +227,15 @@ class _LibraryBrowseTabState extends State<LibraryBrowseTab>
cancelToken: _cancelToken,
);
// Tag items with server info for multi-server support
final taggedItems = items.tagWithLibrary(widget.library);
if (currentRequestId != _requestId) return;
setState(() {
if (loadMore) {
_items.addAll(taggedItems);
_items.addAll(items);
} else {
_items = taggedItems;
_items = items;
}
_hasMoreItems = taggedItems.length >= _pageSize;
_hasMoreItems = items.length >= _pageSize;
_currentPage++;
_isLoading = false;
});
@@ -357,6 +354,7 @@ class _LibraryBrowseTabState extends State<LibraryBrowseTab>
builder: (context) => FiltersBottomSheet(
filters: _filters,
selectedFilters: _selectedFilters,
serverId: widget.library.serverId!,
onFiltersChanged: (filters) async {
setState(() {
_selectedFilters.clear();
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../../models/plex_metadata.dart';
import '../../utils/library_refresh_notifier.dart';
import '../../utils/server_tagging_extensions.dart';
import '../../i18n/strings.g.dart';
import '../../widgets/adaptive_media_grid.dart';
import 'base_library_tab.dart';
@@ -40,10 +39,8 @@ class _LibraryCollectionsTabState
// Use server-specific client for this library
final client = getClientForLibrary();
final collections = await client.getLibraryCollections(widget.library.key);
// Tag collections with server info
return collections.tagWithLibrary(widget.library);
// Collections are automatically tagged with server info by PlexClient
return await client.getLibraryCollections(widget.library.key);
}
@override
@@ -5,7 +5,6 @@ import '../../providers/settings_provider.dart';
import '../../utils/library_refresh_notifier.dart';
import '../../services/settings_service.dart' show ViewMode;
import '../../utils/grid_size_calculator.dart';
import '../../utils/server_tagging_extensions.dart';
import '../../widgets/media_card.dart';
import '../../i18n/strings.g.dart';
import 'base_library_tab.dart';
@@ -43,14 +42,11 @@ class _LibraryPlaylistsTabState
// Use server-specific client for this library
final client = getClientForLibrary();
// Get playlists for this library
final playlists = await client.getLibraryPlaylists(
// Playlists are automatically tagged with server info by PlexClient
return await client.getLibraryPlaylists(
sectionId: widget.library.key,
playlistType: 'video',
);
// Tag playlists with server info
return playlists.tagWithLibrary(widget.library);
}
@override
+1 -1
View File
@@ -51,7 +51,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
/// Get the correct PlexClient for this metadata's server
PlexClient _getClientForMetadata(BuildContext context) {
return context.getClientForServer(widget.metadata.serverId);
return context.getClientForServer(widget.metadata.serverId!);
}
Future<void> _loadFullMetadata() async {
+1 -1
View File
@@ -53,7 +53,7 @@ class _PlaylistDetailScreenState
/// Get the correct PlexClient for this playlist's server
PlexClient _getClientForPlaylist() {
return context.getClientForServer(widget.playlist.serverId);
return context.getClientForServer(widget.playlist.serverId!);
}
Future<void> _deletePlaylist() async {
+3 -2
View File
@@ -33,7 +33,7 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
/// Get the correct PlexClient for this season's server
PlexClient _getClientForSeason(BuildContext context) {
return context.getClientForServer(widget.season.serverId);
return context.getClientForServer(widget.season.serverId!);
}
@override
@@ -52,8 +52,9 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
});
try {
// Episodes are now tagged with server info at the source
// Episodes are automatically tagged with server info by PlexClient
final episodes = await _client.getChildren(widget.season.ratingKey);
setState(() {
_episodes = episodes;
_isLoadingEpisodes = false;
+6 -5
View File
@@ -79,7 +79,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
/// Get the correct PlexClient for this metadata's server
PlexClient _getClientForMetadata(BuildContext context) {
return context.getClientForServer(widget.metadata.serverId);
return context.getClientForServer(widget.metadata.serverId!);
}
final ValueNotifier<bool> _isBuffering = ValueNotifier<bool>(
@@ -462,14 +462,15 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
return;
}
// Check if there's already an active queue for this show
// Check if there's already an active queue
final existingContextKey = playbackState.shuffleContextKey;
final isQueueActive = playbackState.isQueueActive;
if (isQueueActive && existingContextKey == showRatingKey) {
// Queue already exists for this show, just update the current item
if (isQueueActive) {
// A queue already exists (could be shuffle, playlist, or sequential)
// Just update the current item, don't create a new queue
playbackState.setCurrentItem(widget.metadata);
appLogger.d('Using existing play queue for show $showRatingKey');
appLogger.d('Using existing play queue (context: $existingContextKey)');
return;
}
+9 -47
View File
@@ -14,43 +14,24 @@ class DataAggregationService {
DataAggregationService(this._serverManager);
/// Fetch libraries from all online servers and tag them with server info
/// Fetch libraries from all online servers
/// Libraries are automatically tagged with server info by PlexClient
Future<List<PlexLibrary>> getLibrariesFromAllServers() async {
return _perServer<PlexLibrary>(
operationName: 'fetching libraries',
operation: (serverId, client, server) async {
final libraries = await client.getLibraries();
// Tag each library with server info
return libraries.map((lib) {
return PlexLibrary(
key: lib.key,
title: lib.title,
type: lib.type,
agent: lib.agent,
scanner: lib.scanner,
language: lib.language,
uuid: lib.uuid,
updatedAt: lib.updatedAt,
createdAt: lib.createdAt,
hidden: lib.hidden,
);
}).toList();
return await client.getLibraries();
},
);
}
/// Fetch "On Deck" (Continue Watching) from all servers and merge by recency
/// Items are automatically tagged with server info by PlexClient
Future<List<PlexMetadata>> getOnDeckFromAllServers({int? limit}) async {
final allOnDeck = await _perServer<PlexMetadata>(
operationName: 'fetching on deck',
operation: (serverId, client, server) async {
final items = await client.getOnDeck();
// Tag each item with server info
return items.map((item) {
return item.copyWith(serverId: serverId, serverName: server?.name);
}).toList();
return await client.getOnDeck();
},
);
@@ -156,6 +137,7 @@ class DataAggregationService {
}
/// Search across all online servers
/// Results are automatically tagged with server info by PlexClient
Future<List<PlexMetadata>> searchAcrossServers(
String query, {
int? limit,
@@ -167,12 +149,7 @@ class DataAggregationService {
final allResults = await _perServer<PlexMetadata>(
operationName: 'searching for "$query"',
operation: (serverId, client, server) async {
final results = await client.search(query);
// Tag each result with server info
return results.map((item) {
return item.copyWith(serverId: serverId, serverName: server?.name);
}).toList();
return await client.search(query);
},
);
@@ -196,23 +173,8 @@ class DataAggregationService {
}
try {
final libraries = await client.getLibraries();
// Tag with server info
return libraries.map((lib) {
return PlexLibrary(
key: lib.key,
title: lib.title,
type: lib.type,
agent: lib.agent,
scanner: lib.scanner,
language: lib.language,
uuid: lib.uuid,
updatedAt: lib.updatedAt,
createdAt: lib.createdAt,
hidden: lib.hidden,
);
}).toList();
// Libraries are automatically tagged with server info by PlexClient
return await client.getLibraries();
} catch (e, stackTrace) {
appLogger.e(
'Failed to fetch libraries for server $serverId',
+14 -46
View File
@@ -30,13 +30,17 @@ Future<void> playCollectionOrPlaylist({
final PlayQueueResponse? playQueue;
if (isCollection) {
// Validate that machine identifier is available
if (client.config.machineIdentifier == null) {
throw Exception('Machine identifier is required to play collections');
// 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://${client.config.machineIdentifier}/com.plexapp.plugins.library/library/collections/${item.ratingKey}';
'server://$machineId/com.plexapp.plugins.library/library/collections/${item.ratingKey}';
playQueue = await client.createPlayQueue(
uri: collectionUri,
type: 'video',
@@ -61,30 +65,12 @@ Future<void> playCollectionOrPlaylist({
fetchedQueue.items!.isNotEmpty) {
if (!context.mounted) return;
// Preserve serverId for all items in the queue
final itemsWithServerId = fetchedQueue.items!.map((queueItem) {
return queueItem.copyWith(serverId: serverId, serverName: serverName);
}).toList();
final queueWithServerId = PlayQueueResponse(
playQueueID: fetchedQueue.playQueueID,
playQueueSelectedItemID: fetchedQueue.playQueueSelectedItemID,
playQueueSelectedItemOffset: fetchedQueue.playQueueSelectedItemOffset,
playQueueSelectedMetadataItemID:
fetchedQueue.playQueueSelectedMetadataItemID,
playQueueShuffled: fetchedQueue.playQueueShuffled,
playQueueSourceURI: fetchedQueue.playQueueSourceURI,
playQueueTotalCount: fetchedQueue.playQueueTotalCount,
playQueueVersion: fetchedQueue.playQueueVersion,
size: fetchedQueue.size,
items: itemsWithServerId,
);
// Items are automatically tagged with server info by PlexClient
// Set play queue in provider
final playbackState = context.read<PlaybackStateProvider>();
playbackState.setClient(client);
await playbackState.setPlaybackFromPlayQueue(
queueWithServerId,
fetchedQueue,
ratingKey,
serverId: serverId,
serverName: serverName,
@@ -93,7 +79,7 @@ Future<void> playCollectionOrPlaylist({
if (!context.mounted) return;
// Navigate to first item
await navigateToVideoPlayer(context, metadata: itemsWithServerId.first);
await navigateToVideoPlayer(context, metadata: fetchedQueue.items!.first);
return;
}
}
@@ -111,30 +97,12 @@ Future<void> playCollectionOrPlaylist({
if (!context.mounted) return;
// Preserve serverId for all items in the queue
final itemsWithServerId = playQueue.items!.map((queueItem) {
return queueItem.copyWith(serverId: serverId, serverName: serverName);
}).toList();
final queueWithServerId = PlayQueueResponse(
playQueueID: playQueue.playQueueID,
playQueueSelectedItemID: playQueue.playQueueSelectedItemID,
playQueueSelectedItemOffset: playQueue.playQueueSelectedItemOffset,
playQueueSelectedMetadataItemID:
playQueue.playQueueSelectedMetadataItemID,
playQueueShuffled: playQueue.playQueueShuffled,
playQueueSourceURI: playQueue.playQueueSourceURI,
playQueueTotalCount: playQueue.playQueueTotalCount,
playQueueVersion: playQueue.playQueueVersion,
size: playQueue.size,
items: itemsWithServerId,
);
// Items are automatically tagged with server info by PlexClient
// Set play queue in provider
final playbackState = context.read<PlaybackStateProvider>();
playbackState.setClient(client);
await playbackState.setPlaybackFromPlayQueue(
queueWithServerId,
playQueue,
ratingKey,
serverId: serverId,
serverName: serverName,
@@ -143,7 +111,7 @@ Future<void> playCollectionOrPlaylist({
if (!context.mounted) return;
// Navigate to first item
await navigateToVideoPlayer(context, metadata: itemsWithServerId.first);
await navigateToVideoPlayer(context, metadata: playQueue.items!.first);
} catch (e) {
appLogger.e('Failed to ${shuffle ? "shuffle play" : "play"}', error: e);
if (context.mounted) {
+14 -21
View File
@@ -33,31 +33,13 @@ extension ProviderExtensions on BuildContext {
PlexUserProfile? get profileSettings => userProfile.profileSettings;
/// Get PlexClient for a specific server ID
/// If serverId is null, returns the first available online client
/// Throws an exception if no client is available
PlexClient getClientForServer(String? serverId) {
/// Throws an exception if no client is available for the given serverId
PlexClient getClientForServer(String serverId) {
final multiServerProvider = Provider.of<MultiServerProvider>(
this,
listen: false,
);
if (serverId == null) {
// No serverId specified - try to get first online server
appLogger.w('No serverId provided, using first available online server');
if (!multiServerProvider.hasConnectedServers) {
throw Exception(t.errors.noClientAvailable);
}
final firstServerId = multiServerProvider.onlineServerIds.first;
final client = multiServerProvider.getClientForServer(firstServerId);
if (client == null) {
throw Exception(t.errors.noClientAvailable);
}
return client;
}
final serverClient = multiServerProvider.getClientForServer(serverId);
if (serverClient == null) {
@@ -71,6 +53,17 @@ extension ProviderExtensions on BuildContext {
/// Get PlexClient for a library
/// Throws an exception if no client is available
PlexClient getClientForLibrary(PlexLibrary library) {
return getClientForServer(library.serverId);
// If library doesn't have a serverId, fall back to first available server
if (library.serverId == null) {
final multiServerProvider = Provider.of<MultiServerProvider>(
this,
listen: false,
);
if (!multiServerProvider.hasConnectedServers) {
throw Exception(t.errors.noClientAvailable);
}
return getClientForServer(multiServerProvider.onlineServerIds.first);
}
return getClientForServer(library.serverId!);
}
}
-39
View File
@@ -1,39 +0,0 @@
import '../models/plex_metadata.dart';
import '../models/plex_playlist.dart';
import '../models/plex_library.dart';
/// Extension methods for tagging items with server information
/// Used for multi-server support to track which server each item belongs to
extension PlexMetadataServerTagging on Iterable<PlexMetadata> {
/// Tags all items in the collection with the given server ID and name
/// Returns a new list with all items updated
List<PlexMetadata> tagWithServer(String? serverId, String? serverName) {
return map(
(item) => item.copyWith(serverId: serverId, serverName: serverName),
).toList();
}
/// Tags all items in the collection with server info from a library
/// Returns a new list with all items updated
List<PlexMetadata> tagWithLibrary(PlexLibrary library) {
return tagWithServer(library.serverId, library.serverName);
}
}
extension PlexPlaylistServerTagging on Iterable<PlexPlaylist> {
/// Tags all playlists in the collection with the given server ID and name
/// Returns a new list with all playlists updated
List<PlexPlaylist> tagWithServer(String? serverId, String? serverName) {
return map(
(playlist) =>
playlist.copyWith(serverId: serverId, serverName: serverName),
).toList();
}
/// Tags all playlists in the collection with server info from a library
/// Returns a new list with all playlists updated
List<PlexPlaylist> tagWithLibrary(PlexLibrary library) {
return tagWithServer(library.serverId, library.serverName);
}
}
+3 -1
View File
@@ -9,12 +9,14 @@ class FiltersBottomSheet extends StatefulWidget {
final List<PlexFilter> filters;
final Map<String, String> selectedFilters;
final Function(Map<String, String>) onFiltersChanged;
final String serverId;
const FiltersBottomSheet({
super.key,
required this.filters,
required this.selectedFilters,
required this.onFiltersChanged,
required this.serverId,
});
@override
@@ -60,7 +62,7 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
});
try {
final client = context.getClientForServer(null);
final client = context.getClientForServer(widget.serverId);
final values = await client.getFilterValues(filter.key);
setState(() {
+5 -11
View File
@@ -49,7 +49,7 @@ class _FolderTreeViewState extends State<FolderTreeView> {
});
try {
final client = context.getClientForServer(widget.serverId);
final client = context.getClientForServer(widget.serverId!);
final folders = await client.getLibraryFolders(widget.libraryKey);
@@ -58,7 +58,7 @@ class _FolderTreeViewState extends State<FolderTreeView> {
final taggedFolders = folders
.map(
(folder) => folder.copyWith(
serverId: widget.serverId,
serverId: widget.serverId!,
serverName: null, // server name not required for folders listing
),
)
@@ -101,21 +101,15 @@ class _FolderTreeViewState extends State<FolderTreeView> {
});
try {
final client = context.getClientForServer(widget.serverId);
final client = context.getClientForServer(widget.serverId!);
// Items are automatically tagged with server info by PlexClient
final children = await client.getFolderChildren(folder.key);
if (!mounted) return;
final taggedChildren = children
.map(
(child) =>
child.copyWith(serverId: widget.serverId, serverName: null),
)
.toList();
setState(() {
_childrenCache[folder.key] = taggedChildren;
_childrenCache[folder.key] = children;
_expandedFolders.add(folder.key);
_loadingFolders.remove(folder.key);
});
+13
View File
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../client/plex_client.dart';
import '../models/plex_metadata.dart';
import '../models/plex_playlist.dart';
import '../providers/multi_server_provider.dart';
import '../providers/settings_provider.dart';
import '../services/settings_service.dart';
import '../utils/provider_extensions.dart';
@@ -659,6 +660,18 @@ PlexClient _getClientForItem(BuildContext context, dynamic item) {
serverId = item.serverId;
}
// If serverId is null, fall back to first available server
if (serverId == null) {
final multiServerProvider = Provider.of<MultiServerProvider>(
context,
listen: false,
);
if (!multiServerProvider.hasConnectedServers) {
throw Exception('No servers available');
}
serverId = multiServerProvider.onlineServerIds.first;
}
return context.getClientForServer(serverId);
}
+13
View File
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../client/plex_client.dart';
import '../models/plex_metadata.dart';
import '../models/plex_playlist.dart';
import '../providers/multi_server_provider.dart';
import '../providers/playback_state_provider.dart';
import '../utils/provider_extensions.dart';
import '../utils/app_logger.dart';
@@ -71,6 +72,18 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
serverId = (widget.item as PlexPlaylist).serverId;
}
// If serverId is null, fall back to first available server
if (serverId == null) {
final multiServerProvider = Provider.of<MultiServerProvider>(
context,
listen: false,
);
if (!multiServerProvider.hasConnectedServers) {
throw Exception('No servers available');
}
serverId = multiServerProvider.onlineServerIds.first;
}
return context.getClientForServer(serverId);
}
+1 -1
View File
@@ -120,7 +120,7 @@ class PlaylistItemCard extends StatelessWidget {
/// Get the correct PlexClient for this item's server
PlexClient _getClientForItem(BuildContext context) {
return context.getClientForServer(item.serverId);
return context.getClientForServer(item.serverId!);
}
Widget _buildPosterImage(BuildContext context) {
@@ -41,7 +41,7 @@ class ChapterSheet extends StatelessWidget {
/// Get the correct PlexClient for the metadata's server
PlexClient _getClientForChapters(BuildContext context) {
return context.getClientForServer(serverId);
return context.getClientForServer(serverId!);
}
@override
@@ -97,7 +97,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
/// Get the correct PlexClient for this metadata's server
PlexClient _getClientForMetadata() {
return context.getClientForServer(widget.metadata.serverId);
return context.getClientForServer(widget.metadata.serverId!);
}
// Double-tap feedback state