diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 14773156..8dc66955 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -18,6 +18,7 @@ import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/isolates/yt_explode.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/models/video_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; @@ -114,7 +115,8 @@ class SiblingTracksSheet extends HookConsumerWidget { activeSourceInfo, ); } else { - final resultsYt = await youtubeClient.search.search(searchTerm.trim()); + final resultsYt = + await IsolatedYoutubeExplode.instance.search(searchTerm.trim()); final searchResults = await Future.wait( resultsYt diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 0f9efce1..7d624dda 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -11,6 +11,7 @@ import 'package:spotube/provider/audio_player/audio_player_streams.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/isolates/yt_explode.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/utils/platform.dart'; @@ -40,6 +41,10 @@ class UserPreferencesNotifier extends Notifier { ..where((tbl) => tbl.id.equals(0))) .getSingle(); + if (state.audioSource == AudioSource.youtube) { + await IsolatedYoutubeExplode.initialize(); + } + final subscription = (db.select(db.preferencesTable) ..where((tbl) => tbl.id.equals(0))) .watchSingle() @@ -207,6 +212,10 @@ class UserPreferencesNotifier extends Notifier { void setAudioSource(AudioSource type) { setData(PreferencesTableCompanion(audioSource: Value(type))); + + if (type != AudioSource.youtube && IsolatedYoutubeExplode.isInitialized) { + IsolatedYoutubeExplode.instance.dispose(); + } } void setSystemTitleBar(bool isSystemTitleBar) { diff --git a/lib/services/isolates/yt_explode.dart b/lib/services/isolates/yt_explode.dart new file mode 100644 index 00000000..f8b6dc70 --- /dev/null +++ b/lib/services/isolates/yt_explode.dart @@ -0,0 +1,121 @@ +import 'dart:async'; +import 'dart:isolate'; + +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; + +/// A Isolate wrapper for the YoutubeExplode class +/// It contains methods that are computationally expensive +class IsolatedYoutubeExplode { + final Isolate _isolate; + final SendPort _sendPort; + final ReceivePort _receivePort; + + IsolatedYoutubeExplode._( + Isolate isolate, + ReceivePort receivePort, + SendPort sendPort, + ) : _isolate = isolate, + _receivePort = receivePort, + _sendPort = sendPort; + + static IsolatedYoutubeExplode? _instance; + + static IsolatedYoutubeExplode get instance => _instance!; + + static bool get isInitialized => _instance != null; + + static Future initialize() async { + if (_instance != null) { + return; + } + + final completer = Completer(); + + final receivePort = ReceivePort(); + + /// Listen for the main isolate to set the main port + final subscription = receivePort.listen((message) { + if (message is SendPort) { + completer.complete(message); + } + }); + + final isolate = await Isolate.spawn(_isolateEntry, receivePort.sendPort); + + _instance = IsolatedYoutubeExplode._( + isolate, + receivePort, + await completer.future, + ); + + if (completer.isCompleted) { + subscription.cancel(); + } + } + + static void _isolateEntry(SendPort mainSendPort) { + final receivePort = ReceivePort(); + final youtubeExplode = YoutubeExplode(); + + /// Send the main port to the main isolate + mainSendPort.send(receivePort.sendPort); + + receivePort.listen((message) async { + final SendPort replyPort = message[0]; + final String methodName = message[1]; + final List arguments = message[2]; + + // Run the requested method on YoutubeExplode + var result = switch (methodName) { + "search" => youtubeExplode.search + .search(arguments[0] as String, filter: TypeFilters.video) + .then((s) => s.toList()), + "video" => youtubeExplode.videos.get(arguments[0] as String), + "manifest" => youtubeExplode.videos.streamsClient.getManifest( + arguments[0] as String, + requireWatchPage: false, + ytClients: [ + YoutubeApiClient.mediaConnect, + YoutubeApiClient.ios, + YoutubeApiClient.android, + YoutubeApiClient.mweb, + YoutubeApiClient.tv, + ], + ), + _ => throw ArgumentError('Invalid method name: $methodName'), + }; + + replyPort.send(await result); + }); + } + + Future _runMethod(String methodName, List args) { + final completer = Completer(); + final responsePort = ReceivePort(); + + responsePort.listen((message) { + completer.complete(message as T); + responsePort.close(); + }); + + _sendPort.send([responsePort.sendPort, methodName, args]); + return completer.future; + } + + Future> search(String query) async { + return _runMethod>("search", [query]); + } + + Future