Browse Source

add yaml troubleshooting tool

pull/2259/head
Jason Dove 5 days ago
parent
commit
82ef6692a9
No known key found for this signature in database
  1. 3
      CHANGELOG.md
  2. 2
      ErsatzTV.Core/Interfaces/Scheduling/IYamlScheduleValidator.cs
  3. 36
      ErsatzTV.Infrastructure/Scheduling/YamlScheduleValidator.cs
  4. 5
      ErsatzTV/Pages/Troubleshooting.razor
  5. 124
      ErsatzTV/Pages/YamlValidator.razor
  6. 4
      ErsatzTV/Resources/yaml-playout-import.schema.json
  7. 4
      ErsatzTV/Resources/yaml-playout.schema.json

3
CHANGELOG.md

@ -53,6 +53,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -53,6 +53,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `Keep Running` - transcoder will run until manually stopped
- Add support for music video thumbnails that end in `-thumb`
- For example `Music Video.mkv` could have a corresponding thumbnail `Music Video-thumb.jpg`
- Reorganize troubleshooting page
- Add `YAML Validation` tool in `Troubleshooting` > `Tools`
### Fixed
- Fix app startup with MySql/MariaDB
@ -80,7 +82,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -80,7 +82,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `Active` channels will be converted to `Is Enabled` = true and `Show In EPG` = true
- `Hidden` channels will be converted to `Is Enabled` = true and `Show In EPG` = false
- `Inactive` channels will be converted to `Is Enabled` = false and `Show In EPG` = false
- Reorganize troubleshooting page
## [25.3.1] - 2025-07-24
### Fixed

2
ErsatzTV.Core/Interfaces/Scheduling/IYamlScheduleValidator.cs

@ -3,4 +3,6 @@ namespace ErsatzTV.Core.Interfaces.Scheduling; @@ -3,4 +3,6 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IYamlScheduleValidator
{
Task<bool> ValidateSchedule(string yaml, bool isImport);
string ToJson(string yaml);
Task<IList<string>> GetValidationMessages(string yaml, bool isImport);
}

36
ErsatzTV.Infrastructure/Scheduling/YamlScheduleValidator.cs

@ -44,11 +44,45 @@ public class YamlScheduleValidator(ILogger<YamlScheduleValidator> logger) : IYam @@ -44,11 +44,45 @@ public class YamlScheduleValidator(ILogger<YamlScheduleValidator> logger) : IYam
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 visitor.JsonString;
return JsonConvert.SerializeObject(JsonConvert.DeserializeObject(visitor.JsonString), Formatting.Indented);
}
private sealed class YamlToJsonVisitor : IYamlVisitor

5
ErsatzTV/Pages/Troubleshooting.razor

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
@inject IMediator Mediator
@inject IJSRuntime JsRuntime
<MudForm>
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Troubleshooting</MudText>
@ -30,8 +30,7 @@ @@ -30,8 +30,7 @@
StartIcon="@Icons.Material.Filled.Checklist"
Href="system/troubleshooting/yaml"
Class="mt-6"
Style="margin-right: auto"
Disabled="true">
Style="margin-right: auto">
YAML Validation
</MudButton>
</MudStack>

124
ErsatzTV/Pages/YamlValidator.razor

