using System.Collections.Concurrent; using System.IO.Abstractions; using System.Runtime.InteropServices; using CliWrap; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; using Scriban; using Scriban.Runtime; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace ErsatzTV.Infrastructure.FFmpeg; public class MpegTsScriptService( IFileSystem fileSystem, ILocalFileSystem localFileSystem, ITempFilePool tempFilePool, ILogger logger) : IMpegTsScriptService { private static readonly ConcurrentDictionary Scripts = new(); public async Task RefreshScripts() { foreach (string folder in localFileSystem.ListSubdirectories(FileSystemLayout.MpegTsScriptsFolder)) { string definition = Path.Combine(folder, "mpegts.yml"); if (!Scripts.ContainsKey(folder) && fileSystem.File.Exists(definition)) { Option maybeScript = FromYaml(await fileSystem.File.ReadAllTextAsync(definition)); foreach (var script in maybeScript) { script.Id = Path.GetFileName(folder); Scripts[folder] = script; } } } } public List GetScripts() => Scripts.Values.ToList(); public async Task> Execute(MpegTsScript script, Channel channel, string hlsUrl, string ffmpegPath) { string scriptFolder = string.Empty; foreach (KeyValuePair kvp in Scripts.Where(kvp => kvp.Value == script)) { scriptFolder = kvp.Key; } if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { string scriptInput = Path.Combine(scriptFolder, script.WindowsScript); if (File.Exists(scriptInput)) { Option maybeScript = await GetTemplatedScript( scriptInput, hlsUrl, channel.Name, ffmpegPath); foreach (string finalScript in maybeScript) { var fileName = $"{tempFilePool.GetNextTempFile(TempFileCategory.MpegTsScript)}.bat"; await File.WriteAllTextAsync(fileName, finalScript); return Cli.Wrap(fileName); } } } else { string scriptInput = Path.Combine(scriptFolder, script.LinuxScript); if (File.Exists(scriptInput)) { Option maybeScript = await GetTemplatedScript( scriptInput, hlsUrl, channel.Name, ffmpegPath); foreach (string finalScript in maybeScript) { string fileName = tempFilePool.GetNextTempFile(TempFileCategory.MpegTsScript); await File.WriteAllTextAsync(fileName, finalScript); return Cli.Wrap("bash").WithArguments([fileName]); } } } return []; } private async Task> GetTemplatedScript( string fileName, string hlsUrl, string channelName, string ffmpegPath) { string script = await fileSystem.File.ReadAllTextAsync(fileName); try { var data = new Dictionary { ["HlsUrl"] = hlsUrl, ["ChannelName"] = channelName, ["FFmpegPath"] = ffmpegPath }; var scriptObject = new ScriptObject(); scriptObject.Import(data, renamer: member => member.Name); var context = new TemplateContext { MemberRenamer = member => member.Name }; context.PushGlobal(scriptObject); return await Template.Parse(script).RenderAsync(context); } catch (Exception ex) { logger.LogWarning(ex, "Failed to render mpegts script as scriban template"); return Option.None; } } private Option FromYaml(string yaml) { try { IDeserializer deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); return deserializer.Deserialize(yaml); } catch (Exception ex) { logger.LogWarning(ex, "Failed to load mpegts script YAML definition"); return Option.None; } } }