Download naming changes

This commit is contained in:
exttex 2020-09-01 16:41:15 +02:00
parent 37f97f9992
commit 4e5e3a3059
8 changed files with 260 additions and 104 deletions

View File

@ -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;

View File

@ -186,7 +186,7 @@ class _MainScreenState extends State<MainScreen> {
body: AudioServiceWidget( body: AudioServiceWidget(
child: CustomNavigator( child: CustomNavigator(
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
home: _screens[_selected], home: _screens[_selected],
pageRoute: PageRoutes.materialPageRoute, pageRoute: PageRoutes.materialPageRoute,
), ),
) )

View File

@ -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,
}

View File

@ -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',

View File

@ -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,54 +89,99 @@ 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: [
body: ListView( IconButton(
children: <Widget>[ icon: Icon(Icons.delete_sweep),
StreamBuilder( onPressed: () {
stream: Stream.periodic(Duration(milliseconds: 500)), //Periodic to get current download progress showDialog(
builder: (BuildContext context, AsyncSnapshot snapshot) { 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(
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) if (downloadManager.queue.length == 0)
return Container(width: 0, height: 0,); return Container(width: 0, height: 0,);
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(() => {}));
}) })
); );
}, },
), ),
FutureBuilder( FutureBuilder(
future: downloadManager.getFinishedDownloads(), future: downloadManager.getFinishedDownloads(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data.length == 0) return Container(height: 0, width: 0,); if (!snapshot.hasData || snapshot.data.length == 0) return Container(height: 0, width: 0,);
return Column( return Column(
children: <Widget>[ children: <Widget>[
Divider(), Divider(),
Text( Text(
'History', 'History',
style: TextStyle( style: TextStyle(
fontSize: 24.0, fontSize: 24.0,
fontWeight: FontWeight.bold fontWeight: FontWeight.bold
),
), ),
), ...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(() {});
},
),
],
);
},
)
],
)
); );
} }
} }

View File

@ -123,8 +123,8 @@ class _HomePageScreenState extends State<HomePageScreen> {
@override @override
void initState() { void initState() {
_load();
super.initState(); super.initState();
_load();
} }
@override @override

View File

@ -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();
}, },
), ),