Browse Source

add graphics element name (#2613)

* add graphics element name

* update dependencies
pull/2614/head
Jason Dove 9 months ago committed by GitHub
parent
commit
7530c592ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 39
      ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs
  3. 2
      ErsatzTV.Application/Graphics/GraphicsElementViewModel.cs
  4. 20
      ErsatzTV.Application/Graphics/Mapper.cs
  5. 2
      ErsatzTV.Application/Graphics/Queries/GetAllGraphicsElementsHandler.cs
  6. 1
      ErsatzTV.Core/Domain/GraphicsElement.cs
  7. 6
      ErsatzTV.Core/Graphics/BaseGraphicsElement.cs
  8. 2
      ErsatzTV.Core/Graphics/ImageGraphicsElement.cs
  9. 2
      ErsatzTV.Core/Graphics/MotionGraphicsElement.cs
  10. 2
      ErsatzTV.Core/Graphics/SubtitleGraphicsElement.cs
  11. 2
      ErsatzTV.Core/Graphics/TextGraphicsElement.cs
  12. 2
      ErsatzTV.Core/Interfaces/Streaming/IGraphicsElementLoader.cs
  13. 2
      ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj
  14. 6867
      ErsatzTV.Infrastructure.MySql/Migrations/20251108120730_Add_GraphicsElementName.Designer.cs
  15. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20251108120730_Add_GraphicsElementName.cs
  16. 3
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  17. 6694
      ErsatzTV.Infrastructure.Sqlite/Migrations/20251108120658_Add_GraphicsElementName.Designer.cs
  18. 28
      ErsatzTV.Infrastructure.Sqlite/Migrations/20251108120658_Add_GraphicsElementName.cs
  19. 3
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  20. 54
      ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs
  21. 4
      ErsatzTV/ErsatzTV.csproj

1
CHANGELOG.md

@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add `MediaItem_Stop` template data (DateTimeOffset) - Add `MediaItem_Stop` template data (DateTimeOffset)
- Add `ScaledResolution` template data (the final size of the frame before padding) - Add `ScaledResolution` template data (the final size of the frame before padding)
- Add `place_within_source_content` (true/false) field to image graphics element - Add `place_within_source_content` (true/false) field to image graphics element
- Add `name` field to all graphics elements to display in the UI
- Classic schedules: add collection type `Search Query` - Classic schedules: add collection type `Search Query`
- This allows defining search queries directly on schedule items without creating smart collections beforehand - This allows defining search queries directly on schedule items without creating smart collections beforehand
- As an example, this can be used to filter or combine existing smart collections - As an example, this can be used to filter or combine existing smart collections

39
ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs

@ -1,6 +1,7 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -10,6 +11,7 @@ namespace ErsatzTV.Application.Graphics;
public class RefreshGraphicsElementsHandler( public class RefreshGraphicsElementsHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
IGraphicsElementLoader graphicsElementLoader,
ILogger<RefreshGraphicsElementsHandler> logger) ILogger<RefreshGraphicsElementsHandler> logger)
: IRequestHandler<RefreshGraphicsElements> : IRequestHandler<RefreshGraphicsElements>
{ {
@ -21,7 +23,11 @@ public class RefreshGraphicsElementsHandler(
List<GraphicsElement> allExisting = await dbContext.GraphicsElements List<GraphicsElement> allExisting = await dbContext.GraphicsElements
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
foreach (GraphicsElement existing in allExisting.Where(e => !localFileSystem.FileExists(e.Path))) var missing = allExisting
.Where(e => !localFileSystem.FileExists(e.Path) || Path.GetExtension(e.Path) != ".yml")
.ToList();
foreach (GraphicsElement existing in missing)
{ {
logger.LogWarning( logger.LogWarning(
"Removing graphics element that references non-existing file {File}", "Removing graphics element that references non-existing file {File}",
@ -30,8 +36,13 @@ public class RefreshGraphicsElementsHandler(
dbContext.GraphicsElements.Remove(existing); dbContext.GraphicsElements.Remove(existing);
} }
foreach (GraphicsElement existing in allExisting.Except(missing))
{
await TryRefreshName(existing, cancellationToken);
}
// add new text elements // add new text elements
var newTextPaths = localFileSystem.ListFiles(FileSystemLayout.GraphicsElementsTextTemplatesFolder) var newTextPaths = localFileSystem.ListFiles(FileSystemLayout.GraphicsElementsTextTemplatesFolder, "*.yml")
.Where(f => allExisting.All(e => e.Path != f)) .Where(f => allExisting.All(e => e.Path != f))
.ToList(); .ToList();
@ -45,11 +56,13 @@ public class RefreshGraphicsElementsHandler(
Kind = GraphicsElementKind.Text Kind = GraphicsElementKind.Text
}; };
await TryRefreshName(graphicsElement, cancellationToken);
await dbContext.AddAsync(graphicsElement, cancellationToken); await dbContext.AddAsync(graphicsElement, cancellationToken);
} }
// add new image elements // add new image elements
var newImagePaths = localFileSystem.ListFiles(FileSystemLayout.GraphicsElementsImageTemplatesFolder) var newImagePaths = localFileSystem.ListFiles(FileSystemLayout.GraphicsElementsImageTemplatesFolder, "*.yml")
.Where(f => allExisting.All(e => e.Path != f)) .Where(f => allExisting.All(e => e.Path != f))
.ToList(); .ToList();
@ -63,11 +76,13 @@ public class RefreshGraphicsElementsHandler(
Kind = GraphicsElementKind.Image Kind = GraphicsElementKind.Image
}; };
await TryRefreshName(graphicsElement, cancellationToken);
await dbContext.AddAsync(graphicsElement, cancellationToken); await dbContext.AddAsync(graphicsElement, cancellationToken);
} }
// add new motion elements // add new motion elements
var newMotionPaths = localFileSystem.ListFiles(FileSystemLayout.GraphicsElementsMotionTemplatesFolder) var newMotionPaths = localFileSystem.ListFiles(FileSystemLayout.GraphicsElementsMotionTemplatesFolder, "*.yml")
.Where(f => allExisting.All(e => e.Path != f)) .Where(f => allExisting.All(e => e.Path != f))
.ToList(); .ToList();
@ -81,11 +96,13 @@ public class RefreshGraphicsElementsHandler(
Kind = GraphicsElementKind.Motion Kind = GraphicsElementKind.Motion
}; };
await TryRefreshName(graphicsElement, cancellationToken);
await dbContext.AddAsync(graphicsElement, cancellationToken); await dbContext.AddAsync(graphicsElement, cancellationToken);
} }
// add new subtitle elements // add new subtitle elements
var newSubtitlePaths = localFileSystem.ListFiles(FileSystemLayout.GraphicsElementsSubtitleTemplatesFolder) var newSubtitlePaths = localFileSystem.ListFiles(FileSystemLayout.GraphicsElementsSubtitleTemplatesFolder, "*.yml")
.Where(f => allExisting.All(e => e.Path != f)) .Where(f => allExisting.All(e => e.Path != f))
.ToList(); .ToList();
@ -99,9 +116,21 @@ public class RefreshGraphicsElementsHandler(
Kind = GraphicsElementKind.Subtitle Kind = GraphicsElementKind.Subtitle
}; };
await TryRefreshName(graphicsElement, cancellationToken);
await dbContext.AddAsync(graphicsElement, cancellationToken); await dbContext.AddAsync(graphicsElement, cancellationToken);
} }
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
} }
private async Task TryRefreshName(GraphicsElement graphicsElement, CancellationToken cancellationToken)
{
graphicsElement.Name = null;
Option<string> maybeName = await graphicsElementLoader.TryLoadName(graphicsElement.Path, cancellationToken);
foreach (string name in maybeName)
{
graphicsElement.Name = name;
}
}
} }

