0.6.0 - Redesign, downloads, tagging fixes, download quality selector...
This commit is contained in:
parent
bcf709e56d
commit
1384aedb35
28 changed files with 1201 additions and 878 deletions
|
@ -1,91 +1,67 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:dio/adapter.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../settings.dart';
|
||||
import 'definitions.dart';
|
||||
import 'dart:async';
|
||||
|
||||
DeezerAPI deezerAPI = DeezerAPI();
|
||||
|
||||
class DeezerAPI {
|
||||
|
||||
String arl;
|
||||
|
||||
DeezerAPI({this.arl});
|
||||
|
||||
String arl;
|
||||
String token;
|
||||
String userId;
|
||||
String userName;
|
||||
String favoritesPlaylistId;
|
||||
String privateUrl = 'http://www.deezer.com/ajax/gw-light.php';
|
||||
Map<String, String> headers = {
|
||||
String sid;
|
||||
|
||||
Future _authorizing;
|
||||
|
||||
//Get headers
|
||||
Map<String, String> get headers => {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
||||
"Content-Language": '${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'}',
|
||||
"Cache-Control": "max-age=0",
|
||||
"Accept": "*/*",
|
||||
"Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3",
|
||||
"Accept-Language": "${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'},${settings.deezerLanguage??"en"};q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Connection": "keep-alive"
|
||||
"Connection": "keep-alive",
|
||||
"Cookie": "arl=${arl}" + ((sid == null) ? '' : '; sid=${sid}')
|
||||
};
|
||||
Future _authorizing;
|
||||
Dio dio = Dio();
|
||||
CookieJar _cookieJar = new CookieJar();
|
||||
|
||||
//Call private api
|
||||
//Call private API
|
||||
Future<Map<dynamic, dynamic>> callApi(String method, {Map<dynamic, dynamic> params, String gatewayInput}) async {
|
||||
|
||||
//Add headers
|
||||
dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (RequestOptions options) {
|
||||
options.headers = this.headers;
|
||||
return options;
|
||||
//Generate URL
|
||||
Uri uri = Uri.https('www.deezer.com', '/ajax/gw-light.php', {
|
||||
'api_version': '1.0',
|
||||
'api_token': this.token,
|
||||
'input': '3',
|
||||
'method': method,
|
||||
//Used for homepage
|
||||
if (gatewayInput != null)
|
||||
'gateway_input': gatewayInput
|
||||
});
|
||||
//Post
|
||||
http.Response res = await http.post(uri, headers: headers, body: jsonEncode(params));
|
||||
//Grab SID
|
||||
if (method == 'deezer.getUserData') {
|
||||
for (String cookieHeader in res.headers['set-cookie'].split(';')) {
|
||||
if (cookieHeader.startsWith('sid=')) {
|
||||
sid = cookieHeader.split('=')[1];
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
//Proxy
|
||||
if (settings.proxyAddress != null && settings.proxyAddress != '' && settings.proxyAddress.length > 9) {
|
||||
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
|
||||
client.findProxy = (uri) => "PROXY ${settings.proxyAddress}";
|
||||
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
|
||||
};
|
||||
}
|
||||
|
||||
//Add cookies
|
||||
List<Cookie> cookies = [Cookie('arl', this.arl)];
|
||||
_cookieJar.saveFromResponse(Uri.parse(this.privateUrl), cookies);
|
||||
dio.interceptors.add(CookieManager(_cookieJar));
|
||||
//Make request
|
||||
Response<dynamic> response = await dio.post(
|
||||
this.privateUrl,
|
||||
queryParameters: {
|
||||
'api_version': '1.0',
|
||||
'api_token': this.token,
|
||||
'input': '3',
|
||||
'method': method,
|
||||
//Used for homepage
|
||||
if (gatewayInput != null)
|
||||
'gateway_input': gatewayInput
|
||||
},
|
||||
data: jsonEncode(params??{}),
|
||||
options: Options(responseType: ResponseType.json, sendTimeout: 10000, receiveTimeout: 10000)
|
||||
);
|
||||
return response.data;
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<Map> callPublicApi(String path) async {
|
||||
Dio dio = Dio();
|
||||
Response response = await dio.get(
|
||||
'https://api.deezer.com/' + path,
|
||||
options: Options(responseType: ResponseType.json, sendTimeout: 10000, receiveTimeout: 10000)
|
||||
);
|
||||
return response.data;
|
||||
Future<Map<dynamic, dynamic>> callPublicApi(String path) async {
|
||||
http.Response res = await http.get('https://api.deezer.com/' + path);
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
//Wrapper so it can be globally awaited
|
||||
|
@ -128,11 +104,11 @@ class DeezerAPI {
|
|||
}
|
||||
//Share URL
|
||||
if (uri.host == 'deezer.page.link' || uri.host == 'www.deezer.page.link') {
|
||||
Dio dio = Dio();
|
||||
Response res = await dio.head(url, options: RequestOptions(
|
||||
followRedirects: true
|
||||
));
|
||||
return parseLink('http://deezer.com' + res.realUri.toString());
|
||||
http.BaseRequest request = http.Request('HEAD', Uri.parse(url));
|
||||
request.followRedirects = false;
|
||||
http.StreamedResponse response = await request.send();
|
||||
String newUrl = response.headers['location'];
|
||||
return parseLink(newUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,5 +421,14 @@ class DeezerAPI {
|
|||
'songs': []
|
||||
});
|
||||
}
|
||||
|
||||
//Get shuffled library
|
||||
Future<List<Track>> libraryShuffle({int start=0}) async {
|
||||
Map data = await callApi('tracklist.getShuffledCollection', params: {
|
||||
'nb': 50,
|
||||
'start': start
|
||||
});
|
||||
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:disk_space/disk_space.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';
|
||||
|
@ -110,9 +111,63 @@ class DownloadManager {
|
|||
return batch;
|
||||
}
|
||||
|
||||
Future addOfflineTrack(Track track, {private = true}) async {
|
||||
//Quality selector for custom quality
|
||||
Future qualitySelect(BuildContext context) async {
|
||||
AudioQuality quality;
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0, 12, 0, 2),
|
||||
child: Text(
|
||||
'Quality'.i18n,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20.0
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('MP3 128kbps'),
|
||||
onTap: () {
|
||||
quality = AudioQuality.MP3_128;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('MP3 320kbps'),
|
||||
onTap: () {
|
||||
quality = AudioQuality.MP3_320;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('FLAC'),
|
||||
onTap: () {
|
||||
quality = AudioQuality.FLAC;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
return quality;
|
||||
}
|
||||
|
||||
Future<bool> addOfflineTrack(Track track, {private = true, BuildContext context}) async {
|
||||
//Permission
|
||||
if (!private && !(await checkPermission())) return;
|
||||
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;
|
||||
}
|
||||
|
||||
//Add to DB
|
||||
if (private) {
|
||||
|
@ -127,14 +182,21 @@ class DownloadManager {
|
|||
|
||||
//Get path
|
||||
String path = _generatePath(track, private);
|
||||
await platform.invokeMethod('addDownloads', [await Download.jsonFromTrack(track, path, private: private)]);
|
||||
await platform.invokeMethod('addDownloads', [await Download.jsonFromTrack(track, path, private: private, quality: quality)]);
|
||||
await start();
|
||||
}
|
||||
|
||||
Future addOfflineAlbum(Album album, {private = true}) async {
|
||||
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.length == 0) {
|
||||
album = await deezerAPI.album(album.id);
|
||||
|
@ -157,16 +219,22 @@ class DownloadManager {
|
|||
//Create downloads
|
||||
List<Map> out = [];
|
||||
for (Track t in album.tracks) {
|
||||
out.add(await Download.jsonFromTrack(t, _generatePath(t, private), private: private));
|
||||
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}) async {
|
||||
Future addOfflinePlaylist(Playlist playlist, {private = true, BuildContext context, AudioQuality quality}) async {
|
||||
//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);
|
||||
|
@ -193,8 +261,8 @@ class DownloadManager {
|
|||
t,
|
||||
private,
|
||||
playlistName: playlist.title,
|
||||
playlistTrackNumber: i
|
||||
), private: private));
|
||||
playlistTrackNumber: i,
|
||||
), private: private, quality: quality));
|
||||
}
|
||||
await platform.invokeMethod('addDownloads', out);
|
||||
await start();
|
||||
|
@ -375,7 +443,7 @@ class DownloadManager {
|
|||
return true;
|
||||
}
|
||||
//Playlist
|
||||
if (playlist != null) {
|
||||
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;
|
||||
|
@ -553,7 +621,7 @@ class Download {
|
|||
}
|
||||
|
||||
//Track to download JSON for service
|
||||
static Future<Map> jsonFromTrack(Track t, String path, {private = true}) async {
|
||||
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);
|
||||
|
@ -565,7 +633,7 @@ class Download {
|
|||
"mediaVersion": t.playbackDetails[1],
|
||||
"quality": private
|
||||
? settings.getQualityInt(settings.offlineQuality)
|
||||
: settings.getQualityInt(settings.downloadQuality),
|
||||
: settings.getQualityInt((quality??settings.downloadQuality)),
|
||||
"title": t.title,
|
||||
"path": path,
|
||||
"image": t.albumArt.thumb
|
||||
|
|
|
@ -104,6 +104,7 @@ class PlayerHelper {
|
|||
androidNotificationChannelDescription: 'Freezer',
|
||||
androidNotificationChannelName: 'Freezer',
|
||||
androidNotificationIcon: 'drawable/ic_logo',
|
||||
params: {'ignoreInterruptions': settings.ignoreInterruptions}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -138,14 +139,17 @@ class PlayerHelper {
|
|||
await startService();
|
||||
await settings.updateAudioServiceQuality();
|
||||
await AudioService.updateQueue(queue);
|
||||
await AudioService.skipToQueueItem(trackId);
|
||||
if (queue[0].id != trackId)
|
||||
await AudioService.skipToQueueItem(trackId);
|
||||
if (!AudioService.playbackState.playing)
|
||||
AudioService.play();
|
||||
}
|
||||
|
||||
//Called when queue ends to load more tracks
|
||||
Future onQueueEnd() async {
|
||||
//Flow
|
||||
if (queueSource == null) return;
|
||||
print('test');
|
||||
|
||||
if (queueSource.id == 'flow') {
|
||||
List<Track> tracks = await deezerAPI.flow();
|
||||
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
|
||||
|
@ -163,6 +167,15 @@ class PlayerHelper {
|
|||
return;
|
||||
}
|
||||
|
||||
//Library shuffle
|
||||
if (queueSource.source == 'libraryshuffle') {
|
||||
List<Track> tracks = await deezerAPI.libraryShuffle(start: AudioService.queue.length);
|
||||
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
|
||||
await AudioService.addQueueItems(mi);
|
||||
AudioService.skipToNext();
|
||||
return;
|
||||
}
|
||||
|
||||
print(queueSource.toJson());
|
||||
}
|
||||
|
||||
|
@ -245,7 +258,7 @@ void backgroundTaskEntrypoint() async {
|
|||
}
|
||||
|
||||
class AudioPlayerTask extends BackgroundAudioTask {
|
||||
AudioPlayer _player = AudioPlayer();
|
||||
AudioPlayer _player;
|
||||
|
||||
//Queue
|
||||
List<MediaItem> _queue = <MediaItem>[];
|
||||
|
@ -274,6 +287,13 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
final session = await AudioSession.instance;
|
||||
session.configure(AudioSessionConfiguration.music());
|
||||
|
||||
if (params['ignoreInterruptions'] == true) {
|
||||
_player = AudioPlayer(handleInterruptions: false);
|
||||
session.interruptionEventStream.listen((_) {});
|
||||
session.becomingNoisyEventStream.listen((_) {});
|
||||
} else
|
||||
_player = AudioPlayer();
|
||||
|
||||
//Update track index
|
||||
_player.currentIndexStream.listen((index) {
|
||||
if (index != null) {
|
||||
|
@ -365,7 +385,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
|
||||
@override
|
||||
Future<void> onSkipToNext() async {
|
||||
print('skipping');
|
||||
if (_queueIndex == _queue.length-1) return;
|
||||
//Update buffering state
|
||||
_skipState = AudioProcessingState.skippingToNext;
|
||||
|
@ -428,10 +447,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
MediaControl.skipToNext,
|
||||
//Stop
|
||||
MediaControl(
|
||||
androidIcon: 'drawable/ic_action_stop',
|
||||
label: 'stop',
|
||||
action: MediaAction.stop
|
||||
)
|
||||
androidIcon: 'drawable/ic_action_stop',
|
||||
label: 'stop',
|
||||
action: MediaAction.stop
|
||||
),
|
||||
],
|
||||
systemActions: [
|
||||
MediaAction.seekTo,
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
|
@ -32,11 +34,10 @@ class SpotifyAPI {
|
|||
//Extract JSON data form spotify embed page
|
||||
Future<Map> getEmbedData(String url) async {
|
||||
//Fetch
|
||||
Dio dio = Dio();
|
||||
Response response = await dio.get(url);
|
||||
http.Response response = await http.get(url);
|
||||
//Parse
|
||||
Document document = parse(response.data);
|
||||
Element element = document.getElementById('resource');
|
||||
dom.Document document = parse(response.body);
|
||||
dom.Element element = document.getElementById('resource');
|
||||
return jsonDecode(element.innerHtml);
|
||||
}
|
||||
|
||||
|
@ -50,7 +51,7 @@ class SpotifyAPI {
|
|||
}
|
||||
|
||||
|
||||
Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false}) async {
|
||||
Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false, BuildContext context, AudioQuality quality}) async {
|
||||
doneImporting = false;
|
||||
importingSpotifyPlaylist = playlist;
|
||||
|
||||
|
@ -60,6 +61,7 @@ class SpotifyAPI {
|
|||
playlistId = await deezerAPI.createPlaylist(playlist.name, description: playlist.description);
|
||||
|
||||
//Search for tracks
|
||||
List<Track> downloadTracks = [];
|
||||
for (SpotifyTrack track in playlist.tracks) {
|
||||
Map deezer;
|
||||
try {
|
||||
|
@ -71,12 +73,21 @@ class SpotifyAPI {
|
|||
if (!downloadOnly)
|
||||
await deezerAPI.addToPlaylist(id, playlistId);
|
||||
if (downloadOnly)
|
||||
await downloadManager.addOfflineTrack(Track(id: id), private: false);
|
||||
downloadTracks.add(Track(id: id));
|
||||
track.state = TrackImportState.OK;
|
||||
} catch (e) {
|
||||
//On error
|
||||
track.state = TrackImportState.ERROR;
|
||||
}
|
||||
|
||||
//Download
|
||||
if (downloadOnly)
|
||||
await downloadManager.addOfflinePlaylist(
|
||||
Playlist(trackCount: downloadTracks.length, tracks: downloadTracks, title: playlist.name),
|
||||
private: false,
|
||||
quality: quality
|
||||
);
|
||||
|
||||
//Add playlist id to stream, stream is for updating ui only
|
||||
importingStream.add(playlistId);
|
||||
importingSpotifyPlaylist = playlist;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue