Skip to content

Commit

Permalink
Download all supported audio languages (#556)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyrrrz authored Dec 6, 2024
1 parent 1b3f5cb commit e0d9524
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 15 deletions.
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 45 additions & 8 deletions YoutubeDownloader.Core/Downloading/VideoDownloadOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 18,10 @@ IReadOnlyList<IStreamInfo> StreamInfos

public partial record VideoDownloadOption
{
internal static IReadOnlyList<VideoDownloadOption> ResolveAll(StreamManifest manifest)
internal static IReadOnlyList<VideoDownloadOption> ResolveAll(
StreamManifest manifest,
bool includeLanguageSpecificAudioStreams = true
)
{
IEnumerable<VideoDownloadOption> GetVideoAndAudioOptions()
{
Expand All @@ -40,22 43,50 @@ IEnumerable<VideoDownloadOption> 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]
);
}
}
}
}
}
Expand All @@ -66,7 97,10 @@ IEnumerable<VideoDownloadOption> 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();
Expand All @@ -89,7 123,10 @@ IEnumerable<VideoDownloadOption> 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();
Expand Down
10 changes: 8 additions & 2 deletions YoutubeDownloader.Core/Downloading/VideoDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
4 changes: 2 additions & 2 deletions YoutubeDownloader.Core/YoutubeDownloader.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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>
7 changes: 7 additions & 0 deletions YoutubeDownloader/Services/SettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);

Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 11 additions & 2 deletions YoutubeDownloader/Views/Dialogs/SettingsView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down

0 comments on commit e0d9524

Please sign in to comment.