diff --git a/TwitchDownloaderCore.Tests/ExtensionTests/M3U8ExtensionTests.cs b/TwitchDownloaderCore.Tests/ExtensionTests/M3U8ExtensionTests.cs
index 79c6d00d..6f93149b 100644
--- a/TwitchDownloaderCore.Tests/ExtensionTests/M3U8ExtensionTests.cs
+++ b/TwitchDownloaderCore.Tests/ExtensionTests/M3U8ExtensionTests.cs
@@ -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[]
@@ -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[]
@@ -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[]
diff --git a/TwitchDownloaderCore/Extensions/M3U8Extensions.cs b/TwitchDownloaderCore/Extensions/M3U8Extensions.cs
index afb00d7a..3ef590fb 100644
--- a/TwitchDownloaderCore/Extensions/M3U8Extensions.cs
+++ b/TwitchDownloaderCore/Extensions/M3U8Extensions.cs
@@ -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;
}
@@ -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)
@@ -83,8 +72,41 @@ 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;
+ }
+
///
- /// A representing the 's
+ /// A representing the 's
/// and in the format of "{resolution}p{framerate}" or
///
public static string GetResolutionFramerateString(this M3U8.Stream stream)
@@ -92,7 +114,7 @@ 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;
}
@@ -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);
+
+ ///
+ /// Returns the worst quality non-audio stream from the provided M3U8.
+ ///
+ 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);
}
}
\ No newline at end of file