diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index 78fb53b7..b3a1e340 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -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'; diff --git a/lib/models/spotube_track.dart b/lib/models/spotube_track.dart index a0c5f132..a8b94ef5 100644 --- a/lib/models/spotube_track.dart +++ b/lib/models/spotube_track.dart @@ -23,6 +23,7 @@ class TrackNotFoundException implements Exception { class SpotubeTrack extends Track { final YoutubeVideoInfo ytTrack; final String ytUri; + final MusicCodec codec; final List 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 fetchFromTrack( Track track, YoutubeEndpoints client, + MusicCodec codec, ) async { final matchedCachedTrack = await MatchedTrack.box.get(track.id!); var siblings = []; @@ -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>(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, }; } } diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index 78c07270..bf92e9e0 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -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 ?? [])}.m4a"; + "${track.name} - ${TypeConversionUtils.artists_X_String(track.artists ?? [])}.${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; }); diff --git a/lib/provider/proxy_playlist/next_fetcher_mixin.dart b/lib/provider/proxy_playlist/next_fetcher_mixin.dart index fce006b0..2f60dcd4 100644 --- a/lib/provider/proxy_playlist/next_fetcher_mixin.dart +++ b/lib/provider/proxy_playlist/next_fetcher_mixin.dart @@ -30,6 +30,7 @@ mixin NextFetcher on StateNotifier { final future = SpotubeTrack.fetchFromTrack( track, youtube, + preferences.streamMusicCodec, ); if (i == 0) { return await future; diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 69464199..be01978e 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -185,10 +185,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier ), ); } 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 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 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, ); }); diff --git a/lib/provider/user_preferences_provider.dart b/lib/provider/user_preferences_provider.dart index 5cc4086c..2da8e8dd 100644 --- a/lib/provider/user_preferences_provider.dart +++ b/lib/provider/user_preferences_provider.dart @@ -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); diff --git a/lib/services/youtube/youtube.dart b/lib/services/youtube/youtube.dart index fbf559d4..c8c277e3 100644 --- a/lib/services/youtube/youtube.dart +++ b/lib/services/youtube/youtube.dart @@ -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 streamingUrl(String id) async { + Future 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); diff --git a/lib/utils/type_conversion_utils.dart b/lib/utils/type_conversion_utils.dart index 68a8d9a4..a805272c 100644 --- a/lib/utils/type_conversion_utils.dart +++ b/lib/utils/type_conversion_utils.dart @@ -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);