diff --git a/TwitchDownloaderCore.Tests/M3U8Tests.cs b/TwitchDownloaderCore.Tests/M3U8Tests.cs index 64785bfa..9d1776f5 100644 --- a/TwitchDownloaderCore.Tests/M3U8Tests.cs +++ b/TwitchDownloaderCore.Tests/M3U8Tests.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Globalization; +using System.Text; using TwitchDownloaderCore.Tools; namespace TwitchDownloaderCore.Tests @@ -7,9 +8,11 @@ namespace TwitchDownloaderCore.Tests public class M3U8Tests { [Theory] - [InlineData(false)] - [InlineData(true)] - public void CorrectlyParsesTwitchM3U8OfTransportStreams(bool useStream) + [InlineData(false, "en-US")] + [InlineData(true, "en-US")] + [InlineData(false, "ru-RU")] + [InlineData(true, "ru-RU")] + public void CorrectlyParsesTwitchM3U8OfTransportStreams(bool useStream, string culture) { const string ExampleM3U8Twitch = "#EXTM3U" + @@ -28,6 +31,9 @@ public void CorrectlyParsesTwitchM3U8OfTransportStreams(bool useStream) "\n#EXTINF:10.000,\n40.ts\n#EXTINF:10.000,\n41.ts\n#EXTINF:10.000,\n42.ts\n#EXTINF:10.000,\n43.ts\n#EXTINF:10.000,\n44.ts\n#EXTINF:10.000,\n45.ts\n#EXTINF:10.000,\n46.ts\n#EXTINF:10.000,\n47.ts" + "\n#EXTINF:10.000,\n48.ts\n#EXTINF:10.000,\n49.ts\n#EXT-X-ENDLIST"; + var oldCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = new CultureInfo(culture); + M3U8 m3u8; if (useStream) { @@ -40,6 +46,8 @@ public void CorrectlyParsesTwitchM3U8OfTransportStreams(bool useStream) m3u8 = M3U8.Parse(ExampleM3U8Twitch); } + CultureInfo.CurrentCulture = oldCulture; + Assert.Equal(3u, m3u8.FileMetadata.Version); Assert.Equal(10u, m3u8.FileMetadata.StreamTargetDuration); Assert.Equal("2023-09-23T17:37:06", m3u8.FileMetadata.UnparsedValues.FirstOrDefault(x => x.Key == "#ID3-EQUIV-TDTG:").Value); @@ -59,9 +67,11 @@ public void CorrectlyParsesTwitchM3U8OfTransportStreams(bool useStream) } [Theory] - [InlineData(false)] - [InlineData(true)] - public void CorrectlyParsesTwitchM3U8OfLiveStreams(bool useStream) + [InlineData(false, "en-US")] + [InlineData(true, "en-US")] + [InlineData(false, "ru-RU")] + [InlineData(true, "ru-RU")] + public void CorrectlyParsesTwitchM3U8OfLiveStreams(bool useStream, string culture) { const string ExampleM3U8Twitch = "#EXTM3U" + @@ -113,6 +123,9 @@ public void CorrectlyParsesTwitchM3U8OfLiveStreams(bool useStream) (DateTimeOffset.Parse("2023-09-17T02:32:16.242Z"), 2.000m, true, "https://video-edge-foo.bar.abs.hls.ttvnw.net/v1/segment/hij-567KLM_890.ts") }; + var oldCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = new CultureInfo(culture); + M3U8 m3u8; if (useStream) { @@ -125,6 +138,8 @@ public void CorrectlyParsesTwitchM3U8OfLiveStreams(bool useStream) m3u8 = M3U8.Parse(ExampleM3U8Twitch); } + CultureInfo.CurrentCulture = oldCulture; + Assert.Equal(3u, m3u8.FileMetadata.Version); Assert.Equal(5u, m3u8.FileMetadata.StreamTargetDuration); Assert.Equal(M3U8.Metadata.PlaylistType.Unknown, m3u8.FileMetadata.Type); @@ -146,9 +161,11 @@ public void CorrectlyParsesTwitchM3U8OfLiveStreams(bool useStream) } [Theory] - [InlineData(false)] - [InlineData(true)] - public void CorrectlyParsesTwitchM3U8OfPlaylists(bool useStream) + [InlineData(false, "en-US")] + [InlineData(true, "en-US")] + [InlineData(false, "ru-RU")] + [InlineData(true, "ru-RU")] + public void CorrectlyParsesTwitchM3U8OfPlaylists(bool useStream, string culture) { const string ExampleM3U8Twitch = "#EXTM3U" + @@ -194,6 +211,9 @@ public void CorrectlyParsesTwitchM3U8OfPlaylists(bool useStream) "https://abc123def456gh.cloudfront.net/123abc456def789ghi01_streamer42_12345678901_1234567890/160p30/index-dvr.m3u8") }; + var oldCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = new CultureInfo(culture); + M3U8 m3u8; if (useStream) { @@ -206,6 +226,8 @@ public void CorrectlyParsesTwitchM3U8OfPlaylists(bool useStream) m3u8 = M3U8.Parse(ExampleM3U8Twitch); } + CultureInfo.CurrentCulture = oldCulture; + Assert.Equal(streams.Length, m3u8.Streams.Length); Assert.Equivalent(streams[0], m3u8.Streams[0], true); Assert.Equivalent(streams[1], m3u8.Streams[1], true); @@ -252,9 +274,11 @@ public void CorrectlyParsesTwitchM3U8StreamInfo(string streamInfoString, int ban } [Theory] - [InlineData(false)] - [InlineData(true)] - public void CorrectlyParsesKickM3U8OfTransportStreams(bool useStream) + [InlineData(false, "en-US")] + [InlineData(true, "en-US")] + [InlineData(false, "ru-RU")] + [InlineData(true, "ru-RU")] + public void CorrectlyParsesKickM3U8OfTransportStreams(bool useStream, string culture) { const string ExampleM3U8Kick = "#EXTM3U" + @@ -313,6 +337,9 @@ public void CorrectlyParsesKickM3U8OfTransportStreams(bool useStream) (DateTimeOffset.Parse("2023-11-16T05:35:07.97Z"), (1506068, 6462876), "506.ts") }; + var oldCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = new CultureInfo(culture); + M3U8 m3u8; if (useStream) { @@ -325,6 +352,8 @@ public void CorrectlyParsesKickM3U8OfTransportStreams(bool useStream) m3u8 = M3U8.Parse(ExampleM3U8Kick); } + CultureInfo.CurrentCulture = oldCulture; + Assert.Equal(4u, m3u8.FileMetadata.Version); Assert.Equal(2u, m3u8.FileMetadata.StreamTargetDuration); Assert.Equal(M3U8.Metadata.PlaylistType.Unknown, m3u8.FileMetadata.Type); @@ -343,9 +372,11 @@ public void CorrectlyParsesKickM3U8OfTransportStreams(bool useStream) } [Theory] - [InlineData(false)] - [InlineData(true)] - public void CorrectlyParsesKickM3U8OfPlaylists(bool useStream) + [InlineData(false, "en-US")] + [InlineData(true, "en-US")] + [InlineData(false, "ru-RU")] + [InlineData(true, "ru-RU")] + public void CorrectlyParsesKickM3U8OfPlaylists(bool useStream, string culture) { const string ExampleM3U8Kick = "#EXTM3U" + @@ -386,6 +417,9 @@ public void CorrectlyParsesKickM3U8OfPlaylists(bool useStream) "160p30/playlist.m3u8") }; + var oldCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = new CultureInfo(culture); + M3U8 m3u8; if (useStream) { @@ -398,6 +432,8 @@ public void CorrectlyParsesKickM3U8OfPlaylists(bool useStream) m3u8 = M3U8.Parse(ExampleM3U8Kick); } + CultureInfo.CurrentCulture = oldCulture; + Assert.Equal(streams.Length, m3u8.Streams.Length); Assert.Equivalent(streams[0], m3u8.Streams[0], true); Assert.Equivalent(streams[1], m3u8.Streams[1], true); diff --git a/TwitchDownloaderCore/Tools/M3U8.cs b/TwitchDownloaderCore/Tools/M3U8.cs index ca8c505f..900b5470 100644 --- a/TwitchDownloaderCore/Tools/M3U8.cs +++ b/TwitchDownloaderCore/Tools/M3U8.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Text; using System.Text.RegularExpressions; @@ -149,7 +151,7 @@ private static bool ParseM3U8Key(ReadOnlySpan text, Metadata.Builder metad } else if (text.StartsWith(PROGRAM_DATE_TIME_KEY)) { - extProgramDateTime = ParsingHelpers.ParseDateTimeOffset(text, PROGRAM_DATE_TIME_KEY); + extProgramDateTime = ParsingHelpers.ParseDateTimeOffset(text, PROGRAM_DATE_TIME_KEY, false); } else if (text.StartsWith(Stream.ExtByteRange.BYTE_RANGE_KEY)) { @@ -339,10 +341,10 @@ public static ExtByteRange Parse(ReadOnlySpan text) if (separatorIndex == -1) throw new FormatException($"Unable to parse ByteRange from {text}."); - if (!uint.TryParse(text[..separatorIndex], out var start)) + if (!uint.TryParse(text[..separatorIndex], NumberStyles.Integer, CultureInfo.InvariantCulture, out var start)) throw new FormatException($"Unable to parse ByteRange from {text}."); - if (!uint.TryParse(text[(separatorIndex + 1)..], out var end)) + if (!uint.TryParse(text[(separatorIndex + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out var end)) throw new FormatException($"Unable to parse ByteRange from {text}."); return new ExtByteRange(start, end); @@ -488,10 +490,10 @@ public static StreamResolution Parse(ReadOnlySpan text) if (separatorIndex == -1 || separatorIndex == text.Length) throw new FormatException($"Unable to parse Resolution from {text}."); - if (!uint.TryParse(text[..separatorIndex], out var width)) + if (!uint.TryParse(text[..separatorIndex], NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)) throw new FormatException($"Unable to parse Resolution from {text}."); - if (!uint.TryParse(text[(separatorIndex + 1)..], out var height)) + if (!uint.TryParse(text[(separatorIndex + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) throw new FormatException($"Unable to parse Resolution from {text}."); return new StreamResolution(width, height); @@ -588,11 +590,11 @@ public static ExtStreamInfo Parse(ReadOnlySpan text) if (text.StartsWith(KEY_PROGRAM_ID)) { - streamInfo.ProgramId = ParsingHelpers.ParseIntValue(text, KEY_PROGRAM_ID); + streamInfo.ProgramId = ParsingHelpers.ParseIntValue(text, KEY_PROGRAM_ID, false); } else if (text.StartsWith(KEY_BANDWIDTH)) { - streamInfo.Bandwidth = ParsingHelpers.ParseIntValue(text, KEY_BANDWIDTH); + streamInfo.Bandwidth = ParsingHelpers.ParseIntValue(text, KEY_BANDWIDTH, false); } else if (text.StartsWith(KEY_CODECS)) { @@ -608,7 +610,7 @@ public static ExtStreamInfo Parse(ReadOnlySpan text) } else if (text.StartsWith(KEY_FRAMERATE)) { - streamInfo.Framerate = ParsingHelpers.ParseDecimalValue(text, KEY_FRAMERATE); + streamInfo.Framerate = ParsingHelpers.ParseDecimalValue(text, KEY_FRAMERATE, false); } var nextIndex = text.UnEscapedIndexOf(','); @@ -711,40 +713,49 @@ public static string ParseStringValue(ReadOnlySpan text, ReadOnlySpan text, ReadOnlySpan keyName) + public static int ParseIntValue(ReadOnlySpan text, ReadOnlySpan keyName, bool strict = true) { var temp = text[keyName.Length..]; temp = temp[..NextKeyStart(temp)]; - if (int.TryParse(temp, out var intValue)) + if (int.TryParse(temp, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) return intValue; + if (!strict) + return default; + throw new FormatException($"Unable to parse integer from: {text}"); } - public static uint ParseUIntValue(ReadOnlySpan text, ReadOnlySpan keyName) + public static uint ParseUIntValue(ReadOnlySpan text, ReadOnlySpan keyName, bool strict = true) { var temp = text[keyName.Length..]; temp = temp[..NextKeyStart(temp)]; - if (uint.TryParse(temp, out var uIntValue)) + if (uint.TryParse(temp, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uIntValue)) return uIntValue; + if (!strict) + return default; + throw new FormatException($"Unable to parse integer from: {text}"); } - public static decimal ParseDecimalValue(ReadOnlySpan text, ReadOnlySpan keyName) + public static decimal ParseDecimalValue(ReadOnlySpan text, ReadOnlySpan keyName, bool strict = true) { var temp = text[keyName.Length..]; temp = temp[..NextKeyStart(temp)]; - if (decimal.TryParse(temp, out var decimalValue)) + if (decimal.TryParse(temp, NumberStyles.Number, CultureInfo.InvariantCulture, out var decimalValue)) return decimalValue; + if (!strict) + return default; + throw new FormatException($"Unable to parse decimal from: {text}"); } - public static bool ParseBooleanValue(ReadOnlySpan text, ReadOnlySpan keyName) + public static bool ParseBooleanValue(ReadOnlySpan text, ReadOnlySpan keyName, bool strict = true) { var temp = text[keyName.Length..]; @@ -759,6 +770,9 @@ public static bool ParseBooleanValue(ReadOnlySpan text, ReadOnlySpan if (bool.TryParse(temp, out var booleanValue)) return booleanValue; + if (!strict) + return default; + throw new FormatException($"Unable to parse boolean from: {text}"); } @@ -770,7 +784,7 @@ public static Stream.ExtStreamInfo.StreamResolution ParseResolution(ReadOnlySpan return Stream.ExtStreamInfo.StreamResolution.Parse(temp); } - public static DateTimeOffset ParseDateTimeOffset(ReadOnlySpan text, ReadOnlySpan keyName) + public static DateTimeOffset ParseDateTimeOffset(ReadOnlySpan text, ReadOnlySpan keyName, bool strict = true) { var temp = text[keyName.Length..]; temp = temp[..NextKeyStart(temp)]; @@ -778,6 +792,9 @@ public static DateTimeOffset ParseDateTimeOffset(ReadOnlySpan text, ReadOn if (DateTimeOffset.TryParse(temp, out var dateTimeOffset)) return dateTimeOffset; + if (!strict) + return default; + throw new FormatException($"Unable to parse DateTimeOffset from: {text}"); }