From 8f8ca660cc80bb22bb668b5f8ee4fc70a801102f Mon Sep 17 00:00:00 2001 From: Khaled Date: Mon, 28 Oct 2024 19:35:45 +0600 Subject: [PATCH] feat: Fixed / Improved downloader handling --- lib/app/extensions.dart | 10 -- .../downloads/active_downloads_screen.dart | 11 ++- .../downloads/download_entry_builder.dart | 7 +- lib/app/player/expanded_player_sheet.dart | 2 +- lib/app/playlist/media_entry_builder.dart | 2 +- lib/app/playlist/media_menu_sheet.dart | 7 +- lib/app/playlist/playlist_menu_sheet.dart | 7 +- lib/extensions.dart | 24 +++++ lib/main.dart | 7 ++ lib/provider/media_provider.dart | 95 +++++++++++++------ 10 files changed, 121 insertions(+), 51 deletions(-) delete mode 100644 lib/app/extensions.dart create mode 100644 lib/extensions.dart diff --git a/lib/app/extensions.dart b/lib/app/extensions.dart deleted file mode 100644 index b7c286d..0000000 --- a/lib/app/extensions.dart +++ /dev/null @@ -1,10 +0,0 @@ -extension DurationExtensions on Duration? { - String formatHHMM() { - if (this == null) return "??:??"; - String twoDigits(int n) => n.toString().padLeft(2, '0'); - final String twoDigitMinutes = twoDigits(this!.inMinutes.remainder(60)); - final String twoDigitSeconds = twoDigits(this!.inSeconds.remainder(60)); - final hour = twoDigits(this!.inHours); - return " ${hour == '00' ? '' : '$hour:'}$twoDigitMinutes:$twoDigitSeconds "; - } -} diff --git a/lib/app/more/downloads/active_downloads_screen.dart b/lib/app/more/downloads/active_downloads_screen.dart index c65d1c0..3401758 100644 --- a/lib/app/more/downloads/active_downloads_screen.dart +++ b/lib/app/more/downloads/active_downloads_screen.dart @@ -2,6 +2,7 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; import 'package:tube_sync/app/more/downloads/download_entry_builder.dart'; import 'package:tube_sync/main.dart'; +import 'package:tube_sync/provider/media_provider.dart'; class ActiveDownloadsScreen extends StatefulWidget { const ActiveDownloadsScreen({super.key}); @@ -67,6 +68,8 @@ class _ActiveDownloadsScreenState extends State { } Future cancelAll() async { + FileDownloader().taskQueues.forEach(FileDownloader().removeTaskQueue); + MediaProvider().abortQueueing(); Iterable records = await FileDownloader().database.allRecords(); await FileDownloader().cancelTasksWithIds( @@ -84,6 +87,7 @@ class _ActiveDownloadsScreenState extends State { future: FileDownloader().database.allRecords(), initialData: [], builder: (context, snapshot) { + if (snapshot.hasError) return const SizedBox(); if (snapshot.requireData.isEmpty) return const SizedBox(); return FloatingActionButton.extended( icon: Icon(Icons.clear_all_rounded), @@ -96,10 +100,11 @@ class _ActiveDownloadsScreenState extends State { future: FileDownloader().database.allRecords(), initialData: [], builder: (context, snapshot) { + if (snapshot.hasError) { + return Center(child: Text(snapshot.error.toString())); + } if (snapshot.requireData.isEmpty) { - return Center( - child: Text("No Active Downloads!"), - ); + return Center(child: Text("No Active Downloads!")); } return ListView.builder( diff --git a/lib/app/more/downloads/download_entry_builder.dart b/lib/app/more/downloads/download_entry_builder.dart index a67ed69..74620fa 100644 --- a/lib/app/more/downloads/download_entry_builder.dart +++ b/lib/app/more/downloads/download_entry_builder.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; +import 'package:tube_sync/extensions.dart'; class DownloadEntryBuilder extends StatelessWidget { const DownloadEntryBuilder({ @@ -26,7 +27,11 @@ class DownloadEntryBuilder extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - children: [Text(getFileSizeString(bytes: entry.expectedFileSize))], + children: [ + Text(entry.status.name.normalizeCamelCase().toCapitalCase()), + Text(" \u2022 "), + Text(getFileSizeString(bytes: entry.expectedFileSize)) + ], ), LinearProgressIndicator(value: entry.progress) ], diff --git a/lib/app/player/expanded_player_sheet.dart b/lib/app/player/expanded_player_sheet.dart index 72aaab7..1c834f1 100644 --- a/lib/app/player/expanded_player_sheet.dart +++ b/lib/app/player/expanded_player_sheet.dart @@ -2,7 +2,7 @@ import 'package:audioplayers/audioplayers.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:tube_sync/app/extensions.dart'; +import 'package:tube_sync/extensions.dart'; import 'package:tube_sync/model/media.dart'; import 'package:tube_sync/model/playlist.dart'; import 'package:tube_sync/provider/player_provider.dart'; diff --git a/lib/app/playlist/media_entry_builder.dart b/lib/app/playlist/media_entry_builder.dart index 91e4c45..5b8a7ed 100644 --- a/lib/app/playlist/media_entry_builder.dart +++ b/lib/app/playlist/media_entry_builder.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:tube_sync/app/extensions.dart'; +import 'package:tube_sync/extensions.dart'; import 'package:tube_sync/app/playlist/media_menu_sheet.dart'; import 'package:tube_sync/model/media.dart'; import 'package:tube_sync/provider/playlist_provider.dart'; diff --git a/lib/app/playlist/media_menu_sheet.dart b/lib/app/playlist/media_menu_sheet.dart index 9ae73ea..677c788 100644 --- a/lib/app/playlist/media_menu_sheet.dart +++ b/lib/app/playlist/media_menu_sheet.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:tube_sync/app/more/downloads/active_downloads_screen.dart'; import 'package:tube_sync/model/media.dart'; -import 'package:tube_sync/provider/playlist_provider.dart'; +import 'package:tube_sync/provider/media_provider.dart'; class MediaMenuSheet extends StatelessWidget { final Media media; @@ -32,7 +31,7 @@ class MediaMenuSheet extends StatelessWidget { if (media.downloaded != true) ListTile( onTap: () { - // context.read().downloadMedia(media); + MediaProvider().download(media); Navigator.pop(context); ActiveDownloadsScreen.showEnqueuedSnackbar(context); }, @@ -42,7 +41,7 @@ class MediaMenuSheet extends StatelessWidget { if (media.downloaded == true) ListTile( onTap: () { - // context.read().deleteMedia(media); + MediaProvider().delete(media); Navigator.pop(context); }, leading: Icon(Icons.delete_rounded), diff --git a/lib/app/playlist/playlist_menu_sheet.dart b/lib/app/playlist/playlist_menu_sheet.dart index b57a61a..a737896 100644 --- a/lib/app/playlist/playlist_menu_sheet.dart +++ b/lib/app/playlist/playlist_menu_sheet.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:tube_sync/app/more/downloads/active_downloads_screen.dart'; +import 'package:tube_sync/provider/media_provider.dart'; +import 'package:tube_sync/provider/playlist_provider.dart'; class PlaylistMenuSheet extends StatelessWidget { const PlaylistMenuSheet({super.key}); @@ -26,7 +29,9 @@ class PlaylistMenuSheet extends StatelessWidget { ), ListTile( onTap: () { - // context.read().downloadAll(); + MediaProvider().downloadAll( + context.read().medias, + ); ActiveDownloadsScreen.showEnqueuedSnackbar(context); Navigator.pop(context); }, diff --git a/lib/extensions.dart b/lib/extensions.dart new file mode 100644 index 0000000..2171ce6 --- /dev/null +++ b/lib/extensions.dart @@ -0,0 +1,24 @@ +extension DurationExtensions on Duration? { + String formatHHMM() { + if (this == null) return "??:??"; + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final String twoDigitMinutes = twoDigits(this!.inMinutes.remainder(60)); + final String twoDigitSeconds = twoDigits(this!.inSeconds.remainder(60)); + final hour = twoDigits(this!.inHours); + return " ${hour == '00' ? '' : '$hour:'}$twoDigitMinutes:$twoDigitSeconds "; + } +} + +extension StringExtensions on String { + /// Converts SUSSY BAKA, SuSSy bAKA sussY BaKA etc to Sussy Baka + String toCapitalCase() => splitMapJoin(" ", onNonMatch: (n) { + if (n.length <= 2) return n.toUpperCase(); + return "${n[0].toUpperCase()}${n.substring(1).toLowerCase()}"; + }); + + /// sussyBaka -> sussy Baka + String normalizeCamelCase() => replaceAllMapped( + RegExp(r"([A-Z]){1,3}"), + (match) => " ${match[0]}", + ); +} diff --git a/lib/main.dart b/lib/main.dart index 2151573..79f93e2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:tube_sync/app/more/downloads/active_downloads_screen.dart'; import 'package:tube_sync/model/media.dart'; import 'package:tube_sync/model/playlist.dart'; import 'package:tube_sync/model/preferences.dart'; +import 'package:tube_sync/provider/media_provider.dart'; final rootNavigator = GlobalKey(); @@ -26,6 +27,11 @@ void main() async { Preference.materialYou, ); + // Limit concurrent downloads + await FileDownloader().configure( + globalConfig: (Config.holdingQueue, (3, 3, 3)), + ); + // Background downloader notifications FileDownloader().configureNotification( running: TaskNotification('Downloading', '{displayName}'), @@ -41,6 +47,7 @@ void main() async { // Using the database to track Tasks FileDownloader().trackTasks(); + await MediaProvider().init(); runApp( ValueListenableBuilder( diff --git a/lib/provider/media_provider.dart b/lib/provider/media_provider.dart index 6cbed6d..f082c8b 100644 --- a/lib/provider/media_provider.dart +++ b/lib/provider/media_provider.dart @@ -8,54 +8,89 @@ import 'package:tube_sync/model/media.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart' as yt; class MediaProvider { + MediaProvider._(); + + static final MediaProvider _instance = MediaProvider._(); + + factory MediaProvider() => _instance; + + Future init() async { + final dir = await getApplicationCacheDirectory(); + mediaFileDir = dir.path + Platform.pathSeparator; + } + + late final String mediaFileDir; final _ytClient = yt.YoutubeExplode().videos.streamsClient; + bool _abortQueueing = false; + + File mediaFile(Media media) => File(mediaFileDir + media.id); + Future getMedia(Media media) async { // Try from offline - final downloaded = File(await mediaFilePath(media)); + final downloaded = mediaFile(media); if (downloaded.existsSync()) return DeviceFileSource(downloaded.path); if (!await hasInternet) return null; final videoManifest = await _ytClient.getManifest(media.id); final streamUri = videoManifest.audioOnly.withHighestBitrate().url; - final source = UrlSource(streamUri.toString()); - - return source; + return UrlSource(streamUri.toString()); } - static Future mediaFilePath(Media media) async { - // return "/home/khaled/.cache/tubesync.app/${media.id}"; - final dir = await getApplicationCacheDirectory(); - return dir.path + Platform.pathSeparator + media.id; - } + Future download(Media media) async { + try { + final manifest = await _ytClient.getManifest(media.id); + final url = manifest.audioOnly.withHighestBitrate().url.toString(); - static Future download(Media media) async { - final ytClient = yt.YoutubeExplode().videos.streamsClient; - final manifest = await ytClient.getManifest(media.id); - final url = manifest.audioOnly.withHighestBitrate().url.toString(); - final directory = await mediaFilePath(media); - - final task = DownloadTask( - url: url, - displayName: media.title, - directory: directory.replaceFirst(media.id, ''), - filename: media.id, - baseDirectory: BaseDirectory.root, - updates: Updates.statusAndProgress, - ); - await FileDownloader().enqueue(task); - await FileDownloader().database.recordForId(task.taskId); + final task = DownloadTask( + url: url, + displayName: media.title, + directory: mediaFileDir, + filename: media.id, + baseDirectory: BaseDirectory.root, + updates: Updates.statusAndProgress, + ); + + await FileDownloader().enqueue(task); + } catch (_) { + //TODO Error + } } - static Future isDownloaded(Media media) async { - return File(await mediaFilePath(media)).exists(); + Future downloadAll(List medias) async { + _abortQueueing = false; + for (final media in medias) { + try { + if (mediaFile(media).existsSync()) continue; + + final manifest = await _ytClient.getManifest(media.id); + final url = manifest.audioOnly.withHighestBitrate().url.toString(); + + if (_abortQueueing) break; + + FileDownloader().enqueue(DownloadTask( + url: url, + displayName: media.title, + directory: mediaFileDir, + filename: media.id, + baseDirectory: BaseDirectory.root, + updates: Updates.statusAndProgress, + )); + } catch (_) { + // TODO Error + } + } } - static Future delete(Media media) async { - final file = File(await mediaFilePath(media)); + void abortQueueing() => _abortQueueing = true; + + Future isDownloaded(Media media) async => mediaFile(media).exists(); + + Future delete(Media media) async { + final file = mediaFile(media); if (file.existsSync()) await file.delete(); } - Future get hasInternet => InternetConnection().hasInternetAccess; + static Future get hasInternet => InternetConnection().hasInternetAccess; }