mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
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:
parent
3ccb525260
commit
1d82bb0987
@ -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);
|
||||
}
|
||||
},
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
@ -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>({
|
||||
|
@ -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;
|
||||
|
@ -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,22 +307,21 @@ class SettingsPage extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (kIsMobile)
|
||||
PlatformListTile(
|
||||
leading: const Icon(SpotubeIcons.download),
|
||||
title: const PlatformText(
|
||||
"Pre download and play",
|
||||
),
|
||||
subtitle: const PlatformText(
|
||||
"Instead of streaming audio, download bytes and play instead (Recommended for higher bandwidth users)",
|
||||
),
|
||||
trailing: PlatformSwitch(
|
||||
value: preferences.androidBytesPlay,
|
||||
onChanged: (state) {
|
||||
preferences.setAndroidBytesPlay(state);
|
||||
},
|
||||
),
|
||||
PlatformListTile(
|
||||
leading: const Icon(SpotubeIcons.download),
|
||||
title: const PlatformText(
|
||||
"Pre download and play",
|
||||
),
|
||||
subtitle: const PlatformText(
|
||||
"Instead of streaming audio, download bytes and play instead (Recommended for higher bandwidth users)",
|
||||
),
|
||||
trailing: PlatformSwitch(
|
||||
value: preferences.predownload,
|
||||
onChanged: (state) {
|
||||
preferences.setPredownload(state);
|
||||
},
|
||||
),
|
||||
),
|
||||
PlatformListTile(
|
||||
leading: const Icon(SpotubeIcons.fastForward),
|
||||
title: const PlatformText(
|
||||
|
@ -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);
|
||||
} else {
|
||||
return Track.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(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
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
||||
PlaylistQueue? get playlist => playlistNotifier.state;
|
||||
|
||||
MobileAudioService(this.ref) {
|
||||
MobileAudioService(this.playlistNotifier) {
|
||||
AudioSession.instance.then((s) {
|
||||
session = s;
|
||||
s.interruptionEventStream.listen((event) async {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user