feat: implement configurable codec for download & streaming music

This commit is contained in:
Kingkor Roy Tirtho 2023-09-28 16:17:40 +06:00
parent 25a105b247
commit 181169cbab
8 changed files with 77 additions and 36 deletions

View File

@ -13,7 +13,6 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';

View File

@ -23,6 +23,7 @@ class TrackNotFoundException implements Exception {
class SpotubeTrack extends Track {
final YoutubeVideoInfo ytTrack;
final String ytUri;
final MusicCodec codec;
final List<YoutubeVideoInfo> siblings;
@ -30,6 +31,7 @@ class SpotubeTrack extends Track {
this.ytTrack,
this.ytUri,
this.siblings,
this.codec,
) : super();
SpotubeTrack.fromTrack({
@ -37,6 +39,7 @@ class SpotubeTrack extends Track {
required this.ytTrack,
required this.ytUri,
required this.siblings,
required this.codec,
}) : super() {
album = track.album;
artists = track.artists;
@ -149,6 +152,7 @@ class SpotubeTrack extends Track {
static Future<SpotubeTrack> fetchFromTrack(
Track track,
YoutubeEndpoints client,
MusicCodec codec,
) async {
final matchedCachedTrack = await MatchedTrack.box.get(track.id!);
var siblings = <YoutubeVideoInfo>[];
@ -157,16 +161,17 @@ class SpotubeTrack extends Track {
if (matchedCachedTrack != null &&
matchedCachedTrack.searchMode == client.preferences.searchMode) {
(ytVideo, ytStreamUrl) = await client.video(
matchedCachedTrack.youtubeId,
matchedCachedTrack.searchMode,
);
matchedCachedTrack.youtubeId, matchedCachedTrack.searchMode, codec);
} else {
siblings = await fetchSiblings(track, client);
if (siblings.isEmpty) {
throw TrackNotFoundException(track);
}
(ytVideo, ytStreamUrl) =
await client.video(siblings.first.id, siblings.first.searchMode);
(ytVideo, ytStreamUrl) = await client.video(
siblings.first.id,
siblings.first.searchMode,
codec,
);
await MatchedTrack.box.put(
track.id!,
@ -183,6 +188,7 @@ class SpotubeTrack extends Track {
ytTrack: ytVideo,
ytUri: ytStreamUrl,
siblings: siblings,
codec: codec,
);
}
@ -193,8 +199,12 @@ class SpotubeTrack extends Track {
// sibling tracks that were manually searched and swapped
final isStepSibling = siblings.none((element) => element.id == video.id);
final (ytVideo, ytStreamUrl) =
await client.video(video.id, siblings.first.searchMode);
final (ytVideo, ytStreamUrl) = await client.video(
video.id,
siblings.first.searchMode,
// siblings are always swapped when streaming
client.preferences.streamMusicCodec,
);
if (!isStepSibling) {
await MatchedTrack.box.put(
@ -215,6 +225,7 @@ class SpotubeTrack extends Track {
video,
...siblings.where((element) => element.id != video.id),
],
codec: client.preferences.streamMusicCodec,
);
}
@ -226,6 +237,10 @@ class SpotubeTrack extends Track {
siblings: List.castFrom<dynamic, Map<String, dynamic>>(map["siblings"])
.map((sibling) => YoutubeVideoInfo.fromJson(sibling))
.toList(),
codec: MusicCodec.values.firstWhere(
(element) => element.name == map["codec"],
orElse: () => MusicCodec.m4a,
),
);
}
@ -242,6 +257,7 @@ class SpotubeTrack extends Track {
ytTrack: ytTrack,
ytUri: ytUri,
siblings: siblings,
codec: codec,
);
}
@ -268,6 +284,7 @@ class SpotubeTrack extends Track {
"ytTrack": ytTrack.toJson(),
"ytUri": ytUri,
"siblings": siblings.map((sibling) => sibling.toJson()).toList(),
"codec": codec.name,
};
}
}

View File

@ -40,7 +40,11 @@ class DownloadManagerProvider extends ChangeNotifier {
await oldFile.exists()) {
await oldFile.rename(savePath);
}
if (status != DownloadStatus.completed) return;
if (status != DownloadStatus.completed ||
//? WebA audiotagging is not supported yet
//? Although in future by converting weba to opus & then tagging it
//? is possible using vorbis comments
downloadCodec == MusicCodec.weba) return;
final file = File(request.path);
@ -89,6 +93,8 @@ class DownloadManagerProvider extends ChangeNotifier {
YoutubeEndpoints get yt => ref.read(youtubeProvider);
String get downloadDirectory =>
ref.read(userPreferencesProvider.select((s) => s.downloadLocation));
MusicCodec get downloadCodec =>
ref.read(userPreferencesProvider.select((s) => s.downloadMusicCodec));
int get $downloadCount => dl
.getAllDownloads()
@ -130,7 +136,7 @@ class DownloadManagerProvider extends ChangeNotifier {
String getTrackFileUrl(Track track) {
final name =
"${track.name} - ${TypeConversionUtils.artists_X_String(track.artists ?? <Artist>[])}.m4a";
"${track.name} - ${TypeConversionUtils.artists_X_String(track.artists ?? <Artist>[])}.${downloadCodec.name}";
return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name));
}
@ -166,7 +172,7 @@ class DownloadManagerProvider extends ChangeNotifier {
await oldFile.rename("$savePath.old");
}
if (track is SpotubeTrack) {
if (track is SpotubeTrack && track.codec == downloadCodec) {
final downloadTask = await dl.addDownload(track.ytUri, savePath);
if (downloadTask != null) {
$history.add(track);
@ -174,7 +180,7 @@ class DownloadManagerProvider extends ChangeNotifier {
} else {
$backHistory.add(track);
final spotubeTrack =
await SpotubeTrack.fetchFromTrack(track, yt).then((d) {
await SpotubeTrack.fetchFromTrack(track, yt, downloadCodec).then((d) {
$backHistory.remove(track);
return d;
});

View File

@ -30,6 +30,7 @@ mixin NextFetcher on StateNotifier<ProxyPlaylist> {
final future = SpotubeTrack.fetchFromTrack(
track,
youtube,
preferences.streamMusicCodec,
);
if (i == 0) {
return await future;

View File

@ -185,10 +185,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
),
);
} catch (e) {
currentSegments.value = (
source: audioPlayer.currentSource!,
segments: [],
);
if (audioPlayer.currentSource != null) {
currentSegments.value = (
source: audioPlayer.currentSource!,
segments: [],
);
}
} finally {
isFetchingSegments.value = false;
}
@ -223,7 +225,11 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
final nthFetchedTrack = switch (track.runtimeType) {
SpotubeTrack => track as SpotubeTrack,
_ => await SpotubeTrack.fetchFromTrack(track, youtube),
_ => await SpotubeTrack.fetchFromTrack(
track,
youtube,
preferences.streamMusicCodec,
),
};
await audioPlayer.replaceSource(
@ -309,10 +315,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
final addableTrack = await SpotubeTrack.fetchFromTrack(
tracks.elementAtOrNull(initialIndex) ?? tracks.first,
youtube,
preferences.streamMusicCodec,
).catchError((e, stackTrace) {
return SpotubeTrack.fetchFromTrack(
tracks.elementAtOrNull(initialIndex + 1) ?? tracks.first,
youtube,
preferences.streamMusicCodec,
);
});

View File

@ -41,8 +41,8 @@ enum YoutubeApiType {
}
enum MusicCodec {
m4a._("M4a\n(best for downloaded music)"),
weba._("WebA\n(best for streamed music)");
m4a._("M4a (Best for downloaded music)"),
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
final String label;
const MusicCodec._(this.label);

View File

@ -181,24 +181,33 @@ class YoutubeEndpoints {
}
}
String _pipedStreamResponseToStreamUrl(PipedStreamResponse stream) {
String _pipedStreamResponseToStreamUrl(
PipedStreamResponse stream,
MusicCodec codec,
) {
final pipedStreamFormat = switch (codec) {
MusicCodec.m4a => PipedAudioStreamFormat.m4a,
MusicCodec.weba => PipedAudioStreamFormat.webm,
};
return switch (preferences.audioQuality) {
AudioQuality.high => stream
.highestBitrateAudioStreamOfFormat(PipedAudioStreamFormat.m4a)!
.url,
AudioQuality.low => stream
.lowestBitrateAudioStreamOfFormat(PipedAudioStreamFormat.m4a)!
.url,
AudioQuality.high =>
stream.highestBitrateAudioStreamOfFormat(pipedStreamFormat)!.url,
AudioQuality.low =>
stream.lowestBitrateAudioStreamOfFormat(pipedStreamFormat)!.url,
};
}
Future<String> streamingUrl(String id) async {
Future<String> streamingUrl(String id, MusicCodec codec) async {
if (youtube != null) {
final res = await PrimitiveUtils.raceMultiple(
() => youtube!.videos.streams.getManifest(id),
);
final audioOnlyManifests = res.audioOnly.where((info) {
return info.codec.mimeType == "audio/mp4";
return switch (codec) {
MusicCodec.m4a => info.codec.mimeType == "audio/mp4",
MusicCodec.weba => info.codec.mimeType == "audio/webm",
};
});
return switch (preferences.audioQuality) {
@ -208,26 +217,27 @@ class YoutubeEndpoints {
audioOnlyManifests.sortByBitrate().last.url.toString(),
};
} else {
return _pipedStreamResponseToStreamUrl(await piped!.streams(id));
return _pipedStreamResponseToStreamUrl(await piped!.streams(id), codec);
}
}
Future<(YoutubeVideoInfo info, String streamingUrl)> video(
String id,
SearchMode searchMode,
MusicCodec codec,
) async {
if (youtube != null) {
final res = await youtube!.videos.get(id);
return (
YoutubeVideoInfo.fromVideo(res),
await streamingUrl(id),
await streamingUrl(id, codec),
);
} else {
try {
final res = await piped!.streams(id);
return (
YoutubeVideoInfo.fromStreamResponse(res, searchMode),
_pipedStreamResponseToStreamUrl(res),
_pipedStreamResponseToStreamUrl(res, codec),
);
} on Exception catch (e) {
await showPipedErrorDialog(e);

View File

@ -126,21 +126,21 @@ abstract class TypeConversionUtils {
}) {
final track = Track();
track.album = Album()
..name = metadata?.album ?? "Spotube"
..name = metadata?.album ?? "Unknown"
..images = [if (art != null) Image()..url = art]
..genres = [if (metadata?.genre != null) metadata!.genre!]
..artists = [
Artist()
..name = metadata?.albumArtist ?? "Spotube"
..id = metadata?.albumArtist ?? "Spotube"
..name = metadata?.albumArtist ?? "Unknown"
..id = metadata?.albumArtist ?? "Unknown"
..type = "artist",
]
..id = metadata?.album
..releaseDate = metadata?.year?.toString();
track.artists = [
Artist()
..name = metadata?.artist ?? "Spotube"
..id = metadata?.artist ?? "Spotube"
..name = metadata?.artist ?? "Unknown"
..id = metadata?.artist ?? "Unknown"
];
track.id = metadata?.title ?? basenameWithoutExtension(file.path);