feat: customizable stream/download file formats (#757)

* feat: add codec configuration in settings

* fix: show no value for codec configuration in smaller screen

* feat: implement configurable codec for download & streaming music
This commit is contained in:
Kingkor Roy Tirtho 2023-09-28 17:02:41 +06:00 committed by GitHub
parent 5a758d8671
commit e54762be6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 210 additions and 76 deletions

View File

@ -98,4 +98,6 @@ abstract class SpotubeIcons {
static const edit = FeatherIcons.edit;
static const web = FeatherIcons.globe;
static const amoled = FeatherIcons.sunset;
static const file = FeatherIcons.file;
static const stream = Icons.stream_rounded;
}

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

@ -268,5 +268,7 @@
"normalize_audio": "Normalize audio",
"change_cover": "Change cover",
"add_cover": "Add cover",
"restore_defaults": "Restore defaults"
"restore_defaults": "Restore defaults",
"download_music_codec": "Download music codec",
"streaming_music_codec": "Streaming music codec"
}

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

@ -459,6 +459,44 @@ class SettingsPage extends HookConsumerWidget {
value: preferences.normalizeAudio,
onChanged: preferences.setNormalizeAudio,
),
AdaptiveSelectTile<MusicCodec>(
secondary: const Icon(SpotubeIcons.stream),
title: Text(context.l10n.streaming_music_codec),
value: preferences.streamMusicCodec,
showValueWhenUnfolded: false,
options: MusicCodec.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(
e.label,
style: theme.textTheme.labelMedium,
),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferences.setStreamMusicCodec(value);
},
),
AdaptiveSelectTile<MusicCodec>(
secondary: const Icon(SpotubeIcons.file),
title: Text(context.l10n.download_music_codec),
value: preferences.downloadMusicCodec,
showValueWhenUnfolded: false,
options: MusicCodec.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(
e.label,
style: theme.textTheme.labelMedium,
),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferences.setDownloadMusicCodec(value);
},
),
],
),
SectionCardWithHeading(

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) {
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

@ -40,39 +40,36 @@ enum YoutubeApiType {
String get label => name[0].toUpperCase() + name.substring(1);
}
enum MusicCodec {
m4a._("M4a (Best for downloaded music)"),
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
final String label;
const MusicCodec._(this.label);
}
class UserPreferences extends PersistedChangeNotifier {
ThemeMode themeMode;
Market recommendationMarket;
bool saveTrackLyrics;
bool checkUpdate;
AudioQuality audioQuality;
late SpotubeColor accentColorScheme;
bool albumColorSync;
String downloadLocation;
LayoutMode layoutMode;
CloseBehavior closeBehavior;
bool showSystemTrayIcon;
Locale locale;
String pipedInstance;
SearchMode searchMode;
bool skipNonMusic;
YoutubeApiType youtubeApiType;
bool systemTitleBar;
bool amoledDarkTheme;
bool checkUpdate;
bool normalizeAudio;
bool saveTrackLyrics;
bool showSystemTrayIcon;
bool skipNonMusic;
bool systemTitleBar;
CloseBehavior closeBehavior;
late SpotubeColor accentColorScheme;
LayoutMode layoutMode;
Locale locale;
Market recommendationMarket;
SearchMode searchMode;
String downloadLocation;
String pipedInstance;
ThemeMode themeMode;
YoutubeApiType youtubeApiType;
MusicCodec streamMusicCodec;
MusicCodec downloadMusicCodec;
final Ref ref;
@ -96,6 +93,8 @@ class UserPreferences extends PersistedChangeNotifier {
this.systemTitleBar = false,
this.amoledDarkTheme = false,
this.normalizeAudio = true,
this.streamMusicCodec = MusicCodec.weba,
this.downloadMusicCodec = MusicCodec.m4a,
SpotubeColor? accentColorScheme,
}) : super() {
this.accentColorScheme =
@ -129,6 +128,20 @@ class UserPreferences extends PersistedChangeNotifier {
setAmoledDarkTheme(false);
setNormalizeAudio(true);
setAccentColorScheme(SpotubeColor(Colors.blue.value, name: "Blue"));
setStreamMusicCodec(MusicCodec.weba);
setDownloadMusicCodec(MusicCodec.m4a);
}
void setStreamMusicCodec(MusicCodec codec) {
streamMusicCodec = codec;
notifyListeners();
updatePersistence();
}
void setDownloadMusicCodec(MusicCodec codec) {
downloadMusicCodec = codec;
notifyListeners();
updatePersistence();
}
void setThemeMode(ThemeMode mode) {
@ -327,6 +340,16 @@ class UserPreferences extends PersistedChangeNotifier {
normalizeAudio = map["normalizeAudio"] ?? normalizeAudio;
audioPlayer.setAudioNormalization(normalizeAudio);
streamMusicCodec = MusicCodec.values.firstWhere(
(codec) => codec.name == map["streamMusicCodec"],
orElse: () => MusicCodec.weba,
);
downloadMusicCodec = MusicCodec.values.firstWhere(
(codec) => codec.name == map["downloadMusicCodec"],
orElse: () => MusicCodec.m4a,
);
}
@override
@ -352,6 +375,8 @@ class UserPreferences extends PersistedChangeNotifier {
'systemTitleBar': systemTitleBar,
"amoledDarkTheme": amoledDarkTheme,
"normalizeAudio": normalizeAudio,
"streamMusicCodec": streamMusicCodec.name,
"downloadMusicCodec": downloadMusicCodec.name,
};
}

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);

View File

@ -4,7 +4,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"bn": [
@ -12,7 +14,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"ca": [
@ -20,7 +24,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"de": [
@ -28,7 +34,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"es": [
@ -36,7 +44,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"fr": [
@ -44,7 +54,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"hi": [
@ -52,7 +64,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"ja": [
@ -60,7 +74,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"pl": [
@ -68,7 +84,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"pt": [
@ -76,7 +94,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"ru": [
@ -84,7 +104,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"uk": [
@ -92,7 +114,9 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
],
"zh": [
@ -100,6 +124,8 @@
"normalize_audio",
"change_cover",
"add_cover",
"restore_defaults"
"restore_defaults",
"download_music_codec",
"streaming_music_codec"
]
}