diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index ec463ed86..e00410116 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; @@ -32,6 +33,14 @@ class SiblingTracksSheet extends HookConsumerWidget { topRight: Radius.circular(10), ); + useEffect(() { + if (playlist?.activeTrack is SpotubeTrack && + (playlist?.activeTrack as SpotubeTrack).siblings.isEmpty) { + playlistNotifier.populateSibling(); + } + return null; + }, [playlist?.activeTrack]); + return BackdropFilter( filter: ImageFilter.blur( sigmaX: 12.0, diff --git a/lib/components/shared/image/universal_image.dart b/lib/components/shared/image/universal_image.dart index 4e222a1b2..74cc76bec 100644 --- a/lib/components/shared/image/universal_image.dart +++ b/lib/components/shared/image/universal_image.dart @@ -35,6 +35,8 @@ class UniversalImage extends HookWidget { cacheKey: path, scale: scale, ); + } else if (path.startsWith("assets/")) { + return AssetImage(path); } else if (Uri.tryParse(path) != null) { return FileImage(File(path), scale: scale); } diff --git a/lib/models/spotube_track.dart b/lib/models/spotube_track.dart index e682cf239..087a09290 100644 --- a/lib/models/spotube_track.dart +++ b/lib/models/spotube_track.dart @@ -1,3 +1,7 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/video.dart'; import 'package:spotube/extensions/album_simple.dart'; @@ -97,8 +101,8 @@ class SpotubeTrack extends Track { VideoSearchList videos = await PrimitiveUtils.raceMultiple( () => youtube.search.search("${artists.join(", ")} - $title"), ); - siblings = videos.take(10).toList(); - ytVideo = videos.where((video) => !video.isLive).first; + siblings = videos.where((video) => !video.isLive).take(10).toList(); + ytVideo = siblings.first; } StreamManifest trackManifest = await PrimitiveUtils.raceMultiple( @@ -132,6 +136,36 @@ class SpotubeTrack extends Track { ); } + if (preferences.androidBytesPlay) { + await DefaultCacheManager().getFileFromCache(track.id!).then( + (file) async { + if (file != null) return file.file; + final List bytesStore = []; + final bytesFuture = Completer(); + + youtube.videos.streams.get(chosenStreamInfo).listen( + (data) { + bytesStore.addAll(data); + }, + onDone: () { + bytesFuture.complete(Uint8List.fromList(bytesStore)); + }, + onError: (e) { + bytesFuture.completeError(e); + }, + ); + + final cached = await DefaultCacheManager().putFile( + track.id!, + await bytesFuture.future, + fileExtension: chosenStreamInfo.codec.mimeType.split("/").last, + ); + + return cached; + }, + ); + } + return SpotubeTrack.fromTrack( track: track, ytTrack: ytVideo, @@ -197,6 +231,37 @@ class SpotubeTrack extends Track { }, ); } + + if (preferences.androidBytesPlay) { + await DefaultCacheManager().getFileFromCache(id!).then( + (file) async { + if (file != null) return file.file; + final List bytesStore = []; + final bytesFuture = Completer(); + + youtube.videos.streams.get(chosenStreamInfo).listen( + (data) { + bytesStore.addAll(data); + }, + onDone: () { + bytesFuture.complete(Uint8List.fromList(bytesStore)); + }, + onError: (e) { + bytesFuture.completeError(e); + }, + ); + + final cached = await DefaultCacheManager().putFile( + id!, + await bytesFuture.future, + fileExtension: chosenStreamInfo.codec.mimeType.split("/").last, + ); + + return cached; + }, + ); + } + return SpotubeTrack.fromTrack( track: this, ytTrack: video, @@ -224,6 +289,34 @@ class SpotubeTrack extends Track { ); } + Future populatedCopy() async { + if (this.siblings.isNotEmpty) return this; + final artists = (this.artists ?? []) + .map((ar) => ar.name) + .toList() + .whereNotNull() + .toList(); + + final title = ServiceUtils.getTitle( + name!, + artists: artists, + onlyCleanArtist: true, + ).trim(); + VideoSearchList videos = await PrimitiveUtils.raceMultiple( + () => youtube.search.search("${artists.join(", ")} - $title"), + ); + + final siblings = videos.where((video) => !video.isLive).take(10).toList(); + + return SpotubeTrack.fromTrack( + track: this, + ytTrack: ytTrack, + ytUri: ytUri, + skipSegments: skipSegments, + siblings: siblings, + ); + } + Map toJson() { return { "album": album?.toJson(), diff --git a/lib/provider/playlist_queue_provider.dart b/lib/provider/playlist_queue_provider.dart index 70e03e9c5..c359be4cc 100644 --- a/lib/provider/playlist_queue_provider.dart +++ b/lib/provider/playlist_queue_provider.dart @@ -2,7 +2,6 @@ import 'package:audio_service/audio_service.dart'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:path/path.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/spotube_track.dart'; @@ -81,6 +80,8 @@ class PlaylistQueue { class PlaylistQueueNotifier extends PersistedStateNotifier { final Ref ref; + MobileAudioService? mobileService; + LinuxAudioService? linuxService; static final provider = StateNotifierProvider( @@ -94,6 +95,19 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { } void configure() async { + if (kIsMobile || kIsMacOS) { + mobileService = await AudioService.init( + builder: () => MobileAudioService(ref), + config: const AudioServiceConfig( + androidNotificationChannelId: 'com.krtirtho.Spotube', + androidNotificationChannelName: 'Spotube', + androidNotificationOngoing: true, + ), + ); + } + if (kIsLinux) { + linuxService = LinuxAudioService(ref); + } addListener((state) { linuxService?.player.updateProperties(); }); @@ -137,9 +151,6 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { UserPreferences get preferences => ref.read(userPreferencesProvider); BlackListNotifier get blacklist => ref.read(BlackListNotifier.provider.notifier); - LinuxAudioService? get linuxService => ref.read(linuxAudioServiceProvider); - Future get mobileService => - ref.read(mobileAudioServiceProvider); bool get isLoaded => state != null; bool get isShuffled => _tempTracks.isNotEmpty; @@ -205,14 +216,37 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { await play(); } + Future populateSibling() async { + if (!isLoaded || state!.isLoading) return; + final tracks = state!.tracks.toList(); + final track = await (state!.activeTrack as SpotubeTrack).populatedCopy(); + tracks[state!.active] = track; + state = state!.copyWith(tracks: Set.from(tracks)); + } + Future play() async { if (!isLoaded) return; - pause(); - (await mobileService)?.session?.setActive(true); + await pause(); + await mobileService?.session?.setActive(true); + final mediaItem = MediaItem( + id: state!.activeTrack.id!, + title: state!.activeTrack.name!, + album: state!.activeTrack.album?.name, + artist: TypeConversionUtils.artists_X_String( + state!.activeTrack.artists ?? []), + artUri: Uri.parse( + TypeConversionUtils.image_X_UrlString( + state!.activeTrack.album?.images, + placeholder: ImagePlaceholder.online, + ), + ), + duration: state!.activeTrack.duration, + ); + mobileService?.addItem(mediaItem); if (state!.activeTrack is LocalTrack) { await audioPlayer.play( DeviceFileSource((state!.activeTrack as LocalTrack).path), - mode: PlayerMode.lowLatency, + mode: PlayerMode.mediaPlayer, ); return; } @@ -224,47 +258,22 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { ); state = state!.copyWith(tracks: Set.from(tracks)); } - (await mobileService)?.addItem( - MediaItem( - id: state!.activeTrack.id!, - title: state!.activeTrack.name!, - album: state!.activeTrack.album?.name, - artist: TypeConversionUtils.artists_X_String( - state!.activeTrack.artists ?? []), - artUri: Uri.parse( - TypeConversionUtils.image_X_UrlString( - state!.activeTrack.album?.images, - placeholder: ImagePlaceholder.online, - ), - ), - duration: (state!.activeTrack as SpotubeTrack).ytTrack.duration, - ), - ); - if (preferences.androidBytesPlay) { - final cached = await DefaultCacheManager() - .getFileFromCache(state!.activeTrack.id!) - .then( - (file) async { - if (file != null) return file.file; - final downloaded = await DefaultCacheManager() - .downloadFile((state!.activeTrack as SpotubeTrack).ytUri); - final cached = await DefaultCacheManager().putFile( - state!.activeTrack.id!, - await downloaded.file.readAsBytes(), - fileExtension: extension(downloaded.file.path).replaceAll('.', ''), - ); - await DefaultCacheManager().removeFile(downloaded.originalUrl); - return cached; - }, - ); + + mobileService?.addItem(mediaItem.copyWith( + duration: (state!.activeTrack as SpotubeTrack).ytTrack.duration, + )); + + final cached = + await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!); + if (preferences.androidBytesPlay && cached != null) { await audioPlayer.play( - DeviceFileSource(cached.path), - mode: PlayerMode.lowLatency, + DeviceFileSource(cached.file.path), + mode: PlayerMode.mediaPlayer, ); } else { await audioPlayer.play( UrlSource((state!.activeTrack as SpotubeTrack).ytUri), - mode: PlayerMode.lowLatency, + mode: PlayerMode.mediaPlayer, ); } } @@ -299,7 +308,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { } Future stop() async { - (await mobileService)?.session?.setActive(false); + (mobileService)?.session?.setActive(false); state = null; _tempTracks = {}; return audioPlayer.stop(); @@ -407,21 +416,3 @@ class VolumeProvider extends PersistedStateNotifier { return {'volume': state}; } } - -final linuxAudioServiceProvider = Provider((ref) { - if (!kIsLinux) return null; - return LinuxAudioService(ref); -}); - -final mobileAudioServiceProvider = - Provider>((ref) async { - if (!kIsMobile) return null; - return AudioService.init( - builder: () => MobileAudioService(ref), - config: const AudioServiceConfig( - androidNotificationChannelId: 'com.krtirtho.Spotube', - androidNotificationChannelName: 'Spotube', - androidNotificationOngoing: true, - ), - ); -});