2
ErsatzTV.Application/Graphics/GraphicsElementViewModel.cs

@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Graphics; namespace ErsatzTV.Application.Graphics;
public record GraphicsElementViewModel(int Id, string Name); public record GraphicsElementViewModel(int Id, string Name, string FileName);

20
ErsatzTV.Application/Graphics/Mapper.cs

@ -7,13 +7,21 @@ public static class Mapper
public static GraphicsElementViewModel ProjectToViewModel(GraphicsElement graphicsElement) public static GraphicsElementViewModel ProjectToViewModel(GraphicsElement graphicsElement)
{ {
string fileName = Path.GetFileName(graphicsElement.Path); string fileName = Path.GetFileName(graphicsElement.Path);
return graphicsElement.Kind switch fileName = graphicsElement.Kind switch
{ {
GraphicsElementKind.Text => new GraphicsElementViewModel(graphicsElement.Id, $"text/{fileName}"), GraphicsElementKind.Text => $"text/{fileName}",
GraphicsElementKind.Image => new GraphicsElementViewModel(graphicsElement.Id, $"image/{fileName}"), GraphicsElementKind.Image => $"image/{fileName}",
GraphicsElementKind.Subtitle => new GraphicsElementViewModel(graphicsElement.Id, $"subtitle/{fileName}"), GraphicsElementKind.Subtitle => $"subtitle/{fileName}",
GraphicsElementKind.Motion => new GraphicsElementViewModel(graphicsElement.Id, $"motion/{fileName}"), GraphicsElementKind.Motion => $"motion/{fileName}",
_ => new GraphicsElementViewModel(graphicsElement.Id, graphicsElement.Path) _ => graphicsElement.Path
}; };
string name = fileName;
if (!string.IsNullOrWhiteSpace(graphicsElement.Name))
{
name = $"{graphicsElement.Name} ({fileName})";
}
return new GraphicsElementViewModel(graphicsElement.Id, name, fileName);
} }
} }

