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

Fix M3U8.ToString() being culture dependent #996

Merged
merged 2 commits into from
Mar 20, 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
66 changes: 64 additions & 2 deletions TwitchDownloaderCore.Tests/ToolTests/M3U8Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ public void CorrectlyParsesByteRange(uint start, uint length, string byteRangeSt
[InlineData("429496729500@1")]
[InlineData("1@429496729500")]
[InlineData("42949672950000")]
public void CorrectlyThrowsFormatExceptionForBadByteRangeString(string byteRangeString)
public void ThrowsFormatExceptionForBadByteRangeString(string byteRangeString)
{
Assert.Throws<FormatException>(() => M3U8.Stream.ExtByteRange.Parse(byteRangeString));
}
Expand All @@ -512,9 +512,71 @@ public void CorrectlyParsesResolution(uint start, uint length, string byteRangeS
[InlineData("429496729500x1")]
[InlineData("1x429496729500")]
[InlineData("42949672950000")]
public void CorrectlyThrowsFormatExceptionForBadResolutionString(string byteRangeString)
public void ThrowsFormatExceptionForBadResolutionString(string byteRangeString)
{
Assert.Throws<FormatException>(() => M3U8.Stream.ExtStreamInfo.StreamResolution.Parse(byteRangeString));
}


[Theory]
[InlineData("en-GB")]
[InlineData("tr-TR")]
[InlineData("ru-RU")]
public void CorrectlyStringifiesInvariantOfCulture(string culture)
{
const string EXAMPLE_M3U8 =
"#EXTM3U" +
"\n#EXT-X-TWITCH-INFO:ORIGIN=\"s3\",B=\"false\",REGION=\"NA\",USER-IP=\"255.255.255.255\",SERVING-ID=\"123abc456def789ghi012jkl345mno67\",CLUSTER=\"cloudfront_vod\",USER-COUNTRY=\"US\",MANIFEST-CLUSTER=\"cloudfront_vod\"" +
"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"chunked\",NAME=\"1080p60\",AUTOSELECT=NO,DEFAULT=NO" +
"\n#EXT-X-STREAM-INF:BANDWIDTH=5898203,CODECS=\"avc1.64002A,mp4a.40.2\",RESOLUTION=1920x1080,VIDEO=\"chunked\",FRAME-RATE=59.995" +
"\nhttps://abc123def456gh.cloudfront.net/123abc456def789ghi01_streamer42_12345678901_1234567890/chunked/index-dvr.m3u8" +
"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"720p60\",NAME=\"720p60\",AUTOSELECT=YES,DEFAULT=YES" +
"\n#EXT-X-STREAM-INF:BANDWIDTH=3443956,CODECS=\"avc1.4D0020,mp4a.40.2\",RESOLUTION=1280x720,VIDEO=\"720p60\",FRAME-RATE=59.995" +
"\nhttps://abc123def456gh.cloudfront.net/123abc456def789ghi01_streamer42_12345678901_1234567890/720p60/index-dvr.m3u8" +
"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"480p30\",NAME=\"480p\",AUTOSELECT=YES,DEFAULT=YES" +
"\n#EXT-X-STREAM-INF:BANDWIDTH=1454397,CODECS=\"avc1.4D001F,mp4a.40.2\",RESOLUTION=852x480,VIDEO=\"480p30\",FRAME-RATE=29.998" +
"\nhttps://abc123def456gh.cloudfront.net/123abc456def789ghi01_streamer42_12345678901_1234567890/480p30/index-dvr.m3u8" +
"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"audio_only\",NAME=\"Audio Only\",AUTOSELECT=NO,DEFAULT=NO" +
"\n#EXT-X-STREAM-INF:BANDWIDTH=220328,CODECS=\"mp4a.40.2\",VIDEO=\"audio_only\"" +
"\nhttps://abc123def456gh.cloudfront.net/123abc456def789ghi01_streamer42_12345678901_1234567890/audio_only/index-dvr.m3u8" +
"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"360p30\",NAME=\"360p\",AUTOSELECT=YES,DEFAULT=YES" +
"\n#EXT-X-STREAM-INF:BANDWIDTH=708016,CODECS=\"avc1.4D001E,mp4a.40.2\",RESOLUTION=640x360,VIDEO=\"360p30\",FRAME-RATE=29.998" +
"\nhttps://abc123def456gh.cloudfront.net/123abc456def789ghi01_streamer42_12345678901_1234567890/360p30/index-dvr.m3u8" +
"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"160p30\",NAME=\"160p\",AUTOSELECT=YES,DEFAULT=YES" +
"\n#EXT-X-STREAM-INF:BANDWIDTH=288409,CODECS=\"avc1.4D000C,mp4a.40.2\",RESOLUTION=284x160,VIDEO=\"160p30\",FRAME-RATE=29.998" +
"\nhttps://abc123def456gh.cloudfront.net/123abc456def789ghi01_streamer42_12345678901_1234567890/160p30/index-dvr.m3u8" +
"\n#EXT-X-VERSION:4" +
"\n#EXT-X-MEDIA-SEQUENCE:0" +
"\n#EXT-X-TARGETDURATION:2" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:07.97Z\n#EXT-X-BYTERANGE:1601196@6470396\n#EXTINF:2.000,\n500.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:09.97Z\n#EXT-X-BYTERANGE:1588224@0\n#EXTINF:2.000,\n501.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:11.97Z\n#EXT-X-BYTERANGE:1579200@1588224\n#EXTINF:2.000,\n501.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:13.97Z\n#EXT-X-BYTERANGE:1646128@3167424\n#EXTINF:2.000,\n501.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:15.97Z\n#EXT-X-BYTERANGE:1587472@4813552\n#EXTINF:2.000,\n501.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:17.97Z\n#EXT-X-BYTERANGE:1594052@6401024\n#EXTINF:2.000,\n501.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:19.97Z\n#EXT-X-BYTERANGE:1851236@0\n#EXTINF:2.000,\n502.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:21.97Z\n#EXT-X-BYTERANGE:1437448@1851236\n#EXTINF:2.000,\n502.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:23.97Z\n#EXT-X-BYTERANGE:1535960@3288684\n#EXTINF:2.000,\n502.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:25.97Z\n#EXT-X-BYTERANGE:1568672@4824644\n#EXTINF:2.000,\n502.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:27.97Z\n#EXT-X-BYTERANGE:1625824@6393316\n#EXTINF:2.000,\n502.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:29.97Z\n#EXT-X-BYTERANGE:1583524@0\n#EXTINF:2.000,\n503.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:31.97Z\n#EXT-X-BYTERANGE:1597060@1583524\n#EXTINF:2.000,\n503.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:33.97Z\n#EXT-X-BYTERANGE:1642368@3180584\n#EXTINF:2.000,\n503.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:35.97Z\n#EXT-X-BYTERANGE:1556076@4822952\n#EXTINF:2.000,\n503.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:37.97Z\n#EXT-X-BYTERANGE:1669252@6379028\n#EXTINF:2.000,\n503.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:39.97Z\n#EXT-X-BYTERANGE:1544984@0\n#EXTINF:2.000,\n504.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:41.97Z\n#EXT-X-BYTERANGE:1601384@1544984\n#EXTINF:2.000,\n504.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:43.97Z\n#EXT-X-BYTERANGE:1672260@3146368\n#EXTINF:2.000,\n504.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:45.97Z\n#EXT-X-BYTERANGE:1623192@4818628\n#EXTINF:2.000,\n504.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:47.97Z\n#EXT-X-BYTERANGE:1526748@6441820\n#EXTINF:2.000,\n504.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:49.97Z\n#EXT-X-BYTERANGE:1731668@0\n#EXTINF:2.000,\n505.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:51.97Z\n#EXT-X-BYTERANGE:1454368@1731668\n#EXTINF:2.000,\n505.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:53.97Z\n#EXT-X-BYTERANGE:1572432@3186036\n#EXTINF:2.000,\n505.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:55.97Z\n#EXT-X-BYTERANGE:1625824@4758468\n#EXTINF:2.000,\n505.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:57.97Z\n#EXT-X-BYTERANGE:1616988@6384292\n#EXTINF:2.000,\n505.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:34:59.97Z\n#EXT-X-BYTERANGE:1632028@0\n#EXTINF:2.000,\n506.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:35:01.97Z\n#EXT-X-BYTERANGE:1543668@1632028\n#EXTINF:2.000,\n506.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:35:03.97Z\n#EXT-X-BYTERANGE:1768140@3175696\n#EXTINF:2.000,\n506.ts\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:35:05.97Z\n#EXT-X-BYTERANGE:1519040@4943836\n#EXTINF:2.000,\n506.ts" +
"\n#EXT-X-PROGRAM-DATE-TIME:2023-11-16T05:35:07.97Z\n#EXT-X-BYTERANGE:1506068@6462876\n#EXTINF:2.000,\n506.ts\n#EXT-X-ENDLIST";

var m3u8 = M3U8.Parse(EXAMPLE_M3U8);

var oldCulture = CultureInfo.CurrentCulture;

CultureInfo.CurrentCulture = new CultureInfo("en-US");
var stringExpected = m3u8.ToString();
CultureInfo.CurrentCulture = new CultureInfo(culture);
var stringActual = m3u8.ToString();

CultureInfo.CurrentCulture = oldCulture;

Assert.Equal(stringExpected, stringActual);
}
}
}
66 changes: 56 additions & 10 deletions TwitchDownloaderCore/Tools/M3U8.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ public enum PlaylistType
Event
}

