refactor: simplify server tracking
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user