Skip to content

Commit e0d9524

Browse files
authored
Download all supported audio languages (#556)
1 parent 1b3f5cb commit e0d9524

File tree

8 files changed

+85
-15
lines changed

8 files changed

+85
-15
lines changed

Readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ To learn more about the war and how you can help, [click here](https://tyrrrz.me
5656
- Download videos from playlists or channels
5757
- Download videos by search query
5858
- Selectable video quality and format
59+
- Automatically embed audio tracks in alternative languages
5960
- Automatically embed subtitles
6061
- Automatically inject media tags
6162
- Log in with a YouTube account to access private content

YoutubeDownloader.Core/Downloading/VideoDownloadOption.cs

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ IReadOnlyList<IStreamInfo> StreamInfos
1818

1919
public partial record VideoDownloadOption
2020
{
21-
internal static IReadOnlyList<VideoDownloadOption> ResolveAll(StreamManifest manifest)
21+
internal static IReadOnlyList<VideoDownloadOption> ResolveAll(
22+
StreamManifest manifest,
23+
bool includeLanguageSpecificAudioStreams = true
24+
)
2225
{
2326
IEnumerable<VideoDownloadOption> GetVideoAndAudioOptions()
2427
{
@@ -40,22 +43,50 @@ IEnumerable<VideoDownloadOption> GetVideoAndAudioOptions()
4043
// Separate audio + video stream
4144
else
4245
{
43-
// Prefer audio stream with the same container
44-
var audioStreamInfo = manifest
46+
var audioStreamInfos = manifest
4547
.GetAudioStreams()
48+
// Prefer audio streams with the same container
4649
.OrderByDescending(s => s.Container == videoStreamInfo.Container)
4750
.ThenByDescending(s => s is AudioOnlyStreamInfo)
4851
.ThenByDescending(s => s.Bitrate)
49-
.FirstOrDefault();
52+
.ToArray();
5053

51-
if (audioStreamInfo is not null)
54+
// Prefer language-specific audio streams, if available and if allowed
55+
var languageSpecificAudioStreamInfos = includeLanguageSpecificAudioStreams
56+
? audioStreamInfos
57+
.Where(s => s.AudioLanguage is not null)
58+
.DistinctBy(s => s.AudioLanguage)
59+
// Default language first so it's encoded as the first audio track in the output file
60+
.OrderByDescending(s => s.IsAudioLanguageDefault)
61+
.ToArray()
62+
: [];
63+
64+
// If there are language-specific streams, include them all
65+
if (languageSpecificAudioStreamInfos.Any())
5266
{
5367
yield return new VideoDownloadOption(
5468
videoStreamInfo.Container,
5569
false,
56-
new IStreamInfo[] { videoStreamInfo, audioStreamInfo }
70+
[videoStreamInfo, .. languageSpecificAudioStreamInfos]
5771
);
5872
}
73+
// If there are no language-specific streams, download the single best quality audio stream
74+
else
75+
{
76+
var audioStreamInfo = audioStreamInfos
77+
// Prefer audio streams in the default language (or non-language-specific streams)
78+
.OrderByDescending(s => s.IsAudioLanguageDefault ?? true)
79+
.FirstOrDefault();
80+
81+
if (audioStreamInfo is not null)
82+
{
83+
yield return new VideoDownloadOption(
84+
videoStreamInfo.Container,
85+
false,
86+
[videoStreamInfo, audioStreamInfo]
87+
);
88+
}
89+
}
5990
}
6091
}
6192
}
@@ -66,7 +97,10 @@ IEnumerable<VideoDownloadOption> GetAudioOnlyOptions()
6697
{
6798
var audioStreamInfo = manifest
6899
.GetAudioStreams()
69-
.OrderByDescending(s => s.Container == Container.WebM)
100+
// Prefer audio streams in the default language (or non-language-specific streams)
101+
.OrderByDescending(s => s.IsAudioLanguageDefault ?? true)
102+
// Prefer audio streams with the same container
103+
.ThenByDescending(s => s.Container == Container.WebM)
70104
.ThenByDescending(s => s is AudioOnlyStreamInfo)
71105
.ThenByDescending(s => s.Bitrate)
72106
.FirstOrDefault();
@@ -89,7 +123,10 @@ IEnumerable<VideoDownloadOption> GetAudioOnlyOptions()
89123
{
90124
var audioStreamInfo = manifest
91125
.GetAudioStreams()
92-
.OrderByDescending(s => s.Container == Container.Mp4)
126+
// Prefer audio streams in the default language (or non-language-specific streams)
127+
.OrderByDescending(s => s.IsAudioLanguageDefault ?? true)
128+
// Prefer audio streams with the same container
129+
.ThenByDescending(s => s.Container == Container.Mp4)
93130
.ThenByDescending(s => s is AudioOnlyStreamInfo)
94131
.ThenByDescending(s => s.Bitrate)
95132
.FirstOrDefault();

YoutubeDownloader.Core/Downloading/VideoDownloader.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,26 @@ public class VideoDownloader(IReadOnlyList<Cookie>? initialCookies = null)
1919

2020
public async Task<IReadOnlyList<VideoDownloadOption>> GetDownloadOptionsAsync(
2121
VideoId videoId,
22+
bool includeLanguageSpecificAudioStreams = true,
2223
CancellationToken cancellationToken = default
2324
)
2425
{
2526
var manifest = await _youtube.Videos.Streams.GetManifestAsync(videoId, cancellationToken);
26-
return VideoDownloadOption.ResolveAll(manifest);
27+
return VideoDownloadOption.ResolveAll(manifest, includeLanguageSpecificAudioStreams);
2728
}
2829

2930
public async Task<VideoDownloadOption> GetBestDownloadOptionAsync(
3031
VideoId videoId,
3132
VideoDownloadPreference preference,
33+
bool includeLanguageSpecificAudioStreams = true,
3234
CancellationToken cancellationToken = default
3335
)
3436
{
35-
var options = await GetDownloadOptionsAsync(videoId, cancellationToken);
37+
var options = await GetDownloadOptionsAsync(
38+
videoId,
39+
includeLanguageSpecificAudioStreams,
40+
cancellationToken
41+
);
3642

3743
return preference.TryGetBestOption(options)
3844
?? throw new InvalidOperationException("No suitable download option found.");

YoutubeDownloader.Core/YoutubeDownloader.Core.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
<PackageReference Include="Gress" Version="2.1.1" />
66
<PackageReference Include="JsonExtensions" Version="1.2.0" />
77
<PackageReference Include="TagLibSharp" Version="2.3.0" />
8-
<PackageReference Include="YoutubeExplode" Version="6.4.4" />
9-
<PackageReference Include="YoutubeExplode.Converter" Version="6.4.4" />
8+
<PackageReference Include="YoutubeExplode" Version="6.5.0" />
9+
<PackageReference Include="YoutubeExplode.Converter" Version="6.5.0" />
1010
</ItemGroup>
1111

1212
</Project>

YoutubeDownloader/Services/SettingsService.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ public bool IsAuthPersisted
4949
set => SetProperty(ref _isAuthPersisted, value);
5050
}
5151

52+
private bool _shouldInjectLanguageSpecificAudioStreams = true;
53+
public bool ShouldInjectLanguageSpecificAudioStreams
54+
{
55+
get => _shouldInjectLanguageSpecificAudioStreams;
56+
set => SetProperty(ref _shouldInjectLanguageSpecificAudioStreams, value);
57+
}
58+
5259
private bool _shouldInjectSubtitles = true;
5360
public bool ShouldInjectSubtitles
5461
{

YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ private async void EnqueueDownload(DownloadViewModel download, int position = 0)
104104
?? await downloader.GetBestDownloadOptionAsync(
105105
download.Video!.Id,
106106
download.DownloadPreference!,
107+
_settingsService.ShouldInjectLanguageSpecificAudioStreams,
107108
download.CancellationToken
108109
);
109110

@@ -190,7 +191,10 @@ private async Task ProcessQueryAsync()
190191
if (result.Videos.Count == 1)
191192
{
192193
var video = result.Videos.Single();
193-
var downloadOptions = await downloader.GetDownloadOptionsAsync(video.Id);
194+
var downloadOptions = await downloader.GetDownloadOptionsAsync(
195+
video.Id,
196+
_settingsService.ShouldInjectLanguageSpecificAudioStreams
197+
);
194198

195199
var download = await _dialogManager.ShowDialogAsync(
196200
_viewModelManager.CreateDownloadSingleSetupViewModel(video, downloadOptions)

YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ public bool IsAuthPersisted
4040
set => _settingsService.IsAuthPersisted = value;
4141
}
4242

43+
public bool ShouldInjectLanguageSpecificAudioStreams
44+
{
45+
get => _settingsService.ShouldInjectLanguageSpecificAudioStreams;
46+
set => _settingsService.ShouldInjectLanguageSpecificAudioStreams = value;
47+
}
48+
4349
public bool ShouldInjectSubtitles
4450
{
4551
get => _settingsService.ShouldInjectSubtitles;

YoutubeDownloader/Views/Dialogs/SettingsView.axaml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,20 @@
7070
IsChecked="{Binding IsAuthPersisted}" />
7171
</DockPanel>
7272

73+
<!-- Inject language-specific audio streams -->
74+
<DockPanel
75+
Margin="16,8"
76+
LastChildFill="False"
77+
ToolTip.Tip="Inject audio tracks in alternative languages (if available) into downloaded files">
78+
<TextBlock DockPanel.Dock="Left" Text="Inject alternative languages" />
79+
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldInjectLanguageSpecificAudioStreams}" />
80+
</DockPanel>
81+
7382
<!-- Inject subtitles -->
7483
<DockPanel
7584
Margin="16,8"
7685
LastChildFill="False"
77-
ToolTip.Tip="Inject subtitles into downloaded files">
86+
ToolTip.Tip="Inject subtitles (if available) into downloaded files">
7887
<TextBlock DockPanel.Dock="Left" Text="Inject subtitles" />
7988
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldInjectSubtitles}" />
8089
</DockPanel>
@@ -83,7 +92,7 @@
8392
<DockPanel
8493
Margin="16,8"
8594
LastChildFill="False"
86-
ToolTip.Tip="Inject media tags into downloaded files">
95+
ToolTip.Tip="Inject media tags (if available) into downloaded files">
8796
<TextBlock DockPanel.Dock="Left" Text="Inject media tags" />
8897
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldInjectTags}" />
8998
</DockPanel>

0 commit comments

Comments
 (0)