mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat(android-playback): option to download track bytes and play instead of Streaming
This commit is contained in:
parent
3b4306b7ab
commit
dcc8ba5a54
@ -14,6 +14,7 @@ import 'package:spotube/models/SpotubeTrack.dart';
|
|||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.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/utils/platform.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class Settings extends HookConsumerWidget {
|
class Settings extends HookConsumerWidget {
|
||||||
@ -272,6 +273,23 @@ class Settings extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (kIsMobile)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.download_for_offline_rounded),
|
||||||
|
title: const Text(
|
||||||
|
"Pre download and play",
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
"Instead of streaming audio, download bytes and play instead (Recommended for higher bandwidth users)",
|
||||||
|
),
|
||||||
|
trailing: Switch.adaptive(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
value: preferences.androidBytesPlay,
|
||||||
|
onChanged: (state) {
|
||||||
|
preferences.setAndroidBytesPlay(state);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.fast_forward_rounded),
|
leading: const Icon(Icons.fast_forward_rounded),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
|
@ -52,10 +52,11 @@ class Downloader with ChangeNotifier {
|
|||||||
// Using android Audio Focus to keep the app run in background
|
// Using android Audio Focus to keep the app run in background
|
||||||
_playback.mobileAudioService?.session?.setActive(true);
|
_playback.mobileAudioService?.session?.setActive(true);
|
||||||
grabberQueue.add(() async {
|
grabberQueue.add(() async {
|
||||||
final track = await ref.read(playbackProvider).toSpotubeTrack(
|
final track = (await ref.read(playbackProvider).toSpotubeTrack(
|
||||||
baseTrack,
|
baseTrack,
|
||||||
noSponsorBlock: true,
|
noSponsorBlock: true,
|
||||||
);
|
))
|
||||||
|
.item1;
|
||||||
_queue.add(() async {
|
_queue.add(() async {
|
||||||
final cleanTitle = track.ytTrack.title.replaceAll(
|
final cleanTitle = track.ytTrack.title.replaceAll(
|
||||||
RegExp(r'[/\\?%*:|"<>]'),
|
RegExp(r'[/\\?%*:|"<>]'),
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
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:flutter/services.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';
|
||||||
@ -21,6 +22,7 @@ import 'package:spotube/utils/platform.dart';
|
|||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
import 'package:tuple/tuple.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';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:spotube/extensions/list-sort-multiple.dart';
|
import 'package:spotube/extensions/list-sort-multiple.dart';
|
||||||
@ -154,7 +156,7 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
playlist!.tracks[currentTrackIndex + 1],
|
playlist!.tracks[currentTrackIndex + 1],
|
||||||
).then((v) {
|
).then((v) {
|
||||||
_isPreSearching = false;
|
_isPreSearching = false;
|
||||||
return v;
|
return v.item1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (track != null && preferences.skipSponsorSegments) {
|
if (track != null && preferences.skipSponsorSegments) {
|
||||||
@ -211,10 +213,12 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
status = PlaybackStatus.loading;
|
status = PlaybackStatus.loading;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
AudioOnlyStreamInfo? manifest;
|
||||||
// the track is not a SpotubeTrack so turning it to one
|
// the track is not a SpotubeTrack so turning it to one
|
||||||
if (track is! SpotubeTrack) {
|
if (track is! SpotubeTrack) {
|
||||||
track = await toSpotubeTrack(track);
|
final s = await toSpotubeTrack(track);
|
||||||
|
track = s.item1;
|
||||||
|
manifest = s.item2;
|
||||||
}
|
}
|
||||||
|
|
||||||
final tag = MediaItem(
|
final tag = MediaItem(
|
||||||
@ -238,7 +242,7 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
updatePersistence();
|
updatePersistence();
|
||||||
await player.play(
|
await player.play(
|
||||||
track.ytUri.startsWith("http")
|
track.ytUri.startsWith("http")
|
||||||
? UrlSource(track.ytUri)
|
? await getAppropriateSource(track, manifest)
|
||||||
: DeviceFileSource(track.ytUri),
|
: DeviceFileSource(track.ytUri),
|
||||||
);
|
);
|
||||||
status = PlaybackStatus.playing;
|
status = PlaybackStatus.playing;
|
||||||
@ -372,7 +376,7 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// playlist & track list methods
|
// playlist & track list methods
|
||||||
Future<SpotubeTrack> toSpotubeTrack(
|
Future<Tuple2<SpotubeTrack, AudioOnlyStreamInfo>> toSpotubeTrack(
|
||||||
Track track, {
|
Track track, {
|
||||||
bool noSponsorBlock = false,
|
bool noSponsorBlock = false,
|
||||||
}) async {
|
}) async {
|
||||||
@ -389,7 +393,7 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
_logger.v("[Track Search Artists] $artistsName");
|
_logger.v("[Track Search Artists] $artistsName");
|
||||||
final mainArtist = artistsName.first;
|
final mainArtist = artistsName.first;
|
||||||
final featuredArtists = artistsName.length > 1
|
final featuredArtists = artistsName.length > 1
|
||||||
? "feat. " + artistsName.sublist(1).join(" ")
|
? "feat. ${artistsName.sublist(1).join(" ")}"
|
||||||
: "";
|
: "";
|
||||||
final title = ServiceUtils.getTitle(
|
final title = ServiceUtils.getTitle(
|
||||||
track.name!,
|
track.name!,
|
||||||
@ -487,11 +491,11 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final ytUri = (audioQuality == AudioQuality.high
|
final chosenStreamInfo = audioQuality == AudioQuality.high
|
||||||
? audioManifest.withHighestBitrate()
|
? audioManifest.withHighestBitrate()
|
||||||
: audioManifest.sortByBitrate().last)
|
: audioManifest.sortByBitrate().last;
|
||||||
.url
|
|
||||||
.toString();
|
final ytUri = chosenStreamInfo.url.toString();
|
||||||
|
|
||||||
final skipSegments = cachedTrack?.skipSegments != null &&
|
final skipSegments = cachedTrack?.skipSegments != null &&
|
||||||
cachedTrack!.skipSegments!.isNotEmpty
|
cachedTrack!.skipSegments!.isNotEmpty
|
||||||
@ -517,7 +521,8 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SpotubeTrack.fromTrack(
|
return Tuple2(
|
||||||
|
SpotubeTrack.fromTrack(
|
||||||
track: track,
|
track: track,
|
||||||
ytTrack: ytVideo,
|
ytTrack: ytVideo,
|
||||||
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
|
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
|
||||||
@ -525,6 +530,8 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
// codec/mimetype for those Platforms
|
// codec/mimetype for those Platforms
|
||||||
ytUri: ytUri,
|
ytUri: ytUri,
|
||||||
skipSegments: skipSegments,
|
skipSegments: skipSegments,
|
||||||
|
),
|
||||||
|
chosenStreamInfo,
|
||||||
);
|
);
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
_logger.e("topSpotubeTrack", e, stack);
|
_logger.e("topSpotubeTrack", e, stack);
|
||||||
@ -532,6 +539,52 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Source> getAppropriateSource(
|
||||||
|
SpotubeTrack track, [
|
||||||
|
AudioOnlyStreamInfo? manifest,
|
||||||
|
]) async {
|
||||||
|
if (!kIsMobile || !preferences.androidBytesPlay) {
|
||||||
|
return UrlSource(track.ytUri);
|
||||||
|
}
|
||||||
|
final List<int> bytesStore = [];
|
||||||
|
final bytesFuture = Completer<Uint8List>();
|
||||||
|
|
||||||
|
if (manifest == null) {
|
||||||
|
StreamManifest trackManifest = await raceMultiple(
|
||||||
|
() => youtube.videos.streams.getManifest(track.ytTrack.id),
|
||||||
|
);
|
||||||
|
final audioManifest = trackManifest.audioOnly.where((info) {
|
||||||
|
final isMp4a = info.codec.mimeType == "audio/mp4";
|
||||||
|
if (kIsLinux) {
|
||||||
|
return !isMp4a;
|
||||||
|
} else if (kIsMacOS || kIsIOS) {
|
||||||
|
return isMp4a;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
manifest ??= audioManifest.sortByBitrate().last;
|
||||||
|
}
|
||||||
|
|
||||||
|
youtube.videos.streamsClient.get(manifest).listen(
|
||||||
|
(data) {
|
||||||
|
bytesStore.addAll(data);
|
||||||
|
},
|
||||||
|
onDone: () {
|
||||||
|
bytesFuture.complete(Uint8List.fromList(bytesStore));
|
||||||
|
},
|
||||||
|
onError: (e) {
|
||||||
|
_logger.e("toByteTrack", e);
|
||||||
|
bytesFuture.completeError(e);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final bytes = await bytesFuture.future;
|
||||||
|
|
||||||
|
return bytes.isNotEmpty ? BytesSource(bytes) : UrlSource(track.ytUri);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setPlaylistPosition(int position) async {
|
Future<void> setPlaylistPosition(int position) async {
|
||||||
if (playlist == null) return;
|
if (playlist == null) return;
|
||||||
await playPlaylist(playlist!, position);
|
await playPlaylist(playlist!, position);
|
||||||
|
@ -38,12 +38,15 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
LayoutMode layoutMode;
|
LayoutMode layoutMode;
|
||||||
bool rotatingAlbumArt;
|
bool rotatingAlbumArt;
|
||||||
|
|
||||||
|
bool androidBytesPlay;
|
||||||
|
|
||||||
UserPreferences({
|
UserPreferences({
|
||||||
required this.geniusAccessToken,
|
required this.geniusAccessToken,
|
||||||
required this.recommendationMarket,
|
required this.recommendationMarket,
|
||||||
required this.themeMode,
|
required this.themeMode,
|
||||||
required this.ytSearchFormat,
|
required this.ytSearchFormat,
|
||||||
required this.layoutMode,
|
required this.layoutMode,
|
||||||
|
this.androidBytesPlay = true,
|
||||||
this.saveTrackLyrics = false,
|
this.saveTrackLyrics = false,
|
||||||
this.accentColorScheme = Colors.green,
|
this.accentColorScheme = Colors.green,
|
||||||
this.backgroundColorScheme = Colors.grey,
|
this.backgroundColorScheme = Colors.grey,
|
||||||
@ -63,6 +66,11 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAndroidBytesPlay(bool value) {
|
||||||
|
androidBytesPlay = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void setThemeMode(ThemeMode mode) {
|
void setThemeMode(ThemeMode mode) {
|
||||||
themeMode = mode;
|
themeMode = mode;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@ -191,6 +199,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -210,6 +219,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
"downloadLocation": downloadLocation,
|
"downloadLocation": downloadLocation,
|
||||||
"layoutMode": layoutMode.name,
|
"layoutMode": layoutMode.name,
|
||||||
"rotatingAlbumArt": rotatingAlbumArt,
|
"rotatingAlbumArt": rotatingAlbumArt,
|
||||||
|
"androidBytesPlay": androidBytesPlay,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1229,6 +1229,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
tuple:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: tuple
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -60,6 +60,7 @@ dependencies:
|
|||||||
fl_query: ^0.3.0
|
fl_query: ^0.3.0
|
||||||
fl_query_hooks: ^0.3.0
|
fl_query_hooks: ^0.3.0
|
||||||
flutter_inappwebview: ^5.4.3+7
|
flutter_inappwebview: ^5.4.3+7
|
||||||
|
tuple: ^2.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user