Skip to content

Commit

Permalink
refactor(playback): new immutable queue based playback manager
Browse files Browse the repository at this point in the history
Dropping support for search format, track match algorithm in favor of server track cache and alternative track source
  • Loading branch information
KRTirtho committed Feb 2, 2023
1 parent ad90c11 commit 312f7fb
Show file tree
Hide file tree
Showing 44 changed files with 1,398 additions and 1,416 deletions.
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@ flutter {
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:multidex:1.0.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'com.android.support:multidex:2.0.1'
}
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.7.21'
repositories {
google()
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
42 changes: 20 additions & 22 deletions lib/collections/intents.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/player/player_controls.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/playback_provider.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart';

Expand All @@ -23,23 +22,22 @@ class PlayPauseAction extends Action<PlayPauseIntent> {
if (PlayerControls.focusNode.canRequestFocus) {
PlayerControls.focusNode.requestFocus();
}
final playback = intent.ref.read(playbackProvider);
if (playback.track == null) {
final playlist = intent.ref.read(PlaylistQueueNotifier.provider);
final playlistNotifier = intent.ref.read(PlaylistQueueNotifier.notifier);
if (playlist == null) {
return null;
} else if (playback.track != null &&
playback.currentDuration == Duration.zero &&
await playback.player.getCurrentPosition() == Duration.zero) {
if (playback.track!.ytUri.startsWith("http")) {
final track = Track.fromJson(playback.track!.toJson());
playback.track = null;
await playback.play(track);
} else {
final track = playback.track;
playback.track = null;
await playback.play(track!);
}
} else if (!PlaylistQueueNotifier.isPlaying) {
// if (playlist.activeTrack is SpotubeTrack &&
// (playlist.activeTrack as SpotubeTrack).ytUri.startsWith("http")) {
// final track =
// Track.fromJson((playlist.activeTrack as SpotubeTrack).toJson());

// await playlistNotifier.play(track);
// } else {
// }
await playlistNotifier.play();
} else {
await playback.togglePlayPause();
await playlistNotifier.pause();
}
return null;
}
Expand Down Expand Up @@ -102,9 +100,9 @@ class SeekIntent extends Intent {
class SeekAction extends Action<SeekIntent> {
@override
invoke(intent) async {
final playback = intent.ref.read(playbackProvider);
if ((playback.playlist == null && playback.track == null) ||
playback.status == PlaybackStatus.loading) {
final playlist = intent.ref.read(PlaylistQueueNotifier.provider);
final playlistNotifier = intent.ref.read(PlaylistQueueNotifier.notifier);
if (playlist == null || playlist.isLoading) {
DirectionalFocusAction().invoke(
DirectionalFocusIntent(
intent.forward ? TraversalDirection.right : TraversalDirection.left,
Expand All @@ -113,8 +111,8 @@ class SeekAction extends Action<SeekIntent> {
return null;
}
final position =
(await playback.player.getCurrentPosition() ?? Duration.zero).inSeconds;
await playback.seekPosition(
(await audioPlayer.getCurrentPosition() ?? Duration.zero).inSeconds;
await playlistNotifier.seek(
Duration(
seconds: intent.forward ? position + 5 : position - 5,
),
Expand Down
31 changes: 13 additions & 18 deletions lib/components/album/album_card.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/models/current_playlist.dart';
import 'package:spotube/provider/playback_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';

Expand All @@ -20,9 +20,10 @@ class AlbumCard extends HookConsumerWidget {

@override
Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider);
bool isPlaylistPlaying =
playback.playlist != null && playback.playlist!.id == album.id;
final playlist = ref.watch(PlaylistQueueNotifier.provider);
final playing = useStream(PlaylistQueueNotifier.playing).data ?? false;
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(album.tracks!);
final int marginH =
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
return PlaybuttonCard(
Expand All @@ -32,9 +33,8 @@ class AlbumCard extends HookConsumerWidget {
),
viewType: viewType,
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
isPlaying: isPlaylistPlaying && playback.isPlaying,
isLoading: playback.status == PlaybackStatus.loading &&
playback.playlist?.id == album.id,
isPlaying: isPlaylistPlaying && playing,
isLoading: isPlaylistPlaying && playlist?.isLoading == true,
title: album.name!,
description:
"Album • ${TypeConversionUtils.artists_X_String<ArtistSimple>(album.artists ?? [])}",
Expand All @@ -43,23 +43,18 @@ class AlbumCard extends HookConsumerWidget {
},
onPlaybuttonPressed: () async {
SpotifyApi spotify = ref.read(spotifyProvider);
if (isPlaylistPlaying && playback.isPlaying) {
return playback.pause();
} else if (isPlaylistPlaying && !playback.isPlaying) {
return playback.resume();
if (isPlaylistPlaying && playing) {
return playlistNotifier.pause();
} else if (isPlaylistPlaying && !playing) {
return playlistNotifier.resume();
}
List<Track> tracks = (await spotify.albums.getTracks(album.id!).all())
.map((track) =>
TypeConversionUtils.simpleTrack_X_Track(track, album))
.toList();
if (tracks.isEmpty) return;

await playback.playPlaylist(CurrentPlaylist(
tracks: tracks,
id: album.id!,
name: album.name!,
thumbnail: album.images!.first.url!,
));
await playlistNotifier.loadAndPlay(tracks);
},
);
}
Expand Down
71 changes: 36 additions & 35 deletions lib/components/library/user_local_tracks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
import 'package:spotube/hooks/use_async_effect.dart';
import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/models/current_playlist.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/playback_provider.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/primitive_utils.dart';
Expand Down Expand Up @@ -58,7 +57,7 @@ enum SortBy {
dateAdded,
}

final localTracksProvider = FutureProvider<List<Track>>((ref) async {
final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
try {
if (kIsWeb) return [];
final downloadLocation = ref.watch(
Expand Down Expand Up @@ -97,9 +96,8 @@ final localTracksProvider = FutureProvider<List<Track>>((ref) async {

return {"metadata": metadata, "file": f, "art": imageFile.path};
} on FfiException catch (e) {
if (e.message == "NoTag: reader does not contain an id3 tag") {
getLogger(FutureProvider<List<Track>>)
.v("[Fetching metadata]", e.message);
if (e.message != "NoTag: reader does not contain an id3 tag") {
rethrow;
}
return {};
} catch (e, stack) {
Expand All @@ -114,10 +112,13 @@ final localTracksProvider = FutureProvider<List<Track>>((ref) async {

final tracks = filesWithMetadata
.map(
(fileWithMetadata) => TypeConversionUtils.localTrack_X_Track(
fileWithMetadata["file"],
metadata: fileWithMetadata["metadata"],
art: fileWithMetadata["art"],
(fileWithMetadata) => LocalTrack.fromTrack(
track: TypeConversionUtils.localTrack_X_Track(
fileWithMetadata["file"],
metadata: fileWithMetadata["metadata"],
art: fileWithMetadata["art"],
),
path: fileWithMetadata["file"].path,
),
)
.toList();
Expand All @@ -132,37 +133,36 @@ final localTracksProvider = FutureProvider<List<Track>>((ref) async {
class UserLocalTracks extends HookConsumerWidget {
const UserLocalTracks({Key? key}) : super(key: key);

void playLocalTracks(Playback playback, List<Track> tracks,
{Track? currentTrack}) async {
void playLocalTracks(
PlaylistQueueNotifier playback,
List<LocalTrack> tracks, {
LocalTrack? currentTrack,
}) async {
currentTrack ??= tracks.first;
final isPlaylistPlaying = playback.playlist?.id == "local";
final isPlaylistPlaying = playback.isPlayingPlaylist(tracks);
if (!isPlaylistPlaying) {
await playback.playPlaylist(
CurrentPlaylist(
tracks: tracks,
id: "local",
name: "Local Tracks",
thumbnail: TypeConversionUtils.image_X_UrlString(
null,
placeholder: ImagePlaceholder.collection,
),
isLocal: true,
),
tracks.indexWhere((s) => s.id == currentTrack?.id),
await playback.loadAndPlay(
tracks,
active: tracks.indexWhere((s) => s.id == currentTrack?.id),
);
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playback.track?.id) {
await playback.play(currentTrack);
currentTrack.id != playback.state?.activeTrack.id) {
await playback.playAt(
tracks.indexWhere((s) => s.id == currentTrack?.id),
);
}
}

@override
Widget build(BuildContext context, ref) {
final sortBy = useState<SortBy>(SortBy.none);
final playback = ref.watch(playbackProvider);
final isPlaylistPlaying = playback.playlist?.id == "local";
final playlist = ref.watch(PlaylistQueueNotifier.provider);
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
final trackSnapshot = ref.watch(localTracksProvider);
final isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(
trackSnapshot.value ?? [],
);
final isMounted = useIsMounted();
final breakpoint = useBreakpoints();

Expand Down Expand Up @@ -198,9 +198,10 @@ class UserLocalTracks extends HookConsumerWidget {
? () {
if (trackSnapshot.value?.isNotEmpty == true) {
if (!isPlaylistPlaying) {
playLocalTracks(playback, trackSnapshot.value!);
playLocalTracks(
playlistNotifier, trackSnapshot.value!);
} else {
playback.stop();
playlistNotifier.stop();
}
}
}
Expand Down Expand Up @@ -267,17 +268,17 @@ class UserLocalTracks extends HookConsumerWidget {
itemBuilder: (context, index) {
final track = filteredTracks[index];
return TrackTile(
playback,
playlist,
duration:
"${track.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.duration?.inSeconds.remainder(60) ?? 0)}",
track: MapEntry(index, track),
isActive: playback.track?.id == track.id,
isActive: playlist?.activeTrack.id == track.id,
isChecked: false,
showCheck: false,
isLocal: true,
onTrackPlayButtonPressed: (currentTrack) {
return playLocalTracks(
playback,
playlistNotifier,
sortedTracks,
currentTrack: track,
);
Expand Down
36 changes: 18 additions & 18 deletions lib/components/player/player_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/library/user_local_tracks.dart';
import 'package:spotube/components/player/player_queue.dart';
import 'package:spotube/components/player/sibling_tracks_sheet.dart';
import 'package:spotube/components/shared/heart_button.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/auth_provider.dart';
import 'package:spotube/provider/downloader_provider.dart';
import 'package:spotube/provider/playback_provider.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/utils/type_conversion_utils.dart';

class PlayerActions extends HookConsumerWidget {
Expand All @@ -29,34 +29,34 @@ class PlayerActions extends HookConsumerWidget {

@override
Widget build(BuildContext context, ref) {
final Playback playback = ref.watch(playbackProvider);
final isLocalTrack = playback.playlist?.isLocal == true;
final playlist = ref.watch(PlaylistQueueNotifier.provider);
final isLocalTrack = playlist?.activeTrack is LocalTrack;
final downloader = ref.watch(downloaderProvider);
final isInQueue =
downloader.inQueue.any((element) => element.id == playback.track?.id);
final localTracks = ref.watch(localTracksProvider).value;
final isInQueue = downloader.inQueue
.any((element) => element.id == playlist?.activeTrack.id);
final localTracks = [] /* ref.watch(localTracksProvider).value */;
final auth = ref.watch(authProvider);

final isDownloaded = useMemoized(() {
return localTracks?.any(
return localTracks.any(
(element) =>
element.name == playback.track?.name &&
element.album?.name == playback.track?.album?.name &&
element.name == playlist?.activeTrack.name &&
element.album?.name == playlist?.activeTrack.album?.name &&
TypeConversionUtils.artists_X_String<Artist>(
element.artists ?? []) ==
TypeConversionUtils.artists_X_String<Artist>(
playback.track?.artists ?? []),
playlist?.activeTrack.artists ?? []),
) ==
true;
}, [localTracks, playback.track]);
}, [localTracks, playlist?.activeTrack]);

return Row(
mainAxisAlignment: mainAxisAlignment,
children: [
PlatformIconButton(
icon: const Icon(SpotubeIcons.queue),
tooltip: 'Queue',
onPressed: playback.playlist != null
onPressed: playlist != null
? () {
showModalBottomSheet(
context: context,
Expand All @@ -82,7 +82,7 @@ class PlayerActions extends HookConsumerWidget {
PlatformIconButton(
icon: const Icon(SpotubeIcons.alternativeRoute),
tooltip: "Alternative Track Sources",
onPressed: playback.track != null
onPressed: playlist?.activeTrack != null
? () {
showModalBottomSheet(
context: context,
Expand Down Expand Up @@ -119,12 +119,12 @@ class PlayerActions extends HookConsumerWidget {
icon: Icon(
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
),
onPressed: playback.track != null
? () => downloader.addToQueue(playback.track!)
onPressed: playlist?.activeTrack != null
? () => downloader.addToQueue(playlist!.activeTrack)
: null,
),
if (playback.track != null && !isLocalTrack && auth.isLoggedIn)
TrackHeartButton(track: playback.track!),
if (playlist?.activeTrack != null && !isLocalTrack && auth.isLoggedIn)
TrackHeartButton(track: playlist!.activeTrack),
...(extraActions ?? [])
],
);
Expand Down
Loading

0 comments on commit 312f7fb

Please sign in to comment.