Skip to content

Commit

Permalink
Support best and worst keywords in CLI video downloader (#998)
Browse files Browse the repository at this point in the history
  • Loading branch information
ScrubN authored Mar 21, 2024
1 parent 7cbf5c0 commit 9f1b827
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 20 deletions.
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);
}
}

0 comments on commit 9f1b827

Please sign in to comment.