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
73 lines
2.3 KiB
Dart
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;
|
|
}
|
|
}
|