@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
Active Mode
Active Hidden Inactive
Progress Mode
Always On Demand
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.ProgressMode = channelViewModel.ProgressMode; _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.ActiveMode = channelViewModel.ActiveMode; }, () => 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; } } 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); } } }