@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Commands
|
||||
{
|
||||
public record UpdateHDHRTunerCount(int TunerCount) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Commands
|
||||
{
|
||||
public class UpdateHDHRTunerCountHandler : MediatR.IRequestHandler<UpdateHDHRTunerCount, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public UpdateHDHRTunerCountHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
UpdateHDHRTunerCount request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString()))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
||||
Optional(request.TunerCount)
|
||||
.Filter(tc => tc > 0)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||
.AsTask();
|
||||
|
||||
private Task<Unit> Upsert(ConfigElementKey key, string value) =>
|
||||
_configElementRepository.Get(key).Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = value;
|
||||
return _configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement { Key = key.Key, Value = value };
|
||||
return _configElementRepository.Add(ce);
|
||||
}).ToUnit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Queries
|
||||
{
|
||||
public record GetHDHRTunerCount : IRequest<int>;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Queries
|
||||
{
|
||||
public class GetHDHRTunerCountHandler : IRequestHandler<GetHDHRTunerCount, int>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public GetHDHRTunerCountHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public Task<int> Handle(GetHDHRTunerCount request, CancellationToken cancellationToken) =>
|
||||
_configElementRepository.GetValue<int>(ConfigElementKey.HDHRTunerCount)
|
||||
.Map(result => result.IfNone(2));
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ namespace ErsatzTV.Application.Search.Commands
|
||||
bool indexFolderExists = Directory.Exists(FileSystemLayout.SearchIndexFolder);
|
||||
|
||||
await _searchIndex.Initialize(_localFileSystem);
|
||||
|
||||
|
||||
if (!indexFolderExists ||
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.SearchIndexVersion) <
|
||||
_searchIndex.Version)
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports");
|
||||
public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code");
|
||||
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
|
||||
public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_searchIndex.Commit();
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.Channels.Queries;
|
||||
using ErsatzTV.Application.HDHR.Queries;
|
||||
using ErsatzTV.Core.Hdhr;
|
||||
using ErsatzTV.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -20,8 +22,10 @@ namespace ErsatzTV.Controllers
|
||||
new OkObjectResult(new DeviceXml(Request.Scheme, Request.Host.ToString()));
|
||||
|
||||
[HttpGet("discover.json")]
|
||||
public IActionResult Discover() =>
|
||||
new OkObjectResult(new Discover(Request.Scheme, Request.Host.ToString(), 2));
|
||||
[ResponseCache(NoStore = true)]
|
||||
public Task<IActionResult> Discover() =>
|
||||
_mediator.Send(new GetHDHRTunerCount()).Map<int, IActionResult>(
|
||||
tunerCount => new OkObjectResult(new Discover(Request.Scheme, Request.Host.ToString(), tunerCount)));
|
||||
|
||||
[HttpGet("lineup_status.json")]
|
||||
public IActionResult LineupStatus() =>
|
||||
|
||||
@@ -2,55 +2,13 @@
|
||||
@using ErsatzTV.Application.FFmpegProfiles
|
||||
@using ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
@using ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
@using ErsatzTV.Application.MediaItems.Queries
|
||||
@using System.Globalization
|
||||
@using Unit = LanguageExt.Unit
|
||||
@inject IDialogService Dialog
|
||||
@inject IMediator Mediator
|
||||
@inject ILogger<FFmpeg> Logger
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||
<MudCard>
|
||||
<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>
|
||||
</MudForm>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_success)" OnClick="@(_ => SaveSettings())">Save Settings</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
|
||||
<MudTable Hover="true" Items="_ffmpegProfiles" Class="mt-8">
|
||||
<MudTable Hover="true" Items="_ffmpegProfiles">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">FFmpeg Profiles</MudText>
|
||||
<MudToolBarSpacer></MudToolBarSpacer>
|
||||
@@ -114,34 +72,9 @@
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
|
||||
private bool _success;
|
||||
private FFmpegSettingsViewModel _ffmpegSettings;
|
||||
|
||||
private List<FFmpegProfileViewModel> _ffmpegProfiles;
|
||||
private List<CultureInfo> _availableCultures;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
_ffmpegSettings = await Mediator.Send(new GetFFmpegSettings());
|
||||
_success = File.Exists(_ffmpegSettings.FFmpegPath) && File.Exists(_ffmpegSettings.FFprobePath);
|
||||
await LoadFFmpegProfilesAsync();
|
||||
_availableCultures = await Mediator.Send(new GetAllLanguageCodes());
|
||||
}
|
||||
|
||||
private async Task SaveSettings()
|
||||
{
|
||||
Either<BaseError, Unit> result = await Mediator.Send(new UpdateFFmpegSettings(_ffmpegSettings));
|
||||
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 static string ValidatePathExists(string path) => !File.Exists(path) ? "Path does not exist" : null;
|
||||
protected override async Task OnParametersSetAsync() => await LoadFFmpegProfilesAsync();
|
||||
|
||||
private async Task LoadFFmpegProfilesAsync() =>
|
||||
_ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles());
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
@if (!string.IsNullOrWhiteSpace(_movie.Poster))
|
||||
{
|
||||
<img class="mud-elevation-2 mr-6"
|
||||
style="border-radius: 4px; max-height: 440px; flex-shrink: 0"
|
||||
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
|
||||
src="@($"/artwork/posters/{_movie.Poster}")" alt="movie poster"/>
|
||||
}
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
|
||||
@@ -130,7 +130,10 @@
|
||||
|
||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
||||
{
|
||||
List<MediaCardViewModel> GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
|
||||
List<MediaCardViewModel> GetSortedItems()
|
||||
{
|
||||
return _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
|
||||
}
|
||||
|
||||
SelectClicked(GetSortedItems, card, e);
|
||||
}
|
||||
|
||||
@@ -131,7 +131,10 @@
|
||||
|
||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
||||
{
|
||||
List<MediaCardViewModel> GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
|
||||
List<MediaCardViewModel> GetSortedItems()
|
||||
{
|
||||
return _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
|
||||
}
|
||||
|
||||
SelectClicked(GetSortedItems, card, e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
@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 Unit = LanguageExt.Unit
|
||||
@inject IMediator Mediator
|
||||
@inject ISnackbar Snackbar
|
||||
@inject ILogger<Settings> Logger
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="display: flex; flex-direction: row">
|
||||
<MudCard Class="mr-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>
|
||||
</MudForm>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_success)" OnClick="@(_ => SaveFFmpegSettings())">Save Settings</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
<MudCard 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>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private bool _success;
|
||||
private bool _hdhrSuccess;
|
||||
private List<FFmpegProfileViewModel> _ffmpegProfiles;
|
||||
private FFmpegSettingsViewModel _ffmpegSettings;
|
||||
private List<CultureInfo> _availableCultures;
|
||||
private int _tunerCount;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await LoadFFmpegProfilesAsync();
|
||||
|
||||
_ffmpegSettings = await Mediator.Send(new GetFFmpegSettings());
|
||||
_success = File.Exists(_ffmpegSettings.FFmpegPath) && File.Exists(_ffmpegSettings.FFprobePath);
|
||||
_availableCultures = await Mediator.Send(new GetAllLanguageCodes());
|
||||
_tunerCount = await Mediator.Send(new GetHDHRTunerCount());
|
||||
_hdhrSuccess = string.IsNullOrWhiteSpace(ValidateTunerCount(_tunerCount));
|
||||
}
|
||||
|
||||
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 async Task LoadFFmpegProfilesAsync() =>
|
||||
_ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles());
|
||||
|
||||
private async Task SaveFFmpegSettings()
|
||||
{
|
||||
Either<BaseError, Unit> result = await Mediator.Send(new UpdateFFmpegSettings(_ffmpegSettings));
|
||||
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));
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
@if (!string.IsNullOrWhiteSpace(_season.Poster))
|
||||
{
|
||||
<img class="mud-elevation-2 mr-6"
|
||||
style="border-radius: 4px; max-height: 440px; flex-shrink: 0"
|
||||
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
|
||||
src="@($"/artwork/posters/{_season.Poster}")" alt="show poster"/>
|
||||
}
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
@if (!string.IsNullOrWhiteSpace(_show.Poster))
|
||||
{
|
||||
<img class="mud-elevation-2 mr-6"
|
||||
style="border-radius: 4px; max-height: 440px; flex-shrink: 0"
|
||||
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
|
||||
src="@($"/artwork/posters/{_show.Poster}")" alt="show poster"/>
|
||||
}
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
|
||||
@@ -130,7 +130,10 @@
|
||||
|
||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
||||
{
|
||||
List<MediaCardViewModel> GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
|
||||
List<MediaCardViewModel> GetSortedItems()
|
||||
{
|
||||
return _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
|
||||
}
|
||||
|
||||
SelectClicked(GetSortedItems, card, e);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@using ErsatzTV.Application.MediaCards
|
||||
@using LanguageExt.UnsafeValueAccess
|
||||
@using LanguageExt.UnsafeValueAccess
|
||||
@typeparam TCard
|
||||
|
||||
@{ var letters = new System.Collections.Generic.HashSet<char>(); }
|
||||
@@ -25,4 +24,4 @@
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
<MudDrawer Open="true" Elevation="2" ClipMode="DrawerClipMode.Always">
|
||||
<MudNavMenu>
|
||||
<MudNavLink Href="/channels">Channels</MudNavLink>
|
||||
<MudNavLink Href="/ffmpeg">FFmpeg</MudNavLink>
|
||||
<MudNavLink Href="/ffmpeg">FFmpeg Profiles</MudNavLink>
|
||||
<MudNavGroup Title="Media Sources" Expanded="true">
|
||||
<MudNavLink Href="/media/plex">Plex</MudNavLink>
|
||||
</MudNavGroup>
|
||||
@@ -52,6 +52,7 @@
|
||||
</MudNavGroup>
|
||||
<MudNavLink Href="/schedules">Schedules</MudNavLink>
|
||||
<MudNavLink Href="/playouts">Playouts</MudNavLink>
|
||||
<MudNavLink Href="/settings">Settings</MudNavLink>
|
||||
<MudNavLink Href="/system/logs">Logs</MudNavLink>
|
||||
<MudDivider Class="my-6" DividerType="DividerType.Middle"/>
|
||||
<MudContainer Style="text-align: right" Class="mr-6">
|
||||
|
||||
Reference in New Issue
Block a user