Download naming changes
This commit is contained in:
parent
37f97f9992
commit
4e5e3a3059
|
@ -136,7 +136,8 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
updateQueue();
|
updateQueue();
|
||||||
}
|
}
|
||||||
).catchError((err) async {
|
).catchError((e, st) async {
|
||||||
|
print('Download error: $e\n$st');
|
||||||
//Catch download errors
|
//Catch download errors
|
||||||
_download = null;
|
_download = null;
|
||||||
_cancelNotifications = true;
|
_cancelNotifications = true;
|
||||||
|
@ -455,6 +456,24 @@ class DownloadManager {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Delete download from db
|
||||||
|
Future removeDownload(Download download) async {
|
||||||
|
await db.delete('downloads', where: 'trackId == ?', whereArgs: [download.track.id]);
|
||||||
|
queue.removeWhere((d) => d.track.id == download.track.id);
|
||||||
|
//TODO: remove files for downloaded
|
||||||
|
}
|
||||||
|
|
||||||
|
//Delete queue
|
||||||
|
Future clearQueue() async {
|
||||||
|
for (int i=queue.length-1; i>0; i--) {
|
||||||
|
await removeDownload(queue[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove non-private downloads
|
||||||
|
Future cleanDownloadHistory() async {
|
||||||
|
await db.delete('downloads', where: 'private == 0');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,25 +498,29 @@ class Download {
|
||||||
if (!this.private) {
|
if (!this.private) {
|
||||||
String ext = this.path;
|
String ext = this.path;
|
||||||
//Get track details
|
//Get track details
|
||||||
this.track = await deezerAPI.track(track.id);
|
Map rawTrack = (await deezerAPI.callApi('song.getListData', params: {'sng_ids': [track.id]}))['results']['data'][0];
|
||||||
|
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
|
||||||
if (settings.downloadFolderStructure) {
|
this.path = settings.downloadPath ?? (await ExtStorage.getExternalStoragePublicDirectory(ExtStorage.DIRECTORY_MUSIC));
|
||||||
this.path = p.join(
|
if (settings.artistFolder)
|
||||||
settings.downloadPath ?? (await ExtStorage.getExternalStoragePublicDirectory(ExtStorage.DIRECTORY_MUSIC)),
|
this.path = p.join(this.path, track.artists[0].name.replaceAll(sanitize, ''));
|
||||||
track.artists[0].name.replaceAll(sanitize, ''),
|
if (settings.albumFolder) {
|
||||||
track.album.title.replaceAll(sanitize, ''),
|
String folderName = track.album.title.replaceAll(sanitize, '');
|
||||||
);
|
//Add disk number
|
||||||
} else {
|
if (settings.albumDiscFolder) folderName += ' - Disk ${rawTrack["DISK_NUMBER"]}';
|
||||||
this.path = settings.downloadPath;
|
|
||||||
|
this.path = p.join(this.path, folderName);
|
||||||
}
|
}
|
||||||
//Make dirs
|
//Make dirs
|
||||||
await Directory(this.path).create(recursive: true);
|
await Directory(this.path).create(recursive: true);
|
||||||
|
|
||||||
//Grab cover
|
//Grab cover
|
||||||
_cover = p.join(this.path, 'cover.jpg');
|
_cover = p.join(this.path, 'cover.jpg');
|
||||||
if (!settings.downloadFolderStructure) _cover = p.join(this.path, randomAlpha(12) + '_cover.jpg');
|
if (!settings.albumFolder) _cover = p.join(this.path, randomAlpha(12) + '_cover.jpg');
|
||||||
|
|
||||||
if (!await File(_cover).exists()) {
|
if (!await File(_cover).exists()) {
|
||||||
try {
|
try {
|
||||||
|
@ -508,11 +531,22 @@ class Download {
|
||||||
} catch (e) {print('Error downloading cover');}
|
} catch (e) {print('Error downloading cover');}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add filename
|
//Create filename
|
||||||
String _filename = '${track.trackNumber.toString().padLeft(2, '0')}. ${track.title.replaceAll(sanitize, "")}.$ext';
|
String _filename = settings.downloadFilename;
|
||||||
//Different naming types
|
//Filters
|
||||||
if (settings.downloadNaming == DownloadNaming.STANDALONE)
|
Map<String, String> vars = {
|
||||||
_filename = '${track.artistString.replaceAll(sanitize, "")} - ${track.title.replaceAll(sanitize, "")}.$ext';
|
'%artists%': track.artistString.replaceAll(sanitize, ''),
|
||||||
|
'%artist%': track.artists[0].name.replaceAll(sanitize, ''),
|
||||||
|
'%title%': track.title.replaceAll(sanitize, ''),
|
||||||
|
'%album%': track.album.title.replaceAll(sanitize, ''),
|
||||||
|
'%trackNumber%': track.trackNumber.toString(),
|
||||||
|
'%0trackNumber%': track.trackNumber.toString().padLeft(2, '0')
|
||||||
|
};
|
||||||
|
//Replace
|
||||||
|
vars.forEach((key, value) {
|
||||||
|
_filename = _filename.replaceAll(key, value);
|
||||||
|
});
|
||||||
|
_filename += '.$ext';
|
||||||
|
|
||||||
this.path = p.join(this.path, _filename);
|
this.path = p.join(this.path, _filename);
|
||||||
}
|
}
|
||||||
|
@ -551,7 +585,7 @@ class Download {
|
||||||
}
|
}
|
||||||
//Remove encrypted
|
//Remove encrypted
|
||||||
await File(path + '.ENC').delete();
|
await File(path + '.ENC').delete();
|
||||||
if (!settings.downloadFolderStructure) await File(_cover).delete();
|
if (!settings.albumFolder) await File(_cover).delete();
|
||||||
this.state = DownloadState.DONE;
|
this.state = DownloadState.DONE;
|
||||||
onDone();
|
onDone();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -36,10 +36,16 @@ class Settings {
|
||||||
|
|
||||||
//Download options
|
//Download options
|
||||||
String downloadPath;
|
String downloadPath;
|
||||||
@JsonKey(defaultValue: DownloadNaming.DEFAULT)
|
|
||||||
DownloadNaming downloadNaming;
|
@JsonKey(defaultValue: "%artists% - %title%")
|
||||||
|
String downloadFilename;
|
||||||
@JsonKey(defaultValue: true)
|
@JsonKey(defaultValue: true)
|
||||||
bool downloadFolderStructure;
|
bool albumFolder;
|
||||||
|
@JsonKey(defaultValue: true)
|
||||||
|
bool artistFolder;
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
bool albumDiscFolder;
|
||||||
|
|
||||||
|
|
||||||
//Appearance
|
//Appearance
|
||||||
@JsonKey(defaultValue: Themes.Light)
|
@JsonKey(defaultValue: Themes.Light)
|
||||||
|
@ -208,9 +214,3 @@ enum Themes {
|
||||||
Deezer,
|
Deezer,
|
||||||
Black
|
Black
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DownloadNaming {
|
|
||||||
DEFAULT,
|
|
||||||
STANDALONE,
|
|
||||||
|
|
||||||
}
|
|
|
@ -23,10 +23,11 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
||||||
..downloadQuality =
|
..downloadQuality =
|
||||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['downloadQuality']) ??
|
_$enumDecodeNullable(_$AudioQualityEnumMap, json['downloadQuality']) ??
|
||||||
AudioQuality.FLAC
|
AudioQuality.FLAC
|
||||||
..downloadNaming =
|
..downloadFilename =
|
||||||
_$enumDecodeNullable(_$DownloadNamingEnumMap, json['downloadNaming']) ??
|
json['downloadFilename'] as String ?? '%artists% - %title%'
|
||||||
DownloadNaming.DEFAULT
|
..albumFolder = json['albumFolder'] as bool ?? true
|
||||||
..downloadFolderStructure = json['downloadFolderStructure'] as bool ?? true
|
..artistFolder = json['artistFolder'] as bool ?? true
|
||||||
|
..albumDiscFolder = json['albumDiscFolder'] as bool ?? false
|
||||||
..theme =
|
..theme =
|
||||||
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Light
|
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Light
|
||||||
..primaryColor = Settings._colorFromJson(json['primaryColor'] as int)
|
..primaryColor = Settings._colorFromJson(json['primaryColor'] as int)
|
||||||
|
@ -43,8 +44,10 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
||||||
'offlineQuality': _$AudioQualityEnumMap[instance.offlineQuality],
|
'offlineQuality': _$AudioQualityEnumMap[instance.offlineQuality],
|
||||||
'downloadQuality': _$AudioQualityEnumMap[instance.downloadQuality],
|
'downloadQuality': _$AudioQualityEnumMap[instance.downloadQuality],
|
||||||
'downloadPath': instance.downloadPath,
|
'downloadPath': instance.downloadPath,
|
||||||
'downloadNaming': _$DownloadNamingEnumMap[instance.downloadNaming],
|
'downloadFilename': instance.downloadFilename,
|
||||||
'downloadFolderStructure': instance.downloadFolderStructure,
|
'albumFolder': instance.albumFolder,
|
||||||
|
'artistFolder': instance.artistFolder,
|
||||||
|
'albumDiscFolder': instance.albumDiscFolder,
|
||||||
'theme': _$ThemesEnumMap[instance.theme],
|
'theme': _$ThemesEnumMap[instance.theme],
|
||||||
'primaryColor': Settings._colorToJson(instance.primaryColor),
|
'primaryColor': Settings._colorToJson(instance.primaryColor),
|
||||||
'useArtColor': instance.useArtColor,
|
'useArtColor': instance.useArtColor,
|
||||||
|
@ -91,11 +94,6 @@ const _$AudioQualityEnumMap = {
|
||||||
AudioQuality.FLAC: 'FLAC',
|
AudioQuality.FLAC: 'FLAC',
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$DownloadNamingEnumMap = {
|
|
||||||
DownloadNaming.DEFAULT: 'DEFAULT',
|
|
||||||
DownloadNaming.STANDALONE: 'STANDALONE',
|
|
||||||
};
|
|
||||||
|
|
||||||
const _$ThemesEnumMap = {
|
const _$ThemesEnumMap = {
|
||||||
Themes.Light: 'Light',
|
Themes.Light: 'Light',
|
||||||
Themes.Dark: 'Dark',
|
Themes.Dark: 'Dark',
|
||||||
|
|
|
@ -8,7 +8,8 @@ import '../api/download.dart';
|
||||||
class DownloadTile extends StatelessWidget {
|
class DownloadTile extends StatelessWidget {
|
||||||
|
|
||||||
final Download download;
|
final Download download;
|
||||||
DownloadTile(this.download);
|
Function onDelete;
|
||||||
|
DownloadTile(this.download, {this.onDelete});
|
||||||
|
|
||||||
String get subtitle {
|
String get subtitle {
|
||||||
switch (download.state) {
|
switch (download.state) {
|
||||||
|
@ -53,6 +54,34 @@ class DownloadTile extends StatelessWidget {
|
||||||
url: download.track.albumArt.thumb,
|
url: download.track.albumArt.thumb,
|
||||||
),
|
),
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
|
onTap: () {
|
||||||
|
//Delete if none
|
||||||
|
if (download.state == DownloadState.NONE) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Delete'),
|
||||||
|
content: Text('Are you sure, you want to delete this download?'),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Cancel'),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Delete'),
|
||||||
|
onPressed: () {
|
||||||
|
downloadManager.removeDownload(download);
|
||||||
|
if (this.onDelete != null) this.onDelete();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
progressBar
|
progressBar
|
||||||
],
|
],
|
||||||
|
@ -60,17 +89,51 @@ class DownloadTile extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadsScreen extends StatelessWidget {
|
class DownloadsScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_DownloadsScreenState createState() => _DownloadsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DownloadsScreenState extends State<DownloadsScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Downloads'),
|
title: Text('Downloads'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.delete_sweep),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Delete'),
|
||||||
|
content: Text('Are you sure, you want to delete all queued downloads?'),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Cancel'),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Delete'),
|
||||||
|
onPressed: () async {
|
||||||
|
await downloadManager.clearQueue();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
StreamBuilder(
|
StreamBuilder(
|
||||||
stream: Stream.periodic(Duration(milliseconds: 500)), //Periodic to get current download progress
|
stream: Stream.periodic(Duration(milliseconds: 500)).asBroadcastStream(), //Periodic to get current download progress
|
||||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
|
|
||||||
if (downloadManager.queue.length == 0)
|
if (downloadManager.queue.length == 0)
|
||||||
|
@ -78,7 +141,7 @@ class DownloadsScreen extends StatelessWidget {
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: List.generate(downloadManager.queue.length, (i) {
|
children: List.generate(downloadManager.queue.length, (i) {
|
||||||
return DownloadTile(downloadManager.queue[i]);
|
return DownloadTile(downloadManager.queue[i], onDelete: () => setState(() => {}));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -101,7 +164,16 @@ class DownloadsScreen extends StatelessWidget {
|
||||||
...List.generate(snapshot.data.length, (i) {
|
...List.generate(snapshot.data.length, (i) {
|
||||||
Download d = snapshot.data[i];
|
Download d = snapshot.data[i];
|
||||||
return DownloadTile(d);
|
return DownloadTile(d);
|
||||||
})
|
}),
|
||||||
|
ListTile(
|
||||||
|
title: Text('Clear downloads history'),
|
||||||
|
leading: Icon(Icons.delete),
|
||||||
|
subtitle: Text('WARNING: This will only clear non-offline (external downloads)'),
|
||||||
|
onTap: () async {
|
||||||
|
await downloadManager.cleanDownloadHistory();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -111,3 +183,5 @@ class DownloadsScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -123,8 +123,8 @@ class _HomePageScreenState extends State<HomePageScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_load();
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -474,31 +474,62 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Downloads naming'),
|
title: Text('Downloads naming'),
|
||||||
|
subtitle: Text('Currently: ${settings.downloadFilename}'),
|
||||||
leading: Icon(Icons.text_format),
|
leading: Icon(Icons.text_format),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return SimpleDialog(
|
|
||||||
children: <Widget>[
|
TextEditingController _controller = TextEditingController();
|
||||||
ListTile(
|
String filename = settings.downloadFilename;
|
||||||
title: Text('Default naming'),
|
_controller.value = _controller.value.copyWith(text: filename);
|
||||||
subtitle: Text('01. Title'),
|
|
||||||
onTap: () {
|
//Dialog with filename format
|
||||||
settings.downloadNaming = DownloadNaming.DEFAULT;
|
return AlertDialog(
|
||||||
Navigator.of(context).pop();
|
title: Text('Downloaded tracks filename'),
|
||||||
settings.save();
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
),
|
||||||
|
Container(height: 8.0),
|
||||||
|
Text(
|
||||||
|
'Valid variables are: %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Cancel'),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Reset'),
|
||||||
|
onPressed: () {
|
||||||
|
_controller.value = _controller.value.copyWith(
|
||||||
|
text: '%artists% - %title%'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
FlatButton(
|
||||||
title: Text('Standalone naming'),
|
child: Text('Clear'),
|
||||||
subtitle: Text('Artist - Title'),
|
onPressed: () => _controller.clear(),
|
||||||
onTap: () {
|
|
||||||
settings.downloadNaming = DownloadNaming.STANDALONE;
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
settings.save();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Save'),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
settings.downloadFilename = _controller.text;
|
||||||
|
settings.save();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -506,12 +537,31 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Create download folder structure'),
|
title: Text('Create folders for artist'),
|
||||||
subtitle: Text('Artist/Album/Track'),
|
|
||||||
leading: Switch(
|
leading: Switch(
|
||||||
value: settings.downloadFolderStructure,
|
value: settings.artistFolder,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
setState(() => settings.downloadFolderStructure = v);
|
setState(() => settings.artistFolder = v);
|
||||||
|
settings.save();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('Create folders for albums'),
|
||||||
|
leading: Switch(
|
||||||
|
value: settings.albumFolder,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() => settings.albumFolder = v);
|
||||||
|
settings.save();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('Separate albums by discs'),
|
||||||
|
leading: Switch(
|
||||||
|
value: settings.albumDiscFolder,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() => settings.albumDiscFolder = v);
|
||||||
settings.save();
|
settings.save();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in New Issue