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

Support best and worst keywords in CLI video downloader #998

Merged
merged 1 commit into from
Mar 21, 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
11 changes: 11 additions & 0 deletions TwitchDownloaderCore.Tests/ExtensionTests/M3U8ExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public static class M3U8ExtensionTests
[InlineData("1920x1080p", "1080p60")]
[InlineData("1920x1080p60", "1080p60")]
[InlineData("Source", "1080p60")]
[InlineData("chunked", "1080p60")]
[InlineData("Best", "1080p60")]
[InlineData("Worst", "720p30")]
public static void CorrectlyFindsStreamOfQualityFromLiveM3U8Response(string qualityString, string expectedPath)
{
var m3u8 = new M3U8(new M3U8.Metadata(), new[]
Expand Down Expand Up @@ -61,6 +64,10 @@ public static void CorrectlyFindsStreamOfQualityFromLiveM3U8Response(string qual
[InlineData("audio", "audio_only")]
[InlineData("Audio", "audio_only")]
[InlineData("Audio Only", "audio_only")]
[InlineData("Source", "1080p60")]
[InlineData("chunked", "1080p60")]
[InlineData("Best", "1080p60")]
[InlineData("Worst", "144p30")]
public static void CorrectlyFindsStreamOfQualityFromOldM3U8Response(string qualityString, string expectedPath)
{
var m3u8 = new M3U8(new M3U8.Metadata(), new[]
Expand Down Expand Up @@ -105,6 +112,10 @@ public static void CorrectlyFindsStreamOfQualityFromOldM3U8Response(string quali
[InlineData("1080p60", "1080p60")]
[InlineData("720p60", "720p60")]
[InlineData("foo", "1080p60")]
[InlineData("Source", "1080p60")]
[InlineData("chunked", "1080p60")]
[InlineData("Best", "1080p60")]
[InlineData("Worst", "720p60")]
public static void CorrectlyFindsStreamOfQualityFromM3U8ResponseWithoutFramerate(string qualityString, string expectedPath)
{
var m3u8 = new M3U8(new M3U8.Metadata(), new[]
Expand Down
77 changes: 57 additions & 20 deletions TwitchDownloaderCore/Extensions/M3U8Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,22 @@ public static void SortStreamsByQuality(this M3U8 m3u8)

public static M3U8.Stream GetStreamOfQuality(this M3U8 m3u8, string qualityString)
{
var streams = m3u8.Streams;
if (streams.Length == 0)
if (m3u8.Streams.Length == 0)
{
throw new ArgumentException(nameof(m3u8), "M3U8 does not contain any streams.");
}

if (string.IsNullOrWhiteSpace(qualityString))
if (TryGetKeywordStream(m3u8, qualityString, out var keywordStream))
{
return m3u8.BestQualityStream();
}

if (qualityString.Contains("source", StringComparison.OrdinalIgnoreCase) || qualityString.Contains("chunked", StringComparison.OrdinalIgnoreCase))
{
return m3u8.BestQualityStream();
}

if (qualityString.Contains("audio", StringComparison.OrdinalIgnoreCase) &&
streams.FirstOrDefault(x => x.MediaInfo.Name.Contains("audio", StringComparison.OrdinalIgnoreCase)) is { } audioStream)
{
return audioStream;
return keywordStream;
}

if (!qualityString.Contains('x') && qualityString.Contains('p'))
{
foreach (var stream in streams)
foreach (var stream in m3u8.Streams)
{
if (qualityString.Equals(stream.StreamInfo.Video, StringComparison.OrdinalIgnoreCase) || qualityString.Equals(stream.MediaInfo.Name, StringComparison.OrdinalIgnoreCase))
if (qualityString.Equals(stream.StreamInfo.Video, StringComparison.OrdinalIgnoreCase) ||
qualityString.Equals(stream.MediaInfo.Name, StringComparison.OrdinalIgnoreCase))
{
return stream;
}
Expand All @@ -69,7 +58,7 @@ public static M3U8.Stream GetStreamOfQuality(this M3U8 m3u8, string qualityStrin
var desiredHeight = qualityStringMatch.Groups["Height"];
var desiredFramerate = qualityStringMatch.Groups["Framerate"];

var filteredStreams = streams
var filteredStreams = m3u8.Streams
.WhereOnlyIf(x => x.StreamInfo.Resolution.Width == int.Parse(desiredWidth.ValueSpan), desiredWidth.Success)
.WhereOnlyIf(x => x.StreamInfo.Resolution.Height == int.Parse(desiredHeight.ValueSpan), desiredHeight.Success)
.WhereOnlyIf(x => Math.Abs(x.StreamInfo.Framerate - int.Parse(desiredFramerate.ValueSpan)) <= 2, desiredFramerate.Success)
Expand All @@ -83,16 +72,49 @@ public static M3U8.Stream GetStreamOfQuality(this M3U8 m3u8, string qualityStrin
};
}

private static bool TryGetKeywordStream(M3U8 m3u8, string qualityString, out M3U8.Stream stream)
{
if (string.IsNullOrWhiteSpace(qualityString))
{
stream = m3u8.BestQualityStream();
return true;
}

if (qualityString.Contains("best", StringComparison.OrdinalIgnoreCase)
|| qualityString.Contains("source", StringComparison.OrdinalIgnoreCase)
|| qualityString.Contains("chunked", StringComparison.OrdinalIgnoreCase))
{
stream = m3u8.BestQualityStream();
return true;
}

if (qualityString.Contains("worst", StringComparison.OrdinalIgnoreCase))
{
stream = m3u8.WorstQualityStream();
return true;
}

if (qualityString.Contains("audio", StringComparison.OrdinalIgnoreCase)
&& m3u8.Streams.FirstOrDefault(x => x.IsAudioOnly()) is { } audioStream)
{
stream = audioStream;
return true;
}

stream = null;
return false;
}

/// <returns>
/// A <see cref="string"/> representing the <paramref name="streamInfo"/>'s <see cref="M3U8.Stream.ExtStreamInfo.Resolution"/>
/// A <see cref="string"/> representing the <paramref name="stream"/>'s <see cref="M3U8.Stream.ExtStreamInfo.Resolution"/>
/// and <see cref="M3U8.Stream.ExtStreamInfo.Framerate"/> in the format of "{resolution}p{framerate}" or <see langword="null"/>
/// </returns>
public static string GetResolutionFramerateString(this M3U8.Stream stream)
{
const string RESOLUTION_FRAMERATE_PATTERN = /*lang=regex*/@"\d{3,4}p\d{2,3}";

var mediaInfo = stream.MediaInfo;
if (mediaInfo.Name.Contains("audio", StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(mediaInfo.Name, RESOLUTION_FRAMERATE_PATTERN))
if (stream.IsAudioOnly() || Regex.IsMatch(mediaInfo.Name, RESOLUTION_FRAMERATE_PATTERN))
{
return mediaInfo.Name;
}
Expand Down Expand Up @@ -144,5 +166,20 @@ public static M3U8.Stream BestQualityStream(this M3U8 m3u8)
internal static bool IsSource(this M3U8.Stream stream)
=> stream.MediaInfo.Name.Contains("source", StringComparison.OrdinalIgnoreCase) ||
stream.MediaInfo.GroupId.Equals("chunked", StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Returns the worst quality non-audio stream from the provided M3U8.
/// </summary>
public static M3U8.Stream WorstQualityStream(this M3U8 m3u8)
{
var worstQuality = m3u8.Streams
.Where(x => !x.IsSource() && !x.IsAudioOnly())
.MinBy(x => x.StreamInfo.Resolution.Width * x.StreamInfo.Resolution.Height * x.StreamInfo.Framerate);

return worstQuality ?? m3u8.Streams.First();
}

private static bool IsAudioOnly(this M3U8.Stream stream)
=> stream.MediaInfo.Name.Contains("audio", StringComparison.OrdinalIgnoreCase);
}
}
Loading