Skip to content

Commit

Permalink
feat: blazingly™ fast download manager (#619)
Browse files Browse the repository at this point in the history
* feat: concurrent download service & download prorvider

* feat: implement chunked downloader

* fix: no audio-tags in Linux and duration not showing up for local tracks

* feat: show matching tracks in queue as well

* feat: always uses piped api for download to avoid IP block

* fix: invalid downloadCount
  • Loading branch information
KRTirtho authored Aug 7, 2023
1 parent ae5edd1 commit 38dc4be
Show file tree
Hide file tree
Showing 31 changed files with 1,143 additions and 302 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,12 @@ Do the following:
- Install Development dependencies in linux
- Debian (>=12/Bookworm)/Ubuntu
```bash
$ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev
$ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev network-manager
```
- Use `libjsoncpp1` instead of `libjsoncpp25` (for Ubuntu < 22.04)
- Arch/Manjaro
```bash
yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify
yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify networkmanager
```
- Fedora
```bash
Expand Down
75 changes: 11 additions & 64 deletions lib/components/library/user_downloads.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:background_downloader/background_downloader.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/components/library/user_downloads/download_item.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/utils/type_conversion_utils.dart';

class UserDownloads extends HookConsumerWidget {
const UserDownloads({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, ref) {
ref.watch(downloadManagerProvider);
final downloadManager = ref.watch(downloadManagerProvider.notifier);
final downloadManager = ref.watch(downloadManagerProvider);

final history = [
...downloadManager.$history,
...downloadManager.$backHistory,
];

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
Expand All @@ -31,7 +29,7 @@ class UserDownloads extends HookConsumerWidget {
Expanded(
child: AutoSizeText(
context.l10n
.currently_downloading(downloadManager.totalDownloads),
.currently_downloading(downloadManager.$downloadCount),
maxLines: 1,
style: Theme.of(context).textTheme.headlineMedium,
),
Expand All @@ -42,7 +40,7 @@ class UserDownloads extends HookConsumerWidget {
backgroundColor: Colors.red[50],
foregroundColor: Colors.red[400],
),
onPressed: downloadManager.totalDownloads == 0
onPressed: downloadManager.$downloadCount == 0
? null
: downloadManager.cancelAll,
child: Text(context.l10n.cancel_all),
Expand All @@ -53,60 +51,9 @@ class UserDownloads extends HookConsumerWidget {
Expanded(
child: SafeArea(
child: ListView.builder(
itemCount: downloadManager.totalDownloads,
itemCount: history.length,
itemBuilder: (context, 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(
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,
),
),
),
),
horizontalTitleGap: 10,
trailing: downloadManager.activeItem?.id == track.id &&
!hasFailed
? 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 ?? <Artist>[],
mainAxisAlignment: WrapAlignment.start,
),
);
});
return DownloadItem(track: history[index]);
},
),
),
Expand Down
145 changes: 145 additions & 0 deletions lib/components/library/user_downloads/download_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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/models/spotube_track.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/services/download_manager/download_status.dart';
import 'package:spotube/utils/type_conversion_utils.dart';

class DownloadItem extends HookConsumerWidget {
final Track track;
const DownloadItem({
Key? key,
required this.track,
}) : super(key: key);

@override
Widget build(BuildContext context, ref) {
final downloadManager = ref.watch(downloadManagerProvider);

final taskStatus = useState<DownloadStatus?>(null);

useEffect(() {
if (track is! SpotubeTrack) return null;
final notifier = downloadManager.getStatusNotifier(track as SpotubeTrack);

taskStatus.value = notifier?.value;
listener() {
taskStatus.value = notifier?.value;
}

downloadManager
.getStatusNotifier(track as SpotubeTrack)
?.addListener(listener);

return () {
downloadManager
.getStatusNotifier(track as SpotubeTrack)
?.removeListener(listener);
};
}, [track]);

return ListTile(
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,
),
),
),
),
title: Text(track.name ?? ''),
subtitle: TypeConversionUtils.artists_X_ClickableArtists(
track.artists ?? <Artist>[],
mainAxisAlignment: WrapAlignment.start,
),
trailing: taskStatus.value == null || track is! SpotubeTrack
? Text(
context.l10n.querying_info,
style: Theme.of(context).textTheme.labelMedium,
)
: switch (taskStatus.value!) {
DownloadStatus.downloading => HookBuilder(builder: (context) {
final taskProgress = useListenable(useMemoized(
() => downloadManager
.getProgressNotifier(track as SpotubeTrack),
[track],
));
return SizedBox(
width: 140,
child: Row(
children: [
CircularProgressIndicator(
value: taskProgress?.value ?? 0,
),
const SizedBox(width: 10),
IconButton(
icon: const Icon(SpotubeIcons.pause),
onPressed: () {
downloadManager.pause(track as SpotubeTrack);
}),
const SizedBox(width: 10),
IconButton(
icon: const Icon(SpotubeIcons.close),
onPressed: () {
downloadManager.cancel(track as SpotubeTrack);
}),
],
),
);
}),
DownloadStatus.paused => Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(SpotubeIcons.play),
onPressed: () {
downloadManager.resume(track as SpotubeTrack);
}),
const SizedBox(width: 10),
IconButton(
icon: const Icon(SpotubeIcons.close),
onPressed: () {
downloadManager.cancel(track as SpotubeTrack);
})
],
),
DownloadStatus.failed || DownloadStatus.canceled => SizedBox(
width: 100,
child: Row(
children: [
Icon(
SpotubeIcons.error,
color: Colors.red[400],
),
const SizedBox(width: 10),
IconButton(
icon: const Icon(SpotubeIcons.refresh),
onPressed: () {
downloadManager.retry(track as SpotubeTrack);
},
),
],
),
),
DownloadStatus.completed =>
Icon(SpotubeIcons.done, color: Colors.green[400]),
DownloadStatus.queued => IconButton(
icon: const Icon(SpotubeIcons.close),
onPressed: () {
downloadManager.removeFromQueue(track as SpotubeTrack);
}),
},
);
}
}
13 changes: 10 additions & 3 deletions lib/components/player/player_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
Expand All @@ -39,8 +40,14 @@ class PlayerActions extends HookConsumerWidget {
final isLocalTrack = playlist.activeTrack is LocalTrack;
ref.watch(downloadManagerProvider);
final downloader = ref.watch(downloadManagerProvider.notifier);
final isInQueue = downloader.activeItem != null &&
downloader.activeItem!.id == playlist.activeTrack?.id;
final isInQueue = useMemoized(() {
if (playlist.activeTrack == null) return false;
return downloader.isActive(playlist.activeTrack!);
}, [
playlist.activeTrack,
downloader,
]);

final localTracks = [] /* ref.watch(localTracksProvider).value */;
final auth = ref.watch(AuthenticationNotifier.provider);
final sleepTimer = ref.watch(SleepTimerNotifier.provider);
Expand Down Expand Up @@ -139,7 +146,7 @@ class PlayerActions extends HookConsumerWidget {
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
),
onPressed: playlist.activeTrack != null
? () => downloader.enqueue(playlist.activeTrack!)
? () => downloader.addToQueue(playlist.activeTrack!)
: null,
),
if (playlist.activeTrack != null && !isLocalTrack && auth != null)
Expand Down
4 changes: 1 addition & 3 deletions lib/components/root/sidebar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ class Sidebar extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final mediaQuery = MediaQuery.of(context);

final downloadCount = ref.watch(
downloadManagerProvider.select((s) => s.length),
);
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;

final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
Expand Down
4 changes: 1 addition & 3 deletions lib/components/root/spotube_navigation_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ class SpotubeNavigationBar extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final downloadCount = ref.watch(
downloadManagerProvider.select((s) => s.length),
);
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
final mediaQuery = MediaQuery.of(context);
final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
Expand Down
28 changes: 24 additions & 4 deletions lib/components/shared/track_table/track_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:spotube/components/shared/heart_button.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/download_manager_provider.dart';
Expand Down Expand Up @@ -99,6 +100,20 @@ class TrackOptions extends HookConsumerWidget {
playlistId ?? "",
);

final isInQueue = useMemoized(() {
if (playlist.activeTrack == null) return false;
return downloadManager.isActive(playlist.activeTrack!);
}, [
playlist.activeTrack,
downloadManager,
]);

final progressNotifier = useMemoized(() {
final spotubeTrack = downloadManager.mapToSpotubeTrack(track);
if (spotubeTrack == null) return null;
return downloadManager.getProgressNotifier(spotubeTrack);
});

return ListTileTheme(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
Expand Down Expand Up @@ -175,7 +190,7 @@ class TrackOptions extends HookConsumerWidget {
);
break;
case TrackOptionValue.download:
await downloadManager.enqueue(track);
await downloadManager.addToQueue(track);
break;
}
},
Expand Down Expand Up @@ -268,9 +283,14 @@ class TrackOptions extends HookConsumerWidget {
),
PopSheetEntry(
value: TrackOptionValue.download,
enabled: downloadManager.activeItem?.id != track.id!,
leading: downloadManager.activeItem?.id == track.id!
? const CircularProgressIndicator()
enabled: !isInQueue,
leading: isInQueue
? HookBuilder(builder: (context) {
final progress = useListenable(progressNotifier!);
return CircularProgressIndicator(
value: progress.value,
);
})
: const Icon(SpotubeIcons.download),
title: Text(context.l10n.download_track),
),
Expand Down
2 changes: 1 addition & 1 deletion lib/components/shared/track_table/tracks_table_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class TracksTableView extends HookConsumerWidget {
);
if (confirmed != true) return;
await downloader
.enqueueAll(selectedTracks.toList());
.batchAddToQueue(selectedTracks.toList());
if (context.mounted) {
selected.value = [];
showCheck.value = false;
Expand Down
Loading

0 comments on commit 38dc4be

Please sign in to comment.