using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; using OF_DL.CLI; using OF_DL.Models; using OF_DL.Enumerations; using OF_DL.Models.Config; using OF_DL.Models.Downloads; using OF_DL.Models.Entities.Users; using OF_DL.Services; using Serilog; using Spectre.Console; namespace OF_DL; public class Program(IServiceProvider serviceProvider) { private async Task LoadAuthFromBrowser() { IAuthService authService = serviceProvider.GetRequiredService(); bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; // Show the initial message AnsiConsole.MarkupLine("[yellow]Downloading dependencies. Please wait ...[/]"); // Show instructions based on the environment await Task.Delay(5000); if (runningInDocker) { AnsiConsole.MarkupLine( "[yellow]In your web browser, navigate to the port forwarded from your docker container.[/]"); AnsiConsole.MarkupLine( "[yellow]For instance, if your docker run command included \"-p 8080:8080\", open your web browser to \"http://localhost:8080\".[/]"); AnsiConsole.MarkupLine( "[yellow]Once on that webpage, please use it to log in to your OF account. Do not navigate away from the page.[/]"); } else { AnsiConsole.MarkupLine( "[yellow]In the new window that has opened, please log in to your OF account. Do not close the window or tab. Do not navigate away from the page.[/]\n"); AnsiConsole.MarkupLine( "[yellow]If you use this method or encounter other issues while logging in, use one of the legacy authentication methods documented here:[/]"); AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); } // Load auth from browser using the service bool success = await authService.LoadFromBrowserAsync(); if (!success || authService.CurrentAuth == null) { AnsiConsole.MarkupLine( "\n[red]Authentication failed. Be sure to log into to OF using the new window that opened automatically.[/]"); AnsiConsole.MarkupLine( "[red]The window will close automatically when the authentication process is finished.[/]"); AnsiConsole.MarkupLine( "[red]If the problem persists, you may want to try using a legacy authentication method documented here:[/]\n"); AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); Log.Error("auth invalid after attempt to get auth from browser"); Environment.Exit(2); } await authService.SaveToFileAsync(); } public static async Task Main(string[] args) { AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red)); AnsiConsole.Markup("Documentation: [link]https://docs.ofdl.tools/[/]\n"); AnsiConsole.Markup("Discord server: [link]https://discord.com/invite/6bUW8EJ53j[/]\n\n"); ServiceCollection services = await ConfigureServices(args); ServiceProvider serviceProvider = services.BuildServiceProvider(); // Get the Program instance and run Program program = serviceProvider.GetRequiredService(); await program.RunAsync(); } private static async Task ConfigureServices(string[] args) { // Set up dependency injection with LoggingService and ConfigService ServiceCollection services = new(); services.AddSingleton(); services.AddSingleton(); ServiceProvider tempServiceProvider = services.BuildServiceProvider(); ILoggingService loggingService = tempServiceProvider.GetRequiredService(); IConfigService configService = tempServiceProvider.GetRequiredService(); if (!await configService.LoadConfigurationAsync(args)) { AnsiConsole.MarkupLine("\n[red]config.conf is not valid, check your syntax![/]\n"); if (!configService.IsCliNonInteractive) { AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); Console.ReadKey(); } Environment.Exit(3); } AnsiConsole.Markup("[green]config.conf located successfully!\n[/]"); // Set up full dependency injection with loaded config services = []; services.AddSingleton(loggingService); services.AddSingleton(configService); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); return services; } private async Task RunAsync() { IConfigService configService = serviceProvider.GetRequiredService(); IAuthService authService = serviceProvider.GetRequiredService(); IStartupService startupService = serviceProvider.GetRequiredService(); IDownloadOrchestrationService orchestrationService = serviceProvider.GetRequiredService(); try { // Version check VersionCheckResult versionResult = await startupService.CheckVersionAsync(); DisplayVersionResult(versionResult); // Environment validation StartupResult startupResult = await startupService.ValidateEnvironmentAsync(); DisplayStartupResult(startupResult); if (!startupResult.IsWindowsVersionValid) { Console.Write( "This appears to be running on an older version of Windows which is not supported.\n\n"); Console.Write( "OF-DL requires Windows 10 or higher when being run on Windows. Your reported version is: {0}\n\n", startupResult.OsVersionString); if (!configService.CurrentConfig.NonInteractiveMode) { Console.Write("Press any key to continue.\n"); Console.ReadKey(); } Environment.Exit(1); } if (!startupResult.FfmpegFound) { if (!configService.CurrentConfig.NonInteractiveMode) { AnsiConsole.Markup( "[red]Cannot locate FFmpeg; please modify config.conf with the correct path. Press any key to exit.[/]"); Console.ReadKey(); } else { AnsiConsole.Markup( "[red]Cannot locate FFmpeg; please modify config.conf with the correct path.[/]"); } Environment.Exit(4); } if (!startupResult.FfprobeFound) { if (!configService.CurrentConfig.NonInteractiveMode) { AnsiConsole.Markup( "[red]Cannot locate FFprobe; please modify config.conf with the correct path. Press any key to exit.[/]"); Console.ReadKey(); } else { AnsiConsole.Markup( "[red]Cannot locate FFprobe; please modify config.conf with the correct path.[/]"); } Environment.Exit(4); } // Auth flow await HandleAuthFlow(authService, configService); // Validate cookie string authService.ValidateCookieString(); // rules.json validation DisplayRulesJsonResult(startupResult, configService); // NonInteractiveMode if (configService.CurrentConfig.NonInteractiveMode) { configService.CurrentConfig.NonInteractiveMode = true; Log.Debug("NonInteractiveMode = true"); } // Validate auth via API User? validate = await authService.ValidateAuthAsync(); if (validate == null || (validate.Name == null && validate.Username == null)) { Log.Error("Auth failed"); authService.CurrentAuth = null; if (!configService.CurrentConfig.DisableBrowserAuth) { if (File.Exists("auth.json")) { File.Delete("auth.json"); } } if (!configService.CurrentConfig.NonInteractiveMode && !configService.CurrentConfig.DisableBrowserAuth) { await LoadAuthFromBrowser(); } if (authService.CurrentAuth == null) { AnsiConsole.MarkupLine( "\n[red]Auth failed. Please try again or use other authentication methods detailed here:[/]\n"); AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth[/]\n"); if (!configService.CurrentConfig.NonInteractiveMode) { Console.WriteLine("\nPress any key to exit."); Console.ReadKey(); } Environment.Exit(2); } } AnsiConsole.Markup( $"[green]Logged In successfully as {(!string.IsNullOrEmpty(validate?.Name) ? validate.Name : "Unknown Name")} {(!string.IsNullOrEmpty(validate?.Username) ? validate.Username : "Unknown Username")}\n[/]"); // Main download loop await DownloadAllData(orchestrationService, configService, startupResult); } catch (Exception ex) { Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); } if (!configService.CurrentConfig.NonInteractiveMode) { Console.WriteLine("\nPress any key to exit."); Console.ReadKey(); } Environment.Exit(5); } } private async Task DownloadAllData( IDownloadOrchestrationService orchestrationService, IConfigService configService, StartupResult startupResult) { Config config = configService.CurrentConfig; SpectreDownloadEventHandler eventHandler = new(); Log.Debug("Calling DownloadAllData"); do { DateTime startTime = DateTime.Now; UserListResult userListResult = await orchestrationService.GetAvailableUsersAsync(); Dictionary users = userListResult.Users; Dictionary lists = userListResult.Lists; if (userListResult.IgnoredListError != null) { AnsiConsole.Markup($"[red]{Markup.Escape(userListResult.IgnoredListError)}\n[/]"); } KeyValuePair> hasSelectedUsersKVP; if (config.NonInteractiveMode && config.NonInteractiveModePurchasedTab) { hasSelectedUsersKVP = new KeyValuePair>(true, new Dictionary { { "PurchasedTab", 0 } }); } else if (config.NonInteractiveMode && string.IsNullOrEmpty(config.NonInteractiveModeListName)) { hasSelectedUsersKVP = new KeyValuePair>(true, users); } else if (config.NonInteractiveMode && !string.IsNullOrEmpty(config.NonInteractiveModeListName)) { Dictionary selectedUsers = await orchestrationService.GetUsersForListAsync(config.NonInteractiveModeListName, users, lists); hasSelectedUsersKVP = new KeyValuePair>(true, selectedUsers); } else { (bool IsExit, Dictionary? selectedUsers) userSelectionResult = await HandleUserSelection(users, lists); config = configService.CurrentConfig; hasSelectedUsersKVP = new KeyValuePair>(userSelectionResult.IsExit, userSelectionResult.selectedUsers ?? []); } if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value.ContainsKey("SinglePost")) { await HandleSinglePostDownload(orchestrationService, users, startupResult, eventHandler); } else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value.ContainsKey("PurchasedTab")) { await orchestrationService.DownloadPurchasedTabAsync(users, startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler); DateTime endTime = DateTime.Now; eventHandler.OnScrapeComplete(endTime - startTime); } else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value.ContainsKey("SingleMessage")) { await HandleSingleMessageDownload(orchestrationService, users, startupResult, eventHandler); } else if (hasSelectedUsersKVP.Key && !hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) { foreach (KeyValuePair user in hasSelectedUsersKVP.Value) { string path = orchestrationService.ResolveDownloadPath(user.Key); Log.Debug($"Download path: {path}"); await orchestrationService.DownloadCreatorContentAsync( user.Key, user.Value, path, users, startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler); } DateTime endTime = DateTime.Now; eventHandler.OnScrapeComplete(endTime - startTime); } else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) { // Config was changed, loop will re-read } else { break; } } while (!config.NonInteractiveMode); } private async Task HandleSinglePostDownload( IDownloadOrchestrationService orchestrationService, Dictionary users, StartupResult startupResult, IDownloadEventHandler eventHandler) { AnsiConsole.Markup( "[red]To find an individual post URL, click on the ... at the top right corner of the post and select 'Copy link to post'.\n\nTo return to the main menu, enter 'back' or 'exit' when prompted for the URL.\n\n[/]"); string postUrl = AnsiConsole.Prompt( new TextPrompt("[red]Please enter a post URL: [/]") .ValidationErrorMessage("[red]Please enter a valid post URL[/]") .Validate(url => { Log.Debug($"Single Post URL: {url}"); Regex regex = new("https://onlyfans\\.com/[0-9]+/[A-Za-z0-9]+", RegexOptions.IgnoreCase); if (regex.IsMatch(url)) { return ValidationResult.Success(); } if (url == "" || url == "exit" || url == "back") { return ValidationResult.Success(); } Log.Error("Post URL invalid"); return ValidationResult.Error("[red]Please enter a valid post URL[/]"); })); if (postUrl != "" && postUrl != "exit" && postUrl != "back") { long postId = Convert.ToInt64(postUrl.Split("/")[3]); string username = postUrl.Split("/")[4]; Log.Debug($"Single Post ID: {postId}"); Log.Debug($"Single Post Creator: {username}"); if (users.ContainsKey(username)) { string path = orchestrationService.ResolveDownloadPath(username); Log.Debug($"Download path: {path}"); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); AnsiConsole.Markup($"[red]Created folder for {Markup.Escape(username)}\n[/]"); Log.Debug($"Created folder for {username}"); } else { AnsiConsole.Markup($"[red]Folder for {Markup.Escape(username)} already created\n[/]"); } IDbService dbService = serviceProvider.GetRequiredService(); await dbService.CreateDb(path); await orchestrationService.DownloadSinglePostAsync(username, postId, path, users, startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler); } } } private async Task HandleSingleMessageDownload( IDownloadOrchestrationService orchestrationService, Dictionary users, StartupResult startupResult, IDownloadEventHandler eventHandler) { AnsiConsole.Markup( "[red]To find an individual message URL, note that you can only do so for PPV messages that you have unlocked. Go the main OnlyFans timeline, click on the Purchased tab, find the relevant message, click on the ... at the top right corner of the message, and select 'Copy link to message'. For all other messages, you cannot scrape them individually, you must scrape all messages from that creator.\n\nTo return to the main menu, enter 'back' or 'exit' when prompted for the URL.\n\n[/]"); string messageUrl = AnsiConsole.Prompt( new TextPrompt("[red]Please enter a message URL: [/]") .ValidationErrorMessage("[red]Please enter a valid message URL[/]") .Validate(url => { Log.Debug($"Single Paid Message URL: {url}"); Regex regex = new("https://onlyfans\\.com/my/chats/chat/[0-9]+/\\?firstId=[0-9]+$", RegexOptions.IgnoreCase); if (regex.IsMatch(url)) { return ValidationResult.Success(); } if (url == "" || url == "back" || url == "exit") { return ValidationResult.Success(); } Log.Error("Message URL invalid"); return ValidationResult.Error("[red]Please enter a valid message URL[/]"); })); if (messageUrl != "" && messageUrl != "exit" && messageUrl != "back") { long messageId = Convert.ToInt64(messageUrl.Split("?firstId=")[1]); long userId = Convert.ToInt64(messageUrl.Split("/")[6]); Log.Debug($"Message ID: {messageId}"); Log.Debug($"User ID: {userId}"); string? username = await orchestrationService.ResolveUsernameAsync(userId); Log.Debug("Content creator: {Username}", username); if (username == null) { Log.Error("Could not resolve username for user ID: {userId}", userId); AnsiConsole.MarkupLine("[red]Could not resolve username for user ID[/]"); return; } string path = orchestrationService.ResolveDownloadPath(username); Log.Debug($"Download path: {path}"); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); AnsiConsole.Markup($"[red]Created folder for {Markup.Escape(username)}\n[/]"); Log.Debug($"Created folder for {username}"); } else { AnsiConsole.Markup($"[red]Folder for {Markup.Escape(username)} already created\n[/]"); Log.Debug($"Folder for {username} already created"); } IDbService dbService = serviceProvider.GetRequiredService(); await dbService.CreateDb(path); await orchestrationService.DownloadSinglePaidMessageAsync(username, messageId, path, users, startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler); } } public async Task<(bool IsExit, Dictionary? selectedUsers)> HandleUserSelection( Dictionary users, Dictionary lists) { IConfigService configService = serviceProvider.GetRequiredService(); IAuthService authService = serviceProvider.GetRequiredService(); IApiService apiService = serviceProvider.GetRequiredService(); ILoggingService loggingService = serviceProvider.GetRequiredService(); bool hasSelectedUsers = false; Dictionary selectedUsers = new(); Config currentConfig = configService.CurrentConfig; while (!hasSelectedUsers) { List mainMenuOptions = GetMainMenuOptions(users, lists); string mainMenuSelection = AnsiConsole.Prompt( new SelectionPrompt() .Title( "[red]Select Accounts to Scrape | Select All = All Accounts | List = Download content from users on List | Custom = Specific Account(s)[/]") .AddChoices(mainMenuOptions) ); switch (mainMenuSelection) { case "[red]Select All[/]": selectedUsers = users; hasSelectedUsers = true; break; case "[red]List[/]": while (true) { MultiSelectionPrompt listSelectionPrompt = new(); listSelectionPrompt.Title = "[red]Select List[/]"; listSelectionPrompt.PageSize = 10; listSelectionPrompt.AddChoice("[red]Go Back[/]"); foreach (string key in lists.Keys.Select(k => $"[red]{k}[/]").ToList()) { listSelectionPrompt.AddChoice(key); } List listSelection = AnsiConsole.Prompt(listSelectionPrompt); if (listSelection.Contains("[red]Go Back[/]")) { break; } hasSelectedUsers = true; List listUsernames = new(); foreach (string item in listSelection) { long listId = lists[item.Replace("[red]", "").Replace("[/]", "")]; List usernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; foreach (string user in usernames) { listUsernames.Add(user); } } selectedUsers = users.Where(x => listUsernames.Contains($"{x.Key}")) .ToDictionary(x => x.Key, x => x.Value); AnsiConsole.Markup(string.Format("[red]Downloading from List(s): {0}[/]", string.Join(", ", listSelection))); break; } break; case "[red]Custom[/]": while (true) { MultiSelectionPrompt selectedNamesPrompt = new(); selectedNamesPrompt.MoreChoicesText("[grey](Move up and down to reveal more choices)[/]"); selectedNamesPrompt.InstructionsText( "[grey](Press to select, to accept)[/]\n[grey](Press A-Z to easily navigate the list)[/]"); selectedNamesPrompt.Title("[red]Select users[/]"); selectedNamesPrompt.PageSize(10); selectedNamesPrompt.AddChoice("[red]Go Back[/]"); foreach (string key in users.Keys.OrderBy(k => k).Select(k => $"[red]{k}[/]").ToList()) { selectedNamesPrompt.AddChoice(key); } List userSelection = AnsiConsole.Prompt(selectedNamesPrompt); if (userSelection.Contains("[red]Go Back[/]")) { break; } hasSelectedUsers = true; selectedUsers = users.Where(x => userSelection.Contains($"[red]{x.Key}[/]")) .ToDictionary(x => x.Key, x => x.Value); break; } break; case "[red]Download Single Post[/]": return (true, new Dictionary { { "SinglePost", 0 } }); case "[red]Download Single Paid Message[/]": return (true, new Dictionary { { "SingleMessage", 0 } }); case "[red]Download Purchased Tab[/]": return (true, new Dictionary { { "PurchasedTab", 0 } }); case "[red]Edit config.conf[/]": while (true) { List<(string Name, bool Value)> toggleableProps = configService.GetToggleableProperties(); List<(string choice, bool isSelected)> choices = new() { ("[red]Go Back[/]", false) }; foreach ((string Name, bool Value) prop in toggleableProps) { choices.Add(($"[red]{prop.Name}[/]", prop.Value)); } MultiSelectionPrompt multiSelectionPrompt = new MultiSelectionPrompt() .Title("[red]Edit config.conf[/]") .PageSize(25); foreach ((string choice, bool isSelected) choice in choices) { multiSelectionPrompt.AddChoices(choice.choice, selectionItem => { if (choice.isSelected) { selectionItem.Select(); } }); } List configOptions = AnsiConsole.Prompt(multiSelectionPrompt); if (configOptions.Contains("[red]Go Back[/]")) { break; } // Extract plain names from selections List selectedNames = configOptions .Select(o => o.Replace("[red]", "").Replace("[/]", "")) .ToList(); bool configChanged = configService.ApplyToggleableSelections(selectedNames); await configService.SaveConfigurationAsync(); currentConfig = configService.CurrentConfig; if (configChanged) { return (true, new Dictionary { { "ConfigChanged", 0 } }); } break; } break; case "[red]Change logging level[/]": while (true) { List<(string choice, bool isSelected)> choices = [("[red]Go Back[/]", false)]; foreach (string name in typeof(LoggingLevel).GetEnumNames()) { string itemLabel = $"[red]{name}[/]"; choices.Add(new ValueTuple(itemLabel, name == loggingService.GetCurrentLoggingLevel().ToString())); } SelectionPrompt selectionPrompt = new SelectionPrompt() .Title("[red]Select logging level[/]") .PageSize(25); foreach ((string choice, bool isSelected) choice in choices) { selectionPrompt.AddChoice(choice.choice); } string levelOption = AnsiConsole.Prompt(selectionPrompt); if (levelOption.Contains("[red]Go Back[/]")) { break; } levelOption = levelOption.Replace("[red]", "").Replace("[/]", ""); LoggingLevel newLogLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), levelOption, true); Log.Debug($"Logging level changed to: {levelOption}"); Config newConfig = currentConfig; newConfig.LoggingLevel = newLogLevel; currentConfig = newConfig; configService.UpdateConfig(newConfig); await configService.SaveConfigurationAsync(); break; } break; case "[red]Logout and Exit[/]": authService.Logout(); return (false, null); case "[red]Exit[/]": return (false, null); } } return (true, selectedUsers); } public static List GetMainMenuOptions(Dictionary users, Dictionary lists) { if (lists.Count > 0) { return new List { "[red]Select All[/]", "[red]List[/]", "[red]Custom[/]", "[red]Download Single Post[/]", "[red]Download Single Paid Message[/]", "[red]Download Purchased Tab[/]", "[red]Edit config.conf[/]", "[red]Change logging level[/]", "[red]Logout and Exit[/]", "[red]Exit[/]" }; } return new List { "[red]Select All[/]", "[red]Custom[/]", "[red]Download Single Post[/]", "[red]Download Single Paid Message[/]", "[red]Download Purchased Tab[/]", "[red]Edit config.conf[/]", "[red]Change logging level[/]", "[red]Logout and Exit[/]", "[red]Exit[/]" }; } private async Task HandleAuthFlow(IAuthService authService, IConfigService configService) { if (await authService.LoadFromFileAsync()) { AnsiConsole.Markup("[green]auth.json located successfully!\n[/]"); } else if (File.Exists("auth.json")) { Log.Information("Auth file found but could not be deserialized"); if (!configService.CurrentConfig.DisableBrowserAuth) { Log.Debug("Deleting auth.json"); File.Delete("auth.json"); } if (configService.CurrentConfig.NonInteractiveMode) { AnsiConsole.MarkupLine( "\n[red]auth.json has invalid JSON syntax. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n"); AnsiConsole.MarkupLine( "[red]You may also want to try using the browser extension which is documented here:[/]\n"); AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); Environment.Exit(2); } if (!configService.CurrentConfig.DisableBrowserAuth) { await LoadAuthFromBrowser(); } else { ShowAuthMissingError(configService.CurrentConfig.NonInteractiveMode); } } else { if (configService.CurrentConfig.NonInteractiveMode) { ShowAuthMissingError(configService.CurrentConfig.NonInteractiveMode); } else if (!configService.CurrentConfig.DisableBrowserAuth) { await LoadAuthFromBrowser(); } else { ShowAuthMissingError(configService.CurrentConfig.NonInteractiveMode); } } } private static void ShowAuthMissingError(bool nonInteractiveMode) { AnsiConsole.MarkupLine( "\n[red]auth.json is missing. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n"); AnsiConsole.MarkupLine( "[red]You may also want to try using the browser extension which is documented here:[/]\n"); AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); if (!nonInteractiveMode) { AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); Console.ReadKey(); } Environment.Exit(2); } private static void DisplayVersionResult(VersionCheckResult result) { if (result.TimedOut) { AnsiConsole.Markup("[yellow]Version check timed out after 30 seconds.\n[/]"); return; } if (result.CheckFailed) { AnsiConsole.Markup("[yellow]Failed to verify that OF-DL is up-to-date.\n[/]"); return; } if (result.LocalVersion == null || result.LatestVersion == null) { // Debug mode or no version info AnsiConsole.Markup("[yellow]Running in Debug/Local mode. Version check skipped.\n[/]"); return; } if (result.IsUpToDate) { AnsiConsole.Markup("[green]You are running OF-DL version " + $"{result.LocalVersion.Major}.{result.LocalVersion.Minor}.{result.LocalVersion.Build}\n[/]"); AnsiConsole.Markup("[green]Latest Release version: " + $"{result.LatestVersion.Major}.{result.LatestVersion.Minor}.{result.LatestVersion.Build}\n[/]"); } else { AnsiConsole.Markup("[red]You are running OF-DL version " + $"{result.LocalVersion.Major}.{result.LocalVersion.Minor}.{result.LocalVersion.Build}\n[/]"); AnsiConsole.Markup("[red]Please update to the current release, " + $"{result.LatestVersion.Major}.{result.LatestVersion.Minor}.{result.LatestVersion.Build}: [link=https://git.ofdl.tools/sim0n00ps/OF-DL/releases]https://git.ofdl.tools/sim0n00ps/OF-DL/releases[/]\n[/]"); } } private static void DisplayStartupResult(StartupResult result) { // OS if (result.IsWindowsVersionValid && result.OsVersionString != null && Environment.OSVersion.Platform == PlatformID.Win32NT) { AnsiConsole.Markup("[green]Valid version of Windows found.\n[/]"); } // FFmpeg if (result.FfmpegFound) { AnsiConsole.Markup( result is { FfmpegPathAutoDetected: true, FfmpegPath: not null } ? $"[green]FFmpeg located successfully. Path auto-detected: {Markup.Escape(result.FfmpegPath)}\n[/]" : "[green]FFmpeg located successfully\n[/]"); AnsiConsole.Markup(result.FfmpegVersion != null ? $"[green]ffmpeg version detected as {Markup.Escape(result.FfmpegVersion)}[/]\n" : "[yellow]ffmpeg version could not be parsed[/]\n"); } // FFprobe if (result.FfprobeFound) { AnsiConsole.Markup( result is { FfprobePathAutoDetected: true, FfprobePath: not null } ? $"[green]FFprobe located successfully. Path auto-detected: {Markup.Escape(result.FfprobePath)}\n[/]" : "[green]FFprobe located successfully\n[/]"); AnsiConsole.Markup(result.FfprobeVersion != null ? $"[green]FFprobe version detected as {Markup.Escape(result.FfprobeVersion)}[/]\n" : "[yellow]FFprobe version could not be parsed[/]\n"); } // Widevine if (!result.ClientIdBlobMissing) { AnsiConsole.Markup("[green]device_client_id_blob located successfully![/]\n"); } if (!result.DevicePrivateKeyMissing) { AnsiConsole.Markup("[green]device_private_key located successfully![/]\n"); } if (result.ClientIdBlobMissing || result.DevicePrivateKeyMissing) { AnsiConsole.Markup( "[yellow]device_client_id_blob and/or device_private_key missing, https://ofdl.tools/ or https://cdrm-project.com/ will be used instead for DRM protected videos\n[/]"); } } private static void DisplayRulesJsonResult(StartupResult result, IConfigService configService) { if (result.RulesJsonExists) { if (result.RulesJsonValid) { AnsiConsole.Markup("[green]rules.json located successfully!\n[/]"); } else { AnsiConsole.MarkupLine("\n[red]rules.json is not valid, check your JSON syntax![/]\n"); AnsiConsole.MarkupLine("[red]Please ensure you are using the latest version of the software.[/]\n"); Log.Error("rules.json processing failed: {Error}", result.RulesJsonError); if (!configService.CurrentConfig.NonInteractiveMode) { AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); Console.ReadKey(); } Environment.Exit(2); } } } }