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/Playback.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
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(
|
||||
leading: const Icon(Icons.fast_forward_rounded),
|
||||
title: const Text(
|
||||
|
@ -52,10 +52,11 @@ class Downloader with ChangeNotifier {
|
||||
// Using android Audio Focus to keep the app run in background
|
||||
_playback.mobileAudioService?.session?.setActive(true);
|
||||
grabberQueue.add(() async {
|
||||
final track = await ref.read(playbackProvider).toSpotubeTrack(
|
||||
baseTrack,
|
||||
noSponsorBlock: true,
|
||||
);
|
||||
final track = (await ref.read(playbackProvider).toSpotubeTrack(
|
||||
baseTrack,
|
||||
noSponsorBlock: true,
|
||||
))
|
||||
.item1;
|
||||
_queue.add(() async {
|
||||
final cleanTitle = track.ytTrack.title.replaceAll(
|
||||
RegExp(r'[/\\?%*:|"<>]'),
|
||||
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive/hive.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/service_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:collection/collection.dart';
|
||||
import 'package:spotube/extensions/list-sort-multiple.dart';
|
||||
@ -154,7 +156,7 @@ class Playback extends PersistedChangeNotifier {
|
||||
playlist!.tracks[currentTrackIndex + 1],
|
||||
).then((v) {
|
||||
_isPreSearching = false;
|
||||
return v;
|
||||
return v.item1;
|
||||
});
|
||||
}
|
||||
if (track != null && preferences.skipSponsorSegments) {
|
||||
@ -211,10 +213,12 @@ class Playback extends PersistedChangeNotifier {
|
||||
status = PlaybackStatus.loading;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
AudioOnlyStreamInfo? manifest;
|
||||
// the track is not a SpotubeTrack so turning it to one
|
||||
if (track is! SpotubeTrack) {
|
||||
track = await toSpotubeTrack(track);
|
||||
final s = await toSpotubeTrack(track);
|
||||
track = s.item1;
|
||||
manifest = s.item2;
|
||||
}
|
||||
|
||||
final tag = MediaItem(
|
||||
@ -238,7 +242,7 @@ class Playback extends PersistedChangeNotifier {
|
||||
updatePersistence();
|
||||
await player.play(
|
||||
track.ytUri.startsWith("http")
|
||||
? UrlSource(track.ytUri)
|
||||
? await getAppropriateSource(track, manifest)
|
||||
: DeviceFileSource(track.ytUri),
|
||||
);
|
||||
status = PlaybackStatus.playing;
|
||||
@ -372,7 +376,7 @@ class Playback extends PersistedChangeNotifier {
|
||||
}
|
||||
|
||||
// playlist & track list methods
|
||||
Future<SpotubeTrack> toSpotubeTrack(
|
||||
Future<Tuple2<SpotubeTrack, AudioOnlyStreamInfo>> toSpotubeTrack(
|
||||
Track track, {
|
||||
bool noSponsorBlock = false,
|
||||
}) async {
|
||||
@ -389,7 +393,7 @@ class Playback extends PersistedChangeNotifier {
|
||||
_logger.v("[Track Search Artists] $artistsName");
|
||||
final mainArtist = artistsName.first;
|
||||
final featuredArtists = artistsName.length > 1
|
||||
? "feat. " + artistsName.sublist(1).join(" ")
|
||||
? "feat. ${artistsName.sublist(1).join(" ")}"
|
||||
: "";
|
||||
final title = ServiceUtils.getTitle(
|
||||
track.name!,
|
||||
@ -487,11 +491,11 @@ class Playback extends PersistedChangeNotifier {
|
||||
}
|
||||
});
|
||||
|
||||
final ytUri = (audioQuality == AudioQuality.high
|
||||
? audioManifest.withHighestBitrate()
|
||||
: audioManifest.sortByBitrate().last)
|
||||
.url
|
||||
.toString();
|
||||
final chosenStreamInfo = audioQuality == AudioQuality.high
|
||||
? audioManifest.withHighestBitrate()
|
||||
: audioManifest.sortByBitrate().last;
|
||||
|
||||
final ytUri = chosenStreamInfo.url.toString();
|
||||
|
||||
final skipSegments = cachedTrack?.skipSegments != null &&
|
||||
cachedTrack!.skipSegments!.isNotEmpty
|
||||
@ -517,14 +521,17 @@ class Playback extends PersistedChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
skipSegments: skipSegments,
|
||||
return Tuple2(
|
||||
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,
|
||||
skipSegments: skipSegments,
|
||||
),
|
||||
chosenStreamInfo,
|
||||
);
|
||||
} catch (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 {
|
||||
if (playlist == null) return;
|
||||
await playPlaylist(playlist!, position);
|
||||
|
@ -38,12 +38,15 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
LayoutMode layoutMode;
|
||||
bool rotatingAlbumArt;
|
||||
|
||||
bool androidBytesPlay;
|
||||
|
||||
UserPreferences({
|
||||
required this.geniusAccessToken,
|
||||
required this.recommendationMarket,
|
||||
required this.themeMode,
|
||||
required this.ytSearchFormat,
|
||||
required this.layoutMode,
|
||||
this.androidBytesPlay = true,
|
||||
this.saveTrackLyrics = false,
|
||||
this.accentColorScheme = Colors.green,
|
||||
this.backgroundColorScheme = Colors.grey,
|
||||
@ -63,6 +66,11 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void setAndroidBytesPlay(bool value) {
|
||||
androidBytesPlay = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setThemeMode(ThemeMode mode) {
|
||||
themeMode = mode;
|
||||
notifyListeners();
|
||||
@ -191,6 +199,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact,
|
||||
);
|
||||
rotatingAlbumArt = map["rotatingAlbumArt"] ?? rotatingAlbumArt;
|
||||
androidBytesPlay = map["androidBytesPlay"] ?? androidBytesPlay;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -210,6 +219,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
"downloadLocation": downloadLocation,
|
||||
"layoutMode": layoutMode.name,
|
||||
"rotatingAlbumArt": rotatingAlbumArt,
|
||||
"androidBytesPlay": androidBytesPlay,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1229,6 +1229,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
tuple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tuple
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -60,6 +60,7 @@ dependencies:
|
||||
fl_query: ^0.3.0
|
||||
fl_query_hooks: ^0.3.0
|
||||
flutter_inappwebview: ^5.4.3+7
|
||||
tuple: ^2.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
Reference in New Issue
Block a user