freezer/lib/api/audio_sources/deezer_audio_source.dart

175 lines
5.6 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 {
_downloadUrl =
await _deezerAudio.getUrl(_trackToken!, _trackTokenExpiration!);
} 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");
}
}