feat: bring pre download on desktop, disable pre download for long videos

fix: audio service calling self ref of playlist queue provider
This commit is contained in:
Kingkor Roy Tirtho 2023-02-03 13:21:41 +06:00
parent 3ccb525260
commit 1d82bb0987
10 changed files with 95 additions and 72 deletions

View File

@ -92,15 +92,19 @@ class SiblingTracksSheet extends HookConsumerWidget {
subtitle: PlatformText(video.author), subtitle: PlatformText(video.author),
enabled: playlist?.isLoading != true, enabled: playlist?.isLoading != true,
selected: playlist?.isLoading != true && selected: playlist?.isLoading != true &&
video.id == video.id.value ==
(playlist?.activeTrack as SpotubeTrack).ytTrack.id, (playlist?.activeTrack as SpotubeTrack)
.ytTrack
.id
.value,
selectedTileColor: Theme.of(context).popupMenuTheme.color, selectedTileColor: Theme.of(context).popupMenuTheme.color,
onTap: () async { onTap: () async {
if (playlist?.isLoading == false && if (playlist?.isLoading == false &&
video.id != video.id.value !=
(playlist?.activeTrack as SpotubeTrack) (playlist?.activeTrack as SpotubeTrack)
.ytTrack .ytTrack
.id) { .id
.value) {
await playlistNotifier.swapSibling(video); await playlistNotifier.swapSibling(video);
} }
}, },

View File

@ -124,10 +124,11 @@ class BottomPlayer extends HookConsumerWidget {
useEffect(() { useEffect(() {
if (volume.value != volumeState) { if (volume.value != volumeState) {
volumeNotifier.setVolume(volume.value); volume.value = volumeState;
} }
return null; return null;
}, [volumeState]); }, [volumeState]);
return Listener( return Listener(
onPointerSignal: (event) async { onPointerSignal: (event) async {
if (event is PointerScrollEvent) { if (event is PointerScrollEvent) {
@ -147,12 +148,7 @@ class BottomPlayer extends HookConsumerWidget {
onChanged: (v) { onChanged: (v) {
volume.value = v; volume.value = v;
}, },
onChangeEnd: (value) async { onChangeEnd: volumeNotifier.setVolume,
// You don't really need to know why but this
// way it works only
await volumeNotifier.setVolume(value);
await volumeNotifier.setVolume(value);
},
), ),
); );
}), }),

View File

@ -136,6 +136,10 @@ extension GetSkipSegments on Video {
}, },
)); ));
if (res.body == "Not Found") {
return List.castFrom<dynamic, Map<String, int>>([]);
}
final data = jsonDecode(res.body); final data = jsonDecode(res.body);
final segments = data.map((obj) { final segments = data.map((obj) {
return Map.castFrom<String, dynamic, String, int>({ return Map.castFrom<String, dynamic, String, int>({

View File

@ -136,7 +136,8 @@ class SpotubeTrack extends Track {
); );
} }
if (preferences.androidBytesPlay) { if (preferences.predownload &&
ytVideo.duration! < const Duration(minutes: 15)) {
await DefaultCacheManager().getFileFromCache(track.id!).then( await DefaultCacheManager().getFileFromCache(track.id!).then(
(file) async { (file) async {
if (file != null) return file.file; if (file != null) return file.file;
@ -232,7 +233,8 @@ class SpotubeTrack extends Track {
); );
} }
if (preferences.androidBytesPlay) { if (preferences.predownload &&
video.duration! < const Duration(minutes: 15)) {
await DefaultCacheManager().getFileFromCache(id!).then( await DefaultCacheManager().getFileFromCache(id!).then(
(file) async { (file) async {
if (file != null) return file.file; if (file != null) return file.file;

View File

@ -15,7 +15,6 @@ import 'package:spotube/collections/spotify_markets.dart';
import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/provider/auth_provider.dart'; import 'package:spotube/provider/auth_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class SettingsPage extends HookConsumerWidget { class SettingsPage extends HookConsumerWidget {
@ -308,7 +307,6 @@ class SettingsPage extends HookConsumerWidget {
}, },
), ),
), ),
if (kIsMobile)
PlatformListTile( PlatformListTile(
leading: const Icon(SpotubeIcons.download), leading: const Icon(SpotubeIcons.download),
title: const PlatformText( title: const PlatformText(
@ -318,9 +316,9 @@ class SettingsPage extends HookConsumerWidget {
"Instead of streaming audio, download bytes and play instead (Recommended for higher bandwidth users)", "Instead of streaming audio, download bytes and play instead (Recommended for higher bandwidth users)",
), ),
trailing: PlatformSwitch( trailing: PlatformSwitch(
value: preferences.androidBytesPlay, value: preferences.predownload,
onChanged: (state) { onChanged: (state) {
preferences.setAndroidBytesPlay(state); preferences.setPredownload(state);
}, },
), ),
), ),

View File

@ -14,6 +14,7 @@ import 'package:spotube/utils/persisted_state_notifier.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Playlist; import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Playlist;
import 'package:collection/collection.dart';
final audioPlayer = AudioPlayer(); final audioPlayer = AudioPlayer();
final youtube = YoutubeExplode(); final youtube = YoutubeExplode();
@ -24,20 +25,32 @@ class PlaylistQueue {
Track get activeTrack => tracks.elementAt(active); Track get activeTrack => tracks.elementAt(active);
factory PlaylistQueue.fromJson(Map<String, dynamic> json) { static Future<PlaylistQueue> fromJson(
Map<String, dynamic> json, UserPreferences preferences) async {
final List? tracks = json['tracks'];
return PlaylistQueue( return PlaylistQueue(
Set.from(json['tracks'].map( Set.from(
(e) { await Future.wait(
final json = Map.castFrom<dynamic, dynamic, String, dynamic>(e); tracks?.mapIndexed(
if (e["ytTrack"] != null) { (i, e) async {
return SpotubeTrack.fromJson(json); final jsonTrack =
} else if (e["path"] != null) { Map.castFrom<dynamic, dynamic, String, dynamic>(e);
return LocalTrack.fromJson(json);
if (e["path"] != null) {
return LocalTrack.fromJson(jsonTrack);
} else if (i == json["active"] && !json.containsKey("path")) {
return await SpotubeTrack.fromFetchTrack(
Track.fromJson(jsonTrack),
preferences,
);
} else { } else {
return Track.fromJson(json); return Track.fromJson(jsonTrack);
} }
}, },
)), ) ??
[],
),
),
active: json['active'], active: json['active'],
); );
} }
@ -97,7 +110,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
void configure() async { void configure() async {
if (kIsMobile || kIsMacOS) { if (kIsMobile || kIsMacOS) {
mobileService = await AudioService.init( mobileService = await AudioService.init(
builder: () => MobileAudioService(ref), builder: () => MobileAudioService(this),
config: const AudioServiceConfig( config: const AudioServiceConfig(
androidNotificationChannelId: 'com.krtirtho.Spotube', androidNotificationChannelId: 'com.krtirtho.Spotube',
androidNotificationChannelName: 'Spotube', androidNotificationChannelName: 'Spotube',
@ -106,7 +119,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
); );
} }
if (kIsLinux) { if (kIsLinux) {
linuxService = LinuxAudioService(ref); linuxService = LinuxAudioService(ref, this);
} }
addListener((state) { addListener((state) {
linuxService?.player.updateProperties(); linuxService?.player.updateProperties();
@ -160,10 +173,13 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
static bool get isPaused => audioPlayer.state == PlayerState.paused; static bool get isPaused => audioPlayer.state == PlayerState.paused;
static bool get isStopped => audioPlayer.state == PlayerState.stopped; static bool get isStopped => audioPlayer.state == PlayerState.stopped;
static Stream<Duration> get duration => audioPlayer.onDurationChanged; static Stream<Duration> get duration =>
static Stream<Duration> get position => audioPlayer.onPositionChanged; audioPlayer.onDurationChanged.asBroadcastStream();
static Stream<Duration> get position =>
audioPlayer.onPositionChanged.asBroadcastStream();
static Stream<bool> get playing => audioPlayer.onPlayerStateChanged static Stream<bool> get playing => audioPlayer.onPlayerStateChanged
.map((event) => event == PlayerState.playing); .map((event) => event == PlayerState.playing)
.asBroadcastStream();
List<Video> get siblings => state?.isLoading == false List<Video> get siblings => state?.isLoading == false
? (state!.activeTrack as SpotubeTrack).siblings ? (state!.activeTrack as SpotubeTrack).siblings
@ -192,6 +208,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
..removeAt(state!.active) ..removeAt(state!.active)
..shuffle() ..shuffle()
}, },
active: 0,
); );
} }
@ -199,6 +216,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
if (!isShuffled || !isLoaded) return; if (!isShuffled || !isLoaded) return;
state = state?.copyWith( state = state?.copyWith(
tracks: _tempTracks, tracks: _tempTracks,
active: _tempTracks.toList().indexOf(state!.activeTrack),
); );
_tempTracks = {}; _tempTracks = {};
} }
@ -265,7 +283,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
final cached = final cached =
await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!); await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!);
if (preferences.androidBytesPlay && cached != null) { if (preferences.predownload && cached != null) {
await audioPlayer.play( await audioPlayer.play(
DeviceFileSource(cached.file.path), DeviceFileSource(cached.file.path),
mode: PlayerMode.mediaPlayer, mode: PlayerMode.mediaPlayer,
@ -368,9 +386,9 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
} }
@override @override
PlaylistQueue? fromJson(Map<String, dynamic> json) { Future<PlaylistQueue>? fromJson(Map<String, dynamic> json) {
if (json.isEmpty) return null; if (json.isEmpty) return null;
return PlaylistQueue.fromJson(json); return PlaylistQueue.fromJson(json, preferences);
} }
@override @override

View File

@ -42,7 +42,7 @@ class UserPreferences extends PersistedChangeNotifier {
LayoutMode layoutMode; LayoutMode layoutMode;
bool rotatingAlbumArt; bool rotatingAlbumArt;
bool androidBytesPlay; bool predownload;
UserPreferences({ UserPreferences({
required this.geniusAccessToken, required this.geniusAccessToken,
@ -50,7 +50,7 @@ class UserPreferences extends PersistedChangeNotifier {
required this.themeMode, required this.themeMode,
required this.ytSearchFormat, required this.ytSearchFormat,
required this.layoutMode, required this.layoutMode,
this.androidBytesPlay = true, required this.predownload,
this.saveTrackLyrics = false, this.saveTrackLyrics = false,
this.accentColorScheme = Colors.green, this.accentColorScheme = Colors.green,
this.backgroundColorScheme = Colors.grey, this.backgroundColorScheme = Colors.grey,
@ -70,9 +70,10 @@ class UserPreferences extends PersistedChangeNotifier {
} }
} }
void setAndroidBytesPlay(bool value) { void setPredownload(bool value) {
androidBytesPlay = value; predownload = value;
notifyListeners(); notifyListeners();
updatePersistence();
} }
void setThemeMode(ThemeMode mode) { void setThemeMode(ThemeMode mode) {
@ -203,7 +204,7 @@ class UserPreferences extends PersistedChangeNotifier {
orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact, orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact,
); );
rotatingAlbumArt = map["rotatingAlbumArt"] ?? rotatingAlbumArt; rotatingAlbumArt = map["rotatingAlbumArt"] ?? rotatingAlbumArt;
androidBytesPlay = map["androidBytesPlay"] ?? androidBytesPlay; predownload = map["predownload"] ?? predownload;
} }
@override @override
@ -223,7 +224,7 @@ class UserPreferences extends PersistedChangeNotifier {
"downloadLocation": downloadLocation, "downloadLocation": downloadLocation,
"layoutMode": layoutMode.name, "layoutMode": layoutMode.name,
"rotatingAlbumArt": rotatingAlbumArt, "rotatingAlbumArt": rotatingAlbumArt,
"androidBytesPlay": androidBytesPlay, "predownload": predownload,
}; };
} }
} }
@ -235,5 +236,6 @@ final userPreferencesProvider = ChangeNotifierProvider(
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
ytSearchFormat: "\$MAIN_ARTIST - \$TITLE \$FEATURED_ARTISTS", ytSearchFormat: "\$MAIN_ARTIST - \$TITLE \$FEATURED_ARTISTS",
layoutMode: kIsMobile ? LayoutMode.compact : LayoutMode.adaptive, layoutMode: kIsMobile ? LayoutMode.compact : LayoutMode.adaptive,
predownload: kIsMobile,
), ),
); );

View File

@ -218,10 +218,11 @@ class _MprisMediaPlayer2 extends DBusObject {
} }
class _MprisMediaPlayer2Player extends DBusObject { class _MprisMediaPlayer2Player extends DBusObject {
Ref ref; final Ref ref;
final PlaylistQueueNotifier playlistNotifier;
/// Creates a new object to expose on [path]. /// Creates a new object to expose on [path].
_MprisMediaPlayer2Player(this.ref) _MprisMediaPlayer2Player(this.ref, this.playlistNotifier)
: super(DBusObjectPath("/org/mpris/MediaPlayer2")) { : super(DBusObjectPath("/org/mpris/MediaPlayer2")) {
(() async { (() async {
final nameStatus = final nameStatus =
@ -233,9 +234,7 @@ class _MprisMediaPlayer2Player extends DBusObject {
}()); }());
} }
PlaylistQueue? get playlist => ref.read(PlaylistQueueNotifier.provider); PlaylistQueue? get playlist => playlistNotifier.state;
PlaylistQueueNotifier get playlistNotifier =>
ref.read(PlaylistQueueNotifier.notifier);
double get volume => ref.read(VolumeProvider.provider); double get volume => ref.read(VolumeProvider.provider);
VolumeProvider get volumeNotifier => VolumeProvider get volumeNotifier =>
ref.read(VolumeProvider.provider.notifier); ref.read(VolumeProvider.provider.notifier);
@ -727,9 +726,9 @@ class LinuxAudioService {
_MprisMediaPlayer2 mp2; _MprisMediaPlayer2 mp2;
_MprisMediaPlayer2Player player; _MprisMediaPlayer2Player player;
LinuxAudioService(Ref ref) LinuxAudioService(Ref ref, PlaylistQueueNotifier playlistNotifier)
: mp2 = _MprisMediaPlayer2(), : mp2 = _MprisMediaPlayer2(),
player = _MprisMediaPlayer2Player(ref); player = _MprisMediaPlayer2Player(ref, playlistNotifier);
void dispose() { void dispose() {
mp2.dispose(); mp2.dispose();

View File

@ -3,18 +3,16 @@ import 'dart:async';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart'; import 'package:audio_session/audio_session.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
class MobileAudioService extends BaseAudioHandler { class MobileAudioService extends BaseAudioHandler {
final Ref ref;
AudioSession? session; AudioSession? session;
final PlaylistQueueNotifier playlistNotifier;
PlaylistQueue? get playlist => ref.watch(PlaylistQueueNotifier.provider);
PlaylistQueueNotifier get playlistNotifier =>
ref.watch(PlaylistQueueNotifier.notifier);
MobileAudioService(this.ref) { PlaylistQueue? get playlist => playlistNotifier.state;
MobileAudioService(this.playlistNotifier) {
AudioSession.instance.then((s) { AudioSession.instance.then((s) {
session = s; session = s;
s.interruptionEventStream.listen((event) async { s.interruptionEventStream.listen((event) async {

View File

@ -1,10 +1,12 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
abstract class PersistedStateNotifier<T> extends StateNotifier<T> { abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
final String cacheKey; final String cacheKey;
PersistedStateNotifier(super.state, this.cacheKey) : super() { PersistedStateNotifier(super.state, this.cacheKey) {
_load(); _load();
} }
@ -13,7 +15,7 @@ abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
final json = await box.get(cacheKey); final json = await box.get(cacheKey);
if (json != null) { if (json != null) {
state = fromJson(castNestedJson(json)); state = await fromJson(castNestedJson(json));
} }
} }
@ -44,7 +46,7 @@ abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
box.put(cacheKey, toJson()); box.put(cacheKey, toJson());
} }
T fromJson(Map<String, dynamic> json); FutureOr<T> fromJson(Map<String, dynamic> json);
Map<String, dynamic> toJson(); Map<String, dynamic> toJson();
@override @override