Stream custom live channels using your own media
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

166 lines
5.5 KiB

using System.Globalization;
using System.Text.Json;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using YamlDotNet.RepresentationModel;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace ErsatzTV.Infrastructure.Scheduling;
public class YamlScheduleValidator(ILogger<YamlScheduleValidator> logger) : IYamlScheduleValidator
{
public async Task<bool> ValidateSchedule(string yaml, bool isImport)
{
try
{
string schemaFileName = Path.Combine(FileSystemLayout.ResourcesCacheFolder,
isImport ? "yaml-playout-import.schema.json" : "yaml-playout.schema.json");
using StreamReader sr = File.OpenText(schemaFileName);
await using var reader = new JsonTextReader(sr);
var schema = JSchema.Load(reader);
using var textReader = new StringReader(yaml);
var yamlStream = new YamlStream();
yamlStream.Load(textReader);
var schedule = JObject.Parse(Convert(yamlStream));
if (!schedule.IsValid(schema, out IList<string> errorMessages))
{
logger.LogWarning("Failed to validate YAML schedule definition: {ErrorMessages}", errorMessages);
return false;
}
return true;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Unexpected error while validating YAML schedule definition");
}
return false;
}
public string ToJson(string yaml)
{
using var textReader = new StringReader(yaml);
var yamlStream = new YamlStream();
yamlStream.Load(textReader);
var schedule = JObject.Parse(Convert(yamlStream));
var formatted = JsonConvert.SerializeObject(schedule, Formatting.Indented);
var lines = formatted.Split('\n');
return string.Join('\n', lines.Select((line, index) => $"{index + 1,4}: {line}"));
}
public async Task<IList<string>> GetValidationMessages(string yaml, bool isImport)
{
try
{
string schemaFileName = Path.Combine(FileSystemLayout.ResourcesCacheFolder,
isImport ? "yaml-playout-import.schema.json" : "yaml-playout.schema.json");
using StreamReader sr = File.OpenText(schemaFileName);
await using var reader = new JsonTextReader(sr);
var schema = JSchema.Load(reader);
using var textReader = new StringReader(yaml);
var yamlStream = new YamlStream();
yamlStream.Load(textReader);
var schedule = JObject.Parse(Convert(yamlStream));
return schedule.IsValid(schema, out IList<string> errorMessages) ? [] : errorMessages;
}
catch (Exception ex)
{
return [ex.Message];
}
}
private static string Convert(YamlStream yamlStream)
{
var visitor = new YamlToJsonVisitor();
yamlStream.Accept(visitor);
return JsonConvert.SerializeObject(JsonConvert.DeserializeObject(visitor.JsonString), Formatting.Indented);
}
private sealed class YamlToJsonVisitor : IYamlVisitor
{
private readonly JsonSerializerOptions _options = new() { WriteIndented = false, };
private object _currentValue;
public string JsonString => JsonSerializer.Serialize(_currentValue, _options);
public void Visit(YamlScalarNode scalar)
{
var value = scalar.Value;
if (string.IsNullOrEmpty(value))
{
_currentValue = null;
return;
}
// Try to parse in order of most specific to most general
if (value.Equals("true", StringComparison.OrdinalIgnoreCase))
{
_currentValue = true;
return;
}
if (value.Equals("false", StringComparison.OrdinalIgnoreCase))
{
_currentValue = false;
return;
}
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intResult))
{
_currentValue = intResult;
return;
}
if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var doubleResult))
{
_currentValue = doubleResult;
return;
}
_currentValue = value;
}
public void Visit(YamlSequenceNode sequence)
{
var array = new List<object>();
foreach (var node in sequence.Children)
{
node.Accept(this);
array.Add(_currentValue);
}
_currentValue = array;
}
public void Visit(YamlMappingNode mapping)
{
Dictionary<string, object> dict = new(StringComparer.OrdinalIgnoreCase);
foreach (var entry in mapping.Children)
{
var key = entry.Key switch
{
YamlScalarNode scalar => scalar.Value,
_ => entry.Key.ToString(),
};
entry.Value.Accept(this);
dict[key!] = _currentValue;
}
_currentValue = dict;
}
public void Visit(YamlDocument document) => document.RootNode.Accept(this);
public void Visit(YamlStream stream) => stream.Documents[0].RootNode.Accept(this);
}
}