internal const string PLAYLIST_TYPE_VOD = "VOD";
internal const string PLAYLIST_TYPE_EVENT = "EVENT";

private const string TARGET_VERSION_KEY = "#EXT-X-VERSION:";
private const string TARGET_DURATION_KEY = "#EXT-X-TARGETDURATION:";
private const string PLAYLIST_TYPE_KEY = "#EXT-X-PLAYLIST-TYPE:";
Expand Down Expand Up @@ -236,7 +239,7 @@ public override string ToString()
if (Type != PlaylistType.Unknown)
{
sb.Append(PLAYLIST_TYPE_KEY);
sb.Append(Type.ToString().ToUpper());
sb.Append(Type.AsString());
sb.Append(itemSeparator);
}

Expand Down Expand Up @@ -292,9 +295,9 @@ private void ParseAndAppendCore(ReadOnlySpan<char> text)
{
_metadata ??= new Metadata();
var temp = text[PLAYLIST_TYPE_KEY.Length..];
if (temp.StartsWith("VOD"))
if (temp.StartsWith(PLAYLIST_TYPE_VOD))
_metadata.Type = PlaylistType.Vod;
else if (temp.StartsWith("EVENT"))
else if (temp.StartsWith(PLAYLIST_TYPE_EVENT))
_metadata.Type = PlaylistType.Event;
else
throw new FormatException($"Unable to parse PlaylistType from: {text}");
Expand Down Expand Up @@ -422,6 +425,9 @@ public enum MediaType
Audio
}

