worldhopper/lib/services/server_software/opds_server_software.dart
Felipe M. b5debe2117
All checks were successful
Release / build (push) Successful in 9m40s
feat: sync EPUB reading progress to Kavita with debounced updates
2026-04-17 12:09:44 +02:00

151 lines
4.7 KiB
Dart

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:worldhopper/models/entry.dart';
import 'package:worldhopper/models/server.dart';
import 'package:worldhopper/models/stream_link.dart';
import 'package:worldhopper/services/auth_service.dart';
import 'package:worldhopper/services/epub_download_service.dart';
import 'package:worldhopper/services/image_stream_download_service.dart';
import 'package:worldhopper/services/opds_parser.dart' show ParsedFeed;
import 'package:worldhopper/services/opds_service.dart';
import 'package:worldhopper/services/server_software/server_software.dart';
/// OPDS implementation of [ServerSoftware].
///
/// Wraps the existing OPDS service layer (fetching XML feeds, PSE page
/// streaming, HTTP Basic Auth) to implement the full server interface.
/// This is the default/fallback implementation for generic OPDS servers.
class OPDSServerSoftware implements ServerSoftware {
final OPDSService _opdsService;
final AuthService _authService;
final EpubDownloadService _epubDownloadService;
final ImageStreamDownloadService _imageStreamDownloadService;
OPDSServerSoftware({
OPDSService? opdsService,
AuthService? authService,
EpubDownloadService? epubDownloadService,
ImageStreamDownloadService? imageStreamDownloadService,
}) : _opdsService = opdsService ?? OPDSService(),
_authService = authService ?? AuthService(),
_epubDownloadService = epubDownloadService ?? EpubDownloadService(),
_imageStreamDownloadService =
imageStreamDownloadService ?? ImageStreamDownloadService();
@override
bool detectFromUrl(Server server) => true; // OPDS is the fallback
@override
Future<bool> testConnection(Server server, {CancelToken? cancelToken}) async {
return _opdsService.testConnection(server, cancelToken: cancelToken);
}
@override
Future<ParsedFeed> fetchRootFeed(Server server,
{CancelToken? cancelToken}) async {
return _opdsService.fetchRootFeed(server, cancelToken: cancelToken);
}
@override
Future<ParsedFeed> fetchFeed(Server server, String url,
{CancelToken? cancelToken}) async {
return _opdsService.fetchFeed(server, url, cancelToken: cancelToken);
}
@override
Future<Map<String, EntryProgress>> fetchEntryProgress(
Server server,
List<Entry> entries,
) async {
// For OPDS, progress is already embedded in StreamLink.lastRead from the
// feed response. Extract it from the entries directly.
final result = <String, EntryProgress>{};
for (final entry in entries) {
if (entry.hasStreamLink &&
entry.streamLink!.lastRead != null &&
entry.streamLink!.lastRead! > 0) {
result[entry.id] = EntryProgress(
entryId: entry.id,
currentPage: entry.streamLink!.lastRead!,
totalPages: entry.streamLink!.pageCount,
);
}
}
return result;
}
@override
Future<void> reportPageProgress(
Server server, StreamLink streamLink, int page) async {
await _opdsService.reportPageProgress(server, streamLink, page);
}
@override
Future<void> reportEpubProgress(
Server server,
Entry entry,
double progress,
) async {
// Generic OPDS has no standard EPUB progress endpoint.
}
@override
Future<void> markAsRead(Server server, Entry entry) async {
// PSE-based: fetch the last page URL to trigger server-side progress
if (entry.hasStreamLink) {
await _opdsService.markAsRead(server, entry.streamLink!);
}
}
@override
Future<void> markAsUnread(Server server, Entry entry) async {
// No-op for generic OPDS — PSE has no unread concept
}
@override
Future<File> downloadEpub(
Server server,
Entry entry, {
String? savePath,
void Function(double progress)? onProgress,
CancelToken? cancelToken,
}) async {
return _epubDownloadService.downloadEpub(
entry,
server,
savePath: savePath,
onProgress: onProgress,
cancelToken: cancelToken,
);
}
@override
Future<String> downloadImageStreamChapter({
required Server server,
required StreamLink streamLink,
required String seriesId,
required String chapterId,
void Function(int downloaded, int total)? onProgress,
CancelToken? cancelToken,
}) async {
return _imageStreamDownloadService.downloadChapter(
server: server,
streamLink: streamLink,
seriesId: seriesId,
chapterId: chapterId,
onProgress: onProgress,
cancelToken: cancelToken,
);
}
@override
Map<String, String> getAuthHeaders(Server server) {
return _authService.getAuthHeaders(server);
}
@override
void clearCachedState(String serverId) {
_opdsService.clearClient(serverId);
}
}