diff --git a/lib/client/plex_client.dart b/lib/client/plex_client.dart index bafa1672..bd7a6286 100644 --- a/lib/client/plex_client.dart +++ b/lib/client/plex_client.dart @@ -32,6 +32,12 @@ class PlexClient { final EndpointFailoverManager? _endpointManager; final Future Function(String newBaseUrl)? _onEndpointChanged; + /// Server identifier - all PlexMetadata items created by this client are tagged with this + final String serverId; + + /// Server name - all PlexMetadata items created by this client are tagged with this + final String? serverName; + /// Custom response decoder that handles malformed UTF-8 gracefully static String _lenientUtf8Decoder( List responseBytes, @@ -43,6 +49,8 @@ class PlexClient { PlexClient( this.config, { + required this.serverId, + this.serverName, List? prioritizedEndpoints, Future Function(String newBaseUrl)? onEndpointChanged, }) : _endpointManager = diff --git a/lib/models/play_queue_response.dart b/lib/models/play_queue_response.dart index dbfd0b1a..511989b1 100644 --- a/lib/models/play_queue_response.dart +++ b/lib/models/play_queue_response.dart @@ -49,10 +49,38 @@ class PlayQueueResponse { this.items, }); - factory PlayQueueResponse.fromJson(Map json) { + factory PlayQueueResponse.fromJson( + Map json, { + String? serverId, + String? serverName, + }) { // The API returns data wrapped in MediaContainer final container = json['MediaContainer'] as Map? ?? json; - return _$PlayQueueResponseFromJson(container); + final response = _$PlayQueueResponseFromJson(container); + + // Tag all items with server info + if (response.items != null && (serverId != null || serverName != null)) { + final taggedItems = response.items! + .map( + (item) => item.copyWith(serverId: serverId, serverName: serverName), + ) + .toList(); + return PlayQueueResponse( + playQueueID: response.playQueueID, + playQueueSelectedItemID: response.playQueueSelectedItemID, + playQueueSelectedItemOffset: response.playQueueSelectedItemOffset, + playQueueSelectedMetadataItemID: + response.playQueueSelectedMetadataItemID, + playQueueShuffled: response.playQueueShuffled, + playQueueSourceURI: response.playQueueSourceURI, + playQueueTotalCount: response.playQueueTotalCount, + playQueueVersion: response.playQueueVersion, + size: response.size, + items: taggedItems, + ); + } + + return response; } /// Get the current selected item from the queue diff --git a/lib/providers/multi_server_provider.dart b/lib/providers/multi_server_provider.dart index 4ea6c06d..2b903654 100644 --- a/lib/providers/multi_server_provider.dart +++ b/lib/providers/multi_server_provider.dart @@ -49,19 +49,6 @@ class MultiServerProvider extends ChangeNotifier { /// Check if any servers are connected bool get hasConnectedServers => onlineServerCount > 0; - /// Backward compatibility: Get first available client - /// This allows existing code to work during migration - /// Prefer using getClientForServer() for new code - @Deprecated('Use getClientForServer() or aggregationService instead') - PlexClient? get client { - final onlineClients = _serverManager.onlineClients; - if (onlineClients.isEmpty) { - return null; - } - // Return first online client for backward compatibility - return onlineClients.values.first; - } - /// Update token for a specific server void updateTokenForServer(String serverId, String newToken) { final client = _serverManager.getClient(serverId); diff --git a/lib/screens/hub_detail_screen.dart b/lib/screens/hub_detail_screen.dart index 09a9b790..53277847 100644 --- a/lib/screens/hub_detail_screen.dart +++ b/lib/screens/hub_detail_screen.dart @@ -202,29 +202,19 @@ class _HubDetailScreenState extends State with Refreshable { try { final client = _getClientForHub(); - // Fetch more items from the hub using the hubKey - final response = await client.getHubContent(widget.hub.hubKey); - - // Tag all items with serverId from the hub - final taggedItems = response.map((item) { - return item.copyWith( - serverId: widget.hub.serverId, - serverName: widget.hub.serverName, - ); - }).toList(); + // Fetch items from the hub, tagged with server info at the source + final items = await client.getHubContent(widget.hub.hubKey); setState(() { - _items = taggedItems; - _filteredItems = taggedItems; + _items = items; + _filteredItems = items; _isLoading = false; }); // Apply any existing sort _applySort(); - appLogger.d( - 'Loaded ${taggedItems.length} items for hub: ${widget.hub.title}', - ); + appLogger.d('Loaded ${items.length} items for hub: ${widget.hub.title}'); } catch (e) { appLogger.e('Failed to load hub content', error: e); setState(() { diff --git a/lib/screens/library_tabs/library_recommended_tab.dart b/lib/screens/library_tabs/library_recommended_tab.dart index 15c9b13a..9121b4a6 100644 --- a/lib/screens/library_tabs/library_recommended_tab.dart +++ b/lib/screens/library_tabs/library_recommended_tab.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import '../../models/plex_hub.dart'; -import '../../utils/server_tagging_extensions.dart'; import '../../widgets/hub_section.dart'; import '../../i18n/strings.g.dart'; import 'base_library_tab.dart'; @@ -30,24 +29,8 @@ class _LibraryRecommendedTabState // Use server-specific client for this library final client = getClientForLibrary(); - final hubs = await client.getLibraryHubs(widget.library.key, limit: 12); - - // Tag hubs and items with server info - return hubs - .map( - (hub) => PlexHub( - hubKey: hub.hubKey, - title: hub.title, - type: hub.type, - hubIdentifier: hub.hubIdentifier, - size: hub.size, - more: hub.more, - items: hub.items.tagWithLibrary(widget.library), - serverId: widget.library.serverId, - serverName: widget.library.serverName, - ), - ) - .toList(); + // Hubs are now tagged with server info at the source + return await client.getLibraryHubs(widget.library.key, limit: 12); } @override diff --git a/lib/screens/playlist_detail_screen.dart b/lib/screens/playlist_detail_screen.dart index 3f2b4aa0..85886b23 100644 --- a/lib/screens/playlist_detail_screen.dart +++ b/lib/screens/playlist_detail_screen.dart @@ -238,8 +238,6 @@ class _PlaylistDetailScreenState await playbackState.setPlaybackFromPlayQueue( playQueue, widget.playlist.ratingKey, - serverId: widget.playlist.serverId, - serverName: widget.playlist.serverName, ); // Navigate to selected item (should be first in the queue response) diff --git a/lib/screens/season_detail_screen.dart b/lib/screens/season_detail_screen.dart index 99f6ec65..40015969 100644 --- a/lib/screens/season_detail_screen.dart +++ b/lib/screens/season_detail_screen.dart @@ -52,18 +52,10 @@ class _SeasonDetailScreenState extends State }); try { + // Episodes are now tagged with server info at the source final episodes = await _client.getChildren(widget.season.ratingKey); - // Preserve serverId for each episode - final episodesWithServerId = episodes - .map( - (episode) => episode.copyWith( - serverId: widget.season.serverId, - serverName: widget.season.serverName, - ), - ) - .toList(); setState(() { - _episodes = episodesWithServerId; + _episodes = episodes; _isLoadingEpisodes = false; }); } catch (e) { diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 1e7fe5bc..16704030 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -17,7 +17,6 @@ import '../services/media_controls_manager.dart'; import '../services/playback_initialization_service.dart'; import '../services/playback_progress_tracker.dart'; import '../services/settings_service.dart'; -import '../services/track_selection_service.dart'; import '../services/video_filter_manager.dart'; import '../utils/app_logger.dart'; import '../utils/orientation_helper.dart'; @@ -75,7 +74,6 @@ class VideoPlayerScreenState extends State MediaControlsManager? _mediaControlsManager; PlaybackProgressTracker? _progressTracker; VideoFilterManager? _videoFilterManager; - TrackSelectionService? _trackSelectionService; final EpisodeNavigationService _episodeNavigation = EpisodeNavigationService(); @@ -557,6 +555,10 @@ class VideoPlayerScreenState extends State final result = await playbackService.startPlayback( metadata: widget.metadata, selectedMediaIndex: widget.selectedMediaIndex, + profileSettings: profileSettings, + preferredAudioTrack: widget.preferredAudioTrack, + preferredSubtitleTrack: widget.preferredSubtitleTrack, + preferredPlaybackRate: widget.preferredPlaybackRate, ); // Update available versions from the playback data @@ -577,18 +579,8 @@ class VideoPlayerScreenState extends State } } - // Initialize track selection service and apply tracks - _trackSelectionService = TrackSelectionService( - player: player!, - profileSettings: profileSettings, - metadata: widget.metadata, - ); - - await _trackSelectionService!.selectAndApplyTracks( - preferredAudioTrack: widget.preferredAudioTrack, - preferredSubtitleTrack: widget.preferredSubtitleTrack, - preferredPlaybackRate: widget.preferredPlaybackRate, - ); + // Track selection is now handled inside PlaybackInitializationService + // to ensure tracks are selected BEFORE seeking to resume position } on PlaybackException catch (e) { if (mounted) { ScaffoldMessenger.of( diff --git a/lib/services/data_aggregation_service.dart b/lib/services/data_aggregation_service.dart index d74aa023..8998be07 100644 --- a/lib/services/data_aggregation_service.dart +++ b/lib/services/data_aggregation_service.dart @@ -34,8 +34,6 @@ class DataAggregationService { updatedAt: lib.updatedAt, createdAt: lib.createdAt, hidden: lib.hidden, - serverId: serverId, - serverName: server?.name, ); }).toList(); }, @@ -91,7 +89,6 @@ class DataAggregationService { final hubFutures = clients.entries.map((entry) async { final serverId = entry.key; final client = entry.value; - final server = _serverManager.getServer(serverId); try { // Get libraries for this server @@ -111,29 +108,8 @@ class DataAggregationService { // Fetch hubs from all libraries in parallel final libraryHubFutures = visibleLibraries.map((library) async { try { - final libraryHubs = await client.getLibraryHubs(library.key); - - // Tag hubs and their items with server info - return libraryHubs.map((hub) { - final taggedItems = hub.items.map((item) { - return item.copyWith( - serverId: serverId, - serverName: server?.name, - ); - }).toList(); - - return PlexHub( - hubKey: hub.hubKey, - title: hub.title, - type: hub.type, - hubIdentifier: hub.hubIdentifier, - size: hub.size, - more: hub.more, - items: taggedItems, - serverId: serverId, - serverName: server?.name, - ); - }).toList(); + // Hubs are now tagged with server info at the source + return await client.getLibraryHubs(library.key); } catch (e) { appLogger.w( 'Failed to fetch hubs for library ${library.title}: $e', @@ -213,7 +189,6 @@ class DataAggregationService { /// Get libraries for a specific server Future> getLibrariesForServer(String serverId) async { final client = _serverManager.getClient(serverId); - final server = _serverManager.getServer(serverId); if (client == null) { appLogger.w('No client found for server $serverId'); @@ -236,8 +211,6 @@ class DataAggregationService { updatedAt: lib.updatedAt, createdAt: lib.createdAt, hidden: lib.hidden, - serverId: serverId, - serverName: server?.name, ); }).toList(); } catch (e, stackTrace) { diff --git a/lib/services/multi_server_manager.dart b/lib/services/multi_server_manager.dart index 0799a6df..d0776531 100644 --- a/lib/services/multi_server_manager.dart +++ b/lib/services/multi_server_manager.dart @@ -125,6 +125,8 @@ class MultiServerManager { final client = PlexClient( config, + serverId: serverId, + serverName: server.name, prioritizedEndpoints: prioritizedEndpoints, onEndpointChanged: (newUrl) async { await storage.saveServerEndpoint(serverId, newUrl); @@ -194,11 +196,7 @@ class MultiServerManager { } /// Add a single server connection - Future addServer( - PlexServer server, { - String? clientIdentifier, - Duration timeout = const Duration(seconds: 10), - }) async { + Future addServer(PlexServer server, {String? clientIdentifier}) async { final serverId = server.clientIdentifier; final effectiveClientId = clientIdentifier ?? DateTime.now().millisecondsSinceEpoch.toString(); @@ -236,6 +234,8 @@ class MultiServerManager { final client = PlexClient( config, + serverId: serverId, + serverName: server.name, prioritizedEndpoints: prioritizedEndpoints, onEndpointChanged: (newUrl) async { await storage.saveServerEndpoint(serverId, newUrl); diff --git a/lib/services/playback_initialization_service.dart b/lib/services/playback_initialization_service.dart index 3aa6f701..dc57abba 100644 --- a/lib/services/playback_initialization_service.dart +++ b/lib/services/playback_initialization_service.dart @@ -4,8 +4,10 @@ import 'package:media_kit/media_kit.dart'; import '../client/plex_client.dart'; import '../models/plex_media_info.dart'; import '../models/plex_metadata.dart'; +import '../models/plex_user_profile.dart'; import '../utils/app_logger.dart'; import '../i18n/strings.g.dart'; +import 'track_selection_service.dart'; /// Service responsible for initializing video playback /// @@ -14,8 +16,9 @@ import '../i18n/strings.g.dart'; /// 2. Building external subtitle tracks /// 3. Opening media in the player /// 4. Adding external subtitles to the player -/// 5. Seeking to resume position -/// 6. Starting playback +/// 5. Selecting and applying audio/subtitle tracks +/// 6. Seeking to resume position +/// 7. Starting playback class PlaybackInitializationService { final Player player; final PlexClient client; @@ -33,6 +36,10 @@ class PlaybackInitializationService { Future startPlayback({ required PlexMetadata metadata, required int selectedMediaIndex, + required PlexUserProfile? profileSettings, + AudioTrack? preferredAudioTrack, + SubtitleTrack? preferredSubtitleTrack, + double? preferredPlaybackRate, }) async { try { // Get consolidated playback data (URL, media info, and versions) in a single API call @@ -62,8 +69,23 @@ class PlaybackInitializationService { await _addExternalSubtitles(externalSubtitles); } - // Set up playback position if resuming + // Select and apply tracks BEFORE seeking to resume position + // This prevents track changes from resetting the playback position on Android + final trackSelectionService = TrackSelectionService( + player: player, + profileSettings: profileSettings, + metadata: metadata, + ); + + await trackSelectionService.selectAndApplyTracks( + preferredAudioTrack: preferredAudioTrack, + preferredSubtitleTrack: preferredSubtitleTrack, + preferredPlaybackRate: preferredPlaybackRate, + ); + + // Set up playback position if resuming (AFTER track selection) if (metadata.viewOffset != null && metadata.viewOffset! > 0) { + appLogger.d('Resuming playback at ${metadata.viewOffset} ms'); final resumePosition = Duration(milliseconds: metadata.viewOffset!); await player.seek(resumePosition); } diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index ca257a49..7abdaf28 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -160,11 +160,6 @@ class StorageService { }; } - // Selected Library Index (deprecated - use library key instead) - Future saveSelectedLibraryIndex(int index) async { - await _prefs.setInt(_keySelectedLibraryIndex, index); - } - int? getSelectedLibraryIndex() { return _prefs.getInt(_keySelectedLibraryIndex); } diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 527a74b6..73160a81 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -441,7 +441,7 @@ class _MediaContextMenuState extends State { final client = _getClientForItem(); - try{ + try { final metadata = await client.getMetadata(ratingKey); if (metadata != null && context.mounted) { await Navigator.push( @@ -463,7 +463,7 @@ class _MediaContextMenuState extends State { Future _showFileInfo(BuildContext context) async { final client = _getClientForItem(); - try{ + try { // Show loading indicator if (context.mounted) { showDialog( @@ -688,7 +688,7 @@ class _MediaContextMenuState extends State { Future _showAddToPlaylistDialog(BuildContext context) async { final client = _getClientForItem(); - try{ + try { final metadata = widget.item as PlexMetadata; final itemType = metadata.type.toLowerCase(); @@ -798,7 +798,7 @@ class _MediaContextMenuState extends State { Future _showAddToCollectionDialog(BuildContext context) async { final client = _getClientForItem(); - try{ + try { final metadata = widget.item as PlexMetadata; final itemType = metadata.type.toLowerCase(); diff --git a/lib/widgets/server_badge.dart b/lib/widgets/server_badge.dart index aac3865c..dc054a96 100644 --- a/lib/widgets/server_badge.dart +++ b/lib/widgets/server_badge.dart @@ -26,7 +26,8 @@ class ServerBadge extends StatelessWidget { final theme = Theme.of(context); final bgColor = - backgroundColor ?? theme.colorScheme.primaryContainer.withValues(alpha: 0.8); + backgroundColor ?? + theme.colorScheme.primaryContainer.withValues(alpha: 0.8); final fgColor = textColor ?? theme.colorScheme.onPrimaryContainer; final displayText = showFullName