freezer/lib/api/audio_sources/deezer_audio_source.dart
Pato05 87c9733f51
add build script for linux
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
2024-02-19 00:49:32 +01:00

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");
}
}