using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using Newtonsoft.Json; using OF_DL.Helpers; using OF_DL.Models; using OF_DL.Models.OfdlApi; using Serilog; using static Newtonsoft.Json.JsonConvert; using WidevineConstants = OF_DL.Widevine.Constants; namespace OF_DL.Services; public class StartupService(IConfigService configService, IAuthService authService) : IStartupService { public async Task ValidateEnvironmentAsync() { StartupResult result = new(); // OS validation OperatingSystem os = Environment.OSVersion; result.OsVersionString = os.VersionString; Log.Debug($"Operating system information: {os.VersionString}"); if (os.Platform == PlatformID.Win32NT && os.Version.Major < 10) { result.IsWindowsVersionValid = false; Log.Error("Windows version prior to 10.x: {0}", os.VersionString); } // FFmpeg detection DetectFfmpeg(result); if (result.FfmpegFound && result.FfmpegPath != null) { // Escape backslashes for Windows if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && result.FfmpegPath.Contains(@":\") && !result.FfmpegPath.Contains(@":\\")) { result.FfmpegPath = result.FfmpegPath.Replace(@"\", @"\\"); configService.CurrentConfig.FFmpegPath = result.FfmpegPath; } // Get FFmpeg version result.FfmpegVersion = await GetFfmpegVersionAsync(result.FfmpegPath); } // Widevine device checks result.ClientIdBlobMissing = !File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, WidevineConstants.DEVICE_NAME, "device_client_id_blob")); result.DevicePrivateKeyMissing = !File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, WidevineConstants.DEVICE_NAME, "device_private_key")); Log.Debug("device_client_id_blob {Status}", result.ClientIdBlobMissing ? "missing" : "found"); Log.Debug("device_private_key {Status}", result.DevicePrivateKeyMissing ? " missing" : "found"); // rules.json validation if (!File.Exists("rules.json")) { return result; } result.RulesJsonExists = true; try { DeserializeObject(await File.ReadAllTextAsync("rules.json")); Log.Debug("Rules.json: "); Log.Debug(SerializeObject(await File.ReadAllTextAsync("rules.json"), Formatting.Indented)); result.RulesJsonValid = true; } catch (Exception e) { result.RulesJsonError = e.Message; Log.Error("rules.json processing failed. {ErrorMessage}", e.Message); } return result; } public async Task CheckVersionAsync() { VersionCheckResult result = new(); #if !DEBUG try { result.LocalVersion = Assembly.GetEntryAssembly()?.GetName().Version; using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); string? latestReleaseTag; try { latestReleaseTag = await VersionHelper.GetLatestReleaseTag(cts.Token); } catch (OperationCanceledException) { result.TimedOut = true; Log.Warning("Version check timed out after 30 seconds"); return result; } if (latestReleaseTag == null) { result.CheckFailed = true; Log.Error("Failed to get the latest release tag."); return result; } result.LatestVersion = new Version(latestReleaseTag.Replace("OFDLV", "")); int? versionComparison = result.LocalVersion?.CompareTo(result.LatestVersion); result.IsUpToDate = versionComparison >= 0; Log.Debug("Detected client running version " + $"{result.LocalVersion?.Major}.{result.LocalVersion?.Minor}.{result.LocalVersion?.Build}"); Log.Debug("Latest release version " + $"{result.LatestVersion.Major}.{result.LatestVersion.Minor}.{result.LatestVersion.Build}"); } catch (Exception e) { result.CheckFailed = true; Log.Error("Error checking latest release on GitHub. {Message}", e.Message); } #else Log.Debug("Running in Debug/Local mode. Version check skipped."); result.IsUpToDate = true; #endif return result; } private void DetectFfmpeg(StartupResult result) { if (!string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath) && ValidateFilePath(configService.CurrentConfig.FFmpegPath)) { result.FfmpegFound = true; result.FfmpegPath = configService.CurrentConfig.FFmpegPath; Log.Debug($"FFMPEG found: {result.FfmpegPath}"); Log.Debug("FFMPEG path set in config.conf"); } else if (!string.IsNullOrEmpty(authService.CurrentAuth?.FfmpegPath) && ValidateFilePath(authService.CurrentAuth.FfmpegPath)) { result.FfmpegFound = true; result.FfmpegPath = authService.CurrentAuth.FfmpegPath; configService.CurrentConfig.FFmpegPath = result.FfmpegPath; Log.Debug($"FFMPEG found: {result.FfmpegPath}"); Log.Debug("FFMPEG path set in auth.json"); } else if (string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath)) { string? ffmpegPath = GetFullPath("ffmpeg") ?? GetFullPath("ffmpeg.exe"); if (ffmpegPath != null) { result.FfmpegFound = true; result.FfmpegPathAutoDetected = true; result.FfmpegPath = ffmpegPath; configService.CurrentConfig.FFmpegPath = ffmpegPath; Log.Debug($"FFMPEG found: {ffmpegPath}"); Log.Debug("FFMPEG path found via PATH or current directory"); } } if (!result.FfmpegFound) { Log.Error($"Cannot locate FFmpeg with path: {configService.CurrentConfig.FFmpegPath}"); } } private static async Task GetFfmpegVersionAsync(string ffmpegPath) { try { ProcessStartInfo processStartInfo = new() { FileName = ffmpegPath, Arguments = "-version", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using Process? process = Process.Start(processStartInfo); if (process != null) { string output = await process.StandardOutput.ReadToEndAsync(); await process.WaitForExitAsync(); Log.Information("FFmpeg version output:\n{Output}", output); string firstLine = output.Split('\n')[0].Trim(); if (firstLine.StartsWith("ffmpeg version")) { int versionStart = "ffmpeg version ".Length; int copyrightIndex = firstLine.IndexOf(" Copyright", StringComparison.Ordinal); return copyrightIndex > versionStart ? firstLine.Substring(versionStart, copyrightIndex - versionStart) : firstLine.Substring(versionStart); } } } catch (Exception ex) { Log.Warning(ex, "Failed to get FFmpeg version"); } return null; } private static bool ValidateFilePath(string path) { char[] invalidChars = Path.GetInvalidPathChars(); if (path.Any(c => invalidChars.Contains(c))) { return false; } return File.Exists(path); } private static string? GetFullPath(string filename) { if (File.Exists(filename)) { return Path.GetFullPath(filename); } string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? ""; return pathEnv.Split(Path.PathSeparator).Select(path => Path.Combine(path, filename)) .FirstOrDefault(File.Exists); } }