import 'dart:io'; import 'package:dio/dio.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer_audio.dart'; import 'package:freezer/api/definitions.dart' as d; import 'package:freezer/api/download.dart'; import 'package:freezer/api/download/database.dart'; import 'package:freezer/api/download/service_interface.dart'; import 'package:freezer/main.dart'; import 'package:freezer/settings.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:isar/isar.dart'; import 'package:path/path.dart'; part 'service.freezed.dart'; part 'service.g.dart'; class DownloadService { static const NOTIFICATION_ID = 6969; static const NOTIFICATION_CHANNEL_ID = "freezerdownloads"; final ServiceInterface service; DownloadService(this.service); AudioQuality? _preferredQuality; AudioQuality? _downloadQuality; bool useGetURL = false; late String downloadFilename; late DeezerAPI _deezerAPI; late final Isar _isar; void run() async { service.on('addDownloads', (event) { downloadTrack(DownloadInfo.fromJson(event!)); }); service.on('updateQuality', (event) { _preferredQuality = AudioQuality.values[event!['q']!]; }); service.on('updateCapabilities', (event) { final bool canStreamHQ = event!['canStreamHQ']; final bool canStreamLossless = event['canStreamLossless']; if (canStreamHQ || canStreamLossless) useGetURL = true; _downloadQuality = settings.maxQualityFor( _preferredQuality!, canStreamHQ, canStreamLossless); }); service.on('updateSettings', (event) { if (event!['downloadFilename'] != null) { downloadFilename = event['downloadFilename']; } if (event['deezerLanguage'] != null) { _deezerAPI.deezerLanguage = event['deezerLanguage']; } if (event['deezerCountry'] != null) { _deezerAPI.deezerCountry = event['deezerCountry']; } }); _isar = await Database.open(); _deezerAPI = DeezerAPI(await getCookieJar()); service.send('ready'); } void downloadTrack(DownloadInfo info) async { await _deezerAPI.authorize(); final trackId = info.trackId; final deezerAudio = DeezerAudio( deezerAPI: _deezerAPI, quality: _downloadQuality ?? AudioQuality.MP3_128, trackId: trackId); bool offline = info.path == null; final track = await _deezerAPI.track(trackId); final file = File(fillVariables(track, info.path!)); print('downloading to ${file.path}'); final Uri uri; if (useGetURL) { // TODO: use pipe API to get track token! final res = await deezerAudio.getUrl( track.trackToken!, track.trackTokenExpiration!); uri = res!.$1; } else { final res = await deezerAudio.fallback( md5origin: track.playbackDetails![0], mediaVersion: track.playbackDetails![1]); uri = res.uri; } await _isar.writeTxn(() async { await _isar.tracks.put(Track.from(track)); if (track.album != null) { await _isar.albums.put(Album.from(track.album!)); } if (track.artists != null) { await _isar.artists .putAll(track.artists!.map(Artist.from).toList(growable: false)); } }); // download url and decrypt final req = await _deezerAPI.dio.get(uri.toString(), options: Options(responseType: ResponseType.bytes)); final stream = DeezerAudio.decryptionStream(req.data, start: 0, trackId: trackId); final fWrite = file.openWrite(); await stream.pipe(fWrite); print('download complete!'); } String pathForOffline(d.Track t) { // TODO: implement throw Exception(); } String fillVariables(d.Track t, String path) { return path .replaceAll('%title%', Database.sanitize(t.title!)) .replaceAll('%album%', t.album!.title!) .replaceAll('%artist%', t.artists?[0].name ?? '') .replaceAll('%albumArtist%', Database.sanitize(t.album?.artistString ?? t.artistString)) .replaceAll('%artists%', t.artistString) .replaceAll('%feats%', t.artists?.sublist(1).map((e) => e.name!).join(', ') ?? '') .replaceAll('%trackNumber%', t.trackNumber!.toString()) .replaceAll('%0trackNumber%', t.trackNumber.toString().padLeft(2, '0')) .replaceAll('%year%', '0'); // TODO: find year } } @freezed class DownloadInfo with _$DownloadInfo { const factory DownloadInfo({required String trackId, String? path}) = _DownloadInfo; factory DownloadInfo.fromJson(Map json) => _$DownloadInfoFromJson(json); }