- Add pre-flight check for tracks with available provider mappings - Skip tracks that lack available providers instead of sending them to MA - Log detailed info about which tracks are playable vs unavailable - Add smart retry logic that doesn't retry non-transient errors - Improve user-facing error messages for unavailable content - Reduce unnecessary retry delays for data-related failures Fixes "No playable items found" errors when albums contain tracks that don't have available streaming providers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
139 lines
3.9 KiB
Dart
139 lines
3.9 KiB
Dart
import 'debug_logger.dart';
|
|
|
|
/// Error types for user-friendly messages
|
|
enum ErrorType {
|
|
connection,
|
|
authentication,
|
|
network,
|
|
playback,
|
|
library,
|
|
unknown,
|
|
}
|
|
|
|
class ErrorInfo {
|
|
final ErrorType type;
|
|
final String userMessage;
|
|
final String technicalMessage;
|
|
final bool canRetry;
|
|
|
|
ErrorInfo({
|
|
required this.type,
|
|
required this.userMessage,
|
|
required this.technicalMessage,
|
|
this.canRetry = true,
|
|
});
|
|
}
|
|
|
|
class ErrorHandler {
|
|
static final _logger = DebugLogger();
|
|
|
|
/// Convert technical errors into user-friendly error messages
|
|
static ErrorInfo handleError(dynamic error, {String context = ''}) {
|
|
_logger.log('❌ Error in $context: $error');
|
|
|
|
final errorStr = error.toString().toLowerCase();
|
|
|
|
// Connection errors
|
|
if (errorStr.contains('not connected') ||
|
|
errorStr.contains('disconnected') ||
|
|
errorStr.contains('connection closed')) {
|
|
return ErrorInfo(
|
|
type: ErrorType.connection,
|
|
userMessage: 'Not connected to Music Assistant',
|
|
technicalMessage: error.toString(),
|
|
canRetry: true,
|
|
);
|
|
}
|
|
|
|
// Network errors
|
|
if (errorStr.contains('socket') ||
|
|
errorStr.contains('network') ||
|
|
errorStr.contains('timeout') ||
|
|
errorStr.contains('failed host lookup')) {
|
|
return ErrorInfo(
|
|
type: ErrorType.network,
|
|
userMessage: 'Network connection failed. Please check your connection.',
|
|
technicalMessage: error.toString(),
|
|
canRetry: true,
|
|
);
|
|
}
|
|
|
|
// Authentication errors
|
|
if (errorStr.contains('401') ||
|
|
errorStr.contains('unauthorized') ||
|
|
errorStr.contains('authentication') ||
|
|
errorStr.contains('auth')) {
|
|
return ErrorInfo(
|
|
type: ErrorType.authentication,
|
|
userMessage: 'Authentication failed. Please check your credentials.',
|
|
technicalMessage: error.toString(),
|
|
canRetry: false,
|
|
);
|
|
}
|
|
|
|
// Unavailable content errors - these won't resolve with retry
|
|
if (errorStr.contains('no playable') ||
|
|
errorStr.contains('lack available providers') ||
|
|
errorStr.contains('no available providers')) {
|
|
return ErrorInfo(
|
|
type: ErrorType.playback,
|
|
userMessage: 'These tracks are unavailable. The music provider may not have them available for streaming.',
|
|
technicalMessage: error.toString(),
|
|
canRetry: false,
|
|
);
|
|
}
|
|
|
|
// Playback errors
|
|
if (errorStr.contains('queue') ||
|
|
errorStr.contains('player') ||
|
|
errorStr.contains('track')) {
|
|
return ErrorInfo(
|
|
type: ErrorType.playback,
|
|
userMessage: 'Playback failed. Please try again.',
|
|
technicalMessage: error.toString(),
|
|
canRetry: true,
|
|
);
|
|
}
|
|
|
|
// Library/content errors
|
|
if (errorStr.contains('library') ||
|
|
errorStr.contains('not found') ||
|
|
errorStr.contains('no result')) {
|
|
return ErrorInfo(
|
|
type: ErrorType.library,
|
|
userMessage: 'Failed to load content. Please try again.',
|
|
technicalMessage: error.toString(),
|
|
canRetry: true,
|
|
);
|
|
}
|
|
|
|
// Unknown errors
|
|
return ErrorInfo(
|
|
type: ErrorType.unknown,
|
|
userMessage: 'An unexpected error occurred. Please try again.',
|
|
technicalMessage: error.toString(),
|
|
canRetry: true,
|
|
);
|
|
}
|
|
|
|
/// Get user-friendly message for specific operations
|
|
static String getOperationErrorMessage(String operation, dynamic error) {
|
|
final errorInfo = handleError(error, context: operation);
|
|
return errorInfo.userMessage;
|
|
}
|
|
|
|
/// Check if error is retryable
|
|
static bool isRetryable(dynamic error) {
|
|
final errorInfo = handleError(error);
|
|
return errorInfo.canRetry;
|
|
}
|
|
|
|
/// Log error with context
|
|
static void logError(String context, dynamic error, {StackTrace? stackTrace}) {
|
|
_logger.log('❌ [$context] $error');
|
|
if (stackTrace != null) {
|
|
_logger.log('Stack trace: $stackTrace');
|
|
}
|
|
}
|
|
}
|