fix: pre downloading not working properly, audio service circular deps and sibling not loading for backend track

This commit is contained in:
Kingkor Roy Tirtho 2023-02-02 19:53:35 +06:00
parent 312f7fbe77
commit 3ccb525260
4 changed files with 159 additions and 64 deletions

View File

@ -1,6 +1,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart'; import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
@ -32,6 +33,14 @@ class SiblingTracksSheet extends HookConsumerWidget {
topRight: Radius.circular(10), topRight: Radius.circular(10),
); );
useEffect(() {
if (playlist?.activeTrack is SpotubeTrack &&
(playlist?.activeTrack as SpotubeTrack).siblings.isEmpty) {
playlistNotifier.populateSibling();
}
return null;
}, [playlist?.activeTrack]);
return BackdropFilter( return BackdropFilter(
filter: ImageFilter.blur( filter: ImageFilter.blur(
sigmaX: 12.0, sigmaX: 12.0,

View File

@ -35,6 +35,8 @@ class UniversalImage extends HookWidget {
cacheKey: path, cacheKey: path,
scale: scale, scale: scale,
); );
} else if (path.startsWith("assets/")) {
return AssetImage(path);
} else if (Uri.tryParse(path) != null) { } else if (Uri.tryParse(path) != null) {
return FileImage(File(path), scale: scale); return FileImage(File(path), scale: scale);
} }

View File

@ -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:spotify/spotify.dart';
import 'package:spotube/extensions/video.dart'; import 'package:spotube/extensions/video.dart';
import 'package:spotube/extensions/album_simple.dart'; import 'package:spotube/extensions/album_simple.dart';
@ -97,8 +101,8 @@ class SpotubeTrack extends Track {
VideoSearchList videos = await PrimitiveUtils.raceMultiple( VideoSearchList videos = await PrimitiveUtils.raceMultiple(
() => youtube.search.search("${artists.join(", ")} - $title"), () => youtube.search.search("${artists.join(", ")} - $title"),
); );
siblings = videos.take(10).toList(); siblings = videos.where((video) => !video.isLive).take(10).toList();
ytVideo = videos.where((video) => !video.isLive).first; ytVideo = siblings.first;
} }
StreamManifest trackManifest = await PrimitiveUtils.raceMultiple( 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<int> bytesStore = [];
final bytesFuture = Completer<Uint8List>();
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( return SpotubeTrack.fromTrack(
track: track, track: track,
ytTrack: ytVideo, 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<int> bytesStore = [];
final bytesFuture = Completer<Uint8List>();
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( return SpotubeTrack.fromTrack(
track: this, track: this,
ytTrack: video, ytTrack: video,
@ -224,6 +289,34 @@ class SpotubeTrack extends Track {
); );
} }
Future<SpotubeTrack> 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<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
"album": album?.toJson(), "album": album?.toJson(),

View File

@ -2,7 +2,6 @@ import 'package:audio_service/audio_service.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/models/spotube_track.dart';
@ -81,6 +80,8 @@ class PlaylistQueue {
class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> { class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
final Ref ref; final Ref ref;
MobileAudioService? mobileService;
LinuxAudioService? linuxService;
static final provider = static final provider =
StateNotifierProvider<PlaylistQueueNotifier, PlaylistQueue?>( StateNotifierProvider<PlaylistQueueNotifier, PlaylistQueue?>(
@ -94,6 +95,19 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
} }
void configure() async { 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) { addListener((state) {
linuxService?.player.updateProperties(); linuxService?.player.updateProperties();
}); });
@ -137,9 +151,6 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
UserPreferences get preferences => ref.read(userPreferencesProvider); UserPreferences get preferences => ref.read(userPreferencesProvider);
BlackListNotifier get blacklist => BlackListNotifier get blacklist =>
ref.read(BlackListNotifier.provider.notifier); ref.read(BlackListNotifier.provider.notifier);
LinuxAudioService? get linuxService => ref.read(linuxAudioServiceProvider);
Future<MobileAudioService?> get mobileService =>
ref.read(mobileAudioServiceProvider);
bool get isLoaded => state != null; bool get isLoaded => state != null;
bool get isShuffled => _tempTracks.isNotEmpty; bool get isShuffled => _tempTracks.isNotEmpty;
@ -205,14 +216,37 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
await play(); await play();
} }
Future<void> 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<void> play() async { Future<void> play() async {
if (!isLoaded) return; if (!isLoaded) return;
pause(); await pause();
(await mobileService)?.session?.setActive(true); 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 ?? <ArtistSimple>[]),
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) { if (state!.activeTrack is LocalTrack) {
await audioPlayer.play( await audioPlayer.play(
DeviceFileSource((state!.activeTrack as LocalTrack).path), DeviceFileSource((state!.activeTrack as LocalTrack).path),
mode: PlayerMode.lowLatency, mode: PlayerMode.mediaPlayer,
); );
return; return;
} }
@ -224,47 +258,22 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
); );
state = state!.copyWith(tracks: Set.from(tracks)); state = state!.copyWith(tracks: Set.from(tracks));
} }
(await mobileService)?.addItem(
MediaItem( mobileService?.addItem(mediaItem.copyWith(
id: state!.activeTrack.id!, duration: (state!.activeTrack as SpotubeTrack).ytTrack.duration,
title: state!.activeTrack.name!, ));
album: state!.activeTrack.album?.name,
artist: TypeConversionUtils.artists_X_String( final cached =
state!.activeTrack.artists ?? <ArtistSimple>[]), await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!);
artUri: Uri.parse( if (preferences.androidBytesPlay && cached != null) {
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;
},
);
await audioPlayer.play( await audioPlayer.play(
DeviceFileSource(cached.path), DeviceFileSource(cached.file.path),
mode: PlayerMode.lowLatency, mode: PlayerMode.mediaPlayer,
); );
} else { } else {
await audioPlayer.play( await audioPlayer.play(
UrlSource((state!.activeTrack as SpotubeTrack).ytUri), UrlSource((state!.activeTrack as SpotubeTrack).ytUri),
mode: PlayerMode.lowLatency, mode: PlayerMode.mediaPlayer,
); );
} }
} }
@ -299,7 +308,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
} }
Future<void> stop() async { Future<void> stop() async {
(await mobileService)?.session?.setActive(false); (mobileService)?.session?.setActive(false);
state = null; state = null;
_tempTracks = {}; _tempTracks = {};
return audioPlayer.stop(); return audioPlayer.stop();
@ -407,21 +416,3 @@ class VolumeProvider extends PersistedStateNotifier<double> {
return {'volume': state}; return {'volume': state};
} }
} }
final linuxAudioServiceProvider = Provider<LinuxAudioService?>((ref) {
if (!kIsLinux) return null;
return LinuxAudioService(ref);
});
final mobileAudioServiceProvider =
Provider<Future<MobileAudioService?>>((ref) async {
if (!kIsMobile) return null;
return AudioService.init(
builder: () => MobileAudioService(ref),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.krtirtho.Spotube',
androidNotificationChannelName: 'Spotube',
androidNotificationOngoing: true,
),
);
});