Skip to content

Commit

Permalink
Fully support converting parsed M3U8 playlists into strings
Browse files Browse the repository at this point in the history
  • Loading branch information
ScrubN committed Jan 13, 2024
1 parent c600d48 commit cdd8ef4
Showing 1 changed file with 145 additions and 68 deletions.
213 changes: 145 additions & 68 deletions TwitchDownloaderCore/Tools/M3U8.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,27 @@ namespace TwitchDownloaderCore.Tools
// ReSharper disable StringLiteralTypo
public sealed record M3U8(M3U8.Metadata FileMetadata, M3U8.Stream[] Streams)
{
public override string ToString()
{
var sb = new StringBuilder();

sb.AppendLine("#EXTM3U");

if (FileMetadata?.ToString() is { Length: > 0} metadataString)
{
sb.AppendLine(metadataString);
}

foreach (var stream in Streams)
{
sb.AppendLine(stream.ToString());
}

sb.Append("#EXT-X-ENDLIST");

return sb.ToString();
}

public static M3U8 Parse(System.IO.Stream stream, Encoding streamEncoding, string basePath = "")
{
var sr = new StreamReader(stream, streamEncoding);
Expand Down Expand Up @@ -91,7 +112,7 @@ public static M3U8 Parse(ReadOnlySpan<char> text, string basePath = "")
var workingSlice = text[textStart..];
lineEnd = workingSlice.IndexOf('\n');
if (lineEnd != -1)
workingSlice = workingSlice[..lineEnd];
workingSlice = workingSlice[..lineEnd].TrimEnd('\r');

if (workingSlice.IsWhiteSpace())
{
Expand Down Expand Up @@ -181,6 +202,15 @@ public enum PlaylistType
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:";
private const string MEDIA_SEQUENCE_KEY = "#EXT-X-MEDIA-SEQUENCE:";
private const string TWITCH_LIVE_SEQUENCE_KEY = "#EXT-X-TWITCH-LIVE-SEQUENCE:";
private const string TWITCH_ELAPSED_SECS_KEY = "#EXT-X-TWITCH-ELAPSED-SECS:";
private const string TWITCH_TOTAL_SECS_KEY = "#EXT-X-TWITCH-TOTAL-SECS:";
private const string TWITCH_INFO_KEY = "#EXT-X-TWITCH-INFO:";

// Generic M3U headers
public uint Version { get; private set; }
public uint StreamTargetDuration { get; private set; }
Expand All @@ -196,6 +226,40 @@ public enum PlaylistType
private readonly List<KeyValuePair<string, string>> _unparsedValues = new();
public IReadOnlyList<KeyValuePair<string, string>> UnparsedValues => _unparsedValues;

public override string ToString()
{
var sb = new StringBuilder();
var itemSeparator = Environment.NewLine;

StringBuilderHelpers.AppendIfNotDefault(sb, TARGET_VERSION_KEY, Version, itemSeparator);
StringBuilderHelpers.AppendIfNotDefault(sb, TARGET_DURATION_KEY, StreamTargetDuration, itemSeparator);
if (Type != PlaylistType.Unknown)
{
sb.Append(PLAYLIST_TYPE_KEY);
sb.Append(Type.ToString().ToUpper());
sb.Append(itemSeparator);
}

StringBuilderHelpers.AppendIfNotDefault(sb, MEDIA_SEQUENCE_KEY, MediaSequence, itemSeparator);
StringBuilderHelpers.AppendIfNotDefault(sb, TWITCH_LIVE_SEQUENCE_KEY, TwitchLiveSequence, itemSeparator);
StringBuilderHelpers.AppendIfNotDefault(sb, TWITCH_ELAPSED_SECS_KEY, TwitchElapsedSeconds, itemSeparator);
StringBuilderHelpers.AppendIfNotDefault(sb, TWITCH_TOTAL_SECS_KEY, TwitchTotalSeconds, itemSeparator);

foreach (var (key, value) in _unparsedValues)
{
sb.Append(key);
sb.Append(value);
sb.Append(itemSeparator);
}

if (sb.Length == 0)
{
return "";
}

return sb.ToString().AsSpan().TrimEnd(itemSeparator).ToString();
}

public sealed class Builder
{
private Metadata _metadata;
Expand All @@ -214,26 +278,19 @@ public Builder ParseAndAppend(ReadOnlySpan<char> text)

private void ParseAndAppendCore(ReadOnlySpan<char> text)
{
_metadata ??= new Metadata();

const string TARGET_VERSION_KEY = "#EXT-X-VERSION:";
const string TARGET_DURATION_KEY = "#EXT-X-TARGETDURATION:";
const string PLAYLIST_TYPE_KEY = "#EXT-X-PLAYLIST-TYPE:";
const string MEDIA_SEQUENCE_KEY = "#EXT-X-MEDIA-SEQUENCE:";
const string TWITCH_LIVE_SEQUENCE_KEY = "#EXT-X-TWITCH-LIVE-SEQUENCE:";
const string TWITCH_ELAPSED_SECS_KEY = "#EXT-X-TWITCH-ELAPSED-SECS:";
const string TWITCH_TOTAL_SECS_KEY = "#EXT-X-TWITCH-TOTAL-SECS:";
const string TWITCH_INFO_KEY = "#EXT-X-TWITCH-INFO:";
if (text.StartsWith(TARGET_VERSION_KEY))
{
_metadata ??= new Metadata();
_metadata.Version = ParsingHelpers.ParseUIntValue(text, TARGET_VERSION_KEY);
}
else if (text.StartsWith(TARGET_DURATION_KEY))
{
_metadata ??= new Metadata();
_metadata.StreamTargetDuration = ParsingHelpers.ParseUIntValue(text, TARGET_DURATION_KEY);
}
else if (text.StartsWith(PLAYLIST_TYPE_KEY))
{
_metadata ??= new Metadata();
var temp = text[PLAYLIST_TYPE_KEY.Length..];
if (temp.StartsWith("VOD"))
_metadata.Type = PlaylistType.Vod;
Expand All @@ -244,18 +301,22 @@ private void ParseAndAppendCore(ReadOnlySpan<char> text)
}
else if (text.StartsWith(MEDIA_SEQUENCE_KEY))
{
_metadata ??= new Metadata();
_metadata.MediaSequence = ParsingHelpers.ParseUIntValue(text, MEDIA_SEQUENCE_KEY);
}
else if (text.StartsWith(TWITCH_LIVE_SEQUENCE_KEY))
{
_metadata ??= new Metadata();
_metadata.TwitchLiveSequence = ParsingHelpers.ParseUIntValue(text, TWITCH_LIVE_SEQUENCE_KEY);
}
else if (text.StartsWith(TWITCH_ELAPSED_SECS_KEY))
{
_metadata ??= new Metadata();
_metadata.TwitchElapsedSeconds = ParsingHelpers.ParseDecimalValue(text, TWITCH_ELAPSED_SECS_KEY);
}
else if (text.StartsWith(TWITCH_TOTAL_SECS_KEY))
{
_metadata ??= new Metadata();
_metadata.TwitchTotalSeconds = ParsingHelpers.ParseDecimalValue(text, TWITCH_TOTAL_SECS_KEY);
}
else if (text.StartsWith(TWITCH_INFO_KEY))
Expand All @@ -264,6 +325,7 @@ private void ParseAndAppendCore(ReadOnlySpan<char> text)
}
else if (text[0] == '#')
{
_metadata ??= new Metadata();
var colonIndex = text.IndexOf(':');
if (colonIndex != -1)
{
Expand Down Expand Up @@ -316,12 +378,11 @@ public override string ToString()
sb.AppendLine(ByteRange.ToString());

if (!string.IsNullOrEmpty(Path))
sb.AppendLine(Path);
sb.Append(Path);

if (sb.Length == 0)
return "";

sb.Append("#EXT-X-ENDLIST");
return sb.ToString();
}

Expand Down Expand Up @@ -383,31 +444,21 @@ public ExtMediaInfo(MediaType type, string groupId, string name, bool autoSelect
public override string ToString()
{
var sb = new StringBuilder(MEDIA_INFO_KEY);
ReadOnlySpan<char> keyValueSeparator = stackalloc char[] { ',' };

if (Type != MediaType.Unknown)
{
sb.Append("TYPE=");
sb.Append(Type.ToString().ToUpper());
sb.Append(",");
sb.Append(keyValueSeparator);
}

if (GroupId != null)
{
sb.Append("GROUP-ID=\"");
sb.Append(GroupId);
sb.Append("\",");
}

if (Name != null)
{
sb.Append("NAME=\"");
sb.Append(Name);
sb.Append("\",");
}
StringBuilderHelpers.AppendStringIfNotNullOrEmpty(sb, "GROUP-ID=", GroupId, keyValueSeparator);
StringBuilderHelpers.AppendStringIfNotNullOrEmpty(sb, "NAME=", Name, keyValueSeparator);

sb.Append("AUTOSELECT=");
sb.Append(BooleanToWord(AutoSelect));
sb.Append(",");
sb.Append(keyValueSeparator);

sb.Append("DEFAULT=");
sb.Append(BooleanToWord(Default));
Expand Down Expand Up @@ -525,47 +576,14 @@ public ExtStreamInfo(int programId, int bandwidth, string codecs, StreamResoluti
public override string ToString()
{
var sb = new StringBuilder(STREAM_INFO_KEY);
ReadOnlySpan<char> keyValueSeparator = stackalloc char[] { ',' };

if (ProgramId != default)
{
sb.Append("PROGRAM-ID=");
sb.Append(ProgramId);
sb.Append(",");
}

if (Bandwidth != default)
{
sb.Append("BANDWIDTH=");
sb.Append(Bandwidth);
sb.Append(",");
}

if (Codecs != null)
{
sb.Append("CODECS=\"");
sb.Append(Codecs);
sb.Append("\",");
}

if (Resolution != default)
{
sb.Append("RESOLUTION=");
sb.Append(Resolution.ToString());
sb.Append(",");
}

if (Video != null)
{
sb.Append("VIDEO=\"");
sb.Append(Video);
sb.Append("\",");
}

if (Framerate != default)
{
sb.Append("FRAME-RATE=");
sb.Append(Framerate);
}
StringBuilderHelpers.AppendIfNotDefault(sb, "PROGRAM-ID=", ProgramId, keyValueSeparator);
StringBuilderHelpers.AppendIfNotDefault(sb, "BANDWIDTH=", Bandwidth, keyValueSeparator);
StringBuilderHelpers.AppendStringIfNotNullOrEmpty(sb, "CODECS=", Codecs, keyValueSeparator);
StringBuilderHelpers.AppendIfNotDefault(sb, "RESOLUTION=", Resolution, keyValueSeparator);
StringBuilderHelpers.AppendStringIfNotNullOrEmpty(sb, "VIDEO=", Video, keyValueSeparator);
StringBuilderHelpers.AppendIfNotDefault(sb, "FRAME-RATE=", Framerate, default);

return sb.ToString();
}
Expand Down Expand Up @@ -807,5 +825,64 @@ private static Index NextKeyStart(ReadOnlySpan<char> text)
};
}
}

private static class StringBuilderHelpers
{
public static void AppendIfNotDefault(StringBuilder sb, string keyName, uint value, ReadOnlySpan<char> end)
{
if (value == default)
return;

sb.Append(keyName);
sb.Append(value);
sb.Append(end);
}

public static void AppendIfNotDefault(StringBuilder sb, string keyName, int value, ReadOnlySpan<char> end)
{
if (value == default)
return;

sb.Append(keyName);
sb.Append(value);
sb.Append(end);
}

public static void AppendIfNotDefault(StringBuilder sb, string keyName, decimal value, ReadOnlySpan<char> end)
{
if (value == default)
return;

sb.Append(keyName);
sb.Append(value);
sb.Append(end);
}

public static void AppendIfNotDefault(StringBuilder sb, string keyName, M3U8.Stream.ExtStreamInfo.StreamResolution value, ReadOnlySpan<char> end)
{
if (value == default)
return;

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

public static void AppendStringIfNotNullOrEmpty(StringBuilder sb, string keyName, string value, ReadOnlySpan<char> end)
{
if (string.IsNullOrEmpty(value))
return;

sb.Append(keyName);

if (!keyName.EndsWith('"'))
{
sb.Append('"');
}
sb.Append(value);
sb.Append('"');
sb.Append(end);
}
}
}
}

0 comments on commit cdd8ef4

Please sign in to comment.