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> 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.instance, 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 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>.empty(), contentType: _cachedContentType!); } } _logger.fine("authorizing..."); if (!await DeezerAPI.instance.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.instance.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"); } }