@@ -9,9 +9,11 @@ import 'package:crypto/crypto.dart';
99// Project imports:
1010import 'package:openlib/services/database.dart' show MyLibraryDb, MyBook;
1111import 'package:openlib/services/download_notification.dart' ;
12+ import 'package:openlib/services/mirror_fetcher.dart' ;
1213
1314enum DownloadStatus {
1415 queued,
16+ fetchingMirrors,
1517 downloadingMirrors,
1618 downloading,
1719 verifying,
@@ -67,12 +69,13 @@ class DownloadTask {
6769 int ? totalBytes,
6870 String ? errorMessage,
6971 CancelToken ? cancelToken,
72+ List <String >? mirrors,
7073 }) {
7174 return DownloadTask (
7275 id: id,
7376 md5: md5,
7477 title: title,
75- mirrors: mirrors,
78+ mirrors: mirrors ?? this .mirrors ,
7679 format: format,
7780 author: author,
7881 thumbnail: thumbnail,
@@ -192,6 +195,23 @@ class DownloadManager {
192195 _startDownload (task);
193196 }
194197
198+ Future <void > addDownloadWithMirrorUrl (DownloadTask task, String mirrorUrl) async {
199+ if (_activeDownloads.containsKey (task.id)) {
200+ return ;
201+ }
202+
203+ _activeDownloads[task.id] = task;
204+ _notifyListeners ();
205+
206+ await _notificationService.showDownloadNotification (
207+ id: task.id.hashCode,
208+ title: 'Queued: ${task .title }' ,
209+ progress: 0 ,
210+ );
211+
212+ _startDownloadWithMirrorUrl (task, mirrorUrl);
213+ }
214+
195215 Future <void > _startDownload (DownloadTask task) async {
196216 Dio ? dio;
197217 try {
@@ -389,6 +409,231 @@ class DownloadManager {
389409 }
390410 }
391411
412+ Future <void > _startDownloadWithMirrorUrl (DownloadTask task, String mirrorUrl) async {
413+ Dio ? dio;
414+ try {
415+ // Update status to fetching mirrors
416+ _updateTaskStatus (task.id, DownloadStatus .fetchingMirrors);
417+ await _notificationService.showDownloadNotification (
418+ id: task.id.hashCode,
419+ title: task.title,
420+ body: 'Getting mirrors...' ,
421+ progress: 0 ,
422+ );
423+
424+ // Fetch mirrors in background using headless webview
425+ final mirrorFetcher = MirrorFetcherService ();
426+ List <String > fetchedMirrors = await mirrorFetcher.fetchMirrors (mirrorUrl);
427+
428+ if (! _activeDownloads.containsKey (task.id)) {
429+ return ; // Task was cancelled while fetching mirrors
430+ }
431+
432+ if (fetchedMirrors.isEmpty) {
433+ _updateTaskStatus (task.id, DownloadStatus .failed,
434+ errorMessage: 'Failed to fetch mirrors!' );
435+ await _notificationService.showDownloadNotification (
436+ id: task.id.hashCode,
437+ title: task.title,
438+ body: 'Failed: Could not get mirrors' ,
439+ progress: - 1 ,
440+ );
441+ return ;
442+ }
443+
444+ // Update task with fetched mirrors
445+ final updatedTask = task.copyWith (mirrors: fetchedMirrors);
446+ _activeDownloads[task.id] = updatedTask;
447+
448+ // Now proceed with the regular download flow
449+ dio = Dio ();
450+ String path = await _getFilePath ('${updatedTask .md5 }.${updatedTask .format }' );
451+ List <String > orderedMirrors = _reorderMirrors (updatedTask.mirrors);
452+
453+ _updateTaskStatus (updatedTask.id, DownloadStatus .downloadingMirrors);
454+ await _notificationService.showDownloadNotification (
455+ id: updatedTask.id.hashCode,
456+ title: updatedTask.title,
457+ body: 'Finding available mirror...' ,
458+ progress: 0 ,
459+ );
460+
461+ String ? workingMirror = await _getAliveMirror (orderedMirrors);
462+
463+ if (workingMirror == null ) {
464+ _updateTaskStatus (updatedTask.id, DownloadStatus .failed,
465+ errorMessage: 'No working mirrors available!' );
466+ await _notificationService.showDownloadNotification (
467+ id: updatedTask.id.hashCode,
468+ title: updatedTask.title,
469+ body: 'Failed: No working mirrors' ,
470+ progress: - 1 ,
471+ );
472+ return ;
473+ }
474+
475+ // Try to download from each mirror until successful
476+ bool downloadSuccessful = false ;
477+ int mirrorIndex = orderedMirrors.indexOf (workingMirror);
478+
479+ // Create a single cancel token for the entire mirror retry sequence
480+ CancelToken cancelToken = CancelToken ();
481+ _activeDownloads[updatedTask.id] =
482+ _activeDownloads[updatedTask.id]! .copyWith (cancelToken: cancelToken);
483+
484+ while (mirrorIndex < orderedMirrors.length && ! downloadSuccessful) {
485+ final currentMirror = orderedMirrors[mirrorIndex];
486+
487+ try {
488+ _updateTaskStatus (updatedTask.id, DownloadStatus .downloading);
489+
490+ await dio.download (
491+ currentMirror,
492+ path,
493+ options: Options (headers: {
494+ 'Connection' : 'Keep-Alive' ,
495+ 'User-Agent' :
496+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'
497+ }),
498+ onReceiveProgress: (rcv, total) {
499+ if (! (rcv.isNaN || rcv.isInfinite) &&
500+ ! (total.isNaN || total.isInfinite)) {
501+ double progress = rcv / total;
502+ _updateTaskProgress (updatedTask.id, progress, rcv, total);
503+
504+ _notificationService.showDownloadNotification (
505+ id: updatedTask.id.hashCode,
506+ title: updatedTask.title,
507+ body: 'Downloading...' ,
508+ progress: (progress * 100 ).toInt (),
509+ );
510+ }
511+ },
512+ deleteOnError: true ,
513+ cancelToken: cancelToken,
514+ );
515+
516+ // Download completed successfully
517+ downloadSuccessful = true ;
518+
519+ } on DioException catch (e) {
520+ if (e.type == DioExceptionType .cancel) {
521+ _updateTaskStatus (updatedTask.id, DownloadStatus .cancelled);
522+ await _notificationService.cancelNotification (updatedTask.id.hashCode);
523+ return ;
524+ }
525+
526+ // Try next mirror if available
527+ mirrorIndex++ ;
528+ if (mirrorIndex < orderedMirrors.length) {
529+ _updateTaskStatus (updatedTask.id, DownloadStatus .downloadingMirrors);
530+ await _notificationService.showDownloadNotification (
531+ id: updatedTask.id.hashCode,
532+ title: updatedTask.title,
533+ body: 'Retrying with alternate mirror...' ,
534+ progress: 0 ,
535+ );
536+
537+ // Wait up to 2 seconds before retrying, but check for cancellation
538+ const totalDelay = Duration (seconds: 2 );
539+ const stepDelay = Duration (milliseconds: 100 );
540+ var elapsed = Duration .zero;
541+ while (elapsed < totalDelay) {
542+ await Future .delayed (stepDelay);
543+ elapsed += stepDelay;
544+
545+ // Check if task was cancelled during the delay
546+ if (! _activeDownloads.containsKey (updatedTask.id) ||
547+ _activeDownloads[updatedTask.id]? .cancelToken? .isCancelled == true ) {
548+ _updateTaskStatus (updatedTask.id, DownloadStatus .cancelled);
549+ await _notificationService.cancelNotification (updatedTask.id.hashCode);
550+ return ;
551+ }
552+ }
553+ } else {
554+ // No more mirrors to try; mark task as failed before re-throwing
555+ _updateTaskStatus (updatedTask.id, DownloadStatus .failed,
556+ errorMessage: 'All mirrors failed!' );
557+ await _notificationService.showDownloadNotification (
558+ id: updatedTask.id.hashCode,
559+ title: updatedTask.title,
560+ body: 'Download failed: All mirrors exhausted' ,
561+ progress: - 1 ,
562+ );
563+ rethrow ;
564+ }
565+ }
566+ }
567+
568+ if (! _activeDownloads.containsKey (updatedTask.id)) {
569+ return ;
570+ }
571+
572+ _updateTaskStatus (updatedTask.id, DownloadStatus .verifying);
573+ await _notificationService.showDownloadNotification (
574+ id: updatedTask.id.hashCode,
575+ title: updatedTask.title,
576+ body: 'Verifying file...' ,
577+ progress: 100 ,
578+ );
579+
580+ bool checkSumValid =
581+ await _verifyFileCheckSum (md5Hash: updatedTask.md5, format: updatedTask.format);
582+
583+ await _database.insert (MyBook (
584+ id: updatedTask.md5,
585+ title: updatedTask.title,
586+ author: updatedTask.author,
587+ thumbnail: updatedTask.thumbnail,
588+ link: updatedTask.link,
589+ publisher: updatedTask.publisher,
590+ info: updatedTask.info,
591+ format: updatedTask.format,
592+ description: updatedTask.description,
593+ ));
594+
595+ _updateTaskStatus (updatedTask.id, DownloadStatus .completed);
596+
597+ await _notificationService.showDownloadNotification (
598+ id: updatedTask.id.hashCode,
599+ title: updatedTask.title,
600+ body: checkSumValid
601+ ? 'Download completed!'
602+ : 'Download completed (checksum failed)' ,
603+ progress: - 1 ,
604+ );
605+
606+ await Future .delayed (const Duration (seconds: 3 ));
607+ removeDownload (updatedTask.id);
608+ } on DioException catch (e) {
609+ if (e.type == DioExceptionType .cancel) {
610+ _updateTaskStatus (task.id, DownloadStatus .cancelled);
611+ await _notificationService.cancelNotification (task.id.hashCode);
612+ } else {
613+ _updateTaskStatus (task.id, DownloadStatus .failed,
614+ errorMessage: 'Download failed! Try again...' );
615+ await _notificationService.showDownloadNotification (
616+ id: task.id.hashCode,
617+ title: task.title,
618+ body: 'Download failed' ,
619+ progress: - 1 ,
620+ );
621+ }
622+ } catch (e) {
623+ _updateTaskStatus (task.id, DownloadStatus .failed,
624+ errorMessage: 'Download failed! Try again...' );
625+ await _notificationService.showDownloadNotification (
626+ id: task.id.hashCode,
627+ title: task.title,
628+ body: 'Download failed' ,
629+ progress: - 1 ,
630+ );
631+ } finally {
632+ // Always close the Dio instance to prevent resource leaks
633+ dio? .close ();
634+ }
635+ }
636+
392637 void _updateTaskStatus (String taskId, DownloadStatus status,
393638 {String ? errorMessage}) {
394639 if (_activeDownloads.containsKey (taskId)) {
0 commit comments