diff --git a/lib/client/plex_client.dart b/lib/client/plex_client.dart index bd7a6286..ccf78ff1 100644 --- a/lib/client/plex_client.dart +++ b/lib/client/plex_client.dart @@ -223,11 +223,15 @@ class PlexClient { } /// Extract list of PlexMetadata from response + /// Automatically tags all items with this client's serverId and serverName List _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 _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) + .copyWith(serverId: serverId, serverName: serverName)) + .toList(); + } + return []; + } + + /// Extract PlexPlaylist list from response with auto-tagging + List _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) + .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> 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) { - final type = item.type.toLowerCase(); - return type == 'movie' || type == 'show'; - }).toList(); + // 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'; + }) + .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 metadata = container['Metadata'] as List; - - if (metadata.isEmpty) { - return []; - } - - return metadata - .map((item) => PlexPlaylist.fromJson(item as Map)) - .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 _extractMetadataAndDirectories(Response response) { final List 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) { diff --git a/lib/models/plex_library.dart b/lib/models/plex_library.dart index 72bbd8bc..d5c5d088 100644 --- a/lib/models/plex_library.dart +++ b/lib/models/plex_library.dart @@ -47,4 +47,35 @@ class PlexLibrary with MultiServerFields { _$PlexLibraryFromJson(json); Map 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, + ); + } } diff --git a/lib/providers/playback_state_provider.dart b/lib/providers/playback_state_provider.dart index 5336634c..a1960fea 100644 --- a/lib/providers/playback_state_provider.dart +++ b/lib/providers/playback_state_provider.dart @@ -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; } diff --git a/lib/screens/base_media_list_detail_screen.dart b/lib/screens/base_media_list_detail_screen.dart index 2859dd0d..aadc322d 100644 --- a/lib/screens/base_media_list_detail_screen.dart +++ b/lib/screens/base_media_list_detail_screen.dart @@ -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 } } + // If serverId is null, fall back to first available server + if (serverId == null) { + final multiServerProvider = Provider.of( + 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 } 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; }); } diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index 274940b4..de09c315 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -37,7 +37,18 @@ class _DiscoverScreenState extends State static const Duration _heroAutoScrollDuration = Duration(seconds: 8); @override - PlexClient get client => _getClientForItem(null); + PlexClient get client { + final multiServerProvider = Provider.of( + context, + listen: false, + ); + if (!multiServerProvider.hasConnectedServers) { + throw Exception('No servers available'); + } + return context.getClientForServer( + multiServerProvider.onlineServerIds.first, + ); + } List _onDeck = []; List _hubs = []; @@ -52,7 +63,21 @@ class _DiscoverScreenState extends State /// 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( + context, + listen: false, + ); + if (!multiServerProvider.hasConnectedServers) { + throw Exception('No servers available'); + } + return context.getClientForServer( + multiServerProvider.onlineServerIds.first, + ); + } + return context.getClientForServer(serverId); } @override diff --git a/lib/screens/hub_detail_screen.dart b/lib/screens/hub_detail_screen.dart index 53277847..979d04b4 100644 --- a/lib/screens/hub_detail_screen.dart +++ b/lib/screens/hub_detail_screen.dart @@ -37,7 +37,7 @@ class _HubDetailScreenState extends State 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 diff --git a/lib/screens/libraries_screen.dart b/lib/screens/libraries_screen.dart index b9bb047f..3d04313e 100644 --- a/lib/screens/libraries_screen.dart +++ b/lib/screens/libraries_screen.dart @@ -33,7 +33,18 @@ class LibrariesScreen extends StatefulWidget { class _LibrariesScreenState extends State with Refreshable, ItemUpdatable, SingleTickerProviderStateMixin { @override - PlexClient get client => context.getClientForServer(null); + PlexClient get client { + final multiServerProvider = Provider.of( + context, + listen: false, + ); + if (!multiServerProvider.hasConnectedServers) { + throw Exception(t.errors.noClientAvailable); + } + return context.getClientForServer( + multiServerProvider.onlineServerIds.first, + ); + } late TabController _tabController; diff --git a/lib/screens/library_tabs/library_browse_tab.dart b/lib/screens/library_tabs/library_browse_tab.dart index 0140d816..04976c17 100644 --- a/lib/screens/library_tabs/library_browse_tab.dart +++ b/lib/screens/library_tabs/library_browse_tab.dart @@ -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 ); } + // 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 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 builder: (context) => FiltersBottomSheet( filters: _filters, selectedFilters: _selectedFilters, + serverId: widget.library.serverId!, onFiltersChanged: (filters) async { setState(() { _selectedFilters.clear(); diff --git a/lib/screens/library_tabs/library_collections_tab.dart b/lib/screens/library_tabs/library_collections_tab.dart index b392543e..cf9d9254 100644 --- a/lib/screens/library_tabs/library_collections_tab.dart +++ b/lib/screens/library_tabs/library_collections_tab.dart @@ -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 diff --git a/lib/screens/library_tabs/library_playlists_tab.dart b/lib/screens/library_tabs/library_playlists_tab.dart index c4e06116..003f9de9 100644 --- a/lib/screens/library_tabs/library_playlists_tab.dart +++ b/lib/screens/library_tabs/library_playlists_tab.dart @@ -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 diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index 512ef958..8ccf7b37 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -51,7 +51,7 @@ class _MediaDetailScreenState extends State { /// 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 _loadFullMetadata() async { diff --git a/lib/screens/playlist_detail_screen.dart b/lib/screens/playlist_detail_screen.dart index 85886b23..df2015af 100644 --- a/lib/screens/playlist_detail_screen.dart +++ b/lib/screens/playlist_detail_screen.dart @@ -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 _deletePlaylist() async { diff --git a/lib/screens/season_detail_screen.dart b/lib/screens/season_detail_screen.dart index 40015969..c0d241cb 100644 --- a/lib/screens/season_detail_screen.dart +++ b/lib/screens/season_detail_screen.dart @@ -33,7 +33,7 @@ class _SeasonDetailScreenState extends State /// 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 }); 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; diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 16704030..96f76209 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -79,7 +79,7 @@ class VideoPlayerScreenState extends State /// 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 _isBuffering = ValueNotifier( @@ -462,14 +462,15 @@ class VideoPlayerScreenState extends State 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; } diff --git a/lib/services/data_aggregation_service.dart b/lib/services/data_aggregation_service.dart index 8998be07..92b77026 100644 --- a/lib/services/data_aggregation_service.dart +++ b/lib/services/data_aggregation_service.dart @@ -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> getLibrariesFromAllServers() async { return _perServer( 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> getOnDeckFromAllServers({int? limit}) async { final allOnDeck = await _perServer( 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> searchAcrossServers( String query, { int? limit, @@ -167,12 +149,7 @@ class DataAggregationService { final allResults = await _perServer( 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', diff --git a/lib/utils/collection_playlist_play_helper.dart b/lib/utils/collection_playlist_play_helper.dart index c8287e0e..3f293fc0 100644 --- a/lib/utils/collection_playlist_play_helper.dart +++ b/lib/utils/collection_playlist_play_helper.dart @@ -30,13 +30,17 @@ Future 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 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(); playbackState.setClient(client); await playbackState.setPlaybackFromPlayQueue( - queueWithServerId, + fetchedQueue, ratingKey, serverId: serverId, serverName: serverName, @@ -93,7 +79,7 @@ Future 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 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(); playbackState.setClient(client); await playbackState.setPlaybackFromPlayQueue( - queueWithServerId, + playQueue, ratingKey, serverId: serverId, serverName: serverName, @@ -143,7 +111,7 @@ Future 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) { diff --git a/lib/utils/provider_extensions.dart b/lib/utils/provider_extensions.dart index 0c5c3636..ce036e7a 100644 --- a/lib/utils/provider_extensions.dart +++ b/lib/utils/provider_extensions.dart @@ -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( 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( + this, + listen: false, + ); + if (!multiServerProvider.hasConnectedServers) { + throw Exception(t.errors.noClientAvailable); + } + return getClientForServer(multiServerProvider.onlineServerIds.first); + } + return getClientForServer(library.serverId!); } } diff --git a/lib/utils/server_tagging_extensions.dart b/lib/utils/server_tagging_extensions.dart deleted file mode 100644 index 5284dd3d..00000000 --- a/lib/utils/server_tagging_extensions.dart +++ /dev/null @@ -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 { - /// Tags all items in the collection with the given server ID and name - /// Returns a new list with all items updated - List 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 tagWithLibrary(PlexLibrary library) { - return tagWithServer(library.serverId, library.serverName); - } -} - -extension PlexPlaylistServerTagging on Iterable { - /// Tags all playlists in the collection with the given server ID and name - /// Returns a new list with all playlists updated - List 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 tagWithLibrary(PlexLibrary library) { - return tagWithServer(library.serverId, library.serverName); - } -} diff --git a/lib/widgets/filters_bottom_sheet.dart b/lib/widgets/filters_bottom_sheet.dart index 54e463d9..57f2a5d2 100644 --- a/lib/widgets/filters_bottom_sheet.dart +++ b/lib/widgets/filters_bottom_sheet.dart @@ -9,12 +9,14 @@ class FiltersBottomSheet extends StatefulWidget { final List filters; final Map selectedFilters; final Function(Map) 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 { }); try { - final client = context.getClientForServer(null); + final client = context.getClientForServer(widget.serverId); final values = await client.getFilterValues(filter.key); setState(() { diff --git a/lib/widgets/folder_tree_view.dart b/lib/widgets/folder_tree_view.dart index ffaabc77..02137234 100644 --- a/lib/widgets/folder_tree_view.dart +++ b/lib/widgets/folder_tree_view.dart @@ -49,7 +49,7 @@ class _FolderTreeViewState extends State { }); 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 { 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 { }); 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); }); diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index 69f2dec9..2b4f3552 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -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( + context, + listen: false, + ); + if (!multiServerProvider.hasConnectedServers) { + throw Exception('No servers available'); + } + serverId = multiServerProvider.onlineServerIds.first; + } + return context.getClientForServer(serverId); } diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 73160a81..d43fe82f 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -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 { serverId = (widget.item as PlexPlaylist).serverId; } + // If serverId is null, fall back to first available server + if (serverId == null) { + final multiServerProvider = Provider.of( + context, + listen: false, + ); + if (!multiServerProvider.hasConnectedServers) { + throw Exception('No servers available'); + } + serverId = multiServerProvider.onlineServerIds.first; + } + return context.getClientForServer(serverId); } diff --git a/lib/widgets/playlist_item_card.dart b/lib/widgets/playlist_item_card.dart index 46682a3f..f2894d0e 100644 --- a/lib/widgets/playlist_item_card.dart +++ b/lib/widgets/playlist_item_card.dart @@ -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) { diff --git a/lib/widgets/video_controls/sheets/chapter_sheet.dart b/lib/widgets/video_controls/sheets/chapter_sheet.dart index d0b1e57f..65ced99d 100644 --- a/lib/widgets/video_controls/sheets/chapter_sheet.dart +++ b/lib/widgets/video_controls/sheets/chapter_sheet.dart @@ -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 diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index 8b6dcad1..0d5b2330 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -97,7 +97,7 @@ class _PlexVideoControlsState extends State /// 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