Browse Source

api changes to support etvcli (#2519)

pull/2520/head
Jason Dove 7 months ago committed by GitHub
parent
commit
48e7c85f7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs
  2. 3
      ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionResult.cs
  3. 3
      ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollection.cs
  4. 8
      ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs
  5. 3
      ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionResult.cs
  6. 4
      ErsatzTV.Application/MediaCollections/Mapper.cs
  7. 5
      ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsForApi.cs
  8. 22
      ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsForApiHandler.cs
  9. 3
      ErsatzTV.Core/Api/SmartCollections/SmartCollectionResponseModel.cs
  10. 15
      ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs
  11. 2
      ErsatzTV/Controllers/Api/ChannelController.cs
  12. 16
      ErsatzTV/Controllers/Api/FFmpegProfileController.cs
  13. 43
      ErsatzTV/Controllers/Api/SmartCollectionController.cs
  14. 16
      ErsatzTV/Controllers/Api/VersionController.cs
  15. 2
      ErsatzTV/Pages/Search.razor
  16. 2
      ErsatzTV/Pages/SmartCollectionEditor.razor
  17. 305
      ErsatzTV/wwwroot/openapi/v1.json

9
ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs

@ -5,17 +5,14 @@ using static ErsatzTV.Application.Channels.Mapper; @@ -5,17 +5,14 @@ using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels;
public class GetAllChannelsForApiHandler : IRequestHandler<GetAllChannelsForApi, List<ChannelResponseModel>>
public class GetAllChannelsForApiHandler(IChannelRepository channelRepository)
: IRequestHandler<GetAllChannelsForApi, List<ChannelResponseModel>>
{
private readonly IChannelRepository _channelRepository;
public GetAllChannelsForApiHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public async Task<List<ChannelResponseModel>> Handle(
GetAllChannelsForApi request,
CancellationToken cancellationToken)
{
IEnumerable<Channel> channels = Optional(await _channelRepository.GetAll(cancellationToken)).Flatten();
IEnumerable<Channel> channels = Optional(await channelRepository.GetAll(cancellationToken)).Flatten();
return channels.Map(ProjectToResponseModel).ToList();
}
}

3
ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionResult.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.MediaCollections;
public record CreateSmartCollectionResult(int SmartCollectionId) : EntityIdResult(SmartCollectionId);

3
ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollection.cs

@ -2,4 +2,5 @@ @@ -2,4 +2,5 @@
namespace ErsatzTV.Application.MediaCollections;
public record UpdateSmartCollection(int Id, string Name, string Query) : IRequest<Either<BaseError, Unit>>;
public record UpdateSmartCollection(int Id, string Name, string Query)
: IRequest<Either<BaseError, UpdateSmartCollectionResult>>;

8
ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs

@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore; @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollection, Either<BaseError, Unit>>
public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollection, Either<BaseError, UpdateSmartCollectionResult>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
@ -34,7 +34,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio @@ -34,7 +34,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
_smartCollectionCache = smartCollectionCache;
}
public async Task<Either<BaseError, Unit>> Handle(
public async Task<Either<BaseError, UpdateSmartCollectionResult>> Handle(
UpdateSmartCollection request,
CancellationToken cancellationToken)
{
@ -43,7 +43,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio @@ -43,7 +43,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request, cancellationToken));
}
private async Task<Unit> ApplyUpdateRequest(
private async Task<UpdateSmartCollectionResult> ApplyUpdateRequest(
TvContext dbContext,
SmartCollection c,
UpdateSmartCollection request,
@ -65,7 +65,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio @@ -65,7 +65,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
}
}
return Unit.Default;
return new UpdateSmartCollectionResult(c.Id);
}
private static Task<Validation<BaseError, SmartCollection>> Validate(

3
ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionResult.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.MediaCollections;
public record UpdateSmartCollectionResult(int SmartCollectionId) : EntityIdResult(SmartCollectionId);

4
ErsatzTV.Application/MediaCollections/Mapper.cs

@ -1,4 +1,5 @@ @@ -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 @@ -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,

5
ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsForApi.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core.Api.SmartCollections;
namespace ErsatzTV.Application.MediaCollections;
public record GetAllSmartCollectionsForApi : IRequest<List<SmartCollectionResponseModel>>;

22
ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsForApiHandler.cs

@ -0,0 +1,22 @@ @@ -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<TvContext> dbContextFactory)
: IRequestHandler<GetAllSmartCollectionsForApi, List<SmartCollectionResponseModel>>
{
public async Task<List<SmartCollectionResponseModel>> Handle(
GetAllSmartCollectionsForApi request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<SmartCollection> ffmpegProfiles = await dbContext.SmartCollections
.AsNoTracking()
.ToListAsync(cancellationToken);
return ffmpegProfiles.Map(ProjectToResponseModel).ToList();
}
}

3
ErsatzTV.Core/Api/SmartCollections/SmartCollectionResponseModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Core.Api.SmartCollections;
public record SmartCollectionResponseModel(int Id, string Name, string Query);

15
ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs

