Browse Source

add playout days to build setting (#316)

pull/317/head
Jason Dove 5 years ago committed by GitHub
parent
commit
a94d831866
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 7
      ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuild.cs
  3. 66
      ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs
  4. 6
      ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuild.cs
  5. 21
      ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuildHandler.cs
  6. 80
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  7. 1
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  8. 26
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  9. 200
      ErsatzTV/Pages/Settings.razor

1
CHANGELOG.md

@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- Add `Multi Collection` to support shuffling multiple collections within a single schedule item
- Collections within a multi collection are optionally grouped together and ordered when scheduling; this can be useful for franchises
- Add `Playout Days To Build` setting to control how many days of playout data/program guide data should be built into the future
### Changed
- Move `Playback Order` from schedule to schedule items

7
ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuild.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest<Either<BaseError, Unit>>;
}

66
ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands
{
public class
UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdatePlayoutDaysToBuildHandler(
IConfigElementRepository configElementRepository,
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_configElementRepository = configElementRepository;
_dbContextFactory = dbContextFactory;
_workerChannel = workerChannel;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdatePlayoutDaysToBuild request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Unit> validation = await Validate(request);
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.DaysToBuild));
}
private async Task<Unit> ApplyUpdate(TvContext dbContext, int daysToBuild)
{
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild);
// build all playouts to proper number of days
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Channel)
.ToListAsync();
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
{
await _workerChannel.WriteAsync(new BuildPlayout(playoutId));
}
return Unit.Default;
}
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
Optional(request.DaysToBuild)
.Filter(days => days > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Days to build must be greater than zero")
.AsTask();
}
}

6
ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuild.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetPlayoutDaysToBuild : IRequest<int>;
}

21
ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuildHandler.cs

@ -0,0 +1,21 @@ @@ -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.Configuration.Queries
{
public class GetPlayoutDaysToBuildHandler : IRequestHandler<GetPlayoutDaysToBuild, int>
{
private readonly IConfigElementRepository _configElementRepository;
public GetPlayoutDaysToBuildHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<int> Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.Map(result => result.IfNone(2));
}
}

80
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -351,9 +351,15 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -351,9 +351,15 @@ namespace ErsatzTV.Core.Tests.Scheduling
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger);
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
@ -433,9 +439,15 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -433,9 +439,15 @@ namespace ErsatzTV.Core.Tests.Scheduling
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger);
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(7);
@ -519,9 +531,15 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -519,9 +531,15 @@ namespace ErsatzTV.Core.Tests.Scheduling
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger);
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(24);
@ -611,9 +629,15 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -611,9 +629,15 @@ namespace ErsatzTV.Core.Tests.Scheduling
}
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger);
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(32);
@ -699,9 +723,15 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -699,9 +723,15 @@ namespace ErsatzTV.Core.Tests.Scheduling
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger);
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
@ -791,9 +821,15 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -791,9 +821,15 @@ namespace ErsatzTV.Core.Tests.Scheduling
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger);
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
@ -887,9 +923,15 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -887,9 +923,15 @@ namespace ErsatzTV.Core.Tests.Scheduling
}
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger);
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5);
@ -976,9 +1018,15 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -976,9 +1018,15 @@ namespace ErsatzTV.Core.Tests.Scheduling
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger);
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5);
@ -1073,9 +1121,15 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1073,9 +1121,15 @@ namespace ErsatzTV.Core.Tests.Scheduling
}
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger);
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5);
@ -1133,10 +1187,16 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1133,10 +1187,16 @@ namespace ErsatzTV.Core.Tests.Scheduling
MediaItems = mediaItems
};
var configRepo = new Mock<IConfigElementRepository>();
var collectionRepo = new FakeMediaCollectionRepository(Map((mediaCollection.Id, mediaItems)));
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(collectionRepo, televisionRepo, artistRepo.Object, _logger);
var builder = new PlayoutBuilder(
configRepo.Object,
collectionRepo,
televisionRepo,
artistRepo.Object,
_logger);
var items = new List<ProgramScheduleItem> { Flood(mediaCollection, playbackOrder) };

1
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -24,5 +24,6 @@ @@ -24,5 +24,6 @@
public static ConfigElementKey PlayoutsDetailPageSize => new("pages.playouts.detail_page_size");
public static ConfigElementKey LogsPageSize => new("pages.logs.page_size");
public static ConfigElementKey LibraryRefreshInterval => new("scanner.library_refresh_interval");
public static ConfigElementKey PlayoutDaysToBuild => new("playout.days_to_build");
}
}

