Adds the REST surface for the #145 troubleshooting leftovers (#158 items 4-5): - GET /api/playouts/{id}/blocks - blocks a block playout schedules - GET /api/playouts/{id}/blocks/{blockId}/history - paged block history - GET /api/playouts/history/{id} - decode a history row by id - POST /api/troubleshoot/validate-schedule - validate sequential YAML New MediatR queries GetPlayoutHistoryDetails (Either, 404 unknown row / 422 malformed JSON) and ValidateSequentialSchedule (wraps ISequentialScheduleValidator, never throws). DecodePlayoutHistoryHandler and the new by-id handler now share PlayoutHistoryDecoder. Regenerated openapi/v1.json. Refs #145 #158 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
100 lines
3.9 KiB
C#
100 lines
3.9 KiB
C#
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using ErsatzTV.Infrastructure.Extensions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace ErsatzTV.Application.Troubleshooting.Queries;
|
|
|
|
// Shared decode logic for a single PlayoutHistory row's Key/Details JSON. Both
|
|
// DecodePlayoutHistoryHandler (Blazor) and GetPlayoutHistoryDetailsHandler (API) use this
|
|
// so the collection/media-item lookup lives in exactly one place.
|
|
internal static class PlayoutHistoryDecoder
|
|
{
|
|
public static async Task<PlayoutHistoryDetailsViewModel> Decode(
|
|
TvContext dbContext,
|
|
string key,
|
|
string details,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var decodedKey = JsonConvert.DeserializeObject<BlockItemHistoryKey>(key);
|
|
|
|
PlaybackOrder playbackOrder = decodedKey.PlaybackOrder ?? PlaybackOrder.None;
|
|
CollectionType collectionType = decodedKey.CollectionType ?? CollectionType.Collection;
|
|
|
|
string name = string.Empty;
|
|
|
|
switch (collectionType)
|
|
{
|
|
case CollectionType.Collection:
|
|
name = await dbContext.Collections
|
|
.AsNoTracking()
|
|
.Where(c => c.Id == (decodedKey.CollectionId ?? 0))
|
|
.Map(c => c.Name)
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
break;
|
|
case CollectionType.SmartCollection:
|
|
name = await dbContext.SmartCollections
|
|
.AsNoTracking()
|
|
.Where(c => c.Id == (decodedKey.SmartCollectionId ?? 0))
|
|
.Map(c => c.Name)
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
break;
|
|
}
|
|
|
|
string mediaItemType = string.Empty;
|
|
string mediaItemTitle = string.Empty;
|
|
|
|
Details decodedDetails = JsonConvert.DeserializeObject<Details>(details);
|
|
if (decodedDetails?.MediaItemId != null)
|
|
{
|
|
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems
|
|
.AsNoTracking()
|
|
.Include(i => i.LibraryPath)
|
|
.ThenInclude(lp => lp.Library)
|
|
.ThenInclude(l => l.MediaSource)
|
|
.Include(i => (i as Movie).MovieMetadata)
|
|
.Include(i => (i as Episode).EpisodeMetadata)
|
|
.Include(i => (i as Episode).Season)
|
|
.ThenInclude(s => s.Show)
|
|
.ThenInclude(s => s.ShowMetadata)
|
|
.Include(i => (i as OtherVideo).OtherVideoMetadata)
|
|
.Include(i => (i as Image).ImageMetadata)
|
|
.Include(i => (i as RemoteStream).RemoteStreamMetadata)
|
|
.Include(i => (i as Song).SongMetadata)
|
|
.Include(i => (i as MusicVideo).MusicVideoMetadata)
|
|
.Include(i => (i as MusicVideo).Artist)
|
|
.ThenInclude(a => a.ArtistMetadata)
|
|
.SelectOneAsync(i => i.Id, i => i.Id == decodedDetails.MediaItemId, cancellationToken);
|
|
|
|
foreach (var mediaItem in maybeMediaItem)
|
|
{
|
|
mediaItemType = mediaItem switch
|
|
{
|
|
Episode => "Episode",
|
|
Movie => "Movie",
|
|
MusicVideo => "Music Video",
|
|
OtherVideo => "Other Video",
|
|
Song => "Song",
|
|
Image => "Image",
|
|
RemoteStream => "Remote Stream",
|
|
_ => $"Unknown ({mediaItem.GetType().Name})"
|
|
};
|
|
|
|
mediaItemTitle = Playouts.Mapper.GetDisplayTitle(mediaItem, Option<string>.None);
|
|
}
|
|
}
|
|
|
|
return new PlayoutHistoryDetailsViewModel(playbackOrder, collectionType, name, mediaItemType, mediaItemTitle);
|
|
}
|
|
|
|
private sealed record BlockItemHistoryKey(
|
|
int? BlockId,
|
|
PlaybackOrder? PlaybackOrder,
|
|
CollectionType? CollectionType,
|
|
int? CollectionId,
|
|
int? SmartCollectionId);
|
|
|
|
private sealed record Details(int? MediaItemId);
|
|
}
|