mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
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:
parent
5a758d8671
commit
e54762be6a
@ -98,4 +98,6 @@ abstract class SpotubeIcons {
|
|||||||
static const edit = FeatherIcons.edit;
|
static const edit = FeatherIcons.edit;
|
||||||
static const web = FeatherIcons.globe;
|
static const web = FeatherIcons.globe;
|
||||||
static const amoled = FeatherIcons.sunset;
|
static const amoled = FeatherIcons.sunset;
|
||||||
|
static const file = FeatherIcons.file;
|
||||||
|
static const stream = Icons.stream_rounded;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/models/logger.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/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
@ -268,5 +268,7 @@
|
|||||||
"normalize_audio": "Normalize audio",
|
"normalize_audio": "Normalize audio",
|
||||||
"change_cover": "Change cover",
|
"change_cover": "Change cover",
|
||||||
"add_cover": "Add 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"
|
||||||
}
|
}
|
@ -23,6 +23,7 @@ class TrackNotFoundException implements Exception {
|
|||||||
class SpotubeTrack extends Track {
|
class SpotubeTrack extends Track {
|
||||||
final YoutubeVideoInfo ytTrack;
|
final YoutubeVideoInfo ytTrack;
|
||||||
final String ytUri;
|
final String ytUri;
|
||||||
|
final MusicCodec codec;
|
||||||
|
|
||||||
final List<YoutubeVideoInfo> siblings;
|
final List<YoutubeVideoInfo> siblings;
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ class SpotubeTrack extends Track {
|
|||||||
this.ytTrack,
|
this.ytTrack,
|
||||||
this.ytUri,
|
this.ytUri,
|
||||||
this.siblings,
|
this.siblings,
|
||||||
|
this.codec,
|
||||||
) : super();
|
) : super();
|
||||||
|
|
||||||
SpotubeTrack.fromTrack({
|
SpotubeTrack.fromTrack({
|
||||||
@ -37,6 +39,7 @@ class SpotubeTrack extends Track {
|
|||||||
required this.ytTrack,
|
required this.ytTrack,
|
||||||
required this.ytUri,
|
required this.ytUri,
|
||||||
required this.siblings,
|
required this.siblings,
|
||||||
|
required this.codec,
|
||||||
}) : super() {
|
}) : super() {
|
||||||
album = track.album;
|
album = track.album;
|
||||||
artists = track.artists;
|
artists = track.artists;
|
||||||
@ -149,6 +152,7 @@ class SpotubeTrack extends Track {
|
|||||||
static Future<SpotubeTrack> fetchFromTrack(
|
static Future<SpotubeTrack> fetchFromTrack(
|
||||||
Track track,
|
Track track,
|
||||||
YoutubeEndpoints client,
|
YoutubeEndpoints client,
|
||||||
|
MusicCodec codec,
|
||||||
) async {
|
) async {
|
||||||
final matchedCachedTrack = await MatchedTrack.box.get(track.id!);
|
final matchedCachedTrack = await MatchedTrack.box.get(track.id!);
|
||||||
var siblings = <YoutubeVideoInfo>[];
|
var siblings = <YoutubeVideoInfo>[];
|
||||||
@ -157,16 +161,17 @@ class SpotubeTrack extends Track {
|
|||||||
if (matchedCachedTrack != null &&
|
if (matchedCachedTrack != null &&
|
||||||
matchedCachedTrack.searchMode == client.preferences.searchMode) {
|
matchedCachedTrack.searchMode == client.preferences.searchMode) {
|
||||||
(ytVideo, ytStreamUrl) = await client.video(
|
(ytVideo, ytStreamUrl) = await client.video(
|
||||||
matchedCachedTrack.youtubeId,
|
matchedCachedTrack.youtubeId, matchedCachedTrack.searchMode, codec);
|
||||||
matchedCachedTrack.searchMode,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
siblings = await fetchSiblings(track, client);
|
siblings = await fetchSiblings(track, client);
|
||||||
if (siblings.isEmpty) {
|
if (siblings.isEmpty) {
|
||||||
throw TrackNotFoundException(track);
|
throw TrackNotFoundException(track);
|
||||||
}
|
}
|
||||||
(ytVideo, ytStreamUrl) =
|
(ytVideo, ytStreamUrl) = await client.video(
|
||||||
await client.video(siblings.first.id, siblings.first.searchMode);
|
siblings.first.id,
|
||||||
|
siblings.first.searchMode,
|
||||||
|
codec,
|
||||||
|
);
|
||||||
|
|
||||||
await MatchedTrack.box.put(
|
await MatchedTrack.box.put(
|
||||||
track.id!,
|
track.id!,
|
||||||
@ -183,6 +188,7 @@ class SpotubeTrack extends Track {
|
|||||||
ytTrack: ytVideo,
|
ytTrack: ytVideo,
|
||||||
ytUri: ytStreamUrl,
|
ytUri: ytStreamUrl,
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
|
codec: codec,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,8 +199,12 @@ class SpotubeTrack extends Track {
|
|||||||
// sibling tracks that were manually searched and swapped
|
// sibling tracks that were manually searched and swapped
|
||||||
final isStepSibling = siblings.none((element) => element.id == video.id);
|
final isStepSibling = siblings.none((element) => element.id == video.id);
|
||||||
|
|
||||||
final (ytVideo, ytStreamUrl) =
|
final (ytVideo, ytStreamUrl) = await client.video(
|
||||||
await client.video(video.id, siblings.first.searchMode);
|
video.id,
|
||||||
|
siblings.first.searchMode,
|
||||||
|
// siblings are always swapped when streaming
|
||||||
|
client.preferences.streamMusicCodec,
|
||||||
|
);
|
||||||
|
|
||||||
if (!isStepSibling) {
|
if (!isStepSibling) {
|
||||||
await MatchedTrack.box.put(
|
await MatchedTrack.box.put(
|
||||||
@ -215,6 +225,7 @@ class SpotubeTrack extends Track {
|
|||||||
video,
|
video,
|
||||||
...siblings.where((element) => element.id != video.id),
|
...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"])
|
siblings: List.castFrom<dynamic, Map<String, dynamic>>(map["siblings"])
|
||||||
.map((sibling) => YoutubeVideoInfo.fromJson(sibling))
|
.map((sibling) => YoutubeVideoInfo.fromJson(sibling))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
codec: MusicCodec.values.firstWhere(
|
||||||
|
(element) => element.name == map["codec"],
|
||||||
|
orElse: () => MusicCodec.m4a,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,6 +257,7 @@ class SpotubeTrack extends Track {
|
|||||||
ytTrack: ytTrack,
|
ytTrack: ytTrack,
|
||||||
ytUri: ytUri,
|
ytUri: ytUri,
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
|
codec: codec,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,6 +284,7 @@ class SpotubeTrack extends Track {
|
|||||||
"ytTrack": ytTrack.toJson(),
|
"ytTrack": ytTrack.toJson(),
|
||||||
"ytUri": ytUri,
|
"ytUri": ytUri,
|
||||||
"siblings": siblings.map((sibling) => sibling.toJson()).toList(),
|
"siblings": siblings.map((sibling) => sibling.toJson()).toList(),
|
||||||
|
"codec": codec.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -459,6 +459,44 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
value: preferences.normalizeAudio,
|
value: preferences.normalizeAudio,
|
||||||
onChanged: preferences.setNormalizeAudio,
|
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(
|
SectionCardWithHeading(
|
||||||
|
@ -40,7 +40,11 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
await oldFile.exists()) {
|
await oldFile.exists()) {
|
||||||
await oldFile.rename(savePath);
|
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);
|
final file = File(request.path);
|
||||||
|
|
||||||
@ -89,6 +93,8 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
YoutubeEndpoints get yt => ref.read(youtubeProvider);
|
YoutubeEndpoints get yt => ref.read(youtubeProvider);
|
||||||
String get downloadDirectory =>
|
String get downloadDirectory =>
|
||||||
ref.read(userPreferencesProvider.select((s) => s.downloadLocation));
|
ref.read(userPreferencesProvider.select((s) => s.downloadLocation));
|
||||||
|
MusicCodec get downloadCodec =>
|
||||||
|
ref.read(userPreferencesProvider.select((s) => s.downloadMusicCodec));
|
||||||
|
|
||||||
int get $downloadCount => dl
|
int get $downloadCount => dl
|
||||||
.getAllDownloads()
|
.getAllDownloads()
|
||||||
@ -130,7 +136,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
String getTrackFileUrl(Track track) {
|
String getTrackFileUrl(Track track) {
|
||||||
final name =
|
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));
|
return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +172,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
await oldFile.rename("$savePath.old");
|
await oldFile.rename("$savePath.old");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track is SpotubeTrack) {
|
if (track is SpotubeTrack && track.codec == downloadCodec) {
|
||||||
final downloadTask = await dl.addDownload(track.ytUri, savePath);
|
final downloadTask = await dl.addDownload(track.ytUri, savePath);
|
||||||
if (downloadTask != null) {
|
if (downloadTask != null) {
|
||||||
$history.add(track);
|
$history.add(track);
|
||||||
@ -174,7 +180,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
$backHistory.add(track);
|
$backHistory.add(track);
|
||||||
final spotubeTrack =
|
final spotubeTrack =
|
||||||
await SpotubeTrack.fetchFromTrack(track, yt).then((d) {
|
await SpotubeTrack.fetchFromTrack(track, yt, downloadCodec).then((d) {
|
||||||
$backHistory.remove(track);
|
$backHistory.remove(track);
|
||||||
return d;
|
return d;
|
||||||
});
|
});
|
||||||
|
@ -30,6 +30,7 @@ mixin NextFetcher on StateNotifier<ProxyPlaylist> {
|
|||||||
final future = SpotubeTrack.fetchFromTrack(
|
final future = SpotubeTrack.fetchFromTrack(
|
||||||
track,
|
track,
|
||||||
youtube,
|
youtube,
|
||||||
|
preferences.streamMusicCodec,
|
||||||
);
|
);
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
return await future;
|
return await future;
|
||||||
|
@ -185,10 +185,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
currentSegments.value = (
|
if (audioPlayer.currentSource != null) {
|
||||||
source: audioPlayer.currentSource!,
|
currentSegments.value = (
|
||||||
segments: [],
|
source: audioPlayer.currentSource!,
|
||||||
);
|
segments: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isFetchingSegments.value = false;
|
isFetchingSegments.value = false;
|
||||||
}
|
}
|
||||||
@ -223,7 +225,11 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
|
|
||||||
final nthFetchedTrack = switch (track.runtimeType) {
|
final nthFetchedTrack = switch (track.runtimeType) {
|
||||||
SpotubeTrack => track as SpotubeTrack,
|
SpotubeTrack => track as SpotubeTrack,
|
||||||
_ => await SpotubeTrack.fetchFromTrack(track, youtube),
|
_ => await SpotubeTrack.fetchFromTrack(
|
||||||
|
track,
|
||||||
|
youtube,
|
||||||
|
preferences.streamMusicCodec,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
await audioPlayer.replaceSource(
|
await audioPlayer.replaceSource(
|
||||||
@ -309,10 +315,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
final addableTrack = await SpotubeTrack.fetchFromTrack(
|
final addableTrack = await SpotubeTrack.fetchFromTrack(
|
||||||
tracks.elementAtOrNull(initialIndex) ?? tracks.first,
|
tracks.elementAtOrNull(initialIndex) ?? tracks.first,
|
||||||
youtube,
|
youtube,
|
||||||
|
preferences.streamMusicCodec,
|
||||||
).catchError((e, stackTrace) {
|
).catchError((e, stackTrace) {
|
||||||
return SpotubeTrack.fetchFromTrack(
|
return SpotubeTrack.fetchFromTrack(
|
||||||
tracks.elementAtOrNull(initialIndex + 1) ?? tracks.first,
|
tracks.elementAtOrNull(initialIndex + 1) ?? tracks.first,
|
||||||
youtube,
|
youtube,
|
||||||
|
preferences.streamMusicCodec,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,39 +40,36 @@ enum YoutubeApiType {
|
|||||||
String get label => name[0].toUpperCase() + name.substring(1);
|
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 {
|
class UserPreferences extends PersistedChangeNotifier {
|
||||||
ThemeMode themeMode;
|
|
||||||
Market recommendationMarket;
|
|
||||||
bool saveTrackLyrics;
|
|
||||||
bool checkUpdate;
|
|
||||||
AudioQuality audioQuality;
|
AudioQuality audioQuality;
|
||||||
|
|
||||||
late SpotubeColor accentColorScheme;
|
|
||||||
bool albumColorSync;
|
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 amoledDarkTheme;
|
||||||
|
bool checkUpdate;
|
||||||
bool normalizeAudio;
|
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;
|
final Ref ref;
|
||||||
|
|
||||||
@ -96,6 +93,8 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
this.systemTitleBar = false,
|
this.systemTitleBar = false,
|
||||||
this.amoledDarkTheme = false,
|
this.amoledDarkTheme = false,
|
||||||
this.normalizeAudio = true,
|
this.normalizeAudio = true,
|
||||||
|
this.streamMusicCodec = MusicCodec.weba,
|
||||||
|
this.downloadMusicCodec = MusicCodec.m4a,
|
||||||
SpotubeColor? accentColorScheme,
|
SpotubeColor? accentColorScheme,
|
||||||
}) : super() {
|
}) : super() {
|
||||||
this.accentColorScheme =
|
this.accentColorScheme =
|
||||||
@ -129,6 +128,20 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
setAmoledDarkTheme(false);
|
setAmoledDarkTheme(false);
|
||||||
setNormalizeAudio(true);
|
setNormalizeAudio(true);
|
||||||
setAccentColorScheme(SpotubeColor(Colors.blue.value, name: "Blue"));
|
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) {
|
void setThemeMode(ThemeMode mode) {
|
||||||
@ -327,6 +340,16 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
|
|
||||||
normalizeAudio = map["normalizeAudio"] ?? normalizeAudio;
|
normalizeAudio = map["normalizeAudio"] ?? normalizeAudio;
|
||||||
audioPlayer.setAudioNormalization(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
|
@override
|
||||||
@ -352,6 +375,8 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
'systemTitleBar': systemTitleBar,
|
'systemTitleBar': systemTitleBar,
|
||||||
"amoledDarkTheme": amoledDarkTheme,
|
"amoledDarkTheme": amoledDarkTheme,
|
||||||
"normalizeAudio": normalizeAudio,
|
"normalizeAudio": normalizeAudio,
|
||||||
|
"streamMusicCodec": streamMusicCodec.name,
|
||||||
|
"downloadMusicCodec": downloadMusicCodec.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
return switch (preferences.audioQuality) {
|
||||||
AudioQuality.high => stream
|
AudioQuality.high =>
|
||||||
.highestBitrateAudioStreamOfFormat(PipedAudioStreamFormat.m4a)!
|
stream.highestBitrateAudioStreamOfFormat(pipedStreamFormat)!.url,
|
||||||
.url,
|
AudioQuality.low =>
|
||||||
AudioQuality.low => stream
|
stream.lowestBitrateAudioStreamOfFormat(pipedStreamFormat)!.url,
|
||||||
.lowestBitrateAudioStreamOfFormat(PipedAudioStreamFormat.m4a)!
|
|
||||||
.url,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> streamingUrl(String id) async {
|
Future<String> streamingUrl(String id, MusicCodec codec) async {
|
||||||
if (youtube != null) {
|
if (youtube != null) {
|
||||||
final res = await PrimitiveUtils.raceMultiple(
|
final res = await PrimitiveUtils.raceMultiple(
|
||||||
() => youtube!.videos.streams.getManifest(id),
|
() => youtube!.videos.streams.getManifest(id),
|
||||||
);
|
);
|
||||||
final audioOnlyManifests = res.audioOnly.where((info) {
|
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) {
|
return switch (preferences.audioQuality) {
|
||||||
@ -208,26 +217,27 @@ class YoutubeEndpoints {
|
|||||||
audioOnlyManifests.sortByBitrate().last.url.toString(),
|
audioOnlyManifests.sortByBitrate().last.url.toString(),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return _pipedStreamResponseToStreamUrl(await piped!.streams(id));
|
return _pipedStreamResponseToStreamUrl(await piped!.streams(id), codec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<(YoutubeVideoInfo info, String streamingUrl)> video(
|
Future<(YoutubeVideoInfo info, String streamingUrl)> video(
|
||||||
String id,
|
String id,
|
||||||
SearchMode searchMode,
|
SearchMode searchMode,
|
||||||
|
MusicCodec codec,
|
||||||
) async {
|
) async {
|
||||||
if (youtube != null) {
|
if (youtube != null) {
|
||||||
final res = await youtube!.videos.get(id);
|
final res = await youtube!.videos.get(id);
|
||||||
return (
|
return (
|
||||||
YoutubeVideoInfo.fromVideo(res),
|
YoutubeVideoInfo.fromVideo(res),
|
||||||
await streamingUrl(id),
|
await streamingUrl(id, codec),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
final res = await piped!.streams(id);
|
final res = await piped!.streams(id);
|
||||||
return (
|
return (
|
||||||
YoutubeVideoInfo.fromStreamResponse(res, searchMode),
|
YoutubeVideoInfo.fromStreamResponse(res, searchMode),
|
||||||
_pipedStreamResponseToStreamUrl(res),
|
_pipedStreamResponseToStreamUrl(res, codec),
|
||||||
);
|
);
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
await showPipedErrorDialog(e);
|
await showPipedErrorDialog(e);
|
||||||
|
@ -126,21 +126,21 @@ abstract class TypeConversionUtils {
|
|||||||
}) {
|
}) {
|
||||||
final track = Track();
|
final track = Track();
|
||||||
track.album = Album()
|
track.album = Album()
|
||||||
..name = metadata?.album ?? "Spotube"
|
..name = metadata?.album ?? "Unknown"
|
||||||
..images = [if (art != null) Image()..url = art]
|
..images = [if (art != null) Image()..url = art]
|
||||||
..genres = [if (metadata?.genre != null) metadata!.genre!]
|
..genres = [if (metadata?.genre != null) metadata!.genre!]
|
||||||
..artists = [
|
..artists = [
|
||||||
Artist()
|
Artist()
|
||||||
..name = metadata?.albumArtist ?? "Spotube"
|
..name = metadata?.albumArtist ?? "Unknown"
|
||||||
..id = metadata?.albumArtist ?? "Spotube"
|
..id = metadata?.albumArtist ?? "Unknown"
|
||||||
..type = "artist",
|
..type = "artist",
|
||||||
]
|
]
|
||||||
..id = metadata?.album
|
..id = metadata?.album
|
||||||
..releaseDate = metadata?.year?.toString();
|
..releaseDate = metadata?.year?.toString();
|
||||||
track.artists = [
|
track.artists = [
|
||||||
Artist()
|
Artist()
|
||||||
..name = metadata?.artist ?? "Spotube"
|
..name = metadata?.artist ?? "Unknown"
|
||||||
..id = metadata?.artist ?? "Spotube"
|
..id = metadata?.artist ?? "Unknown"
|
||||||
];
|
];
|
||||||
|
|
||||||
track.id = metadata?.title ?? basenameWithoutExtension(file.path);
|
track.id = metadata?.title ?? basenameWithoutExtension(file.path);
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"bn": [
|
"bn": [
|
||||||
@ -12,7 +14,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ca": [
|
"ca": [
|
||||||
@ -20,7 +24,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"de": [
|
"de": [
|
||||||
@ -28,7 +34,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
@ -36,7 +44,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
@ -44,7 +54,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"hi": [
|
"hi": [
|
||||||
@ -52,7 +64,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ja": [
|
"ja": [
|
||||||
@ -60,7 +74,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pl": [
|
"pl": [
|
||||||
@ -68,7 +84,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
@ -76,7 +94,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
@ -84,7 +104,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"uk": [
|
"uk": [
|
||||||
@ -92,7 +114,9 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
@ -100,6 +124,8 @@
|
|||||||
"normalize_audio",
|
"normalize_audio",
|
||||||
"change_cover",
|
"change_cover",
|
||||||
"add_cover",
|
"add_cover",
|
||||||
"restore_defaults"
|
"restore_defaults",
|
||||||
|
"download_music_codec",
|
||||||
|
"streaming_music_codec"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user