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