internal const string MEDIA_TYPE_VIDEO = "VIDEO";
internal const string MEDIA_TYPE_AUDIO = "AUDIO";

internal const string MEDIA_INFO_KEY = "#EXT-X-MEDIA:";

private ExtMediaInfo() { }
Expand Down Expand Up @@ -449,7 +455,7 @@ public override string ToString()
if (Type != MediaType.Unknown)
{
sb.Append("TYPE=");
sb.Append(Type.ToString().ToUpper());
sb.Append(Type.AsString());
sb.Append(keyValueSeparator);
}

Expand Down Expand Up @@ -490,9 +496,9 @@ public static ExtMediaInfo Parse(ReadOnlySpan<char> text)
if (text.StartsWith(KEY_TYPE))
{
var temp = text[KEY_TYPE.Length..];
if (temp.StartsWith("VIDEO"))
if (temp.StartsWith(MEDIA_TYPE_VIDEO))
mediaInfo.Type = MediaType.Video;
else if (temp.StartsWith("AUDIO"))
else if (temp.StartsWith(MEDIA_TYPE_AUDIO))
mediaInfo.Type = MediaType.Audio;
else
throw new FormatException($"Unable to parse MediaType from: {text}");
Expand Down Expand Up @@ -663,7 +669,22 @@ public ExtPartInfo(decimal duration, bool live)
public decimal Duration { get; private set; }
public bool Live { get; private set; }

public override string ToString() => $"{PART_INFO_KEY}{Duration},{(Live ? "live" : "")}";
public override string ToString()
{
var sb = new StringBuilder(PART_INFO_KEY);

sb.Append(Duration.ToString(CultureInfo.InvariantCulture));

// Twitch leaves a trailing comma, so we will too.
sb.Append(',');

if (Live)
{
sb.Append("live");
}

return sb.ToString();
}

public static ExtPartInfo Parse(ReadOnlySpan<char> text)
{
Expand Down Expand Up @@ -806,7 +827,7 @@ public static DateTimeOffset ParseDateTimeOffset(ReadOnlySpan<char> text, ReadOn
var temp = text[keyName.Length..];
temp = temp[..NextKeyStart(temp)];

if (DateTimeOffset.TryParse(temp, out var dateTimeOffset))
if (DateTimeOffset.TryParse(temp, null, DateTimeStyles.AssumeUniversal, out var dateTimeOffset))
return dateTimeOffset;

if (!strict)
Expand Down Expand Up @@ -854,11 +875,11 @@ public static void AppendIfNotDefault(StringBuilder sb, string keyName, decimal
return;

sb.Append(keyName);
sb.Append(value);
sb.Append(value.ToString(CultureInfo.InvariantCulture));
sb.Append(end);
}

public static void AppendIfNotDefault(StringBuilder sb, string keyName, M3U8.Stream.ExtStreamInfo.StreamResolution value, ReadOnlySpan<char> end)
public static void AppendIfNotDefault(StringBuilder sb, string keyName, Stream.ExtStreamInfo.StreamResolution value, ReadOnlySpan<char> end)
{
if (value == default)
return;
Expand All @@ -885,4 +906,29 @@ public static void AppendStringIfNotNullOrEmpty(StringBuilder sb, string keyName
}
}
}

public static class EnumExtensions
{
public static string AsString(this M3U8.Stream.ExtMediaInfo.MediaType mediaType)
{
return mediaType switch
{
M3U8.Stream.ExtMediaInfo.MediaType.Unknown => null,
M3U8.Stream.ExtMediaInfo.MediaType.Video => M3U8.Stream.ExtMediaInfo.MEDIA_TYPE_VIDEO,
M3U8.Stream.ExtMediaInfo.MediaType.Audio => M3U8.Stream.ExtMediaInfo.MEDIA_TYPE_AUDIO,
_ => throw new ArgumentOutOfRangeException(nameof(mediaType), mediaType, null)
};
}

public static string AsString(this M3U8.Metadata.PlaylistType playlistType)
{
return playlistType switch
{
M3U8.Metadata.PlaylistType.Unknown => null,
M3U8.Metadata.PlaylistType.Vod => M3U8.Metadata.PLAYLIST_TYPE_VOD,
M3U8.Metadata.PlaylistType.Event => M3U8.Metadata.PLAYLIST_TYPE_EVENT,
_ => throw new ArgumentOutOfRangeException(nameof(playlistType), playlistType, null)
};
}
}
}