fix: better server selector errors

This commit is contained in:
edde746
2025-11-09 21:24:44 +01:00
parent 7fbd320fa7
commit a607904a35
2 changed files with 190 additions and 25 deletions
+81 -9
View File
@@ -1,4 +1,6 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../services/plex_auth_service.dart';
import '../services/storage_service.dart';
import '../services/server_connection_service.dart';
@@ -27,6 +29,7 @@ class _ServerSelectionScreenState extends State<ServerSelectionScreen> {
bool _isLoading = true;
String? _errorMessage;
String? _currentServerUrl;
List<Map<String, dynamic>>? _debugServerData;
@override
void initState() {
@@ -52,12 +55,58 @@ class _ServerSelectionScreenState extends State<ServerSelectionScreen> {
setState(() {
_servers = servers;
_isLoading = false;
_debugServerData = null; // Clear any previous debug data
});
} catch (e) {
setState(() {
_errorMessage = 'Failed to load servers: $e';
_errorMessage = _getErrorMessage(e);
_isLoading = false;
// Store debug data if it's a parsing exception
if (e is ServerParsingException) {
_debugServerData = e.invalidServerData;
} else {
_debugServerData = null;
}
});
appLogger.e('Failed to load servers', error: e);
}
}
String _getErrorMessage(dynamic error) {
if (error is ServerParsingException) {
return 'Found ${error.invalidServerData.length} server(s) with malformed data. No valid servers available.';
} else if (error is FormatException) {
// Handle JSON parsing errors with more user-friendly messages
if (error.message.contains('Invalid server data')) {
return 'Some servers have incomplete information and were skipped. Please check your Plex.tv account.';
} else if (error.message.contains('Invalid connection data')) {
return 'Server connection information is incomplete. Please try again.';
}
return 'Server information is malformed: ${error.message}';
} else if (error.toString().contains('SocketException') ||
error.toString().contains('TimeoutException')) {
return 'Network connection failed. Please check your internet connection and try again.';
} else if (error.toString().contains('401') ||
error.toString().contains('Unauthorized')) {
return 'Authentication failed. Please sign in again.';
} else if (error.toString().contains('404') ||
error.toString().contains('Not Found')) {
return 'Plex service unavailable. Please try again later.';
}
return 'Failed to load servers: ${error.toString()}';
}
Future<void> _copyDebugDataToClipboard() async {
if (_debugServerData == null) return;
final jsonString = const JsonEncoder.withIndent(' ').convert(_debugServerData);
await Clipboard.setData(ClipboardData(text: jsonString));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Server debug data copied to clipboard')),
);
}
}
@@ -199,18 +248,41 @@ class _ServerSelectionScreenState extends State<ServerSelectionScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_errorMessage!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
_errorMessage!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
textAlign: TextAlign.center,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadServers,
child: const Text('Retry'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _loadServers,
child: const Text('Retry'),
),
if (_debugServerData != null) ...[
const SizedBox(width: 16),
OutlinedButton.icon(
onPressed: _copyDebugDataToClipboard,
icon: const Icon(Icons.copy),
label: const Text('Copy Debug Data'),
),
],
],
),
if (_debugServerData != null) ...[
const SizedBox(height: 12),
Text(
'Debug data available for ${_debugServerData!.length} server(s)',
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
)
+109 -16
View File
@@ -143,10 +143,30 @@ class PlexAuthService {
final List<dynamic> resources = response.data as List<dynamic>;
// Filter for server resources and map to PlexServer objects
return resources
.where((r) => r['provides'] == 'server')
.map((r) => PlexServer.fromJson(r as Map<String, dynamic>))
.toList();
final servers = <PlexServer>[];
final invalidServers = <Map<String, dynamic>>[];
for (final resource in resources.where((r) => r['provides'] == 'server')) {
try {
final server = PlexServer.fromJson(resource as Map<String, dynamic>);
servers.add(server);
} catch (e) {
// Collect invalid servers for debugging
invalidServers.add(resource as Map<String, dynamic>);
continue;
}
}
// If we have invalid servers but some valid ones, that's okay
// If we have no valid servers but some invalid ones, throw with debug info
if (servers.isEmpty && invalidServers.isNotEmpty) {
throw ServerParsingException(
'No valid servers found. All ${invalidServers.length} server(s) have malformed data.',
invalidServers,
);
}
return servers;
}
/// Get user information
@@ -249,20 +269,35 @@ class PlexServer {
});
factory PlexServer.fromJson(Map<String, dynamic> json) {
// Validate required fields first
if (!_isValidServerJson(json)) {
throw FormatException('Invalid server data: missing required fields (name, clientIdentifier, accessToken, or connections)');
}
final List<dynamic> connectionsJson = json['connections'] as List<dynamic>;
final connections = <PlexConnection>[];
// Parse connections and generate HTTP fallbacks for HTTPS connections
for (final c in connectionsJson) {
final connection = PlexConnection.fromJson(c as Map<String, dynamic>);
connections.add(connection);
try {
final connection = PlexConnection.fromJson(c as Map<String, dynamic>);
connections.add(connection);
// Generate HTTP fallback for HTTPS connections
if (connection.protocol == 'https') {
connections.add(connection.toHttpFallback());
// Generate HTTP fallback for HTTPS connections
if (connection.protocol == 'https') {
connections.add(connection.toHttpFallback());
}
} catch (e) {
// Skip invalid connections rather than failing the entire server
continue;
}
}
// If no valid connections were parsed, this server is unusable
if (connections.isEmpty) {
throw FormatException('Server has no valid connections');
}
DateTime? lastSeenAt;
if (json['lastSeenAt'] != null) {
try {
@@ -273,9 +308,9 @@ class PlexServer {
}
return PlexServer(
name: json['name'] as String,
clientIdentifier: json['clientIdentifier'] as String,
accessToken: json['accessToken'] as String,
name: json['name'] as String, // Safe because validated above
clientIdentifier: json['clientIdentifier'] as String, // Safe because validated above
accessToken: json['accessToken'] as String, // Safe because validated above
connections: connections,
owned: json['owned'] as bool? ?? false,
product: json['product'] as String?,
@@ -285,6 +320,27 @@ class PlexServer {
);
}
/// Validates that server JSON contains all required fields with correct types
static bool _isValidServerJson(Map<String, dynamic> json) {
// Check for required string fields
if (json['name'] is! String || (json['name'] as String).isEmpty) {
return false;
}
if (json['clientIdentifier'] is! String || (json['clientIdentifier'] as String).isEmpty) {
return false;
}
if (json['accessToken'] is! String || (json['accessToken'] as String).isEmpty) {
return false;
}
// Check for connections array
if (json['connections'] is! List || (json['connections'] as List).isEmpty) {
return false;
}
return true;
}
Map<String, dynamic> toJson() {
return {
'name': name,
@@ -516,17 +572,43 @@ class PlexConnection {
});
factory PlexConnection.fromJson(Map<String, dynamic> json) {
// Validate required fields
if (!_isValidConnectionJson(json)) {
throw FormatException('Invalid connection data: missing required fields (protocol, address, port, or uri)');
}
return PlexConnection(
protocol: json['protocol'] as String,
address: json['address'] as String,
port: json['port'] as int,
uri: json['uri'] as String,
protocol: json['protocol'] as String, // Safe because validated above
address: json['address'] as String, // Safe because validated above
port: json['port'] as int, // Safe because validated above
uri: json['uri'] as String, // Safe because validated above
local: json['local'] as bool? ?? false,
relay: json['relay'] as bool? ?? false,
ipv6: json['IPv6'] as bool? ?? false,
);
}
/// Validates that connection JSON contains all required fields with correct types
static bool _isValidConnectionJson(Map<String, dynamic> json) {
// Check for required string fields
if (json['protocol'] is! String || (json['protocol'] as String).isEmpty) {
return false;
}
if (json['address'] is! String || (json['address'] as String).isEmpty) {
return false;
}
if (json['uri'] is! String || (json['uri'] as String).isEmpty) {
return false;
}
// Check for required port (integer)
if (json['port'] is! int) {
return false;
}
return true;
}
Map<String, dynamic> toJson() {
return {
'protocol': protocol,
@@ -565,3 +647,14 @@ class PlexConnection {
);
}
}
/// Custom exception for server parsing errors that includes debug data
class ServerParsingException implements Exception {
final String message;
final List<Map<String, dynamic>> invalidServerData;
ServerParsingException(this.message, this.invalidServerData);
@override
String toString() => message;
}