Pato05
2862c9ec05
restore translations functionality make scrollViews handle mouse pointers like touch, so that pull to refresh functionality is available exit app if opening cache or settings fails (another instance running) remove draggable_scrollbar and use builtin widget instead fix email login better way to manage lyrics (less updates and lookups in the lyrics List) fix player_screen on mobile (too big -> just average :)) right click: use TapUp events instead desktop: show context menu on triple dots button also avoid showing connection error if the homepage is cached and available offline i'm probably forgetting something idk
752 lines
24 KiB
Dart
752 lines
24 KiB
Dart
import 'package:freezer/main.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: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 (!isSupported) 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 (!isSupported) return;
|
|
|
|
await platform.invokeMethod('stop');
|
|
}
|
|
|
|
Future init() async {
|
|
if (!isSupported) 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 (!isSupported) 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,
|
|
{bool private = true,
|
|
BuildContext? context,
|
|
bool isSingleton = false}) async {
|
|
if (!isSupported) 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
|
|
cacheManager.getSingleFile(track.albumArt!.thumb);
|
|
cacheManager.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
|
|
cacheManager.getSingleFile(album.art!.thumb);
|
|
cacheManager.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 (!isSupported) 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
|
|
cacheManager.getSingleFile(t.albumArt!.thumb);
|
|
cacheManager.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 (!isSupported) 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 {
|
|
if (!isSupported) return [];
|
|
|
|
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 {
|
|
if (!isSupported) return [];
|
|
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 {
|
|
if (!isSupported) return;
|
|
//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 (!isSupported) 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 (!isSupported) 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 (!isSupported) return;
|
|
|
|
await platform.invokeMethod('removeDownload', {'id': id});
|
|
}
|
|
|
|
//Restart failed downloads
|
|
Future retryDownloads() async {
|
|
if (!isSupported) return;
|
|
|
|
await platform.invokeMethod('retryDownloads');
|
|
}
|
|
|
|
//Delete downloads by state
|
|
Future removeDownloads(DownloadState state) async {
|
|
if (!isSupported) 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 }
|