26
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -20,25 +20,29 @@ namespace ErsatzTV.Core.Scheduling @@ -20,25 +20,29 @@ namespace ErsatzTV.Core.Scheduling
private static readonly Random Random = new();
private readonly IArtistRepository _artistRepository;
private readonly ILogger<PlayoutBuilder> _logger;
private readonly IConfigElementRepository _configElementRepository;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public PlayoutBuilder(
IConfigElementRepository configElementRepository,
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
IArtistRepository artistRepository,
ILogger<PlayoutBuilder> logger)
{
_configElementRepository = configElementRepository;
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
_artistRepository = artistRepository;
_logger = logger;
}
public Task<Playout> BuildPlayoutItems(Playout playout, bool rebuild = false)
public async Task<Playout> BuildPlayoutItems(Playout playout, bool rebuild = false)
{
DateTimeOffset now = DateTimeOffset.Now;
return BuildPlayoutItems(playout, now, now.AddDays(2), rebuild);
Option<int> daysToBuild = await _configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild);
return await BuildPlayoutItems(playout, now, now.AddDays(await daysToBuild.IfNoneAsync(2)), rebuild);
}
public async Task<Playout> BuildPlayoutItems(
@ -392,22 +396,26 @@ namespace ErsatzTV.Core.Scheduling @@ -392,22 +396,26 @@ namespace ErsatzTV.Core.Scheduling
// once more to get playout anchor
ProgramScheduleItem nextScheduleItem = sortedScheduleItems[index % sortedScheduleItems.Count];
// build program schedule anchors
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators);
// remove any items outside the desired range
playout.Items.RemoveAll(old => old.FinishOffset < playoutStart || old.StartOffset > playoutFinish);
DateTimeOffset maxStartTime = playout.Items.Max(i => i.FinishOffset);
DateTimeOffset minCurrentTime = maxStartTime < currentTime ? maxStartTime : currentTime;
playout.Anchor = new PlayoutAnchor
{
NextScheduleItem = nextScheduleItem,
NextScheduleItemId = nextScheduleItem.Id,
NextStart = GetStartTimeAfter(nextScheduleItem, currentTime).UtcDateTime,
NextStart = GetStartTimeAfter(nextScheduleItem, minCurrentTime).UtcDateTime,
MultipleRemaining = multipleRemaining.IsSome ? multipleRemaining.ValueUnsafe() : null,
DurationFinish = durationFinish.IsSome ? durationFinish.ValueUnsafe().UtcDateTime : null,
InFlood = inFlood
};
// build program schedule anchors
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators);
// remove any items outside the desired range
playout.Items.RemoveAll(old => old.FinishOffset < playoutStart || old.StartOffset > playoutFinish);
return playout;
}

200
ErsatzTV/Pages/Settings.razor

@ -17,100 +17,126 @@ @@ -17,100 +17,126 @@
@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)
<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="@profile.Id">@profile.Name</MudSelectItem>
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</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)">
<MudSelectItem T="int?" Value="@((int?) null)">(none)</MudSelectItem>
@foreach (WatermarkViewModel watermark in _watermarks)
{
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
}
</MudSelect>
</MudForm>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_success)" OnClick="@(_ => SaveFFmpegSettings())">Save Settings</MudButton>
</MudCardActions>
</MudCard>
<MudCard Class="mr-6" 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 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>
<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)">
<MudSelectItem T="int?" Value="@((int?) null)">(none)</MudSelectItem>
@foreach (WatermarkViewModel watermark in _watermarks)
{
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
}
</MudSelect>
</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 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 int _tunerCount;
private int _libraryRefreshInterval;
private int _playoutDaysToBuild;
protected override async Task OnParametersSetAsync()
{
@ -124,6 +150,8 @@ @@ -124,6 +150,8 @@
_hdhrSuccess = string.IsNullOrWhiteSpace(ValidateTunerCount(_tunerCount));
_libraryRefreshInterval = await _mediator.Send(new GetLibraryRefreshInterval());
_scannerSuccess = _libraryRefreshInterval > 0;
_playoutDaysToBuild = await _mediator.Send(new GetPlayoutDaysToBuild());
_playoutSuccess = _playoutDaysToBuild > 0;
}
private static string ValidatePathExists(string path) => !File.Exists(path) ? "Path does not exist" : null;
@ -131,6 +159,8 @@ @@ -131,6 +159,8 @@
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 async Task LoadFFmpegProfilesAsync() =>
_ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles());
@ -171,4 +201,16 @@ @@ -171,4 +201,16 @@
Right: _ => _snackbar.Add("Successfully saved scanner settings", Severity.Success));
}
private async Task SavePlayoutSettings()
{
Either<BaseError, Unit> result = await _mediator.Send(new UpdatePlayoutDaysToBuild(_playoutDaysToBuild));
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));
}
}
Loading…
Cancel
Save