diff --git a/.gitignore b/.gitignore index 0552fc8..54bd5ed 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ android/app/.cxx /build/ .gradle/ *.g.dart +*.freezed.dart # Web related lib/generated_plugin_registrant.dart diff --git a/lib/api/audio_sources/deezer_audio_source.dart b/lib/api/audio_sources/deezer_audio_source.dart index 8b806d3..ca18d5f 100644 --- a/lib/api/audio_sources/deezer_audio_source.dart +++ b/lib/api/audio_sources/deezer_audio_source.dart @@ -45,7 +45,7 @@ class DeezerAudioSource extends StreamAudioSource { int? trackTokenExpiration, this.onStreamObtained, }) : _deezerAudio = DeezerAudio( - deezerAPI: deezerAPI, + deezerAPI: DeezerAPI.instance, quality: getQuality.call(), trackId: trackId, ) { @@ -78,7 +78,7 @@ class DeezerAudioSource extends StreamAudioSource { } _logger.fine("authorizing..."); - if (!await deezerAPI.authorize()) { + if (!await DeezerAPI.instance.authorize()) { _logger.severe("authorization failed! cannot continue!"); throw Exception("Authorization failed!"); } @@ -96,7 +96,7 @@ class DeezerAudioSource extends StreamAudioSource { if (_downloadUrl == null) { if (_trackToken == null) { // TODO: get new track token? - final track = await deezerAPI.track(trackId); + final track = await DeezerAPI.instance.track(trackId); _trackToken = track.trackToken; _trackTokenExpiration = track.trackTokenExpiration; _mediaVersion = track.playbackDetails![1]; diff --git a/lib/api/cookie_jar_hive_storage.dart b/lib/api/cookie_jar_hive_storage.dart deleted file mode 100644 index 890d00e..0000000 --- a/lib/api/cookie_jar_hive_storage.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:cookie_jar/cookie_jar.dart'; -import 'package:hive_flutter/adapters.dart'; - -class HiveStorage implements Storage { - final String boxName; - final String? boxPath; - HiveStorage(this.boxName, {this.boxPath}); - - bool _initialized = false; - late final Box _box; - - Future? _initFuture; - - Future init(bool persistSession, bool ignoreExpires) => - _initFuture ??= _init(persistSession, ignoreExpires); - - Future _init(bool persistSession, bool ignoreExpires) async { - if (_initialized) return; - _initialized = true; - _box = await Hive.openBox(boxName, path: boxPath); - print('init() finished'); - } - - @override - Future read(String key) async { - await _initFuture; - return _box.get(key); - } - - @override - Future write(String key, String value) => _box.put(key, value); - @override - Future delete(String key) => _box.delete(key); - @override - Future deleteAll(List keys) => _box.deleteAll(keys); -} diff --git a/lib/api/cookie_jar_isar_storage.dart b/lib/api/cookie_jar_isar_storage.dart new file mode 100644 index 0000000..5f84efb --- /dev/null +++ b/lib/api/cookie_jar_isar_storage.dart @@ -0,0 +1,53 @@ +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:freezer/utils.dart'; +import 'package:isar/isar.dart'; + +part 'cookie_jar_isar_storage.g.dart'; + +class IsarStorage implements Storage { + final String dbName; + final String dbPath; + IsarStorage(this.dbName, this.dbPath); + + late final Isar _isar; + + Future? _initFuture; + + @override + Future init(bool persistSession, bool ignoreExpires) => + _initFuture ??= _init(persistSession, ignoreExpires); + + Future _init(bool persistSession, bool ignoreExpires) async { + _isar = await Isar.open([CookieSchema], directory: dbPath, name: dbName); + print('init() finished'); + } + + @override + Future read(String key) async { + await _initFuture; + final cookie = await _isar.cookies.get(Utils.fastHash(key)); + if (cookie == null) return null; + return cookie.value; + } + + @override + Future write(String key, String value) => + _isar.writeTxn(() => _isar.cookies.put(Cookie() + ..key = key + ..value = value)); + @override + Future delete(String key) => + _isar.writeTxn(() => _isar.cookies.delete(Utils.fastHash(key))); + @override + Future deleteAll(List keys) => + _isar.writeTxn(() => _isar.cookies + .deleteAll(keys.map(Utils.fastHash).toList(growable: false))); +} + +@collection +class Cookie { + Id get isarId => Utils.fastHash(key); + + late String key; + late String value; +} diff --git a/lib/api/deezer.dart b/lib/api/deezer.dart index ecfd8ae..e40918c 100644 --- a/lib/api/deezer.dart +++ b/lib/api/deezer.dart @@ -8,17 +8,17 @@ import 'package:freezer/api/cache.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/spotify.dart'; import 'package:freezer/settings.dart'; +import 'package:get_it/get_it.dart'; import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; -import 'cookie_jar_hive_storage.dart'; import 'dart:convert'; import 'dart:async'; -final deezerAPI = DeezerAPI(); -final cookieJar = PersistCookieJar(storage: HiveStorage('cookies')); - class DeezerAPI { + /// Shorthand for GetIt.instance() + static DeezerAPI get instance => GetIt.instance(); + // from deemix: https://gitlab.com/RemixDev/deemix-js/-/blob/main/deemix/utils/deezer.js?ref_type=heads#L6 static const CLIENT_ID = "172365"; static const CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34"; @@ -40,7 +40,9 @@ class DeezerAPI { static String get userAgent => USER_AGENTS[defaultTargetPlatform]!; static final _logger = Logger('DeezerAPI'); - DeezerAPI(); + + final CookieJar cookieJar; + DeezerAPI(this.cookieJar); set arl(String? arl) { if (arl == null) { @@ -62,6 +64,9 @@ class DeezerAPI { String? favoritesPlaylistId; String? sid; + late String deezerLanguage; + late String deezerCountry; + late String licenseToken; late bool canStreamLossless; late bool canStreamHQ; @@ -77,13 +82,12 @@ class DeezerAPI { //Get headers Map get headers => { "User-Agent": userAgent, - "Content-Language": - '${settings.deezerLanguage}-${settings.deezerCountry}', + "Content-Language": '$deezerLanguage-$deezerCountry', "Cache-Control": "max-age=0", "Accept": "*/*", "Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3", "Accept-Language": - "${settings.deezerLanguage}-${settings.deezerCountry},${settings.deezerLanguage};q=0.9,en-US;q=0.8,en;q=0.7", + "$deezerLanguage-$deezerCountry,$deezerLanguage;q=0.9,en-US;q=0.8,en;q=0.7", "Connection": "keep-alive", }; @@ -292,6 +296,9 @@ class DeezerAPI { } } + /// Get single track url + /// + /// Shorcut for [getTracksUrl([trackToken], format)[0]] Future getTrackUrl( String trackToken, String format) async => (await getTracksUrl([trackToken], format))[0]; @@ -306,7 +313,11 @@ class DeezerAPI { { "type": "FULL", "formats": [ - {"cipher": "BF_CBC_STRIPE", "format": format} + {"cipher": "BF_CBC_STRIPE", "format": format}, + { + "cipher": "BF_CBC_STRIPE", + "format": "MP3_MISC" // allow for custom MP3s + }, ], } ], @@ -771,7 +782,7 @@ class DeezerAPI { 'nb': 1000, 'show_id': showId, 'start': 0, - 'user_id': int.parse(deezerAPI.userId!) + 'user_id': int.parse(userId!) }); return data['results']['EPISODES']['data'] .map((e) => ShowEpisode.fromPrivateJson(e)) diff --git a/lib/api/download.dart b/lib/api/download.dart index 0f35eca..df8bf28 100644 --- a/lib/api/download.dart +++ b/lib/api/download.dart @@ -174,7 +174,7 @@ class DownloadManager { if (track.artists == null || track.artists!.isEmpty || track.album == null) { - track = await deezerAPI.track(track.id); + track = await DeezerAPI.instance.track(track.id); } //Add to DB @@ -212,7 +212,7 @@ class DownloadManager { //Get from API if no tracks if (album!.tracks == null || album.tracks!.isEmpty) { - album = await deezerAPI.album(album.id); + album = await DeezerAPI.instance.album(album.id); } //Add to DB @@ -258,7 +258,7 @@ class DownloadManager { //Get tracks if missing if (playlist!.tracks == null || playlist.tracks!.length < playlist.trackCount!) { - playlist = await deezerAPI.fullPlaylist(playlist.id); + playlist = await DeezerAPI.instance.fullPlaylist(playlist.id); } //Add to DB @@ -643,6 +643,7 @@ class DownloadManager { //Check storage permission static Future checkPermission() async { + if (Platform.isLinux || Platform.isWindows) return true; if (await Permission.storage.request().isGranted) { return true; } else if ( // android 12 or later @@ -748,7 +749,7 @@ class Download { {private = true, AudioQuality? quality}) async { //Get download info if (t.playbackDetails == null || t.playbackDetails == []) { - t = await deezerAPI.track(t.id); + t = await DeezerAPI.instance.track(t.id); } return { "private": private, diff --git a/lib/api/download_manager/database.dart b/lib/api/download_manager/database.dart index 5201f23..3720a06 100644 --- a/lib/api/download_manager/database.dart +++ b/lib/api/download_manager/database.dart @@ -21,6 +21,7 @@ class Track { late final bool favorite; late final int? diskNumber; late final bool explicit; + late final String localPath; Track(); diff --git a/lib/api/download_manager/download_manager.dart b/lib/api/download_manager/download_manager.dart index 5ecf2a5..fe3b430 100644 --- a/lib/api/download_manager/download_manager.dart +++ b/lib/api/download_manager/download_manager.dart @@ -1,7 +1,8 @@ +import 'dart:async'; import 'dart:io'; import 'dart:isolate'; -import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart' as d; @@ -20,8 +21,19 @@ class DownloadManager { late Isar _isar; - SendPort? _sendPort; Isolate? _isolate; + ServiceInterface? _service; + + bool _started = false; + + Future startDebug() async { + if (_started) return; + _started = true; + + await configure(); + await startService(); + await Future.delayed(const Duration(milliseconds: 500)); + } Future configure() async { _isar = await Isar.open( @@ -56,13 +68,28 @@ class DownloadManager { Future startService() async { if (Platform.isAndroid) { - return FlutterBackgroundService().startService(); + final didStart = await FlutterBackgroundService().startService(); + + if (!didStart) return false; + } else { + // UI -> Service communication + final receivePort = ReceivePort(); + _isolate = await Isolate.spawn(_startIsolate, receivePort.sendPort); + _service = ServiceInterface(receivePort: receivePort); + _service!.on('sendPort', (args) { + _service!.sendPort = args!['s']; + _service!.no('sendPort'); + }); } - final receivePort = ReceivePort(); - _sendPort = receivePort.sendPort; - _isolate = await Isolate.spawn( - _startService, ServiceInterface(receivePort: receivePort)); + final completer = Completer(); + on('ready', (args) => completer.complete()); + await completer.future; + invoke('updateSettings', { + 'downloadFilename': settings.downloadFilename, + 'deezerLanguage': settings.deezerLanguage, + 'deezerCountry': settings.deezerCountry, + }); return true; } @@ -72,34 +99,44 @@ class DownloadManager { } void invoke(String method, [Map? args]) { - if (_sendPort != null) { - _sendPort!.send({ - 'method': method, - if (args != null) ...args, - }); + if (_service != null) { + _service!.send(method, args); return; } + FlutterBackgroundService().invoke(method, args); } - Future addOfflineTrack(d.Track track, - {bool private = true, BuildContext? context, isSingleton = false}) async { + void on(String method, ListenerCallback listener) { + if (_service != null) { + _service!.on(method, listener); + return; + } + + FlutterBackgroundService().on(method).listen(listener); + } + + Future checkOffline(String trackId) async { + final c = + await _isar.tracks.where().isarIdEqualTo(int.parse(trackId)).count(); + return c > 0; + } + + Future addOfflineTrack(d.Track track, AudioQuality downloadQuality, + {bool private = true}) async { //Permission if (!private && !(await dl.DownloadManager.checkCanDownload())) { return false; } - //Ask for quality - //AudioQuality? quality; - if (!private && settings.downloadQuality == AudioQuality.ASK) { - // quality = await qualitySelect(context!); - // if (quality == null) return false; + if (downloadQuality == AudioQuality.ASK) { + throw Exception('Invalid quality.'); } if (private) { if (track.artists == null || track.artists!.isEmpty || track.album == null) { - track = await deezerAPI.track(track.id); + track = await DeezerAPI.instance.track(track.id); } // cache album art @@ -120,7 +157,12 @@ class DownloadManager { } // logic for downloading the track - invoke('addDownloads', {'track': track.toJson()}); + invoke( + 'addDownloads', + DownloadInfo( + trackId: track.id, + path: private ? null : settings.downloadPath, + ).toJson()); return true; } @@ -128,6 +170,14 @@ class DownloadManager { static void _startNative(ServiceInstance service) => _startService(ServiceInterface(service: service)); + static void _startIsolate(SendPort sendPort) async { + final receivePort = ReceivePort(); + final service = ServiceInterface(receivePort: receivePort) + ..sendPort = sendPort; + service.send('sendPort', {'s': receivePort.sendPort}); + return _startService(service); + } + static void _startService(ServiceInterface service) => DownloadService(service).run(); } diff --git a/lib/api/download_manager/download_service.dart b/lib/api/download_manager/download_service.dart index 269cead..7ed42c9 100644 --- a/lib/api/download_manager/download_service.dart +++ b/lib/api/download_manager/download_service.dart @@ -1,5 +1,17 @@ +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/download.dart'; import 'package:freezer/api/download_manager/service_interface.dart'; +import 'package:freezer/main.dart'; import 'package:freezer/settings.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:path/path.dart'; + +part 'download_service.freezed.dart'; +part 'download_service.g.dart'; class DownloadService { static const NOTIFICATION_ID = 6969; @@ -12,12 +24,18 @@ class DownloadService { AudioQuality? _downloadQuality; bool useGetURL = false; - void run() { - service.on('addDownloads').listen((event) {}); - service.on('updateQuality').listen((event) { + late String downloadFilename; + + late DeezerAPI _deezerAPI; + + void run() async { + service.on('addDownloads', (event) { + downloadTrack(DownloadInfo.fromJson(event!)); + }); + service.on('updateQuality', (event) { _preferredQuality = AudioQuality.values[event!['q']!]; }); - service.on('updateCapabilities').listen((event) { + service.on('updateCapabilities', (event) { final bool canStreamHQ = event!['canStreamHQ']; final bool canStreamLossless = event['canStreamLossless']; @@ -26,12 +44,67 @@ class DownloadService { _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']; + } + }); + + _deezerAPI = DeezerAPI(await getCookieJar()); + await _deezerAPI.authorize(); + + service.send('ready'); } - void downloadTrack(String trackId) { - // final deezerAudio = DeezerAudio(deezerAPI: deezerAPI, md5origin: md5origin, quality: quality, trackId: trackId, mediaVersion: mediaVersion) - // if (useGetURL) { - // final url = - // } + void downloadTrack(DownloadInfo info) async { + final trackId = info.trackId; + final deezerAudio = DeezerAudio( + deezerAPI: _deezerAPI, + quality: _downloadQuality ?? AudioQuality.MP3_128, + trackId: trackId); + + final track = await _deezerAPI.track(trackId); + final file = File(join( + info.path!, + downloadFilename + .replaceAll('%artist%', track.artists?.join(',') ?? '') + .replaceAll('%title%', track.title!))); + 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; + } + + // 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!'); } } + +@freezed +class DownloadInfo with _$DownloadInfo { + const factory DownloadInfo({required String trackId, String? path}) = + _DownloadInfo; + factory DownloadInfo.fromJson(Map json) => + _$DownloadInfoFromJson(json); +} diff --git a/lib/api/download_manager/service_interface.dart b/lib/api/download_manager/service_interface.dart index f2bf9ac..134ac1e 100644 --- a/lib/api/download_manager/service_interface.dart +++ b/lib/api/download_manager/service_interface.dart @@ -1,21 +1,46 @@ +import 'dart:collection'; import 'dart:isolate'; import 'package:flutter_background_service/flutter_background_service.dart'; +typedef ListenerCallback = void Function(Map? args); + class ServiceInterface { final ReceivePort? receivePort; + late SendPort sendPort; final ServiceInstance? service; + bool _isListening = false; + final _listeners = HashMap(); + ServiceInterface({this.receivePort, this.service}) : assert(receivePort != null || service != null); - Stream?> on(String method) { + void on(String method, ListenerCallback listener) { if (service != null) { - return service!.on(method); + service!.on(method).listen(listener); } - return receivePort! - .where((event) => event['method'] == method) - .map((event) => (event as Map?)?.cast()); + if (!_isListening) { + _isListening = true; + receivePort!.listen((message) { + final method = message['_']; + _listeners[method]!.call(message); + }); + } + + _listeners[method] = listener; + } + + void no(String method) { + _listeners.remove(method); + } + + void send(String method, [Map? args]) { + if (service != null) { + return service!.invoke(method, args); + } + + sendPort!.send({'_': method, if (args != null) ...args}); } } diff --git a/lib/api/importer.dart b/lib/api/importer.dart index a217636..52ef7b0 100644 --- a/lib/api/importer.dart +++ b/lib/api/importer.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/download.dart'; +import 'package:get_it/get_it.dart'; Importer importer = Importer(); @@ -29,6 +30,8 @@ class Importer { int get error => tracks.fold(0, (v, t) => (t.state == TrackImportState.ERROR) ? v + 1 : v); + final deezerAPI = GetIt.instance(); + Importer(); //Start importing wrapper @@ -45,8 +48,8 @@ class Importer { }).toList(); //Create playlist - playlistId = - await deezerAPI.createPlaylist(title, description: description); + playlistId = await DeezerAPI.instance + .createPlaylist(title, description: description); busy = true; done = false; diff --git a/lib/api/paths.dart b/lib/api/paths.dart index 136fce8..68cc71b 100644 --- a/lib/api/paths.dart +++ b/lib/api/paths.dart @@ -61,4 +61,12 @@ class Paths { return (await getTemporaryDirectory()).path; } + + static Future offlineDir() async { + final topDir = Platform.isLinux || Platform.isWindows + ? await dataDirectory() + : (await getExternalStorageDirectory())!.path; + final target = await Directory(path.join(topDir, 'offline')).create(); + return target.path; + } } diff --git a/lib/api/pipe_api.dart b/lib/api/pipe_api.dart index a5e9a63..b2468a8 100644 --- a/lib/api/pipe_api.dart +++ b/lib/api/pipe_api.dart @@ -16,7 +16,7 @@ class PipeAPI { final _logger = Logger('PipeAPI'); - Dio get dio => deezerAPI.dio; + Dio get dio => DeezerAPI.instance.dio; Future authorize({bool force = false}) async { // authorize on pipe.deezer.com diff --git a/lib/api/player/audio_handler.dart b/lib/api/player/audio_handler.dart index cd398a6..c2cc530 100644 --- a/lib/api/player/audio_handler.dart +++ b/lib/api/player/audio_handler.dart @@ -27,7 +27,6 @@ import 'dart:async'; import 'dart:convert'; PlayerHelper playerHelper = PlayerHelper(); -late AudioPlayerTask audioHandler; bool failsafe = false; class AudioPlayerTaskInitArguments { @@ -57,15 +56,6 @@ class AudioPlayerTaskInitArguments { lastFMUsername: settings.lastFMUsername, lastFMPassword: settings.lastFMPassword); } - - static Future loadSettings() async { - final settings = await Settings.load(); - - final deezerAPI = DeezerAPI()..arl = settings.arl; - await deezerAPI.authorize(); - - return from(settings: settings, deezerAPI: deezerAPI); - } } class AudioPlayerTask extends BaseAudioHandler { @@ -145,11 +135,7 @@ class AudioPlayerTask extends BaseAudioHandler { late final LazyBox _box; AudioPlayerTask([AudioPlayerTaskInitArguments? initArgs]) { - if (initArgs == null) { - unawaited(AudioPlayerTaskInitArguments.loadSettings().then(_start)); - return; - } - unawaited(_start(initArgs)); + unawaited(_start(initArgs!)); } Future _start(AudioPlayerTaskInitArguments initArgs) async { diff --git a/lib/api/player/player_helper.dart b/lib/api/player/player_helper.dart index 6347359..66fa1ae 100644 --- a/lib/api/player/player_helper.dart +++ b/lib/api/player/player_helper.dart @@ -8,9 +8,12 @@ import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/main.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/translations.i18n.dart'; +import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; +AudioPlayerTask get audioHandler => GetIt.instance(); + class PlayerHelper { late StreamSubscription _customEventSubscription; late StreamSubscription _mediaItemSubscription; @@ -66,9 +69,9 @@ class PlayerHelper { failsafe = true; final initArgs = AudioPlayerTaskInitArguments.from( - settings: settings, deezerAPI: deezerAPI); + settings: settings, deezerAPI: DeezerAPI.instance); // initialize our audiohandler instance - audioHandler = await AudioService.init( + final audioHandler = await AudioService.init( builder: () => AudioPlayerTask(initArgs), config: AudioServiceConfig( notificationColor: settings.primaryColor, @@ -87,6 +90,8 @@ class PlayerHelper { ), cacheManager: cacheManager, ); + + GetIt.instance.registerSingleton(audioHandler); } Future start() async { @@ -214,7 +219,7 @@ class PlayerHelper { //Play mix by track Future playMix(String trackId, String trackTitle) async { - List tracks = await deezerAPI.playMix(trackId); + List tracks = await DeezerAPI.instance.playMix(trackId); await playFromTrackList( tracks, tracks[0].id, @@ -232,7 +237,8 @@ class PlayerHelper { id: track.id, text: 'Mix based on %s'.i18n.fill([track.title!]), source: 'searchMix')); - List tracks = await deezerAPI.getSearchTrackMix(track.id, false); + List tracks = + await DeezerAPI.instance.getSearchTrackMix(track.id, false); // discard first track (if it is the searched track) if (tracks[0].id == track.id) tracks.removeAt(0); await playFuture; // avoid race conditions @@ -243,7 +249,8 @@ class PlayerHelper { } Future playSearchMix(String trackId, String trackTitle) async { - List tracks = await deezerAPI.getSearchTrackMix(trackId, true); + List tracks = + await DeezerAPI.instance.getSearchTrackMix(trackId, true); await playFromTrackList( tracks, null, // we can avoid passing it, as the index is 0 @@ -309,9 +316,9 @@ class PlayerHelper { //Flow songs cannot be accessed by smart track list call if (stl.id! == 'flow') { - stl.tracks = await deezerAPI.flow(stl.flowConfig); + stl.tracks = await DeezerAPI.instance.flow(stl.flowConfig); } else { - stl = await deezerAPI.smartTrackList(stl.id); + stl = await DeezerAPI.instance.smartTrackList(stl.id); } } QueueSource queueSource = QueueSource( diff --git a/lib/api/player/systray.dart b/lib/api/player/systray.dart index f190d09..c905c9f 100644 --- a/lib/api/player/systray.dart +++ b/lib/api/player/systray.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/services.dart'; import 'package:freezer/api/player/audio_handler.dart'; +import 'package:freezer/api/player/player_helper.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:tray_manager/tray_manager.dart'; diff --git a/lib/api/spotify.dart b/lib/api/spotify.dart index c5c87c0..73ca652 100644 --- a/lib/api/spotify.dart +++ b/lib/api/spotify.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/importer.dart'; import 'package:freezer/settings.dart'; +import 'package:get_it/get_it.dart'; import 'package:html/parser.dart'; import 'package:html/dom.dart' as dom; import 'package:http/http.dart' as http; @@ -13,6 +14,7 @@ import 'dart:io'; import 'package:url_launcher/url_launcher.dart'; class SpotifyScrapper { + static final deezerAPI = GetIt.instance(); //Parse spotify URL to URI (spotify:track:1234) static String? parseUrl(String url) { Uri uri = Uri.parse(url); diff --git a/lib/main.dart b/lib/main.dart index 2af0aa7..ce9a4eb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:audio_service/audio_service.dart'; +import 'package:cookie_jar/cookie_jar.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -13,6 +14,7 @@ import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:freezer/api/cache.dart'; +import 'package:freezer/api/cookie_jar_isar_storage.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/paths.dart'; import 'package:freezer/icons.dart'; @@ -28,6 +30,7 @@ import 'package:freezer/ui/login_screen.dart'; import 'package:freezer/ui/player_screen.dart'; import 'package:freezer/ui/search.dart'; import 'package:freezer/ui/settings_screen.dart'; +import 'package:get_it/get_it.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:i18n_extension/i18n_widget.dart'; import 'package:logging/logging.dart'; @@ -94,7 +97,17 @@ void main() async { ..registerAdapter(NavigationRailAppearanceAdapter()) ..registerAdapter(HiveCacheObjectAdapter(typeId: 35)); // not working? - Hive.init(await Paths.dataDirectory()); + final dataDir = await Paths.dataDirectory(); + + Hive.init(dataDir); + + // photos + cacheManager = CacheManager(Config( + DefaultCacheManager.key, + // cache aggressively + stalePeriod: const Duration(days: 30), + maxNrOfCacheObjects: 5000, + )); //Initialize globals try { @@ -110,16 +123,16 @@ void main() async { exit(1); } downloadManager.init(); - // photos - cacheManager = CacheManager(Config( - DefaultCacheManager.key, - // cache aggressively - stalePeriod: const Duration(days: 30), - maxNrOfCacheObjects: 5000, - )); + // cacheManager = HiveCacheManager( // boxName: 'freezer-images', boxPath: await Paths.cacheDir()); // TODO: WA + final cookieJar = + GetIt.instance.registerSingleton(await getCookieJar()); + final deezerAPI = + GetIt.instance.registerSingleton(DeezerAPI(cookieJar)); + deezerAPI.deezerCountry = settings.deezerCountry; + deezerAPI.deezerLanguage = settings.deezerLanguage; deezerAPI.favoritesPlaylistId = cache.favoritesPlaylistId; Logger.root.onRecord.listen((record) { @@ -138,6 +151,9 @@ void main() async { runApp(const FreezerApp()); } +Future getCookieJar() async => PersistCookieJar( + storage: IsarStorage('cookies', await Paths.dataDirectory())); + class FreezerApp extends StatefulWidget { const FreezerApp({super.key}); @@ -276,6 +292,7 @@ class _LoginMainWrapperState extends State { @override void initState() { if (settings.arl != null) { + final deezerAPI = GetIt.instance(); //Load token on background deezerAPI.arl = settings.arl!; settings.offlineMode = true; @@ -297,7 +314,7 @@ class _LoginMainWrapperState extends State { } Future _logOut() async { - await deezerAPI.logout(); + await GetIt.instance().logout(); setState(() { settings.arl = null; settings.offlineMode = false; @@ -423,13 +440,14 @@ class MainScreenState extends State } void _startPreload(String type) async { - await deezerAPI.authorize(); + await DeezerAPI.instance.authorize(); if (type == 'flow') { await playerHelper.playFromSmartTrackList(SmartTrackList(id: 'flow')); return; } if (type == 'favorites') { - Playlist p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId); + Playlist p = await DeezerAPI.instance + .fullPlaylist(DeezerAPI.instance.favoritesPlaylistId); playerHelper.playFromPlaylist(p, p.tracks![0].id); } } @@ -440,7 +458,7 @@ class MainScreenState extends State await DownloadManager.platform.invokeMethod('getPreloadInfo'); if (info != null) { //Used if started from android auto - await deezerAPI.authorize(); + await DeezerAPI.instance.authorize(); _startPreload(info); } } diff --git a/lib/ui/details_screens.dart b/lib/ui/details_screens.dart index 3a8cd5f..99b4380 100644 --- a/lib/ui/details_screens.dart +++ b/lib/ui/details_screens.dart @@ -34,7 +34,7 @@ class _AlbumDetailsState extends State { //Get album from API, if doesn't have tracks if (album!.tracks == null || album!.tracks!.isEmpty) { try { - Album a = await deezerAPI.album(album!.id); + Album a = await DeezerAPI.instance.album(album!.id); //Preserve library a.library = album!.library; setState(() => album = a); @@ -185,14 +185,15 @@ class _AlbumDetailsState extends State { onPressed: () async { //Add to library if (!album!.library!) { - await deezerAPI.addFavoriteAlbum(album!.id); + await DeezerAPI.instance + .addFavoriteAlbum(album!.id); ScaffoldMessenger.of(context) .snack('Added to library'.i18n); setState(() => album!.library = true); return; } //Remove - await deezerAPI.removeAlbum(album!.id); + await DeezerAPI.instance.removeAlbum(album!.id); ScaffoldMessenger.of(context) .snack('Album removed from library!'.i18n); setState(() => album!.library = false); @@ -278,7 +279,7 @@ class _MakeAlbumOfflineState extends State { onChanged: (v) async { if (v) { //Add to offline - await deezerAPI.addFavoriteAlbum(widget.album!.id); + await DeezerAPI.instance.addFavoriteAlbum(widget.album!.id); downloadManager.addOfflineAlbum(widget.album, private: true); MenuSheet(context).showDownloadStartedToast(); setState(() { @@ -325,7 +326,7 @@ class _ArtistDetailsState extends State { Future _loadArtist(Artist artist) async { //Load artist from api if no albums if ((artist.albums ?? []).isEmpty) { - return await deezerAPI.artist(artist.id); + return await DeezerAPI.instance.artist(artist.id); } return artist; @@ -428,7 +429,8 @@ class _ArtistDetailsState extends State { icon: const Icon(Icons.favorite), label: Text('Library'.i18n), onPressed: () async { - await deezerAPI.addFavoriteArtist(widget.artist.id); + await DeezerAPI.instance + .addFavoriteArtist(widget.artist.id); ScaffoldMessenger.of(context) .snack('Added to library'.i18n); }, @@ -439,7 +441,7 @@ class _ArtistDetailsState extends State { label: Text('Radio'.i18n), onPressed: () async { List tracks = - (await deezerAPI.smartRadio(artist.id))!; + (await DeezerAPI.instance.smartRadio(artist.id))!; playerHelper.playFromTrackList( tracks, tracks[0].id, @@ -593,8 +595,8 @@ class _DiscographyScreenState extends State { //Fetch data List? data; try { - data = await deezerAPI.discographyPage(artist!.id, - start: artist!.albums!.length); + data = await DeezerAPI.instance + .discographyPage(artist!.id, start: artist!.albums!.length); } catch (e) { setState(() { _error = true; @@ -787,7 +789,7 @@ class _PlaylistDetailsState extends State { //Get another page of tracks List? tracks; try { - tracks = await deezerAPI.playlistTracksPage(playlist!.id, pos); + tracks = await DeezerAPI.instance.playlistTracksPage(playlist!.id, pos); } catch (e) { setState(() { _error = true; @@ -816,7 +818,7 @@ class _PlaylistDetailsState extends State { //Preload tracks if (playlist!.tracks!.length < playlist!.trackCount!) { - playlist = await deezerAPI.fullPlaylist(playlist!.id); + playlist = await DeezerAPI.instance.fullPlaylist(playlist!.id); } setState(() => _sort = cache.sorts[index]); } @@ -834,7 +836,7 @@ class _PlaylistDetailsState extends State { //Preload for sorting if (playlist!.tracks!.length < playlist!.trackCount!) { - playlist = await deezerAPI.fullPlaylist(playlist!.id); + playlist = await DeezerAPI.instance.fullPlaylist(playlist!.id); } } @@ -852,7 +854,7 @@ class _PlaylistDetailsState extends State { //Load if no tracks if (playlist!.tracks!.isEmpty) { //Get correct metadata - deezerAPI.playlist(playlist!.id).then((Playlist p) { + DeezerAPI.instance.playlist(playlist!.id).then((Playlist p) { setState(() { playlist = p; }); @@ -987,7 +989,7 @@ class _PlaylistDetailsState extends State { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ MakePlaylistOffline(playlist), - if (playlist!.user!.name != deezerAPI.userName) + if (playlist!.user!.name != DeezerAPI.instance.userName) IconButton( icon: Icon( playlist!.library! @@ -1000,14 +1002,14 @@ class _PlaylistDetailsState extends State { onPressed: () async { //Add to library if (!playlist!.library!) { - await deezerAPI.addPlaylist(playlist!.id); + await DeezerAPI.instance.addPlaylist(playlist!.id); ScaffoldMessenger.of(context) .snack('Added to library'.i18n); setState(() => playlist!.library = true); return; } //Remove - await deezerAPI.removePlaylist(playlist!.id); + await DeezerAPI.instance.removePlaylist(playlist!.id); ScaffoldMessenger.of(context) .snack('Playlist removed from library!'.i18n); setState(() => playlist!.library = false); @@ -1030,7 +1032,8 @@ class _PlaylistDetailsState extends State { onSelected: (SortType s) async { if (playlist!.tracks!.length < playlist!.trackCount!) { //Preload whole playlist - playlist = await deezerAPI.fullPlaylist(playlist!.id); + playlist = + await DeezerAPI.instance.fullPlaylist(playlist!.id); } setState(() => _sort!.type = s); @@ -1097,7 +1100,7 @@ class _PlaylistDetailsState extends State { }, onSecondary: (details) { MenuSheet m = MenuSheet(context); m.defaultTrackMenu(t, details: details, options: [ - if (playlist!.user!.id == deezerAPI.userId) + if (playlist!.user!.id == DeezerAPI.instance.userId) m.removeFromPlaylist(t, playlist) ]); }); @@ -1150,8 +1153,8 @@ class _MakePlaylistOfflineState extends State { if (v) { //Add to offline if (widget.playlist!.user != null && - widget.playlist!.user!.id != deezerAPI.userId) { - await deezerAPI.addPlaylist(widget.playlist!.id); + widget.playlist!.user!.id != DeezerAPI.instance.userId) { + await DeezerAPI.instance.addPlaylist(widget.playlist!.id); } downloadManager.addOfflinePlaylist(widget.playlist, private: true); @@ -1197,7 +1200,7 @@ class _ShowScreenState extends State { //Fetch List? e; try { - e = await deezerAPI.allShowEpisodes(_show!.id); + e = await DeezerAPI.instance.allShowEpisodes(_show!.id); } catch (e) { setState(() { _loading = false; diff --git a/lib/ui/external_link_route.dart b/lib/ui/external_link_route.dart index 557470d..31c121b 100644 --- a/lib/ui/external_link_route.dart +++ b/lib/ui/external_link_route.dart @@ -2,6 +2,7 @@ import 'package:cookie_jar/cookie_jar.dart'; import 'package:flutter/material.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/settings.dart'; +import 'package:get_it/get_it.dart'; import 'package:webview_flutter/webview_flutter.dart'; class ExternalLinkRoute extends StatefulWidget { @@ -45,7 +46,8 @@ class _ExternalLinkRouteState extends State { } Future> _resolveHeaders(Uri uri) async { - List cookies = await cookieJar.loadForRequest(uri); + List cookies = + await GetIt.instance().loadForRequest(uri); print(cookies); return {'Cookie': cookies.join(';')}; } diff --git a/lib/ui/home_screen.dart b/lib/ui/home_screen.dart index 95b44ea..d45380f 100644 --- a/lib/ui/home_screen.dart +++ b/lib/ui/home_screen.dart @@ -148,9 +148,9 @@ class _HomePageWidgetState extends State { //Fetch channel from api try { if (widget.channel == null) { - homePage = await deezerAPI.homePage(); + homePage = await DeezerAPI.instance.homePage(); } else { - homePage = await deezerAPI.getChannel(widget.channel!.target); + homePage = await DeezerAPI.instance.getChannel(widget.channel!.target); } } catch (e) { homePage = null; diff --git a/lib/ui/library.dart b/lib/ui/library.dart index b69d210..02206c0 100644 --- a/lib/ui/library.dart +++ b/lib/ui/library.dart @@ -13,6 +13,7 @@ import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/importer_screen.dart'; import 'package:freezer/ui/tiles.dart'; import 'package:freezer/translations.i18n.dart'; +import 'package:get_it/get_it.dart'; import 'menu.dart'; import '../api/download.dart'; @@ -241,6 +242,8 @@ class _LibraryTracksState extends State { int? trackCount; Sorting? _sort = Sorting(sourceType: SortSourceTypes.TRACKS); + final deezerAPI = DeezerAPI.instance; + Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId!); List get _sorted { @@ -595,7 +598,7 @@ class _LibraryAlbumsState extends State { Future _load() async { if (settings.offlineMode) return; try { - List albums = await deezerAPI.getAlbums(); + List albums = await DeezerAPI.instance.getAlbums(); setState(() => _albums = albums); } catch (e) {} } @@ -799,7 +802,7 @@ class _LibraryArtistsState extends State { //Fetch List? data; try { - data = await deezerAPI.getArtists(); + data = await DeezerAPI.instance.getArtists(); } catch (e) {} //Update UI setState(() { @@ -929,6 +932,8 @@ class _LibraryPlaylistsState extends State { final ScrollController _scrollController = ScrollController(); String _filter = ''; + final deezerAPI = DeezerAPI.instance; + List get _sorted { List playlists = List.from(_playlists! .where((p) => p.title!.toLowerCase().contains(_filter.toLowerCase()))); diff --git a/lib/ui/login_screen.dart b/lib/ui/login_screen.dart index 3908dd8..b5d6139 100644 --- a/lib/ui/login_screen.dart +++ b/lib/ui/login_screen.dart @@ -31,6 +31,7 @@ class LoginWidget extends StatefulWidget { class _LoginWidgetState extends State { late String _arl; String? _error; + final deezerAPI = DeezerAPI.instance; //Initialize deezer etc Future _init() async { diff --git a/lib/ui/lyrics_screen.dart b/lib/ui/lyrics_screen.dart index de8a10f..8fdf6c8 100644 --- a/lib/ui/lyrics_screen.dart +++ b/lib/ui/lyrics_screen.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/pipe_api.dart'; -import 'package:freezer/api/player/audio_handler.dart'; +import 'package:freezer/api/player/player_helper.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:freezer/ui/error.dart'; @@ -38,7 +38,7 @@ class LyricsScreen extends StatelessWidget { } class LyricsWidget extends StatefulWidget { - const LyricsWidget({Key? key}) : super(key: key); + const LyricsWidget({super.key}); @override State createState() => _LyricsWidgetState(); diff --git a/lib/ui/menu.dart b/lib/ui/menu.dart index 3d0fa77..9ec43b7 100644 --- a/lib/ui/menu.dart +++ b/lib/ui/menu.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'package:freezer/api/player/player_helper.dart'; import 'package:freezer/main.dart'; +import 'package:freezer/settings.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:flutter/material.dart'; import 'package:freezer/api/cache.dart'; @@ -15,6 +17,7 @@ import 'package:freezer/ui/cached_image.dart'; import 'package:numberpicker/numberpicker.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:freezer/api/download_manager/download_manager.dart' as newDl; class SliverTrackPersistentHeader extends SliverPersistentHeaderDelegate { final Track track; @@ -267,21 +270,21 @@ class MenuSheet { MenuSheetOption(Text('Play next'.i18n), icon: const Icon(Icons.playlist_play), onTap: () async { //-1 = next - await audioHandler.insertQueueItem(-1, await t.toMediaItem()); + await audioHandler.insertQueueItem(-1, t.toMediaItem()); }); MenuSheetOption addToQueue(Track t) => MenuSheetOption(Text('Add to queue'.i18n), icon: const Icon(Icons.playlist_add), onTap: () async { - await audioHandler.addQueueItem(await t.toMediaItem()); + await audioHandler.addQueueItem(t.toMediaItem()); }); MenuSheetOption addTrackFavorite(Track t) => MenuSheetOption(Text('Add track to favorites'.i18n), icon: const Icon(Icons.favorite), onTap: () async { - await deezerAPI.addFavoriteTrack(t.id); + await DeezerAPI.instance.addFavoriteTrack(t.id); //Make track offline, if favorites are offline - Playlist p = Playlist(id: deezerAPI.favoritesPlaylistId!); + Playlist p = Playlist(id: DeezerAPI.instance.favoritesPlaylistId!); if (await downloadManager.checkOffline(playlist: p)) { downloadManager.addOfflinePlaylist(p); } @@ -294,9 +297,12 @@ class MenuSheet { Text('Download'.i18n), icon: const Icon(Icons.file_download), onTap: () async { - if (await downloadManager.addOfflineTrack(t, - private: false, context: context, isSingleton: true) != - false) showDownloadStartedToast(); + final dl = newDl.DownloadManager(); + await dl.startDebug(); + dl.addOfflineTrack(t, settings.downloadQuality, private: false); + //if (await downloadManager.addOfflineTrack(t, + // private: false, context: context, isSingleton: true) != + // false) showDownloadStartedToast(); }, ); @@ -311,7 +317,7 @@ class MenuSheet { return SelectPlaylistDialog( track: t, callback: (Playlist p) async { - await deezerAPI.addToPlaylist(t.id, p.id); + await DeezerAPI.instance.addToPlaylist(t.id, p.id); //Update the playlist if offline if (await downloadManager.checkOffline(playlist: p)) { downloadManager.addOfflinePlaylist(p); @@ -327,7 +333,7 @@ class MenuSheet { Text('Remove from playlist'.i18n), icon: const Icon(Icons.delete), onTap: () async { - await deezerAPI.removeFromPlaylist(t.id, p!.id); + await DeezerAPI.instance.removeFromPlaylist(t.id, p!.id); ScaffoldMessenger.of(context) .snack('${'Track removed from'.i18n} ${p.title}'); }, @@ -337,9 +343,9 @@ class MenuSheet { Text('Remove favorite'.i18n), icon: const Icon(Icons.delete), onTap: () async { - await deezerAPI.removeFavorite(t.id); + await DeezerAPI.instance.removeFavorite(t.id); //Check if favorites playlist is offline, update it - Playlist p = Playlist(id: deezerAPI.favoritesPlaylistId!); + Playlist p = Playlist(id: DeezerAPI.instance.favoritesPlaylistId!); if (await downloadManager.checkOffline(playlist: p)) { await downloadManager.addOfflinePlaylist(p); } @@ -454,7 +460,7 @@ class MenuSheet { Text('Make offline'.i18n), icon: const Icon(Icons.offline_pin), onTap: () async { - await deezerAPI.addFavoriteAlbum(a.id); + await DeezerAPI.instance.addFavoriteAlbum(a.id); await downloadManager.addOfflineAlbum(a, private: true); showDownloadStartedToast(); @@ -465,7 +471,7 @@ class MenuSheet { Text('Add to library'.i18n), icon: const Icon(Icons.library_music), onTap: () async { - await deezerAPI.addFavoriteAlbum(a.id); + await DeezerAPI.instance.addFavoriteAlbum(a.id); ScaffoldMessenger.of(context).snack('Added to library'.i18n); }, ); @@ -475,7 +481,7 @@ class MenuSheet { Text('Remove album'.i18n), icon: const Icon(Icons.delete), onTap: () async { - await deezerAPI.removeAlbum(a.id); + await DeezerAPI.instance.removeAlbum(a.id); await downloadManager.removeOfflineAlbum(a.id); ScaffoldMessenger.of(context).snack('Album removed'.i18n); if (onRemove != null) onRemove(); @@ -508,7 +514,7 @@ class MenuSheet { Text('Remove from favorites'.i18n), icon: const Icon(Icons.delete), onTap: () async { - await deezerAPI.removeArtist(a.id); + await DeezerAPI.instance.removeArtist(a.id); ScaffoldMessenger.of(context) .snack('Artist removed from library'.i18n); if (onRemove != null) onRemove(); @@ -519,7 +525,7 @@ class MenuSheet { Text('Add to favorites'.i18n), icon: const Icon(Icons.favorite), onTap: () async { - await deezerAPI.addFavoriteArtist(a.id); + await DeezerAPI.instance.addFavoriteArtist(a.id); ScaffoldMessenger.of(context).snack('Added to library'.i18n); }, ); @@ -541,7 +547,7 @@ class MenuSheet { addPlaylistOffline(playlist), downloadPlaylist(playlist), shareTile('playlist', playlist.id), - if (playlist.user!.id == deezerAPI.userId) + if (playlist.user!.id == DeezerAPI.instance.userId) editPlaylist(playlist, onUpdate: onUpdate), ...options ]); @@ -556,12 +562,12 @@ class MenuSheet { Text('Remove from library'.i18n), icon: const Icon(Icons.delete), onTap: () async { - if (p.user!.id!.trim() == deezerAPI.userId) { + if (p.user!.id!.trim() == DeezerAPI.instance.userId) { //Delete playlist if own - await deezerAPI.deletePlaylist(p.id); + await DeezerAPI.instance.deletePlaylist(p.id); } else { //Just remove from library - await deezerAPI.removePlaylist(p.id); + await DeezerAPI.instance.removePlaylist(p.id); } downloadManager.removeOfflinePlaylist(p.id); if (onRemove != null) onRemove(); @@ -572,7 +578,7 @@ class MenuSheet { Text('Add playlist to library'.i18n), icon: const Icon(Icons.favorite), onTap: () async { - await deezerAPI.addPlaylist(p.id); + await DeezerAPI.instance.addPlaylist(p.id); ScaffoldMessenger.of(context).snack('Added playlist to library'.i18n); }, ); @@ -582,7 +588,7 @@ class MenuSheet { icon: const Icon(Icons.offline_pin), onTap: () async { //Add to library - await deezerAPI.addPlaylist(p.id); + await DeezerAPI.instance.addPlaylist(p.id); downloadManager.addOfflinePlaylist(p, private: true); showDownloadStartedToast(); @@ -825,7 +831,7 @@ class _SelectPlaylistDialogState extends State { return AlertDialog( title: Text('Select playlist'.i18n), content: FutureBuilder( - future: deezerAPI.getPlaylists(), + future: DeezerAPI.instance.getPlaylists(), builder: (context, snapshot) { if (snapshot.hasError) { const SizedBox( @@ -957,7 +963,7 @@ class _CreatePlaylistDialogState extends State { onPressed: () async { if (edit) { //Update - await deezerAPI.updatePlaylist(widget.playlist!.id, + await DeezerAPI.instance.updatePlaylist(widget.playlist!.id, _titleController!.value.text, _descController!.value.text, status: _playlistType); ScaffoldMessenger.of(context).snack('Playlist updated!'.i18n); @@ -966,7 +972,7 @@ class _CreatePlaylistDialogState extends State { if (widget.tracks != null) { tracks = widget.tracks!.map((t) => t!.id).toList(); } - await deezerAPI.createPlaylist(_title, + await DeezerAPI.instance.createPlaylist(_title, status: _playlistType, description: _description, trackIds: tracks); diff --git a/lib/ui/player_bar.dart b/lib/ui/player_bar.dart index 246fe70..79f7359 100644 --- a/lib/ui/player_bar.dart +++ b/lib/ui/player_bar.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; +import 'package:freezer/api/player/player_helper.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:rxdart/rxdart.dart'; @@ -15,12 +16,12 @@ class PlayerBar extends StatelessWidget { final Color? backgroundColor; final FocusNode? focusNode; const PlayerBar({ - Key? key, + super.key, this.onTap, this.shouldHaveHero = true, this.backgroundColor, this.focusNode, - }) : super(key: key); + }); final double iconSize = 28; @@ -193,12 +194,12 @@ class PlayPauseButton extends StatefulWidget { final Color? color; const PlayPauseButton( this.size, { - Key? key, + super.key, this.filled = false, this.material3 = true, this.color, this.iconColor, - }) : super(key: key); + }); @override State createState() => _PlayPauseButtonState(); diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index 81648a0..63591d2 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -13,6 +13,7 @@ import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/player/audio_handler.dart'; +import 'package:freezer/api/player/player_helper.dart'; import 'package:freezer/main.dart'; import 'package:freezer/page_routes/fade.dart'; import 'package:freezer/settings.dart'; @@ -750,12 +751,12 @@ class _FavoriteButtonState extends State { if (cache.checkTrackFavorite(Track.fromMediaItem(mediaItem))) { //Remove from library setState(() => cache.libraryTracks.remove(mediaItem.id)); - await deezerAPI.removeFavorite(mediaItem.id); + await DeezerAPI.instance.removeFavorite(mediaItem.id); await cache.save(); } else { //Add setState(() => cache.libraryTracks.add(mediaItem.id)); - await deezerAPI.addFavoriteTrack(mediaItem.id); + await DeezerAPI.instance.addFavoriteTrack(mediaItem.id); await cache.save(); } }, @@ -1266,8 +1267,8 @@ class BottomBarControls extends StatelessWidget { ), iconSize: iconSize, onPressed: () async { - unawaited( - deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id)); + unawaited(DeezerAPI.instance + .dislikeTrack(audioHandler.mediaItem.value!.id)); if (playerHelper.queueIndex < audioHandler.queue.value.length - 1) { audioHandler.skipToNext(); diff --git a/lib/ui/queue_screen.dart b/lib/ui/queue_screen.dart index fd7063b..1e0958e 100644 --- a/lib/ui/queue_screen.dart +++ b/lib/ui/queue_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/player/audio_handler.dart'; +import 'package:freezer/api/player/player_helper.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/tiles.dart'; diff --git a/lib/ui/search.dart b/lib/ui/search.dart index 16cf762..1ef50f9 100644 --- a/lib/ui/search.dart +++ b/lib/ui/search.dart @@ -22,6 +22,7 @@ import '../api/definitions.dart'; import 'error.dart'; FutureOr openScreenByURL(BuildContext context, String url) async { + final deezerAPI = DeezerAPI.instance; DeezerLinkResponse? res = await deezerAPI.parseLink(Uri.parse(url)); if (res == null) return; @@ -138,8 +139,8 @@ class _SearchScreenState extends State { final List? suggestions; try { _searchCancelToken = CancelToken(); - suggestions = await deezerAPI.searchSuggestions(_controller.text, - cancelToken: _searchCancelToken); + suggestions = await DeezerAPI.instance + .searchSuggestions(_controller.text, cancelToken: _searchCancelToken); } on DioException catch (e) { if (e.type != DioExceptionType.cancel) rethrow; return; @@ -456,7 +457,7 @@ class _SearchResultsScreenState extends State { if (widget.offline ?? false) { results = await downloadManager.search(widget.query); } else { - results = await deezerAPI.search(widget.query); + results = await DeezerAPI.instance.search(widget.query); } setState(() { _results = results; @@ -896,8 +897,10 @@ class _SearchResultsScreenState extends State { onTap: () async { //Load entire show, then play List episodes = - (await deezerAPI.allShowEpisodes( - episode.show!.id))!; + (await DeezerAPI + .instance + .allShowEpisodes( + episode.show!.id))!; await playerHelper.playShowEpisode( episode.show!, episodes, index: episodes.indexWhere( @@ -1051,7 +1054,7 @@ class EpisodeListScreen extends StatelessWidget { onTap: () async { //Load entire show, then play List episodes = - (await deezerAPI.allShowEpisodes(e.show!.id))!; + (await DeezerAPI.instance.allShowEpisodes(e.show!.id))!; await playerHelper.playShowEpisode(e.show!, episodes, index: episodes.indexWhere((ep) => e.id == ep.id)); }, diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index 8d3c4b6..a7f5a7c 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:freezer/api/definitions.dart'; +import 'package:freezer/api/player/player_helper.dart'; import 'package:freezer/api/player/systray.dart'; import 'package:freezer/icons.dart'; import 'package:freezer/ui/login_on_other_device.dart'; @@ -876,10 +877,13 @@ class _DeezerSettingsState extends State { title: Text(ContentLanguage.all[i].name), subtitle: Text(ContentLanguage.all[i].code), onTap: () async { - setState(() => settings.deezerLanguage = - ContentLanguage.all[i].code); + settings.deezerLanguage = + ContentLanguage.all[i].code; + setState(() {}); await settings.save(); - deezerAPI.updateHeaders(); + DeezerAPI.instance.deezerLanguage = + settings.deezerLanguage; + DeezerAPI.instance.updateHeaders(); Navigator.of(context).pop(); }, )), @@ -898,9 +902,10 @@ class _DeezerSettingsState extends State { titlePadding: const EdgeInsets.all(8.0), isSearchable: true, onValuePicked: (Country country) { - setState( - () => settings.deezerCountry = country.isoCode); - deezerAPI.updateHeaders(); + DeezerAPI.instance.deezerCountry = + settings.deezerCountry = country.isoCode; + setState(() {}); + DeezerAPI.instance.updateHeaders(); settings.save(); }, )); @@ -1390,7 +1395,7 @@ class _GeneralSettingsState extends State { showDialog( context: context, builder: (context) { - deezerAPI.authorize().then((v) { + DeezerAPI.instance.authorize().then((v) { if (v) { setState(() => settings.offlineMode = false); } else { diff --git a/lib/ui/tiles.dart b/lib/ui/tiles.dart index bfe40e3..37d49b7 100644 --- a/lib/ui/tiles.dart +++ b/lib/ui/tiles.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/download.dart'; import 'package:freezer/api/player/audio_handler.dart'; +import 'package:freezer/api/player/player_helper.dart'; import 'package:freezer/icons.dart'; import 'package:freezer/main.dart'; import 'package:freezer/translations.i18n.dart'; @@ -246,7 +247,7 @@ class PlaylistTile extends StatelessWidget { if (playlist!.user == null || playlist!.user!.name == null || playlist!.user!.name == '' || - playlist!.user!.id == deezerAPI.userId) { + playlist!.user!.id == DeezerAPI.instance.userId) { if (playlist!.trackCount == null) return ''; return '${playlist!.trackCount} ${'Tracks'.i18n}'; } @@ -341,8 +342,9 @@ class PlaylistCardTile extends StatelessWidget { left: 8.0, child: PlayItemButton( onTap: () async { - final Playlist fullPlaylist = - await deezerAPI.fullPlaylist(playlist!.id); + final Playlist fullPlaylist = await DeezerAPI + .instance + .fullPlaylist(playlist!.id); await playerHelper.playFromPlaylist(fullPlaylist); }, )) @@ -639,7 +641,8 @@ class AlbumCard extends StatelessWidget { left: 8.0, child: PlayItemButton( onTap: () async { - final fullAlbum = await deezerAPI.album(album.id); + final fullAlbum = + await DeezerAPI.instance.album(album.id); await playerHelper.playFromAlbum(fullAlbum); }, ), diff --git a/lib/utils.dart b/lib/utils.dart index 81c9501..36d02e7 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -27,4 +27,21 @@ class Utils { } return bi; } + + /// FOR ISAR, FROM ISAR DOCUMENTATION + /// FNV-1a 64bit hash algorithm optimized for Dart Strings + static int fastHash(String string) { + var hash = 0xcbf29ce484222325; + + var i = 0; + while (i < string.length) { + final codeUnit = string.codeUnitAt(i++); + hash ^= codeUnit >> 8; + hash *= 0x100000001b3; + hash ^= codeUnit & 0xFF; + hash *= 0x100000001b3; + } + + return hash; + } } diff --git a/pubspec.lock b/pubspec.lock index 31b1aac..326eda8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -567,6 +567,22 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.4" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" + url: "https://pub.dev" + source: hosted + version: "2.4.7" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -575,6 +591,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 + url: "https://pub.dev" + source: hosted + version: "7.6.7" gettext_parser: dependency: transitive description: @@ -789,7 +813,7 @@ packages: path: "../just_audio_media_kit" relative: true source: path - version: "2.0.1" + version: "2.0.0" just_audio_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d4b9684..77f0091 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,6 +107,8 @@ dependencies: tray_manager: ^0.2.1 window_manager: ^0.3.8 + get_it: ^7.6.7 + freezed_annotation: ^2.4.1 #deezcryptor: #path: deezcryptor/ @@ -119,6 +121,7 @@ dev_dependencies: hive_generator: ^2.0.0 flutter_lints: ^3.0.1 isar_generator: ^3.1.0+1 + freezed: ^2.4.7 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec