From e0d95244b96b7cc6c703ed4d7a55671f0519e774 Mon Sep 17 00:00:00 2001 From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> Date: Fri, 6 Dec 2024 21:47:04 +0200 Subject: [PATCH] Download all supported audio languages (#556) --- Readme.md | 1 + .../Downloading/VideoDownloadOption.cs | 53 ++++++++++++++++--- .../Downloading/VideoDownloader.cs | 10 +++- .../YoutubeDownloader.Core.csproj | 4 +- YoutubeDownloader/Services/SettingsService.cs | 7 +++ .../Components/DashboardViewModel.cs | 6 ++- .../ViewModels/Dialogs/SettingsViewModel.cs | 6 +++ .../Views/Dialogs/SettingsView.axaml | 13 ++++- 8 files changed, 85 insertions(+), 15 deletions(-) diff --git a/Readme.md b/Readme.md index e9adca5bf..3a8b32a5b 100644 --- a/Readme.md +++ b/Readme.md @@ -56,6 +56,7 @@ To learn more about the war and how you can help, [click here](https://tyrrrz.me - Download videos from playlists or channels - Download videos by search query - Selectable video quality and format +- Automatically embed audio tracks in alternative languages - Automatically embed subtitles - Automatically inject media tags - Log in with a YouTube account to access private content diff --git a/YoutubeDownloader.Core/Downloading/VideoDownloadOption.cs b/YoutubeDownloader.Core/Downloading/VideoDownloadOption.cs index 69c7f90d1..8798a0468 100644 --- a/YoutubeDownloader.Core/Downloading/VideoDownloadOption.cs +++ b/YoutubeDownloader.Core/Downloading/VideoDownloadOption.cs @@ -18,7 +18,10 @@ IReadOnlyList StreamInfos public partial record VideoDownloadOption { - internal static IReadOnlyList ResolveAll(StreamManifest manifest) + internal static IReadOnlyList ResolveAll( + StreamManifest manifest, + bool includeLanguageSpecificAudioStreams = true + ) { IEnumerable GetVideoAndAudioOptions() { @@ -40,22 +43,50 @@ IEnumerable GetVideoAndAudioOptions() // Separate audio + video stream else { - // Prefer audio stream with the same container - var audioStreamInfo = manifest + var audioStreamInfos = manifest .GetAudioStreams() + // Prefer audio streams with the same container .OrderByDescending(s => s.Container == videoStreamInfo.Container) .ThenByDescending(s => s is AudioOnlyStreamInfo) .ThenByDescending(s => s.Bitrate) - .FirstOrDefault(); + .ToArray(); - if (audioStreamInfo is not null) + // Prefer language-specific audio streams, if available and if allowed + var languageSpecificAudioStreamInfos = includeLanguageSpecificAudioStreams + ? audioStreamInfos + .Where(s => s.AudioLanguage is not null) + .DistinctBy(s => s.AudioLanguage) + // Default language first so it's encoded as the first audio track in the output file + .OrderByDescending(s => s.IsAudioLanguageDefault) + .ToArray() + : []; + + // If there are language-specific streams, include them all + if (languageSpecificAudioStreamInfos.Any()) { yield return new VideoDownloadOption( videoStreamInfo.Container, false, - new IStreamInfo[] { videoStreamInfo, audioStreamInfo } + [videoStreamInfo, .. languageSpecificAudioStreamInfos] ); } + // If there are no language-specific streams, download the single best quality audio stream + else + { + var audioStreamInfo = audioStreamInfos + // Prefer audio streams in the default language (or non-language-specific streams) + .OrderByDescending(s => s.IsAudioLanguageDefault ?? true) + .FirstOrDefault(); + + if (audioStreamInfo is not null) + { + yield return new VideoDownloadOption( + videoStreamInfo.Container, + false, + [videoStreamInfo, audioStreamInfo] + ); + } + } } } } @@ -66,7 +97,10 @@ IEnumerable GetAudioOnlyOptions() { var audioStreamInfo = manifest .GetAudioStreams() - .OrderByDescending(s => s.Container == Container.WebM) + // Prefer audio streams in the default language (or non-language-specific streams) + .OrderByDescending(s => s.IsAudioLanguageDefault ?? true) + // Prefer audio streams with the same container + .ThenByDescending(s => s.Container == Container.WebM) .ThenByDescending(s => s is AudioOnlyStreamInfo) .ThenByDescending(s => s.Bitrate) .FirstOrDefault(); @@ -89,7 +123,10 @@ IEnumerable GetAudioOnlyOptions() { var audioStreamInfo = manifest .GetAudioStreams() - .OrderByDescending(s => s.Container == Container.Mp4) + // Prefer audio streams in the default language (or non-language-specific streams) + .OrderByDescending(s => s.IsAudioLanguageDefault ?? true) + // Prefer audio streams with the same container + .ThenByDescending(s => s.Container == Container.Mp4) .ThenByDescending(s => s is AudioOnlyStreamInfo) .ThenByDescending(s => s.Bitrate) .FirstOrDefault(); diff --git a/YoutubeDownloader.Core/Downloading/VideoDownloader.cs b/YoutubeDownloader.Core/Downloading/VideoDownloader.cs index 610d44d26..37cca56ef 100644 --- a/YoutubeDownloader.Core/Downloading/VideoDownloader.cs +++ b/YoutubeDownloader.Core/Downloading/VideoDownloader.cs @@ -19,20 +19,26 @@ public class VideoDownloader(IReadOnlyList? initialCookies = null) public async Task> GetDownloadOptionsAsync( VideoId videoId, + bool includeLanguageSpecificAudioStreams = true, CancellationToken cancellationToken = default ) { var manifest = await _youtube.Videos.Streams.GetManifestAsync(videoId, cancellationToken); - return VideoDownloadOption.ResolveAll(manifest); + return VideoDownloadOption.ResolveAll(manifest, includeLanguageSpecificAudioStreams); } public async Task GetBestDownloadOptionAsync( VideoId videoId, VideoDownloadPreference preference, + bool includeLanguageSpecificAudioStreams = true, CancellationToken cancellationToken = default ) { - var options = await GetDownloadOptionsAsync(videoId, cancellationToken); + var options = await GetDownloadOptionsAsync( + videoId, + includeLanguageSpecificAudioStreams, + cancellationToken + ); return preference.TryGetBestOption(options) ?? throw new InvalidOperationException("No suitable download option found."); diff --git a/YoutubeDownloader.Core/YoutubeDownloader.Core.csproj b/YoutubeDownloader.Core/YoutubeDownloader.Core.csproj index 542a3bc8b..b8c2f9661 100644 --- a/YoutubeDownloader.Core/YoutubeDownloader.Core.csproj +++ b/YoutubeDownloader.Core/YoutubeDownloader.Core.csproj @@ -5,8 +5,8 @@ - - + + \ No newline at end of file diff --git a/YoutubeDownloader/Services/SettingsService.cs b/YoutubeDownloader/Services/SettingsService.cs index e328212cf..5d6ccc1af 100644 --- a/YoutubeDownloader/Services/SettingsService.cs +++ b/YoutubeDownloader/Services/SettingsService.cs @@ -49,6 +49,13 @@ public bool IsAuthPersisted set => SetProperty(ref _isAuthPersisted, value); } + private bool _shouldInjectLanguageSpecificAudioStreams = true; + public bool ShouldInjectLanguageSpecificAudioStreams + { + get => _shouldInjectLanguageSpecificAudioStreams; + set => SetProperty(ref _shouldInjectLanguageSpecificAudioStreams, value); + } + private bool _shouldInjectSubtitles = true; public bool ShouldInjectSubtitles { diff --git a/YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs b/YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs index 8d130ac39..e5d9dd816 100644 --- a/YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs +++ b/YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs @@ -104,6 +104,7 @@ private async void EnqueueDownload(DownloadViewModel download, int position = 0) ?? await downloader.GetBestDownloadOptionAsync( download.Video!.Id, download.DownloadPreference!, + _settingsService.ShouldInjectLanguageSpecificAudioStreams, download.CancellationToken ); @@ -190,7 +191,10 @@ private async Task ProcessQueryAsync() if (result.Videos.Count == 1) { var video = result.Videos.Single(); - var downloadOptions = await downloader.GetDownloadOptionsAsync(video.Id); + var downloadOptions = await downloader.GetDownloadOptionsAsync( + video.Id, + _settingsService.ShouldInjectLanguageSpecificAudioStreams + ); var download = await _dialogManager.ShowDialogAsync( _viewModelManager.CreateDownloadSingleSetupViewModel(video, downloadOptions) diff --git a/YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs b/YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs index e77ae9e0f..b8f2f83de 100644 --- a/YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs +++ b/YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs @@ -40,6 +40,12 @@ public bool IsAuthPersisted set => _settingsService.IsAuthPersisted = value; } + public bool ShouldInjectLanguageSpecificAudioStreams + { + get => _settingsService.ShouldInjectLanguageSpecificAudioStreams; + set => _settingsService.ShouldInjectLanguageSpecificAudioStreams = value; + } + public bool ShouldInjectSubtitles { get => _settingsService.ShouldInjectSubtitles; diff --git a/YoutubeDownloader/Views/Dialogs/SettingsView.axaml b/YoutubeDownloader/Views/Dialogs/SettingsView.axaml index d6c677d2c..57b3fff91 100644 --- a/YoutubeDownloader/Views/Dialogs/SettingsView.axaml +++ b/YoutubeDownloader/Views/Dialogs/SettingsView.axaml @@ -70,11 +70,20 @@ IsChecked="{Binding IsAuthPersisted}" /> + + + + + + + ToolTip.Tip="Inject subtitles (if available) into downloaded files"> @@ -83,7 +92,7 @@ + ToolTip.Tip="Inject media tags (if available) into downloaded files">