From 102959ed4913067400cd60b96e6375d459451115 Mon Sep 17 00:00:00 2001 From: Adam Jacob Muller Date: Mon, 22 Dec 2025 21:13:44 -0500 Subject: [PATCH] Update API controllers with flat DTOs and OpenAPI attributes Refactor all API controllers to: - Use flat API response DTOs instead of nested ViewModels - Add [EndpointGroupName("general")] attributes for OpenAPI grouping - Add [Tags] and [EndpointSummary] attributes for documentation - Add mapping methods to convert ViewModels to flat DTOs Controllers updated: - BlockController: Use BlockItemApiResponse with inline mappings - CollectionController: Use CollectionApiResponse, MultiCollectionApiResponse - DecoController: Use DecoApiResponse with break content mappings - PlaylistController: Use PlaylistItemApiResponse - ScheduleController: Use ScheduleItemApiResponse with filler mappings - TemplateController: Add OpenAPI attributes and request models - ChannelController, ConfigController, EmbyController, FillerController, JellyfinController, LibrariesController, MaintenanceController, MediaController, PlayoutController, PlexController, ResolutionController, ScannerController, ScriptedScheduleController, SearchController, SessionController, SmartCollectionController, TraktController, VersionController, WatermarkController: Add OpenAPI attributes This enables complete OpenAPI schema generation without depth limit errors. --- ErsatzTV/Controllers/Api/BlockController.cs | 77 +++++++++++- ErsatzTV/Controllers/Api/ChannelController.cs | 11 +- .../Controllers/Api/CollectionController.cs | 69 +++++++++-- ErsatzTV/Controllers/Api/ConfigController.cs | 10 +- ErsatzTV/Controllers/Api/DecoController.cs | 114 +++++++++++++++++- ErsatzTV/Controllers/Api/EmbyController.cs | 9 +- .../Api/FFmpegProfileController.cs | 10 +- ErsatzTV/Controllers/Api/FillerController.cs | 8 +- .../Controllers/Api/JellyfinController.cs | 9 +- .../Controllers/Api/LibrariesController.cs | 12 +- .../Controllers/Api/MaintenanceController.cs | 6 +- ErsatzTV/Controllers/Api/MediaController.cs | 8 +- .../Controllers/Api/PlaylistController.cs | 62 +++++++++- ErsatzTV/Controllers/Api/PlayoutController.cs | 16 ++- ErsatzTV/Controllers/Api/PlexController.cs | 8 +- .../Controllers/Api/ResolutionController.cs | 6 +- ErsatzTV/Controllers/Api/ScannerController.cs | 3 + .../Controllers/Api/ScheduleController.cs | 107 +++++++++++++++- .../Api/ScriptedScheduleController.cs | 28 +++++ ErsatzTV/Controllers/Api/SearchController.cs | 11 +- ErsatzTV/Controllers/Api/SessionController.cs | 4 +- .../Api/SmartCollectionController.cs | 5 +- .../Controllers/Api/TemplateController.cs | 13 +- ErsatzTV/Controllers/Api/TraktController.cs | 7 +- ErsatzTV/Controllers/Api/VersionController.cs | 3 +- .../Controllers/Api/WatermarkController.cs | 8 +- 26 files changed, 579 insertions(+), 45 deletions(-) diff --git a/ErsatzTV/Controllers/Api/BlockController.cs b/ErsatzTV/Controllers/Api/BlockController.cs index 142969761..c23d73406 100644 --- a/ErsatzTV/Controllers/Api/BlockController.cs +++ b/ErsatzTV/Controllers/Api/BlockController.cs @@ -1,6 +1,10 @@ #nullable enable using System.ComponentModel.DataAnnotations; +using ErsatzTV.Application.Graphics; +using ErsatzTV.Application.MediaCollections; +using ErsatzTV.Application.MediaItems; using ErsatzTV.Application.Scheduling; +using ErsatzTV.Application.Watermarks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Scheduling; @@ -11,19 +15,74 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] public class BlockController(IMediator mediator) : ControllerBase { + private static CollectionApiResponse? ToApiResponse(MediaCollectionViewModel? c) => + c == null ? null : new(c.Id, c.Name, c.CollectionType, c.UseCustomPlaybackOrder, c.State); + + private static SmartCollectionApiResponse? ToApiResponse(SmartCollectionViewModel? s) => + s == null ? null : new(s.Id, s.Name, s.Query); + + private static MultiCollectionApiResponse? ToApiResponse(MultiCollectionViewModel? mc) => + mc == null ? null : new( + mc.Id, + mc.Name, + mc.Items?.Select(i => new MultiCollectionItemApiResponse( + i.MultiCollectionId, + i.Collection?.Id ?? 0, + i.Collection?.Name ?? "", + i.Collection?.CollectionType ?? CollectionType.Collection, + i.Collection?.UseCustomPlaybackOrder ?? false, + i.Collection?.State ?? MediaItemState.Normal, + i.ScheduleAsGroup, + i.PlaybackOrder)).ToList() ?? [], + mc.SmartItems?.Select(s => new MultiCollectionSmartItemApiResponse( + s.MultiCollectionId, + s.SmartCollection?.Id ?? 0, + s.SmartCollection?.Name ?? "", + s.SmartCollection?.Query ?? "", + s.ScheduleAsGroup, + s.PlaybackOrder)).ToList() ?? []); + + private static MediaItemApiResponse? ToMediaItemApiResponse(NamedMediaItemViewModel? m) => + m == null ? null : new(m.MediaItemId, m.Name); + + private static WatermarkApiResponse ToApiResponse(WatermarkViewModel w) => + new(w.Id, w.Name, w.Image?.Path, w.Mode, w.ImageSource, w.Location, w.Size, w.Width, + w.HorizontalMargin, w.VerticalMargin, w.FrequencyMinutes, w.DurationSeconds, + w.Opacity, w.PlaceWithinSourceContent, w.OpacityExpression, w.ZIndex); + + private static GraphicsElementApiResponse ToApiResponse(GraphicsElementViewModel g) => + new(g.Id, g.Name, g.FileName); + + private static BlockItemApiResponse ToApiResponse(BlockItemViewModel item) => + new( + item.Id, + item.Index, + item.CollectionType, + ToApiResponse(item.Collection), + ToApiResponse(item.MultiCollection), + ToApiResponse(item.SmartCollection), + ToMediaItemApiResponse(item.MediaItem), + item.SearchTitle, + item.SearchQuery, + item.PlaybackOrder, + item.IncludeInProgramGuide, + item.DisableWatermarks, + item.Watermarks?.Select(ToApiResponse).ToList() ?? [], + item.GraphicsElements?.Select(ToApiResponse).ToList() ?? []); // Block Groups [HttpGet("/api/blocks/groups", Name = "GetBlockGroups")] [Tags("Blocks")] [EndpointSummary("Get all block groups")] + [EndpointGroupName("general")] public async Task> GetBlockGroups(CancellationToken cancellationToken) => await mediator.Send(new GetAllBlockGroups(), cancellationToken); [HttpPost("/api/blocks/groups", Name = "CreateBlockGroup")] [Tags("Blocks")] [EndpointSummary("Create a block group")] + [EndpointGroupName("general")] public async Task CreateBlockGroup( [Required] [FromBody] CreateBlockGroupRequest request, CancellationToken cancellationToken) @@ -36,6 +95,7 @@ public class BlockController(IMediator mediator) : ControllerBase [HttpDelete("/api/blocks/groups/{id:int}", Name = "DeleteBlockGroup")] [Tags("Blocks")] [EndpointSummary("Delete a block group")] + [EndpointGroupName("general")] public async Task DeleteBlockGroup(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeleteBlockGroup(id), cancellationToken); @@ -46,18 +106,21 @@ public class BlockController(IMediator mediator) : ControllerBase [HttpGet("/api/blocks/groups/{groupId:int}/blocks", Name = "GetBlocksByGroup")] [Tags("Blocks")] [EndpointSummary("Get blocks by group")] + [EndpointGroupName("general")] public async Task> GetBlocksByGroup(int groupId, CancellationToken cancellationToken) => await mediator.Send(new GetBlocksByBlockGroupId(groupId), cancellationToken); [HttpGet("/api/blocks", Name = "GetAllBlocks")] [Tags("Blocks")] [EndpointSummary("Get all blocks")] + [EndpointGroupName("general")] public async Task> GetAllBlocks(CancellationToken cancellationToken) => await mediator.Send(new GetAllBlocks(), cancellationToken); [HttpGet("/api/blocks/{id:int}", Name = "GetBlockById")] [Tags("Blocks")] [EndpointSummary("Get block by ID")] + [EndpointGroupName("general")] public async Task GetBlockById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetBlockById(id), cancellationToken); @@ -67,12 +130,17 @@ public class BlockController(IMediator mediator) : ControllerBase [HttpGet("/api/blocks/{id:int}/items", Name = "GetBlockItems")] [Tags("Blocks")] [EndpointSummary("Get block items")] - public async Task> GetBlockItems(int id, CancellationToken cancellationToken) => - await mediator.Send(new GetBlockItems(id), cancellationToken); + [EndpointGroupName("general")] + public async Task> GetBlockItems(int id, CancellationToken cancellationToken) + { + List items = await mediator.Send(new GetBlockItems(id), cancellationToken); + return items.Select(ToApiResponse).ToList(); + } [HttpPost("/api/blocks", Name = "CreateBlock")] [Tags("Blocks")] [EndpointSummary("Create a block")] + [EndpointGroupName("general")] public async Task CreateBlock( [Required] [FromBody] CreateBlockRequest request, CancellationToken cancellationToken) @@ -85,6 +153,7 @@ public class BlockController(IMediator mediator) : ControllerBase [HttpDelete("/api/blocks/{id:int}", Name = "DeleteBlock")] [Tags("Blocks")] [EndpointSummary("Delete a block")] + [EndpointGroupName("general")] public async Task DeleteBlock(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeleteBlock(id), cancellationToken); @@ -94,6 +163,7 @@ public class BlockController(IMediator mediator) : ControllerBase [HttpPost("/api/blocks/{id:int}/copy", Name = "CopyBlock")] [Tags("Blocks")] [EndpointSummary("Copy a block")] + [EndpointGroupName("general")] public async Task CopyBlock( int id, [Required] [FromBody] CopyBlockRequest request, @@ -107,6 +177,7 @@ public class BlockController(IMediator mediator) : ControllerBase [HttpPut("/api/blocks/{id:int}/items", Name = "ReplaceBlockItems")] [Tags("Blocks")] [EndpointSummary("Replace block items")] + [EndpointGroupName("general")] public async Task ReplaceBlockItems( int id, [Required] [FromBody] ReplaceBlockItemsRequest request, diff --git a/ErsatzTV/Controllers/Api/ChannelController.cs b/ErsatzTV/Controllers/Api/ChannelController.cs index b35b4ddae..5288220a9 100644 --- a/ErsatzTV/Controllers/Api/ChannelController.cs +++ b/ErsatzTV/Controllers/Api/ChannelController.cs @@ -16,18 +16,21 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] public class ChannelController(ChannelWriter workerChannel, IMediator mediator) : ControllerBase { [HttpGet("/api/channels", Name = "GetAllChannels")] [Tags("Channels")] [EndpointSummary("Get all channels")] + [EndpointGroupName("general")] + public async Task> GetAll(CancellationToken cancellationToken) => await mediator.Send(new GetAllChannelsForApi(), cancellationToken); [HttpGet("/api/channels/{id:int}", Name = "GetChannelById")] [Tags("Channels")] [EndpointSummary("Get channel by ID")] + [EndpointGroupName("general")] + public async Task GetChannelById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetChannelById(id), cancellationToken); @@ -37,6 +40,7 @@ public class ChannelController(ChannelWriter workerCh [HttpGet("/api/channels/by-number/{channelNumber}", Name = "GetChannelByNumber")] [Tags("Channels")] [EndpointSummary("Get channel by number")] + [EndpointGroupName("general")] public async Task GetChannelByNumber(string channelNumber, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetChannelByNumber(channelNumber), cancellationToken); @@ -46,6 +50,7 @@ public class ChannelController(ChannelWriter workerCh [HttpPost("/api/channels", Name = "CreateChannel")] [Tags("Channels")] [EndpointSummary("Create a channel")] + [EndpointGroupName("general")] public async Task CreateChannel( [Required] [FromBody] CreateChannelRequest request, CancellationToken cancellationToken) @@ -89,6 +94,7 @@ public class ChannelController(ChannelWriter workerCh [HttpPut("/api/channels/{id:int}", Name = "UpdateChannel")] [Tags("Channels")] [EndpointSummary("Update a channel")] + [EndpointGroupName("general")] public async Task UpdateChannel( int id, [Required] [FromBody] UpdateChannelRequest request, @@ -134,6 +140,7 @@ public class ChannelController(ChannelWriter workerCh [HttpDelete("/api/channels/{id:int}", Name = "DeleteChannel")] [Tags("Channels")] [EndpointSummary("Delete a channel")] + [EndpointGroupName("general")] public async Task DeleteChannel(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new DeleteChannel(id), cancellationToken); @@ -143,6 +150,7 @@ public class ChannelController(ChannelWriter workerCh [HttpPost("/api/channels/{channelNumber}/playout/reset", Name = "ResetChannelPlayout")] [Tags("Channels")] [EndpointSummary("Reset channel playout")] + [EndpointGroupName("general")] public async Task ResetPlayout(string channelNumber) { Option maybePlayoutId = await mediator.Send(new GetPlayoutIdByChannelNumber(channelNumber)); @@ -158,6 +166,7 @@ public class ChannelController(ChannelWriter workerCh [HttpPost("/api/channels/{channelNumber}/playout/build", Name = "BuildChannelPlayout")] [Tags("Channels")] [EndpointSummary("Build channel playout")] + [EndpointGroupName("general")] public async Task BuildPlayout(string channelNumber) { Option maybePlayoutId = await mediator.Send(new GetPlayoutIdByChannelNumber(channelNumber)); diff --git a/ErsatzTV/Controllers/Api/CollectionController.cs b/ErsatzTV/Controllers/Api/CollectionController.cs index 19f61a2c7..595176f7a 100644 --- a/ErsatzTV/Controllers/Api/CollectionController.cs +++ b/ErsatzTV/Controllers/Api/CollectionController.cs @@ -11,38 +11,70 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] public class CollectionController(IMediator mediator) : ControllerBase { + private static CollectionApiResponse ToApiResponse(MediaCollectionViewModel collection) => + new(collection.Id, collection.Name, collection.CollectionType, collection.UseCustomPlaybackOrder, collection.State); + + private static MultiCollectionApiResponse ToApiResponse(MultiCollectionViewModel multiCollection) => + new( + multiCollection.Id, + multiCollection.Name, + multiCollection.Items?.Select(i => new MultiCollectionItemApiResponse( + i.MultiCollectionId, + i.Collection?.Id ?? 0, + i.Collection?.Name ?? "", + i.Collection?.CollectionType ?? CollectionType.Collection, + i.Collection?.UseCustomPlaybackOrder ?? false, + i.Collection?.State ?? MediaItemState.Normal, + i.ScheduleAsGroup, + i.PlaybackOrder)).ToList() ?? [], + multiCollection.SmartItems?.Select(s => new MultiCollectionSmartItemApiResponse( + s.MultiCollectionId, + s.SmartCollection?.Id ?? 0, + s.SmartCollection?.Name ?? "", + s.SmartCollection?.Query ?? "", + s.ScheduleAsGroup, + s.PlaybackOrder)).ToList() ?? []); // Collections [HttpGet("/api/collections", Name = "GetCollections")] [Tags("Collections")] [EndpointSummary("Get all collections (paginated)")] - public async Task GetCollections( + [EndpointGroupName("general")] + public async Task GetCollections( [FromQuery] string query = "", [FromQuery] int pageNumber = 0, [FromQuery] int pageSize = 50, - CancellationToken cancellationToken = default) => - await mediator.Send(new GetPagedCollections(query, pageNumber, pageSize), cancellationToken); + CancellationToken cancellationToken = default) + { + PagedMediaCollectionsViewModel result = await mediator.Send(new GetPagedCollections(query, pageNumber, pageSize), cancellationToken); + return new PagedCollectionsApiResponse(result.TotalCount, result.Page.Select(ToApiResponse).ToList()); + } [HttpGet("/api/collections/all", Name = "GetAllCollections")] [Tags("Collections")] [EndpointSummary("Get all collections")] - public async Task> GetAllCollections(CancellationToken cancellationToken) => - await mediator.Send(new GetAllCollections(), cancellationToken); + [EndpointGroupName("general")] + public async Task> GetAllCollections(CancellationToken cancellationToken) + { + List collections = await mediator.Send(new GetAllCollections(), cancellationToken); + return collections.Select(ToApiResponse).ToList(); + } [HttpGet("/api/collections/{id:int}", Name = "GetCollectionById")] [Tags("Collections")] [EndpointSummary("Get collection by ID")] + [EndpointGroupName("general")] public async Task GetCollectionById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetCollectionById(id), cancellationToken); - return result.Match(Ok, () => NotFound()); + return result.Match(c => Ok(ToApiResponse(c)), () => NotFound()); } [HttpGet("/api/collections/{id:int}/items", Name = "GetCollectionItems")] [Tags("Collections")] [EndpointSummary("Get collection items")] + [EndpointGroupName("general")] public async Task GetCollectionItems(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new GetCollectionCards(id), cancellationToken); @@ -52,17 +84,19 @@ public class CollectionController(IMediator mediator) : ControllerBase [HttpPost("/api/collections", Name = "CreateCollection")] [Tags("Collections")] [EndpointSummary("Create a new collection")] + [EndpointGroupName("general")] public async Task CreateCollection( [Required] [FromBody] CreateCollectionRequest request, CancellationToken cancellationToken) { Either result = await mediator.Send(new CreateCollection(request.Name), cancellationToken); - return result.Match(Ok, error => Problem(error.ToString())); + return result.Match(c => Ok(ToApiResponse(c)), error => Problem(error.ToString())); } [HttpPut("/api/collections/{id:int}", Name = "UpdateCollection")] [Tags("Collections")] [EndpointSummary("Update a collection")] + [EndpointGroupName("general")] public async Task UpdateCollection( int id, [Required] [FromBody] UpdateCollectionRequest request, @@ -75,6 +109,7 @@ public class CollectionController(IMediator mediator) : ControllerBase [HttpDelete("/api/collections/{id:int}", Name = "DeleteCollection")] [Tags("Collections")] [EndpointSummary("Delete a collection")] + [EndpointGroupName("general")] public async Task DeleteCollection(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new DeleteCollection(id), cancellationToken); @@ -84,6 +119,7 @@ public class CollectionController(IMediator mediator) : ControllerBase [HttpPost("/api/collections/{id:int}/items", Name = "AddItemsToCollection")] [Tags("Collections")] [EndpointSummary("Add items to a collection")] + [EndpointGroupName("general")] public async Task AddItemsToCollection( int id, [Required] [FromBody] AddItemsToCollectionRequest request, @@ -109,6 +145,7 @@ public class CollectionController(IMediator mediator) : ControllerBase [HttpDelete("/api/collections/{id:int}/items", Name = "RemoveItemsFromCollection")] [Tags("Collections")] [EndpointSummary("Remove items from a collection")] + [EndpointGroupName("general")] public async Task RemoveItemsFromCollection( int id, [Required] [FromBody] RemoveItemsFromCollectionRequest request, @@ -123,21 +160,27 @@ public class CollectionController(IMediator mediator) : ControllerBase [HttpGet("/api/collections/multi", Name = "GetMultiCollections")] [Tags("Collections")] [EndpointSummary("Get all multi-collections")] - public async Task> GetMultiCollections(CancellationToken cancellationToken) => - await mediator.Send(new GetAllMultiCollections(), cancellationToken); + [EndpointGroupName("general")] + public async Task> GetMultiCollections(CancellationToken cancellationToken) + { + List collections = await mediator.Send(new GetAllMultiCollections(), cancellationToken); + return collections.Select(ToApiResponse).ToList(); + } [HttpGet("/api/collections/multi/{id:int}", Name = "GetMultiCollectionById")] [Tags("Collections")] [EndpointSummary("Get multi-collection by ID")] + [EndpointGroupName("general")] public async Task GetMultiCollectionById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetMultiCollectionById(id), cancellationToken); - return result.Match(Ok, () => NotFound()); + return result.Match(c => Ok(ToApiResponse(c)), () => NotFound()); } [HttpPost("/api/collections/multi", Name = "CreateMultiCollection")] [Tags("Collections")] [EndpointSummary("Create a multi-collection")] + [EndpointGroupName("general")] public async Task CreateMultiCollection( [Required] [FromBody] CreateMultiCollectionRequest request, CancellationToken cancellationToken) @@ -146,12 +189,13 @@ public class CollectionController(IMediator mediator) : ControllerBase i.CollectionId, i.SmartCollectionId, i.ScheduleAsGroup, i.PlaybackOrder)).ToList() ?? []; Either result = await mediator.Send( new CreateMultiCollection(request.Name, items), cancellationToken); - return result.Match(Ok, error => Problem(error.ToString())); + return result.Match(c => Ok(ToApiResponse(c)), error => Problem(error.ToString())); } [HttpPut("/api/collections/multi/{id:int}", Name = "UpdateMultiCollection")] [Tags("Collections")] [EndpointSummary("Update a multi-collection")] + [EndpointGroupName("general")] public async Task UpdateMultiCollection( int id, [Required] [FromBody] UpdateMultiCollectionRequest request, @@ -167,6 +211,7 @@ public class CollectionController(IMediator mediator) : ControllerBase [HttpDelete("/api/collections/multi/{id:int}", Name = "DeleteMultiCollection")] [Tags("Collections")] [EndpointSummary("Delete a multi-collection")] + [EndpointGroupName("general")] public async Task DeleteMultiCollection(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new DeleteMultiCollection(id), cancellationToken); diff --git a/ErsatzTV/Controllers/Api/ConfigController.cs b/ErsatzTV/Controllers/Api/ConfigController.cs index d5e5918af..a65cbd8d6 100644 --- a/ErsatzTV/Controllers/Api/ConfigController.cs +++ b/ErsatzTV/Controllers/Api/ConfigController.cs @@ -7,18 +7,20 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class ConfigController(IMediator mediator) : ControllerBase { [HttpGet("/api/config/playout", Name = "GetPlayoutSettings")] [Tags("Configuration")] [EndpointSummary("Get playout settings")] + [EndpointGroupName("general")] public async Task GetPlayoutSettings(CancellationToken cancellationToken) => await mediator.Send(new GetPlayoutSettings(), cancellationToken); [HttpPut("/api/config/playout", Name = "UpdatePlayoutSettings")] [Tags("Configuration")] [EndpointSummary("Update playout settings")] + [EndpointGroupName("general")] public async Task UpdatePlayoutSettings( [Required] [FromBody] PlayoutSettingsViewModel settings, CancellationToken cancellationToken) @@ -30,12 +32,14 @@ public class ConfigController(IMediator mediator) : ControllerBase [HttpGet("/api/config/xmltv", Name = "GetXmltvSettings")] [Tags("Configuration")] [EndpointSummary("Get XMLTV settings")] + [EndpointGroupName("general")] public async Task GetXmltvSettings(CancellationToken cancellationToken) => await mediator.Send(new GetXmltvSettings(), cancellationToken); [HttpPut("/api/config/xmltv", Name = "UpdateXmltvSettings")] [Tags("Configuration")] [EndpointSummary("Update XMLTV settings")] + [EndpointGroupName("general")] public async Task UpdateXmltvSettings( [Required] [FromBody] XmltvSettingsViewModel settings, CancellationToken cancellationToken) @@ -47,12 +51,14 @@ public class ConfigController(IMediator mediator) : ControllerBase [HttpGet("/api/config/logging", Name = "GetLoggingSettings")] [Tags("Configuration")] [EndpointSummary("Get logging settings")] + [EndpointGroupName("general")] public async Task GetLoggingSettings(CancellationToken cancellationToken) => await mediator.Send(new GetLoggingSettings(), cancellationToken); [HttpPut("/api/config/logging", Name = "UpdateLoggingSettings")] [Tags("Configuration")] [EndpointSummary("Update logging settings")] + [EndpointGroupName("general")] public async Task UpdateLoggingSettings( [Required] [FromBody] LoggingSettingsViewModel settings, CancellationToken cancellationToken) @@ -64,12 +70,14 @@ public class ConfigController(IMediator mediator) : ControllerBase [HttpGet("/api/config/library-refresh-interval", Name = "GetLibraryRefreshInterval")] [Tags("Configuration")] [EndpointSummary("Get library refresh interval")] + [EndpointGroupName("general")] public async Task GetLibraryRefreshInterval(CancellationToken cancellationToken) => await mediator.Send(new GetLibraryRefreshInterval(), cancellationToken); [HttpPut("/api/config/library-refresh-interval", Name = "UpdateLibraryRefreshInterval")] [Tags("Configuration")] [EndpointSummary("Update library refresh interval")] + [EndpointGroupName("general")] public async Task UpdateLibraryRefreshInterval( [Required] [FromBody] UpdateLibraryRefreshIntervalRequest request, CancellationToken cancellationToken) diff --git a/ErsatzTV/Controllers/Api/DecoController.cs b/ErsatzTV/Controllers/Api/DecoController.cs index f85fa9780..283b79624 100644 --- a/ErsatzTV/Controllers/Api/DecoController.cs +++ b/ErsatzTV/Controllers/Api/DecoController.cs @@ -1,7 +1,11 @@ #nullable enable using System.ComponentModel.DataAnnotations; +using ErsatzTV.Application.Graphics; +using ErsatzTV.Application.MediaCollections; +using ErsatzTV.Application.MediaItems; using ErsatzTV.Application.Scheduling; using ErsatzTV.Application.Tree; +using ErsatzTV.Application.Watermarks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Scheduling; @@ -12,19 +16,99 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] public class DecoController(IMediator mediator) : ControllerBase { + private static CollectionApiResponse? ToApiResponse(MediaCollectionViewModel? c) => + c == null ? null : new(c.Id, c.Name, c.CollectionType, c.UseCustomPlaybackOrder, c.State); + + private static SmartCollectionApiResponse? ToApiResponse(SmartCollectionViewModel? s) => + s == null ? null : new(s.Id, s.Name, s.Query); + + private static MultiCollectionApiResponse? ToApiResponse(MultiCollectionViewModel? mc) => + mc == null ? null : new( + mc.Id, + mc.Name, + mc.Items?.Select(i => new MultiCollectionItemApiResponse( + i.MultiCollectionId, + i.Collection?.Id ?? 0, + i.Collection?.Name ?? "", + i.Collection?.CollectionType ?? CollectionType.Collection, + i.Collection?.UseCustomPlaybackOrder ?? false, + i.Collection?.State ?? MediaItemState.Normal, + i.ScheduleAsGroup, + i.PlaybackOrder)).ToList() ?? [], + mc.SmartItems?.Select(s => new MultiCollectionSmartItemApiResponse( + s.MultiCollectionId, + s.SmartCollection?.Id ?? 0, + s.SmartCollection?.Name ?? "", + s.SmartCollection?.Query ?? "", + s.ScheduleAsGroup, + s.PlaybackOrder)).ToList() ?? []); + + private static MediaItemApiResponse? ToMediaItemApiResponse(NamedMediaItemViewModel? m) => + m == null ? null : new(m.MediaItemId, m.Name); + + private static PlaylistApiResponse? ToApiResponse(PlaylistViewModel? p) => + p == null ? null : new(p.Id, p.PlaylistGroupId, p.Name, p.IsSystem); + + private static WatermarkApiResponse ToApiResponse(WatermarkViewModel w) => + new(w.Id, w.Name, w.Image?.Path, w.Mode, w.ImageSource, w.Location, w.Size, w.Width, + w.HorizontalMargin, w.VerticalMargin, w.FrequencyMinutes, w.DurationSeconds, + w.Opacity, w.PlaceWithinSourceContent, w.OpacityExpression, w.ZIndex); + + private static GraphicsElementApiResponse ToApiResponse(GraphicsElementViewModel g) => + new(g.Id, g.Name, g.FileName); + + private static DecoApiResponse ToApiResponse(DecoViewModel deco) => + new( + deco.Id, + deco.DecoGroupId, + deco.DecoGroupName, + deco.Name, + deco.WatermarkMode, + deco.Watermarks?.Select(ToApiResponse).ToList() ?? [], + deco.UseWatermarkDuringFiller, + deco.GraphicsElementsMode, + deco.GraphicsElements?.Select(ToApiResponse).ToList() ?? [], + deco.UseGraphicsElementsDuringFiller, + deco.BreakContentMode, + deco.BreakContent?.Select(ToApiResponse).ToList() ?? [], + deco.DefaultFillerMode, + deco.DefaultFillerCollectionType, + ToApiResponse(deco.DefaultFillerCollection), + ToMediaItemApiResponse(deco.DefaultFillerMediaItem), + ToApiResponse(deco.DefaultFillerMultiCollection), + ToApiResponse(deco.DefaultFillerSmartCollection), + deco.DefaultFillerTrimToFit, + deco.DeadAirFallbackMode, + deco.DeadAirFallbackCollectionType, + ToApiResponse(deco.DeadAirFallbackCollection), + ToMediaItemApiResponse(deco.DeadAirFallbackMediaItem), + ToApiResponse(deco.DeadAirFallbackMultiCollection), + ToApiResponse(deco.DeadAirFallbackSmartCollection)); + + private static DecoBreakContentApiResponse ToApiResponse(DecoBreakContentViewModel bc) => + new( + bc.Id, + bc.CollectionType, + ToApiResponse(bc.Collection), + ToMediaItemApiResponse(bc.MediaItem), + ToApiResponse(bc.MultiCollection), + ToApiResponse(bc.SmartCollection), + ToApiResponse(bc.Playlist), + bc.Placement); // Deco Groups [HttpGet("/api/decos/groups", Name = "GetDecoGroups")] [Tags("Decos")] [EndpointSummary("Get all deco groups")] + [EndpointGroupName("general")] public async Task> GetDecoGroups(CancellationToken cancellationToken) => await mediator.Send(new GetAllDecoGroups(), cancellationToken); [HttpPost("/api/decos/groups", Name = "CreateDecoGroup")] [Tags("Decos")] [EndpointSummary("Create a deco group")] + [EndpointGroupName("general")] public async Task CreateDecoGroup( [Required] [FromBody] CreateDecoGroupRequest request, CancellationToken cancellationToken) @@ -37,6 +121,7 @@ public class DecoController(IMediator mediator) : ControllerBase [HttpDelete("/api/decos/groups/{id:int}", Name = "DeleteDecoGroup")] [Tags("Decos")] [EndpointSummary("Delete a deco group")] + [EndpointGroupName("general")] public async Task DeleteDecoGroup(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeleteDecoGroup(id), cancellationToken); @@ -47,39 +132,47 @@ public class DecoController(IMediator mediator) : ControllerBase [HttpGet("/api/decos/groups/{groupId:int}/decos", Name = "GetDecosByGroup")] [Tags("Decos")] [EndpointSummary("Get decos by group")] - public async Task> GetDecosByGroup(int groupId, CancellationToken cancellationToken) => - await mediator.Send(new GetDecosByDecoGroupId(groupId), cancellationToken); + [EndpointGroupName("general")] + public async Task> GetDecosByGroup(int groupId, CancellationToken cancellationToken) + { + List decos = await mediator.Send(new GetDecosByDecoGroupId(groupId), cancellationToken); + return decos.Select(ToApiResponse).ToList(); + } [HttpGet("/api/decos/tree", Name = "GetDecoTree")] [Tags("Decos")] [EndpointSummary("Get deco tree")] + [EndpointGroupName("general")] public async Task GetDecoTree(CancellationToken cancellationToken) => await mediator.Send(new GetDecoTree(), cancellationToken); [HttpGet("/api/decos/{id:int}", Name = "GetDecoById")] [Tags("Decos")] [EndpointSummary("Get deco by ID")] + [EndpointGroupName("general")] public async Task GetDecoById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetDecoById(id), cancellationToken); - return result.Match(Ok, () => NotFound()); + return result.Match(d => Ok(ToApiResponse(d)), () => NotFound()); } [HttpPost("/api/decos", Name = "CreateDeco")] [Tags("Decos")] [EndpointSummary("Create a deco")] + [EndpointGroupName("general")] public async Task CreateDeco( [Required] [FromBody] CreateDecoRequest request, CancellationToken cancellationToken) { Either result = await mediator.Send( new CreateDeco(request.DecoGroupId, request.Name), cancellationToken); - return result.Match(Ok, error => Problem(error.ToString())); + return result.Match(d => Ok(ToApiResponse(d)), error => Problem(error.ToString())); } [HttpPut("/api/decos/{id:int}", Name = "UpdateDeco")] [Tags("Decos")] [EndpointSummary("Update a deco")] + [EndpointGroupName("general")] public async Task UpdateDeco( int id, [Required] [FromBody] UpdateDecoRequest request, @@ -128,6 +221,7 @@ public class DecoController(IMediator mediator) : ControllerBase [HttpDelete("/api/decos/{id:int}", Name = "DeleteDeco")] [Tags("Decos")] [EndpointSummary("Delete a deco")] + [EndpointGroupName("general")] public async Task DeleteDeco(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeleteDeco(id), cancellationToken); @@ -137,6 +231,7 @@ public class DecoController(IMediator mediator) : ControllerBase [HttpPut("/api/playouts/{playoutId:int}/deco", Name = "UpdateDefaultDeco")] [Tags("Decos")] [EndpointSummary("Update default deco for playout")] + [EndpointGroupName("general")] public async Task UpdateDefaultDeco( int playoutId, [Required] [FromBody] UpdateDefaultDecoRequest request, @@ -151,12 +246,14 @@ public class DecoController(IMediator mediator) : ControllerBase [HttpGet("/api/decos/template-groups", Name = "GetDecoTemplateGroups")] [Tags("Decos")] [EndpointSummary("Get all deco template groups")] + [EndpointGroupName("general")] public async Task> GetDecoTemplateGroups(CancellationToken cancellationToken) => await mediator.Send(new GetAllDecoTemplateGroups(), cancellationToken); [HttpPost("/api/decos/template-groups", Name = "CreateDecoTemplateGroup")] [Tags("Decos")] [EndpointSummary("Create a deco template group")] + [EndpointGroupName("general")] public async Task CreateDecoTemplateGroup( [Required] [FromBody] CreateDecoTemplateGroupRequest request, CancellationToken cancellationToken) @@ -169,6 +266,7 @@ public class DecoController(IMediator mediator) : ControllerBase [HttpDelete("/api/decos/template-groups/{id:int}", Name = "DeleteDecoTemplateGroup")] [Tags("Decos")] [EndpointSummary("Delete a deco template group")] + [EndpointGroupName("general")] public async Task DeleteDecoTemplateGroup(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeleteDecoTemplateGroup(id), cancellationToken); @@ -179,18 +277,21 @@ public class DecoController(IMediator mediator) : ControllerBase [HttpGet("/api/decos/template-groups/{groupId:int}/templates", Name = "GetDecoTemplatesByGroup")] [Tags("Decos")] [EndpointSummary("Get deco templates by group")] + [EndpointGroupName("general")] public async Task> GetDecoTemplatesByGroup(int groupId, CancellationToken cancellationToken) => await mediator.Send(new GetDecoTemplatesByDecoTemplateGroupId(groupId), cancellationToken); [HttpGet("/api/decos/templates/tree", Name = "GetDecoTemplateTree")] [Tags("Decos")] [EndpointSummary("Get deco template tree")] + [EndpointGroupName("general")] public async Task GetDecoTemplateTree(CancellationToken cancellationToken) => await mediator.Send(new GetDecoTemplateTree(), cancellationToken); [HttpGet("/api/decos/templates/{id:int}", Name = "GetDecoTemplateById")] [Tags("Decos")] [EndpointSummary("Get deco template by ID")] + [EndpointGroupName("general")] public async Task GetDecoTemplateById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetDecoTemplateById(id), cancellationToken); @@ -200,12 +301,14 @@ public class DecoController(IMediator mediator) : ControllerBase [HttpGet("/api/decos/templates/{id:int}/items", Name = "GetDecoTemplateItems")] [Tags("Decos")] [EndpointSummary("Get deco template items")] + [EndpointGroupName("general")] public async Task> GetDecoTemplateItems(int id, CancellationToken cancellationToken) => await mediator.Send(new GetDecoTemplateItems(id), cancellationToken); [HttpPost("/api/decos/templates", Name = "CreateDecoTemplate")] [Tags("Decos")] [EndpointSummary("Create a deco template")] + [EndpointGroupName("general")] public async Task CreateDecoTemplate( [Required] [FromBody] CreateDecoTemplateRequest request, CancellationToken cancellationToken) @@ -218,6 +321,7 @@ public class DecoController(IMediator mediator) : ControllerBase [HttpDelete("/api/decos/templates/{id:int}", Name = "DeleteDecoTemplate")] [Tags("Decos")] [EndpointSummary("Delete a deco template")] + [EndpointGroupName("general")] public async Task DeleteDecoTemplate(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeleteDecoTemplate(id), cancellationToken); diff --git a/ErsatzTV/Controllers/Api/EmbyController.cs b/ErsatzTV/Controllers/Api/EmbyController.cs index 902adcdcc..fc9679324 100644 --- a/ErsatzTV/Controllers/Api/EmbyController.cs +++ b/ErsatzTV/Controllers/Api/EmbyController.cs @@ -10,18 +10,20 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class EmbyController(IMediator mediator) : ControllerBase { [HttpGet("/api/emby/sources", Name = "GetEmbyMediaSources")] [Tags("Emby")] [EndpointSummary("Get all Emby media sources")] + [EndpointGroupName("general")] public async Task> GetEmbyMediaSources(CancellationToken cancellationToken) => await mediator.Send(new GetAllEmbyMediaSources(), cancellationToken); [HttpGet("/api/emby/sources/{id:int}", Name = "GetEmbyMediaSourceById")] [Tags("Emby")] [EndpointSummary("Get Emby media source by ID")] + [EndpointGroupName("general")] public async Task GetEmbyMediaSourceById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetEmbyMediaSourceById(id), cancellationToken); @@ -31,18 +33,21 @@ public class EmbyController(IMediator mediator) : ControllerBase [HttpGet("/api/emby/sources/{id:int}/libraries", Name = "GetEmbyLibraries")] [Tags("Emby")] [EndpointSummary("Get Emby libraries by source")] + [EndpointGroupName("general")] public async Task> GetEmbyLibraries(int id, CancellationToken cancellationToken) => await mediator.Send(new GetEmbyLibrariesBySourceId(id), cancellationToken); [HttpGet("/api/emby/sources/{id:int}/path-replacements", Name = "GetEmbyPathReplacements")] [Tags("Emby")] [EndpointSummary("Get Emby path replacements by source")] + [EndpointGroupName("general")] public async Task> GetEmbyPathReplacements(int id, CancellationToken cancellationToken) => await mediator.Send(new GetEmbyPathReplacementsBySourceId(id), cancellationToken); [HttpPut("/api/emby/sources/{id:int}/path-replacements", Name = "UpdateEmbyPathReplacements")] [Tags("Emby")] [EndpointSummary("Update Emby path replacements")] + [EndpointGroupName("general")] public async Task UpdateEmbyPathReplacements( int id, [Required] [FromBody] UpdateEmbyPathReplacementsRequest request, @@ -58,6 +63,7 @@ public class EmbyController(IMediator mediator) : ControllerBase [HttpPut("/api/emby/library-preferences", Name = "UpdateEmbyLibraryPreferences")] [Tags("Emby")] [EndpointSummary("Update Emby library preferences")] + [EndpointGroupName("general")] public async Task UpdateEmbyLibraryPreferences( [Required] [FromBody] UpdateEmbyLibraryPreferencesRequest request, CancellationToken cancellationToken) @@ -72,6 +78,7 @@ public class EmbyController(IMediator mediator) : ControllerBase [HttpGet("/api/emby/secrets", Name = "GetEmbySecrets")] [Tags("Emby")] [EndpointSummary("Get Emby secrets")] + [EndpointGroupName("general")] public async Task GetEmbySecrets(CancellationToken cancellationToken) => await mediator.Send(new GetEmbySecrets(), cancellationToken); } diff --git a/ErsatzTV/Controllers/Api/FFmpegProfileController.cs b/ErsatzTV/Controllers/Api/FFmpegProfileController.cs index 7f567d882..c84cd41e9 100644 --- a/ErsatzTV/Controllers/Api/FFmpegProfileController.cs +++ b/ErsatzTV/Controllers/Api/FFmpegProfileController.cs @@ -9,18 +9,20 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class FFmpegProfileController(IMediator mediator) : ControllerBase { [HttpGet("/api/ffmpeg/profiles", Name = "GetFFmpegProfiles")] [Tags("FFmpeg")] [EndpointSummary("Get all FFmpeg profiles")] + [EndpointGroupName("general")] public async Task> GetAll(CancellationToken cancellationToken) => await mediator.Send(new GetAllFFmpegProfilesForApi(), cancellationToken); [HttpGet("/api/ffmpeg/profiles/{id:int}", Name = "GetFFmpegProfileById")] [Tags("FFmpeg")] [EndpointSummary("Get FFmpeg profile by ID")] + [EndpointGroupName("general")] public async Task GetFFmpegProfileById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetFFmpegProfileById(id), cancellationToken); @@ -30,6 +32,7 @@ public class FFmpegProfileController(IMediator mediator) : ControllerBase [HttpPost("/api/ffmpeg/profiles/new", Name = "CreateFFmpegProfile")] [Tags("FFmpeg")] [EndpointSummary("Create FFmpeg profile")] + [EndpointGroupName("general")] public async Task AddOne( [Required] [FromBody] CreateFFmpegProfile request, @@ -42,6 +45,7 @@ public class FFmpegProfileController(IMediator mediator) : ControllerBase [HttpPut("/api/ffmpeg/profiles/update", Name = "UpdateFFmpegProfile")] [Tags("FFmpeg")] [EndpointSummary("Update FFmpeg profile")] + [EndpointGroupName("general")] public async Task UpdateOne( [Required] [FromBody] UpdateFFmpegProfile request, @@ -54,6 +58,7 @@ public class FFmpegProfileController(IMediator mediator) : ControllerBase [HttpDelete("/api/ffmpeg/delete/{id:int}", Name = "DeleteFFmpegProfile")] [Tags("FFmpeg")] [EndpointSummary("Delete FFmpeg profile")] + [EndpointGroupName("general")] public async Task DeleteProfileAsync(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new DeleteFFmpegProfile(id), cancellationToken); @@ -63,6 +68,7 @@ public class FFmpegProfileController(IMediator mediator) : ControllerBase [HttpPost("/api/ffmpeg/profiles/{id:int}/copy", Name = "CopyFFmpegProfile")] [Tags("FFmpeg")] [EndpointSummary("Copy FFmpeg profile")] + [EndpointGroupName("general")] public async Task CopyFFmpegProfile( int id, [Required] [FromBody] CopyFFmpegProfileRequest request, @@ -76,12 +82,14 @@ public class FFmpegProfileController(IMediator mediator) : ControllerBase [HttpGet("/api/ffmpeg/settings", Name = "GetFFmpegSettings")] [Tags("FFmpeg")] [EndpointSummary("Get FFmpeg settings")] + [EndpointGroupName("general")] public async Task GetFFmpegSettings(CancellationToken cancellationToken) => await mediator.Send(new GetFFmpegSettings(), cancellationToken); [HttpPut("/api/ffmpeg/settings", Name = "UpdateFFmpegSettings")] [Tags("FFmpeg")] [EndpointSummary("Update FFmpeg settings")] + [EndpointGroupName("general")] public async Task UpdateFFmpegSettings( [Required] [FromBody] FFmpegSettingsViewModel settings, CancellationToken cancellationToken) diff --git a/ErsatzTV/Controllers/Api/FillerController.cs b/ErsatzTV/Controllers/Api/FillerController.cs index 40990501a..7bb34a95e 100644 --- a/ErsatzTV/Controllers/Api/FillerController.cs +++ b/ErsatzTV/Controllers/Api/FillerController.cs @@ -11,12 +11,13 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class FillerController(IMediator mediator) : ControllerBase { [HttpGet("/api/filler/presets", Name = "GetFillerPresets")] [Tags("Filler")] [EndpointSummary("Get all filler presets (paginated)")] + [EndpointGroupName("general")] public async Task GetFillerPresets( [FromQuery] int pageNumber = 0, [FromQuery] int pageSize = 50, @@ -26,12 +27,14 @@ public class FillerController(IMediator mediator) : ControllerBase [HttpGet("/api/filler/presets/all", Name = "GetAllFillerPresets")] [Tags("Filler")] [EndpointSummary("Get all filler presets")] + [EndpointGroupName("general")] public async Task> GetAllFillerPresets(CancellationToken cancellationToken) => await mediator.Send(new GetAllFillerPresets(), cancellationToken); [HttpGet("/api/filler/presets/{id:int}", Name = "GetFillerPresetById")] [Tags("Filler")] [EndpointSummary("Get filler preset by ID")] + [EndpointGroupName("general")] public async Task GetFillerPresetById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetFillerPresetById(id), cancellationToken); @@ -41,6 +44,7 @@ public class FillerController(IMediator mediator) : ControllerBase [HttpPost("/api/filler/presets", Name = "CreateFillerPreset")] [Tags("Filler")] [EndpointSummary("Create a filler preset")] + [EndpointGroupName("general")] public async Task CreateFillerPreset( [Required] [FromBody] CreateFillerPresetRequest request, CancellationToken cancellationToken) @@ -69,6 +73,7 @@ public class FillerController(IMediator mediator) : ControllerBase [HttpPut("/api/filler/presets/{id:int}", Name = "UpdateFillerPreset")] [Tags("Filler")] [EndpointSummary("Update a filler preset")] + [EndpointGroupName("general")] public async Task UpdateFillerPreset( int id, [Required] [FromBody] UpdateFillerPresetRequest request, @@ -99,6 +104,7 @@ public class FillerController(IMediator mediator) : ControllerBase [HttpDelete("/api/filler/presets/{id:int}", Name = "DeleteFillerPreset")] [Tags("Filler")] [EndpointSummary("Delete a filler preset")] + [EndpointGroupName("general")] public async Task DeleteFillerPreset(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new DeleteFillerPreset(id), cancellationToken); diff --git a/ErsatzTV/Controllers/Api/JellyfinController.cs b/ErsatzTV/Controllers/Api/JellyfinController.cs index e2796961d..eea1b8536 100644 --- a/ErsatzTV/Controllers/Api/JellyfinController.cs +++ b/ErsatzTV/Controllers/Api/JellyfinController.cs @@ -10,18 +10,20 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class JellyfinController(IMediator mediator) : ControllerBase { [HttpGet("/api/jellyfin/sources", Name = "GetJellyfinMediaSources")] [Tags("Jellyfin")] [EndpointSummary("Get all Jellyfin media sources")] + [EndpointGroupName("general")] public async Task> GetJellyfinMediaSources(CancellationToken cancellationToken) => await mediator.Send(new GetAllJellyfinMediaSources(), cancellationToken); [HttpGet("/api/jellyfin/sources/{id:int}", Name = "GetJellyfinMediaSourceById")] [Tags("Jellyfin")] [EndpointSummary("Get Jellyfin media source by ID")] + [EndpointGroupName("general")] public async Task GetJellyfinMediaSourceById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetJellyfinMediaSourceById(id), cancellationToken); @@ -31,18 +33,21 @@ public class JellyfinController(IMediator mediator) : ControllerBase [HttpGet("/api/jellyfin/sources/{id:int}/libraries", Name = "GetJellyfinLibraries")] [Tags("Jellyfin")] [EndpointSummary("Get Jellyfin libraries by source")] + [EndpointGroupName("general")] public async Task> GetJellyfinLibraries(int id, CancellationToken cancellationToken) => await mediator.Send(new GetJellyfinLibrariesBySourceId(id), cancellationToken); [HttpGet("/api/jellyfin/sources/{id:int}/path-replacements", Name = "GetJellyfinPathReplacements")] [Tags("Jellyfin")] [EndpointSummary("Get Jellyfin path replacements by source")] + [EndpointGroupName("general")] public async Task> GetJellyfinPathReplacements(int id, CancellationToken cancellationToken) => await mediator.Send(new GetJellyfinPathReplacementsBySourceId(id), cancellationToken); [HttpPut("/api/jellyfin/sources/{id:int}/path-replacements", Name = "UpdateJellyfinPathReplacements")] [Tags("Jellyfin")] [EndpointSummary("Update Jellyfin path replacements")] + [EndpointGroupName("general")] public async Task UpdateJellyfinPathReplacements( int id, [Required] [FromBody] UpdateJellyfinPathReplacementsRequest request, @@ -58,6 +63,7 @@ public class JellyfinController(IMediator mediator) : ControllerBase [HttpPut("/api/jellyfin/library-preferences", Name = "UpdateJellyfinLibraryPreferences")] [Tags("Jellyfin")] [EndpointSummary("Update Jellyfin library preferences")] + [EndpointGroupName("general")] public async Task UpdateJellyfinLibraryPreferences( [Required] [FromBody] UpdateJellyfinLibraryPreferencesRequest request, CancellationToken cancellationToken) @@ -72,6 +78,7 @@ public class JellyfinController(IMediator mediator) : ControllerBase [HttpGet("/api/jellyfin/secrets", Name = "GetJellyfinSecrets")] [Tags("Jellyfin")] [EndpointSummary("Get Jellyfin secrets")] + [EndpointGroupName("general")] public async Task GetJellyfinSecrets(CancellationToken cancellationToken) => await mediator.Send(new GetJellyfinSecrets(), cancellationToken); } diff --git a/ErsatzTV/Controllers/Api/LibrariesController.cs b/ErsatzTV/Controllers/Api/LibrariesController.cs index ece43813e..751177c5a 100644 --- a/ErsatzTV/Controllers/Api/LibrariesController.cs +++ b/ErsatzTV/Controllers/Api/LibrariesController.cs @@ -11,24 +11,27 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class LibrariesController(ITelevisionRepository televisionRepository, IMediator mediator) : ControllerBase { [HttpGet("/api/libraries", Name = "GetAllLibraries")] [Tags("Libraries")] [EndpointSummary("Get all configured libraries")] + [EndpointGroupName("general")] public async Task> GetAllLibraries(CancellationToken cancellationToken) => await mediator.Send(new GetConfiguredLibraries(), cancellationToken); [HttpGet("/api/libraries/local", Name = "GetLocalLibraries")] [Tags("Libraries")] [EndpointSummary("Get all local libraries")] + [EndpointGroupName("general")] public async Task> GetLocalLibraries(CancellationToken cancellationToken) => await mediator.Send(new GetAllLocalLibraries(), cancellationToken); [HttpGet("/api/libraries/local/{id:int}", Name = "GetLocalLibraryById")] [Tags("Libraries")] [EndpointSummary("Get local library by ID")] + [EndpointGroupName("general")] public async Task GetLocalLibraryById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetLocalLibraryById(id), cancellationToken); @@ -38,12 +41,14 @@ public class LibrariesController(ITelevisionRepository televisionRepository, IMe [HttpGet("/api/libraries/local/{id:int}/paths", Name = "GetLocalLibraryPaths")] [Tags("Libraries")] [EndpointSummary("Get local library paths")] + [EndpointGroupName("general")] public async Task> GetLocalLibraryPaths(int id, CancellationToken cancellationToken) => await mediator.Send(new GetLocalLibraryPaths(id), cancellationToken); [HttpPost("/api/libraries/local", Name = "CreateLocalLibrary")] [Tags("Libraries")] [EndpointSummary("Create a local library")] + [EndpointGroupName("general")] public async Task CreateLocalLibrary( [Required] [FromBody] CreateLocalLibraryRequest request, CancellationToken cancellationToken) @@ -56,6 +61,7 @@ public class LibrariesController(ITelevisionRepository televisionRepository, IMe [HttpPut("/api/libraries/local/{id:int}", Name = "UpdateLocalLibrary")] [Tags("Libraries")] [EndpointSummary("Update a local library")] + [EndpointGroupName("general")] public async Task UpdateLocalLibrary( int id, [Required] [FromBody] UpdateLocalLibraryRequest request, @@ -70,6 +76,7 @@ public class LibrariesController(ITelevisionRepository televisionRepository, IMe [HttpDelete("/api/libraries/local/{id:int}", Name = "DeleteLocalLibrary")] [Tags("Libraries")] [EndpointSummary("Delete a local library")] + [EndpointGroupName("general")] public async Task DeleteLocalLibrary(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new DeleteLocalLibrary(id), cancellationToken); @@ -79,6 +86,7 @@ public class LibrariesController(ITelevisionRepository televisionRepository, IMe [HttpPost("/api/libraries/{id:int}/scan", Name = "ScanLibrary")] [Tags("Libraries")] [EndpointSummary("Scan library")] + [EndpointGroupName("general")] public async Task ScanLibrary(int id) => await mediator.Send(new QueueLibraryScanByLibraryId(id)) ? Ok() @@ -87,6 +95,7 @@ public class LibrariesController(ITelevisionRepository televisionRepository, IMe [HttpPost("/api/libraries/{id:int}/scan-show", Name = "ScanShow")] [Tags("Libraries")] [EndpointSummary("Scan show")] + [EndpointGroupName("general")] public async Task ScanShow(int id, [FromBody] ScanShowRequest request) { if (string.IsNullOrWhiteSpace(request.ShowTitle)) @@ -112,6 +121,7 @@ public class LibrariesController(ITelevisionRepository televisionRepository, IMe [HttpGet("/api/libraries/external-collections", Name = "GetExternalCollections")] [Tags("Libraries")] [EndpointSummary("Get external collections")] + [EndpointGroupName("general")] public async Task> GetExternalCollections(CancellationToken cancellationToken) => await mediator.Send(new GetExternalCollections(), cancellationToken); } diff --git a/ErsatzTV/Controllers/Api/MaintenanceController.cs b/ErsatzTV/Controllers/Api/MaintenanceController.cs index 3a5fc777e..1e7a218d0 100644 --- a/ErsatzTV/Controllers/Api/MaintenanceController.cs +++ b/ErsatzTV/Controllers/Api/MaintenanceController.cs @@ -6,12 +6,13 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class MaintenanceController(IMediator mediator) : ControllerBase { [HttpPost("/api/maintenance/gc", Name = "GarbageCollection")] [Tags("Maintenance")] [EndpointSummary("Garbage collect")] + [EndpointGroupName("general")] public async Task GarbageCollection([FromQuery] bool force = false) { await mediator.Send(new ReleaseMemory(force)); @@ -21,6 +22,7 @@ public class MaintenanceController(IMediator mediator) : ControllerBase [HttpPost("/api/maintenance/empty-trash", Name = "EmptyTrash")] [Tags("Maintenance")] [EndpointSummary("Empty trash")] + [EndpointGroupName("general")] public async Task EmptyTrash() { Either result = await mediator.Send(new EmptyTrash()); @@ -30,6 +32,7 @@ public class MaintenanceController(IMediator mediator) : ControllerBase [HttpPost("/api/maintenance/delete-orphaned-artwork", Name = "DeleteOrphanedArtwork")] [Tags("Maintenance")] [EndpointSummary("Delete orphaned artwork")] + [EndpointGroupName("general")] public async Task DeleteOrphanedArtwork() { Either result = await mediator.Send(new DeleteOrphanedArtwork()); @@ -39,6 +42,7 @@ public class MaintenanceController(IMediator mediator) : ControllerBase [HttpPost("/api/maintenance/delete-orphaned-subtitles", Name = "DeleteOrphanedSubtitles")] [Tags("Maintenance")] [EndpointSummary("Delete orphaned subtitles")] + [EndpointGroupName("general")] public async Task DeleteOrphanedSubtitles() { Either result = await mediator.Send(new DeleteOrphanedSubtitles()); diff --git a/ErsatzTV/Controllers/Api/MediaController.cs b/ErsatzTV/Controllers/Api/MediaController.cs index 0e85a670a..d1962a29d 100644 --- a/ErsatzTV/Controllers/Api/MediaController.cs +++ b/ErsatzTV/Controllers/Api/MediaController.cs @@ -9,13 +9,14 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class MediaController(IMediator mediator) : ControllerBase { // Movies [HttpGet("/api/media/movies/{id:int}", Name = "GetMovieById")] [Tags("Media")] [EndpointSummary("Get movie by ID")] + [EndpointGroupName("general")] public async Task GetMovieById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetMovieById(id), cancellationToken); @@ -26,6 +27,7 @@ public class MediaController(IMediator mediator) : ControllerBase [HttpGet("/api/media/shows/{id:int}", Name = "GetShowById")] [Tags("Media")] [EndpointSummary("Get TV show by ID")] + [EndpointGroupName("general")] public async Task GetShowById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetTelevisionShowById(id), cancellationToken); @@ -35,6 +37,7 @@ public class MediaController(IMediator mediator) : ControllerBase [HttpGet("/api/media/shows/{id:int}/seasons", Name = "GetShowSeasons")] [Tags("Media")] [EndpointSummary("Get seasons for a TV show")] + [EndpointGroupName("general")] public async Task GetShowSeasons( int id, [FromQuery] int pageNumber = 0, @@ -45,6 +48,7 @@ public class MediaController(IMediator mediator) : ControllerBase [HttpGet("/api/media/seasons/{id:int}", Name = "GetSeasonById")] [Tags("Media")] [EndpointSummary("Get season by ID")] + [EndpointGroupName("general")] public async Task GetSeasonById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetTelevisionSeasonById(id), cancellationToken); @@ -54,6 +58,7 @@ public class MediaController(IMediator mediator) : ControllerBase [HttpGet("/api/media/seasons/{id:int}/episodes", Name = "GetSeasonEpisodes")] [Tags("Media")] [EndpointSummary("Get episodes for a season")] + [EndpointGroupName("general")] public async Task GetSeasonEpisodes( int id, [FromQuery] int pageNumber = 0, @@ -65,6 +70,7 @@ public class MediaController(IMediator mediator) : ControllerBase [HttpGet("/api/media/artists/{id:int}", Name = "GetArtistById")] [Tags("Media")] [EndpointSummary("Get artist by ID")] + [EndpointGroupName("general")] public async Task GetArtistById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetArtistById(id), cancellationToken); diff --git a/ErsatzTV/Controllers/Api/PlaylistController.cs b/ErsatzTV/Controllers/Api/PlaylistController.cs index e87f58d21..0faeabaa8 100644 --- a/ErsatzTV/Controllers/Api/PlaylistController.cs +++ b/ErsatzTV/Controllers/Api/PlaylistController.cs @@ -1,6 +1,7 @@ #nullable enable using System.ComponentModel.DataAnnotations; using ErsatzTV.Application.MediaCollections; +using ErsatzTV.Application.MediaItems; using ErsatzTV.Application.Tree; using ErsatzTV.Core; using ErsatzTV.Core.Domain; @@ -11,19 +12,63 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] public class PlaylistController(IMediator mediator) : ControllerBase { + private static CollectionApiResponse? ToApiResponse(MediaCollectionViewModel? c) => + c == null ? null : new(c.Id, c.Name, c.CollectionType, c.UseCustomPlaybackOrder, c.State); + + private static SmartCollectionApiResponse? ToApiResponse(SmartCollectionViewModel? s) => + s == null ? null : new(s.Id, s.Name, s.Query); + + private static MultiCollectionApiResponse? ToApiResponse(MultiCollectionViewModel? mc) => + mc == null ? null : new( + mc.Id, + mc.Name, + mc.Items?.Select(i => new MultiCollectionItemApiResponse( + i.MultiCollectionId, + i.Collection?.Id ?? 0, + i.Collection?.Name ?? "", + i.Collection?.CollectionType ?? CollectionType.Collection, + i.Collection?.UseCustomPlaybackOrder ?? false, + i.Collection?.State ?? MediaItemState.Normal, + i.ScheduleAsGroup, + i.PlaybackOrder)).ToList() ?? [], + mc.SmartItems?.Select(s => new MultiCollectionSmartItemApiResponse( + s.MultiCollectionId, + s.SmartCollection?.Id ?? 0, + s.SmartCollection?.Name ?? "", + s.SmartCollection?.Query ?? "", + s.ScheduleAsGroup, + s.PlaybackOrder)).ToList() ?? []); + + private static MediaItemApiResponse? ToMediaItemApiResponse(NamedMediaItemViewModel? m) => + m == null ? null : new(m.MediaItemId, m.Name); + + private static PlaylistItemApiResponse ToApiResponse(PlaylistItemViewModel item) => + new( + item.Id, + item.Index, + item.CollectionType, + ToApiResponse(item.Collection), + ToApiResponse(item.MultiCollection), + ToApiResponse(item.SmartCollection), + ToMediaItemApiResponse(item.MediaItem), + item.PlaybackOrder, + item.Count, + item.PlayAll, + item.IncludeInProgramGuide); // Playlist Groups [HttpGet("/api/playlists/groups", Name = "GetPlaylistGroups")] [Tags("Playlists")] [EndpointSummary("Get all playlist groups")] + [EndpointGroupName("general")] public async Task> GetPlaylistGroups(CancellationToken cancellationToken) => await mediator.Send(new GetAllPlaylistGroups(), cancellationToken); [HttpPost("/api/playlists/groups", Name = "CreatePlaylistGroup")] [Tags("Playlists")] [EndpointSummary("Create a playlist group")] + [EndpointGroupName("general")] public async Task CreatePlaylistGroup( [Required] [FromBody] CreatePlaylistGroupRequest request, CancellationToken cancellationToken) @@ -36,6 +81,7 @@ public class PlaylistController(IMediator mediator) : ControllerBase [HttpDelete("/api/playlists/groups/{id:int}", Name = "DeletePlaylistGroup")] [Tags("Playlists")] [EndpointSummary("Delete a playlist group")] + [EndpointGroupName("general")] public async Task DeletePlaylistGroup(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeletePlaylistGroup(id), cancellationToken); @@ -46,18 +92,21 @@ public class PlaylistController(IMediator mediator) : ControllerBase [HttpGet("/api/playlists/groups/{groupId:int}/playlists", Name = "GetPlaylistsByGroup")] [Tags("Playlists")] [EndpointSummary("Get playlists by group")] + [EndpointGroupName("general")] public async Task> GetPlaylistsByGroup(int groupId, CancellationToken cancellationToken) => await mediator.Send(new GetPlaylistsByPlaylistGroupId(groupId), cancellationToken); [HttpGet("/api/playlists/tree", Name = "GetPlaylistTree")] [Tags("Playlists")] [EndpointSummary("Get playlist tree")] + [EndpointGroupName("general")] public async Task GetPlaylistTree(CancellationToken cancellationToken) => await mediator.Send(new GetPlaylistTree(), cancellationToken); [HttpGet("/api/playlists/{id:int}", Name = "GetPlaylistById")] [Tags("Playlists")] [EndpointSummary("Get playlist by ID")] + [EndpointGroupName("general")] public async Task GetPlaylistById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetPlaylistById(id), cancellationToken); @@ -67,12 +116,17 @@ public class PlaylistController(IMediator mediator) : ControllerBase [HttpGet("/api/playlists/{id:int}/items", Name = "GetPlaylistItems")] [Tags("Playlists")] [EndpointSummary("Get playlist items")] - public async Task> GetPlaylistItems(int id, CancellationToken cancellationToken) => - await mediator.Send(new GetPlaylistItems(id), cancellationToken); + [EndpointGroupName("general")] + public async Task> GetPlaylistItems(int id, CancellationToken cancellationToken) + { + List items = await mediator.Send(new GetPlaylistItems(id), cancellationToken); + return items.Select(ToApiResponse).ToList(); + } [HttpPost("/api/playlists", Name = "CreatePlaylistApi")] [Tags("Playlists")] [EndpointSummary("Create a playlist")] + [EndpointGroupName("general")] public async Task CreatePlaylist( [Required] [FromBody] CreatePlaylistRequest request, CancellationToken cancellationToken) @@ -85,6 +139,7 @@ public class PlaylistController(IMediator mediator) : ControllerBase [HttpDelete("/api/playlists/{id:int}", Name = "DeletePlaylist")] [Tags("Playlists")] [EndpointSummary("Delete a playlist")] + [EndpointGroupName("general")] public async Task DeletePlaylist(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeletePlaylist(id), cancellationToken); @@ -94,6 +149,7 @@ public class PlaylistController(IMediator mediator) : ControllerBase [HttpPut("/api/playlists/{id:int}/items", Name = "ReplacePlaylistItems")] [Tags("Playlists")] [EndpointSummary("Replace playlist items")] + [EndpointGroupName("general")] public async Task ReplacePlaylistItems( int id, [Required] [FromBody] ReplacePlaylistItemsRequest request, diff --git a/ErsatzTV/Controllers/Api/PlayoutController.cs b/ErsatzTV/Controllers/Api/PlayoutController.cs index 04ff58bb2..1c4cce503 100644 --- a/ErsatzTV/Controllers/Api/PlayoutController.cs +++ b/ErsatzTV/Controllers/Api/PlayoutController.cs @@ -12,12 +12,13 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class PlayoutController(ChannelWriter workerChannel, IMediator mediator) : ControllerBase { [HttpGet("/api/playouts", Name = "GetAllPlayouts")] [Tags("Playouts")] [EndpointSummary("Get all playouts (paginated)")] + [EndpointGroupName("general")] public async Task GetAllPlayouts( [FromQuery] string query = "", [FromQuery] int pageNumber = 0, @@ -28,12 +29,14 @@ public class PlayoutController(ChannelWriter workerCh [HttpGet("/api/playouts/block", Name = "GetBlockPlayouts")] [Tags("Playouts")] [EndpointSummary("Get all block playouts")] + [EndpointGroupName("general")] public async Task> GetBlockPlayouts(CancellationToken cancellationToken) => await mediator.Send(new GetAllBlockPlayouts(), cancellationToken); [HttpGet("/api/playouts/{id:int}", Name = "GetPlayoutById")] [Tags("Playouts")] [EndpointSummary("Get playout by ID")] + [EndpointGroupName("general")] public async Task GetPlayoutById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetPlayoutById(id), cancellationToken); @@ -43,6 +46,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpGet("/api/playouts/{id:int}/items", Name = "GetPlayoutItems")] [Tags("Playouts")] [EndpointSummary("Get playout items")] + [EndpointGroupName("general")] public async Task GetPlayoutItems( int id, [FromQuery] bool showFiller = false, @@ -54,6 +58,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpPost("/api/playouts/classic", Name = "CreateClassicPlayout")] [Tags("Playouts")] [EndpointSummary("Create a classic playout")] + [EndpointGroupName("general")] public async Task CreateClassicPlayout( [Required] [FromBody] CreateClassicPlayoutRequest request, CancellationToken cancellationToken) @@ -66,6 +71,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpPost("/api/playouts/block", Name = "CreateBlockPlayout")] [Tags("Playouts")] [EndpointSummary("Create a block playout")] + [EndpointGroupName("general")] public async Task CreateBlockPlayout( [Required] [FromBody] CreateBlockPlayoutRequest request, CancellationToken cancellationToken) @@ -78,6 +84,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpPost("/api/playouts/sequential", Name = "CreateSequentialPlayout")] [Tags("Playouts")] [EndpointSummary("Create a sequential playout")] + [EndpointGroupName("general")] public async Task CreateSequentialPlayout( [Required] [FromBody] CreateSequentialPlayoutRequest request, CancellationToken cancellationToken) @@ -90,6 +97,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpPost("/api/playouts/scripted", Name = "CreateScriptedPlayout")] [Tags("Playouts")] [EndpointSummary("Create a scripted playout")] + [EndpointGroupName("general")] public async Task CreateScriptedPlayout( [Required] [FromBody] CreateScriptedPlayoutRequest request, CancellationToken cancellationToken) @@ -102,6 +110,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpPost("/api/playouts/external-json", Name = "CreateExternalJsonPlayout")] [Tags("Playouts")] [EndpointSummary("Create an external JSON playout")] + [EndpointGroupName("general")] public async Task CreateExternalJsonPlayout( [Required] [FromBody] CreateExternalJsonPlayoutRequest request, CancellationToken cancellationToken) @@ -114,6 +123,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpDelete("/api/playouts/{id:int}", Name = "DeletePlayout")] [Tags("Playouts")] [EndpointSummary("Delete a playout")] + [EndpointGroupName("general")] public async Task DeletePlayout(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new DeletePlayout(id), cancellationToken); @@ -123,6 +133,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpPost("/api/playouts/{id:int}/build", Name = "BuildPlayout")] [Tags("Playouts")] [EndpointSummary("Build a playout")] + [EndpointGroupName("general")] public async Task BuildPlayout(int id) { await workerChannel.WriteAsync(new BuildPlayout(id, PlayoutBuildMode.Refresh)); @@ -132,6 +143,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpPost("/api/playouts/{id:int}/reset", Name = "ResetPlayout")] [Tags("Playouts")] [EndpointSummary("Reset a playout")] + [EndpointGroupName("general")] public async Task ResetPlayout(int id) { await workerChannel.WriteAsync(new BuildPlayout(id, PlayoutBuildMode.Reset)); @@ -141,6 +153,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpPost("/api/playouts/reset-all", Name = "ResetAllPlayouts")] [Tags("Playouts")] [EndpointSummary("Reset all playouts")] + [EndpointGroupName("general")] public async Task ResetAllPlayouts(CancellationToken cancellationToken) { await mediator.Send(new ResetAllPlayouts(), cancellationToken); @@ -150,6 +163,7 @@ public class PlayoutController(ChannelWriter workerCh [HttpGet("/api/playouts/warnings/count", Name = "GetPlayoutWarningsCount")] [Tags("Playouts")] [EndpointSummary("Get playout warnings count")] + [EndpointGroupName("general")] public async Task GetPlayoutWarningsCount(CancellationToken cancellationToken) => await mediator.Send(new GetPlayoutWarningsCount(), cancellationToken); } diff --git a/ErsatzTV/Controllers/Api/PlexController.cs b/ErsatzTV/Controllers/Api/PlexController.cs index 584ca6efe..9718e1329 100644 --- a/ErsatzTV/Controllers/Api/PlexController.cs +++ b/ErsatzTV/Controllers/Api/PlexController.cs @@ -9,18 +9,20 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class PlexController(IMediator mediator) : ControllerBase { [HttpGet("/api/plex/sources", Name = "GetPlexMediaSources")] [Tags("Plex")] [EndpointSummary("Get all Plex media sources")] + [EndpointGroupName("general")] public async Task> GetPlexMediaSources(CancellationToken cancellationToken) => await mediator.Send(new GetAllPlexMediaSources(), cancellationToken); [HttpGet("/api/plex/sources/{id:int}", Name = "GetPlexMediaSourceById")] [Tags("Plex")] [EndpointSummary("Get Plex media source by ID")] + [EndpointGroupName("general")] public async Task GetPlexMediaSourceById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetPlexMediaSourceById(id), cancellationToken); @@ -30,18 +32,21 @@ public class PlexController(IMediator mediator) : ControllerBase [HttpGet("/api/plex/sources/{id:int}/libraries", Name = "GetPlexLibraries")] [Tags("Plex")] [EndpointSummary("Get Plex libraries by source")] + [EndpointGroupName("general")] public async Task> GetPlexLibraries(int id, CancellationToken cancellationToken) => await mediator.Send(new GetPlexLibrariesBySourceId(id), cancellationToken); [HttpGet("/api/plex/sources/{id:int}/path-replacements", Name = "GetPlexPathReplacements")] [Tags("Plex")] [EndpointSummary("Get Plex path replacements by source")] + [EndpointGroupName("general")] public async Task> GetPlexPathReplacements(int id, CancellationToken cancellationToken) => await mediator.Send(new GetPlexPathReplacementsBySourceId(id), cancellationToken); [HttpPut("/api/plex/sources/{id:int}/path-replacements", Name = "UpdatePlexPathReplacements")] [Tags("Plex")] [EndpointSummary("Update Plex path replacements")] + [EndpointGroupName("general")] public async Task UpdatePlexPathReplacements( int id, [Required] [FromBody] UpdatePlexPathReplacementsRequest request, @@ -57,6 +62,7 @@ public class PlexController(IMediator mediator) : ControllerBase [HttpPut("/api/plex/library-preferences", Name = "UpdatePlexLibraryPreferences")] [Tags("Plex")] [EndpointSummary("Update Plex library preferences")] + [EndpointGroupName("general")] public async Task UpdatePlexLibraryPreferences( [Required] [FromBody] UpdatePlexLibraryPreferencesRequest request, CancellationToken cancellationToken) diff --git a/ErsatzTV/Controllers/Api/ResolutionController.cs b/ErsatzTV/Controllers/Api/ResolutionController.cs index 71f80154d..28b39f1d9 100644 --- a/ErsatzTV/Controllers/Api/ResolutionController.cs +++ b/ErsatzTV/Controllers/Api/ResolutionController.cs @@ -7,18 +7,20 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class ResolutionController(IMediator mediator) : ControllerBase { [HttpGet("/api/resolutions", Name = "GetAllResolutions")] [Tags("Resolutions")] [EndpointSummary("Get all resolutions")] + [EndpointGroupName("general")] public async Task> GetAllResolutions(CancellationToken cancellationToken) => await mediator.Send(new GetAllResolutions(), cancellationToken); [HttpGet("/api/resolutions/by-name/{name}", Name = "GetResolutionByName")] [Tags("Resolutions")] [EndpointSummary("Get resolution by name")] + [EndpointGroupName("general")] public async Task GetResolutionByName(string name, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetResolutionByName(name), cancellationToken); @@ -28,6 +30,7 @@ public class ResolutionController(IMediator mediator) : ControllerBase [HttpPost("/api/resolutions", Name = "CreateCustomResolution")] [Tags("Resolutions")] [EndpointSummary("Create a custom resolution")] + [EndpointGroupName("general")] public async Task CreateCustomResolution( [Required] [FromBody] CreateCustomResolutionRequest request, CancellationToken cancellationToken) @@ -40,6 +43,7 @@ public class ResolutionController(IMediator mediator) : ControllerBase [HttpDelete("/api/resolutions/{id:int}", Name = "DeleteCustomResolution")] [Tags("Resolutions")] [EndpointSummary("Delete a custom resolution")] + [EndpointGroupName("general")] public async Task DeleteCustomResolution(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeleteCustomResolution(id), cancellationToken); diff --git a/ErsatzTV/Controllers/Api/ScannerController.cs b/ErsatzTV/Controllers/Api/ScannerController.cs index 8c8404160..976cdc07c 100644 --- a/ErsatzTV/Controllers/Api/ScannerController.cs +++ b/ErsatzTV/Controllers/Api/ScannerController.cs @@ -15,6 +15,7 @@ public class ScannerController( { [HttpPost("progress")] [EndpointSummary("Scanner progress update")] + [EndpointGroupName("general")] public async Task Progress(Guid scanId, [FromBody] decimal percentComplete) { await scannerProxyService.Progress(scanId, percentComplete); @@ -23,6 +24,7 @@ public class ScannerController( [HttpPost("items/reindex")] [EndpointSummary("Scanner reindex items in search index")] + [EndpointGroupName("general")] public async Task UpdateItems( Guid scanId, [FromBody] List itemsToUpdate, @@ -38,6 +40,7 @@ public class ScannerController( [HttpPost("items/remove")] [EndpointSummary("Scanner remove items from search index")] + [EndpointGroupName("general")] public async Task RemoveItems( Guid scanId, [FromBody] List itemsToRemove, diff --git a/ErsatzTV/Controllers/Api/ScheduleController.cs b/ErsatzTV/Controllers/Api/ScheduleController.cs index 179f7f477..bf7e71780 100644 --- a/ErsatzTV/Controllers/Api/ScheduleController.cs +++ b/ErsatzTV/Controllers/Api/ScheduleController.cs @@ -1,6 +1,13 @@ +#nullable enable using System.ComponentModel.DataAnnotations; +using ErsatzTV.Application.Filler; +using ErsatzTV.Application.Graphics; +using ErsatzTV.Application.MediaCollections; +using ErsatzTV.Application.MediaItems; using ErsatzTV.Application.ProgramSchedules; +using ErsatzTV.Application.Watermarks; using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Scheduling; using LanguageExt; using MediatR; @@ -9,12 +16,97 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] public class ScheduleController(IMediator mediator) : ControllerBase { + private static CollectionApiResponse? ToApiResponse(MediaCollectionViewModel? c) => + c == null ? null : new(c.Id, c.Name, c.CollectionType, c.UseCustomPlaybackOrder, c.State); + + private static SmartCollectionApiResponse? ToApiResponse(SmartCollectionViewModel? s) => + s == null ? null : new(s.Id, s.Name, s.Query); + + private static MultiCollectionApiResponse? ToApiResponse(MultiCollectionViewModel? mc) => + mc == null ? null : new( + mc.Id, + mc.Name, + mc.Items?.Select(i => new MultiCollectionItemApiResponse( + i.MultiCollectionId, + i.Collection?.Id ?? 0, + i.Collection?.Name ?? "", + i.Collection?.CollectionType ?? CollectionType.Collection, + i.Collection?.UseCustomPlaybackOrder ?? false, + i.Collection?.State ?? MediaItemState.Normal, + i.ScheduleAsGroup, + i.PlaybackOrder)).ToList() ?? [], + mc.SmartItems?.Select(s => new MultiCollectionSmartItemApiResponse( + s.MultiCollectionId, + s.SmartCollection?.Id ?? 0, + s.SmartCollection?.Name ?? "", + s.SmartCollection?.Query ?? "", + s.ScheduleAsGroup, + s.PlaybackOrder)).ToList() ?? []); + + private static MediaItemApiResponse? ToMediaItemApiResponse(NamedMediaItemViewModel? m) => + m == null ? null : new(m.MediaItemId, m.Name); + + private static RerunCollectionApiResponse? ToApiResponse(RerunCollectionViewModel? r) => + r == null ? null : new(r.Id, r.Name); + + private static PlaylistApiResponse? ToApiResponse(PlaylistViewModel? p) => + p == null ? null : new(p.Id, p.PlaylistGroupId, p.Name, p.IsSystem); + + private static FillerPresetApiResponse? ToApiResponse(FillerPresetViewModel? f) => + f == null ? null : new(f.Id, f.Name); + + private static WatermarkApiResponse ToApiResponse(WatermarkViewModel w) => + new(w.Id, w.Name, w.Image?.Path, w.Mode, w.ImageSource, w.Location, w.Size, w.Width, + w.HorizontalMargin, w.VerticalMargin, w.FrequencyMinutes, w.DurationSeconds, + w.Opacity, w.PlaceWithinSourceContent, w.OpacityExpression, w.ZIndex); + + private static GraphicsElementApiResponse ToApiResponse(GraphicsElementViewModel g) => + new(g.Id, g.Name, g.FileName); + + private static ScheduleItemApiResponse ToApiResponse(ProgramScheduleItemViewModel item) => + new( + item.Id, + item.Index, + item.StartType, + item.StartTime, + item.FixedStartTimeBehavior, + item.PlayoutMode, + item.CollectionType, + ToApiResponse(item.Collection), + ToApiResponse(item.MultiCollection), + ToApiResponse(item.SmartCollection), + ToApiResponse(item.RerunCollection), + ToApiResponse(item.Playlist), + ToMediaItemApiResponse(item.MediaItem), + item.SearchTitle, + item.SearchQuery, + item.PlaybackOrder, + item.MarathonGroupBy, + item.MarathonShuffleGroups, + item.MarathonShuffleItems, + item.MarathonBatchSize, + item.FillWithGroupMode, + item.CustomTitle, + item.GuideMode, + ToApiResponse(item.PreRollFiller), + ToApiResponse(item.MidRollFiller), + ToApiResponse(item.PostRollFiller), + ToApiResponse(item.TailFiller), + ToApiResponse(item.FallbackFiller), + item.Watermarks?.Select(ToApiResponse).ToList() ?? [], + item.GraphicsElements?.Select(ToApiResponse).ToList() ?? [], + item.PreferredAudioLanguageCode, + item.PreferredAudioTitle, + item.PreferredSubtitleLanguageCode, + item.SubtitleMode, + item is ProgramScheduleItemMultipleViewModel multiple ? multiple.Count : null, + item is ProgramScheduleItemDurationViewModel duration ? duration.PlayoutDuration : null); [HttpGet("/api/schedules", Name = "GetAllSchedules")] [Tags("Schedules")] [EndpointSummary("Get all schedules (paginated)")] + [EndpointGroupName("general")] public async Task GetAllSchedules( [FromQuery] string query = "", [FromQuery] int pageNumber = 0, @@ -25,12 +117,14 @@ public class ScheduleController(IMediator mediator) : ControllerBase [HttpGet("/api/schedules/all", Name = "GetAllSchedulesList")] [Tags("Schedules")] [EndpointSummary("Get all schedules")] + [EndpointGroupName("general")] public async Task> GetAllSchedulesList(CancellationToken cancellationToken) => await mediator.Send(new GetAllProgramSchedules(), cancellationToken); [HttpGet("/api/schedules/{id:int}", Name = "GetScheduleById")] [Tags("Schedules")] [EndpointSummary("Get schedule by ID")] + [EndpointGroupName("general")] public async Task GetScheduleById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetProgramScheduleById(id), cancellationToken); @@ -40,12 +134,17 @@ public class ScheduleController(IMediator mediator) : ControllerBase [HttpGet("/api/schedules/{id:int}/items", Name = "GetScheduleItems")] [Tags("Schedules")] [EndpointSummary("Get schedule items")] - public async Task> GetScheduleItems(int id, CancellationToken cancellationToken) => - await mediator.Send(new GetProgramScheduleItems(id), cancellationToken); + [EndpointGroupName("general")] + public async Task> GetScheduleItems(int id, CancellationToken cancellationToken) + { + List items = await mediator.Send(new GetProgramScheduleItems(id), cancellationToken); + return items.Select(ToApiResponse).ToList(); + } [HttpPost("/api/schedules", Name = "CreateSchedule")] [Tags("Schedules")] [EndpointSummary("Create a schedule")] + [EndpointGroupName("general")] public async Task CreateSchedule( [Required] [FromBody] CreateScheduleRequest request, CancellationToken cancellationToken) @@ -65,6 +164,7 @@ public class ScheduleController(IMediator mediator) : ControllerBase [HttpDelete("/api/schedules/{id:int}", Name = "DeleteSchedule")] [Tags("Schedules")] [EndpointSummary("Delete a schedule")] + [EndpointGroupName("general")] public async Task DeleteSchedule(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new DeleteProgramSchedule(id), cancellationToken); @@ -74,6 +174,7 @@ public class ScheduleController(IMediator mediator) : ControllerBase [HttpPost("/api/schedules/{id:int}/copy", Name = "CopySchedule")] [Tags("Schedules")] [EndpointSummary("Copy a schedule")] + [EndpointGroupName("general")] public async Task CopySchedule( int id, [Required] [FromBody] CopyScheduleRequest request, diff --git a/ErsatzTV/Controllers/Api/ScriptedScheduleController.cs b/ErsatzTV/Controllers/Api/ScriptedScheduleController.cs index a7d5a2a8b..1ed912ada 100644 --- a/ErsatzTV/Controllers/Api/ScriptedScheduleController.cs +++ b/ErsatzTV/Controllers/Api/ScriptedScheduleController.cs @@ -16,6 +16,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpGet("context", Name = "GetContext")] [Tags("Scripted Metadata")] [EndpointSummary("Get the current context")] + [EndpointGroupName("general")] public ActionResult GetContext([FromRoute] Guid buildId) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -30,6 +31,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("add_collection", Name = "AddCollection")] [Tags("Scripted Content")] [EndpointSummary("Add a collection")] + [EndpointGroupName("general")] public async Task AddCollection( [FromRoute] Guid buildId, @@ -55,6 +57,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("add_marathon", Name = "AddMarathon")] [Tags("Scripted Content")] [EndpointSummary("Add a marathon")] + [EndpointGroupName("general")] public async Task AddMarathon([FromRoute] Guid buildId, [FromBody] ContentMarathon request) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -82,6 +85,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("add_multi_collection", Name = "AddMultiCollection")] [Tags("Scripted Content")] [EndpointSummary("Add a multi-collection")] + [EndpointGroupName("general")] public async Task AddMultiCollection( [FromRoute] Guid buildId, @@ -107,6 +111,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("add_playlist", Name = "AddPlaylist")] [Tags("Scripted Content")] [EndpointSummary("Add a playlist")] + [EndpointGroupName("general")] public async Task AddPlaylist( [FromRoute] Guid buildId, @@ -127,6 +132,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("create_playlist", Name="CreatePlaylist")] [Tags("Scripted Content")] [EndpointSummary("Create a playlist")] + [EndpointGroupName("general")] public async Task CreatePlaylist( [FromRoute] Guid buildId, @@ -150,6 +156,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("add_search", Name = "AddSearch")] [Tags("Scripted Content")] [EndpointSummary("Add a search query")] + [EndpointGroupName("general")] public async Task AddSearch( [FromRoute] Guid buildId, @@ -175,6 +182,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("add_smart_collection", Name = "AddSmartCollection")] [Tags("Scripted Content")] [EndpointSummary("Add a smart collection")] + [EndpointGroupName("general")] public async Task AddSmartCollection( [FromRoute] Guid buildId, @@ -200,6 +208,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("add_show", Name = "AddShow")] [Tags("Scripted Content")] [EndpointSummary("Add a show")] + [EndpointGroupName("general")] public async Task AddShow( [FromRoute] Guid buildId, @@ -225,6 +234,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("add_all", Name = "AddAll")] [Tags("Scripted Scheduling")] [EndpointSummary("Add all content")] + [EndpointGroupName("general")] public ActionResult AddAll([FromRoute] Guid buildId, [FromBody] ContentAll request) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -246,6 +256,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("add_count", Name = "AddCount")] [Tags("Scripted Scheduling")] [EndpointSummary("Add a specific number of content items")] + [EndpointGroupName("general")] public ActionResult AddCount( [FromRoute] Guid buildId, @@ -276,6 +287,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("add_duration", Name = "AddDuration")] [Tags("Scripted Scheduling")] [EndpointSummary("Add content for a specific duration")] + [EndpointGroupName("general")] public ActionResult AddDuration( [FromRoute] Guid buildId, @@ -311,6 +323,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("pad_to_next", Name = "PadToNext")] [Tags("Scripted Scheduling")] [EndpointSummary("Add content until a specific minutes interval")] + [EndpointGroupName("general")] public ActionResult PadToNext( [FromRoute] Guid buildId, [FromBody] PlayoutPadToNext request) @@ -344,6 +357,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("pad_until", Name = "PadUntil")] [Tags("Scripted Scheduling")] [EndpointSummary("Add content until a specified time of day")] + [EndpointGroupName("general")] public ActionResult PadUntil( [FromRoute] Guid buildId, [FromBody] PlayoutPadUntil request) @@ -378,6 +392,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("pad_until_exact", Name = "PadUntilExact")] [Tags("Scripted Scheduling")] [EndpointSummary("Add content until an exact time")] + [EndpointGroupName("general")] public ActionResult PadUntilExact( [FromRoute] Guid buildId, @@ -413,6 +428,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpGet("peek_next/{content}", Name="PeekNext")] [Tags("Scripted Scheduling")] [EndpointSummary("Peek the next content item")] + [EndpointGroupName("general")] public ActionResult PeekNext(Guid buildId, string content) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -437,6 +453,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("start_epg_group", Name = "StartEpgGroup")] [Tags("Scripted Control")] [EndpointSummary("Start an EPG group")] + [EndpointGroupName("general")] public IActionResult StartEpgGroup([FromRoute] Guid buildId, [FromBody] ControlStartEpgGroup request) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -452,6 +469,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("stop_epg_group", Name = "StopEpgGroup")] [Tags("Scripted Control")] [EndpointSummary("Finish an EPG group")] + [EndpointGroupName("general")] public IActionResult StopEpgGroup([FromRoute] Guid buildId) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -467,6 +485,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("graphics_on", Name = "GraphicsOn")] [Tags("Scripted Control")] [EndpointSummary("Turn on graphics elements")] + [EndpointGroupName("general")] public async Task GraphicsOn( [FromRoute] Guid buildId, @@ -487,6 +506,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("graphics_off", Name = "GraphicsOff")] [Tags("Scripted Control")] [EndpointSummary("Turn off graphics elements")] + [EndpointGroupName("general")] public async Task GraphicsOff( [FromRoute] Guid buildId, @@ -507,6 +527,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("watermark_on", Name = "WatermarkOn")] [Tags("Scripted Control")] [EndpointSummary("Turn on watermarks")] + [EndpointGroupName("general")] public async Task WatermarkOn([FromRoute] Guid buildId, [FromBody] ControlWatermarkOn request) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -522,6 +543,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("watermark_off", Name = "WatermarkOff")] [Tags("Scripted Control")] [EndpointSummary("Turn off watermarks")] + [EndpointGroupName("general")] public async Task WatermarkOff([FromRoute] Guid buildId, [FromBody] ControlWatermarkOff request) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -537,6 +559,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("pre_roll_on", Name = "PreRollOn")] [Tags("Scripted Control")] [EndpointSummary("Turn on pre-roll playlist")] + [EndpointGroupName("general")] public IActionResult PreRollOn([FromRoute] Guid buildId, [FromBody] ControlPreRollOn request) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -552,6 +575,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("pre_roll_off", Name = "PreRollOff")] [Tags("Scripted Control")] [EndpointSummary("Turn off pre-roll playlist")] + [EndpointGroupName("general")] public IActionResult PreRollOff([FromRoute] Guid buildId) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -567,6 +591,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("skip_items", Name = "SkipItems")] [Tags("Scripted Control")] [EndpointSummary("Skip a specific number of items")] + [EndpointGroupName("general")] public IActionResult SkipItems([FromRoute] Guid buildId, [FromBody] ControlSkipItems request) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -582,6 +607,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("skip_to_item", Name = "SkipToItem")] [Tags("Scripted Control")] [EndpointSummary("Skip to a specific episode")] + [EndpointGroupName("general")] public IActionResult SkipToItem([FromRoute] Guid buildId, [FromBody] ControlSkipToItem request) { ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); @@ -597,6 +623,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("wait_until_exact", Name = "WaitUntilExact")] [Tags("Scripted Control")] [EndpointSummary("Wait until an exact time")] + [EndpointGroupName("general")] public ActionResult WaitUntilExact( [FromRoute] Guid buildId, @@ -617,6 +644,7 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP [HttpPost("wait_until", Name = "WaitUntil")] [Tags("Scripted Control")] [EndpointSummary("Wait until the specified time of day")] + [EndpointGroupName("general")] public ActionResult WaitUntilTime( [FromRoute] Guid buildId, diff --git a/ErsatzTV/Controllers/Api/SearchController.cs b/ErsatzTV/Controllers/Api/SearchController.cs index 6115219ad..feb980c2a 100644 --- a/ErsatzTV/Controllers/Api/SearchController.cs +++ b/ErsatzTV/Controllers/Api/SearchController.cs @@ -6,12 +6,13 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class SearchController(IMediator mediator) : ControllerBase { [HttpGet("/api/search", Name = "SearchAll")] [Tags("Search")] [EndpointSummary("Search all media types")] + [EndpointGroupName("general")] public async Task SearchAll( [FromQuery] string query, CancellationToken cancellationToken) => @@ -20,6 +21,7 @@ public class SearchController(IMediator mediator) : ControllerBase [HttpGet("/api/search/movies", Name = "SearchMovies")] [Tags("Search")] [EndpointSummary("Search movies")] + [EndpointGroupName("general")] public async Task SearchMovies( [FromQuery] string query, [FromQuery] int pageNumber = 0, @@ -30,6 +32,7 @@ public class SearchController(IMediator mediator) : ControllerBase [HttpGet("/api/search/shows", Name = "SearchShows")] [Tags("Search")] [EndpointSummary("Search TV shows")] + [EndpointGroupName("general")] public async Task SearchShows( [FromQuery] string query, [FromQuery] int pageNumber = 0, @@ -40,6 +43,7 @@ public class SearchController(IMediator mediator) : ControllerBase [HttpGet("/api/search/episodes", Name = "SearchEpisodes")] [Tags("Search")] [EndpointSummary("Search episodes")] + [EndpointGroupName("general")] public async Task SearchEpisodes( [FromQuery] string query, [FromQuery] int pageNumber = 0, @@ -50,6 +54,7 @@ public class SearchController(IMediator mediator) : ControllerBase [HttpGet("/api/search/artists", Name = "SearchArtists")] [Tags("Search")] [EndpointSummary("Search artists")] + [EndpointGroupName("general")] public async Task SearchArtists( [FromQuery] string query, [FromQuery] int pageNumber = 0, @@ -60,6 +65,7 @@ public class SearchController(IMediator mediator) : ControllerBase [HttpGet("/api/search/music-videos", Name = "SearchMusicVideos")] [Tags("Search")] [EndpointSummary("Search music videos")] + [EndpointGroupName("general")] public async Task SearchMusicVideos( [FromQuery] string query, [FromQuery] int pageNumber = 0, @@ -70,6 +76,7 @@ public class SearchController(IMediator mediator) : ControllerBase [HttpGet("/api/search/songs", Name = "SearchSongs")] [Tags("Search")] [EndpointSummary("Search songs")] + [EndpointGroupName("general")] public async Task SearchSongs( [FromQuery] string query, [FromQuery] int pageNumber = 0, @@ -80,6 +87,7 @@ public class SearchController(IMediator mediator) : ControllerBase [HttpGet("/api/search/images", Name = "SearchImages")] [Tags("Search")] [EndpointSummary("Search images")] + [EndpointGroupName("general")] public async Task SearchImages( [FromQuery] string query, [FromQuery] int pageNumber = 0, @@ -90,6 +98,7 @@ public class SearchController(IMediator mediator) : ControllerBase [HttpPost("/api/search/rebuild", Name = "RebuildSearchIndex")] [Tags("Search")] [EndpointSummary("Rebuild search index")] + [EndpointGroupName("general")] public async Task RebuildSearchIndex(CancellationToken cancellationToken) { await mediator.Send(new RebuildSearchIndex(), cancellationToken); diff --git a/ErsatzTV/Controllers/Api/SessionController.cs b/ErsatzTV/Controllers/Api/SessionController.cs index 29508bacc..c6a3d131c 100644 --- a/ErsatzTV/Controllers/Api/SessionController.cs +++ b/ErsatzTV/Controllers/Api/SessionController.cs @@ -5,17 +5,19 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class SessionController(IFFmpegSegmenterService ffmpegSegmenterService) { [HttpGet("api/sessions")] [Tags("Sessions")] [EndpointSummary("Get sessions")] + [EndpointGroupName("general")] public List GetSessions() => ffmpegSegmenterService.Workers.Map(w => w.GetModel()).ToList(); [HttpDelete("api/session/{channelNumber}")] [Tags("Sessions")] [EndpointSummary("Stop session")] + [EndpointGroupName("general")] public async Task StopSession(string channelNumber, CancellationToken cancellationToken) { if (await ffmpegSegmenterService.StopChannel(channelNumber, cancellationToken)) diff --git a/ErsatzTV/Controllers/Api/SmartCollectionController.cs b/ErsatzTV/Controllers/Api/SmartCollectionController.cs index 9c12269f6..744000471 100644 --- a/ErsatzTV/Controllers/Api/SmartCollectionController.cs +++ b/ErsatzTV/Controllers/Api/SmartCollectionController.cs @@ -8,14 +8,15 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] public class SmartCollectionController(IMediator mediator) : ControllerBase { [HttpGet("/api/collections/smart", Name="GetSmartCollections")] + [EndpointGroupName("general")] public async Task> GetAll() => await mediator.Send(new GetAllSmartCollectionsForApi()); [HttpPost("/api/collections/smart/new", Name = "CreateSmartCollection")] + [EndpointGroupName("general")] public async Task AddOne( [Required] [FromBody] CreateSmartCollection request) @@ -26,6 +27,7 @@ public class SmartCollectionController(IMediator mediator) : ControllerBase } [HttpPut("/api/collections/smart/update", Name="UpdateSmartCollection")] + [EndpointGroupName("general")] public async Task UpdateOne( [Required] [FromBody] UpdateSmartCollection request) @@ -35,6 +37,7 @@ public class SmartCollectionController(IMediator mediator) : ControllerBase } [HttpDelete("/api/collections/smart/delete/{id:int}", Name="DeleteSmartCollection")] + [EndpointGroupName("general")] public async Task DeleteSmartCollection(int id) { Either result = await mediator.Send(new DeleteSmartCollection(id)); diff --git a/ErsatzTV/Controllers/Api/TemplateController.cs b/ErsatzTV/Controllers/Api/TemplateController.cs index bf4847ec8..3b4cbc65b 100644 --- a/ErsatzTV/Controllers/Api/TemplateController.cs +++ b/ErsatzTV/Controllers/Api/TemplateController.cs @@ -9,19 +9,21 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class TemplateController(IMediator mediator) : ControllerBase { // Template Groups [HttpGet("/api/templates/groups", Name = "GetTemplateGroups")] [Tags("Templates")] [EndpointSummary("Get all template groups")] + [EndpointGroupName("general")] public async Task> GetTemplateGroups(CancellationToken cancellationToken) => await mediator.Send(new GetAllTemplateGroups(), cancellationToken); [HttpPost("/api/templates/groups", Name = "CreateTemplateGroup")] [Tags("Templates")] [EndpointSummary("Create a template group")] + [EndpointGroupName("general")] public async Task CreateTemplateGroup( [Required] [FromBody] CreateTemplateGroupRequest request, CancellationToken cancellationToken) @@ -34,6 +36,7 @@ public class TemplateController(IMediator mediator) : ControllerBase [HttpDelete("/api/templates/groups/{id:int}", Name = "DeleteTemplateGroup")] [Tags("Templates")] [EndpointSummary("Delete a template group")] + [EndpointGroupName("general")] public async Task DeleteTemplateGroup(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeleteTemplateGroup(id), cancellationToken); @@ -44,18 +47,21 @@ public class TemplateController(IMediator mediator) : ControllerBase [HttpGet("/api/templates/groups/{groupId:int}/templates", Name = "GetTemplatesByGroup")] [Tags("Templates")] [EndpointSummary("Get templates by group")] + [EndpointGroupName("general")] public async Task> GetTemplatesByGroup(int groupId, CancellationToken cancellationToken) => await mediator.Send(new GetTemplatesByTemplateGroupId(groupId), cancellationToken); [HttpGet("/api/templates", Name = "GetAllTemplates")] [Tags("Templates")] [EndpointSummary("Get all templates")] + [EndpointGroupName("general")] public async Task> GetAllTemplates(CancellationToken cancellationToken) => await mediator.Send(new GetAllTemplates(), cancellationToken); [HttpGet("/api/templates/{id:int}", Name = "GetTemplateById")] [Tags("Templates")] [EndpointSummary("Get template by ID")] + [EndpointGroupName("general")] public async Task GetTemplateById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetTemplateById(id), cancellationToken); @@ -65,12 +71,14 @@ public class TemplateController(IMediator mediator) : ControllerBase [HttpGet("/api/templates/{id:int}/items", Name = "GetTemplateItems")] [Tags("Templates")] [EndpointSummary("Get template items")] + [EndpointGroupName("general")] public async Task> GetTemplateItems(int id, CancellationToken cancellationToken) => await mediator.Send(new GetTemplateItems(id), cancellationToken); [HttpPost("/api/templates", Name = "CreateTemplate")] [Tags("Templates")] [EndpointSummary("Create a template")] + [EndpointGroupName("general")] public async Task CreateTemplate( [Required] [FromBody] CreateTemplateRequest request, CancellationToken cancellationToken) @@ -83,6 +91,7 @@ public class TemplateController(IMediator mediator) : ControllerBase [HttpDelete("/api/templates/{id:int}", Name = "DeleteTemplate")] [Tags("Templates")] [EndpointSummary("Delete a template")] + [EndpointGroupName("general")] public async Task DeleteTemplate(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new DeleteTemplate(id), cancellationToken); @@ -92,6 +101,7 @@ public class TemplateController(IMediator mediator) : ControllerBase [HttpPost("/api/templates/{id:int}/copy", Name = "CopyTemplate")] [Tags("Templates")] [EndpointSummary("Copy a template")] + [EndpointGroupName("general")] public async Task CopyTemplate( int id, [Required] [FromBody] CopyTemplateRequest request, @@ -105,6 +115,7 @@ public class TemplateController(IMediator mediator) : ControllerBase [HttpPut("/api/templates/{id:int}/items", Name = "ReplaceTemplateItems")] [Tags("Templates")] [EndpointSummary("Replace template items")] + [EndpointGroupName("general")] public async Task ReplaceTemplateItems( int id, [Required] [FromBody] ReplaceTemplateItemsRequest request, diff --git a/ErsatzTV/Controllers/Api/TraktController.cs b/ErsatzTV/Controllers/Api/TraktController.cs index 381535c72..1ad586f68 100644 --- a/ErsatzTV/Controllers/Api/TraktController.cs +++ b/ErsatzTV/Controllers/Api/TraktController.cs @@ -8,12 +8,13 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class TraktController(IMediator mediator) : ControllerBase { [HttpGet("/api/trakt/lists", Name = "GetTraktLists")] [Tags("Trakt")] [EndpointSummary("Get all Trakt lists (paginated)")] + [EndpointGroupName("general")] public async Task GetTraktLists( [FromQuery] int pageNumber = 0, [FromQuery] int pageSize = 50, @@ -23,6 +24,7 @@ public class TraktController(IMediator mediator) : ControllerBase [HttpGet("/api/trakt/lists/{id:int}", Name = "GetTraktListById")] [Tags("Trakt")] [EndpointSummary("Get Trakt list by ID")] + [EndpointGroupName("general")] public async Task GetTraktListById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetTraktListById(id), cancellationToken); @@ -32,6 +34,7 @@ public class TraktController(IMediator mediator) : ControllerBase [HttpPut("/api/trakt/lists/{id:int}", Name = "UpdateTraktList")] [Tags("Trakt")] [EndpointSummary("Update a Trakt list")] + [EndpointGroupName("general")] public async Task UpdateTraktList( int id, [Required] [FromBody] UpdateTraktListRequest request, @@ -45,6 +48,7 @@ public class TraktController(IMediator mediator) : ControllerBase [HttpDelete("/api/trakt/lists/{id:int}", Name = "DeleteTraktList")] [Tags("Trakt")] [EndpointSummary("Delete a Trakt list")] + [EndpointGroupName("general")] public async Task DeleteTraktList(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new DeleteTraktList(id), cancellationToken); @@ -54,6 +58,7 @@ public class TraktController(IMediator mediator) : ControllerBase [HttpPost("/api/trakt/lists/{id:int}/match", Name = "MatchTraktListItems")] [Tags("Trakt")] [EndpointSummary("Match Trakt list items")] + [EndpointGroupName("general")] public async Task MatchTraktListItems(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new MatchTraktListItems(id), cancellationToken); diff --git a/ErsatzTV/Controllers/Api/VersionController.cs b/ErsatzTV/Controllers/Api/VersionController.cs index 6c144c312..61405ec77 100644 --- a/ErsatzTV/Controllers/Api/VersionController.cs +++ b/ErsatzTV/Controllers/Api/VersionController.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class VersionController { private static readonly CombinedVersion Version; @@ -19,6 +19,7 @@ public class VersionController [HttpGet("/api/version", Name="GetVersion")] [Tags("Version")] [EndpointSummary("Get version")] + [EndpointGroupName("general")] public CombinedVersion GetVersion() => Version; public record CombinedVersion(int ApiVersion, string AppVersion); diff --git a/ErsatzTV/Controllers/Api/WatermarkController.cs b/ErsatzTV/Controllers/Api/WatermarkController.cs index 8c2ca279c..a96989038 100644 --- a/ErsatzTV/Controllers/Api/WatermarkController.cs +++ b/ErsatzTV/Controllers/Api/WatermarkController.cs @@ -12,18 +12,20 @@ using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] -[EndpointGroupName("general")] + public class WatermarkController(IMediator mediator) : ControllerBase { [HttpGet("/api/watermarks", Name = "GetAllWatermarks")] [Tags("Watermarks")] [EndpointSummary("Get all watermarks")] + [EndpointGroupName("general")] public async Task> GetAllWatermarks(CancellationToken cancellationToken) => await mediator.Send(new GetAllWatermarks(), cancellationToken); [HttpGet("/api/watermarks/{id:int}", Name = "GetWatermarkById")] [Tags("Watermarks")] [EndpointSummary("Get watermark by ID")] + [EndpointGroupName("general")] public async Task GetWatermarkById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetWatermarkById(id), cancellationToken); @@ -33,6 +35,7 @@ public class WatermarkController(IMediator mediator) : ControllerBase [HttpPost("/api/watermarks", Name = "CreateWatermark")] [Tags("Watermarks")] [EndpointSummary("Create a watermark")] + [EndpointGroupName("general")] public async Task CreateWatermark( [Required] [FromBody] CreateWatermarkRequest request, CancellationToken cancellationToken) @@ -65,6 +68,7 @@ public class WatermarkController(IMediator mediator) : ControllerBase [HttpPut("/api/watermarks/{id:int}", Name = "UpdateWatermark")] [Tags("Watermarks")] [EndpointSummary("Update a watermark")] + [EndpointGroupName("general")] public async Task UpdateWatermark( int id, [Required] [FromBody] UpdateWatermarkRequest request, @@ -99,6 +103,7 @@ public class WatermarkController(IMediator mediator) : ControllerBase [HttpDelete("/api/watermarks/{id:int}", Name = "DeleteWatermark")] [Tags("Watermarks")] [EndpointSummary("Delete a watermark")] + [EndpointGroupName("general")] public async Task DeleteWatermark(int id, CancellationToken cancellationToken) { Either result = await mediator.Send(new DeleteWatermark(id), cancellationToken); @@ -108,6 +113,7 @@ public class WatermarkController(IMediator mediator) : ControllerBase [HttpPost("/api/watermarks/{id:int}/copy", Name = "CopyWatermark")] [Tags("Watermarks")] [EndpointSummary("Copy a watermark")] + [EndpointGroupName("general")] public async Task CopyWatermark( int id, [Required] [FromBody] CopyWatermarkRequest request,