using ErsatzTV.Application.Artworks; using ErsatzTV.Application.MediaCollections; using ErsatzTV.Core; using ErsatzTV.Core.Api.Channels; using ErsatzTV.Core.Api.LibraryBrowse; using ErsatzTV.Core.Domain; using LanguageExt; using MediatR; namespace ErsatzTV.Application.Channels; public class CreateAutoTunedChannelsHandler(ISender mediator) : IRequestHandler { private const string NumberTakenError = "Channel number must be unique"; private const string DefaultGroup = "Auto-Tuned"; public async Task Handle( CreateAutoTunedChannels request, CancellationToken cancellationToken) { string group = string.IsNullOrWhiteSpace(request.Group) ? DefaultGroup : request.Group.Trim(); var outcomes = new List(); foreach (AutoTuneChannelSelection selection in request.Channels ?? []) { outcomes.Add(await CreateOne(request.TemplateId, group, selection, cancellationToken)); } return new AutoTuneResult(outcomes); } private async Task CreateOne( int templateId, string group, AutoTuneChannelSelection selection, CancellationToken cancellationToken) { string name = (selection.Name ?? string.Empty).Trim(); if (name.Length is 0 or > 50) { return new AutoTuneChannelOutcome(name, AutoTuneOutcomeStatus.Failed, null, "Invalid channel name"); } string query = AutoTuneAxisMap.GenerateQuery(selection.Axis, selection.Value); PlaybackOrder order = AutoTuneAxisMap.PlaybackOrderFor(selection.Axis); // 1. Create the smart collection that drives this channel. Either scResult = await mediator.Send(new CreateSmartCollection(query, name), cancellationToken); SmartCollectionViewModel smartCollection = null; foreach (BaseError error in scResult.LeftToSeq()) { return new AutoTuneChannelOutcome( name, AutoTuneOutcomeStatus.Failed, null, $"Smart collection: {error.Value}"); } foreach (SmartCollectionViewModel vm in scResult.RightToSeq()) { smartCollection = vm; } // 2. Create the channel from a single-item lineup referencing the smart collection. var command = new CreateChannelFromLineup( name, selection.Number, group, string.Empty, ArtworkContentTypeModel.None, IsEnabled: true, ShowInEpg: true, templateId, new CreateChannelFromLineupAdvancedOptions(PlaybackOrder: order), [ new CreateChannelFromLineupItem( LibraryBrowseMediaType.SmartCollection, CollectionType.SmartCollection, CollectionId: null, MultiCollectionId: null, SmartCollectionId: smartCollection.Id, RerunCollectionId: null, MediaItemId: null, PlaylistId: null) ]); Either channelResult = await mediator.Send(command, cancellationToken); foreach (BaseError error in channelResult.LeftToSeq()) { // Roll back the smart collection we just created so a retry of this // axis/value doesn't fail on SmartCollection-name uniqueness. Best-effort; // the primary outcome below is still Skipped/Failed regardless of the delete result. // Swallow any exception (not just an Either.Left) so a transient infra failure // during rollback never aborts this channel's outcome or the batch; the // orphaned SmartCollection is an acceptable degraded outcome. try { await mediator.Send(new DeleteSmartCollection(smartCollection.Id), cancellationToken); } catch (Exception) { // intentionally ignored; see comment above } AutoTuneOutcomeStatus status = error.Value.Contains(NumberTakenError, StringComparison.Ordinal) ? AutoTuneOutcomeStatus.Skipped : AutoTuneOutcomeStatus.Failed; return new AutoTuneChannelOutcome(name, status, null, error.Value); } int channelId = channelResult.Match(Left: _ => 0, Right: r => r.ChannelId); return new AutoTuneChannelOutcome(name, AutoTuneOutcomeStatus.Created, channelId, null); } }