diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f252d39..e9e98a951 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Fixed +- Do not allow deleting ffmpeg profiles that are used by channels ## [25.7.0] - 2025-09-14 ### Added diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs index 088380bbc..6023241d3 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs @@ -7,23 +7,17 @@ using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.FFmpegProfiles; -public class DeleteFFmpegProfileHandler : IRequestHandler> +public class DeleteFFmpegProfileHandler(IDbContextFactory dbContextFactory, ISearchTargets searchTargets) + : IRequestHandler> { - private readonly IDbContextFactory _dbContextFactory; - private readonly ISearchTargets _searchTargets; - - public DeleteFFmpegProfileHandler(IDbContextFactory dbContextFactory, ISearchTargets searchTargets) - { - _dbContextFactory = dbContextFactory; - _searchTargets = searchTargets; - } - public async Task> Handle( DeleteFFmpegProfile request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Validation validation = await FFmpegProfileMustExist(dbContext, request, cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = + await FFmpegProfileMustNotBeUsed(dbContext, request, cancellationToken) + .BindT(_ => FFmpegProfileMustExist(dbContext, request, cancellationToken)); return await validation.Apply(p => DoDeletion(dbContext, p)); } @@ -31,7 +25,7 @@ public class DeleteFFmpegProfileHandler : IRequestHandler p.Id, p => p.Id == request.FFmpegProfileId, cancellationToken) .Map(o => o.ToValidation($"FFmpegProfile {request.FFmpegProfileId} does not exist")); + + private static async Task> FFmpegProfileMustNotBeUsed( + TvContext dbContext, + DeleteFFmpegProfile request, + CancellationToken cancellationToken) + { + int count = await dbContext.Channels + .AsNoTracking() + .Where(c => c.FFmpegProfileId == request.FFmpegProfileId) + .CountAsync(cancellationToken); + + if (count > 0) + { + return BaseError.New($"Cannot delete FFmpegProfile {request.FFmpegProfileId} that is used by {count} {(count > 1 ? "channels" : "channel")}"); + } + + return Unit.Default; + } } diff --git a/ErsatzTV/Pages/FFmpeg.razor b/ErsatzTV/Pages/FFmpeg.razor index 5af6d0817..661d40c37 100644 --- a/ErsatzTV/Pages/FFmpeg.razor +++ b/ErsatzTV/Pages/FFmpeg.razor @@ -2,6 +2,7 @@ @using ErsatzTV.Application.FFmpegProfiles @implements IDisposable @inject IDialogService Dialog +@inject ISnackbar Snackbar @inject IMediator Mediator @inject NavigationManager NavigationManager @@ -111,8 +112,16 @@ DialogResult result = await dialog.Result; if (result is { Canceled: false }) { - await Mediator.Send(new DeleteFFmpegProfile(ffmpegProfile.Id), _cts.Token); - await LoadFFmpegProfilesAsync(_cts.Token); + Either deleteResult = await Mediator.Send(new DeleteFFmpegProfile(ffmpegProfile.Id), _cts.Token); + foreach (BaseError error in deleteResult.LeftToSeq()) + { + Snackbar.Add(error.Value, Severity.Error); + } + + if (deleteResult.IsRight) + { + await LoadFFmpegProfilesAsync(_cts.Token); + } } } @@ -123,7 +132,7 @@ IDialogReference dialog = await Dialog.ShowAsync("Copy FFmpeg Profile", parameters, options); DialogResult dialogResult = await dialog.Result; - if (!dialogResult.Canceled && dialogResult.Data is FFmpegProfileViewModel data) + if (dialogResult is { Canceled: false, Data: FFmpegProfileViewModel data }) { NavigationManager.NavigateTo($"ffmpeg/{data.Id}"); }