fix audio service stop on android getTrack backend improvements get new track token when expired move shuffle button into LibraryPlaylists as FAB move favoriteButton next to track title move lyrics button on top of album art search: fix chips, and remove checkbox when selected
178 lines
5.7 KiB
Dart
178 lines
5.7 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:isolate';
|
|
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/deezer_audio.dart';
|
|
import 'package:freezer/api/definitions.dart';
|
|
import 'package:freezer/settings.dart';
|
|
import 'package:just_audio/just_audio.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:logging/logging.dart';
|
|
|
|
typedef _IsolateMessage = (
|
|
Stream<List<int>> source,
|
|
int start,
|
|
String trackId,
|
|
SendPort sendPort
|
|
);
|
|
|
|
// Maybe better implementation of Blowfish CBC instead of random-ass, unpublished library from github?
|
|
// This class can be considered a rewrite in Dart of the Java backend (from the StreamServer.deezer() function and also from the Deezer class)
|
|
class DeezerAudioSource extends StreamAudioSource {
|
|
static final _logger = Logger("DeezerAudioSource");
|
|
|
|
late AudioQuality Function() _getQuality;
|
|
late String _trackId;
|
|
late String? _md5origin;
|
|
late String? _mediaVersion;
|
|
String? _trackToken;
|
|
int? _trackTokenExpiration;
|
|
final StreamInfoCallback? onStreamObtained;
|
|
|
|
// some cache
|
|
final DeezerAudio _deezerAudio;
|
|
int? _cachedSourceLength;
|
|
String? _cachedContentType;
|
|
Uri? _downloadUrl;
|
|
|
|
DeezerAudioSource({
|
|
required AudioQuality Function() getQuality,
|
|
required String trackId,
|
|
String? md5origin,
|
|
String? mediaVersion,
|
|
String? trackToken,
|
|
int? trackTokenExpiration,
|
|
this.onStreamObtained,
|
|
}) : _deezerAudio = DeezerAudio(
|
|
deezerAPI: deezerAPI,
|
|
quality: getQuality.call(),
|
|
trackId: trackId,
|
|
) {
|
|
_getQuality = getQuality;
|
|
_trackId = trackId;
|
|
_md5origin = md5origin;
|
|
_mediaVersion = mediaVersion;
|
|
_trackToken = trackToken;
|
|
_trackTokenExpiration = trackTokenExpiration;
|
|
}
|
|
|
|
AudioQuality? get quality => _deezerAudio.quality;
|
|
String get trackId => _trackId;
|
|
String? get md5origin => _md5origin;
|
|
String? get mediaVersion => _mediaVersion;
|
|
|
|
@override
|
|
Future<StreamAudioResponse> request([int? start, int? end]) async {
|
|
start ??= 0;
|
|
|
|
if (_cachedSourceLength != null) {
|
|
if (start == _cachedSourceLength) {
|
|
return StreamAudioResponse(
|
|
sourceLength: _cachedSourceLength,
|
|
contentLength: 0,
|
|
offset: start,
|
|
stream: const Stream<List<int>>.empty(),
|
|
contentType: _cachedContentType!);
|
|
}
|
|
}
|
|
|
|
_logger.fine("authorizing...");
|
|
if (!await deezerAPI.authorize()) {
|
|
_logger.severe("authorization failed! cannot continue!");
|
|
throw Exception("Authorization failed!");
|
|
}
|
|
|
|
// determine quality to use
|
|
final newQuality = _getQuality.call();
|
|
|
|
if (_downloadUrl != null && quality != newQuality) {
|
|
// update currentUrl to get tracks with new quality
|
|
_downloadUrl = null;
|
|
}
|
|
|
|
_deezerAudio.quality = newQuality;
|
|
|
|
if (_downloadUrl == null) {
|
|
if (_trackToken == null) {
|
|
// TODO: get new track token?
|
|
final track = await deezerAPI.track(trackId);
|
|
_trackToken = track.trackToken;
|
|
_trackTokenExpiration = track.trackTokenExpiration;
|
|
_mediaVersion = track.playbackDetails![1];
|
|
_md5origin = track.playbackDetails![0];
|
|
}
|
|
try {
|
|
final res =
|
|
await _deezerAudio.getUrl(_trackToken!, _trackTokenExpiration!);
|
|
_downloadUrl = res!.$1;
|
|
_trackToken = res.$2;
|
|
_trackTokenExpiration = res.$3;
|
|
} catch (e) {
|
|
_logger.warning('get_url API failed with error: $e');
|
|
_logger.warning('falling back to old url generation!');
|
|
|
|
if (_md5origin == null || _mediaVersion == null) {
|
|
throw Exception(
|
|
'Can\'t use old URL API: md5origin and mediaVersion are missing!');
|
|
}
|
|
// OLD URL GENERATION
|
|
try {
|
|
final res = await _deezerAudio.fallback(
|
|
md5origin: _md5origin!, mediaVersion: _mediaVersion!);
|
|
_downloadUrl = res.uri;
|
|
_md5origin = res.md5origin;
|
|
_mediaVersion = res.mediaVersion;
|
|
} on QualityException {
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
|
|
_logger.fine("Downloading track from ${_downloadUrl!.toString()}");
|
|
final int deezerStart = start - (start % 2048);
|
|
final req = http.Request('GET', _downloadUrl!)
|
|
..headers.addAll({
|
|
'User-Agent': DeezerAPI.userAgent,
|
|
'Accept-Language': '*',
|
|
'Accept': '*/*',
|
|
if (deezerStart > 0)
|
|
"Range": "bytes=$deezerStart-${end == null ? '' : end.toString()}"
|
|
});
|
|
|
|
final res = await req.send();
|
|
final rc = res.statusCode;
|
|
_logger
|
|
.finest("request sent! rc: $rc, content-length: ${res.contentLength}");
|
|
onStreamObtained?.call(StreamQualityInfo(
|
|
format: quality == AudioQuality.FLAC ? Format.FLAC : Format.MP3,
|
|
source: Source.stream,
|
|
size: start + res.contentLength!,
|
|
quality: quality,
|
|
));
|
|
if (rc != HttpStatus.ok && rc != HttpStatus.partialContent) {
|
|
throw Exception(await res.stream.bytesToString());
|
|
}
|
|
|
|
int dropBytes = start % DeezerAudio.chunkSize;
|
|
_logger.finest(
|
|
"deezerStart: $deezerStart (actual start: $start), end: $end, dropBytes: $dropBytes");
|
|
final stream = DeezerAudio.decryptionStream(res.stream,
|
|
start: start, trackId: trackId);
|
|
|
|
final cl = res.contentLength! - dropBytes;
|
|
|
|
if (end == null) {
|
|
_cachedSourceLength = cl + start;
|
|
}
|
|
|
|
return StreamAudioResponse(
|
|
sourceLength: _cachedSourceLength,
|
|
contentLength: cl,
|
|
offset: start,
|
|
stream: stream,
|
|
contentType: _cachedContentType =
|
|
quality == AudioQuality.FLAC ? "audio/flac" : "audio/mpeg");
|
|
}
|
|
}
|