refactor: simplify server tracking

This commit is contained in:
edde746
2025-11-22 23:41:46 +01:00
parent 21627f7a97
commit 6aecaaa44a
14 changed files with 91 additions and 122 deletions
+8
View File
@@ -32,6 +32,12 @@ class PlexClient {
final EndpointFailoverManager? _endpointManager;
final Future<void> 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<int> responseBytes,
@@ -43,6 +49,8 @@ class PlexClient {
PlexClient(
this.config, {
required this.serverId,
this.serverName,
List<String>? prioritizedEndpoints,
Future<void> Function(String newBaseUrl)? onEndpointChanged,
}) : _endpointManager =
+30 -2
View File
@@ -49,10 +49,38 @@ class PlayQueueResponse {
this.items,
});
factory PlayQueueResponse.fromJson(Map<String, dynamic> json) {
factory PlayQueueResponse.fromJson(
Map<String, dynamic> json, {
String? serverId,
String? serverName,
}) {
// The API returns data wrapped in MediaContainer
final container = json['MediaContainer'] as Map<String, dynamic>? ?? 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
-13
View File
@@ -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);
+5 -15
View File
@@ -202,29 +202,19 @@ class _HubDetailScreenState extends State<HubDetailScreen> 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(() {
@@ -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
-2
View File
@@ -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)
+2 -10
View File
@@ -52,18 +52,10 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
});
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) {
+6 -14
View File
@@ -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<VideoPlayerScreen>
MediaControlsManager? _mediaControlsManager;
PlaybackProgressTracker? _progressTracker;
VideoFilterManager? _videoFilterManager;
TrackSelectionService? _trackSelectionService;
final EpisodeNavigationService _episodeNavigation =
EpisodeNavigationService();
@@ -557,6 +555,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
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<VideoPlayerScreen>
}
}
// 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(
+2 -29
View File
@@ -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<List<PlexLibrary>> 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) {
+5 -5
View File
@@ -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<bool> addServer(
PlexServer server, {
String? clientIdentifier,
Duration timeout = const Duration(seconds: 10),
}) async {
Future<bool> 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);
@@ -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<PlaybackInitializationResult> 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);
}
-5
View File
@@ -160,11 +160,6 @@ class StorageService {
};
}
// Selected Library Index (deprecated - use library key instead)
Future<void> saveSelectedLibraryIndex(int index) async {
await _prefs.setInt(_keySelectedLibraryIndex, index);
}
int? getSelectedLibraryIndex() {
return _prefs.getInt(_keySelectedLibraryIndex);
}
+4 -4
View File
@@ -441,7 +441,7 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
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<MediaContextMenu> {
Future<void> _showFileInfo(BuildContext context) async {
final client = _getClientForItem();
try{
try {
// Show loading indicator
if (context.mounted) {
showDialog(
@@ -688,7 +688,7 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
Future<void> _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<MediaContextMenu> {
Future<void> _showAddToCollectionDialog(BuildContext context) async {
final client = _getClientForItem();
try{
try {
final metadata = widget.item as PlexMetadata;
final itemType = metadata.type.toLowerCase();
+2 -1
View File
@@ -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