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 testConnection(Server server, {CancelToken? cancelToken}) async { return _opdsService.testConnection(server, cancelToken: cancelToken); } @override Future fetchRootFeed(Server server, {CancelToken? cancelToken}) async { return _opdsService.fetchRootFeed(server, cancelToken: cancelToken); } @override Future fetchFeed(Server server, String url, {CancelToken? cancelToken}) async { return _opdsService.fetchFeed(server, url, cancelToken: cancelToken); } @override Future> fetchEntryProgress( Server server, List entries, ) async { // For OPDS, progress is already embedded in StreamLink.lastRead from the // feed response. Extract it from the entries directly. final result = {}; 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 reportPageProgress( Server server, StreamLink streamLink, int page) async { await _opdsService.reportPageProgress(server, streamLink, page); } @override Future reportEpubProgress( Server server, Entry entry, double progress, ) async { // Generic OPDS has no standard EPUB progress endpoint. } @override Future 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 markAsUnread(Server server, Entry entry) async { // No-op for generic OPDS — PSE has no unread concept } @override Future 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 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 getAuthHeaders(Server server) { return _authService.getAuthHeaders(server); } @override void clearCachedState(String serverId) { _opdsService.clearClient(serverId); } }