@page "/channels/{Id:int?}" @page "/channels/add" @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 Mode
Continuous On Demand
Is Enabled
Show In EPG
Streaming Mode
MPEG-TS @* MPEG-TS (Legacy) *@ HLS Direct HLS Segmenter HLS Segmenter V2
FFmpeg Profile
@foreach (FFmpegProfileViewModel profile in _ffmpegProfiles) { @profile.Name }
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 readonly CancellationTokenSource _cts = new(); [Parameter] public int? Id { get; set; } private readonly ChannelEditViewModel _model = new(); private readonly ChannelEditViewModelValidator _validator = new(); private MudForm _form; 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() { await LoadFFmpegProfiles(_cts.Token); _availableCultures = await Mediator.Send(new GetAllLanguageCodes(), _cts.Token); await LoadWatermarks(_cts.Token); await LoadFillerPresets(_cts.Token); await LoadMusicVideoCreditsTemplates(_cts.Token); await LoadChannelStreamSelectors(_cts.Token); if (Id.HasValue) { Option maybeChannel = await Mediator.Send(new GetChannelById(Id.Value), _cts.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; if (channelViewModel.Logo.IsExternalUrl) { _model.ExternalLogoUrl = channelViewModel.Logo.Path; } else { _model.Logo = channelViewModel.Logo; } _model.PlayoutMode = channelViewModel.PlayoutMode; _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(), _cts.Token); // TODO: command for new channel IEnumerable channelNumbers = await Mediator.Send(new GetAllChannels(), _cts.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; } } private bool IsEdit => Id.HasValue; private async Task LoadFFmpegProfiles(CancellationToken cancellationToken) => _ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles(), cancellationToken); private async Task LoadWatermarks(CancellationToken cancellationToken) => _watermarks = await Mediator.Send(new GetAllWatermarks(), cancellationToken); private async Task LoadFillerPresets(CancellationToken cancellationToken) => _fillerPresets = await Mediator.Send(new GetAllFillerPresets(), cancellationToken) .Map(list => list.Filter(vm => vm.FillerKind == FillerKind.Fallback).ToList()); private async Task LoadMusicVideoCreditsTemplates(CancellationToken cancellationToken) => _musicVideoCreditsTemplates = await Mediator.Send(new GetMusicVideoCreditTemplates(), cancellationToken); private async Task LoadChannelStreamSelectors(CancellationToken cancellationToken) => _streamSelectors = await Mediator.Send(new GetChannelStreamSelectors(), cancellationToken); private async Task HandleSubmitAsync() { await _form.Validate(); ValidationResult result = await _validator.ValidateAsync(_model, _cts.Token); if (result.IsValid) { Seq errorMessage = IsEdit ? (await Mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() : (await Mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq(); errorMessage.HeadOrNone().Match( error => { Snackbar.Add(error.Value, Severity.Error); Logger.LogError("Unexpected error saving channel: {Error}", error.Value); }, () => NavigationManager.NavigateTo("channels")); } } private async Task UploadLogo(InputFileChangeEventArgs e) { try { Either maybeCacheFileName = await Mediator.Send(new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Logo, e.File.ContentType), _cts.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 (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); } } }