2
ErsatzTV.Application/Graphics/Queries/GetAllGraphicsElementsHandler.cs

@ -14,6 +14,6 @@ public class GetAllGraphicsElementsHandler(IDbContextFactory<TvContext> dbContex
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.GraphicsElements return await dbContext.GraphicsElements
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList()); .Map(list => list.Map(ProjectToViewModel).OrderBy(e => e.Name == e.FileName).ThenBy(e => e.Name).ToList());
} }
} }

1
ErsatzTV.Core/Domain/GraphicsElement.cs

@ -6,6 +6,7 @@ public class GraphicsElement
{ {
public int Id { get; set; } public int Id { get; set; }
public string Path { get; set; } public string Path { get; set; }
public string Name { get; set; }
public GraphicsElementKind Kind { get; set; } public GraphicsElementKind Kind { get; set; }
public List<PlayoutItem> PlayoutItems { get; set; } public List<PlayoutItem> PlayoutItems { get; set; }
public List<PlayoutItemGraphicsElement> PlayoutItemGraphicsElements { get; set; } public List<PlayoutItemGraphicsElement> PlayoutItemGraphicsElements { get; set; }

6
ErsatzTV.Core/Graphics/BaseGraphicsElement.cs

@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Graphics;
public class BaseGraphicsElement
{
public string Name { get; set; }
}

2
ErsatzTV.Core/Graphics/ImageGraphicsElement.cs

@ -3,7 +3,7 @@ using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Graphics; namespace ErsatzTV.Core.Graphics;
public class ImageGraphicsElement public class ImageGraphicsElement : BaseGraphicsElement
{ {
public string Image { get; set; } public string Image { get; set; }

2
ErsatzTV.Core/Graphics/MotionGraphicsElement.cs

@ -3,7 +3,7 @@ using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Graphics; namespace ErsatzTV.Core.Graphics;
public class MotionGraphicsElement public class MotionGraphicsElement : BaseGraphicsElement
{ {
[YamlMember(Alias = "video_path", ApplyNamingConventions = false)] [YamlMember(Alias = "video_path", ApplyNamingConventions = false)]
public string VideoPath { get; set; } public string VideoPath { get; set; }

2
ErsatzTV.Core/Graphics/SubtitleGraphicsElement.cs

@ -2,7 +2,7 @@ using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Graphics; namespace ErsatzTV.Core.Graphics;
public class SubtitleGraphicsElement public class SubtitleGraphicsElement : BaseGraphicsElement
{ {
[YamlMember(Alias = "z_index", ApplyNamingConventions = false)] [YamlMember(Alias = "z_index", ApplyNamingConventions = false)]
public int? ZIndex { get; set; } public int? ZIndex { get; set; }

2
ErsatzTV.Core/Graphics/TextGraphicsElement.cs

@ -3,7 +3,7 @@ using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Graphics; namespace ErsatzTV.Core.Graphics;
public class TextGraphicsElement public class TextGraphicsElement : BaseGraphicsElement
{ {
[YamlMember(Alias = "opacity_percent", ApplyNamingConventions = false)] [YamlMember(Alias = "opacity_percent", ApplyNamingConventions = false)]
public int? OpacityPercent { get; set; } public int? OpacityPercent { get; set; }

2
ErsatzTV.Core/Interfaces/Streaming/IGraphicsElementLoader.cs

@ -8,4 +8,6 @@ public interface IGraphicsElementLoader
GraphicsEngineContext context, GraphicsEngineContext context,
List<PlayoutItemGraphicsElement> elements, List<PlayoutItemGraphicsElement> elements,
CancellationToken cancellationToken); CancellationToken cancellationToken);
Task<Option<string>> TryLoadName(string fileName, CancellationToken cancellationToken);
} }

2
ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj

@ -11,7 +11,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CliWrap" Version="3.9.0" /> <PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Hardware.Info" Version="101.1.0" /> <PackageReference Include="Hardware.Info" Version="101.1.0.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" /> <PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Lennox.NvEncSharp" Version="2.0.0" /> <PackageReference Include="Lennox.NvEncSharp" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />

6867
ErsatzTV.Infrastructure.MySql/Migrations/20251108120730_Add_GraphicsElementName.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20251108120730_Add_GraphicsElementName.cs

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_GraphicsElementName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Name",
table: "GraphicsElement",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Name",
table: "GraphicsElement");
}
}
}

3
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -934,6 +934,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("Kind") b.Property<int>("Kind")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("longtext");
b.Property<string>("Path") b.Property<string>("Path")
.HasColumnType("longtext"); .HasColumnType("longtext");

6694
ErsatzTV.Infrastructure.Sqlite/Migrations/20251108120658_Add_GraphicsElementName.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.Sqlite/Migrations/20251108120658_Add_GraphicsElementName.cs

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_GraphicsElementName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Name",
table: "GraphicsElement",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Name",
table: "GraphicsElement");
}
}
}

