Download bug fixes, resuming, lyrics, explicit marking
This commit is contained in:
parent
b9004c3004
commit
a494601ab0
|
@ -3,6 +3,8 @@ package com.ryanheise.just_audio;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
@ -31,7 +33,6 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import io.flutter.Log;
|
|
||||||
import io.flutter.plugin.common.BinaryMessenger;
|
import io.flutter.plugin.common.BinaryMessenger;
|
||||||
import io.flutter.plugin.common.EventChannel;
|
import io.flutter.plugin.common.EventChannel;
|
||||||
import io.flutter.plugin.common.EventChannel.EventSink;
|
import io.flutter.plugin.common.EventChannel.EventSink;
|
||||||
|
@ -446,7 +447,7 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener, Met
|
||||||
case "progressive":
|
case "progressive":
|
||||||
Uri uri = Uri.parse((String)map.get("uri"));
|
Uri uri = Uri.parse((String)map.get("uri"));
|
||||||
//Deezer
|
//Deezer
|
||||||
if (uri.getHost().contains("dzcdn.net")) {
|
if (uri.getHost() != null && uri.getHost().contains("dzcdn.net")) {
|
||||||
//Track id is stored in URL fragment (after #)
|
//Track id is stored in URL fragment (after #)
|
||||||
String fragment = uri.getFragment();
|
String fragment = uri.getFragment();
|
||||||
uri = Uri.parse(((String)map.get("uri")).replace("#" + fragment, ""));
|
uri = Uri.parse(((String)map.get("uri")).replace("#" + fragment, ""));
|
||||||
|
|
|
@ -64,7 +64,7 @@ class DeezerAPI {
|
||||||
'gateway_input': gatewayInput
|
'gateway_input': gatewayInput
|
||||||
},
|
},
|
||||||
data: jsonEncode(params??{}),
|
data: jsonEncode(params??{}),
|
||||||
options: Options(responseType: ResponseType.json, sendTimeout: 5000, receiveTimeout: 5000)
|
options: Options(responseType: ResponseType.json, sendTimeout: 10000, receiveTimeout: 10000)
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ class DeezerAPI {
|
||||||
Dio dio = Dio();
|
Dio dio = Dio();
|
||||||
Response response = await dio.get(
|
Response response = await dio.get(
|
||||||
'https://api.deezer.com/' + path,
|
'https://api.deezer.com/' + path,
|
||||||
options: Options(responseType: ResponseType.json, sendTimeout: 5000, receiveTimeout: 5000)
|
options: Options(responseType: ResponseType.json, sendTimeout: 10000, receiveTimeout: 10000)
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,12 @@ class Track {
|
||||||
|
|
||||||
//TODO: Not in DB
|
//TODO: Not in DB
|
||||||
int diskNumber;
|
int diskNumber;
|
||||||
|
bool explicit;
|
||||||
|
|
||||||
List<dynamic> playbackDetails;
|
List<dynamic> playbackDetails;
|
||||||
|
|
||||||
Track({this.id, this.title, this.duration, this.album, this.playbackDetails, this.albumArt,
|
Track({this.id, this.title, this.duration, this.album, this.playbackDetails, this.albumArt,
|
||||||
this.artists, this.trackNumber, this.offline, this.lyrics, this.favorite, this.diskNumber});
|
this.artists, this.trackNumber, this.offline, this.lyrics, this.favorite, this.diskNumber, this.explicit});
|
||||||
|
|
||||||
String get artistString => artists.map<String>((art) => art.name).join(', ');
|
String get artistString => artists.map<String>((art) => art.name).join(', ');
|
||||||
String get durationString => "${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
String get durationString => "${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||||
|
@ -134,7 +135,8 @@ class Track {
|
||||||
playbackDetails: [json['MD5_ORIGIN'], json['MEDIA_VERSION']],
|
playbackDetails: [json['MD5_ORIGIN'], json['MEDIA_VERSION']],
|
||||||
lyrics: Lyrics(id: json['LYRICS_ID'].toString()),
|
lyrics: Lyrics(id: json['LYRICS_ID'].toString()),
|
||||||
favorite: favorite,
|
favorite: favorite,
|
||||||
diskNumber: int.parse(json['DISK_NUMBER']??'1')
|
diskNumber: int.parse(json['DISK_NUMBER']??'1'),
|
||||||
|
explicit: (json['EXPLICIT_LYRICS'].toString() == '1') ? true:false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Map<String, dynamic> toSQL({off = false}) => {
|
Map<String, dynamic> toSQL({off = false}) => {
|
||||||
|
@ -470,15 +472,17 @@ class Lyrics {
|
||||||
class Lyric {
|
class Lyric {
|
||||||
Duration offset;
|
Duration offset;
|
||||||
String text;
|
String text;
|
||||||
|
String lrcTimestamp;
|
||||||
|
|
||||||
Lyric({this.offset, this.text});
|
Lyric({this.offset, this.text, this.lrcTimestamp});
|
||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
factory Lyric.fromPrivateJson(Map<dynamic, dynamic> json) {
|
factory Lyric.fromPrivateJson(Map<dynamic, dynamic> json) {
|
||||||
if (json['milliseconds'] == null || json['line'] == null) return Lyric(); //Empty lyric
|
if (json['milliseconds'] == null || json['line'] == null) return Lyric(); //Empty lyric
|
||||||
return Lyric(
|
return Lyric(
|
||||||
offset: Duration(milliseconds: int.parse(json['milliseconds'].toString())),
|
offset: Duration(milliseconds: int.parse(json['milliseconds'].toString())),
|
||||||
text: json['line']
|
text: json['line'],
|
||||||
|
lrcTimestamp: json['lrc_timestamp']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ Track _$TrackFromJson(Map<String, dynamic> json) {
|
||||||
: Lyrics.fromJson(json['lyrics'] as Map<String, dynamic>),
|
: Lyrics.fromJson(json['lyrics'] as Map<String, dynamic>),
|
||||||
favorite: json['favorite'] as bool,
|
favorite: json['favorite'] as bool,
|
||||||
diskNumber: json['diskNumber'] as int,
|
diskNumber: json['diskNumber'] as int,
|
||||||
|
explicit: json['explicit'] as bool,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +47,7 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
|
||||||
'lyrics': instance.lyrics,
|
'lyrics': instance.lyrics,
|
||||||
'favorite': instance.favorite,
|
'favorite': instance.favorite,
|
||||||
'diskNumber': instance.diskNumber,
|
'diskNumber': instance.diskNumber,
|
||||||
|
'explicit': instance.explicit,
|
||||||
'playbackDetails': instance.playbackDetails,
|
'playbackDetails': instance.playbackDetails,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -244,12 +246,14 @@ Lyric _$LyricFromJson(Map<String, dynamic> json) {
|
||||||
? null
|
? null
|
||||||
: Duration(microseconds: json['offset'] as int),
|
: Duration(microseconds: json['offset'] as int),
|
||||||
text: json['text'] as String,
|
text: json['text'] as String,
|
||||||
|
lrcTimestamp: json['lrcTimestamp'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _$LyricToJson(Lyric instance) => <String, dynamic>{
|
Map<String, dynamic> _$LyricToJson(Lyric instance) => <String, dynamic>{
|
||||||
'offset': instance.offset?.inMicroseconds,
|
'offset': instance.offset?.inMicroseconds,
|
||||||
'text': instance.text,
|
'text': instance.text,
|
||||||
|
'lrcTimestamp': instance.lrcTimestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
QueueSource _$QueueSourceFromJson(Map<String, dynamic> json) {
|
QueueSource _$QueueSourceFromJson(Map<String, dynamic> json) {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:disk_space/disk_space.dart';
|
import 'package:disk_space/disk_space.dart';
|
||||||
import 'package:ext_storage/ext_storage.dart';
|
import 'package:ext_storage/ext_storage.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -30,7 +32,7 @@ class DownloadManager {
|
||||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
||||||
bool _cancelNotifications = true;
|
bool _cancelNotifications = true;
|
||||||
|
|
||||||
bool get stopped => queue.length > 0 && _download == null;
|
bool stopped = true;
|
||||||
|
|
||||||
Future init() async {
|
Future init() async {
|
||||||
//Prepare DB
|
//Prepare DB
|
||||||
|
@ -115,7 +117,7 @@ class DownloadManager {
|
||||||
|
|
||||||
//Update queue, start new download
|
//Update queue, start new download
|
||||||
void updateQueue() async {
|
void updateQueue() async {
|
||||||
if (_download == null && queue.length > 0) {
|
if (_download == null && queue.length > 0 && !stopped) {
|
||||||
_download = queue[0].download(
|
_download = queue[0].download(
|
||||||
onDone: () async {
|
onDone: () async {
|
||||||
//On download finished
|
//On download finished
|
||||||
|
@ -137,10 +139,12 @@ class DownloadManager {
|
||||||
updateQueue();
|
updateQueue();
|
||||||
}
|
}
|
||||||
).catchError((e, st) async {
|
).catchError((e, st) async {
|
||||||
|
if (stopped) return;
|
||||||
print('Download error: $e\n$st');
|
print('Download error: $e\n$st');
|
||||||
//Catch download errors
|
//Catch download errors
|
||||||
_download = null;
|
_download = null;
|
||||||
_cancelNotifications = true;
|
_cancelNotifications = true;
|
||||||
|
//Cancellation error i guess
|
||||||
await _showError();
|
await _showError();
|
||||||
});
|
});
|
||||||
//Show download progress notifications
|
//Show download progress notifications
|
||||||
|
@ -148,6 +152,22 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Stop downloading and end my life
|
||||||
|
Future stop() async {
|
||||||
|
stopped = true;
|
||||||
|
if (_download != null) {
|
||||||
|
await queue[0].stop();
|
||||||
|
}
|
||||||
|
_download = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Start again downloads
|
||||||
|
Future start() async {
|
||||||
|
if (_download != null) return;
|
||||||
|
stopped = false;
|
||||||
|
updateQueue();
|
||||||
|
}
|
||||||
|
|
||||||
//Show error notification
|
//Show error notification
|
||||||
Future _showError() async {
|
Future _showError() async {
|
||||||
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
|
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
|
||||||
|
@ -290,18 +310,13 @@ class DownloadManager {
|
||||||
return d.path;
|
return d.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future addOfflineTrack(Track track, {private = true}) async {
|
Future addOfflineTrack(Track track, {private = true, forceStart = true}) async {
|
||||||
//Paths
|
//Paths
|
||||||
String path = p.join(_offlinePath, track.id);
|
String path = p.join(_offlinePath, track.id);
|
||||||
if (track.playbackDetails == null) {
|
if (track.playbackDetails == null) {
|
||||||
//Get track from API if download info missing
|
//Get track from API if download info missing
|
||||||
track = await deezerAPI.track(track.id);
|
track = await deezerAPI.track(track.id);
|
||||||
}
|
}
|
||||||
//Load lyrics
|
|
||||||
try {
|
|
||||||
Lyrics l = await deezerAPI.lyrics(track.id);
|
|
||||||
track.lyrics = l;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
String url = track.getUrl(settings.getQualityInt(settings.offlineQuality));
|
String url = track.getUrl(settings.getQualityInt(settings.offlineQuality));
|
||||||
if (!private) {
|
if (!private) {
|
||||||
|
@ -316,6 +331,12 @@ class DownloadManager {
|
||||||
if (settings.downloadQuality == AudioQuality.FLAC) {
|
if (settings.downloadQuality == AudioQuality.FLAC) {
|
||||||
path = 'flac';
|
path = 'flac';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
//Load lyrics for private
|
||||||
|
try {
|
||||||
|
Lyrics l = await deezerAPI.lyrics(track.id);
|
||||||
|
track.lyrics = l;
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Download download = Download(track: track, path: path, url: url, private: private);
|
Download download = Download(track: track, path: path, url: url, private: private);
|
||||||
|
@ -339,7 +360,7 @@ class DownloadManager {
|
||||||
await b.commit();
|
await b.commit();
|
||||||
|
|
||||||
queue.add(download);
|
queue.add(download);
|
||||||
updateQueue();
|
if (forceStart) start();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future addOfflineAlbum(Album album, {private = true}) async {
|
Future addOfflineAlbum(Album album, {private = true}) async {
|
||||||
|
@ -353,8 +374,9 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
//Save all tracks
|
//Save all tracks
|
||||||
for (Track track in album.tracks) {
|
for (Track track in album.tracks) {
|
||||||
await addOfflineTrack(track, private: private);
|
await addOfflineTrack(track, private: private, forceStart: false);
|
||||||
}
|
}
|
||||||
|
start();
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add offline playlist, can be also used as update
|
//Add offline playlist, can be also used as update
|
||||||
|
@ -370,8 +392,9 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
//Download all tracks
|
//Download all tracks
|
||||||
for (Track t in playlist.tracks) {
|
for (Track t in playlist.tracks) {
|
||||||
await addOfflineTrack(t, private: private);
|
await addOfflineTrack(t, private: private, forceStart: false);
|
||||||
}
|
}
|
||||||
|
start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -465,8 +488,13 @@ class DownloadManager {
|
||||||
|
|
||||||
//Delete queue
|
//Delete queue
|
||||||
Future clearQueue() async {
|
Future clearQueue() async {
|
||||||
for (int i=queue.length-1; i>0; i--) {
|
while (queue.length > 0) {
|
||||||
await removeDownload(queue[i]);
|
if (queue.length == 1) {
|
||||||
|
if (_download != null) break;
|
||||||
|
await removeDownload(queue[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await removeDownload(queue[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -485,23 +513,39 @@ class Download {
|
||||||
DownloadState state;
|
DownloadState state;
|
||||||
String _cover;
|
String _cover;
|
||||||
|
|
||||||
|
//For canceling
|
||||||
|
IOSink _outSink;
|
||||||
|
CancelToken _cancel;
|
||||||
|
StreamSubscription _progressSub;
|
||||||
|
|
||||||
int received = 0;
|
int received = 0;
|
||||||
int total = 1;
|
int total = 1;
|
||||||
|
|
||||||
Download({this.track, this.path, this.url, this.private, this.state = DownloadState.NONE});
|
Download({this.track, this.path, this.url, this.private, this.state = DownloadState.NONE});
|
||||||
|
|
||||||
|
//Stop download
|
||||||
|
Future stop() async {
|
||||||
|
if (_cancel != null) _cancel.cancel();
|
||||||
|
//if (_outSink != null) _outSink.close();
|
||||||
|
if (_progressSub != null) _progressSub.cancel();
|
||||||
|
|
||||||
|
received = 0;
|
||||||
|
total = 1;
|
||||||
|
state = DownloadState.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
Future download({onDone}) async {
|
Future download({onDone}) async {
|
||||||
Dio dio = Dio();
|
Dio dio = Dio();
|
||||||
|
|
||||||
//TODO: Check for internet before downloading
|
//TODO: Check for internet before downloading
|
||||||
|
|
||||||
if (!this.private) {
|
if (!this.private && !(this.path.endsWith('.mp3') || this.path.endsWith('.flac'))) {
|
||||||
String ext = this.path;
|
String ext = this.path;
|
||||||
//Get track details
|
//Get track details
|
||||||
Map rawTrack = (await deezerAPI.callApi('song.getListData', params: {'sng_ids': [track.id]}))['results']['data'][0];
|
Map _rawTrackData = await deezerAPI.callApi('song.getListData', params: {'sng_ids': [track.id]});
|
||||||
|
Map rawTrack = _rawTrackData['results']['data'][0];
|
||||||
this.track = Track.fromPrivateJson(rawTrack);
|
this.track = Track.fromPrivateJson(rawTrack);
|
||||||
|
|
||||||
|
|
||||||
//Get path if public
|
//Get path if public
|
||||||
RegExp sanitize = RegExp(r'[\/\\\?\%\*\:\|\"\<\>]');
|
RegExp sanitize = RegExp(r'[\/\\\?\%\*\:\|\"\<\>]');
|
||||||
//Download path
|
//Download path
|
||||||
|
@ -533,6 +577,9 @@ class Download {
|
||||||
|
|
||||||
//Create filename
|
//Create filename
|
||||||
String _filename = settings.downloadFilename;
|
String _filename = settings.downloadFilename;
|
||||||
|
//Feats filter
|
||||||
|
String feats = '';
|
||||||
|
if (track.artists.length > 1) feats = "feat. ${track.artists.sublist(1).map((a) => a.name).join(', ')}";
|
||||||
//Filters
|
//Filters
|
||||||
Map<String, String> vars = {
|
Map<String, String> vars = {
|
||||||
'%artists%': track.artistString.replaceAll(sanitize, ''),
|
'%artists%': track.artistString.replaceAll(sanitize, ''),
|
||||||
|
@ -540,7 +587,8 @@ class Download {
|
||||||
'%title%': track.title.replaceAll(sanitize, ''),
|
'%title%': track.title.replaceAll(sanitize, ''),
|
||||||
'%album%': track.album.title.replaceAll(sanitize, ''),
|
'%album%': track.album.title.replaceAll(sanitize, ''),
|
||||||
'%trackNumber%': track.trackNumber.toString(),
|
'%trackNumber%': track.trackNumber.toString(),
|
||||||
'%0trackNumber%': track.trackNumber.toString().padLeft(2, '0')
|
'%0trackNumber%': track.trackNumber.toString().padLeft(2, '0'),
|
||||||
|
'%feats%': feats
|
||||||
};
|
};
|
||||||
//Replace
|
//Replace
|
||||||
vars.forEach((key, value) {
|
vars.forEach((key, value) {
|
||||||
|
@ -553,15 +601,48 @@ class Download {
|
||||||
//Download
|
//Download
|
||||||
this.state = DownloadState.DOWNLOADING;
|
this.state = DownloadState.DOWNLOADING;
|
||||||
|
|
||||||
await dio.download(
|
//Create download file
|
||||||
this.url,
|
File downloadFile = File(this.path + '.ENC');
|
||||||
this.path + '.ENC',
|
//Get start position
|
||||||
deleteOnError: true,
|
int start = 0;
|
||||||
onReceiveProgress: (rec, total) {
|
if (await downloadFile.exists()) {
|
||||||
this.received = rec;
|
FileStat stat = await downloadFile.stat();
|
||||||
this.total = total;
|
start = stat.size;
|
||||||
|
} else {
|
||||||
|
//Create file if doesnt exist
|
||||||
|
await downloadFile.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
//Download
|
||||||
|
_cancel = CancelToken();
|
||||||
|
Response response = await dio.get(
|
||||||
|
this.url,
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.stream,
|
||||||
|
headers: {
|
||||||
|
'Range': 'bytes=$start-'
|
||||||
|
},
|
||||||
|
),
|
||||||
|
cancelToken: _cancel
|
||||||
);
|
);
|
||||||
|
//Size
|
||||||
|
this.total = int.parse(response.headers['Content-Length'][0]) + start;
|
||||||
|
this.received = start;
|
||||||
|
//Save
|
||||||
|
_outSink = downloadFile.openWrite(mode: FileMode.append);
|
||||||
|
Stream<Uint8List> _data = response.data.stream.asBroadcastStream();
|
||||||
|
_progressSub = _data.listen((Uint8List c) {
|
||||||
|
this.received += c.length;
|
||||||
|
});
|
||||||
|
//Pipe to file
|
||||||
|
try {
|
||||||
|
await _outSink.addStream(_data);
|
||||||
|
} catch (e) {
|
||||||
|
await _outSink.close();
|
||||||
|
throw Exception('Download error');
|
||||||
|
}
|
||||||
|
await _outSink.close();
|
||||||
|
_cancel = null;
|
||||||
|
|
||||||
|
|
||||||
this.state = DownloadState.POST;
|
this.state = DownloadState.POST;
|
||||||
//Decrypt
|
//Decrypt
|
||||||
|
@ -586,6 +667,28 @@ class Download {
|
||||||
//Remove encrypted
|
//Remove encrypted
|
||||||
await File(path + '.ENC').delete();
|
await File(path + '.ENC').delete();
|
||||||
if (!settings.albumFolder) await File(_cover).delete();
|
if (!settings.albumFolder) await File(_cover).delete();
|
||||||
|
|
||||||
|
//Get lyrics
|
||||||
|
Lyrics lyrics;
|
||||||
|
try {
|
||||||
|
lyrics = await deezerAPI.lyrics(track.id);
|
||||||
|
} catch (e) {}
|
||||||
|
if (lyrics != null && lyrics.lyrics != null) {
|
||||||
|
//Create .LRC file
|
||||||
|
String lrcPath = p.join(p.dirname(path), p.basenameWithoutExtension(path)) + '.lrc';
|
||||||
|
File lrcFile = File(lrcPath);
|
||||||
|
String lrcData = '';
|
||||||
|
//Generate file
|
||||||
|
lrcData += '[ar:${track.artistString}]\r\n';
|
||||||
|
lrcData += '[al:${track.album.title}]\r\n';
|
||||||
|
lrcData += '[ti:${track.title}]\r\n';
|
||||||
|
for (Lyric l in lyrics.lyrics) {
|
||||||
|
if (l.lrcTimestamp != null && l.lrcTimestamp != '' && l.text != null)
|
||||||
|
lrcData += '${l.lrcTimestamp}${l.text}\r\n';
|
||||||
|
}
|
||||||
|
lrcFile.writeAsString(lrcData);
|
||||||
|
}
|
||||||
|
|
||||||
this.state = DownloadState.DONE;
|
this.state = DownloadState.DONE;
|
||||||
onDone();
|
onDone();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -309,12 +309,13 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
||||||
MediaControl.skipToPrevious,
|
MediaControl.skipToPrevious,
|
||||||
if (_player.playing) MediaControl.pause else MediaControl.play,
|
if (_player.playing) MediaControl.pause else MediaControl.play,
|
||||||
MediaControl.skipToNext,
|
MediaControl.skipToNext,
|
||||||
//MediaControl.stop
|
MediaControl.stop
|
||||||
],
|
],
|
||||||
systemActions: [
|
systemActions: [
|
||||||
MediaAction.seekTo,
|
MediaAction.seekTo,
|
||||||
MediaAction.seekForward,
|
MediaAction.seekForward,
|
||||||
MediaAction.seekBackward
|
MediaAction.seekBackward,
|
||||||
|
//MediaAction.stop
|
||||||
],
|
],
|
||||||
processingState: _getProcessingState(),
|
processingState: _getProcessingState(),
|
||||||
playing: _player.playing,
|
playing: _player.playing,
|
||||||
|
|
|
@ -52,6 +52,7 @@ class DownloadTile extends StatelessWidget {
|
||||||
subtitle: Text(subtitle),
|
subtitle: Text(subtitle),
|
||||||
leading: CachedImage(
|
leading: CachedImage(
|
||||||
url: download.track.albumArt.thumb,
|
url: download.track.albumArt.thumb,
|
||||||
|
width: 48.0,
|
||||||
),
|
),
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -102,8 +103,36 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
||||||
title: Text('Downloads'),
|
title: Text('Downloads'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.delete_sweep),
|
icon: Icon(downloadManager.stopped ? Icons.play_arrow : Icons.stop),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
if (downloadManager.stopped) downloadManager.start();
|
||||||
|
else downloadManager.stop();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
StreamBuilder(
|
||||||
|
stream: Stream.periodic(Duration(milliseconds: 500)).asBroadcastStream(), //Periodic to get current download progress
|
||||||
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
|
|
||||||
|
if (downloadManager.queue.length == 0)
|
||||||
|
return Container(width: 0, height: 0,);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
...List.generate(downloadManager.queue.length, (i) {
|
||||||
|
return DownloadTile(downloadManager.queue[i], onDelete: () => setState(() => {}));
|
||||||
|
}),
|
||||||
|
if (downloadManager.queue.length > 1 || (downloadManager.stopped && downloadManager.queue.length > 0))
|
||||||
|
ListTile(
|
||||||
|
title: Text('Clear queue'),
|
||||||
|
subtitle: Text("This won't delete currently downloading item"),
|
||||||
|
leading: Icon(Icons.delete),
|
||||||
|
onTap: () async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
@ -128,21 +157,7 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
]
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
children: <Widget>[
|
|
||||||
StreamBuilder(
|
|
||||||
stream: Stream.periodic(Duration(milliseconds: 500)).asBroadcastStream(), //Periodic to get current download progress
|
|
||||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
|
||||||
|
|
||||||
if (downloadManager.queue.length == 0)
|
|
||||||
return Container(width: 0, height: 0,);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: List.generate(downloadManager.queue.length, (i) {
|
|
||||||
return DownloadTile(downloadManager.queue[i], onDelete: () => setState(() => {}));
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -56,20 +56,20 @@ class LibraryScreen extends StatelessWidget {
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(height: 4.0,),
|
Container(height: 4.0,),
|
||||||
if (downloadManager.stopped)
|
if (downloadManager.stopped && downloadManager.queue.length > 0)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Downloads'),
|
title: Text('Downloads'),
|
||||||
leading: Icon(Icons.file_download),
|
leading: Icon(Icons.file_download),
|
||||||
subtitle: Text('Downloading is currently stopped, click here to resume.'),
|
subtitle: Text('Downloading is currently stopped, click here to resume.'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
downloadManager.updateQueue();
|
downloadManager.start();
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
builder: (context) => DownloadsScreen()
|
builder: (context) => DownloadsScreen()
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
//Dirty if to not use columns
|
//Dirty if to not use columns
|
||||||
if (downloadManager.stopped)
|
if (downloadManager.stopped && downloadManager.queue.length > 0)
|
||||||
Divider(),
|
Divider(),
|
||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
|
@ -497,7 +497,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||||
),
|
),
|
||||||
Container(height: 8.0),
|
Container(height: 8.0),
|
||||||
Text(
|
Text(
|
||||||
'Valid variables are: %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%',
|
'Valid variables are: %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12.0,
|
fontSize: 12.0,
|
||||||
),
|
),
|
||||||
|
|
|
@ -49,6 +49,7 @@ class _TrackTileState extends State<TrackTile> {
|
||||||
title: Text(
|
title: Text(
|
||||||
widget.track.title,
|
widget.track.title,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.clip,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: nowPlaying?Theme.of(context).primaryColor:null
|
color: nowPlaying?Theme.of(context).primaryColor:null
|
||||||
),
|
),
|
||||||
|
@ -59,12 +60,23 @@ class _TrackTileState extends State<TrackTile> {
|
||||||
),
|
),
|
||||||
leading: CachedImage(
|
leading: CachedImage(
|
||||||
url: widget.track.albumArt.thumb,
|
url: widget.track.albumArt.thumb,
|
||||||
|
width: 48,
|
||||||
),
|
),
|
||||||
onTap: widget.onTap,
|
onTap: widget.onTap,
|
||||||
onLongPress: widget.onHold,
|
onLongPress: widget.onHold,
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
if (widget.track.explicit??false)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
child: Text(
|
||||||
|
'E',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 2.0),
|
padding: EdgeInsets.symmetric(horizontal: 2.0),
|
||||||
child: Text(widget.track.durationString),
|
child: Text(widget.track.durationString),
|
||||||
|
@ -98,6 +110,7 @@ class AlbumTile extends StatelessWidget {
|
||||||
),
|
),
|
||||||
leading: CachedImage(
|
leading: CachedImage(
|
||||||
url: album.art.thumb,
|
url: album.art.thumb,
|
||||||
|
width: 48,
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onHold,
|
onLongPress: onHold,
|
||||||
|
@ -172,6 +185,7 @@ class PlaylistTile extends StatelessWidget {
|
||||||
),
|
),
|
||||||
leading: CachedImage(
|
leading: CachedImage(
|
||||||
url: playlist.image.thumb,
|
url: playlist.image.thumb,
|
||||||
|
width: 48,
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onHold,
|
onLongPress: onHold,
|
||||||
|
|
Loading…
Reference in New Issue