mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-10 17:07:30 +00:00
feat: implement configurable codec for download & streaming music
This commit is contained in:
parent
25a105b247
commit
181169cbab
@ -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';
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -30,6 +30,7 @@ mixin NextFetcher on StateNotifier<ProxyPlaylist> {
|
||||
final future = SpotubeTrack.fetchFromTrack(
|
||||
track,
|
||||
youtube,
|
||||
preferences.streamMusicCodec,
|
||||
);
|
||||
if (i == 0) {
|
||||
return await future;
|
||||
|
||||
@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user