Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download all supported audio languages #556

Merged
merged 4 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Tyrrrz marked this conversation as resolved.
Show resolved Hide resolved
);
}
// 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
Loading