fix: better server selector errors
This commit is contained in:
@@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user