0.6.0 - Redesign, downloads, tagging fixes, download quality selector...

This commit is contained in:
exttex 2020-10-19 21:28:45 +02:00
parent bcf709e56d
commit 1384aedb35
28 changed files with 1201 additions and 878 deletions

View file

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

View file

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

View file

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

View file

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