Update MkvInfoReader, Add MkvNode extensions

This commit is contained in:
2022-03-17 13:32:20 +01:00
parent 09d1a52aed
commit df86aee35f
9 changed files with 142 additions and 28 deletions

View File

@@ -0,0 +1,121 @@
using System.Runtime.CompilerServices;
namespace MkvPropEditWrapper.MkvInfo.Reader;
public class MkvInfoReader
{
public const char LineDelimiter = '\n';
private readonly ReadOnlyMemory<char> _memory;
private ReadOnlyMemory<char> _remainingMemory;
private readonly MkvNodeRoot _root;
private MkvNode _currentNode;
private int _currentPosition;
public MkvInfoReader(string mkvInfoOutput)
{
_memory = mkvInfoOutput.AsMemory();
_remainingMemory = _memory;
_root = new MkvNodeRoot();
_currentNode = _root;
_currentPosition = 0;
}
public MkvNodeRoot Read()
{
if (_root.Children.Count > 0)
{
return _root;
}
while (_currentPosition < _memory.Length)
{
var line = ReadLine();
// Skip empty lines
if (line.Length < 1)
{
continue;
}
ProcessLine(line);
}
return _root;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessLine(ReadOnlyMemory<char> line)
{
var lineSpan = line.Span;
// Find depth
int depth = lineSpan.IndexOf('+');
// Climb up the node tree
while (depth <= _currentNode.Depth)
{
_currentNode = _currentNode.Parent!;
}
// Node content
var content = line[(depth + 2)..];
var contentSpan = content.Span;
// Check for property
int propertyIndex = contentSpan.IndexOf(':');
MkvNode node;
// Property node
if (propertyIndex != -1)
{
var name = content[..propertyIndex];
var value = content[(propertyIndex + 2)..];
node = new MkvProperty(_currentNode, name, value);
}
// Generic node
else
{
node = new MkvNode(_currentNode, content);
}
_currentNode.Children.Add(node);
_currentNode = node;
}
/// <summary>
/// Read in the span till next \n or EOF while incrementing _currentPosition
/// </summary>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ReadOnlyMemory<char> ReadLine()
{
var remainingSpan = _remainingMemory.Span;
ReadOnlyMemory<char> line;
int index = remainingSpan.IndexOf(LineDelimiter);
// Reach the end
if (index == -1)
{
line = _remainingMemory;
_currentPosition = _memory.Length;
}
else
{
line = _remainingMemory[..(index - 1)]; // -1 to skip \r
_currentPosition += index + 1; // Skip \n
}
// Move forward in the span
_remainingMemory = _memory[_currentPosition..];
return line;
}
}

View File

@@ -0,0 +1,80 @@
using System.Collections;
using System.Text;
namespace MkvPropEditWrapper.MkvInfo.Reader;
public class MkvNode : IEnumerable<MkvNode>
{
private static class KnowNodeTypes
{
public const string Head = "EBML head";
public const string Segment = "Segment";
public const string Chapter = "Chapters";
public const string Tag = "Tags";
public const string Track = "Tracks";
public const string Attachment = "Attachments";
}
public MkvNode? Parent { get; }
public IList<MkvNode> Children { get; }
public virtual NodeType NodeType { get; }
public virtual int Depth { get; }
public ReadOnlyMemory<char> Name { get; }
public MkvNode(MkvNode? parent, ReadOnlyMemory<char> name)
{
Parent = parent;
Name = name;
Depth = parent?.Depth + 1 ?? 0;
NodeType = parent is null ? NodeType.Root : FindNodeType();
Children = new List<MkvNode>();
}
private NodeType FindNodeType()
{
var span = Name.Span;
return Depth switch
{
0 when span.SequenceEqual(KnowNodeTypes.Head) => NodeType.Head,
0 when span.StartsWith(KnowNodeTypes.Segment) => NodeType.Segment,
1 when span.SequenceEqual(KnowNodeTypes.Chapter) => NodeType.Chapter,
1 when span.SequenceEqual(KnowNodeTypes.Tag) => NodeType.Tag,
1 when span.SequenceEqual(KnowNodeTypes.Track) => NodeType.Track,
1 when span.SequenceEqual(KnowNodeTypes.Attachment) => NodeType.Attachment,
1 => NodeType.Unknown,
_ => Parent!.NodeType
};
}
protected virtual void AppendSelf(StringBuilder builder) => builder.AppendLine($"{new string('-', Depth)}> [{NodeType}] {Name}");
public IEnumerator<MkvNode> GetEnumerator()
{
return Children.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public override string ToString()
{
var builder = new StringBuilder();
AppendSelf(builder);
foreach (var mkvNode in Children)
{
builder.Append(mkvNode);
}
return builder.ToString();
}
}

View File

@@ -0,0 +1,19 @@
using System.Text;
namespace MkvPropEditWrapper.MkvInfo.Reader;
public class MkvNodeRoot : MkvNode
{
public override NodeType NodeType => NodeType.Root;
public override int Depth => -1;
public MkvNodeRoot()
: base(null, "Root".AsMemory())
{
}
protected override void AppendSelf(StringBuilder builder)
{
}
}

View File

@@ -0,0 +1,64 @@
using System.Text;
namespace MkvPropEditWrapper.MkvInfo.Reader;
public class MkvProperty : MkvNode
{
private static class KnownProperties
{
public const string TrackType = "Track type";
public const string TrackNumber = "Track number";
public const string FlagDefault = "\"Default track\" flag";
public const string FlagForced = "\"Forced display\" flag";
public const string Language = "Language";
}
public override NodeType NodeType => NodeType.Property;
public PropertyType PropertyType { get; }
public ReadOnlyMemory<char> Value { get; }
public MkvProperty(MkvNode? parent, ReadOnlyMemory<char> name, ReadOnlyMemory<char> value)
: base(parent, name)
{
Value = value;
PropertyType = FindPropertyType(name);
}
private static PropertyType FindPropertyType(ReadOnlyMemory<char> name)
{
var span = name.Span;
if (span.SequenceEqual(KnownProperties.TrackType))
{
return PropertyType.TrackType;
}
if (span.SequenceEqual(KnownProperties.TrackNumber))
{
return PropertyType.TrackNumber;
}
if (span.SequenceEqual(KnownProperties.FlagDefault))
{
return PropertyType.FlagDefault;
}
if (span.SequenceEqual(KnownProperties.FlagForced))
{
return PropertyType.FlagForced;
}
if (span.SequenceEqual(KnownProperties.Language))
{
return PropertyType.Language;
}
return PropertyType.Unknown;
}
protected override void AppendSelf(StringBuilder builder)
{
builder.AppendLine($"{new string('-', Depth)}> [{NodeType}] {Name}: {Value}");
}
}

View File

@@ -0,0 +1,14 @@
namespace MkvPropEditWrapper.MkvInfo.Reader;
public enum NodeType
{
Unknown,
Property,
Root,
Head,
Segment,
Track,
Tag,
Attachment,
Chapter
}

View File

@@ -0,0 +1,107 @@
using MkvPropEditWrapper.Shared;
namespace MkvPropEditWrapper.MkvInfo.Reader;
public static class NodeUtils
{
public static IEnumerable<MkvNode> Descendants(this MkvNode root)
{
var nodes = new Stack<MkvNode>(new[] {root});
while (nodes.Any())
{
var node = nodes.Pop();
yield return node;
foreach (var newNode in node.Children)
{
nodes.Push(newNode);
}
}
}
/// <summary>
/// Generate track info from parsed output, only works on the first root node as it uses absolute depth
/// </summary>
/// <param name="root">Node root</param>
/// <returns>All parseable <see cref="TrackInfo"/></returns>
public static IEnumerable<TrackInfo> GetTrackInfos(this MkvNodeRoot root)
{
TrackInfo? ParseTrackInfo(MkvNode track)
{
ReadOnlySpan<char> audioType = "audio";
ReadOnlySpan<char> subtitleType = "subtitles";
int number = -1;
var trackType = TrackType.Unknown;
string language = "Unknown";
bool defaultFlag = default;
bool forcedFlag = default;
foreach (var child in track.Children)
{
if (child is MkvProperty property)
{
ReadOnlySpan<char> span;
switch (property.PropertyType)
{
case PropertyType.TrackType:
span = property.Value.Span;
if (span.SequenceEqual(audioType))
{
trackType = TrackType.Audio;
}
else if (span.SequenceEqual(subtitleType))
{
trackType = TrackType.Subtitles;
}
else
{
return null;
}
break;
case PropertyType.TrackNumber:
span = property.Value.Span;
number = int.Parse(span[..span.IndexOf(' ')]);
break;
case PropertyType.Language:
language = property.Value.ToString();
break;
case PropertyType.FlagDefault:
defaultFlag = property.Value.Span[0] == '1';
break;
case PropertyType.FlagForced:
forcedFlag = property.Value.Span[0] == '1';
break;
}
}
}
return number == -1 ? null : new TrackInfo(number, trackType, language, defaultFlag, forcedFlag);
}
var tracks = root.Descendants().Where(n => n is { Depth: 2, NodeType: NodeType.Track });
foreach (var track in tracks)
{
var trackInfo = ParseTrackInfo(track);
if (trackInfo is not null)
{
yield return trackInfo;
}
}
}
public static (IEnumerable<TrackInfo> audio, IEnumerable<TrackInfo> subtitles) SplitByType(this IEnumerable<TrackInfo> trackInfos)
{
var lookup = trackInfos.OrderBy(t => t.Number).ToLookup(t => t.TrackType);
return (lookup[TrackType.Audio], lookup[TrackType.Subtitles]);
}
}

View File

@@ -0,0 +1,11 @@
namespace MkvPropEditWrapper.MkvInfo.Reader;
public enum PropertyType
{
TrackType,
TrackNumber,
Unknown,
FlagDefault,
FlagForced,
Language
}