@ -0,0 +1,124 @@ @@ -0,0 +1,124 @@
@page "/system/troubleshooting/yaml"
@using ErsatzTV.Core.Interfaces.Scheduling
@implements IDisposable
@inject IYamlScheduleValidator YamlScheduleValidator
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">YAML Validation</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>YAML File</MudText>
</div>
<MudTextField T="string" Value="_yamlFile" ValueChanged="@(async x => await OnYamlFileChanged(x))" />
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Schema</MudText>
</div>
<MudSelect @bind-Value="_isImport">
<MudSelectItem Value="@false">Full</MudSelectItem>
<MudSelectItem Value="@true">Import</MudSelectItem>
</MudSelect>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Validate</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Checklist"
OnClick="@ValidateYaml"
Disabled="@(!_exists)">
Validate
</MudButton>
</MudStack>
<MudTabs Class="mb-6">
<MudTabPanel Text="YAML">
<MudPaper Class="pa-6">
<div class="overflow-y-scroll" style="max-height: 500px">
<pre class="wrap-pre">
<code>@_yamlText</code>
</pre>
</div>
</MudPaper>
</MudTabPanel>
<MudTabPanel Text="JSON">
<MudPaper Class="pa-6">
<div class="overflow-y-scroll" style="max-height: 500px">
<pre class="wrap-pre">
<code>@_jsonText</code>
</pre>
</div>
</MudPaper>
</MudTabPanel>
<MudTabPanel Text="Messages"
BadgeData="@(_messagesCount > 0 ? _messagesCount.ToString() : null)"
BadgeIcon="@(!string.IsNullOrWhiteSpace(_yamlText) && _messagesCount == 0 ? Icons.Material.Filled.Check : null)"
BadgeColor="@(!string.IsNullOrWhiteSpace(_yamlText) && _messagesCount == 0 ? Color.Success : Color.Error)">
<MudPaper Class="pa-6">
<div class="overflow-y-scroll" style="max-height: 500px">
<pre class="wrap-pre">
<code>@_messages</code>
</pre>
</div>
</MudPaper>
</MudTabPanel>
</MudTabs>
<div class="mb-6">
<br />
<br />
</div>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private string _yamlFile;
private bool _isImport;
private bool _exists;
private string _yamlText;
private string _jsonText;
private string _messages;
private int _messagesCount;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private async Task ValidateYaml()
{
if (_exists)
{
_yamlText = await File.ReadAllTextAsync(_yamlFile);
_jsonText = YamlScheduleValidator.ToJson(_yamlText);
var messages = await YamlScheduleValidator.GetValidationMessages(_yamlText, _isImport);
_messagesCount = messages.Count;
_messages = string.Join("\n", messages);
StateHasChanged();
}
}
private async Task OnYamlFileChanged(string yamlFile)
{
var extension = Path.GetExtension(yamlFile);
_exists = extension is ".yaml" or ".yml" && File.Exists(yamlFile);
await Task.Delay(100);
_yamlFile = yamlFile;
_yamlText = string.Empty;
_jsonText = string.Empty;
_messages = string.Empty;
_messagesCount = 0;
StateHasChanged();
}
}

4
ErsatzTV/Resources/yaml-playout-import.schema.json

@ -231,7 +231,7 @@ @@ -231,7 +231,7 @@
"properties": {
"pad_until": { "type": "string" },
"content": { "type": "string" },
"tomorrow": { "type": "string" },
"tomorrow": { "type": ["boolean", "string"] },
"offline_tail": { "type": "boolean" },
"trim": { "type": "boolean" },
"fallback": { "type": "string" },
@ -318,7 +318,7 @@ @@ -318,7 +318,7 @@
"skipItemsInstruction": {
"type": "object",
"properties": {
"skip_items": { "type": "integer" },
"skip_items": { "type": ["integer", "string"] },
"content": { "type": "string" }
},
"required": [ "skip_items", "content" ],

4
ErsatzTV/Resources/yaml-playout.schema.json

@ -275,7 +275,7 @@ @@ -275,7 +275,7 @@
"properties": {
"pad_until": { "type": "string" },
"content": { "type": "string" },
"tomorrow": { "type": "string" },
"tomorrow": { "type": ["boolean", "string"] },
"offline_tail": { "type": "boolean" },
"trim": { "type": "boolean" },
"fallback": { "type": "string" },
@ -362,7 +362,7 @@ @@ -362,7 +362,7 @@
"skipItemsInstruction": {
"type": "object",
"properties": {
"skip_items": { "type": "integer" },
"skip_items": { "type": ["integer", "string"] },
"content": { "type": "string" }
},
"required": [ "skip_items", "content" ],

Loading…
Cancel
Save