747 lines
24 KiB
Dart
747 lines
24 KiB
Dart
import 'package:sqflite/sqflite.dart';
|
|
import 'package:disk_space_plus/disk_space_plus.dart';
|
|
import 'package:filesize/filesize.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
import 'package:fluttertoast/fluttertoast.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/definitions.dart';
|
|
import 'package:freezer/settings.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:freezer/translations.i18n.dart';
|
|
|
|
import 'dart:io';
|
|
import 'dart:async';
|
|
|
|
final downloadManager = DownloadManager();
|
|
|
|
class DownloadManager {
|
|
// DownloadManager currently only supports android
|
|
static bool get isSupported => Platform.isAndroid;
|
|
|
|
|
|
//Platform channels
|
|
static MethodChannel platform = const MethodChannel('f.f.freezer/native');
|
|
static EventChannel eventChannel =
|
|
const EventChannel('f.f.freezer/downloads');
|
|
|
|
bool running = false;
|
|
int? queueSize = 0;
|
|
|
|
StreamController serviceEvents = StreamController.broadcast();
|
|
late String offlinePath;
|
|
late Database db;
|
|
|
|
//Start/Resume downloads
|
|
Future start() async {
|
|
if (!Platform.isAndroid) return;
|
|
|
|
//Returns whether service is bound or not, the delay is really shitty/hacky way, until i find a real solution
|
|
await updateServiceSettings();
|
|
await platform.invokeMethod('start');
|
|
}
|
|
|
|
//Stop/Pause downloads
|
|
Future stop() async {
|
|
if (!Platform.isAndroid) return;
|
|
|
|
await platform.invokeMethod('stop');
|
|
}
|
|
|
|
Future init() async {
|
|
if (!Platform.isAndroid) return;
|
|
//Remove old DB
|
|
File oldDbFile = File(p.join((await getDatabasesPath()), 'offline.db'));
|
|
if (await oldDbFile.exists()) {
|
|
await oldDbFile.delete();
|
|
}
|
|
|
|
String dbPath = p.join((await getDatabasesPath()), 'offline2.db');
|
|
//Open db
|
|
db = await openDatabase(dbPath, version: 1,
|
|
onCreate: (Database db, int version) async {
|
|
Batch b = db.batch();
|
|
//Create tables, if doesn't exit
|
|
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, diskNumber INTEGER, explicit INTEGER)""");
|
|
b.execute("""CREATE TABLE Albums (
|
|
id TEXT PRIMARY KEY, title TEXT, artists TEXT, tracks TEXT, art TEXT, fans INTEGER, offline INTEGER, library INTEGER, type INTEGER, releaseDate TEXT)""");
|
|
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, radio 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();
|
|
});
|
|
|
|
//Create offline directory
|
|
offlinePath =
|
|
p.join((await getExternalStorageDirectory())!.path, 'offline/');
|
|
await Directory(offlinePath).create(recursive: true);
|
|
|
|
//Update settings
|
|
await updateServiceSettings();
|
|
|
|
//Listen to state change event
|
|
eventChannel.receiveBroadcastStream().listen((e) {
|
|
if (e['action'] == 'onStateChange') {
|
|
running = e['running'];
|
|
queueSize = e['queueSize'];
|
|
}
|
|
|
|
//Forward
|
|
serviceEvents.add(e);
|
|
});
|
|
|
|
await platform.invokeMethod('loadDownloads');
|
|
}
|
|
|
|
//Get all downloads from db
|
|
Future<List<Download>> getDownloads() async {
|
|
if (!Platform.isAndroid) return [];
|
|
|
|
List raw = await platform.invokeMethod('getDownloads');
|
|
return raw.map((d) => Download.fromJson(d)).toList();
|
|
}
|
|
|
|
//Insert track and metadata to DB
|
|
Future<Batch> _addTrackToDB(
|
|
Batch batch, Track track, bool overwriteTrack) async {
|
|
batch.insert('Tracks', track.toSQL(off: true),
|
|
conflictAlgorithm: overwriteTrack
|
|
? ConflictAlgorithm.replace
|
|
: ConflictAlgorithm.ignore);
|
|
batch.insert('Albums', track.album!.toSQL(off: false),
|
|
conflictAlgorithm: ConflictAlgorithm.ignore);
|
|
//Artists
|
|
for (Artist a in track.artists!) {
|
|
batch.insert('Artists', a.toSQL(off: false),
|
|
conflictAlgorithm: ConflictAlgorithm.ignore);
|
|
}
|
|
return batch;
|
|
}
|
|
|
|
//Quality selector for custom quality
|
|
Future<AudioQuality?> qualitySelect(BuildContext context) {
|
|
return showModalBottomSheet<AudioQuality>(
|
|
context: context,
|
|
builder: (context) {
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(0, 12, 0, 2),
|
|
child: Text(
|
|
'Quality'.i18n,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold, fontSize: 20.0),
|
|
),
|
|
),
|
|
ListTile(
|
|
title: const Text('MP3 128kbps'),
|
|
onTap: () => Navigator.pop(context, AudioQuality.MP3_128),
|
|
),
|
|
ListTile(
|
|
title: const Text('MP3 320kbps'),
|
|
onTap: () => Navigator.pop(context, AudioQuality.MP3_320),
|
|
),
|
|
ListTile(
|
|
title: const Text('FLAC'),
|
|
onTap: () => Navigator.pop(context, AudioQuality.FLAC),
|
|
)
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
Future<bool> addOfflineTrack(Track track,
|
|
{private = true, BuildContext? context, isSingleton = false}) async {
|
|
if (!Platform.isAndroid) return false;
|
|
//Permission
|
|
if (!private && !(await checkPermission())) return false;
|
|
|
|
//Ask for quality
|
|
AudioQuality? quality;
|
|
if (!private && settings.downloadQuality == AudioQuality.ASK) {
|
|
quality = await qualitySelect(context!);
|
|
if (quality == null) return false;
|
|
}
|
|
|
|
//Fetch track if missing meta
|
|
if (track.artists == null ||
|
|
track.artists!.isEmpty ||
|
|
track.album == null) {
|
|
track = await deezerAPI.track(track.id);
|
|
}
|
|
|
|
//Add to DB
|
|
if (private) {
|
|
Batch b = db.batch();
|
|
b = await _addTrackToDB(b, track, true);
|
|
await b.commit();
|
|
|
|
//Cache art
|
|
DefaultCacheManager().getSingleFile(track.albumArt!.thumb);
|
|
DefaultCacheManager().getSingleFile(track.albumArt!.full);
|
|
}
|
|
|
|
//Get path
|
|
String? path = _generatePath(track, private, isSingleton: isSingleton);
|
|
await platform.invokeMethod('addDownloads', [
|
|
await Download.jsonFromTrack(track, path,
|
|
private: private, quality: quality)
|
|
]);
|
|
await start();
|
|
return true;
|
|
}
|
|
|
|
Future addOfflineAlbum(Album? album,
|
|
{private = true, BuildContext? context}) async {
|
|
//Permission
|
|
if (!private && !(await checkPermission())) return;
|
|
|
|
//Ask for quality
|
|
AudioQuality? quality;
|
|
if (!private && settings.downloadQuality == AudioQuality.ASK) {
|
|
quality = await qualitySelect(context!);
|
|
if (quality == null) return false;
|
|
}
|
|
|
|
//Get from API if no tracks
|
|
if (album!.tracks == null || album.tracks!.isEmpty) {
|
|
album = await deezerAPI.album(album.id);
|
|
}
|
|
|
|
//Add to DB
|
|
if (private) {
|
|
//Cache art
|
|
DefaultCacheManager().getSingleFile(album.art!.thumb);
|
|
DefaultCacheManager().getSingleFile(album.art!.full);
|
|
|
|
Batch b = db.batch();
|
|
b.insert('Albums', album.toSQL(off: true),
|
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
|
for (Track? t in album.tracks!) {
|
|
b = await (_addTrackToDB(b, t!, false));
|
|
}
|
|
await b.commit();
|
|
}
|
|
|
|
//Create downloads
|
|
List<Map> out = [];
|
|
for (Track? t in album.tracks!) {
|
|
out.add(await Download.jsonFromTrack(t!, _generatePath(t, private),
|
|
private: private, quality: quality));
|
|
}
|
|
await platform.invokeMethod('addDownloads', out);
|
|
await start();
|
|
}
|
|
|
|
Future addOfflinePlaylist(Playlist? playlist,
|
|
{private = true, BuildContext? context, AudioQuality? quality}) async {
|
|
if (!Platform.isAndroid) return false;
|
|
|
|
//Permission
|
|
if (!private && !(await checkPermission())) return;
|
|
|
|
//Ask for quality
|
|
if (!private &&
|
|
settings.downloadQuality == AudioQuality.ASK &&
|
|
quality == null) {
|
|
quality = await qualitySelect(context!);
|
|
if (quality == null) return false;
|
|
}
|
|
|
|
//Get tracks if missing
|
|
if (playlist!.tracks == null ||
|
|
playlist.tracks!.length < playlist.trackCount!) {
|
|
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
|
}
|
|
|
|
//Add to DB
|
|
if (private) {
|
|
Batch b = db.batch();
|
|
b.insert('Playlists', playlist.toSQL(),
|
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
|
for (Track? t in playlist.tracks!) {
|
|
b = await _addTrackToDB(b, t!, false);
|
|
//Cache art
|
|
DefaultCacheManager().getSingleFile(t.albumArt!.thumb);
|
|
DefaultCacheManager().getSingleFile(t.albumArt!.full);
|
|
}
|
|
await b.commit();
|
|
}
|
|
|
|
//Generate downloads
|
|
List<Map> out = [];
|
|
for (int i = 0; i < playlist.tracks!.length; i++) {
|
|
Track t = playlist.tracks![i];
|
|
out.add(await Download.jsonFromTrack(
|
|
t,
|
|
_generatePath(
|
|
t,
|
|
private,
|
|
playlistName: playlist.title,
|
|
playlistTrackNumber: i,
|
|
),
|
|
private: private,
|
|
quality: quality));
|
|
}
|
|
await platform.invokeMethod('addDownloads', out);
|
|
await start();
|
|
}
|
|
|
|
//Get track and meta from offline DB
|
|
Future<Track?> getOfflineTrack(String? id,
|
|
{Album? album, List<Artist>? artists}) async {
|
|
List tracks = await db.query('Tracks', where: 'id == ?', whereArgs: [id]);
|
|
if (tracks.isEmpty) return null;
|
|
Track track = Track.fromSQL(tracks[0]);
|
|
|
|
//Get album
|
|
if (album == null) {
|
|
List rawAlbums = await db
|
|
.query('Albums', where: 'id == ?', whereArgs: [track.album?.id]);
|
|
if (rawAlbums.isNotEmpty) track.album = Album.fromSQL(rawAlbums[0]);
|
|
} else {
|
|
track.album = album;
|
|
}
|
|
|
|
//Get artists
|
|
if (artists == null) {
|
|
List<Artist> newArtists = [];
|
|
for (Artist artist in track.artists!) {
|
|
List rawArtist =
|
|
await db.query('Artists', where: 'id == ?', whereArgs: [artist.id]);
|
|
if (rawArtist.isNotEmpty) newArtists.add(Artist.fromSQL(rawArtist[0]));
|
|
}
|
|
if (newArtists.isNotEmpty) track.artists = newArtists;
|
|
} else {
|
|
track.artists = artists;
|
|
}
|
|
return track;
|
|
}
|
|
|
|
//Get offline library tracks
|
|
Future<List<Track?>> getOfflineTracks() async {
|
|
List rawTracks = await db.query('Tracks',
|
|
where: 'library == 1 AND offline == 1', columns: ['id']);
|
|
List<Track?> out = [];
|
|
//Load track meta individually
|
|
for (Map rawTrack in rawTracks as Iterable<Map<dynamic, dynamic>>) {
|
|
out.add(await getOfflineTrack(rawTrack['id']));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
//Get all offline available tracks
|
|
Future<List<Track?>> allOfflineTracks() async {
|
|
if (!Platform.isAndroid) return [];
|
|
|
|
List rawTracks =
|
|
await db.query('Tracks', where: 'offline == 1', columns: ['id']);
|
|
List<Track?> out = [];
|
|
//Load track meta individually
|
|
for (Map rawTrack in rawTracks as Iterable<Map<dynamic, dynamic>>) {
|
|
out.add(await getOfflineTrack(rawTrack['id']));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
//Get all offline albums
|
|
Future<List<Album>> getOfflineAlbums() async {
|
|
List rawAlbums =
|
|
await db.query('Albums', where: 'offline == 1', columns: ['id']);
|
|
List<Album> out = [];
|
|
//Load each album
|
|
for (Map rawAlbum in rawAlbums as Iterable<Map<dynamic, dynamic>>) {
|
|
out.add(await getOfflineAlbum(rawAlbum['id']));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
//Get offline album with meta
|
|
Future<Album> getOfflineAlbum(String id) async {
|
|
List rawAlbums =
|
|
await db.query('Albums', where: 'id == ?', whereArgs: [id]);
|
|
if (rawAlbums.isEmpty) throw Exception();
|
|
Album album = Album.fromSQL(rawAlbums[0]);
|
|
|
|
List<Track> tracks = [];
|
|
//Load tracks
|
|
for (int i = 0; i < album.tracks!.length; i++) {
|
|
tracks.add((await getOfflineTrack(album.tracks![i].id, album: album))!);
|
|
}
|
|
album.tracks = tracks;
|
|
//Load artists
|
|
List<Artist> artists = [];
|
|
for (int i = 0; i < album.artists!.length; i++) {
|
|
artists.add(
|
|
(await getOfflineArtist(album.artists![i].id)) ?? album.artists![i]);
|
|
}
|
|
album.artists = artists;
|
|
|
|
return album;
|
|
}
|
|
|
|
//Get offline artist METADATA, not tracks
|
|
Future<Artist?> getOfflineArtist(String? id) async {
|
|
List rawArtists =
|
|
await db.query("Artists", where: 'id == ?', whereArgs: [id]);
|
|
if (rawArtists.isEmpty) return null;
|
|
return Artist.fromSQL(rawArtists[0]);
|
|
}
|
|
|
|
//Get all offline playlists
|
|
Future<List<Playlist>> getOfflinePlaylists() async {
|
|
final rawPlaylists = await db.query('Playlists', columns: ['id']);
|
|
final out = <Playlist>[];
|
|
for (final rawPlaylist in rawPlaylists) {
|
|
out.add(await getPlaylist(rawPlaylist));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
Future<Playlist?> getPlaylistFromId(String id) async {
|
|
final rawPlaylists =
|
|
await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
|
if (rawPlaylists.isEmpty) return null;
|
|
|
|
return await getPlaylist(rawPlaylists[0]);
|
|
}
|
|
|
|
//Get offline playlist
|
|
Future<Playlist> getPlaylist(Map<String, Object?> rawPlaylist) async {
|
|
// List rawPlaylists =
|
|
// await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
|
Playlist playlist = Playlist.fromSQL(rawPlaylist);
|
|
//Load tracks
|
|
List<Track> tracks = [];
|
|
for (Track t in playlist.tracks!) {
|
|
tracks.add((await getOfflineTrack(t.id))!);
|
|
}
|
|
playlist.tracks = tracks;
|
|
return playlist;
|
|
}
|
|
|
|
Future removeOfflineTracks(List<Track> tracks) async {
|
|
for (Track t in tracks) {
|
|
//Check if library
|
|
List rawTrack = await db.query('Tracks',
|
|
where: 'id == ?', whereArgs: [t.id], columns: ['favorite']);
|
|
if (rawTrack.isNotEmpty) {
|
|
//Count occurrences in playlists and albums
|
|
List albums = await db
|
|
.rawQuery('SELECT (id) FROM Albums WHERE tracks LIKE "%${t.id}%"');
|
|
List playlists = await db.rawQuery(
|
|
'SELECT (id) FROM Playlists WHERE tracks LIKE "%${t.id}%"');
|
|
if (albums.isEmpty &&
|
|
playlists.isEmpty &&
|
|
rawTrack[0]['favorite'] == 0) {
|
|
//Safe to remove
|
|
await db.delete('Tracks', where: 'id == ?', whereArgs: [t.id]);
|
|
} else {
|
|
await db.update('Tracks', {'offline': 0},
|
|
where: 'id == ?', whereArgs: [t.id]);
|
|
}
|
|
}
|
|
|
|
//Remove file
|
|
try {
|
|
File(p.join(offlinePath, t.id)).delete();
|
|
} catch (e) {
|
|
print(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future removeOfflineAlbum(String? id) async {
|
|
//Get album
|
|
List rawAlbums =
|
|
await db.query('Albums', where: 'id == ?', whereArgs: [id]);
|
|
if (rawAlbums.isEmpty) return;
|
|
Album album = Album.fromSQL(rawAlbums[0]);
|
|
//Remove album
|
|
await db.delete('Albums', where: 'id == ?', whereArgs: [id]);
|
|
//Remove tracks
|
|
await removeOfflineTracks(album.tracks!);
|
|
}
|
|
|
|
Future removeOfflinePlaylist(String? id) async {
|
|
//Fetch playlist
|
|
List rawPlaylists =
|
|
await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
|
if (rawPlaylists.isEmpty) return;
|
|
Playlist playlist = Playlist.fromSQL(rawPlaylists[0]);
|
|
//Remove playlist
|
|
await db.delete('Playlists', where: 'id == ?', whereArgs: [id]);
|
|
await removeOfflineTracks(playlist.tracks!);
|
|
}
|
|
|
|
//Check if album, track or playlist is offline
|
|
Future<bool> checkOffline(
|
|
{Album? album, Track? track, Playlist? playlist}) async {
|
|
if (!Platform.isAndroid) return false;
|
|
|
|
//Track
|
|
if (track != null) {
|
|
List res = await db.query('Tracks',
|
|
where: 'id == ? AND offline == 1', whereArgs: [track.id]);
|
|
if (res.isEmpty) return false;
|
|
return true;
|
|
}
|
|
//Album
|
|
if (album != null) {
|
|
List res = await db.query('Albums',
|
|
where: 'id == ? AND offline == 1', whereArgs: [album.id]);
|
|
if (res.isEmpty) return false;
|
|
return true;
|
|
}
|
|
//Playlist
|
|
if (playlist != null) {
|
|
List res = await db
|
|
.query('Playlists', where: 'id == ?', whereArgs: [playlist.id]);
|
|
if (res.isEmpty) 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 as Iterable<Map<dynamic, dynamic>>) {
|
|
results.tracks!.add((await getOfflineTrack(trackData['id']))!);
|
|
}
|
|
//Albums
|
|
List albumsData = await db.rawQuery(
|
|
'SELECT (id) FROM Albums WHERE offline == 1 AND title like "%$query%"');
|
|
for (Map rawAlbum in albumsData as Iterable<Map<dynamic, dynamic>>) {
|
|
results.albums!.add((await getOfflineAlbum(rawAlbum['id'])));
|
|
}
|
|
//Playlists
|
|
|
|
List playlists = await db.query("Playlists",
|
|
where: "title like '%' || ? || '%'", whereArgs: [query]);
|
|
for (Map playlist in playlists as Iterable<Map<dynamic, dynamic>>) {
|
|
results.playlists!.add((await getPlaylist(playlist['id'])));
|
|
}
|
|
return results;
|
|
}
|
|
|
|
//Sanitize filename
|
|
String sanitize(String input) {
|
|
RegExp sanitize = RegExp(r'[\/\\\?\%\*\:\|\"\<\>]');
|
|
return input.replaceAll(sanitize, '');
|
|
}
|
|
|
|
//Generate track download path
|
|
String? _generatePath(Track? track, bool private,
|
|
{String? playlistName,
|
|
int? playlistTrackNumber,
|
|
bool isSingleton = false}) {
|
|
String? path;
|
|
if (private) {
|
|
// TODO: add extension
|
|
return p.join(offlinePath, track!.id);
|
|
}
|
|
|
|
//Download path
|
|
path = settings.downloadPath;
|
|
|
|
if (settings.playlistFolder && playlistName != null) {
|
|
path = p.join(path!, sanitize(playlistName));
|
|
}
|
|
|
|
if (settings.artistFolder) path = p.join(path!, '%albumArtist%');
|
|
|
|
//Album folder / with disk number
|
|
if (settings.albumFolder) {
|
|
if (settings.albumDiscFolder) {
|
|
path = p.join(path!, '%album% - Disk ${track!.diskNumber ?? 1}');
|
|
} else {
|
|
path = p.join(path!, '%album%');
|
|
}
|
|
}
|
|
//Final path
|
|
path = p.join(path!,
|
|
isSingleton ? settings.singletonFilename : settings.downloadFilename);
|
|
//Playlist track number variable (not accessible in service)
|
|
if (playlistTrackNumber != null) {
|
|
path = path.replaceAll(
|
|
'%playlistTrackNumber%', playlistTrackNumber.toString());
|
|
path = path.replaceAll('%0playlistTrackNumber%',
|
|
playlistTrackNumber.toString().padLeft(2, '0'));
|
|
} else {
|
|
path = path.replaceAll('%playlistTrackNumber%', '');
|
|
path = path.replaceAll('%0playlistTrackNumber%', '');
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
//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(*)'] as int?;
|
|
int? albumCount =
|
|
(await db.rawQuery('SELECT COUNT(*) FROM Albums WHERE offline == 1'))[0]
|
|
['COUNT(*)'] as int?;
|
|
int? playlistCount = (await db
|
|
.rawQuery('SELECT COUNT(*) FROM Playlists'))[0]['COUNT(*)'] as int?;
|
|
//Free space
|
|
double diskSpace =
|
|
await DiskSpacePlus.getFreeDiskSpace.then((value) => value ?? 0.0);
|
|
//Used space
|
|
List<FileSystemEntity> offlineStat =
|
|
await Directory(offlinePath).list().toList();
|
|
int offlineSize = 0;
|
|
for (var fs in offlineStat) {
|
|
offlineSize += (await fs.stat()).size;
|
|
}
|
|
//Return in list, //TODO: Make into class in future
|
|
return ([
|
|
trackCount.toString(),
|
|
albumCount.toString(),
|
|
playlistCount.toString(),
|
|
filesize(offlineSize),
|
|
filesize((diskSpace * 1000000).floor())
|
|
]);
|
|
}
|
|
|
|
//Send settings to download service
|
|
Future updateServiceSettings() async {
|
|
if (!Platform.isAndroid) return;
|
|
await platform.invokeMethod(
|
|
'updateSettings', settings.getServiceSettings());
|
|
}
|
|
|
|
//Check storage permission
|
|
Future<bool> checkPermission() async {
|
|
if (await Permission.storage.request().isGranted) {
|
|
return true;
|
|
} else {
|
|
Fluttertoast.showToast(
|
|
msg: 'Storage permission denied!'.i18n,
|
|
toastLength: Toast.LENGTH_SHORT,
|
|
gravity: ToastGravity.BOTTOM);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//Remove download from queue/finished
|
|
Future removeDownload(int? id) async {
|
|
if (!Platform.isAndroid) return;
|
|
|
|
await platform.invokeMethod('removeDownload', {'id': id});
|
|
}
|
|
|
|
//Restart failed downloads
|
|
Future retryDownloads() async {
|
|
if (!Platform.isAndroid) return;
|
|
|
|
await platform.invokeMethod('retryDownloads');
|
|
}
|
|
|
|
//Delete downloads by state
|
|
Future removeDownloads(DownloadState state) async {
|
|
if (!Platform.isAndroid) return;
|
|
|
|
await platform.invokeMethod(
|
|
'removeDownloads', {'state': DownloadState.values.indexOf(state)});
|
|
}
|
|
|
|
static Future<String?> getDirectory(String title) =>
|
|
platform.invokeMethod('getDirectory', <String, String>{'title': title});
|
|
}
|
|
|
|
class Download {
|
|
int id;
|
|
String path;
|
|
bool private;
|
|
String trackId;
|
|
String? md5origin;
|
|
String? mediaVersion;
|
|
String title;
|
|
String image;
|
|
int quality;
|
|
//Dynamic
|
|
DownloadState state;
|
|
int? received;
|
|
int? filesize;
|
|
|
|
Download({
|
|
required this.id,
|
|
required this.path,
|
|
required this.private,
|
|
required this.trackId,
|
|
required this.title,
|
|
required this.image,
|
|
required this.state,
|
|
required this.quality,
|
|
this.md5origin,
|
|
this.mediaVersion,
|
|
this.received,
|
|
this.filesize,
|
|
});
|
|
|
|
//Get progress between 0 - 1
|
|
double get progress {
|
|
return ((received?.toDouble() ?? 0.0) / (filesize?.toDouble() ?? 1.0))
|
|
.toDouble();
|
|
}
|
|
|
|
factory Download.fromJson(Map<dynamic, dynamic> data) {
|
|
return Download(
|
|
path: data['path'],
|
|
image: data['image'],
|
|
private: data['private'],
|
|
trackId: data['trackId'],
|
|
id: data['id'],
|
|
state: DownloadState.values[data['state']],
|
|
title: data['title'],
|
|
quality: data['quality']);
|
|
}
|
|
|
|
//Change values from "update json"
|
|
void updateFromJson(Map<dynamic, dynamic> data) {
|
|
quality = data['quality'];
|
|
received = data['received'] ?? 0;
|
|
state = DownloadState.values[data['state']];
|
|
//Prevent null division later
|
|
filesize = ((data['filesize'] ?? 0) <= 0) ? 1 : (data['filesize'] ?? 1);
|
|
}
|
|
|
|
//Track to download JSON for service
|
|
static Future<Map> jsonFromTrack(Track t, String? path,
|
|
{private = true, AudioQuality? quality}) async {
|
|
//Get download info
|
|
if (t.playbackDetails == null || t.playbackDetails == []) {
|
|
t = await deezerAPI.track(t.id);
|
|
}
|
|
return {
|
|
"private": private,
|
|
"trackId": t.id,
|
|
"md5origin": t.playbackDetails![0],
|
|
"mediaVersion": t.playbackDetails![1],
|
|
"quality": private
|
|
? settings.offlineQuality.toDeezerQualityInt()
|
|
: (quality ?? settings.downloadQuality).toDeezerQualityInt(),
|
|
"title": t.title,
|
|
"path": path,
|
|
"image": t.albumArt?.thumb
|
|
};
|
|
}
|
|
}
|
|
|
|
//Has to be same order as in java
|
|
enum DownloadState { NONE, DOWNLOADING, POST, DONE, DEEZER_ERROR, ERROR }
|