forked from sim0n00ps/OF-DL
309 lines
12 KiB
C#
309 lines
12 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Validates the runtime environment and returns a structured result.
|
|
/// </summary>
|
|
/// <returns>A result describing environment checks and detected tools.</returns>
|
|
public async Task<StartupResult> 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<DynamicRules>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks the current application version against the latest release tag.
|
|
/// </summary>
|
|
/// <returns>A result describing the version check status.</returns>
|
|
public async Task<VersionCheckResult> 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<string?> 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);
|
|
}
|
|
}
|