@ -4,16 +4,11 @@ using Microsoft.EntityFrameworkCore; @@ -4,16 +4,11 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class ChannelRepository : IChannelRepository
public class ChannelRepository(IDbContextFactory<TvContext> dbContextFactory) : IChannelRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public ChannelRepository(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Option<Channel>> 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 @@ -25,7 +20,7 @@ public class ChannelRepository : IChannelRepository
public async Task<Option<Channel>> 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 @@ -39,7 +34,7 @@ public class ChannelRepository : IChannelRepository
public async Task<List<Channel>> 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 @@ -52,7 +47,7 @@ public class ChannelRepository : IChannelRepository
public async Task<Option<ChannelWatermark>> GetWatermarkByName(string name)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
List<ChannelWatermark> maybeWatermarks = await dbContext.ChannelWatermarks
.AsNoTracking()

2
ErsatzTV/Controllers/Api/ChannelController.cs

@ -14,7 +14,7 @@ namespace ErsatzTV.Controllers.Api; @@ -14,7 +14,7 @@ namespace ErsatzTV.Controllers.Api;
public class ChannelController(ChannelWriter<IBackgroundServiceRequest> workerChannel, IMediator mediator)
{
[HttpGet("/api/channels")]
[V2ApiActionFilter]
[EndpointGroupName("general")]
public async Task<List<ChannelResponseModel>> GetAll() => await mediator.Send(new GetAllChannelsForApi());
[HttpPost("/api/channels/{channelNumber}/playout/reset")]

16
ErsatzTV/Controllers/Api/FFmpegProfileController.cs

@ -10,30 +10,26 @@ namespace ErsatzTV.Controllers.Api; @@ -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<List<FFmpegProfileResponseModel>> GetAll() =>
await _mediator.Send(new GetAllFFmpegProfilesForApi());
await mediator.Send(new GetAllFFmpegProfilesForApi());
[HttpPost("/api/ffmpeg/profiles/new")]
public async Task<Either<BaseError, CreateFFmpegProfileResult>> AddOne(
[Required] [FromBody]
CreateFFmpegProfile request) => await _mediator.Send(request);
CreateFFmpegProfile request) => await mediator.Send(request);
[HttpPut("/api/ffmpeg/profiles/update")]
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> UpdateOne(
[Required] [FromBody]
UpdateFFmpegProfile request) => await _mediator.Send(request);
UpdateFFmpegProfile request) => await mediator.Send(request);
[HttpGet("/api/ffmpeg/profiles/{id:int}")]
public async Task<Option<FFmpegFullProfileResponseModel>> 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));
}

43
ErsatzTV/Controllers/Api/SmartCollectionController.cs

@ -0,0 +1,43 @@ @@ -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<List<SmartCollectionResponseModel>> GetAll() =>
await mediator.Send(new GetAllSmartCollectionsForApi());
[HttpPost("/api/collections/smart/new", Name = "CreateSmartCollection")]
public async Task<IActionResult> AddOne(
[Required] [FromBody]
CreateSmartCollection request)
{
Either<BaseError, CreateSmartCollectionResult> result =
await mediator.Send(request).MapT(r => new CreateSmartCollectionResult(r.Id));
return result.Match<IActionResult>(Ok, error => Problem(error.ToString()));
}
[HttpPut("/api/collections/smart/update", Name="UpdateSmartCollection")]
public async Task<IActionResult> UpdateOne(
[Required] [FromBody]
UpdateSmartCollection request)
{
Either<BaseError, UpdateSmartCollectionResult> result = await mediator.Send(request);
return result.Match<IActionResult>(Ok, error => Problem(error.ToString()));
}
[HttpDelete("/api/collections/smart/delete/{id:int}", Name="DeleteSmartCollection")]
public async Task<IActionResult> DeleteSmartCollection(int id)
{
Either<BaseError, Unit> result = await mediator.Send(new DeleteSmartCollection(id));
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString()));
}
}

16
ErsatzTV/Controllers/Api/VersionController.cs

@ -7,15 +7,19 @@ namespace ErsatzTV.Controllers.Api; @@ -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<AssemblyInformationalVersionAttribute>()?
.InformationalVersion ?? "unknown";
Version = new CombinedVersion(
1,
Assembly.GetEntryAssembly()?
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.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);
}

2
ErsatzTV/Pages/Search.razor

@ -953,7 +953,7 @@ @@ -953,7 +953,7 @@
collectionResult.Item1.Name,
_query);
Either<BaseError, Unit> updateResult = await Mediator.Send(request, CancellationToken);
Either<BaseError, UpdateSmartCollectionResult> updateResult = await Mediator.Send(request, CancellationToken);
updateResult.Match(
Left: error =>
{

2
ErsatzTV/Pages/SmartCollectionEditor.razor

@ -70,7 +70,7 @@ @@ -70,7 +70,7 @@
await _form.Validate();
if (_success)
{
Either<BaseError, Unit> result = await Mediator.Send(new UpdateSmartCollection(Id, _model.Name, _model.Query), _cts.Token);
Either<BaseError, UpdateSmartCollectionResult> result = await Mediator.Send(new UpdateSmartCollection(Id, _model.Name, _model.Query), _cts.Token);
result.Match(
_ => NavigationManager.NavigateTo("media/smart-collections"),
error =>

305
ErsatzTV/wwwroot/openapi/v1.json

@ -5,6 +5,44 @@ @@ -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 @@ @@ -199,29 +237,169 @@
}
}
},
"/api/collections/smart": {
"get": {
"tags": [
"SmartCollection"
],
"operationId": "GetSmartCollections",
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SmartCollectionResponseModel"
}
}
},
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SmartCollectionResponseModel"
}
}
},
"text/json": {
"schema": {
"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": {
"type": "string"
"$ref": "#/components/schemas/CombinedVersion"
}
},
"application/json": {
"schema": {
"type": "string"
"$ref": "#/components/schemas/CombinedVersion"
}
},
"text/json": {
"schema": {
"type": "string"
"$ref": "#/components/schemas/CombinedVersion"
}
}
}
@ -232,6 +410,77 @@ @@ -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 @@ @@ -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 @@ @@ -290,6 +586,9 @@
{
"name": "Sessions"
},
{
"name": "SmartCollection"
},
{
"name": "Version"
}

Loading…
Cancel
Save