From 6752adc9398818f51b69fced226b4b8410fb9e9b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 8 Jun 2023 08:36:56 +0600 Subject: [PATCH] feat: Better download manager with download progress --- lib/components/library/user_downloads.dart | 91 ++++++--- lib/components/player/player_actions.dart | 11 +- lib/components/root/sidebar.dart | 5 +- .../root/spotube_navigation_bar.dart | 4 +- .../shared/track_table/tracks_table_view.dart | 13 +- lib/main.dart | 33 ---- lib/pages/root/root_app.dart | 4 +- lib/pages/settings/settings.dart | 5 +- lib/provider/download_manager_provider.dart | 179 ++++++++++++++++++ lib/provider/downloader_provider.dart | 165 ---------------- pubspec.lock | 8 + pubspec.yaml | 1 + 12 files changed, 270 insertions(+), 249 deletions(-) create mode 100644 lib/provider/download_manager_provider.dart delete mode 100644 lib/provider/downloader_provider.dart diff --git a/lib/components/library/user_downloads.dart b/lib/components/library/user_downloads.dart index f8ad1d70e..1c217d9cd 100644 --- a/lib/components/library/user_downloads.dart +++ b/lib/components/library/user_downloads.dart @@ -1,11 +1,14 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/downloader_provider.dart'; +import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class UserDownloads extends HookConsumerWidget { @@ -13,7 +16,8 @@ class UserDownloads extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final downloader = ref.watch(downloaderProvider); + ref.watch(downloadManagerProvider); + final downloadManager = ref.watch(downloadManagerProvider.notifier); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -26,7 +30,7 @@ class UserDownloads extends HookConsumerWidget { Expanded( child: AutoSizeText( context.l10n - .currently_downloading(downloader.currentlyRunning), + .currently_downloading(downloadManager.totalDownloads), maxLines: 1, style: Theme.of(context).textTheme.headlineMedium, ), @@ -37,9 +41,9 @@ class UserDownloads extends HookConsumerWidget { backgroundColor: Colors.red[50], foregroundColor: Colors.red[400], ), - onPressed: downloader.currentlyRunning > 0 - ? downloader.cancelAll - : null, + onPressed: downloadManager.totalDownloads == 0 + ? null + : downloadManager.cancelAll, child: Text(context.l10n.cancel_all), ), ], @@ -48,36 +52,63 @@ class UserDownloads extends HookConsumerWidget { Expanded( child: SafeArea( child: ListView.builder( - itemCount: downloader.inQueue.length, + itemCount: downloadManager.totalDownloads, itemBuilder: (context, index) { - final track = downloader.inQueue.elementAt(index); - return ListTile( - title: Text(track.name ?? ''), - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - height: 40, - width: 40, - path: TypeConversionUtils.image_X_UrlString( - track.album?.images, - placeholder: ImagePlaceholder.albumArt, + final track = downloadManager.items.elementAt(index); + return HookBuilder(builder: (context) { + final task = useStream( + downloadManager.activeDownloadProgress.stream + .where((element) => element.task.taskId == track.id), + ); + final failedTaskStream = useStream( + downloadManager.failedDownloads.stream + .where((element) => element.taskId == track.id), + ); + final taskItSelf = useFuture( + FileDownloader().database.recordForId(track.id!), + ); + + final hasFailed = failedTaskStream.hasData || + taskItSelf.data?.status == TaskStatus.failed; + + return ListTile( + title: Text(track.name ?? ''), + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + height: 40, + width: 40, + path: TypeConversionUtils.image_X_UrlString( + track.album?.images, + placeholder: ImagePlaceholder.albumArt, + ), ), ), ), - ), - trailing: const SizedBox( - width: 30, - height: 30, - child: CircularProgressIndicator(), - ), - subtitle: Text( - TypeConversionUtils.artists_X_String( + horizontalTitleGap: 10, + trailing: SizedBox( + width: 30, + height: 30, + child: downloadManager.activeItem?.id == track.id + ? CircularProgressIndicator( + value: task.data?.progress ?? 0, + ) + : hasFailed + ? Icon(SpotubeIcons.error, color: Colors.red[400]) + : IconButton( + icon: const Icon(SpotubeIcons.close), + onPressed: () { + downloadManager.cancel(track); + }), + ), + subtitle: TypeConversionUtils.artists_X_ClickableArtists( track.artists ?? [], + mainAxisAlignment: WrapAlignment.start, ), - ), - ); + ); + }); }, ), ), diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index deded47a3..4026ef4d1 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -11,8 +11,8 @@ import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; +import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -32,9 +32,10 @@ class PlayerActions extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlist = ref.watch(ProxyPlaylistNotifier.provider); final isLocalTrack = playlist.activeTrack is LocalTrack; - final downloader = ref.watch(downloaderProvider); - final isInQueue = downloader.inQueue - .any((element) => element.id == playlist.activeTrack?.id); + ref.watch(downloadManagerProvider); + final downloader = ref.watch(downloadManagerProvider.notifier); + final isInQueue = downloader.activeItem != null && + downloader.activeItem!.id == playlist.activeTrack?.id; final localTracks = [] /* ref.watch(localTracksProvider).value */; final auth = ref.watch(AuthenticationNotifier.provider); @@ -121,7 +122,7 @@ class PlayerActions extends HookConsumerWidget { isDownloaded ? SpotubeIcons.done : SpotubeIcons.download, ), onPressed: playlist.activeTrack != null - ? () => downloader.addToQueue(playlist.activeTrack!) + ? () => downloader.enqueue(playlist.activeTrack!) : null, ), if (playlist.activeTrack != null && !isLocalTrack && auth != null) diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 618dc7baa..3bbb2d606 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -3,7 +3,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter/material.dart'; -import 'package:motion_toast/motion_toast.dart'; import 'package:sidebarx/sidebarx.dart'; import 'package:spotube/collections/assets.gen.dart'; @@ -14,8 +13,8 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_brightness_value.dart'; import 'package:spotube/hooks/use_sidebarx_controller.dart'; +import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -53,7 +52,7 @@ class Sidebar extends HookConsumerWidget { final breakpoints = useBreakpoints(); final downloadCount = ref.watch( - downloaderProvider.select((s) => s.currentlyRunning), + downloadManagerProvider.select((s) => s.length), ); final layoutMode = diff --git a/lib/components/root/spotube_navigation_bar.dart b/lib/components/root/spotube_navigation_bar.dart index f58d24c00..2bf4dfc69 100644 --- a/lib/components/root/spotube_navigation_bar.dart +++ b/lib/components/root/spotube_navigation_bar.dart @@ -10,7 +10,7 @@ import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_brightness_value.dart'; -import 'package:spotube/provider/downloader_provider.dart'; +import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; class SpotubeNavigationBar extends HookConsumerWidget { @@ -27,7 +27,7 @@ class SpotubeNavigationBar extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final downloadCount = ref.watch( - downloaderProvider.select((s) => s.currentlyRunning), + downloadManagerProvider.select((s) => s.length), ); final breakpoint = useBreakpoints(); final layoutMode = diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index ceeb7a99f..ae6dbdf58 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -12,8 +12,8 @@ import 'package:spotube/components/shared/track_table/track_tile.dart'; import 'package:spotube/components/library/user_local_tracks.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; +import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; -import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -43,7 +43,8 @@ class TracksTableView extends HookConsumerWidget { Widget build(context, ref) { final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playback = ref.watch(ProxyPlaylistNotifier.notifier); - final downloader = ref.watch(downloaderProvider); + ref.watch(downloadManagerProvider); + final downloader = ref.watch(downloadManagerProvider.notifier); TextStyle tableHeadStyle = const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); @@ -208,11 +209,11 @@ class TracksTableView extends HookConsumerWidget { }, ); if (confirmed != true) return; - for (final selectedTrack in selectedTracks) { - downloader.addToQueue(selectedTrack); + await downloader.enqueueAll(selectedTracks.toList()); + if (context.mounted) { + selected.value = []; + showCheck.value = false; } - selected.value = []; - showCheck.value = false; break; } case "add-to-playlist": diff --git a/lib/main.dart b/lib/main.dart index a4443da1b..a89ee19cf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,13 +17,11 @@ import 'package:metadata_god/metadata_god.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/env.dart'; -import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/models/matched_track.dart'; -import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -143,37 +141,6 @@ Future main(List rawArgs) async { enabled: !kReleaseMode, builder: (context) { return ProviderScope( - overrides: [ - downloaderProvider.overrideWith( - (ref) { - return Downloader( - ref, - queueInstance, - downloadPath: ref.watch( - userPreferencesProvider.select( - (s) => s.downloadLocation, - ), - ), - onFileExists: (track) { - final logger = getLogger(Downloader); - try { - logger.v( - "[onFileExists] download confirmation for ${track.name}", - ); - return showDialog( - context: context, - builder: (_) => - ReplaceDownloadedDialog(track: track), - ).then((s) => s ?? false); - } catch (e, stack) { - Catcher.reportCheckedError(e, stack); - return false; - } - }, - ); - }, - ) - ], child: QueryClientProvider( staleDuration: const Duration(minutes: 30), child: const Spotube(), diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 672ddb715..f520bf005 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -8,8 +8,8 @@ import 'package:spotube/components/root/bottom_player.dart'; import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/root/spotube_navigation_bar.dart'; import 'package:spotube/hooks/use_update_checker.dart'; +import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/downloader_provider.dart'; const rootPaths = { 0: "/", @@ -31,7 +31,7 @@ class RootApp extends HookConsumerWidget { final isMounted = useIsMounted(); final auth = ref.watch(AuthenticationNotifier.provider); - final downloader = ref.watch(downloaderProvider); + final downloader = ref.watch(downloadManagerProvider.notifier); useEffect(() { downloader.onFileExists = (track) async { if (!isMounted()) return false; diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 78e680b69..de0454c93 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -6,7 +6,6 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:piped_client/piped_client.dart'; import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/language_codes.dart'; @@ -20,8 +19,8 @@ import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/l10n/l10n.dart'; +import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/piped_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -34,7 +33,7 @@ class SettingsPage extends HookConsumerWidget { final UserPreferences preferences = ref.watch(userPreferencesProvider); final auth = ref.watch(AuthenticationNotifier.provider); final isDownloading = - ref.watch(downloaderProvider.select((s) => s.currentlyRunning > 0)); + ref.watch(downloadManagerProvider.select((s) => s.isNotEmpty)); final theme = Theme.of(context); final pickColorScheme = useCallback(() { diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart new file mode 100644 index 000000000..90e860645 --- /dev/null +++ b/lib/provider/download_manager_provider.dart @@ -0,0 +1,179 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:collection/collection.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:metadata_god/metadata_god.dart'; +import 'package:path/path.dart'; +import 'package:piped_client/piped_client.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/routes.dart'; +import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; +import 'package:spotube/models/spotube_track.dart'; +import 'package:spotube/provider/piped_provider.dart'; +import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +class DownloadManagerProvider extends StateNotifier> { + final Ref ref; + + final StreamController activeDownloadProgress; + final StreamController failedDownloads; + Track? _activeItem; + + FutureOr Function(Track)? onFileExists; + + DownloadManagerProvider(this.ref) + : activeDownloadProgress = StreamController.broadcast(), + failedDownloads = StreamController.broadcast(), + super([]) { + FileDownloader().registerCallbacks( + group: FileDownloader.defaultGroup, + taskNotificationTapCallback: (task, notificationType) { + router.go("/library"); + }, + taskStatusCallback: (update) async { + if (update.status == TaskStatus.running) { + _activeItem = + state.firstWhereOrNull((track) => track.id == update.task.taskId); + state = state.toList(); + } + + if (update.status == TaskStatus.failed || + update.status == TaskStatus.notFound) { + failedDownloads.add(update.task); + } + + if (update.status == TaskStatus.complete) { + final track = + state.firstWhere((element) => element.id == update.task.taskId); + state = state + .where((element) => element.id != update.task.taskId) + .toList(); + + final imageUri = TypeConversionUtils.image_X_UrlString( + track.album?.images ?? [], + placeholder: ImagePlaceholder.online, + ); + final response = await get(Uri.parse(imageUri)); + + final tempFile = File(await update.task.filePath()); + + final file = tempFile.copySync(_getPathForTrack(track)); + + await tempFile.delete(); + + await MetadataGod.writeMetadata( + file: file.path, + 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: response.headers['content-type'] != null + ? Picture( + data: response.bodyBytes, + mimeType: response.headers['content-type']!, + ) + : null, + ), + ); + } + }, + taskProgressCallback: (update) { + activeDownloadProgress.add(update); + }, + ); + FileDownloader().trackTasks(markDownloadedComplete: true); + } + + UserPreferences get preferences => ref.read(userPreferencesProvider); + PipedClient get pipedClient => ref.read(pipedClientProvider); + + int get totalDownloads => state.length; + List get items => state; + Track? get activeItem => _activeItem; + + String _getPathForTrack(Track track) => join( + preferences.downloadLocation, + "${track.name} - ${track.artists?.map((a) => a.name).join(", ")}.m4a", + ); + + Future _ensureSpotubeTrack(Track track) async { + if (state.any((element) => element.id == track.id)) { + final task = await FileDownloader().taskForId(track.id!); + if (task != null) { + return task; + } + // this makes sure we already have the fetched track + track = state.firstWhere((element) => element.id == track.id); + state.removeWhere((element) => element.id == track.id); + } + final spotubeTrack = track is SpotubeTrack + ? track + : await SpotubeTrack.fetchFromTrack( + track, + preferences, + pipedClient, + ); + state = [...state, spotubeTrack]; + final task = DownloadTask( + url: spotubeTrack.ytUri, + baseDirectory: BaseDirectory.applicationSupport, + taskId: spotubeTrack.id!, + updates: Updates.statusAndProgress, + ); + return task; + } + + Future enqueue(Track track) async { + final replaceFileGlobal = ref.read(replaceDownloadedFileState); + final file = File(_getPathForTrack(track)); + if (file.existsSync() && + (replaceFileGlobal ?? await onFileExists?.call(track)) != true) { + return null; + } + + final task = await _ensureSpotubeTrack(track); + + await FileDownloader().enqueue(task); + return task; + } + + Future> enqueueAll(List tracks) async { + final tasks = await Future.wait(tracks.mapIndexed((i, e) { + if (i != 0) { + /// One second delay between each download to avoid + /// clogging the Piped server with too many requests + return Future.delayed(const Duration(seconds: 1), () => enqueue(e)); + } + return enqueue(e); + })); + return tasks.whereType().toList(); + } + + Future cancel(Track track) async { + await FileDownloader().cancelTaskWithId(track.id!); + state = state.where((element) => element.id != track.id).toList(); + } + + Future cancelAll() async { + (await FileDownloader().reset()); + state = []; + } +} + +final downloadManagerProvider = + StateNotifierProvider>( + DownloadManagerProvider.new, +); diff --git a/lib/provider/downloader_provider.dart b/lib/provider/downloader_provider.dart deleted file mode 100644 index 51ad9f912..000000000 --- a/lib/provider/downloader_provider.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:http/http.dart'; -import 'package:metadata_god/metadata_god.dart'; -import 'package:queue/queue.dart'; -import 'package:path/path.dart' as path; -import 'package:spotify/spotify.dart' hide Image, Queue; -import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; -import 'package:spotube/models/logger.dart'; -import 'package:spotube/models/spotube_track.dart'; -import 'package:spotube/provider/piped_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; -import 'package:spotube/utils/type_conversion_utils.dart'; - -Queue queueInstance = Queue(delay: const Duration(seconds: 5)); -Queue grabberQueue = Queue(delay: const Duration(seconds: 5)); - -class Downloader with ChangeNotifier { - Ref ref; - Queue _queue; - - String downloadPath; - FutureOr Function(SpotubeTrack track)? onFileExists; - Downloader( - this.ref, - this._queue, { - required this.downloadPath, - this.onFileExists, - }); - - int currentlyRunning = 0; - // ignore: prefer_collection_literals - Set inQueue = Set(); - - final logger = getLogger(Downloader); - - // Playback get _playback => ref.read(playbackProvider); - - void addToQueue(Track baseTrack) async { - if (kIsWeb) return; - if (inQueue.any((t) => t.id == baseTrack.id!)) return; - inQueue.add(baseTrack); - currentlyRunning++; - notifyListeners(); - - // Using android Audio Focus to keep the app run in background - grabberQueue.add(() async { - final track = await SpotubeTrack.fetchFromTrack( - baseTrack, - ref.read(userPreferencesProvider), - ref.read(pipedClientProvider), - ); - - _queue.add(() async { - final cleanTitle = track.ytTrack.title.replaceAll( - RegExp(r'[/\\?%*:|"<>]'), - "", - ); - final filename = '$cleanTitle.m4a'; - final file = File(path.join(downloadPath, filename)); - try { - final replaceFileGlobal = ref.read(replaceDownloadedFileState); - logger.v("[addToQueue] Download starting for ${file.path}"); - if (file.existsSync() && - (replaceFileGlobal ?? await onFileExists?.call(track)) != true) { - return; - } - file.createSync(recursive: true); - logger.v( - "[addToQueue] Getting download information for ${file.path}", - ); - final audioStream = await get( - Uri.parse( - SpotubeTrack.getStreamInfo( - track.ytTrack, - ref.read(userPreferencesProvider).audioQuality, - ).url, - ), - ); - logger.v( - "[addToQueue] ${file.path} download started", - ); - - IOSink outputFileStream = file.openWrite(); - outputFileStream.write(audioStream.bodyBytes); - await outputFileStream.flush(); - logger.v( - "[addToQueue] Download of ${file.path} is done successfully", - ); - - logger.v( - "[addToQueue] Writing metadata to ${file.path}", - ); - final imageUri = TypeConversionUtils.image_X_UrlString( - track.album?.images ?? [], - placeholder: ImagePlaceholder.online, - ); - final response = await get(Uri.parse(imageUri)); - - await MetadataGod.writeMetadata( - file: file.path, - 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: response.headers['content-type'] != null - ? Picture( - data: response.bodyBytes, - mimeType: response.headers['content-type']!, - ) - : null, - ), - ); - logger.v( - "[addToQueue] Writing metadata to ${file.path} is successful", - ); - } catch (e) { - logger.v("[addToQueue] Failed download of ${file.path}", e); - rethrow; - } finally { - currentlyRunning--; - inQueue.removeWhere((t) => t.id == track.id); - notifyListeners(); - } - }); - }); - } - - cancelAll() { - grabberQueue.cancel(); - grabberQueue = Queue(); - inQueue.clear(); - currentlyRunning = 0; - _queue.cancel(); - queueInstance = Queue(); - _queue = queueInstance; - notifyListeners(); - } -} - -final downloaderProvider = ChangeNotifierProvider( - (ref) { - return Downloader( - ref, - queueInstance, - downloadPath: ref.watch( - userPreferencesProvider.select( - (s) => s.downloadLocation, - ), - ), - ); - }, -); diff --git a/pubspec.lock b/pubspec.lock index a913095db..3f680e731 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -153,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + background_downloader: + dependency: "direct main" + description: + name: background_downloader + sha256: "58318c7141ac30c559004a58ab2fdbdb5433e37227a926196b88525085af3d8e" + url: "https://pub.dev" + source: hosted + version: "7.3.1" badges: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 07db73fde..03d10b5fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -103,6 +103,7 @@ dependencies: media_kit_native_event_loop: ^1.0.4 dbus: ^0.7.8 motion_toast: ^2.6.8 + background_downloader: ^7.3.1 dev_dependencies: build_runner: ^2.3.2