import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:catcher/catcher.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:metadata_god/metadata_god.dart'; import 'package:path/path.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/services/download_manager/download_manager.dart'; import 'package:spotube/services/youtube/youtube.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class DownloadManagerProvider extends ChangeNotifier { DownloadManagerProvider({required this.ref}) : $history = {}, backHistory = {}, dl = DownloadManager() { dl.statusStream.listen((event) async { final (:request, :status) = event; final track = $history.firstWhereOrNull( (element) => element.ytUri == request.url, ); if (track == null) return; final savePath = getTrackFileUrl(track); final oldFile = File("$savePath.old"); if ((status == DownloadStatus.failed || status == DownloadStatus.canceled) && await oldFile.exists()) { await oldFile.rename(savePath); } if (status != DownloadStatus.completed) return; var file = File(request.path); file.copySync(savePath); file.deleteSync(); file = File(savePath); if (await oldFile.exists()) { await oldFile.delete(); } final imageBytes = await downloadImage( TypeConversionUtils.image_X_UrlString(track.album?.images, placeholder: ImagePlaceholder.albumArt, index: 1), ); final metadata = Metadata( title: track.name, artist: track.artists?.map((a) => a.name).join(", "), album: track.album?.name, albumArtist: track.artists?.map((a) => a.name).join(", "), year: track.album?.releaseDate != null ? int.tryParse(track.album!.releaseDate!) : null, trackNumber: track.trackNumber, discNumber: track.discNumber, durationMs: track.durationMs?.toDouble(), fileSize: file.lengthSync(), trackTotal: track.album?.tracks?.length, picture: imageBytes != null ? Picture( data: imageBytes, // Spotify images are always JPEGs mimeType: 'image/jpeg', ) : null, ); await MetadataGod.writeMetadata( file: file.path, metadata: metadata, ); }); } Future Function(Track track) onFileExists = (Track track) async => true; final Ref ref; YoutubeEndpoints get yt => ref.read(youtubeProvider); String get downloadDirectory => ref.read(userPreferencesProvider.select((s) => s.downloadLocation)); int get $downloadCount => dl .getAllDownloads() .where( (download) => download.status.value == DownloadStatus.downloading || download.status.value == DownloadStatus.paused || download.status.value == DownloadStatus.queued, ) .length; final Set $history; // these are the tracks which metadata hasn't been fetched yet final Set backHistory; final DownloadManager dl; /// Spotify Images are always JPEGs Future downloadImage( String imageUrl, ) async { try { final fileStream = DefaultCacheManager().getImageFile(imageUrl); final bytes = List.empty(growable: true); await for (final data in fileStream) { if (data is FileInfo) { bytes.addAll(data.file.readAsBytesSync()); break; } } return Uint8List.fromList(bytes); } catch (e, stackTrace) { Catcher.reportCheckedError(e, stackTrace); return null; } } String getTrackFileUrl(Track track) { final name = "${track.name} - ${TypeConversionUtils.artists_X_String(track.artists ?? [])}.m4a"; return join(downloadDirectory, name); } bool isActive(Track track) { if (backHistory.contains(track)) return true; final spotubeTrack = mapToSpotubeTrack(track); if (spotubeTrack == null) return false; return dl .getAllDownloads() .where( (download) => download.status.value == DownloadStatus.downloading || download.status.value == DownloadStatus.paused || download.status.value == DownloadStatus.queued, ) .map((e) => e.request.url) .contains(spotubeTrack.ytUri); } /// For singular downloads Future addToQueue(Track track) async { final savePath = getTrackFileUrl(track); final oldFile = File(savePath); if (await oldFile.exists() && !await onFileExists(track)) { return; } if (await oldFile.exists()) { await oldFile.rename("$savePath.old"); } if (track is SpotubeTrack) { final downloadTask = await dl.addDownload(track.ytUri, savePath); if (downloadTask != null) { $history.add(track); } } else { backHistory.add(track); final spotubeTrack = await SpotubeTrack.fetchFromTrack(track, yt).then((d) { backHistory.remove(track); return d; }); final downloadTask = await dl.addDownload(spotubeTrack.ytUri, savePath); if (downloadTask != null) { $history.add(spotubeTrack); } } notifyListeners(); } Future batchAddToQueue(List tracks) async { backHistory.addAll( tracks.where((element) => element is! SpotubeTrack), ); for (final track in tracks) { await addToQueue(track); await Future.delayed(const Duration(seconds: 2)); } } Future removeFromQueue(SpotubeTrack track) async { await dl.removeDownload(track.ytUri); $history.remove(track); } Future pause(SpotubeTrack track) { return dl.pauseDownload(track.ytUri); } Future resume(SpotubeTrack track) { return dl.resumeDownload(track.ytUri); } Future retry(SpotubeTrack track) { return addToQueue(track); } void cancel(SpotubeTrack track) { dl.cancelDownload(track.ytUri); } void cancelAll() { for (final download in dl.getAllDownloads()) { dl.cancelDownload(download.request.url); } } SpotubeTrack? mapToSpotubeTrack(Track track) { if (track is SpotubeTrack) { return track; } else { return $history.firstWhereOrNull((element) => element.id == track.id); } } ValueNotifier? getStatusNotifier(SpotubeTrack track) { return dl.getDownload(track.ytUri)?.status; } ValueNotifier? getProgressNotifier(SpotubeTrack track) { return dl.getDownload(track.ytUri)?.progress; } } final downloadManagerProvider = ChangeNotifierProvider( (ref) => DownloadManagerProvider(ref: ref), );