Download all supported audio languages (#556)

This commit is contained in:
Oleksii Holub 2024-12-06 21:47:04 +02:00 committed by GitHub
parent 1b3f5cb623
commit e0d95244b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 85 additions and 15 deletions

View File

@ -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

View File

@ -18,7 +18,10 @@ public partial record VideoDownloadOption(
public partial record VideoDownloadOption
{
internal static IReadOnlyList<VideoDownloadOption> ResolveAll(StreamManifest manifest)
internal static IReadOnlyList<VideoDownloadOption> ResolveAll(
StreamManifest manifest,
bool includeLanguageSpecificAudioStreams = true
)
{
IEnumerable<VideoDownloadOption> GetVideoAndAudioOptions()
{
@ -40,22 +43,50 @@ public partial record VideoDownloadOption
// 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 @@ public partial record VideoDownloadOption
{
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 @@ public partial record VideoDownloadOption
{
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();

View File

@ -19,20 +19,26 @@ public class VideoDownloader(IReadOnlyList<Cookie>? initialCookies = null)
public async Task<IReadOnlyList<VideoDownloadOption>> 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<VideoDownloadOption> 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.");

View File

@ -5,8 +5,8 @@
<PackageReference Include="Gress" Version="2.1.1" />
<PackageReference Include="JsonExtensions" Version="1.2.0" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="YoutubeExplode" Version="6.4.4" />
<PackageReference Include="YoutubeExplode.Converter" Version="6.4.4" />
<PackageReference Include="YoutubeExplode" Version="6.5.0" />
<PackageReference Include="YoutubeExplode.Converter" Version="6.5.0" />
</ItemGroup>
</Project>

View File

@ -49,6 +49,13 @@ public partial class SettingsService()
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
{

View File

@ -104,6 +104,7 @@ public partial class DashboardViewModel : ViewModelBase
?? await downloader.GetBestDownloadOptionAsync(
download.Video!.Id,
download.DownloadPreference!,
_settingsService.ShouldInjectLanguageSpecificAudioStreams,
download.CancellationToken
);
@ -190,7 +191,10 @@ public partial class DashboardViewModel : ViewModelBase
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)

View File

@ -40,6 +40,12 @@ public class SettingsViewModel : DialogViewModelBase
set => _settingsService.IsAuthPersisted = value;
}
public bool ShouldInjectLanguageSpecificAudioStreams
{
get => _settingsService.ShouldInjectLanguageSpecificAudioStreams;
set => _settingsService.ShouldInjectLanguageSpecificAudioStreams = value;
}
public bool ShouldInjectSubtitles
{
get => _settingsService.ShouldInjectSubtitles;

View File

@ -70,11 +70,20 @@
IsChecked="{Binding IsAuthPersisted}" />
</DockPanel>
<!-- Inject language-specific audio streams -->
<DockPanel
Margin="16,8"
LastChildFill="False"
ToolTip.Tip="Inject audio tracks in alternative languages (if available) into downloaded files">
<TextBlock DockPanel.Dock="Left" Text="Inject alternative languages" />
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldInjectLanguageSpecificAudioStreams}" />
</DockPanel>
<!-- Inject subtitles -->
<DockPanel
Margin="16,8"
LastChildFill="False"
ToolTip.Tip="Inject subtitles into downloaded files">
ToolTip.Tip="Inject subtitles (if available) into downloaded files">
<TextBlock DockPanel.Dock="Left" Text="Inject subtitles" />
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldInjectSubtitles}" />
</DockPanel>
@ -83,7 +92,7 @@
<DockPanel
Margin="16,8"
LastChildFill="False"
ToolTip.Tip="Inject media tags into downloaded files">
ToolTip.Tip="Inject media tags (if available) into downloaded files">
<TextBlock DockPanel.Dock="Left" Text="Inject media tags" />
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldInjectTags}" />
</DockPanel>