mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-15 00:25:17 +00:00
feat: Better download manager with download progress
This commit is contained in:
parent
f4b0d134ca
commit
6752adc939
@ -1,11 +1,14 @@
|
|||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:spotify/spotify.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/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.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';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
class UserDownloads extends HookConsumerWidget {
|
class UserDownloads extends HookConsumerWidget {
|
||||||
@ -13,7 +16,8 @@ class UserDownloads extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final downloader = ref.watch(downloaderProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
|
final downloadManager = ref.watch(downloadManagerProvider.notifier);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -26,7 +30,7 @@ class UserDownloads extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: AutoSizeText(
|
child: AutoSizeText(
|
||||||
context.l10n
|
context.l10n
|
||||||
.currently_downloading(downloader.currentlyRunning),
|
.currently_downloading(downloadManager.totalDownloads),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
),
|
),
|
||||||
@ -37,9 +41,9 @@ class UserDownloads extends HookConsumerWidget {
|
|||||||
backgroundColor: Colors.red[50],
|
backgroundColor: Colors.red[50],
|
||||||
foregroundColor: Colors.red[400],
|
foregroundColor: Colors.red[400],
|
||||||
),
|
),
|
||||||
onPressed: downloader.currentlyRunning > 0
|
onPressed: downloadManager.totalDownloads == 0
|
||||||
? downloader.cancelAll
|
? null
|
||||||
: null,
|
: downloadManager.cancelAll,
|
||||||
child: Text(context.l10n.cancel_all),
|
child: Text(context.l10n.cancel_all),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -48,9 +52,25 @@ class UserDownloads extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: downloader.inQueue.length,
|
itemCount: downloadManager.totalDownloads,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final track = downloader.inQueue.elementAt(index);
|
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(
|
return ListTile(
|
||||||
title: Text(track.name ?? ''),
|
title: Text(track.name ?? ''),
|
||||||
leading: Padding(
|
leading: Padding(
|
||||||
@ -67,17 +87,28 @@ class UserDownloads extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: const SizedBox(
|
horizontalTitleGap: 10,
|
||||||
|
trailing: SizedBox(
|
||||||
width: 30,
|
width: 30,
|
||||||
height: 30,
|
height: 30,
|
||||||
child: CircularProgressIndicator(),
|
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: Text(
|
subtitle: TypeConversionUtils.artists_X_ClickableArtists(
|
||||||
TypeConversionUtils.artists_X_String(
|
|
||||||
track.artists ?? <Artist>[],
|
track.artists ?? <Artist>[],
|
||||||
),
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -11,8 +11,8 @@ import 'package:spotube/components/shared/heart_button.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/models/logger.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/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/downloader_provider.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
@ -32,9 +32,10 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final isLocalTrack = playlist.activeTrack is LocalTrack;
|
final isLocalTrack = playlist.activeTrack is LocalTrack;
|
||||||
final downloader = ref.watch(downloaderProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final isInQueue = downloader.inQueue
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
.any((element) => element.id == playlist.activeTrack?.id);
|
final isInQueue = downloader.activeItem != null &&
|
||||||
|
downloader.activeItem!.id == playlist.activeTrack?.id;
|
||||||
final localTracks = [] /* ref.watch(localTracksProvider).value */;
|
final localTracks = [] /* ref.watch(localTracksProvider).value */;
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||||
|
|
||||||
@ -121,7 +122,7 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
||||||
),
|
),
|
||||||
onPressed: playlist.activeTrack != null
|
onPressed: playlist.activeTrack != null
|
||||||
? () => downloader.addToQueue(playlist.activeTrack!)
|
? () => downloader.enqueue(playlist.activeTrack!)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
if (playlist.activeTrack != null && !isLocalTrack && auth != null)
|
if (playlist.activeTrack != null && !isLocalTrack && auth != null)
|
||||||
|
@ -3,7 +3,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:motion_toast/motion_toast.dart';
|
|
||||||
import 'package:sidebarx/sidebarx.dart';
|
import 'package:sidebarx/sidebarx.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/assets.gen.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_breakpoints.dart';
|
||||||
import 'package:spotube/hooks/use_brightness_value.dart';
|
import 'package:spotube/hooks/use_brightness_value.dart';
|
||||||
import 'package:spotube/hooks/use_sidebarx_controller.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/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/downloader_provider.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
@ -53,7 +52,7 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
final breakpoints = useBreakpoints();
|
final breakpoints = useBreakpoints();
|
||||||
|
|
||||||
final downloadCount = ref.watch(
|
final downloadCount = ref.watch(
|
||||||
downloaderProvider.select((s) => s.currentlyRunning),
|
downloadManagerProvider.select((s) => s.length),
|
||||||
);
|
);
|
||||||
|
|
||||||
final layoutMode =
|
final layoutMode =
|
||||||
|
@ -10,7 +10,7 @@ import 'package:spotube/components/root/sidebar.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
import 'package:spotube/hooks/use_brightness_value.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';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
|
|
||||||
class SpotubeNavigationBar extends HookConsumerWidget {
|
class SpotubeNavigationBar extends HookConsumerWidget {
|
||||||
@ -27,7 +27,7 @@ class SpotubeNavigationBar extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final downloadCount = ref.watch(
|
final downloadCount = ref.watch(
|
||||||
downloaderProvider.select((s) => s.currentlyRunning),
|
downloadManagerProvider.select((s) => s.length),
|
||||||
);
|
);
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
final layoutMode =
|
final layoutMode =
|
||||||
|
@ -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/components/library/user_local_tracks.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoints.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/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/downloader_provider.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -43,7 +43,8 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
Widget build(context, ref) {
|
Widget build(context, ref) {
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
final downloader = ref.watch(downloaderProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
TextStyle tableHeadStyle =
|
TextStyle tableHeadStyle =
|
||||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
||||||
|
|
||||||
@ -208,11 +209,11 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
for (final selectedTrack in selectedTracks) {
|
await downloader.enqueueAll(selectedTracks.toList());
|
||||||
downloader.addToQueue(selectedTrack);
|
if (context.mounted) {
|
||||||
}
|
|
||||||
selected.value = [];
|
selected.value = [];
|
||||||
showCheck.value = false;
|
showCheck.value = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "add-to-playlist":
|
case "add-to-playlist":
|
||||||
|
@ -17,13 +17,11 @@ import 'package:metadata_god/metadata_god.dart';
|
|||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:spotube/collections/env.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/routes.dart';
|
||||||
import 'package:spotube/collections/intents.dart';
|
import 'package:spotube/collections/intents.dart';
|
||||||
import 'package:spotube/l10n/l10n.dart';
|
import 'package:spotube/l10n/l10n.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/models/matched_track.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/palette_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -143,37 +141,6 @@ Future<void> main(List<String> rawArgs) async {
|
|||||||
enabled: !kReleaseMode,
|
enabled: !kReleaseMode,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return ProviderScope(
|
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<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (_) =>
|
|
||||||
ReplaceDownloadedDialog(track: track),
|
|
||||||
).then((s) => s ?? false);
|
|
||||||
} catch (e, stack) {
|
|
||||||
Catcher.reportCheckedError(e, stack);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
child: QueryClientProvider(
|
child: QueryClientProvider(
|
||||||
staleDuration: const Duration(minutes: 30),
|
staleDuration: const Duration(minutes: 30),
|
||||||
child: const Spotube(),
|
child: const Spotube(),
|
||||||
|
@ -8,8 +8,8 @@ import 'package:spotube/components/root/bottom_player.dart';
|
|||||||
import 'package:spotube/components/root/sidebar.dart';
|
import 'package:spotube/components/root/sidebar.dart';
|
||||||
import 'package:spotube/components/root/spotube_navigation_bar.dart';
|
import 'package:spotube/components/root/spotube_navigation_bar.dart';
|
||||||
import 'package:spotube/hooks/use_update_checker.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/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/downloader_provider.dart';
|
|
||||||
|
|
||||||
const rootPaths = {
|
const rootPaths = {
|
||||||
0: "/",
|
0: "/",
|
||||||
@ -31,7 +31,7 @@ class RootApp extends HookConsumerWidget {
|
|||||||
final isMounted = useIsMounted();
|
final isMounted = useIsMounted();
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||||
|
|
||||||
final downloader = ref.watch(downloaderProvider);
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
downloader.onFileExists = (track) async {
|
downloader.onFileExists = (track) async {
|
||||||
if (!isMounted()) return false;
|
if (!isMounted()) return false;
|
||||||
|
@ -6,7 +6,6 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/env.dart';
|
||||||
import 'package:spotube/collections/language_codes.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/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
import 'package:spotube/l10n/l10n.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/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/downloader_provider.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/piped_provider.dart';
|
import 'package:spotube/provider/piped_provider.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@ -34,7 +33,7 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
final UserPreferences preferences = ref.watch(userPreferencesProvider);
|
final UserPreferences preferences = ref.watch(userPreferencesProvider);
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||||
final isDownloading =
|
final isDownloading =
|
||||||
ref.watch(downloaderProvider.select((s) => s.currentlyRunning > 0));
|
ref.watch(downloadManagerProvider.select((s) => s.isNotEmpty));
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
final pickColorScheme = useCallback(() {
|
final pickColorScheme = useCallback(() {
|
||||||
|
179
lib/provider/download_manager_provider.dart
Normal file
179
lib/provider/download_manager_provider.dart
Normal file
@ -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<List<SpotubeTrack>> {
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
|
final StreamController<TaskProgressUpdate> activeDownloadProgress;
|
||||||
|
final StreamController<Task> failedDownloads;
|
||||||
|
Track? _activeItem;
|
||||||
|
|
||||||
|
FutureOr<bool> 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<Track> 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<Task> _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<Task?> 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<List<Task>> enqueueAll(List<Track> 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<Task>().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancel(Track track) async {
|
||||||
|
await FileDownloader().cancelTaskWithId(track.id!);
|
||||||
|
state = state.where((element) => element.id != track.id).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelAll() async {
|
||||||
|
(await FileDownloader().reset());
|
||||||
|
state = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final downloadManagerProvider =
|
||||||
|
StateNotifierProvider<DownloadManagerProvider, List<Track>>(
|
||||||
|
DownloadManagerProvider.new,
|
||||||
|
);
|
@ -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<bool> Function(SpotubeTrack track)? onFileExists;
|
|
||||||
Downloader(
|
|
||||||
this.ref,
|
|
||||||
this._queue, {
|
|
||||||
required this.downloadPath,
|
|
||||||
this.onFileExists,
|
|
||||||
});
|
|
||||||
|
|
||||||
int currentlyRunning = 0;
|
|
||||||
// ignore: prefer_collection_literals
|
|
||||||
Set<Track> 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
@ -153,6 +153,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
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:
|
badges:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -103,6 +103,7 @@ dependencies:
|
|||||||
media_kit_native_event_loop: ^1.0.4
|
media_kit_native_event_loop: ^1.0.4
|
||||||
dbus: ^0.7.8
|
dbus: ^0.7.8
|
||||||
motion_toast: ^2.6.8
|
motion_toast: ^2.6.8
|
||||||
|
background_downloader: ^7.3.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.3.2
|
build_runner: ^2.3.2
|
||||||
|
Loading…
Reference in New Issue
Block a user