2020-09-09 18:50:15 +00:00
import ' dart:typed_data ' ;
2020-06-23 19:23:12 +00:00
import ' package:disk_space/disk_space.dart ' ;
import ' package:ext_storage/ext_storage.dart ' ;
import ' package:flutter/services.dart ' ;
import ' package:path_provider/path_provider.dart ' ;
import ' package:random_string/random_string.dart ' ;
import ' package:sqflite/sqflite.dart ' ;
import ' package:path/path.dart ' as p ;
import ' package:dio/dio.dart ' ;
import ' package:filesize/filesize.dart ' ;
import ' package:permission_handler/permission_handler.dart ' ;
import ' package:flutter_local_notifications/flutter_local_notifications.dart ' ;
import ' dart:io ' ;
import ' dart:async ' ;
import ' deezer.dart ' ;
import ' ../settings.dart ' ;
import ' definitions.dart ' ;
import ' ../ui/cached_image.dart ' ;
DownloadManager downloadManager = DownloadManager ( ) ;
MethodChannel platformChannel = const MethodChannel ( ' f.f.freezer/native ' ) ;
class DownloadManager {
Database db ;
List < Download > queue = [ ] ;
String _offlinePath ;
Future _download ;
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin ;
bool _cancelNotifications = true ;
2020-09-09 18:50:15 +00:00
bool stopped = true ;
2020-06-23 19:23:12 +00:00
Future init ( ) async {
//Prepare DB
String dir = await getDatabasesPath ( ) ;
String path = p . join ( dir , ' offline.db ' ) ;
db = await openDatabase (
path ,
version: 1 ,
onCreate: ( Database db , int version ) async {
Batch b = db . batch ( ) ;
//Create tables
b . execute ( """ CREATE TABLE downloads (
id INTEGER PRIMARY KEY AUTOINCREMENT , path TEXT , url TEXT , private INTEGER , state INTEGER , trackId TEXT ) """ );
b . execute ( """ CREATE TABLE tracks (
id TEXT PRIMARY KEY , title TEXT , album TEXT , artists TEXT , duration INTEGER , albumArt TEXT , trackNumber INTEGER , offline INTEGER , lyrics TEXT , favorite INTEGER ) """ );
b . execute ( """ CREATE TABLE albums (
id TEXT PRIMARY KEY , title TEXT , artists TEXT , tracks TEXT , art TEXT , fans INTEGER , offline INTEGER , library INTEGER ) """ );
b . execute ( """ CREATE TABLE artists (
id TEXT PRIMARY KEY , name TEXT , albums TEXT , topTracks TEXT , picture TEXT , fans INTEGER , albumCount INTEGER , offline INTEGER , library INTEGER ) """ );
b . execute ( """ CREATE TABLE playlists (
id TEXT PRIMARY KEY , title TEXT , tracks TEXT , image TEXT , duration INTEGER , userId TEXT , userName TEXT , fans INTEGER , library INTEGER , description TEXT ) """ );
await b . commit ( ) ;
}
) ;
//Prepare folders (/sdcard/Android/data/freezer/data/)
_offlinePath = p . join ( ( await getExternalStorageDirectory ( ) ) . path , ' offline/ ' ) ;
await Directory ( _offlinePath ) . create ( recursive: true ) ;
//Notifications
await _prepareNotifications ( ) ;
//Restore
List < Map > downloads = await db . rawQuery ( " SELECT * FROM downloads INNER JOIN tracks ON tracks.id = downloads.trackId WHERE downloads.state = 0 " ) ;
downloads . forEach ( ( download ) = > queue . add ( Download . fromSQL ( download , parseTrack: true ) ) ) ;
}
//Initialize flutter local notification plugin
Future _prepareNotifications ( ) async {
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin ( ) ;
AndroidInitializationSettings androidInitializationSettings = AndroidInitializationSettings ( ' @drawable/ic_logo ' ) ;
InitializationSettings initializationSettings = InitializationSettings ( androidInitializationSettings , null ) ;
await flutterLocalNotificationsPlugin . initialize ( initializationSettings ) ;
}
//Show download progress notification, if now/total = null, show intermediate
Future _startProgressNotification ( ) async {
_cancelNotifications = false ;
Timer . periodic ( Duration ( milliseconds: 500 ) , ( timer ) async {
//Cancel notifications
if ( _cancelNotifications ) {
flutterLocalNotificationsPlugin . cancel ( 10 ) ;
timer . cancel ( ) ;
return ;
}
//Not downloading
if ( this . queue . length < = 0 ) return ;
Download d = queue [ 0 ] ;
//Prepare and show notification
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails (
' download ' , ' Download ' , ' Download ' ,
importance: Importance . Default ,
priority: Priority . Default ,
showProgress: true ,
maxProgress: d . total ? ? 1 ,
progress: d . received ? ? 1 ,
playSound: false ,
enableVibration: false ,
autoCancel: true ,
//ongoing: true, //Allow dismissing
indeterminate: ( d . total = = null | | d . total = = d . received ) ,
onlyAlertOnce: true
) ;
NotificationDetails notificationDetails = NotificationDetails ( androidNotificationDetails , null ) ;
await downloadManager . flutterLocalNotificationsPlugin . show (
10 ,
' Downloading: ${ d . track . title } ' ,
( d . state = = DownloadState . POST ) ? ' Post processing... ' : ' ${ filesize ( d . received ) } / ${ filesize ( d . total ) } ( ${ queue . length } in queue) ' ,
notificationDetails
) ;
} ) ;
}
//Update queue, start new download
void updateQueue ( ) async {
2020-09-09 18:50:15 +00:00
if ( _download = = null & & queue . length > 0 & & ! stopped ) {
2020-06-23 19:23:12 +00:00
_download = queue [ 0 ] . download (
onDone: ( ) async {
//On download finished
await db . rawUpdate ( ' UPDATE downloads SET state = 1 WHERE trackId = ? ' , [ queue [ 0 ] . track . id ] ) ;
queue . removeAt ( 0 ) ;
_download = null ;
//Remove notification if no more downloads
if ( queue . length = = 0 ) {
_cancelNotifications = true ;
}
updateQueue ( ) ;
}
2020-09-01 14:41:15 +00:00
) . catchError ( ( e , st ) async {
2020-09-09 18:50:15 +00:00
if ( stopped ) return ;
2020-06-23 19:23:12 +00:00
_cancelNotifications = true ;
2020-09-22 17:13:54 +00:00
//Deezer error - track is unavailable
if ( queue [ 0 ] . state = = DownloadState . DEEZER_ERROR ) {
await db . rawUpdate ( ' UPDATE downloads SET state = 4 WHERE trackId = ? ' , [ queue [ 0 ] . track . id ] ) ;
queue . removeAt ( 0 ) ;
_cancelNotifications = false ;
_download = null ;
updateQueue ( ) ;
return ;
}
//Clean
_download = null ;
stopped = true ;
print ( ' Download error: $ e \n $ st ' ) ;
2020-09-18 17:36:41 +00:00
queue [ 0 ] . state = DownloadState . NONE ;
//Shift to end
queue . add ( queue [ 0 ] ) ;
queue . removeAt ( 0 ) ;
//Show error
2020-06-23 19:23:12 +00:00
await _showError ( ) ;
} ) ;
//Show download progress notifications
if ( _cancelNotifications = = null | | _cancelNotifications ) _startProgressNotification ( ) ;
}
}
2020-09-09 18:50:15 +00:00
//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 ( ) ;
}
2020-06-23 19:23:12 +00:00
//Show error notification
Future _showError ( ) async {
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails (
' downloadError ' , ' Download Error ' , ' Download Error '
) ;
NotificationDetails notificationDetails = NotificationDetails ( androidNotificationDetails , null ) ;
flutterLocalNotificationsPlugin . show (
11 , ' Error while downloading! ' , ' Please restart downloads in the library ' , notificationDetails
) ;
}
//Returns all offline tracks
Future < List < Track > > allOfflineTracks ( ) async {
List data = await db . query ( ' tracks ' , where: ' offline == 1 ' ) ;
List < Track > tracks = [ ] ;
//Load track data
for ( var t in data ) {
tracks . add ( await getTrack ( t [ ' id ' ] ) ) ;
}
return tracks ;
}
//Get all offline playlists
Future < List < Playlist > > getOfflinePlaylists ( ) async {
List data = await db . query ( ' playlists ' ) ;
List < Playlist > playlists = [ ] ;
//Load playlists
for ( var p in data ) {
playlists . add ( await getPlaylist ( p [ ' id ' ] ) ) ;
}
return playlists ;
}
//Get playlist metadata with tracks
Future < Playlist > getPlaylist ( String id ) async {
if ( id = = null ) return null ;
List data = await db . query ( ' playlists ' , where: ' id == ? ' , whereArgs: [ id ] ) ;
if ( data . length = = 0 ) return null ;
//Load playlist tracks
Playlist p = Playlist . fromSQL ( data [ 0 ] ) ;
for ( int i = 0 ; i < p . tracks . length ; i + + ) {
p . tracks [ i ] = await getTrack ( p . tracks [ i ] . id ) ;
}
return p ;
}
//Gets favorites
Future < Playlist > getFavorites ( ) async {
return await getPlaylist ( ' FAVORITES ' ) ;
}
Future < List < Album > > getOfflineAlbums ( { List albumsData } ) async {
//Load albums
if ( albumsData = = null ) {
albumsData = await db . query ( ' albums ' , where: ' offline == 1 ' ) ;
}
List < Album > albums = albumsData . map ( ( alb ) = > Album . fromSQL ( alb ) ) . toList ( ) ;
for ( int i = 0 ; i < albums . length ; i + + ) {
albums [ i ] . library = true ;
//Load tracks
for ( int j = 0 ; j < albums [ i ] . tracks . length ; j + + ) {
albums [ i ] . tracks [ j ] = await getTrack ( albums [ i ] . tracks [ j ] . id , album: albums [ i ] ) ;
}
//Load artists
List artistsData = await db . rawQuery ( ' SELECT * FROM artists WHERE id IN ( ${ albumsData [ i ] [ ' artists ' ] } ) ' ) ;
albums [ i ] . artists = artistsData . map < Artist > ( ( a ) = > Artist . fromSQL ( a ) ) . toList ( ) ;
}
return albums ;
}
//Get track with metadata from db
Future < Track > getTrack ( String id , { Album album , List < Artist > artists } ) async {
List tracks = await db . query ( ' tracks ' , where: ' id == ? ' , whereArgs: [ id ] ) ;
if ( tracks . length = = 0 ) return null ;
Track t = Track . fromSQL ( tracks [ 0 ] ) ;
//Load album from DB
t . album = album ? ? Album . fromSQL ( ( await db . query ( ' albums ' , where: ' id == ? ' , whereArgs: [ t . album . id ] ) ) [ 0 ] ) ;
if ( artists ! = null ) {
t . artists = artists ;
return t ;
}
//Load artists from DB
for ( int i = 0 ; i < t . artists . length ; i + + ) {
t . artists [ i ] = Artist . fromSQL (
( await db . query ( ' artists ' , where: ' id == ? ' , whereArgs: [ t . artists [ i ] . id ] ) ) [ 0 ] ) ;
}
return t ;
}
Future removeOfflineTrack ( String id ) async {
//Check if track present in albums
List counter = await db . rawQuery ( ' SELECT COUNT(*) FROM albums WHERE tracks LIKE "% $ id %" ' ) ;
if ( counter [ 0 ] [ ' COUNT(*) ' ] > 0 ) return ;
//and in playlists
counter = await db . rawQuery ( ' SELECT COUNT(*) FROM playlists WHERE tracks LIKE "% $ id %" ' ) ;
if ( counter [ 0 ] [ ' COUNT(*) ' ] > 0 ) return ;
//Remove file
List download = await db . query ( ' downloads ' , where: ' trackId == ? ' , whereArgs: [ id ] ) ;
await File ( download [ 0 ] [ ' path ' ] ) . delete ( ) ;
//Delete from db
await db . delete ( ' tracks ' , where: ' id == ? ' , whereArgs: [ id ] ) ;
await db . delete ( ' downloads ' , where: ' trackId == ? ' , whereArgs: [ id ] ) ;
}
//Delete offline album
Future removeOfflineAlbum ( String id ) async {
List data = await db . rawQuery ( ' SELECT * FROM albums WHERE id == ? AND offline == 1 ' , [ id ] ) ;
if ( data . length = = 0 ) return ;
Map < String , dynamic > album = Map . from ( data [ 0 ] ) ; //make writable
//Remove DB
album [ ' offline ' ] = 0 ;
await db . update ( ' albums ' , album , where: ' id == ? ' , whereArgs: [ id ] ) ;
//Get track ids
List < String > tracks = album [ ' tracks ' ] . split ( ' , ' ) ;
for ( String t in tracks ) {
//Remove tracks
await removeOfflineTrack ( t ) ;
}
}
Future removeOfflinePlaylist ( String id ) async {
List data = await db . query ( ' playlists ' , where: ' id == ? ' , whereArgs: [ id ] ) ;
if ( data . length = = 0 ) return ;
Playlist p = Playlist . fromSQL ( data [ 0 ] ) ;
//Remove db
await db . delete ( ' playlists ' , where: ' id == ? ' , whereArgs: [ id ] ) ;
//Remove tracks
for ( Track t in p . tracks ) {
await removeOfflineTrack ( t . id ) ;
}
}
//Get path to offline track
Future < String > getOfflineTrackPath ( String id ) async {
List < Map > tracks = await db . rawQuery ( ' SELECT path FROM downloads WHERE state == 1 AND trackId == ? ' , [ id ] ) ;
if ( tracks . length < 1 ) {
return null ;
}
Download d = Download . fromSQL ( tracks [ 0 ] ) ;
return d . path ;
}
2020-09-09 18:50:15 +00:00
Future addOfflineTrack ( Track track , { private = true , forceStart = true } ) async {
2020-06-23 19:23:12 +00:00
//Paths
String path = p . join ( _offlinePath , track . id ) ;
if ( track . playbackDetails = = null ) {
//Get track from API if download info missing
track = await deezerAPI . track ( track . id ) ;
}
if ( ! private ) {
//Check permissions
if ( ! ( await Permission . storage . request ( ) . isGranted ) ) {
return ;
}
//If saving to external
//Save just extension to path, will be generated before download
path = ' mp3 ' ;
if ( settings . downloadQuality = = AudioQuality . FLAC ) {
path = ' flac ' ;
}
2020-09-09 18:50:15 +00:00
} else {
//Load lyrics for private
try {
Lyrics l = await deezerAPI . lyrics ( track . id ) ;
track . lyrics = l ;
} catch ( e ) { }
2020-06-23 19:23:12 +00:00
}
2020-09-18 17:36:41 +00:00
Download download = Download ( track: track , path: path , private: private ) ;
2020-06-23 19:23:12 +00:00
//Database
Batch b = db . batch ( ) ;
b . insert ( ' downloads ' , download . toSQL ( ) ) ;
b . insert ( ' tracks ' , track . toSQL ( off: false ) , conflictAlgorithm: ConflictAlgorithm . ignore ) ;
if ( private ) {
//Duplicate check
List < Map > duplicate = await db . rawQuery ( ' SELECT * FROM downloads WHERE trackId == ? ' , [ track . id ] ) ;
if ( duplicate . length ! = 0 ) return ;
//Save art
2020-08-13 17:39:22 +00:00
//await imagesDatabase.getImage(track.albumArt.full);
imagesDatabase . saveImage ( track . albumArt . full ) ;
2020-06-23 19:23:12 +00:00
//Save to db
b . insert ( ' tracks ' , track . toSQL ( off: true ) , conflictAlgorithm: ConflictAlgorithm . replace ) ;
b . insert ( ' albums ' , track . album . toSQL ( ) , conflictAlgorithm: ConflictAlgorithm . ignore ) ;
track . artists . forEach ( ( art ) = > b . insert ( ' artists ' , art . toSQL ( ) , conflictAlgorithm: ConflictAlgorithm . ignore ) ) ;
}
await b . commit ( ) ;
queue . add ( download ) ;
2020-09-09 18:50:15 +00:00
if ( forceStart ) start ( ) ;
2020-06-23 19:23:12 +00:00
}
Future addOfflineAlbum ( Album album , { private = true } ) async {
//Get full album from API if tracks are missing
if ( album . tracks = = null | | album . tracks . length = = 0 ) {
album = await deezerAPI . album ( album . id ) ;
}
//Update album in database
if ( private ) {
await db . insert ( ' albums ' , album . toSQL ( off: true ) , conflictAlgorithm: ConflictAlgorithm . replace ) ;
}
//Save all tracks
for ( Track track in album . tracks ) {
2020-09-09 18:50:15 +00:00
await addOfflineTrack ( track , private: private , forceStart: false ) ;
2020-06-23 19:23:12 +00:00
}
2020-09-09 18:50:15 +00:00
start ( ) ;
2020-06-23 19:23:12 +00:00
}
//Add offline playlist, can be also used as update
Future addOfflinePlaylist ( Playlist playlist , { private = true } ) async {
//Load full playlist if missing tracks
if ( playlist . tracks = = null | | playlist . tracks . length ! = playlist . trackCount ) {
playlist = await deezerAPI . fullPlaylist ( playlist . id ) ;
}
playlist . library = true ;
//To DB
if ( private ) {
await db . insert ( ' playlists ' , playlist . toSQL ( ) , conflictAlgorithm: ConflictAlgorithm . replace ) ;
}
//Download all tracks
for ( Track t in playlist . tracks ) {
2020-09-09 18:50:15 +00:00
await addOfflineTrack ( t , private: private , forceStart: false ) ;
2020-06-23 19:23:12 +00:00
}
2020-09-09 18:50:15 +00:00
start ( ) ;
2020-06-23 19:23:12 +00:00
}
Future checkOffline ( { Album album , Track track , Playlist playlist } ) async {
//Check if album/track (TODO: Artist, playlist) is offline
if ( track ! = null ) {
List res = await db . query ( ' tracks ' , where: ' id == ? AND offline == 1 ' , whereArgs: [ track . id ] ) ;
if ( res . length = = 0 ) return false ;
return true ;
}
if ( album ! = null ) {
List res = await db . query ( ' albums ' , where: ' id == ? AND offline == 1 ' , whereArgs: [ album . id ] ) ;
if ( res . length = = 0 ) return false ;
return true ;
}
if ( playlist ! = null & & playlist . id ! = null ) {
List res = await db . query ( ' playlists ' , where: ' id == ? ' , whereArgs: [ playlist . id ] ) ;
if ( res . length = = 0 ) return false ;
return true ;
}
return false ;
}
//Offline search
Future < SearchResults > search ( String query ) async {
SearchResults results = SearchResults (
tracks: [ ] ,
albums: [ ] ,
artists: [ ] ,
playlists: [ ]
) ;
//Tracks
List tracksData = await db . rawQuery ( ' SELECT * FROM tracks WHERE offline == 1 AND title like "% $ query %" ' ) ;
for ( Map trackData in tracksData ) {
results . tracks . add ( await getTrack ( trackData [ ' id ' ] ) ) ;
}
//Albums
List albumsData = await db . rawQuery ( ' SELECT * FROM albums WHERE offline == 1 AND title like "% $ query %" ' ) ;
results . albums = await getOfflineAlbums ( albumsData: albumsData ) ;
//Artists
//TODO: offline artists
//Playlists
List playlists = await db . rawQuery ( ' SELECT * FROM playlists WHERE title like "% $ query %" ' ) ;
for ( Map playlist in playlists ) {
results . playlists . add ( await getPlaylist ( playlist [ ' id ' ] ) ) ;
}
return results ;
}
Future < List < Download > > getFinishedDownloads ( ) async {
//Fetch from db
2020-09-22 17:13:54 +00:00
List < Map > data = await db . rawQuery ( " SELECT * FROM downloads INNER JOIN tracks ON tracks.id = downloads.trackId WHERE downloads.state = 1 OR downloads.state > 3 " ) ;
2020-06-23 19:23:12 +00:00
List < Download > downloads = data . map < Download > ( ( d ) = > Download . fromSQL ( d , parseTrack: true ) ) . toList ( ) ;
return downloads ;
}
//Get stats for library screen
Future < List < String > > getStats ( ) async {
//Get offline counts
int trackCount = ( await db . rawQuery ( ' SELECT COUNT(*) FROM tracks WHERE offline == 1 ' ) ) [ 0 ] [ ' COUNT(*) ' ] ;
int albumCount = ( await db . rawQuery ( ' SELECT COUNT(*) FROM albums WHERE offline == 1 ' ) ) [ 0 ] [ ' COUNT(*) ' ] ;
int playlistCount = ( await db . rawQuery ( ' SELECT COUNT(*) FROM albums WHERE offline == 1 ' ) ) [ 0 ] [ ' COUNT(*) ' ] ;
//Free space
double diskSpace = await DiskSpace . getFreeDiskSpace ;
//Used space
List < FileSystemEntity > offlineStat = await Directory ( _offlinePath ) . list ( ) . toList ( ) ;
int offlineSize = 0 ;
for ( var fs in offlineStat ) {
offlineSize + = ( await fs . stat ( ) ) . size ;
}
//Return as a list, maybe refactor in future if feature stays
return ( [
trackCount . toString ( ) ,
albumCount . toString ( ) ,
playlistCount . toString ( ) ,
filesize ( offlineSize ) ,
filesize ( ( diskSpace * 1000000 ) . floor ( ) )
] ) ;
}
2020-09-01 14:41:15 +00:00
//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 {
2020-09-09 18:50:15 +00:00
while ( queue . length > 0 ) {
if ( queue . length = = 1 ) {
if ( _download ! = null ) break ;
await removeDownload ( queue [ 0 ] ) ;
return ;
}
await removeDownload ( queue [ 1 ] ) ;
2020-09-01 14:41:15 +00:00
}
}
//Remove non-private downloads
Future cleanDownloadHistory ( ) async {
await db . delete ( ' downloads ' , where: ' private == 0 ' ) ;
}
2020-06-23 19:23:12 +00:00
}
class Download {
Track track ;
String path ;
String url ;
bool private ;
DownloadState state ;
String _cover ;
2020-09-09 18:50:15 +00:00
//For canceling
IOSink _outSink ;
CancelToken _cancel ;
StreamSubscription _progressSub ;
2020-06-23 19:23:12 +00:00
int received = 0 ;
int total = 1 ;
Download ( { this . track , this . path , this . url , this . private , this . state = DownloadState . NONE } ) ;
2020-09-09 18:50:15 +00:00
//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 ;
}
2020-06-23 19:23:12 +00:00
Future download ( { onDone } ) async {
Dio dio = Dio ( ) ;
//TODO: Check for internet before downloading
2020-09-18 17:36:41 +00:00
Map rawTrackPublic = { } ;
Map rawAlbumPublic = { } ;
2020-09-09 18:50:15 +00:00
if ( ! this . private & & ! ( this . path . endsWith ( ' .mp3 ' ) | | this . path . endsWith ( ' .flac ' ) ) ) {
2020-06-23 19:23:12 +00:00
String ext = this . path ;
//Get track details
2020-09-09 18:50:15 +00:00
Map _rawTrackData = await deezerAPI . callApi ( ' song.getListData ' , params: { ' sng_ids ' : [ track . id ] } ) ;
Map rawTrack = _rawTrackData [ ' results ' ] [ ' data ' ] [ 0 ] ;
2020-09-01 14:41:15 +00:00
this . track = Track . fromPrivateJson ( rawTrack ) ;
2020-09-18 17:36:41 +00:00
//RAW Public API call (for genre and other tags)
try { rawTrackPublic = await deezerAPI . callPublicApi ( ' track/ ${ this . track . id } ' ) ; } catch ( e ) { rawTrackPublic = { } ; }
try { rawAlbumPublic = await deezerAPI . callPublicApi ( ' album/ ${ this . track . album . id } ' ) ; } catch ( e ) { rawAlbumPublic = { } ; }
2020-09-01 14:41:15 +00:00
2020-09-22 17:13:54 +00:00
//Global block check
if ( rawTrackPublic [ ' available_countries ' ] ! = null & & rawTrackPublic [ ' available_countries ' ] . length = = 0 ) {
this . state = DownloadState . DEEZER_ERROR ;
throw Exception ( ' Download error - not on Deezer ' ) ;
}
2020-06-23 19:23:12 +00:00
//Get path if public
RegExp sanitize = RegExp ( r'[\/\\\?\%\*\:\|\"\<\>]' ) ;
//Download path
2020-09-01 14:41:15 +00:00
this . path = settings . downloadPath ? ? ( await ExtStorage . getExternalStoragePublicDirectory ( ExtStorage . DIRECTORY_MUSIC ) ) ;
if ( settings . artistFolder )
this . path = p . join ( this . path , track . artists [ 0 ] . name . replaceAll ( sanitize , ' ' ) ) ;
if ( settings . albumFolder ) {
String folderName = track . album . title . replaceAll ( sanitize , ' ' ) ;
//Add disk number
2020-09-03 19:17:53 +00:00
if ( settings . albumDiscFolder ) folderName + = ' - Disk ${ track . diskNumber } ' ;
2020-09-01 14:41:15 +00:00
this . path = p . join ( this . path , folderName ) ;
2020-06-23 19:23:12 +00:00
}
//Make dirs
await Directory ( this . path ) . create ( recursive: true ) ;
//Grab cover
_cover = p . join ( this . path , ' cover.jpg ' ) ;
2020-09-01 14:41:15 +00:00
if ( ! settings . albumFolder ) _cover = p . join ( this . path , randomAlpha ( 12 ) + ' _cover.jpg ' ) ;
2020-06-23 19:23:12 +00:00
if ( ! await File ( _cover ) . exists ( ) ) {
try {
await dio . download (
this . track . albumArt . full ,
_cover ,
) ;
} catch ( e ) { print ( ' Error downloading cover ' ) ; }
}
2020-09-01 14:41:15 +00:00
//Create filename
String _filename = settings . downloadFilename ;
2020-09-09 18:50:15 +00:00
//Feats filter
String feats = ' ' ;
if ( track . artists . length > 1 ) feats = " feat. ${ track . artists . sublist ( 1 ) . map ( ( a ) = > a . name ) . join ( ' , ' ) } " ;
2020-09-01 14:41:15 +00:00
//Filters
Map < String , String > vars = {
' %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 ( ) ,
2020-09-09 18:50:15 +00:00
' %0trackNumber% ' : track . trackNumber . toString ( ) . padLeft ( 2 , ' 0 ' ) ,
' %feats% ' : feats
2020-09-01 14:41:15 +00:00
} ;
//Replace
vars . forEach ( ( key , value ) {
_filename = _filename . replaceAll ( key , value ) ;
} ) ;
_filename + = ' . $ ext ' ;
2020-06-23 19:23:12 +00:00
this . path = p . join ( this . path , _filename ) ;
}
2020-09-13 14:06:12 +00:00
//Check if file exists
if ( await File ( this . path ) . exists ( ) & & ! settings . overwriteDownload ) {
this . state = DownloadState . DONE ;
onDone ( ) ;
return ;
}
2020-06-23 19:23:12 +00:00
//Download
this . state = DownloadState . DOWNLOADING ;
2020-09-22 17:13:54 +00:00
//Quality fallback
if ( this . url = = null )
await _fallback ( ) ;
2020-09-09 18:50:15 +00:00
//Create download file
File downloadFile = File ( this . path + ' .ENC ' ) ;
//Get start position
int start = 0 ;
if ( await downloadFile . exists ( ) ) {
FileStat stat = await downloadFile . stat ( ) ;
start = stat . size ;
} else {
2020-09-22 17:13:54 +00:00
//Create file if doesn't exist
2020-09-09 18:50:15 +00:00
await downloadFile . create ( recursive: true ) ;
}
2020-09-18 17:36:41 +00:00
2020-09-09 18:50:15 +00:00
//Download
_cancel = CancelToken ( ) ;
2020-09-22 17:13:54 +00:00
Response response ;
try {
response = await dio . get (
this . url ,
options: Options (
responseType: ResponseType . stream ,
headers: {
' Range ' : ' bytes= $ start - '
} ,
) ,
cancelToken: _cancel
) ;
} on DioError catch ( e ) {
//Deezer fetch error
if ( e . response . statusCode = = 403 | | e . response . statusCode = = 404 ) {
this . state = DownloadState . DEEZER_ERROR ;
}
throw Exception ( ' Download error - Deezer blocked. ' ) ;
}
2020-09-09 18:50:15 +00:00
//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 ;
2020-06-23 19:23:12 +00:00
this . state = DownloadState . POST ;
//Decrypt
await platformChannel . invokeMethod ( ' decryptTrack ' , { ' id ' : track . id , ' path ' : path } ) ;
//Tag
if ( ! private ) {
//Tag track in native
2020-09-18 17:36:41 +00:00
String year ;
if ( rawTrackPublic [ ' release_date ' ] ! = null & & rawTrackPublic [ ' release_date ' ] . length > = 4 )
year = rawTrackPublic [ ' release_date ' ] . substring ( 0 , 4 ) ;
2020-06-23 19:23:12 +00:00
await platformChannel . invokeMethod ( ' tagTrack ' , {
' path ' : path ,
' title ' : track . title ,
' album ' : track . album . title ,
' artists ' : track . artistString ,
' artist ' : track . artists [ 0 ] . name ,
' cover ' : _cover ,
2020-09-18 17:36:41 +00:00
' trackNumber ' : track . trackNumber ,
' diskNumber ' : track . diskNumber ,
' genres ' : ( ( rawAlbumPublic [ ' genres ' ] ? ? { } ) [ ' data ' ] ? ? [ ] ) . map ( ( g ) = > g [ ' name ' ] ) . toList ( ) ,
' year ' : year ,
' bpm ' : rawTrackPublic [ ' bpm ' ] ,
' explicit ' : ( track . explicit ? ? false ) ? " 1 " : " 0 " ,
' label ' : rawAlbumPublic [ ' label ' ] ,
' albumTracks ' : rawAlbumPublic [ ' nb_tracks ' ] ,
' date ' : rawTrackPublic [ ' release_date ' ] ,
' albumArtist ' : ( rawAlbumPublic [ ' artist ' ] ? ? { } ) [ ' name ' ]
2020-06-23 19:23:12 +00:00
} ) ;
2020-07-19 12:41:05 +00:00
//Rescan android library
await platformChannel . invokeMethod ( ' rescanLibrary ' , {
' path ' : path
} ) ;
2020-06-23 19:23:12 +00:00
}
//Remove encrypted
await File ( path + ' .ENC ' ) . delete ( ) ;
2020-09-01 14:41:15 +00:00
if ( ! settings . albumFolder ) await File ( _cover ) . delete ( ) ;
2020-09-09 18:50:15 +00:00
//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 ) ;
}
2020-06-23 19:23:12 +00:00
this . state = DownloadState . DONE ;
onDone ( ) ;
return ;
}
2020-09-18 17:36:41 +00:00
Future _fallback ( { fallback } ) async {
//Get quality
AudioQuality quality = private ? settings . offlineQuality : settings . downloadQuality ;
if ( fallback = = AudioQuality . MP3_320 ) quality = AudioQuality . MP3_128 ;
if ( fallback = = AudioQuality . FLAC ) {
quality = AudioQuality . MP3_320 ;
if ( this . path . toLowerCase ( ) . endsWith ( ' flac ' ) )
this . path = this . path . substring ( 0 , this . path . length - 4 ) + ' mp3 ' ;
}
//No more fallback
if ( quality = = AudioQuality . MP3_128 ) {
url = track . getUrl ( settings . getQualityInt ( quality ) ) ;
return ;
}
//Check
int q = settings . getQualityInt ( quality ) ;
try {
Response res = await Dio ( ) . head ( track . getUrl ( q ) ) ;
if ( res . statusCode = = 200 | | res . statusCode = = 206 ) {
this . url = track . getUrl ( q ) ;
return ;
}
} catch ( e ) { }
//Fallback
return _fallback ( fallback: quality ) ;
}
2020-06-23 19:23:12 +00:00
//JSON
Map < String , dynamic > toSQL ( ) = > {
' trackId ' : track . id ,
' path ' : path ,
' url ' : url ,
2020-09-22 17:13:54 +00:00
' state ' : state . index ,
2020-06-23 19:23:12 +00:00
' private ' : private ? 1 : 0
} ;
factory Download . fromSQL ( Map < String , dynamic > data , { parseTrack = false } ) = > Download (
track: parseTrack ? Track . fromSQL ( data ) : Track ( id: data [ ' trackId ' ] ) ,
path: data [ ' path ' ] ,
url: data [ ' url ' ] ,
2020-09-22 17:13:54 +00:00
state: DownloadState . values [ data [ ' state ' ] ] ,
2020-06-23 19:23:12 +00:00
private: data [ ' private ' ] = = 1
) ;
}
enum DownloadState {
NONE ,
2020-09-22 17:13:54 +00:00
DONE ,
2020-06-23 19:23:12 +00:00
DOWNLOADING ,
POST ,
2020-09-22 17:13:54 +00:00
DEEZER_ERROR ,
ERROR
2020-06-23 19:23:12 +00:00
}