perf(auth): better connection choice
This commit is contained in:
@@ -6,6 +6,14 @@ import '../models/plex_media_info.dart';
|
||||
import '../models/plex_filter.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
/// Result of testing a connection, including success status and latency
|
||||
class ConnectionTestResult {
|
||||
final bool success;
|
||||
final int latencyMs;
|
||||
|
||||
ConnectionTestResult({required this.success, required this.latencyMs});
|
||||
}
|
||||
|
||||
class PlexClient {
|
||||
final PlexConfig config;
|
||||
late final Dio _dio;
|
||||
@@ -43,12 +51,28 @@ class PlexClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test connection to a specific URL with token
|
||||
/// Test connection to a specific URL with token (legacy method)
|
||||
static Future<bool> testConnectionUrl(
|
||||
String baseUrl,
|
||||
String token, {
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
}) async {
|
||||
final result = await testConnectionWithLatency(
|
||||
baseUrl,
|
||||
token,
|
||||
timeout: timeout,
|
||||
);
|
||||
return result.success;
|
||||
}
|
||||
|
||||
/// Test connection to a specific URL with token and measure latency
|
||||
static Future<ConnectionTestResult> testConnectionWithLatency(
|
||||
String baseUrl,
|
||||
String token, {
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
}) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
@@ -64,12 +88,57 @@ class PlexClient {
|
||||
options: Options(headers: {'X-Plex-Token': token}),
|
||||
);
|
||||
|
||||
return response.statusCode == 200 || response.statusCode == 401;
|
||||
stopwatch.stop();
|
||||
final success = response.statusCode == 200 || response.statusCode == 401;
|
||||
|
||||
return ConnectionTestResult(
|
||||
success: success,
|
||||
latencyMs: stopwatch.elapsedMilliseconds,
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
stopwatch.stop();
|
||||
return ConnectionTestResult(
|
||||
success: false,
|
||||
latencyMs: stopwatch.elapsedMilliseconds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test connection multiple times and return average latency
|
||||
static Future<ConnectionTestResult> testConnectionWithAverageLatency(
|
||||
String baseUrl,
|
||||
String token, {
|
||||
int attempts = 3,
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
}) async {
|
||||
final results = <ConnectionTestResult>[];
|
||||
|
||||
for (int i = 0; i < attempts; i++) {
|
||||
final result = await testConnectionWithLatency(
|
||||
baseUrl,
|
||||
token,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
// If any attempt fails, return failed result immediately
|
||||
if (!result.success) {
|
||||
return ConnectionTestResult(
|
||||
success: false,
|
||||
latencyMs: result.latencyMs,
|
||||
);
|
||||
}
|
||||
|
||||
results.add(result);
|
||||
}
|
||||
|
||||
// Calculate average latency from successful attempts
|
||||
final avgLatency =
|
||||
results.fold<int>(0, (sum, result) => sum + result.latencyMs) ~/
|
||||
results.length;
|
||||
|
||||
return ConnectionTestResult(success: true, latencyMs: avgLatency);
|
||||
}
|
||||
|
||||
/// Get server identity
|
||||
Future<Map<String, dynamic>> getServerIdentity() async {
|
||||
final response = await _dio.get('/identity');
|
||||
|
||||
+24
-97
@@ -3,15 +3,13 @@ import 'package:flutter/services.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'client/plex_client.dart';
|
||||
import 'config/plex_config.dart';
|
||||
import 'screens/main_screen.dart';
|
||||
import 'screens/auth_screen.dart';
|
||||
import 'services/storage_service.dart';
|
||||
import 'services/plex_auth_service.dart';
|
||||
import 'services/server_connection_service.dart';
|
||||
import 'services/macos_titlebar_service.dart';
|
||||
import 'services/fullscreen_state_manager.dart';
|
||||
import 'models/plex_user_profile.dart';
|
||||
import 'utils/language_codes.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
|
||||
@@ -97,52 +95,37 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
// Recreate PlexServer from stored data
|
||||
final server = PlexServer.fromJson(serverData);
|
||||
|
||||
// Test connections to find best working one
|
||||
final connection = await server.findBestWorkingConnection();
|
||||
// Connect using the optimized service
|
||||
final result = await ServerConnectionService.connectToServer(
|
||||
server,
|
||||
clientIdentifier: clientId,
|
||||
verifyServer: true,
|
||||
fetchUserProfile: plexToken != null,
|
||||
plexToken: plexToken,
|
||||
);
|
||||
|
||||
if (connection != null) {
|
||||
// Update stored server URL with working connection
|
||||
await storage.saveServerUrl(connection.uri);
|
||||
|
||||
// Create client with working connection
|
||||
final config = await PlexConfig.create(
|
||||
baseUrl: connection.uri,
|
||||
token: server.accessToken,
|
||||
clientIdentifier: clientId,
|
||||
);
|
||||
final client = PlexClient(config);
|
||||
|
||||
// Verify server is accessible
|
||||
try {
|
||||
await client.getServerIdentity();
|
||||
|
||||
// Fetch and cache user profile if we have a plex token
|
||||
PlexUserProfile? userProfile;
|
||||
if (plexToken != null) {
|
||||
userProfile = await _fetchAndCacheUserProfile(plexToken);
|
||||
}
|
||||
|
||||
// Success! Navigate to main screen
|
||||
if (mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
MainScreen(client: client, userProfile: userProfile),
|
||||
// Handle result
|
||||
if (result.isSuccess) {
|
||||
// Success! Navigate to main screen
|
||||
if (mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MainScreen(
|
||||
client: result.client!,
|
||||
userProfile: result.userProfile,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Server identity check failed
|
||||
await storage.clearCredentials();
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// No working connections found
|
||||
// Connection failed, clear credentials
|
||||
await storage.clearCredentials();
|
||||
}
|
||||
} catch (e) {
|
||||
// Error loading or testing server
|
||||
appLogger.e('Error during auto-login', error: e);
|
||||
await storage.clearCredentials();
|
||||
}
|
||||
}
|
||||
@@ -156,62 +139,6 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
|
||||
@@ -2,11 +2,11 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../client/plex_client.dart';
|
||||
import '../config/plex_config.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../models/plex_user_profile.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../services/plex_auth_service.dart';
|
||||
import '../services/server_connection_service.dart';
|
||||
import '../widgets/media_card.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import '../widgets/server_list_tile.dart';
|
||||
@@ -229,34 +229,16 @@ class _DiscoverScreenState extends State<DiscoverScreen> with Refreshable {
|
||||
);
|
||||
}
|
||||
|
||||
// Test connections to find best working one
|
||||
final connection = await server.findBestWorkingConnection();
|
||||
|
||||
// Close loading dialog
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
if (connection == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('No working connections found for this server'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Store server information
|
||||
final storage = await StorageService.getInstance();
|
||||
await storage.saveServerData(server.toJson());
|
||||
await storage.saveServerUrl(connection.uri);
|
||||
await storage.saveServerAccessToken(server.accessToken);
|
||||
|
||||
// Get client identifier
|
||||
final storage = await StorageService.getInstance();
|
||||
final clientId = storage.getClientIdentifier();
|
||||
|
||||
if (clientId == null) {
|
||||
// Close loading dialog
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Client identifier not found')),
|
||||
@@ -265,20 +247,35 @@ class _DiscoverScreenState extends State<DiscoverScreen> with Refreshable {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new client
|
||||
final config = await PlexConfig.create(
|
||||
baseUrl: connection.uri,
|
||||
token: server.accessToken,
|
||||
// Connect using the optimized service
|
||||
final result = await ServerConnectionService.connectToServer(
|
||||
server,
|
||||
clientIdentifier: clientId,
|
||||
);
|
||||
final client = PlexClient(config);
|
||||
|
||||
// Replace current screen with main screen (includes bottom nav)
|
||||
// Close loading dialog
|
||||
if (mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => MainScreen(client: client)),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
// Handle result
|
||||
if (result.isSuccess) {
|
||||
// Replace current screen with main screen (includes bottom nav)
|
||||
if (mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MainScreen(client: result.client!),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Show error
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(result.error ?? 'Connection failed')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/plex_auth_service.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../client/plex_client.dart';
|
||||
import '../config/plex_config.dart';
|
||||
import '../services/server_connection_service.dart';
|
||||
import '../widgets/server_list_tile.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import 'main_screen.dart';
|
||||
@@ -66,49 +65,41 @@ class _ServerSelectionScreenState extends State<ServerSelectionScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// Test connections to find best working one
|
||||
final connection = await server.findBestWorkingConnection();
|
||||
// Get client identifier
|
||||
final storage = await StorageService.getInstance();
|
||||
final clientId =
|
||||
storage.getClientIdentifier() ?? widget.authService.clientIdentifier;
|
||||
|
||||
// Connect using the optimized service
|
||||
final result = await ServerConnectionService.connectToServer(
|
||||
server,
|
||||
clientIdentifier: clientId,
|
||||
plexToken: widget.plexToken,
|
||||
);
|
||||
|
||||
// Close loading dialog
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
if (connection == null) {
|
||||
// Handle result
|
||||
if (result.isSuccess) {
|
||||
// Navigate to main app
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('No working connections found for this server'),
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MainScreen(client: result.client!),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Store server information
|
||||
final storage = await StorageService.getInstance();
|
||||
await storage.saveServerData(server.toJson());
|
||||
await storage.saveServerUrl(connection.uri);
|
||||
await storage.saveServerAccessToken(server.accessToken);
|
||||
await storage.savePlexToken(widget.plexToken);
|
||||
|
||||
// Get client identifier
|
||||
final clientId =
|
||||
storage.getClientIdentifier() ?? widget.authService.clientIdentifier;
|
||||
|
||||
// Create client and navigate to main app
|
||||
final config = await PlexConfig.create(
|
||||
baseUrl: connection.uri,
|
||||
token: server.accessToken,
|
||||
clientIdentifier: clientId,
|
||||
);
|
||||
final client = PlexClient(config);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => MainScreen(client: client)),
|
||||
);
|
||||
} else {
|
||||
// Show error
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(result.error ?? 'Connection failed')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -281,9 +281,121 @@ class PlexServer {
|
||||
}
|
||||
|
||||
/// Find the best working connection by testing them
|
||||
/// Tests ALL connections simultaneously and returns the best working one
|
||||
/// Returns a Stream that emits connections progressively:
|
||||
/// 1. First emission: The first connection that responds successfully
|
||||
/// 2. Second emission (optional): The best connection after latency testing
|
||||
/// Priority: local > remote > relay (from successful connections)
|
||||
Future<PlexConnection?> findBestWorkingConnection() async {
|
||||
Stream<PlexConnection> findBestWorkingConnection() async* {
|
||||
if (connections.isEmpty) return;
|
||||
|
||||
// Phase 1: Race to find first working connection
|
||||
final completer = Completer<PlexConnection?>();
|
||||
PlexConnection? firstConnection;
|
||||
int completedTests = 0;
|
||||
|
||||
// Start testing all connections simultaneously
|
||||
for (final connection in connections) {
|
||||
PlexClient.testConnectionWithLatency(connection.uri, accessToken).then((
|
||||
result,
|
||||
) {
|
||||
completedTests++;
|
||||
|
||||
// If this is the first successful connection, emit it immediately
|
||||
if (result.success && !completer.isCompleted) {
|
||||
completer.complete(connection);
|
||||
}
|
||||
|
||||
// If all tests complete without success, complete with null
|
||||
if (completedTests == connections.length && !completer.isCompleted) {
|
||||
completer.complete(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for and emit the first successful connection
|
||||
firstConnection = await completer.future;
|
||||
if (firstConnection == null) {
|
||||
return; // No working connections found
|
||||
}
|
||||
|
||||
yield firstConnection;
|
||||
|
||||
// Phase 2: Continue testing in background to find best connection
|
||||
// Test each connection 2-3 times and average the latency
|
||||
final connectionResults = <PlexConnection, ConnectionTestResult>{};
|
||||
|
||||
await Future.wait(
|
||||
connections.map((connection) async {
|
||||
final result = await PlexClient.testConnectionWithAverageLatency(
|
||||
connection.uri,
|
||||
accessToken,
|
||||
attempts: 2,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
connectionResults[connection] = result;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// If no connections succeeded, we're done
|
||||
if (connectionResults.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the best connection considering both priority and latency
|
||||
PlexConnection? bestConnection;
|
||||
int bestLatency = double.maxFinite.toInt();
|
||||
|
||||
// Group connections by priority
|
||||
final localConnections = connectionResults.entries
|
||||
.where((e) => e.key.local && !e.key.relay)
|
||||
.toList();
|
||||
final remoteConnections = connectionResults.entries
|
||||
.where((e) => !e.key.local && !e.key.relay)
|
||||
.toList();
|
||||
final relayConnections = connectionResults.entries
|
||||
.where((e) => e.key.relay)
|
||||
.toList();
|
||||
|
||||
// Find best local connection
|
||||
for (final entry in localConnections) {
|
||||
if (entry.value.latencyMs < bestLatency) {
|
||||
bestLatency = entry.value.latencyMs;
|
||||
bestConnection = entry.key;
|
||||
}
|
||||
}
|
||||
|
||||
// If no local connection, find best remote connection
|
||||
if (bestConnection == null) {
|
||||
for (final entry in remoteConnections) {
|
||||
if (entry.value.latencyMs < bestLatency) {
|
||||
bestLatency = entry.value.latencyMs;
|
||||
bestConnection = entry.key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no remote connection, find best relay connection
|
||||
if (bestConnection == null) {
|
||||
for (final entry in relayConnections) {
|
||||
if (entry.value.latencyMs < bestLatency) {
|
||||
bestLatency = entry.value.latencyMs;
|
||||
bestConnection = entry.key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the best connection if it's different from the first one
|
||||
if (bestConnection != null && bestConnection.uri != firstConnection.uri) {
|
||||
yield bestConnection;
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy method for backward compatibility - returns first working connection
|
||||
/// For optimal performance, use findBestWorkingConnection() stream instead
|
||||
@Deprecated('Use findBestWorkingConnection() stream for optimized connection')
|
||||
Future<PlexConnection?> findBestWorkingConnectionLegacy() async {
|
||||
if (connections.isEmpty) return null;
|
||||
|
||||
// Test all connections simultaneously
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user