Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20ca71b388 | ||
|
|
335a8b8a77 | ||
|
|
922bec4b24 | ||
|
|
272174ee75 | ||
|
|
29407f637b | ||
|
|
83c9122b6f | ||
|
|
86f07594e4 | ||
|
|
446f50763a | ||
|
|
0eaedb9cf6 | ||
|
|
8912686a47 | ||
|
|
d1dfe6eb5a |
@@ -0,0 +1,15 @@
|
||||
# Codex Instructions
|
||||
|
||||
## Verification Commands
|
||||
|
||||
Run .NET restore, build, and test commands outside the sandbox by default in this repo. Sandboxed .NET commands can stall on NuGet/package/compiler cache access, while the same commands complete normally with approved unsandboxed execution.
|
||||
|
||||
Preferred verification commands:
|
||||
|
||||
```bash
|
||||
TZ=UTC dotnet restore ErsatzTV.sln -v minimal
|
||||
TZ=UTC dotnet build ErsatzTV.sln --no-restore -v minimal
|
||||
TZ=UTC dotnet test ErsatzTV.sln --no-build -v minimal
|
||||
```
|
||||
|
||||
Use scoped escalated execution for these commands rather than first trying a sandboxed run.
|
||||
@@ -1,9 +1,13 @@
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Api.Filler;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
|
||||
namespace ErsatzTV.Application.Filler;
|
||||
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static FillerPresetResponseModel ProjectToResponseModel(FillerPreset fillerPreset) =>
|
||||
new(fillerPreset.Id, fillerPreset.Name);
|
||||
|
||||
internal static FillerPresetViewModel ProjectToViewModel(FillerPreset fillerPreset) =>
|
||||
new(
|
||||
fillerPreset.Id,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core.Api.Filler;
|
||||
|
||||
namespace ErsatzTV.Application.Filler;
|
||||
|
||||
public record GetAllFillerPresetsForApi : IRequest<List<FillerPresetResponseModel>>;
|
||||
@@ -0,0 +1,22 @@
|
||||
using ErsatzTV.Core.Api.Filler;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.Filler.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Filler;
|
||||
|
||||
public class GetAllFillerPresetsForApiHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetAllFillerPresetsForApi, List<FillerPresetResponseModel>>
|
||||
{
|
||||
public async Task<List<FillerPresetResponseModel>> Handle(
|
||||
GetAllFillerPresetsForApi request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
List<FillerPreset> fillerPresets = await dbContext.FillerPresets
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
return fillerPresets.Map(ProjectToResponseModel).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core.Api.Graphics;
|
||||
|
||||
namespace ErsatzTV.Application.Graphics;
|
||||
|
||||
public record GetAllGraphicsElementsForApi : IRequest<List<GraphicsElementResponseModel>>;
|
||||
@@ -0,0 +1,27 @@
|
||||
using ErsatzTV.Core.Api.Graphics;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.Graphics.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Graphics;
|
||||
|
||||
public class GetAllGraphicsElementsForApiHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetAllGraphicsElementsForApi, List<GraphicsElementResponseModel>>
|
||||
{
|
||||
public async Task<List<GraphicsElementResponseModel>> Handle(
|
||||
GetAllGraphicsElementsForApi request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
List<GraphicsElement> graphicsElements = await dbContext.GraphicsElements
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
return graphicsElements
|
||||
.Map(ProjectToViewModel)
|
||||
.OrderBy(e => e.Name == e.FileName)
|
||||
.ThenBy(e => e.Name)
|
||||
.Select(vm => new GraphicsElementResponseModel(vm.Id, vm.Name))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using ErsatzTV.Core.Api.Health;
|
||||
using ErsatzTV.Core.Health;
|
||||
|
||||
namespace ErsatzTV.Application.Health;
|
||||
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static HealthCheckResponseModel ProjectToResponseModel(HealthCheckResult result) =>
|
||||
new(
|
||||
result.Title,
|
||||
GetStatus(result.Status),
|
||||
result.Message,
|
||||
result.Link.MatchUnsafe(l => l.Link, () => null));
|
||||
|
||||
private static string GetStatus(HealthCheckStatus status) =>
|
||||
status switch
|
||||
{
|
||||
HealthCheckStatus.Pass => "pass",
|
||||
HealthCheckStatus.Fail => "fail",
|
||||
HealthCheckStatus.Warning => "warn",
|
||||
HealthCheckStatus.Info => "info",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core.Api.Health;
|
||||
|
||||
namespace ErsatzTV.Application.Health;
|
||||
|
||||
public record GetAllHealthCheckResultsForApi : IRequest<List<HealthCheckResponseModel>>;
|
||||
@@ -0,0 +1,32 @@
|
||||
using ErsatzTV.Core.Api.Health;
|
||||
using ErsatzTV.Core.Health;
|
||||
using static ErsatzTV.Application.Health.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Health;
|
||||
|
||||
public class GetAllHealthCheckResultsForApiHandler
|
||||
: IRequestHandler<GetAllHealthCheckResultsForApi, List<HealthCheckResponseModel>>
|
||||
{
|
||||
private readonly IHealthCheckService _healthCheckService;
|
||||
|
||||
public GetAllHealthCheckResultsForApiHandler(IHealthCheckService healthCheckService) =>
|
||||
_healthCheckService = healthCheckService;
|
||||
|
||||
public async Task<List<HealthCheckResponseModel>> Handle(
|
||||
GetAllHealthCheckResultsForApi request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<HealthCheckResult> results = await _healthCheckService.PerformHealthChecks(cancellationToken);
|
||||
return results
|
||||
.Filter(r => r.Status != HealthCheckStatus.NotApplicable)
|
||||
.Map(ProjectToResponseModel)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public class GetPlayoutByIdHandler(IDbContextFactory<TvContext> dbContextFactory
|
||||
.AsNoTracking()
|
||||
.Include(p => p.ProgramSchedule)
|
||||
.Include(p => p.Channel)
|
||||
.Include(p => p.BuildStatus)
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId, cancellationToken)
|
||||
.MapT(p => new PlayoutNameViewModel(
|
||||
p.Id,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using ErsatzTV.Application.Artworks;
|
||||
using ErsatzTV.Core.Api.Watermarks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Watermarks;
|
||||
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static WatermarkResponseModel ProjectToResponseModel(ChannelWatermark watermark) =>
|
||||
new(watermark.Id, watermark.Name);
|
||||
|
||||
public static WatermarkViewModel ProjectToViewModel(ChannelWatermark watermark) =>
|
||||
new(
|
||||
watermark.Id,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core.Api.Watermarks;
|
||||
|
||||
namespace ErsatzTV.Application.Watermarks;
|
||||
|
||||
public record GetAllWatermarksForApi : IRequest<List<WatermarkResponseModel>>;
|
||||
@@ -0,0 +1,22 @@
|
||||
using ErsatzTV.Core.Api.Watermarks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.Watermarks.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Watermarks;
|
||||
|
||||
public class GetAllWatermarksForApiHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetAllWatermarksForApi, List<WatermarkResponseModel>>
|
||||
{
|
||||
public async Task<List<WatermarkResponseModel>> Handle(
|
||||
GetAllWatermarksForApi request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
List<ChannelWatermark> watermarks = await dbContext.ChannelWatermarks
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
return watermarks.Map(ProjectToResponseModel).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Core.Api.Filler;
|
||||
|
||||
public record FillerPresetResponseModel(int Id, string Name);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Core.Api.Graphics;
|
||||
|
||||
public record GraphicsElementResponseModel(int Id, string Name);
|
||||
@@ -0,0 +1,8 @@
|
||||
#nullable enable
|
||||
namespace ErsatzTV.Core.Api.Health;
|
||||
|
||||
public record HealthCheckResponseModel(
|
||||
string Title,
|
||||
string Status,
|
||||
string Detail,
|
||||
string? Link);
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace ErsatzTV.Core.Api.Playouts;
|
||||
|
||||
public record PagedPlayoutItemsResponseModel(
|
||||
int TotalCount,
|
||||
List<PlayoutItemResponseModel> Page);
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace ErsatzTV.Core.Api.Playouts;
|
||||
|
||||
public record PagedPlayoutsResponseModel(
|
||||
int TotalCount,
|
||||
List<PlayoutListItemResponseModel> Page);
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Api.Playouts;
|
||||
|
||||
public record PlayoutBuildStatusResponseModel(
|
||||
DateTimeOffset LastBuild,
|
||||
bool Success,
|
||||
string Message);
|
||||
@@ -0,0 +1,10 @@
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
|
||||
namespace ErsatzTV.Core.Api.Playouts;
|
||||
|
||||
public record PlayoutItemResponseModel(
|
||||
string Title,
|
||||
DateTimeOffset Start,
|
||||
DateTimeOffset Finish,
|
||||
string Duration,
|
||||
FillerKind? FillerKind);
|
||||
@@ -0,0 +1,13 @@
|
||||
#nullable enable
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Core.Api.Playouts;
|
||||
|
||||
public record PlayoutListItemResponseModel(
|
||||
int Id,
|
||||
string ChannelNumber,
|
||||
string ChannelName,
|
||||
PlayoutScheduleKind ScheduleKind,
|
||||
string ScheduleName,
|
||||
TimeSpan? DailyRebuildTime,
|
||||
PlayoutBuildStatusResponseModel? BuildStatus);
|
||||
@@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Core.Api.Playouts;
|
||||
@@ -9,8 +10,9 @@ public record PlayoutResponseModel(
|
||||
string ChannelNumber,
|
||||
ChannelPlayoutMode PlayoutMode,
|
||||
string ScheduleName,
|
||||
string ScheduleFile,
|
||||
TimeSpan? DailyRebuildTime)
|
||||
string? ScheduleFile,
|
||||
TimeSpan? DailyRebuildTime,
|
||||
PlayoutBuildStatusResponseModel? BuildStatus)
|
||||
{
|
||||
public static PlayoutResponseModel From(
|
||||
int id,
|
||||
@@ -19,8 +21,9 @@ public record PlayoutResponseModel(
|
||||
string channelNumber,
|
||||
ChannelPlayoutMode playoutMode,
|
||||
string scheduleName,
|
||||
string scheduleFile,
|
||||
TimeSpan? dailyRebuildTime) =>
|
||||
string? scheduleFile,
|
||||
TimeSpan? dailyRebuildTime,
|
||||
PlayoutBuildStatusResponseModel? buildStatus) =>
|
||||
new(
|
||||
id,
|
||||
scheduleKind,
|
||||
@@ -29,5 +32,6 @@ public record PlayoutResponseModel(
|
||||
playoutMode,
|
||||
scheduleName,
|
||||
scheduleFile,
|
||||
dailyRebuildTime);
|
||||
dailyRebuildTime,
|
||||
buildStatus);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Core.Api.Watermarks;
|
||||
|
||||
public record WatermarkResponseModel(int Id, string Name);
|
||||
@@ -0,0 +1,63 @@
|
||||
using ErsatzTV.Application.Filler;
|
||||
using ErsatzTV.Core.Api.Filler;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Tests.Support;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Application.Filler;
|
||||
|
||||
[TestFixture]
|
||||
public class FillerPresetHandlerTests
|
||||
{
|
||||
private InMemoryTvContext _db = null!;
|
||||
|
||||
[SetUp]
|
||||
public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync();
|
||||
|
||||
[TearDown]
|
||||
public async Task TearDown() => await _db.DisposeAsync();
|
||||
|
||||
[Test]
|
||||
public async Task GetAllFillerPresetsForApi_Should_Return_All_Presets()
|
||||
{
|
||||
await SeedPreset(1, "Intro");
|
||||
await SeedPreset(2, "Outro");
|
||||
|
||||
var handler = new GetAllFillerPresetsForApiHandler(_db.Factory);
|
||||
|
||||
List<FillerPresetResponseModel> result =
|
||||
await handler.Handle(new GetAllFillerPresetsForApi(), CancellationToken.None);
|
||||
|
||||
result.Count.ShouldBe(2);
|
||||
result.ShouldContain(new FillerPresetResponseModel(1, "Intro"));
|
||||
result.ShouldContain(new FillerPresetResponseModel(2, "Outro"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllFillerPresetsForApi_Should_Return_Empty_List_When_None_Exist()
|
||||
{
|
||||
var handler = new GetAllFillerPresetsForApiHandler(_db.Factory);
|
||||
|
||||
List<FillerPresetResponseModel> result =
|
||||
await handler.Handle(new GetAllFillerPresetsForApi(), CancellationToken.None);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
private async Task SeedPreset(int id, string name)
|
||||
{
|
||||
await using TvContext context = _db.CreateContext();
|
||||
context.FillerPresets.Add(new FillerPreset
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
FillerKind = FillerKind.PreRoll,
|
||||
FillerMode = FillerMode.Duration,
|
||||
CollectionType = CollectionType.Collection
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using ErsatzTV.Application.Graphics;
|
||||
using ErsatzTV.Core.Api.Graphics;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Tests.Support;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Application.Graphics;
|
||||
|
||||
[TestFixture]
|
||||
public class GraphicsElementHandlerTests
|
||||
{
|
||||
private InMemoryTvContext _db = null!;
|
||||
|
||||
[SetUp]
|
||||
public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync();
|
||||
|
||||
[TearDown]
|
||||
public async Task TearDown() => await _db.DisposeAsync();
|
||||
|
||||
[Test]
|
||||
public async Task GetAllGraphicsElementsForApi_Should_Return_All_Elements()
|
||||
{
|
||||
await SeedElement(1, "watermark.png", GraphicsElementKind.Image, "Custom Watermark");
|
||||
await SeedElement(2, "clock.png", GraphicsElementKind.Image, string.Empty);
|
||||
|
||||
var handler = new GetAllGraphicsElementsForApiHandler(_db.Factory);
|
||||
|
||||
List<GraphicsElementResponseModel> result =
|
||||
await handler.Handle(new GetAllGraphicsElementsForApi(), CancellationToken.None);
|
||||
|
||||
result.Count.ShouldBe(2);
|
||||
result.Select(e => e.Id).ShouldBe([1, 2]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllGraphicsElementsForApi_Should_Order_Named_Elements_Before_Unnamed()
|
||||
{
|
||||
// unnamed element's projected Name equals its FileName -> should sort after named elements
|
||||
await SeedElement(1, "aaa.png", GraphicsElementKind.Image, string.Empty);
|
||||
await SeedElement(2, "zzz.png", GraphicsElementKind.Image, "A Custom Name");
|
||||
|
||||
var handler = new GetAllGraphicsElementsForApiHandler(_db.Factory);
|
||||
|
||||
List<GraphicsElementResponseModel> result =
|
||||
await handler.Handle(new GetAllGraphicsElementsForApi(), CancellationToken.None);
|
||||
|
||||
result.Count.ShouldBe(2);
|
||||
result[0].Id.ShouldBe(2);
|
||||
result[1].Id.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllGraphicsElementsForApi_Should_Return_Empty_List_When_None_Exist()
|
||||
{
|
||||
var handler = new GetAllGraphicsElementsForApiHandler(_db.Factory);
|
||||
|
||||
List<GraphicsElementResponseModel> result =
|
||||
await handler.Handle(new GetAllGraphicsElementsForApi(), CancellationToken.None);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
private async Task SeedElement(int id, string path, GraphicsElementKind kind, string name)
|
||||
{
|
||||
await using TvContext context = _db.CreateContext();
|
||||
context.GraphicsElements.Add(new GraphicsElement
|
||||
{
|
||||
Id = id,
|
||||
Path = path,
|
||||
Name = name,
|
||||
Kind = kind
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using ErsatzTV.Application.Health;
|
||||
using ErsatzTV.Core.Api.Health;
|
||||
using ErsatzTV.Core.Health;
|
||||
using LanguageExt;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Application.Health;
|
||||
|
||||
[TestFixture]
|
||||
public class GetAllHealthCheckResultsForApiHandlerTests
|
||||
{
|
||||
private IHealthCheckService _healthCheckService = null!;
|
||||
private GetAllHealthCheckResultsForApiHandler _handler = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_healthCheckService = Substitute.For<IHealthCheckService>();
|
||||
_handler = new GetAllHealthCheckResultsForApiHandler(_healthCheckService);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Map_Status_Codes_To_Lowercase_Strings()
|
||||
{
|
||||
var results = new List<HealthCheckResult>
|
||||
{
|
||||
new("Pass Check", HealthCheckStatus.Pass, "all good", "ok", Option<HealthCheckLink>.None),
|
||||
new("Fail Check", HealthCheckStatus.Fail, "broken", "bad", Option<HealthCheckLink>.None),
|
||||
new("Warn Check", HealthCheckStatus.Warning, "watch out", "warn", Option<HealthCheckLink>.None),
|
||||
new("Info Check", HealthCheckStatus.Info, "fyi", "info", Option<HealthCheckLink>.None)
|
||||
};
|
||||
|
||||
_healthCheckService.PerformHealthChecks(Arg.Any<CancellationToken>()).Returns(results);
|
||||
|
||||
List<HealthCheckResponseModel> response =
|
||||
await _handler.Handle(new GetAllHealthCheckResultsForApi(), CancellationToken.None);
|
||||
|
||||
response.Count.ShouldBe(4);
|
||||
response.Select(r => r.Status).ShouldBe(["pass", "fail", "warn", "info"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Filter_Out_NotApplicable_Results()
|
||||
{
|
||||
var results = new List<HealthCheckResult>
|
||||
{
|
||||
new("Pass Check", HealthCheckStatus.Pass, "all good", "ok", Option<HealthCheckLink>.None),
|
||||
new("NA Check", HealthCheckStatus.NotApplicable, "skip", "skip", Option<HealthCheckLink>.None)
|
||||
};
|
||||
|
||||
_healthCheckService.PerformHealthChecks(Arg.Any<CancellationToken>()).Returns(results);
|
||||
|
||||
List<HealthCheckResponseModel> response =
|
||||
await _handler.Handle(new GetAllHealthCheckResultsForApi(), CancellationToken.None);
|
||||
|
||||
response.Count.ShouldBe(1);
|
||||
response[0].Title.ShouldBe("Pass Check");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Include_Link_When_Present()
|
||||
{
|
||||
var results = new List<HealthCheckResult>
|
||||
{
|
||||
new(
|
||||
"Linked Check",
|
||||
HealthCheckStatus.Warning,
|
||||
"detail message",
|
||||
"brief",
|
||||
Option<HealthCheckLink>.Some(new HealthCheckLink("https://example.com/docs")))
|
||||
};
|
||||
|
||||
_healthCheckService.PerformHealthChecks(Arg.Any<CancellationToken>()).Returns(results);
|
||||
|
||||
List<HealthCheckResponseModel> response =
|
||||
await _handler.Handle(new GetAllHealthCheckResultsForApi(), CancellationToken.None);
|
||||
|
||||
response[0].Link.ShouldBe("https://example.com/docs");
|
||||
response[0].Detail.ShouldBe("detail message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Have_Null_Link_When_Absent()
|
||||
{
|
||||
var results = new List<HealthCheckResult>
|
||||
{
|
||||
new("No Link Check", HealthCheckStatus.Pass, "detail", "brief", Option<HealthCheckLink>.None)
|
||||
};
|
||||
|
||||
_healthCheckService.PerformHealthChecks(Arg.Any<CancellationToken>()).Returns(results);
|
||||
|
||||
List<HealthCheckResponseModel> response =
|
||||
await _handler.Handle(new GetAllHealthCheckResultsForApi(), CancellationToken.None);
|
||||
|
||||
response[0].Link.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Return_Empty_List_On_Cancellation()
|
||||
{
|
||||
_healthCheckService.PerformHealthChecks(Arg.Any<CancellationToken>())
|
||||
.Returns<Task<List<HealthCheckResult>>>(_ => throw new TaskCanceledException());
|
||||
|
||||
List<HealthCheckResponseModel> response =
|
||||
await _handler.Handle(new GetAllHealthCheckResultsForApi(), CancellationToken.None);
|
||||
|
||||
response.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using ErsatzTV.Application.Watermarks;
|
||||
using ErsatzTV.Core.Api.Watermarks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Tests.Support;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Application.Watermarks;
|
||||
|
||||
[TestFixture]
|
||||
public class WatermarkHandlerTests
|
||||
{
|
||||
private InMemoryTvContext _db = null!;
|
||||
|
||||
[SetUp]
|
||||
public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync();
|
||||
|
||||
[TearDown]
|
||||
public async Task TearDown() => await _db.DisposeAsync();
|
||||
|
||||
[Test]
|
||||
public async Task GetAllWatermarksForApi_Should_Return_All_Watermarks()
|
||||
{
|
||||
await SeedWatermark(1, "Bug");
|
||||
await SeedWatermark(2, "Logo");
|
||||
|
||||
var handler = new GetAllWatermarksForApiHandler(_db.Factory);
|
||||
|
||||
List<WatermarkResponseModel> result =
|
||||
await handler.Handle(new GetAllWatermarksForApi(), CancellationToken.None);
|
||||
|
||||
result.Count.ShouldBe(2);
|
||||
result.ShouldContain(new WatermarkResponseModel(1, "Bug"));
|
||||
result.ShouldContain(new WatermarkResponseModel(2, "Logo"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllWatermarksForApi_Should_Return_Empty_List_When_None_Exist()
|
||||
{
|
||||
var handler = new GetAllWatermarksForApiHandler(_db.Factory);
|
||||
|
||||
List<WatermarkResponseModel> result =
|
||||
await handler.Handle(new GetAllWatermarksForApi(), CancellationToken.None);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
private async Task SeedWatermark(int id, string name)
|
||||
{
|
||||
await using TvContext context = _db.CreateContext();
|
||||
context.ChannelWatermarks.Add(new ChannelWatermark
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Mode = ChannelWatermarkMode.Permanent,
|
||||
ImageSource = ChannelWatermarkImageSource.Custom,
|
||||
Image = "watermark.png",
|
||||
Location = WatermarkLocation.BottomRight,
|
||||
Size = WatermarkSize.Scaled,
|
||||
WidthPercent = 10,
|
||||
HorizontalMarginPercent = 2,
|
||||
VerticalMarginPercent = 2,
|
||||
FrequencyMinutes = 15,
|
||||
DurationSeconds = 30,
|
||||
Opacity = 100
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using ErsatzTV.Controllers.Api.Requests;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
using MediatR;
|
||||
@@ -23,15 +24,15 @@ namespace ErsatzTV.Tests.Controllers;
|
||||
public class ChannelControllerTests
|
||||
{
|
||||
private IMediator _mediator = null!;
|
||||
private Channel<IBackgroundServiceRequest> _workerChannel = null!;
|
||||
private ChannelController _controller = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mediator = Substitute.For<IMediator>();
|
||||
ChannelWriter<IBackgroundServiceRequest> writer =
|
||||
System.Threading.Channels.Channel.CreateUnbounded<IBackgroundServiceRequest>().Writer;
|
||||
_controller = new ChannelController(writer, _mediator);
|
||||
_workerChannel = System.Threading.Channels.Channel.CreateUnbounded<IBackgroundServiceRequest>();
|
||||
_controller = new ChannelController(_workerChannel.Writer, _mediator);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -157,7 +158,7 @@ public class ChannelControllerTests
|
||||
_mediator.Send(Arg.Any<GetPlayoutIdByChannelNumber>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Option<int>.None);
|
||||
|
||||
IActionResult result = await _controller.ResetPlayout("404");
|
||||
IActionResult result = await _controller.ResetPlayout("404", mode: null, CancellationToken.None);
|
||||
|
||||
var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
|
||||
var problemDetails = notFound.Value.ShouldBeOfType<ProblemDetails>();
|
||||
@@ -165,6 +166,53 @@ public class ChannelControllerTests
|
||||
problemDetails.Title.ShouldBe("Resource not found");
|
||||
}
|
||||
|
||||
[TestCase(PlayoutScheduleKind.Classic, PlayoutBuildMode.Refresh)]
|
||||
[TestCase(PlayoutScheduleKind.Block, PlayoutBuildMode.Reset)]
|
||||
[TestCase(PlayoutScheduleKind.Sequential, PlayoutBuildMode.Reset)]
|
||||
public async Task ResetPlayout_Should_Default_Mode_By_ScheduleKind(
|
||||
PlayoutScheduleKind scheduleKind,
|
||||
PlayoutBuildMode expectedMode)
|
||||
{
|
||||
_mediator.Send(Arg.Any<GetPlayoutIdByChannelNumber>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Option<int>.Some(9));
|
||||
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9, scheduleKind)));
|
||||
|
||||
IActionResult result = await _controller.ResetPlayout("5", mode: null, CancellationToken.None);
|
||||
|
||||
result.ShouldBeOfType<OkResult>();
|
||||
_workerChannel.Reader.TryRead(out IBackgroundServiceRequest? request).ShouldBeTrue();
|
||||
var buildPlayout = request.ShouldBeOfType<BuildPlayout>();
|
||||
buildPlayout.PlayoutId.ShouldBe(9);
|
||||
buildPlayout.Mode.ShouldBe(expectedMode);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResetPlayout_Should_Honor_Explicit_Mode()
|
||||
{
|
||||
_mediator.Send(Arg.Any<GetPlayoutIdByChannelNumber>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Option<int>.Some(9));
|
||||
|
||||
IActionResult result = await _controller.ResetPlayout("5", PlayoutBuildMode.Continue, CancellationToken.None);
|
||||
|
||||
result.ShouldBeOfType<OkResult>();
|
||||
_workerChannel.Reader.TryRead(out IBackgroundServiceRequest? request).ShouldBeTrue();
|
||||
request.ShouldBeOfType<BuildPlayout>().Mode.ShouldBe(PlayoutBuildMode.Continue);
|
||||
await _mediator.DidNotReceive().Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
private static PlayoutNameViewModel MakePlayout(int id, PlayoutScheduleKind scheduleKind) =>
|
||||
new(
|
||||
id,
|
||||
scheduleKind,
|
||||
"Channel",
|
||||
"5",
|
||||
ChannelPlayoutMode.Continuous,
|
||||
"Schedule",
|
||||
string.Empty,
|
||||
null,
|
||||
null);
|
||||
|
||||
private static ChannelViewModel MakeVm(int id) =>
|
||||
new(
|
||||
id,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Reflection;
|
||||
using ErsatzTV.Application.Filler;
|
||||
using ErsatzTV.Controllers.Api;
|
||||
using ErsatzTV.Core.Api.Filler;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Controllers;
|
||||
|
||||
[TestFixture]
|
||||
public class FillerPresetControllerTests
|
||||
{
|
||||
private FillerPresetController _controller = null!;
|
||||
private IMediator _mediator = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mediator = Substitute.For<IMediator>();
|
||||
_controller = new FillerPresetController(_mediator);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Controller_Should_Expose_Idiomatic_Rest_Route()
|
||||
{
|
||||
MethodInfo action = typeof(FillerPresetController).GetMethod(nameof(FillerPresetController.GetAll))
|
||||
?? throw new AssertionException("Missing action GetAll");
|
||||
|
||||
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
|
||||
attribute.HttpMethods.ShouldContain("GET");
|
||||
attribute.Template.ShouldBe("/api/filler-presets");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Return_FillerPresets()
|
||||
{
|
||||
List<FillerPresetResponseModel> models =
|
||||
[
|
||||
new FillerPresetResponseModel(1, "Intro"),
|
||||
new FillerPresetResponseModel(2, "Outro")
|
||||
];
|
||||
_mediator.Send(Arg.Any<GetAllFillerPresetsForApi>(), Arg.Any<CancellationToken>())
|
||||
.Returns(models);
|
||||
|
||||
List<FillerPresetResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
||||
|
||||
result.ShouldBe(models);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Return_Empty_List_When_None_Exist()
|
||||
{
|
||||
_mediator.Send(Arg.Any<GetAllFillerPresetsForApi>(), Arg.Any<CancellationToken>())
|
||||
.Returns([]);
|
||||
|
||||
List<FillerPresetResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Reflection;
|
||||
using ErsatzTV.Application.Graphics;
|
||||
using ErsatzTV.Controllers.Api;
|
||||
using ErsatzTV.Core.Api.Graphics;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Controllers;
|
||||
|
||||
[TestFixture]
|
||||
public class GraphicsElementControllerTests
|
||||
{
|
||||
private GraphicsElementController _controller = null!;
|
||||
private IMediator _mediator = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mediator = Substitute.For<IMediator>();
|
||||
_controller = new GraphicsElementController(_mediator);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Controller_Should_Expose_Idiomatic_Rest_Route()
|
||||
{
|
||||
MethodInfo action = typeof(GraphicsElementController).GetMethod(nameof(GraphicsElementController.GetAll))
|
||||
?? throw new AssertionException("Missing action GetAll");
|
||||
|
||||
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
|
||||
attribute.HttpMethods.ShouldContain("GET");
|
||||
attribute.Template.ShouldBe("/api/graphics-elements");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Return_GraphicsElements()
|
||||
{
|
||||
List<GraphicsElementResponseModel> models =
|
||||
[
|
||||
new GraphicsElementResponseModel(1, "Lower Third (lower-third.png)"),
|
||||
new GraphicsElementResponseModel(2, "bug.png")
|
||||
];
|
||||
_mediator.Send(Arg.Any<GetAllGraphicsElementsForApi>(), Arg.Any<CancellationToken>())
|
||||
.Returns(models);
|
||||
|
||||
List<GraphicsElementResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
||||
|
||||
result.ShouldBe(models);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Return_Empty_List_When_None_Exist()
|
||||
{
|
||||
_mediator.Send(Arg.Any<GetAllGraphicsElementsForApi>(), Arg.Any<CancellationToken>())
|
||||
.Returns([]);
|
||||
|
||||
List<GraphicsElementResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Reflection;
|
||||
using ErsatzTV.Application.Health;
|
||||
using ErsatzTV.Controllers.Api;
|
||||
using ErsatzTV.Core.Api.Health;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Controllers;
|
||||
|
||||
[TestFixture]
|
||||
public class HealthControllerTests
|
||||
{
|
||||
private IMediator _mediator = null!;
|
||||
private HealthController _controller = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mediator = Substitute.For<IMediator>();
|
||||
_controller = new HealthController(_mediator);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Controller_Should_Expose_Idiomatic_Rest_Route()
|
||||
{
|
||||
MethodInfo action = typeof(HealthController).GetMethod(nameof(HealthController.GetAll))
|
||||
?? throw new AssertionException("Missing action GetAll");
|
||||
|
||||
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
|
||||
attribute.HttpMethods.ShouldContain("GET");
|
||||
attribute.Template.ShouldBe("/api/health");
|
||||
attribute.Name.ShouldBe("GetHealthChecks");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Return_Results_From_Mediator()
|
||||
{
|
||||
var expected = new List<HealthCheckResponseModel>
|
||||
{
|
||||
new("Check One", "pass", "all good", null),
|
||||
new("Check Two", "fail", "broken", "https://example.com")
|
||||
};
|
||||
|
||||
_mediator.Send(Arg.Any<GetAllHealthCheckResultsForApi>(), Arg.Any<CancellationToken>())
|
||||
.Returns(expected);
|
||||
|
||||
List<HealthCheckResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
||||
|
||||
result.ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Return_Empty_List_When_None_Exist()
|
||||
{
|
||||
_mediator.Send(Arg.Any<GetAllHealthCheckResultsForApi>(), Arg.Any<CancellationToken>())
|
||||
.Returns([]);
|
||||
|
||||
List<HealthCheckResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ public class OpenApiErrorResponseContractTests
|
||||
[TestCase("/api/playouts", "post", "422")]
|
||||
[TestCase("/api/playouts/{id}", "delete", "404")]
|
||||
[TestCase("/api/playouts/{id}", "delete", "422")]
|
||||
[TestCase("/api/playouts/{id}/items", "get", "404")]
|
||||
[TestCase("/api/ffmpeg/profiles/{id}", "get", "404")]
|
||||
[TestCase("/api/ffmpeg/profiles", "post", "404")]
|
||||
[TestCase("/api/ffmpeg/profiles", "post", "401")]
|
||||
|
||||
@@ -5,6 +5,7 @@ using ErsatzTV.Controllers.Api.Requests;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Api.Playouts;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
@@ -34,8 +35,12 @@ public class PlayoutControllerTests
|
||||
[Test]
|
||||
public void Controller_Should_Expose_Idiomatic_Rest_Routes()
|
||||
{
|
||||
ShouldHaveActionRoute(nameof(PlayoutController.GetAll), "GET", "/api/playouts");
|
||||
ShouldHaveActionRoute(nameof(PlayoutController.GetById), "GET", "/api/playouts/{id:int}");
|
||||
ShouldHaveActionRoute(nameof(PlayoutController.GetItems), "GET", "/api/playouts/{id:int}/items");
|
||||
ShouldHaveActionRoute(nameof(PlayoutController.GetWarningsCount), "GET", "/api/playouts/warnings/count");
|
||||
ShouldHaveActionRoute(nameof(PlayoutController.Create), "POST", "/api/playouts");
|
||||
ShouldHaveActionRoute(nameof(PlayoutController.ResetAll), "POST", "/api/playouts/reset-all");
|
||||
ShouldHaveActionRoute(nameof(PlayoutController.Delete), "DELETE", "/api/playouts/{id:int}");
|
||||
}
|
||||
|
||||
@@ -162,6 +167,130 @@ public class PlayoutControllerTests
|
||||
problem.Title.ShouldBe("Resource not found");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Project_Paged_List_With_BuildStatus()
|
||||
{
|
||||
var buildStatus = new PlayoutBuildStatus
|
||||
{
|
||||
LastBuild = new DateTimeOffset(2026, 7, 2, 10, 0, 0, TimeSpan.Zero),
|
||||
Success = false,
|
||||
Message = "boom"
|
||||
};
|
||||
PlayoutNameViewModel vm = MakePlayout(9) with
|
||||
{
|
||||
BuildStatus = buildStatus,
|
||||
DbDailyRebuildTime = TimeSpan.FromHours(4)
|
||||
};
|
||||
_mediator.Send(Arg.Any<GetPagedPlayouts>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PagedPlayoutsViewModel(1, [vm]));
|
||||
|
||||
PagedPlayoutsResponseModel result = await _controller.GetAll("q", 2, 25, CancellationToken.None);
|
||||
|
||||
result.TotalCount.ShouldBe(1);
|
||||
PlayoutListItemResponseModel item = result.Page.Single();
|
||||
item.Id.ShouldBe(9);
|
||||
item.ChannelNumber.ShouldBe("101");
|
||||
item.ChannelName.ShouldBe("Channel");
|
||||
item.ScheduleKind.ShouldBe(PlayoutScheduleKind.Classic);
|
||||
item.ScheduleName.ShouldBe("Schedule");
|
||||
item.DailyRebuildTime.ShouldBe(TimeSpan.FromHours(4));
|
||||
item.BuildStatus.ShouldNotBeNull();
|
||||
item.BuildStatus.Success.ShouldBeFalse();
|
||||
item.BuildStatus.Message.ShouldBe("boom");
|
||||
item.BuildStatus.LastBuild.ShouldBe(buildStatus.LastBuild);
|
||||
|
||||
await _mediator.Received(1).Send(
|
||||
Arg.Is<GetPagedPlayouts>(q => q.Query == "q" && q.PageNum == 2 && q.PageSize == 25),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Emit_Null_BuildStatus_When_Absent()
|
||||
{
|
||||
PlayoutNameViewModel vm = MakePlayout(9) with { BuildStatus = null };
|
||||
_mediator.Send(Arg.Any<GetPagedPlayouts>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PagedPlayoutsViewModel(1, [vm]));
|
||||
|
||||
PagedPlayoutsResponseModel result = await _controller.GetAll("", 0, 100, CancellationToken.None);
|
||||
|
||||
result.Page.Single().BuildStatus.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetItems_Should_Project_Items_And_Null_FillerKind_For_Gaps()
|
||||
{
|
||||
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
|
||||
|
||||
var item = new PlayoutItemViewModel(
|
||||
"Movie",
|
||||
new DateTimeOffset(2026, 7, 2, 12, 0, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2026, 7, 2, 13, 0, 0, TimeSpan.Zero),
|
||||
"1:00:00",
|
||||
string.Empty,
|
||||
Some(FillerKind.MidRoll));
|
||||
var gap = new PlayoutItemViewModel(
|
||||
"UNSCHEDULED",
|
||||
new DateTimeOffset(2026, 7, 2, 13, 0, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2026, 7, 2, 13, 30, 0, TimeSpan.Zero),
|
||||
"30:00",
|
||||
string.Empty,
|
||||
Option<FillerKind>.None);
|
||||
_mediator.Send(Arg.Any<GetFuturePlayoutItemsById>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PagedPlayoutItemsViewModel(2, [item, gap]));
|
||||
|
||||
IActionResult actionResult = await _controller.GetItems(9, showFiller: true, 1, 10, CancellationToken.None);
|
||||
|
||||
var result = actionResult.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<PagedPlayoutItemsResponseModel>();
|
||||
result.TotalCount.ShouldBe(2);
|
||||
result.Page[0].Title.ShouldBe("Movie");
|
||||
result.Page[0].Duration.ShouldBe("1:00:00");
|
||||
result.Page[0].FillerKind.ShouldBe(FillerKind.MidRoll);
|
||||
result.Page[1].Title.ShouldBe("UNSCHEDULED");
|
||||
result.Page[1].FillerKind.ShouldBeNull();
|
||||
|
||||
await _mediator.Received(1).Send(
|
||||
Arg.Is<GetFuturePlayoutItemsById>(q =>
|
||||
q.PlayoutId == 9 && q.ShowFiller && q.PageNum == 1 && q.PageSize == 10),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetItems_Should_Return_404_For_Unknown_Playout_With_ProblemDetails()
|
||||
{
|
||||
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Option<PlayoutNameViewModel>.None);
|
||||
|
||||
IActionResult result = await _controller.GetItems(404, showFiller: false, 0, 100, CancellationToken.None);
|
||||
|
||||
var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
|
||||
var problem = notFound.Value.ShouldBeOfType<ProblemDetails>();
|
||||
problem.Status.ShouldBe(404);
|
||||
problem.Title.ShouldBe("Resource not found");
|
||||
|
||||
await _mediator.DidNotReceive().Send(Arg.Any<GetFuturePlayoutItemsById>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetWarningsCount_Should_Return_Count()
|
||||
{
|
||||
_mediator.Send(Arg.Any<GetPlayoutWarningsCount>(), Arg.Any<CancellationToken>())
|
||||
.Returns(7);
|
||||
|
||||
int result = await _controller.GetWarningsCount(CancellationToken.None);
|
||||
|
||||
result.ShouldBe(7);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResetAll_Should_Return_202_And_Send_Command()
|
||||
{
|
||||
IActionResult result = await _controller.ResetAll(CancellationToken.None);
|
||||
|
||||
result.ShouldBeOfType<AcceptedResult>();
|
||||
await _mediator.Received(1).Send(Arg.Any<ResetAllPlayouts>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
private static PlayoutNameViewModel MakePlayout(int id) =>
|
||||
new(
|
||||
id,
|
||||
@@ -183,7 +312,13 @@ public class PlayoutControllerTests
|
||||
vm.PlayoutMode,
|
||||
vm.ScheduleName,
|
||||
vm.ScheduleFile,
|
||||
vm.DbDailyRebuildTime);
|
||||
vm.DbDailyRebuildTime,
|
||||
vm.BuildStatus is null
|
||||
? null
|
||||
: new PlayoutBuildStatusResponseModel(
|
||||
vm.BuildStatus.LastBuild,
|
||||
vm.BuildStatus.Success,
|
||||
vm.BuildStatus.Message));
|
||||
|
||||
private static void ShouldHaveActionRoute(string actionName, string httpMethod, string route)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Reflection;
|
||||
using ErsatzTV.Application.Watermarks;
|
||||
using ErsatzTV.Controllers.Api;
|
||||
using ErsatzTV.Core.Api.Watermarks;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Tests.Controllers;
|
||||
|
||||
[TestFixture]
|
||||
public class WatermarkControllerTests
|
||||
{
|
||||
private WatermarkController _controller = null!;
|
||||
private IMediator _mediator = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mediator = Substitute.For<IMediator>();
|
||||
_controller = new WatermarkController(_mediator);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Controller_Should_Expose_Idiomatic_Rest_Route()
|
||||
{
|
||||
MethodInfo action = typeof(WatermarkController).GetMethod(nameof(WatermarkController.GetAll))
|
||||
?? throw new AssertionException("Missing action GetAll");
|
||||
|
||||
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
|
||||
attribute.HttpMethods.ShouldContain("GET");
|
||||
attribute.Template.ShouldBe("/api/watermarks");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Return_Watermarks()
|
||||
{
|
||||
List<WatermarkResponseModel> models =
|
||||
[
|
||||
new WatermarkResponseModel(1, "Corner Logo"),
|
||||
new WatermarkResponseModel(2, "Ticker")
|
||||
];
|
||||
_mediator.Send(Arg.Any<GetAllWatermarksForApi>(), Arg.Any<CancellationToken>())
|
||||
.Returns(models);
|
||||
|
||||
List<WatermarkResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
||||
|
||||
result.ShouldBe(models);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAll_Should_Return_Empty_List_When_None_Exist()
|
||||
{
|
||||
_mediator.Send(Arg.Any<GetAllWatermarksForApi>(), Arg.Any<CancellationToken>())
|
||||
.Returns([]);
|
||||
|
||||
List<WatermarkResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Controllers.Api.Requests;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Api.Channels;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Extensions;
|
||||
using MediatR;
|
||||
@@ -90,18 +91,42 @@ public class ChannelController(ChannelWriter<IBackgroundServiceRequest> workerCh
|
||||
[HttpPost("/api/channels/{channelNumber}/playout/reset")]
|
||||
[Tags("Channels")]
|
||||
[EndpointSummary("Reset channel playout")]
|
||||
[EndpointDescription(
|
||||
"When mode is omitted, classic playouts use Refresh (rebuild while maintaining collection " +
|
||||
"progress) and all other playout kinds use Reset (rebuild from scratch), matching the Blazor UI. " +
|
||||
"Pass mode to force a specific PlayoutBuildMode.")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ResetPlayout(string channelNumber)
|
||||
public async Task<IActionResult> ResetPlayout(
|
||||
string channelNumber,
|
||||
[FromQuery] PlayoutBuildMode? mode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Option<int> maybePlayoutId = await mediator.Send(new GetPlayoutIdByChannelNumber(channelNumber));
|
||||
Option<int> maybePlayoutId =
|
||||
await mediator.Send(new GetPlayoutIdByChannelNumber(channelNumber), cancellationToken);
|
||||
foreach (int playoutId in maybePlayoutId)
|
||||
{
|
||||
await workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Reset));
|
||||
PlayoutBuildMode buildMode = mode ?? await DefaultResetMode(playoutId, cancellationToken);
|
||||
await workerChannel.WriteAsync(new BuildPlayout(playoutId, buildMode), cancellationToken);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
return ApiResults.NotFoundProblem();
|
||||
}
|
||||
|
||||
// Match Blazor's Playouts.razor reset semantics: classic playouts refresh (preserve progress),
|
||||
// every other kind resets from scratch.
|
||||
private async Task<PlayoutBuildMode> DefaultResetMode(int playoutId, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<PlayoutNameViewModel> maybePlayout =
|
||||
await mediator.Send(new GetPlayoutById(playoutId), cancellationToken);
|
||||
return maybePlayout.Match(
|
||||
Some: vm => vm.ScheduleKind switch
|
||||
{
|
||||
PlayoutScheduleKind.Classic => PlayoutBuildMode.Refresh,
|
||||
_ => PlayoutBuildMode.Reset
|
||||
},
|
||||
None: () => PlayoutBuildMode.Reset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.Application.Filler;
|
||||
using ErsatzTV.Core.Api.Filler;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ErsatzTV.Controllers.Api;
|
||||
|
||||
[ApiController]
|
||||
public class FillerPresetController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/filler-presets", Name = "GetFillerPresets")]
|
||||
[Tags("Filler Presets")]
|
||||
[EndpointSummary("Get all filler presets")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(List<FillerPresetResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<List<FillerPresetResponseModel>> GetAll(CancellationToken cancellationToken) =>
|
||||
await mediator.Send(new GetAllFillerPresetsForApi(), cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.Application.Graphics;
|
||||
using ErsatzTV.Core.Api.Graphics;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ErsatzTV.Controllers.Api;
|
||||
|
||||
[ApiController]
|
||||
public class GraphicsElementController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/graphics-elements", Name = "GetGraphicsElements")]
|
||||
[Tags("Graphics Elements")]
|
||||
[EndpointSummary("Get all graphics elements")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(List<GraphicsElementResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<List<GraphicsElementResponseModel>> GetAll(CancellationToken cancellationToken) =>
|
||||
await mediator.Send(new GetAllGraphicsElementsForApi(), cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using ErsatzTV.Application.Health;
|
||||
using ErsatzTV.Core.Api.Health;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ErsatzTV.Controllers.Api;
|
||||
|
||||
[ApiController]
|
||||
public class HealthController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/health", Name = "GetHealthChecks")]
|
||||
[Tags("Health")]
|
||||
[EndpointSummary("Get health check results")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(List<HealthCheckResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<List<HealthCheckResponseModel>> GetAll(CancellationToken cancellationToken) =>
|
||||
await mediator.Send(new GetAllHealthCheckResultsForApi(), cancellationToken);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Controllers.Api.Requests;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Api.Playouts;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Extensions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -13,6 +15,32 @@ namespace ErsatzTV.Controllers.Api;
|
||||
[ApiController]
|
||||
public class PlayoutController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/playouts", Name = "GetPlayouts")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("List playouts")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(PagedPlayoutsResponseModel), StatusCodes.Status200OK)]
|
||||
public async Task<PagedPlayoutsResponseModel> GetAll(
|
||||
[FromQuery] string query = "",
|
||||
[FromQuery] int pageNum = 0,
|
||||
[FromQuery] int pageSize = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
PagedPlayoutsViewModel result =
|
||||
await mediator.Send(new GetPagedPlayouts(query, pageNum, pageSize), cancellationToken);
|
||||
return new PagedPlayoutsResponseModel(
|
||||
result.TotalCount,
|
||||
result.Page.Map(ToListItemResponse).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("/api/playouts/warnings/count", Name = "GetPlayoutWarningsCount")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Count playouts with a failed build")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
|
||||
public async Task<int> GetWarningsCount(CancellationToken cancellationToken) =>
|
||||
await mediator.Send(new GetPlayoutWarningsCount(), cancellationToken);
|
||||
|
||||
[HttpGet("/api/playouts/{id:int}", Name = "GetPlayoutById")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Get a playout by id")]
|
||||
@@ -25,6 +53,34 @@ public class PlayoutController(IMediator mediator) : ControllerBase
|
||||
return result.Map(ToResponse).ToGetResult();
|
||||
}
|
||||
|
||||
[HttpGet("/api/playouts/{id:int}/items", Name = "GetPlayoutItems")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Get upcoming items (and unscheduled gaps) for a playout")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(PagedPlayoutItemsResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetItems(
|
||||
int id,
|
||||
[FromQuery] bool showFiller = false,
|
||||
[FromQuery] int pageNum = 0,
|
||||
[FromQuery] int pageSize = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Option<PlayoutNameViewModel> maybePlayout = await mediator.Send(new GetPlayoutById(id), cancellationToken);
|
||||
if (maybePlayout.IsNone)
|
||||
{
|
||||
return ApiResults.NotFoundProblem();
|
||||
}
|
||||
|
||||
PagedPlayoutItemsViewModel result = await mediator.Send(
|
||||
new GetFuturePlayoutItemsById(id, showFiller, pageNum, pageSize),
|
||||
cancellationToken);
|
||||
return new OkObjectResult(
|
||||
new PagedPlayoutItemsResponseModel(
|
||||
result.TotalCount,
|
||||
result.Page.Map(ToItemResponse).ToList()));
|
||||
}
|
||||
|
||||
[HttpPost("/api/playouts")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Create a classic playout")]
|
||||
@@ -49,6 +105,17 @@ public class PlayoutController(IMediator mediator) : ControllerBase
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("/api/playouts/reset-all", Name = "ResetAllPlayouts")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Reset all playouts")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
public async Task<IActionResult> ResetAll(CancellationToken cancellationToken)
|
||||
{
|
||||
await mediator.Send(new ResetAllPlayouts(), cancellationToken);
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
[HttpDelete("/api/playouts/{id:int}")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Delete a playout")]
|
||||
@@ -71,5 +138,32 @@ public class PlayoutController(IMediator mediator) : ControllerBase
|
||||
vm.PlayoutMode,
|
||||
vm.ScheduleName,
|
||||
vm.ScheduleFile,
|
||||
vm.DbDailyRebuildTime);
|
||||
vm.DbDailyRebuildTime,
|
||||
ToBuildStatus(vm.BuildStatus));
|
||||
|
||||
private static PlayoutListItemResponseModel ToListItemResponse(PlayoutNameViewModel vm) =>
|
||||
new(
|
||||
vm.PlayoutId,
|
||||
vm.ChannelNumber,
|
||||
vm.ChannelName,
|
||||
vm.ScheduleKind,
|
||||
vm.ScheduleName,
|
||||
vm.DbDailyRebuildTime,
|
||||
ToBuildStatus(vm.BuildStatus));
|
||||
|
||||
private static PlayoutBuildStatusResponseModel ToBuildStatus(PlayoutBuildStatus buildStatus) =>
|
||||
buildStatus is null
|
||||
? null
|
||||
: new PlayoutBuildStatusResponseModel(
|
||||
buildStatus.LastBuild,
|
||||
buildStatus.Success,
|
||||
buildStatus.Message);
|
||||
|
||||
private static PlayoutItemResponseModel ToItemResponse(PlayoutItemViewModel vm) =>
|
||||
new(
|
||||
vm.Title,
|
||||
vm.Start,
|
||||
vm.Finish,
|
||||
vm.Duration,
|
||||
vm.FillerKind.MatchUnsafe(fk => (FillerKind?)fk, () => null));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.Application.Watermarks;
|
||||
using ErsatzTV.Core.Api.Watermarks;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ErsatzTV.Controllers.Api;
|
||||
|
||||
[ApiController]
|
||||
public class WatermarkController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/watermarks", Name = "GetWatermarks")]
|
||||
[Tags("Watermarks")]
|
||||
[EndpointSummary("Get all watermarks")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(List<WatermarkResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<List<WatermarkResponseModel>> GetAll(CancellationToken cancellationToken) =>
|
||||
await mediator.Send(new GetAllWatermarksForApi(), cancellationToken);
|
||||
}
|
||||
@@ -370,6 +370,7 @@
|
||||
"Channels"
|
||||
],
|
||||
"summary": "Reset channel playout",
|
||||
"description": "When mode is omitted, classic playouts use Refresh (rebuild while maintaining collection progress) and all other playout kinds use Reset (rebuild from scratch), matching the Blazor UI. Pass mode to force a specific PlayoutBuildMode.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "channelNumber",
|
||||
@@ -378,6 +379,13 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mode",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutBuildMode"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -1353,6 +1361,126 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/filler-presets": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Filler Presets"
|
||||
],
|
||||
"summary": "Get all filler presets",
|
||||
"operationId": "GetFillerPresets",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/FillerPresetResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/FillerPresetResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/FillerPresetResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/graphics-elements": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Graphics Elements"
|
||||
],
|
||||
"summary": "Get all graphics elements",
|
||||
"operationId": "GetGraphicsElements",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GraphicsElementResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GraphicsElementResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GraphicsElementResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/health": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Health"
|
||||
],
|
||||
"summary": "Get health check results",
|
||||
"operationId": "GetHealthChecks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/HealthCheckResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/HealthCheckResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/HealthCheckResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/libraries/{id}/scan": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -1475,6 +1603,192 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/playouts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Playouts"
|
||||
],
|
||||
"summary": "List playouts",
|
||||
"operationId": "GetPlayouts",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "query",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pageNum",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pageSize",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"default": 100
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PagedPlayoutsResponseModel"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PagedPlayoutsResponseModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PagedPlayoutsResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Playouts"
|
||||
],
|
||||
"summary": "Create a classic playout",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json-patch+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePlayoutRequest"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePlayoutRequest"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePlayoutRequest"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePlayoutRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutResponseModel"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutResponseModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/playouts/warnings/count": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Playouts"
|
||||
],
|
||||
"summary": "Count playouts with a failed build",
|
||||
"operationId": "GetPlayoutWarningsCount",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/playouts/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1599,54 +1913,67 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/playouts": {
|
||||
"post": {
|
||||
"/api/playouts/{id}/items": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Playouts"
|
||||
],
|
||||
"summary": "Create a classic playout",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json-patch+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePlayoutRequest"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePlayoutRequest"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePlayoutRequest"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePlayoutRequest"
|
||||
}
|
||||
"summary": "Get upcoming items (and unscheduled gaps) for a playout",
|
||||
"operationId": "GetPlayoutItems",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "showFiller",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pageNum",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pageSize",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"default": 100
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutResponseModel"
|
||||
"$ref": "#/components/schemas/PagedPlayoutItemsResponseModel"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutResponseModel"
|
||||
"$ref": "#/components/schemas/PagedPlayoutItemsResponseModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutResponseModel"
|
||||
"$ref": "#/components/schemas/PagedPlayoutItemsResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1670,26 +1997,20 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/playouts/reset-all": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Playouts"
|
||||
],
|
||||
"summary": "Reset all playouts",
|
||||
"operationId": "ResetAllPlayouts",
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Accepted"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2906,6 +3227,46 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/watermarks": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Watermarks"
|
||||
],
|
||||
"summary": "Get all watermarks",
|
||||
"operationId": "GetWatermarks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WatermarkResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WatermarkResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WatermarkResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -4017,6 +4378,25 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FillerPresetResponseModel": {
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"FillerPresetViewModel": {
|
||||
"required": [
|
||||
"id",
|
||||
@@ -4145,6 +4525,25 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GraphicsElementResponseModel": {
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"GraphicsElementViewModel": {
|
||||
"required": [
|
||||
"id",
|
||||
@@ -4191,6 +4590,32 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HealthCheckResponseModel": {
|
||||
"required": [
|
||||
"title",
|
||||
"status",
|
||||
"detail",
|
||||
"link"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"detail": {
|
||||
"type": "string"
|
||||
},
|
||||
"link": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"HlsSessionModel": {
|
||||
"required": [
|
||||
"channelNumber",
|
||||
@@ -4426,6 +4851,50 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PagedPlayoutItemsResponseModel": {
|
||||
"required": [
|
||||
"totalCount",
|
||||
"page"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"totalCount": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"page": {
|
||||
"type": [
|
||||
"null",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PlayoutItemResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PagedPlayoutsResponseModel": {
|
||||
"required": [
|
||||
"totalCount",
|
||||
"page"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"totalCount": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"page": {
|
||||
"type": [
|
||||
"null",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PlayoutListItemResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PlaybackOrder": {
|
||||
"enum": [
|
||||
"None",
|
||||
@@ -4468,6 +4937,126 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"PlayoutBuildMode": {
|
||||
"enum": [
|
||||
"Continue",
|
||||
"Refresh",
|
||||
"Reset"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PlayoutBuildStatusResponseModel": {
|
||||
"required": [
|
||||
"lastBuild",
|
||||
"success",
|
||||
"message"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lastBuild": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"PlayoutItemResponseModel": {
|
||||
"required": [
|
||||
"title",
|
||||
"start",
|
||||
"finish",
|
||||
"duration",
|
||||
"fillerKind"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"start": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"finish": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"duration": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"fillerKind": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/FillerKind"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"PlayoutListItemResponseModel": {
|
||||
"required": [
|
||||
"id",
|
||||
"channelNumber",
|
||||
"channelName",
|
||||
"scheduleKind",
|
||||
"scheduleName",
|
||||
"dailyRebuildTime",
|
||||
"buildStatus"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"channelNumber": {
|
||||
"type": "string"
|
||||
},
|
||||
"channelName": {
|
||||
"type": "string"
|
||||
},
|
||||
"scheduleKind": {
|
||||
"$ref": "#/components/schemas/PlayoutScheduleKind"
|
||||
},
|
||||
"scheduleName": {
|
||||
"type": "string"
|
||||
},
|
||||
"dailyRebuildTime": {
|
||||
"pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$",
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"buildStatus": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PlayoutBuildStatusResponseModel"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"PlayoutMode": {
|
||||
"enum": [
|
||||
"Flood",
|
||||
@@ -4486,7 +5075,8 @@
|
||||
"playoutMode",
|
||||
"scheduleName",
|
||||
"scheduleFile",
|
||||
"dailyRebuildTime"
|
||||
"dailyRebuildTime",
|
||||
"buildStatus"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4498,25 +5088,16 @@
|
||||
"$ref": "#/components/schemas/PlayoutScheduleKind"
|
||||
},
|
||||
"channelName": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
"type": "string"
|
||||
},
|
||||
"channelNumber": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
"type": "string"
|
||||
},
|
||||
"playoutMode": {
|
||||
"$ref": "#/components/schemas/ChannelPlayoutMode"
|
||||
},
|
||||
"scheduleName": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
"type": "string"
|
||||
},
|
||||
"scheduleFile": {
|
||||
"type": [
|
||||
@@ -4530,6 +5111,16 @@
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"buildStatus": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PlayoutBuildStatusResponseModel"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5660,6 +6251,25 @@
|
||||
"WatermarkLocation": {
|
||||
"type": "integer"
|
||||
},
|
||||
"WatermarkResponseModel": {
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"WatermarkSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5763,6 +6373,15 @@
|
||||
{
|
||||
"name": "FFmpeg Profiles"
|
||||
},
|
||||
{
|
||||
"name": "Filler Presets"
|
||||
},
|
||||
{
|
||||
"name": "Graphics Elements"
|
||||
},
|
||||
{
|
||||
"name": "Health"
|
||||
},
|
||||
{
|
||||
"name": "Libraries"
|
||||
},
|
||||
@@ -5786,6 +6405,9 @@
|
||||
},
|
||||
{
|
||||
"name": "Version"
|
||||
},
|
||||
{
|
||||
"name": "Watermarks"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user