Skip to content

Commit

Permalink
feat: add download queue for desktop & initial playlist download support
Browse files Browse the repository at this point in the history
  • Loading branch information
KRTirtho committed Aug 9, 2022
1 parent 92bc611 commit 08f913e
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 11 deletions.
12 changes: 12 additions & 0 deletions lib/components/Shared/TrackTile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class TrackTile extends HookConsumerWidget {

final bool isActive;

final bool isChecked;
final bool showCheck;
final void Function(bool?)? onCheckChange;

TrackTile(
this.playback, {
required this.track,
Expand All @@ -40,6 +44,9 @@ class TrackTile extends HookConsumerWidget {
this.thumbnailUrl,
this.onTrackPlayButtonPressed,
this.showAlbum = true,
this.isChecked = false,
this.showCheck = false,
this.onCheckChange,
Key? key,
}) : super(key: key);

Expand Down Expand Up @@ -182,6 +189,11 @@ class TrackTile extends HookConsumerWidget {
type: MaterialType.transparency,
child: Row(
children: [
if (showCheck)
Checkbox(
value: isChecked,
onChanged: (s) => onCheckChange?.call(s),
),
SizedBox(
height: 20,
width: 25,
Expand Down
84 changes: 73 additions & 11 deletions lib/components/Shared/TracksTableView.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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/components/Shared/TrackTile.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
Expand All @@ -26,16 +28,31 @@ class TracksTableView extends HookConsumerWidget {
@override
Widget build(context, ref) {
Playback playback = ref.watch(playbackProvider);
final downloader = ref.watch(downloaderProvider);
TextStyle tableHeadStyle =
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);

final breakpoint = useBreakpoints();

final selected = useState<List<String>>([]);
final showCheck = useState<bool>(false);

return SliverList(
delegate: SliverChildListDelegate([
if (heading != null) heading!,
Row(
children: [
Checkbox(
value: selected.value.length == tracks.length,
onChanged: (checked) {
if (!showCheck.value) showCheck.value = true;
if (checked == true) {
selected.value = tracks.map((s) => s.id!).toList();
} else {
selected.value = [];
}
},
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
Expand Down Expand Up @@ -75,8 +92,36 @@ class TracksTableView extends HookConsumerWidget {
Text("Time", style: tableHeadStyle),
const SizedBox(width: 10),
],
SizedBox(
width: breakpoint.isLessThan(Breakpoints.lg) ? 40 : 110,
PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
child: Row(
children: const [
Icon(Icons.file_download_outlined),
Text("Download"),
],
),
onTap: () async {
final spotubeTracks = await Future.wait(
tracks
.where(
(track) => selected.value.contains(track.id),
)
.map((track) {
return Future.delayed(const Duration(seconds: 2),
() => playback.toSpotubeTrack(track));
}),
);

for (var spotubeTrack in spotubeTracks) {
downloader.addToQueue(spotubeTrack);
}
},
value: "download",
),
];
},
),
],
),
Expand All @@ -87,15 +132,32 @@ class TracksTableView extends HookConsumerWidget {
);
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return TrackTile(
playback,
playlistId: playlistId,
track: track,
duration: duration,
thumbnailUrl: thumbnailUrl,
userPlaylist: userPlaylist,
isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: onTrackPlayButtonPressed,
return GestureDetector(
onDoubleTap: () {
showCheck.value = true;
selected.value = [...selected.value, track.value.id!];
},
child: TrackTile(
playback,
playlistId: playlistId,
track: track,
duration: duration,
thumbnailUrl: thumbnailUrl,
userPlaylist: userPlaylist,
isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: onTrackPlayButtonPressed,
isChecked: selected.value.contains(track.value.id),
showCheck: showCheck.value,
onCheckChange: (checked) {
if (checked == true) {
selected.value = [...selected.value, track.value.id!];
} else {
selected.value = selected.value
.where((id) => id != track.value.id)
.toList();
}
},
),
);
}).toList()
]),
Expand Down
75 changes: 75 additions & 0 deletions lib/provider/Downloader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'dart:io';

import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:queue/queue.dart';
import 'package:path/path.dart' as path;
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/provider/YouTube.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';

Queue _queueInstance = Queue(delay: const Duration(seconds: 1));

class Downloader with ChangeNotifier {
Queue _queue;
YoutubeExplode yt;
String downloadPath;
Downloader(
this._queue, {
required this.downloadPath,
required this.yt,
});

int currentlyRunning = 0;

void addToQueue(SpotubeTrack track) {
currentlyRunning++;
notifyListeners();
_queue.add(() async {
try {
final file =
File(path.join(downloadPath, '${track.ytTrack.title}.mp3'));
// TODO find a way to let the UI know there's already provided file is available
if (file.existsSync()) return;
file.createSync(recursive: true);
StreamManifest manifest =
await yt.videos.streamsClient.getManifest(track.ytTrack.url);
final audioStream = yt.videos.streamsClient
.get(
manifest.audioOnly
.where((audio) => audio.codec.mimeType == "audio/mp4")
.withHighestBitrate(),
)
.asBroadcastStream();

IOSink outputFileStream = file.openWrite();
await audioStream.pipe(outputFileStream);
await outputFileStream.flush();
} finally {
currentlyRunning--;
notifyListeners();
}
});
}

cancel() {
_queue.cancel();
_queueInstance = Queue();
_queue = _queueInstance;
}
}

final downloaderProvider = ChangeNotifierProvider(
(ref) {
return Downloader(
_queueInstance,
yt: ref.watch(youtubeProvider),
downloadPath: ref.watch(
userPreferencesProvider.select(
(s) => s.downloadLocation,
),
),
);
},
);
14 changes: 14 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.9"
flutter_downloader:
dependency: "direct main"
description:
name: flutter_downloader
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
flutter_hooks:
dependency: "direct main"
description:
Expand Down Expand Up @@ -1017,6 +1024,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
queue:
dependency: "direct main"
description:
name: queue
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0+1"
riverpod:
dependency: transitive
description:
Expand Down
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ dependencies:
audio_session: ^0.1.9
file_picker: ^4.6.1
popover: ^0.2.6+3
queue: ^3.1.0+1
flutter_downloader: ^1.8.1

dev_dependencies:
flutter_test:
Expand Down

0 comments on commit 08f913e

Please sign in to comment.