using System.Diagnostics; using System.Reflection; 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 { /// /// Validates the runtime environment and returns a structured result. /// /// A result describing environment checks and detected tools. public async Task ValidateEnvironmentAsync() { StartupResult result = new(); // OS validation OperatingSystem os = Environment.OSVersion; result.OsVersionString = os.VersionString; Log.Debug("Operating system information: {OsVersionString}", os.VersionString); if (EnvironmentHelper.IsRunningOnWindows() && os.Version.Major < 10) { result.IsWindowsVersionValid = false; Log.Error("Windows version prior to 10.x: {0}", os.VersionString); } // FFmpeg detection DetectFfmpeg(result); // FFprobe detection DetectFfprobe(result); if (result is { FfmpegFound: true, FfmpegPath: not null }) { // Escape backslashes for Windows if (EnvironmentHelper.IsRunningOnWindows() && result.FfmpegPath.Contains(@":\") && !result.FfmpegPath.Contains(@":\\")) { result.FfmpegPath = result.FfmpegPath.Replace(@"\", @"\\"); configService.CurrentConfig.FFmpegPath = result.FfmpegPath; } // Get FFmpeg version result.FfmpegVersion = await GetToolVersionAsync(result.FfmpegPath, "ffmpeg"); } if (result is { FfprobeFound: true, FfprobePath: not null }) { // Escape backslashes for Windows if (EnvironmentHelper.IsRunningOnWindows() && result.FfprobePath.Contains(@":\") && !result.FfprobePath.Contains(@":\\")) { result.FfprobePath = result.FfprobePath.Replace(@"\", @"\\"); configService.CurrentConfig.FFprobePath = result.FfprobePath; } // Get FFprobe version result.FfprobeVersion = await GetToolVersionAsync(result.FfprobePath, "ffprobe"); } // 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; } /// /// Checks the current application version against the latest release tag. /// /// A result describing the version check status. 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 await Task.CompletedTask; 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 void DetectFfprobe(StartupResult result) { if (!string.IsNullOrEmpty(configService.CurrentConfig.FFprobePath) && ValidateFilePath(configService.CurrentConfig.FFprobePath)) { result.FfprobeFound = true; result.FfprobePath = configService.CurrentConfig.FFprobePath; Log.Debug($"FFprobe found: {result.FfprobePath}"); Log.Debug("FFprobe path set in config.conf"); } if (!result.FfprobeFound && !string.IsNullOrEmpty(result.FfmpegPath)) { string? ffmpegDirectory = Path.GetDirectoryName(result.FfmpegPath); if (!string.IsNullOrEmpty(ffmpegDirectory)) { string ffprobeFileName = EnvironmentHelper.IsRunningOnWindows() ? "ffprobe.exe" : "ffprobe"; string inferredFfprobePath = Path.Combine(ffmpegDirectory, ffprobeFileName); if (ValidateFilePath(inferredFfprobePath)) { result.FfprobeFound = true; result.FfprobePathAutoDetected = true; result.FfprobePath = inferredFfprobePath; configService.CurrentConfig.FFprobePath = inferredFfprobePath; Log.Debug($"FFprobe found: {inferredFfprobePath}"); Log.Debug("FFprobe path inferred from FFmpeg path"); } } } if (!result.FfprobeFound && string.IsNullOrEmpty(configService.CurrentConfig.FFprobePath)) { string? ffprobePath = GetFullPath("ffprobe") ?? GetFullPath("ffprobe.exe"); if (ffprobePath != null) { result.FfprobeFound = true; result.FfprobePathAutoDetected = true; result.FfprobePath = ffprobePath; configService.CurrentConfig.FFprobePath = ffprobePath; Log.Debug($"FFprobe found: {ffprobePath}"); Log.Debug("FFprobe path found via PATH or current directory"); } } if (!result.FfprobeFound) { Log.Error($"Cannot locate FFprobe with path: {configService.CurrentConfig.FFprobePath}"); } } private static async Task GetToolVersionAsync(string toolPath, string toolName) { try { ProcessStartInfo processStartInfo = new() { FileName = toolPath, 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("{ToolName} version output:\n{Output}", toolName, output); string firstLine = output.Split('\n')[0].Trim(); string expectedPrefix = $"{toolName} version "; if (firstLine.StartsWith(expectedPrefix, StringComparison.OrdinalIgnoreCase)) { int versionStart = expectedPrefix.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 {ToolName} version", toolName); } return null; } private static bool ValidateFilePath(string path) { char[] invalidChars = Path.GetInvalidPathChars(); return !path.Any(c => invalidChars.Contains(c)) && 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); } }