3
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -897,6 +897,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("Kind") b.Property<int>("Kind")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Path") b.Property<string>("Path")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

54
ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs

@ -1,3 +1,4 @@
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Graphics; using ErsatzTV.Core.Graphics;
@ -9,6 +10,7 @@ using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Scriban; using Scriban;
using Scriban.Runtime; using Scriban.Runtime;
using Scriban.Syntax;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;
@ -131,6 +133,41 @@ public partial class GraphicsElementLoader(
return context; return context;
} }
public async Task<Option<string>> TryLoadName(string fileName, CancellationToken cancellationToken)
{
try
{
string yaml = await localFileSystem.ReadAllText(fileName);
var template = Template.Parse(yaml);
var builder = new StringBuilder();
var scriptPage = template.Page;
if (scriptPage.Body != null)
{
foreach (var statement in scriptPage.Body.Statements)
{
if (statement is ScriptRawStatement rawStatement)
{
builder.Append(rawStatement.Text);
}
}
}
Option<BaseGraphicsElement> maybeElement = FromYamlIgnoreUnmatched<BaseGraphicsElement>(builder.ToString());
foreach (BaseGraphicsElement element in maybeElement.Where(e => !string.IsNullOrWhiteSpace(e.Name)))
{
return element.Name;
}
}
catch (Exception)
{
// do nothing
}
return Option<string>.None;
}
private async Task<int> GetMaxEpgEntries(List<PlayoutItemGraphicsElement> elements) private async Task<int> GetMaxEpgEntries(List<PlayoutItemGraphicsElement> elements)
{ {
var epgEntries = 0; var epgEntries = 0;
@ -254,6 +291,23 @@ public partial class GraphicsElementLoader(
} }
} }
private static Option<T> FromYamlIgnoreUnmatched<T>(string yaml)
{
try
{
IDeserializer deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
return deserializer.Deserialize<T>(yaml);
}
catch (Exception)
{
return Option<T>.None;
}
}
[GeneratedRegex(@"epg_entries:\s*(\d+)")] [GeneratedRegex(@"epg_entries:\s*(\d+)")]
private static partial Regex EpgEntriesRegex(); private static partial Regex EpgEntriesRegex();
} }

4
ErsatzTV/ErsatzTV.csproj

@ -28,7 +28,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Blazored.FluentValidation" Version="2.2.0" /> <PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
<PackageReference Include="BlazorSortable" Version="5.1.3" /> <PackageReference Include="BlazorSortable" Version="5.1.4" />
<PackageReference Include="Bugsnag.AspNet.Core" Version="4.1.0" /> <PackageReference Include="Bugsnag.AspNet.Core" Version="4.1.0" />
<PackageReference Include="FluentValidation" Version="12.1.0" /> <PackageReference Include="FluentValidation" Version="12.1.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" /> <PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
@ -54,7 +54,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MudBlazor" Version="8.13.0" /> <PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="NaturalSort.Extension" Version="4.4.0" /> <PackageReference Include="NaturalSort.Extension" Version="4.4.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" /> <PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.10.1" /> <PackageReference Include="Scalar.AspNetCore" Version="2.10.1" />

Loading…
Cancel
Save