159 lines
4.9 KiB
Dart
159 lines
4.9 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;
|
|
final String trackToken;
|
|
final int trackTokenExpiration;
|
|
final StreamInfoCallback? onStreamObtained;
|
|
|
|
// some cache
|
|
final DeezerAudio _deezerAudio;
|
|
int? _cachedSourceLength;
|
|
String? _cachedContentType;
|
|
Uri? _downloadUrl;
|
|
|
|
DeezerAudioSource({
|
|
required AudioQuality Function() getQuality,
|
|
required String trackId,
|
|
required String md5origin,
|
|
required String mediaVersion,
|
|
required this.trackToken,
|
|
required this.trackTokenExpiration,
|
|
this.onStreamObtained,
|
|
}) : _deezerAudio = DeezerAudio(
|
|
deezerAPI: deezerAPI,
|
|
md5origin: md5origin,
|
|
quality: getQuality.call(),
|
|
trackId: trackId,
|
|
mediaVersion: mediaVersion,
|
|
) {
|
|
_getQuality = getQuality;
|
|
_trackId = trackId;
|
|
_md5origin = md5origin;
|
|
_mediaVersion = mediaVersion;
|
|
}
|
|
|
|
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) {
|
|
final gottenUrl =
|
|
await _deezerAudio.getUrl(trackToken, trackTokenExpiration);
|
|
if (gottenUrl != null) {
|
|
_downloadUrl = gottenUrl;
|
|
} else {
|
|
_logger.warning('falling back to old url generation!');
|
|
|
|
// OLD URL GENERATION
|
|
try {
|
|
_downloadUrl = await _deezerAudio.fallback();
|
|
} 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");
|
|
}
|
|
}
|