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),
enabled: playlist?.isLoading != true,
selected: playlist?.isLoading != true &&
video.id ==
(playlist?.activeTrack as SpotubeTrack).ytTrack.id,
video.id.value ==
(playlist?.activeTrack as SpotubeTrack)
.ytTrack
.id
.value,
selectedTileColor: Theme.of(context).popupMenuTheme.color,
onTap: () async {
if (playlist?.isLoading == false &&
video.id !=
video.id.value !=
(playlist?.activeTrack as SpotubeTrack)
.ytTrack
.id) {
.id
.value) {
await playlistNotifier.swapSibling(video);
}
},

View File

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

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

View File

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

View File

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

View File

@ -3,18 +3,16 @@ import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
class MobileAudioService extends BaseAudioHandler {
final Ref ref;
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) {
session = s;
s.interruptionEventStream.listen((event) async {

View File

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