worldhopper/lib/services/opds_service.dart

167 lines
4.6 KiB
Dart

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:worldhopper/models/opds_server.dart';
import 'package:worldhopper/services/auth_service.dart';
import 'package:worldhopper/services/opds_parser.dart'
show OPDSParser, ParsedFeed;
/// Service for fetching OPDS feeds over HTTP
class OPDSService {
final AuthService _authService;
final OPDSParser _parser;
final Map<String, Dio> _clients = {};
OPDSService({
AuthService? authService,
OPDSParser? parser,
}) : _authService = authService ?? AuthService(),
_parser = parser ?? OPDSParser();
/// Get or create a Dio client for a server
Dio _getClient(OPDSServer server) {
if (!_clients.containsKey(server.id)) {
_clients[server.id] = _authService.createAuthenticatedClient(server);
}
return _clients[server.id]!;
}
/// Fetch and parse an OPDS feed from a URL
Future<ParsedFeed> fetchFeed(
OPDSServer server,
String url, {
CancelToken? cancelToken,
}) async {
try {
final client = _getClient(server);
// Log the URL being fetched for debugging
debugPrint('Fetching OPDS feed: $url');
final response = await client.get(
url,
cancelToken: cancelToken,
options: Options(
responseType: ResponseType.plain,
),
);
if (response.statusCode == 200) {
final xmlContent = response.data as String;
return _parser.parseFeed(xmlContent, server: server);
} else {
throw OPDSException(
'Failed to fetch feed: HTTP ${response.statusCode}',
statusCode: response.statusCode,
);
}
} on DioException catch (e) {
debugPrint(
'DioException fetching feed from $url: ${e.type}, ${e.message}');
throw _handleDioException(e);
} catch (e) {
debugPrint('Error fetching feed from $url: $e');
throw OPDSException('Error fetching feed: $e');
}
}
/// Fetch the root feed for a server
Future<ParsedFeed> fetchRootFeed(
OPDSServer server, {
CancelToken? cancelToken,
}) async {
return fetchFeed(server, server.url, cancelToken: cancelToken);
}
/// Test connection to a server
Future<bool> testConnection(
OPDSServer server, {
CancelToken? cancelToken,
}) async {
try {
await fetchRootFeed(server, cancelToken: cancelToken);
return true;
} catch (e) {
return false;
}
}
/// Clear cached clients (useful when server credentials change)
void clearClients() {
_clients.clear();
}
/// Clear client for a specific server
void clearClient(String serverId) {
_clients.remove(serverId);
}
/// Handle Dio exceptions and convert to OPDSException
OPDSException _handleDioException(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return OPDSException(
'Connection timeout. Please check your network connection.',
statusCode: null,
isNetworkError: true,
);
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
if (statusCode == 401 || statusCode == 403) {
return OPDSException(
'Authentication failed. Please check your credentials.',
statusCode: statusCode,
isAuthError: true,
);
} else if (statusCode == 404) {
return OPDSException(
'Feed not found (404).',
statusCode: statusCode,
);
} else {
return OPDSException(
'Server error: HTTP $statusCode',
statusCode: statusCode,
);
}
case DioExceptionType.cancel:
return OPDSException('Request cancelled');
case DioExceptionType.connectionError:
return OPDSException(
'Connection error: ${e.message ?? "Could not connect to server"}',
isNetworkError: true,
);
case DioExceptionType.unknown:
return OPDSException(
'Network error: ${e.message ?? e.error?.toString() ?? "Unknown error"}',
isNetworkError: true,
);
default:
return OPDSException('Error: ${e.message ?? "Unknown error"}');
}
}
}
/// Custom exception for OPDS-related errors
class OPDSException implements Exception {
final String message;
final int? statusCode;
final bool isNetworkError;
final bool isAuthError;
OPDSException(
this.message, {
this.statusCode,
this.isNetworkError = false,
this.isAuthError = false,
});
@override
String toString() => message;
}