Task A (#126): new non-polymorphic ScheduleItemResponseModel / ScheduleItemsResponseModel in Core/Api/Scheduling, plus shared NamedIdResponseModel. ScheduleItemResponseMapper flattens the One/Flood/Multiple/Duration VM hierarchy. ScheduleController GET/POST/PUT items now return the flat DTOs. Task B: GET /api/languages (LanguagesController + LanguageCodeResponseModel); GET /api/channels/music-video-credits-templates and GET /api/channels/stream-selectors; FillerKind added to FillerPresetResponseModel with optional ?fillerKind= filter on GET /api/filler-presets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ namespace ErsatzTV.Application.Filler;
|
|||||||
internal static class Mapper
|
internal static class Mapper
|
||||||
{
|
{
|
||||||
internal static FillerPresetResponseModel ProjectToResponseModel(FillerPreset fillerPreset) =>
|
internal static FillerPresetResponseModel ProjectToResponseModel(FillerPreset fillerPreset) =>
|
||||||
new(fillerPreset.Id, fillerPreset.Name);
|
new(fillerPreset.Id, fillerPreset.Name, fillerPreset.FillerKind);
|
||||||
|
|
||||||
internal static FillerPresetFullResponseModel ProjectToFullResponseModel(FillerPreset fillerPreset) =>
|
internal static FillerPresetFullResponseModel ProjectToFullResponseModel(FillerPreset fillerPreset) =>
|
||||||
new(
|
new(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ErsatzTV.Core.Api.Filler;
|
using ErsatzTV.Core.Api.Filler;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Filler;
|
namespace ErsatzTV.Application.Filler;
|
||||||
|
|
||||||
public record GetAllFillerPresetsForApi : IRequest<List<FillerPresetResponseModel>>;
|
public record GetAllFillerPresetsForApi(FillerKind? FillerKind = null) : IRequest<List<FillerPresetResponseModel>>;
|
||||||
|
|||||||
@@ -14,9 +14,13 @@ public class GetAllFillerPresetsForApiHandler(IDbContextFactory<TvContext> dbCon
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
List<FillerPreset> fillerPresets = await dbContext.FillerPresets
|
IQueryable<FillerPreset> query = dbContext.FillerPresets.AsNoTracking();
|
||||||
.AsNoTracking()
|
if (request.FillerKind is { } fillerKind)
|
||||||
.ToListAsync(cancellationToken);
|
{
|
||||||
|
query = query.Where(fp => fp.FillerKind == fillerKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FillerPreset> fillerPresets = await query.ToListAsync(cancellationToken);
|
||||||
return fillerPresets.Map(ProjectToResponseModel).ToList();
|
return fillerPresets.Map(ProjectToResponseModel).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using ErsatzTV.Core.Api;
|
||||||
|
using ErsatzTV.Core.Api.Scheduling;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.ProgramSchedules;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps the polymorphic <see cref="ProgramScheduleItemViewModel" /> hierarchy to the flat,
|
||||||
|
/// fully-described <see cref="ScheduleItemResponseModel" /> API DTO (issue #126). Subtype-specific
|
||||||
|
/// fields (Multiple's mode/count, Duration's playoutDuration/tailMode/discardToFillAttempts) are
|
||||||
|
/// read by pattern-matching the concrete VM type; they are null for other subtypes.
|
||||||
|
/// </summary>
|
||||||
|
public static class ScheduleItemResponseMapper
|
||||||
|
{
|
||||||
|
public static ScheduleItemResponseModel ProjectToResponseModel(ProgramScheduleItemViewModel item)
|
||||||
|
{
|
||||||
|
MultipleMode? multipleMode = null;
|
||||||
|
string multipleCount = null;
|
||||||
|
TimeSpan? playoutDuration = null;
|
||||||
|
TailMode? tailMode = null;
|
||||||
|
int? discardToFillAttempts = null;
|
||||||
|
|
||||||
|
switch (item)
|
||||||
|
{
|
||||||
|
case ProgramScheduleItemMultipleViewModel multiple:
|
||||||
|
multipleMode = multiple.MultipleMode;
|
||||||
|
multipleCount = multiple.Count;
|
||||||
|
break;
|
||||||
|
case ProgramScheduleItemDurationViewModel duration:
|
||||||
|
playoutDuration = duration.PlayoutDuration;
|
||||||
|
tailMode = duration.TailMode;
|
||||||
|
discardToFillAttempts = duration.DiscardToFillAttempts;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> watermarkIds = item.Watermarks?.Map(w => w.Id).ToList() ?? new List<int>();
|
||||||
|
List<int> graphicsElementIds = item.GraphicsElements?.Map(g => g.Id).ToList() ?? new List<int>();
|
||||||
|
List<NamedIdResponseModel> watermarks =
|
||||||
|
item.Watermarks?.Map(w => new NamedIdResponseModel(w.Id, w.Name)).ToList()
|
||||||
|
?? new List<NamedIdResponseModel>();
|
||||||
|
List<NamedIdResponseModel> graphicsElements =
|
||||||
|
item.GraphicsElements?.Map(g => new NamedIdResponseModel(g.Id, g.Name)).ToList()
|
||||||
|
?? new List<NamedIdResponseModel>();
|
||||||
|
|
||||||
|
return new ScheduleItemResponseModel(
|
||||||
|
item.Id,
|
||||||
|
item.Index,
|
||||||
|
item.StartType,
|
||||||
|
item.StartTime,
|
||||||
|
item.FixedStartTimeBehavior,
|
||||||
|
item.PlayoutMode,
|
||||||
|
item.CollectionType,
|
||||||
|
item.Collection?.Id,
|
||||||
|
item.MultiCollection?.Id,
|
||||||
|
item.SmartCollection?.Id,
|
||||||
|
item.RerunCollection?.Id,
|
||||||
|
item.MediaItem?.MediaItemId,
|
||||||
|
item.Playlist?.Id,
|
||||||
|
item.SearchTitle,
|
||||||
|
item.SearchQuery,
|
||||||
|
item.PlaybackOrder,
|
||||||
|
item.MarathonGroupBy,
|
||||||
|
item.MarathonShuffleGroups,
|
||||||
|
item.MarathonShuffleItems,
|
||||||
|
item.MarathonBatchSize,
|
||||||
|
item.FillWithGroupMode,
|
||||||
|
multipleMode,
|
||||||
|
multipleCount,
|
||||||
|
playoutDuration,
|
||||||
|
tailMode,
|
||||||
|
discardToFillAttempts,
|
||||||
|
item.CustomTitle,
|
||||||
|
item.GuideMode,
|
||||||
|
item.PreRollFiller?.Id,
|
||||||
|
item.MidRollFiller?.Id,
|
||||||
|
item.PostRollFiller?.Id,
|
||||||
|
item.TailFiller?.Id,
|
||||||
|
item.FallbackFiller?.Id,
|
||||||
|
watermarkIds,
|
||||||
|
graphicsElementIds,
|
||||||
|
item.PreferredAudioLanguageCode,
|
||||||
|
item.PreferredAudioTitle,
|
||||||
|
item.PreferredSubtitleLanguageCode,
|
||||||
|
item.SubtitleMode,
|
||||||
|
item.Collection?.Name,
|
||||||
|
item.MultiCollection?.Name,
|
||||||
|
item.SmartCollection?.Name,
|
||||||
|
item.RerunCollection?.Name,
|
||||||
|
item.Playlist?.Name,
|
||||||
|
item.Playlist?.PlaylistGroupId,
|
||||||
|
item.MediaItem?.Name,
|
||||||
|
item.PreRollFiller?.Name,
|
||||||
|
item.MidRollFiller?.Name,
|
||||||
|
item.PostRollFiller?.Name,
|
||||||
|
item.TailFiller?.Name,
|
||||||
|
item.FallbackFiller?.Name,
|
||||||
|
watermarks,
|
||||||
|
graphicsElements,
|
||||||
|
item.Name,
|
||||||
|
item.DurationEstimate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ScheduleItemsResponseModel ProjectToResponseModel(ProgramScheduleItemsWithDurationViewModel vm) =>
|
||||||
|
new(
|
||||||
|
vm.Items.Map(ProjectToResponseModel).ToList(),
|
||||||
|
vm.TotalDurationEstimate);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Api.Filler;
|
namespace ErsatzTV.Core.Api.Filler;
|
||||||
|
|
||||||
public record FillerPresetResponseModel(int Id, string Name);
|
public record FillerPresetResponseModel(int Id, string Name, FillerKind FillerKind);
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ErsatzTV.Core.Api.MediaItems;
|
||||||
|
|
||||||
|
public record LanguageCodeResponseModel(string Code, string EnglishName);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ErsatzTV.Core.Api;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A lightweight {id, name} pair used when an API response needs to embed a reference to a
|
||||||
|
/// named entity (e.g. a schedule item's watermarks or graphics elements) without projecting
|
||||||
|
/// the full entity DTO.
|
||||||
|
/// </summary>
|
||||||
|
public record NamedIdResponseModel(int Id, string Name);
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Scheduling;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Api.Scheduling;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A flat, non-polymorphic projection of a program schedule item (issue #126). Unlike the
|
||||||
|
/// Application-layer <c>ProgramScheduleItemViewModel</c> hierarchy (One/Flood/Multiple/Duration
|
||||||
|
/// subtypes), every subtype-specific field is a nullable member here so the OpenAPI schema fully
|
||||||
|
/// describes the shape. The mutation fields are named 1:1 with
|
||||||
|
/// <c>ScheduleItemRequest</c> so a GET can be mapped losslessly back to a PUT/POST request.
|
||||||
|
/// </summary>
|
||||||
|
public record ScheduleItemResponseModel(
|
||||||
|
int Id,
|
||||||
|
int Index,
|
||||||
|
StartType StartType,
|
||||||
|
TimeSpan? StartTime,
|
||||||
|
FixedStartTimeBehavior? FixedStartTimeBehavior,
|
||||||
|
PlayoutMode PlayoutMode,
|
||||||
|
CollectionType CollectionType,
|
||||||
|
int? CollectionId,
|
||||||
|
int? MultiCollectionId,
|
||||||
|
int? SmartCollectionId,
|
||||||
|
int? RerunCollectionId,
|
||||||
|
int? MediaItemId,
|
||||||
|
int? PlaylistId,
|
||||||
|
string? SearchTitle,
|
||||||
|
string? SearchQuery,
|
||||||
|
PlaybackOrder PlaybackOrder,
|
||||||
|
MarathonGroupBy MarathonGroupBy,
|
||||||
|
bool MarathonShuffleGroups,
|
||||||
|
bool MarathonShuffleItems,
|
||||||
|
int? MarathonBatchSize,
|
||||||
|
FillWithGroupMode FillWithGroupMode,
|
||||||
|
MultipleMode? MultipleMode,
|
||||||
|
string? MultipleCount,
|
||||||
|
TimeSpan? PlayoutDuration,
|
||||||
|
TailMode? TailMode,
|
||||||
|
int? DiscardToFillAttempts,
|
||||||
|
string? CustomTitle,
|
||||||
|
GuideMode GuideMode,
|
||||||
|
int? PreRollFillerId,
|
||||||
|
int? MidRollFillerId,
|
||||||
|
int? PostRollFillerId,
|
||||||
|
int? TailFillerId,
|
||||||
|
int? FallbackFillerId,
|
||||||
|
List<int> WatermarkIds,
|
||||||
|
List<int> GraphicsElementIds,
|
||||||
|
string? PreferredAudioLanguageCode,
|
||||||
|
string? PreferredAudioTitle,
|
||||||
|
string? PreferredSubtitleLanguageCode,
|
||||||
|
ChannelSubtitleMode? SubtitleMode,
|
||||||
|
string? CollectionName,
|
||||||
|
string? MultiCollectionName,
|
||||||
|
string? SmartCollectionName,
|
||||||
|
string? RerunCollectionName,
|
||||||
|
string? PlaylistName,
|
||||||
|
int? PlaylistGroupId,
|
||||||
|
string? MediaItemName,
|
||||||
|
string? PreRollFillerName,
|
||||||
|
string? MidRollFillerName,
|
||||||
|
string? PostRollFillerName,
|
||||||
|
string? TailFillerName,
|
||||||
|
string? FallbackFillerName,
|
||||||
|
List<NamedIdResponseModel> Watermarks,
|
||||||
|
List<NamedIdResponseModel> GraphicsElements,
|
||||||
|
string? Name,
|
||||||
|
TimeSpan? DurationEstimate);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Api.Scheduling;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GET envelope for a schedule's items. Carries the flat <see cref="ScheduleItemResponseModel" />
|
||||||
|
/// list plus a best-effort total runtime estimate (null when unbounded/unknown).
|
||||||
|
/// </summary>
|
||||||
|
public record ScheduleItemsResponseModel(
|
||||||
|
List<ScheduleItemResponseModel> Items,
|
||||||
|
TimeSpan? TotalDurationEstimate);
|
||||||
@@ -32,8 +32,24 @@ public class FillerPresetHandlerTests
|
|||||||
await handler.Handle(new GetAllFillerPresetsForApi(), CancellationToken.None);
|
await handler.Handle(new GetAllFillerPresetsForApi(), CancellationToken.None);
|
||||||
|
|
||||||
result.Count.ShouldBe(2);
|
result.Count.ShouldBe(2);
|
||||||
result.ShouldContain(new FillerPresetResponseModel(1, "Intro"));
|
result.ShouldContain(new FillerPresetResponseModel(1, "Intro", FillerKind.PreRoll));
|
||||||
result.ShouldContain(new FillerPresetResponseModel(2, "Outro"));
|
result.ShouldContain(new FillerPresetResponseModel(2, "Outro", FillerKind.PreRoll));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetAllFillerPresetsForApi_Should_Filter_By_FillerKind()
|
||||||
|
{
|
||||||
|
await SeedPreset(1, "Intro", FillerKind.PreRoll);
|
||||||
|
await SeedPreset(2, "Mid", FillerKind.MidRoll);
|
||||||
|
await SeedPreset(3, "Outro", FillerKind.PostRoll);
|
||||||
|
|
||||||
|
var handler = new GetAllFillerPresetsForApiHandler(_db.Factory);
|
||||||
|
|
||||||
|
List<FillerPresetResponseModel> result =
|
||||||
|
await handler.Handle(new GetAllFillerPresetsForApi(FillerKind.MidRoll), CancellationToken.None);
|
||||||
|
|
||||||
|
result.Count.ShouldBe(1);
|
||||||
|
result.ShouldContain(new FillerPresetResponseModel(2, "Mid", FillerKind.MidRoll));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -47,14 +63,14 @@ public class FillerPresetHandlerTests
|
|||||||
result.ShouldBeEmpty();
|
result.ShouldBeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SeedPreset(int id, string name)
|
private async Task SeedPreset(int id, string name, FillerKind fillerKind = FillerKind.PreRoll)
|
||||||
{
|
{
|
||||||
await using TvContext context = _db.CreateContext();
|
await using TvContext context = _db.CreateContext();
|
||||||
context.FillerPresets.Add(new FillerPreset
|
context.FillerPresets.Add(new FillerPreset
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Name = name,
|
Name = name,
|
||||||
FillerKind = FillerKind.PreRoll,
|
FillerKind = fillerKind,
|
||||||
FillerMode = FillerMode.Duration,
|
FillerMode = FillerMode.Duration,
|
||||||
CollectionType = CollectionType.Collection
|
CollectionType = CollectionType.Collection
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,17 +63,30 @@ public class FillerPresetControllerTests
|
|||||||
{
|
{
|
||||||
List<FillerPresetResponseModel> models =
|
List<FillerPresetResponseModel> models =
|
||||||
[
|
[
|
||||||
new FillerPresetResponseModel(1, "Intro"),
|
new FillerPresetResponseModel(1, "Intro", FillerKind.PreRoll),
|
||||||
new FillerPresetResponseModel(2, "Outro")
|
new FillerPresetResponseModel(2, "Outro", FillerKind.PostRoll)
|
||||||
];
|
];
|
||||||
_mediator.Send(Arg.Any<GetAllFillerPresetsForApi>(), Arg.Any<CancellationToken>())
|
_mediator.Send(Arg.Any<GetAllFillerPresetsForApi>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(models);
|
.Returns(models);
|
||||||
|
|
||||||
List<FillerPresetResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
List<FillerPresetResponseModel> result = await _controller.GetAll(null, CancellationToken.None);
|
||||||
|
|
||||||
result.ShouldBe(models);
|
result.ShouldBe(models);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetAll_Should_Forward_FillerKind_Filter()
|
||||||
|
{
|
||||||
|
_mediator.Send(Arg.Any<GetAllFillerPresetsForApi>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns([]);
|
||||||
|
|
||||||
|
await _controller.GetAll(FillerKind.MidRoll, CancellationToken.None);
|
||||||
|
|
||||||
|
await _mediator.Received(1).Send(
|
||||||
|
Arg.Is<GetAllFillerPresetsForApi>(q => q.FillerKind == FillerKind.MidRoll),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task GetById_Should_Return_200_For_Some()
|
public async Task GetById_Should_Return_200_For_Some()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Threading.Channels;
|
|||||||
using ErsatzTV.Application;
|
using ErsatzTV.Application;
|
||||||
using ErsatzTV.Application.Channels;
|
using ErsatzTV.Application.Channels;
|
||||||
using ErsatzTV.Application.Playouts;
|
using ErsatzTV.Application.Playouts;
|
||||||
|
using ErsatzTV.Application.Templates;
|
||||||
using ErsatzTV.Controllers.Api.Requests;
|
using ErsatzTV.Controllers.Api.Requests;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Api.Channels;
|
using ErsatzTV.Core.Api.Channels;
|
||||||
@@ -43,6 +44,22 @@ public class ChannelController(ChannelWriter<IBackgroundServiceRequest> workerCh
|
|||||||
CancellationToken cancellationToken) =>
|
CancellationToken cancellationToken) =>
|
||||||
await mediator.Send(new GetChannelGuideData(start, end), cancellationToken);
|
await mediator.Send(new GetChannelGuideData(start, end), cancellationToken);
|
||||||
|
|
||||||
|
[HttpGet("/api/channels/music-video-credits-templates", Name = "GetMusicVideoCreditsTemplates")]
|
||||||
|
[Tags("Channels")]
|
||||||
|
[EndpointSummary("Get available music video credits template names")]
|
||||||
|
[EndpointGroupName("general")]
|
||||||
|
[ProducesResponseType(typeof(List<string>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<List<string>> GetMusicVideoCreditsTemplates(CancellationToken cancellationToken) =>
|
||||||
|
await mediator.Send(new GetMusicVideoCreditTemplates(), cancellationToken);
|
||||||
|
|
||||||
|
[HttpGet("/api/channels/stream-selectors", Name = "GetChannelStreamSelectors")]
|
||||||
|
[Tags("Channels")]
|
||||||
|
[EndpointSummary("Get available channel stream selector names")]
|
||||||
|
[EndpointGroupName("general")]
|
||||||
|
[ProducesResponseType(typeof(List<string>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<List<string>> GetStreamSelectors(CancellationToken cancellationToken) =>
|
||||||
|
await mediator.Send(new GetChannelStreamSelectors(), cancellationToken);
|
||||||
|
|
||||||
[HttpGet("/api/channels/{id:int}", Name = "GetChannelById")]
|
[HttpGet("/api/channels/{id:int}", Name = "GetChannelById")]
|
||||||
[Tags("Channels")]
|
[Tags("Channels")]
|
||||||
[EndpointSummary("Get a channel by id")]
|
[EndpointSummary("Get a channel by id")]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ErsatzTV.Application.Filler;
|
|||||||
using ErsatzTV.Controllers.Api.Requests;
|
using ErsatzTV.Controllers.Api.Requests;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Api.Filler;
|
using ErsatzTV.Core.Api.Filler;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
using ErsatzTV.Extensions;
|
using ErsatzTV.Extensions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -16,10 +17,13 @@ public class FillerPresetController(IMediator mediator) : ControllerBase
|
|||||||
[HttpGet("/api/filler-presets", Name = "GetFillerPresets")]
|
[HttpGet("/api/filler-presets", Name = "GetFillerPresets")]
|
||||||
[Tags("Filler Presets")]
|
[Tags("Filler Presets")]
|
||||||
[EndpointSummary("Get all filler presets")]
|
[EndpointSummary("Get all filler presets")]
|
||||||
|
[EndpointDescription("Optionally filter to a single filler kind via the fillerKind query parameter.")]
|
||||||
[EndpointGroupName("general")]
|
[EndpointGroupName("general")]
|
||||||
[ProducesResponseType(typeof(List<FillerPresetResponseModel>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(List<FillerPresetResponseModel>), StatusCodes.Status200OK)]
|
||||||
public async Task<List<FillerPresetResponseModel>> GetAll(CancellationToken cancellationToken) =>
|
public async Task<List<FillerPresetResponseModel>> GetAll(
|
||||||
await mediator.Send(new GetAllFillerPresetsForApi(), cancellationToken);
|
[FromQuery] FillerKind? fillerKind,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
await mediator.Send(new GetAllFillerPresetsForApi(fillerKind), cancellationToken);
|
||||||
|
|
||||||
[HttpGet("/api/filler-presets/{id:int}", Name = "GetFillerPresetById")]
|
[HttpGet("/api/filler-presets/{id:int}", Name = "GetFillerPresetById")]
|
||||||
[Tags("Filler Presets")]
|
[Tags("Filler Presets")]
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using ErsatzTV.Application.MediaItems;
|
||||||
|
using ErsatzTV.Core.Api.MediaItems;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Controllers.Api;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
public class LanguagesController(IMediator mediator) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("/api/languages", Name = "GetLanguages")]
|
||||||
|
[Tags("Languages")]
|
||||||
|
[EndpointSummary("Get all available language codes")]
|
||||||
|
[EndpointGroupName("general")]
|
||||||
|
[ProducesResponseType(typeof(List<LanguageCodeResponseModel>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<List<LanguageCodeResponseModel>> GetLanguages(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
List<LanguageCodeViewModel> languages = await mediator.Send(new GetAllLanguageCodes(), cancellationToken);
|
||||||
|
return languages
|
||||||
|
.Select(l => new LanguageCodeResponseModel(l.ThreeLetterISOLanguageName, l.EnglishName))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
using ErsatzTV.Application.ProgramSchedules;
|
using ErsatzTV.Application.ProgramSchedules;
|
||||||
using ErsatzTV.Controllers.Api.Requests;
|
using ErsatzTV.Controllers.Api.Requests;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Api.Scheduling;
|
||||||
using ErsatzTV.Extensions;
|
using ErsatzTV.Extensions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -104,7 +106,7 @@ public class ScheduleController(IMediator mediator) : ControllerBase
|
|||||||
"nullable durationEstimate and the envelope carries a nullable totalDurationEstimate. Estimates are " +
|
"nullable durationEstimate and the envelope carries a nullable totalDurationEstimate. Estimates are " +
|
||||||
"derived from referenced collection/media runtimes and are null when unbounded or unknown.")]
|
"derived from referenced collection/media runtimes and are null when unbounded or unknown.")]
|
||||||
[EndpointGroupName("general")]
|
[EndpointGroupName("general")]
|
||||||
[ProducesResponseType(typeof(ProgramScheduleItemsWithDurationViewModel), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ScheduleItemsResponseModel), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> GetItems(int id, CancellationToken cancellationToken)
|
public async Task<IActionResult> GetItems(int id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -116,14 +118,14 @@ public class ScheduleController(IMediator mediator) : ControllerBase
|
|||||||
|
|
||||||
ProgramScheduleItemsWithDurationViewModel items =
|
ProgramScheduleItemsWithDurationViewModel items =
|
||||||
await mediator.Send(new GetProgramScheduleItemsWithDurations(id), cancellationToken);
|
await mediator.Send(new GetProgramScheduleItemsWithDurations(id), cancellationToken);
|
||||||
return new OkObjectResult(items);
|
return new OkObjectResult(ScheduleItemResponseMapper.ProjectToResponseModel(items));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("/api/schedules/{id:int}/items")]
|
[HttpPost("/api/schedules/{id:int}/items")]
|
||||||
[Tags("Schedules")]
|
[Tags("Schedules")]
|
||||||
[EndpointSummary("Add a schedule item")]
|
[EndpointSummary("Add a schedule item")]
|
||||||
[EndpointGroupName("general")]
|
[EndpointGroupName("general")]
|
||||||
[ProducesResponseType(typeof(ProgramScheduleItemViewModel), StatusCodes.Status201Created)]
|
[ProducesResponseType(typeof(ScheduleItemResponseModel), StatusCodes.Status201Created)]
|
||||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
||||||
public async Task<IActionResult> AddItem(
|
public async Task<IActionResult> AddItem(
|
||||||
@@ -133,14 +135,16 @@ public class ScheduleController(IMediator mediator) : ControllerBase
|
|||||||
{
|
{
|
||||||
Either<BaseError, ProgramScheduleItemViewModel> result =
|
Either<BaseError, ProgramScheduleItemViewModel> result =
|
||||||
await mediator.Send(request.ToAddCommand(id), cancellationToken);
|
await mediator.Send(request.ToAddCommand(id), cancellationToken);
|
||||||
return result.ToCreatedResult(item => $"/api/schedules/{id}/items/{item.Id}", item => item);
|
return result.ToCreatedResult(
|
||||||
|
item => $"/api/schedules/{id}/items/{item.Id}",
|
||||||
|
item => ScheduleItemResponseMapper.ProjectToResponseModel(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("/api/schedules/{id:int}/items")]
|
[HttpPut("/api/schedules/{id:int}/items")]
|
||||||
[Tags("Schedules")]
|
[Tags("Schedules")]
|
||||||
[EndpointSummary("Replace schedule items")]
|
[EndpointSummary("Replace schedule items")]
|
||||||
[EndpointGroupName("general")]
|
[EndpointGroupName("general")]
|
||||||
[ProducesResponseType(typeof(IEnumerable<ProgramScheduleItemViewModel>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(List<ScheduleItemResponseModel>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
||||||
public async Task<IActionResult> ReplaceItems(
|
public async Task<IActionResult> ReplaceItems(
|
||||||
@@ -150,7 +154,9 @@ public class ScheduleController(IMediator mediator) : ControllerBase
|
|||||||
{
|
{
|
||||||
Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>> result =
|
Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>> result =
|
||||||
await mediator.Send(request.ToCommand(id), cancellationToken);
|
await mediator.Send(request.ToCommand(id), cancellationToken);
|
||||||
return result.ToUpdatedResult();
|
return result
|
||||||
|
.Map(items => items.Select(ScheduleItemResponseMapper.ProjectToResponseModel).ToList())
|
||||||
|
.ToUpdatedResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("/api/schedules/{id:int}/items/{itemId:int}")]
|
[HttpDelete("/api/schedules/{id:int}/items/{itemId:int}")]
|
||||||
|
|||||||
Reference in New Issue
Block a user