mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Refactored Playback works nicely in Desktop
This commit is contained in:
parent
6a05c57dcf
commit
f07a142274
@ -18,15 +18,15 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
Playback playback = ref.watch(playbackProvider);
|
Playback playback = ref.watch(playbackProvider);
|
||||||
bool isPlaylistPlaying = playback.currentPlaylist != null &&
|
bool isPlaylistPlaying =
|
||||||
playback.currentPlaylist!.id == album.id;
|
playback.playlist != null && playback.playlist!.id == album.id;
|
||||||
final int marginH =
|
final int marginH =
|
||||||
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
||||||
return PlaybuttonCard(
|
return PlaybuttonCard(
|
||||||
imageUrl: imageToUrlString(album.images),
|
imageUrl: imageToUrlString(album.images),
|
||||||
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
||||||
isPlaying: playback.currentPlaylist?.id != null &&
|
isPlaying:
|
||||||
playback.currentPlaylist?.id == album.id,
|
playback.playlist?.id != null && playback.playlist?.id == album.id,
|
||||||
title: album.name!,
|
title: album.name!,
|
||||||
description:
|
description:
|
||||||
"Album • ${artistsToString<ArtistSimple>(album.artists ?? [])}",
|
"Album • ${artistsToString<ArtistSimple>(album.artists ?? [])}",
|
||||||
@ -41,14 +41,12 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
.toList();
|
.toList();
|
||||||
if (tracks.isEmpty) return;
|
if (tracks.isEmpty) return;
|
||||||
|
|
||||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
await playback.playPlaylist(CurrentPlaylist(
|
||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
id: album.id!,
|
id: album.id!,
|
||||||
name: album.name!,
|
name: album.name!,
|
||||||
thumbnail: album.images!.first.url!,
|
thumbnail: album.images!.first.url!,
|
||||||
);
|
));
|
||||||
playback.setCurrentTrack = tracks.first;
|
|
||||||
await playback.startPlaying();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||||
@ -18,24 +17,25 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
final AlbumSimple album;
|
final AlbumSimple album;
|
||||||
const AlbumView(this.album, {Key? key}) : super(key: key);
|
const AlbumView(this.album, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
playPlaylist(Playback playback, List<Track> tracks,
|
Future<void> playPlaylist(Playback playback, List<Track> tracks,
|
||||||
{Track? currentTrack}) async {
|
{Track? currentTrack}) async {
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
var isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
|
final isPlaylistPlaying = playback.playlist?.id == album.id;
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
await playback.playPlaylist(
|
||||||
|
CurrentPlaylist(
|
||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
id: album.id!,
|
id: album.id!,
|
||||||
name: album.name!,
|
name: album.name!,
|
||||||
thumbnail: imageToUrlString(album.images),
|
thumbnail: imageToUrlString(album.images),
|
||||||
|
),
|
||||||
|
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
);
|
);
|
||||||
playback.setCurrentTrack = currentTrack;
|
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playback.currentTrack?.id) {
|
currentTrack.id != playback.track?.id) {
|
||||||
playback.setCurrentTrack = currentTrack;
|
await playback.play(currentTrack);
|
||||||
}
|
}
|
||||||
await playback.startPlaying();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -54,8 +54,8 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
|
|
||||||
return TrackCollectionView(
|
return TrackCollectionView(
|
||||||
id: album.id!,
|
id: album.id!,
|
||||||
isPlaying: playback.currentPlaylist?.id != null &&
|
isPlaying:
|
||||||
playback.currentPlaylist?.id == album.id,
|
playback.playlist?.id != null && playback.playlist?.id == album.id,
|
||||||
title: album.name!,
|
title: album.name!,
|
||||||
titleImage: albumArt,
|
titleImage: albumArt,
|
||||||
tracksSnapshot: tracksSnapshot,
|
tracksSnapshot: tracksSnapshot,
|
||||||
|
@ -183,24 +183,25 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
topTracksSnapshot.when(
|
topTracksSnapshot.when(
|
||||||
data: (topTracks) {
|
data: (topTracks) {
|
||||||
final isPlaylistPlaying =
|
final isPlaylistPlaying =
|
||||||
playback.currentPlaylist?.id == data.id;
|
playback.playlist?.id == data.id;
|
||||||
playPlaylist(List<Track> tracks,
|
playPlaylist(List<Track> tracks,
|
||||||
{Track? currentTrack}) async {
|
{Track? currentTrack}) async {
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
await playback.playPlaylist(
|
||||||
|
CurrentPlaylist(
|
||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
id: data.id!,
|
id: data.id!,
|
||||||
name: "${data.name!} To Tracks",
|
name: "${data.name!} To Tracks",
|
||||||
thumbnail: imageToUrlString(data.images),
|
thumbnail: imageToUrlString(data.images),
|
||||||
|
),
|
||||||
|
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
);
|
);
|
||||||
playback.setCurrentTrack = currentTrack;
|
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playback.currentTrack?.id) {
|
currentTrack.id != playback.track?.id) {
|
||||||
playback.setCurrentTrack = currentTrack;
|
await playback.play(currentTrack);
|
||||||
}
|
}
|
||||||
await playback.startPlaying();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(children: [
|
return Column(children: [
|
||||||
|
@ -23,7 +23,7 @@ class Lyrics extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
playback.currentTrack?.name ?? "",
|
playback.track?.name ?? "",
|
||||||
style: breakpoint >= Breakpoints.md
|
style: breakpoint >= Breakpoints.md
|
||||||
? textTheme.headline3
|
? textTheme.headline3
|
||||||
: textTheme.headline4?.copyWith(fontSize: 25),
|
: textTheme.headline4?.copyWith(fontSize: 25),
|
||||||
@ -31,7 +31,7 @@ class Lyrics extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
artistsToString<Artist>(playback.currentTrack?.artists ?? []),
|
artistsToString<Artist>(playback.track?.artists ?? []),
|
||||||
style: breakpoint >= Breakpoints.md
|
style: breakpoint >= Breakpoints.md
|
||||||
? textTheme.headline5
|
? textTheme.headline5
|
||||||
: textTheme.headline6,
|
: textTheme.headline6,
|
||||||
@ -45,7 +45,7 @@ class Lyrics extends HookConsumerWidget {
|
|||||||
child: geniusLyricsSnapshot.when(
|
child: geniusLyricsSnapshot.when(
|
||||||
data: (lyrics) {
|
data: (lyrics) {
|
||||||
return Text(
|
return Text(
|
||||||
lyrics == null && playback.currentTrack == null
|
lyrics == null && playback.track == null
|
||||||
? "No Track being played currently"
|
? "No Track being played currently"
|
||||||
: lyrics!,
|
: lyrics!,
|
||||||
style: textTheme.headline6
|
style: textTheme.headline6
|
||||||
@ -53,7 +53,7 @@ class Lyrics extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
error: (error, __) => Text(
|
error: (error, __) => Text(
|
||||||
"Sorry, no Lyrics were found for `${playback.currentTrack?.name}` :'("),
|
"Sorry, no Lyrics were found for `${playback.track?.name}` :'("),
|
||||||
loading: () => const ShimmerLyrics(),
|
loading: () => const ShimmerLyrics(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -43,7 +43,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
controller.scrollToIndex(0);
|
controller.scrollToIndex(0);
|
||||||
failed.value = false;
|
failed.value = false;
|
||||||
return null;
|
return null;
|
||||||
}, [playback.currentTrack]);
|
}, [playback.track]);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (lyricValue != null && lyricValue.rating <= 2) {
|
if (lyricValue != null && lyricValue.rating <= 2) {
|
||||||
@ -99,20 +99,20 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
Center(
|
Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: breakpoint >= Breakpoints.md ? 50 : 30,
|
height: breakpoint >= Breakpoints.md ? 50 : 30,
|
||||||
child: playback.currentTrack?.name != null &&
|
child: playback.track?.name != null &&
|
||||||
playback.currentTrack!.name!.length > 29
|
playback.track!.name!.length > 29
|
||||||
? SpotubeMarqueeText(
|
? SpotubeMarqueeText(
|
||||||
text: playback.currentTrack?.name ?? "Not Playing",
|
text: playback.track?.name ?? "Not Playing",
|
||||||
style: headlineTextStyle,
|
style: headlineTextStyle,
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
playback.currentTrack?.name ?? "Not Playing",
|
playback.track?.name ?? "Not Playing",
|
||||||
style: headlineTextStyle,
|
style: headlineTextStyle,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
artistsToString<Artist>(playback.currentTrack?.artists ?? []),
|
artistsToString<Artist>(playback.track?.artists ?? []),
|
||||||
style: breakpoint >= Breakpoints.md
|
style: breakpoint >= Breakpoints.md
|
||||||
? textTheme.headline5
|
? textTheme.headline5
|
||||||
: textTheme.headline6,
|
: textTheme.headline6,
|
||||||
@ -157,7 +157,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (playback.currentTrack != null &&
|
if (playback.track != null &&
|
||||||
(lyricValue == null || lyricValue.lyrics.isEmpty == true))
|
(lyricValue == null || lyricValue.lyrics.isEmpty == true))
|
||||||
const Expanded(child: ShimmerLyrics()),
|
const Expanded(child: ShimmerLyrics()),
|
||||||
],
|
],
|
||||||
|
@ -24,12 +24,8 @@ class Player extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
Playback playback = ref.watch(playbackProvider);
|
Playback playback = ref.watch(playbackProvider);
|
||||||
|
|
||||||
final _volume = useState(0.0);
|
|
||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
final AudioPlayerHandler player = playback.player;
|
|
||||||
|
|
||||||
final Future<SharedPreferences> future =
|
final Future<SharedPreferences> future =
|
||||||
useMemoized(SharedPreferences.getInstance);
|
useMemoized(SharedPreferences.getInstance);
|
||||||
final AsyncSnapshot<SharedPreferences?> localStorage =
|
final AsyncSnapshot<SharedPreferences?> localStorage =
|
||||||
@ -37,10 +33,10 @@ class Player extends HookConsumerWidget {
|
|||||||
|
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => imageToUrlString(
|
() => imageToUrlString(
|
||||||
playback.currentTrack?.album?.images,
|
playback.track?.album?.images,
|
||||||
index: (playback.currentTrack?.album?.images?.length ?? 1) - 1,
|
index: (playback.track?.album?.images?.length ?? 1) - 1,
|
||||||
),
|
),
|
||||||
[playback.currentTrack?.album?.images],
|
[playback.track?.album?.images],
|
||||||
);
|
);
|
||||||
|
|
||||||
final entryRef = useRef<OverlayEntry?>(null);
|
final entryRef = useRef<OverlayEntry?>(null);
|
||||||
@ -65,7 +61,7 @@ class Player extends HookConsumerWidget {
|
|||||||
// entry will result in splashing while resizing the window
|
// entry will result in splashing while resizing the window
|
||||||
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
|
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
|
||||||
entryRef.value == null &&
|
entryRef.value == null &&
|
||||||
playback.currentTrack != null) {
|
playback.track != null) {
|
||||||
entryRef.value = OverlayEntry(
|
entryRef.value = OverlayEntry(
|
||||||
opaque: false,
|
opaque: false,
|
||||||
builder: (context) => PlayerOverlay(albumArt: albumArt),
|
builder: (context) => PlayerOverlay(albumArt: albumArt),
|
||||||
@ -87,7 +83,7 @@ class Player extends HookConsumerWidget {
|
|||||||
return () {
|
return () {
|
||||||
disposeOverlay();
|
disposeOverlay();
|
||||||
};
|
};
|
||||||
}, [breakpoint, playback.currentTrack]);
|
}, [breakpoint, playback.track]);
|
||||||
|
|
||||||
// returning an empty non spacious Container as the overlay will take
|
// returning an empty non spacious Container as the overlay will take
|
||||||
// place in the global overlay stack aka [_entries]
|
// place in the global overlay stack aka [_entries]
|
||||||
@ -119,16 +115,10 @@ class Player extends HookConsumerWidget {
|
|||||||
height: 20,
|
height: 20,
|
||||||
constraints: const BoxConstraints(maxWidth: 200),
|
constraints: const BoxConstraints(maxWidth: 200),
|
||||||
child: Slider.adaptive(
|
child: Slider.adaptive(
|
||||||
value: _volume.value,
|
value: playback.volume,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
try {
|
try {
|
||||||
await player.core.setVolume(value).then((_) {
|
await playback.setVolume(value);
|
||||||
_volume.value = value;
|
|
||||||
localStorage.data?.setDouble(
|
|
||||||
LocalStorageKeys.volume,
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logger.e("onChange", e, stack);
|
logger.e("onChange", e, stack);
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,12 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
mainAxisAlignment: mainAxisAlignment,
|
mainAxisAlignment: mainAxisAlignment,
|
||||||
children: [
|
children: [
|
||||||
DownloadTrackButton(
|
DownloadTrackButton(
|
||||||
track: playback.currentTrack,
|
track: playback.track,
|
||||||
),
|
),
|
||||||
if (auth.isLoggedIn)
|
if (auth.isLoggedIn)
|
||||||
FutureBuilder<bool>(
|
FutureBuilder<bool>(
|
||||||
future: playback.currentTrack?.id != null
|
future: playback.track?.id != null
|
||||||
? spotifyApi.tracks.me.containsOne(playback.currentTrack!.id!)
|
? spotifyApi.tracks.me.containsOne(playback.track!.id!)
|
||||||
: Future.value(false),
|
: Future.value(false),
|
||||||
initialData: false,
|
initialData: false,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
@ -42,12 +42,12 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
isLiked: isLiked,
|
isLiked: isLiked,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
if (playback.currentTrack?.id == null) return;
|
if (playback.track?.id == null) return;
|
||||||
isLiked
|
isLiked
|
||||||
? await spotifyApi.tracks.me
|
? await spotifyApi.tracks.me
|
||||||
.removeOne(playback.currentTrack!.id!)
|
.removeOne(playback.track!.id!)
|
||||||
: await spotifyApi.tracks.me
|
: await spotifyApi.tracks.me
|
||||||
.saveOne(playback.currentTrack!.id!);
|
.saveOne(playback.track!.id!);
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logger.e("FavoriteButton.onPressed", e, stack);
|
logger.e("FavoriteButton.onPressed", e, stack);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -18,7 +18,6 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final Playback playback = ref.watch(playbackProvider);
|
final Playback playback = ref.watch(playbackProvider);
|
||||||
final AudioPlayerHandler player = playback.player;
|
|
||||||
|
|
||||||
final onNext = useNextTrack(playback);
|
final onNext = useNextTrack(playback);
|
||||||
|
|
||||||
@ -26,14 +25,14 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
|
|
||||||
final _playOrPause = useTogglePlayPause(playback);
|
final _playOrPause = useTogglePlayPause(playback);
|
||||||
|
|
||||||
final duration = playback.duration ?? Duration.zero;
|
final duration = playback.currentDuration;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 600),
|
constraints: const BoxConstraints(maxWidth: 600),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
StreamBuilder<Duration>(
|
StreamBuilder<Duration>(
|
||||||
stream: player.core.onPositionChanged,
|
stream: playback.player.onPositionChanged,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final totalMinutes =
|
final totalMinutes =
|
||||||
zeroPadNumStr(duration.inMinutes.remainder(60));
|
zeroPadNumStr(duration.inMinutes.remainder(60));
|
||||||
@ -61,7 +60,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
value: value.toDouble(),
|
value: value.toDouble(),
|
||||||
onChanged: (_) {},
|
onChanged: (_) {},
|
||||||
onChangeEnd: (value) async {
|
onChangeEnd: (value) async {
|
||||||
await player.seek(
|
await playback.seekPosition(
|
||||||
Duration(
|
Duration(
|
||||||
seconds: (value * sliderMax).toInt(),
|
seconds: (value * sliderMax).toInt(),
|
||||||
),
|
),
|
||||||
@ -89,20 +88,15 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.shuffle_rounded),
|
icon: const Icon(Icons.shuffle_rounded),
|
||||||
color: playback.shuffled
|
color: playback.isShuffled
|
||||||
? Theme.of(context).primaryColor
|
? Theme.of(context).primaryColor
|
||||||
: iconColor,
|
: iconColor,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (playback.currentTrack == null ||
|
if (playback.track == null || playback.playlist == null) {
|
||||||
playback.currentPlaylist == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!playback.shuffled) {
|
playback.toggleShuffle();
|
||||||
playback.shuffle();
|
|
||||||
} else {
|
|
||||||
playback.unshuffle();
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logger.e("onShuffle", e, stack);
|
logger.e("onShuffle", e, stack);
|
||||||
}
|
}
|
||||||
@ -130,12 +124,10 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.stop_rounded),
|
icon: const Icon(Icons.stop_rounded),
|
||||||
color: iconColor,
|
color: iconColor,
|
||||||
onPressed: playback.currentTrack != null
|
onPressed: playback.track != null
|
||||||
? () async {
|
? () async {
|
||||||
try {
|
try {
|
||||||
await player.pause();
|
await playback.stop();
|
||||||
await player.seek(Duration.zero);
|
|
||||||
playback.reset();
|
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logger.e("onStop", e, stack);
|
logger.e("onStop", e, stack);
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md))
|
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md))
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
playback.currentTrack?.name ?? "Not playing",
|
playback.track?.name ?? "Not playing",
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
@ -54,7 +54,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
playback.currentTrack?.name ?? "Not playing",
|
playback.track?.name ?? "Not playing",
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
@ -62,7 +62,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
?.copyWith(fontWeight: FontWeight.bold, color: color),
|
?.copyWith(fontWeight: FontWeight.bold, color: color),
|
||||||
),
|
),
|
||||||
artistsToClickableArtists(
|
artistsToClickableArtists(
|
||||||
playback.currentTrack?.artists ?? [],
|
playback.track?.artists ?? [],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -23,7 +23,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final currentTrack = ref.watch(playbackProvider.select(
|
final currentTrack = ref.watch(playbackProvider.select(
|
||||||
(value) => value.currentTrack,
|
(value) => value.track,
|
||||||
));
|
));
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
|
@ -15,8 +15,8 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
Playback playback = ref.watch(playbackProvider);
|
Playback playback = ref.watch(playbackProvider);
|
||||||
bool isPlaylistPlaying = playback.currentPlaylist != null &&
|
bool isPlaylistPlaying =
|
||||||
playback.currentPlaylist!.id == playlist.id;
|
playback.playlist != null && playback.playlist!.id == playlist.id;
|
||||||
|
|
||||||
final int marginH =
|
final int marginH =
|
||||||
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
||||||
@ -46,14 +46,14 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (tracks.isEmpty) return;
|
if (tracks.isEmpty) return;
|
||||||
|
|
||||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
await playback.playPlaylist(
|
||||||
|
CurrentPlaylist(
|
||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
id: playlist.id!,
|
id: playlist.id!,
|
||||||
name: playlist.name!,
|
name: playlist.name!,
|
||||||
thumbnail: imageToUrlString(playlist.images),
|
thumbnail: imageToUrlString(playlist.images),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
playback.setCurrentTrack = tracks.first;
|
|
||||||
await playback.startPlaying();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,22 +24,23 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
playPlaylist(Playback playback, List<Track> tracks,
|
playPlaylist(Playback playback, List<Track> tracks,
|
||||||
{Track? currentTrack}) async {
|
{Track? currentTrack}) async {
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
final isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
final isPlaylistPlaying =
|
||||||
playback.currentPlaylist?.id == playlist.id;
|
playback.playlist?.id != null && playback.playlist?.id == playlist.id;
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
await playback.playPlaylist(
|
||||||
|
CurrentPlaylist(
|
||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
id: playlist.id!,
|
id: playlist.id!,
|
||||||
name: playlist.name!,
|
name: playlist.name!,
|
||||||
thumbnail: imageToUrlString(playlist.images),
|
thumbnail: imageToUrlString(playlist.images),
|
||||||
|
),
|
||||||
|
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
);
|
);
|
||||||
playback.setCurrentTrack = currentTrack;
|
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playback.currentTrack?.id) {
|
currentTrack.id != playback.track?.id) {
|
||||||
playback.setCurrentTrack = currentTrack;
|
await playback.play(currentTrack);
|
||||||
}
|
}
|
||||||
await playback.startPlaying();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -47,8 +48,8 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
Playback playback = ref.watch(playbackProvider);
|
Playback playback = ref.watch(playbackProvider);
|
||||||
final Auth auth = ref.watch(authProvider);
|
final Auth auth = ref.watch(authProvider);
|
||||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||||
final isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
final isPlaylistPlaying =
|
||||||
playback.currentPlaylist?.id == playlist.id;
|
playback.playlist?.id != null && playback.playlist?.id == playlist.id;
|
||||||
|
|
||||||
final meSnapshot = ref.watch(currentUserQuery);
|
final meSnapshot = ref.watch(currentUserQuery);
|
||||||
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
||||||
|
@ -115,26 +115,24 @@ class Search extends HookConsumerWidget {
|
|||||||
thumbnailUrl:
|
thumbnailUrl:
|
||||||
imageToUrlString(track.value.album?.images),
|
imageToUrlString(track.value.album?.images),
|
||||||
onTrackPlayButtonPressed: (currentTrack) async {
|
onTrackPlayButtonPressed: (currentTrack) async {
|
||||||
var isPlaylistPlaying =
|
var isPlaylistPlaying = playback.playlist?.id !=
|
||||||
playback.currentPlaylist?.id != null &&
|
null &&
|
||||||
playback.currentPlaylist?.id ==
|
playback.playlist?.id == currentTrack.id;
|
||||||
currentTrack.id;
|
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
playback.playPlaylist(
|
||||||
|
CurrentPlaylist(
|
||||||
tracks: [currentTrack],
|
tracks: [currentTrack],
|
||||||
id: currentTrack.id!,
|
id: currentTrack.id!,
|
||||||
name: currentTrack.name!,
|
name: currentTrack.name!,
|
||||||
thumbnail: imageToUrlString(
|
thumbnail: imageToUrlString(
|
||||||
currentTrack.album?.images),
|
currentTrack.album?.images),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
playback.setCurrentTrack = currentTrack;
|
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id !=
|
currentTrack.id != playback.track?.id) {
|
||||||
playback.currentTrack?.id) {
|
playback.play(currentTrack);
|
||||||
playback.setCurrentTrack = currentTrack;
|
|
||||||
}
|
}
|
||||||
await playback.startPlaying();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
@ -133,16 +133,13 @@ class DownloadTrackButton extends HookConsumerWidget {
|
|||||||
return statusCb.cancel();
|
return statusCb.cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (preferences.saveTrackLyrics && playback.currentTrack != null) {
|
if (preferences.saveTrackLyrics && playback.track != null) {
|
||||||
if (!await outputLyricsFile.exists()) {
|
if (!await outputLyricsFile.exists()) {
|
||||||
await outputLyricsFile.create(recursive: true);
|
await outputLyricsFile.create(recursive: true);
|
||||||
}
|
}
|
||||||
final lyrics = await getLyrics(
|
final lyrics = await getLyrics(
|
||||||
playback.currentTrack!.name!,
|
playback.track!.name!,
|
||||||
playback.currentTrack!.artists
|
playback.track!.artists?.map((s) => s.name).whereNotNull().toList() ??
|
||||||
?.map((s) => s.name)
|
|
||||||
.whereNotNull()
|
|
||||||
.toList() ??
|
|
||||||
[],
|
[],
|
||||||
apiKey: preferences.geniusAccessToken,
|
apiKey: preferences.geniusAccessToken,
|
||||||
optimizeQuery: true,
|
optimizeQuery: true,
|
||||||
@ -159,7 +156,7 @@ class DownloadTrackButton extends HookConsumerWidget {
|
|||||||
status,
|
status,
|
||||||
yt,
|
yt,
|
||||||
preferences.saveTrackLyrics,
|
preferences.saveTrackLyrics,
|
||||||
playback.currentTrack,
|
playback.track,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
|
@ -84,7 +84,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
actionAddToPlaylist() async {
|
Future<void> actionAddToPlaylist() async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@ -196,8 +196,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
playback.currentTrack?.id != null &&
|
playback.track?.id != null && playback.track?.id == track.value.id
|
||||||
playback.currentTrack?.id == track.value.id
|
|
||||||
? Icons.pause_circle_rounded
|
? Icons.pause_circle_rounded
|
||||||
: Icons.play_circle_rounded,
|
: Icons.play_circle_rounded,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
|
@ -8,7 +8,7 @@ Future<void> Function() useNextTrack(Playback playback) {
|
|||||||
try {
|
try {
|
||||||
await playback.player.pause();
|
await playback.player.pause();
|
||||||
await playback.player.seek(Duration.zero);
|
await playback.player.seek(Duration.zero);
|
||||||
playback.movePlaylistPositionBy(1);
|
playback.seekForward();
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logger.e("useNextTrack", e, stack);
|
logger.e("useNextTrack", e, stack);
|
||||||
}
|
}
|
||||||
@ -20,7 +20,7 @@ Future<void> Function() usePreviousTrack(Playback playback) {
|
|||||||
try {
|
try {
|
||||||
await playback.player.pause();
|
await playback.player.pause();
|
||||||
await playback.player.seek(Duration.zero);
|
await playback.player.seek(Duration.zero);
|
||||||
playback.movePlaylistPositionBy(-1);
|
playback.seekBackward();
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logger.e("onPrevious", e, stack);
|
logger.e("onPrevious", e, stack);
|
||||||
}
|
}
|
||||||
@ -30,10 +30,8 @@ Future<void> Function() usePreviousTrack(Playback playback) {
|
|||||||
Future<void> Function([dynamic]) useTogglePlayPause(Playback playback) {
|
Future<void> Function([dynamic]) useTogglePlayPause(Playback playback) {
|
||||||
return ([key]) async {
|
return ([key]) async {
|
||||||
try {
|
try {
|
||||||
if (playback.currentTrack == null) return;
|
if (playback.track == null) return;
|
||||||
playback.isPlaying
|
await playback.togglePlayPause();
|
||||||
? await playback.player.pause()
|
|
||||||
: await playback.player.play();
|
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logger.e("useTogglePlayPause", e, stack);
|
logger.e("useTogglePlayPause", e, stack);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ useSyncedLyrics(WidgetRef ref, Map<int, String> lyricsMap) {
|
|||||||
final player = ref.watch(playbackProvider.select(
|
final player = ref.watch(playbackProvider.select(
|
||||||
(value) => (value.player),
|
(value) => (value.player),
|
||||||
));
|
));
|
||||||
final stream = player.core.onPositionChanged;
|
final stream = player.onPositionChanged;
|
||||||
|
|
||||||
final currentTime = useState(0);
|
final currentTime = useState(0);
|
||||||
|
|
||||||
|
@ -3,10 +3,17 @@
|
|||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:dbus/dbus.dart';
|
import 'package:dbus/dbus.dart';
|
||||||
|
import 'package:spotube/provider/DBus.dart';
|
||||||
|
|
||||||
class Media_Player extends DBusObject {
|
class Media_Player extends DBusObject {
|
||||||
/// Creates a new object to expose on [path].
|
/// Creates a new object to expose on [path].
|
||||||
Media_Player() : super(DBusObjectPath('/org/mpris/MediaPlayer2'));
|
Media_Player() : super(DBusObjectPath('/org/mpris/MediaPlayer2')) {
|
||||||
|
dbus.registerObject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
dbus.unregisterObject(this);
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.CanQuit
|
/// Gets value of property org.mpris.MediaPlayer2.CanQuit
|
||||||
Future<DBusMethodResponse> getCanQuit() async {
|
Future<DBusMethodResponse> getCanQuit() async {
|
||||||
|
@ -1,27 +1,40 @@
|
|||||||
// This file was generated using the following command and may be overwritten.
|
// This file was generated using the following command and may be overwritten.
|
||||||
// dart-dbus generate-object defs/org.mpris.MediaPlayer2.Player.xml
|
// dart-dbus generate-object defs/org.mpris.MediaPlayer2.Player.xml
|
||||||
|
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dbus/dbus.dart';
|
import 'package:dbus/dbus.dart';
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
|
import 'package:spotube/provider/DBus.dart';
|
||||||
|
|
||||||
class Player_Interface extends DBusObject {
|
class Player_Interface extends DBusObject {
|
||||||
final AudioPlayer player;
|
|
||||||
final Playback playback;
|
final Playback playback;
|
||||||
|
|
||||||
/// Creates a new object to expose on [path].
|
/// Creates a new object to expose on [path].
|
||||||
Player_Interface({
|
Player_Interface({
|
||||||
required this.player,
|
|
||||||
required this.playback,
|
required this.playback,
|
||||||
}) : super(DBusObjectPath("/org/mpris/MediaPlayer2"));
|
}) : super(DBusObjectPath("/org/mpris/MediaPlayer2")) {
|
||||||
|
(() async {
|
||||||
|
final nameStatus =
|
||||||
|
await dbus.requestName("org.mpris.MediaPlayer2.spotube");
|
||||||
|
if (nameStatus == DBusRequestNameReply.exists) {
|
||||||
|
await dbus.requestName("org.mpris.MediaPlayer2.spotube.instance$pid");
|
||||||
|
}
|
||||||
|
await dbus.registerObject(this);
|
||||||
|
}());
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
dbus.unregisterObject(this);
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.Player.PlaybackStatus
|
/// Gets value of property org.mpris.MediaPlayer2.Player.PlaybackStatus
|
||||||
Future<DBusMethodResponse> getPlaybackStatus() async {
|
Future<DBusMethodResponse> getPlaybackStatus() async {
|
||||||
final status = player.state == PlayerState.playing
|
final status = playback.isPlaying
|
||||||
? "Playing"
|
? "Playing"
|
||||||
: playback.currentPlaylist == null
|
: playback.playlist == null
|
||||||
? "Stopped"
|
? "Stopped"
|
||||||
: "Paused";
|
: "Paused";
|
||||||
return DBusMethodSuccessResponse([DBusString(status)]);
|
return DBusMethodSuccessResponse([DBusString(status)]);
|
||||||
@ -45,34 +58,29 @@ class Player_Interface extends DBusObject {
|
|||||||
|
|
||||||
/// Sets property org.mpris.MediaPlayer2.Player.Rate
|
/// Sets property org.mpris.MediaPlayer2.Player.Rate
|
||||||
Future<DBusMethodResponse> setRate(double value) async {
|
Future<DBusMethodResponse> setRate(double value) async {
|
||||||
player.setPlaybackRate(value);
|
|
||||||
return DBusMethodSuccessResponse();
|
return DBusMethodSuccessResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.Player.Shuffle
|
/// Gets value of property org.mpris.MediaPlayer2.Player.Shuffle
|
||||||
Future<DBusMethodResponse> getShuffle() async {
|
Future<DBusMethodResponse> getShuffle() async {
|
||||||
return DBusMethodSuccessResponse([DBusBoolean(playback.shuffled)]);
|
return DBusMethodSuccessResponse([DBusBoolean(playback.isShuffled)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets property org.mpris.MediaPlayer2.Player.Shuffle
|
/// Sets property org.mpris.MediaPlayer2.Player.Shuffle
|
||||||
Future<DBusMethodResponse> setShuffle(bool value) async {
|
Future<DBusMethodResponse> setShuffle(bool value) async {
|
||||||
if (value) {
|
playback.toggleShuffle();
|
||||||
playback.shuffle();
|
|
||||||
} else {
|
|
||||||
playback.unshuffle();
|
|
||||||
}
|
|
||||||
return DBusMethodSuccessResponse();
|
return DBusMethodSuccessResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.Player.Metadata
|
/// Gets value of property org.mpris.MediaPlayer2.Player.Metadata
|
||||||
Future<DBusMethodResponse> getMetadata() async {
|
Future<DBusMethodResponse> getMetadata() async {
|
||||||
try {
|
try {
|
||||||
if (playback.currentTrack == null) {
|
if (playback.track == null) {
|
||||||
return DBusMethodSuccessResponse([DBusDict.stringVariant({})]);
|
return DBusMethodSuccessResponse([DBusDict.stringVariant({})]);
|
||||||
}
|
}
|
||||||
final id = (playback.currentPlaylist != null
|
final id = (playback.playlist != null
|
||||||
? playback.currentPlaylist!.tracks.indexWhere(
|
? playback.playlist!.tracks.indexWhere(
|
||||||
(track) => playback.currentTrack!.id == track.id!,
|
(track) => playback.track!.id == track.id!,
|
||||||
)
|
)
|
||||||
: 0)
|
: 0)
|
||||||
.abs();
|
.abs();
|
||||||
@ -80,18 +88,18 @@ class Player_Interface extends DBusObject {
|
|||||||
return DBusMethodSuccessResponse([
|
return DBusMethodSuccessResponse([
|
||||||
DBusDict.stringVariant({
|
DBusDict.stringVariant({
|
||||||
"mpris:trackid": DBusString("${path.value}/Track/$id"),
|
"mpris:trackid": DBusString("${path.value}/Track/$id"),
|
||||||
"mpris:length": DBusInt32(playback.duration?.inMicroseconds ?? 0),
|
"mpris:length": DBusInt32(playback.currentDuration.inMicroseconds),
|
||||||
"mpris:artUrl": DBusString(
|
"mpris:artUrl":
|
||||||
imageToUrlString(playback.currentTrack?.album?.images)),
|
DBusString(imageToUrlString(playback.track?.album?.images)),
|
||||||
"xesam:album": DBusString(playback.currentTrack!.album!.name!),
|
"xesam:album": DBusString(playback.track!.album!.name!),
|
||||||
"xesam:artist": DBusArray.string(
|
"xesam:artist": DBusArray.string(
|
||||||
playback.currentTrack!.artists!.map((artist) => artist.name!),
|
playback.track!.artists!.map((artist) => artist.name!),
|
||||||
),
|
),
|
||||||
"xesam:title": DBusString(playback.currentTrack!.name!),
|
"xesam:title": DBusString(playback.track!.name!),
|
||||||
"xesam:url": DBusString(
|
"xesam:url": DBusString(
|
||||||
playback.currentTrack is SpotubeTrack
|
playback.track is SpotubeTrack
|
||||||
? (playback.currentTrack as SpotubeTrack).ytUri
|
? (playback.track as SpotubeTrack).ytUri
|
||||||
: playback.currentTrack!.previewUrl!,
|
: playback.track!.previewUrl!,
|
||||||
),
|
),
|
||||||
"xesam:genre": const DBusString("Unknown"),
|
"xesam:genre": const DBusString("Unknown"),
|
||||||
}),
|
}),
|
||||||
@ -116,7 +124,7 @@ class Player_Interface extends DBusObject {
|
|||||||
/// Gets value of property org.mpris.MediaPlayer2.Player.Position
|
/// Gets value of property org.mpris.MediaPlayer2.Player.Position
|
||||||
Future<DBusMethodResponse> getPosition() async {
|
Future<DBusMethodResponse> getPosition() async {
|
||||||
return DBusMethodSuccessResponse([
|
return DBusMethodSuccessResponse([
|
||||||
DBusInt64((await player.getDuration())?.inMicroseconds ?? 0),
|
DBusInt64((await playback.player.getDuration())?.inMicroseconds ?? 0),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +142,7 @@ class Player_Interface extends DBusObject {
|
|||||||
Future<DBusMethodResponse> getCanGoNext() async {
|
Future<DBusMethodResponse> getCanGoNext() async {
|
||||||
return DBusMethodSuccessResponse([
|
return DBusMethodSuccessResponse([
|
||||||
DBusBoolean(
|
DBusBoolean(
|
||||||
playback.currentPlaylist?.tracks.isNotEmpty == true,
|
playback.playlist?.tracks.isNotEmpty == true,
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -143,7 +151,7 @@ class Player_Interface extends DBusObject {
|
|||||||
Future<DBusMethodResponse> getCanGoPrevious() async {
|
Future<DBusMethodResponse> getCanGoPrevious() async {
|
||||||
return DBusMethodSuccessResponse([
|
return DBusMethodSuccessResponse([
|
||||||
DBusBoolean(
|
DBusBoolean(
|
||||||
playback.currentPlaylist?.tracks.isNotEmpty == true,
|
playback.playlist?.tracks.isNotEmpty == true,
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -170,45 +178,43 @@ class Player_Interface extends DBusObject {
|
|||||||
|
|
||||||
/// Implementation of org.mpris.MediaPlayer2.Player.Next()
|
/// Implementation of org.mpris.MediaPlayer2.Player.Next()
|
||||||
Future<DBusMethodResponse> doNext() async {
|
Future<DBusMethodResponse> doNext() async {
|
||||||
playback.movePlaylistPositionBy(1);
|
playback.seekForward();
|
||||||
return DBusMethodSuccessResponse();
|
return DBusMethodSuccessResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of org.mpris.MediaPlayer2.Player.Previous()
|
/// Implementation of org.mpris.MediaPlayer2.Player.Previous()
|
||||||
Future<DBusMethodResponse> doPrevious() async {
|
Future<DBusMethodResponse> doPrevious() async {
|
||||||
playback.movePlaylistPositionBy(-1);
|
playback.seekBackward();
|
||||||
return DBusMethodSuccessResponse();
|
return DBusMethodSuccessResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of org.mpris.MediaPlayer2.Player.Pause()
|
/// Implementation of org.mpris.MediaPlayer2.Player.Pause()
|
||||||
Future<DBusMethodResponse> doPause() async {
|
Future<DBusMethodResponse> doPause() async {
|
||||||
player.pause();
|
playback.pause();
|
||||||
return DBusMethodSuccessResponse();
|
return DBusMethodSuccessResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of org.mpris.MediaPlayer2.Player.PlayPause()
|
/// Implementation of org.mpris.MediaPlayer2.Player.PlayPause()
|
||||||
Future<DBusMethodResponse> doPlayPause() async {
|
Future<DBusMethodResponse> doPlayPause() async {
|
||||||
player.state == PlayerState.playing ? player.pause() : player.resume();
|
playback.isPlaying ? playback.pause() : playback.resume();
|
||||||
return DBusMethodSuccessResponse();
|
return DBusMethodSuccessResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of org.mpris.MediaPlayer2.Player.Stop()
|
/// Implementation of org.mpris.MediaPlayer2.Player.Stop()
|
||||||
Future<DBusMethodResponse> doStop() async {
|
Future<DBusMethodResponse> doStop() async {
|
||||||
await player.pause();
|
playback.stop();
|
||||||
await player.seek(Duration.zero);
|
|
||||||
playback.reset();
|
|
||||||
return DBusMethodSuccessResponse();
|
return DBusMethodSuccessResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of org.mpris.MediaPlayer2.Player.Play()
|
/// Implementation of org.mpris.MediaPlayer2.Player.Play()
|
||||||
Future<DBusMethodResponse> doPlay() async {
|
Future<DBusMethodResponse> doPlay() async {
|
||||||
player.resume();
|
playback.resume();
|
||||||
return DBusMethodSuccessResponse();
|
return DBusMethodSuccessResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of org.mpris.MediaPlayer2.Player.Seek()
|
/// Implementation of org.mpris.MediaPlayer2.Player.Seek()
|
||||||
Future<DBusMethodResponse> doSeek(int offset) async {
|
Future<DBusMethodResponse> doSeek(int offset) async {
|
||||||
player.seek(Duration(microseconds: offset));
|
playback.seekPosition(Duration(microseconds: offset));
|
||||||
return DBusMethodSuccessResponse();
|
return DBusMethodSuccessResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:dbus/dbus.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/entities/CacheTrack.dart';
|
import 'package:spotube/entities/CacheTrack.dart';
|
||||||
import 'package:spotube/interfaces/media_player2.dart';
|
|
||||||
import 'package:spotube/models/GoRouteDeclarations.dart';
|
import 'package:spotube/models/GoRouteDeclarations.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/AudioPlayer.dart';
|
import 'package:spotube/provider/AudioPlayer.dart';
|
||||||
import 'package:spotube/provider/DBus.dart';
|
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
@ -24,14 +21,6 @@ void main() async {
|
|||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
Hive.registerAdapter(CacheTrackAdapter());
|
Hive.registerAdapter(CacheTrackAdapter());
|
||||||
Hive.registerAdapter(CacheTrackEngagementAdapter());
|
Hive.registerAdapter(CacheTrackEngagementAdapter());
|
||||||
AudioPlayerHandler audioPlayerHandler = await AudioService.init(
|
|
||||||
builder: () => AudioPlayerHandler(),
|
|
||||||
config: const AudioServiceConfig(
|
|
||||||
androidNotificationChannelId: 'com.krtirtho.Spotube',
|
|
||||||
androidNotificationChannelName: 'Spotube',
|
|
||||||
androidNotificationOngoing: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (kIsDesktop) {
|
if (kIsDesktop) {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
// final client = DBusClient.session();
|
// final client = DBusClient.session();
|
||||||
@ -44,19 +33,38 @@ void main() async {
|
|||||||
appWindow.show();
|
appWindow.show();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
AudioPlayerHandler? audioServiceHandler;
|
||||||
runApp(ProviderScope(
|
runApp(ProviderScope(
|
||||||
child: Spotube(),
|
child: Spotube(),
|
||||||
overrides: [
|
overrides: [
|
||||||
playbackProvider.overrideWithProvider(ChangeNotifierProvider(
|
playbackProvider.overrideWithProvider(ChangeNotifierProvider(
|
||||||
(ref) {
|
(ref) {
|
||||||
final youtube = ref.watch(youtubeProvider);
|
final youtube = ref.watch(youtubeProvider);
|
||||||
final dbus = ref.watch(dbusClientProvider);
|
final player = ref.watch(audioPlayerProvider);
|
||||||
return Playback(
|
|
||||||
player: audioPlayerHandler,
|
final playback = Playback(
|
||||||
|
player: player,
|
||||||
youtube: youtube,
|
youtube: youtube,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
dbus: dbus,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (audioServiceHandler == null) {
|
||||||
|
AudioService.init(
|
||||||
|
builder: () => AudioPlayerHandler(playback),
|
||||||
|
config: const AudioServiceConfig(
|
||||||
|
androidNotificationChannelId: 'com.krtirtho.Spotube',
|
||||||
|
androidNotificationChannelName: 'Spotube',
|
||||||
|
androidNotificationOngoing: true,
|
||||||
|
),
|
||||||
|
).then(
|
||||||
|
(value) {
|
||||||
|
playback.mobileAudioService = value;
|
||||||
|
audioServiceHandler = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return playback;
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
],
|
],
|
||||||
|
@ -8,3 +8,5 @@ final Provider<DBusClient?> dbusClientProvider = Provider<DBusClient?>((ref) {
|
|||||||
return DBusClient.session();
|
return DBusClient.session();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final dbus = DBusClient.session();
|
||||||
|
325
lib/provider/LegacyPlayback.dart
Normal file
325
lib/provider/LegacyPlayback.dart
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
|
import 'package:dbus/dbus.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/entities/CacheTrack.dart';
|
||||||
|
import 'package:spotube/helpers/artist-to-string.dart';
|
||||||
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
|
import 'package:spotube/helpers/search-youtube.dart';
|
||||||
|
import 'package:spotube/interfaces/media_player2.dart';
|
||||||
|
import 'package:spotube/interfaces/media_player2_player.dart';
|
||||||
|
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||||
|
import 'package:spotube/models/Logger.dart';
|
||||||
|
import 'package:spotube/provider/DBus.dart';
|
||||||
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
|
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
||||||
|
import 'package:spotube/utils/PersistedChangeNotifier.dart';
|
||||||
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
|
class LegacyPlayback extends PersistedChangeNotifier {
|
||||||
|
UrlSource? _currentAudioSource;
|
||||||
|
final _logger = getLogger(LegacyPlayback);
|
||||||
|
CurrentPlaylist? _currentPlaylist;
|
||||||
|
Track? _currentTrack;
|
||||||
|
|
||||||
|
// states
|
||||||
|
bool _isPlaying = false;
|
||||||
|
Duration? duration;
|
||||||
|
|
||||||
|
bool _shuffled = false;
|
||||||
|
|
||||||
|
AudioPlayerHandler player;
|
||||||
|
YoutubeExplode youtube;
|
||||||
|
Ref ref;
|
||||||
|
|
||||||
|
LazyBox<CacheTrack>? cacheTrackBox;
|
||||||
|
|
||||||
|
@protected
|
||||||
|
final DBusClient? dbus;
|
||||||
|
Media_Player? _media_player;
|
||||||
|
Player_Interface? _mpris;
|
||||||
|
|
||||||
|
double volume = 1;
|
||||||
|
|
||||||
|
LegacyPlayback({
|
||||||
|
required this.player,
|
||||||
|
required this.youtube,
|
||||||
|
required this.ref,
|
||||||
|
required this.dbus,
|
||||||
|
CurrentPlaylist? currentPlaylist,
|
||||||
|
Track? currentTrack,
|
||||||
|
}) : _currentPlaylist = currentPlaylist,
|
||||||
|
_currentTrack = currentTrack,
|
||||||
|
super() {
|
||||||
|
player.onNextRequest = () {
|
||||||
|
movePlaylistPositionBy(1);
|
||||||
|
};
|
||||||
|
player.onPreviousRequest = () {
|
||||||
|
movePlaylistPositionBy(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription<Duration?>? _durationStream;
|
||||||
|
StreamSubscription<PlayerState>? _playingStream;
|
||||||
|
StreamSubscription<Duration>? _positionStream;
|
||||||
|
|
||||||
|
void _init() async {
|
||||||
|
// dbus m.p.r.i.s stuff
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
try {
|
||||||
|
_media_player = Media_Player();
|
||||||
|
_mpris = Player_Interface(player: player.core, playback: this);
|
||||||
|
await dbus?.registerObject(_media_player!);
|
||||||
|
await dbus?.registerObject(_mpris!);
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("[MPRIS initialization error]", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheTrackBox = await Hive.openLazyBox<CacheTrack>("track-cache");
|
||||||
|
|
||||||
|
_playingStream = player.core.onPlayerStateChanged.listen(
|
||||||
|
(state) async {
|
||||||
|
_isPlaying = state == PlayerState.playing;
|
||||||
|
if (state == PlayerState.completed) {
|
||||||
|
if (_currentTrack?.id != null) {
|
||||||
|
movePlaylistPositionBy(1);
|
||||||
|
} else {
|
||||||
|
_isPlaying = false;
|
||||||
|
duration = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_durationStream = player.core.onDurationChanged.listen((event) {
|
||||||
|
duration = event;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
_positionStream = player.core.onPositionChanged.listen((pos) async {
|
||||||
|
if (pos > Duration.zero &&
|
||||||
|
(duration == null || duration == Duration.zero)) {
|
||||||
|
duration = await player.core.getDuration();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_playingStream?.cancel();
|
||||||
|
_durationStream?.cancel();
|
||||||
|
_positionStream?.cancel();
|
||||||
|
cacheTrackBox?.close();
|
||||||
|
if (Platform.isLinux && _media_player != null && _mpris != null) {
|
||||||
|
dbus?.unregisterObject(_media_player!);
|
||||||
|
dbus?.unregisterObject(_mpris!);
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get shuffled => _shuffled;
|
||||||
|
CurrentPlaylist? get currentPlaylist => _currentPlaylist;
|
||||||
|
Track? get currentTrack => _currentTrack;
|
||||||
|
bool get isPlaying => _isPlaying;
|
||||||
|
|
||||||
|
set setCurrentTrack(Track track) {
|
||||||
|
_logger.v("[Setting Current Track] ${track.name} - ${track.id}");
|
||||||
|
_currentTrack = track;
|
||||||
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
set setCurrentPlaylist(CurrentPlaylist playlist) {
|
||||||
|
_logger.v("[Current Playlist Changed] ${playlist.name} - ${playlist.id}");
|
||||||
|
_currentPlaylist = playlist;
|
||||||
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_logger.v("Playback Reset");
|
||||||
|
_isPlaying = false;
|
||||||
|
_shuffled = false;
|
||||||
|
duration = null;
|
||||||
|
_currentPlaylist = null;
|
||||||
|
_currentTrack = null;
|
||||||
|
notifyListeners();
|
||||||
|
updatePersistence(clearNullEntries: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setVolume(double newVolume) {
|
||||||
|
volume = newVolume;
|
||||||
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the provided id matched track's uri\
|
||||||
|
/// Doesn't notify listeners\
|
||||||
|
/// @returns `bool` - `true` if succeed & `false` when failed
|
||||||
|
bool setTrackUriById(String id, String uri) {
|
||||||
|
if (_currentPlaylist == null) return false;
|
||||||
|
try {
|
||||||
|
int index =
|
||||||
|
_currentPlaylist!.tracks.indexWhere((element) => element.id == id);
|
||||||
|
if (index == -1) return false;
|
||||||
|
_currentPlaylist!.tracks[index].uri = uri;
|
||||||
|
updatePersistence();
|
||||||
|
return _currentPlaylist!.tracks[index].uri == uri;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void movePlaylistPositionBy(int pos) {
|
||||||
|
_logger.v("[Playlist Position Move] $pos");
|
||||||
|
if (_currentTrack != null && _currentPlaylist != null) {
|
||||||
|
final int index =
|
||||||
|
_currentPlaylist!.trackIds.indexOf(_currentTrack!.id!) + pos;
|
||||||
|
|
||||||
|
final safeIndex = index > _currentPlaylist!.trackIds.length - 1
|
||||||
|
? 0
|
||||||
|
: index < 0
|
||||||
|
? _currentPlaylist!.trackIds.length
|
||||||
|
: index;
|
||||||
|
Track? track = _currentPlaylist!.tracks.asMap().containsKey(safeIndex)
|
||||||
|
? _currentPlaylist!.tracks.elementAt(safeIndex)
|
||||||
|
: null;
|
||||||
|
if (track != null) {
|
||||||
|
duration = null;
|
||||||
|
_currentTrack = track;
|
||||||
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
|
// starts to play the newly entered next/prev track
|
||||||
|
startPlaying();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startPlaying([Track? track]) async {
|
||||||
|
_logger.v("[Track Playing] ${track?.name} - ${track?.id}");
|
||||||
|
try {
|
||||||
|
// the track is already playing so no need to change that
|
||||||
|
if (track != null && track.id == _currentTrack?.id) return;
|
||||||
|
track ??= _currentTrack;
|
||||||
|
if (track != null) {
|
||||||
|
Uri? parsedUri = Uri.tryParse(track.uri ?? "");
|
||||||
|
final tag = MediaItem(
|
||||||
|
id: track.id!,
|
||||||
|
title: track.name!,
|
||||||
|
album: track.album?.name,
|
||||||
|
artist: artistsToString(track.artists ?? <ArtistSimple>[]),
|
||||||
|
artUri: Uri.parse(imageToUrlString(track.album?.images)),
|
||||||
|
);
|
||||||
|
player.addItem(tag);
|
||||||
|
if (parsedUri != null && parsedUri.hasAbsolutePath) {
|
||||||
|
_currentAudioSource = UrlSource(parsedUri.toString());
|
||||||
|
await player.core
|
||||||
|
.play(
|
||||||
|
_currentAudioSource!,
|
||||||
|
)
|
||||||
|
.then((value) async {
|
||||||
|
_currentTrack = track;
|
||||||
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
|
final spotubeTrack = await toSpotubeTrack(
|
||||||
|
youtube: youtube,
|
||||||
|
track: track,
|
||||||
|
format: preferences.ytSearchFormat,
|
||||||
|
matchAlgorithm: preferences.trackMatchAlgorithm,
|
||||||
|
audioQuality: preferences.audioQuality,
|
||||||
|
box: cacheTrackBox,
|
||||||
|
);
|
||||||
|
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
|
||||||
|
logger.v("[Track Direct Source] - ${spotubeTrack.ytUri}");
|
||||||
|
_currentAudioSource = UrlSource(spotubeTrack.ytUri);
|
||||||
|
await player.core
|
||||||
|
.play(
|
||||||
|
_currentAudioSource!,
|
||||||
|
)
|
||||||
|
.then((value) {
|
||||||
|
_currentTrack = spotubeTrack;
|
||||||
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
_logger.e("startPlaying", e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void shuffle() {
|
||||||
|
if (currentPlaylist?.shuffle() == true) {
|
||||||
|
_shuffled = true;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void unshuffle() {
|
||||||
|
if (currentPlaylist?.unshuffle() == true) {
|
||||||
|
_shuffled = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> loadFromLocal(Map<String, dynamic> map) {
|
||||||
|
if (map["currentPlaylist"] != null) {
|
||||||
|
_currentPlaylist =
|
||||||
|
CurrentPlaylist.fromJson(jsonDecode(map["currentPlaylist"]));
|
||||||
|
}
|
||||||
|
if (map["currentTrack"] != null) {
|
||||||
|
_currentTrack = Track.fromJson(jsonDecode(map["currentTrack"]));
|
||||||
|
startPlaying().then((_) {
|
||||||
|
Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||||
|
if (player.core.state == PlayerState.playing) {
|
||||||
|
player.pause();
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
volume = map["volume"] ?? volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<Map<String, dynamic>> toMap() {
|
||||||
|
return {
|
||||||
|
"currentPlaylist": currentPlaylist != null
|
||||||
|
? jsonEncode(currentPlaylist?.toJson())
|
||||||
|
: null,
|
||||||
|
"currentTrack":
|
||||||
|
currentTrack != null ? jsonEncode(currentTrack?.toJson()) : null,
|
||||||
|
"volume": volume,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final legacyPlaybackProvider = ChangeNotifierProvider<LegacyPlayback>((ref) {
|
||||||
|
final player = AudioPlayerHandler();
|
||||||
|
final youtube = ref.watch(youtubeProvider);
|
||||||
|
final dbus = ref.watch(dbusClientProvider);
|
||||||
|
return LegacyPlayback(
|
||||||
|
player: player,
|
||||||
|
youtube: youtube,
|
||||||
|
ref: ref,
|
||||||
|
dbus: dbus,
|
||||||
|
);
|
||||||
|
});
|
@ -1,330 +1,364 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
import 'package:dbus/dbus.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/entities/CacheTrack.dart';
|
import 'package:spotube/entities/CacheTrack.dart';
|
||||||
|
import 'package:spotube/extensions/yt-video-from-cache-track.dart';
|
||||||
import 'package:spotube/helpers/artist-to-string.dart';
|
import 'package:spotube/helpers/artist-to-string.dart';
|
||||||
|
import 'package:spotube/helpers/contains-text-in-bracket.dart';
|
||||||
|
import 'package:spotube/helpers/getLyrics.dart';
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/helpers/search-youtube.dart';
|
import 'package:spotube/helpers/search-youtube.dart';
|
||||||
import 'package:spotube/interfaces/media_player2.dart';
|
import 'package:spotube/interfaces/media_player2.dart';
|
||||||
import 'package:spotube/interfaces/media_player2_player.dart';
|
import 'package:spotube/interfaces/media_player2_player.dart';
|
||||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/DBus.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
|
import 'package:spotube/provider/AudioPlayer.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
||||||
import 'package:spotube/utils/PersistedChangeNotifier.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Playlist;
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:spotube/extensions/list-sort-multiple.dart';
|
||||||
|
|
||||||
class Playback extends PersistedChangeNotifier {
|
class Playback with ChangeNotifier {
|
||||||
UrlSource? _currentAudioSource;
|
// player properties
|
||||||
final _logger = getLogger(Playback);
|
bool isShuffled;
|
||||||
CurrentPlaylist? _currentPlaylist;
|
bool isPlaying;
|
||||||
Track? _currentTrack;
|
Duration currentDuration;
|
||||||
|
double volume;
|
||||||
|
|
||||||
// states
|
// class dependencies
|
||||||
bool _isPlaying = false;
|
Media_Player? linuxMPRIS;
|
||||||
Duration? duration;
|
Player_Interface? linuxMPRIS_Player;
|
||||||
|
AudioPlayerHandler? mobileAudioService;
|
||||||
|
|
||||||
bool _shuffled = false;
|
// foreign/passed properties
|
||||||
|
AudioPlayer player;
|
||||||
AudioPlayerHandler player;
|
|
||||||
YoutubeExplode youtube;
|
YoutubeExplode youtube;
|
||||||
Ref ref;
|
Ref ref;
|
||||||
|
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
LazyBox<CacheTrack>? cacheTrackBox;
|
// playlist & track list properties
|
||||||
|
late LazyBox<CacheTrack> cache;
|
||||||
|
CurrentPlaylist? playlist;
|
||||||
|
SpotubeTrack? track;
|
||||||
|
|
||||||
@protected
|
// internal stuff
|
||||||
final DBusClient? dbus;
|
final List<StreamSubscription> _subscriptions;
|
||||||
Media_Player? _media_player;
|
final _logger = getLogger(Playback);
|
||||||
Player_Interface? _mpris;
|
|
||||||
|
|
||||||
double volume = 1;
|
|
||||||
|
|
||||||
Playback({
|
Playback({
|
||||||
required this.player,
|
required this.player,
|
||||||
required this.youtube,
|
required this.youtube,
|
||||||
required this.ref,
|
required this.ref,
|
||||||
required this.dbus,
|
this.mobileAudioService,
|
||||||
CurrentPlaylist? currentPlaylist,
|
}) : volume = 0,
|
||||||
Track? currentTrack,
|
isShuffled = false,
|
||||||
}) : _currentPlaylist = currentPlaylist,
|
isPlaying = false,
|
||||||
_currentTrack = currentTrack,
|
currentDuration = Duration.zero,
|
||||||
|
_subscriptions = [],
|
||||||
super() {
|
super() {
|
||||||
player.onNextRequest = () {
|
|
||||||
movePlaylistPositionBy(1);
|
|
||||||
};
|
|
||||||
player.onPreviousRequest = () {
|
|
||||||
movePlaylistPositionBy(-1);
|
|
||||||
};
|
|
||||||
|
|
||||||
_init();
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamSubscription<Duration?>? _durationStream;
|
|
||||||
StreamSubscription<PlayerState>? _playingStream;
|
|
||||||
StreamSubscription<Duration>? _positionStream;
|
|
||||||
|
|
||||||
void _init() async {
|
|
||||||
// dbus m.p.r.i.s stuff
|
|
||||||
if (Platform.isLinux) {
|
if (Platform.isLinux) {
|
||||||
try {
|
linuxMPRIS = Media_Player();
|
||||||
_media_player = Media_Player();
|
linuxMPRIS_Player = Player_Interface(playback: this);
|
||||||
_mpris = Player_Interface(player: player.core, playback: this);
|
|
||||||
final nameStatus =
|
|
||||||
await dbus?.requestName("org.mpris.MediaPlayer2.spotube");
|
|
||||||
if (nameStatus == DBusRequestNameReply.exists) {
|
|
||||||
await dbus
|
|
||||||
?.requestName("org.mpris.MediaPlayer2.spotube.instance$pid");
|
|
||||||
}
|
|
||||||
await dbus?.registerObject(_media_player!);
|
|
||||||
await dbus?.registerObject(_mpris!);
|
|
||||||
} catch (e) {
|
|
||||||
logger.e("[MPRIS initialization error]", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheTrackBox = await Hive.openLazyBox<CacheTrack>("track-cache");
|
(() async {
|
||||||
|
cache = await Hive.openLazyBox<CacheTrack>("track-cache");
|
||||||
_playingStream = player.core.onPlayerStateChanged.listen(
|
_subscriptions.addAll([
|
||||||
|
player.onPlayerStateChanged.listen(
|
||||||
(state) async {
|
(state) async {
|
||||||
_isPlaying = state == PlayerState.playing;
|
isPlaying = state == PlayerState.playing;
|
||||||
if (state == PlayerState.completed) {
|
|
||||||
if (_currentTrack?.id != null) {
|
|
||||||
movePlaylistPositionBy(1);
|
|
||||||
} else {
|
|
||||||
_isPlaying = false;
|
|
||||||
duration = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
|
player.onPlayerComplete.listen((_) {
|
||||||
_durationStream = player.core.onDurationChanged.listen((event) {
|
if (track?.id != null) {
|
||||||
duration = event;
|
seekForward();
|
||||||
notifyListeners();
|
} else {
|
||||||
});
|
isPlaying = false;
|
||||||
|
currentDuration = Duration.zero;
|
||||||
_positionStream = player.core.onPositionChanged.listen((pos) async {
|
|
||||||
if (pos > Duration.zero &&
|
|
||||||
(duration == null || duration == Duration.zero)) {
|
|
||||||
duration = await player.core.getDuration();
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
});
|
}),
|
||||||
|
player.onDurationChanged.listen((event) {
|
||||||
|
currentDuration = event;
|
||||||
|
notifyListeners();
|
||||||
|
}),
|
||||||
|
player.onPositionChanged.listen((pos) async {
|
||||||
|
if (pos > Duration.zero && currentDuration == Duration.zero) {
|
||||||
|
currentDuration = await player.getDuration() ?? Duration.zero;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_playingStream?.cancel();
|
linuxMPRIS?.dispose();
|
||||||
_durationStream?.cancel();
|
linuxMPRIS_Player?.dispose();
|
||||||
_positionStream?.cancel();
|
for (var subscription in _subscriptions) {
|
||||||
cacheTrackBox?.close();
|
subscription.cancel();
|
||||||
if (Platform.isLinux && _media_player != null && _mpris != null) {
|
|
||||||
dbus?.unregisterObject(_media_player!);
|
|
||||||
dbus?.unregisterObject(_mpris!);
|
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get shuffled => _shuffled;
|
Future<void> playPlaylist(CurrentPlaylist playlist, [int index = 0]) async {
|
||||||
CurrentPlaylist? get currentPlaylist => _currentPlaylist;
|
if (index < 0 || index > playlist.tracks.length - 1) return;
|
||||||
Track? get currentTrack => _currentTrack;
|
this.playlist = playlist;
|
||||||
bool get isPlaying => _isPlaying;
|
final played = this.playlist!.tracks[index];
|
||||||
|
await play(played).then((_) {
|
||||||
set setCurrentTrack(Track track) {
|
int i = this
|
||||||
_logger.v("[Setting Current Track] ${track.name} - ${track.id}");
|
.playlist!
|
||||||
_currentTrack = track;
|
.tracks
|
||||||
notifyListeners();
|
.indexWhere((element) => element.id == played.id);
|
||||||
updatePersistence();
|
if (index == -1) return;
|
||||||
|
this.playlist!.tracks[i] = track!;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
set setCurrentPlaylist(CurrentPlaylist playlist) {
|
// player methods
|
||||||
_logger.v("[Current Playlist Changed] ${playlist.name} - ${playlist.id}");
|
Future<void> play([Track? track]) async {
|
||||||
_currentPlaylist = playlist;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() {
|
|
||||||
_logger.v("Playback Reset");
|
|
||||||
_isPlaying = false;
|
|
||||||
_shuffled = false;
|
|
||||||
duration = null;
|
|
||||||
_currentPlaylist = null;
|
|
||||||
_currentTrack = null;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence(clearNullEntries: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setVolume(double newVolume) {
|
|
||||||
volume = newVolume;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// sets the provided id matched track's uri\
|
|
||||||
/// Doesn't notify listeners\
|
|
||||||
/// @returns `bool` - `true` if succeed & `false` when failed
|
|
||||||
bool setTrackUriById(String id, String uri) {
|
|
||||||
if (_currentPlaylist == null) return false;
|
|
||||||
try {
|
|
||||||
int index =
|
|
||||||
_currentPlaylist!.tracks.indexWhere((element) => element.id == id);
|
|
||||||
if (index == -1) return false;
|
|
||||||
_currentPlaylist!.tracks[index].uri = uri;
|
|
||||||
updatePersistence();
|
|
||||||
return _currentPlaylist!.tracks[index].uri == uri;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void movePlaylistPositionBy(int pos) {
|
|
||||||
_logger.v("[Playlist Position Move] $pos");
|
|
||||||
if (_currentTrack != null && _currentPlaylist != null) {
|
|
||||||
int index = _currentPlaylist!.trackIds.indexOf(_currentTrack!.id!) + pos;
|
|
||||||
|
|
||||||
var safeIndex = index > _currentPlaylist!.trackIds.length - 1
|
|
||||||
? 0
|
|
||||||
: index < 0
|
|
||||||
? _currentPlaylist!.trackIds.length
|
|
||||||
: index;
|
|
||||||
Track? track = _currentPlaylist!.tracks.asMap().containsKey(safeIndex)
|
|
||||||
? _currentPlaylist!.tracks.elementAt(safeIndex)
|
|
||||||
: null;
|
|
||||||
if (track != null) {
|
|
||||||
duration = null;
|
|
||||||
_currentTrack = track;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
// starts to play the newly entered next/prev track
|
|
||||||
startPlaying();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> startPlaying([Track? track]) async {
|
|
||||||
_logger.v("[Track Playing] ${track?.name} - ${track?.id}");
|
_logger.v("[Track Playing] ${track?.name} - ${track?.id}");
|
||||||
try {
|
try {
|
||||||
// the track is already playing so no need to change that
|
// the track is already playing so no need to change that
|
||||||
if (track != null && track.id == _currentTrack?.id) return;
|
if ((track != null && track.id == this.track?.id) ||
|
||||||
track ??= _currentTrack;
|
(this.track == null && track == null)) return;
|
||||||
if (track != null) {
|
track ??= this.track;
|
||||||
Uri? parsedUri = Uri.tryParse(track.uri ?? "");
|
|
||||||
final tag = MediaItem(
|
final tag = MediaItem(
|
||||||
id: track.id!,
|
id: track!.id!,
|
||||||
title: track.name!,
|
title: track.name!,
|
||||||
album: track.album?.name,
|
album: track.album?.name,
|
||||||
artist: artistsToString(track.artists ?? <ArtistSimple>[]),
|
artist: artistsToString(track.artists ?? <ArtistSimple>[]),
|
||||||
artUri: Uri.parse(imageToUrlString(track.album?.images)),
|
artUri: Uri.parse(imageToUrlString(track.album?.images)),
|
||||||
);
|
);
|
||||||
player.addItem(tag);
|
mobileAudioService?.addItem(tag);
|
||||||
if (parsedUri != null && parsedUri.hasAbsolutePath) {
|
|
||||||
_currentAudioSource = UrlSource(parsedUri.toString());
|
// the track is not a SpotubeTrack so turning it to one
|
||||||
await player.core
|
if (track is! SpotubeTrack) {
|
||||||
.play(
|
track = await toSpotubeTrack(track);
|
||||||
_currentAudioSource!,
|
}
|
||||||
)
|
_logger.v("[Track Direct Source] - ${(track).ytUri}");
|
||||||
.then((value) async {
|
await player.play(UrlSource(track.ytUri)).then((_) {
|
||||||
_currentTrack = track;
|
this.track = track as SpotubeTrack;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
updatePersistence();
|
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
|
||||||
final spotubeTrack = await toSpotubeTrack(
|
|
||||||
youtube: youtube,
|
|
||||||
track: track,
|
|
||||||
format: preferences.ytSearchFormat,
|
|
||||||
matchAlgorithm: preferences.trackMatchAlgorithm,
|
|
||||||
audioQuality: preferences.audioQuality,
|
|
||||||
box: cacheTrackBox,
|
|
||||||
);
|
|
||||||
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
|
|
||||||
logger.v("[Track Direct Source] - ${spotubeTrack.ytUri}");
|
|
||||||
_currentAudioSource = UrlSource(spotubeTrack.ytUri);
|
|
||||||
await player.core
|
|
||||||
.play(
|
|
||||||
_currentAudioSource!,
|
|
||||||
)
|
|
||||||
.then((value) {
|
|
||||||
_currentTrack = spotubeTrack;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
_logger.e("startPlaying", e, stack);
|
_logger.e("play", e, stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void shuffle() {
|
Future<void> resume() async {
|
||||||
if (currentPlaylist?.shuffle() == true) {
|
if (isPlaying || (playlist == null && track == null)) return;
|
||||||
_shuffled = true;
|
await player.resume();
|
||||||
|
isPlaying = true;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pause() async {
|
||||||
|
if (!isPlaying || (playlist == null && track == null)) return;
|
||||||
|
await player.pause();
|
||||||
|
isPlaying = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> togglePlayPause() async {
|
||||||
|
isPlaying ? await pause() : await resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleShuffle() {
|
||||||
|
final result = isShuffled ? playlist?.unshuffle() : playlist?.shuffle();
|
||||||
|
if (result == true) {
|
||||||
|
isShuffled = !isShuffled;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void unshuffle() {
|
Future<void> seekPosition(Duration position) {
|
||||||
if (currentPlaylist?.unshuffle() == true) {
|
return player.seek(position);
|
||||||
_shuffled = false;
|
}
|
||||||
|
|
||||||
|
Future<void> setVolume(double newVolume) async {
|
||||||
|
await player.setVolume(volume);
|
||||||
|
volume = newVolume;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> stop() async {
|
||||||
|
await player.stop();
|
||||||
|
await player.release();
|
||||||
|
isPlaying = false;
|
||||||
|
isShuffled = false;
|
||||||
|
playlist = null;
|
||||||
|
track = null;
|
||||||
|
currentDuration = Duration.zero;
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
destroy() {}
|
||||||
FutureOr<void> loadFromLocal(Map<String, dynamic> map) {
|
|
||||||
if (map["currentPlaylist"] != null) {
|
|
||||||
_currentPlaylist =
|
|
||||||
CurrentPlaylist.fromJson(jsonDecode(map["currentPlaylist"]));
|
|
||||||
}
|
|
||||||
if (map["currentTrack"] != null) {
|
|
||||||
_currentTrack = Track.fromJson(jsonDecode(map["currentTrack"]));
|
|
||||||
startPlaying().then((_) {
|
|
||||||
Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
|
||||||
if (player.core.state == PlayerState.playing) {
|
|
||||||
player.pause();
|
|
||||||
timer.cancel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
volume = map["volume"] ?? volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
// playlist & track list methods
|
||||||
FutureOr<Map<String, dynamic>> toMap() {
|
Future<SpotubeTrack> toSpotubeTrack(Track track) async {
|
||||||
|
final format = preferences.ytSearchFormat;
|
||||||
|
final matchAlgorithm = preferences.trackMatchAlgorithm;
|
||||||
|
final artistsName =
|
||||||
|
track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ??
|
||||||
|
[];
|
||||||
|
final audioQuality = preferences.audioQuality;
|
||||||
|
_logger.v("[Track Search Artists] $artistsName");
|
||||||
|
final mainArtist = artistsName.first;
|
||||||
|
final featuredArtists = artistsName.length > 1
|
||||||
|
? "feat. " + artistsName.sublist(1).join(" ")
|
||||||
|
: "";
|
||||||
|
final title = getTitle(
|
||||||
|
track.name!,
|
||||||
|
artists: artistsName,
|
||||||
|
onlyCleanArtist: true,
|
||||||
|
).trim();
|
||||||
|
_logger.v("[Track Search Title] $title");
|
||||||
|
final queryString = format
|
||||||
|
.replaceAll("\$MAIN_ARTIST", mainArtist)
|
||||||
|
.replaceAll("\$TITLE", title)
|
||||||
|
.replaceAll("\$FEATURED_ARTISTS", featuredArtists);
|
||||||
|
_logger.v("[Youtube Search Term] $queryString");
|
||||||
|
|
||||||
|
Video ytVideo;
|
||||||
|
final cachedTrack = await cache.get(track.id);
|
||||||
|
if (cachedTrack != null && cachedTrack.mode == matchAlgorithm.name) {
|
||||||
|
_logger.v(
|
||||||
|
"[Playing track from cache] youtubeId: ${cachedTrack.id} mode: ${cachedTrack.mode}",
|
||||||
|
);
|
||||||
|
ytVideo = VideoFromCacheTrackExtension.fromCacheTrack(cachedTrack);
|
||||||
|
} else {
|
||||||
|
VideoSearchList videos = await youtube.search.search(queryString);
|
||||||
|
if (matchAlgorithm != SpotubeTrackMatchAlgorithm.youtube) {
|
||||||
|
List<Map> ratedRankedVideos = videos
|
||||||
|
.map((video) {
|
||||||
|
// the find should be lazy thus everything case insensitive
|
||||||
|
final ytTitle = video.title.toLowerCase();
|
||||||
|
final bool hasTitle = ytTitle.contains(title);
|
||||||
|
final bool hasAllArtists = track.artists?.every(
|
||||||
|
(artist) => ytTitle.contains(artist.name!.toLowerCase()),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
final bool authorIsArtist =
|
||||||
|
track.artists?.first.name?.toLowerCase() ==
|
||||||
|
video.author.toLowerCase();
|
||||||
|
|
||||||
|
final bool hasNoLiveInTitle =
|
||||||
|
!containsTextInBracket(ytTitle, "live");
|
||||||
|
|
||||||
|
int rate = 0;
|
||||||
|
for (final el in [
|
||||||
|
hasTitle,
|
||||||
|
hasAllArtists,
|
||||||
|
if (matchAlgorithm ==
|
||||||
|
SpotubeTrackMatchAlgorithm.authenticPopular)
|
||||||
|
authorIsArtist,
|
||||||
|
hasNoLiveInTitle,
|
||||||
|
!video.isLive,
|
||||||
|
]) {
|
||||||
|
if (el) rate++;
|
||||||
|
}
|
||||||
|
// can't let pass any non title matching track
|
||||||
|
if (!hasTitle) rate = rate - 2;
|
||||||
return {
|
return {
|
||||||
"currentPlaylist": currentPlaylist != null
|
"video": video,
|
||||||
? jsonEncode(currentPlaylist?.toJson())
|
"points": rate,
|
||||||
: null,
|
"views": video.engagement.viewCount,
|
||||||
"currentTrack":
|
|
||||||
currentTrack != null ? jsonEncode(currentTrack?.toJson()) : null,
|
|
||||||
"volume": volume,
|
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
.toList()
|
||||||
|
.sortByProperties(
|
||||||
|
[false, false],
|
||||||
|
["points", "views"],
|
||||||
|
);
|
||||||
|
|
||||||
|
ytVideo = ratedRankedVideos.first["video"] as Video;
|
||||||
|
} else {
|
||||||
|
ytVideo = videos.where((video) => !video.isLive).first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id);
|
||||||
|
|
||||||
|
_logger.v(
|
||||||
|
"[YouTube Matched Track] ${ytVideo.title} | ${ytVideo.author} - ${ytVideo.url}",
|
||||||
|
);
|
||||||
|
|
||||||
|
final audioManifest = trackManifest.audioOnly.where((info) {
|
||||||
|
final isMp4a = info.codec.mimeType == "audio/mp4";
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
return !isMp4a;
|
||||||
|
} else if (Platform.isMacOS || Platform.isIOS) {
|
||||||
|
return isMp4a;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final ytUri = (audioQuality == AudioQuality.high
|
||||||
|
? audioManifest.withHighestBitrate()
|
||||||
|
: audioManifest.sortByBitrate().last)
|
||||||
|
.url
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
// only save when the track isn't available in the cache with same
|
||||||
|
// matchAlgorithm
|
||||||
|
if (cachedTrack == null || cachedTrack.mode != matchAlgorithm.name) {
|
||||||
|
await cache.put(
|
||||||
|
track.id!, CacheTrack.fromVideo(ytVideo, matchAlgorithm.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SpotubeTrack.fromTrack(
|
||||||
|
track: track,
|
||||||
|
ytTrack: ytVideo,
|
||||||
|
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
|
||||||
|
// ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg'
|
||||||
|
// codec/mimetype for those Platforms
|
||||||
|
ytUri: ytUri,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setPlaylistPosition(int position) async {
|
||||||
|
if (playlist == null) return;
|
||||||
|
await playPlaylist(playlist!, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> seekForward() async {
|
||||||
|
if (playlist == null || track == null) return;
|
||||||
|
final int nextTrackIndex =
|
||||||
|
(playlist!.trackIds.indexOf(track!.id!) + 1).toInt();
|
||||||
|
// checking if there's any track available forward
|
||||||
|
if (nextTrackIndex > (playlist?.tracks.length ?? 0) - 1) return;
|
||||||
|
await play(playlist!.tracks.elementAt(nextTrackIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> seekBackward() async {
|
||||||
|
if (playlist == null || track == null) return;
|
||||||
|
final int prevTrackIndex =
|
||||||
|
(playlist!.trackIds.indexOf(track!.id!) - 1).toInt();
|
||||||
|
// checking if there's any track available behind
|
||||||
|
if (prevTrackIndex < 0) return;
|
||||||
|
await play(playlist!.tracks.elementAt(prevTrackIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
|
final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
|
||||||
final player = AudioPlayerHandler();
|
|
||||||
final youtube = ref.watch(youtubeProvider);
|
final youtube = ref.watch(youtubeProvider);
|
||||||
final dbus = ref.watch(dbusClientProvider);
|
final player = ref.watch(audioPlayerProvider);
|
||||||
return Playback(
|
return Playback(
|
||||||
player: player,
|
player: player,
|
||||||
youtube: youtube,
|
youtube: youtube,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
dbus: dbus,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -166,8 +166,7 @@ final searchQuery = FutureProvider.family<List<Page>, String>((ref, term) {
|
|||||||
|
|
||||||
final geniusLyricsQuery = FutureProvider<String?>(
|
final geniusLyricsQuery = FutureProvider<String?>(
|
||||||
(ref) {
|
(ref) {
|
||||||
final currentTrack =
|
final currentTrack = ref.watch(playbackProvider.select((s) => s.track));
|
||||||
ref.watch(playbackProvider.select((s) => s.currentTrack));
|
|
||||||
final geniusAccessToken =
|
final geniusAccessToken =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.geniusAccessToken));
|
ref.watch(userPreferencesProvider.select((s) => s.geniusAccessToken));
|
||||||
if (currentTrack == null) {
|
if (currentTrack == null) {
|
||||||
@ -184,8 +183,7 @@ final geniusLyricsQuery = FutureProvider<String?>(
|
|||||||
|
|
||||||
final rentanadviserLyricsQuery = FutureProvider<SubtitleSimple?>(
|
final rentanadviserLyricsQuery = FutureProvider<SubtitleSimple?>(
|
||||||
(ref) {
|
(ref) {
|
||||||
final currentTrack =
|
final currentTrack = ref.watch(playbackProvider.select((s) => s.track));
|
||||||
ref.watch(playbackProvider.select((s) => s.currentTrack));
|
|
||||||
if (currentTrack == null) return null;
|
if (currentTrack == null) return null;
|
||||||
return getTimedLyrics(currentTrack as SpotubeTrack);
|
return getTimedLyrics(currentTrack as SpotubeTrack);
|
||||||
},
|
},
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
|
|
||||||
/// An [AudioHandler] for playing a single item.
|
/// An [AudioHandler] for playing a single item.
|
||||||
class AudioPlayerHandler extends BaseAudioHandler {
|
class AudioPlayerHandler extends BaseAudioHandler {
|
||||||
final _player = AudioPlayer();
|
final Playback playback;
|
||||||
|
|
||||||
FutureOr<void> Function()? onNextRequest;
|
|
||||||
FutureOr<void> Function()? onPreviousRequest;
|
|
||||||
|
|
||||||
/// Initialise our audio handler.
|
/// Initialise our audio handler.
|
||||||
AudioPlayerHandler() {
|
AudioPlayerHandler(this.playback) {
|
||||||
|
final _player = playback.player;
|
||||||
// So that our clients (the Flutter UI and the system notification) know
|
// So that our clients (the Flutter UI and the system notification) know
|
||||||
// what state to display, here we set up our audio handler to broadcast all
|
// what state to display, here we set up our audio handler to broadcast all
|
||||||
// playback state changes as they happen via playbackState...
|
// playback state changes as they happen via playbackState...
|
||||||
@ -27,8 +25,6 @@ class AudioPlayerHandler extends BaseAudioHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioPlayer get core => _player;
|
|
||||||
|
|
||||||
void addItem(MediaItem item) {
|
void addItem(MediaItem item) {
|
||||||
mediaItem.add(item);
|
mediaItem.add(item);
|
||||||
}
|
}
|
||||||
@ -39,32 +35,32 @@ class AudioPlayerHandler extends BaseAudioHandler {
|
|||||||
// your audio playback logic in one place.
|
// your audio playback logic in one place.
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> play() => _player.resume();
|
Future<void> play() => playback.resume();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> pause() => _player.pause();
|
Future<void> pause() => playback.pause();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> seek(Duration position) => _player.seek(position);
|
Future<void> seek(Duration position) => playback.seekPosition(position);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> stop() => _player.stop();
|
Future<void> stop() => playback.stop();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> skipToNext() async {
|
Future<void> skipToNext() async {
|
||||||
await onNextRequest?.call();
|
playback.seekForward();
|
||||||
await super.skipToNext();
|
await super.skipToNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> skipToPrevious() async {
|
Future<void> skipToPrevious() async {
|
||||||
await onPreviousRequest?.call();
|
playback.seekBackward();
|
||||||
await super.skipToPrevious();
|
await super.skipToPrevious();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onTaskRemoved() {
|
Future<void> onTaskRemoved() {
|
||||||
_player.stop();
|
playback.destroy();
|
||||||
return super.onTaskRemoved();
|
return super.onTaskRemoved();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,16 +73,14 @@ class AudioPlayerHandler extends BaseAudioHandler {
|
|||||||
return PlaybackState(
|
return PlaybackState(
|
||||||
controls: [
|
controls: [
|
||||||
MediaControl.skipToPrevious,
|
MediaControl.skipToPrevious,
|
||||||
if (_player.state == PlayerState.playing)
|
if (playback.isPlaying) MediaControl.pause else MediaControl.play,
|
||||||
MediaControl.pause
|
|
||||||
else
|
|
||||||
MediaControl.play,
|
|
||||||
MediaControl.skipToNext,
|
MediaControl.skipToNext,
|
||||||
MediaControl.stop,
|
MediaControl.stop,
|
||||||
],
|
],
|
||||||
androidCompactActionIndices: const [0, 1, 2],
|
androidCompactActionIndices: const [0, 1, 2],
|
||||||
playing: _player.state == PlayerState.playing,
|
playing: playback.isPlaying,
|
||||||
updatePosition: (await _player.getCurrentPosition()) ?? Duration.zero,
|
updatePosition:
|
||||||
|
(await playback.player.getCurrentPosition()) ?? Duration.zero,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user