worldhopper/lib/services/image_stream_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

73 lines
2.3 KiB
Dart

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path/path.dart' as path;
import 'package:worldhopper/models/server.dart';
import 'package:worldhopper/models/stream_link.dart';
import 'package:worldhopper/services/auth_service.dart';
import 'package:worldhopper/services/download_storage_service.dart';
/// Downloads all pages of an OPDS-PS chapter to a local directory
class ImageStreamDownloadService {
final AuthService _authService;
final DownloadStorageService _storageService;
ImageStreamDownloadService({
AuthService? authService,
DownloadStorageService? storageService,
}) : _authService = authService ?? AuthService(),
_storageService = storageService ?? DownloadStorageService();
/// Derive file extension from MIME type
String _extensionForType(String mimeType) {
final lower = mimeType.toLowerCase();
if (lower.contains('png')) return '.png';
if (lower.contains('gif')) return '.gif';
if (lower.contains('webp')) return '.webp';
return '.jpg'; // default for image/jpeg and unknown
}
/// Download all pages of an image stream chapter
///
/// Returns the chapter directory path on success.
Future<String> downloadChapter({
required Server server,
required StreamLink streamLink,
required String seriesId,
required String chapterId,
void Function(int downloaded, int total)? onProgress,
CancelToken? cancelToken,
}) async {
final dio = _authService.createAuthenticatedClient(server);
final chapterDir = await _storageService.getChapterDir(seriesId, chapterId);
final ext = _extensionForType(streamLink.type);
final totalPages = streamLink.pageCount;
for (var i = 0; i < totalPages; i++) {
if (cancelToken?.isCancelled == true) {
throw DioException(
requestOptions: RequestOptions(path: ''),
type: DioExceptionType.cancel,
);
}
final pageUrl = streamLink.getPageUrl(i);
final filePath = path.join(chapterDir.path, '$i$ext');
// Skip if already downloaded
if (await File(filePath).exists()) {
onProgress?.call(i + 1, totalPages);
continue;
}
await dio.download(
pageUrl,
filePath,
cancelToken: cancelToken,
);
onProgress?.call(i + 1, totalPages);
}
return chapterDir.path;
}
}