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 _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 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 fetchRootFeed( OPDSServer server, { CancelToken? cancelToken, }) async { return fetchFeed(server, server.url, cancelToken: cancelToken); } /// Test connection to a server Future 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; }