@page "/channels/{Id:int?}" @page "/channels/add" @using System.Globalization @using ErsatzTV.Application.Artworks @using ErsatzTV.Application.Channels @using ErsatzTV.Application.FFmpegProfiles @using ErsatzTV.Application.Filler @using ErsatzTV.Application.Images @using ErsatzTV.Application.MediaItems @using ErsatzTV.Application.Templates @using ErsatzTV.Application.Watermarks @using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Validators @using FluentValidation.Results @using static Prelude @implements IDisposable @inject NavigationManager NavigationManager @inject ILogger Logger @inject ISnackbar Snackbar @inject IMediator Mediator @(IsEdit ? "Save Channel" : "Add Channel")
Channel
Number
Name
Group
Categories
Transcode Mode
On Demand
Idle Behavior
Stop On Disconnect Keep Running
Playout Source
Generated Mirror
@if (_model.PlayoutSource is ChannelPlayoutSource.Mirror) {
Mirror Source Channel
(none) @foreach (ChannelViewModel channel in _channels .Where(c => c.Id != Id && c.PlayoutSource is ChannelPlayoutSource.Generated) .OrderBy(c => decimal.Parse(c.Number, CultureInfo.InvariantCulture))) { (@channel.Number) - @channel.Name }
Playout Offset
} else {
Playout Mode
Continuous On Demand
}
Is Enabled
Show In EPG
Streaming Mode
MPEG-TS MPEG-TS (Legacy) HLS Direct HLS Segmenter
FFmpeg Profile
@foreach (FFmpegProfileViewModel profile in _ffmpegProfiles) { @profile.Name }
Slug Seconds
(none) 0.5 seconds 1 second 2 seconds 3 seconds 5 seconds
Stream Selector Mode
Default Custom
@if (_model.StreamSelectorMode is ChannelStreamSelectorMode.Default) {
Preferred Audio Language
(none) @foreach (LanguageCodeViewModel culture in _availableCultures) { @culture.EnglishName }
Preferred Audio Title
Preferred Subtitle Language
(none) @foreach (LanguageCodeViewModel culture in _availableCultures) { @culture.EnglishName }
Subtitle Mode
None Forced Default Any
} else {
Stream Selector
(none) @foreach (string selector in _streamSelectors) { @selector }
}
Music Video Credits Mode
None Generate Subtitles
Music Video Credits Template
(none) @foreach (string template in _musicVideoCreditsTemplates) { @template }
Song Video Mode
Default With Progress
Logo
External Logo URL
Logo Preview
@if (!string.IsNullOrWhiteSpace(_model.Logo?.Path) || !string.IsNullOrWhiteSpace(_model.ExternalLogoUrl)) { }
Watermark
(none) @foreach (WatermarkViewModel watermark in _watermarks) { @watermark.Name }
Fallback Filler
(none) @foreach (FillerPresetViewModel fillerPreset in _fillerPresets) { @fillerPreset.Name }
@code { private CancellationTokenSource _cts; [Parameter] public int? Id { get; set; } private readonly ChannelEditViewModel _model = new(); private readonly ChannelEditViewModelValidator _validator = new(); private MudForm _form; private List _channels = []; private List _ffmpegProfiles = []; private List _availableCultures = []; private List _watermarks = []; private List _fillerPresets = []; private List _musicVideoCreditsTemplates = []; private List _streamSelectors = []; public void Dispose() { _cts?.Cancel(); _cts?.Dispose(); } protected override async Task OnParametersSetAsync() { _cts?.Cancel(); _cts?.Dispose(); _cts = new CancellationTokenSource(); var token = _cts.Token; try { var loadingTasks = new List { LoadDataAsync(() => Mediator.Send(new GetAllChannels(), token), r => _channels = r), LoadDataAsync(() => Mediator.Send(new GetAllFFmpegProfiles(), token), r => _ffmpegProfiles = r), LoadDataAsync(() => Mediator.Send(new GetAllLanguageCodes(), token), r => _availableCultures = r), LoadDataAsync(() => Mediator.Send(new GetAllWatermarks(), token), r => _watermarks = r), LoadDataAsync(() => Mediator.Send(new GetAllFillerPresets(), token) .Map(list => list.Filter(vm => vm.FillerKind == FillerKind.Fallback).ToList()), r => _fillerPresets = r), LoadDataAsync(() => Mediator.Send(new GetMusicVideoCreditTemplates(), token), r => _musicVideoCreditsTemplates = r), LoadDataAsync(() => Mediator.Send(new GetChannelStreamSelectors(), token), r => _streamSelectors = r), }; await Task.WhenAll(loadingTasks); if (Id.HasValue) { Option maybeChannel = await Mediator.Send(new GetChannelById(Id.Value), token); maybeChannel.Match( channelViewModel => { _model.Id = channelViewModel.Id; _model.Name = channelViewModel.Name; _model.Group = channelViewModel.Group; _model.Categories = channelViewModel.Categories; _model.Number = channelViewModel.Number; _model.FFmpegProfileId = channelViewModel.FFmpegProfileId; _model.SlugSeconds = channelViewModel.SlugSeconds; if (channelViewModel.Logo.IsExternalUrl) { _model.ExternalLogoUrl = channelViewModel.Logo.Path; } else { _model.Logo = channelViewModel.Logo; } _model.PlayoutSource = channelViewModel.PlayoutSource; _model.PlayoutMode = channelViewModel.PlayoutMode; _model.MirrorSourceChannelId = channelViewModel.MirrorSourceChannelId; _model.PlayoutOffset = channelViewModel.PlayoutOffset; _model.StreamingMode = channelViewModel.StreamingMode; _model.StreamSelectorMode = channelViewModel.StreamSelectorMode; _model.StreamSelector = channelViewModel.StreamSelector; _model.PreferredAudioLanguageCode = channelViewModel.PreferredAudioLanguageCode; _model.PreferredAudioTitle = channelViewModel.PreferredAudioTitle; _model.WatermarkId = channelViewModel.WatermarkId; _model.FallbackFillerId = channelViewModel.FallbackFillerId; _model.PreferredSubtitleLanguageCode = channelViewModel.PreferredSubtitleLanguageCode; _model.SubtitleMode = channelViewModel.SubtitleMode; _model.MusicVideoCreditsMode = channelViewModel.MusicVideoCreditsMode; _model.MusicVideoCreditsTemplate = channelViewModel.MusicVideoCreditsTemplate; _model.SongVideoMode = channelViewModel.SongVideoMode; _model.TranscodeMode = channelViewModel.TranscodeMode; _model.IdleBehavior = channelViewModel.IdleBehavior; _model.IsEnabled = channelViewModel.IsEnabled; _model.ShowInEpg = channelViewModel.ShowInEpg; }, () => NavigationManager.NavigateTo("404")); } else { FFmpegSettingsViewModel ffmpegSettings = await Mediator.Send(new GetFFmpegSettings(), token); // TODO: command for new channel IEnumerable channelNumbers = await Mediator.Send(new GetAllChannels(), token) .Map(list => list.Map(c => int.TryParse(c.Number.Split(".").Head(), out int result) ? result : 0)); int maxNumber = Optional(channelNumbers).Flatten().DefaultIfEmpty(0).Max(); _model.Number = (maxNumber + 1).ToString(); _model.Name = "New Channel"; _model.Group = "ErsatzTV"; _model.FFmpegProfileId = ffmpegSettings.DefaultFFmpegProfileId; _model.StreamingMode = StreamingMode.TransportStreamHybrid; _model.IsEnabled = true; _model.ShowInEpg = true; } } catch (OperationCanceledException) { // do nothing } } private async Task LoadDataAsync(Func> fetch, Action assign) { assign(await fetch()); } private bool IsEdit => Id.HasValue; private async Task HandleSubmitAsync() { await _form.Validate(); if (!_form.IsValid) { return; } try { var token = _cts?.Token ?? CancellationToken.None; ValidationResult result = await _validator.ValidateAsync(_model, token); if (result.IsValid) { Seq errorMessage = IsEdit ? (await Mediator.Send(_model.ToUpdate(), token)).LeftToSeq() : (await Mediator.Send(_model.ToCreate(), token)).LeftToSeq(); errorMessage.HeadOrNone().Match( error => { Snackbar.Add(error.Value, Severity.Error); Logger.LogError("Unexpected error saving channel: {Error}", error.Value); }, () => NavigationManager.NavigateTo("channels")); } } catch (OperationCanceledException) { // do nothing } } private async Task UploadLogo(InputFileChangeEventArgs e) { try { var token = _cts?.Token ?? CancellationToken.None; Either maybeCacheFileName = await Mediator.Send(new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Logo, e.File.ContentType), token); maybeCacheFileName.Match( relativeFileName => { _model.Logo = new ArtworkContentTypeModel($"iptv/logos/{relativeFileName}", e.File.ContentType); _model.ExternalLogoUrl = null; StateHasChanged(); }, error => { Snackbar.Add($"Unexpected error saving channel logo: {error.Value}", Severity.Error); Logger.LogError("Unexpected error saving channel logo: {Error}", error.Value); }); } catch (OperationCanceledException) { // do nothing } catch (IOException) { Snackbar.Add("Channel logo exceeds maximum allowed file size of 10 MB", Severity.Error); Logger.LogError("Channel logo exceeds maximum allowed file size of 10 MB"); } catch (Exception ex) { Snackbar.Add($"Unexpected error saving channel logo: {ex.Message}", Severity.Error); Logger.LogError("Unexpected error saving channel logo: {Error}", ex.Message); } } }