import 'package:dio/dio.dart'; import 'package:worldhopper/models/feed.dart'; import 'package:worldhopper/services/server_software/server_software_service.dart'; import 'package:worldhopper/services/url_helper.dart'; import 'package:worldhopper/repositories/server_repository.dart'; /// Repository for managing server feeds with caching. /// /// Dispatches to the appropriate [ServerSoftware] implementation based on /// the server's software type. class FeedRepository { final ServerSoftwareService _softwareService; final ServerRepository _serverRepository; // Simple in-memory cache final Map _feedCache = {}; final Map _cacheTimestamps = {}; final Duration _cacheDuration = const Duration(minutes: 5); FeedRepository({ ServerSoftwareService? softwareService, ServerRepository? serverRepository, }) : _softwareService = softwareService ?? ServerSoftwareService(), _serverRepository = serverRepository ?? ServerRepository(); /// Fetch a feed with caching. Future fetchFeed( String serverId, String url, { bool forceRefresh = false, CancelToken? cancelToken, }) async { final server = await _serverRepository.getServer(serverId); if (server == null) { throw Exception('Server not found: $serverId'); } // Resolve relative URLs against server base URL, but skip for // synthetic URLs (e.g., kavita://...) which are already absolute. final resolvedUrl = url.startsWith('kavita://') ? url : UrlHelper.resolveUrl(server.url, url); final cacheKey = '$serverId:$resolvedUrl'; // Check cache if not forcing refresh if (!forceRefresh && _isCached(cacheKey)) { return _feedCache[cacheKey]!; } // Dispatch to the right ServerSoftware implementation final software = _softwareService.getImplementation(server); final parsedFeed = await software.fetchFeed( server, resolvedUrl, cancelToken: cancelToken, ); final feed = parsedFeed.feed; // Update cache _feedCache[cacheKey] = feed; _cacheTimestamps[cacheKey] = DateTime.now(); // Update server's last synced timestamp await _serverRepository.updateLastSynced(serverId, DateTime.now()); return feed; } /// Fetch the root feed for a server. Future fetchRootFeed( String serverId, { bool forceRefresh = false, CancelToken? cancelToken, }) async { final server = await _serverRepository.getServer(serverId); if (server == null) { throw Exception('Server not found: $serverId'); } final cacheKey = '$serverId:__root__'; if (!forceRefresh && _isCached(cacheKey)) { return _feedCache[cacheKey]!; } final software = _softwareService.getImplementation(server); final parsedFeed = await software.fetchRootFeed( server, cancelToken: cancelToken, ); final feed = parsedFeed.feed; _feedCache[cacheKey] = feed; _cacheTimestamps[cacheKey] = DateTime.now(); await _serverRepository.updateLastSynced(serverId, DateTime.now()); return feed; } /// Test connection to a server. Future testConnection(String serverId) async { final server = await _serverRepository.getServer(serverId); if (server == null) { throw Exception('Server not found: $serverId'); } final software = _softwareService.getImplementation(server); return await software.testConnection(server); } /// Clear cache for a specific feed. void clearFeedCache(String serverId, String url) { final cacheKey = '$serverId:$url'; _feedCache.remove(cacheKey); _cacheTimestamps.remove(cacheKey); } /// Clear cache for a specific server. void clearServerCache(String serverId) { _feedCache.removeWhere((key, _) => key.startsWith('$serverId:')); _cacheTimestamps.removeWhere((key, _) => key.startsWith('$serverId:')); } /// Clear all cache. void clearAllCache() { _feedCache.clear(); _cacheTimestamps.clear(); } /// Check if a feed is cached and still valid. bool _isCached(String cacheKey) { if (!_feedCache.containsKey(cacheKey)) { return false; } final timestamp = _cacheTimestamps[cacheKey]; if (timestamp == null) { return false; } final age = DateTime.now().difference(timestamp); return age < _cacheDuration; } }