Merge remote-tracking branch 'origin/main' into feat/141-media-browse
# Conflicts: # ErsatzTV.Tests/Controllers/OpenApiErrorResponseContractTests.cs # web/src/App.tsx # web/src/screens/SettingsScreen.tsx
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Controllers.Api.Requests;
|
||||
|
||||
public record AddTraktListRequest(string Url);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Controllers.Api.Requests;
|
||||
|
||||
public record UpdateTraktListRequest(bool AutoRefresh, bool GeneratePlaylist);
|
||||
@@ -0,0 +1,248 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.MediaCollections;
|
||||
using ErsatzTV.Controllers.Api.Requests;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Api.Trakt;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Extensions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ErsatzTV.Controllers.Api;
|
||||
|
||||
[ApiController]
|
||||
public partial class TraktController(
|
||||
IMediator mediator,
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||
IEntityLocker entityLocker) : ControllerBase
|
||||
{
|
||||
private const int MaxPageSize = 100;
|
||||
|
||||
[HttpGet("/api/trakt/lists", Name = "GetTraktLists")]
|
||||
[Tags("Trakt")]
|
||||
[EndpointSummary("Get paged Trakt lists")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(PagedTraktListsResponseModel), StatusCodes.Status200OK)]
|
||||
public async Task<PagedTraktListsResponseModel> GetAll(
|
||||
[FromQuery] int pageNum = 0,
|
||||
[FromQuery] int pageSize = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
int clampedPageNum = Math.Max(0, pageNum);
|
||||
int clampedPageSize = Math.Clamp(pageSize, 1, MaxPageSize);
|
||||
|
||||
PagedTraktListsViewModel result = await mediator.Send(
|
||||
new GetPagedTraktLists(clampedPageNum, clampedPageSize),
|
||||
cancellationToken);
|
||||
|
||||
return new PagedTraktListsResponseModel(
|
||||
result.TotalCount,
|
||||
result.Page.Map(ProjectToResponseModel).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("/api/trakt/lists/{id:int}", Name = "GetTraktListById")]
|
||||
[Tags("Trakt")]
|
||||
[EndpointSummary("Get a Trakt list by id")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(TraktListResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<TraktListViewModel> result = await mediator.Send(new GetTraktListById(id), cancellationToken);
|
||||
return result.Map(ProjectToResponseModel).ToGetResult();
|
||||
}
|
||||
|
||||
[HttpPost("/api/trakt/lists")]
|
||||
[Tags("Trakt")]
|
||||
[EndpointSummary("Add a Trakt list by URL")]
|
||||
[EndpointDescription(
|
||||
"Dispatches to the same background worker channel used by the classic UI's \"Add Trakt List\" dialog; " +
|
||||
"the list is fetched, saved, and matched asynchronously. Poll GET /api/trakt/status while busy.")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Add(
|
||||
[Required] [FromBody] AddTraktListRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsValidTraktListUrl(request.Url))
|
||||
{
|
||||
return new UnprocessableEntityObjectResult(
|
||||
CreateProblemDetails(422, "Validation failed", "Invalid Trakt list url"));
|
||||
}
|
||||
|
||||
return await EnqueueWithTraktLock(AddTraktList.FromUrl(request.Url), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("/api/trakt/lists/{id:int}/match")]
|
||||
[Tags("Trakt")]
|
||||
[EndpointSummary("Match a Trakt list's items")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Match(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<TraktListViewModel> existing = await mediator.Send(new GetTraktListById(id), cancellationToken);
|
||||
if (existing.IsNone)
|
||||
{
|
||||
return ApiResults.NotFoundProblem();
|
||||
}
|
||||
|
||||
return await EnqueueWithTraktLock(new MatchTraktListItems(id), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpDelete("/api/trakt/lists/{id:int}")]
|
||||
[Tags("Trakt")]
|
||||
[EndpointSummary("Delete a Trakt list")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<TraktListViewModel> existing = await mediator.Send(new GetTraktListById(id), cancellationToken);
|
||||
if (existing.IsNone)
|
||||
{
|
||||
return ApiResults.NotFoundProblem();
|
||||
}
|
||||
|
||||
return await EnqueueWithTraktLock(new DeleteTraktList(id), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("/api/trakt/lists/{id:int}")]
|
||||
[Tags("Trakt")]
|
||||
[EndpointSummary("Update a Trakt list's settings")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(TraktListResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<IActionResult> Update(
|
||||
int id,
|
||||
[Required] [FromBody] UpdateTraktListRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Option<TraktListViewModel> existing = await mediator.Send(new GetTraktListById(id), cancellationToken);
|
||||
if (existing.IsNone)
|
||||
{
|
||||
return ApiResults.NotFoundProblem();
|
||||
}
|
||||
|
||||
Option<BaseError> maybeError = await mediator.Send(
|
||||
new UpdateTraktList(id, request.AutoRefresh, request.GeneratePlaylist),
|
||||
cancellationToken);
|
||||
|
||||
foreach (BaseError error in maybeError)
|
||||
{
|
||||
return error.ToErrorResult();
|
||||
}
|
||||
|
||||
Option<TraktListViewModel> updated = await mediator.Send(new GetTraktListById(id), cancellationToken);
|
||||
return updated.Map(ProjectToResponseModel).ToGetResult();
|
||||
}
|
||||
|
||||
[HttpGet("/api/trakt/status", Name = "GetTraktStatus")]
|
||||
[Tags("Trakt")]
|
||||
[EndpointSummary("Get Trakt background operation status")]
|
||||
[EndpointDescription(
|
||||
"Wraps IEntityLocker.IsTraktLocked() — the HTTP-observable substitute for the Blazor page's live lock " +
|
||||
"event. The SPA polls this while add/match/delete are in flight.")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(TraktStatusResponseModel), StatusCodes.Status200OK)]
|
||||
public TraktStatusResponseModel GetStatus() => new(entityLocker.IsTraktLocked());
|
||||
|
||||
private static TraktListResponseModel ProjectToResponseModel(TraktListViewModel viewModel) =>
|
||||
new(
|
||||
viewModel.Id,
|
||||
viewModel.TraktId,
|
||||
viewModel.Slug,
|
||||
viewModel.Name,
|
||||
viewModel.ItemCount,
|
||||
viewModel.MatchCount,
|
||||
viewModel.AutoRefresh,
|
||||
viewModel.GeneratePlaylist);
|
||||
|
||||
private async Task<IActionResult> EnqueueWithTraktLock(
|
||||
IBackgroundServiceRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!entityLocker.LockTrakt())
|
||||
{
|
||||
return ConflictProblem();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await workerChannel.WriteAsync(request, cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// the background handler only unlocks when it receives the message;
|
||||
// if enqueueing fails (e.g. request aborted), release the lock here or it is held forever
|
||||
entityLocker.UnlockTrakt();
|
||||
throw;
|
||||
}
|
||||
|
||||
return new AcceptedResult();
|
||||
}
|
||||
|
||||
private static ConflictObjectResult ConflictProblem() =>
|
||||
new ConflictObjectResult(
|
||||
CreateProblemDetails(
|
||||
409,
|
||||
"Trakt operation in progress",
|
||||
"A Trakt background operation is already in progress"));
|
||||
|
||||
private static ProblemDetails CreateProblemDetails(int status, string title, string detail) =>
|
||||
new()
|
||||
{
|
||||
Status = status,
|
||||
Title = title,
|
||||
Detail = detail
|
||||
};
|
||||
|
||||
// The following mirrors AddTraktListHandler.ValidateUrl (ErsatzTV.Application/MediaCollections/Commands/
|
||||
// AddTraktListHandler.cs). That method is private to the handler, operates on the handler's own request type,
|
||||
// and returns a handler-private record, so it can't be called from here directly — replicated minimally so the
|
||||
// controller can reject an obviously-invalid URL with a synchronous 422 before dispatching to the background
|
||||
// worker (which otherwise would silently no-op on a bad URL, since AddTraktListHandler's own ValidateUrl runs
|
||||
// fire-and-forget on the worker channel).
|
||||
[GeneratedRegex(@"https:\/\/(?:app\.)?trakt\.tv\/users\/([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")]
|
||||
private static partial Regex UriTraktListRegex();
|
||||
|
||||
[GeneratedRegex(@"https:\/\/(?:app\.)?trakt\.tv\/lists\/([\w\-_]+)\/([\w\-_]+)")]
|
||||
private static partial Regex UriTraktListRegex2();
|
||||
|
||||
[GeneratedRegex(@"([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")]
|
||||
private static partial Regex ShorthandTraktListRegex();
|
||||
|
||||
private static bool IsValidTraktListUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Match match = Uri.IsWellFormedUriString(url, UriKind.Absolute)
|
||||
? MatchTraktListUrl(url)
|
||||
: ShorthandTraktListRegex().Match(url);
|
||||
|
||||
return match.Success;
|
||||
}
|
||||
|
||||
private static Match MatchTraktListUrl(string url)
|
||||
{
|
||||
Match match = UriTraktListRegex().Match(url);
|
||||
if (!match.Success)
|
||||
{
|
||||
match = UriTraktListRegex2().Match(url);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
}
|
||||
@@ -6045,6 +6045,458 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/trakt/lists": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Trakt"
|
||||
],
|
||||
"summary": "Get paged Trakt lists",
|
||||
"operationId": "GetTraktLists",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pageNum",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pageSize",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"default": 100
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PagedTraktListsResponseModel"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PagedTraktListsResponseModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PagedTraktListsResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Trakt"
|
||||
],
|
||||
"summary": "Add a Trakt list by URL",
|
||||
"description": "Dispatches to the same background worker channel used by the classic UI's \"Add Trakt List\" dialog; the list is fetched, saved, and matched asynchronously. Poll GET /api/trakt/status while busy.",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json-patch+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AddTraktListRequest"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AddTraktListRequest"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AddTraktListRequest"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AddTraktListRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Accepted"
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/trakt/lists/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Trakt"
|
||||
],
|
||||
"summary": "Get a Trakt list by id",
|
||||
"operationId": "GetTraktListById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TraktListResponseModel"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TraktListResponseModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TraktListResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Trakt"
|
||||
],
|
||||
"summary": "Delete a Trakt list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Accepted"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Trakt"
|
||||
],
|
||||
"summary": "Update a Trakt list's settings",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json-patch+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateTraktListRequest"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateTraktListRequest"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateTraktListRequest"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateTraktListRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TraktListResponseModel"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TraktListResponseModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TraktListResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/trakt/lists/{id}/match": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Trakt"
|
||||
],
|
||||
"summary": "Match a Trakt list's items",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Accepted"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/trakt/status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Trakt"
|
||||
],
|
||||
"summary": "Get Trakt background operation status",
|
||||
"description": "Wraps IEntityLocker.IsTraktLocked() — the HTTP-observable substitute for the Blazor page's live lock event. The SPA polls this while add/match/delete are in flight.",
|
||||
"operationId": "GetTraktStatus",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TraktStatusResponseModel"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TraktStatusResponseModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TraktStatusResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/troubleshoot/info": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -6917,6 +7369,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"AddTraktListRequest": {
|
||||
"required": [
|
||||
"url"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ArtworkContentTypeModel": {
|
||||
"required": [
|
||||
"path",
|
||||
@@ -10082,6 +10548,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"PagedTraktListsResponseModel": {
|
||||
"required": [
|
||||
"totalCount",
|
||||
"page"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"totalCount": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"page": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TraktListResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PlaybackOrder": {
|
||||
"enum": [
|
||||
"None",
|
||||
@@ -11189,6 +11674,60 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TraktListResponseModel": {
|
||||
"required": [
|
||||
"id",
|
||||
"traktId",
|
||||
"slug",
|
||||
"name",
|
||||
"itemCount",
|
||||
"matchCount",
|
||||
"autoRefresh",
|
||||
"generatePlaylist"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"traktId": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"itemCount": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"matchCount": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"autoRefresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"generatePlaylist": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraktStatusResponseModel": {
|
||||
"required": [
|
||||
"busy"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"busy": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TroubleshootingInfoResponseModel": {
|
||||
"required": [
|
||||
"generalJson",
|
||||
@@ -12052,6 +12591,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateTraktListRequest": {
|
||||
"required": [
|
||||
"autoRefresh",
|
||||
"generatePlaylist"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"autoRefresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"generatePlaylist": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateUiSettingsRequest": {
|
||||
"required": [
|
||||
"isDarkMode",
|
||||
@@ -12504,6 +13058,9 @@
|
||||
{
|
||||
"name": "Smart Collections"
|
||||
},
|
||||
{
|
||||
"name": "Trakt"
|
||||
},
|
||||
{
|
||||
"name": "Troubleshooting"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user