Files
plezy/lib/services/server_connection_service.dart
T

185 lines
6.4 KiB
Dart

import 'plex_auth_service.dart';
import 'storage_service.dart';
import '../client/plex_client.dart';
import '../config/plex_config.dart';
import '../models/plex_user_profile.dart';
import '../utils/app_logger.dart';
/// Result of a server connection attempt
class ServerConnectionResult {
final PlexClient? client;
final PlexUserProfile? userProfile;
final String? error;
ServerConnectionResult({this.client, this.userProfile, this.error});
bool get isSuccess => client != null;
}
/// Service for handling optimized server connections
/// Implements fast-first connection with background optimization
class ServerConnectionService {
/// Connect to a Plex server with optimized connection testing
///
/// Returns immediately with first working connection, then continues
/// testing in background to find the optimal connection.
///
/// Parameters:
/// - [server]: The PlexServer to connect to
/// - [clientIdentifier]: Client identifier for the PlexClient
/// - [plexToken]: Optional plex.tv token to save to storage
/// - [verifyServer]: Whether to verify server is accessible before returning
/// - [fetchUserProfile]: Whether to fetch and cache user profile
/// - [onProgress]: Callback for progress updates (e.g., show/hide loading)
static Future<ServerConnectionResult> connectToServer(
PlexServer server, {
required String clientIdentifier,
String? plexToken,
bool verifyServer = false,
bool fetchUserProfile = false,
void Function(String message)? onProgress,
}) async {
PlexConnection? firstConnection;
final storage = await StorageService.getInstance();
try {
// Listen to the connection stream for progressive connection testing
await for (final connection in server.findBestWorkingConnection()) {
if (firstConnection == null) {
// First emission - use this connection immediately
firstConnection = connection;
if (onProgress != null) {
onProgress('Connected to ${connection.displayType} endpoint');
}
// Save server information to storage
await storage.saveServerData(server.toJson());
await storage.saveServerUrl(connection.uri);
await storage.saveServerAccessToken(server.accessToken);
// Save plex token if provided
if (plexToken != null) {
await storage.savePlexToken(plexToken);
}
// Create client with working connection
final config = await PlexConfig.create(
baseUrl: connection.uri,
token: server.accessToken,
clientIdentifier: clientIdentifier,
);
final client = PlexClient(config);
// Verify server is accessible if requested
if (verifyServer) {
try {
await client.getServerIdentity();
} catch (e) {
appLogger.w('Server identity verification failed', error: e);
await storage.clearCredentials();
return ServerConnectionResult(
error: 'Server is not accessible: $e',
);
}
}
// Fetch user profile if requested
PlexUserProfile? userProfile;
if (fetchUserProfile && plexToken != null) {
userProfile = await _fetchAndCacheUserProfile(plexToken);
}
// Return success result
// Note: Stream continues in background to find better connection
return ServerConnectionResult(
client: client,
userProfile: userProfile,
);
} else {
// Second emission - better connection found
// Update stored connection seamlessly for future app launches
await storage.saveServerUrl(connection.uri);
appLogger.d(
'Switched to better connection: ${connection.displayType} (${connection.uri})',
);
}
}
// Handle case where no connections were found
if (firstConnection == null) {
return ServerConnectionResult(
error: 'No working connections found for this server',
);
}
// Should never reach here due to return in the stream loop
return ServerConnectionResult(
error: 'Unexpected error in connection flow',
);
} catch (e) {
appLogger.e('Error connecting to server', error: e);
return ServerConnectionResult(error: 'Connection failed: $e');
}
}
/// Fetch user profile from Plex API and cache it locally
static Future<PlexUserProfile?> _fetchAndCacheUserProfile(
String plexToken,
) async {
appLogger.d('Fetching user profile from Plex API');
try {
final authService = await PlexAuthService.create();
final profile = await authService.getUserProfile(plexToken);
appLogger.i(
'Successfully fetched user profile',
error: {
'autoSelectAudio': profile.autoSelectAudio,
'defaultAudioLanguage': profile.defaultAudioLanguage ?? 'not set',
'autoSelectSubtitle': profile.autoSelectSubtitle,
'defaultSubtitleLanguage':
profile.defaultSubtitleLanguage ?? 'not set',
'defaultSubtitleForced': profile.defaultSubtitleForced,
},
);
// Cache the profile
final storage = await StorageService.getInstance();
await storage.saveUserProfile(profile.toJson());
appLogger.d('User profile cached locally');
return profile;
} catch (e) {
appLogger.w(
'Failed to fetch user profile from API, attempting to load from cache',
error: e,
);
// Failed to fetch profile, try to load from cache
final storage = await StorageService.getInstance();
final cachedProfile = storage.getUserProfile();
if (cachedProfile != null) {
final profile = PlexUserProfile.fromJson(cachedProfile);
appLogger.i(
'Loaded user profile from cache',
error: {
'autoSelectAudio': profile.autoSelectAudio,
'defaultAudioLanguage': profile.defaultAudioLanguage ?? 'not set',
'autoSelectSubtitle': profile.autoSelectSubtitle,
'defaultSubtitleLanguage':
profile.defaultSubtitleLanguage ?? 'not set',
'defaultSubtitleForced': profile.defaultSubtitleForced,
},
);
return profile;
}
appLogger.w(
'No cached user profile available, track selection will use defaults',
);
return null;
}
}
}