worldhopper/lib/services/epub_download_service.dart
Felipe M. 58a562abe8
feat: refactor server software abstraction with Kavita REST API integration
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
2026-04-06 17:46:48 +02:00

137 lines
3.8 KiB
Dart

import 'dart:io';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:worldhopper/models/entry.dart';
import 'package:worldhopper/models/server.dart';
import 'package:worldhopper/services/auth_service.dart';
/// Service for downloading and caching EPUB files
class EpubDownloadService {
final AuthService _authService;
final CacheManager _cacheManager;
EpubDownloadService({
AuthService? authService,
CacheManager? cacheManager,
}) : _authService = authService ?? AuthService(),
_cacheManager = cacheManager ?? DefaultCacheManager();
/// Download EPUB file from server with authentication
/// Returns the local file path of the downloaded EPUB
Future<File> downloadEpub(
Entry entry,
Server server, {
void Function(double progress)? onProgress,
}) async {
final acquisitionLink = entry.acquisitionLink;
if (acquisitionLink == null) {
throw Exception('No acquisition link found for entry: ${entry.title}');
}
final url = acquisitionLink.href;
if (url.isEmpty) {
throw Exception('Invalid acquisition link URL');
}
// Check if file is already cached
final fileInfo = await _cacheManager.getFileFromCache(url);
if (fileInfo != null && await fileInfo.file.exists()) {
return fileInfo.file;
}
// Create authenticated Dio client
final dio = _authService.createAuthenticatedClient(server);
// Download file
final cacheDir = await getTemporaryDirectory();
final fileName = '${entry.id}.epub';
final filePath = path.join(cacheDir.path, fileName);
final file = File(filePath);
await dio.download(
url,
filePath,
onReceiveProgress: (received, total) {
if (total != -1 && onProgress != null) {
final progress = received / total;
onProgress(progress);
}
},
);
// Store in cache manager
await _cacheManager.putFile(
url,
await file.readAsBytes(),
fileExtension: 'epub',
);
return file;
}
/// Get cached EPUB file if it exists
Future<File?> getCachedEpub(Entry entry) async {
final acquisitionLink = entry.acquisitionLink;
if (acquisitionLink == null) return null;
final url = acquisitionLink.href;
if (url.isEmpty) return null;
final fileInfo = await _cacheManager.getFileFromCache(url);
if (fileInfo != null && await fileInfo.file.exists()) {
return fileInfo.file;
}
return null;
}
/// Remove cached EPUB file
Future<void> removeCachedEpub(Entry entry) async {
final acquisitionLink = entry.acquisitionLink;
if (acquisitionLink == null) return;
final url = acquisitionLink.href;
if (url.isEmpty) return;
await _cacheManager.removeFile(url);
}
/// Check if EPUB is cached
Future<bool> isEpubCached(Entry entry) async {
final acquisitionLink = entry.acquisitionLink;
if (acquisitionLink == null) return false;
final url = acquisitionLink.href;
if (url.isEmpty) return false;
final fileInfo = await _cacheManager.getFileFromCache(url);
return fileInfo != null && await fileInfo.file.exists();
}
/// Get download progress for an EPUB
Stream<double> getDownloadProgress(Entry entry, Server server) async* {
final acquisitionLink = entry.acquisitionLink;
if (acquisitionLink == null) {
throw Exception('No acquisition link found for entry: ${entry.title}');
}
final url = acquisitionLink.href;
if (url.isEmpty) {
throw Exception('Invalid acquisition link URL');
}
double progress = 0.0;
yield progress;
await downloadEpub(
entry,
server,
onProgress: (p) {
progress = p;
},
);
yield progress;
}
}