+16
-4
@@ -378,13 +378,25 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
final storage = await StorageService.getInstance();
|
||||
final registry = ServerRegistry(storage);
|
||||
|
||||
// Check network connectivity early to fast-path airplane mode
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
// Check network connectivity early to fast-path airplane mode.
|
||||
// Timeout guards against connectivity_plus hanging on some Android TV devices after force-close.
|
||||
final connectivityResult = await Connectivity()
|
||||
.checkConnectivity()
|
||||
.timeout(const Duration(seconds: 3), onTimeout: () => [ConnectivityResult.other]);
|
||||
final hasNetwork = !connectivityResult.contains(ConnectivityResult.none);
|
||||
|
||||
if (hasNetwork) {
|
||||
// Refresh servers from API to get updated connection info (IPs may change)
|
||||
await registry.refreshServersFromApi();
|
||||
// Refresh servers from API to get updated connection info (IPs may change).
|
||||
// If the stored token is invalid (e.g. after removing a Plex profile PIN),
|
||||
// redirect to AuthScreen so the user can re-authenticate.
|
||||
final refreshResult = await registry.refreshServersFromApi();
|
||||
if (refreshResult == ServerRefreshResult.authError) {
|
||||
await storage.clearCredentials();
|
||||
if (mounted) {
|
||||
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const AuthScreen()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Load all configured servers
|
||||
|
||||
@@ -28,7 +28,9 @@ class OfflineModeProvider extends ChangeNotifier {
|
||||
|
||||
/// Updates network and server connection flags
|
||||
Future<void> _updateConnectionFlags() async {
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
final connectivityResult = await Connectivity()
|
||||
.checkConnectivity()
|
||||
.timeout(const Duration(seconds: 3), onTimeout: () => [ConnectivityResult.other]);
|
||||
_hasNetworkConnection = !connectivityResult.contains(ConnectivityResult.none);
|
||||
_hasServerConnection = _serverManager.onlineServerIds.isNotEmpty;
|
||||
}
|
||||
|
||||
@@ -288,7 +288,9 @@ class MultiServerManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test connection health for all servers
|
||||
/// Test connection health for all servers.
|
||||
/// Uses [PlexClient.isHealthy] which checks for HTTP 200, so servers with
|
||||
/// invalid tokens (401) are correctly reported as offline.
|
||||
Future<void> checkServerHealth() async {
|
||||
appLogger.d('Checking health for ${_clients.length} servers');
|
||||
|
||||
@@ -296,13 +298,10 @@ class MultiServerManager {
|
||||
final serverId = entry.key;
|
||||
final client = entry.value;
|
||||
|
||||
try {
|
||||
// Simple ping by fetching server identity
|
||||
await client.getServerIdentity();
|
||||
updateServerStatus(serverId, true);
|
||||
} catch (e) {
|
||||
appLogger.w('Server $serverId health check failed: $e');
|
||||
updateServerStatus(serverId, false);
|
||||
final healthy = await client.isHealthy();
|
||||
updateServerStatus(serverId, healthy);
|
||||
if (!healthy) {
|
||||
appLogger.w('Server $serverId health check failed');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ class PlexClient {
|
||||
final response = await dio.get('/', options: Options(headers: {'X-Plex-Token': token}));
|
||||
|
||||
stopwatch.stop();
|
||||
final success = response.statusCode == 200 || response.statusCode == 401;
|
||||
final success = response.statusCode == 200;
|
||||
|
||||
return ConnectionTestResult(
|
||||
success: success,
|
||||
@@ -371,6 +371,17 @@ class PlexClient {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/// Check if the server connection is healthy (reachable AND authenticated).
|
||||
/// Returns true only if the server responds with HTTP 200.
|
||||
Future<bool> isHealthy() async {
|
||||
try {
|
||||
final response = await _dio.get('/identity');
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get library sections
|
||||
/// Returns libraries automatically tagged with this client's serverId and serverName
|
||||
Future<List<PlexLibrary>> getLibraries() async {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../utils/app_logger.dart';
|
||||
import 'plex_auth_service.dart';
|
||||
import 'storage_service.dart';
|
||||
|
||||
enum ServerRefreshResult { success, networkError, authError, noToken }
|
||||
|
||||
/// Centralized server configuration registry
|
||||
/// Manages which servers are available and their configurations
|
||||
class ServerRegistry {
|
||||
@@ -95,13 +99,15 @@ class ServerRegistry {
|
||||
appLogger.i('Cleared all servers from registry');
|
||||
}
|
||||
|
||||
/// Refresh servers from Plex API and update storage
|
||||
/// This updates connection info (IPs, ports) that may have changed
|
||||
Future<void> refreshServersFromApi() async {
|
||||
/// Refresh servers from Plex API and update storage.
|
||||
/// This updates connection info (IPs, ports) that may have changed.
|
||||
/// Returns [ServerRefreshResult.authError] when the stored token is rejected
|
||||
/// (e.g. after removing a Plex profile PIN), so the caller can redirect to re-auth.
|
||||
Future<ServerRefreshResult> refreshServersFromApi() async {
|
||||
final token = _storage.getPlexToken();
|
||||
if (token == null || token.isEmpty) {
|
||||
appLogger.d('No Plex token available, skipping server refresh');
|
||||
return;
|
||||
return ServerRefreshResult.noToken;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -111,7 +117,7 @@ class ServerRegistry {
|
||||
|
||||
if (freshServers.isEmpty) {
|
||||
appLogger.w('API returned no servers, keeping existing data');
|
||||
return;
|
||||
return ServerRefreshResult.success;
|
||||
}
|
||||
|
||||
// Get existing servers to preserve any local-only data
|
||||
@@ -133,9 +139,17 @@ class ServerRegistry {
|
||||
|
||||
await saveServers(updatedServers);
|
||||
appLogger.i('Refreshed ${updatedServers.length} servers from API');
|
||||
return ServerRefreshResult.success;
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 401) {
|
||||
appLogger.w('Plex token is invalid (401), re-authentication required');
|
||||
return ServerRefreshResult.authError;
|
||||
}
|
||||
appLogger.w('Failed to refresh servers from API, using cached data', error: e);
|
||||
return ServerRefreshResult.networkError;
|
||||
} catch (e, stackTrace) {
|
||||
appLogger.w('Failed to refresh servers from API, using cached data', error: e, stackTrace: stackTrace);
|
||||
// Don't rethrow - we can continue with cached servers
|
||||
return ServerRefreshResult.networkError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user