From 378bde172e7bc22b8476effce423a5402c173b0f Mon Sep 17 00:00:00 2001 From: Casper Sparre Date: Wed, 18 Feb 2026 22:05:54 +0100 Subject: [PATCH] Added custom implementation for scraping creator content. --- .../CLI/CajetanDownloadEventHandler.cs | 37 ++ Cajetan.OF-DL/Exceptions/ExitCodeException.cs | 6 + Cajetan.OF-DL/Models/CajetanConfig.cs | 11 + Cajetan.OF-DL/Models/EMode.cs | 9 + Cajetan.OF-DL/Models/Extensions.cs | 19 + Cajetan.OF-DL/ProgramCajetan.cs | 114 +++++- Cajetan.OF-DL/Properties/GlobalUsings.cs | 3 + Cajetan.OF-DL/Properties/launchSettings.json | 10 + Cajetan.OF-DL/Worker.cs | 348 ++++++++++++++++++ 9 files changed, 550 insertions(+), 7 deletions(-) create mode 100644 Cajetan.OF-DL/CLI/CajetanDownloadEventHandler.cs create mode 100644 Cajetan.OF-DL/Exceptions/ExitCodeException.cs create mode 100644 Cajetan.OF-DL/Models/CajetanConfig.cs create mode 100644 Cajetan.OF-DL/Models/EMode.cs create mode 100644 Cajetan.OF-DL/Models/Extensions.cs create mode 100644 Cajetan.OF-DL/Properties/launchSettings.json create mode 100644 Cajetan.OF-DL/Worker.cs diff --git a/Cajetan.OF-DL/CLI/CajetanDownloadEventHandler.cs b/Cajetan.OF-DL/CLI/CajetanDownloadEventHandler.cs new file mode 100644 index 0000000..a3947a3 --- /dev/null +++ b/Cajetan.OF-DL/CLI/CajetanDownloadEventHandler.cs @@ -0,0 +1,37 @@ +using OF_DL.Models.Downloads; + +namespace OF_DL.CLI; + +internal class CajetanDownloadEventHandler : IDownloadEventHandler +{ + private readonly SpectreDownloadEventHandler _eventHandler = new(); + + public void OnContentFound(string contentType, int mediaCount, int objectCount) + => _eventHandler.OnContentFound(contentType, mediaCount, objectCount); + + public void OnDownloadComplete(string contentType, DownloadResult result) + => _eventHandler.OnDownloadComplete(contentType, result); + + public void OnMessage(string message) + => _eventHandler.OnMessage(message); + + public void OnNoContentFound(string contentType) + => _eventHandler.OnNoContentFound(contentType); + + public void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount) + => _eventHandler.OnPurchasedTabUserComplete(username, paidPostCount, paidMessagesCount); + + public void OnScrapeComplete(TimeSpan elapsed) + => _eventHandler.OnScrapeComplete(elapsed); + + public void OnUserComplete(string username, CreatorDownloadResult result) + => _eventHandler.OnUserComplete(username, result); + + public void OnUserStarting(string username) { } + + public Task WithProgressAsync(string description, long maxValue, bool showSize, Func> work) + => _eventHandler.WithProgressAsync(description, maxValue, showSize, work); + + public Task WithStatusAsync(string statusMessage, Func> work) + => _eventHandler.WithStatusAsync(statusMessage, work); +} diff --git a/Cajetan.OF-DL/Exceptions/ExitCodeException.cs b/Cajetan.OF-DL/Exceptions/ExitCodeException.cs new file mode 100644 index 0000000..ffbaa1a --- /dev/null +++ b/Cajetan.OF-DL/Exceptions/ExitCodeException.cs @@ -0,0 +1,6 @@ +namespace OF_DL.Exceptions; + +public sealed class ExitCodeException(int exitCode) : Exception +{ + public int ExitCode { get; } = exitCode; +} diff --git a/Cajetan.OF-DL/Models/CajetanConfig.cs b/Cajetan.OF-DL/Models/CajetanConfig.cs new file mode 100644 index 0000000..9c42302 --- /dev/null +++ b/Cajetan.OF-DL/Models/CajetanConfig.cs @@ -0,0 +1,11 @@ +namespace OF_DL.Models; + +public class CajetanConfig +{ + public string[] NonInteractiveSpecificLists { get; set; } = []; + public string[] NonInteractiveSpecificUsers { get; set; } = []; + + public EMode Mode { get; set; } = EMode.None; + + public string ErrorMessage { get; set; } = string.Empty; +} diff --git a/Cajetan.OF-DL/Models/EMode.cs b/Cajetan.OF-DL/Models/EMode.cs new file mode 100644 index 0000000..b1f4710 --- /dev/null +++ b/Cajetan.OF-DL/Models/EMode.cs @@ -0,0 +1,9 @@ +namespace OF_DL.Models; + +public enum EMode +{ + None, + DownloadCreatorContent, + OutputBlockedUsers, + UpdateAllUserInfo +} diff --git a/Cajetan.OF-DL/Models/Extensions.cs b/Cajetan.OF-DL/Models/Extensions.cs new file mode 100644 index 0000000..ac45a92 --- /dev/null +++ b/Cajetan.OF-DL/Models/Extensions.cs @@ -0,0 +1,19 @@ +namespace OF_DL.Models.Downloads; + +internal static class DownloadsExtensions +{ + extension(CreatorDownloadResult result) + { + public void Add(CreatorDownloadResult other) + { + result.PaidPostCount += other.PaidPostCount; + result.PostCount += other.PostCount; + result.ArchivedCount += other.ArchivedCount; + result.StreamsCount += other.StreamsCount; + result.StoriesCount += other.StoriesCount; + result.HighlightsCount += other.HighlightsCount; + result.MessagesCount += other.MessagesCount; + result.PaidMessagesCount += other.PaidMessagesCount; + } + } +} diff --git a/Cajetan.OF-DL/ProgramCajetan.cs b/Cajetan.OF-DL/ProgramCajetan.cs index a7bcb1c..88b5277 100644 --- a/Cajetan.OF-DL/ProgramCajetan.cs +++ b/Cajetan.OF-DL/ProgramCajetan.cs @@ -3,8 +3,10 @@ using Microsoft.Extensions.DependencyInjection; AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red)); ServiceCollection services = await ConfigureServices(args); +ServiceProvider serviceProvider = services.BuildServiceProvider(); - +Worker worker = serviceProvider.GetRequiredService(); +await worker.RunAsync(); static async Task ConfigureServices(string[] args) { @@ -22,22 +24,39 @@ static async Task ConfigureServices(string[] args) 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(); - } + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); + Console.ReadKey(); + + exitHelper.ExitWithCode(3); + } + + if (configService.CurrentConfig.NonInteractiveMode is false) + { + AnsiConsole.MarkupLine("\n[red]Cannot run in Interactive Mode![/]\n"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); + Console.ReadKey(); exitHelper.ExitWithCode(3); } AnsiConsole.Markup("[green]config.conf located successfully!\n[/]"); + if (!ParseCommandlineArgs(args, configService.CurrentConfig, out CajetanConfig cajetanConfig)) + { + AnsiConsole.MarkupLine($"\n[red]{cajetanConfig.ErrorMessage}[/]\n"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); + Console.ReadKey(); + + exitHelper.ExitWithCode(3); + } + // Set up full dependency injection with loaded config services = []; services.AddSingleton(loggingService); services.AddSingleton(configService); services.AddSingleton(exitHelper); + services.AddSingleton(cajetanConfig); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -45,7 +64,88 @@ static async Task ConfigureServices(string[] args) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + + services.AddSingleton(); return services; } + +static bool ParseCommandlineArgs(string[] args, Config currentConfig, out CajetanConfig parsedConfig) +{ + const string SPECIFIC_LISTS_ARG = "--specific-lists"; + const string SPECIFIC_USERS_ARG = "--specific-users"; + const string OUTPUT_BLOCKED_USERS_ARG = "--output-blocked"; + const string UPDATE_ALL_USER_INFO_ARG = "--update-userinfo"; + + parsedConfig = new(); + + if (ParseListAndUserArguments(ref parsedConfig)) + return true; + + if (ParseFlagArgument(OUTPUT_BLOCKED_USERS_ARG, EMode.OutputBlockedUsers, ref parsedConfig)) + return true; + + if (ParseFlagArgument(UPDATE_ALL_USER_INFO_ARG, EMode.UpdateAllUserInfo, ref parsedConfig)) + return true; + + parsedConfig.ErrorMessage = "No mode argument provided!"; + return false; + + bool ParseListAndUserArguments(ref CajetanConfig parsedConfig) + { + bool hasSpecificListsArg = ParseCommaSeparatedListArgument(SPECIFIC_LISTS_ARG, ref parsedConfig, (c, v) => + { + c.NonInteractiveSpecificLists = v; + Log.Logger = Log.Logger.ForContext(nameof(CajetanConfig.NonInteractiveSpecificLists), string.Join(",", v)); + }); + + if (hasSpecificListsArg) + return true; + + + bool hasSpecificUsersArg = ParseCommaSeparatedListArgument(SPECIFIC_USERS_ARG, ref parsedConfig, (c, v) => + { + c.NonInteractiveSpecificUsers = v; + Log.Logger = Log.Logger.ForContext(nameof(CajetanConfig.NonInteractiveSpecificUsers), string.Join(",", v)); + }); + + if (hasSpecificUsersArg) + return true; + + return false; + } + + bool ParseCommaSeparatedListArgument(string argName, ref CajetanConfig parsedConfig, Action assignmentFunc) + { + char[] separator = [',']; + int indexOfArg = Array.FindIndex(args, a => argName.Equals(a, StringComparison.OrdinalIgnoreCase)); + + if (indexOfArg < 0) + return false; + + int indexOfListValues = indexOfArg + 1; + string[] strListValues = args.ElementAtOrDefault(indexOfListValues)?.Split(separator, StringSplitOptions.RemoveEmptyEntries) ?? []; + + if (strListValues.Length == 0) + return false; + + assignmentFunc(parsedConfig, strListValues); + parsedConfig.Mode = EMode.DownloadCreatorContent; + + Log.Logger = Log.Logger.ForContext("Mode", $"{EMode.DownloadCreatorContent}"); + return true; + } + + bool ParseFlagArgument(string argName, EMode modeIfArgIsSet, ref CajetanConfig parsedConfig) + { + if (!args.Any(a => argName.Equals(a, StringComparison.OrdinalIgnoreCase))) + return false; + + currentConfig.NonInteractiveMode = true; + parsedConfig.Mode = modeIfArgIsSet; + + Log.Logger = Log.Logger.ForContext("Mode", $"{modeIfArgIsSet}"); + return true; + } +} + diff --git a/Cajetan.OF-DL/Properties/GlobalUsings.cs b/Cajetan.OF-DL/Properties/GlobalUsings.cs index 3bb5015..32d2d56 100644 --- a/Cajetan.OF-DL/Properties/GlobalUsings.cs +++ b/Cajetan.OF-DL/Properties/GlobalUsings.cs @@ -1,9 +1,12 @@ +global using Serilog; global using Spectre.Console; global using OF_DL; global using OF_DL.CLI; global using OF_DL.Crypto; global using OF_DL.Enumerations; +global using OF_DL.Exceptions; global using OF_DL.Helpers; global using OF_DL.Models; +global using OF_DL.Models.Config; global using OF_DL.Services; global using OF_DL.Utils; diff --git a/Cajetan.OF-DL/Properties/launchSettings.json b/Cajetan.OF-DL/Properties/launchSettings.json new file mode 100644 index 0000000..0b259a6 --- /dev/null +++ b/Cajetan.OF-DL/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Cajetan.OF-DL": { + "commandName": "Project", + "workingDirectory": "..\\.dev\\", + //"commandLineArgs": "--non-interactive --specific-lists Capture" + "commandLineArgs": "--non-interactive --specific-users amyboz" + } + } +} diff --git a/Cajetan.OF-DL/Worker.cs b/Cajetan.OF-DL/Worker.cs new file mode 100644 index 0000000..56f535b --- /dev/null +++ b/Cajetan.OF-DL/Worker.cs @@ -0,0 +1,348 @@ +using Microsoft.Extensions.DependencyInjection; +using OF_DL.Models.Downloads; +using OF_DL.Models.Entities.Users; + +namespace OF_DL; + +internal class Worker(IServiceProvider serviceProvider) +{ + private readonly IConfigService _configService = serviceProvider.GetRequiredService(); + private readonly IAuthService _authService = serviceProvider.GetRequiredService(); + private readonly IStartupService _startupService = serviceProvider.GetRequiredService(); + private readonly IDownloadOrchestrationService _orchestrationService = serviceProvider.GetRequiredService(); + private readonly IDbService _dbService = serviceProvider.GetRequiredService(); + private readonly IApiService _apiService = serviceProvider.GetRequiredService(); + private readonly ExitHelper _exitHelper = serviceProvider.GetRequiredService(); + + private readonly CajetanConfig _cajetanConfig = serviceProvider.GetRequiredService(); + private bool _clientIdBlobMissing = true; + private bool _devicePrivateKeyMissing = true; + + public async Task RunAsync() + { + try + { + await InitializeAsync(); + + Task tMode = _cajetanConfig.Mode switch + { + EMode.DownloadCreatorContent => DownloadCreatorContentAsync(), + EMode.OutputBlockedUsers => OutputBlockedUsersAsync(), + EMode.UpdateAllUserInfo => UpdateUserInfoAsync(), + _ => Task.CompletedTask + }; + + await tMode; + } + catch (ExitCodeException ex) + { + _exitHelper.ExitWithCode(ex.ExitCode); + } + catch (Exception ex) + { + Log.Fatal(ex, "Unhandled Exception! {ExceptionMessage:l}", ex.Message); + } + finally + { + _exitHelper.ExitWithCode(0); + } + } + + private async Task InitializeAsync() + { + StartupResult startupResult = await _startupService.ValidateEnvironmentAsync(); + + if (startupResult.IsWindowsVersionValid is false) + throw new ExitCodeException(1); + + if (startupResult.FfmpegFound is false) + throw new ExitCodeException(4); + + if (startupResult.ClientIdBlobMissing || startupResult.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[/]"); + + _clientIdBlobMissing = startupResult.ClientIdBlobMissing; + _devicePrivateKeyMissing = startupResult.DevicePrivateKeyMissing; + + if (IsRulesJsonValid() is false) + { + AnsiConsole.MarkupLine("\n[red]Press any key to exit.[/]"); + Console.ReadKey(); + throw new ExitCodeException(2); + } + + if (await IsAuthorizedAsync() is false) + { + AnsiConsole.MarkupLine("\n[red]Press any key to exit.[/]"); + Console.ReadKey(); + throw new ExitCodeException(2); + } + + bool IsRulesJsonValid() + { + if (startupResult.RulesJsonExists is false) + return true; + + if (startupResult.RulesJsonValid) + { + AnsiConsole.Markup("[green]rules.json located successfully!\n[/]"); + return true; + } + + AnsiConsole.MarkupLine("\n[red]rules.json is not valid, check your JSON syntax![/]\n"); + Log.Error("processing of rules.json failed: {Error:l}", startupResult.RulesJsonError); + return false; + } + + async Task IsAuthorizedAsync() + { + if (await _authService.LoadFromFileAsync() is false) + { + if (File.Exists("auth.json")) + { + Log.Error("Auth file was found but could not be deserialized!"); + AnsiConsole.MarkupLine("\n[red]auth.json could not be deserialized.[/]"); + return false; + } + + Log.Error("Auth file was not found!"); + AnsiConsole.MarkupLine("\n[red]auth.json is missing.[/]"); + return false; + } + + AnsiConsole.Markup("[green]auth.json located successfully!\n[/]"); + + // Validate cookie string + _authService.ValidateCookieString(); + + User? user = await _authService.ValidateAuthAsync(); + if (user is null || (user.Name is null && user.Username is null)) + { + Log.Error("Auth failed"); + _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"); + return false; + } + + string displayName = $"{user.Name} {user.Username}".Trim(); + AnsiConsole.MarkupLine($"[green]Logged In successfully as {displayName}\n[/]"); + return true; + } + } + + private async Task DownloadCreatorContentAsync() + { + DateTime startTime = DateTime.Now; + + UserListResult allUsersAndLists = await GetAvailableUsersAsync(); + Dictionary usersToDownload = []; + + if (_cajetanConfig.NonInteractiveSpecificLists is not null && _cajetanConfig.NonInteractiveSpecificLists.Length > 0) + usersToDownload = await GetUsersFromSpecificListsAsync(allUsersAndLists, _cajetanConfig.NonInteractiveSpecificLists); + + else if (_cajetanConfig.NonInteractiveSpecificUsers is not null && _cajetanConfig.NonInteractiveSpecificUsers.Length > 0) + usersToDownload = GetUsersFromSpecificUsernames(allUsersAndLists, [.. _cajetanConfig.NonInteractiveSpecificUsers]); + + if (usersToDownload.Count == 0) + return; + + int userNum = 0; + int userCount = usersToDownload.Count; + CajetanDownloadEventHandler eventHandler = new(); + CreatorDownloadResult totalResults = new(); + + LoggerWithConfigContext(_configService.CurrentConfig, _cajetanConfig) + .Information("Scraping Data for {UserCount} user(s)", usersToDownload.Count); + eventHandler.OnMessage($"Scraping Data for {usersToDownload.Count} user(s)\n"); + + foreach ((string username, long userId) in usersToDownload) + { + try + { + DateTime userStartTime = DateTime.Now; + + Log.Information("Scraping Data for '{Username:l}' ({UserNum} of {UserCount})", username, ++userNum, userCount); + eventHandler.OnMessage($"\nScraping Data for {Markup.Escape(username)} ({userNum} of {userCount})\n"); + + string path = _orchestrationService.ResolveDownloadPath(username); + Log.Debug($"Download path: {path}"); + + CreatorDownloadResult results = await _orchestrationService.DownloadCreatorContentAsync( + username: username, + userId: userId, + path: path, + users: usersToDownload, + clientIdBlobMissing: _clientIdBlobMissing, + devicePrivateKeyMissing: _devicePrivateKeyMissing, + eventHandler: eventHandler + ); + + totalResults.Add(results); + + DateTime userEndTime = DateTime.Now; + TimeSpan userTotalTime = userEndTime - userStartTime; + + Log.ForContext("Paid Posts", results.PaidPostCount) + .ForContext("Posts", results.PostCount) + .ForContext("Archived", results.ArchivedCount) + .ForContext("Streams", results.StreamsCount) + .ForContext("Stories", results.StoriesCount) + .ForContext("Highlights", results.HighlightsCount) + .ForContext("Messages", results.MessagesCount) + .ForContext("Paid Messages", results.PaidMessagesCount) + .ForContext("Username", username) + .ForContext("TotalMinutes", userTotalTime.TotalMinutes) + .Information("Scraped Data for '{Username:l}', took {TotalMinutes:0.000} minutes"); + } + catch (Exception ex) + { + Log.ForContext("Username", username) + .ForContext("UserNum", userNum) + .ForContext("UserCount", userCount) + .ForContext("ExceptionMessage", ex.Message) + .Error(ex, "Scrape for '{Username:l}' ({UserNum} of {UserCount}) failed! {ExceptionMessage:l}"); + } + } + + DateTime endTime = DateTime.Now; + TimeSpan totalTime = endTime - startTime; + + eventHandler.OnScrapeComplete(totalTime); + + Log.ForContext("Paid Posts", totalResults.PaidPostCount) + .ForContext("Posts", totalResults.PostCount) + .ForContext("Archived", totalResults.ArchivedCount) + .ForContext("Streams", totalResults.StreamsCount) + .ForContext("Stories", totalResults.StoriesCount) + .ForContext("Highlights", totalResults.HighlightsCount) + .ForContext("Messages", totalResults.MessagesCount) + .ForContext("Paid Messages", totalResults.PaidMessagesCount) + .ForContext("TotalMinutes", totalTime.TotalMinutes) + .Information("Scrape Completed in {TotalMinutes:0.00} minutes"); + } + + private async Task OutputBlockedUsersAsync() + { + + } + + private async Task UpdateUserInfoAsync() + { + + } + + private async Task> GetUsersFromSpecificListsAsync(UserListResult allUsersAndLists, string[] listNames) + { + Config currentConfig = _configService.CurrentConfig; + Dictionary usersFromLists = new(StringComparer.OrdinalIgnoreCase); + + foreach (string name in listNames) + { + if (!allUsersAndLists.Lists.TryGetValue(name, out long listId)) + continue; + + Log.Information("Getting Users from list '{ListName:l}' (Include Restricted: {IncludeRestrictedSubscriptions})", name, currentConfig.IncludeRestrictedSubscriptions); + AnsiConsole.Markup($"[green]Getting Users from list '{name}' (Include Restricted: {currentConfig.IncludeRestrictedSubscriptions})\n[/]"); + + List listUsernames = await _apiService.GetListUsers($"/lists/{listId}/users") ?? []; + + foreach (string u in listUsernames) + { + if (usersFromLists.ContainsKey(u)) + continue; + + if (!allUsersAndLists.Users.TryGetValue(u, out long userId)) + continue; + + usersFromLists[u] = userId; + } + } + + return usersFromLists; + } + + private static Dictionary GetUsersFromSpecificUsernames(UserListResult allUsersAndLists, HashSet usernames) + { + Dictionary filteredUsers = allUsersAndLists.Users + .Where(u => usernames.Contains(u.Key)) + .ToDictionary(u => u.Key, u => u.Value); + + return filteredUsers; + } + + private async Task GetAvailableUsersAsync() + { + Config currentConfig = _configService.CurrentConfig; + UserListResult result = new() + { + Users = new(StringComparer.OrdinalIgnoreCase) + }; + + await FetchUsersAsync(); + await FetchListsAsync(); + + AnsiConsole.WriteLine(); + + await _dbService.CreateUsersDb(result.Users); + + return result; + + async Task FetchUsersAsync() + { + Log.Information("Getting Active Subscriptions (Include Restricted: {IncludeRestrictedSubscriptions})", currentConfig.IncludeRestrictedSubscriptions); + AnsiConsole.Markup($"[green]Getting Active Subscriptions (Include Restricted: {currentConfig.IncludeRestrictedSubscriptions})\n[/]"); + + Dictionary? activeSubs = await _apiService.GetActiveSubscriptions("/subscriptions/subscribes", currentConfig.IncludeRestrictedSubscriptions); + AddToResult(activeSubs); + + if (currentConfig.IncludeExpiredSubscriptions) + { + Log.Information("Getting Expired Subscriptions (Include Restricted: {IncludeRestrictedSubscriptions})", currentConfig.IncludeRestrictedSubscriptions); + AnsiConsole.Markup($"[green]Getting Expired Subscriptions (Include Restricted: {currentConfig.IncludeRestrictedSubscriptions})\n[/]"); + + Dictionary? expiredSubs = await _apiService.GetExpiredSubscriptions("/subscriptions/subscribes", currentConfig.IncludeRestrictedSubscriptions); + AddToResult(expiredSubs); + } + } + + async Task FetchListsAsync() + { + Log.Information("Getting Lists"); + result.Lists = await _apiService.GetLists("/lists") ?? []; + } + + void AddToResult(Dictionary? subscriptions) + { + foreach ((string username, long userId) in subscriptions ?? []) + { + if (result.Users.TryAdd(username, userId)) + Log.Debug($"Name: {username} ID: {userId}"); + } + } + } + + static ILogger LoggerWithConfigContext(Config config, CajetanConfig cajetanConfig) + => Log.Logger + .ForContext(nameof(Config.DownloadPath), config.DownloadPath) + .ForContext(nameof(Config.DownloadPosts), config.DownloadPosts) + .ForContext(nameof(Config.DownloadPaidPosts), config.DownloadPaidPosts) + .ForContext(nameof(Config.DownloadMessages), config.DownloadMessages) + .ForContext(nameof(Config.DownloadPaidMessages), config.DownloadPaidMessages) + .ForContext(nameof(Config.DownloadStories), config.DownloadStories) + .ForContext(nameof(Config.DownloadStreams), config.DownloadStreams) + .ForContext(nameof(Config.DownloadHighlights), config.DownloadHighlights) + .ForContext(nameof(Config.DownloadArchived), config.DownloadArchived) + .ForContext(nameof(Config.DownloadAvatarHeaderPhoto), config.DownloadAvatarHeaderPhoto) + .ForContext(nameof(Config.DownloadImages), config.DownloadImages) + .ForContext(nameof(Config.DownloadVideos), config.DownloadVideos) + .ForContext(nameof(Config.DownloadAudios), config.DownloadAudios) + .ForContext(nameof(Config.IgnoreOwnMessages), config.IgnoreOwnMessages) + .ForContext(nameof(Config.DownloadPostsIncrementally), config.DownloadPostsIncrementally) + .ForContext(nameof(Config.BypassContentForCreatorsWhoNoLongerExist), config.BypassContentForCreatorsWhoNoLongerExist) + .ForContext(nameof(Config.SkipAds), config.SkipAds) + .ForContext(nameof(Config.IncludeExpiredSubscriptions), config.IncludeExpiredSubscriptions) + .ForContext(nameof(Config.IncludeRestrictedSubscriptions), config.IncludeRestrictedSubscriptions) + .ForContext(nameof(CajetanConfig.NonInteractiveSpecificLists), cajetanConfig.NonInteractiveSpecificLists) + .ForContext(nameof(CajetanConfig.NonInteractiveSpecificUsers), cajetanConfig.NonInteractiveSpecificUsers); +}