diff --git a/ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs b/ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs index f9e6183fc..c9f20b63f 100644 --- a/ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs @@ -5,17 +5,14 @@ using static ErsatzTV.Application.Channels.Mapper; namespace ErsatzTV.Application.Channels; -public class GetAllChannelsForApiHandler : IRequestHandler> +public class GetAllChannelsForApiHandler(IChannelRepository channelRepository) + : IRequestHandler> { - private readonly IChannelRepository _channelRepository; - - public GetAllChannelsForApiHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; - public async Task> Handle( GetAllChannelsForApi request, CancellationToken cancellationToken) { - IEnumerable channels = Optional(await _channelRepository.GetAll(cancellationToken)).Flatten(); + IEnumerable channels = Optional(await channelRepository.GetAll(cancellationToken)).Flatten(); return channels.Map(ProjectToResponseModel).ToList(); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionResult.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionResult.cs new file mode 100644 index 000000000..f72be6932 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionResult.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.MediaCollections; + +public record CreateSmartCollectionResult(int SmartCollectionId) : EntityIdResult(SmartCollectionId); diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollection.cs index 8980550e5..3510aa475 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollection.cs @@ -2,4 +2,5 @@ namespace ErsatzTV.Application.MediaCollections; -public record UpdateSmartCollection(int Id, string Name, string Query) : IRequest>; +public record UpdateSmartCollection(int Id, string Name, string Query) + : IRequest>; diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs index f13308088..ba79c2dde 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.MediaCollections; -public class UpdateSmartCollectionHandler : IRequestHandler> +public class UpdateSmartCollectionHandler : IRequestHandler> { private readonly ChannelWriter _channel; private readonly IDbContextFactory _dbContextFactory; @@ -34,7 +34,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler> Handle( + public async Task> Handle( UpdateSmartCollection request, CancellationToken cancellationToken) { @@ -43,7 +43,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler ApplyUpdateRequest(dbContext, c, request, cancellationToken)); } - private async Task ApplyUpdateRequest( + private async Task ApplyUpdateRequest( TvContext dbContext, SmartCollection c, UpdateSmartCollection request, @@ -65,7 +65,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler> Validate( diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionResult.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionResult.cs new file mode 100644 index 000000000..10f21a22c --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionResult.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.MediaCollections; + +public record UpdateSmartCollectionResult(int SmartCollectionId) : EntityIdResult(SmartCollectionId); diff --git a/ErsatzTV.Application/MediaCollections/Mapper.cs b/ErsatzTV.Application/MediaCollections/Mapper.cs index 8fec80461..05271e474 100644 --- a/ErsatzTV.Application/MediaCollections/Mapper.cs +++ b/ErsatzTV.Application/MediaCollections/Mapper.cs @@ -1,4 +1,5 @@ using ErsatzTV.Application.Tree; +using ErsatzTV.Core.Api.SmartCollections; using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.MediaCollections; @@ -23,6 +24,9 @@ internal static class Mapper internal static SmartCollectionViewModel ProjectToViewModel(SmartCollection collection) => new(collection.Id, collection.Name, collection.Query); + internal static SmartCollectionResponseModel ProjectToResponseModel(SmartCollection collection) => + new(collection.Id, collection.Name, collection.Query); + internal static RerunCollectionViewModel ProjectToViewModel(RerunCollection collection) => new( collection.Id, diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsForApi.cs b/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsForApi.cs new file mode 100644 index 000000000..0ad0458bf --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsForApi.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core.Api.SmartCollections; + +namespace ErsatzTV.Application.MediaCollections; + +public record GetAllSmartCollectionsForApi : IRequest>; diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsForApiHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsForApiHandler.cs new file mode 100644 index 000000000..f71aa6b32 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsForApiHandler.cs @@ -0,0 +1,22 @@ +using ErsatzTV.Core.Api.SmartCollections; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using static ErsatzTV.Application.MediaCollections.Mapper; + +namespace ErsatzTV.Application.MediaCollections; + +public class GetAllSmartCollectionsForApiHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + GetAllSmartCollectionsForApi request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + List ffmpegProfiles = await dbContext.SmartCollections + .AsNoTracking() + .ToListAsync(cancellationToken); + return ffmpegProfiles.Map(ProjectToResponseModel).ToList(); + } +} diff --git a/ErsatzTV.Core/Api/SmartCollections/SmartCollectionResponseModel.cs b/ErsatzTV.Core/Api/SmartCollections/SmartCollectionResponseModel.cs new file mode 100644 index 000000000..faeb2117c --- /dev/null +++ b/ErsatzTV.Core/Api/SmartCollections/SmartCollectionResponseModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Core.Api.SmartCollections; + +public record SmartCollectionResponseModel(int Id, string Name, string Query); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs index 05bb500d4..0afe4d005 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs @@ -4,16 +4,11 @@ using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Infrastructure.Data.Repositories; -public class ChannelRepository : IChannelRepository +public class ChannelRepository(IDbContextFactory dbContextFactory) : IChannelRepository { - private readonly IDbContextFactory _dbContextFactory; - - public ChannelRepository(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - public async Task> GetChannel(int id) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Channels .AsNoTracking() .Include(c => c.Artwork) @@ -25,7 +20,7 @@ public class ChannelRepository : IChannelRepository public async Task> GetByNumber(string number) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Channels .AsNoTracking() .Include(c => c.FFmpegProfile) @@ -39,7 +34,7 @@ public class ChannelRepository : IChannelRepository public async Task> GetAll(CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); return await dbContext.Channels .AsNoTracking() .Include(c => c.FFmpegProfile) @@ -52,7 +47,7 @@ public class ChannelRepository : IChannelRepository public async Task> GetWatermarkByName(string name) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); List maybeWatermarks = await dbContext.ChannelWatermarks .AsNoTracking() diff --git a/ErsatzTV/Controllers/Api/ChannelController.cs b/ErsatzTV/Controllers/Api/ChannelController.cs index 0a20a0689..baa1edb5d 100644 --- a/ErsatzTV/Controllers/Api/ChannelController.cs +++ b/ErsatzTV/Controllers/Api/ChannelController.cs @@ -14,7 +14,7 @@ namespace ErsatzTV.Controllers.Api; public class ChannelController(ChannelWriter workerChannel, IMediator mediator) { [HttpGet("/api/channels")] - [V2ApiActionFilter] + [EndpointGroupName("general")] public async Task> GetAll() => await mediator.Send(new GetAllChannelsForApi()); [HttpPost("/api/channels/{channelNumber}/playout/reset")] diff --git a/ErsatzTV/Controllers/Api/FFmpegProfileController.cs b/ErsatzTV/Controllers/Api/FFmpegProfileController.cs index f7742ee18..2cd5f2371 100644 --- a/ErsatzTV/Controllers/Api/FFmpegProfileController.cs +++ b/ErsatzTV/Controllers/Api/FFmpegProfileController.cs @@ -10,30 +10,26 @@ namespace ErsatzTV.Controllers.Api; [ApiController] [V2ApiActionFilter] -public class FFmpegProfileController +public class FFmpegProfileController(IMediator mediator) { - private readonly IMediator _mediator; - - public FFmpegProfileController(IMediator mediator) => _mediator = mediator; - [HttpGet("/api/ffmpeg/profiles")] public async Task> GetAll() => - await _mediator.Send(new GetAllFFmpegProfilesForApi()); + await mediator.Send(new GetAllFFmpegProfilesForApi()); [HttpPost("/api/ffmpeg/profiles/new")] public async Task> AddOne( [Required] [FromBody] - CreateFFmpegProfile request) => await _mediator.Send(request); + CreateFFmpegProfile request) => await mediator.Send(request); [HttpPut("/api/ffmpeg/profiles/update")] public async Task> UpdateOne( [Required] [FromBody] - UpdateFFmpegProfile request) => await _mediator.Send(request); + UpdateFFmpegProfile request) => await mediator.Send(request); [HttpGet("/api/ffmpeg/profiles/{id:int}")] public async Task> GetOne(int id) => - await _mediator.Send(new GetFFmpegFullProfileByIdForApi(id)); + await mediator.Send(new GetFFmpegFullProfileByIdForApi(id)); [HttpDelete("/api/ffmpeg/delete/{id:int}")] - public async Task DeleteProfileAsync(int id) => await _mediator.Send(new DeleteFFmpegProfile(id)); + public async Task DeleteProfileAsync(int id) => await mediator.Send(new DeleteFFmpegProfile(id)); } diff --git a/ErsatzTV/Controllers/Api/SmartCollectionController.cs b/ErsatzTV/Controllers/Api/SmartCollectionController.cs new file mode 100644 index 000000000..9c12269f6 --- /dev/null +++ b/ErsatzTV/Controllers/Api/SmartCollectionController.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; +using ErsatzTV.Application.MediaCollections; +using ErsatzTV.Core; +using ErsatzTV.Core.Api.SmartCollections; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace ErsatzTV.Controllers.Api; + +[ApiController] +[EndpointGroupName("general")] +public class SmartCollectionController(IMediator mediator) : ControllerBase +{ + [HttpGet("/api/collections/smart", Name="GetSmartCollections")] + public async Task> GetAll() => + await mediator.Send(new GetAllSmartCollectionsForApi()); + + [HttpPost("/api/collections/smart/new", Name = "CreateSmartCollection")] + public async Task AddOne( + [Required] [FromBody] + CreateSmartCollection request) + { + Either result = + await mediator.Send(request).MapT(r => new CreateSmartCollectionResult(r.Id)); + return result.Match(Ok, error => Problem(error.ToString())); + } + + [HttpPut("/api/collections/smart/update", Name="UpdateSmartCollection")] + public async Task UpdateOne( + [Required] [FromBody] + UpdateSmartCollection request) + { + Either result = await mediator.Send(request); + return result.Match(Ok, error => Problem(error.ToString())); + } + + [HttpDelete("/api/collections/smart/delete/{id:int}", Name="DeleteSmartCollection")] + public async Task DeleteSmartCollection(int id) + { + Either result = await mediator.Send(new DeleteSmartCollection(id)); + return result.Match(_ => Ok(), error => Problem(error.ToString())); + } +} diff --git a/ErsatzTV/Controllers/Api/VersionController.cs b/ErsatzTV/Controllers/Api/VersionController.cs index 5cd1a1884..4a7dafef2 100644 --- a/ErsatzTV/Controllers/Api/VersionController.cs +++ b/ErsatzTV/Controllers/Api/VersionController.cs @@ -7,15 +7,19 @@ namespace ErsatzTV.Controllers.Api; [EndpointGroupName("general")] public class VersionController { - private static readonly string Version; + private static readonly CombinedVersion Version; static VersionController() => - Version = Assembly.GetEntryAssembly()? - .GetCustomAttribute()? - .InformationalVersion ?? "unknown"; + Version = new CombinedVersion( + 1, + Assembly.GetEntryAssembly()? + .GetCustomAttribute()? + .InformationalVersion ?? "unknown"); - [HttpGet("/api/version")] + [HttpGet("/api/version", Name="GetVersion")] [Tags("Version")] [EndpointSummary("Get version")] - public string GetVersion() => Version; + public CombinedVersion GetVersion() => Version; + + public record CombinedVersion(int ApiVersion, string AppVersion); } diff --git a/ErsatzTV/Pages/Search.razor b/ErsatzTV/Pages/Search.razor index ce9797220..ea41be20d 100644 --- a/ErsatzTV/Pages/Search.razor +++ b/ErsatzTV/Pages/Search.razor @@ -953,7 +953,7 @@ collectionResult.Item1.Name, _query); - Either updateResult = await Mediator.Send(request, CancellationToken); + Either updateResult = await Mediator.Send(request, CancellationToken); updateResult.Match( Left: error => { diff --git a/ErsatzTV/Pages/SmartCollectionEditor.razor b/ErsatzTV/Pages/SmartCollectionEditor.razor index 1cafa1418..05464e1c2 100644 --- a/ErsatzTV/Pages/SmartCollectionEditor.razor +++ b/ErsatzTV/Pages/SmartCollectionEditor.razor @@ -70,7 +70,7 @@ await _form.Validate(); if (_success) { - Either result = await Mediator.Send(new UpdateSmartCollection(Id, _model.Name, _model.Query), _cts.Token); + Either result = await Mediator.Send(new UpdateSmartCollection(Id, _model.Name, _model.Query), _cts.Token); result.Match( _ => NavigationManager.NavigateTo("media/smart-collections"), error => diff --git a/ErsatzTV/wwwroot/openapi/v1.json b/ErsatzTV/wwwroot/openapi/v1.json index 60bef572c..c949e9538 100644 --- a/ErsatzTV/wwwroot/openapi/v1.json +++ b/ErsatzTV/wwwroot/openapi/v1.json @@ -5,6 +5,44 @@ "version": "1.0.0" }, "paths": { + "/api/channels": { + "get": { + "tags": [ + "Channel" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelResponseModel" + } + } + } + } + } + } + } + }, "/api/channels/{channelNumber}/playout/reset": { "post": { "tags": [ @@ -199,29 +237,169 @@ } } }, - "/api/version": { + "/api/collections/smart": { "get": { "tags": [ - "Version" + "SmartCollection" ], - "summary": "Get version", + "operationId": "GetSmartCollections", "responses": { "200": { "description": "OK", "content": { "text/plain": { "schema": { - "type": "string" + "type": "array", + "items": { + "$ref": "#/components/schemas/SmartCollectionResponseModel" + } } }, "application/json": { "schema": { - "type": "string" + "type": "array", + "items": { + "$ref": "#/components/schemas/SmartCollectionResponseModel" + } } }, "text/json": { "schema": { - "type": "string" + "type": "array", + "items": { + "$ref": "#/components/schemas/SmartCollectionResponseModel" + } + } + } + } + } + } + } + }, + "/api/collections/smart/new": { + "post": { + "tags": [ + "SmartCollection" + ], + "operationId": "CreateSmartCollection", + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/CreateSmartCollection" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSmartCollection" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateSmartCollection" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateSmartCollection" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/collections/smart/update": { + "put": { + "tags": [ + "SmartCollection" + ], + "operationId": "UpdateSmartCollection", + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/UpdateSmartCollection" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSmartCollection" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSmartCollection" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateSmartCollection" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/collections/smart/delete/{id}": { + "delete": { + "tags": [ + "SmartCollection" + ], + "operationId": "DeleteSmartCollection", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/version": { + "get": { + "tags": [ + "Version" + ], + "summary": "Get version", + "operationId": "GetVersion", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CombinedVersion" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CombinedVersion" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CombinedVersion" } } } @@ -232,6 +410,77 @@ }, "components": { "schemas": { + "ChannelResponseModel": { + "required": [ + "id", + "number", + "name", + "fFmpegProfile", + "language", + "streamingMode" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "number": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "fFmpegProfile": { + "type": "string", + "nullable": true + }, + "language": { + "type": "string", + "nullable": true + }, + "streamingMode": { + "type": "string", + "nullable": true + } + } + }, + "CombinedVersion": { + "required": [ + "apiVersion", + "appVersion" + ], + "type": "object", + "properties": { + "apiVersion": { + "type": "integer", + "format": "int32" + }, + "appVersion": { + "type": "string", + "nullable": true + } + } + }, + "CreateSmartCollection": { + "required": [ + "query", + "name" + ], + "type": "object", + "properties": { + "query": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + } + } + }, "HlsSessionModel": { "required": [ "channelNumber", @@ -274,10 +523,57 @@ "default": false } } + }, + "SmartCollectionResponseModel": { + "required": [ + "id", + "name", + "query" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "query": { + "type": "string", + "nullable": true + } + } + }, + "UpdateSmartCollection": { + "required": [ + "id", + "name", + "query" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "query": { + "type": "string", + "nullable": true + } + } } } }, "tags": [ + { + "name": "Channel" + }, { "name": "Channels" }, @@ -290,6 +586,9 @@ { "name": "Sessions" }, + { + "name": "SmartCollection" + }, { "name": "Version" }