[new] Play playlist starting from any track

[new] skip to another track in the currently playing playlist
[improved] Download track now with infinite progressbar & completion indicator
This commit is contained in:
Kingkor Roy Tirtho 2022-01-21 20:00:58 +06:00
parent 0801d6170b
commit b75256b481
2 changed files with 127 additions and 56 deletions

View File

@ -15,8 +15,11 @@ class DownloadTrackButton extends StatefulWidget {
_DownloadTrackButtonState createState() => _DownloadTrackButtonState(); _DownloadTrackButtonState createState() => _DownloadTrackButtonState();
} }
enum TrackStatus { downloading, idle, done }
class _DownloadTrackButtonState extends State<DownloadTrackButton> { class _DownloadTrackButtonState extends State<DownloadTrackButton> {
late YoutubeExplode yt; late YoutubeExplode yt;
TrackStatus status = TrackStatus.idle;
@override @override
void initState() { void initState() {
@ -30,33 +33,89 @@ class _DownloadTrackButtonState extends State<DownloadTrackButton> {
super.dispose(); super.dispose();
} }
_downloadTrack() async {
if (widget.track == null) return;
StreamManifest manifest =
await yt.videos.streamsClient.getManifest(widget.track?.href);
var audioStream = yt.videos.streamsClient
.get(manifest.audioOnly.withHighestBitrate())
.asBroadcastStream();
var statusCb = audioStream.listen(
(event) {
if (status != TrackStatus.downloading) {
setState(() {
status = TrackStatus.downloading;
});
}
},
onDone: () async {
setState(() {
status = TrackStatus.done;
});
await Future.delayed(
const Duration(seconds: 3),
() {
if (status == TrackStatus.done) {
setState(() {
status = TrackStatus.idle;
});
}
},
);
},
);
String downloadFolder = path.join(
(await path_provider.getDownloadsDirectory())!.path, "Spotube");
String fileName =
"${widget.track?.name} - ${artistsToString(widget.track?.artists ?? [])}.mp3";
File outputFile = File(path.join(downloadFolder, fileName));
if (!outputFile.existsSync()) {
outputFile.createSync(recursive: true);
IOSink outputFileStream = outputFile.openWrite();
await audioStream.pipe(outputFileStream);
await outputFileStream.flush();
await outputFileStream.close().then((value) async {
if (status == TrackStatus.downloading) {
setState(() {
status = TrackStatus.done;
});
await Future.delayed(
const Duration(seconds: 3),
() {
if (status == TrackStatus.done) {
setState(() {
status = TrackStatus.idle;
});
}
},
);
}
return statusCb.cancel();
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (status == TrackStatus.downloading) {
return const SizedBox(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
height: 20,
width: 20,
);
} else if (status == TrackStatus.done) {
return const Icon(Icons.download_done_rounded);
}
return IconButton( return IconButton(
icon: const Icon(Icons.download_rounded), icon: const Icon(Icons.download_rounded),
onPressed: widget.track != null onPressed: widget.track != null &&
? () async { !(widget.track!.href ?? "").startsWith("https://api.spotify.com")
if (widget.track == null) return; ? _downloadTrack
StreamManifest manifest = await yt.videos.streamsClient
.getManifest(widget.track?.href!.split("watch?v=").last);
var audioStream = yt.videos.streamsClient
.get(manifest.audioOnly.withHighestBitrate());
String downloadFolder = path.join(
(await path_provider.getDownloadsDirectory())!.path,
"Spotube");
String fileName =
"${widget.track?.name} - ${artistsToString(widget.track?.artists ?? [])}.mp3";
File outputFile = File(path.join(downloadFolder, fileName));
if (!outputFile.existsSync()) {
outputFile.createSync(recursive: true);
IOSink outputFileStream = outputFile.openWrite();
await audioStream.pipe(outputFileStream);
await outputFileStream.flush();
await outputFileStream.close();
}
}
: null, : null,
); );
} }

View File

@ -15,7 +15,25 @@ class PlaylistView extends StatefulWidget {
} }
class _PlaylistViewState extends State<PlaylistView> { class _PlaylistViewState extends State<PlaylistView> {
List<TableRow> trackToTableRow(List<Track> tracks) { playPlaylist(Playback playback, List<Track> tracks, {Track? currentTrack}) {
currentTrack ??= tracks.first;
var isPlaylistPlaying = playback.currentPlaylist?.id == widget.playlist.id;
if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks,
id: widget.playlist.id!,
name: widget.playlist.name!,
thumbnail: widget.playlist.images![0].url!,
);
playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playback.currentTrack?.id) {
playback.setCurrentTrack = currentTrack;
}
}
List<TableRow> trackToTableRow(Playback playback, List<Track> tracks) {
return tracks.asMap().entries.map((track) { return tracks.asMap().entries.map((track) {
String? thumbnailUrl = (track.value.album?.images?.isNotEmpty ?? false) String? thumbnailUrl = (track.value.album?.images?.isNotEmpty ?? false)
? track.value.album?.images?.last.url ? track.value.album?.images?.last.url
@ -35,6 +53,18 @@ class _PlaylistViewState extends State<PlaylistView> {
TableCell( TableCell(
child: Row( child: Row(
children: [ children: [
IconButton(
icon: Icon(
playback.currentTrack?.id != null &&
playback.currentTrack?.id == track.value.id
? Icons.pause_circle_rounded
: Icons.play_circle_rounded,
color: Theme.of(context).primaryColor,
),
onPressed: () {
playPlaylist(playback, tracks, currentTrack: track.value);
},
),
if (thumbnailUrl != null) if (thumbnailUrl != null)
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@ -105,6 +135,9 @@ class _PlaylistViewState extends State<PlaylistView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Playback playback = context.watch<Playback>();
var isPlaylistPlaying =
playback.currentPlaylist?.id == this.widget.playlist.id;
return Consumer<SpotifyDI>(builder: (_, data, __) { return Consumer<SpotifyDI>(builder: (_, data, __) {
return Scaffold( return Scaffold(
body: FutureBuilder<Iterable<Track>>( body: FutureBuilder<Iterable<Track>>(
@ -132,37 +165,16 @@ class _PlaylistViewState extends State<PlaylistView> {
onPressed: () {}, onPressed: () {},
), ),
// play playlist // play playlist
Consumer<Playback>( IconButton(
builder: (context, playback, widget) { icon: Icon(
var isPlaylistPlaying = isPlaylistPlaying
playback.currentPlaylist?.id == ? Icons.stop_rounded
this.widget.playlist.id; : Icons.play_arrow_rounded,
return IconButton( ),
icon: Icon( onPressed: snapshot.hasData
isPlaylistPlaying ? () => playPlaylist(playback, tracks)
? Icons.stop_rounded : null,
: Icons.play_arrow_rounded, )
),
onPressed: snapshot.hasData
? () {
if (!isPlaylistPlaying) {
playback.setCurrentPlaylist =
CurrentPlaylist(
tracks: tracks,
id: this.widget.playlist.id!,
name: this.widget.playlist.name!,
thumbnail: this
.widget
.playlist
.images![0]
.url!,
);
playback.setCurrentTrack = tracks.first;
}
}
: null,
);
}),
], ],
), ),
), ),
@ -216,7 +228,7 @@ class _PlaylistViewState extends State<PlaylistView> {
)), )),
], ],
), ),
...trackToTableRow(tracks), ...trackToTableRow(playback, tracks),
], ],
), ),
), ),