Stream custom live channels using your own media
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

286 lines
15 KiB

@page "/settings"
@using ErsatzTV.Application.FFmpegProfiles
@using ErsatzTV.Application.FFmpegProfiles.Commands
@using ErsatzTV.Application.FFmpegProfiles.Queries
@using ErsatzTV.Application.HDHR.Commands
@using ErsatzTV.Application.HDHR.Queries
@using ErsatzTV.Application.MediaItems.Queries
@using System.Globalization
@using ErsatzTV.Application.Configuration.Queries
@using Unit = LanguageExt.Unit
@using ErsatzTV.Application.Configuration.Commands
@using ErsatzTV.Application.Filler
@using ErsatzTV.Application.Filler.Queries
@using ErsatzTV.Application.Watermarks
@using ErsatzTV.Application.Watermarks.Queries
@using ErsatzTV.Core.Domain.Filler
@using ErsatzTV.Core.FFmpeg
@using Microsoft.AspNetCore.Components
@using System.Threading
@implements IDisposable
@inject IMediator _mediator
@inject ISnackbar _snackbar
@inject ILogger<Settings> _logger
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="display: flex; flex-direction: row">
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudCard Class="mr-6 mb-6" Style="max-width: 400px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">FFmpeg Settings</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudForm @bind-IsValid="@_success">
<MudTextField T="string" Label="FFmpeg Path" @bind-Value="_ffmpegSettings.FFmpegPath" Validation="@(new Func<string, string>(ValidatePathExists))" Required="true" RequiredError="FFmpeg path is required!"/>
<MudElement HtmlTag="div" Class="mt-3">
<MudTextField T="string" Label="FFprobe Path" @bind-Value="_ffmpegSettings.FFprobePath" Validation="@(new Func<string, string>(ValidatePathExists))" Required="true" RequiredError="FFprobe path is required!"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-3">
<MudSelect Label="Default Profile" @bind-Value="_ffmpegSettings.DefaultFFmpegProfileId" For="@(() => _ffmpegSettings.DefaultFFmpegProfileId)">
@foreach (FFmpegProfileViewModel profile in _ffmpegProfiles)
{
<MudSelectItem Value="@profile.Id">@profile.Name</MudSelectItem>
}
</MudSelect>
</MudElement>
<MudSelect Class="mt-3" Label="Preferred Language" @bind-Value="_ffmpegSettings.PreferredLanguageCode" For="@(() => _ffmpegSettings.PreferredLanguageCode)" Required="true" RequiredError="Preferred Language Code is required!">
@foreach (CultureInfo culture in _availableCultures)
{
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
}
</MudSelect>
<MudElement HtmlTag="div" Class="mt-3">
<MudSwitch T="bool"
Label="Save troubleshooting reports to disk"
Color="Color.Primary"
@bind-Checked="@_ffmpegSettings.SaveReports"/>
</MudElement>
<MudSelect Class="mt-3"
Label="Global Watermark"
@bind-Value="_ffmpegSettings.GlobalWatermarkId"
For="@(() => _ffmpegSettings.GlobalWatermarkId)"
Clearable="true">
<MudSelectItem T="int?" Value="@((int?) null)">(none)</MudSelectItem>
@foreach (WatermarkViewModel watermark in _watermarks)
{
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
}
</MudSelect>
<MudSelect Class="mt-3"
Label="Global Fallback Filler"
@bind-Value="_ffmpegSettings.GlobalFallbackFillerId"
For="@(() => _ffmpegSettings.GlobalFallbackFillerId)"
Clearable="true">
<MudSelectItem T="int?" Value="@((int?) null)">(none)</MudSelectItem>
@foreach (FillerPresetViewModel fillerPreset in _fillerPresets)
{
<MudSelectItem T="int?" Value="@fillerPreset.Id">@fillerPreset.Name</MudSelectItem>
}
</MudSelect>
<MudElement HtmlTag="div" Class="mt-3">
<MudTextField T="int"
Label="HLS Segmenter Idle Timeout"
@bind-Value="_ffmpegSettings.HlsSegmenterIdleTimeout"
Validation="@(new Func<int, string>(ValidateHlsSegmenterIdleTimeout))"
Required="true"
RequiredError="HLS Segmenter idle timeout is required!"
Adornment="Adornment.End"
AdornmentText="Seconds"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-3">
<MudTextField T="int"
Label="Work-Ahead HLS Segmenter Limit"
@bind-Value="_ffmpegSettings.WorkAheadSegmenterLimit"
Validation="@(new Func<int, string>(ValidateWorkAheadSegmenterLimit))"
Required="true"
RequiredError="Work-ahead HLS Segmenter limit is required!"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-3">
<MudTextField T="int"
Label="HLS Segmenter Initial Segment Count"
@bind-Value="_ffmpegSettings.InitialSegmentCount"
Validation="@(new Func<int, string>(ValidateInitialSegmentCount))"
Required="true"
RequiredError="HLS Segmenter initial segment count is required!"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-3">
<MudSwitch T="bool"
Label="Use Legacy Transcoder Logic"
Color="Color.Primary"
@bind-Checked="@_ffmpegSettings.UseLegacyTranscoder"/>
</MudElement>
</MudForm>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_success)" OnClick="@(_ => SaveFFmpegSettings())">Save Settings</MudButton>
</MudCardActions>
</MudCard>
<MudCard Class="mr-6 mb-auto" Style="width: 350px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">HDHomeRun Settings</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudForm @bind-IsValid="@_hdhrSuccess">
<MudTextField T="int" Label="Tuner Count" @bind-Value="_tunerCount" Validation="@(new Func<int, string>(ValidateTunerCount))" Required="true" RequiredError="Tuner count is required!"/>
</MudForm>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_hdhrSuccess)" OnClick="@(_ => SaveHDHRSettings())">Save Settings</MudButton>
</MudCardActions>
</MudCard>
<MudCard Class="mr-6" Style="width: 350px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Scanner Settings</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudForm @bind-IsValid="@_scannerSuccess">
<MudTextField T="int"
Label="Library Refresh Interval"
@bind-Value="_libraryRefreshInterval"
Validation="@(new Func<int, string>(ValidateLibraryRefreshInterval))"
Required="true"
RequiredError="Library refresh interval is required!"
Adornment="Adornment.End"
AdornmentText="Hours"/>
</MudForm>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_scannerSuccess)" OnClick="@(_ => SaveScannerSettings())">Save Settings</MudButton>
</MudCardActions>
</MudCard>
<MudCard Style="width: 350px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Playout Settings</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudForm @bind-IsValid="@_playoutSuccess">
<MudTextField T="int"
Label="Days To Build"
@bind-Value="_playoutDaysToBuild"
Validation="@(new Func<int, string>(ValidatePlayoutDaysToBuild))"
Required="true"
RequiredError="Days to build is required!"
Adornment="Adornment.End"
AdornmentText="Days"/>
</MudForm>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_playoutSuccess)" OnClick="@(_ => SavePlayoutSettings())">Save Settings</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private bool _success;
private bool _hdhrSuccess;
private bool _scannerSuccess;
private bool _playoutSuccess;
private List<FFmpegProfileViewModel> _ffmpegProfiles;
private FFmpegSettingsViewModel _ffmpegSettings;
private List<CultureInfo> _availableCultures;
private List<WatermarkViewModel> _watermarks;
private List<FillerPresetViewModel> _fillerPresets;
private int _tunerCount;
private int _libraryRefreshInterval;
private int _playoutDaysToBuild;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await LoadFFmpegProfilesAsync();
_ffmpegSettings = await _mediator.Send(new GetFFmpegSettings(), _cts.Token);
_success = File.Exists(_ffmpegSettings.FFmpegPath) && File.Exists(_ffmpegSettings.FFprobePath);
_availableCultures = await _mediator.Send(new GetAllLanguageCodes(), _cts.Token);
_watermarks = await _mediator.Send(new GetAllWatermarks(), _cts.Token);
_fillerPresets = await _mediator.Send(new GetAllFillerPresets(), _cts.Token)
.Map(list => list.Filter(fp => fp.FillerKind == FillerKind.Fallback).ToList());
_tunerCount = await _mediator.Send(new GetHDHRTunerCount(), _cts.Token);
_hdhrSuccess = string.IsNullOrWhiteSpace(ValidateTunerCount(_tunerCount));
_libraryRefreshInterval = await _mediator.Send(new GetLibraryRefreshInterval(), _cts.Token);
_scannerSuccess = _libraryRefreshInterval > 0;
_playoutDaysToBuild = await _mediator.Send(new GetPlayoutDaysToBuild(), _cts.Token);
_playoutSuccess = _playoutDaysToBuild > 0;
}
private static string ValidatePathExists(string path) => !File.Exists(path) ? "Path does not exist" : null;
private static string ValidateTunerCount(int tunerCount) => tunerCount <= 0 ? "Tuner count must be greater than zero" : null;
private static string ValidateLibraryRefreshInterval(int libraryRefreshInterval) => libraryRefreshInterval <= 0 ? "Library refresh interval must be greater than zero" : null;
private static string ValidatePlayoutDaysToBuild(int daysToBuild) => daysToBuild <= 0 ? "Days to build must be greater than zero" : null;
private static string ValidateHlsSegmenterIdleTimeout(int idleTimeout) => idleTimeout < 30 ? "HLS Segmenter idle timeout must be greater than or equal to 30" : null;
private static string ValidateWorkAheadSegmenterLimit(int limit) => limit < 0 ? "Work-Ahead HLS Segmenter limit must be greater than or equal to 0" : null;
private static string ValidateInitialSegmentCount(int count) => count < 1 ? "HLS Segmenter initial segment count must be greater than or equal to 1" : null;
private async Task LoadFFmpegProfilesAsync() =>
_ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles(), _cts.Token);
private async Task SaveFFmpegSettings()
{
Either<BaseError, Unit> result = await _mediator.Send(new UpdateFFmpegSettings(_ffmpegSettings), _cts.Token);
result.Match(
Left: error =>
{
_snackbar.Add(error.Value, Severity.Error);
_logger.LogError("Unexpected error saving FFmpeg settings: {Error}", error.Value);
},
Right: _ => _snackbar.Add("Successfully saved FFmpeg settings", Severity.Success));
}
private async Task SaveHDHRSettings()
{
Either<BaseError, Unit> result = await _mediator.Send(new UpdateHDHRTunerCount(_tunerCount), _cts.Token);
result.Match(
Left: error =>
{
_snackbar.Add(error.Value, Severity.Error);
_logger.LogError("Unexpected error saving HDHomeRun settings: {Error}", error.Value);
},
Right: _ => _snackbar.Add("Successfully saved HDHomeRun settings", Severity.Success));
}
private async Task SaveScannerSettings()
{
Either<BaseError, Unit> result = await _mediator.Send(new UpdateLibraryRefreshInterval(_libraryRefreshInterval), _cts.Token);
result.Match(
Left: error =>
{
_snackbar.Add(error.Value, Severity.Error);
_logger.LogError("Unexpected error saving scanner settings: {Error}", error.Value);
},
Right: _ => _snackbar.Add("Successfully saved scanner settings", Severity.Success));
}
private async Task SavePlayoutSettings()
{
Either<BaseError, Unit> result = await _mediator.Send(new UpdatePlayoutDaysToBuild(_playoutDaysToBuild), _cts.Token);
result.Match(
Left: error =>
{
_snackbar.Add(error.Value, Severity.Error);
_logger.LogError("Unexpected error saving playout settings: {Error}", error.Value);
},
Right: _ => _snackbar.Add("Successfully saved playout settings", Severity.Success));
}
}