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);
}
}