mirror of https://github.com/ErsatzTV/ErsatzTV.git
21 changed files with 2407 additions and 62 deletions
@ -0,0 +1,159 @@
@@ -0,0 +1,159 @@
|
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.Scheduling; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace ErsatzTV.Controllers.Api; |
||||
|
||||
[ApiController] |
||||
[EndpointGroupName("general")] |
||||
public class BlockController(IMediator mediator) : ControllerBase |
||||
{ |
||||
// Block Groups
|
||||
[HttpGet("/api/blocks/groups", Name = "GetBlockGroups")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Get all block groups")] |
||||
public async Task<List<BlockGroupViewModel>> GetBlockGroups(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllBlockGroups(), cancellationToken); |
||||
|
||||
[HttpPost("/api/blocks/groups", Name = "CreateBlockGroup")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Create a block group")] |
||||
public async Task<IActionResult> CreateBlockGroup( |
||||
[Required] [FromBody] CreateBlockGroupRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, BlockGroupViewModel> result = await mediator.Send( |
||||
new CreateBlockGroup(request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/blocks/groups/{id:int}", Name = "DeleteBlockGroup")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Delete a block group")] |
||||
public async Task<IActionResult> DeleteBlockGroup(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send(new DeleteBlockGroup(id), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => NoContent()); |
||||
} |
||||
|
||||
// Blocks
|
||||
[HttpGet("/api/blocks/groups/{groupId:int}/blocks", Name = "GetBlocksByGroup")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Get blocks by group")] |
||||
public async Task<List<BlockViewModel>> GetBlocksByGroup(int groupId, CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetBlocksByBlockGroupId(groupId), cancellationToken); |
||||
|
||||
[HttpGet("/api/blocks", Name = "GetAllBlocks")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Get all blocks")] |
||||
public async Task<List<BlockViewModel>> GetAllBlocks(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllBlocks(), cancellationToken); |
||||
|
||||
[HttpGet("/api/blocks/{id:int}", Name = "GetBlockById")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Get block by ID")] |
||||
public async Task<IActionResult> GetBlockById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BlockViewModel> result = await mediator.Send(new GetBlockById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/blocks/{id:int}/items", Name = "GetBlockItems")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Get block items")] |
||||
public async Task<List<BlockItemViewModel>> GetBlockItems(int id, CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetBlockItems(id), cancellationToken); |
||||
|
||||
[HttpPost("/api/blocks", Name = "CreateBlock")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Create a block")] |
||||
public async Task<IActionResult> CreateBlock( |
||||
[Required] [FromBody] CreateBlockRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, BlockViewModel> result = await mediator.Send( |
||||
new CreateBlock(request.BlockGroupId, request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/blocks/{id:int}", Name = "DeleteBlock")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Delete a block")] |
||||
public async Task<IActionResult> DeleteBlock(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send(new DeleteBlock(id), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => NoContent()); |
||||
} |
||||
|
||||
[HttpPost("/api/blocks/{id:int}/copy", Name = "CopyBlock")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Copy a block")] |
||||
public async Task<IActionResult> CopyBlock( |
||||
int id, |
||||
[Required] [FromBody] CopyBlockRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, BlockViewModel> result = await mediator.Send( |
||||
new CopyBlock(id, request.NewBlockGroupId, request.NewBlockName), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/blocks/{id:int}/items", Name = "ReplaceBlockItems")] |
||||
[Tags("Blocks")] |
||||
[EndpointSummary("Replace block items")] |
||||
public async Task<IActionResult> ReplaceBlockItems( |
||||
int id, |
||||
[Required] [FromBody] ReplaceBlockItemsRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var items = request.Items?.Select(i => new ReplaceBlockItem( |
||||
i.Index, |
||||
i.CollectionType, |
||||
i.CollectionId, |
||||
i.MultiCollectionId, |
||||
i.SmartCollectionId, |
||||
i.MediaItemId, |
||||
i.SearchTitle ?? "", |
||||
i.SearchQuery ?? "", |
||||
i.PlaybackOrder, |
||||
i.IncludeInProgramGuide, |
||||
i.DisableWatermarks, |
||||
i.WatermarkIds ?? [], |
||||
i.GraphicsElementIds ?? [])).ToList() ?? []; |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new ReplaceBlockItems(request.BlockGroupId, id, request.Name, request.Minutes, request.StopScheduling, items), |
||||
cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record CreateBlockGroupRequest(string Name); |
||||
public record CreateBlockRequest(int BlockGroupId, string Name); |
||||
public record CopyBlockRequest(int NewBlockGroupId, string NewBlockName); |
||||
public record ReplaceBlockItemRequest( |
||||
int Index, |
||||
CollectionType CollectionType, |
||||
int? CollectionId, |
||||
int? MultiCollectionId, |
||||
int? SmartCollectionId, |
||||
int? MediaItemId, |
||||
string? SearchTitle, |
||||
string? SearchQuery, |
||||
PlaybackOrder PlaybackOrder, |
||||
bool IncludeInProgramGuide, |
||||
bool DisableWatermarks, |
||||
List<int>? WatermarkIds, |
||||
List<int>? GraphicsElementIds); |
||||
public record ReplaceBlockItemsRequest( |
||||
int BlockGroupId, |
||||
string Name, |
||||
int Minutes, |
||||
BlockStopScheduling StopScheduling, |
||||
List<ReplaceBlockItemRequest>? Items); |
||||
@ -1,55 +1,231 @@
@@ -1,55 +1,231 @@
|
||||
using System.Threading.Channels; |
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using System.Threading.Channels; |
||||
using ErsatzTV.Application; |
||||
using ErsatzTV.Application.Artworks; |
||||
using ErsatzTV.Application.Channels; |
||||
using ErsatzTV.Application.Playouts; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Api.Channels; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using ErsatzTV.Filters; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace ErsatzTV.Controllers.Api; |
||||
|
||||
[ApiController] |
||||
public class ChannelController(ChannelWriter<IBackgroundServiceRequest> workerChannel, IMediator mediator) |
||||
[EndpointGroupName("general")] |
||||
public class ChannelController(ChannelWriter<IBackgroundServiceRequest> workerChannel, IMediator mediator) : ControllerBase |
||||
{ |
||||
[HttpGet("/api/channels")] |
||||
[EndpointGroupName("general")] |
||||
public async Task<List<ChannelResponseModel>> GetAll() => await mediator.Send(new GetAllChannelsForApi()); |
||||
[HttpGet("/api/channels", Name = "GetAllChannels")] |
||||
[Tags("Channels")] |
||||
[EndpointSummary("Get all channels")] |
||||
public async Task<List<ChannelResponseModel>> GetAll(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllChannelsForApi(), cancellationToken); |
||||
|
||||
[HttpGet("/api/channels/{id:int}", Name = "GetChannelById")] |
||||
[Tags("Channels")] |
||||
[EndpointSummary("Get channel by ID")] |
||||
public async Task<IActionResult> GetChannelById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<ChannelViewModel> result = await mediator.Send(new GetChannelById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/channels/by-number/{channelNumber}", Name = "GetChannelByNumber")] |
||||
[Tags("Channels")] |
||||
[EndpointSummary("Get channel by number")] |
||||
public async Task<IActionResult> GetChannelByNumber(string channelNumber, CancellationToken cancellationToken) |
||||
{ |
||||
Option<ChannelViewModel> result = await mediator.Send(new GetChannelByNumber(channelNumber), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpPost("/api/channels/{channelNumber}/playout/reset")] |
||||
[HttpPost("/api/channels", Name = "CreateChannel")] |
||||
[Tags("Channels")] |
||||
[EndpointSummary("Create a channel")] |
||||
public async Task<IActionResult> CreateChannel( |
||||
[Required] [FromBody] CreateChannelRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var logo = request.LogoPath != null |
||||
? new ArtworkContentTypeModel(request.LogoPath, request.LogoContentType ?? "") |
||||
: ArtworkContentTypeModel.None; |
||||
|
||||
Either<BaseError, CreateChannelResult> result = await mediator.Send( |
||||
new CreateChannel( |
||||
request.Name, |
||||
request.Number, |
||||
request.Group ?? "", |
||||
request.Categories ?? "", |
||||
request.FFmpegProfileId, |
||||
logo, |
||||
request.StreamSelectorMode, |
||||
request.StreamSelector ?? "", |
||||
request.PreferredAudioLanguageCode ?? "", |
||||
request.PreferredAudioTitle ?? "", |
||||
request.PlayoutSource, |
||||
request.PlayoutMode, |
||||
request.MirrorSourceChannelId, |
||||
request.PlayoutOffset, |
||||
request.StreamingMode, |
||||
request.WatermarkId, |
||||
request.FallbackFillerId, |
||||
request.PreferredSubtitleLanguageCode ?? "", |
||||
request.SubtitleMode, |
||||
request.MusicVideoCreditsMode, |
||||
request.MusicVideoCreditsTemplate ?? "", |
||||
request.SongVideoMode, |
||||
request.TranscodeMode, |
||||
request.IdleBehavior, |
||||
request.IsEnabled, |
||||
request.ShowInEpg), |
||||
cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/channels/{id:int}", Name = "UpdateChannel")] |
||||
[Tags("Channels")] |
||||
[EndpointSummary("Update a channel")] |
||||
public async Task<IActionResult> UpdateChannel( |
||||
int id, |
||||
[Required] [FromBody] UpdateChannelRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var logo = request.LogoPath != null |
||||
? new ArtworkContentTypeModel(request.LogoPath, request.LogoContentType ?? "") |
||||
: ArtworkContentTypeModel.None; |
||||
|
||||
Either<BaseError, ChannelViewModel> result = await mediator.Send( |
||||
new UpdateChannel( |
||||
id, |
||||
request.Name, |
||||
request.Number, |
||||
request.Group ?? "", |
||||
request.Categories ?? "", |
||||
request.FFmpegProfileId, |
||||
logo, |
||||
request.StreamSelectorMode, |
||||
request.StreamSelector ?? "", |
||||
request.PreferredAudioLanguageCode ?? "", |
||||
request.PreferredAudioTitle ?? "", |
||||
request.PlayoutSource, |
||||
request.PlayoutMode, |
||||
request.MirrorSourceChannelId, |
||||
request.PlayoutOffset, |
||||
request.StreamingMode, |
||||
request.WatermarkId, |
||||
request.FallbackFillerId, |
||||
request.PreferredSubtitleLanguageCode ?? "", |
||||
request.SubtitleMode, |
||||
request.MusicVideoCreditsMode, |
||||
request.MusicVideoCreditsTemplate ?? "", |
||||
request.SongVideoMode, |
||||
request.TranscodeMode, |
||||
request.IdleBehavior, |
||||
request.IsEnabled, |
||||
request.ShowInEpg), |
||||
cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/channels/{id:int}", Name = "DeleteChannel")] |
||||
[Tags("Channels")] |
||||
[EndpointSummary("Delete a channel")] |
||||
public async Task<IActionResult> DeleteChannel(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new DeleteChannel(id), cancellationToken); |
||||
return result.Match<IActionResult>(_ => NoContent(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPost("/api/channels/{channelNumber}/playout/reset", Name = "ResetChannelPlayout")] |
||||
[Tags("Channels")] |
||||
[EndpointSummary("Reset channel playout")] |
||||
[EndpointGroupName("general")] |
||||
public async Task<IActionResult> ResetPlayout(string channelNumber) |
||||
{ |
||||
Option<int> maybePlayoutId = await mediator.Send(new GetPlayoutIdByChannelNumber(channelNumber)); |
||||
foreach (int playoutId in maybePlayoutId) |
||||
{ |
||||
await workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Reset)); |
||||
return new OkResult(); |
||||
return Ok(); |
||||
} |
||||
|
||||
return new NotFoundResult(); |
||||
return NotFound(); |
||||
} |
||||
|
||||
// for debugging by fast-forwarding a playout
|
||||
// [HttpPost("/api/channels/{channelNumber}/playout/continue")]
|
||||
// public async Task<IActionResult> ContinuePlayout(string channelNumber, [FromQuery] int days = 1)
|
||||
// {
|
||||
// Option<int> maybePlayoutId = await mediator.Send(new GetPlayoutIdByChannelNumber(channelNumber));
|
||||
// foreach (int playoutId in maybePlayoutId)
|
||||
// {
|
||||
// DateTimeOffset start = DateTimeOffset.Now;
|
||||
// for (int i = 0; i < 24 * days; i++)
|
||||
// {
|
||||
// await workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Continue, start));
|
||||
// start += TimeSpan.FromHours(1);
|
||||
// }
|
||||
//
|
||||
// return new OkResult();
|
||||
// }
|
||||
//
|
||||
// return new NotFoundResult();
|
||||
// }
|
||||
[HttpPost("/api/channels/{channelNumber}/playout/build", Name = "BuildChannelPlayout")] |
||||
[Tags("Channels")] |
||||
[EndpointSummary("Build channel playout")] |
||||
public async Task<IActionResult> BuildPlayout(string channelNumber) |
||||
{ |
||||
Option<int> maybePlayoutId = await mediator.Send(new GetPlayoutIdByChannelNumber(channelNumber)); |
||||
foreach (int playoutId in maybePlayoutId) |
||||
{ |
||||
await workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); |
||||
return Accepted(); |
||||
} |
||||
|
||||
return NotFound(); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record CreateChannelRequest( |
||||
string Name, |
||||
string Number, |
||||
string? Group, |
||||
string? Categories, |
||||
int FFmpegProfileId, |
||||
string? LogoPath, |
||||
string? LogoContentType, |
||||
ChannelStreamSelectorMode StreamSelectorMode, |
||||
string? StreamSelector, |
||||
string? PreferredAudioLanguageCode, |
||||
string? PreferredAudioTitle, |
||||
ChannelPlayoutSource PlayoutSource, |
||||
ChannelPlayoutMode PlayoutMode, |
||||
int? MirrorSourceChannelId, |
||||
TimeSpan? PlayoutOffset, |
||||
StreamingMode StreamingMode, |
||||
int? WatermarkId, |
||||
int? FallbackFillerId, |
||||
string? PreferredSubtitleLanguageCode, |
||||
ChannelSubtitleMode SubtitleMode, |
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode, |
||||
string? MusicVideoCreditsTemplate, |
||||
ChannelSongVideoMode SongVideoMode, |
||||
ChannelTranscodeMode TranscodeMode, |
||||
ChannelIdleBehavior IdleBehavior, |
||||
bool IsEnabled, |
||||
bool ShowInEpg); |
||||
|
||||
public record UpdateChannelRequest( |
||||
string Name, |
||||
string Number, |
||||
string? Group, |
||||
string? Categories, |
||||
int FFmpegProfileId, |
||||
string? LogoPath, |
||||
string? LogoContentType, |
||||
ChannelStreamSelectorMode StreamSelectorMode, |
||||
string? StreamSelector, |
||||
string? PreferredAudioLanguageCode, |
||||
string? PreferredAudioTitle, |
||||
ChannelPlayoutSource PlayoutSource, |
||||
ChannelPlayoutMode PlayoutMode, |
||||
int? MirrorSourceChannelId, |
||||
TimeSpan? PlayoutOffset, |
||||
StreamingMode StreamingMode, |
||||
int? WatermarkId, |
||||
int? FallbackFillerId, |
||||
string? PreferredSubtitleLanguageCode, |
||||
ChannelSubtitleMode SubtitleMode, |
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode, |
||||
string? MusicVideoCreditsTemplate, |
||||
ChannelSongVideoMode SongVideoMode, |
||||
ChannelTranscodeMode TranscodeMode, |
||||
ChannelIdleBehavior IdleBehavior, |
||||
bool IsEnabled, |
||||
bool ShowInEpg); |
||||
|
||||
@ -0,0 +1,195 @@
@@ -0,0 +1,195 @@
|
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.MediaCards; |
||||
using ErsatzTV.Application.MediaCollections; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace ErsatzTV.Controllers.Api; |
||||
|
||||
[ApiController] |
||||
[EndpointGroupName("general")] |
||||
public class CollectionController(IMediator mediator) : ControllerBase |
||||
{ |
||||
// Collections
|
||||
[HttpGet("/api/collections", Name = "GetCollections")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Get all collections (paginated)")] |
||||
public async Task<PagedMediaCollectionsViewModel> GetCollections( |
||||
[FromQuery] string query = "", |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new GetPagedCollections(query, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/collections/all", Name = "GetAllCollections")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Get all collections")] |
||||
public async Task<List<MediaCollectionViewModel>> GetAllCollections(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllCollections(), cancellationToken); |
||||
|
||||
[HttpGet("/api/collections/{id:int}", Name = "GetCollectionById")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Get collection by ID")] |
||||
public async Task<IActionResult> GetCollectionById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<MediaCollectionViewModel> result = await mediator.Send(new GetCollectionById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/collections/{id:int}/items", Name = "GetCollectionItems")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Get collection items")] |
||||
public async Task<IActionResult> GetCollectionItems(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, CollectionCardResultsViewModel> result = await mediator.Send(new GetCollectionCards(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => NotFound()); |
||||
} |
||||
|
||||
[HttpPost("/api/collections", Name = "CreateCollection")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Create a new collection")] |
||||
public async Task<IActionResult> CreateCollection( |
||||
[Required] [FromBody] CreateCollectionRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, MediaCollectionViewModel> result = await mediator.Send(new CreateCollection(request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/collections/{id:int}", Name = "UpdateCollection")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Update a collection")] |
||||
public async Task<IActionResult> UpdateCollection( |
||||
int id, |
||||
[Required] [FromBody] UpdateCollectionRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new UpdateCollection(id, request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/collections/{id:int}", Name = "DeleteCollection")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Delete a collection")] |
||||
public async Task<IActionResult> DeleteCollection(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new DeleteCollection(id), cancellationToken); |
||||
return result.Match<IActionResult>(_ => NoContent(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPost("/api/collections/{id:int}/items", Name = "AddItemsToCollection")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Add items to a collection")] |
||||
public async Task<IActionResult> AddItemsToCollection( |
||||
int id, |
||||
[Required] [FromBody] AddItemsToCollectionRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new AddItemsToCollection( |
||||
id, |
||||
request.MovieIds ?? [], |
||||
request.ShowIds ?? [], |
||||
request.SeasonIds ?? [], |
||||
request.EpisodeIds ?? [], |
||||
request.ArtistIds ?? [], |
||||
request.MusicVideoIds ?? [], |
||||
request.OtherVideoIds ?? [], |
||||
request.SongIds ?? [], |
||||
request.ImageIds ?? [], |
||||
request.RemoteStreamIds ?? []), |
||||
cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/collections/{id:int}/items", Name = "RemoveItemsFromCollection")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Remove items from a collection")] |
||||
public async Task<IActionResult> RemoveItemsFromCollection( |
||||
int id, |
||||
[Required] [FromBody] RemoveItemsFromCollectionRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var command = new RemoveItemsFromCollection(id) { MediaItemIds = request.MediaItemIds ?? [] }; |
||||
Either<BaseError, Unit> result = await mediator.Send(command, cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
// Multi-collections
|
||||
[HttpGet("/api/collections/multi", Name = "GetMultiCollections")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Get all multi-collections")] |
||||
public async Task<List<MultiCollectionViewModel>> GetMultiCollections(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllMultiCollections(), cancellationToken); |
||||
|
||||
[HttpGet("/api/collections/multi/{id:int}", Name = "GetMultiCollectionById")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Get multi-collection by ID")] |
||||
public async Task<IActionResult> GetMultiCollectionById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<MultiCollectionViewModel> result = await mediator.Send(new GetMultiCollectionById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpPost("/api/collections/multi", Name = "CreateMultiCollection")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Create a multi-collection")] |
||||
public async Task<IActionResult> CreateMultiCollection( |
||||
[Required] [FromBody] CreateMultiCollectionRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var items = request.Items?.Select(i => new CreateMultiCollectionItem( |
||||
i.CollectionId, i.SmartCollectionId, i.ScheduleAsGroup, i.PlaybackOrder)).ToList() ?? []; |
||||
Either<BaseError, MultiCollectionViewModel> result = await mediator.Send( |
||||
new CreateMultiCollection(request.Name, items), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/collections/multi/{id:int}", Name = "UpdateMultiCollection")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Update a multi-collection")] |
||||
public async Task<IActionResult> UpdateMultiCollection( |
||||
int id, |
||||
[Required] [FromBody] UpdateMultiCollectionRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var items = request.Items?.Select(i => new UpdateMultiCollectionItem( |
||||
i.CollectionId, i.SmartCollectionId, i.ScheduleAsGroup, i.PlaybackOrder)).ToList() ?? []; |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new UpdateMultiCollection(id, request.Name, items), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/collections/multi/{id:int}", Name = "DeleteMultiCollection")] |
||||
[Tags("Collections")] |
||||
[EndpointSummary("Delete a multi-collection")] |
||||
public async Task<IActionResult> DeleteMultiCollection(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new DeleteMultiCollection(id), cancellationToken); |
||||
return result.Match<IActionResult>(_ => NoContent(), error => Problem(error.ToString())); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record CreateCollectionRequest(string Name); |
||||
public record UpdateCollectionRequest(string Name); |
||||
public record AddItemsToCollectionRequest( |
||||
List<int>? MovieIds, |
||||
List<int>? ShowIds, |
||||
List<int>? SeasonIds, |
||||
List<int>? EpisodeIds, |
||||
List<int>? ArtistIds, |
||||
List<int>? MusicVideoIds, |
||||
List<int>? OtherVideoIds, |
||||
List<int>? SongIds, |
||||
List<int>? ImageIds, |
||||
List<int>? RemoteStreamIds); |
||||
public record RemoveItemsFromCollectionRequest(List<int>? MediaItemIds); |
||||
public record CreateMultiCollectionItemRequest(int? CollectionId, int? SmartCollectionId, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder); |
||||
public record CreateMultiCollectionRequest(string Name, List<CreateMultiCollectionItemRequest>? Items); |
||||
public record UpdateMultiCollectionItemRequest(int? CollectionId, int? SmartCollectionId, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder); |
||||
public record UpdateMultiCollectionRequest(string Name, List<UpdateMultiCollectionItemRequest>? Items); |
||||
@ -0,0 +1,84 @@
@@ -0,0 +1,84 @@
|
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.Configuration; |
||||
using ErsatzTV.Core; |
||||
using MediatR; |
||||
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")] |
||||
public async Task<PlayoutSettingsViewModel> GetPlayoutSettings(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetPlayoutSettings(), cancellationToken); |
||||
|
||||
[HttpPut("/api/config/playout", Name = "UpdatePlayoutSettings")] |
||||
[Tags("Configuration")] |
||||
[EndpointSummary("Update playout settings")] |
||||
public async Task<IActionResult> UpdatePlayoutSettings( |
||||
[Required] [FromBody] PlayoutSettingsViewModel settings, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new UpdatePlayoutSettings(settings), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpGet("/api/config/xmltv", Name = "GetXmltvSettings")] |
||||
[Tags("Configuration")] |
||||
[EndpointSummary("Get XMLTV settings")] |
||||
public async Task<XmltvSettingsViewModel> GetXmltvSettings(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetXmltvSettings(), cancellationToken); |
||||
|
||||
[HttpPut("/api/config/xmltv", Name = "UpdateXmltvSettings")] |
||||
[Tags("Configuration")] |
||||
[EndpointSummary("Update XMLTV settings")] |
||||
public async Task<IActionResult> UpdateXmltvSettings( |
||||
[Required] [FromBody] XmltvSettingsViewModel settings, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new UpdateXmltvSettings(settings), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpGet("/api/config/logging", Name = "GetLoggingSettings")] |
||||
[Tags("Configuration")] |
||||
[EndpointSummary("Get logging settings")] |
||||
public async Task<LoggingSettingsViewModel> GetLoggingSettings(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetLoggingSettings(), cancellationToken); |
||||
|
||||
[HttpPut("/api/config/logging", Name = "UpdateLoggingSettings")] |
||||
[Tags("Configuration")] |
||||
[EndpointSummary("Update logging settings")] |
||||
public async Task<IActionResult> UpdateLoggingSettings( |
||||
[Required] [FromBody] LoggingSettingsViewModel settings, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new UpdateLoggingSettings(settings), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpGet("/api/config/library-refresh-interval", Name = "GetLibraryRefreshInterval")] |
||||
[Tags("Configuration")] |
||||
[EndpointSummary("Get library refresh interval")] |
||||
public async Task<int> GetLibraryRefreshInterval(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetLibraryRefreshInterval(), cancellationToken); |
||||
|
||||
[HttpPut("/api/config/library-refresh-interval", Name = "UpdateLibraryRefreshInterval")] |
||||
[Tags("Configuration")] |
||||
[EndpointSummary("Update library refresh interval")] |
||||
public async Task<IActionResult> UpdateLibraryRefreshInterval( |
||||
[Required] [FromBody] UpdateLibraryRefreshIntervalRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new UpdateLibraryRefreshInterval(request.Interval), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record UpdateLibraryRefreshIntervalRequest(int Interval); |
||||
@ -0,0 +1,266 @@
@@ -0,0 +1,266 @@
|
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.Scheduling; |
||||
using ErsatzTV.Application.Tree; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace ErsatzTV.Controllers.Api; |
||||
|
||||
[ApiController] |
||||
[EndpointGroupName("general")] |
||||
public class DecoController(IMediator mediator) : ControllerBase |
||||
{ |
||||
// Deco Groups
|
||||
[HttpGet("/api/decos/groups", Name = "GetDecoGroups")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Get all deco groups")] |
||||
public async Task<List<DecoGroupViewModel>> GetDecoGroups(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllDecoGroups(), cancellationToken); |
||||
|
||||
[HttpPost("/api/decos/groups", Name = "CreateDecoGroup")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Create a deco group")] |
||||
public async Task<IActionResult> CreateDecoGroup( |
||||
[Required] [FromBody] CreateDecoGroupRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, DecoGroupViewModel> result = await mediator.Send( |
||||
new CreateDecoGroup(request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/decos/groups/{id:int}", Name = "DeleteDecoGroup")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Delete a deco group")] |
||||
public async Task<IActionResult> DeleteDecoGroup(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send(new DeleteDecoGroup(id), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => NoContent()); |
||||
} |
||||
|
||||
// Decos
|
||||
[HttpGet("/api/decos/groups/{groupId:int}/decos", Name = "GetDecosByGroup")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Get decos by group")] |
||||
public async Task<List<DecoViewModel>> GetDecosByGroup(int groupId, CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetDecosByDecoGroupId(groupId), cancellationToken); |
||||
|
||||
[HttpGet("/api/decos/tree", Name = "GetDecoTree")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Get deco tree")] |
||||
public async Task<TreeViewModel> GetDecoTree(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetDecoTree(), cancellationToken); |
||||
|
||||
[HttpGet("/api/decos/{id:int}", Name = "GetDecoById")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Get deco by ID")] |
||||
public async Task<IActionResult> GetDecoById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<DecoViewModel> result = await mediator.Send(new GetDecoById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpPost("/api/decos", Name = "CreateDeco")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Create a deco")] |
||||
public async Task<IActionResult> CreateDeco( |
||||
[Required] [FromBody] CreateDecoRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, DecoViewModel> result = await mediator.Send( |
||||
new CreateDeco(request.DecoGroupId, request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/decos/{id:int}", Name = "UpdateDeco")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Update a deco")] |
||||
public async Task<IActionResult> UpdateDeco( |
||||
int id, |
||||
[Required] [FromBody] UpdateDecoRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var breakContent = request.BreakContent?.Select(b => new UpdateDecoBreakContent( |
||||
b.Id, |
||||
b.CollectionType, |
||||
b.CollectionId, |
||||
b.MediaItemId, |
||||
b.MultiCollectionId, |
||||
b.SmartCollectionId, |
||||
b.PlaylistId, |
||||
b.Placement)).ToList() ?? []; |
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new UpdateDeco( |
||||
id, |
||||
request.DecoGroupId, |
||||
request.Name, |
||||
request.WatermarkMode, |
||||
request.WatermarkIds ?? [], |
||||
request.UseWatermarkDuringFiller, |
||||
request.GraphicsElementsMode, |
||||
request.GraphicsElementIds ?? [], |
||||
request.UseGraphicsElementsDuringFiller, |
||||
request.BreakContentMode, |
||||
breakContent, |
||||
request.DefaultFillerMode, |
||||
request.DefaultFillerCollectionType, |
||||
request.DefaultFillerCollectionId, |
||||
request.DefaultFillerMediaItemId, |
||||
request.DefaultFillerMultiCollectionId, |
||||
request.DefaultFillerSmartCollectionId, |
||||
request.DefaultFillerTrimToFit, |
||||
request.DeadAirFallbackMode, |
||||
request.DeadAirFallbackCollectionType, |
||||
request.DeadAirFallbackCollectionId, |
||||
request.DeadAirFallbackMediaItemId, |
||||
request.DeadAirFallbackMultiCollectionId, |
||||
request.DeadAirFallbackSmartCollectionId), |
||||
cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/decos/{id:int}", Name = "DeleteDeco")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Delete a deco")] |
||||
public async Task<IActionResult> DeleteDeco(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send(new DeleteDeco(id), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => NoContent()); |
||||
} |
||||
|
||||
[HttpPut("/api/playouts/{playoutId:int}/deco", Name = "UpdateDefaultDeco")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Update default deco for playout")] |
||||
public async Task<IActionResult> UpdateDefaultDeco( |
||||
int playoutId, |
||||
[Required] [FromBody] UpdateDefaultDecoRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send( |
||||
new UpdateDefaultDeco(playoutId, request.DecoId), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => Ok()); |
||||
} |
||||
|
||||
// Deco Template Groups
|
||||
[HttpGet("/api/decos/template-groups", Name = "GetDecoTemplateGroups")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Get all deco template groups")] |
||||
public async Task<List<DecoTemplateGroupViewModel>> GetDecoTemplateGroups(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllDecoTemplateGroups(), cancellationToken); |
||||
|
||||
[HttpPost("/api/decos/template-groups", Name = "CreateDecoTemplateGroup")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Create a deco template group")] |
||||
public async Task<IActionResult> CreateDecoTemplateGroup( |
||||
[Required] [FromBody] CreateDecoTemplateGroupRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, DecoTemplateGroupViewModel> result = await mediator.Send( |
||||
new CreateDecoTemplateGroup(request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/decos/template-groups/{id:int}", Name = "DeleteDecoTemplateGroup")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Delete a deco template group")] |
||||
public async Task<IActionResult> DeleteDecoTemplateGroup(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send(new DeleteDecoTemplateGroup(id), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => NoContent()); |
||||
} |
||||
|
||||
// Deco Templates
|
||||
[HttpGet("/api/decos/template-groups/{groupId:int}/templates", Name = "GetDecoTemplatesByGroup")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Get deco templates by group")] |
||||
public async Task<List<DecoTemplateViewModel>> 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")] |
||||
public async Task<TreeViewModel> 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")] |
||||
public async Task<IActionResult> GetDecoTemplateById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<DecoTemplateViewModel> result = await mediator.Send(new GetDecoTemplateById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/decos/templates/{id:int}/items", Name = "GetDecoTemplateItems")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Get deco template items")] |
||||
public async Task<List<DecoTemplateItemViewModel>> 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")] |
||||
public async Task<IActionResult> CreateDecoTemplate( |
||||
[Required] [FromBody] CreateDecoTemplateRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, DecoTemplateViewModel> result = await mediator.Send( |
||||
new CreateDecoTemplate(request.DecoTemplateGroupId, request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/decos/templates/{id:int}", Name = "DeleteDecoTemplate")] |
||||
[Tags("Decos")] |
||||
[EndpointSummary("Delete a deco template")] |
||||
public async Task<IActionResult> DeleteDecoTemplate(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send(new DeleteDecoTemplate(id), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => NoContent()); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record CreateDecoGroupRequest(string Name); |
||||
public record CreateDecoRequest(int DecoGroupId, string Name); |
||||
public record UpdateDecoBreakContentRequest( |
||||
int Id, |
||||
CollectionType CollectionType, |
||||
int? CollectionId, |
||||
int? MediaItemId, |
||||
int? MultiCollectionId, |
||||
int? SmartCollectionId, |
||||
int? PlaylistId, |
||||
DecoBreakPlacement Placement); |
||||
public record UpdateDecoRequest( |
||||
int DecoGroupId, |
||||
string Name, |
||||
DecoMode WatermarkMode, |
||||
List<int>? WatermarkIds, |
||||
bool UseWatermarkDuringFiller, |
||||
DecoMode GraphicsElementsMode, |
||||
List<int>? GraphicsElementIds, |
||||
bool UseGraphicsElementsDuringFiller, |
||||
DecoMode BreakContentMode, |
||||
List<UpdateDecoBreakContentRequest>? BreakContent, |
||||
DecoMode DefaultFillerMode, |
||||
CollectionType DefaultFillerCollectionType, |
||||
int? DefaultFillerCollectionId, |
||||
int? DefaultFillerMediaItemId, |
||||
int? DefaultFillerMultiCollectionId, |
||||
int? DefaultFillerSmartCollectionId, |
||||
bool DefaultFillerTrimToFit, |
||||
DecoMode DeadAirFallbackMode, |
||||
CollectionType DeadAirFallbackCollectionType, |
||||
int? DeadAirFallbackCollectionId, |
||||
int? DeadAirFallbackMediaItemId, |
||||
int? DeadAirFallbackMultiCollectionId, |
||||
int? DeadAirFallbackSmartCollectionId); |
||||
public record UpdateDefaultDecoRequest(int? DecoId); |
||||
public record CreateDecoTemplateGroupRequest(string Name); |
||||
public record CreateDecoTemplateRequest(int DecoTemplateGroupId, string Name); |
||||
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.Emby; |
||||
using ErsatzTV.Core.Emby; |
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
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")] |
||||
public async Task<List<EmbyMediaSourceViewModel>> 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")] |
||||
public async Task<IActionResult> GetEmbyMediaSourceById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<EmbyMediaSourceViewModel> result = await mediator.Send(new GetEmbyMediaSourceById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/emby/sources/{id:int}/libraries", Name = "GetEmbyLibraries")] |
||||
[Tags("Emby")] |
||||
[EndpointSummary("Get Emby libraries by source")] |
||||
public async Task<List<EmbyLibraryViewModel>> 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")] |
||||
public async Task<List<EmbyPathReplacementViewModel>> 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")] |
||||
public async Task<IActionResult> UpdateEmbyPathReplacements( |
||||
int id, |
||||
[Required] [FromBody] UpdateEmbyPathReplacementsRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var replacements = request.PathReplacements?.Select(p => |
||||
new EmbyPathReplacementItem(p.Id, p.EmbyPath, p.LocalPath)).ToList() ?? []; |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new UpdateEmbyPathReplacements(id, replacements), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/emby/library-preferences", Name = "UpdateEmbyLibraryPreferences")] |
||||
[Tags("Emby")] |
||||
[EndpointSummary("Update Emby library preferences")] |
||||
public async Task<IActionResult> UpdateEmbyLibraryPreferences( |
||||
[Required] [FromBody] UpdateEmbyLibraryPreferencesRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var preferences = request.Preferences?.Select(p => |
||||
new EmbyLibraryPreference(p.Id, p.ShouldSyncItems)).ToList() ?? []; |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new UpdateEmbyLibraryPreferences(preferences), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpGet("/api/emby/secrets", Name = "GetEmbySecrets")] |
||||
[Tags("Emby")] |
||||
[EndpointSummary("Get Emby secrets")] |
||||
public async Task<EmbySecrets> GetEmbySecrets(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetEmbySecrets(), cancellationToken); |
||||
} |
||||
|
||||
// Request models
|
||||
public record EmbyPathReplacementRequest(int Id, string EmbyPath, string LocalPath); |
||||
public record UpdateEmbyPathReplacementsRequest(List<EmbyPathReplacementRequest>? PathReplacements); |
||||
public record EmbyLibraryPreferenceRequest(int Id, bool ShouldSyncItems); |
||||
public record UpdateEmbyLibraryPreferencesRequest(List<EmbyLibraryPreferenceRequest>? Preferences); |
||||
@ -0,0 +1,142 @@
@@ -0,0 +1,142 @@
|
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.Filler; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
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)")] |
||||
public async Task<PagedFillerPresetsViewModel> GetFillerPresets( |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new GetPagedFillerPresets(pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/filler/presets/all", Name = "GetAllFillerPresets")] |
||||
[Tags("Filler")] |
||||
[EndpointSummary("Get all filler presets")] |
||||
public async Task<List<FillerPresetViewModel>> 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")] |
||||
public async Task<IActionResult> GetFillerPresetById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<FillerPresetViewModel> result = await mediator.Send(new GetFillerPresetById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpPost("/api/filler/presets", Name = "CreateFillerPreset")] |
||||
[Tags("Filler")] |
||||
[EndpointSummary("Create a filler preset")] |
||||
public async Task<IActionResult> CreateFillerPreset( |
||||
[Required] [FromBody] CreateFillerPresetRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new CreateFillerPreset( |
||||
request.Name, |
||||
request.FillerKind, |
||||
request.FillerMode, |
||||
request.Duration, |
||||
request.Count, |
||||
request.PadToNearestMinute, |
||||
request.AllowWatermarks, |
||||
request.CollectionType, |
||||
request.CollectionId, |
||||
request.MediaItemId, |
||||
request.MultiCollectionId, |
||||
request.SmartCollectionId, |
||||
request.PlaylistId, |
||||
request.Expression ?? "", |
||||
request.UseChaptersAsMediaItems), |
||||
cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/filler/presets/{id:int}", Name = "UpdateFillerPreset")] |
||||
[Tags("Filler")] |
||||
[EndpointSummary("Update a filler preset")] |
||||
public async Task<IActionResult> UpdateFillerPreset( |
||||
int id, |
||||
[Required] [FromBody] UpdateFillerPresetRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new UpdateFillerPreset( |
||||
id, |
||||
request.Name, |
||||
request.FillerKind, |
||||
request.FillerMode, |
||||
request.Duration, |
||||
request.Count, |
||||
request.PadToNearestMinute, |
||||
request.AllowWatermarks, |
||||
request.CollectionType, |
||||
request.CollectionId, |
||||
request.MediaItemId, |
||||
request.MultiCollectionId, |
||||
request.SmartCollectionId, |
||||
request.PlaylistId, |
||||
request.Expression ?? "", |
||||
request.UseChaptersAsMediaItems), |
||||
cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/filler/presets/{id:int}", Name = "DeleteFillerPreset")] |
||||
[Tags("Filler")] |
||||
[EndpointSummary("Delete a filler preset")] |
||||
public async Task<IActionResult> DeleteFillerPreset(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new DeleteFillerPreset(id), cancellationToken); |
||||
return result.Match<IActionResult>(_ => NoContent(), error => Problem(error.ToString())); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record CreateFillerPresetRequest( |
||||
string Name, |
||||
FillerKind FillerKind, |
||||
FillerMode FillerMode, |
||||
TimeSpan? Duration, |
||||
int? Count, |
||||
int? PadToNearestMinute, |
||||
bool AllowWatermarks, |
||||
CollectionType CollectionType, |
||||
int? CollectionId, |
||||
int? MediaItemId, |
||||
int? MultiCollectionId, |
||||
int? SmartCollectionId, |
||||
int? PlaylistId, |
||||
string? Expression, |
||||
bool UseChaptersAsMediaItems); |
||||
|
||||
public record UpdateFillerPresetRequest( |
||||
string Name, |
||||
FillerKind FillerKind, |
||||
FillerMode FillerMode, |
||||
TimeSpan? Duration, |
||||
int? Count, |
||||
int? PadToNearestMinute, |
||||
bool AllowWatermarks, |
||||
CollectionType CollectionType, |
||||
int? CollectionId, |
||||
int? MediaItemId, |
||||
int? MultiCollectionId, |
||||
int? SmartCollectionId, |
||||
int? PlaylistId, |
||||
string? Expression, |
||||
bool UseChaptersAsMediaItems); |
||||
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.Jellyfin; |
||||
using ErsatzTV.Core.Jellyfin; |
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
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")] |
||||
public async Task<List<JellyfinMediaSourceViewModel>> 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")] |
||||
public async Task<IActionResult> GetJellyfinMediaSourceById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<JellyfinMediaSourceViewModel> result = await mediator.Send(new GetJellyfinMediaSourceById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/jellyfin/sources/{id:int}/libraries", Name = "GetJellyfinLibraries")] |
||||
[Tags("Jellyfin")] |
||||
[EndpointSummary("Get Jellyfin libraries by source")] |
||||
public async Task<List<JellyfinLibraryViewModel>> 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")] |
||||
public async Task<List<JellyfinPathReplacementViewModel>> 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")] |
||||
public async Task<IActionResult> UpdateJellyfinPathReplacements( |
||||
int id, |
||||
[Required] [FromBody] UpdateJellyfinPathReplacementsRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var replacements = request.PathReplacements?.Select(p => |
||||
new JellyfinPathReplacementItem(p.Id, p.JellyfinPath, p.LocalPath)).ToList() ?? []; |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new UpdateJellyfinPathReplacements(id, replacements), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/jellyfin/library-preferences", Name = "UpdateJellyfinLibraryPreferences")] |
||||
[Tags("Jellyfin")] |
||||
[EndpointSummary("Update Jellyfin library preferences")] |
||||
public async Task<IActionResult> UpdateJellyfinLibraryPreferences( |
||||
[Required] [FromBody] UpdateJellyfinLibraryPreferencesRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var preferences = request.Preferences?.Select(p => |
||||
new JellyfinLibraryPreference(p.Id, p.ShouldSyncItems)).ToList() ?? []; |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new UpdateJellyfinLibraryPreferences(preferences), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpGet("/api/jellyfin/secrets", Name = "GetJellyfinSecrets")] |
||||
[Tags("Jellyfin")] |
||||
[EndpointSummary("Get Jellyfin secrets")] |
||||
public async Task<JellyfinSecrets> GetJellyfinSecrets(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetJellyfinSecrets(), cancellationToken); |
||||
} |
||||
|
||||
// Request models
|
||||
public record JellyfinPathReplacementRequest(int Id, string JellyfinPath, string LocalPath); |
||||
public record UpdateJellyfinPathReplacementsRequest(List<JellyfinPathReplacementRequest>? PathReplacements); |
||||
public record JellyfinLibraryPreferenceRequest(int Id, bool ShouldSyncItems); |
||||
public record UpdateJellyfinLibraryPreferencesRequest(List<JellyfinLibraryPreferenceRequest>? Preferences); |
||||
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
using ErsatzTV.Application.Artists; |
||||
using ErsatzTV.Application.MediaCards; |
||||
using ErsatzTV.Application.Movies; |
||||
using ErsatzTV.Application.Television; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
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")] |
||||
public async Task<IActionResult> GetMovieById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<MovieViewModel> result = await mediator.Send(new GetMovieById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
// TV Shows
|
||||
[HttpGet("/api/media/shows/{id:int}", Name = "GetShowById")] |
||||
[Tags("Media")] |
||||
[EndpointSummary("Get TV show by ID")] |
||||
public async Task<IActionResult> GetShowById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<TelevisionShowViewModel> result = await mediator.Send(new GetTelevisionShowById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/media/shows/{id:int}/seasons", Name = "GetShowSeasons")] |
||||
[Tags("Media")] |
||||
[EndpointSummary("Get seasons for a TV show")] |
||||
public async Task<TelevisionSeasonCardResultsViewModel> GetShowSeasons( |
||||
int id, |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new GetTelevisionSeasonCards(id, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/media/seasons/{id:int}", Name = "GetSeasonById")] |
||||
[Tags("Media")] |
||||
[EndpointSummary("Get season by ID")] |
||||
public async Task<IActionResult> GetSeasonById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<TelevisionSeasonViewModel> result = await mediator.Send(new GetTelevisionSeasonById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/media/seasons/{id:int}/episodes", Name = "GetSeasonEpisodes")] |
||||
[Tags("Media")] |
||||
[EndpointSummary("Get episodes for a season")] |
||||
public async Task<TelevisionEpisodeCardResultsViewModel> GetSeasonEpisodes( |
||||
int id, |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new GetTelevisionEpisodeCards(id, pageNumber, pageSize), cancellationToken); |
||||
|
||||
// Artists
|
||||
[HttpGet("/api/media/artists/{id:int}", Name = "GetArtistById")] |
||||
[Tags("Media")] |
||||
[EndpointSummary("Get artist by ID")] |
||||
public async Task<IActionResult> GetArtistById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<ArtistViewModel> result = await mediator.Send(new GetArtistById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
} |
||||
@ -0,0 +1,133 @@
@@ -0,0 +1,133 @@
|
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.MediaCollections; |
||||
using ErsatzTV.Application.Tree; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace ErsatzTV.Controllers.Api; |
||||
|
||||
[ApiController] |
||||
[EndpointGroupName("general")] |
||||
public class PlaylistController(IMediator mediator) : ControllerBase |
||||
{ |
||||
// Playlist Groups
|
||||
[HttpGet("/api/playlists/groups", Name = "GetPlaylistGroups")] |
||||
[Tags("Playlists")] |
||||
[EndpointSummary("Get all playlist groups")] |
||||
public async Task<List<PlaylistGroupViewModel>> GetPlaylistGroups(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllPlaylistGroups(), cancellationToken); |
||||
|
||||
[HttpPost("/api/playlists/groups", Name = "CreatePlaylistGroup")] |
||||
[Tags("Playlists")] |
||||
[EndpointSummary("Create a playlist group")] |
||||
public async Task<IActionResult> CreatePlaylistGroup( |
||||
[Required] [FromBody] CreatePlaylistGroupRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, PlaylistGroupViewModel> result = await mediator.Send( |
||||
new CreatePlaylistGroup(request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/playlists/groups/{id:int}", Name = "DeletePlaylistGroup")] |
||||
[Tags("Playlists")] |
||||
[EndpointSummary("Delete a playlist group")] |
||||
public async Task<IActionResult> DeletePlaylistGroup(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send(new DeletePlaylistGroup(id), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => NoContent()); |
||||
} |
||||
|
||||
// Playlists
|
||||
[HttpGet("/api/playlists/groups/{groupId:int}/playlists", Name = "GetPlaylistsByGroup")] |
||||
[Tags("Playlists")] |
||||
[EndpointSummary("Get playlists by group")] |
||||
public async Task<List<PlaylistViewModel>> GetPlaylistsByGroup(int groupId, CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetPlaylistsByPlaylistGroupId(groupId), cancellationToken); |
||||
|
||||
[HttpGet("/api/playlists/tree", Name = "GetPlaylistTree")] |
||||
[Tags("Playlists")] |
||||
[EndpointSummary("Get playlist tree")] |
||||
public async Task<TreeViewModel> GetPlaylistTree(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetPlaylistTree(), cancellationToken); |
||||
|
||||
[HttpGet("/api/playlists/{id:int}", Name = "GetPlaylistById")] |
||||
[Tags("Playlists")] |
||||
[EndpointSummary("Get playlist by ID")] |
||||
public async Task<IActionResult> GetPlaylistById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<PlaylistViewModel> result = await mediator.Send(new GetPlaylistById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/playlists/{id:int}/items", Name = "GetPlaylistItems")] |
||||
[Tags("Playlists")] |
||||
[EndpointSummary("Get playlist items")] |
||||
public async Task<List<PlaylistItemViewModel>> GetPlaylistItems(int id, CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetPlaylistItems(id), cancellationToken); |
||||
|
||||
[HttpPost("/api/playlists", Name = "CreatePlaylist")] |
||||
[Tags("Playlists")] |
||||
[EndpointSummary("Create a playlist")] |
||||
public async Task<IActionResult> CreatePlaylist( |
||||
[Required] [FromBody] CreatePlaylistRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, PlaylistViewModel> result = await mediator.Send( |
||||
new CreatePlaylist(request.PlaylistGroupId, request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/playlists/{id:int}", Name = "DeletePlaylist")] |
||||
[Tags("Playlists")] |
||||
[EndpointSummary("Delete a playlist")] |
||||
public async Task<IActionResult> DeletePlaylist(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send(new DeletePlaylist(id), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => NoContent()); |
||||
} |
||||
|
||||
[HttpPut("/api/playlists/{id:int}/items", Name = "ReplacePlaylistItems")] |
||||
[Tags("Playlists")] |
||||
[EndpointSummary("Replace playlist items")] |
||||
public async Task<IActionResult> ReplacePlaylistItems( |
||||
int id, |
||||
[Required] [FromBody] ReplacePlaylistItemsRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var items = request.Items?.Select(i => new ReplacePlaylistItem( |
||||
i.Index, |
||||
i.CollectionType, |
||||
i.CollectionId, |
||||
i.MultiCollectionId, |
||||
i.SmartCollectionId, |
||||
i.MediaItemId, |
||||
i.PlaybackOrder, |
||||
i.Count, |
||||
i.PlayAll, |
||||
i.IncludeInProgramGuide)).ToList() ?? []; |
||||
Either<BaseError, List<PlaylistItemViewModel>> result = await mediator.Send( |
||||
new ReplacePlaylistItems(id, request.Name, items), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record CreatePlaylistGroupRequest(string Name); |
||||
public record CreatePlaylistRequest(int PlaylistGroupId, string Name); |
||||
public record ReplacePlaylistItemRequest( |
||||
int Index, |
||||
CollectionType CollectionType, |
||||
int? CollectionId, |
||||
int? MultiCollectionId, |
||||
int? SmartCollectionId, |
||||
int? MediaItemId, |
||||
PlaybackOrder PlaybackOrder, |
||||
int? Count, |
||||
bool PlayAll, |
||||
bool IncludeInProgramGuide); |
||||
public record ReplacePlaylistItemsRequest(string Name, List<ReplacePlaylistItemRequest>? Items); |
||||
@ -0,0 +1,162 @@
@@ -0,0 +1,162 @@
|
||||
using System.ComponentModel.DataAnnotations; |
||||
using System.Threading.Channels; |
||||
using ErsatzTV.Application; |
||||
using ErsatzTV.Application.Playouts; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace ErsatzTV.Controllers.Api; |
||||
|
||||
[ApiController] |
||||
[EndpointGroupName("general")] |
||||
public class PlayoutController(ChannelWriter<IBackgroundServiceRequest> workerChannel, IMediator mediator) : ControllerBase |
||||
{ |
||||
[HttpGet("/api/playouts", Name = "GetAllPlayouts")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Get all playouts (paginated)")] |
||||
public async Task<PagedPlayoutsViewModel> GetAllPlayouts( |
||||
[FromQuery] string query = "", |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new GetPagedPlayouts(query, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/playouts/block", Name = "GetBlockPlayouts")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Get all block playouts")] |
||||
public async Task<List<PlayoutNameViewModel>> GetBlockPlayouts(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllBlockPlayouts(), cancellationToken); |
||||
|
||||
[HttpGet("/api/playouts/{id:int}", Name = "GetPlayoutById")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Get playout by ID")] |
||||
public async Task<IActionResult> GetPlayoutById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<PlayoutNameViewModel> result = await mediator.Send(new GetPlayoutById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/playouts/{id:int}/items", Name = "GetPlayoutItems")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Get playout items")] |
||||
public async Task<PagedPlayoutItemsViewModel> GetPlayoutItems( |
||||
int id, |
||||
[FromQuery] bool showFiller = false, |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new GetFuturePlayoutItemsById(id, showFiller, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpPost("/api/playouts/classic", Name = "CreateClassicPlayout")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Create a classic playout")] |
||||
public async Task<IActionResult> CreateClassicPlayout( |
||||
[Required] [FromBody] CreateClassicPlayoutRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, CreatePlayoutResponse> result = await mediator.Send( |
||||
new CreateClassicPlayout(request.ChannelId, request.ProgramScheduleId), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPost("/api/playouts/block", Name = "CreateBlockPlayout")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Create a block playout")] |
||||
public async Task<IActionResult> CreateBlockPlayout( |
||||
[Required] [FromBody] CreateBlockPlayoutRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, CreatePlayoutResponse> result = await mediator.Send( |
||||
new CreateBlockPlayout(request.ChannelId), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPost("/api/playouts/sequential", Name = "CreateSequentialPlayout")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Create a sequential playout")] |
||||
public async Task<IActionResult> CreateSequentialPlayout( |
||||
[Required] [FromBody] CreateSequentialPlayoutRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, CreatePlayoutResponse> result = await mediator.Send( |
||||
new CreateSequentialPlayout(request.ChannelId, request.ScheduleFile), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPost("/api/playouts/scripted", Name = "CreateScriptedPlayout")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Create a scripted playout")] |
||||
public async Task<IActionResult> CreateScriptedPlayout( |
||||
[Required] [FromBody] CreateScriptedPlayoutRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, CreatePlayoutResponse> result = await mediator.Send( |
||||
new CreateScriptedPlayout(request.ChannelId, request.ScheduleFile), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPost("/api/playouts/external-json", Name = "CreateExternalJsonPlayout")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Create an external JSON playout")] |
||||
public async Task<IActionResult> CreateExternalJsonPlayout( |
||||
[Required] [FromBody] CreateExternalJsonPlayoutRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, CreatePlayoutResponse> result = await mediator.Send( |
||||
new CreateExternalJsonPlayout(request.ChannelId, request.ScheduleFile), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/playouts/{id:int}", Name = "DeletePlayout")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Delete a playout")] |
||||
public async Task<IActionResult> DeletePlayout(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new DeletePlayout(id), cancellationToken); |
||||
return result.Match<IActionResult>(_ => NoContent(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPost("/api/playouts/{id:int}/build", Name = "BuildPlayout")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Build a playout")] |
||||
public async Task<IActionResult> BuildPlayout(int id) |
||||
{ |
||||
await workerChannel.WriteAsync(new BuildPlayout(id, PlayoutBuildMode.Refresh)); |
||||
return Accepted(); |
||||
} |
||||
|
||||
[HttpPost("/api/playouts/{id:int}/reset", Name = "ResetPlayout")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Reset a playout")] |
||||
public async Task<IActionResult> ResetPlayout(int id) |
||||
{ |
||||
await workerChannel.WriteAsync(new BuildPlayout(id, PlayoutBuildMode.Reset)); |
||||
return Accepted(); |
||||
} |
||||
|
||||
[HttpPost("/api/playouts/reset-all", Name = "ResetAllPlayouts")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Reset all playouts")] |
||||
public async Task<IActionResult> ResetAllPlayouts(CancellationToken cancellationToken) |
||||
{ |
||||
await mediator.Send(new ResetAllPlayouts(), cancellationToken); |
||||
return Accepted(); |
||||
} |
||||
|
||||
[HttpGet("/api/playouts/warnings/count", Name = "GetPlayoutWarningsCount")] |
||||
[Tags("Playouts")] |
||||
[EndpointSummary("Get playout warnings count")] |
||||
public async Task<int> GetPlayoutWarningsCount(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetPlayoutWarningsCount(), cancellationToken); |
||||
} |
||||
|
||||
// Request models
|
||||
public record CreateClassicPlayoutRequest(int ChannelId, int ProgramScheduleId); |
||||
public record CreateBlockPlayoutRequest(int ChannelId); |
||||
public record CreateSequentialPlayoutRequest(int ChannelId, string ScheduleFile); |
||||
public record CreateScriptedPlayoutRequest(int ChannelId, string ScheduleFile); |
||||
public record CreateExternalJsonPlayoutRequest(int ChannelId, string ScheduleFile); |
||||
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.Plex; |
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
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")] |
||||
public async Task<List<PlexMediaSourceViewModel>> 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")] |
||||
public async Task<IActionResult> GetPlexMediaSourceById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<PlexMediaSourceViewModel> result = await mediator.Send(new GetPlexMediaSourceById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/plex/sources/{id:int}/libraries", Name = "GetPlexLibraries")] |
||||
[Tags("Plex")] |
||||
[EndpointSummary("Get Plex libraries by source")] |
||||
public async Task<List<PlexLibraryViewModel>> 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")] |
||||
public async Task<List<PlexPathReplacementViewModel>> 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")] |
||||
public async Task<IActionResult> UpdatePlexPathReplacements( |
||||
int id, |
||||
[Required] [FromBody] UpdatePlexPathReplacementsRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var replacements = request.PathReplacements?.Select(p => |
||||
new PlexPathReplacementItem(p.Id, p.PlexPath, p.LocalPath)).ToList() ?? []; |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new UpdatePlexPathReplacements(id, replacements), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/plex/library-preferences", Name = "UpdatePlexLibraryPreferences")] |
||||
[Tags("Plex")] |
||||
[EndpointSummary("Update Plex library preferences")] |
||||
public async Task<IActionResult> UpdatePlexLibraryPreferences( |
||||
[Required] [FromBody] UpdatePlexLibraryPreferencesRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var preferences = request.Preferences?.Select(p => |
||||
new PlexLibraryPreference(p.Id, p.ShouldSyncItems)).ToList() ?? []; |
||||
Either<BaseError, Unit> result = await mediator.Send( |
||||
new UpdatePlexLibraryPreferences(preferences), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Ok(), error => Problem(error.ToString())); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record PlexPathReplacementRequest(int Id, string PlexPath, string LocalPath); |
||||
public record UpdatePlexPathReplacementsRequest(List<PlexPathReplacementRequest>? PathReplacements); |
||||
public record PlexLibraryPreferenceRequest(int Id, bool ShouldSyncItems); |
||||
public record UpdatePlexLibraryPreferencesRequest(List<PlexLibraryPreferenceRequest>? Preferences); |
||||
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.ProgramSchedules; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace ErsatzTV.Controllers.Api; |
||||
|
||||
[ApiController] |
||||
[EndpointGroupName("general")] |
||||
public class ScheduleController(IMediator mediator) : ControllerBase |
||||
{ |
||||
[HttpGet("/api/schedules", Name = "GetAllSchedules")] |
||||
[Tags("Schedules")] |
||||
[EndpointSummary("Get all schedules (paginated)")] |
||||
public async Task<PagedProgramSchedulesViewModel> GetAllSchedules( |
||||
[FromQuery] string query = "", |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new GetPagedProgramSchedules(query, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/schedules/all", Name = "GetAllSchedulesList")] |
||||
[Tags("Schedules")] |
||||
[EndpointSummary("Get all schedules")] |
||||
public async Task<List<ProgramScheduleViewModel>> GetAllSchedulesList(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllProgramSchedules(), cancellationToken); |
||||
|
||||
[HttpGet("/api/schedules/{id:int}", Name = "GetScheduleById")] |
||||
[Tags("Schedules")] |
||||
[EndpointSummary("Get schedule by ID")] |
||||
public async Task<IActionResult> GetScheduleById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<ProgramScheduleViewModel> result = await mediator.Send(new GetProgramScheduleById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/schedules/{id:int}/items", Name = "GetScheduleItems")] |
||||
[Tags("Schedules")] |
||||
[EndpointSummary("Get schedule items")] |
||||
public async Task<List<ProgramScheduleItemViewModel>> GetScheduleItems(int id, CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetProgramScheduleItems(id), cancellationToken); |
||||
|
||||
[HttpPost("/api/schedules", Name = "CreateSchedule")] |
||||
[Tags("Schedules")] |
||||
[EndpointSummary("Create a schedule")] |
||||
public async Task<IActionResult> CreateSchedule( |
||||
[Required] [FromBody] CreateScheduleRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, CreateProgramScheduleResult> result = await mediator.Send( |
||||
new CreateProgramSchedule( |
||||
request.Name, |
||||
request.KeepMultiPartEpisodesTogether, |
||||
request.TreatCollectionsAsShows, |
||||
request.ShuffleScheduleItems, |
||||
request.RandomStartPoint, |
||||
request.FixedStartTimeBehavior), |
||||
cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/schedules/{id:int}", Name = "DeleteSchedule")] |
||||
[Tags("Schedules")] |
||||
[EndpointSummary("Delete a schedule")] |
||||
public async Task<IActionResult> DeleteSchedule(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new DeleteProgramSchedule(id), cancellationToken); |
||||
return result.Match<IActionResult>(_ => NoContent(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPost("/api/schedules/{id:int}/copy", Name = "CopySchedule")] |
||||
[Tags("Schedules")] |
||||
[EndpointSummary("Copy a schedule")] |
||||
public async Task<IActionResult> CopySchedule( |
||||
int id, |
||||
[Required] [FromBody] CopyScheduleRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, ProgramScheduleViewModel> result = await mediator.Send( |
||||
new CopyProgramSchedule(id, request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record CreateScheduleRequest( |
||||
string Name, |
||||
bool KeepMultiPartEpisodesTogether, |
||||
bool TreatCollectionsAsShows, |
||||
bool ShuffleScheduleItems, |
||||
bool RandomStartPoint, |
||||
FixedStartTimeBehavior FixedStartTimeBehavior); |
||||
|
||||
public record CopyScheduleRequest(string Name); |
||||
@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
|
||||
using ErsatzTV.Application.MediaCards; |
||||
using ErsatzTV.Application.Search; |
||||
using MediatR; |
||||
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")] |
||||
public async Task<SearchResultAllItemsViewModel> SearchAll( |
||||
[FromQuery] string query, |
||||
CancellationToken cancellationToken) => |
||||
await mediator.Send(new QuerySearchIndexAllItems(query), cancellationToken); |
||||
|
||||
[HttpGet("/api/search/movies", Name = "SearchMovies")] |
||||
[Tags("Search")] |
||||
[EndpointSummary("Search movies")] |
||||
public async Task<MovieCardResultsViewModel> SearchMovies( |
||||
[FromQuery] string query, |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new QuerySearchIndexMovies(query, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/search/shows", Name = "SearchShows")] |
||||
[Tags("Search")] |
||||
[EndpointSummary("Search TV shows")] |
||||
public async Task<TelevisionShowCardResultsViewModel> SearchShows( |
||||
[FromQuery] string query, |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new QuerySearchIndexShows(query, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/search/episodes", Name = "SearchEpisodes")] |
||||
[Tags("Search")] |
||||
[EndpointSummary("Search episodes")] |
||||
public async Task<TelevisionEpisodeCardResultsViewModel> SearchEpisodes( |
||||
[FromQuery] string query, |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new QuerySearchIndexEpisodes(query, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/search/artists", Name = "SearchArtists")] |
||||
[Tags("Search")] |
||||
[EndpointSummary("Search artists")] |
||||
public async Task<ArtistCardResultsViewModel> SearchArtists( |
||||
[FromQuery] string query, |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new QuerySearchIndexArtists(query, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/search/music-videos", Name = "SearchMusicVideos")] |
||||
[Tags("Search")] |
||||
[EndpointSummary("Search music videos")] |
||||
public async Task<MusicVideoCardResultsViewModel> SearchMusicVideos( |
||||
[FromQuery] string query, |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new QuerySearchIndexMusicVideos(query, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/search/songs", Name = "SearchSongs")] |
||||
[Tags("Search")] |
||||
[EndpointSummary("Search songs")] |
||||
public async Task<SongCardResultsViewModel> SearchSongs( |
||||
[FromQuery] string query, |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new QuerySearchIndexSongs(query, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/search/images", Name = "SearchImages")] |
||||
[Tags("Search")] |
||||
[EndpointSummary("Search images")] |
||||
public async Task<ImageCardResultsViewModel> SearchImages( |
||||
[FromQuery] string query, |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new QuerySearchIndexImages(query, pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpPost("/api/search/rebuild", Name = "RebuildSearchIndex")] |
||||
[Tags("Search")] |
||||
[EndpointSummary("Rebuild search index")] |
||||
public async Task<IActionResult> RebuildSearchIndex(CancellationToken cancellationToken) |
||||
{ |
||||
await mediator.Send(new RebuildSearchIndex(), cancellationToken); |
||||
return Accepted(); |
||||
} |
||||
} |
||||
@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
|
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.Scheduling; |
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
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")] |
||||
public async Task<List<TemplateGroupViewModel>> GetTemplateGroups(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllTemplateGroups(), cancellationToken); |
||||
|
||||
[HttpPost("/api/templates/groups", Name = "CreateTemplateGroup")] |
||||
[Tags("Templates")] |
||||
[EndpointSummary("Create a template group")] |
||||
public async Task<IActionResult> CreateTemplateGroup( |
||||
[Required] [FromBody] CreateTemplateGroupRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, TemplateGroupViewModel> result = await mediator.Send( |
||||
new CreateTemplateGroup(request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/templates/groups/{id:int}", Name = "DeleteTemplateGroup")] |
||||
[Tags("Templates")] |
||||
[EndpointSummary("Delete a template group")] |
||||
public async Task<IActionResult> DeleteTemplateGroup(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send(new DeleteTemplateGroup(id), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => NoContent()); |
||||
} |
||||
|
||||
// Templates
|
||||
[HttpGet("/api/templates/groups/{groupId:int}/templates", Name = "GetTemplatesByGroup")] |
||||
[Tags("Templates")] |
||||
[EndpointSummary("Get templates by group")] |
||||
public async Task<List<TemplateViewModel>> GetTemplatesByGroup(int groupId, CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetTemplatesByTemplateGroupId(groupId), cancellationToken); |
||||
|
||||
[HttpGet("/api/templates", Name = "GetAllTemplates")] |
||||
[Tags("Templates")] |
||||
[EndpointSummary("Get all templates")] |
||||
public async Task<List<TemplateViewModel>> GetAllTemplates(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllTemplates(), cancellationToken); |
||||
|
||||
[HttpGet("/api/templates/{id:int}", Name = "GetTemplateById")] |
||||
[Tags("Templates")] |
||||
[EndpointSummary("Get template by ID")] |
||||
public async Task<IActionResult> GetTemplateById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<TemplateViewModel> result = await mediator.Send(new GetTemplateById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpGet("/api/templates/{id:int}/items", Name = "GetTemplateItems")] |
||||
[Tags("Templates")] |
||||
[EndpointSummary("Get template items")] |
||||
public async Task<List<TemplateItemViewModel>> GetTemplateItems(int id, CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetTemplateItems(id), cancellationToken); |
||||
|
||||
[HttpPost("/api/templates", Name = "CreateTemplate")] |
||||
[Tags("Templates")] |
||||
[EndpointSummary("Create a template")] |
||||
public async Task<IActionResult> CreateTemplate( |
||||
[Required] [FromBody] CreateTemplateRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, TemplateViewModel> result = await mediator.Send( |
||||
new CreateTemplate(request.TemplateGroupId, request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/templates/{id:int}", Name = "DeleteTemplate")] |
||||
[Tags("Templates")] |
||||
[EndpointSummary("Delete a template")] |
||||
public async Task<IActionResult> DeleteTemplate(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send(new DeleteTemplate(id), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => NoContent()); |
||||
} |
||||
|
||||
[HttpPost("/api/templates/{id:int}/copy", Name = "CopyTemplate")] |
||||
[Tags("Templates")] |
||||
[EndpointSummary("Copy a template")] |
||||
public async Task<IActionResult> CopyTemplate( |
||||
int id, |
||||
[Required] [FromBody] CopyTemplateRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, TemplateViewModel> result = await mediator.Send( |
||||
new CopyTemplate(id, request.NewTemplateGroupId, request.NewTemplateName), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/templates/{id:int}/items", Name = "ReplaceTemplateItems")] |
||||
[Tags("Templates")] |
||||
[EndpointSummary("Replace template items")] |
||||
public async Task<IActionResult> ReplaceTemplateItems( |
||||
int id, |
||||
[Required] [FromBody] ReplaceTemplateItemsRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var items = request.Items.Select(i => new ReplaceTemplateItem(i.BlockId, i.StartTime)).ToList(); |
||||
Either<BaseError, List<TemplateItemViewModel>> result = await mediator.Send( |
||||
new ReplaceTemplateItems(request.TemplateGroupId, id, request.Name, items), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record CreateTemplateGroupRequest(string Name); |
||||
public record CreateTemplateRequest(int TemplateGroupId, string Name); |
||||
public record CopyTemplateRequest(int NewTemplateGroupId, string NewTemplateName); |
||||
public record ReplaceTemplateItemRequest(int BlockId, TimeSpan StartTime); |
||||
public record ReplaceTemplateItemsRequest(int TemplateGroupId, string Name, List<ReplaceTemplateItemRequest> Items); |
||||
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.MediaCollections; |
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
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)")] |
||||
public async Task<PagedTraktListsViewModel> GetTraktLists( |
||||
[FromQuery] int pageNumber = 0, |
||||
[FromQuery] int pageSize = 50, |
||||
CancellationToken cancellationToken = default) => |
||||
await mediator.Send(new GetPagedTraktLists(pageNumber, pageSize), cancellationToken); |
||||
|
||||
[HttpGet("/api/trakt/lists/{id:int}", Name = "GetTraktListById")] |
||||
[Tags("Trakt")] |
||||
[EndpointSummary("Get Trakt list by ID")] |
||||
public async Task<IActionResult> GetTraktListById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<TraktListViewModel> result = await mediator.Send(new GetTraktListById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpPut("/api/trakt/lists/{id:int}", Name = "UpdateTraktList")] |
||||
[Tags("Trakt")] |
||||
[EndpointSummary("Update a Trakt list")] |
||||
public async Task<IActionResult> UpdateTraktList( |
||||
int id, |
||||
[Required] [FromBody] UpdateTraktListRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Option<BaseError> result = await mediator.Send( |
||||
new UpdateTraktList(id, request.AutoRefresh, request.GeneratePlaylist), cancellationToken); |
||||
return result.Match<IActionResult>(error => Problem(error.ToString()), () => Ok()); |
||||
} |
||||
|
||||
[HttpDelete("/api/trakt/lists/{id:int}", Name = "DeleteTraktList")] |
||||
[Tags("Trakt")] |
||||
[EndpointSummary("Delete a Trakt list")] |
||||
public async Task<IActionResult> DeleteTraktList(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new DeleteTraktList(id), cancellationToken); |
||||
return result.Match<IActionResult>(_ => NoContent(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPost("/api/trakt/lists/{id:int}/match", Name = "MatchTraktListItems")] |
||||
[Tags("Trakt")] |
||||
[EndpointSummary("Match Trakt list items")] |
||||
public async Task<IActionResult> MatchTraktListItems(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new MatchTraktListItems(id), cancellationToken); |
||||
return result.Match<IActionResult>(_ => Accepted(), error => Problem(error.ToString())); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record UpdateTraktListRequest(bool AutoRefresh, bool GeneratePlaylist); |
||||
@ -0,0 +1,159 @@
@@ -0,0 +1,159 @@
|
||||
#nullable enable |
||||
using System.ComponentModel.DataAnnotations; |
||||
using ErsatzTV.Application.Artworks; |
||||
using ErsatzTV.Application.Watermarks; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.FFmpeg.State; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
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")] |
||||
public async Task<List<WatermarkViewModel>> GetAllWatermarks(CancellationToken cancellationToken) => |
||||
await mediator.Send(new GetAllWatermarks(), cancellationToken); |
||||
|
||||
[HttpGet("/api/watermarks/{id:int}", Name = "GetWatermarkById")] |
||||
[Tags("Watermarks")] |
||||
[EndpointSummary("Get watermark by ID")] |
||||
public async Task<IActionResult> GetWatermarkById(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Option<WatermarkViewModel> result = await mediator.Send(new GetWatermarkById(id), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, () => NotFound()); |
||||
} |
||||
|
||||
[HttpPost("/api/watermarks", Name = "CreateWatermark")] |
||||
[Tags("Watermarks")] |
||||
[EndpointSummary("Create a watermark")] |
||||
public async Task<IActionResult> CreateWatermark( |
||||
[Required] [FromBody] CreateWatermarkRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var image = request.ImagePath != null |
||||
? new ArtworkContentTypeModel(request.ImagePath, request.ImageContentType ?? "") |
||||
: ArtworkContentTypeModel.None; |
||||
|
||||
Either<BaseError, CreateWatermarkResult> result = await mediator.Send( |
||||
new CreateWatermark( |
||||
request.Name, |
||||
image, |
||||
request.Mode, |
||||
request.ImageSource, |
||||
request.Location, |
||||
request.Size, |
||||
request.Width, |
||||
request.HorizontalMargin, |
||||
request.VerticalMargin, |
||||
request.FrequencyMinutes, |
||||
request.DurationSeconds, |
||||
request.Opacity, |
||||
request.PlaceWithinSourceContent, |
||||
request.OpacityExpression ?? "", |
||||
request.ZIndex), |
||||
cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPut("/api/watermarks/{id:int}", Name = "UpdateWatermark")] |
||||
[Tags("Watermarks")] |
||||
[EndpointSummary("Update a watermark")] |
||||
public async Task<IActionResult> UpdateWatermark( |
||||
int id, |
||||
[Required] [FromBody] UpdateWatermarkRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var image = request.ImagePath != null |
||||
? new ArtworkContentTypeModel(request.ImagePath, request.ImageContentType ?? "") |
||||
: ArtworkContentTypeModel.None; |
||||
|
||||
Either<BaseError, UpdateWatermarkResult> result = await mediator.Send( |
||||
new UpdateWatermark( |
||||
id, |
||||
request.Name, |
||||
image, |
||||
request.Mode, |
||||
request.ImageSource, |
||||
request.Location, |
||||
request.Size, |
||||
request.Width, |
||||
request.HorizontalMargin, |
||||
request.VerticalMargin, |
||||
request.FrequencyMinutes, |
||||
request.DurationSeconds, |
||||
request.Opacity, |
||||
request.PlaceWithinSourceContent, |
||||
request.OpacityExpression ?? "", |
||||
request.ZIndex), |
||||
cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpDelete("/api/watermarks/{id:int}", Name = "DeleteWatermark")] |
||||
[Tags("Watermarks")] |
||||
[EndpointSummary("Delete a watermark")] |
||||
public async Task<IActionResult> DeleteWatermark(int id, CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, Unit> result = await mediator.Send(new DeleteWatermark(id), cancellationToken); |
||||
return result.Match<IActionResult>(_ => NoContent(), error => Problem(error.ToString())); |
||||
} |
||||
|
||||
[HttpPost("/api/watermarks/{id:int}/copy", Name = "CopyWatermark")] |
||||
[Tags("Watermarks")] |
||||
[EndpointSummary("Copy a watermark")] |
||||
public async Task<IActionResult> CopyWatermark( |
||||
int id, |
||||
[Required] [FromBody] CopyWatermarkRequest request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
Either<BaseError, WatermarkViewModel> result = await mediator.Send( |
||||
new CopyWatermark(id, request.Name), cancellationToken); |
||||
return result.Match<IActionResult>(Ok, error => Problem(error.ToString())); |
||||
} |
||||
} |
||||
|
||||
// Request models
|
||||
public record CreateWatermarkRequest( |
||||
string Name, |
||||
string? ImagePath, |
||||
string? ImageContentType, |
||||
ChannelWatermarkMode Mode, |
||||
ChannelWatermarkImageSource ImageSource, |
||||
WatermarkLocation Location, |
||||
WatermarkSize Size, |
||||
double Width, |
||||
double HorizontalMargin, |
||||
double VerticalMargin, |
||||
int FrequencyMinutes, |
||||
int DurationSeconds, |
||||
int Opacity, |
||||
bool PlaceWithinSourceContent, |
||||
string? OpacityExpression, |
||||
int ZIndex); |
||||
|
||||
public record UpdateWatermarkRequest( |
||||
string Name, |
||||
string? ImagePath, |
||||
string? ImageContentType, |
||||
ChannelWatermarkMode Mode, |
||||
ChannelWatermarkImageSource ImageSource, |
||||
WatermarkLocation Location, |
||||
WatermarkSize Size, |
||||
double Width, |
||||
double HorizontalMargin, |
||||
double VerticalMargin, |
||||
int FrequencyMinutes, |
||||
int DurationSeconds, |
||||
int Opacity, |
||||
bool PlaceWithinSourceContent, |
||||
string? OpacityExpression, |
||||
int ZIndex); |
||||
|
||||
public record CopyWatermarkRequest(string Name); |
||||
Loading…
Reference in new issue