From 9c5d86a16140461fda40eed7f52ea6a9c9544d5e Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sat, 23 May 2026 13:06:00 -0500 Subject: [PATCH] feat: show next version in ui (#2910) --- .../Streaming/Commands/GetNextVersion.cs | 3 + .../Commands/GetNextVersionHandler.cs | 38 +++++++++++ .../Commands/NextChannelHandlerBase.cs | 59 +++++++++++++++++ .../Commands/StartFFmpegNextSessionHandler.cs | 63 ++----------------- ErsatzTV/NextVersion.cs | 6 ++ .../RunOnce/PlatformSettingsService.cs | 7 +++ ErsatzTV/Shared/MainLayout.razor | 3 + 7 files changed, 122 insertions(+), 57 deletions(-) create mode 100644 ErsatzTV.Application/Streaming/Commands/GetNextVersion.cs create mode 100644 ErsatzTV.Application/Streaming/Commands/GetNextVersionHandler.cs create mode 100644 ErsatzTV.Application/Streaming/Commands/NextChannelHandlerBase.cs create mode 100644 ErsatzTV/NextVersion.cs diff --git a/ErsatzTV.Application/Streaming/Commands/GetNextVersion.cs b/ErsatzTV.Application/Streaming/Commands/GetNextVersion.cs new file mode 100644 index 000000000..b7e3ac2e7 --- /dev/null +++ b/ErsatzTV.Application/Streaming/Commands/GetNextVersion.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Streaming; + +public record GetNextVersion : IRequest; diff --git a/ErsatzTV.Application/Streaming/Commands/GetNextVersionHandler.cs b/ErsatzTV.Application/Streaming/Commands/GetNextVersionHandler.cs new file mode 100644 index 000000000..cfec198ba --- /dev/null +++ b/ErsatzTV.Application/Streaming/Commands/GetNextVersionHandler.cs @@ -0,0 +1,38 @@ +using System.IO.Abstractions; +using System.Text; +using CliWrap; +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Streaming; + +public class GetNextVersionHandler(IFileSystem fileSystem) + : NextChannelHandlerBase(fileSystem), IRequestHandler +{ + public async Task Handle(GetNextVersion request, CancellationToken cancellationToken) + { + try + { + Validation validation = await ChannelBinaryMustExist(); + foreach (string channelBinary in validation.SuccessToSeq()) + { + var stdOutBuffer = new StringBuilder(); + CommandResult command = await Cli.Wrap(channelBinary) + .WithArguments(["--version"]) + .WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuffer)) + .WithValidation(CommandResultValidation.None) + .ExecuteAsync(cancellationToken); + + if (command.IsSuccess) + { + return stdOutBuffer.ToString().Replace("ersatztv-channel", string.Empty).Trim(); + } + } + } + catch (Exception) + { + // do nothing + } + + return "n/a"; + } +} diff --git a/ErsatzTV.Application/Streaming/Commands/NextChannelHandlerBase.cs b/ErsatzTV.Application/Streaming/Commands/NextChannelHandlerBase.cs new file mode 100644 index 000000000..5f59b1f9e --- /dev/null +++ b/ErsatzTV.Application/Streaming/Commands/NextChannelHandlerBase.cs @@ -0,0 +1,59 @@ +using System.IO.Abstractions; +using System.Runtime.InteropServices; +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Streaming; + +public abstract class NextChannelHandlerBase(IFileSystem fileSystem) +{ + protected Task> ChannelBinaryMustExist() + { + string nextFolder = SystemEnvironment.NextFolder; + if (string.IsNullOrWhiteSpace(nextFolder)) + { + string processFileName = Environment.ProcessPath ?? string.Empty; + string processExecutable = Path.GetFileNameWithoutExtension(processFileName); + nextFolder = Path.GetDirectoryName(processFileName); + if ("dotnet".Equals(processExecutable, StringComparison.OrdinalIgnoreCase)) + { + nextFolder = AppContext.BaseDirectory; + } + } + + string executable = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? "ersatztv-channel.exe" + : "ersatztv-channel"; + + string channelBinary = fileSystem.Path.Combine(ReplaceTilde(nextFolder), executable); + if (!fileSystem.Path.Exists(channelBinary)) + { + return Task.FromResult>( + BaseError.New("ersatztv-channel binary does not exist!")); + } + + return Task.FromResult>(channelBinary); + } + + private string ReplaceTilde(string path) + { + if (!path.StartsWith('~')) + { + return path; + } + + string userFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + switch (path) + { + case "~": + return userFolder; + case not null + when path.Length == 2 && + (path[1] == fileSystem.Path.DirectorySeparatorChar || + path[1] == fileSystem.Path.AltDirectorySeparatorChar): + return userFolder + fileSystem.Path.DirectorySeparatorChar; + default: + return fileSystem.Path.Combine(userFolder, path[2..]); + } + } +} diff --git a/ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs b/ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs index 61264cf44..43c93161b 100644 --- a/ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs +++ b/ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.IO.Abstractions; -using System.Runtime.InteropServices; using System.Threading.Channels; using ErsatzTV.Application.Channels; using ErsatzTV.Application.FFmpegProfiles; @@ -32,8 +31,10 @@ public class StartFFmpegNextSessionHandler( ChannelWriter workerChannel, ILogger logger, ILogger sessionWorkerLogger) - : IRequestHandler> + : NextChannelHandlerBase(fileSystem), IRequestHandler> { + private readonly IFileSystem _fileSystem = fileSystem; + public Task> Handle( StartFFmpegNextSession request, CancellationToken cancellationToken) => @@ -73,7 +74,7 @@ public class StartFFmpegNextSessionHandler( NextSessionWorker worker = new NextSessionWorker( validationResult.ChannelBinary, config, - fileSystem, + _fileSystem, localFileSystem, serviceScopeFactory, sessionWorkerLogger); @@ -108,7 +109,7 @@ public class StartFFmpegNextSessionHandler( SessionMustBeInactive(request) .BindT(_ => FolderMustBeEmpty(request)) .BindT(_ => ChannelBinaryMustExist()) - .BindT(result => ChannelMustExist(request, result, cancellationToken)) + .BindT(channelBinary => ChannelMustExist(request, new ValidationResult(channelBinary, null, null), cancellationToken)) .BindT(result => FFmpegProfileMustExist(result, cancellationToken)); private async Task> SessionMustBeInactive(StartFFmpegNextSession request) @@ -139,35 +140,6 @@ public class StartFFmpegNextSessionHandler( return Task.FromResult>(Unit.Default); } - private Task> ChannelBinaryMustExist() - { - string nextFolder = SystemEnvironment.NextFolder; - if (string.IsNullOrWhiteSpace(nextFolder)) - { - string processFileName = Environment.ProcessPath ?? string.Empty; - string processExecutable = Path.GetFileNameWithoutExtension(processFileName); - nextFolder = Path.GetDirectoryName(processFileName); - if ("dotnet".Equals(processExecutable, StringComparison.OrdinalIgnoreCase)) - { - nextFolder = AppContext.BaseDirectory; - } - } - - string executable = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? "ersatztv-channel.exe" - : "ersatztv-channel"; - - string channelBinary = fileSystem.Path.Combine(ReplaceTilde(nextFolder), executable); - if (!fileSystem.Path.Exists(channelBinary)) - { - return Task.FromResult>( - BaseError.New("ersatztv-channel binary does not exist!")); - } - - return Task.FromResult>( - new ValidationResult(channelBinary, null, null)); - } - private async Task> ChannelMustExist( StartFFmpegNextSession request, ValidationResult result, @@ -201,29 +173,6 @@ public class StartFFmpegNextSessionHandler( return BaseError.New($"FFmpeg profile {result.Channel.FFmpegProfileId} not exist"); } - public string ReplaceTilde(string path) - { - if (!path.StartsWith('~')) - { - return path; - } - - string userFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - - switch (path) - { - case "~": - return userFolder; - case not null - when path.Length == 2 && - (path[1] == fileSystem.Path.DirectorySeparatorChar || - path[1] == fileSystem.Path.AltDirectorySeparatorChar): - return userFolder + fileSystem.Path.DirectorySeparatorChar; - default: - return fileSystem.Path.Combine(userFolder, path[2..]); - } - } - private async Task GetMultiVariantPlaylist(StartFFmpegNextSession request) { var variantPlaylist = @@ -407,7 +356,7 @@ public class StartFFmpegNextSessionHandler( } }; - string playoutFolder = fileSystem.Path.Combine(FileSystemLayout.NextPlayoutsFolder, channel.Number, "current"); + string playoutFolder = _fileSystem.Path.Combine(FileSystemLayout.NextPlayoutsFolder, channel.Number, "current"); return new ChannelConfig { diff --git a/ErsatzTV/NextVersion.cs b/ErsatzTV/NextVersion.cs new file mode 100644 index 000000000..53cfbdbe9 --- /dev/null +++ b/ErsatzTV/NextVersion.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV; + +public class NextVersion +{ + public static string Version { get; set; } +} diff --git a/ErsatzTV/Services/RunOnce/PlatformSettingsService.cs b/ErsatzTV/Services/RunOnce/PlatformSettingsService.cs index 2f5b02956..f7be0fde4 100644 --- a/ErsatzTV/Services/RunOnce/PlatformSettingsService.cs +++ b/ErsatzTV/Services/RunOnce/PlatformSettingsService.cs @@ -1,8 +1,10 @@ using System.Runtime.InteropServices; +using ErsatzTV.Application.Streaming; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Capabilities.Nvidia; using ErsatzTV.FFmpeg.Runtime; +using MediatR; using Microsoft.Extensions.Caching.Memory; namespace ErsatzTV.Services.RunOnce; @@ -58,6 +60,11 @@ public class PlatformSettingsService(IServiceScopeFactory serviceScopeFactory) : memoryCache.Set("ffmpeg.vaapi_displays", displays); } } + + var mediator = scope.ServiceProvider.GetRequiredService(); + NextVersion.Version = await mediator.Send(new GetNextVersion(), stoppingToken); + + Serilog.Log.Logger.Information("ErsatzTV Next version {Version}", NextVersion.Version); } } } diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index b0c2c5e57..c4e280f90 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -250,6 +250,9 @@ { @BuildConfiguration } +
+ ErsatzTV Next Version + @NextVersion.Version