Rename all OPDS-prefixed models to generic names (Server, Feed, Entry, Link, StreamLink), expand the ServerSoftware interface to cover all server interactions, and implement a full Kavita REST API client that replaces the OPDS delegation. - Rename OPDS* models to generic names across ~60 files - Add database migrations 13 (opds_id → entry_id) and 14 (unify credentials) - Create OPDSServerSoftware wrapping existing OPDS services - Create KavitaApiClient for direct Kavita REST API calls - Create KavitaFeedMapper to convert Kavita JSON to Feed/Entry models - Rewrite KavitaServerSoftware to use native API (no OPDS delegation) - Unify server credentials (remove softwareUsername/softwarePassword) - Simplify add/edit server UI to single auth section - Add test connection button to server add/edit screens - Add progress indicator to PublicationCard using local and server data - Eliminate softwareType branching in reader screens - Add EntryProgress and fetchEntryProgress to ServerSoftware interface - Fix Entry.acquisitionLink crash on empty links - Add 59 new tests covering models, services, and providers
145 lines
4.3 KiB
Dart
145 lines
4.3 KiB
Dart
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<String, Feed> _feedCache = {};
|
|
final Map<String, DateTime> _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<Feed> 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<Feed> 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<bool> 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;
|
|
}
|
|
}
|