From a566cd0b7140a5aa5cc653c839baa246dfc9d9d8 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Thu, 5 Feb 2026 00:22:22 -0600 Subject: [PATCH 01/55] Add dependency injection for config --- OF DL/OF DL.csproj | 1 + OF DL/Program.cs | 86 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/OF DL/OF DL.csproj b/OF DL/OF DL.csproj index 2ee9f51..be35c42 100644 --- a/OF DL/OF DL.csproj +++ b/OF DL/OF DL.csproj @@ -18,6 +18,7 @@ + diff --git a/OF DL/Program.cs b/OF DL/Program.cs index ae0e740..2c005bb 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -13,14 +13,13 @@ using Serilog; using Serilog.Core; using Serilog.Events; using Spectre.Console; -using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using static OF_DL.Entities.Messages.Messages; using Akka.Configuration; using System.Text; -using static Akka.Actor.ProviderSelection; +using Microsoft.Extensions.DependencyInjection; namespace OF_DL; @@ -31,11 +30,16 @@ public class Program private static bool clientIdBlobMissing = false; private static bool devicePrivateKeyMissing = false; - private static Entities.Config? config = null; - private static Auth? auth = null; + private readonly Entities.Config config; + private Auth? auth = null; private static LoggingLevelSwitch levelSwitch = new LoggingLevelSwitch(); - private static async Task LoadAuthFromBrowser() + public Program(Entities.Config config) + { + this.config = config; + } + + private async Task LoadAuthFromBrowser() { bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; @@ -104,21 +108,48 @@ public class Program public static async Task Main(string[] args) { - bool cliNonInteractive = false; + levelSwitch.MinimumLevel = LogEventLevel.Error; //set initial level (until we've read from config) + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.ControlledBy(levelSwitch) + .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); + + 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"); + + // Load configuration + Entities.Config? config = LoadConfiguration(args, out bool cliNonInteractive); + if (config == null) + { + Environment.Exit(3); + return; + } + + // Set up dependency injection + var services = new ServiceCollection(); + ConfigureServices(services, config); + var serviceProvider = services.BuildServiceProvider(); + + // Get the Program instance and run + var program = serviceProvider.GetRequiredService(); + await program.RunAsync(args, cliNonInteractive); + } + + private static void ConfigureServices(IServiceCollection services, Entities.Config config) + { + services.AddSingleton(config); + services.AddSingleton(); + } + + private static Entities.Config? LoadConfiguration(string[] args, out bool cliNonInteractive) + { + cliNonInteractive = false; + Entities.Config? config = null; try { - levelSwitch.MinimumLevel = LogEventLevel.Error; //set initial level (until we've read from config) - - Log.Logger = new LoggerConfiguration() - .MinimumLevel.ControlledBy(levelSwitch) - .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day) - .CreateLogger(); - - 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"); - //Remove config.json and convert to config.conf if (File.Exists("config.json")) { @@ -502,7 +533,21 @@ public class Program Log.Debug(argument); } } + } + catch (Exception e) + { + Console.WriteLine(e); + Log.Error("Configuration loading failed.", e.Message); + return null; + } + return config; + } + + private async Task RunAsync(string[] args, bool cliNonInteractive) + { + try + { var os = Environment.OSVersion; Log.Debug($"Operating system information: {os.VersionString}"); @@ -668,7 +713,7 @@ public class Program } //Added to stop cookie being filled with un-needed headers - ValidateCookieString(); + ValidateCookieString(auth!); if (File.Exists("rules.json")) { @@ -917,7 +962,6 @@ public class Program } } - private static async Task DownloadAllData(APIHelper m_ApiHelper, Auth Auth, Entities.Config Config) { DBHelper dBHelper = new DBHelper(Config); @@ -2959,7 +3003,7 @@ public class Program foreach (var item in listSelection) { long listId = lists[item.Replace("[red]", "").Replace("[/]", "")]; - List usernames = await apiHelper.GetListUsers($"/lists/{listId}/users", config); + List usernames = await apiHelper.GetListUsers($"/lists/{listId}/users", currentConfig); foreach (string user in usernames) { listUsernames.Add(user); @@ -3456,7 +3500,7 @@ public class Program return null; } - public static void ValidateCookieString() + public static void ValidateCookieString(Auth auth) { string pattern = @"(auth_id=\d+)|(sess=[^;]+)"; var matches = Regex.Matches(auth.COOKIE, pattern); From 6c00f1a6aabc6d6114520780a39fd5074cca3662 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Thu, 5 Feb 2026 01:04:50 -0600 Subject: [PATCH 02/55] Add helpers as services --- OF DL/Helpers/APIHelper.cs | 120 ++++++++--------- OF DL/Helpers/AuthHelper.cs | 3 +- OF DL/Helpers/DownloadContext.cs | 38 +++--- OF DL/Helpers/DownloadHelper.cs | 166 ++++++++++++------------ OF DL/Helpers/Interfaces/IAPIHelper.cs | 1 + OF DL/Helpers/Interfaces/IAuthHelper.cs | 10 ++ OF DL/Program.cs | 53 ++++++-- 7 files changed, 214 insertions(+), 177 deletions(-) create mode 100644 OF DL/Helpers/Interfaces/IAuthHelper.cs diff --git a/OF DL/Helpers/APIHelper.cs b/OF DL/Helpers/APIHelper.cs index ea209c8..ae9c645 100644 --- a/OF DL/Helpers/APIHelper.cs +++ b/OF DL/Helpers/APIHelper.cs @@ -26,7 +26,7 @@ namespace OF_DL.Helpers; public class APIHelper : IAPIHelper { private static readonly JsonSerializerSettings m_JsonSerializerSettings; - private readonly IDBHelper m_DBHelper; + private readonly IDBHelper dbHelper; private readonly IDownloadConfig downloadConfig; private readonly Auth auth; private static DateTime? cachedDynamicRulesExpiration; @@ -42,11 +42,11 @@ public class APIHelper : IAPIHelper }; } - public APIHelper(Auth auth, IDownloadConfig downloadConfig) + public APIHelper(Auth auth, IDownloadConfig downloadConfig, IDBHelper dbHelper) { this.auth = auth; - m_DBHelper = new DBHelper(downloadConfig); this.downloadConfig = downloadConfig; + this.dbHelper = dbHelper; } @@ -570,21 +570,21 @@ public class APIHelper : IAPIHelper { if (story.media[0].createdAt.HasValue) { - await m_DBHelper.AddStory(folder, story.id, string.Empty, "0", false, false, story.media[0].createdAt.Value); + await dbHelper.AddStory(folder, story.id, string.Empty, "0", false, false, story.media[0].createdAt.Value); } else if (story.createdAt.HasValue) { - await m_DBHelper.AddStory(folder, story.id, string.Empty, "0", false, false, story.createdAt.Value); + await dbHelper.AddStory(folder, story.id, string.Empty, "0", false, false, story.createdAt.Value); } else { - await m_DBHelper.AddStory(folder, story.id, string.Empty, "0", false, false, DateTime.Now); + await dbHelper.AddStory(folder, story.id, string.Empty, "0", false, false, DateTime.Now); } if (story.media != null && story.media.Count > 0) { foreach (Stories.Medium medium in story.media) { - await m_DBHelper.AddMedia(folder, medium.id, story.id, medium.files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); + await dbHelper.AddMedia(folder, medium.id, story.id, medium.files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); if (medium.type == "photo" && !config.DownloadImages) { continue; @@ -671,21 +671,21 @@ public class APIHelper : IAPIHelper { if (item.media[0].createdAt.HasValue) { - await m_DBHelper.AddStory(folder, item.id, string.Empty, "0", false, false, item.media[0].createdAt.Value); + await dbHelper.AddStory(folder, item.id, string.Empty, "0", false, false, item.media[0].createdAt.Value); } else if (item.createdAt.HasValue) { - await m_DBHelper.AddStory(folder, item.id, string.Empty, "0", false, false, item.createdAt.Value); + await dbHelper.AddStory(folder, item.id, string.Empty, "0", false, false, item.createdAt.Value); } else { - await m_DBHelper.AddStory(folder, item.id, string.Empty, "0", false, false, DateTime.Now); + await dbHelper.AddStory(folder, item.id, string.Empty, "0", false, false, DateTime.Now); } if (item.media.Count > 0 && item.media[0].canView) { foreach (HighlightMedia.Medium medium in item.media) { - await m_DBHelper.AddMedia(folder, medium.id, item.id, item.media[0].files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); + await dbHelper.AddMedia(folder, medium.id, item.id, item.media[0].files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); if (medium.type == "photo" && !config.DownloadImages) { continue; @@ -808,7 +808,7 @@ public class APIHelper : IAPIHelper } } } - await m_DBHelper.AddPost(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); + await dbHelper.AddPost(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); paidPostCollection.PaidPostObjects.Add(purchase); foreach (Messages.Medium medium in purchase.media) { @@ -840,7 +840,7 @@ public class APIHelper : IAPIHelper { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, medium.files.full.url); paidPostCollection.PaidPostMedia.Add(medium); } @@ -850,7 +850,7 @@ public class APIHelper : IAPIHelper if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidPostCollection.PaidPostMedia.Add(medium); } @@ -863,7 +863,7 @@ public class APIHelper : IAPIHelper { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, medium.files.full.url); paidPostCollection.PaidPostMedia.Add(medium); } @@ -872,7 +872,7 @@ public class APIHelper : IAPIHelper { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidPostCollection.PaidPostMedia.Add(medium); } @@ -925,7 +925,7 @@ public class APIHelper : IAPIHelper } else if (config.DownloadPostsIncrementally) { - var mostRecentPostDate = await m_DBHelper.GetMostRecentPostDate(folder); + var mostRecentPostDate = await dbHelper.GetMostRecentPostDate(folder); if (mostRecentPostDate.HasValue) { downloadDateSelection = Enumerations.DownloadDateSelection.after; @@ -1001,7 +1001,7 @@ public class APIHelper : IAPIHelper } } } - await m_DBHelper.AddPost(folder, post.id, post.rawText != null ? post.rawText : string.Empty, post.price != null ? post.price.ToString() : "0", post.price != null && post.isOpened ? true : false, post.isArchived, post.postedAt); + await dbHelper.AddPost(folder, post.id, post.rawText != null ? post.rawText : string.Empty, post.price != null ? post.price.ToString() : "0", post.price != null && post.isOpened ? true : false, post.isArchived, post.postedAt); postCollection.PostObjects.Add(post); if (post.media != null && post.media.Count > 0) { @@ -1032,7 +1032,7 @@ public class APIHelper : IAPIHelper { if (!postCollection.Posts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, post.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, post.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); postCollection.Posts.Add(medium.id, medium.files!.full.url); postCollection.PostMedia.Add(medium); } @@ -1044,7 +1044,7 @@ public class APIHelper : IAPIHelper { if (!postCollection.Posts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, post.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, post.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); postCollection.Posts.Add(medium.id, medium.files.preview.url); postCollection.PostMedia.Add(medium); } @@ -1058,7 +1058,7 @@ public class APIHelper : IAPIHelper { if (!postCollection.Posts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, post.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, post.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); postCollection.Posts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{post.id}"); postCollection.PostMedia.Add(medium); } @@ -1115,7 +1115,7 @@ public class APIHelper : IAPIHelper } } } - await m_DBHelper.AddPost(folder, singlePost.id, singlePost.text != null ? singlePost.text : string.Empty, singlePost.price != null ? singlePost.price.ToString() : "0", singlePost.price != null && singlePost.isOpened ? true : false, singlePost.isArchived, singlePost.postedAt); + await dbHelper.AddPost(folder, singlePost.id, singlePost.text != null ? singlePost.text : string.Empty, singlePost.price != null ? singlePost.price.ToString() : "0", singlePost.price != null && singlePost.isOpened ? true : false, singlePost.isArchived, singlePost.postedAt); singlePostCollection.SinglePostObjects.Add(singlePost); if (singlePost.media != null && singlePost.media.Count > 0) { @@ -1146,7 +1146,7 @@ public class APIHelper : IAPIHelper { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, singlePost.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, singlePost.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.files!.full.url); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1159,7 +1159,7 @@ public class APIHelper : IAPIHelper { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._240, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._240, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.videoSources._240); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1173,7 +1173,7 @@ public class APIHelper : IAPIHelper { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._720, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._720, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.videoSources._720); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1189,7 +1189,7 @@ public class APIHelper : IAPIHelper { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, singlePost.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, singlePost.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{singlePost.id}"); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1199,7 +1199,7 @@ public class APIHelper : IAPIHelper { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, singlePost.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, singlePost.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.files.preview.url); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1304,7 +1304,7 @@ public class APIHelper : IAPIHelper } } } - await m_DBHelper.AddPost(folder, stream.id, stream.text != null ? stream.text : string.Empty, stream.price != null ? stream.price.ToString() : "0", stream.price != null && stream.isOpened ? true : false, stream.isArchived, stream.postedAt); + await dbHelper.AddPost(folder, stream.id, stream.text != null ? stream.text : string.Empty, stream.price != null ? stream.price.ToString() : "0", stream.price != null && stream.isOpened ? true : false, stream.isArchived, stream.postedAt); streamsCollection.StreamObjects.Add(stream); if (stream.media != null && stream.media.Count > 0) { @@ -1333,7 +1333,7 @@ public class APIHelper : IAPIHelper { if (!streamsCollection.Streams.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, stream.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, stream.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); streamsCollection.Streams.Add(medium.id, medium.files.full.url); streamsCollection.StreamMedia.Add(medium); } @@ -1346,7 +1346,7 @@ public class APIHelper : IAPIHelper { if (!streamsCollection.Streams.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, stream.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, stream.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); streamsCollection.Streams.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{stream.id}"); streamsCollection.StreamMedia.Add(medium); } @@ -1452,7 +1452,7 @@ public class APIHelper : IAPIHelper } } } - await m_DBHelper.AddPost(folder, archive.id, archive.text != null ? archive.text : string.Empty, archive.price != null ? archive.price.ToString() : "0", archive.price != null && archive.isOpened ? true : false, archive.isArchived, archive.postedAt); + await dbHelper.AddPost(folder, archive.id, archive.text != null ? archive.text : string.Empty, archive.price != null ? archive.price.ToString() : "0", archive.price != null && archive.isOpened ? true : false, archive.isArchived, archive.postedAt); archivedCollection.ArchivedPostObjects.Add(archive); if (archive.media != null && archive.media.Count > 0) { @@ -1478,7 +1478,7 @@ public class APIHelper : IAPIHelper { if (!archivedCollection.ArchivedPosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, archive.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, archive.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); archivedCollection.ArchivedPosts.Add(medium.id, medium.files.full.url); archivedCollection.ArchivedPostMedia.Add(medium); } @@ -1487,7 +1487,7 @@ public class APIHelper : IAPIHelper { if (!archivedCollection.ArchivedPosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, archive.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, archive.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); archivedCollection.ArchivedPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{archive.id}"); archivedCollection.ArchivedPostMedia.Add(medium); } @@ -1581,7 +1581,7 @@ public class APIHelper : IAPIHelper } if (!config.IgnoreOwnMessages || list.fromUser.id != Convert.ToInt32(auth.USER_ID)) { - await m_DBHelper.AddMessage(folder, list.id, list.text != null ? list.text : string.Empty, list.price != null ? list.price.ToString() : "0", list.canPurchaseReason == "opened" ? true : list.canPurchaseReason != "opened" ? false : (bool?)null ?? false, false, list.createdAt.HasValue ? list.createdAt.Value : DateTime.Now, list.fromUser != null && list.fromUser.id != null ? list.fromUser.id.Value : int.MinValue); + await dbHelper.AddMessage(folder, list.id, list.text != null ? list.text : string.Empty, list.price != null ? list.price.ToString() : "0", list.canPurchaseReason == "opened" ? true : list.canPurchaseReason != "opened" ? false : (bool?)null ?? false, false, list.createdAt.HasValue ? list.createdAt.Value : DateTime.Now, list.fromUser != null && list.fromUser.id != null ? list.fromUser.id.Value : int.MinValue); messageCollection.MessageObjects.Add(list); if (list.canPurchaseReason != "opened" && list.media != null && list.media.Count > 0) { @@ -1607,7 +1607,7 @@ public class APIHelper : IAPIHelper } if (!messageCollection.Messages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, medium.files.full.url); messageCollection.MessageMedia.Add(medium); } @@ -1632,7 +1632,7 @@ public class APIHelper : IAPIHelper } if (!messageCollection.Messages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); messageCollection.MessageMedia.Add(medium); } @@ -1663,7 +1663,7 @@ public class APIHelper : IAPIHelper } if (!messageCollection.Messages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, medium.files.full.url); messageCollection.MessageMedia.Add(medium); } @@ -1688,7 +1688,7 @@ public class APIHelper : IAPIHelper } if (!messageCollection.Messages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); messageCollection.MessageMedia.Add(medium); } @@ -1734,7 +1734,7 @@ public class APIHelper : IAPIHelper if (!config.IgnoreOwnMessages || message.fromUser.id != Convert.ToInt32(auth.USER_ID)) { - await m_DBHelper.AddMessage(folder, message.id, message.text != null ? message.text : string.Empty, message.price != null ? message.price.ToString() : "0", true, false, message.createdAt.HasValue ? message.createdAt.Value : DateTime.Now, message.fromUser != null && message.fromUser.id != null ? message.fromUser.id.Value : int.MinValue); + await dbHelper.AddMessage(folder, message.id, message.text != null ? message.text : string.Empty, message.price != null ? message.price.ToString() : "0", true, false, message.createdAt.HasValue ? message.createdAt.Value : DateTime.Now, message.fromUser != null && message.fromUser.id != null ? message.fromUser.id.Value : int.MinValue); singlePaidMessageCollection.SingleMessageObjects.Add(message); List messagePreviewIds = new(); if (message.previews != null && message.previews.Count > 0) @@ -1776,7 +1776,7 @@ public class APIHelper : IAPIHelper if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); singlePaidMessageCollection.SingleMessages.Add(medium.id, medium.files.full.url.ToString()); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } @@ -1802,7 +1802,7 @@ public class APIHelper : IAPIHelper if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, medium.files.full.url.ToString()); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } @@ -1828,7 +1828,7 @@ public class APIHelper : IAPIHelper if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); singlePaidMessageCollection.SingleMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } @@ -1854,7 +1854,7 @@ public class APIHelper : IAPIHelper if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } @@ -1945,11 +1945,11 @@ public class APIHelper : IAPIHelper { if (purchase.postedAt != null) { - await m_DBHelper.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); + await dbHelper.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); } else { - await m_DBHelper.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); + await dbHelper.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); } paidMessageCollection.PaidMessageObjects.Add(purchase); if (purchase.media != null && purchase.media.Count > 0) @@ -2007,7 +2007,7 @@ public class APIHelper : IAPIHelper } if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, medium.files.full.url); paidMessageCollection.PaidMessageMedia.Add(medium); } @@ -2032,7 +2032,7 @@ public class APIHelper : IAPIHelper } if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidMessageCollection.PaidMessageMedia.Add(medium); } @@ -2060,7 +2060,7 @@ public class APIHelper : IAPIHelper } if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, medium.files.full.url); paidMessageCollection.PaidMessageMedia.Add(medium); } @@ -2085,7 +2085,7 @@ public class APIHelper : IAPIHelper } if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidMessageCollection.PaidMessageMedia.Add(medium); } @@ -2406,7 +2406,7 @@ public class APIHelper : IAPIHelper } } } - await m_DBHelper.AddPost(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); + await dbHelper.AddPost(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); purchasedTabCollection.PaidPosts.PaidPostObjects.Add(purchase); foreach (Messages.Medium medium in purchase.media) { @@ -2434,7 +2434,7 @@ public class APIHelper : IAPIHelper if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, medium.files.full.url); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } @@ -2444,7 +2444,7 @@ public class APIHelper : IAPIHelper if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } @@ -2457,7 +2457,7 @@ public class APIHelper : IAPIHelper { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, medium.files.full.url); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } @@ -2466,7 +2466,7 @@ public class APIHelper : IAPIHelper { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } @@ -2477,11 +2477,11 @@ public class APIHelper : IAPIHelper case "message": if (purchase.postedAt != null) { - await m_DBHelper.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); + await dbHelper.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); } else { - await m_DBHelper.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); + await dbHelper.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); } purchasedTabCollection.PaidMessages.PaidMessageObjects.Add(purchase); if (purchase.media != null && purchase.media.Count > 0) @@ -2539,7 +2539,7 @@ public class APIHelper : IAPIHelper } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, medium.files.full.url); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } @@ -2564,7 +2564,7 @@ public class APIHelper : IAPIHelper } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } @@ -2592,7 +2592,7 @@ public class APIHelper : IAPIHelper } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, medium.files.full.url); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } @@ -2617,7 +2617,7 @@ public class APIHelper : IAPIHelper } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) { - await m_DBHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); + await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } diff --git a/OF DL/Helpers/AuthHelper.cs b/OF DL/Helpers/AuthHelper.cs index b8c47d9..33d522c 100644 --- a/OF DL/Helpers/AuthHelper.cs +++ b/OF DL/Helpers/AuthHelper.cs @@ -1,11 +1,12 @@ using OF_DL.Entities; +using OF_DL.Helpers.Interfaces; using PuppeteerSharp; using PuppeteerSharp.BrowserData; using Serilog; namespace OF_DL.Helpers; -public class AuthHelper +public class AuthHelper : IAuthHelper { private readonly LaunchOptions _options = new() { diff --git a/OF DL/Helpers/DownloadContext.cs b/OF DL/Helpers/DownloadContext.cs index cc65dcb..a3146d7 100644 --- a/OF DL/Helpers/DownloadContext.cs +++ b/OF DL/Helpers/DownloadContext.cs @@ -1,9 +1,4 @@ using OF_DL.Entities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OF_DL.Helpers { @@ -11,26 +6,23 @@ namespace OF_DL.Helpers { public IDownloadConfig DownloadConfig { get; } public IFileNameFormatConfig FileNameFormatConfig { get; } - public APIHelper ApiHelper { get; } - public DBHelper DBHelper { get; } - public DownloadHelper DownloadHelper { get; } + public IAPIHelper ApiHelper { get; } + public IDBHelper DBHelper { get; } + public IDownloadHelper DownloadHelper { get; } } - internal class DownloadContext : IDownloadContext + internal class DownloadContext( + IDownloadConfig downloadConfig, + IFileNameFormatConfig fileNameFormatConfig, + IAPIHelper apiHelper, + IDBHelper dBHelper, + IDownloadHelper downloadHelper) + : IDownloadContext { - public APIHelper ApiHelper { get; } - public DBHelper DBHelper { get; } - public DownloadHelper DownloadHelper { get; } - public IDownloadConfig DownloadConfig { get; } - public IFileNameFormatConfig FileNameFormatConfig { get; } - - public DownloadContext(Auth auth, IDownloadConfig downloadConfig, IFileNameFormatConfig fileNameFormatConfig, APIHelper apiHelper, DBHelper dBHelper) - { - ApiHelper = apiHelper; - DBHelper = dBHelper; - DownloadConfig = downloadConfig; - FileNameFormatConfig = fileNameFormatConfig; - DownloadHelper = new DownloadHelper(auth, downloadConfig, fileNameFormatConfig); - } + public IAPIHelper ApiHelper { get; } = apiHelper; + public IDBHelper DBHelper { get; } = dBHelper; + public IDownloadHelper DownloadHelper { get; } = downloadHelper; + public IDownloadConfig DownloadConfig { get; } = downloadConfig; + public IFileNameFormatConfig FileNameFormatConfig { get; } = fileNameFormatConfig; } } diff --git a/OF DL/Helpers/DownloadHelper.cs b/OF DL/Helpers/DownloadHelper.cs index f5ad8e2..8454bfc 100644 --- a/OF DL/Helpers/DownloadHelper.cs +++ b/OF DL/Helpers/DownloadHelper.cs @@ -36,17 +36,17 @@ namespace OF_DL.Helpers; public class DownloadHelper : IDownloadHelper { private readonly Auth auth; - private readonly IDBHelper m_DBHelper; - private readonly IFileNameHelper _FileNameHelper; + private readonly IDBHelper dbHelper; + private readonly IFileNameHelper fileNameHelper; private readonly IDownloadConfig downloadConfig; private readonly IFileNameFormatConfig fileNameFormatConfig; private TaskCompletionSource _completionSource; - public DownloadHelper(Auth auth, IDownloadConfig downloadConfig, IFileNameFormatConfig fileNameFormatConfig) + public DownloadHelper(Auth auth, IDownloadConfig downloadConfig, IFileNameFormatConfig fileNameFormatConfig, IDBHelper dbHelper, IFileNameHelper fileNameHelper) { this.auth = auth; - this.m_DBHelper = new DBHelper(downloadConfig); - this._FileNameHelper = new FileNameHelper(auth); + this.dbHelper = dbHelper; + this.fileNameHelper = fileNameHelper; this.downloadConfig = downloadConfig; this.fileNameFormatConfig = fileNameFormatConfig; } @@ -296,7 +296,7 @@ public class DownloadHelper : IDownloadHelper try { - if (!await m_DBHelper.CheckDownloaded(folder, media_id, api_type)) + if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) { return await HandleNewMedia(folder: folder, media_id: media_id, @@ -352,9 +352,9 @@ public class DownloadHelper : IDownloadHelper return false; } - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); var lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName); - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, resolvedFilename + extension, size, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, resolvedFilename + extension, size, true, lastModified); return true; } @@ -459,7 +459,7 @@ public class DownloadHelper : IDownloadHelper //finaly check which filename we should use. Custom or the server one. //if a custom is used, then the servefilename will be different from the resolved filename. string finalName = serverFilename == resolvedFilename ? serverFilename : resolvedFilename; - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, finalName + extension, fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, finalName + extension, fileSizeInBytes, true, lastModified); return status; } @@ -476,7 +476,7 @@ public class DownloadHelper : IDownloadHelper { if (downloadConfig.ShowScrapeSize) { - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); task.Increment(size); } else @@ -724,7 +724,7 @@ public class DownloadHelper : IDownloadHelper Uri uri = new(url); string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, folder.Split("/")[^1], users, _FileNameHelper, CustomFileNameOption.ReturnOriginal); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); } @@ -742,7 +742,7 @@ public class DownloadHelper : IDownloadHelper Uri uri = new(url); string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, folder.Split("/")[^1], users, _FileNameHelper, CustomFileNameOption.ReturnOriginal); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); } @@ -760,7 +760,7 @@ public class DownloadHelper : IDownloadHelper Uri uri = new(url); string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, streamInfo, streamMedia, author, folder.Split("/")[^1], users, _FileNameHelper, CustomFileNameOption.ReturnOriginal); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, streamInfo, streamMedia, author, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); } @@ -779,7 +779,7 @@ public class DownloadHelper : IDownloadHelper } Uri uri = new(url); string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, _FileNameHelper, CustomFileNameOption.ReturnOriginal); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); } @@ -796,7 +796,7 @@ public class DownloadHelper : IDownloadHelper } Uri uri = new(url); string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, _FileNameHelper, CustomFileNameOption.ReturnOriginal); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); } @@ -806,7 +806,7 @@ public class DownloadHelper : IDownloadHelper string path = "/Archived/Posts/Free"; Uri uri = new(url); string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, author, folder.Split("/")[^1], users, _FileNameHelper, CustomFileNameOption.ReturnOriginal); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, author, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); } @@ -833,7 +833,7 @@ public class DownloadHelper : IDownloadHelper } Uri uri = new(url); string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, _FileNameHelper, CustomFileNameOption.ReturnOriginal); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); } @@ -850,7 +850,7 @@ public class DownloadHelper : IDownloadHelper } Uri uri = new(url); string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, _FileNameHelper, CustomFileNameOption.ReturnOriginal); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); } @@ -876,7 +876,7 @@ public class DownloadHelper : IDownloadHelper } Uri uri = new(url); string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, _FileNameHelper, CustomFileNameOption.ReturnOriginal); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); } @@ -1014,7 +1014,7 @@ public class DownloadHelper : IDownloadHelper task.Increment(1); } - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } catch (Exception ex) { @@ -1077,11 +1077,11 @@ public class DownloadHelper : IDownloadHelper { properties.Add(match.Groups[1].Value); } - Dictionary values = await _FileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1],users); - customFileName = await _FileNameHelper.BuildFilename(filenameFormat, values); + Dictionary values = await fileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1],users); + customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); } - if (!await m_DBHelper.CheckDownloaded(folder, media_id, api_type)) + if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) { if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { @@ -1098,7 +1098,7 @@ public class DownloadHelper : IDownloadHelper { task.Increment(1); } - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } } else @@ -1122,14 +1122,14 @@ public class DownloadHelper : IDownloadHelper Console.WriteLine($"An error occurred: {ex.Message}"); return false; } - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } } if (downloadConfig.ShowScrapeSize) { - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); task.Increment(size); } else @@ -1184,11 +1184,11 @@ public class DownloadHelper : IDownloadHelper { properties.Add(match.Groups[1].Value); } - Dictionary values = await _FileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1],users); - customFileName = await _FileNameHelper.BuildFilename(filenameFormat, values); + Dictionary values = await fileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1],users); + customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); } - if (!await m_DBHelper.CheckDownloaded(folder, media_id, api_type)) + if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) { if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { @@ -1205,7 +1205,7 @@ public class DownloadHelper : IDownloadHelper { task.Increment(1); } - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } } else @@ -1229,14 +1229,14 @@ public class DownloadHelper : IDownloadHelper Console.WriteLine($"An error occurred: {ex.Message}"); return false; } - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } } if (downloadConfig.ShowScrapeSize) { - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); task.Increment(size); } else @@ -1291,11 +1291,11 @@ public class DownloadHelper : IDownloadHelper { properties.Add(match.Groups[1].Value); } - Dictionary values = await _FileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); - customFileName = await _FileNameHelper.BuildFilename(filenameFormat, values); + Dictionary values = await fileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); + customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); } - if (!await m_DBHelper.CheckDownloaded(folder, media_id, api_type)) + if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) { if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { @@ -1312,7 +1312,7 @@ public class DownloadHelper : IDownloadHelper { task.Increment(1); } - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } } else @@ -1336,14 +1336,14 @@ public class DownloadHelper : IDownloadHelper Console.WriteLine($"An error occurred: {ex.Message}"); return false; } - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } } if (downloadConfig.ShowScrapeSize) { - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); task.Increment(size); } else @@ -1397,11 +1397,11 @@ public class DownloadHelper : IDownloadHelper { properties.Add(match.Groups[1].Value); } - Dictionary values = await _FileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); - customFileName = await _FileNameHelper.BuildFilename(filenameFormat, values); + Dictionary values = await fileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); + customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); } - if (!await m_DBHelper.CheckDownloaded(folder, media_id, api_type)) + if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) { if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { @@ -1418,7 +1418,7 @@ public class DownloadHelper : IDownloadHelper { task.Increment(1); } - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } } else @@ -1442,14 +1442,14 @@ public class DownloadHelper : IDownloadHelper Console.WriteLine($"An error occurred: {ex.Message}"); return false; } - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } } if (downloadConfig.ShowScrapeSize) { - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); task.Increment(size); } else @@ -1504,11 +1504,11 @@ public class DownloadHelper : IDownloadHelper { properties.Add(match.Groups[1].Value); } - Dictionary values = await _FileNameHelper.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); - customFileName = await _FileNameHelper.BuildFilename(filenameFormat, values); + Dictionary values = await fileNameHelper.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); + customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); } - if (!await m_DBHelper.CheckDownloaded(folder, media_id, api_type)) + if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) { if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { @@ -1525,7 +1525,7 @@ public class DownloadHelper : IDownloadHelper { task.Increment(1); } - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } } else @@ -1549,14 +1549,14 @@ public class DownloadHelper : IDownloadHelper Console.WriteLine($"An error occurred: {ex.Message}"); return false; } - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } } if (downloadConfig.ShowScrapeSize) { - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); task.Increment(size); } else @@ -1609,11 +1609,11 @@ public class DownloadHelper : IDownloadHelper { properties.Add(match.Groups[1].Value); } - Dictionary values = await _FileNameHelper.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); - customFileName = await _FileNameHelper.BuildFilename(filenameFormat, values); + Dictionary values = await fileNameHelper.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); + customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); } - if (!await m_DBHelper.CheckDownloaded(folder, media_id, api_type)) + if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) { if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { @@ -1630,7 +1630,7 @@ public class DownloadHelper : IDownloadHelper { task.Increment(1); } - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } } else @@ -1654,14 +1654,14 @@ public class DownloadHelper : IDownloadHelper Console.WriteLine($"An error occurred: {ex.Message}"); return false; } - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } } if (downloadConfig.ShowScrapeSize) { - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); task.Increment(size); } else @@ -1714,11 +1714,11 @@ public class DownloadHelper : IDownloadHelper { properties.Add(match.Groups[1].Value); } - Dictionary values = await _FileNameHelper.GetFilename(streamInfo, streamMedia, author, properties, folder.Split("/")[^1], users); - customFileName = await _FileNameHelper.BuildFilename(filenameFormat, values); + Dictionary values = await fileNameHelper.GetFilename(streamInfo, streamMedia, author, properties, folder.Split("/")[^1], users); + customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); } - if (!await m_DBHelper.CheckDownloaded(folder, media_id, api_type)) + if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) { if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { @@ -1735,7 +1735,7 @@ public class DownloadHelper : IDownloadHelper { task.Increment(1); } - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } } else @@ -1759,14 +1759,14 @@ public class DownloadHelper : IDownloadHelper Console.WriteLine($"An error occurred: {ex.Message}"); return false; } - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } } if (downloadConfig.ShowScrapeSize) { - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); task.Increment(size); } else @@ -1821,11 +1821,11 @@ public class DownloadHelper : IDownloadHelper { properties.Add(match.Groups[1].Value); } - Dictionary values = await _FileNameHelper.GetFilename(postInfo, postMedia, fromUser, properties, folder.Split("/")[^1], users); - customFileName = await _FileNameHelper.BuildFilename(filenameFormat, values); + Dictionary values = await fileNameHelper.GetFilename(postInfo, postMedia, fromUser, properties, folder.Split("/")[^1], users); + customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); } - if (!await m_DBHelper.CheckDownloaded(folder, media_id, api_type)) + if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) { if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { @@ -1842,7 +1842,7 @@ public class DownloadHelper : IDownloadHelper { task.Increment(1); } - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } } else @@ -1866,14 +1866,14 @@ public class DownloadHelper : IDownloadHelper Console.WriteLine($"An error occurred: {ex.Message}"); return false; } - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } } if (downloadConfig.ShowScrapeSize) { - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); task.Increment(size); } else @@ -1920,11 +1920,11 @@ public class DownloadHelper : IDownloadHelper { properties.Add(match.Groups[1].Value); } - Dictionary values = await _FileNameHelper.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); - customFileName = await _FileNameHelper.BuildFilename(filenameFormat, values); + Dictionary values = await fileNameHelper.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); + customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); } - if (!await m_DBHelper.CheckDownloaded(folder, media_id, api_type)) + if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) { if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { @@ -1941,7 +1941,7 @@ public class DownloadHelper : IDownloadHelper { task.Increment(1); } - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } } else @@ -1965,14 +1965,14 @@ public class DownloadHelper : IDownloadHelper Console.WriteLine($"An error occurred: {ex.Message}"); return false; } - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); - await m_DBHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); + await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } } if (downloadConfig.ShowScrapeSize) { - long size = await m_DBHelper.GetStoredFileSize(folder, media_id, api_type); + long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); task.Increment(size); } else diff --git a/OF DL/Helpers/Interfaces/IAPIHelper.cs b/OF DL/Helpers/Interfaces/IAPIHelper.cs index d94d509..a917fff 100644 --- a/OF DL/Helpers/Interfaces/IAPIHelper.cs +++ b/OF DL/Helpers/Interfaces/IAPIHelper.cs @@ -26,6 +26,7 @@ namespace OF_DL.Helpers Task GetArchived(string endpoint, string folder, IDownloadConfig config, StatusContext ctx); Task GetMessages(string endpoint, string folder, IDownloadConfig config, StatusContext ctx); Task GetPaidMessages(string endpoint, string folder, string username, IDownloadConfig config, StatusContext ctx); + Task GetPaidMessage(string endpoint, string folder, IDownloadConfig config); Task> GetPurchasedTabUsers(string endpoint, IDownloadConfig config, Dictionary users); Task> GetPurchasedTab(string endpoint, string folder, IDownloadConfig config, Dictionary users); Task GetUserInfo(string endpoint); diff --git a/OF DL/Helpers/Interfaces/IAuthHelper.cs b/OF DL/Helpers/Interfaces/IAuthHelper.cs new file mode 100644 index 0000000..e1d0726 --- /dev/null +++ b/OF DL/Helpers/Interfaces/IAuthHelper.cs @@ -0,0 +1,10 @@ +using OF_DL.Entities; + +namespace OF_DL.Helpers.Interfaces +{ + public interface IAuthHelper + { + Task SetupBrowser(bool runningInDocker); + Task GetAuthFromBrowser(bool isDocker = false); + } +} diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 2c005bb..07f3c6b 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -20,6 +20,7 @@ using static OF_DL.Entities.Messages.Messages; using Akka.Configuration; using System.Text; using Microsoft.Extensions.DependencyInjection; +using OF_DL.Helpers.Interfaces; namespace OF_DL; @@ -31,12 +32,16 @@ public class Program private static bool clientIdBlobMissing = false; private static bool devicePrivateKeyMissing = false; private readonly Entities.Config config; + private readonly IAuthHelper authHelper; + private readonly IServiceProvider serviceProvider; private Auth? auth = null; private static LoggingLevelSwitch levelSwitch = new LoggingLevelSwitch(); - public Program(Entities.Config config) + public Program(Entities.Config config, IAuthHelper authHelper, IServiceProvider serviceProvider) { this.config = config; + this.authHelper = authHelper; + this.serviceProvider = serviceProvider; } private async Task LoadAuthFromBrowser() @@ -45,7 +50,6 @@ public class Program try { - AuthHelper authHelper = new(); Task setupBrowserTask = authHelper.SetupBrowser(runningInDocker); Task.Delay(1000).Wait(); @@ -140,6 +144,15 @@ public class Program private static void ConfigureServices(IServiceCollection services, Entities.Config config) { services.AddSingleton(config); + services.AddSingleton(config); + services.AddSingleton(config); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); } @@ -910,7 +923,7 @@ public class Program } //Check if auth is valid - var apiHelper = new APIHelper(auth, config); + var apiHelper = ActivatorUtilities.CreateInstance(serviceProvider, auth); Entities.User? validate = await apiHelper.GetUserInfo($"/users/me"); if (validate == null || (validate?.name == null && validate?.username == null)) @@ -962,9 +975,9 @@ public class Program } } - private static async Task DownloadAllData(APIHelper m_ApiHelper, Auth Auth, Entities.Config Config) + private async Task DownloadAllData(IAPIHelper m_ApiHelper, Auth Auth, Entities.Config Config) { - DBHelper dBHelper = new DBHelper(Config); + IDBHelper dBHelper = serviceProvider.GetRequiredService(); Log.Debug("Calling DownloadAllData"); @@ -1096,7 +1109,12 @@ public class Program await dBHelper.CreateDB(path); - var downloadContext = new DownloadContext(Auth, Config, GetCreatorFileNameFormatConfig(Config, username), m_ApiHelper, dBHelper); + var downloadContext = ActivatorUtilities.CreateInstance( + serviceProvider, + Config, + GetCreatorFileNameFormatConfig(Config, username), + m_ApiHelper, + dBHelper); await DownloadSinglePost(downloadContext, post_id, path, users); } @@ -1168,7 +1186,12 @@ public class Program Log.Debug($"Download path: {path}"); - var downloadContext = new DownloadContext(Auth, Config, GetCreatorFileNameFormatConfig(Config, purchasedTabCollection.Username), m_ApiHelper, dBHelper); + var downloadContext = ActivatorUtilities.CreateInstance( + serviceProvider, + Config, + GetCreatorFileNameFormatConfig(Config, purchasedTabCollection.Username), + m_ApiHelper, + dBHelper); int paidPostCount = 0; int paidMessagesCount = 0; @@ -1256,7 +1279,12 @@ public class Program await dBHelper.CreateDB(path); - var downloadContext = new DownloadContext(Auth, Config, GetCreatorFileNameFormatConfig(Config, username), m_ApiHelper, dBHelper); + var downloadContext = ActivatorUtilities.CreateInstance( + serviceProvider, + Config, + GetCreatorFileNameFormatConfig(Config, username), + m_ApiHelper, + dBHelper); await DownloadPaidMessage(downloadContext, hasSelectedUsersKVP, username, 1, path, message_id); } @@ -1306,7 +1334,12 @@ public class Program await dBHelper.CreateDB(path); - var downloadContext = new DownloadContext(Auth, Config, GetCreatorFileNameFormatConfig(Config, user.Key), m_ApiHelper, dBHelper); + var downloadContext = ActivatorUtilities.CreateInstance( + serviceProvider, + Config, + GetCreatorFileNameFormatConfig(Config, user.Key), + m_ApiHelper, + dBHelper); if (Config.DownloadAvatarHeaderPhoto) { @@ -2958,7 +2991,7 @@ public class Program } } - public static async Task<(bool IsExit, Dictionary? selectedUsers, Entities.Config? updatedConfig)> HandleUserSelection(APIHelper apiHelper, Entities.Config currentConfig, Dictionary users, Dictionary lists) + public static async Task<(bool IsExit, Dictionary? selectedUsers, Entities.Config? updatedConfig)> HandleUserSelection(IAPIHelper apiHelper, Entities.Config currentConfig, Dictionary users, Dictionary lists) { bool hasSelectedUsers = false; Dictionary selectedUsers = new Dictionary(); From d7bae3e260b9eee9e08351d2da734d07b853eee0 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Thu, 5 Feb 2026 02:21:05 -0600 Subject: [PATCH 03/55] Move config and logging into services --- OF DL/Program.cs | 865 ++++-------------------------- OF DL/Services/AuthService.cs | 72 +++ OF DL/Services/ConfigService.cs | 385 +++++++++++++ OF DL/Services/IAuthService.cs | 12 + OF DL/Services/IConfigService.cs | 13 + OF DL/Services/ILoggingService.cs | 12 + OF DL/Services/LoggingService.cs | 42 ++ 7 files changed, 649 insertions(+), 752 deletions(-) create mode 100644 OF DL/Services/AuthService.cs create mode 100644 OF DL/Services/ConfigService.cs create mode 100644 OF DL/Services/IAuthService.cs create mode 100644 OF DL/Services/IConfigService.cs create mode 100644 OF DL/Services/ILoggingService.cs create mode 100644 OF DL/Services/LoggingService.cs diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 07f3c6b..d79378b 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -9,144 +9,125 @@ using OF_DL.Entities.Streams; using OF_DL.Enumerations; using OF_DL.Enumurations; using OF_DL.Helpers; +using OF_DL.Services; using Serilog; -using Serilog.Core; -using Serilog.Events; using Spectre.Console; using System.Reflection; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using static OF_DL.Entities.Messages.Messages; -using Akka.Configuration; -using System.Text; using Microsoft.Extensions.DependencyInjection; using OF_DL.Helpers.Interfaces; namespace OF_DL; -public class Program +public class Program(IServiceProvider serviceProvider) { public int MAX_AGE = 0; public static List paid_post_ids = new(); private static bool clientIdBlobMissing = false; private static bool devicePrivateKeyMissing = false; - private readonly Entities.Config config; - private readonly IAuthHelper authHelper; - private readonly IServiceProvider serviceProvider; - private Auth? auth = null; - private static LoggingLevelSwitch levelSwitch = new LoggingLevelSwitch(); - public Program(Entities.Config config, IAuthHelper authHelper, IServiceProvider serviceProvider) - { - this.config = config; - this.authHelper = authHelper; - this.serviceProvider = serviceProvider; - } - - private async Task LoadAuthFromBrowser() + private async Task LoadAuthFromBrowser() { + var authService = serviceProvider.GetRequiredService(); bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; - try + // Show initial message + AnsiConsole.MarkupLine($"[yellow]Downloading dependencies. Please wait ...[/]"); + + // Show instructions based on the environment + await Task.Delay(5000); + if (runningInDocker) { - Task setupBrowserTask = authHelper.SetupBrowser(runningInDocker); - - Task.Delay(1000).Wait(); - if (!setupBrowserTask.IsCompleted) - { - AnsiConsole.MarkupLine($"[yellow]Downloading dependencies. Please wait ...[/]"); - } - setupBrowserTask.Wait(); - - Task getAuthTask = authHelper.GetAuthFromBrowser(); - Task.Delay(5000).Wait(); - if (!getAuthTask.IsCompleted) - { - 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]Note: Some users have reported that \"Sign in with Google\" has not been working with the new authentication method.[/]"); - 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[/]"); - } - } - auth = await getAuthTask; + 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.[/]"); } - catch (Exception e) + 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]Note: Some users have reported that \"Sign in with Google\" has not been working with the new authentication method.[/]"); + 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(e, "auth invalid after attempt to get auth from browser"); - - Environment.Exit(2); - } - - if (auth == 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($"[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); } - else - { - await File.WriteAllTextAsync("auth.json", JsonConvert.SerializeObject(auth, Formatting.Indented)); - } + + await authService.SaveToFileAsync(); } public static async Task Main(string[] args) { - levelSwitch.MinimumLevel = LogEventLevel.Error; //set initial level (until we've read from config) - - Log.Logger = new LoggerConfiguration() - .MinimumLevel.ControlledBy(levelSwitch) - .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day) - .CreateLogger(); - 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"); - // Load configuration - Entities.Config? config = LoadConfiguration(args, out bool cliNonInteractive); - if (config == null) + // Set up dependency injection with LoggingService and ConfigService + ServiceCollection services = new(); + services.AddSingleton(); + services.AddSingleton(); + ServiceProvider tempServiceProvider = services.BuildServiceProvider(); + + // Load configuration using ConfigService + IConfigService configService = tempServiceProvider.GetRequiredService(); + + if (!await configService.LoadConfigurationAsync(args)) { + AnsiConsole.MarkupLine($"\n[red]config.conf is not valid, check your syntax![/]\n"); + AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); + if (!configService.IsCliNonInteractive) + { + Console.ReadKey(); + } Environment.Exit(3); return; } - // Set up dependency injection - var services = new ServiceCollection(); - ConfigureServices(services, config); + if (configService.CurrentConfig == null) + { + AnsiConsole.MarkupLine($"\n[red]Failed to load configuration.[/]\n"); + Environment.Exit(3); + return; + } + + AnsiConsole.Markup("[green]config.conf located successfully!\n[/]"); + + // Set up full dependency injection with loaded config + services = new ServiceCollection(); + ConfigureServices(services, configService.CurrentConfig, tempServiceProvider.GetRequiredService()); var serviceProvider = services.BuildServiceProvider(); // Get the Program instance and run var program = serviceProvider.GetRequiredService(); - await program.RunAsync(args, cliNonInteractive); + await program.RunAsync(args, configService.IsCliNonInteractive); } - private static void ConfigureServices(IServiceCollection services, Entities.Config config) + private static void ConfigureServices(IServiceCollection services, Entities.Config config, ILoggingService loggingService) { services.AddSingleton(config); services.AddSingleton(config); services.AddSingleton(config); + services.AddSingleton(loggingService); + services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -156,409 +137,11 @@ public class Program services.AddSingleton(); } - private static Entities.Config? LoadConfiguration(string[] args, out bool cliNonInteractive) - { - cliNonInteractive = false; - Entities.Config? config = null; - - try - { - //Remove config.json and convert to config.conf - if (File.Exists("config.json")) - { - AnsiConsole.Markup("[green]config.json located successfully!\n[/]"); - try - { - string jsonText = File.ReadAllText("config.json"); - var jsonConfig = JsonConvert.DeserializeObject(jsonText); - - if (jsonConfig != null) - { - var hoconConfig = new StringBuilder(); - hoconConfig.AppendLine("# Auth"); - hoconConfig.AppendLine("Auth {"); - hoconConfig.AppendLine($" DisableBrowserAuth = {jsonConfig.DisableBrowserAuth.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - hoconConfig.AppendLine("# External Tools"); - hoconConfig.AppendLine("External {"); - hoconConfig.AppendLine($" FFmpegPath = \"{jsonConfig.FFmpegPath}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Download Settings"); - hoconConfig.AppendLine("Download {"); - hoconConfig.AppendLine(" Media {"); - hoconConfig.AppendLine($" DownloadAvatarHeaderPhoto = {jsonConfig.DownloadAvatarHeaderPhoto.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPaidPosts = {jsonConfig.DownloadPaidPosts.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPosts = {jsonConfig.DownloadPosts.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadArchived = {jsonConfig.DownloadArchived.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadStreams = {jsonConfig.DownloadStreams.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadStories = {jsonConfig.DownloadStories.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadHighlights = {jsonConfig.DownloadHighlights.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadMessages = {jsonConfig.DownloadMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPaidMessages = {jsonConfig.DownloadPaidMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadImages = {jsonConfig.DownloadImages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadVideos = {jsonConfig.DownloadVideos.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadAudios = {jsonConfig.DownloadAudios.ToString().ToLower()}"); - hoconConfig.AppendLine(" }"); - hoconConfig.AppendLine($" IgnoreOwnMessages = {jsonConfig.IgnoreOwnMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPostsIncrementally = {jsonConfig.DownloadPostsIncrementally.ToString().ToLower()}"); - hoconConfig.AppendLine($" BypassContentForCreatorsWhoNoLongerExist = {jsonConfig.BypassContentForCreatorsWhoNoLongerExist.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadDuplicatedMedia = {jsonConfig.DownloadDuplicatedMedia.ToString().ToLower()}"); - hoconConfig.AppendLine($" SkipAds = {jsonConfig.SkipAds.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPath = \"{jsonConfig.DownloadPath}\""); - hoconConfig.AppendLine($" DownloadOnlySpecificDates = {jsonConfig.DownloadOnlySpecificDates.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadDateSelection = \"{jsonConfig.DownloadDateSelection.ToString().ToLower()}\""); - hoconConfig.AppendLine($" CustomDate = \"{jsonConfig.CustomDate?.ToString("yyyy-MM-dd")}\""); - hoconConfig.AppendLine($" ShowScrapeSize = {jsonConfig.ShowScrapeSize.ToString().ToLower()}"); - hoconConfig.AppendLine($" DisableTextSanitization = false"); - hoconConfig.AppendLine($" DownloadVideoResolution = \"{(jsonConfig.DownloadVideoResolution == VideoResolution.source ? "source" : jsonConfig.DownloadVideoResolution.ToString().TrimStart('_'))}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# File Settings"); - hoconConfig.AppendLine("File {"); - hoconConfig.AppendLine($" PaidPostFileNameFormat = \"{jsonConfig.PaidPostFileNameFormat}\""); - hoconConfig.AppendLine($" PostFileNameFormat = \"{jsonConfig.PostFileNameFormat}\""); - hoconConfig.AppendLine($" PaidMessageFileNameFormat = \"{jsonConfig.PaidMessageFileNameFormat}\""); - hoconConfig.AppendLine($" MessageFileNameFormat = \"{jsonConfig.MessageFileNameFormat}\""); - hoconConfig.AppendLine($" RenameExistingFilesWhenCustomFormatIsSelected = {jsonConfig.RenameExistingFilesWhenCustomFormatIsSelected.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Creator-Specific Configurations"); - hoconConfig.AppendLine("CreatorConfigs {"); - foreach (var creatorConfig in jsonConfig.CreatorConfigs) - { - hoconConfig.AppendLine($" \"{creatorConfig.Key}\" {{"); - hoconConfig.AppendLine($" PaidPostFileNameFormat = \"{creatorConfig.Value.PaidPostFileNameFormat}\""); - hoconConfig.AppendLine($" PostFileNameFormat = \"{creatorConfig.Value.PostFileNameFormat}\""); - hoconConfig.AppendLine($" PaidMessageFileNameFormat = \"{creatorConfig.Value.PaidMessageFileNameFormat}\""); - hoconConfig.AppendLine($" MessageFileNameFormat = \"{creatorConfig.Value.MessageFileNameFormat}\""); - hoconConfig.AppendLine(" }"); - } - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Folder Settings"); - hoconConfig.AppendLine("Folder {"); - hoconConfig.AppendLine($" FolderPerPaidPost = {jsonConfig.FolderPerPaidPost.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerPost = {jsonConfig.FolderPerPost.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerPaidMessage = {jsonConfig.FolderPerPaidMessage.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerMessage = {jsonConfig.FolderPerMessage.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Subscription Settings"); - hoconConfig.AppendLine("Subscriptions {"); - hoconConfig.AppendLine($" IncludeExpiredSubscriptions = {jsonConfig.IncludeExpiredSubscriptions.ToString().ToLower()}"); - hoconConfig.AppendLine($" IncludeRestrictedSubscriptions = {jsonConfig.IncludeRestrictedSubscriptions.ToString().ToLower()}"); - hoconConfig.AppendLine($" IgnoredUsersListName = \"{jsonConfig.IgnoredUsersListName}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Interaction Settings"); - hoconConfig.AppendLine("Interaction {"); - hoconConfig.AppendLine($" NonInteractiveMode = {jsonConfig.NonInteractiveMode.ToString().ToLower()}"); - hoconConfig.AppendLine($" NonInteractiveModeListName = \"{jsonConfig.NonInteractiveModeListName}\""); - hoconConfig.AppendLine($" NonInteractiveModePurchasedTab = {jsonConfig.NonInteractiveModePurchasedTab.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Performance Settings"); - hoconConfig.AppendLine("Performance {"); - hoconConfig.AppendLine($" Timeout = {(jsonConfig.Timeout.HasValue ? jsonConfig.Timeout.Value : -1)}"); - hoconConfig.AppendLine($" LimitDownloadRate = {jsonConfig.LimitDownloadRate.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadLimitInMbPerSec = {jsonConfig.DownloadLimitInMbPerSec}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Logging/Debug Settings"); - hoconConfig.AppendLine("Logging {"); - hoconConfig.AppendLine($" LoggingLevel = \"{jsonConfig.LoggingLevel.ToString().ToLower()}\""); - hoconConfig.AppendLine("}"); - - File.WriteAllText("config.conf", hoconConfig.ToString()); - File.Delete("config.json"); - AnsiConsole.Markup("[green]config.conf created successfully from config.json!\n[/]"); - } - } - catch (Exception e) - { - Console.WriteLine(e); - AnsiConsole.MarkupLine($"\n[red]config.conf is not valid, check your syntax![/]\n"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - Log.Error("config.conf processing failed.", e.Message); - - if (!cliNonInteractive) - { - Console.ReadKey(); - } - Environment.Exit(3); - } - } - - //I dont like it... but I needed to move config here, otherwise the logging level gets changed too late after we missed a whole bunch of important info - if (File.Exists("config.conf")) - { - AnsiConsole.Markup("[green]config.conf located successfully!\n[/]"); - try - { - string hoconText = File.ReadAllText("config.conf"); - - var hoconConfig = ConfigurationFactory.ParseString(hoconText); - - config = new Entities.Config - { - //Auth - DisableBrowserAuth = hoconConfig.GetBoolean("Auth.DisableBrowserAuth"), - - // FFmpeg Settings - FFmpegPath = hoconConfig.GetString("External.FFmpegPath"), - - // Download Settings - DownloadAvatarHeaderPhoto = hoconConfig.GetBoolean("Download.Media.DownloadAvatarHeaderPhoto"), - DownloadPaidPosts = hoconConfig.GetBoolean("Download.Media.DownloadPaidPosts"), - DownloadPosts = hoconConfig.GetBoolean("Download.Media.DownloadPosts"), - DownloadArchived = hoconConfig.GetBoolean("Download.Media.DownloadArchived"), - DownloadStreams = hoconConfig.GetBoolean("Download.Media.DownloadStreams"), - DownloadStories = hoconConfig.GetBoolean("Download.Media.DownloadStories"), - DownloadHighlights = hoconConfig.GetBoolean("Download.Media.DownloadHighlights"), - DownloadMessages = hoconConfig.GetBoolean("Download.Media.DownloadMessages"), - DownloadPaidMessages = hoconConfig.GetBoolean("Download.Media.DownloadPaidMessages"), - DownloadImages = hoconConfig.GetBoolean("Download.Media.DownloadImages"), - DownloadVideos = hoconConfig.GetBoolean("Download.Media.DownloadVideos"), - DownloadAudios = hoconConfig.GetBoolean("Download.Media.DownloadAudios"), - IgnoreOwnMessages = hoconConfig.GetBoolean("Download.IgnoreOwnMessages"), - DownloadPostsIncrementally = hoconConfig.GetBoolean("Download.DownloadPostsIncrementally"), - BypassContentForCreatorsWhoNoLongerExist = hoconConfig.GetBoolean("Download.BypassContentForCreatorsWhoNoLongerExist"), - DownloadDuplicatedMedia = hoconConfig.GetBoolean("Download.DownloadDuplicatedMedia"), - SkipAds = hoconConfig.GetBoolean("Download.SkipAds"), - DownloadPath = hoconConfig.GetString("Download.DownloadPath"), - DownloadOnlySpecificDates = hoconConfig.GetBoolean("Download.DownloadOnlySpecificDates"), - DownloadDateSelection = Enum.Parse(hoconConfig.GetString("Download.DownloadDateSelection"), true), - CustomDate = !string.IsNullOrWhiteSpace(hoconConfig.GetString("Download.CustomDate")) ? DateTime.Parse(hoconConfig.GetString("Download.CustomDate")) : null, - ShowScrapeSize = hoconConfig.GetBoolean("Download.ShowScrapeSize"), - // Optional flag; default to false when missing - DisableTextSanitization = bool.TryParse(hoconConfig.GetString("Download.DisableTextSanitization", "false"), out var dts) ? dts : false, - DownloadVideoResolution = ParseVideoResolution(hoconConfig.GetString("Download.DownloadVideoResolution", "source")), - - // File Settings - PaidPostFileNameFormat = hoconConfig.GetString("File.PaidPostFileNameFormat"), - PostFileNameFormat = hoconConfig.GetString("File.PostFileNameFormat"), - PaidMessageFileNameFormat = hoconConfig.GetString("File.PaidMessageFileNameFormat"), - MessageFileNameFormat = hoconConfig.GetString("File.MessageFileNameFormat"), - RenameExistingFilesWhenCustomFormatIsSelected = hoconConfig.GetBoolean("File.RenameExistingFilesWhenCustomFormatIsSelected"), - - // Folder Settings - FolderPerPaidPost = hoconConfig.GetBoolean("Folder.FolderPerPaidPost"), - FolderPerPost = hoconConfig.GetBoolean("Folder.FolderPerPost"), - FolderPerPaidMessage = hoconConfig.GetBoolean("Folder.FolderPerPaidMessage"), - FolderPerMessage = hoconConfig.GetBoolean("Folder.FolderPerMessage"), - - // Subscription Settings - IncludeExpiredSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeExpiredSubscriptions"), - IncludeRestrictedSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeRestrictedSubscriptions"), - IgnoredUsersListName = hoconConfig.GetString("Subscriptions.IgnoredUsersListName"), - - // Interaction Settings - NonInteractiveMode = hoconConfig.GetBoolean("Interaction.NonInteractiveMode"), - NonInteractiveModeListName = hoconConfig.GetString("Interaction.NonInteractiveModeListName"), - NonInteractiveModePurchasedTab = hoconConfig.GetBoolean("Interaction.NonInteractiveModePurchasedTab"), - - // Performance Settings - Timeout = string.IsNullOrWhiteSpace(hoconConfig.GetString("Performance.Timeout")) ? -1 : hoconConfig.GetInt("Performance.Timeout"), - LimitDownloadRate = hoconConfig.GetBoolean("Performance.LimitDownloadRate"), - DownloadLimitInMbPerSec = hoconConfig.GetInt("Performance.DownloadLimitInMbPerSec"), - - // Logging/Debug Settings - LoggingLevel = Enum.Parse(hoconConfig.GetString("Logging.LoggingLevel"), true) - }; - - ValidateFileNameFormat(config.PaidPostFileNameFormat, "PaidPostFileNameFormat"); - ValidateFileNameFormat(config.PostFileNameFormat, "PostFileNameFormat"); - ValidateFileNameFormat(config.PaidMessageFileNameFormat, "PaidMessageFileNameFormat"); - ValidateFileNameFormat(config.MessageFileNameFormat, "MessageFileNameFormat"); - - var creatorConfigsSection = hoconConfig.GetConfig("CreatorConfigs"); - if (creatorConfigsSection != null) - { - foreach (var key in creatorConfigsSection.AsEnumerable()) - { - var creatorKey = key.Key; - var creatorHocon = creatorConfigsSection.GetConfig(creatorKey); - if (!config.CreatorConfigs.ContainsKey(creatorKey) && creatorHocon != null) - { - config.CreatorConfigs.Add(key.Key, new CreatorConfig - { - PaidPostFileNameFormat = creatorHocon.GetString("PaidPostFileNameFormat"), - PostFileNameFormat = creatorHocon.GetString("PostFileNameFormat"), - PaidMessageFileNameFormat = creatorHocon.GetString("PaidMessageFileNameFormat"), - MessageFileNameFormat = creatorHocon.GetString("MessageFileNameFormat") - }); - - ValidateFileNameFormat(config.CreatorConfigs[key.Key].PaidPostFileNameFormat, $"{key.Key}.PaidPostFileNameFormat"); - ValidateFileNameFormat(config.CreatorConfigs[key.Key].PostFileNameFormat, $"{key.Key}.PostFileNameFormat"); - ValidateFileNameFormat(config.CreatorConfigs[key.Key].PaidMessageFileNameFormat, $"{key.Key}.PaidMessageFileNameFormat"); - ValidateFileNameFormat(config.CreatorConfigs[key.Key].MessageFileNameFormat, $"{key.Key}.MessageFileNameFormat"); - } - } - } - - levelSwitch.MinimumLevel = (LogEventLevel)config.LoggingLevel; //set the logging level based on config - // Apply text sanitization preference globally - OF_DL.Utils.XmlUtils.Passthrough = config.DisableTextSanitization; - Log.Debug("Configuration:"); - string configString = JsonConvert.SerializeObject(config, Formatting.Indented); - Log.Debug(configString); - } - catch (Exception e) - { - Console.WriteLine(e); - AnsiConsole.MarkupLine($"\n[red]config.conf is not valid, check your syntax![/]\n"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - Log.Error("config.conf processing failed.", e.Message); - - if (!cliNonInteractive) - { - Console.ReadKey(); - } - Environment.Exit(3); - } - } - else - { - Entities.Config jsonConfig = new Entities.Config(); - var hoconConfig = new StringBuilder(); - hoconConfig.AppendLine("# Auth"); - hoconConfig.AppendLine("Auth {"); - hoconConfig.AppendLine($" DisableBrowserAuth = {jsonConfig.DisableBrowserAuth.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - hoconConfig.AppendLine("# External Tools"); - hoconConfig.AppendLine("External {"); - hoconConfig.AppendLine($" FFmpegPath = \"{jsonConfig.FFmpegPath}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Download Settings"); - hoconConfig.AppendLine("Download {"); - hoconConfig.AppendLine(" Media {"); - hoconConfig.AppendLine($" DownloadAvatarHeaderPhoto = {jsonConfig.DownloadAvatarHeaderPhoto.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPaidPosts = {jsonConfig.DownloadPaidPosts.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPosts = {jsonConfig.DownloadPosts.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadArchived = {jsonConfig.DownloadArchived.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadStreams = {jsonConfig.DownloadStreams.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadStories = {jsonConfig.DownloadStories.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadHighlights = {jsonConfig.DownloadHighlights.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadMessages = {jsonConfig.DownloadMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPaidMessages = {jsonConfig.DownloadPaidMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadImages = {jsonConfig.DownloadImages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadVideos = {jsonConfig.DownloadVideos.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadAudios = {jsonConfig.DownloadAudios.ToString().ToLower()}"); - hoconConfig.AppendLine(" }"); - hoconConfig.AppendLine($" IgnoreOwnMessages = {jsonConfig.IgnoreOwnMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPostsIncrementally = {jsonConfig.DownloadPostsIncrementally.ToString().ToLower()}"); - hoconConfig.AppendLine($" BypassContentForCreatorsWhoNoLongerExist = {jsonConfig.BypassContentForCreatorsWhoNoLongerExist.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadDuplicatedMedia = {jsonConfig.DownloadDuplicatedMedia.ToString().ToLower()}"); - hoconConfig.AppendLine($" SkipAds = {jsonConfig.SkipAds.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPath = \"{jsonConfig.DownloadPath}\""); - hoconConfig.AppendLine($" DownloadOnlySpecificDates = {jsonConfig.DownloadOnlySpecificDates.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadDateSelection = \"{jsonConfig.DownloadDateSelection.ToString().ToLower()}\""); - hoconConfig.AppendLine($" CustomDate = \"{jsonConfig.CustomDate?.ToString("yyyy-MM-dd")}\""); - hoconConfig.AppendLine($" ShowScrapeSize = {jsonConfig.ShowScrapeSize.ToString().ToLower()}"); - // New option defaults to false when converting legacy json - hoconConfig.AppendLine($" DisableTextSanitization = false"); - hoconConfig.AppendLine($" DownloadVideoResolution = \"{(jsonConfig.DownloadVideoResolution == VideoResolution.source ? "source" : jsonConfig.DownloadVideoResolution.ToString().TrimStart('_'))}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# File Settings"); - hoconConfig.AppendLine("File {"); - hoconConfig.AppendLine($" PaidPostFileNameFormat = \"{jsonConfig.PaidPostFileNameFormat}\""); - hoconConfig.AppendLine($" PostFileNameFormat = \"{jsonConfig.PostFileNameFormat}\""); - hoconConfig.AppendLine($" PaidMessageFileNameFormat = \"{jsonConfig.PaidMessageFileNameFormat}\""); - hoconConfig.AppendLine($" MessageFileNameFormat = \"{jsonConfig.MessageFileNameFormat}\""); - hoconConfig.AppendLine($" RenameExistingFilesWhenCustomFormatIsSelected = {jsonConfig.RenameExistingFilesWhenCustomFormatIsSelected.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Creator-Specific Configurations"); - hoconConfig.AppendLine("CreatorConfigs {"); - foreach (var creatorConfig in jsonConfig.CreatorConfigs) - { - hoconConfig.AppendLine($" \"{creatorConfig.Key}\" {{"); - hoconConfig.AppendLine($" PaidPostFileNameFormat = \"{creatorConfig.Value.PaidPostFileNameFormat}\""); - hoconConfig.AppendLine($" PostFileNameFormat = \"{creatorConfig.Value.PostFileNameFormat}\""); - hoconConfig.AppendLine($" PaidMessageFileNameFormat = \"{creatorConfig.Value.PaidMessageFileNameFormat}\""); - hoconConfig.AppendLine($" MessageFileNameFormat = \"{creatorConfig.Value.MessageFileNameFormat}\""); - hoconConfig.AppendLine(" }"); - } - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Folder Settings"); - hoconConfig.AppendLine("Folder {"); - hoconConfig.AppendLine($" FolderPerPaidPost = {jsonConfig.FolderPerPaidPost.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerPost = {jsonConfig.FolderPerPost.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerPaidMessage = {jsonConfig.FolderPerPaidMessage.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerMessage = {jsonConfig.FolderPerMessage.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Subscription Settings"); - hoconConfig.AppendLine("Subscriptions {"); - hoconConfig.AppendLine($" IncludeExpiredSubscriptions = {jsonConfig.IncludeExpiredSubscriptions.ToString().ToLower()}"); - hoconConfig.AppendLine($" IncludeRestrictedSubscriptions = {jsonConfig.IncludeRestrictedSubscriptions.ToString().ToLower()}"); - hoconConfig.AppendLine($" IgnoredUsersListName = \"{jsonConfig.IgnoredUsersListName}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Interaction Settings"); - hoconConfig.AppendLine("Interaction {"); - hoconConfig.AppendLine($" NonInteractiveMode = {jsonConfig.NonInteractiveMode.ToString().ToLower()}"); - hoconConfig.AppendLine($" NonInteractiveModeListName = \"{jsonConfig.NonInteractiveModeListName}\""); - hoconConfig.AppendLine($" NonInteractiveModePurchasedTab = {jsonConfig.NonInteractiveModePurchasedTab.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Performance Settings"); - hoconConfig.AppendLine("Performance {"); - hoconConfig.AppendLine($" Timeout = {(jsonConfig.Timeout.HasValue ? jsonConfig.Timeout.Value : -1)}"); - hoconConfig.AppendLine($" LimitDownloadRate = {jsonConfig.LimitDownloadRate.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadLimitInMbPerSec = {jsonConfig.DownloadLimitInMbPerSec}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Logging/Debug Settings"); - hoconConfig.AppendLine("Logging {"); - hoconConfig.AppendLine($" LoggingLevel = \"{jsonConfig.LoggingLevel.ToString().ToLower()}\""); - hoconConfig.AppendLine("}"); - - File.WriteAllText("config.conf", hoconConfig.ToString()); - AnsiConsole.Markup("[red]config.conf does not exist, a default file has been created in the folder you are running the program from[/]"); - Log.Error("config.conf does not exist"); - - if (!cliNonInteractive) - { - Console.ReadKey(); - } - Environment.Exit(3); - } - - - if (args is not null && args.Length > 0) - { - const string NON_INTERACTIVE_ARG = "--non-interactive"; - - if (args.Any(a => NON_INTERACTIVE_ARG.Equals(NON_INTERACTIVE_ARG, StringComparison.OrdinalIgnoreCase))) - { - cliNonInteractive = true; - Log.Debug("NonInteractiveMode set via command line"); - } - - Log.Debug("Additional arguments:"); - foreach (string argument in args) - { - Log.Debug(argument); - } - } - } - catch (Exception e) - { - Console.WriteLine(e); - Log.Error("Configuration loading failed.", e.Message); - return null; - } - - return config; - } - private async Task RunAsync(string[] args, bool cliNonInteractive) { + var config = serviceProvider.GetRequiredService(); + var authService = serviceProvider.GetRequiredService(); + try { var os = Environment.OSVersion; @@ -650,51 +233,46 @@ public class Program } - if (File.Exists("auth.json")) + if (await authService.LoadFromFileAsync()) { AnsiConsole.Markup("[green]auth.json located successfully!\n[/]"); - Log.Debug("Auth file found"); - try + } + else if (File.Exists("auth.json")) + { + // File exists but failed to load + Log.Information("Auth file found but could not be deserialized"); + if (!config!.DisableBrowserAuth) { - auth = JsonConvert.DeserializeObject(await File.ReadAllTextAsync("auth.json")); - Log.Debug("Auth file found and deserialized"); + Log.Debug("Deleting auth.json"); + File.Delete("auth.json"); } - catch (Exception _) - { - Log.Information("Auth file found but could not be deserialized"); - if (!config!.DisableBrowserAuth) - { - Log.Debug("Deleting auth.json"); - File.Delete("auth.json"); - } - if (cliNonInteractive) - { - 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[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); + if (cliNonInteractive) + { + 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[/]"); + AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - Console.ReadKey(); - Environment.Exit(2); - } + Console.ReadKey(); + Environment.Exit(2); + } - if (!config!.DisableBrowserAuth) - { - await LoadAuthFromBrowser(); - } - else - { - 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[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); + if (!config!.DisableBrowserAuth) + { + await LoadAuthFromBrowser(); + } + else + { + 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[/]"); + AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - Console.ReadKey(); - Environment.Exit(2); - } - } + Console.ReadKey(); + Environment.Exit(2); + } } else { @@ -726,7 +304,7 @@ public class Program } //Added to stop cookie being filled with un-needed headers - ValidateCookieString(auth!); + ValidateCookieString(authService.CurrentAuth!); if (File.Exists("rules.json")) { @@ -775,11 +353,11 @@ public class Program Log.Debug($"FFMPEG found: {config.FFmpegPath}"); Log.Debug("FFMPEG path set in config.conf"); } - else if (!string.IsNullOrEmpty(auth!.FFMPEG_PATH) && ValidateFilePath(auth.FFMPEG_PATH)) + else if (!string.IsNullOrEmpty(authService.CurrentAuth!.FFMPEG_PATH) && ValidateFilePath(authService.CurrentAuth.FFMPEG_PATH)) { // FFmpeg path is set in auth.json and is valid (config.conf takes precedence and auth.json is only available for backward compatibility) ffmpegFound = true; - config.FFmpegPath = auth.FFMPEG_PATH; + config.FFmpegPath = authService.CurrentAuth.FFMPEG_PATH; Log.Debug($"FFMPEG found: {config.FFmpegPath}"); Log.Debug("FFMPEG path set in auth.json"); } @@ -923,14 +501,14 @@ public class Program } //Check if auth is valid - var apiHelper = ActivatorUtilities.CreateInstance(serviceProvider, auth); + var apiHelper = ActivatorUtilities.CreateInstance(serviceProvider, authService.CurrentAuth); Entities.User? validate = await apiHelper.GetUserInfo($"/users/me"); if (validate == null || (validate?.name == null && validate?.username == null)) { Log.Error("Auth failed"); - auth = null; + authService.CurrentAuth = null; if (!config!.DisableBrowserAuth) { if (File.Exists("auth.json")) @@ -944,7 +522,7 @@ public class Program await LoadAuthFromBrowser(); } - if (auth == null) + 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"); @@ -954,7 +532,7 @@ public class Program } AnsiConsole.Markup($"[green]Logged In successfully as {validate.name} {validate.username}\n[/]"); - await DownloadAllData(apiHelper, auth, config); + await DownloadAllData(apiHelper, authService.CurrentAuth, config); } catch (Exception ex) { @@ -978,6 +556,7 @@ public class Program private async Task DownloadAllData(IAPIHelper m_ApiHelper, Auth Auth, Entities.Config Config) { IDBHelper dBHelper = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); Log.Debug("Calling DownloadAllData"); @@ -1048,7 +627,8 @@ public class Program } else { - var userSelectionResult = await HandleUserSelection(m_ApiHelper, Config, users, lists); + var loggingService = serviceProvider.GetRequiredService(); + var userSelectionResult = await HandleUserSelection(m_ApiHelper, Config, users, lists, configService, loggingService); Config = userSelectionResult.updatedConfig; hasSelectedUsersKVP = new KeyValuePair>(userSelectionResult.IsExit, userSelectionResult.selectedUsers); @@ -2991,7 +2571,7 @@ public class Program } } - public static async Task<(bool IsExit, Dictionary? selectedUsers, Entities.Config? updatedConfig)> HandleUserSelection(IAPIHelper apiHelper, Entities.Config currentConfig, Dictionary users, Dictionary lists) + public static async Task<(bool IsExit, Dictionary? selectedUsers, Entities.Config? updatedConfig)> HandleUserSelection(IAPIHelper apiHelper, Entities.Config currentConfig, Dictionary users, Dictionary lists, IConfigService configService, ILoggingService loggingService) { bool hasSelectedUsers = false; Dictionary selectedUsers = new Dictionary(); @@ -3145,108 +2725,8 @@ public class Program } } - var hoconConfig = new StringBuilder(); - hoconConfig.AppendLine("# Auth"); - hoconConfig.AppendLine("Auth {"); - hoconConfig.AppendLine($" DisableBrowserAuth = {newConfig.DisableBrowserAuth.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - hoconConfig.AppendLine("# External Tools"); - hoconConfig.AppendLine("External {"); - hoconConfig.AppendLine($" FFmpegPath = \"{newConfig.FFmpegPath}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Download Settings"); - hoconConfig.AppendLine("Download {"); - hoconConfig.AppendLine(" Media {"); - hoconConfig.AppendLine($" DownloadAvatarHeaderPhoto = {newConfig.DownloadAvatarHeaderPhoto.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPaidPosts = {newConfig.DownloadPaidPosts.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPosts = {newConfig.DownloadPosts.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadArchived = {newConfig.DownloadArchived.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadStreams = {newConfig.DownloadStreams.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadStories = {newConfig.DownloadStories.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadHighlights = {newConfig.DownloadHighlights.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadMessages = {newConfig.DownloadMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPaidMessages = {newConfig.DownloadPaidMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadImages = {newConfig.DownloadImages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadVideos = {newConfig.DownloadVideos.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadAudios = {newConfig.DownloadAudios.ToString().ToLower()}"); - hoconConfig.AppendLine(" }"); - hoconConfig.AppendLine($" IgnoreOwnMessages = {newConfig.IgnoreOwnMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPostsIncrementally = {newConfig.DownloadPostsIncrementally.ToString().ToLower()}"); - hoconConfig.AppendLine($" BypassContentForCreatorsWhoNoLongerExist = {newConfig.BypassContentForCreatorsWhoNoLongerExist.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadDuplicatedMedia = {newConfig.DownloadDuplicatedMedia.ToString().ToLower()}"); - hoconConfig.AppendLine($" SkipAds = {newConfig.SkipAds.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPath = \"{newConfig.DownloadPath}\""); - hoconConfig.AppendLine($" DownloadOnlySpecificDates = {newConfig.DownloadOnlySpecificDates.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadDateSelection = \"{newConfig.DownloadDateSelection.ToString().ToLower()}\""); - hoconConfig.AppendLine($" CustomDate = \"{newConfig.CustomDate?.ToString("yyyy-MM-dd")}\""); - hoconConfig.AppendLine($" ShowScrapeSize = {newConfig.ShowScrapeSize.ToString().ToLower()}"); - hoconConfig.AppendLine($" DisableTextSanitization = {newConfig.DisableTextSanitization.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadVideoResolution = \"{(newConfig.DownloadVideoResolution == VideoResolution.source ? "source" : newConfig.DownloadVideoResolution.ToString().TrimStart('_'))}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# File Settings"); - hoconConfig.AppendLine("File {"); - hoconConfig.AppendLine($" PaidPostFileNameFormat = \"{newConfig.PaidPostFileNameFormat}\""); - hoconConfig.AppendLine($" PostFileNameFormat = \"{newConfig.PostFileNameFormat}\""); - hoconConfig.AppendLine($" PaidMessageFileNameFormat = \"{newConfig.PaidMessageFileNameFormat}\""); - hoconConfig.AppendLine($" MessageFileNameFormat = \"{newConfig.MessageFileNameFormat}\""); - hoconConfig.AppendLine($" RenameExistingFilesWhenCustomFormatIsSelected = {newConfig.RenameExistingFilesWhenCustomFormatIsSelected.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Creator-Specific Configurations"); - hoconConfig.AppendLine("CreatorConfigs {"); - foreach (var creatorConfig in newConfig.CreatorConfigs) - { - hoconConfig.AppendLine($" \"{creatorConfig.Key}\" {{"); - hoconConfig.AppendLine($" PaidPostFileNameFormat = \"{creatorConfig.Value.PaidPostFileNameFormat}\""); - hoconConfig.AppendLine($" PostFileNameFormat = \"{creatorConfig.Value.PostFileNameFormat}\""); - hoconConfig.AppendLine($" PaidMessageFileNameFormat = \"{creatorConfig.Value.PaidMessageFileNameFormat}\""); - hoconConfig.AppendLine($" MessageFileNameFormat = \"{creatorConfig.Value.MessageFileNameFormat}\""); - hoconConfig.AppendLine(" }"); - } - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Folder Settings"); - hoconConfig.AppendLine("Folder {"); - hoconConfig.AppendLine($" FolderPerPaidPost = {newConfig.FolderPerPaidPost.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerPost = {newConfig.FolderPerPost.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerPaidMessage = {newConfig.FolderPerPaidMessage.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerMessage = {newConfig.FolderPerMessage.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Subscription Settings"); - hoconConfig.AppendLine("Subscriptions {"); - hoconConfig.AppendLine($" IncludeExpiredSubscriptions = {newConfig.IncludeExpiredSubscriptions.ToString().ToLower()}"); - hoconConfig.AppendLine($" IncludeRestrictedSubscriptions = {newConfig.IncludeRestrictedSubscriptions.ToString().ToLower()}"); - hoconConfig.AppendLine($" IgnoredUsersListName = \"{newConfig.IgnoredUsersListName}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Interaction Settings"); - hoconConfig.AppendLine("Interaction {"); - hoconConfig.AppendLine($" NonInteractiveMode = {newConfig.NonInteractiveMode.ToString().ToLower()}"); - hoconConfig.AppendLine($" NonInteractiveModeListName = \"{newConfig.NonInteractiveModeListName}\""); - hoconConfig.AppendLine($" NonInteractiveModePurchasedTab = {newConfig.NonInteractiveModePurchasedTab.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Performance Settings"); - hoconConfig.AppendLine("Performance {"); - hoconConfig.AppendLine($" Timeout = {(newConfig.Timeout.HasValue ? newConfig.Timeout.Value : -1)}"); - hoconConfig.AppendLine($" LimitDownloadRate = {newConfig.LimitDownloadRate.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadLimitInMbPerSec = {newConfig.DownloadLimitInMbPerSec}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Logging/Debug Settings"); - hoconConfig.AppendLine("Logging {"); - hoconConfig.AppendLine($" LoggingLevel = \"{newConfig.LoggingLevel.ToString().ToLower()}\""); - hoconConfig.AppendLine("}"); - - File.WriteAllText("config.conf", hoconConfig.ToString()); - - string newConfigString = JsonConvert.SerializeObject(newConfig, Formatting.Indented); - - Log.Debug($"Config changed:"); - Log.Debug(newConfigString); + configService.UpdateConfig(newConfig); + await configService.SaveConfigurationAsync(); currentConfig = newConfig; if (configChanged) @@ -3267,7 +2747,7 @@ public class Program foreach (string name in typeof(LoggingLevel).GetEnumNames()) { string itemLabel = $"[red]{name}[/]"; - choices.Add(new(itemLabel, name == levelSwitch.MinimumLevel.ToString())); + choices.Add(new(itemLabel, name == loggingService.GetCurrentLoggingLevel().ToString())); } SelectionPrompt selectionPrompt = new SelectionPrompt() @@ -3288,7 +2768,6 @@ public class Program levelOption = levelOption.Replace("[red]", "").Replace("[/]", ""); LoggingLevel newLogLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), levelOption, true); - levelSwitch.MinimumLevel = (LogEventLevel)newLogLevel; Log.Debug($"Logging level changed to: {levelOption}"); @@ -3302,108 +2781,8 @@ public class Program currentConfig = newConfig; - // Dump new config in the log file - Log.Debug("Configuration:"); - string configString = JsonConvert.SerializeObject(currentConfig, Formatting.Indented); - Log.Debug(configString); - - var hoconConfig = new StringBuilder(); - hoconConfig.AppendLine("# Auth"); - hoconConfig.AppendLine("Auth {"); - hoconConfig.AppendLine($" DisableBrowserAuth = {newConfig.DisableBrowserAuth.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - hoconConfig.AppendLine("# External Tools"); - hoconConfig.AppendLine("External {"); - hoconConfig.AppendLine($" FFmpegPath = \"{newConfig.FFmpegPath}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Download Settings"); - hoconConfig.AppendLine("Download {"); - hoconConfig.AppendLine(" Media {"); - hoconConfig.AppendLine($" DownloadAvatarHeaderPhoto = {newConfig.DownloadAvatarHeaderPhoto.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPaidPosts = {newConfig.DownloadPaidPosts.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPosts = {newConfig.DownloadPosts.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadArchived = {newConfig.DownloadArchived.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadStreams = {newConfig.DownloadStreams.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadStories = {newConfig.DownloadStories.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadHighlights = {newConfig.DownloadHighlights.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadMessages = {newConfig.DownloadMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPaidMessages = {newConfig.DownloadPaidMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadImages = {newConfig.DownloadImages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadVideos = {newConfig.DownloadVideos.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadAudios = {newConfig.DownloadAudios.ToString().ToLower()}"); - hoconConfig.AppendLine(" }"); - hoconConfig.AppendLine($" IgnoreOwnMessages = {newConfig.IgnoreOwnMessages.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPostsIncrementally = {newConfig.DownloadPostsIncrementally.ToString().ToLower()}"); - hoconConfig.AppendLine($" BypassContentForCreatorsWhoNoLongerExist = {newConfig.BypassContentForCreatorsWhoNoLongerExist.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadDuplicatedMedia = {newConfig.DownloadDuplicatedMedia.ToString().ToLower()}"); - hoconConfig.AppendLine($" SkipAds = {newConfig.SkipAds.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadPath = \"{newConfig.DownloadPath}\""); - hoconConfig.AppendLine($" DownloadOnlySpecificDates = {newConfig.DownloadOnlySpecificDates.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadDateSelection = \"{newConfig.DownloadDateSelection.ToString().ToLower()}\""); - hoconConfig.AppendLine($" CustomDate = \"{newConfig.CustomDate?.ToString("yyyy-MM-dd")}\""); - hoconConfig.AppendLine($" ShowScrapeSize = {newConfig.ShowScrapeSize.ToString().ToLower()}"); - hoconConfig.AppendLine($" DisableTextSanitization = {newConfig.DisableTextSanitization.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadVideoResolution = \"{(newConfig.DownloadVideoResolution == VideoResolution.source ? "source" : newConfig.DownloadVideoResolution.ToString().TrimStart('_'))}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# File Settings"); - hoconConfig.AppendLine("File {"); - hoconConfig.AppendLine($" PaidPostFileNameFormat = \"{newConfig.PaidPostFileNameFormat}\""); - hoconConfig.AppendLine($" PostFileNameFormat = \"{newConfig.PostFileNameFormat}\""); - hoconConfig.AppendLine($" PaidMessageFileNameFormat = \"{newConfig.PaidMessageFileNameFormat}\""); - hoconConfig.AppendLine($" MessageFileNameFormat = \"{newConfig.MessageFileNameFormat}\""); - hoconConfig.AppendLine($" RenameExistingFilesWhenCustomFormatIsSelected = {newConfig.RenameExistingFilesWhenCustomFormatIsSelected.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Creator-Specific Configurations"); - hoconConfig.AppendLine("CreatorConfigs {"); - foreach (var creatorConfig in newConfig.CreatorConfigs) - { - hoconConfig.AppendLine($" \"{creatorConfig.Key}\" {{"); - hoconConfig.AppendLine($" PaidPostFileNameFormat = \"{creatorConfig.Value.PaidPostFileNameFormat}\""); - hoconConfig.AppendLine($" PostFileNameFormat = \"{creatorConfig.Value.PostFileNameFormat}\""); - hoconConfig.AppendLine($" PaidMessageFileNameFormat = \"{creatorConfig.Value.PaidMessageFileNameFormat}\""); - hoconConfig.AppendLine($" MessageFileNameFormat = \"{creatorConfig.Value.MessageFileNameFormat}\""); - hoconConfig.AppendLine(" }"); - } - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Folder Settings"); - hoconConfig.AppendLine("Folder {"); - hoconConfig.AppendLine($" FolderPerPaidPost = {newConfig.FolderPerPaidPost.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerPost = {newConfig.FolderPerPost.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerPaidMessage = {newConfig.FolderPerPaidMessage.ToString().ToLower()}"); - hoconConfig.AppendLine($" FolderPerMessage = {newConfig.FolderPerMessage.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Subscription Settings"); - hoconConfig.AppendLine("Subscriptions {"); - hoconConfig.AppendLine($" IncludeExpiredSubscriptions = {newConfig.IncludeExpiredSubscriptions.ToString().ToLower()}"); - hoconConfig.AppendLine($" IncludeRestrictedSubscriptions = {newConfig.IncludeRestrictedSubscriptions.ToString().ToLower()}"); - hoconConfig.AppendLine($" IgnoredUsersListName = \"{newConfig.IgnoredUsersListName}\""); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Interaction Settings"); - hoconConfig.AppendLine("Interaction {"); - hoconConfig.AppendLine($" NonInteractiveMode = {newConfig.NonInteractiveMode.ToString().ToLower()}"); - hoconConfig.AppendLine($" NonInteractiveModeListName = \"{newConfig.NonInteractiveModeListName}\""); - hoconConfig.AppendLine($" NonInteractiveModePurchasedTab = {newConfig.NonInteractiveModePurchasedTab.ToString().ToLower()}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Performance Settings"); - hoconConfig.AppendLine("Performance {"); - hoconConfig.AppendLine($" Timeout = {(newConfig.Timeout.HasValue ? newConfig.Timeout.Value : -1)}"); - hoconConfig.AppendLine($" LimitDownloadRate = {newConfig.LimitDownloadRate.ToString().ToLower()}"); - hoconConfig.AppendLine($" DownloadLimitInMbPerSec = {newConfig.DownloadLimitInMbPerSec}"); - hoconConfig.AppendLine("}"); - - hoconConfig.AppendLine("# Logging/Debug Settings"); - hoconConfig.AppendLine("Logging {"); - hoconConfig.AppendLine($" LoggingLevel = \"{newConfig.LoggingLevel.ToString().ToLower()}\""); - hoconConfig.AppendLine("}"); - - File.WriteAllText("config.conf", hoconConfig.ToString()); + configService.UpdateConfig(newConfig); + await configService.SaveConfigurationAsync(); if (configChanged) { @@ -3553,22 +2932,4 @@ public class Program } } - public static void ValidateFileNameFormat(string? format, string settingName) - { - if(!string.IsNullOrEmpty(format) && !format.Contains("{mediaId}", StringComparison.OrdinalIgnoreCase) && !format.Contains("{filename}", StringComparison.OrdinalIgnoreCase)) - { - AnsiConsole.Markup($"[red]{settingName} is not unique enough, please make sure you include either '{{mediaId}}' or '{{filename}}' to ensure that files are not overwritten with the same filename.[/]\n"); - AnsiConsole.Markup("[red]Press any key to continue.[/]\n"); - Console.ReadKey(); - Environment.Exit(2); - } - } - - public static VideoResolution ParseVideoResolution(string value) - { - if (value.Equals("source", StringComparison.OrdinalIgnoreCase)) - return VideoResolution.source; - - return Enum.Parse("_" + value, ignoreCase: true); - } } diff --git a/OF DL/Services/AuthService.cs b/OF DL/Services/AuthService.cs new file mode 100644 index 0000000..bf93ddd --- /dev/null +++ b/OF DL/Services/AuthService.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json; +using OF_DL.Entities; +using OF_DL.Helpers.Interfaces; +using Serilog; + +namespace OF_DL.Services +{ + public class AuthService(IAuthHelper authHelper) : IAuthService + { + public Auth? CurrentAuth { get; set; } + + public async Task LoadFromFileAsync(string filePath = "auth.json") + { + try + { + if (!File.Exists(filePath)) + { + Log.Debug("Auth file not found: {FilePath}", filePath); + return false; + } + + var json = await File.ReadAllTextAsync(filePath); + CurrentAuth = JsonConvert.DeserializeObject(json); + Log.Debug("Auth file loaded and deserialized successfully"); + return CurrentAuth != null; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load auth from file"); + return false; + } + } + + public async Task LoadFromBrowserAsync() + { + try + { + bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; + + await authHelper.SetupBrowser(runningInDocker); + CurrentAuth = await authHelper.GetAuthFromBrowser(); + + return CurrentAuth != null; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load auth from browser"); + return false; + } + } + + public async Task SaveToFileAsync(string filePath = "auth.json") + { + if (CurrentAuth == null) + { + Log.Warning("Attempted to save null auth to file"); + return; + } + + try + { + var json = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented); + await File.WriteAllTextAsync(filePath, json); + Log.Debug($"Auth saved to file: {filePath}"); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to save auth to file"); + } + } + } +} diff --git a/OF DL/Services/ConfigService.cs b/OF DL/Services/ConfigService.cs new file mode 100644 index 0000000..67a418a --- /dev/null +++ b/OF DL/Services/ConfigService.cs @@ -0,0 +1,385 @@ +using Akka.Configuration; +using Newtonsoft.Json; +using OF_DL.Entities; +using Serilog; +using System.Text; +using OF_DL.Enumerations; +using Config = OF_DL.Entities.Config; + +namespace OF_DL.Services +{ + public class ConfigService(ILoggingService loggingService) : IConfigService + { + public Config? CurrentConfig { get; private set; } + public bool IsCliNonInteractive { get; private set; } + + public async Task LoadConfigurationAsync(string[] args) + { + try + { + IsCliNonInteractive = false; + + // Migrate from config.json to config.conf if needed + await MigrateFromJsonToConfAsync(); + + // Load config.conf or create default + if (File.Exists("config.conf")) + { + Log.Debug("config.conf located successfully"); + if (!await LoadConfigFromFileAsync("config.conf")) + { + return false; + } + } + else + { + Log.Debug("config.conf not found, creating default"); + await CreateDefaultConfigFileAsync(); + if (!await LoadConfigFromFileAsync("config.conf")) + { + return false; + } + } + + // Check for command-line arguments + if (args != null && args.Length > 0) + { + const string NON_INTERACTIVE_ARG = "--non-interactive"; + if (args.Any(a => a.Equals(NON_INTERACTIVE_ARG, StringComparison.OrdinalIgnoreCase))) + { + IsCliNonInteractive = true; + Log.Debug("NonInteractiveMode set via command line"); + } + + Log.Debug("Additional arguments:"); + foreach (string argument in args) + { + Log.Debug(argument); + } + } + + return true; + } + catch (Exception ex) + { + Log.Error(ex, "Configuration loading failed"); + return false; + } + } + + public async Task SaveConfigurationAsync(string filePath = "config.conf") + { + if (CurrentConfig == null) + { + Log.Warning("Attempted to save null config to file"); + return; + } + + try + { + var hoconConfig = BuildHoconFromConfig(CurrentConfig); + await File.WriteAllTextAsync(filePath, hoconConfig); + Log.Debug($"Config saved to file: {filePath}"); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to save config to file"); + } + } + + public void UpdateConfig(Config newConfig) + { + CurrentConfig = newConfig; + + // Update logging level + loggingService.UpdateLoggingLevel(newConfig.LoggingLevel); + + // Apply text sanitization preference globally + OF_DL.Utils.XmlUtils.Passthrough = newConfig.DisableTextSanitization; + + Log.Debug("Configuration updated"); + string configString = JsonConvert.SerializeObject(newConfig, Formatting.Indented); + Log.Debug(configString); + } + + private async Task MigrateFromJsonToConfAsync() + { + if (!File.Exists("config.json")) + return; + + try + { + Log.Debug("config.json found, migrating to config.conf"); + string jsonText = await File.ReadAllTextAsync("config.json"); + var jsonConfig = JsonConvert.DeserializeObject(jsonText); + + if (jsonConfig != null) + { + var hoconConfig = BuildHoconFromConfig(jsonConfig); + await File.WriteAllTextAsync("config.conf", hoconConfig); + File.Delete("config.json"); + Log.Information("config.conf created successfully from config.json"); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to migrate config.json to config.conf"); + throw; + } + } + + private async Task LoadConfigFromFileAsync(string filePath) + { + try + { + string hoconText = await File.ReadAllTextAsync(filePath); + var hoconConfig = ConfigurationFactory.ParseString(hoconText); + + CurrentConfig = new Config + { + // Auth + DisableBrowserAuth = hoconConfig.GetBoolean("Auth.DisableBrowserAuth"), + + // FFmpeg Settings + FFmpegPath = hoconConfig.GetString("External.FFmpegPath"), + + // Download Settings + DownloadAvatarHeaderPhoto = hoconConfig.GetBoolean("Download.Media.DownloadAvatarHeaderPhoto"), + DownloadPaidPosts = hoconConfig.GetBoolean("Download.Media.DownloadPaidPosts"), + DownloadPosts = hoconConfig.GetBoolean("Download.Media.DownloadPosts"), + DownloadArchived = hoconConfig.GetBoolean("Download.Media.DownloadArchived"), + DownloadStreams = hoconConfig.GetBoolean("Download.Media.DownloadStreams"), + DownloadStories = hoconConfig.GetBoolean("Download.Media.DownloadStories"), + DownloadHighlights = hoconConfig.GetBoolean("Download.Media.DownloadHighlights"), + DownloadMessages = hoconConfig.GetBoolean("Download.Media.DownloadMessages"), + DownloadPaidMessages = hoconConfig.GetBoolean("Download.Media.DownloadPaidMessages"), + DownloadImages = hoconConfig.GetBoolean("Download.Media.DownloadImages"), + DownloadVideos = hoconConfig.GetBoolean("Download.Media.DownloadVideos"), + DownloadAudios = hoconConfig.GetBoolean("Download.Media.DownloadAudios"), + IgnoreOwnMessages = hoconConfig.GetBoolean("Download.IgnoreOwnMessages"), + DownloadPostsIncrementally = hoconConfig.GetBoolean("Download.DownloadPostsIncrementally"), + BypassContentForCreatorsWhoNoLongerExist = hoconConfig.GetBoolean("Download.BypassContentForCreatorsWhoNoLongerExist"), + DownloadDuplicatedMedia = hoconConfig.GetBoolean("Download.DownloadDuplicatedMedia"), + SkipAds = hoconConfig.GetBoolean("Download.SkipAds"), + DownloadPath = hoconConfig.GetString("Download.DownloadPath"), + DownloadOnlySpecificDates = hoconConfig.GetBoolean("Download.DownloadOnlySpecificDates"), + DownloadDateSelection = Enum.Parse(hoconConfig.GetString("Download.DownloadDateSelection"), true), + CustomDate = !string.IsNullOrWhiteSpace(hoconConfig.GetString("Download.CustomDate")) ? DateTime.Parse(hoconConfig.GetString("Download.CustomDate")) : null, + ShowScrapeSize = hoconConfig.GetBoolean("Download.ShowScrapeSize"), + DisableTextSanitization = bool.TryParse(hoconConfig.GetString("Download.DisableTextSanitization", "false"), out var dts) ? dts : false, + DownloadVideoResolution = ParseVideoResolution(hoconConfig.GetString("Download.DownloadVideoResolution", "source")), + + // File Settings + PaidPostFileNameFormat = hoconConfig.GetString("File.PaidPostFileNameFormat"), + PostFileNameFormat = hoconConfig.GetString("File.PostFileNameFormat"), + PaidMessageFileNameFormat = hoconConfig.GetString("File.PaidMessageFileNameFormat"), + MessageFileNameFormat = hoconConfig.GetString("File.MessageFileNameFormat"), + RenameExistingFilesWhenCustomFormatIsSelected = hoconConfig.GetBoolean("File.RenameExistingFilesWhenCustomFormatIsSelected"), + + // Folder Settings + FolderPerPaidPost = hoconConfig.GetBoolean("Folder.FolderPerPaidPost"), + FolderPerPost = hoconConfig.GetBoolean("Folder.FolderPerPost"), + FolderPerPaidMessage = hoconConfig.GetBoolean("Folder.FolderPerPaidMessage"), + FolderPerMessage = hoconConfig.GetBoolean("Folder.FolderPerMessage"), + + // Subscription Settings + IncludeExpiredSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeExpiredSubscriptions"), + IncludeRestrictedSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeRestrictedSubscriptions"), + IgnoredUsersListName = hoconConfig.GetString("Subscriptions.IgnoredUsersListName"), + + // Interaction Settings + NonInteractiveMode = hoconConfig.GetBoolean("Interaction.NonInteractiveMode"), + NonInteractiveModeListName = hoconConfig.GetString("Interaction.NonInteractiveModeListName"), + NonInteractiveModePurchasedTab = hoconConfig.GetBoolean("Interaction.NonInteractiveModePurchasedTab"), + + // Performance Settings + Timeout = string.IsNullOrWhiteSpace(hoconConfig.GetString("Performance.Timeout")) ? -1 : hoconConfig.GetInt("Performance.Timeout"), + LimitDownloadRate = hoconConfig.GetBoolean("Performance.LimitDownloadRate"), + DownloadLimitInMbPerSec = hoconConfig.GetInt("Performance.DownloadLimitInMbPerSec"), + + // Logging/Debug Settings + LoggingLevel = Enum.Parse(hoconConfig.GetString("Logging.LoggingLevel"), true) + }; + + // Validate file name formats + ValidateFileNameFormat(CurrentConfig.PaidPostFileNameFormat, "PaidPostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.PostFileNameFormat, "PostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.PaidMessageFileNameFormat, "PaidMessageFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.MessageFileNameFormat, "MessageFileNameFormat"); + + // Load creator-specific configs + var creatorConfigsSection = hoconConfig.GetConfig("CreatorConfigs"); + if (creatorConfigsSection != null) + { + foreach (var key in creatorConfigsSection.AsEnumerable()) + { + var creatorKey = key.Key; + var creatorHocon = creatorConfigsSection.GetConfig(creatorKey); + if (!CurrentConfig.CreatorConfigs.ContainsKey(creatorKey) && creatorHocon != null) + { + CurrentConfig.CreatorConfigs.Add(key.Key, new CreatorConfig + { + PaidPostFileNameFormat = creatorHocon.GetString("PaidPostFileNameFormat"), + PostFileNameFormat = creatorHocon.GetString("PostFileNameFormat"), + PaidMessageFileNameFormat = creatorHocon.GetString("PaidMessageFileNameFormat"), + MessageFileNameFormat = creatorHocon.GetString("MessageFileNameFormat") + }); + + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidPostFileNameFormat, $"{key.Key}.PaidPostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PostFileNameFormat, $"{key.Key}.PostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidMessageFileNameFormat, $"{key.Key}.PaidMessageFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].MessageFileNameFormat, $"{key.Key}.MessageFileNameFormat"); + } + } + } + + // Update logging level + loggingService.UpdateLoggingLevel(CurrentConfig.LoggingLevel); + + // Apply text sanitization preference globally + OF_DL.Utils.XmlUtils.Passthrough = CurrentConfig.DisableTextSanitization; + + Log.Debug("Configuration loaded successfully"); + string configString = JsonConvert.SerializeObject(CurrentConfig, Formatting.Indented); + Log.Debug(configString); + + return true; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to parse config file"); + return false; + } + } + + private async Task CreateDefaultConfigFileAsync() + { + Config defaultConfig = new Config(); + var hoconConfig = BuildHoconFromConfig(defaultConfig); + await File.WriteAllTextAsync("config.conf", hoconConfig); + Log.Information("Created default config.conf file"); + } + + private string BuildHoconFromConfig(Config config) + { + var hocon = new StringBuilder(); + + hocon.AppendLine("# Auth"); + hocon.AppendLine("Auth {"); + hocon.AppendLine($" DisableBrowserAuth = {config.DisableBrowserAuth.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# External Tools"); + hocon.AppendLine("External {"); + hocon.AppendLine($" FFmpegPath = \"{config.FFmpegPath}\""); + hocon.AppendLine("}"); + + hocon.AppendLine("# Download Settings"); + hocon.AppendLine("Download {"); + hocon.AppendLine(" Media {"); + hocon.AppendLine($" DownloadAvatarHeaderPhoto = {config.DownloadAvatarHeaderPhoto.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPaidPosts = {config.DownloadPaidPosts.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPosts = {config.DownloadPosts.ToString().ToLower()}"); + hocon.AppendLine($" DownloadArchived = {config.DownloadArchived.ToString().ToLower()}"); + hocon.AppendLine($" DownloadStreams = {config.DownloadStreams.ToString().ToLower()}"); + hocon.AppendLine($" DownloadStories = {config.DownloadStories.ToString().ToLower()}"); + hocon.AppendLine($" DownloadHighlights = {config.DownloadHighlights.ToString().ToLower()}"); + hocon.AppendLine($" DownloadMessages = {config.DownloadMessages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPaidMessages = {config.DownloadPaidMessages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadImages = {config.DownloadImages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadVideos = {config.DownloadVideos.ToString().ToLower()}"); + hocon.AppendLine($" DownloadAudios = {config.DownloadAudios.ToString().ToLower()}"); + hocon.AppendLine(" }"); + hocon.AppendLine($" IgnoreOwnMessages = {config.IgnoreOwnMessages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPostsIncrementally = {config.DownloadPostsIncrementally.ToString().ToLower()}"); + hocon.AppendLine($" BypassContentForCreatorsWhoNoLongerExist = {config.BypassContentForCreatorsWhoNoLongerExist.ToString().ToLower()}"); + hocon.AppendLine($" DownloadDuplicatedMedia = {config.DownloadDuplicatedMedia.ToString().ToLower()}"); + hocon.AppendLine($" SkipAds = {config.SkipAds.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPath = \"{config.DownloadPath}\""); + hocon.AppendLine($" DownloadOnlySpecificDates = {config.DownloadOnlySpecificDates.ToString().ToLower()}"); + hocon.AppendLine($" DownloadDateSelection = \"{config.DownloadDateSelection.ToString().ToLower()}\""); + hocon.AppendLine($" CustomDate = \"{config.CustomDate?.ToString("yyyy-MM-dd")}\""); + hocon.AppendLine($" ShowScrapeSize = {config.ShowScrapeSize.ToString().ToLower()}"); + hocon.AppendLine($" DisableTextSanitization = {config.DisableTextSanitization.ToString().ToLower()}"); + hocon.AppendLine($" DownloadVideoResolution = \"{(config.DownloadVideoResolution == VideoResolution.source ? "source" : config.DownloadVideoResolution.ToString().TrimStart('_'))}\""); + hocon.AppendLine("}"); + + hocon.AppendLine("# File Settings"); + hocon.AppendLine("File {"); + hocon.AppendLine($" PaidPostFileNameFormat = \"{config.PaidPostFileNameFormat}\""); + hocon.AppendLine($" PostFileNameFormat = \"{config.PostFileNameFormat}\""); + hocon.AppendLine($" PaidMessageFileNameFormat = \"{config.PaidMessageFileNameFormat}\""); + hocon.AppendLine($" MessageFileNameFormat = \"{config.MessageFileNameFormat}\""); + hocon.AppendLine($" RenameExistingFilesWhenCustomFormatIsSelected = {config.RenameExistingFilesWhenCustomFormatIsSelected.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Creator-Specific Configurations"); + hocon.AppendLine("CreatorConfigs {"); + foreach (var creatorConfig in config.CreatorConfigs) + { + hocon.AppendLine($" \"{creatorConfig.Key}\" {{"); + hocon.AppendLine($" PaidPostFileNameFormat = \"{creatorConfig.Value.PaidPostFileNameFormat}\""); + hocon.AppendLine($" PostFileNameFormat = \"{creatorConfig.Value.PostFileNameFormat}\""); + hocon.AppendLine($" PaidMessageFileNameFormat = \"{creatorConfig.Value.PaidMessageFileNameFormat}\""); + hocon.AppendLine($" MessageFileNameFormat = \"{creatorConfig.Value.MessageFileNameFormat}\""); + hocon.AppendLine(" }"); + } + hocon.AppendLine("}"); + + hocon.AppendLine("# Folder Settings"); + hocon.AppendLine("Folder {"); + hocon.AppendLine($" FolderPerPaidPost = {config.FolderPerPaidPost.ToString().ToLower()}"); + hocon.AppendLine($" FolderPerPost = {config.FolderPerPost.ToString().ToLower()}"); + hocon.AppendLine($" FolderPerPaidMessage = {config.FolderPerPaidMessage.ToString().ToLower()}"); + hocon.AppendLine($" FolderPerMessage = {config.FolderPerMessage.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Subscription Settings"); + hocon.AppendLine("Subscriptions {"); + hocon.AppendLine($" IncludeExpiredSubscriptions = {config.IncludeExpiredSubscriptions.ToString().ToLower()}"); + hocon.AppendLine($" IncludeRestrictedSubscriptions = {config.IncludeRestrictedSubscriptions.ToString().ToLower()}"); + hocon.AppendLine($" IgnoredUsersListName = \"{config.IgnoredUsersListName}\""); + hocon.AppendLine("}"); + + hocon.AppendLine("# Interaction Settings"); + hocon.AppendLine("Interaction {"); + hocon.AppendLine($" NonInteractiveMode = {config.NonInteractiveMode.ToString().ToLower()}"); + hocon.AppendLine($" NonInteractiveModeListName = \"{config.NonInteractiveModeListName}\""); + hocon.AppendLine($" NonInteractiveModePurchasedTab = {config.NonInteractiveModePurchasedTab.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Performance Settings"); + hocon.AppendLine("Performance {"); + hocon.AppendLine($" Timeout = {(config.Timeout.HasValue ? config.Timeout.Value : -1)}"); + hocon.AppendLine($" LimitDownloadRate = {config.LimitDownloadRate.ToString().ToLower()}"); + hocon.AppendLine($" DownloadLimitInMbPerSec = {config.DownloadLimitInMbPerSec}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Logging/Debug Settings"); + hocon.AppendLine("Logging {"); + hocon.AppendLine($" LoggingLevel = \"{config.LoggingLevel.ToString().ToLower()}\""); + hocon.AppendLine("}"); + + return hocon.ToString(); + } + + private void ValidateFileNameFormat(string? format, string settingName) + { + if (!string.IsNullOrEmpty(format) && + !format.Contains("{mediaId}", StringComparison.OrdinalIgnoreCase) && + !format.Contains("{filename}", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + $"{settingName} is not unique enough. Please include either '{{mediaId}}' or '{{filename}}' to ensure files are not overwritten."); + } + } + + private VideoResolution ParseVideoResolution(string value) + { + if (value.Equals("source", StringComparison.OrdinalIgnoreCase)) + return VideoResolution.source; + + return Enum.Parse("_" + value, ignoreCase: true); + } + } +} diff --git a/OF DL/Services/IAuthService.cs b/OF DL/Services/IAuthService.cs new file mode 100644 index 0000000..8d14486 --- /dev/null +++ b/OF DL/Services/IAuthService.cs @@ -0,0 +1,12 @@ +using OF_DL.Entities; + +namespace OF_DL.Services +{ + public interface IAuthService + { + Auth? CurrentAuth { get; set; } + Task LoadFromFileAsync(string filePath = "auth.json"); + Task LoadFromBrowserAsync(); + Task SaveToFileAsync(string filePath = "auth.json"); + } +} diff --git a/OF DL/Services/IConfigService.cs b/OF DL/Services/IConfigService.cs new file mode 100644 index 0000000..773e82c --- /dev/null +++ b/OF DL/Services/IConfigService.cs @@ -0,0 +1,13 @@ +using OF_DL.Entities; + +namespace OF_DL.Services +{ + public interface IConfigService + { + Config? CurrentConfig { get; } + bool IsCliNonInteractive { get; } + Task LoadConfigurationAsync(string[] args); + Task SaveConfigurationAsync(string filePath = "config.conf"); + void UpdateConfig(Config newConfig); + } +} diff --git a/OF DL/Services/ILoggingService.cs b/OF DL/Services/ILoggingService.cs new file mode 100644 index 0000000..b0516ef --- /dev/null +++ b/OF DL/Services/ILoggingService.cs @@ -0,0 +1,12 @@ +using OF_DL.Enumerations; +using Serilog.Core; + +namespace OF_DL.Services +{ + public interface ILoggingService + { + LoggingLevelSwitch LevelSwitch { get; } + void UpdateLoggingLevel(LoggingLevel newLevel); + LoggingLevel GetCurrentLoggingLevel(); + } +} diff --git a/OF DL/Services/LoggingService.cs b/OF DL/Services/LoggingService.cs new file mode 100644 index 0000000..c65511a --- /dev/null +++ b/OF DL/Services/LoggingService.cs @@ -0,0 +1,42 @@ +using OF_DL.Enumerations; +using Serilog; +using Serilog.Core; +using Serilog.Events; + +namespace OF_DL.Services +{ + public class LoggingService : ILoggingService + { + public LoggingLevelSwitch LevelSwitch { get; } + + public LoggingService() + { + LevelSwitch = new LoggingLevelSwitch(); + InitializeLogger(); + } + + private void InitializeLogger() + { + // Set the initial level to Error (until we've read from config) + LevelSwitch.MinimumLevel = LogEventLevel.Error; + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.ControlledBy(LevelSwitch) + .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); + + Log.Debug("Logging service initialized"); + } + + public void UpdateLoggingLevel(LoggingLevel newLevel) + { + LevelSwitch.MinimumLevel = (LogEventLevel)newLevel; + Log.Debug("Logging level updated to: {LoggingLevel}", newLevel); + } + + public LoggingLevel GetCurrentLoggingLevel() + { + return (LoggingLevel)LevelSwitch.MinimumLevel; + } + } +} From 4711c537463db4783035cfd5081c6c8ea8a3af3c Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Fri, 6 Feb 2026 00:59:07 -0600 Subject: [PATCH 04/55] Replace helper classes with services --- OF DL/CLI/SpectreProgressReporter.cs | 28 + OF DL/Entities/Config.cs | 36 +- OF DL/Entities/DownloadResult.cs | 37 + OF DL/Entities/IDownloadConfig.cs | 56 - OF DL/Helpers/AuthHelper.cs | 167 -- OF DL/Helpers/DownloadContext.cs | 24 +- OF DL/Helpers/DownloadHelper.cs | 2040 ------------- OF DL/Helpers/Interfaces/IAuthHelper.cs | 10 - OF DL/Helpers/Interfaces/IDownloadHelper.cs | 45 - OF DL/Program.cs | 1401 +++------ .../APIHelper.cs => Services/APIService.cs} | 454 ++- OF DL/Services/AuthService.cs | 166 +- OF DL/Services/ConfigService.cs | 2 +- .../DBHelper.cs => Services/DBService.cs} | 24 +- OF DL/Services/DownloadService.cs | 2630 +++++++++++++++++ .../FileNameService.cs} | 23 +- .../IAPIHelper.cs => Services/IAPIService.cs} | 34 +- OF DL/Services/IConfigService.cs | 2 +- .../IDBHelper.cs => Services/IDBService.cs} | 4 +- OF DL/Services/IDownloadService.cs | 50 + OF DL/Services/IFileNameService.cs | 8 + OF DL/Services/IProgressReporter.cs | 20 + OF DL/{ => Widevine}/CDMApi.cs | 5 +- 23 files changed, 3657 insertions(+), 3609 deletions(-) create mode 100644 OF DL/CLI/SpectreProgressReporter.cs create mode 100644 OF DL/Entities/DownloadResult.cs delete mode 100644 OF DL/Entities/IDownloadConfig.cs delete mode 100644 OF DL/Helpers/AuthHelper.cs delete mode 100644 OF DL/Helpers/DownloadHelper.cs delete mode 100644 OF DL/Helpers/Interfaces/IAuthHelper.cs delete mode 100644 OF DL/Helpers/Interfaces/IDownloadHelper.cs rename OF DL/{Helpers/APIHelper.cs => Services/APIService.cs} (84%) rename OF DL/{Helpers/DBHelper.cs => Services/DBService.cs} (97%) create mode 100644 OF DL/Services/DownloadService.cs rename OF DL/{Helpers/FileNameHelper.cs => Services/FileNameService.cs} (93%) rename OF DL/{Helpers/Interfaces/IAPIHelper.cs => Services/IAPIService.cs} (60%) rename OF DL/{Helpers/Interfaces/IDBHelper.cs => Services/IDBService.cs} (95%) create mode 100644 OF DL/Services/IDownloadService.cs create mode 100644 OF DL/Services/IFileNameService.cs create mode 100644 OF DL/Services/IProgressReporter.cs rename OF DL/{ => Widevine}/CDMApi.cs (89%) diff --git a/OF DL/CLI/SpectreProgressReporter.cs b/OF DL/CLI/SpectreProgressReporter.cs new file mode 100644 index 0000000..f6c65ab --- /dev/null +++ b/OF DL/CLI/SpectreProgressReporter.cs @@ -0,0 +1,28 @@ +using OF_DL.Services; +using Spectre.Console; + +namespace OF_DL.CLI; + +/// +/// Implementation of IProgressReporter that uses Spectre.Console's ProgressTask for CLI output. +/// +public class SpectreProgressReporter : IProgressReporter +{ + private readonly ProgressTask _task; + + public SpectreProgressReporter(ProgressTask task) + { + _task = task ?? throw new ArgumentNullException(nameof(task)); + } + + public void ReportProgress(long increment) + { + _task.Increment(increment); + } + + public void ReportStatus(string message) + { + // Optionally update task description or handle status messages + // For now, we'll leave this empty as the task description is set when creating the progress bar + } +} diff --git a/OF DL/Entities/Config.cs b/OF DL/Entities/Config.cs index 0934422..7fbc8c9 100644 --- a/OF DL/Entities/Config.cs +++ b/OF DL/Entities/Config.cs @@ -1,11 +1,12 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using OF_DL.Enumerations; +using Serilog; namespace OF_DL.Entities { - public class Config : IDownloadConfig, IFileNameFormatConfig + public class Config : IFileNameFormatConfig { [ToggleableConfig] public bool DownloadAvatarHeaderPhoto { get; set; } = true; @@ -107,6 +108,39 @@ namespace OF_DL.Entities // When enabled, post/message text is stored as-is without XML stripping. [ToggleableConfig] public bool DisableTextSanitization { get; set; } = false; + + public IFileNameFormatConfig GetCreatorFileNameFormatConfig(string username) + { + FileNameFormatConfig createFileNameFormatConfig = new FileNameFormatConfig(); + + Func func = (val1, val2) => + { + if (string.IsNullOrEmpty(val1)) + return val2; + else + return val1; + }; + + if(CreatorConfigs.TryGetValue(username, out CreatorConfig? creatorConfig)) + { + createFileNameFormatConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat; + createFileNameFormatConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat; + createFileNameFormatConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat; + createFileNameFormatConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat; + } + + createFileNameFormatConfig.PaidMessageFileNameFormat = func(createFileNameFormatConfig.PaidMessageFileNameFormat, PaidMessageFileNameFormat); + createFileNameFormatConfig.PostFileNameFormat = func(createFileNameFormatConfig.PostFileNameFormat, PostFileNameFormat); + createFileNameFormatConfig.MessageFileNameFormat = func(createFileNameFormatConfig.MessageFileNameFormat, MessageFileNameFormat); + createFileNameFormatConfig.PaidPostFileNameFormat = func(createFileNameFormatConfig.PaidPostFileNameFormat, PaidPostFileNameFormat); + + Log.Debug("PaidMessageFilenameFormat: {CombinedConfigPaidMessageFileNameFormat}", createFileNameFormatConfig.PaidMessageFileNameFormat); + Log.Debug("PostFileNameFormat: {CombinedConfigPostFileNameFormat}", createFileNameFormatConfig.PostFileNameFormat); + Log.Debug("MessageFileNameFormat: {CombinedConfigMessageFileNameFormat}", createFileNameFormatConfig.MessageFileNameFormat); + Log.Debug("PaidPostFileNameFormat: {CombinedConfigPaidPostFileNameFormat}", createFileNameFormatConfig.PaidPostFileNameFormat); + + return createFileNameFormatConfig; + } } public class CreatorConfig : IFileNameFormatConfig diff --git a/OF DL/Entities/DownloadResult.cs b/OF DL/Entities/DownloadResult.cs new file mode 100644 index 0000000..f44d26d --- /dev/null +++ b/OF DL/Entities/DownloadResult.cs @@ -0,0 +1,37 @@ +namespace OF_DL.Entities; + +/// +/// Represents the result of a download operation. +/// +public class DownloadResult +{ + /// + /// Total number of media items processed. + /// + public int TotalCount { get; set; } + + /// + /// Number of newly downloaded media items. + /// + public int NewDownloads { get; set; } + + /// + /// Number of media items that were already downloaded. + /// + public int ExistingDownloads { get; set; } + + /// + /// The type of media downloaded (e.g., "Posts", "Messages", "Stories", etc.). + /// + public string MediaType { get; set; } = string.Empty; + + /// + /// Indicates whether the download operation was successful. + /// + public bool Success { get; set; } = true; + + /// + /// Optional error message if the download failed. + /// + public string? ErrorMessage { get; set; } +} diff --git a/OF DL/Entities/IDownloadConfig.cs b/OF DL/Entities/IDownloadConfig.cs deleted file mode 100644 index 21565d0..0000000 --- a/OF DL/Entities/IDownloadConfig.cs +++ /dev/null @@ -1,56 +0,0 @@ -using OF_DL.Enumerations; - -namespace OF_DL.Entities -{ - public interface IDownloadConfig - { - bool DownloadAvatarHeaderPhoto { get; set; } - bool DownloadPaidPosts { get; set; } - bool DownloadPosts { get; set; } - bool DownloadArchived { get; set; } - bool DownloadStreams { get; set; } - bool DownloadStories { get; set; } - bool DownloadHighlights { get; set; } - bool DownloadMessages { get; set; } - bool DownloadPaidMessages { get; set; } - bool DownloadImages { get; set; } - bool DownloadVideos { get; set; } - bool DownloadAudios { get; set; } - - VideoResolution DownloadVideoResolution { get; set; } - - int? Timeout { get; set; } - bool FolderPerPaidPost { get; set; } - bool FolderPerPost { get; set; } - bool FolderPerPaidMessage { get; set; } - bool FolderPerMessage { get; set; } - - bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } - bool ShowScrapeSize { get; set; } - bool LimitDownloadRate { get; set; } - int DownloadLimitInMbPerSec { get; set; } - string? FFmpegPath { get; set; } - - bool SkipAds { get; set; } - bool BypassContentForCreatorsWhoNoLongerExist { get; set; } - - #region Download Date Configurations - - bool DownloadOnlySpecificDates { get; set; } - - // This enum will define if we want data from before or after the CustomDate. - DownloadDateSelection DownloadDateSelection { get; set; } - - // This is the specific date used in combination with the above enum. - DateTime? CustomDate { get; set; } - #endregion - - bool DownloadPostsIncrementally { get; set; } - - bool DownloadDuplicatedMedia { get; set; } - public LoggingLevel LoggingLevel { get; set; } - - bool IgnoreOwnMessages { get; set; } - } - -} diff --git a/OF DL/Helpers/AuthHelper.cs b/OF DL/Helpers/AuthHelper.cs deleted file mode 100644 index 33d522c..0000000 --- a/OF DL/Helpers/AuthHelper.cs +++ /dev/null @@ -1,167 +0,0 @@ -using OF_DL.Entities; -using OF_DL.Helpers.Interfaces; -using PuppeteerSharp; -using PuppeteerSharp.BrowserData; -using Serilog; - -namespace OF_DL.Helpers; - -public class AuthHelper : IAuthHelper -{ - private readonly LaunchOptions _options = new() - { - Headless = false, - Channel = ChromeReleaseChannel.Stable, - DefaultViewport = null, - Args = ["--no-sandbox", "--disable-setuid-sandbox"], - UserDataDir = Path.GetFullPath("chrome-data") - }; - - private readonly string[] _desiredCookies = - [ - "auth_id", - "sess" - ]; - - private const int LoginTimeout = 600000; // 10 minutes - private const int FeedLoadTimeout = 60000; // 1 minute - - public async Task SetupBrowser(bool runningInDocker) - { - string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH"); - if (executablePath != null) - { - Log.Information("OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}", executablePath); - _options.ExecutablePath = executablePath; - } - else - { - var browserFetcher = new BrowserFetcher(); - var installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList(); - if (installedBrowsers.Count == 0) - { - Log.Information("Downloading browser."); - var downloadedBrowser = await browserFetcher.DownloadAsync(); - Log.Information("Browser downloaded. Path: {executablePath}", - downloadedBrowser.GetExecutablePath()); - _options.ExecutablePath = downloadedBrowser.GetExecutablePath(); - } - else - { - _options.ExecutablePath = installedBrowsers.First().GetExecutablePath(); - } - } - - if (runningInDocker) - { - Log.Information("Running in Docker. Disabling sandbox and GPU."); - _options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]; - } - } - - private async Task GetBcToken(IPage page) - { - return await page.EvaluateExpressionAsync("window.localStorage.getItem('bcTokenSha') || ''"); - } - - public async Task GetAuthFromBrowser(bool isDocker = false) - { - try - { - IBrowser? browser; - try - { - browser = await Puppeteer.LaunchAsync(_options); - } - catch (ProcessException e) - { - if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir)) - { - Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again."); - Directory.Delete(_options.UserDataDir, true); - browser = await Puppeteer.LaunchAsync(_options); - } - else - { - throw; - } - } - - if (browser == null) - { - throw new Exception("Could not get browser"); - } - - IPage[]? pages = await browser.PagesAsync(); - IPage? page = pages.First(); - - if (page == null) - { - throw new Exception("Could not get page"); - } - - Log.Debug("Navigating to OnlyFans."); - await page.GoToAsync("https://onlyfans.com"); - - Log.Debug("Waiting for user to login"); - await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout }); - Log.Debug("Feed element detected (user logged in)"); - - await page.ReloadAsync(); - - await page.WaitForNavigationAsync(new NavigationOptions { - WaitUntil = [WaitUntilNavigation.Networkidle2], - Timeout = FeedLoadTimeout - }); - Log.Debug("DOM loaded. Getting BC token and cookies ..."); - - string xBc; - try - { - xBc = await GetBcToken(page); - } - catch (Exception e) - { - throw new Exception("Error getting bcToken"); - } - - Dictionary mappedCookies = (await page.GetCookiesAsync()) - .Where(cookie => cookie.Domain.Contains("onlyfans.com")) - .ToDictionary(cookie => cookie.Name, cookie => cookie.Value); - - mappedCookies.TryGetValue("auth_id", out string? userId); - if (userId == null) - { - throw new Exception("Could not find 'auth_id' cookie"); - } - - mappedCookies.TryGetValue("sess", out string? sess); - if (sess == null) - { - throw new Exception("Could not find 'sess' cookie"); - } - - string? userAgent = await browser.GetUserAgentAsync(); - if (userAgent == null) - { - throw new Exception("Could not get user agent"); - } - - string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key)) - .Select(key => $"${key}={mappedCookies[key]};")); - - return new Auth() - { - COOKIE = cookies, - USER_AGENT = userAgent, - USER_ID = userId, - X_BC = xBc - }; - } - catch (Exception e) - { - Log.Error(e, "Error getting auth from browser"); - return null; - } - } -} diff --git a/OF DL/Helpers/DownloadContext.cs b/OF DL/Helpers/DownloadContext.cs index a3146d7..cae7598 100644 --- a/OF DL/Helpers/DownloadContext.cs +++ b/OF DL/Helpers/DownloadContext.cs @@ -1,28 +1,26 @@ -using OF_DL.Entities; +using OF_DL.Entities; +using OF_DL.Services; namespace OF_DL.Helpers { internal interface IDownloadContext { - public IDownloadConfig DownloadConfig { get; } public IFileNameFormatConfig FileNameFormatConfig { get; } - public IAPIHelper ApiHelper { get; } - public IDBHelper DBHelper { get; } - public IDownloadHelper DownloadHelper { get; } + public IAPIService ApiService { get; } + public IDBService DBService { get; } + public IDownloadService DownloadService { get; } } internal class DownloadContext( - IDownloadConfig downloadConfig, IFileNameFormatConfig fileNameFormatConfig, - IAPIHelper apiHelper, - IDBHelper dBHelper, - IDownloadHelper downloadHelper) + IAPIService apiService, + IDBService dBService, + IDownloadService downloadService) : IDownloadContext { - public IAPIHelper ApiHelper { get; } = apiHelper; - public IDBHelper DBHelper { get; } = dBHelper; - public IDownloadHelper DownloadHelper { get; } = downloadHelper; - public IDownloadConfig DownloadConfig { get; } = downloadConfig; + public IAPIService ApiService { get; } = apiService; + public IDBService DBService { get; } = dBService; + public IDownloadService DownloadService { get; } = downloadService; public IFileNameFormatConfig FileNameFormatConfig { get; } = fileNameFormatConfig; } } diff --git a/OF DL/Helpers/DownloadHelper.cs b/OF DL/Helpers/DownloadHelper.cs deleted file mode 100644 index 8454bfc..0000000 --- a/OF DL/Helpers/DownloadHelper.cs +++ /dev/null @@ -1,2040 +0,0 @@ -using FFmpeg.NET; -using FFmpeg.NET.Events; -using FFmpeg.NET.Services; -using OF_DL.Entities; -using OF_DL.Entities.Archived; -using OF_DL.Entities.Messages; -using OF_DL.Entities.Post; -using OF_DL.Entities.Purchased; -using OF_DL.Entities.Stories; -using OF_DL.Entities.Streams; -using OF_DL.Enumerations; -using OF_DL.Utils; -using Org.BouncyCastle.Asn1.Tsp; -using Org.BouncyCastle.Asn1.X509; -using Org.BouncyCastle.Tsp; -using Serilog; -using Spectre.Console; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Xml.Linq; -using static OF_DL.Entities.Lists.UserList; -using static OF_DL.Entities.Messages.Messages; -using FromUser = OF_DL.Entities.Messages.FromUser; - -namespace OF_DL.Helpers; - -public class DownloadHelper : IDownloadHelper -{ - private readonly Auth auth; - private readonly IDBHelper dbHelper; - private readonly IFileNameHelper fileNameHelper; - private readonly IDownloadConfig downloadConfig; - private readonly IFileNameFormatConfig fileNameFormatConfig; - private TaskCompletionSource _completionSource; - - public DownloadHelper(Auth auth, IDownloadConfig downloadConfig, IFileNameFormatConfig fileNameFormatConfig, IDBHelper dbHelper, IFileNameHelper fileNameHelper) - { - this.auth = auth; - this.dbHelper = dbHelper; - this.fileNameHelper = fileNameHelper; - this.downloadConfig = downloadConfig; - this.fileNameFormatConfig = fileNameFormatConfig; - } - - #region common - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - protected async Task CreateDirectoriesAndDownloadMedia(string path, - string url, - string folder, - long media_id, - string api_type, - ProgressTask task, - string serverFileName, - string resolvedFileName) - { - try - { - string customFileName = string.Empty; - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - string extension = Path.GetExtension(url.Split("?")[0]); - - path = UpdatePathBasedOnExtension(folder, path, extension); - - return await ProcessMediaDownload(folder, media_id, api_type, url, path, serverFileName, resolvedFileName, extension, task); - } - catch (Exception ex) - { - Console.WriteLine("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); - } - } - return false; - } - - - /// - /// Updates the given path based on the file extension. - /// - /// The parent folder. - /// The initial relative path. - /// The file extension. - /// A string that represents the updated path based on the file extension. - private string UpdatePathBasedOnExtension(string folder, string path, string extension) - { - string subdirectory = string.Empty; - - switch (extension.ToLower()) - { - case ".jpg": - case ".jpeg": - case ".png": - subdirectory = "/Images"; - break; - case ".mp4": - case ".avi": - case ".wmv": - case ".gif": - case ".mov": - subdirectory = "/Videos"; - break; - case ".mp3": - case ".wav": - case ".ogg": - subdirectory = "/Audios"; - break; - } - - if (!string.IsNullOrEmpty(subdirectory)) - { - path += subdirectory; - string fullPath = folder + path; - - if (!Directory.Exists(fullPath)) - { - Directory.CreateDirectory(fullPath); - } - } - - return path; - } - - - /// - /// Generates a custom filename based on the given format and properties. - /// - /// The format string for the filename. - /// General information about the post. - /// Media associated with the post. - /// Author of the post. - /// Dictionary containing user-related data. - /// Helper class for filename operations. - /// A Task resulting in a string that represents the custom filename. - private async Task GenerateCustomFileName(string filename, - string? filenameFormat, - object? postInfo, - object? postMedia, - object? author, - string username, - Dictionary users, - IFileNameHelper fileNameHelper, - CustomFileNameOption option) - { - if (string.IsNullOrEmpty(filenameFormat) || postInfo == null || postMedia == null || author == null) - { - return option switch - { - CustomFileNameOption.ReturnOriginal => filename, - CustomFileNameOption.ReturnEmpty => string.Empty, - _ => filename, - }; - } - - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - properties.AddRange(matches.Select(match => match.Groups[1].Value)); - - Dictionary values = await fileNameHelper.GetFilename(postInfo, postMedia, author, properties, username, users); - return await fileNameHelper.BuildFilename(filenameFormat, values); - } - - - private async Task GetFileSizeAsync(string url, Auth auth) - { - long fileSize = 0; - - try - { - Uri uri = new(url); - - if (uri.Host == "cdn3.onlyfans.com" && uri.LocalPath.Contains("/dash/files")) - { - string[] messageUrlParsed = url.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - - mpdURL = mpdURL.Replace(".mpd", "_source.mp4"); - - using HttpClient client = new(); - client.DefaultRequestHeaders.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {auth.COOKIE}"); - client.DefaultRequestHeaders.Add("User-Agent", auth.USER_AGENT); - - using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) - { - fileSize = response.Content.Headers.ContentLength ?? 0; - } - } - else - { - using HttpClient client = new(); - client.DefaultRequestHeaders.Add("User-Agent", auth.USER_AGENT); - using HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) - { - fileSize = response.Content.Headers.ContentLength ?? 0; - } - } - } - catch (Exception ex) - { - Console.WriteLine($"Error getting file size for URL '{url}': {ex.Message}"); - } - - return fileSize; - } - - public static async Task GetDRMVideoLastModified(string url, Auth auth) - { - Uri uri = new(url); - - string[] messageUrlParsed = url.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - - mpdURL = mpdURL.Replace(".mpd", "_source.mp4"); - - using HttpClient client = new(); - client.DefaultRequestHeaders.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {auth.COOKIE}"); - client.DefaultRequestHeaders.Add("User-Agent", auth.USER_AGENT); - - using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) - { - return response.Content.Headers.LastModified.Value.DateTime; - } - return DateTime.Now; - } - public static async Task GetMediaLastModified(string url) - { - using HttpClient client = new(); - - using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) - { - return response.Content.Headers.LastModified.Value.DateTime; - } - return DateTime.Now; - } - - /// - /// Processes the download and database update of media. - /// - /// The folder where the media is stored. - /// The ID of the media. - /// The full path to the media. - /// The URL from where to download the media. - /// The relative path to the media. - /// The filename after any required manipulations. - /// The file extension. - /// The task object for tracking progress. - /// A Task resulting in a boolean indicating whether the media is newly downloaded or not. - public async Task ProcessMediaDownload(string folder, - long media_id, - string api_type, - string url, - string path, - string serverFilename, - string resolvedFilename, - string extension, - ProgressTask task) - { - - try - { - if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) - { - return await HandleNewMedia(folder: folder, - media_id: media_id, - api_type: api_type, - url: url, - path: path, - serverFilename: serverFilename, - resolvedFilename: resolvedFilename, - extension: extension, - task: task); - } - else - { - bool status = await HandlePreviouslyDownloadedMediaAsync(folder, media_id, api_type, task); - if (downloadConfig.RenameExistingFilesWhenCustomFormatIsSelected && (serverFilename != resolvedFilename)) - { - await HandleRenamingOfExistingFilesAsync(folder, media_id, api_type, path, serverFilename, resolvedFilename, extension); - } - return status; - } - } - catch (Exception ex) - { - // Handle exception (e.g., log it) - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - } - - - private async Task HandleRenamingOfExistingFilesAsync(string folder, - long media_id, - string api_type, - string path, - string serverFilename, - string resolvedFilename, - string extension) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{serverFilename}{extension}"; - string fullPathWithTheNewFileName = $"{folder}{path}/{resolvedFilename}{extension}"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - var lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName); - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, resolvedFilename + extension, size, true, lastModified); - return true; - } - - - /// - /// Handles new media by downloading and updating the database. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// A Task resulting in a boolean indicating whether the media is newly downloaded or not. - private async Task HandleNewMedia(string folder, - long media_id, - string api_type, - string url, - string path, - string serverFilename, - string resolvedFilename, - string extension, - ProgressTask task) - { - long fileSizeInBytes; - DateTime lastModified; - bool status; - - string fullPathWithTheServerFileName = $"{folder}{path}/{serverFilename}{extension}"; - string fullPathWithTheNewFileName = $"{folder}{path}/{resolvedFilename}{extension}"; - - //there are a few possibilities here. - //1.file has been downloaded in the past but it has the server filename - // in that case it should be set as existing and it should be renamed - //2.file has been downloaded in the past but it has custom filename. - // it should be set as existing and nothing else. - // of coures 1 and 2 depends in the fact that there may be a difference in the resolved file name - // (ie user has selected a custom format. If he doesn't then the resolved name will be the same as the server filename - //3.file doesn't exist and it should be downloaded. - - // Handle the case where the file has been downloaded in the past with the server filename - //but it has downloaded outsite of this application so it doesn't exist in the database - if (File.Exists(fullPathWithTheServerFileName)) - { - string finalPath; - if (fullPathWithTheServerFileName != fullPathWithTheNewFileName) - { - finalPath = fullPathWithTheNewFileName; - //rename. - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - } - } - else - { - finalPath = fullPathWithTheServerFileName; - } - - fileSizeInBytes = GetLocalFileSize(finalPath); - lastModified = File.GetLastWriteTime(finalPath); - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - status = false; - } - // Handle the case where the file has been downloaded in the past with a custom filename. - //but it has downloaded outsite of this application so it doesn't exist in the database - // this is a bit improbable but we should check for that. - else if (File.Exists(fullPathWithTheNewFileName)) - { - fileSizeInBytes = GetLocalFileSize(fullPathWithTheNewFileName); - lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName); - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - status = false; - } - else //file doesn't exist and we should download it. - { - lastModified = await DownloadFile(url, fullPathWithTheNewFileName, task); - fileSizeInBytes = GetLocalFileSize(fullPathWithTheNewFileName); - status = true; - } - - //finaly check which filename we should use. Custom or the server one. - //if a custom is used, then the servefilename will be different from the resolved filename. - string finalName = serverFilename == resolvedFilename ? serverFilename : resolvedFilename; - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, finalName + extension, fileSizeInBytes, true, lastModified); - return status; - } - - - /// - /// Handles media that has been previously downloaded and updates the task accordingly. - /// - /// - /// - /// - /// - /// A boolean indicating whether the media is newly downloaded or not. - private async Task HandlePreviouslyDownloadedMediaAsync(string folder, long media_id, string api_type, ProgressTask task) - { - if (downloadConfig.ShowScrapeSize) - { - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - task.Increment(size); - } - else - { - task.Increment(1); - } - return false; - } - - - /// - /// Gets the file size of the media. - /// - /// The path to the file. - /// The file size in bytes. - private long GetLocalFileSize(string filePath) - { - return new FileInfo(filePath).Length; - } - - - /// - /// Downloads a file from the given URL and saves it to the specified destination path. - /// - /// The URL to download the file from. - /// The path where the downloaded file will be saved. - /// Progress tracking object. - /// A Task resulting in a DateTime indicating the last modified date of the downloaded file. - - private async Task DownloadFile(string url, string destinationPath, ProgressTask task) - { - using var client = new HttpClient(); - var request = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri(url) - }; - - using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStreamAsync(); - - // Wrap the body stream with the ThrottledStream to limit read rate. - using (ThrottledStream throttledStream = new(body, downloadConfig.DownloadLimitInMbPerSec * 1_000_000, downloadConfig.LimitDownloadRate)) - { - using FileStream fileStream = new(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 16384, true); - var buffer = new byte[16384]; - int read; - while ((read = await throttledStream.ReadAsync(buffer, CancellationToken.None)) > 0) - { - if (downloadConfig.ShowScrapeSize) - { - task.Increment(read); - } - await fileStream.WriteAsync(buffer.AsMemory(0, read), CancellationToken.None); - } - } - - File.SetLastWriteTime(destinationPath, response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); - if (!downloadConfig.ShowScrapeSize) - { - task.Increment(1); - } - return response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now; - } - - public async Task CalculateTotalFileSize(List urls) - { - long totalFileSize = 0; - if (urls.Count > 250) - { - int batchSize = 250; - - var tasks = new List>(); - - for (int i = 0; i < urls.Count; i += batchSize) - { - var batchUrls = urls.Skip(i).Take(batchSize).ToList(); - - var batchTasks = batchUrls.Select(url => GetFileSizeAsync(url, auth)); - tasks.AddRange(batchTasks); - - await Task.WhenAll(batchTasks); - - await Task.Delay(5000); - } - - long[] fileSizes = await Task.WhenAll(tasks); - foreach (long fileSize in fileSizes) - { - totalFileSize += fileSize; - } - } - else - { - var tasks = new List>(); - - foreach (string url in urls) - { - tasks.Add(GetFileSizeAsync(url, auth)); - } - - long[] fileSizes = await Task.WhenAll(tasks); - foreach (long fileSize in fileSizes) - { - totalFileSize += fileSize; - } - } - - return totalFileSize; - } - #endregion - - #region drm common - - private async Task DownloadDrmMedia(string user_agent, string policy, string signature, string kvp, string sess, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string customFileName, string filename, string path) - { - try - { - _completionSource = new TaskCompletionSource(); - - int pos1 = decryptionKey.IndexOf(':'); - string decKey = ""; - if (pos1 >= 0) - { - decKey = decryptionKey.Substring(pos1 + 1); - } - - int streamIndex = 0; - string tempFilename = $"{folder}{path}/{filename}_source.mp4"; - - //int? streamIndex = await GetVideoStreamIndexFromMpd(url, policy, signature, kvp, downloadConfig.DownloadVideoResolution); - - //if (streamIndex == null) - // throw new Exception($"Could not find video stream for resolution {downloadConfig.DownloadVideoResolution}"); - - //string tempFilename; - - //switch (downloadConfig.DownloadVideoResolution) - //{ - // case VideoResolution.source: - // tempFilename = $"{folder}{path}/{filename}_source.mp4"; - // break; - // case VideoResolution._240: - // tempFilename = $"{folder}{path}/{filename}_240.mp4"; - // break; - // case VideoResolution._720: - // tempFilename = $"{folder}{path}/{filename}_720.mp4"; - // break; - // default: - // tempFilename = $"{folder}{path}/{filename}_source.mp4"; - // break; - //} - - // Configure ffmpeg log level and optional report file location - bool ffmpegDebugLogging = Log.IsEnabled(Serilog.Events.LogEventLevel.Debug); - - string logLevelArgs = ffmpegDebugLogging || downloadConfig.LoggingLevel is LoggingLevel.Verbose or LoggingLevel.Debug - ? "-loglevel debug -report" - : downloadConfig.LoggingLevel switch - { - LoggingLevel.Information => "-loglevel info", - LoggingLevel.Warning => "-loglevel warning", - LoggingLevel.Error => "-loglevel error", - LoggingLevel.Fatal => "-loglevel fatal", - _ => string.Empty - }; - - if (logLevelArgs.Contains("-report", StringComparison.OrdinalIgnoreCase)) - { - // Direct ffmpeg report files into the same logs directory Serilog uses (relative to current working directory) - string logDir = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "logs")); - Directory.CreateDirectory(logDir); - string ffReportPath = Path.Combine(logDir, "ffmpeg-%p-%t.log"); // ffmpeg will replace %p/%t - Environment.SetEnvironmentVariable("FFREPORT", $"file={ffReportPath}:level=32"); - Log.Debug("FFREPORT enabled at: {FFREPORT} (cwd: {Cwd})", Environment.GetEnvironmentVariable("FFREPORT"), Environment.CurrentDirectory); - } - else - { - Environment.SetEnvironmentVariable("FFREPORT", null); - Log.Debug("FFREPORT disabled (cwd: {Cwd})", Environment.CurrentDirectory); - } - - string cookieHeader = - "Cookie: " + - $"CloudFront-Policy={policy}; " + - $"CloudFront-Signature={signature}; " + - $"CloudFront-Key-Pair-Id={kvp}; " + - $"{sess}"; - - string parameters = - $"{logLevelArgs} " + - $"-cenc_decryption_key {decKey} " + - $"-headers \"{cookieHeader}\" " + - $"-user_agent \"{user_agent}\" " + - "-referer \"https://onlyfans.com\" " + - "-rw_timeout 20000000 " + - "-reconnect 1 -reconnect_streamed 1 -reconnect_on_network_error 1 -reconnect_delay_max 10 " + - "-y " + - $"-i \"{url}\" " + - $"-map 0:v:{streamIndex} -map 0:a? " + - "-c copy " + - $"\"{tempFilename}\""; - - Log.Debug($"Calling FFMPEG with Parameters: {parameters}"); - - Engine ffmpeg = new Engine(downloadConfig.FFmpegPath); - ffmpeg.Error += OnError; - ffmpeg.Complete += async (sender, args) => - { - _completionSource.TrySetResult(true); - await OnFFMPEGDownloadComplete(tempFilename, lastModified, folder, path, customFileName, filename, media_id, api_type, task); - }; - await ffmpeg.ExecuteAsync(parameters, CancellationToken.None); - - return await _completionSource.Task; - } - 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); - } - } - return false; - } - #endregion - - #region normal posts - public async Task DownloadPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary users) - { - string path; - if (downloadConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) - { - path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Posts/Free"; - } - - Uri uri = new(url); - string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); - - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); - } - public async Task DownloadPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, SinglePost.Author? author, Dictionary users) - { - string path; - if (downloadConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) - { - path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Posts/Free"; - } - - Uri uri = new(url); - string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); - - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); - } - public async Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary users) - { - string path; - if (downloadConfig.FolderPerPost && streamInfo != null && streamInfo?.id is not null && streamInfo?.postedAt is not null) - { - path = $"/Posts/Free/{streamInfo.id} {streamInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Posts/Free"; - } - - Uri uri = new(url); - string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, streamInfo, streamMedia, author, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); - - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); - } - - - public async Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Messages.List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users) - { - string path; - if (downloadConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) - { - path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Messages/Free"; - } - Uri uri = new(url); - string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); - } - - public async Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Messages.Medium? messageMedia, FromUser? fromUser, Dictionary users) - { - string path; - if (downloadConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) - { - path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Messages/Free"; - } - Uri uri = new(url); - string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); - } - - - public async Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Archived.List? messageInfo, Archived.Medium? messageMedia, Archived.Author? author, Dictionary users) - { - string path = "/Archived/Posts/Free"; - Uri uri = new(url); - string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, author, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); - } - - - - public async Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, ProgressTask task) - { - string path = "/Stories/Free"; - Uri uri = new(url); - string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, filename); - } - - public async Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) - { - string path; - if (downloadConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) - { - path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Messages/Paid"; - } - Uri uri = new(url); - string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); - } - - public async Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary users) - { - string path; - if (downloadConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) - { - path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Messages/Paid"; - } - Uri uri = new(url); - string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); - } - - public async Task DownloadPurchasedPostMedia(string url, - string folder, - long media_id, - string api_type, - ProgressTask task, - string? filenameFormat, - Purchased.List? messageInfo, - Medium? messageMedia, - Purchased.FromUser? fromUser, - Dictionary users) - { - string path; - if (downloadConfig.FolderPerPaidPost && messageInfo != null && messageInfo?.id is not null && messageInfo?.postedAt is not null) - { - path = $"/Posts/Paid/{messageInfo.id} {messageInfo.postedAt.Value:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Posts/Paid"; - } - Uri uri = new(url); - string filename = System.IO.Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, fromUser, folder.Split("/")[^1], users, fileNameHelper, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, task, filename, resolvedFilename); - } - - #endregion - public async Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username) - { - try - { - string path = $"/Profile"; - - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(avatarUrl)) - { - string avatarpath = $"{path}/Avatars"; - if (!Directory.Exists(folder + avatarpath)) - { - Directory.CreateDirectory(folder + avatarpath); - } - - List avatarMD5Hashes = WidevineClient.Utils.CalculateFolderMD5(folder + avatarpath); - - Uri uri = new(avatarUrl); - string destinationPath = $"{folder}{avatarpath}/"; - - var client = new HttpClient(); - - var request = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = uri - - }; - using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - response.EnsureSuccessStatusCode(); - - using var memoryStream = new MemoryStream(); - await response.Content.CopyToAsync(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - - MD5 md5 = MD5.Create(); - byte[] hash = md5.ComputeHash(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - if (!avatarMD5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant())) - { - destinationPath = destinationPath + string.Format("{0} {1}.jpg", username, response.Content.Headers.LastModified.HasValue ? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy") : DateTime.Now.ToString("dd-MM-yyyy")); - - using (FileStream fileStream = File.Create(destinationPath)) - { - await memoryStream.CopyToAsync(fileStream); - } - File.SetLastWriteTime(destinationPath, response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); - } - } - - if (!string.IsNullOrEmpty(headerUrl)) - { - string headerpath = $"{path}/Headers"; - if (!Directory.Exists(folder + headerpath)) - { - Directory.CreateDirectory(folder + headerpath); - } - - List headerMD5Hashes = WidevineClient.Utils.CalculateFolderMD5(folder + headerpath); - - Uri uri = new(headerUrl); - string destinationPath = $"{folder}{headerpath}/"; - - var client = new HttpClient(); - - var request = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = uri - - }; - using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - response.EnsureSuccessStatusCode(); - - using var memoryStream = new MemoryStream(); - await response.Content.CopyToAsync(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - - MD5 md5 = MD5.Create(); - byte[] hash = md5.ComputeHash(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - if (!headerMD5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant())) - { - destinationPath = destinationPath + string.Format("{0} {1}.jpg", username, response.Content.Headers.LastModified.HasValue ? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy") : DateTime.Now.ToString("dd-MM-yyyy")); - - using (FileStream fileStream = File.Create(destinationPath)) - { - await memoryStream.CopyToAsync(fileStream); - } - File.SetLastWriteTime(destinationPath, response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); - } - } - } - catch (Exception ex) - { - Console.WriteLine("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); - } - } - } - - private async Task OnFFMPEGDownloadComplete(string tempFilename, DateTime lastModified, string folder, string path, string customFileName, string filename, long media_id, string api_type, ProgressTask task) - { - try - { - if (File.Exists(tempFilename)) - { - File.SetLastWriteTime(tempFilename, lastModified); - } - if (!string.IsNullOrEmpty(customFileName)) - { - File.Move(tempFilename, $"{folder + path + "/" + customFileName + ".mp4"}"); - } - - // Cleanup Files - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : tempFilename).Length; - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); - } - 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); - } - } - } - - private void OnError(object sender, ConversionErrorEventArgs e) - { - // Guard all fields to avoid NullReference exceptions from FFmpeg.NET - var input = e?.Input?.Name ?? ""; - var output = e?.Output?.Name ?? ""; - var exitCode = e?.Exception?.ExitCode.ToString() ?? ""; - var message = e?.Exception?.Message ?? ""; - var inner = e?.Exception?.InnerException?.Message ?? ""; - - Log.Error("FFmpeg failed. Input={Input} Output={Output} ExitCode={ExitCode} Message={Message} Inner={Inner}", - input, output, exitCode, message, inner); - - _completionSource?.TrySetResult(false); - } - - - #region drm posts - public async Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, Messages.List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users) - { - try - { - string customFileName = string.Empty; - string path; - Uri uri = new(url); - string filename = System.IO.Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (downloadConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) - { - path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Messages/Free/Videos"; - } - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - - if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - Dictionary values = await fileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1],users); - customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); - } - - if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(auth.USER_AGENT, policy, signature, kvp, auth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, task, customFileName, filename, path); - } - else - { - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); - } - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (downloadConfig.RenameExistingFilesWhenCustomFormatIsSelected && (filename + "_source" != customFileName)) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); - } - } - - if (downloadConfig.ShowScrapeSize) - { - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - task.Increment(size); - } - else - { - task.Increment(1); - } - } - return false; - } - 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); - } - } - return false; - } - - public async Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Messages.Medium? messageMedia, FromUser? fromUser, Dictionary users) - { - try - { - string customFileName = string.Empty; - string path; - Uri uri = new(url); - string filename = System.IO.Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (downloadConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) - { - path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Messages/Free/Videos"; - } - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - - if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - Dictionary values = await fileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1],users); - customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); - } - - if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(auth.USER_AGENT, policy, signature, kvp, auth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, task, customFileName, filename, path); - } - else - { - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); - } - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (downloadConfig.RenameExistingFilesWhenCustomFormatIsSelected && (filename + "_source" != customFileName)) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); - } - } - - if (downloadConfig.ShowScrapeSize) - { - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - task.Increment(size); - } - else - { - task.Increment(1); - } - } - return false; - } - 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); - } - } - return false; - } - - - public async Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) - { - try - { - string customFileName = string.Empty; - string path; - Uri uri = new(url); - string filename = System.IO.Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (downloadConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) - { - path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Messages/Paid/Videos"; - } - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - Dictionary values = await fileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); - customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); - } - - if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(auth.USER_AGENT, policy, signature, kvp, auth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, task, customFileName, filename, path); - } - else - { - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); - } - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (downloadConfig.RenameExistingFilesWhenCustomFormatIsSelected && (filename + "_source" != customFileName)) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); - } - } - - if (downloadConfig.ShowScrapeSize) - { - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - task.Increment(size); - } - else - { - task.Increment(1); - } - } - return false; - } - 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); - } - } - return false; - } - - public async Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary users) - { - try - { - string customFileName = string.Empty; - string path; - Uri uri = new(url); - string filename = System.IO.Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (downloadConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) - { - path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Messages/Paid/Videos"; - } - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - Dictionary values = await fileNameHelper.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); - customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); - } - - if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(auth.USER_AGENT, policy, signature, kvp, auth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, task, customFileName, filename, path); - } - else - { - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); - } - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (downloadConfig.RenameExistingFilesWhenCustomFormatIsSelected && (filename + "_source" != customFileName)) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); - } - } - - if (downloadConfig.ShowScrapeSize) - { - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - task.Increment(size); - } - else - { - task.Increment(1); - } - } - return false; - } - 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); - } - } - return false; - } - - - public async Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary users) - { - try - { - string customFileName = string.Empty; - string path; - Uri uri = new(url); - string filename = System.IO.Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (downloadConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) - { - path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Posts/Free/Videos"; - } - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - Dictionary values = await fileNameHelper.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); - customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); - } - - if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(auth.USER_AGENT, policy, signature, kvp, auth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, task, customFileName, filename, path); - } - else - { - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); - } - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (downloadConfig.RenameExistingFilesWhenCustomFormatIsSelected && (filename + "_source" != customFileName)) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); - } - } - - if (downloadConfig.ShowScrapeSize) - { - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - task.Increment(size); - } - else - { - task.Increment(1); - } - } - return false; - } - 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); - } - } - return false; - } - public async Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, Dictionary users) - { - try - { - string customFileName = string.Empty; - string path; - Uri uri = new(url); - string filename = System.IO.Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (downloadConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) - { - path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Posts/Free/Videos"; - } - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - Dictionary values = await fileNameHelper.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); - customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); - } - - if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(auth.USER_AGENT, policy, signature, kvp, auth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, task, customFileName, filename, path); - } - else - { - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); - } - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (downloadConfig.RenameExistingFilesWhenCustomFormatIsSelected && (filename + "_source" != customFileName)) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); - } - } - - if (downloadConfig.ShowScrapeSize) - { - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - task.Increment(size); - } - else - { - task.Increment(1); - } - } - return false; - } - 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); - } - } - return false; - } - public async Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary users) - { - try - { - string customFileName = string.Empty; - string path; - Uri uri = new(url); - string filename = System.IO.Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (downloadConfig.FolderPerPost && streamInfo != null && streamInfo?.id is not null && streamInfo?.postedAt is not null) - { - path = $"/Posts/Free/{streamInfo.id} {streamInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Posts/Free/Videos"; - } - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && streamInfo != null && streamMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - Dictionary values = await fileNameHelper.GetFilename(streamInfo, streamMedia, author, properties, folder.Split("/")[^1], users); - customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); - } - - if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(auth.USER_AGENT, policy, signature, kvp, auth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, task, customFileName, filename, path); - } - else - { - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); - } - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (downloadConfig.RenameExistingFilesWhenCustomFormatIsSelected && (filename + "_source" != customFileName)) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); - } - } - - if (downloadConfig.ShowScrapeSize) - { - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - task.Increment(size); - } - else - { - task.Increment(1); - } - } - return false; - } - 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); - } - } - return false; - } - - public async Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, Purchased.List? postInfo, Medium? postMedia, Purchased.FromUser? fromUser, Dictionary users) - { - try - { - string customFileName = string.Empty; - string path; - Uri uri = new(url); - string filename = System.IO.Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (downloadConfig.FolderPerPaidPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) - { - path = $"/Posts/Paid/{postInfo.id} {postInfo.postedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Posts/Paid/Videos"; - } - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - - if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - Dictionary values = await fileNameHelper.GetFilename(postInfo, postMedia, fromUser, properties, folder.Split("/")[^1], users); - customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); - } - - if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(auth.USER_AGENT, policy, signature, kvp, auth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, task, customFileName, filename, path); - } - else - { - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); - } - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (downloadConfig.RenameExistingFilesWhenCustomFormatIsSelected && (filename + "_source" != customFileName)) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); - } - } - - if (downloadConfig.ShowScrapeSize) - { - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - task.Increment(size); - } - else - { - task.Increment(1); - } - } - return false; - } - 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); - } - } - return false; - } - - - public async Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, Archived.List? postInfo, Archived.Medium? postMedia, Archived.Author? author, Dictionary users) - { - try - { - string customFileName = string.Empty; - Uri uri = new(url); - string filename = System.IO.Path.GetFileName(uri.LocalPath).Split(".")[0]; - string path = "/Archived/Posts/Free/Videos"; - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - Dictionary values = await fileNameHelper.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); - customFileName = await fileNameHelper.BuildFilename(filenameFormat, values); - } - - if (!await dbHelper.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(auth.USER_AGENT, policy, signature, kvp, auth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, task, customFileName, filename, path); - } - else - { - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; - if (downloadConfig.ShowScrapeSize) - { - task.Increment(fileSizeInBytes); - } - else - { - task.Increment(1); - } - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); - } - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (downloadConfig.RenameExistingFilesWhenCustomFormatIsSelected && (filename + "_source" != customFileName)) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - await dbHelper.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); - } - } - - if (downloadConfig.ShowScrapeSize) - { - long size = await dbHelper.GetStoredFileSize(folder, media_id, api_type); - task.Increment(size); - } - else - { - task.Increment(1); - } - } - return false; - } - 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); - } - } - return false; - } - #endregion - - private async Task GetVideoStreamIndexFromMpd(string mpdUrl, string policy, string signature, string kvp, VideoResolution resolution) - { - HttpClient client = new(); - HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); - request.Headers.Add("user-agent", auth.USER_AGENT); - request.Headers.Add("Accept", "*/*"); - request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {auth.COOKIE};"); - using (var response = await client.SendAsync(request)) - { - response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); - XDocument doc = XDocument.Parse(body); - XNamespace ns = "urn:mpeg:dash:schema:mpd:2011"; - XNamespace cenc = "urn:mpeg:cenc:2013"; - var videoAdaptationSet = doc - .Descendants(ns + "AdaptationSet") - .FirstOrDefault(e => (string)e.Attribute("mimeType") == "video/mp4"); - - if (videoAdaptationSet == null) - return null; - - string targetHeight = resolution switch - { - VideoResolution._240 => "240", - VideoResolution._720 => "720", - VideoResolution.source => "1280", - _ => throw new ArgumentOutOfRangeException(nameof(resolution)) - }; - - var representations = videoAdaptationSet.Elements(ns + "Representation").ToList(); - - for (int i = 0; i < representations.Count; i++) - { - if ((string)representations[i].Attribute("height") == targetHeight) - return i; // this is the index FFmpeg will use for `-map 0:v:{i}` - } - } - - return null; - } -} diff --git a/OF DL/Helpers/Interfaces/IAuthHelper.cs b/OF DL/Helpers/Interfaces/IAuthHelper.cs deleted file mode 100644 index e1d0726..0000000 --- a/OF DL/Helpers/Interfaces/IAuthHelper.cs +++ /dev/null @@ -1,10 +0,0 @@ -using OF_DL.Entities; - -namespace OF_DL.Helpers.Interfaces -{ - public interface IAuthHelper - { - Task SetupBrowser(bool runningInDocker); - Task GetAuthFromBrowser(bool isDocker = false); - } -} diff --git a/OF DL/Helpers/Interfaces/IDownloadHelper.cs b/OF DL/Helpers/Interfaces/IDownloadHelper.cs deleted file mode 100644 index 62672ae..0000000 --- a/OF DL/Helpers/Interfaces/IDownloadHelper.cs +++ /dev/null @@ -1,45 +0,0 @@ -using OF_DL.Entities; -using OF_DL.Entities.Archived; -using OF_DL.Entities.Messages; -using OF_DL.Entities.Post; -using OF_DL.Entities.Purchased; -using OF_DL.Entities.Streams; -using Spectre.Console; -using static OF_DL.Entities.Messages.Messages; -using FromUser = OF_DL.Entities.Messages.FromUser; - -namespace OF_DL.Helpers -{ - public interface IDownloadHelper - { - Task CalculateTotalFileSize(List urls); - Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Archived.List messageInfo, Archived.Medium messageMedia, Archived.Author author, Dictionary users); - Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Archived.List postInfo, Archived.Medium postMedia, Archived.Author author, Dictionary users); - Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, Dictionary users); - Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username); - Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Messages.List messageInfo, Messages.Medium messageMedia, Messages.FromUser fromUser, Dictionary users); - Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Messages.List messageInfo, Messages.Medium messageMedia, Messages.FromUser fromUser, Dictionary users); - Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Post.List postInfo, Post.Medium postMedia, Post.Author author, Dictionary users); - Task DownloadPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary users); - Task DownloadPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, SinglePost.Author? author, Dictionary users); - Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List messageInfo, Medium messageMedia, Purchased.FromUser fromUser, Dictionary users); - - Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary users); - Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List messageInfo, Medium messageMedia, Purchased.FromUser fromUser, Dictionary users); - - Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary users); - Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List postInfo, Medium postMedia, Purchased.FromUser fromUser, Dictionary users); - Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List messageInfo, Medium messageMedia, Purchased.FromUser fromUser, Dictionary users); - Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, ProgressTask task); - Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary users); - Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Streams.List streamInfo, Streams.Medium streamMedia, Streams.Author author, Dictionary users); - Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, - FromUser? fromUser, Dictionary users); - - Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, - ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, - FromUser? fromUser, Dictionary users); - } -} diff --git a/OF DL/Program.cs b/OF DL/Program.cs index d79378b..041900c 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -8,6 +8,7 @@ using OF_DL.Entities.Purchased; using OF_DL.Entities.Streams; using OF_DL.Enumerations; using OF_DL.Enumurations; +using OF_DL.CLI; using OF_DL.Helpers; using OF_DL.Services; using Serilog; @@ -17,13 +18,11 @@ using System.Runtime.InteropServices; using System.Text.RegularExpressions; using static OF_DL.Entities.Messages.Messages; using Microsoft.Extensions.DependencyInjection; -using OF_DL.Helpers.Interfaces; namespace OF_DL; public class Program(IServiceProvider serviceProvider) { - public int MAX_AGE = 0; public static List paid_post_ids = new(); private static bool clientIdBlobMissing = false; @@ -79,68 +78,59 @@ public class Program(IServiceProvider serviceProvider) AnsiConsole.Markup("Documentation: [link]https://docs.ofdl.tools/[/]\n"); AnsiConsole.Markup("Discord server: [link]https://discord.com/invite/6bUW8EJ53j[/]\n\n"); - // Set up dependency injection with LoggingService and ConfigService - ServiceCollection services = new(); - services.AddSingleton(); - services.AddSingleton(); - ServiceProvider tempServiceProvider = services.BuildServiceProvider(); - // Load configuration using ConfigService - IConfigService configService = tempServiceProvider.GetRequiredService(); - - if (!await configService.LoadConfigurationAsync(args)) - { - AnsiConsole.MarkupLine($"\n[red]config.conf is not valid, check your syntax![/]\n"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - if (!configService.IsCliNonInteractive) - { - Console.ReadKey(); - } - Environment.Exit(3); - return; - } - - if (configService.CurrentConfig == null) - { - AnsiConsole.MarkupLine($"\n[red]Failed to load configuration.[/]\n"); - Environment.Exit(3); - return; - } - - AnsiConsole.Markup("[green]config.conf located successfully!\n[/]"); - - // Set up full dependency injection with loaded config - services = new ServiceCollection(); - ConfigureServices(services, configService.CurrentConfig, tempServiceProvider.GetRequiredService()); + ServiceCollection services = await ConfigureServices(args); var serviceProvider = services.BuildServiceProvider(); // Get the Program instance and run var program = serviceProvider.GetRequiredService(); - await program.RunAsync(args, configService.IsCliNonInteractive); + await program.RunAsync(); } - private static void ConfigureServices(IServiceCollection services, Entities.Config config, ILoggingService loggingService) + private static async Task ConfigureServices(string[] args) { - services.AddSingleton(config); - services.AddSingleton(config); - services.AddSingleton(config); + // 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"); + AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); + if (!configService.IsCliNonInteractive) + { + 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(); + services.AddSingleton(configService); services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); - } - private async Task RunAsync(string[] args, bool cliNonInteractive) + return services; + } + + private async Task RunAsync() { - var config = serviceProvider.GetRequiredService(); - var authService = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); try { @@ -158,7 +148,7 @@ public class Program(IServiceProvider serviceProvider) Console.Write("Press any key to continue.\n"); Log.Error("Windows version prior to 10.x: {0}", os.VersionString); - if (!cliNonInteractive) + if (!configService.CurrentConfig.NonInteractiveMode) { Console.ReadKey(); } @@ -241,13 +231,13 @@ public class Program(IServiceProvider serviceProvider) { // File exists but failed to load Log.Information("Auth file found but could not be deserialized"); - if (!config!.DisableBrowserAuth) + if (!configService.CurrentConfig!.DisableBrowserAuth) { Log.Debug("Deleting auth.json"); File.Delete("auth.json"); } - if (cliNonInteractive) + 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"); @@ -259,7 +249,7 @@ public class Program(IServiceProvider serviceProvider) } - if (!config!.DisableBrowserAuth) + if (!configService.CurrentConfig!.DisableBrowserAuth) { await LoadAuthFromBrowser(); } @@ -276,7 +266,7 @@ public class Program(IServiceProvider serviceProvider) } else { - if (cliNonInteractive) + if (configService.CurrentConfig.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"); @@ -287,7 +277,7 @@ public class Program(IServiceProvider serviceProvider) Environment.Exit(2); } - if (!config!.DisableBrowserAuth) + if (!configService.CurrentConfig!.DisableBrowserAuth) { await LoadAuthFromBrowser(); } @@ -323,7 +313,7 @@ public class Program(IServiceProvider serviceProvider) AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); Log.Error("rules.json processing failed.", e.Message); - if (!cliNonInteractive) + if (!configService.CurrentConfig.NonInteractiveMode) { Console.ReadKey(); } @@ -331,37 +321,37 @@ public class Program(IServiceProvider serviceProvider) } } - if(cliNonInteractive) + if(configService.CurrentConfig.NonInteractiveMode) { // CLI argument overrides configuration - config!.NonInteractiveMode = true; + configService.CurrentConfig!.NonInteractiveMode = true; Log.Debug("NonInteractiveMode = true"); } - if(config!.NonInteractiveMode) + if(configService.CurrentConfig!.NonInteractiveMode) { - cliNonInteractive = true; // If it was set in the config, reset the cli value so exception handling works + configService.CurrentConfig.NonInteractiveMode = true; // If it was set in the config, reset the cli value so exception handling works Log.Debug("NonInteractiveMode = true (set via config)"); } var ffmpegFound = false; var pathAutoDetected = false; - if (!string.IsNullOrEmpty(config!.FFmpegPath) && ValidateFilePath(config.FFmpegPath)) + if (!string.IsNullOrEmpty(configService.CurrentConfig!.FFmpegPath) && ValidateFilePath(configService.CurrentConfig.FFmpegPath)) { // FFmpeg path is set in config.json and is valid ffmpegFound = true; - Log.Debug($"FFMPEG found: {config.FFmpegPath}"); + Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); Log.Debug("FFMPEG path set in config.conf"); } else if (!string.IsNullOrEmpty(authService.CurrentAuth!.FFMPEG_PATH) && ValidateFilePath(authService.CurrentAuth.FFMPEG_PATH)) { // FFmpeg path is set in auth.json and is valid (config.conf takes precedence and auth.json is only available for backward compatibility) ffmpegFound = true; - config.FFmpegPath = authService.CurrentAuth.FFMPEG_PATH; - Log.Debug($"FFMPEG found: {config.FFmpegPath}"); + configService.CurrentConfig.FFmpegPath = authService.CurrentAuth.FFMPEG_PATH; + Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); Log.Debug("FFMPEG path set in auth.json"); } - else if (string.IsNullOrEmpty(config.FFmpegPath)) + else if (string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath)) { // FFmpeg path is not set in config.conf, so we will try to locate it in the PATH or current directory var ffmpegPath = GetFullPath("ffmpeg"); @@ -370,7 +360,7 @@ public class Program(IServiceProvider serviceProvider) // FFmpeg is found in the PATH or current directory ffmpegFound = true; pathAutoDetected = true; - config.FFmpegPath = ffmpegPath; + configService.CurrentConfig.FFmpegPath = ffmpegPath; Log.Debug($"FFMPEG found: {ffmpegPath}"); Log.Debug("FFMPEG path found via PATH or current directory"); } @@ -383,7 +373,7 @@ public class Program(IServiceProvider serviceProvider) // FFmpeg windows executable is found in the PATH or current directory ffmpegFound = true; pathAutoDetected = true; - config.FFmpegPath = ffmpegPath; + configService.CurrentConfig.FFmpegPath = ffmpegPath; Log.Debug($"FFMPEG found: {ffmpegPath}"); Log.Debug("FFMPEG path found in windows excutable directory"); } @@ -394,7 +384,7 @@ public class Program(IServiceProvider serviceProvider) { if (pathAutoDetected) { - AnsiConsole.Markup($"[green]FFmpeg located successfully. Path auto-detected: {config.FFmpegPath}\n[/]"); + AnsiConsole.Markup($"[green]FFmpeg located successfully. Path auto-detected: {configService.CurrentConfig.FFmpegPath}\n[/]"); } else { @@ -402,9 +392,9 @@ public class Program(IServiceProvider serviceProvider) } // Escape backslashes in the path for Windows - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && config.FFmpegPath!.Contains(@":\") && !config.FFmpegPath.Contains(@":\\")) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && configService.CurrentConfig.FFmpegPath!.Contains(@":\") && !configService.CurrentConfig.FFmpegPath.Contains(@":\\")) { - config.FFmpegPath = config.FFmpegPath.Replace(@"\", @"\\"); + configService.CurrentConfig.FFmpegPath = configService.CurrentConfig.FFmpegPath.Replace(@"\", @"\\"); } // Get FFmpeg version @@ -412,7 +402,7 @@ public class Program(IServiceProvider serviceProvider) { var processStartInfo = new System.Diagnostics.ProcessStartInfo { - FileName = config.FFmpegPath, + FileName = configService.CurrentConfig.FFmpegPath, Arguments = "-version", RedirectStandardOutput = true, RedirectStandardError = true, @@ -465,8 +455,8 @@ public class Program(IServiceProvider serviceProvider) else { AnsiConsole.Markup("[red]Cannot locate FFmpeg; please modify config.conf with the correct path. Press any key to exit.[/]"); - Log.Error($"Cannot locate FFmpeg with path: {config.FFmpegPath}"); - if (!config.NonInteractiveMode) + Log.Error($"Cannot locate FFmpeg with path: {configService.CurrentConfig.FFmpegPath}"); + if (!configService.CurrentConfig.NonInteractiveMode) { Console.ReadKey(); } @@ -500,16 +490,13 @@ public class Program(IServiceProvider serviceProvider) 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[/]"); } - //Check if auth is valid - var apiHelper = ActivatorUtilities.CreateInstance(serviceProvider, authService.CurrentAuth); - - Entities.User? validate = await apiHelper.GetUserInfo($"/users/me"); + Entities.User? validate = await apiService.GetUserInfo($"/users/me"); if (validate == null || (validate?.name == null && validate?.username == null)) { Log.Error("Auth failed"); authService.CurrentAuth = null; - if (!config!.DisableBrowserAuth) + if (!configService.CurrentConfig!.DisableBrowserAuth) { if (File.Exists("auth.json")) { @@ -517,7 +504,7 @@ public class Program(IServiceProvider serviceProvider) } } - if (!cliNonInteractive && !config!.DisableBrowserAuth) + if (!configService.CurrentConfig.NonInteractiveMode && !configService.CurrentConfig!.DisableBrowserAuth) { await LoadAuthFromBrowser(); } @@ -532,7 +519,7 @@ public class Program(IServiceProvider serviceProvider) } AnsiConsole.Markup($"[green]Logged In successfully as {validate.name} {validate.username}\n[/]"); - await DownloadAllData(apiHelper, authService.CurrentAuth, config); + await DownloadAllData(); } catch (Exception ex) { @@ -545,7 +532,7 @@ public class Program(IServiceProvider serviceProvider) Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); } Console.WriteLine("\nPress any key to exit."); - if (!cliNonInteractive) + if (!configService.CurrentConfig.NonInteractiveMode) { Console.ReadKey(); } @@ -553,10 +540,15 @@ public class Program(IServiceProvider serviceProvider) } } - private async Task DownloadAllData(IAPIHelper m_ApiHelper, Auth Auth, Entities.Config Config) + private async Task DownloadAllData() { - IDBHelper dBHelper = serviceProvider.GetRequiredService(); + IDBService dbService = serviceProvider.GetRequiredService(); IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + Entities.Config Config = configService.CurrentConfig!; Log.Debug("Calling DownloadAllData"); @@ -564,7 +556,7 @@ public class Program(IServiceProvider serviceProvider) { DateTime startTime = DateTime.Now; Dictionary users = new(); - Dictionary activeSubs = await m_ApiHelper.GetActiveSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions, Config); + Dictionary activeSubs = await apiService.GetActiveSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions); Log.Debug("Subscriptions: "); @@ -580,7 +572,7 @@ public class Program(IServiceProvider serviceProvider) { Log.Debug("Inactive Subscriptions: "); - Dictionary expiredSubs = await m_ApiHelper.GetExpiredSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions, Config); + Dictionary expiredSubs = await apiService.GetExpiredSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions); foreach (KeyValuePair expiredSub in expiredSubs) { if (!users.ContainsKey(expiredSub.Key)) @@ -591,7 +583,7 @@ public class Program(IServiceProvider serviceProvider) } } - Dictionary lists = await m_ApiHelper.GetLists("/lists", Config); + Dictionary lists = await apiService.GetLists("/lists"); // Remove users from the list if they are in the ignored list if (!string.IsNullOrEmpty(Config.IgnoredUsersListName)) @@ -603,12 +595,12 @@ public class Program(IServiceProvider serviceProvider) } else { - var ignoredUsernames = await m_ApiHelper.GetListUsers($"/lists/{ignoredUsersListId}/users", Config) ?? []; + var ignoredUsernames = await apiService.GetListUsers($"/lists/{ignoredUsersListId}/users") ?? []; users = users.Where(x => !ignoredUsernames.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value); } } - await dBHelper.CreateUsersDB(users); + await dbService.CreateUsersDB(users); KeyValuePair> hasSelectedUsersKVP; if(Config.NonInteractiveMode && Config.NonInteractiveModePurchasedTab) { @@ -621,16 +613,16 @@ public class Program(IServiceProvider serviceProvider) else if (Config.NonInteractiveMode && !string.IsNullOrEmpty(Config.NonInteractiveModeListName)) { var listId = lists[Config.NonInteractiveModeListName]; - var listUsernames = await m_ApiHelper.GetListUsers($"/lists/{listId}/users", Config) ?? []; + var listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; var selectedUsers = users.Where(x => listUsernames.Contains(x.Key)).Distinct().ToDictionary(x => x.Key, x => x.Value); hasSelectedUsersKVP = new KeyValuePair>(true, selectedUsers); } else { var loggingService = serviceProvider.GetRequiredService(); - var userSelectionResult = await HandleUserSelection(m_ApiHelper, Config, users, lists, configService, loggingService); + var userSelectionResult = await HandleUserSelection(users, lists); - Config = userSelectionResult.updatedConfig; + Config = configService.CurrentConfig!; hasSelectedUsersKVP = new KeyValuePair>(userSelectionResult.IsExit, userSelectionResult.selectedUsers); } @@ -687,22 +679,15 @@ public class Program(IServiceProvider serviceProvider) AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); } - await dBHelper.CreateDB(path); + await dbService.CreateDB(path); - var downloadContext = ActivatorUtilities.CreateInstance( - serviceProvider, - Config, - GetCreatorFileNameFormatConfig(Config, username), - m_ApiHelper, - dBHelper); - - await DownloadSinglePost(downloadContext, post_id, path, users); + await DownloadSinglePost(username, post_id, path, users); } } } else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && hasSelectedUsersKVP.Value.ContainsKey("PurchasedTab")) { - Dictionary purchasedTabUsers = await m_ApiHelper.GetPurchasedTabUsers("/posts/paid/all", Config, users); + Dictionary purchasedTabUsers = await apiService.GetPurchasedTabUsers("/posts/paid/all", users); AnsiConsole.Markup($"[red]Checking folders for Users in Purchased Tab\n[/]"); foreach (KeyValuePair user in purchasedTabUsers) { @@ -718,7 +703,7 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Download path: {path}"); - await dBHelper.CheckUsername(user, path); + await dbService.CheckUsername(user, path); if (!Directory.Exists(path)) { @@ -732,9 +717,9 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Folder for {user.Key} already created"); } - Entities.User user_info = await m_ApiHelper.GetUserInfo($"/users/{user.Key}"); + Entities.User user_info = await apiService.GetUserInfo($"/users/{user.Key}"); - await dBHelper.CreateDB(path); + await dbService.CreateDB(path); } string p = ""; @@ -749,7 +734,7 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Download path: {p}"); - List purchasedTabCollections = await m_ApiHelper.GetPurchasedTab("/posts/paid/all", p, Config, users); + List purchasedTabCollections = await apiService.GetPurchasedTab("/posts/paid/all", p, users); foreach(PurchasedTabCollection purchasedTabCollection in purchasedTabCollections) { AnsiConsole.Markup($"[red]\nScraping Data for {purchasedTabCollection.Username}\n[/]"); @@ -766,17 +751,10 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Download path: {path}"); - var downloadContext = ActivatorUtilities.CreateInstance( - serviceProvider, - Config, - GetCreatorFileNameFormatConfig(Config, purchasedTabCollection.Username), - m_ApiHelper, - dBHelper); - int paidPostCount = 0; int paidMessagesCount = 0; - paidPostCount = await DownloadPaidPostsPurchasedTab(downloadContext, purchasedTabCollection.PaidPosts, users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidPostCount, path, users); - paidMessagesCount = await DownloadPaidMessagesPurchasedTab(downloadContext, purchasedTabCollection.PaidMessages, users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidMessagesCount, path, users); + paidPostCount = await DownloadPaidPostsPurchasedTab(purchasedTabCollection.Username, purchasedTabCollection.PaidPosts, users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidPostCount, path, users); + paidMessagesCount = await DownloadPaidMessagesPurchasedTab(purchasedTabCollection.Username, purchasedTabCollection.PaidMessages, users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidMessagesCount, path, users); AnsiConsole.Markup("\n"); AnsiConsole.Write(new BreakdownChart() @@ -816,7 +794,7 @@ public class Program(IServiceProvider serviceProvider) { long message_id = Convert.ToInt64(messageUrl.Split("?firstId=")[1]); long user_id = Convert.ToInt64(messageUrl.Split("/")[6]); - JObject user = await m_ApiHelper.GetUserInfoById($"/users/list?x[]={user_id.ToString()}"); + JObject user = await apiService.GetUserInfoById($"/users/list?x[]={user_id.ToString()}"); string username = string.Empty; Log.Debug($"Message ID: {message_id}"); @@ -857,16 +835,9 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Folder for {username} already created"); } - await dBHelper.CreateDB(path); + await dbService.CreateDB(path); - var downloadContext = ActivatorUtilities.CreateInstance( - serviceProvider, - Config, - GetCreatorFileNameFormatConfig(Config, username), - m_ApiHelper, - dBHelper); - - await DownloadPaidMessage(downloadContext, hasSelectedUsersKVP, username, 1, path, message_id); + await DownloadPaidMessage(username, hasSelectedUsersKVP, 1, path, message_id); } } else if (hasSelectedUsersKVP.Key && !hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) @@ -898,7 +869,7 @@ public class Program(IServiceProvider serviceProvider) Log.Debug("Download path: ", path); - await dBHelper.CheckUsername(user, path); + await dbService.CheckUsername(user, path); if (!Directory.Exists(path)) { @@ -912,62 +883,55 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Folder for {user.Key} already created"); } - await dBHelper.CreateDB(path); - - var downloadContext = ActivatorUtilities.CreateInstance( - serviceProvider, - Config, - GetCreatorFileNameFormatConfig(Config, user.Key), - m_ApiHelper, - dBHelper); + await dbService.CreateDB(path); if (Config.DownloadAvatarHeaderPhoto) { - Entities.User? user_info = await m_ApiHelper.GetUserInfo($"/users/{user.Key}"); + Entities.User? user_info = await apiService.GetUserInfo($"/users/{user.Key}"); if (user_info != null) { - await downloadContext.DownloadHelper.DownloadAvatarHeader(user_info.avatar, user_info.header, path, user.Key); + await downloadService.DownloadAvatarHeader(user_info.avatar, user_info.header, path, user.Key); } } if (Config.DownloadPaidPosts) { - paidPostCount = await DownloadPaidPosts(downloadContext, hasSelectedUsersKVP, user, paidPostCount, path); + paidPostCount = await DownloadPaidPosts(user.Key, hasSelectedUsersKVP, user, paidPostCount, path); } if (Config.DownloadPosts) { - postCount = await DownloadFreePosts(downloadContext, hasSelectedUsersKVP, user, postCount, path); + postCount = await DownloadFreePosts(user.Key, hasSelectedUsersKVP, user, postCount, path); } if (Config.DownloadArchived) { - archivedCount = await DownloadArchived(downloadContext, hasSelectedUsersKVP, user, archivedCount, path); + archivedCount = await DownloadArchived(user.Key, hasSelectedUsersKVP, user, archivedCount, path); } if (Config.DownloadStreams) { - streamsCount = await DownloadStreams(downloadContext, hasSelectedUsersKVP, user, streamsCount, path); + streamsCount = await DownloadStreams(user.Key, hasSelectedUsersKVP, user, streamsCount, path); } if (Config.DownloadStories) { - storiesCount = await DownloadStories(downloadContext, user, storiesCount, path); + storiesCount = await DownloadStories(user.Key, user, storiesCount, path); } if (Config.DownloadHighlights) { - highlightsCount = await DownloadHighlights(downloadContext, user, highlightsCount, path); + highlightsCount = await DownloadHighlights(user.Key, user, highlightsCount, path); } if (Config.DownloadMessages) { - messagesCount = await DownloadMessages(downloadContext, hasSelectedUsersKVP, user, messagesCount, path); + messagesCount = await DownloadMessages(user.Key, hasSelectedUsersKVP, user, messagesCount, path); } if (Config.DownloadPaidMessages) { - paidMessagesCount = await DownloadPaidMessages(downloadContext, hasSelectedUsersKVP, user, paidMessagesCount, path); + paidMessagesCount = await DownloadPaidMessages(user.Key, hasSelectedUsersKVP, user, paidMessagesCount, path); } AnsiConsole.Markup("\n"); @@ -998,570 +962,276 @@ public class Program(IServiceProvider serviceProvider) } while (!Config.NonInteractiveMode); } - private static IFileNameFormatConfig GetCreatorFileNameFormatConfig(Entities.Config config, string userName) + private async Task DownloadPaidMessages(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int paidMessagesCount, string path) { - FileNameFormatConfig combinedConfig = new FileNameFormatConfig(); - - Func func = (val1, val2) => - { - if (string.IsNullOrEmpty(val1)) - return val2; - else - return val1; - }; - - if(config.CreatorConfigs.ContainsKey(userName)) - { - CreatorConfig creatorConfig = config.CreatorConfigs[userName]; - if(creatorConfig != null) - { - combinedConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat; - combinedConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat; - combinedConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat; - combinedConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat; - } - } - - combinedConfig.PaidMessageFileNameFormat = func(combinedConfig.PaidMessageFileNameFormat, config.PaidMessageFileNameFormat); - combinedConfig.PostFileNameFormat = func(combinedConfig.PostFileNameFormat, config.PostFileNameFormat); - combinedConfig.MessageFileNameFormat = func(combinedConfig.MessageFileNameFormat, config.MessageFileNameFormat); - combinedConfig.PaidPostFileNameFormat = func(combinedConfig.PaidPostFileNameFormat, config.PaidPostFileNameFormat); - - Log.Debug($"PaidMessageFilenameFormat: {combinedConfig.PaidMessageFileNameFormat}"); - Log.Debug($"PostFileNameFormat: {combinedConfig.PostFileNameFormat}"); - Log.Debug($"MessageFileNameFormat: {combinedConfig.MessageFileNameFormat}"); - Log.Debug($"PaidPostFileNameFormatt: {combinedConfig.PaidPostFileNameFormat}"); - - return combinedConfig; - } - - private static async Task DownloadPaidMessages(IDownloadContext downloadContext, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int paidMessagesCount, string path) - { - Log.Debug($"Calling DownloadPaidMessages - {user.Key}"); + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); PaidMessageCollection paidMessageCollection = new PaidMessageCollection(); await AnsiConsole.Status() .StartAsync("[red]Getting Paid Messages[/]", async ctx => { - paidMessageCollection = await downloadContext.ApiHelper.GetPaidMessages("/posts/paid/chat", path, user.Key, downloadContext.DownloadConfig!, ctx); + paidMessageCollection = await apiService.GetPaidMessages("/posts/paid/chat", path, user.Key, ctx); }); - int oldPaidMessagesCount = 0; - int newPaidMessagesCount = 0; + if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) { AnsiConsole.Markup($"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); - Log.Debug($"Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages"); - paidMessagesCount = paidMessageCollection.PaidMessages.Count; + long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) + if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values.ToList()); } else { - totalSize = paidMessagesCount; + totalSize = paidMessageCollection.PaidMessages.Count; } + + DownloadResult result = null; await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) .StartAsync(async ctx => { - // Define tasks var task = ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", autoStart: false); - Log.Debug($"Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages"); task.MaxValue = totalSize; task.StartTask(); - foreach (KeyValuePair paidMessageKVP in paidMessageCollection.PaidMessages) - { - bool isNew; - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = paidMessageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) - { - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - else - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } + var progressReporter = new SpectreProgressReporter(task); + result = await downloadService.DownloadPaidMessages(username, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, paidMessageCollection, progressReporter); - Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - - isNew = await downloadContext.DownloadHelper.DownloadPurchasedMessageDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: paidMessageKVP.Key, - api_type: "Messages", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } - else - { - Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - Purchased.List messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - - isNew = await downloadContext.DownloadHelper.DownloadPurchasedMedia( - url: paidMessageKVP.Value, - folder: path, - media_id: paidMessageKVP.Key, - api_type: "Messages", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } task.StopTask(); }); - AnsiConsole.Markup($"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); + + AnsiConsole.Markup($"[red]Paid Messages Already Downloaded: {result.ExistingDownloads} New Paid Messages Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; } else { AnsiConsole.Markup($"[red]Found 0 Paid Messages\n[/]"); + return 0; } - - return paidMessagesCount; } - - private static async Task DownloadMessages(IDownloadContext downloadContext, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int messagesCount, string path) + private async Task DownloadMessages(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int messagesCount, string path) { - Log.Debug($"Calling DownloadMessages - {user.Key}"); + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); MessageCollection messages = new MessageCollection(); await AnsiConsole.Status() .StartAsync("[red]Getting Messages[/]", async ctx => { - messages = await downloadContext.ApiHelper.GetMessages($"/chats/{user.Value}/messages", path, downloadContext.DownloadConfig!, ctx); + messages = await apiService.GetMessages($"/chats/{user.Value}/messages", path, ctx); }); - int oldMessagesCount = 0; - int newMessagesCount = 0; + if (messages != null && messages.Messages.Count > 0) { AnsiConsole.Markup($"[red]Found {messages.Messages.Count} Media from {messages.MessageObjects.Count} Messages\n[/]"); - Log.Debug($"[red]Found {messages.Messages.Count} Media from {messages.MessageObjects.Count} Messages"); - messagesCount = messages.Messages.Count; + long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) + if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(messages.Messages.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(messages.Messages.Values.ToList()); } else { - totalSize = messagesCount; + totalSize = messages.Messages.Count; } + + DownloadResult result = null; await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) .StartAsync(async ctx => { - // Define tasks var task = ctx.AddTask($"[red]Downloading {messages.Messages.Count} Messages[/]", autoStart: false); - Log.Debug($"Downloading {messages.Messages.Count} Messages"); task.MaxValue = totalSize; task.StartTask(); - foreach (KeyValuePair messageKVP in messages.Messages) - { - bool isNew; - if (messageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = messageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) - { - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - else - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - Messages.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); - Messages.List? messageInfo = messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - isNew = await downloadContext.DownloadHelper.DownloadMessageDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: messageKVP.Key, - api_type: "Messages", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.MessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); + var progressReporter = new SpectreProgressReporter(task); + result = await downloadService.DownloadMessages(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, messages, progressReporter); - - if (isNew) - { - newMessagesCount++; - } - else - { - oldMessagesCount++; - } - } - } - else - { - Messages.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); - Messages.List? messageInfo = messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - - isNew = await downloadContext.DownloadHelper.DownloadMessageMedia( - url: messageKVP.Value, - folder: path, - media_id: messageKVP.Key, - api_type: "Messages", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig!.MessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - - if (isNew) - { - newMessagesCount++; - } - else - { - oldMessagesCount++; - } - } - } task.StopTask(); }); - AnsiConsole.Markup($"[red]Messages Already Downloaded: {oldMessagesCount} New Messages Downloaded: {newMessagesCount}[/]\n"); + + AnsiConsole.Markup($"[red]Messages Already Downloaded: {result.ExistingDownloads} New Messages Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; } else { AnsiConsole.Markup($"[red]Found 0 Messages\n[/]"); + return 0; } - - return messagesCount; } - private static async Task DownloadHighlights(IDownloadContext downloadContext, KeyValuePair user, int highlightsCount, string path) + private async Task DownloadHighlights(string username, KeyValuePair user, int highlightsCount, string path) { - Log.Debug($"Calling DownloadHighlights - {user.Key}"); + IConfigService configService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); AnsiConsole.Markup($"[red]Getting Highlights\n[/]"); - Dictionary highlights = await downloadContext.ApiHelper.GetMedia(MediaType.Highlights, $"/users/{user.Value}/stories/highlights", null, path, downloadContext.DownloadConfig!, paid_post_ids); - int oldHighlightsCount = 0; - int newHighlightsCount = 0; - if (highlights != null && highlights.Count > 0) + + // Calculate total size for progress bar + long totalSize = 0; + var tempHighlights = await apiService.GetMedia(MediaType.Highlights, $"/users/{user.Value}/stories/highlights", null, path, paid_post_ids); + if (tempHighlights != null && tempHighlights.Count > 0) { - AnsiConsole.Markup($"[red]Found {highlights.Count} Highlights\n[/]"); - Log.Debug($"Found {highlights.Count} Highlights"); - highlightsCount = highlights.Count; - long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) + AnsiConsole.Markup($"[red]Found {tempHighlights.Count} Highlights\n[/]"); + if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(highlights.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(tempHighlights.Values.ToList()); } else { - totalSize = highlightsCount; + totalSize = tempHighlights.Count; } - await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - var task = ctx.AddTask($"[red]Downloading {highlights.Count} Highlights[/]", autoStart: false); - Log.Debug($"Downloading {highlights.Count} Highlights"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair highlightKVP in highlights) - { - bool isNew = await downloadContext.DownloadHelper.DownloadStoryMedia(highlightKVP.Value, path, highlightKVP.Key, "Stories", task); - if (isNew) - { - newHighlightsCount++; - } - else - { - oldHighlightsCount++; - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Highlights Already Downloaded: {oldHighlightsCount} New Highlights Downloaded: {newHighlightsCount}[/]\n"); - Log.Debug($"Highlights Already Downloaded: {oldHighlightsCount} New Highlights Downloaded: {newHighlightsCount}"); } else { AnsiConsole.Markup($"[red]Found 0 Highlights\n[/]"); - Log.Debug($"Found 0 Highlights"); + return 0; } - return highlightsCount; + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + var task = ctx.AddTask($"[red]Downloading {tempHighlights.Count} Highlights[/]", autoStart: false); + task.MaxValue = totalSize; + task.StartTask(); + + var progressReporter = new SpectreProgressReporter(task); + result = await downloadService.DownloadHighlights(username, user.Value, path, paid_post_ids.ToHashSet(), progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup($"[red]Highlights Already Downloaded: {result.ExistingDownloads} New Highlights Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; } - private static async Task DownloadStories(IDownloadContext downloadContext, KeyValuePair user, int storiesCount, string path) + private async Task DownloadStories(string username, KeyValuePair user, int storiesCount, string path) { - Log.Debug($"Calling DownloadStories - {user.Key}"); + IConfigService configService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); AnsiConsole.Markup($"[red]Getting Stories\n[/]"); - Dictionary stories = await downloadContext.ApiHelper.GetMedia(MediaType.Stories, $"/users/{user.Value}/stories", null, path, downloadContext.DownloadConfig!, paid_post_ids); - int oldStoriesCount = 0; - int newStoriesCount = 0; - if (stories != null && stories.Count > 0) + + // Calculate total size for progress bar + long totalSize = 0; + var tempStories = await serviceProvider.GetRequiredService().GetMedia(MediaType.Stories, $"/users/{user.Value}/stories", null, path, paid_post_ids); + if (tempStories != null && tempStories.Count > 0) { - AnsiConsole.Markup($"[red]Found {stories.Count} Stories\n[/]"); - Log.Debug($"Found {stories.Count} Stories"); - storiesCount = stories.Count; - long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) + AnsiConsole.Markup($"[red]Found {tempStories.Count} Stories\n[/]"); + if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(stories.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(tempStories.Values.ToList()); } else { - totalSize = storiesCount; + totalSize = tempStories.Count; } - await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - var task = ctx.AddTask($"[red]Downloading {stories.Count} Stories[/]", autoStart: false); - Log.Debug($"Downloading {stories.Count} Stories"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair storyKVP in stories) - { - bool isNew = await downloadContext.DownloadHelper.DownloadStoryMedia(storyKVP.Value, path, storyKVP.Key, "Stories", task); - if (isNew) - { - newStoriesCount++; - } - else - { - oldStoriesCount++; - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Stories Already Downloaded: {oldStoriesCount} New Stories Downloaded: {newStoriesCount}[/]\n"); - Log.Debug($"Stories Already Downloaded: {oldStoriesCount} New Stories Downloaded: {newStoriesCount}"); } else { AnsiConsole.Markup($"[red]Found 0 Stories\n[/]"); - Log.Debug($"Found 0 Stories"); + return 0; } - return storiesCount; + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + var task = ctx.AddTask($"[red]Downloading {tempStories.Count} Stories[/]", autoStart: false); + task.MaxValue = totalSize; + task.StartTask(); + + var progressReporter = new SpectreProgressReporter(task); + result = await downloadService.DownloadStories(username, user.Value, path, paid_post_ids.ToHashSet(), progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup($"[red]Stories Already Downloaded: {result.ExistingDownloads} New Stories Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; } - private static async Task DownloadArchived(IDownloadContext downloadContext, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int archivedCount, string path) + private async Task DownloadArchived(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int archivedCount, string path) { - Log.Debug($"Calling DownloadArchived - {user.Key}"); + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); ArchivedCollection archived = new ArchivedCollection(); await AnsiConsole.Status() .StartAsync("[red]Getting Archived Posts[/]", async ctx => { - archived = await downloadContext.ApiHelper.GetArchived($"/users/{user.Value}/posts", path, downloadContext.DownloadConfig!, ctx); + archived = await apiService.GetArchived($"/users/{user.Value}/posts", path, ctx); }); - int oldArchivedCount = 0; - int newArchivedCount = 0; if (archived != null && archived.ArchivedPosts.Count > 0) { AnsiConsole.Markup($"[red]Found {archived.ArchivedPosts.Count} Media from {archived.ArchivedPostObjects.Count} Archived Posts\n[/]"); - Log.Debug($"Found {archived.ArchivedPosts.Count} Media from {archived.ArchivedPostObjects.Count} Archived Posts"); - archivedCount = archived.ArchivedPosts.Count; + long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) + if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(archived.ArchivedPosts.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(archived.ArchivedPosts.Values.ToList()); } else { - totalSize = archivedCount; + totalSize = archived.ArchivedPosts.Count; } + + DownloadResult result = null; await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) .StartAsync(async ctx => { - // Define tasks var task = ctx.AddTask($"[red]Downloading {archived.ArchivedPosts.Count} Archived Posts[/]", autoStart: false); - Log.Debug($"Downloading {archived.ArchivedPosts.Count} Archived Posts"); task.MaxValue = totalSize; task.StartTask(); - foreach (KeyValuePair archivedKVP in archived.ArchivedPosts) - { - bool isNew; - if (archivedKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = archivedKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) - { - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - else - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - Archived.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.id == archivedKVP.Key); - Archived.List? postInfo = archived.ArchivedPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - isNew = await downloadContext.DownloadHelper.DownloadArchivedPostDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: archivedKVP.Key, - api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PostFileNameFormat ?? string.Empty, - postInfo: postInfo, - postMedia: mediaInfo, - author: postInfo?.author, - users: hasSelectedUsersKVP.Value); + var progressReporter = new SpectreProgressReporter(task); + result = await downloadService.DownloadArchived(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, archived, progressReporter); - if (isNew) - { - newArchivedCount++; - } - else - { - oldArchivedCount++; - } - } - } - else - { - Archived.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.id == archivedKVP.Key); - Archived.List? postInfo = archived.ArchivedPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - - isNew = await downloadContext.DownloadHelper.DownloadArchivedMedia( - url: archivedKVP.Value, - folder: path, - media_id: archivedKVP.Key, - api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PostFileNameFormat ?? string.Empty, - messageInfo: postInfo, - messageMedia: mediaInfo, - author: postInfo?.author, - users: hasSelectedUsersKVP.Value); - - if (isNew) - { - newArchivedCount++; - } - else - { - oldArchivedCount++; - } - } - } task.StopTask(); }); - AnsiConsole.Markup($"[red]Archived Posts Already Downloaded: {oldArchivedCount} New Archived Posts Downloaded: {newArchivedCount}[/]\n"); + + AnsiConsole.Markup($"[red]Archived Posts Already Downloaded: {result.ExistingDownloads} New Archived Posts Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; } else { AnsiConsole.Markup($"[red]Found 0 Archived Posts\n[/]"); + return 0; } - - return archivedCount; } - private static async Task DownloadFreePosts(IDownloadContext downloadContext, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int postCount, string path) + private async Task DownloadFreePosts(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int postCount, string path) { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + AnsiConsole.Markup($"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/]"); Log.Debug($"Calling DownloadFreePosts - {user.Key}"); PostCollection posts = new PostCollection(); await AnsiConsole.Status() - .StartAsync("[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)[/]", async ctx => + .StartAsync("[red]Getting Posts[/]", async ctx => { - posts = await downloadContext.ApiHelper.GetPosts($"/users/{user.Value}/posts", path, downloadContext.DownloadConfig!, paid_post_ids, ctx); + posts = await apiService.GetPosts($"/users/{user.Value}/posts", path, paid_post_ids, ctx); }); - int oldPostCount = 0; - int newPostCount = 0; if (posts == null || posts.Posts.Count <= 0) { AnsiConsole.Markup($"[red]Found 0 Posts\n[/]"); @@ -1571,125 +1241,39 @@ public class Program(IServiceProvider serviceProvider) AnsiConsole.Markup($"[red]Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts\n[/]"); Log.Debug($"Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts"); - postCount = posts.Posts.Count; - long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) - { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(posts.Posts.Values.ToList()); - } - else - { - totalSize = postCount; - } + + long totalSize = configService.CurrentConfig.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize(posts.Posts.Values.ToList()) + : posts.Posts.Count; + + DownloadResult result = null; await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) .StartAsync(async ctx => { var task = ctx.AddTask($"[red]Downloading {posts.Posts.Count} Posts[/]", autoStart: false); - Log.Debug($"Downloading {posts.Posts.Count} Posts"); task.MaxValue = totalSize; task.StartTask(); - foreach (KeyValuePair postKVP in posts.Posts) - { - bool isNew; - if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = postKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh == null) - { - continue; - } - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - else - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - Post.Medium mediaInfo = posts.PostMedia.FirstOrDefault(m => m.id == postKVP.Key); - Post.List postInfo = posts.PostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + var progressReporter = new SpectreProgressReporter(task); + result = await downloadService.DownloadFreePosts(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, posts, progressReporter); - isNew = await downloadContext.DownloadHelper.DownloadPostDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: postKVP.Key, - api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PostFileNameFormat ?? string.Empty, - postInfo: postInfo, - postMedia: mediaInfo, - author: postInfo?.author, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newPostCount++; - } - else - { - oldPostCount++; - } - } - else - { - try - { - Post.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => (m?.id == postKVP.Key) == true); - Post.List? postInfo = posts.PostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - - isNew = await downloadContext.DownloadHelper.DownloadPostMedia( - url: postKVP.Value, - folder: path, - media_id: postKVP.Key, - api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PostFileNameFormat ?? string.Empty, - postInfo: postInfo, - postMedia: mediaInfo, - author: postInfo?.author, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newPostCount++; - } - else - { - oldPostCount++; - } - } - catch - { - Console.WriteLine("Media was null"); - } - } - } task.StopTask(); }); - AnsiConsole.Markup($"[red]Posts Already Downloaded: {oldPostCount} New Posts Downloaded: {newPostCount}[/]\n"); - Log.Debug("Posts Already Downloaded: {oldPostCount} New Posts Downloaded: {newPostCount}"); - return postCount; + AnsiConsole.Markup($"[red]Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}[/]\n"); + Log.Debug($"Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}"); + + return result.TotalCount; } - private static async Task DownloadPaidPosts(IDownloadContext downloadContext, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int paidPostCount, string path) + private async Task DownloadPaidPosts(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int paidPostCount, string path) { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + AnsiConsole.Markup($"[red]Getting Paid Posts\n[/]"); Log.Debug($"Calling DownloadPaidPosts - {user.Key}"); PaidPostCollection purchasedPosts = new PaidPostCollection(); @@ -1697,11 +1281,9 @@ public class Program(IServiceProvider serviceProvider) await AnsiConsole.Status() .StartAsync("[red]Getting Paid Posts[/]", async ctx => { - purchasedPosts = await downloadContext.ApiHelper.GetPaidPosts("/posts/paid/post", path, user.Key, downloadContext.DownloadConfig!, paid_post_ids, ctx); + purchasedPosts = await apiService.GetPaidPosts("/posts/paid/post", path, user.Key, paid_post_ids, ctx); }); - int oldPaidPostCount = 0; - int newPaidPostCount = 0; if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) { AnsiConsole.Markup($"[red]Found 0 Paid Posts\n[/]"); @@ -1711,118 +1293,40 @@ public class Program(IServiceProvider serviceProvider) AnsiConsole.Markup($"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); Log.Debug($"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); - paidPostCount = purchasedPosts.PaidPosts.Count; - long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) - { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()); - } - else - { - totalSize = paidPostCount; - } + + long totalSize = configService.CurrentConfig.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()) + : purchasedPosts.PaidPosts.Count; + + DownloadResult result = null; await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) .StartAsync(async ctx => { - // Define tasks var task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", autoStart: false); - Log.Debug($"Downloading {purchasedPosts.PaidPosts.Count} Paid Posts"); task.MaxValue = totalSize; task.StartTask(); - foreach (KeyValuePair purchasedPostKVP in purchasedPosts.PaidPosts) - { - bool isNew; - if (purchasedPostKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = purchasedPostKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh == null) - { - continue; - } - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - else - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == purchasedPostKVP.Key); - Purchased.List? postInfo = purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + var progressReporter = new SpectreProgressReporter(task); + result = await downloadService.DownloadPaidPosts(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, purchasedPosts, progressReporter); - isNew = await downloadContext.DownloadHelper.DownloadPurchasedPostDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: purchasedPostKVP.Key, - api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidPostFileNameFormat ?? string.Empty, - postInfo: postInfo, - postMedia: mediaInfo, - fromUser: postInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newPaidPostCount++; - } - else - { - oldPaidPostCount++; - } - } - else - { - Medium mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == purchasedPostKVP.Key); - Purchased.List postInfo = purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - - isNew = await downloadContext.DownloadHelper.DownloadPurchasedPostMedia( - url: purchasedPostKVP.Value, - folder: path, - media_id: purchasedPostKVP.Key, - api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidPostFileNameFormat ?? string.Empty, - messageInfo: postInfo, - messageMedia: mediaInfo, - fromUser: postInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newPaidPostCount++; - } - else - { - oldPaidPostCount++; - } - } - } task.StopTask(); }); - AnsiConsole.Markup($"[red]Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}[/]\n"); - Log.Debug($"Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}"); - return paidPostCount; + + AnsiConsole.Markup($"[red]Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}[/]\n"); + Log.Debug($"Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}"); + + return result.TotalCount; } - private static async Task DownloadPaidPostsPurchasedTab(IDownloadContext downloadContext, PaidPostCollection purchasedPosts, KeyValuePair user, int paidPostCount, string path, Dictionary users) + private async Task DownloadPaidPostsPurchasedTab(string username, PaidPostCollection purchasedPosts, KeyValuePair user, int paidPostCount, string path, Dictionary users) { + IDBService dbService = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + int oldPaidPostCount = 0; int newPaidPostCount = 0; if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) @@ -1837,16 +1341,16 @@ public class Program(IServiceProvider serviceProvider) paidPostCount = purchasedPosts.PaidPosts.Count; long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) + if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()); } else { totalSize = paidPostCount; } await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) .StartAsync(async ctx => { // Define tasks @@ -1867,27 +1371,27 @@ public class Program(IServiceProvider serviceProvider) string mediaId = messageUrlParsed[4]; string postId = messageUrlParsed[5]; string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); if (pssh == null) { continue; } - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); string decryptionKey; if (clientIdBlobMissing || devicePrivateKeyMissing) { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); } else { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); } Medium? mediaInfo = purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); Purchased.List? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true) : null; - isNew = await downloadContext.DownloadHelper.DownloadPurchasedPostDRMVideo( + isNew = await downloadService.DownloadPurchasedPostDRMVideo( policy: policy, signature: signature, kvp: kvp, @@ -1897,8 +1401,8 @@ public class Program(IServiceProvider serviceProvider) lastModified: lastModified, media_id: purchasedPostKVP.Key, api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidPostFileNameFormat ?? string.Empty, + progressReporter: new SpectreProgressReporter(task), + filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidPostFileNameFormat ?? string.Empty, postInfo: postInfo, postMedia: mediaInfo, fromUser: postInfo?.fromUser, @@ -1917,13 +1421,13 @@ public class Program(IServiceProvider serviceProvider) Medium? mediaInfo = purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); Purchased.List? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true) : null; - isNew = await downloadContext.DownloadHelper.DownloadPurchasedPostMedia( + isNew = await downloadService.DownloadPurchasedPostMedia( url: purchasedPostKVP.Value, folder: path, media_id: purchasedPostKVP.Key, api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidPostFileNameFormat ?? string.Empty, + progressReporter: new SpectreProgressReporter(task), + filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidPostFileNameFormat ?? string.Empty, messageInfo: postInfo, messageMedia: mediaInfo, fromUser: postInfo?.fromUser, @@ -1945,8 +1449,14 @@ public class Program(IServiceProvider serviceProvider) return paidPostCount; } - private static async Task DownloadPaidMessagesPurchasedTab(IDownloadContext downloadContext, PaidMessageCollection paidMessageCollection, KeyValuePair user, int paidMessagesCount, string path, Dictionary users) + private async Task DownloadPaidMessagesPurchasedTab(string username, PaidMessageCollection paidMessageCollection, KeyValuePair user, int paidMessagesCount, string path, Dictionary users) { + IDBService dbService = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + int oldPaidMessagesCount = 0; int newPaidMessagesCount = 0; if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) @@ -1955,16 +1465,16 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages"); paidMessagesCount = paidMessageCollection.PaidMessages.Count; long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) + if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values.ToList()); } else { totalSize = paidMessagesCount; } await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) .StartAsync(async ctx => { // Define tasks @@ -1985,25 +1495,25 @@ public class Program(IServiceProvider serviceProvider) string mediaId = messageUrlParsed[4]; string messageId = messageUrlParsed[5]; string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); if (pssh != null) { - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); string decryptionKey; if (clientIdBlobMissing || devicePrivateKeyMissing) { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); } else { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); } Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - isNew = await downloadContext.DownloadHelper.DownloadPurchasedMessageDRMVideo( + isNew = await downloadService.DownloadPurchasedMessageDRMVideo( policy: policy, signature: signature, kvp: kvp, @@ -2013,8 +1523,8 @@ public class Program(IServiceProvider serviceProvider) lastModified: lastModified, media_id: paidMessageKVP.Key, api_type: "Messages", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidMessageFileNameFormat ?? string.Empty, + progressReporter: new SpectreProgressReporter(task), + filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, messageInfo: messageInfo, messageMedia: mediaInfo, fromUser: messageInfo?.fromUser, @@ -2035,13 +1545,13 @@ public class Program(IServiceProvider serviceProvider) Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); Purchased.List messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - isNew = await downloadContext.DownloadHelper.DownloadPurchasedMedia( + isNew = await downloadService.DownloadPurchasedMedia( url: paidMessageKVP.Value, folder: path, media_id: paidMessageKVP.Key, api_type: "Messages", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidMessageFileNameFormat ?? string.Empty, + progressReporter: new SpectreProgressReporter(task), + filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, messageInfo: messageInfo, messageMedia: mediaInfo, fromUser: messageInfo?.fromUser, @@ -2070,152 +1580,71 @@ public class Program(IServiceProvider serviceProvider) return paidMessagesCount; } - private static async Task DownloadStreams(IDownloadContext downloadContext, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int streamsCount, string path) + private async Task DownloadStreams(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int streamsCount, string path) { - Log.Debug($"Calling DownloadStreams - {user.Key}"); + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); StreamsCollection streams = new StreamsCollection(); await AnsiConsole.Status() .StartAsync("[red]Getting Streams[/]", async ctx => { - streams = await downloadContext.ApiHelper.GetStreams($"/users/{user.Value}/posts/streams", path, downloadContext.DownloadConfig!, paid_post_ids, ctx); + streams = await apiService.GetStreams($"/users/{user.Value}/posts/streams", path, paid_post_ids, ctx); }); - int oldStreamsCount = 0; - int newStreamsCount = 0; - if (streams == null || streams.Streams.Count <= 0) + if (streams != null && streams.Streams.Count > 0) { - AnsiConsole.Markup($"[red]Found 0 Streams\n[/]"); - Log.Debug($"Found 0 Streams"); - return 0; - } + AnsiConsole.Markup($"[red]Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams\n[/]"); - AnsiConsole.Markup($"[red]Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams\n[/]"); - Log.Debug($"Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams"); - streamsCount = streams.Streams.Count; - long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) - { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(streams.Streams.Values.ToList()); + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(streams.Streams.Values.ToList()); + } + else + { + totalSize = streams.Streams.Count; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + var task = ctx.AddTask($"[red]Downloading {streams.Streams.Count} Streams[/]", autoStart: false); + task.MaxValue = totalSize; + task.StartTask(); + + var progressReporter = new SpectreProgressReporter(task); + result = await downloadService.DownloadStreams(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, streams, progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup($"[red]Streams Already Downloaded: {result.ExistingDownloads} New Streams Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; } else { - totalSize = streamsCount; + AnsiConsole.Markup($"[red]Found 0 Streams\n[/]"); + return 0; } - await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {streams.Streams.Count} Streams[/]", autoStart: false); - Log.Debug($"Downloading {streams.Streams.Count} Streams"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair streamKVP in streams.Streams) - { - bool isNew; - if (streamKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = streamKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh == null) - { - continue; - } - - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - else - { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - Streams.Medium mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.id == streamKVP.Key); - Streams.List streamInfo = streams.StreamObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - - isNew = await downloadContext.DownloadHelper.DownloadStreamsDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: streamKVP.Key, - api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PostFileNameFormat ?? string.Empty, - streamInfo: streamInfo, - streamMedia: mediaInfo, - author: streamInfo?.author, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newStreamsCount++; - } - else - { - oldStreamsCount++; - } - } - else - { - try - { - Streams.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => (m?.id == streamKVP.Key) == true); - Streams.List? streamInfo = streams.StreamObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - - isNew = await downloadContext.DownloadHelper.DownloadStreamMedia( - url: streamKVP.Value, - folder: path, - media_id: streamKVP.Key, - api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PostFileNameFormat ?? string.Empty, - streamInfo: streamInfo, - streamMedia: mediaInfo, - author: streamInfo?.author, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newStreamsCount++; - } - else - { - oldStreamsCount++; - } - } - catch - { - Console.WriteLine("Media was null"); - } - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Streams Already Downloaded: {oldStreamsCount} New Streams Downloaded: {newStreamsCount}[/]\n"); - Log.Debug($"Streams Already Downloaded: {oldStreamsCount} New Streams Downloaded: {newStreamsCount}"); - return streamsCount; } - - private static async Task DownloadPaidMessage(IDownloadContext downloadContext, KeyValuePair> hasSelectedUsersKVP, string username, int paidMessagesCount, string path, long message_id) + private async Task DownloadPaidMessage(string username, KeyValuePair> hasSelectedUsersKVP, int paidMessagesCount, string path, long message_id) { + IDBService dbService = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + Log.Debug($"Calling DownloadPaidMessage - {username}"); AnsiConsole.Markup($"[red]Getting Paid Message\n[/]"); - SinglePaidMessageCollection singlePaidMessageCollection = await downloadContext.ApiHelper.GetPaidMessage($"/messages/{message_id.ToString()}", path, downloadContext.DownloadConfig!); + SinglePaidMessageCollection singlePaidMessageCollection = await apiService.GetPaidMessage($"/messages/{message_id.ToString()}", path); int oldPreviewPaidMessagesCount = 0; int newPreviewPaidMessagesCount = 0; int oldPaidMessagesCount = 0; @@ -2226,16 +1655,16 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) + if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(singlePaidMessageCollection.PreviewSingleMessages.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection.PreviewSingleMessages.Values.ToList()); } else { totalSize = paidMessagesCount; } await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) .StartAsync(async ctx => { // Define tasks @@ -2256,25 +1685,25 @@ public class Program(IServiceProvider serviceProvider) string mediaId = messageUrlParsed[4]; string messageId = messageUrlParsed[5]; string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); if (pssh != null) { - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); string decryptionKey; if (clientIdBlobMissing || devicePrivateKeyMissing) { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); } else { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); } Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - isNew = await downloadContext.DownloadHelper.DownloadSingleMessagePreviewDRMVideo( + isNew = await downloadService.DownloadSingleMessagePreviewDRMVideo( policy: policy, signature: signature, kvp: kvp, @@ -2284,8 +1713,8 @@ public class Program(IServiceProvider serviceProvider) lastModified: lastModified, media_id: paidMessageKVP.Key, api_type: "Messages", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidMessageFileNameFormat ?? string.Empty, + progressReporter: new SpectreProgressReporter(task), + filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, messageInfo: messageInfo, messageMedia: mediaInfo, fromUser: messageInfo?.fromUser, @@ -2306,13 +1735,13 @@ public class Program(IServiceProvider serviceProvider) Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - isNew = await downloadContext.DownloadHelper.DownloadMessagePreviewMedia( + isNew = await downloadService.DownloadMessagePreviewMedia( url: paidMessageKVP.Value, folder: path, media_id: paidMessageKVP.Key, api_type: "Messages", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidMessageFileNameFormat ?? string.Empty, + progressReporter: new SpectreProgressReporter(task), + filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, messageInfo: messageInfo, messageMedia: mediaInfo, fromUser: messageInfo?.fromUser, @@ -2338,16 +1767,16 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) + if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(singlePaidMessageCollection.SingleMessages.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection.SingleMessages.Values.ToList()); } else { totalSize = paidMessagesCount; } await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) .StartAsync(async ctx => { // Define tasks @@ -2368,25 +1797,25 @@ public class Program(IServiceProvider serviceProvider) string mediaId = messageUrlParsed[4]; string messageId = messageUrlParsed[5]; string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); if (pssh != null) { - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); string decryptionKey; if (clientIdBlobMissing || devicePrivateKeyMissing) { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); } else { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); } Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - isNew = await downloadContext.DownloadHelper.DownloadSinglePurchasedMessageDRMVideo( + isNew = await downloadService.DownloadSinglePurchasedMessageDRMVideo( policy: policy, signature: signature, kvp: kvp, @@ -2396,8 +1825,8 @@ public class Program(IServiceProvider serviceProvider) lastModified: lastModified, media_id: paidMessageKVP.Key, api_type: "Messages", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidMessageFileNameFormat ?? string.Empty, + progressReporter: new SpectreProgressReporter(task), + filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, messageInfo: messageInfo, messageMedia: mediaInfo, fromUser: messageInfo?.fromUser, @@ -2418,13 +1847,13 @@ public class Program(IServiceProvider serviceProvider) Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - isNew = await downloadContext.DownloadHelper.DownloadSinglePurchasedMedia( + isNew = await downloadService.DownloadSinglePurchasedMedia( url: paidMessageKVP.Value, folder: path, media_id: paidMessageKVP.Key, api_type: "Messages", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PaidMessageFileNameFormat ?? string.Empty, + progressReporter: new SpectreProgressReporter(task), + filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, messageInfo: messageInfo, messageMedia: mediaInfo, fromUser: messageInfo?.fromUser, @@ -2453,12 +1882,16 @@ public class Program(IServiceProvider serviceProvider) return paidMessagesCount; } - private static async Task DownloadSinglePost(IDownloadContext downloadContext, long post_id, string path, Dictionary users) + private async Task DownloadSinglePost(string username, long post_id, string path, Dictionary users) { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + Log.Debug($"Calling DownloadSinglePost - {post_id.ToString()}"); AnsiConsole.Markup($"[red]Getting Post\n[/]"); - SinglePostCollection post = await downloadContext.ApiHelper.GetPost($"/posts/{post_id.ToString()}", path, downloadContext.DownloadConfig!); + SinglePostCollection post = await apiService.GetPost($"/posts/{post_id.ToString()}", path); if (post == null) { AnsiConsole.Markup($"[red]Couldn't find post\n[/]"); @@ -2467,9 +1900,9 @@ public class Program(IServiceProvider serviceProvider) } long totalSize = 0; - if (downloadContext.DownloadConfig.ShowScrapeSize) + if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadContext.DownloadHelper.CalculateTotalFileSize(post.SinglePosts.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(post.SinglePosts.Values.ToList()); } else { @@ -2477,7 +1910,7 @@ public class Program(IServiceProvider serviceProvider) } bool isNew = false; await AnsiConsole.Progress() - .Columns(GetProgressColumns(downloadContext.DownloadConfig.ShowScrapeSize)) + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) .StartAsync(async ctx => { var task = ctx.AddTask($"[red]Downloading Post[/]", autoStart: false); @@ -2495,27 +1928,27 @@ public class Program(IServiceProvider serviceProvider) string mediaId = messageUrlParsed[4]; string postId = messageUrlParsed[5]; string? licenseURL = null; - string? pssh = await downloadContext.ApiHelper.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); if (pssh == null) { continue; } - DateTime lastModified = await downloadContext.ApiHelper.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = downloadContext.ApiHelper.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); string decryptionKey; if (clientIdBlobMissing || devicePrivateKeyMissing) { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); } else { - decryptionKey = await downloadContext.ApiHelper.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); } SinglePost.Medium mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.id == postKVP.Key); SinglePost postInfo = post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - isNew = await downloadContext.DownloadHelper.DownloadPostDRMVideo( + isNew = await downloadService.DownloadPostDRMVideo( policy: policy, signature: signature, kvp: kvp, @@ -2525,8 +1958,8 @@ public class Program(IServiceProvider serviceProvider) lastModified: lastModified, media_id: postKVP.Key, api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PostFileNameFormat ?? string.Empty, + progressReporter: new SpectreProgressReporter(task), + filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? string.Empty, postInfo: postInfo, postMedia: mediaInfo, author: postInfo?.author, @@ -2539,13 +1972,13 @@ public class Program(IServiceProvider serviceProvider) SinglePost.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => (m?.id == postKVP.Key) == true); SinglePost? postInfo = post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - isNew = await downloadContext.DownloadHelper.DownloadPostMedia( + isNew = await downloadService.DownloadPostMedia( url: postKVP.Value, folder: path, media_id: postKVP.Key, api_type: "Posts", - task: task, - filenameFormat: downloadContext.FileNameFormatConfig.PostFileNameFormat ?? string.Empty, + progressReporter: new SpectreProgressReporter(task), + filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? string.Empty, postInfo: postInfo, postMedia: mediaInfo, author: postInfo?.author, @@ -2571,10 +2004,16 @@ public class Program(IServiceProvider serviceProvider) } } - public static async Task<(bool IsExit, Dictionary? selectedUsers, Entities.Config? updatedConfig)> HandleUserSelection(IAPIHelper apiHelper, Entities.Config currentConfig, Dictionary users, Dictionary lists, IConfigService configService, ILoggingService loggingService) + 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 Dictionary(); + Entities.Config currentConfig = configService.CurrentConfig!; while (!hasSelectedUsers) { @@ -2616,7 +2055,7 @@ public class Program(IServiceProvider serviceProvider) foreach (var item in listSelection) { long listId = lists[item.Replace("[red]", "").Replace("[/]", "")]; - List usernames = await apiHelper.GetListUsers($"/lists/{listId}/users", currentConfig); + List usernames = await apiService.GetListUsers($"/lists/{listId}/users"); foreach (string user in usernames) { listUsernames.Add(user); @@ -2655,11 +2094,11 @@ public class Program(IServiceProvider serviceProvider) } break; case "[red]Download Single Post[/]": - return (true, new Dictionary { { "SinglePost", 0 } }, currentConfig); + return (true, new Dictionary { { "SinglePost", 0 } }); case "[red]Download Single Paid Message[/]": - return (true, new Dictionary { { "SingleMessage", 0 } }, currentConfig); + return (true, new Dictionary { { "SingleMessage", 0 } }); case "[red]Download Purchased Tab[/]": - return (true, new Dictionary { { "PurchasedTab", 0 } }, currentConfig); + return (true, new Dictionary { { "PurchasedTab", 0 } }); case "[red]Edit config.conf[/]": while (true) { @@ -2731,7 +2170,7 @@ public class Program(IServiceProvider serviceProvider) currentConfig = newConfig; if (configChanged) { - return (true, new Dictionary { { "ConfigChanged", 0 } }, currentConfig); + return (true, new Dictionary { { "ConfigChanged", 0 } }); } break; } @@ -2786,7 +2225,7 @@ public class Program(IServiceProvider serviceProvider) if (configChanged) { - return (true, new Dictionary { { "ConfigChanged", 0 } }, currentConfig); + return (true, new Dictionary { { "ConfigChanged", 0 } }); } break; @@ -2803,13 +2242,13 @@ public class Program(IServiceProvider serviceProvider) Log.Information("Deleting auth.json"); File.Delete("auth.json"); } - return (false, null, currentConfig); // Return false to indicate exit + return (false, null); // Return false to indicate exit case "[red]Exit[/]": - return (false, null, currentConfig); // Return false to indicate exit + return (false, null); // Return false to indicate exit } } - return (true, selectedUsers, currentConfig); // Return true to indicate selected users + return (true, selectedUsers); // Return true to indicate selected users } public static List GetMainMenuOptions(Dictionary users, Dictionary lists) @@ -2873,7 +2312,8 @@ public class Program(IServiceProvider serviceProvider) return true; } - static ProgressColumn[] GetProgressColumns(bool showScrapeSize) + + static ProgressColumn[] GetProgressColumns(bool showScrapeSize) { List progressColumns; if (showScrapeSize) @@ -2931,5 +2371,4 @@ public class Program(IServiceProvider serviceProvider) File.WriteAllText("auth.json", newAuthString); } } - } diff --git a/OF DL/Helpers/APIHelper.cs b/OF DL/Services/APIService.cs similarity index 84% rename from OF DL/Helpers/APIHelper.cs rename to OF DL/Services/APIService.cs index ae9c645..3d9517e 100644 --- a/OF DL/Helpers/APIHelper.cs +++ b/OF DL/Services/APIService.cs @@ -18,23 +18,23 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Xml.Linq; +using OF_DL.Widevine; using WidevineClient.Widevine; using static WidevineClient.HttpUtil; +using Constants = OF_DL.Helpers.Constants; -namespace OF_DL.Helpers; +namespace OF_DL.Services; -public class APIHelper : IAPIHelper +public class APIService(IAuthService authService, IConfigService configService, IDBService dbService) + : IAPIService { private static readonly JsonSerializerSettings m_JsonSerializerSettings; - private readonly IDBHelper dbHelper; - private readonly IDownloadConfig downloadConfig; - private readonly Auth auth; private static DateTime? cachedDynamicRulesExpiration; private static DynamicRules? cachedDynamicRules; private const int MaxAttempts = 30; private const int DelayBetweenAttempts = 3000; - static APIHelper() + static APIService() { m_JsonSerializerSettings = new() { @@ -42,13 +42,6 @@ public class APIHelper : IAPIHelper }; } - public APIHelper(Auth auth, IDownloadConfig downloadConfig, IDBHelper dbHelper) - { - this.auth = auth; - this.downloadConfig = downloadConfig; - this.dbHelper = dbHelper; - } - public Dictionary GetDynamicHeaders(string path, string queryParams) { @@ -95,7 +88,7 @@ public class APIHelper : IAPIHelper DateTimeOffset dto = (DateTimeOffset)DateTime.UtcNow; long timestamp = dto.ToUnixTimeMilliseconds(); - string input = $"{root!.StaticParam}\n{timestamp}\n{path + queryParams}\n{auth.USER_ID}"; + string input = $"{root!.StaticParam}\n{timestamp}\n{path + queryParams}\n{authService.CurrentAuth.USER_ID}"; byte[] inputBytes = Encoding.UTF8.GetBytes(input); byte[] hashBytes = SHA1.HashData(inputBytes); string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); @@ -107,12 +100,12 @@ public class APIHelper : IAPIHelper { { "accept", "application/json, text/plain" }, { "app-token", root.AppToken! }, - { "cookie", auth!.COOKIE! }, + { "cookie", authService.CurrentAuth!.COOKIE! }, { "sign", sign }, { "time", timestamp.ToString() }, - { "user-id", auth!.USER_ID! }, - { "user-agent", auth!.USER_AGENT! }, - { "x-bc", auth!.X_BC! } + { "user-id", authService.CurrentAuth!.USER_ID! }, + { "user-agent", authService.CurrentAuth!.USER_AGENT! }, + { "x-bc", authService.CurrentAuth!.X_BC! } }; return headers; } @@ -167,12 +160,12 @@ public class APIHelper : IAPIHelper } - private static HttpClient GetHttpClient(IDownloadConfig? config = null) + private HttpClient GetHttpClient() { var client = new HttpClient(); - if (config?.Timeout != null && config.Timeout > 0) + if (configService.CurrentConfig?.Timeout != null && configService.CurrentConfig.Timeout > 0) { - client.Timeout = TimeSpan.FromSeconds(config.Timeout.Value); + client.Timeout = TimeSpan.FromSeconds(configService.CurrentConfig.Timeout.Value); } return client; } @@ -295,7 +288,7 @@ public class APIHelper : IAPIHelper } - public async Task?> GetAllSubscriptions(Dictionary getParams, string endpoint, bool includeRestricted, IDownloadConfig config) + public async Task?> GetAllSubscriptions(Dictionary getParams, string endpoint, bool includeRestricted) { try { @@ -359,7 +352,7 @@ public class APIHelper : IAPIHelper return null; } - public async Task?> GetActiveSubscriptions(string endpoint, bool includeRestricted, IDownloadConfig config) + public async Task?> GetActiveSubscriptions(string endpoint, bool includeRestricted) { Dictionary getParams = new() { @@ -369,11 +362,11 @@ public class APIHelper : IAPIHelper { "format", "infinite"} }; - return await GetAllSubscriptions(getParams, endpoint, includeRestricted, config); + return await GetAllSubscriptions(getParams, endpoint, includeRestricted); } - public async Task?> GetExpiredSubscriptions(string endpoint, bool includeRestricted, IDownloadConfig config) + public async Task?> GetExpiredSubscriptions(string endpoint, bool includeRestricted) { Dictionary getParams = new() @@ -386,11 +379,11 @@ public class APIHelper : IAPIHelper Log.Debug("Calling GetExpiredSubscriptions"); - return await GetAllSubscriptions(getParams, endpoint, includeRestricted, config); + return await GetAllSubscriptions(getParams, endpoint, includeRestricted); } - public async Task> GetLists(string endpoint, IDownloadConfig config) + public async Task> GetLists(string endpoint) { Log.Debug("Calling GetLists"); @@ -456,7 +449,7 @@ public class APIHelper : IAPIHelper } - public async Task?> GetListUsers(string endpoint, IDownloadConfig config) + public async Task?> GetListUsers(string endpoint) { Log.Debug($"Calling GetListUsers - {endpoint}"); @@ -520,7 +513,6 @@ public class APIHelper : IAPIHelper string endpoint, string? username, string folder, - IDownloadConfig config, List paid_post_ids) { @@ -570,34 +562,34 @@ public class APIHelper : IAPIHelper { if (story.media[0].createdAt.HasValue) { - await dbHelper.AddStory(folder, story.id, string.Empty, "0", false, false, story.media[0].createdAt.Value); + await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, story.media[0].createdAt.Value); } else if (story.createdAt.HasValue) { - await dbHelper.AddStory(folder, story.id, string.Empty, "0", false, false, story.createdAt.Value); + await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, story.createdAt.Value); } else { - await dbHelper.AddStory(folder, story.id, string.Empty, "0", false, false, DateTime.Now); + await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, DateTime.Now); } if (story.media != null && story.media.Count > 0) { foreach (Stories.Medium medium in story.media) { - await dbHelper.AddMedia(folder, medium.id, story.id, medium.files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); - if (medium.type == "photo" && !config.DownloadImages) + await dbService.AddMedia(folder, medium.id, story.id, medium.files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } @@ -627,7 +619,7 @@ public class APIHelper : IAPIHelper Log.Debug("Media Highlights - " + endpoint); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newhighlights = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); highlights.list.AddRange(newhighlights.list); @@ -652,7 +644,7 @@ public class APIHelper : IAPIHelper HighlightMedia highlightMedia = new(); Dictionary highlight_headers = GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, string.Empty); - HttpClient highlight_client = GetHttpClient(config); + HttpClient highlight_client = GetHttpClient(); HttpRequestMessage highlight_request = new(HttpMethod.Get, $"https://onlyfans.com/api2/v2/stories/highlights/{highlight_id}"); @@ -671,34 +663,34 @@ public class APIHelper : IAPIHelper { if (item.media[0].createdAt.HasValue) { - await dbHelper.AddStory(folder, item.id, string.Empty, "0", false, false, item.media[0].createdAt.Value); + await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, item.media[0].createdAt.Value); } else if (item.createdAt.HasValue) { - await dbHelper.AddStory(folder, item.id, string.Empty, "0", false, false, item.createdAt.Value); + await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, item.createdAt.Value); } else { - await dbHelper.AddStory(folder, item.id, string.Empty, "0", false, false, DateTime.Now); + await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, DateTime.Now); } if (item.media.Count > 0 && item.media[0].canView) { foreach (HighlightMedia.Medium medium in item.media) { - await dbHelper.AddMedia(folder, medium.id, item.id, item.media[0].files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); - if (medium.type == "photo" && !config.DownloadImages) + await dbService.AddMedia(folder, medium.id, item.id, item.media[0].files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } @@ -730,7 +722,7 @@ public class APIHelper : IAPIHelper } - public async Task GetPaidPosts(string endpoint, string folder, string username, IDownloadConfig config, List paid_post_ids, StatusContext ctx) + public async Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, StatusContext ctx) { Log.Debug($"Calling GetPaidPosts - {username}"); @@ -748,7 +740,7 @@ public class APIHelper : IAPIHelper { "author", username } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); paidPosts = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -761,7 +753,7 @@ public class APIHelper : IAPIHelper Purchased newPaidPosts = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newPaidPosts = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); paidPosts.list.AddRange(newPaidPosts.list); @@ -808,7 +800,7 @@ public class APIHelper : IAPIHelper } } } - await dbHelper.AddPost(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); + await dbService.AddPost(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); paidPostCollection.PaidPostObjects.Add(purchase); foreach (Messages.Medium medium in purchase.media) { @@ -817,19 +809,19 @@ public class APIHelper : IAPIHelper paid_post_ids.Add(medium.id); } - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } @@ -840,7 +832,7 @@ public class APIHelper : IAPIHelper { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, medium.files.full.url); paidPostCollection.PaidPostMedia.Add(medium); } @@ -850,7 +842,7 @@ public class APIHelper : IAPIHelper if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidPostCollection.PaidPostMedia.Add(medium); } @@ -863,7 +855,7 @@ public class APIHelper : IAPIHelper { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, medium.files.full.url); paidPostCollection.PaidPostMedia.Add(medium); } @@ -872,7 +864,7 @@ public class APIHelper : IAPIHelper { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidPostCollection.PaidPostMedia.Add(medium); } @@ -898,7 +890,7 @@ public class APIHelper : IAPIHelper } - public async Task GetPosts(string endpoint, string folder, IDownloadConfig config, List paid_post_ids, StatusContext ctx) + public async Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx) { Log.Debug($"Calling GetPosts - {endpoint}"); @@ -918,14 +910,14 @@ public class APIHelper : IAPIHelper Enumerations.DownloadDateSelection downloadDateSelection = Enumerations.DownloadDateSelection.before; DateTime? downloadAsOf = null; - if (config.DownloadOnlySpecificDates && config.CustomDate.HasValue) + if (configService.CurrentConfig.DownloadOnlySpecificDates && configService.CurrentConfig.CustomDate.HasValue) { - downloadDateSelection = config.DownloadDateSelection; - downloadAsOf = config.CustomDate; + downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; + downloadAsOf = configService.CurrentConfig.CustomDate; } - else if (config.DownloadPostsIncrementally) + else if (configService.CurrentConfig.DownloadPostsIncrementally) { - var mostRecentPostDate = await dbHelper.GetMostRecentPostDate(folder); + var mostRecentPostDate = await dbService.GetMostRecentPostDate(folder); if (mostRecentPostDate.HasValue) { downloadDateSelection = Enumerations.DownloadDateSelection.after; @@ -954,7 +946,7 @@ public class APIHelper : IAPIHelper { Post newposts = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newposts = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); posts.list.AddRange(newposts.list); @@ -975,7 +967,7 @@ public class APIHelper : IAPIHelper foreach (Post.List post in posts.list) { - if (config.SkipAds) + if (configService.CurrentConfig.SkipAds) { if (post.rawText != null && (post.rawText.Contains("#ad") || post.rawText.Contains("/trial/") || post.rawText.Contains("#announcement"))) { @@ -1001,25 +993,25 @@ public class APIHelper : IAPIHelper } } } - await dbHelper.AddPost(folder, post.id, post.rawText != null ? post.rawText : string.Empty, post.price != null ? post.price.ToString() : "0", post.price != null && post.isOpened ? true : false, post.isArchived, post.postedAt); + await dbService.AddPost(folder, post.id, post.rawText != null ? post.rawText : string.Empty, post.price != null ? post.price.ToString() : "0", post.price != null && post.isOpened ? true : false, post.isArchived, post.postedAt); postCollection.PostObjects.Add(post); if (post.media != null && post.media.Count > 0) { foreach (Post.Medium medium in post.media) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } @@ -1032,7 +1024,7 @@ public class APIHelper : IAPIHelper { if (!postCollection.Posts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, post.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, post.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); postCollection.Posts.Add(medium.id, medium.files!.full.url); postCollection.PostMedia.Add(medium); } @@ -1044,7 +1036,7 @@ public class APIHelper : IAPIHelper { if (!postCollection.Posts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, post.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, post.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); postCollection.Posts.Add(medium.id, medium.files.preview.url); postCollection.PostMedia.Add(medium); } @@ -1058,7 +1050,7 @@ public class APIHelper : IAPIHelper { if (!postCollection.Posts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, post.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, post.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); postCollection.Posts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{post.id}"); postCollection.PostMedia.Add(medium); } @@ -1083,7 +1075,7 @@ public class APIHelper : IAPIHelper } return null; } - public async Task GetPost(string endpoint, string folder, IDownloadConfig config) + public async Task GetPost(string endpoint, string folder) { Log.Debug($"Calling GetPost - {endpoint}"); @@ -1115,38 +1107,38 @@ public class APIHelper : IAPIHelper } } } - await dbHelper.AddPost(folder, singlePost.id, singlePost.text != null ? singlePost.text : string.Empty, singlePost.price != null ? singlePost.price.ToString() : "0", singlePost.price != null && singlePost.isOpened ? true : false, singlePost.isArchived, singlePost.postedAt); + await dbService.AddPost(folder, singlePost.id, singlePost.text != null ? singlePost.text : string.Empty, singlePost.price != null ? singlePost.price.ToString() : "0", singlePost.price != null && singlePost.isOpened ? true : false, singlePost.isArchived, singlePost.postedAt); singlePostCollection.SinglePostObjects.Add(singlePost); if (singlePost.media != null && singlePost.media.Count > 0) { foreach (SinglePost.Medium medium in singlePost.media) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (medium.canView && medium.files?.drm == null) { - switch (downloadConfig.DownloadVideoResolution) + switch (configService.CurrentConfig.DownloadVideoResolution) { case VideoResolution.source: if (medium.files!.full != null && !string.IsNullOrEmpty(medium.files!.full.url)) { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, singlePost.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.files!.full.url); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1159,7 +1151,7 @@ public class APIHelper : IAPIHelper { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._240, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._240, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.videoSources._240); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1173,7 +1165,7 @@ public class APIHelper : IAPIHelper { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._720, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._720, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.videoSources._720); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1189,7 +1181,7 @@ public class APIHelper : IAPIHelper { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, singlePost.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{singlePost.id}"); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1199,7 +1191,7 @@ public class APIHelper : IAPIHelper { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, singlePost.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.files.preview.url); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1224,7 +1216,7 @@ public class APIHelper : IAPIHelper return null; } - public async Task GetStreams(string endpoint, string folder, IDownloadConfig config, List paid_post_ids, StatusContext ctx) + public async Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx) { Log.Debug($"Calling GetStreams - {endpoint}"); @@ -1242,15 +1234,15 @@ public class APIHelper : IAPIHelper }; Enumerations.DownloadDateSelection downloadDateSelection = Enumerations.DownloadDateSelection.before; - if (config.DownloadOnlySpecificDates && config.CustomDate.HasValue) + if (configService.CurrentConfig.DownloadOnlySpecificDates && configService.CurrentConfig.CustomDate.HasValue) { - downloadDateSelection = config.DownloadDateSelection; + downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; } UpdateGetParamsForDateSelection( downloadDateSelection, ref getParams, - config.CustomDate); + configService.CurrentConfig.CustomDate); var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); streams = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); @@ -1269,7 +1261,7 @@ public class APIHelper : IAPIHelper { Streams newstreams = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newstreams = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); streams.list.AddRange(newstreams.list); @@ -1304,25 +1296,25 @@ public class APIHelper : IAPIHelper } } } - await dbHelper.AddPost(folder, stream.id, stream.text != null ? stream.text : string.Empty, stream.price != null ? stream.price.ToString() : "0", stream.price != null && stream.isOpened ? true : false, stream.isArchived, stream.postedAt); + await dbService.AddPost(folder, stream.id, stream.text != null ? stream.text : string.Empty, stream.price != null ? stream.price.ToString() : "0", stream.price != null && stream.isOpened ? true : false, stream.isArchived, stream.postedAt); streamsCollection.StreamObjects.Add(stream); if (stream.media != null && stream.media.Count > 0) { foreach (Streams.Medium medium in stream.media) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } @@ -1333,7 +1325,7 @@ public class APIHelper : IAPIHelper { if (!streamsCollection.Streams.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, stream.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, stream.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); streamsCollection.Streams.Add(medium.id, medium.files.full.url); streamsCollection.StreamMedia.Add(medium); } @@ -1346,7 +1338,7 @@ public class APIHelper : IAPIHelper { if (!streamsCollection.Streams.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, stream.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, stream.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); streamsCollection.Streams.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{stream.id}"); streamsCollection.StreamMedia.Add(medium); } @@ -1373,7 +1365,7 @@ public class APIHelper : IAPIHelper } - public async Task GetArchived(string endpoint, string folder, IDownloadConfig config, StatusContext ctx) + public async Task GetArchived(string endpoint, string folder, StatusContext ctx) { Log.Debug($"Calling GetArchived - {endpoint}"); @@ -1393,17 +1385,17 @@ public class APIHelper : IAPIHelper }; Enumerations.DownloadDateSelection downloadDateSelection = Enumerations.DownloadDateSelection.before; - if (config.DownloadOnlySpecificDates && config.CustomDate.HasValue) + if (configService.CurrentConfig.DownloadOnlySpecificDates && configService.CurrentConfig.CustomDate.HasValue) { - downloadDateSelection = config.DownloadDateSelection; + downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; } UpdateGetParamsForDateSelection( downloadDateSelection, ref getParams, - config.CustomDate); + configService.CurrentConfig.CustomDate); - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); archived = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1418,7 +1410,7 @@ public class APIHelper : IAPIHelper { Archived newarchived = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newarchived = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); archived.list.AddRange(newarchived.list); @@ -1452,25 +1444,25 @@ public class APIHelper : IAPIHelper } } } - await dbHelper.AddPost(folder, archive.id, archive.text != null ? archive.text : string.Empty, archive.price != null ? archive.price.ToString() : "0", archive.price != null && archive.isOpened ? true : false, archive.isArchived, archive.postedAt); + await dbService.AddPost(folder, archive.id, archive.text != null ? archive.text : string.Empty, archive.price != null ? archive.price.ToString() : "0", archive.price != null && archive.isOpened ? true : false, archive.isArchived, archive.postedAt); archivedCollection.ArchivedPostObjects.Add(archive); if (archive.media != null && archive.media.Count > 0) { foreach (Archived.Medium medium in archive.media) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } @@ -1478,7 +1470,7 @@ public class APIHelper : IAPIHelper { if (!archivedCollection.ArchivedPosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, archive.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, archive.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); archivedCollection.ArchivedPosts.Add(medium.id, medium.files.full.url); archivedCollection.ArchivedPostMedia.Add(medium); } @@ -1487,7 +1479,7 @@ public class APIHelper : IAPIHelper { if (!archivedCollection.ArchivedPosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, archive.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, archive.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); archivedCollection.ArchivedPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{archive.id}"); archivedCollection.ArchivedPostMedia.Add(medium); } @@ -1513,7 +1505,7 @@ public class APIHelper : IAPIHelper } - public async Task GetMessages(string endpoint, string folder, IDownloadConfig config, StatusContext ctx) + public async Task GetMessages(string endpoint, string folder, StatusContext ctx) { Log.Debug($"Calling GetMessages - {endpoint}"); @@ -1529,7 +1521,7 @@ public class APIHelper : IAPIHelper { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); messages = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1541,7 +1533,7 @@ public class APIHelper : IAPIHelper { Messages newmessages = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newmessages = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); messages.list.AddRange(newmessages.list); @@ -1558,7 +1550,7 @@ public class APIHelper : IAPIHelper foreach (Messages.List list in messages.list) { - if (config.SkipAds) + if (configService.CurrentConfig.SkipAds) { if (list.text != null && (list.text.Contains("#ad") || list.text.Contains("/trial/"))) { @@ -1579,9 +1571,9 @@ public class APIHelper : IAPIHelper } } } - if (!config.IgnoreOwnMessages || list.fromUser.id != Convert.ToInt32(auth.USER_ID)) + if (!configService.CurrentConfig.IgnoreOwnMessages || list.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - await dbHelper.AddMessage(folder, list.id, list.text != null ? list.text : string.Empty, list.price != null ? list.price.ToString() : "0", list.canPurchaseReason == "opened" ? true : list.canPurchaseReason != "opened" ? false : (bool?)null ?? false, false, list.createdAt.HasValue ? list.createdAt.Value : DateTime.Now, list.fromUser != null && list.fromUser.id != null ? list.fromUser.id.Value : int.MinValue); + await dbService.AddMessage(folder, list.id, list.text != null ? list.text : string.Empty, list.price != null ? list.price.ToString() : "0", list.canPurchaseReason == "opened" ? true : list.canPurchaseReason != "opened" ? false : (bool?)null ?? false, false, list.createdAt.HasValue ? list.createdAt.Value : DateTime.Now, list.fromUser != null && list.fromUser.id != null ? list.fromUser.id.Value : int.MinValue); messageCollection.MessageObjects.Add(list); if (list.canPurchaseReason != "opened" && list.media != null && list.media.Count > 0) { @@ -1589,50 +1581,50 @@ public class APIHelper : IAPIHelper { if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, medium.files.full.url); messageCollection.MessageMedia.Add(medium); } } else if (medium.canView && medium.files != null && medium.files.drm != null) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); messageCollection.MessageMedia.Add(medium); } @@ -1645,50 +1637,50 @@ public class APIHelper : IAPIHelper { if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url) && messagePreviewIds.Contains(medium.id)) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, medium.files.full.url); messageCollection.MessageMedia.Add(medium); } } else if (medium.canView && medium.files != null && medium.files.drm != null && messagePreviewIds.Contains(medium.id)) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); messageCollection.MessageMedia.Add(medium); } @@ -1714,7 +1706,7 @@ public class APIHelper : IAPIHelper return null; } - public async Task GetPaidMessage(string endpoint, string folder, IDownloadConfig config) + public async Task GetPaidMessage(string endpoint, string folder) { Log.Debug($"Calling GetPaidMessage - {endpoint}"); @@ -1729,12 +1721,12 @@ public class APIHelper : IAPIHelper { "order", "desc" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); message = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - if (!config.IgnoreOwnMessages || message.fromUser.id != Convert.ToInt32(auth.USER_ID)) + if (!configService.CurrentConfig.IgnoreOwnMessages || message.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - await dbHelper.AddMessage(folder, message.id, message.text != null ? message.text : string.Empty, message.price != null ? message.price.ToString() : "0", true, false, message.createdAt.HasValue ? message.createdAt.Value : DateTime.Now, message.fromUser != null && message.fromUser.id != null ? message.fromUser.id.Value : int.MinValue); + await dbService.AddMessage(folder, message.id, message.text != null ? message.text : string.Empty, message.price != null ? message.price.ToString() : "0", true, false, message.createdAt.HasValue ? message.createdAt.Value : DateTime.Now, message.fromUser != null && message.fromUser.id != null ? message.fromUser.id.Value : int.MinValue); singlePaidMessageCollection.SingleMessageObjects.Add(message); List messagePreviewIds = new(); if (message.previews != null && message.previews.Count > 0) @@ -1757,104 +1749,104 @@ public class APIHelper : IAPIHelper { if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); singlePaidMessageCollection.SingleMessages.Add(medium.id, medium.files.full.url.ToString()); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } } else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, medium.files.full.url.ToString()); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } } else if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.drm != null) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); singlePaidMessageCollection.SingleMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } } else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.drm != null) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } @@ -1880,7 +1872,7 @@ public class APIHelper : IAPIHelper } - public async Task GetPaidMessages(string endpoint, string folder, string username, IDownloadConfig config, StatusContext ctx) + public async Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx) { Log.Debug($"Calling GetPaidMessages - {username}"); @@ -1898,7 +1890,7 @@ public class APIHelper : IAPIHelper { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); paidMessages = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1911,7 +1903,7 @@ public class APIHelper : IAPIHelper string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); Purchased newpaidMessages = new(); Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); - HttpClient loopclient = GetHttpClient(config); + HttpClient loopclient = GetHttpClient(); HttpRequestMessage looprequest = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); @@ -1941,15 +1933,15 @@ public class APIHelper : IAPIHelper { foreach (Purchased.List purchase in paidMessages.list.Where(p => p.responseType == "message").OrderByDescending(p => p.postedAt ?? p.createdAt)) { - if (!config.IgnoreOwnMessages || purchase.fromUser.id != Convert.ToInt32(auth.USER_ID)) + if (!configService.CurrentConfig.IgnoreOwnMessages || purchase.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { if (purchase.postedAt != null) { - await dbHelper.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); + await dbService.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); } else { - await dbHelper.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); + await dbService.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); } paidMessageCollection.PaidMessageObjects.Add(purchase); if (purchase.media != null && purchase.media.Count > 0) @@ -1989,50 +1981,50 @@ public class APIHelper : IAPIHelper bool has = previewids.Any(cus => cus.Equals(medium.id)); if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, medium.files.full.url); paidMessageCollection.PaidMessageMedia.Add(medium); } } else if (!has && medium.canView && medium.files != null && medium.files.drm != null) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidMessageCollection.PaidMessageMedia.Add(medium); } @@ -2042,50 +2034,50 @@ public class APIHelper : IAPIHelper { if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, medium.files.full.url); paidMessageCollection.PaidMessageMedia.Add(medium); } } else if (medium.canView && medium.files != null && medium.files.drm != null) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidMessageCollection.PaidMessageMedia.Add(medium); } @@ -2113,7 +2105,7 @@ public class APIHelper : IAPIHelper return null; } - public async Task> GetPurchasedTabUsers(string endpoint, IDownloadConfig config, Dictionary users) + public async Task> GetPurchasedTabUsers(string endpoint, Dictionary users) { Log.Debug($"Calling GetPurchasedTabUsers - {endpoint}"); @@ -2130,7 +2122,7 @@ public class APIHelper : IAPIHelper { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); purchased = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); if (purchased != null && purchased.hasMore) { @@ -2140,7 +2132,7 @@ public class APIHelper : IAPIHelper string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); Purchased newPurchased = new(); Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); - HttpClient loopclient = GetHttpClient(config); + HttpClient loopclient = GetHttpClient(); HttpRequestMessage looprequest = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); @@ -2192,7 +2184,7 @@ public class APIHelper : IAPIHelper if(user is null) { - if (!config.BypassContentForCreatorsWhoNoLongerExist) + if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) { if(!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) { @@ -2242,7 +2234,7 @@ public class APIHelper : IAPIHelper if (user is null) { - if (!config.BypassContentForCreatorsWhoNoLongerExist) + if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) { if(!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.author.id}")) { @@ -2286,7 +2278,7 @@ public class APIHelper : IAPIHelper return null; } - public async Task> GetPurchasedTab(string endpoint, string folder, IDownloadConfig config, Dictionary users) + public async Task> GetPurchasedTab(string endpoint, string folder, Dictionary users) { Log.Debug($"Calling GetPurchasedTab - {endpoint}"); @@ -2304,7 +2296,7 @@ public class APIHelper : IAPIHelper { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config)); + var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); purchased = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); if (purchased != null && purchased.hasMore) { @@ -2314,7 +2306,7 @@ public class APIHelper : IAPIHelper string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); Purchased newPurchased = new(); Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); - HttpClient loopclient = GetHttpClient(config); + HttpClient loopclient = GetHttpClient(); HttpRequestMessage looprequest = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); @@ -2406,23 +2398,23 @@ public class APIHelper : IAPIHelper } } } - await dbHelper.AddPost(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); + await dbService.AddPost(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); purchasedTabCollection.PaidPosts.PaidPostObjects.Add(purchase); foreach (Messages.Medium medium in purchase.media) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } @@ -2434,7 +2426,7 @@ public class APIHelper : IAPIHelper if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, medium.files.full.url); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } @@ -2444,7 +2436,7 @@ public class APIHelper : IAPIHelper if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } @@ -2457,7 +2449,7 @@ public class APIHelper : IAPIHelper { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, medium.files.full.url); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } @@ -2466,7 +2458,7 @@ public class APIHelper : IAPIHelper { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } @@ -2477,11 +2469,11 @@ public class APIHelper : IAPIHelper case "message": if (purchase.postedAt != null) { - await dbHelper.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); + await dbService.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); } else { - await dbHelper.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); + await dbService.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); } purchasedTabCollection.PaidMessages.PaidMessageObjects.Add(purchase); if (purchase.media != null && purchase.media.Count > 0) @@ -2521,50 +2513,50 @@ public class APIHelper : IAPIHelper bool has = paidMessagePreviewids.Any(cus => cus.Equals(medium.id)); if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, medium.files.full.url); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } } else if (!has && medium.canView && medium.files != null && medium.files.drm != null) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } @@ -2574,50 +2566,50 @@ public class APIHelper : IAPIHelper { if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, medium.files.full.url); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } } else if (medium.canView && medium.files != null && medium.files.drm != null) { - if (medium.type == "photo" && !config.DownloadImages) + if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !config.DownloadVideos) + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !config.DownloadVideos) + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !config.DownloadAudios) + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) { - await dbHelper.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } @@ -2656,9 +2648,9 @@ public class APIHelper : IAPIHelper HttpClient client = new(); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); - request.Headers.Add("user-agent", auth.USER_AGENT); + request.Headers.Add("user-agent", authService.CurrentAuth.USER_AGENT); request.Headers.Add("Accept", "*/*"); - request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {auth.COOKIE};"); + request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); using (var response = await client.SendAsync(request)) { response.EnsureSuccessStatusCode(); @@ -2701,9 +2693,9 @@ public class APIHelper : IAPIHelper HttpClient client = new(); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); - request.Headers.Add("user-agent", auth.USER_AGENT); + request.Headers.Add("user-agent", authService.CurrentAuth.USER_AGENT); request.Headers.Add("Accept", "*/*"); - request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {auth.COOKIE};"); + request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); diff --git a/OF DL/Services/AuthService.cs b/OF DL/Services/AuthService.cs index bf93ddd..c705b2f 100644 --- a/OF DL/Services/AuthService.cs +++ b/OF DL/Services/AuthService.cs @@ -1,12 +1,31 @@ using Newtonsoft.Json; using OF_DL.Entities; -using OF_DL.Helpers.Interfaces; +using PuppeteerSharp; +using PuppeteerSharp.BrowserData; using Serilog; namespace OF_DL.Services { - public class AuthService(IAuthHelper authHelper) : IAuthService + public class AuthService : IAuthService { + private readonly LaunchOptions _options = new() + { + Headless = false, + Channel = ChromeReleaseChannel.Stable, + DefaultViewport = null, + Args = ["--no-sandbox", "--disable-setuid-sandbox"], + UserDataDir = Path.GetFullPath("chrome-data") + }; + + private readonly string[] _desiredCookies = + [ + "auth_id", + "sess" + ]; + + private const int LoginTimeout = 600000; // 10 minutes + private const int FeedLoadTimeout = 60000; // 1 minute + public Auth? CurrentAuth { get; set; } public async Task LoadFromFileAsync(string filePath = "auth.json") @@ -37,8 +56,8 @@ namespace OF_DL.Services { bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; - await authHelper.SetupBrowser(runningInDocker); - CurrentAuth = await authHelper.GetAuthFromBrowser(); + await SetupBrowser(runningInDocker); + CurrentAuth = await GetAuthFromBrowser(); return CurrentAuth != null; } @@ -68,5 +87,144 @@ namespace OF_DL.Services Log.Error(ex, "Failed to save auth to file"); } } + + private async Task SetupBrowser(bool runningInDocker) + { + string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH"); + if (executablePath != null) + { + Log.Information("OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}", executablePath); + _options.ExecutablePath = executablePath; + } + else + { + var browserFetcher = new BrowserFetcher(); + var installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList(); + if (installedBrowsers.Count == 0) + { + Log.Information("Downloading browser."); + var downloadedBrowser = await browserFetcher.DownloadAsync(); + Log.Information("Browser downloaded. Path: {executablePath}", + downloadedBrowser.GetExecutablePath()); + _options.ExecutablePath = downloadedBrowser.GetExecutablePath(); + } + else + { + _options.ExecutablePath = installedBrowsers.First().GetExecutablePath(); + } + } + + if (runningInDocker) + { + Log.Information("Running in Docker. Disabling sandbox and GPU."); + _options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]; + } + } + + private async Task GetBcToken(IPage page) + { + return await page.EvaluateExpressionAsync("window.localStorage.getItem('bcTokenSha') || ''"); + } + + private async Task GetAuthFromBrowser(bool isDocker = false) + { + try + { + IBrowser? browser; + try + { + browser = await Puppeteer.LaunchAsync(_options); + } + catch (ProcessException e) + { + if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir)) + { + Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again."); + Directory.Delete(_options.UserDataDir, true); + browser = await Puppeteer.LaunchAsync(_options); + } + else + { + throw; + } + } + + if (browser == null) + { + throw new Exception("Could not get browser"); + } + + IPage[]? pages = await browser.PagesAsync(); + IPage? page = pages.First(); + + if (page == null) + { + throw new Exception("Could not get page"); + } + + Log.Debug("Navigating to OnlyFans."); + await page.GoToAsync("https://onlyfans.com"); + + Log.Debug("Waiting for user to login"); + await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout }); + Log.Debug("Feed element detected (user logged in)"); + + await page.ReloadAsync(); + + await page.WaitForNavigationAsync(new NavigationOptions { + WaitUntil = [WaitUntilNavigation.Networkidle2], + Timeout = FeedLoadTimeout + }); + Log.Debug("DOM loaded. Getting BC token and cookies ..."); + + string xBc; + try + { + xBc = await GetBcToken(page); + } + catch (Exception e) + { + throw new Exception("Error getting bcToken"); + } + + Dictionary mappedCookies = (await page.GetCookiesAsync()) + .Where(cookie => cookie.Domain.Contains("onlyfans.com")) + .ToDictionary(cookie => cookie.Name, cookie => cookie.Value); + + mappedCookies.TryGetValue("auth_id", out string? userId); + if (userId == null) + { + throw new Exception("Could not find 'auth_id' cookie"); + } + + mappedCookies.TryGetValue("sess", out string? sess); + if (sess == null) + { + throw new Exception("Could not find 'sess' cookie"); + } + + string? userAgent = await browser.GetUserAgentAsync(); + if (userAgent == null) + { + throw new Exception("Could not get user agent"); + } + + string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key)) + .Select(key => $"${key}={mappedCookies[key]};")); + + return new Auth() + { + COOKIE = cookies, + USER_AGENT = userAgent, + USER_ID = userId, + X_BC = xBc + }; + } + catch (Exception e) + { + Log.Error(e, "Error getting auth from browser"); + return null; + } + } } } diff --git a/OF DL/Services/ConfigService.cs b/OF DL/Services/ConfigService.cs index 67a418a..b2c6a28 100644 --- a/OF DL/Services/ConfigService.cs +++ b/OF DL/Services/ConfigService.cs @@ -10,7 +10,7 @@ namespace OF_DL.Services { public class ConfigService(ILoggingService loggingService) : IConfigService { - public Config? CurrentConfig { get; private set; } + public Config CurrentConfig { get; private set; } = new(); public bool IsCliNonInteractive { get; private set; } public async Task LoadConfigurationAsync(string[] args) diff --git a/OF DL/Helpers/DBHelper.cs b/OF DL/Services/DBService.cs similarity index 97% rename from OF DL/Helpers/DBHelper.cs rename to OF DL/Services/DBService.cs index 697fa71..70efd80 100644 --- a/OF DL/Helpers/DBHelper.cs +++ b/OF DL/Services/DBService.cs @@ -1,25 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using OF_DL.Enumurations; -using System.IO; using Microsoft.Data.Sqlite; using Serilog; -using OF_DL.Entities; -namespace OF_DL.Helpers +namespace OF_DL.Services { - public class DBHelper : IDBHelper + public class DBService(IConfigService configService) : IDBService { - private readonly IDownloadConfig downloadConfig; - - public DBHelper(IDownloadConfig downloadConfig) - { - this.downloadConfig = downloadConfig; - } - public async Task CreateDB(string folder) { try @@ -366,7 +352,7 @@ namespace OF_DL.Helpers connection.Open(); await EnsureCreatedAtColumnExists(connection, "medias"); StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM medias WHERE media_id=@media_id"); - if (downloadConfig.DownloadDuplicatedMedia) + if (configService.CurrentConfig.DownloadDuplicatedMedia) { sql.Append(" and api_type=@api_type"); } @@ -405,7 +391,7 @@ namespace OF_DL.Helpers using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) { StringBuilder sql = new StringBuilder("SELECT downloaded FROM medias WHERE media_id=@media_id"); - if(downloadConfig.DownloadDuplicatedMedia) + if(configService.CurrentConfig.DownloadDuplicatedMedia) { sql.Append(" and api_type=@api_type"); } @@ -440,7 +426,7 @@ namespace OF_DL.Helpers // Construct the update command StringBuilder sql = new StringBuilder("UPDATE medias SET directory=@directory, filename=@filename, size=@size, downloaded=@downloaded, created_at=@created_at WHERE media_id=@media_id"); - if (downloadConfig.DownloadDuplicatedMedia) + if (configService.CurrentConfig.DownloadDuplicatedMedia) { sql.Append(" and api_type=@api_type"); } diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs new file mode 100644 index 0000000..9fb0403 --- /dev/null +++ b/OF DL/Services/DownloadService.cs @@ -0,0 +1,2630 @@ +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using FFmpeg.NET; +using FFmpeg.NET.Events; +using OF_DL.Entities; +using OF_DL.Entities.Archived; +using OF_DL.Entities.Messages; +using OF_DL.Entities.Post; +using OF_DL.Entities.Purchased; +using OF_DL.Entities.Streams; +using OF_DL.Enumerations; +using OF_DL.Enumurations; +using OF_DL.Utils; +using Serilog; +using Serilog.Events; +using static OF_DL.Entities.Messages.Messages; +using FromUser = OF_DL.Entities.Messages.FromUser; + +namespace OF_DL.Services; + +public class DownloadService( + IAuthService authService, + IConfigService configService, + IDBService dbService, + IFileNameService fileNameService, + IAPIService apiService) + : IDownloadService +{ + private TaskCompletionSource _completionSource; + + public async Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username) + { + try + { + string path = "/Profile"; + + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + + if (!string.IsNullOrEmpty(avatarUrl)) + { + string avatarpath = $"{path}/Avatars"; + if (!Directory.Exists(folder + avatarpath)) Directory.CreateDirectory(folder + avatarpath); + + List avatarMD5Hashes = WidevineClient.Utils.CalculateFolderMD5(folder + avatarpath); + + Uri uri = new(avatarUrl); + string destinationPath = $"{folder}{avatarpath}/"; + + HttpClient client = new(); + + HttpRequestMessage request = new() + { + Method = HttpMethod.Get, + RequestUri = uri + }; + using HttpResponseMessage response = + await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + using MemoryStream memoryStream = new(); + await response.Content.CopyToAsync(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + + MD5 md5 = MD5.Create(); + byte[] hash = md5.ComputeHash(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + if (!avatarMD5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant())) + { + destinationPath = destinationPath + string.Format("{0} {1}.jpg", username, + response.Content.Headers.LastModified.HasValue + ? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy") + : DateTime.Now.ToString("dd-MM-yyyy")); + + using (FileStream fileStream = File.Create(destinationPath)) + { + await memoryStream.CopyToAsync(fileStream); + } + + File.SetLastWriteTime(destinationPath, + response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); + } + } + + if (!string.IsNullOrEmpty(headerUrl)) + { + string headerpath = $"{path}/Headers"; + if (!Directory.Exists(folder + headerpath)) Directory.CreateDirectory(folder + headerpath); + + List headerMD5Hashes = WidevineClient.Utils.CalculateFolderMD5(folder + headerpath); + + Uri uri = new(headerUrl); + string destinationPath = $"{folder}{headerpath}/"; + + HttpClient client = new(); + + HttpRequestMessage request = new() + { + Method = HttpMethod.Get, + RequestUri = uri + }; + using HttpResponseMessage response = + await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + using MemoryStream memoryStream = new(); + await response.Content.CopyToAsync(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + + MD5 md5 = MD5.Create(); + byte[] hash = md5.ComputeHash(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + if (!headerMD5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant())) + { + destinationPath = destinationPath + string.Format("{0} {1}.jpg", username, + response.Content.Headers.LastModified.HasValue + ? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy") + : DateTime.Now.ToString("dd-MM-yyyy")); + + using (FileStream fileStream = File.Create(destinationPath)) + { + await memoryStream.CopyToAsync(fileStream); + } + + File.SetLastWriteTime(destinationPath, + response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); + } + } + } + catch (Exception ex) + { + Console.WriteLine("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); + } + } + } + + #region drm common + + private async Task DownloadDrmMedia(string user_agent, string policy, string signature, string kvp, + string sess, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, + string api_type, IProgressReporter progressReporter, string customFileName, string filename, string path) + { + try + { + _completionSource = new TaskCompletionSource(); + + int pos1 = decryptionKey.IndexOf(':'); + string decKey = ""; + if (pos1 >= 0) decKey = decryptionKey.Substring(pos1 + 1); + + int streamIndex = 0; + string tempFilename = $"{folder}{path}/{filename}_source.mp4"; + + //int? streamIndex = await GetVideoStreamIndexFromMpd(url, policy, signature, kvp, downloadConfig.DownloadVideoResolution); + + //if (streamIndex == null) + // throw new Exception($"Could not find video stream for resolution {downloadConfig.DownloadVideoResolution}"); + + //string tempFilename; + + //switch (downloadConfig.DownloadVideoResolution) + //{ + // case VideoResolution.source: + // tempFilename = $"{folder}{path}/{filename}_source.mp4"; + // break; + // case VideoResolution._240: + // tempFilename = $"{folder}{path}/{filename}_240.mp4"; + // break; + // case VideoResolution._720: + // tempFilename = $"{folder}{path}/{filename}_720.mp4"; + // break; + // default: + // tempFilename = $"{folder}{path}/{filename}_source.mp4"; + // break; + //} + + // Configure ffmpeg log level and optional report file location + bool ffmpegDebugLogging = Log.IsEnabled(LogEventLevel.Debug); + + string logLevelArgs = ffmpegDebugLogging || + configService.CurrentConfig.LoggingLevel is LoggingLevel.Verbose or LoggingLevel.Debug + ? "-loglevel debug -report" + : configService.CurrentConfig.LoggingLevel switch + { + LoggingLevel.Information => "-loglevel info", + LoggingLevel.Warning => "-loglevel warning", + LoggingLevel.Error => "-loglevel error", + LoggingLevel.Fatal => "-loglevel fatal", + _ => string.Empty + }; + + if (logLevelArgs.Contains("-report", StringComparison.OrdinalIgnoreCase)) + { + // Direct ffmpeg report files into the same logs directory Serilog uses (relative to current working directory) + string logDir = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "logs")); + Directory.CreateDirectory(logDir); + string ffReportPath = Path.Combine(logDir, "ffmpeg-%p-%t.log"); // ffmpeg will replace %p/%t + Environment.SetEnvironmentVariable("FFREPORT", $"file={ffReportPath}:level=32"); + Log.Debug("FFREPORT enabled at: {FFREPORT} (cwd: {Cwd})", + Environment.GetEnvironmentVariable("FFREPORT"), Environment.CurrentDirectory); + } + else + { + Environment.SetEnvironmentVariable("FFREPORT", null); + Log.Debug("FFREPORT disabled (cwd: {Cwd})", Environment.CurrentDirectory); + } + + string cookieHeader = + "Cookie: " + + $"CloudFront-Policy={policy}; " + + $"CloudFront-Signature={signature}; " + + $"CloudFront-Key-Pair-Id={kvp}; " + + $"{sess}"; + + string parameters = + $"{logLevelArgs} " + + $"-cenc_decryption_key {decKey} " + + $"-headers \"{cookieHeader}\" " + + $"-user_agent \"{user_agent}\" " + + "-referer \"https://onlyfans.com\" " + + "-rw_timeout 20000000 " + + "-reconnect 1 -reconnect_streamed 1 -reconnect_on_network_error 1 -reconnect_delay_max 10 " + + "-y " + + $"-i \"{url}\" " + + $"-map 0:v:{streamIndex} -map 0:a? " + + "-c copy " + + $"\"{tempFilename}\""; + + Log.Debug($"Calling FFMPEG with Parameters: {parameters}"); + + Engine ffmpeg = new(configService.CurrentConfig.FFmpegPath); + ffmpeg.Error += OnError; + ffmpeg.Complete += async (sender, args) => + { + _completionSource.TrySetResult(true); + await OnFFMPEGDownloadComplete(tempFilename, lastModified, folder, path, customFileName, filename, + media_id, api_type, progressReporter); + }; + await ffmpeg.ExecuteAsync(parameters, CancellationToken.None); + + return await _completionSource.Task; + } + 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); + } + } + + return false; + } + + #endregion + + private async Task OnFFMPEGDownloadComplete(string tempFilename, DateTime lastModified, string folder, string path, + string customFileName, string filename, long media_id, string api_type, IProgressReporter progressReporter) + { + try + { + if (File.Exists(tempFilename)) File.SetLastWriteTime(tempFilename, lastModified); + if (!string.IsNullOrEmpty(customFileName)) + File.Move(tempFilename, $"{folder + path + "/" + customFileName + ".mp4"}"); + + // Cleanup Files + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : tempFilename).Length; + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + 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); + } + } + } + + private void OnError(object sender, ConversionErrorEventArgs e) + { + // Guard all fields to avoid NullReference exceptions from FFmpeg.NET + string input = e?.Input?.Name ?? ""; + string output = e?.Output?.Name ?? ""; + string exitCode = e?.Exception?.ExitCode.ToString() ?? ""; + string message = e?.Exception?.Message ?? ""; + string inner = e?.Exception?.InnerException?.Message ?? ""; + + Log.Error("FFmpeg failed. Input={Input} Output={Output} ExitCode={ExitCode} Message={Message} Inner={Inner}", + input, output, exitCode, message, inner); + + _completionSource?.TrySetResult(false); + } + + private async Task GetVideoStreamIndexFromMpd(string mpdUrl, string policy, string signature, string kvp, + VideoResolution resolution) + { + HttpClient client = new(); + HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); + request.Headers.Add("user-agent", authService.CurrentAuth.USER_AGENT); + request.Headers.Add("Accept", "*/*"); + request.Headers.Add("Cookie", + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); + using (HttpResponseMessage response = await client.SendAsync(request)) + { + response.EnsureSuccessStatusCode(); + string body = await response.Content.ReadAsStringAsync(); + XDocument doc = XDocument.Parse(body); + XNamespace ns = "urn:mpeg:dash:schema:mpd:2011"; + XNamespace cenc = "urn:mpeg:cenc:2013"; + XElement? videoAdaptationSet = doc + .Descendants(ns + "AdaptationSet") + .FirstOrDefault(e => (string)e.Attribute("mimeType") == "video/mp4"); + + if (videoAdaptationSet == null) + return null; + + string targetHeight = resolution switch + { + VideoResolution._240 => "240", + VideoResolution._720 => "720", + VideoResolution.source => "1280", + _ => throw new ArgumentOutOfRangeException(nameof(resolution)) + }; + + List representations = videoAdaptationSet.Elements(ns + "Representation").ToList(); + + for (int i = 0; i < representations.Count; i++) + if ((string)representations[i].Attribute("height") == targetHeight) + return i; // this is the index FFmpeg will use for `-map 0:v:{i}` + } + + return null; + } + + #region common + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + protected async Task CreateDirectoriesAndDownloadMedia(string path, + string url, + string folder, + long media_id, + string api_type, + IProgressReporter progressReporter, + string serverFileName, + string resolvedFileName) + { + try + { + string customFileName = string.Empty; + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + string extension = Path.GetExtension(url.Split("?")[0]); + + path = UpdatePathBasedOnExtension(folder, path, extension); + + return await ProcessMediaDownload(folder, media_id, api_type, url, path, serverFileName, resolvedFileName, + extension, progressReporter); + } + catch (Exception ex) + { + Console.WriteLine("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); + } + } + + return false; + } + + + /// + /// Updates the given path based on the file extension. + /// + /// The parent folder. + /// The initial relative path. + /// The file extension. + /// A string that represents the updated path based on the file extension. + private string UpdatePathBasedOnExtension(string folder, string path, string extension) + { + string subdirectory = string.Empty; + + switch (extension.ToLower()) + { + case ".jpg": + case ".jpeg": + case ".png": + subdirectory = "/Images"; + break; + case ".mp4": + case ".avi": + case ".wmv": + case ".gif": + case ".mov": + subdirectory = "/Videos"; + break; + case ".mp3": + case ".wav": + case ".ogg": + subdirectory = "/Audios"; + break; + } + + if (!string.IsNullOrEmpty(subdirectory)) + { + path += subdirectory; + string fullPath = folder + path; + + if (!Directory.Exists(fullPath)) Directory.CreateDirectory(fullPath); + } + + return path; + } + + + /// + /// Generates a custom filename based on the given format and properties. + /// + /// The format string for the filename. + /// General information about the post. + /// Media associated with the post. + /// Author of the post. + /// Dictionary containing user-related data. + /// Helper class for filename operations. + /// A Task resulting in a string that represents the custom filename. + private async Task GenerateCustomFileName(string filename, + string? filenameFormat, + object? postInfo, + object? postMedia, + object? author, + string username, + Dictionary users, + IFileNameService fileNameService, + CustomFileNameOption option) + { + if (string.IsNullOrEmpty(filenameFormat) || postInfo == null || postMedia == null || author == null) + return option switch + { + CustomFileNameOption.ReturnOriginal => filename, + CustomFileNameOption.ReturnEmpty => string.Empty, + _ => filename + }; + + List properties = new(); + string pattern = @"\{(.*?)\}"; + MatchCollection matches = Regex.Matches(filenameFormat, pattern); + properties.AddRange(matches.Select(match => match.Groups[1].Value)); + + Dictionary values = + await fileNameService.GetFilename(postInfo, postMedia, author, properties, username, users); + return await fileNameService.BuildFilename(filenameFormat, values); + } + + + private async Task GetFileSizeAsync(string url) + { + long fileSize = 0; + + try + { + Uri uri = new(url); + + if (uri.Host == "cdn3.onlyfans.com" && uri.LocalPath.Contains("/dash/files")) + { + string[] messageUrlParsed = url.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + + mpdURL = mpdURL.Replace(".mpd", "_source.mp4"); + + using HttpClient client = new(); + client.DefaultRequestHeaders.Add("Cookie", + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE}"); + client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.USER_AGENT); + + using HttpResponseMessage response = + await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); + if (response.IsSuccessStatusCode) fileSize = response.Content.Headers.ContentLength ?? 0; + } + else + { + using HttpClient client = new(); + client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.USER_AGENT); + using HttpResponseMessage response = + await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead); + if (response.IsSuccessStatusCode) fileSize = response.Content.Headers.ContentLength ?? 0; + } + } + catch (Exception ex) + { + Console.WriteLine($"Error getting file size for URL '{url}': {ex.Message}"); + } + + return fileSize; + } + + public static async Task GetDRMVideoLastModified(string url, Auth auth) + { + Uri uri = new(url); + + string[] messageUrlParsed = url.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + + mpdURL = mpdURL.Replace(".mpd", "_source.mp4"); + + using HttpClient client = new(); + client.DefaultRequestHeaders.Add("Cookie", + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {auth.COOKIE}"); + client.DefaultRequestHeaders.Add("User-Agent", auth.USER_AGENT); + + using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); + if (response.IsSuccessStatusCode) return response.Content.Headers.LastModified.Value.DateTime; + return DateTime.Now; + } + + public static async Task GetMediaLastModified(string url) + { + using HttpClient client = new(); + + using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + if (response.IsSuccessStatusCode) return response.Content.Headers.LastModified.Value.DateTime; + return DateTime.Now; + } + + /// + /// Processes the download and database update of media. + /// + /// The folder where the media is stored. + /// The ID of the media. + /// The full path to the media. + /// The URL from where to download the media. + /// The relative path to the media. + /// The filename after any required manipulations. + /// The file extension. + /// The task object for tracking progress. + /// A Task resulting in a boolean indicating whether the media is newly downloaded or not. + public async Task ProcessMediaDownload(string folder, + long media_id, + string api_type, + string url, + string path, + string serverFilename, + string resolvedFilename, + string extension, + IProgressReporter progressReporter) + { + try + { + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + return await HandleNewMedia(folder, + media_id, + api_type, + url, + path, + serverFilename, + resolvedFilename, + extension, + progressReporter); + + bool status = await HandlePreviouslyDownloadedMediaAsync(folder, media_id, api_type, progressReporter); + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + serverFilename != resolvedFilename) + await HandleRenamingOfExistingFilesAsync(folder, media_id, api_type, path, serverFilename, + resolvedFilename, extension); + return status; + } + catch (Exception ex) + { + // Handle exception (e.g., log it) + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + } + + + private async Task HandleRenamingOfExistingFilesAsync(string folder, + long media_id, + string api_type, + string path, + string serverFilename, + string resolvedFilename, + string extension) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{serverFilename}{extension}"; + string fullPathWithTheNewFileName = $"{folder}{path}/{resolvedFilename}{extension}"; + if (!File.Exists(fullPathWithTheServerFileName)) return false; + + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + DateTime lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, resolvedFilename + extension, size, true, + lastModified); + return true; + } + + + /// + /// Handles new media by downloading and updating the database. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// A Task resulting in a boolean indicating whether the media is newly downloaded or not. + private async Task HandleNewMedia(string folder, + long media_id, + string api_type, + string url, + string path, + string serverFilename, + string resolvedFilename, + string extension, + IProgressReporter progressReporter) + { + long fileSizeInBytes; + DateTime lastModified; + bool status; + + string fullPathWithTheServerFileName = $"{folder}{path}/{serverFilename}{extension}"; + string fullPathWithTheNewFileName = $"{folder}{path}/{resolvedFilename}{extension}"; + + //there are a few possibilities here. + //1.file has been downloaded in the past but it has the server filename + // in that case it should be set as existing and it should be renamed + //2.file has been downloaded in the past but it has custom filename. + // it should be set as existing and nothing else. + // of coures 1 and 2 depends in the fact that there may be a difference in the resolved file name + // (ie user has selected a custom format. If he doesn't then the resolved name will be the same as the server filename + //3.file doesn't exist and it should be downloaded. + + // Handle the case where the file has been downloaded in the past with the server filename + //but it has downloaded outsite of this application so it doesn't exist in the database + if (File.Exists(fullPathWithTheServerFileName)) + { + string finalPath; + if (fullPathWithTheServerFileName != fullPathWithTheNewFileName) + { + finalPath = fullPathWithTheNewFileName; + //rename. + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + } + } + else + { + finalPath = fullPathWithTheServerFileName; + } + + fileSizeInBytes = GetLocalFileSize(finalPath); + lastModified = File.GetLastWriteTime(finalPath); + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + status = false; + } + // Handle the case where the file has been downloaded in the past with a custom filename. + //but it has downloaded outsite of this application so it doesn't exist in the database + // this is a bit improbable but we should check for that. + else if (File.Exists(fullPathWithTheNewFileName)) + { + fileSizeInBytes = GetLocalFileSize(fullPathWithTheNewFileName); + lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName); + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + status = false; + } + else //file doesn't exist and we should download it. + { + lastModified = await DownloadFile(url, fullPathWithTheNewFileName, progressReporter); + fileSizeInBytes = GetLocalFileSize(fullPathWithTheNewFileName); + status = true; + } + + //finaly check which filename we should use. Custom or the server one. + //if a custom is used, then the servefilename will be different from the resolved filename. + string finalName = serverFilename == resolvedFilename ? serverFilename : resolvedFilename; + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, finalName + extension, fileSizeInBytes, + true, lastModified); + return status; + } + + + /// + /// Handles media that has been previously downloaded and updates the task accordingly. + /// + /// + /// + /// + /// + /// A boolean indicating whether the media is newly downloaded or not. + private async Task HandlePreviouslyDownloadedMediaAsync(string folder, long media_id, string api_type, + IProgressReporter progressReporter) + { + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + + return false; + } + + + /// + /// Gets the file size of the media. + /// + /// The path to the file. + /// The file size in bytes. + private long GetLocalFileSize(string filePath) + { + return new FileInfo(filePath).Length; + } + + + /// + /// Downloads a file from the given URL and saves it to the specified destination path. + /// + /// The URL to download the file from. + /// The path where the downloaded file will be saved. + /// Progress tracking object. + /// A Task resulting in a DateTime indicating the last modified date of the downloaded file. + private async Task DownloadFile(string url, string destinationPath, IProgressReporter progressReporter) + { + using HttpClient client = new(); + HttpRequestMessage request = new() + { + Method = HttpMethod.Get, + RequestUri = new Uri(url) + }; + + using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + Stream body = await response.Content.ReadAsStreamAsync(); + + // Wrap the body stream with the ThrottledStream to limit read rate. + using (ThrottledStream throttledStream = new(body, + configService.CurrentConfig.DownloadLimitInMbPerSec * 1_000_000, + configService.CurrentConfig.LimitDownloadRate)) + { + using FileStream fileStream = new(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 16384, + true); + byte[] buffer = new byte[16384]; + int read; + while ((read = await throttledStream.ReadAsync(buffer, CancellationToken.None)) > 0) + { + if (configService.CurrentConfig.ShowScrapeSize) progressReporter.ReportProgress(read); + await fileStream.WriteAsync(buffer.AsMemory(0, read), CancellationToken.None); + } + } + + File.SetLastWriteTime(destinationPath, response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); + if (!configService.CurrentConfig.ShowScrapeSize) progressReporter.ReportProgress(1); + return response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now; + } + + public async Task CalculateTotalFileSize(List urls) + { + long totalFileSize = 0; + if (urls.Count > 250) + { + int batchSize = 250; + + List> tasks = new(); + + for (int i = 0; i < urls.Count; i += batchSize) + { + List batchUrls = urls.Skip(i).Take(batchSize).ToList(); + + IEnumerable> batchTasks = batchUrls.Select(GetFileSizeAsync); + tasks.AddRange(batchTasks); + + await Task.WhenAll(batchTasks); + + await Task.Delay(5000); + } + + long[] fileSizes = await Task.WhenAll(tasks); + foreach (long fileSize in fileSizes) totalFileSize += fileSize; + } + else + { + List> tasks = new(); + + foreach (string url in urls) tasks.Add(GetFileSizeAsync(url)); + + long[] fileSizes = await Task.WhenAll(tasks); + foreach (long fileSize in fileSizes) totalFileSize += fileSize; + } + + return totalFileSize; + } + + #endregion + + #region normal posts + + public async Task DownloadPostMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, + Post.Author? author, Dictionary users) + { + string path; + if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && + postInfo?.postedAt is not null) + path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + else + path = "/Posts/Free"; + + Uri uri = new(url); + string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, + folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); + + return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + filename, resolvedFilename); + } + + public async Task DownloadPostMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, + SinglePost.Author? author, Dictionary users) + { + string path; + if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && + postInfo?.postedAt is not null) + path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + else + path = "/Posts/Free"; + + Uri uri = new(url); + string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, + folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); + + return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + filename, resolvedFilename); + } + + public async Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, + Streams.Medium? streamMedia, Streams.Author? author, Dictionary users) + { + string path; + if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.id is not null && + streamInfo?.postedAt is not null) + path = $"/Posts/Free/{streamInfo.id} {streamInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + else + path = "/Posts/Free"; + + Uri uri = new(url); + string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, streamInfo, streamMedia, + author, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); + + return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + filename, resolvedFilename); + } + + + public async Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, + Messages.FromUser? fromUser, Dictionary users) + { + string path; + if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && + messageInfo?.createdAt is not null) + path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + else + path = "/Messages/Free"; + Uri uri = new(url); + string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, + fromUser, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); + return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + filename, resolvedFilename); + } + + public async Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users) + { + string path; + if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && + messageInfo?.createdAt is not null) + path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + else + path = "/Messages/Free"; + Uri uri = new(url); + string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, + fromUser, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); + return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + filename, resolvedFilename); + } + + + public async Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Archived.List? messageInfo, + Archived.Medium? messageMedia, Archived.Author? author, Dictionary users) + { + string path = "/Archived/Posts/Free"; + Uri uri = new(url); + string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, + author, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); + return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + filename, resolvedFilename); + } + + + public async Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter) + { + string path = "/Stories/Free"; + Uri uri = new(url); + string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); + return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + filename, filename); + } + + public async Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + Purchased.FromUser? fromUser, Dictionary users) + { + string path; + if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && + messageInfo?.createdAt is not null) + path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + else + path = "/Messages/Paid"; + Uri uri = new(url); + string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, + fromUser, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); + return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + filename, resolvedFilename); + } + + public async Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users) + { + string path; + if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && + messageInfo?.createdAt is not null) + path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + else + path = "/Messages/Paid"; + Uri uri = new(url); + string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, + fromUser, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); + return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + filename, resolvedFilename); + } + + public async Task DownloadPurchasedPostMedia(string url, + string folder, + long media_id, + string api_type, + IProgressReporter progressReporter, + string? filenameFormat, + Purchased.List? messageInfo, + Medium? messageMedia, + Purchased.FromUser? fromUser, + Dictionary users) + { + string path; + if (configService.CurrentConfig.FolderPerPaidPost && messageInfo != null && messageInfo?.id is not null && + messageInfo?.postedAt is not null) + path = $"/Posts/Paid/{messageInfo.id} {messageInfo.postedAt.Value:yyyy-MM-dd HH-mm-ss}"; + else + path = "/Posts/Paid"; + Uri uri = new(url); + string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); + string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, + fromUser, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); + return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + filename, resolvedFilename); + } + + #endregion + + + #region drm posts + + public async Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, + Messages.FromUser? fromUser, Dictionary users) + { + try + { + string customFileName = string.Empty; + string path; + Uri uri = new(url); + string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; + if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && + messageInfo?.createdAt is not null) + path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + else + path = "/Messages/Free/Videos"; + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + + + if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) + { + List properties = new(); + string pattern = @"\{(.*?)\}"; + MatchCollection matches = Regex.Matches(filenameFormat, pattern); + foreach (Match match in matches) properties.Add(match.Groups[1].Value); + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, + fromUser, properties, folder.Split("/")[^1], users); + customFileName = await fileNameService.BuildFilename(filenameFormat, values); + } + + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { + if (!string.IsNullOrEmpty(customFileName) + ? !File.Exists(folder + path + "/" + customFileName + ".mp4") + : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, + authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + progressReporter, customFileName, filename, path); + + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : folder + path + "/" + filename + "_source.mp4").Length; + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + else + { + if (!string.IsNullOrEmpty(customFileName)) + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + filename + "_source" != customFileName) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; + string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; + if (!File.Exists(fullPathWithTheServerFileName)) return false; + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + size, true, lastModified); + } + + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + } + + return false; + } + 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); + } + } + + return false; + } + + public async Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, + string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users) + { + try + { + string customFileName = string.Empty; + string path; + Uri uri = new(url); + string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; + if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && + messageInfo?.createdAt is not null) + path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + else + path = "/Messages/Free/Videos"; + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + + + if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) + { + List properties = new(); + string pattern = @"\{(.*?)\}"; + MatchCollection matches = Regex.Matches(filenameFormat, pattern); + foreach (Match match in matches) properties.Add(match.Groups[1].Value); + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, + fromUser, properties, folder.Split("/")[^1], users); + customFileName = await fileNameService.BuildFilename(filenameFormat, values); + } + + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { + if (!string.IsNullOrEmpty(customFileName) + ? !File.Exists(folder + path + "/" + customFileName + ".mp4") + : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, + authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + progressReporter, customFileName, filename, path); + + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : folder + path + "/" + filename + "_source.mp4").Length; + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + else + { + if (!string.IsNullOrEmpty(customFileName)) + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + filename + "_source" != customFileName) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; + string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; + if (!File.Exists(fullPathWithTheServerFileName)) return false; + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + size, true, lastModified); + } + + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + } + + return false; + } + 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); + } + } + + return false; + } + + + public async Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + Purchased.FromUser? fromUser, Dictionary users) + { + try + { + string customFileName = string.Empty; + string path; + Uri uri = new(url); + string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; + if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && + messageInfo?.id is not null && messageInfo?.createdAt is not null) + path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + else + path = "/Messages/Paid/Videos"; + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + + if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) + { + List properties = new(); + string pattern = @"\{(.*?)\}"; + MatchCollection matches = Regex.Matches(filenameFormat, pattern); + foreach (Match match in matches) properties.Add(match.Groups[1].Value); + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, + fromUser, properties, folder.Split("/")[^1], users); + customFileName = await fileNameService.BuildFilename(filenameFormat, values); + } + + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { + if (!string.IsNullOrEmpty(customFileName) + ? !File.Exists(folder + path + "/" + customFileName + ".mp4") + : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, + authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + progressReporter, customFileName, filename, path); + + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : folder + path + "/" + filename + "_source.mp4").Length; + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + else + { + if (!string.IsNullOrEmpty(customFileName)) + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + filename + "_source" != customFileName) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; + string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; + if (!File.Exists(fullPathWithTheServerFileName)) return false; + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + size, true, lastModified); + } + + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + } + + return false; + } + 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); + } + } + + return false; + } + + public async Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, + string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users) + { + try + { + string customFileName = string.Empty; + string path; + Uri uri = new(url); + string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; + if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && + messageInfo?.id is not null && messageInfo?.createdAt is not null) + path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + else + path = "/Messages/Paid/Videos"; + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + + if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) + { + List properties = new(); + string pattern = @"\{(.*?)\}"; + MatchCollection matches = Regex.Matches(filenameFormat, pattern); + foreach (Match match in matches) properties.Add(match.Groups[1].Value); + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, + fromUser, properties, folder.Split("/")[^1], users); + customFileName = await fileNameService.BuildFilename(filenameFormat, values); + } + + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { + if (!string.IsNullOrEmpty(customFileName) + ? !File.Exists(folder + path + "/" + customFileName + ".mp4") + : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, + authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + progressReporter, customFileName, filename, path); + + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : folder + path + "/" + filename + "_source.mp4").Length; + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + else + { + if (!string.IsNullOrEmpty(customFileName)) + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + filename + "_source" != customFileName) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; + string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; + if (!File.Exists(fullPathWithTheServerFileName)) return false; + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + size, true, lastModified); + } + + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + } + + return false; + } + 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); + } + } + + return false; + } + + + public async Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, + Post.Author? author, Dictionary users) + { + try + { + string customFileName = string.Empty; + string path; + Uri uri = new(url); + string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; + if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && + postInfo?.postedAt is not null) + path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + else + path = "/Posts/Free/Videos"; + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + + if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) + { + List properties = new(); + string pattern = @"\{(.*?)\}"; + MatchCollection matches = Regex.Matches(filenameFormat, pattern); + foreach (Match match in matches) properties.Add(match.Groups[1].Value); + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, + properties, folder.Split("/")[^1], users); + customFileName = await fileNameService.BuildFilename(filenameFormat, values); + } + + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { + if (!string.IsNullOrEmpty(customFileName) + ? !File.Exists(folder + path + "/" + customFileName + ".mp4") + : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, + authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + progressReporter, customFileName, filename, path); + + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : folder + path + "/" + filename + "_source.mp4").Length; + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + else + { + if (!string.IsNullOrEmpty(customFileName)) + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + filename + "_source" != customFileName) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; + string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; + if (!File.Exists(fullPathWithTheServerFileName)) return false; + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + size, true, lastModified); + } + + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + } + + return false; + } + 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); + } + } + + return false; + } + + public async Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, + SinglePost.Author author, Dictionary users) + { + try + { + string customFileName = string.Empty; + string path; + Uri uri = new(url); + string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; + if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && + postInfo?.postedAt is not null) + path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + else + path = "/Posts/Free/Videos"; + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + + if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) + { + List properties = new(); + string pattern = @"\{(.*?)\}"; + MatchCollection matches = Regex.Matches(filenameFormat, pattern); + foreach (Match match in matches) properties.Add(match.Groups[1].Value); + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, + properties, folder.Split("/")[^1], users); + customFileName = await fileNameService.BuildFilename(filenameFormat, values); + } + + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { + if (!string.IsNullOrEmpty(customFileName) + ? !File.Exists(folder + path + "/" + customFileName + ".mp4") + : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, + authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + progressReporter, customFileName, filename, path); + + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : folder + path + "/" + filename + "_source.mp4").Length; + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + else + { + if (!string.IsNullOrEmpty(customFileName)) + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + filename + "_source" != customFileName) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; + string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; + if (!File.Exists(fullPathWithTheServerFileName)) return false; + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + size, true, lastModified); + } + + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + } + + return false; + } + 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); + } + } + + return false; + } + + public async Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, + Streams.Medium? streamMedia, Streams.Author? author, Dictionary users) + { + try + { + string customFileName = string.Empty; + string path; + Uri uri = new(url); + string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; + if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.id is not null && + streamInfo?.postedAt is not null) + path = $"/Posts/Free/{streamInfo.id} {streamInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + else + path = "/Posts/Free/Videos"; + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + + if (!string.IsNullOrEmpty(filenameFormat) && streamInfo != null && streamMedia != null) + { + List properties = new(); + string pattern = @"\{(.*?)\}"; + MatchCollection matches = Regex.Matches(filenameFormat, pattern); + foreach (Match match in matches) properties.Add(match.Groups[1].Value); + Dictionary values = await fileNameService.GetFilename(streamInfo, streamMedia, author, + properties, folder.Split("/")[^1], users); + customFileName = await fileNameService.BuildFilename(filenameFormat, values); + } + + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { + if (!string.IsNullOrEmpty(customFileName) + ? !File.Exists(folder + path + "/" + customFileName + ".mp4") + : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, + authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + progressReporter, customFileName, filename, path); + + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : folder + path + "/" + filename + "_source.mp4").Length; + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + else + { + if (!string.IsNullOrEmpty(customFileName)) + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + filename + "_source" != customFileName) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; + string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; + if (!File.Exists(fullPathWithTheServerFileName)) return false; + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + size, true, lastModified); + } + + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + } + + return false; + } + 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); + } + } + + return false; + } + + public async Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, Medium? postMedia, + Purchased.FromUser? fromUser, Dictionary users) + { + try + { + string customFileName = string.Empty; + string path; + Uri uri = new(url); + string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; + if (configService.CurrentConfig.FolderPerPaidPost && postInfo != null && postInfo?.id is not null && + postInfo?.postedAt is not null) + path = $"/Posts/Paid/{postInfo.id} {postInfo.postedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + else + path = "/Posts/Paid/Videos"; + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + + + if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) + { + List properties = new(); + string pattern = @"\{(.*?)\}"; + MatchCollection matches = Regex.Matches(filenameFormat, pattern); + foreach (Match match in matches) properties.Add(match.Groups[1].Value); + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, fromUser, + properties, folder.Split("/")[^1], users); + customFileName = await fileNameService.BuildFilename(filenameFormat, values); + } + + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { + if (!string.IsNullOrEmpty(customFileName) + ? !File.Exists(folder + path + "/" + customFileName + ".mp4") + : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, + authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + progressReporter, customFileName, filename, path); + + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : folder + path + "/" + filename + "_source.mp4").Length; + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + else + { + if (!string.IsNullOrEmpty(customFileName)) + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + filename + "_source" != customFileName) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; + string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; + if (!File.Exists(fullPathWithTheServerFileName)) return false; + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + size, true, lastModified); + } + + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + } + + return false; + } + 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); + } + } + + return false; + } + + + public async Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Archived.List? postInfo, Archived.Medium? postMedia, + Archived.Author? author, Dictionary users) + { + try + { + string customFileName = string.Empty; + Uri uri = new(url); + string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; + string path = "/Archived/Posts/Free/Videos"; + if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + + if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) + { + List properties = new(); + string pattern = @"\{(.*?)\}"; + MatchCollection matches = Regex.Matches(filenameFormat, pattern); + foreach (Match match in matches) properties.Add(match.Groups[1].Value); + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, + properties, folder.Split("/")[^1], users); + customFileName = await fileNameService.BuildFilename(filenameFormat, values); + } + + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { + if (!string.IsNullOrEmpty(customFileName) + ? !File.Exists(folder + path + "/" + customFileName + ".mp4") + : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, + authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + progressReporter, customFileName, filename, path); + + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : folder + path + "/" + filename + "_source.mp4").Length; + if (configService.CurrentConfig.ShowScrapeSize) + progressReporter.ReportProgress(fileSizeInBytes); + else + progressReporter.ReportProgress(1); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + else + { + if (!string.IsNullOrEmpty(customFileName)) + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + filename + "_source" != customFileName) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; + string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; + if (!File.Exists(fullPathWithTheServerFileName)) return false; + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + size, true, lastModified); + } + + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + } + + return false; + } + 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); + } + } + + return false; + } + + #endregion + + #region Collection Download Methods + + public async Task DownloadHighlights(string username, long userId, string path, + HashSet paidPostIds, IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadHighlights - {username}"); + + Dictionary highlights = await apiService.GetMedia(MediaType.Highlights, + $"/users/{userId}/stories/highlights", null, path, paidPostIds.ToList()); + + if (highlights == null || highlights.Count == 0) + { + Log.Debug("Found 0 Highlights"); + return new DownloadResult + { + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Highlights", + Success = true + }; + } + + Log.Debug($"Found {highlights.Count} Highlights"); + + int oldHighlightsCount = 0; + int newHighlightsCount = 0; + + foreach (KeyValuePair highlightKVP in highlights) + { + bool isNew = + await DownloadStoryMedia(highlightKVP.Value, path, highlightKVP.Key, "Stories", progressReporter); + if (isNew) + newHighlightsCount++; + else + oldHighlightsCount++; + } + + Log.Debug( + $"Highlights Already Downloaded: {oldHighlightsCount} New Highlights Downloaded: {newHighlightsCount}"); + + return new DownloadResult + { + TotalCount = highlights.Count, + NewDownloads = newHighlightsCount, + ExistingDownloads = oldHighlightsCount, + MediaType = "Highlights", + Success = true + }; + } + + public async Task DownloadStories(string username, long userId, string path, + HashSet paidPostIds, IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadStories - {username}"); + + Dictionary stories = await apiService.GetMedia(MediaType.Stories, $"/users/{userId}/stories", + null, path, paidPostIds.ToList()); + + if (stories == null || stories.Count == 0) + { + Log.Debug("Found 0 Stories"); + return new DownloadResult + { + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Stories", + Success = true + }; + } + + Log.Debug($"Found {stories.Count} Stories"); + + int oldStoriesCount = 0; + int newStoriesCount = 0; + + foreach (KeyValuePair storyKVP in stories) + { + bool isNew = await DownloadStoryMedia(storyKVP.Value, path, storyKVP.Key, "Stories", progressReporter); + if (isNew) + newStoriesCount++; + else + oldStoriesCount++; + } + + Log.Debug($"Stories Already Downloaded: {oldStoriesCount} New Stories Downloaded: {newStoriesCount}"); + + return new DownloadResult + { + TotalCount = stories.Count, + NewDownloads = newStoriesCount, + ExistingDownloads = oldStoriesCount, + MediaType = "Stories", + Success = true + }; + } + + public async Task DownloadArchived(string username, long userId, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + ArchivedCollection archived, IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadArchived - {username}"); + + if (archived == null || archived.ArchivedPosts.Count == 0) + { + Log.Debug("Found 0 Archived Posts"); + return new DownloadResult + { + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Archived Posts", + Success = true + }; + } + + Log.Debug( + $"Found {archived.ArchivedPosts.Count} Media from {archived.ArchivedPostObjects.Count} Archived Posts"); + + int oldArchivedCount = 0; + int newArchivedCount = 0; + + foreach (KeyValuePair archivedKVP in archived.ArchivedPosts) + { + bool isNew; + if (archivedKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = archivedKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string postId = messageUrlParsed[5]; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + + if (pssh != null) + { + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", + "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", + pssh); + else + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", + pssh); + Archived.Medium? mediaInfo = + archived.ArchivedPostMedia.FirstOrDefault(m => m.id == archivedKVP.Key); + Archived.List? postInfo = + archived.ArchivedPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await DownloadArchivedPostDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + archivedKVP.Key, + "Posts", + progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? + string.Empty, + postInfo, + mediaInfo, + postInfo?.author, + users); + } + else + { + continue; + } + } + else + { + Archived.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.id == archivedKVP.Key); + Archived.List? postInfo = + archived.ArchivedPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await DownloadArchivedMedia( + archivedKVP.Value, + path, + archivedKVP.Key, + "Posts", + progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? + string.Empty, + postInfo, + mediaInfo, + postInfo?.author, + users); + } + + if (isNew) + newArchivedCount++; + else + oldArchivedCount++; + } + + Log.Debug( + $"Archived Posts Already Downloaded: {oldArchivedCount} New Archived Posts Downloaded: {newArchivedCount}"); + + return new DownloadResult + { + TotalCount = archived.ArchivedPosts.Count, + NewDownloads = newArchivedCount, + ExistingDownloads = oldArchivedCount, + MediaType = "Archived Posts", + Success = true + }; + } + + public async Task DownloadMessages(string username, long userId, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + MessageCollection messages, IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadMessages - {username}"); + + if (messages == null || messages.Messages.Count == 0) + { + Log.Debug("Found 0 Messages"); + return new DownloadResult + { + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Messages", + Success = true + }; + } + + Log.Debug($"Found {messages.Messages.Count} Media from {messages.MessageObjects.Count} Messages"); + + int oldMessagesCount = 0; + int newMessagesCount = 0; + + foreach (KeyValuePair messageKVP in messages.Messages) + { + bool isNew; + if (messageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = messageKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string messageId = messageUrlParsed[5]; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + + if (pssh != null) + { + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", + "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + else + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); + List? messageInfo = + messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await DownloadMessageDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + messageKVP.Key, + "Messages", + progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? + string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + users); + } + else + { + continue; + } + } + else + { + Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); + List? messageInfo = messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await DownloadMessageMedia( + messageKVP.Value, + path, + messageKVP.Key, + "Messages", + progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? + string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + users); + } + + if (isNew) + newMessagesCount++; + else + oldMessagesCount++; + } + + Log.Debug($"Messages Already Downloaded: {oldMessagesCount} New Messages Downloaded: {newMessagesCount}"); + + return new DownloadResult + { + TotalCount = messages.Messages.Count, + NewDownloads = newMessagesCount, + ExistingDownloads = oldMessagesCount, + MediaType = "Messages", + Success = true + }; + } + + + public async Task DownloadPaidMessages(string username, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidMessageCollection paidMessageCollection, + IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadPaidMessages - {username}"); + + if (paidMessageCollection == null || paidMessageCollection.PaidMessages.Count == 0) + { + Log.Debug("Found 0 Paid Messages"); + return new DownloadResult + { + TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Paid Messages", Success = true + }; + } + + Log.Debug( + $"Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages"); + + int oldCount = 0; + int newCount = 0; + + foreach (KeyValuePair kvp in paidMessageCollection.PaidMessages) + { + bool isNew; + if (kvp.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] parsed = kvp.Value.Split(','); + string? pssh = await apiService.GetDRMMPDPSSH(parsed[0], parsed[1], parsed[2], parsed[3]); + if (pssh != null) + { + DateTime lastModified = + await apiService.GetDRMMPDLastModified(parsed[0], parsed[1], parsed[2], parsed[3]); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{parsed[4]}/drm/message/{parsed[5]}", + "?type=widevine"); + string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing + ? await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/message/{parsed[5]}?type=widevine", + pssh) + : await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/message/{parsed[5]}?type=widevine", + pssh); + + Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == kvp.Key); + Purchased.List? messageInfo = + paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); + + isNew = await DownloadPurchasedMessageDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + decryptionKey, path, lastModified, kvp.Key, "Messages", progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? + string.Empty, messageInfo, mediaInfo, messageInfo?.fromUser, users); + } + else + { + continue; + } + } + else + { + Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == kvp.Key); + Purchased.List? messageInfo = + paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + isNew = await DownloadPurchasedMedia(kvp.Value, path, kvp.Key, "Messages", progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? + string.Empty, messageInfo, mediaInfo, messageInfo?.fromUser, users); + } + + if (isNew) newCount++; + else oldCount++; + } + + Log.Debug($"Paid Messages Already Downloaded: {oldCount} New Paid Messages Downloaded: {newCount}"); + return new DownloadResult + { + TotalCount = paidMessageCollection.PaidMessages.Count, NewDownloads = newCount, + ExistingDownloads = oldCount, MediaType = "Paid Messages", Success = true + }; + } + + public async Task DownloadStreams(string username, long userId, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + StreamsCollection streams, IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadStreams - {username}"); + + if (streams == null || streams.Streams.Count == 0) + { + Log.Debug("Found 0 Streams"); + return new DownloadResult + { TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Streams", Success = true }; + } + + Log.Debug($"Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams"); + + int oldCount = 0; + int newCount = 0; + + foreach (KeyValuePair kvp in streams.Streams) + { + bool isNew; + if (kvp.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] parsed = kvp.Value.Split(','); + string? pssh = await apiService.GetDRMMPDPSSH(parsed[0], parsed[1], parsed[2], parsed[3]); + if (pssh != null) + { + DateTime lastModified = + await apiService.GetDRMMPDLastModified(parsed[0], parsed[1], parsed[2], parsed[3]); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}", + "?type=widevine"); + string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing + ? await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", + pssh) + : await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", + pssh); + + Streams.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.id == kvp.Key); + Streams.List? streamInfo = + streams.StreamObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await DownloadStreamsDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, + path, lastModified, kvp.Key, "Streams", progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? + string.Empty, streamInfo, mediaInfo, streamInfo?.author, users); + } + else + { + continue; + } + } + else + { + Streams.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.id == kvp.Key); + Streams.List? streamInfo = + streams.StreamObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + isNew = await DownloadStreamMedia(kvp.Value, path, kvp.Key, "Streams", progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? + string.Empty, streamInfo, mediaInfo, streamInfo?.author, users); + } + + if (isNew) newCount++; + else oldCount++; + } + + Log.Debug($"Streams Already Downloaded: {oldCount} New Streams Downloaded: {newCount}"); + return new DownloadResult + { + TotalCount = streams.Streams.Count, NewDownloads = newCount, ExistingDownloads = oldCount, + MediaType = "Streams", Success = true + }; + } + +// Add these methods to DownloadService.cs before the #endregion line + + public async Task DownloadFreePosts(string username, long userId, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostCollection posts, + IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadFreePosts - {username}"); + + if (posts == null || posts.Posts.Count == 0) + { + Log.Debug("Found 0 Posts"); + return new DownloadResult + { TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Posts", Success = true }; + } + + Log.Debug($"Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts"); + + int oldCount = 0, newCount = 0; + + foreach (KeyValuePair postKVP in posts.Posts) + { + bool isNew; + if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] parsed = postKVP.Value.Split(','); + string? pssh = await apiService.GetDRMMPDPSSH(parsed[0], parsed[1], parsed[2], parsed[3]); + if (pssh != null) + { + DateTime lastModified = + await apiService.GetDRMMPDLastModified(parsed[0], parsed[1], parsed[2], parsed[3]); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}", + "?type=widevine"); + string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing + ? await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", + pssh) + : await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", + pssh); + + Post.Medium mediaInfo = posts.PostMedia.FirstOrDefault(m => m.id == postKVP.Key); + Post.List postInfo = posts.PostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await DownloadPostDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, + lastModified, postKVP.Key, "Posts", progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? + string.Empty, postInfo, mediaInfo, postInfo?.author, users); + } + else + { + continue; + } + } + else + { + Post.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m?.id == postKVP.Key); + Post.List? postInfo = posts.PostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + isNew = await DownloadPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? + string.Empty, postInfo, mediaInfo, postInfo?.author, users); + } + + if (isNew) newCount++; + else oldCount++; + } + + Log.Debug($"Posts Already Downloaded: {oldCount} New Posts Downloaded: {newCount}"); + return new DownloadResult + { + TotalCount = posts.Posts.Count, NewDownloads = newCount, ExistingDownloads = oldCount, MediaType = "Posts", + Success = true + }; + } + + public async Task DownloadPaidPosts(string username, long userId, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PaidPostCollection purchasedPosts, IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadPaidPosts - {username}"); + + if (purchasedPosts == null || purchasedPosts.PaidPosts.Count == 0) + { + Log.Debug("Found 0 Paid Posts"); + return new DownloadResult + { TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Paid Posts", Success = true }; + } + + Log.Debug( + $"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); + + int oldCount = 0, newCount = 0; + + foreach (KeyValuePair postKVP in purchasedPosts.PaidPosts) + { + bool isNew; + if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] parsed = postKVP.Value.Split(','); + string? pssh = await apiService.GetDRMMPDPSSH(parsed[0], parsed[1], parsed[2], parsed[3]); + if (pssh != null) + { + DateTime lastModified = + await apiService.GetDRMMPDLastModified(parsed[0], parsed[1], parsed[2], parsed[3]); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}", + "?type=widevine"); + string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing + ? await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", + pssh) + : await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", + pssh); + + Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); + Purchased.List? postInfo = + purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await DownloadPurchasedPostDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + decryptionKey, path, lastModified, postKVP.Key, "Posts", progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? + string.Empty, postInfo, mediaInfo, postInfo?.fromUser, users); + } + else + { + continue; + } + } + else + { + Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); + Purchased.List? postInfo = + purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + isNew = await DownloadPurchasedPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? + string.Empty, postInfo, mediaInfo, postInfo?.fromUser, users); + } + + if (isNew) newCount++; + else oldCount++; + } + + Log.Debug($"Paid Posts Already Downloaded: {oldCount} New Paid Posts Downloaded: {newCount}"); + return new DownloadResult + { + TotalCount = purchasedPosts.PaidPosts.Count, NewDownloads = newCount, ExistingDownloads = oldCount, + MediaType = "Paid Posts", Success = true + }; + } + + #endregion +} diff --git a/OF DL/Helpers/FileNameHelper.cs b/OF DL/Services/FileNameService.cs similarity index 93% rename from OF DL/Helpers/FileNameHelper.cs rename to OF DL/Services/FileNameService.cs index 178a4e8..584df74 100644 --- a/OF DL/Helpers/FileNameHelper.cs +++ b/OF DL/Services/FileNameService.cs @@ -1,23 +1,10 @@ using HtmlAgilityPack; -using OF_DL.Entities; -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; -namespace OF_DL.Helpers +namespace OF_DL.Services { - public class FileNameHelper : IFileNameHelper + public class FileNameService(IAuthService authService) : IFileNameService { - private readonly Auth auth; - - public FileNameHelper(Auth auth) - { - this.auth = auth; - } - public async Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, string username, Dictionary users = null) { Dictionary values = new(); @@ -43,7 +30,7 @@ namespace OF_DL.Helpers object policy = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontPolicy"); object signature = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontSignature"); object kvp = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontKeyPairId"); - DateTime lastModified = await DownloadHelper.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), auth); + DateTime lastModified = await DownloadService.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), authService.CurrentAuth); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); continue; } @@ -52,7 +39,7 @@ namespace OF_DL.Helpers object source = GetNestedPropertyValue(obj2, "files.full.url"); if(source != null) { - DateTime lastModified = await DownloadHelper.GetMediaLastModified(source.ToString()); + DateTime lastModified = await DownloadService.GetMediaLastModified(source.ToString()); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); continue; } @@ -61,7 +48,7 @@ namespace OF_DL.Helpers object preview = GetNestedPropertyValue(obj2, "preview"); if(preview != null) { - DateTime lastModified = await DownloadHelper.GetMediaLastModified(preview.ToString()); + DateTime lastModified = await DownloadService.GetMediaLastModified(preview.ToString()); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); continue; } diff --git a/OF DL/Helpers/Interfaces/IAPIHelper.cs b/OF DL/Services/IAPIService.cs similarity index 60% rename from OF DL/Helpers/Interfaces/IAPIHelper.cs rename to OF DL/Services/IAPIService.cs index a917fff..e3959ea 100644 --- a/OF DL/Helpers/Interfaces/IAPIHelper.cs +++ b/OF DL/Services/IAPIService.cs @@ -8,32 +8,32 @@ using OF_DL.Entities.Streams; using OF_DL.Enumurations; using Spectre.Console; -namespace OF_DL.Helpers +namespace OF_DL.Services { - public interface IAPIHelper + public interface IAPIService { Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, string pssh); Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, string pssh); Task GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp); Task GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp); - Task> GetLists(string endpoint, IDownloadConfig config); - Task> GetListUsers(string endpoint, IDownloadConfig config); - Task> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, IDownloadConfig config, List paid_post_ids); - Task GetPaidPosts(string endpoint, string folder, string username, IDownloadConfig config, List paid_post_ids, StatusContext ctx); - Task GetPosts(string endpoint, string folder, IDownloadConfig config, List paid_post_ids, StatusContext ctx); - Task GetPost(string endpoint, string folder, IDownloadConfig config); - Task GetStreams(string endpoint, string folder, IDownloadConfig config, List paid_post_ids, StatusContext ctx); - Task GetArchived(string endpoint, string folder, IDownloadConfig config, StatusContext ctx); - Task GetMessages(string endpoint, string folder, IDownloadConfig config, StatusContext ctx); - Task GetPaidMessages(string endpoint, string folder, string username, IDownloadConfig config, StatusContext ctx); - Task GetPaidMessage(string endpoint, string folder, IDownloadConfig config); - Task> GetPurchasedTabUsers(string endpoint, IDownloadConfig config, Dictionary users); - Task> GetPurchasedTab(string endpoint, string folder, IDownloadConfig config, Dictionary users); + Task> GetLists(string endpoint); + Task> GetListUsers(string endpoint); + Task> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, List paid_post_ids); + Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, StatusContext ctx); + Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx); + Task GetPost(string endpoint, string folder); + Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx); + Task GetArchived(string endpoint, string folder, StatusContext ctx); + Task GetMessages(string endpoint, string folder, StatusContext ctx); + Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx); + Task GetPaidMessage(string endpoint, string folder); + Task> GetPurchasedTabUsers(string endpoint, Dictionary users); + Task> GetPurchasedTab(string endpoint, string folder, Dictionary users); Task GetUserInfo(string endpoint); Task GetUserInfoById(string endpoint); Dictionary GetDynamicHeaders(string path, string queryParam); - Task> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions, IDownloadConfig config); - Task> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions, IDownloadConfig config); + Task> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions); + Task> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions); Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, string pssh); } } diff --git a/OF DL/Services/IConfigService.cs b/OF DL/Services/IConfigService.cs index 773e82c..57c639a 100644 --- a/OF DL/Services/IConfigService.cs +++ b/OF DL/Services/IConfigService.cs @@ -4,7 +4,7 @@ namespace OF_DL.Services { public interface IConfigService { - Config? CurrentConfig { get; } + Config CurrentConfig { get; } bool IsCliNonInteractive { get; } Task LoadConfigurationAsync(string[] args); Task SaveConfigurationAsync(string filePath = "config.conf"); diff --git a/OF DL/Helpers/Interfaces/IDBHelper.cs b/OF DL/Services/IDBService.cs similarity index 95% rename from OF DL/Helpers/Interfaces/IDBHelper.cs rename to OF DL/Services/IDBService.cs index ab55249..5a32c03 100644 --- a/OF DL/Helpers/Interfaces/IDBHelper.cs +++ b/OF DL/Services/IDBService.cs @@ -1,6 +1,6 @@ -namespace OF_DL.Helpers +namespace OF_DL.Services { - public interface IDBHelper + public interface IDBService { Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at, long user_id); Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at); diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs new file mode 100644 index 0000000..696eaee --- /dev/null +++ b/OF DL/Services/IDownloadService.cs @@ -0,0 +1,50 @@ +using OF_DL.Entities; +using OF_DL.Entities.Archived; +using OF_DL.Entities.Messages; +using OF_DL.Entities.Post; +using OF_DL.Entities.Purchased; +using OF_DL.Entities.Streams; +using static OF_DL.Entities.Messages.Messages; +using FromUser = OF_DL.Entities.Messages.FromUser; + +namespace OF_DL.Services +{ + public interface IDownloadService + { + Task CalculateTotalFileSize(List urls); + Task ProcessMediaDownload(string folder, long media_id, string api_type, string url, string path, string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); + Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Archived.List? messageInfo, Archived.Medium? messageMedia, Archived.Author? author, Dictionary users); + Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Archived.List? postInfo, Archived.Medium? postMedia, Archived.Author? author, Dictionary users); + Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, Dictionary users); + Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary users); + Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username); + Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Messages.List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users); + Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Messages.List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users); + Task DownloadPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary users); + Task DownloadPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, SinglePost.Author? author, Dictionary users); + Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); + Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary users); + Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); + Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary users); + Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, Medium? postMedia, Purchased.FromUser? fromUser, Dictionary users); + Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); + Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter); + Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary users); + Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary users); + Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users); + Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users); + Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); + Task DownloadStories(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); + Task DownloadArchived(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedCollection archived, IProgressReporter progressReporter); + Task DownloadMessages(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageCollection messages, IProgressReporter progressReporter); + Task DownloadPaidMessages(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter); + Task DownloadStreams(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamsCollection streams, IProgressReporter progressReporter); + Task DownloadFreePosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostCollection posts, IProgressReporter progressReporter); + Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidPostCollection purchasedPosts, IProgressReporter progressReporter); + } +} diff --git a/OF DL/Services/IFileNameService.cs b/OF DL/Services/IFileNameService.cs new file mode 100644 index 0000000..d692f30 --- /dev/null +++ b/OF DL/Services/IFileNameService.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Services +{ + public interface IFileNameService + { + Task BuildFilename(string fileFormat, Dictionary values); + Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, string username, Dictionary users = null); + } +} diff --git a/OF DL/Services/IProgressReporter.cs b/OF DL/Services/IProgressReporter.cs new file mode 100644 index 0000000..d312059 --- /dev/null +++ b/OF DL/Services/IProgressReporter.cs @@ -0,0 +1,20 @@ +namespace OF_DL.Services; + +/// +/// Interface for reporting download progress in a UI-agnostic way. +/// This allows the download service to report progress without being coupled to any specific UI framework. +/// +public interface IProgressReporter +{ + /// + /// Reports progress increment. The value represents either bytes downloaded or file count depending on configuration. + /// + /// The amount to increment progress by + void ReportProgress(long increment); + + /// + /// Reports a status message (optional for implementations). + /// + /// The status message to report + void ReportStatus(string message); +} diff --git a/OF DL/CDMApi.cs b/OF DL/Widevine/CDMApi.cs similarity index 89% rename from OF DL/CDMApi.cs rename to OF DL/Widevine/CDMApi.cs index e01eae2..662377b 100644 --- a/OF DL/CDMApi.cs +++ b/OF DL/Widevine/CDMApi.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; +using WidevineClient.Widevine; -namespace WidevineClient.Widevine +namespace OF_DL.Widevine { public class CDMApi { From 7f1cd03f2f799ec0c21a4539987dce9772cf5dae Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Fri, 6 Feb 2026 01:28:54 -0600 Subject: [PATCH 05/55] Update .editorconfig with additional rules from dotnet/runtime --- .editorconfig | 186 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index b3672fd..62adc2c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,193 @@ -# Editor configuration, see https://editorconfig.org +# editorconfig.org + +# top-most EditorConfig file root = true +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation [*] charset = utf-8 indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true + +[project.json] +indent_size = 2 + +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs}] +generated_code = true + +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# License header +file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +[*.{csproj,vbproj,proj,nativeproj,locproj}] +charset = utf-8 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd,bat}] +end_of_line = crlf From 86ee476dc5f123a73154e60b0df71e5a8aa1c584 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Fri, 6 Feb 2026 01:31:43 -0600 Subject: [PATCH 06/55] Remove unused imports --- OF DL/Crypto/Padding.cs | 5 +---- OF DL/Entities/Archived/Archived.cs | 5 ----- OF DL/Entities/Archived/ArchivedCollection.cs | 6 ------ OF DL/Entities/Auth.cs | 5 ----- OF DL/Entities/CDRMProjectRequest.cs | 5 ----- OF DL/Entities/Highlights/HighlightMedia.cs | 5 ----- OF DL/Entities/Highlights/Highlights.cs | 6 ------ OF DL/Entities/Lists/UserList.cs | 8 +------- OF DL/Entities/Lists/UsersList.cs | 6 ------ OF DL/Entities/Messages/MessageCollection.cs | 6 ------ OF DL/Entities/Messages/Messages.cs | 5 ----- OF DL/Entities/Messages/SingleMessage.cs | 1 - OF DL/Entities/OFDLRequest.cs | 5 ----- OF DL/Entities/Post/Post.cs | 6 ------ OF DL/Entities/Post/PostCollection.cs | 6 ------ OF DL/Entities/Post/SinglePost.cs | 6 ------ OF DL/Entities/Post/SinglePostCollection.cs | 6 ------ OF DL/Entities/Purchased/PaidMessageCollection.cs | 5 ----- OF DL/Entities/Purchased/Purchased.cs | 5 ----- OF DL/Entities/Purchased/PurchasedTabCollection.cs | 6 ------ OF DL/Entities/Purchased/SinglePaidMessageCollection.cs | 6 ------ OF DL/Entities/ShortDateConverter.cs | 1 - OF DL/Entities/Stories/Stories.cs | 5 ----- OF DL/Entities/Streams/Streams.cs | 5 ----- OF DL/Entities/Streams/StreamsCollection.cs | 6 ------ OF DL/Entities/Subscriptions.cs | 6 ------ OF DL/Entities/User.cs | 6 ------ OF DL/Enumerations/CustomFileNameOption.cs | 6 ------ OF DL/Enumerations/DownloadDateSelection.cs | 6 ------ OF DL/Enumerations/LoggingLevel.cs | 6 ------ OF DL/Enumerations/MediaType.cs | 8 +------- OF DL/Enumerations/VideoResolution.cs | 6 ------ OF DL/Helpers/{Interfaces => }/IFileNameHelper.cs | 0 OF DL/Program.cs | 2 +- OF DL/Services/APIService.cs | 4 +--- OF DL/Services/DownloadService.cs | 1 - OF DL/Services/IAPIService.cs | 2 +- OF DL/{ => Utils}/HttpUtil.cs | 3 --- OF DL/Utils/ThrottledStream.cs | 6 ------ OF DL/Utils/XmlUtils.cs | 5 ----- OF DL/Widevine/CDM.cs | 6 +----- OF DL/Widevine/CDMDevice.cs | 7 ++----- OF DL/Widevine/ContentKey.cs | 5 +---- OF DL/Widevine/PSSHBox.cs | 6 +----- OF DL/Widevine/Session.cs | 6 ++---- 45 files changed, 13 insertions(+), 215 deletions(-) rename OF DL/Helpers/{Interfaces => }/IFileNameHelper.cs (100%) rename OF DL/{ => Utils}/HttpUtil.cs (98%) diff --git a/OF DL/Crypto/Padding.cs b/OF DL/Crypto/Padding.cs index 4701c1c..34b5ec7 100644 --- a/OF DL/Crypto/Padding.cs +++ b/OF DL/Crypto/Padding.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; +using System.Security.Cryptography; namespace WidevineClient.Crypto { diff --git a/OF DL/Entities/Archived/Archived.cs b/OF DL/Entities/Archived/Archived.cs index 7f132e2..1231e1c 100644 --- a/OF DL/Entities/Archived/Archived.cs +++ b/OF DL/Entities/Archived/Archived.cs @@ -1,10 +1,5 @@ using Newtonsoft.Json; using OF_DL.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OF_DL.Entities.Archived { diff --git a/OF DL/Entities/Archived/ArchivedCollection.cs b/OF DL/Entities/Archived/ArchivedCollection.cs index 2eb56fb..82a1ab3 100644 --- a/OF DL/Entities/Archived/ArchivedCollection.cs +++ b/OF DL/Entities/Archived/ArchivedCollection.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Entities.Archived { public class ArchivedCollection diff --git a/OF DL/Entities/Auth.cs b/OF DL/Entities/Auth.cs index 86d9f83..e2ea9ff 100644 --- a/OF DL/Entities/Auth.cs +++ b/OF DL/Entities/Auth.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; namespace OF_DL.Entities diff --git a/OF DL/Entities/CDRMProjectRequest.cs b/OF DL/Entities/CDRMProjectRequest.cs index b48279b..69be2be 100644 --- a/OF DL/Entities/CDRMProjectRequest.cs +++ b/OF DL/Entities/CDRMProjectRequest.cs @@ -1,9 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OF_DL.Entities { diff --git a/OF DL/Entities/Highlights/HighlightMedia.cs b/OF DL/Entities/Highlights/HighlightMedia.cs index af51136..12b4ccf 100644 --- a/OF DL/Entities/Highlights/HighlightMedia.cs +++ b/OF DL/Entities/Highlights/HighlightMedia.cs @@ -1,9 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OF_DL.Entities.Highlights { diff --git a/OF DL/Entities/Highlights/Highlights.cs b/OF DL/Entities/Highlights/Highlights.cs index fbcd4dc..8f3df8a 100644 --- a/OF DL/Entities/Highlights/Highlights.cs +++ b/OF DL/Entities/Highlights/Highlights.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Entities.Highlights { public class Highlights diff --git a/OF DL/Entities/Lists/UserList.cs b/OF DL/Entities/Lists/UserList.cs index cc42563..28a60d6 100644 --- a/OF DL/Entities/Lists/UserList.cs +++ b/OF DL/Entities/Lists/UserList.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OF_DL.Entities.Lists +namespace OF_DL.Entities.Lists { public class UserList { diff --git a/OF DL/Entities/Lists/UsersList.cs b/OF DL/Entities/Lists/UsersList.cs index 010db51..acd4c2a 100644 --- a/OF DL/Entities/Lists/UsersList.cs +++ b/OF DL/Entities/Lists/UsersList.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Entities.Lists { public class UsersList diff --git a/OF DL/Entities/Messages/MessageCollection.cs b/OF DL/Entities/Messages/MessageCollection.cs index dd01461..4766fc1 100644 --- a/OF DL/Entities/Messages/MessageCollection.cs +++ b/OF DL/Entities/Messages/MessageCollection.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Entities.Messages { public class MessageCollection diff --git a/OF DL/Entities/Messages/Messages.cs b/OF DL/Entities/Messages/Messages.cs index c211368..0809d57 100644 --- a/OF DL/Entities/Messages/Messages.cs +++ b/OF DL/Entities/Messages/Messages.cs @@ -1,9 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OF_DL.Entities.Messages { diff --git a/OF DL/Entities/Messages/SingleMessage.cs b/OF DL/Entities/Messages/SingleMessage.cs index 2e013f4..8b7be53 100644 --- a/OF DL/Entities/Messages/SingleMessage.cs +++ b/OF DL/Entities/Messages/SingleMessage.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json; using static OF_DL.Entities.Messages.Messages; namespace OF_DL.Entities.Messages diff --git a/OF DL/Entities/OFDLRequest.cs b/OF DL/Entities/OFDLRequest.cs index 56060b9..718fac4 100644 --- a/OF DL/Entities/OFDLRequest.cs +++ b/OF DL/Entities/OFDLRequest.cs @@ -1,9 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OF_DL.Entities { diff --git a/OF DL/Entities/Post/Post.cs b/OF DL/Entities/Post/Post.cs index d7b661b..13613fd 100644 --- a/OF DL/Entities/Post/Post.cs +++ b/OF DL/Entities/Post/Post.cs @@ -1,11 +1,5 @@ using Newtonsoft.Json; using OF_DL.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static OF_DL.Entities.Messages.Messages; namespace OF_DL.Entities.Post; diff --git a/OF DL/Entities/Post/PostCollection.cs b/OF DL/Entities/Post/PostCollection.cs index 1ed0478..bc2acbf 100644 --- a/OF DL/Entities/Post/PostCollection.cs +++ b/OF DL/Entities/Post/PostCollection.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Entities.Post { public class PostCollection diff --git a/OF DL/Entities/Post/SinglePost.cs b/OF DL/Entities/Post/SinglePost.cs index ef37c40..909e3e8 100644 --- a/OF DL/Entities/Post/SinglePost.cs +++ b/OF DL/Entities/Post/SinglePost.cs @@ -1,11 +1,5 @@ using Newtonsoft.Json; using OF_DL.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static OF_DL.Entities.Post.Post; namespace OF_DL.Entities.Post { diff --git a/OF DL/Entities/Post/SinglePostCollection.cs b/OF DL/Entities/Post/SinglePostCollection.cs index aceedb5..6d374e2 100644 --- a/OF DL/Entities/Post/SinglePostCollection.cs +++ b/OF DL/Entities/Post/SinglePostCollection.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Entities.Post { public class SinglePostCollection diff --git a/OF DL/Entities/Purchased/PaidMessageCollection.cs b/OF DL/Entities/Purchased/PaidMessageCollection.cs index 1222ba0..09111eb 100644 --- a/OF DL/Entities/Purchased/PaidMessageCollection.cs +++ b/OF DL/Entities/Purchased/PaidMessageCollection.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using static OF_DL.Entities.Messages.Messages; namespace OF_DL.Entities.Purchased diff --git a/OF DL/Entities/Purchased/Purchased.cs b/OF DL/Entities/Purchased/Purchased.cs index 64ae348..2e0c087 100644 --- a/OF DL/Entities/Purchased/Purchased.cs +++ b/OF DL/Entities/Purchased/Purchased.cs @@ -1,9 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using static OF_DL.Entities.Messages.Messages; namespace OF_DL.Entities.Purchased diff --git a/OF DL/Entities/Purchased/PurchasedTabCollection.cs b/OF DL/Entities/Purchased/PurchasedTabCollection.cs index f3af333..822c0dd 100644 --- a/OF DL/Entities/Purchased/PurchasedTabCollection.cs +++ b/OF DL/Entities/Purchased/PurchasedTabCollection.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Entities.Purchased { public class PurchasedTabCollection diff --git a/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs b/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs index 625b6ff..ba9515e 100644 --- a/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs +++ b/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs @@ -1,10 +1,4 @@ using OF_DL.Entities.Messages; -using OF_DL.Entities.Post; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using static OF_DL.Entities.Messages.Messages; namespace OF_DL.Entities.Purchased diff --git a/OF DL/Entities/ShortDateConverter.cs b/OF DL/Entities/ShortDateConverter.cs index 2ba2cbd..93278f3 100644 --- a/OF DL/Entities/ShortDateConverter.cs +++ b/OF DL/Entities/ShortDateConverter.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json.Converters; -using System.Runtime.Serialization; namespace OF_DL.Entities { diff --git a/OF DL/Entities/Stories/Stories.cs b/OF DL/Entities/Stories/Stories.cs index 1f2efcd..90f0291 100644 --- a/OF DL/Entities/Stories/Stories.cs +++ b/OF DL/Entities/Stories/Stories.cs @@ -1,9 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OF_DL.Entities.Stories { diff --git a/OF DL/Entities/Streams/Streams.cs b/OF DL/Entities/Streams/Streams.cs index 2ad51e3..13bc4b1 100644 --- a/OF DL/Entities/Streams/Streams.cs +++ b/OF DL/Entities/Streams/Streams.cs @@ -1,10 +1,5 @@ using Newtonsoft.Json; using OF_DL.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OF_DL.Entities.Streams { diff --git a/OF DL/Entities/Streams/StreamsCollection.cs b/OF DL/Entities/Streams/StreamsCollection.cs index 247a13b..c9a76f2 100644 --- a/OF DL/Entities/Streams/StreamsCollection.cs +++ b/OF DL/Entities/Streams/StreamsCollection.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Entities.Streams { public class StreamsCollection diff --git a/OF DL/Entities/Subscriptions.cs b/OF DL/Entities/Subscriptions.cs index f5bacdd..b5da541 100644 --- a/OF DL/Entities/Subscriptions.cs +++ b/OF DL/Entities/Subscriptions.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Entities { public class Subscriptions diff --git a/OF DL/Entities/User.cs b/OF DL/Entities/User.cs index e5dae37..1273c2b 100644 --- a/OF DL/Entities/User.cs +++ b/OF DL/Entities/User.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Entities { public class User diff --git a/OF DL/Enumerations/CustomFileNameOption.cs b/OF DL/Enumerations/CustomFileNameOption.cs index eecd762..4e9659f 100644 --- a/OF DL/Enumerations/CustomFileNameOption.cs +++ b/OF DL/Enumerations/CustomFileNameOption.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Enumerations; public enum CustomFileNameOption diff --git a/OF DL/Enumerations/DownloadDateSelection.cs b/OF DL/Enumerations/DownloadDateSelection.cs index 926c7bd..e40c38f 100644 --- a/OF DL/Enumerations/DownloadDateSelection.cs +++ b/OF DL/Enumerations/DownloadDateSelection.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Enumerations { public enum DownloadDateSelection diff --git a/OF DL/Enumerations/LoggingLevel.cs b/OF DL/Enumerations/LoggingLevel.cs index 6757262..876c527 100644 --- a/OF DL/Enumerations/LoggingLevel.cs +++ b/OF DL/Enumerations/LoggingLevel.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Enumerations { public enum LoggingLevel diff --git a/OF DL/Enumerations/MediaType.cs b/OF DL/Enumerations/MediaType.cs index 86078e8..38e3e81 100644 --- a/OF DL/Enumerations/MediaType.cs +++ b/OF DL/Enumerations/MediaType.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OF_DL.Enumurations +namespace OF_DL.Enumerations { public enum MediaType { diff --git a/OF DL/Enumerations/VideoResolution.cs b/OF DL/Enumerations/VideoResolution.cs index 2514dee..6c2f340 100644 --- a/OF DL/Enumerations/VideoResolution.cs +++ b/OF DL/Enumerations/VideoResolution.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace OF_DL.Enumerations { public enum VideoResolution diff --git a/OF DL/Helpers/Interfaces/IFileNameHelper.cs b/OF DL/Helpers/IFileNameHelper.cs similarity index 100% rename from OF DL/Helpers/Interfaces/IFileNameHelper.cs rename to OF DL/Helpers/IFileNameHelper.cs diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 041900c..6cc38a0 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -7,7 +7,6 @@ using OF_DL.Entities.Post; using OF_DL.Entities.Purchased; using OF_DL.Entities.Streams; using OF_DL.Enumerations; -using OF_DL.Enumurations; using OF_DL.CLI; using OF_DL.Helpers; using OF_DL.Services; @@ -18,6 +17,7 @@ using System.Runtime.InteropServices; using System.Text.RegularExpressions; using static OF_DL.Entities.Messages.Messages; using Microsoft.Extensions.DependencyInjection; +using Constants = OF_DL.Widevine.Constants; namespace OF_DL; diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 3d9517e..00a4870 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -10,7 +10,6 @@ using OF_DL.Entities.Purchased; using OF_DL.Entities.Stories; using OF_DL.Entities.Streams; using OF_DL.Enumerations; -using OF_DL.Enumurations; using Serilog; using Spectre.Console; using System.Globalization; @@ -19,8 +18,7 @@ using System.Text; using System.Text.Json; using System.Xml.Linq; using OF_DL.Widevine; -using WidevineClient.Widevine; -using static WidevineClient.HttpUtil; +using static OF_DL.Utils.HttpUtil; using Constants = OF_DL.Helpers.Constants; namespace OF_DL.Services; diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 9fb0403..113be78 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -10,7 +10,6 @@ using OF_DL.Entities.Post; using OF_DL.Entities.Purchased; using OF_DL.Entities.Streams; using OF_DL.Enumerations; -using OF_DL.Enumurations; using OF_DL.Utils; using Serilog; using Serilog.Events; diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index e3959ea..a24c088 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -5,7 +5,7 @@ using OF_DL.Entities.Messages; using OF_DL.Entities.Post; using OF_DL.Entities.Purchased; using OF_DL.Entities.Streams; -using OF_DL.Enumurations; +using OF_DL.Enumerations; using Spectre.Console; namespace OF_DL.Services diff --git a/OF DL/HttpUtil.cs b/OF DL/Utils/HttpUtil.cs similarity index 98% rename from OF DL/HttpUtil.cs rename to OF DL/Utils/HttpUtil.cs index 25f0ebe..89f3ec1 100644 --- a/OF DL/HttpUtil.cs +++ b/OF DL/Utils/HttpUtil.cs @@ -1,7 +1,4 @@ using OF_DL.Helpers; -using System; -using System.Collections.Generic; -using System.Net.Http; using System.Text; namespace WidevineClient diff --git a/OF DL/Utils/ThrottledStream.cs b/OF DL/Utils/ThrottledStream.cs index 07b8857..b52b413 100644 --- a/OF DL/Utils/ThrottledStream.cs +++ b/OF DL/Utils/ThrottledStream.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Reactive.Concurrency; -using System.Text; -using System.Threading.Tasks; namespace OF_DL.Utils; diff --git a/OF DL/Utils/XmlUtils.cs b/OF DL/Utils/XmlUtils.cs index 55a68c6..940ebef 100644 --- a/OF DL/Utils/XmlUtils.cs +++ b/OF DL/Utils/XmlUtils.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Xml.Linq; namespace OF_DL.Utils diff --git a/OF DL/Widevine/CDM.cs b/OF DL/Widevine/CDM.cs index bfb2841..6b5f6fc 100644 --- a/OF DL/Widevine/CDM.cs +++ b/OF DL/Widevine/CDM.cs @@ -1,11 +1,7 @@ -using ProtoBuf; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; using System.Security.Cryptography; using System.Text; +using ProtoBuf; using WidevineClient.Crypto; namespace WidevineClient.Widevine diff --git a/OF DL/Widevine/CDMDevice.cs b/OF DL/Widevine/CDMDevice.cs index 4259511..78b9ea4 100644 --- a/OF DL/Widevine/CDMDevice.cs +++ b/OF DL/Widevine/CDMDevice.cs @@ -1,14 +1,11 @@ -using Org.BouncyCastle.Crypto; +using System.Text; +using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Encodings; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Signers; using Org.BouncyCastle.OpenSsl; using ProtoBuf; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; namespace WidevineClient.Widevine { diff --git a/OF DL/Widevine/ContentKey.cs b/OF DL/Widevine/ContentKey.cs index f10b9be..9810225 100644 --- a/OF DL/Widevine/ContentKey.cs +++ b/OF DL/Widevine/ContentKey.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; +using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; namespace WidevineClient.Widevine diff --git a/OF DL/Widevine/PSSHBox.cs b/OF DL/Widevine/PSSHBox.cs index c735366..55b16de 100644 --- a/OF DL/Widevine/PSSHBox.cs +++ b/OF DL/Widevine/PSSHBox.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace WidevineClient.Widevine +namespace OF_DL.Widevine { class PSSHBox { diff --git a/OF DL/Widevine/Session.cs b/OF DL/Widevine/Session.cs index 51cd618..0c6a515 100644 --- a/OF DL/Widevine/Session.cs +++ b/OF DL/Widevine/Session.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace WidevineClient.Widevine +namespace OF_DL.Widevine { class Session { @@ -24,4 +22,4 @@ namespace WidevineClient.Widevine Device = device; } } -} \ No newline at end of file +} From 5df13775f044a5df6bdf1f8c6669ecbae9ca7b80 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Fri, 6 Feb 2026 01:34:57 -0600 Subject: [PATCH 07/55] Fix incorrect namespaces --- OF DL/Crypto/CryptoUtils.cs | 6 +++--- OF DL/Crypto/Padding.cs | 2 +- OF DL/Program.cs | 8 ++++---- OF DL/Utils/HttpUtil.cs | 2 +- OF DL/Widevine/CDM.cs | 4 ++-- OF DL/Widevine/CDMApi.cs | 4 +--- OF DL/Widevine/CDMDevice.cs | 2 +- OF DL/Widevine/Constants.cs | 2 +- OF DL/Widevine/ContentKey.cs | 2 +- OF DL/Widevine/DerivedKeys.cs | 2 +- 10 files changed, 16 insertions(+), 18 deletions(-) diff --git a/OF DL/Crypto/CryptoUtils.cs b/OF DL/Crypto/CryptoUtils.cs index d433133..05f4c67 100644 --- a/OF DL/Crypto/CryptoUtils.cs +++ b/OF DL/Crypto/CryptoUtils.cs @@ -1,10 +1,10 @@ -using Org.BouncyCastle.Crypto; +using System.Security.Cryptography; +using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Macs; using Org.BouncyCastle.Crypto.Parameters; -using System.Security.Cryptography; -namespace WidevineClient.Crypto +namespace OF_DL.Crypto { public class CryptoUtils { diff --git a/OF DL/Crypto/Padding.cs b/OF DL/Crypto/Padding.cs index 34b5ec7..f819ea8 100644 --- a/OF DL/Crypto/Padding.cs +++ b/OF DL/Crypto/Padding.cs @@ -1,6 +1,6 @@ using System.Security.Cryptography; -namespace WidevineClient.Crypto +namespace OF_DL.Crypto { public class Padding { diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 6cc38a0..54d6934 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -463,7 +463,7 @@ public class Program(IServiceProvider serviceProvider) Environment.Exit(4); } - if (!File.Exists(Path.Join(WidevineClient.Widevine.Constants.DEVICES_FOLDER, WidevineClient.Widevine.Constants.DEVICE_NAME, "device_client_id_blob"))) + if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_client_id_blob"))) { clientIdBlobMissing = true; Log.Debug("clientIdBlobMissing missing"); @@ -471,10 +471,10 @@ public class Program(IServiceProvider serviceProvider) else { AnsiConsole.Markup($"[green]device_client_id_blob located successfully![/]\n"); - Log.Debug("clientIdBlobMissing found: " + File.Exists(Path.Join(WidevineClient.Widevine.Constants.DEVICES_FOLDER, WidevineClient.Widevine.Constants.DEVICE_NAME, "device_client_id_blob"))); + Log.Debug("clientIdBlobMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_client_id_blob"))); } - if (!File.Exists(Path.Join(WidevineClient.Widevine.Constants.DEVICES_FOLDER, WidevineClient.Widevine.Constants.DEVICE_NAME, "device_private_key"))) + if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_private_key"))) { devicePrivateKeyMissing = true; Log.Debug("devicePrivateKeyMissing missing"); @@ -482,7 +482,7 @@ public class Program(IServiceProvider serviceProvider) else { AnsiConsole.Markup($"[green]device_private_key located successfully![/]\n"); - Log.Debug("devicePrivateKeyMissing found: " + File.Exists(Path.Join(WidevineClient.Widevine.Constants.DEVICES_FOLDER, WidevineClient.Widevine.Constants.DEVICE_NAME, "device_private_key"))); + Log.Debug("devicePrivateKeyMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_private_key"))); } if (clientIdBlobMissing || devicePrivateKeyMissing) diff --git a/OF DL/Utils/HttpUtil.cs b/OF DL/Utils/HttpUtil.cs index 89f3ec1..a239547 100644 --- a/OF DL/Utils/HttpUtil.cs +++ b/OF DL/Utils/HttpUtil.cs @@ -1,7 +1,7 @@ using OF_DL.Helpers; using System.Text; -namespace WidevineClient +namespace OF_DL.Utils { class HttpUtil { diff --git a/OF DL/Widevine/CDM.cs b/OF DL/Widevine/CDM.cs index 6b5f6fc..963da77 100644 --- a/OF DL/Widevine/CDM.cs +++ b/OF DL/Widevine/CDM.cs @@ -1,10 +1,10 @@ using System.Reflection; using System.Security.Cryptography; using System.Text; +using OF_DL.Crypto; using ProtoBuf; -using WidevineClient.Crypto; -namespace WidevineClient.Widevine +namespace OF_DL.Widevine { public class CDM { diff --git a/OF DL/Widevine/CDMApi.cs b/OF DL/Widevine/CDMApi.cs index 662377b..4e0f1d9 100644 --- a/OF DL/Widevine/CDMApi.cs +++ b/OF DL/Widevine/CDMApi.cs @@ -1,6 +1,4 @@ -using WidevineClient.Widevine; - -namespace OF_DL.Widevine +namespace OF_DL.Widevine { public class CDMApi { diff --git a/OF DL/Widevine/CDMDevice.cs b/OF DL/Widevine/CDMDevice.cs index 78b9ea4..7dc4023 100644 --- a/OF DL/Widevine/CDMDevice.cs +++ b/OF DL/Widevine/CDMDevice.cs @@ -7,7 +7,7 @@ using Org.BouncyCastle.Crypto.Signers; using Org.BouncyCastle.OpenSsl; using ProtoBuf; -namespace WidevineClient.Widevine +namespace OF_DL.Widevine { public class CDMDevice { diff --git a/OF DL/Widevine/Constants.cs b/OF DL/Widevine/Constants.cs index a206c2f..a692eb2 100644 --- a/OF DL/Widevine/Constants.cs +++ b/OF DL/Widevine/Constants.cs @@ -1,4 +1,4 @@ -namespace WidevineClient.Widevine +namespace OF_DL.Widevine { public class Constants { diff --git a/OF DL/Widevine/ContentKey.cs b/OF DL/Widevine/ContentKey.cs index 9810225..8b611dd 100644 --- a/OF DL/Widevine/ContentKey.cs +++ b/OF DL/Widevine/ContentKey.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; -namespace WidevineClient.Widevine +namespace OF_DL.Widevine { [Serializable] public class ContentKey diff --git a/OF DL/Widevine/DerivedKeys.cs b/OF DL/Widevine/DerivedKeys.cs index 27b95be..fdcebec 100644 --- a/OF DL/Widevine/DerivedKeys.cs +++ b/OF DL/Widevine/DerivedKeys.cs @@ -1,4 +1,4 @@ -namespace WidevineClient.Widevine +namespace OF_DL.Widevine { public class DerivedKeys { From e9ab48518840b7403fe31c0f7720046b4c9612ca Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Fri, 6 Feb 2026 01:35:50 -0600 Subject: [PATCH 08/55] Remove utils class in favor of a couple private functions --- OF DL/Services/DownloadService.cs | 33 +++++- OF DL/Services/FileNameService.cs | 8 +- OF DL/Utils.cs | 189 ------------------------------ OF DL/Widevine/CDM.cs | 21 ++-- 4 files changed, 51 insertions(+), 200 deletions(-) delete mode 100644 OF DL/Utils.cs diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 113be78..ab6cd7e 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -41,7 +41,7 @@ public class DownloadService( string avatarpath = $"{path}/Avatars"; if (!Directory.Exists(folder + avatarpath)) Directory.CreateDirectory(folder + avatarpath); - List avatarMD5Hashes = WidevineClient.Utils.CalculateFolderMD5(folder + avatarpath); + List avatarMD5Hashes = CalculateFolderMD5(folder + avatarpath); Uri uri = new(avatarUrl); string destinationPath = $"{folder}{avatarpath}/"; @@ -86,7 +86,7 @@ public class DownloadService( string headerpath = $"{path}/Headers"; if (!Directory.Exists(folder + headerpath)) Directory.CreateDirectory(folder + headerpath); - List headerMD5Hashes = WidevineClient.Utils.CalculateFolderMD5(folder + headerpath); + List headerMD5Hashes = CalculateFolderMD5(folder + headerpath); Uri uri = new(headerUrl); string destinationPath = $"{folder}{headerpath}/"; @@ -2626,4 +2626,33 @@ public class DownloadService( } #endregion + + private static List CalculateFolderMD5(string folder) + { + List md5Hashes = new List(); + if (Directory.Exists(folder)) + { + string[] files = Directory.GetFiles(folder); + + foreach (string file in files) + { + md5Hashes.Add(CalculateMD5(file)); + } + } + + return md5Hashes; + } + + private static string CalculateMD5(string filePath) + { + using (var md5 = MD5.Create()) + { + using (var stream = File.OpenRead(filePath)) + { + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } + } diff --git a/OF DL/Services/FileNameService.cs b/OF DL/Services/FileNameService.cs index 584df74..f97a1d5 100644 --- a/OF DL/Services/FileNameService.cs +++ b/OF DL/Services/FileNameService.cs @@ -169,7 +169,13 @@ namespace OF_DL.Services fileFormat = fileFormat.Replace(placeholder, kvp.Value); } - return WidevineClient.Utils.RemoveInvalidFileNameChars($"{fileFormat}"); + return RemoveInvalidFileNameChars($"{fileFormat}"); } + + private static string RemoveInvalidFileNameChars(string fileName) + { + return string.IsNullOrEmpty(fileName) ? fileName : string.Concat(fileName.Split(Path.GetInvalidFileNameChars())); + } + } } diff --git a/OF DL/Utils.cs b/OF DL/Utils.cs deleted file mode 100644 index 86ce297..0000000 --- a/OF DL/Utils.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Diagnostics; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; - -namespace WidevineClient -{ - class Utils - { - public static double EvaluateEquation(string equation, int decimals = 3) - { - var dataTable = new DataTable(); - return Math.Round((double)dataTable.Compute(equation, ""), decimals); - } - - public static string RunCommand(string command, string args) - { - Process p = new Process(); - p.StartInfo.UseShellExecute = false; - p.StartInfo.RedirectStandardOutput = true; - p.StartInfo.FileName = command; - p.StartInfo.Arguments = args; - p.StartInfo.CreateNoWindow = true; - p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; - p.Start(); - string output = p.StandardOutput.ReadToEnd(); - p.WaitForExit(); - return output; - } - - public static int RunCommandCode(string command, string args) - { - Process p = new Process(); - p.StartInfo.UseShellExecute = false; - p.StartInfo.RedirectStandardOutput = false; - p.StartInfo.FileName = command; - p.StartInfo.Arguments = args; - p.Start(); - p.WaitForExit(); - return p.ExitCode; - } - - public static byte[] Xor(byte[] a, byte[] b) - { - byte[] x = new byte[Math.Min(a.Length, b.Length)]; - - for (int i = 0; i < x.Length; i++) - { - x[i] = (byte)(a[i] ^ b[i]); - } - - return x; - } - - public static string GenerateRandomId() - { - return BytesToHex(RandomBytes(3)).ToLower(); - } - - public static byte[] RandomBytes(int length) - { - var bytes = new byte[length]; - new Random().NextBytes(bytes); - return bytes; - } - - public static string[] GetElementsInnerTextByAttribute(string html, string element, string attribute) - { - List content = new List(); - - foreach (string line in html.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None)) - { - if (line.Contains("<" + element) && line.Contains(attribute)) - { - string contentPart = line.Substring(0, line.LastIndexOf("<")); - if (contentPart.EndsWith(">")) - contentPart = contentPart[..^1]; - - contentPart = contentPart[(contentPart.LastIndexOf(">") + 1)..]; - - if (contentPart.Contains("<")) - contentPart = contentPart[..contentPart.IndexOf("<")]; - - content.Add(contentPart); - } - } - return content.ToArray(); - } - - public static string BytesToHex(byte[] data) - { - return BitConverter.ToString(data).Replace("-", ""); - } - public static byte[] HexToBytes(string hex) - { - hex = hex.Trim(); - byte[] bytes = new byte[hex.Length / 2]; - - for (int i = 0; i < hex.Length; i += 2) - bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); - - return bytes; - } - - public static bool IsBase64Encoded(string str) - { - try - { - byte[] data = Convert.FromBase64String(str); - return true; - } - catch - { - return false; - } - } - - public static string Base64Pad(string base64) - { - if (base64.Length % 4 != 0) - { - base64 = base64.PadRight(base64.Length + (4 - (base64.Length % 4)), '='); - } - return base64; - } - public static string Base64ToString(string base64) - { - return Encoding.UTF8.GetString(Convert.FromBase64String(base64)); - } - public static string StringToBase64(string str) - { - return Convert.ToBase64String(Encoding.UTF8.GetBytes(str)); - } - - public static void TitleProgress(long read, long length) - { - long readMB = read / 1024 / 1024; - long lengthMB = length / 1024 / 1024; - Console.Title = $"{readMB}/{lengthMB}MB"; - } - - public static void TitleProgressNoConversion(long read, long length) - { - Console.Title = $"{read}/{length}MB"; - } - - public static string Version() - { - return System.Reflection.Assembly.GetCallingAssembly().GetName().Version.ToString(); - } - - public static string? RemoveInvalidFileNameChars(string? fileName) - { - return string.IsNullOrEmpty(fileName) ? fileName : string.Concat(fileName.Split(Path.GetInvalidFileNameChars())); - } - - public static List CalculateFolderMD5(string folder) - { - List md5Hashes = new List(); - if (Directory.Exists(folder)) - { - string[] files = Directory.GetFiles(folder); - - foreach (string file in files) - { - md5Hashes.Add(CalculateMD5(file)); - } - } - - return md5Hashes; - } - - public static string CalculateMD5(string filePath) - { - using (var md5 = MD5.Create()) - { - using (var stream = File.OpenRead(filePath)) - { - byte[] hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - } - } -} diff --git a/OF DL/Widevine/CDM.cs b/OF DL/Widevine/CDM.cs index 963da77..ffe25cc 100644 --- a/OF DL/Widevine/CDM.cs +++ b/OF DL/Widevine/CDM.cs @@ -90,9 +90,9 @@ namespace OF_DL.Widevine return null; } - Sessions.Add(Utils.BytesToHex(sessionId), session); + Sessions.Add(BytesToHex(sessionId), session); - return Utils.BytesToHex(sessionId); + return BytesToHex(sessionId); } static WidevineCencHeader ParseInitData(byte[] initData) @@ -124,7 +124,7 @@ namespace OF_DL.Widevine public static bool CloseSession(string sessionId) { - //Logger.Debug($"CloseSession(session_id={Utils.BytesToHex(sessionId)})"); + //Logger.Debug($"CloseSession(session_id={BytesToHex(sessionId)})"); //Logger.Verbose("Closing CDM session"); if (Sessions.ContainsKey(sessionId)) @@ -142,7 +142,7 @@ namespace OF_DL.Widevine public static bool SetServiceCertificate(string sessionId, byte[] certData) { - //Logger.Debug($"SetServiceCertificate(sessionId={Utils.BytesToHex(sessionId)}, cert={certB64})"); + //Logger.Debug($"SetServiceCertificate(sessionId={BytesToHex(sessionId)}, cert={certB64})"); //Logger.Verbose($"Setting service certificate"); if (!Sessions.ContainsKey(sessionId)) @@ -190,7 +190,7 @@ namespace OF_DL.Widevine public static byte[] GetLicenseRequest(string sessionId) { - //Logger.Debug($"GetLicenseRequest(sessionId={Utils.BytesToHex(sessionId)})"); + //Logger.Debug($"GetLicenseRequest(sessionId={BytesToHex(sessionId)})"); //Logger.Verbose($"Getting license request"); if (!Sessions.ContainsKey(sessionId)) @@ -318,7 +318,7 @@ namespace OF_DL.Widevine public static void ProvideLicense(string sessionId, byte[] license) { - //Logger.Debug($"ProvideLicense(sessionId={Utils.BytesToHex(sessionId)}, licenseB64={licenseB64})"); + //Logger.Debug($"ProvideLicense(sessionId={BytesToHex(sessionId)}, licenseB64={licenseB64})"); //Logger.Verbose("Decrypting provided license"); if (!Sessions.ContainsKey(sessionId)) @@ -365,7 +365,7 @@ namespace OF_DL.Widevine throw new Exception("Unable to decrypt session key"); } - //Logger.Debug("Session key: " + Utils.BytesToHex(session.SessionKey)); + //Logger.Debug("Session key: " + BytesToHex(session.SessionKey)); session.DerivedKeys = DeriveKeys(session.LicenseRequest, session.SessionKey); @@ -476,6 +476,11 @@ namespace OF_DL.Widevine throw new Exception("Session not found"); } } + + private static string BytesToHex(byte[] data) + { + return BitConverter.ToString(data).Replace("-", ""); + } } } @@ -529,7 +534,7 @@ namespace OF_DL.Widevine byte[] encryptionKey = encCmacKey; List keys = new List(); - + foreach (License.KeyContainer key in signedLicense.Msg.Keys) { string type = key.Type.ToString(); From 7af7bd8cfa8bf29d50849bedfd8dd37e1c524af2 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Fri, 6 Feb 2026 01:42:01 -0600 Subject: [PATCH 09/55] Remove license header template and c++ rules --- .editorconfig | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/.editorconfig b/.editorconfig index 62adc2c..6cb91ba 100644 --- a/.editorconfig +++ b/.editorconfig @@ -40,7 +40,7 @@ csharp_indent_switch_labels = true csharp_indent_labels = one_less_than_current # Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion # avoid this. unless absolutely necessary dotnet_style_qualification_for_field = false:suggestion @@ -57,17 +57,17 @@ dotnet_style_predefined_type_for_member_access = true:suggestion # name all constant fields using PascalCase dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_style.pascal_case_style.capitalization = pascal_case # static fields should have s_ prefix dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion -dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields -dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style -dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field dotnet_naming_symbols.static_fields.required_modifiers = static dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected dotnet_naming_style.static_prefix_style.required_prefix = s_ @@ -75,8 +75,8 @@ dotnet_naming_style.static_prefix_style.capitalization = camel_case # internal and private fields should be _camelCase dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion -dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style dotnet_naming_symbols.private_internal_fields.applicable_kinds = field dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal dotnet_naming_style.camel_case_underscore_style.required_prefix = _ @@ -155,14 +155,6 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false -# License header -file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. - -# C++ Files -[*.{cpp,h,in}] -curly_bracket_next_line = true -indent_brace_style = Allman - # Xml project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] indent_size = 2 @@ -189,5 +181,6 @@ indent_size = 2 # Shell scripts [*.sh] end_of_line = lf + [*.{cmd,bat}] end_of_line = crlf From 6784ba0a18da4ca74bdc26ebc0cfb6cacd0e8eff Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Fri, 6 Feb 2026 01:42:57 -0600 Subject: [PATCH 10/55] Autoformat the entire solution --- OF DL/CLI/SpectreProgressReporter.cs | 12 +- OF DL/Crypto/CryptoUtils.cs | 32 +- OF DL/Crypto/Padding.cs | 198 +- OF DL/Entities/Archived/Archived.cs | 478 +- OF DL/Entities/Archived/ArchivedCollection.cs | 13 +- OF DL/Entities/Auth.cs | 19 +- OF DL/Entities/CDRMProjectRequest.cs | 22 +- OF DL/Entities/Config.cs | 274 +- OF DL/Entities/DownloadResult.cs | 14 +- OF DL/Entities/DynamicRules.cs | 46 +- OF DL/Entities/FileNameFormatConfig.cs | 16 +- OF DL/Entities/Highlights/HighlightMedia.cs | 179 +- OF DL/Entities/Highlights/Highlights.cs | 32 +- OF DL/Entities/IFileNameFormatConfig.cs | 16 +- OF DL/Entities/Lists/UserList.cs | 66 +- OF DL/Entities/Lists/UsersList.cs | 309 +- OF DL/Entities/Messages/MessageCollection.cs | 13 +- OF DL/Entities/Messages/Messages.cs | 302 +- OF DL/Entities/Messages/SingleMessage.cs | 223 +- OF DL/Entities/OFDLRequest.cs | 16 +- OF DL/Entities/Post/Post.cs | 29 +- OF DL/Entities/Post/PostCollection.cs | 13 +- OF DL/Entities/Post/SinglePost.cs | 331 +- OF DL/Entities/Post/SinglePostCollection.cs | 13 +- .../Purchased/PaidMessageCollection.cs | 13 +- .../Entities/Purchased/PaidPostCollection.cs | 13 +- OF DL/Entities/Purchased/Purchased.cs | 124 +- .../Purchased/PurchasedTabCollection.cs | 15 +- .../Purchased/SinglePaidMessageCollection.cs | 19 +- OF DL/Entities/ShortDateConverter.cs | 12 +- OF DL/Entities/Stories/Stories.cs | 151 +- OF DL/Entities/Streams/Streams.cs | 362 +- OF DL/Entities/Streams/StreamsCollection.cs | 13 +- OF DL/Entities/Subscriptions.cs | 296 +- OF DL/Entities/ToggleableConfigAttribute.cs | 10 +- OF DL/Entities/User.cs | 352 +- OF DL/Enumerations/CustomFileNameOption.cs | 2 +- OF DL/Enumerations/DownloadDateSelection.cs | 11 +- OF DL/Enumerations/LoggingLevel.cs | 60 +- OF DL/Enumerations/MediaType.cs | 21 +- OF DL/Enumerations/VideoResolution.cs | 13 +- OF DL/Helpers/DownloadContext.cs | 41 +- OF DL/Helpers/IFileNameHelper.cs | 13 +- OF DL/Helpers/VersionHelper.cs | 16 +- OF DL/OF DL.csproj | 28 +- OF DL/Program.cs | 4569 +++++++++-------- OF DL/References/Spectre.Console.deps.json | 216 +- OF DL/Services/APIService.cs | 1529 ++++-- OF DL/Services/AuthService.cs | 365 +- OF DL/Services/ConfigService.cs | 725 +-- OF DL/Services/DBService.cs | 871 ++-- OF DL/Services/DownloadService.cs | 633 ++- OF DL/Services/FileNameService.cs | 303 +- OF DL/Services/IAPIService.cs | 58 +- OF DL/Services/IAuthService.cs | 15 +- OF DL/Services/IConfigService.cs | 17 +- OF DL/Services/IDBService.cs | 40 +- OF DL/Services/IDownloadService.cs | 162 +- OF DL/Services/IFileNameService.cs | 13 +- OF DL/Services/ILoggingService.cs | 13 +- OF DL/Services/IProgressReporter.cs | 8 +- OF DL/Services/LoggingService.cs | 50 +- OF DL/Utils/HttpUtil.cs | 237 +- OF DL/Utils/ThrottledStream.cs | 109 +- OF DL/Utils/XmlUtils.cs | 40 +- OF DL/Widevine/CDM.cs | 962 ++-- OF DL/Widevine/CDMApi.cs | 38 +- OF DL/Widevine/CDMDevice.cs | 128 +- OF DL/Widevine/Constants.cs | 15 +- OF DL/Widevine/ContentKey.cs | 52 +- OF DL/Widevine/DerivedKeys.cs | 13 +- OF DL/Widevine/PSSHBox.cs | 105 +- OF DL/Widevine/Session.cs | 43 +- OF DL/rules.json | 35 +- 74 files changed, 8476 insertions(+), 7139 deletions(-) diff --git a/OF DL/CLI/SpectreProgressReporter.cs b/OF DL/CLI/SpectreProgressReporter.cs index f6c65ab..49cf12b 100644 --- a/OF DL/CLI/SpectreProgressReporter.cs +++ b/OF DL/CLI/SpectreProgressReporter.cs @@ -4,21 +4,15 @@ using Spectre.Console; namespace OF_DL.CLI; /// -/// Implementation of IProgressReporter that uses Spectre.Console's ProgressTask for CLI output. +/// Implementation of IProgressReporter that uses Spectre.Console's ProgressTask for CLI output. /// public class SpectreProgressReporter : IProgressReporter { private readonly ProgressTask _task; - public SpectreProgressReporter(ProgressTask task) - { - _task = task ?? throw new ArgumentNullException(nameof(task)); - } + public SpectreProgressReporter(ProgressTask task) => _task = task ?? throw new ArgumentNullException(nameof(task)); - public void ReportProgress(long increment) - { - _task.Increment(increment); - } + public void ReportProgress(long increment) => _task.Increment(increment); public void ReportStatus(string message) { diff --git a/OF DL/Crypto/CryptoUtils.cs b/OF DL/Crypto/CryptoUtils.cs index 05f4c67..624dce0 100644 --- a/OF DL/Crypto/CryptoUtils.cs +++ b/OF DL/Crypto/CryptoUtils.cs @@ -4,30 +4,26 @@ using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Macs; using Org.BouncyCastle.Crypto.Parameters; -namespace OF_DL.Crypto +namespace OF_DL.Crypto; + +public class CryptoUtils { - public class CryptoUtils + public static byte[] GetHMACSHA256Digest(byte[] data, byte[] key) => new HMACSHA256(key).ComputeHash(data); + + public static byte[] GetCMACDigest(byte[] data, byte[] key) { - public static byte[] GetHMACSHA256Digest(byte[] data, byte[] key) - { - return new HMACSHA256(key).ComputeHash(data); - } + IBlockCipher cipher = new AesEngine(); + IMac mac = new CMac(cipher, 128); - public static byte[] GetCMACDigest(byte[] data, byte[] key) - { - IBlockCipher cipher = new AesEngine(); - IMac mac = new CMac(cipher, 128); + KeyParameter keyParam = new(key); - KeyParameter keyParam = new KeyParameter(key); + mac.Init(keyParam); - mac.Init(keyParam); + mac.BlockUpdate(data, 0, data.Length); - mac.BlockUpdate(data, 0, data.Length); + byte[] outBytes = new byte[16]; - byte[] outBytes = new byte[16]; - - mac.DoFinal(outBytes, 0); - return outBytes; - } + mac.DoFinal(outBytes, 0); + return outBytes; } } diff --git a/OF DL/Crypto/Padding.cs b/OF DL/Crypto/Padding.cs index f819ea8..e3075e4 100644 --- a/OF DL/Crypto/Padding.cs +++ b/OF DL/Crypto/Padding.cs @@ -1,118 +1,126 @@ using System.Security.Cryptography; -namespace OF_DL.Crypto +namespace OF_DL.Crypto; + +public class Padding { - public class Padding + public static byte[] AddPKCS7Padding(byte[] data, int k) { - public static byte[] AddPKCS7Padding(byte[] data, int k) + int m = k - data.Length % k; + + byte[] padding = new byte[m]; + Array.Fill(padding, (byte)m); + + byte[] paddedBytes = new byte[data.Length + padding.Length]; + Buffer.BlockCopy(data, 0, paddedBytes, 0, data.Length); + Buffer.BlockCopy(padding, 0, paddedBytes, data.Length, padding.Length); + + return paddedBytes; + } + + public static byte[] RemovePKCS7Padding(byte[] paddedByteArray) + { + byte last = paddedByteArray[^1]; + if (paddedByteArray.Length <= last) { - int m = k - (data.Length % k); - - byte[] padding = new byte[m]; - Array.Fill(padding, (byte)m); - - byte[] paddedBytes = new byte[data.Length + padding.Length]; - Buffer.BlockCopy(data, 0, paddedBytes, 0, data.Length); - Buffer.BlockCopy(padding, 0, paddedBytes, data.Length, padding.Length); - - return paddedBytes; + return paddedByteArray; } - public static byte[] RemovePKCS7Padding(byte[] paddedByteArray) - { - var last = paddedByteArray[^1]; - if (paddedByteArray.Length <= last) - { - return paddedByteArray; - } + return SubArray(paddedByteArray, 0, paddedByteArray.Length - last); + } - return SubArray(paddedByteArray, 0, (paddedByteArray.Length - last)); + public static T[] SubArray(T[] arr, int start, int length) + { + T[] result = new T[length]; + Buffer.BlockCopy(arr, start, result, 0, length); + + return result; + } + + public static byte[] AddPSSPadding(byte[] hash) + { + int modBits = 2048; + int hLen = 20; + int emLen = 256; + + int lmask = 0; + for (int i = 0; i < 8 * emLen - (modBits - 1); i++) + { + lmask = (lmask >> 1) | 0x80; } - public static T[] SubArray(T[] arr, int start, int length) + if (emLen < hLen + hLen + 2) { - var result = new T[length]; - Buffer.BlockCopy(arr, start, result, 0, length); - - return result; + return null; } - public static byte[] AddPSSPadding(byte[] hash) + byte[] salt = new byte[hLen]; + new Random().NextBytes(salt); + + byte[] m_prime = Enumerable.Repeat((byte)0, 8).ToArray().Concat(hash).Concat(salt).ToArray(); + byte[] h = SHA1.Create().ComputeHash(m_prime); + + byte[] ps = Enumerable.Repeat((byte)0, emLen - hLen - hLen - 2).ToArray(); + byte[] db = ps.Concat(new byte[] { 0x01 }).Concat(salt).ToArray(); + + byte[] dbMask = MGF1(h, emLen - hLen - 1); + + byte[] maskedDb = new byte[dbMask.Length]; + for (int i = 0; i < dbMask.Length; i++) { - int modBits = 2048; - int hLen = 20; - int emLen = 256; - - int lmask = 0; - for (int i = 0; i < 8 * emLen - (modBits - 1); i++) - lmask = lmask >> 1 | 0x80; - - if (emLen < hLen + hLen + 2) - { - return null; - } - - byte[] salt = new byte[hLen]; - new Random().NextBytes(salt); - - byte[] m_prime = Enumerable.Repeat((byte)0, 8).ToArray().Concat(hash).Concat(salt).ToArray(); - byte[] h = SHA1.Create().ComputeHash(m_prime); - - byte[] ps = Enumerable.Repeat((byte)0, emLen - hLen - hLen - 2).ToArray(); - byte[] db = ps.Concat(new byte[] { 0x01 }).Concat(salt).ToArray(); - - byte[] dbMask = MGF1(h, emLen - hLen - 1); - - byte[] maskedDb = new byte[dbMask.Length]; - for (int i = 0; i < dbMask.Length; i++) - maskedDb[i] = (byte)(db[i] ^ dbMask[i]); - - maskedDb[0] = (byte)(maskedDb[0] & ~lmask); - - byte[] padded = maskedDb.Concat(h).Concat(new byte[] { 0xBC }).ToArray(); - - return padded; + maskedDb[i] = (byte)(db[i] ^ dbMask[i]); } - public static byte[] RemoveOAEPPadding(byte[] data) + maskedDb[0] = (byte)(maskedDb[0] & ~lmask); + + byte[] padded = maskedDb.Concat(h).Concat(new byte[] { 0xBC }).ToArray(); + + return padded; + } + + public static byte[] RemoveOAEPPadding(byte[] data) + { + int k = 256; + int hLen = 20; + + byte[] maskedSeed = data[1..(hLen + 1)]; + byte[] maskedDB = data[(hLen + 1)..]; + + byte[] seedMask = MGF1(maskedDB, hLen); + + byte[] seed = new byte[maskedSeed.Length]; + for (int i = 0; i < maskedSeed.Length; i++) { - int k = 256; - int hLen = 20; - - byte[] maskedSeed = data[1..(hLen + 1)]; - byte[] maskedDB = data[(hLen + 1)..]; - - byte[] seedMask = MGF1(maskedDB, hLen); - - byte[] seed = new byte[maskedSeed.Length]; - for (int i = 0; i < maskedSeed.Length; i++) - seed[i] = (byte)(maskedSeed[i] ^ seedMask[i]); - - byte[] dbMask = MGF1(seed, k - hLen - 1); - - byte[] db = new byte[maskedDB.Length]; - for (int i = 0; i < maskedDB.Length; i++) - db[i] = (byte)(maskedDB[i] ^ dbMask[i]); - - int onePos = BitConverter.ToString(db[hLen..]).Replace("-", "").IndexOf("01") / 2; - byte[] unpadded = db[(hLen + onePos + 1)..]; - - return unpadded; + seed[i] = (byte)(maskedSeed[i] ^ seedMask[i]); } - static byte[] MGF1(byte[] seed, int maskLen) + byte[] dbMask = MGF1(seed, k - hLen - 1); + + byte[] db = new byte[maskedDB.Length]; + for (int i = 0; i < maskedDB.Length; i++) { - SHA1 hobj = SHA1.Create(); - int hLen = hobj.HashSize / 8; - List T = new List(); - for (int i = 0; i < (int)Math.Ceiling(((double)maskLen / (double)hLen)); i++) - { - byte[] c = BitConverter.GetBytes(i); - Array.Reverse(c); - byte[] digest = hobj.ComputeHash(seed.Concat(c).ToArray()); - T.AddRange(digest); - } - return T.GetRange(0, maskLen).ToArray(); + db[i] = (byte)(maskedDB[i] ^ dbMask[i]); } + + int onePos = BitConverter.ToString(db[hLen..]).Replace("-", "").IndexOf("01") / 2; + byte[] unpadded = db[(hLen + onePos + 1)..]; + + return unpadded; + } + + private static byte[] MGF1(byte[] seed, int maskLen) + { + SHA1 hobj = SHA1.Create(); + int hLen = hobj.HashSize / 8; + List T = new(); + for (int i = 0; i < (int)Math.Ceiling(maskLen / (double)hLen); i++) + { + byte[] c = BitConverter.GetBytes(i); + Array.Reverse(c); + byte[] digest = hobj.ComputeHash(seed.Concat(c).ToArray()); + T.AddRange(digest); + } + + return T.GetRange(0, maskLen).ToArray(); } } diff --git a/OF DL/Entities/Archived/Archived.cs b/OF DL/Entities/Archived/Archived.cs index 1231e1c..a8ff9dd 100644 --- a/OF DL/Entities/Archived/Archived.cs +++ b/OF DL/Entities/Archived/Archived.cs @@ -1,270 +1,262 @@ using Newtonsoft.Json; using OF_DL.Utils; -namespace OF_DL.Entities.Archived +namespace OF_DL.Entities.Archived; + +public class Archived { - public class Archived + public List list { get; set; } + public bool hasMore { get; set; } + public string headMarker { get; set; } + public string tailMarker { get; set; } + public Counters counters { get; set; } + + public class Author { - public List list { get; set; } - public bool hasMore { get; set; } - public string headMarker { get; set; } - public string tailMarker { get; set; } - public Counters counters { get; set; } - public class Author + public long id { get; set; } + public string _view { get; set; } + } + + public class Counters + { + public int? audiosCount { get; set; } + public int? photosCount { get; set; } + public int? videosCount { get; set; } + public int? mediasCount { get; set; } + public int? postsCount { get; set; } + public int? streamsCount { get; set; } + public int? archivedPostsCount { get; set; } + } + + public class Dash + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } + + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } + + public class Drm + { + public Manifest manifest { get; set; } + public Signature signature { get; set; } + } + + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + public Drm drm { get; set; } + } + + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } + + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } + + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } + + public class Hls + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } + + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } + + public class Info + { + public Source source { get; set; } + public Preview preview { get; set; } + } + + public class LinkedPost + { + private string _rawText; + public string responseType { get; set; } + public long? id { get; set; } + public DateTime? postedAt { get; set; } + public string postedAtPrecise { get; set; } + public object expiredAt { get; set; } + public Author author { get; set; } + public string text { get; set; } + + public string rawText { - public long id { get; set; } - public string _view { get; set; } - } - - public class Counters - { - public int? audiosCount { get; set; } - public int? photosCount { get; set; } - public int? videosCount { get; set; } - public int? mediasCount { get; set; } - public int? postsCount { get; set; } - public int? streamsCount { get; set; } - public int? archivedPostsCount { get; set; } - } - - public class Dash - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } - - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } - - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Hls - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } - - public class LinkedPost - { - public string responseType { get; set; } - public long? id { get; set; } - public DateTime? postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - private string _rawText; - public string rawText + get { - get + if (string.IsNullOrEmpty(_rawText)) { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } + _rawText = XmlUtils.EvaluateInnerText(text); + } - return _rawText; - } - set - { - _rawText = value; - } + return _rawText; } - public bool? lockedText { get; set; } - public bool? isFavorite { get; set; } - public bool? canReport { get; set; } - public bool? canDelete { get; set; } - public bool? canComment { get; set; } - public bool? canEdit { get; set; } - public bool? isPinned { get; set; } - public int? favoritesCount { get; set; } - public int? mediaCount { get; set; } - public bool? isMediaReady { get; set; } - public object voting { get; set; } - public bool? isOpened { get; set; } - public bool? canToggleFavorite { get; set; } - public object streamId { get; set; } - public string? price { get; set; } - public bool? hasVoting { get; set; } - public bool? isAddedToBookmarks { get; set; } - public bool? isArchived { get; set; } - public bool? isPrivateArchived { get; set; } - public bool? isDeleted { get; set; } - public bool? hasUrl { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public string cantCommentReason { get; set; } - public int? commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public List media { get; set; } - public bool? canViewMedia { get; set; } - public List preview { get; set; } + set => _rawText = value; } - public class List + public bool? lockedText { get; set; } + public bool? isFavorite { get; set; } + public bool? canReport { get; set; } + public bool? canDelete { get; set; } + public bool? canComment { get; set; } + public bool? canEdit { get; set; } + public bool? isPinned { get; set; } + public int? favoritesCount { get; set; } + public int? mediaCount { get; set; } + public bool? isMediaReady { get; set; } + public object voting { get; set; } + public bool? isOpened { get; set; } + public bool? canToggleFavorite { get; set; } + public object streamId { get; set; } + public string? price { get; set; } + public bool? hasVoting { get; set; } + public bool? isAddedToBookmarks { get; set; } + public bool? isArchived { get; set; } + public bool? isPrivateArchived { get; set; } + public bool? isDeleted { get; set; } + public bool? hasUrl { get; set; } + public bool? isCouplePeopleMedia { get; set; } + public string cantCommentReason { get; set; } + public int? commentsCount { get; set; } + public List mentionedUsers { get; set; } + public List linkedUsers { get; set; } + public List media { get; set; } + public bool? canViewMedia { get; set; } + public List preview { get; set; } + } + + public class List + { + private string _rawText; + public string responseType { get; set; } + public long id { get; set; } + public DateTime postedAt { get; set; } + public string postedAtPrecise { get; set; } + public object expiredAt { get; set; } + public Author author { get; set; } + public string text { get; set; } + + public string rawText { - public string responseType { get; set; } - public long id { get; set; } - public DateTime postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - private string _rawText; - public string rawText + get { - get + if (string.IsNullOrEmpty(_rawText)) { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } + _rawText = XmlUtils.EvaluateInnerText(text); + } - return _rawText; - } - set - { - _rawText = value; - } + return _rawText; } - public bool? lockedText { get; set; } - public bool? isFavorite { get; set; } - public bool? canReport { get; set; } - public bool? canDelete { get; set; } - public bool? canComment { get; set; } - public bool? canEdit { get; set; } - public bool? isPinned { get; set; } - public int? favoritesCount { get; set; } - public int? mediaCount { get; set; } - public bool? isMediaReady { get; set; } - public object voting { get; set; } - public bool isOpened { get; set; } - public bool? canToggleFavorite { get; set; } - public object streamId { get; set; } - public string price { get; set; } - public bool? hasVoting { get; set; } - public bool? isAddedToBookmarks { get; set; } - public bool isArchived { get; set; } - public bool? isPrivateArchived { get; set; } - public bool? isDeleted { get; set; } - public bool? hasUrl { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public int? commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public List media { get; set; } - public bool? canViewMedia { get; set; } - public List preview { get; set; } - public string cantCommentReason { get; set; } + set => _rawText = value; } - public class Manifest - { - public string hls { get; set; } - public string dash { get; set; } - } + public bool? lockedText { get; set; } + public bool? isFavorite { get; set; } + public bool? canReport { get; set; } + public bool? canDelete { get; set; } + public bool? canComment { get; set; } + public bool? canEdit { get; set; } + public bool? isPinned { get; set; } + public int? favoritesCount { get; set; } + public int? mediaCount { get; set; } + public bool? isMediaReady { get; set; } + public object voting { get; set; } + public bool isOpened { get; set; } + public bool? canToggleFavorite { get; set; } + public object streamId { get; set; } + public string price { get; set; } + public bool? hasVoting { get; set; } + public bool? isAddedToBookmarks { get; set; } + public bool isArchived { get; set; } + public bool? isPrivateArchived { get; set; } + public bool? isDeleted { get; set; } + public bool? hasUrl { get; set; } + public bool? isCouplePeopleMedia { get; set; } + public int? commentsCount { get; set; } + public List mentionedUsers { get; set; } + public List linkedUsers { get; set; } + public List media { get; set; } + public bool? canViewMedia { get; set; } + public List preview { get; set; } + public string cantCommentReason { get; set; } + } - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool? convertedToVideo { get; set; } - public bool canView { get; set; } - public bool? hasError { get; set; } - public DateTime? createdAt { get; set; } - public Info info { get; set; } - public Source source { get; set; } - public string squarePreview { get; set; } - public string full { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public Files files { get; set; } - public VideoSources videoSources { get; set; } - } + public class Manifest + { + public string hls { get; set; } + public string dash { get; set; } + } - public class Preview - { - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - public string url { get; set; } - } + public class Medium + { + public long id { get; set; } + public string type { get; set; } + public bool? convertedToVideo { get; set; } + public bool canView { get; set; } + public bool? hasError { get; set; } + public DateTime? createdAt { get; set; } + public Info info { get; set; } + public Source source { get; set; } + public string squarePreview { get; set; } + public string full { get; set; } + public string preview { get; set; } + public string thumb { get; set; } + public Files files { get; set; } + public VideoSources videoSources { get; set; } + } - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } + public class Preview + { + public int? width { get; set; } + public int? height { get; set; } + public int? size { get; set; } + public string url { get; set; } + } - public class Source - { - public string source { get; set; } - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - public int? duration { get; set; } - } + public class Signature + { + public Hls hls { get; set; } + public Dash dash { get; set; } + } - public class VideoSources - { - [JsonProperty("720")] - public string _720 { get; set; } + public class Source + { + public string source { get; set; } + public int? width { get; set; } + public int? height { get; set; } + public int? size { get; set; } + public int? duration { get; set; } + } - [JsonProperty("240")] - public string _240 { get; set; } - } + public class VideoSources + { + [JsonProperty("720")] public string _720 { get; set; } + + [JsonProperty("240")] public string _240 { get; set; } } } diff --git a/OF DL/Entities/Archived/ArchivedCollection.cs b/OF DL/Entities/Archived/ArchivedCollection.cs index 82a1ab3..c61931a 100644 --- a/OF DL/Entities/Archived/ArchivedCollection.cs +++ b/OF DL/Entities/Archived/ArchivedCollection.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Entities.Archived +namespace OF_DL.Entities.Archived; + +public class ArchivedCollection { - public class ArchivedCollection - { - public Dictionary ArchivedPosts = new Dictionary(); - public List ArchivedPostObjects = new List(); - public List ArchivedPostMedia = new List(); - } + public List ArchivedPostMedia = new(); + public List ArchivedPostObjects = new(); + public Dictionary ArchivedPosts = new(); } diff --git a/OF DL/Entities/Auth.cs b/OF DL/Entities/Auth.cs index e2ea9ff..58b9da5 100644 --- a/OF DL/Entities/Auth.cs +++ b/OF DL/Entities/Auth.cs @@ -1,14 +1,13 @@ using Newtonsoft.Json; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class Auth { - public class Auth - { - public string? USER_ID { get; set; } = string.Empty; - public string? USER_AGENT { get; set; } = string.Empty; - public string? X_BC { get; set; } = string.Empty; - public string? COOKIE { get; set; } = string.Empty; - [JsonIgnore] - public string? FFMPEG_PATH { get; set; } = string.Empty; - } + public string? USER_ID { get; set; } = string.Empty; + public string? USER_AGENT { get; set; } = string.Empty; + public string? X_BC { get; set; } = string.Empty; + public string? COOKIE { get; set; } = string.Empty; + + [JsonIgnore] public string? FFMPEG_PATH { get; set; } = string.Empty; } diff --git a/OF DL/Entities/CDRMProjectRequest.cs b/OF DL/Entities/CDRMProjectRequest.cs index 69be2be..6f92897 100644 --- a/OF DL/Entities/CDRMProjectRequest.cs +++ b/OF DL/Entities/CDRMProjectRequest.cs @@ -1,22 +1,16 @@ using Newtonsoft.Json; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class CDRMProjectRequest { - public class CDRMProjectRequest - { - [JsonProperty("pssh")] - public string PSSH { get; set; } = ""; + [JsonProperty("pssh")] public string PSSH { get; set; } = ""; - [JsonProperty("licurl")] - public string LicenseURL { get; set; } = ""; + [JsonProperty("licurl")] public string LicenseURL { get; set; } = ""; - [JsonProperty("headers")] - public string Headers { get; set; } = ""; + [JsonProperty("headers")] public string Headers { get; set; } = ""; - [JsonProperty("cookies")] - public string Cookies { get; set; } = ""; + [JsonProperty("cookies")] public string Cookies { get; set; } = ""; - [JsonProperty("data")] - public string Data { get; set; } = ""; - } + [JsonProperty("data")] public string Data { get; set; } = ""; } diff --git a/OF DL/Entities/Config.cs b/OF DL/Entities/Config.cs index 7fbc8c9..814cc5c 100644 --- a/OF DL/Entities/Config.cs +++ b/OF DL/Entities/Config.cs @@ -3,152 +3,154 @@ using Newtonsoft.Json.Converters; using OF_DL.Enumerations; using Serilog; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class Config : IFileNameFormatConfig { + [ToggleableConfig] public bool DownloadAvatarHeaderPhoto { get; set; } = true; - public class Config : IFileNameFormatConfig + [ToggleableConfig] public bool DownloadPaidPosts { get; set; } = true; + + [ToggleableConfig] public bool DownloadPosts { get; set; } = true; + + [ToggleableConfig] public bool DownloadArchived { get; set; } = true; + + [ToggleableConfig] public bool DownloadStreams { get; set; } = true; + + [ToggleableConfig] public bool DownloadStories { get; set; } = true; + + [ToggleableConfig] public bool DownloadHighlights { get; set; } = true; + + [ToggleableConfig] public bool DownloadMessages { get; set; } = true; + + [ToggleableConfig] public bool DownloadPaidMessages { get; set; } = true; + + [ToggleableConfig] public bool DownloadImages { get; set; } = true; + + [ToggleableConfig] public bool DownloadVideos { get; set; } = true; + + [ToggleableConfig] public bool DownloadAudios { get; set; } = true; + + [ToggleableConfig] public bool IncludeExpiredSubscriptions { get; set; } = false; + + [ToggleableConfig] public bool IncludeRestrictedSubscriptions { get; set; } = false; + + [ToggleableConfig] public bool SkipAds { get; set; } = false; + + public string? DownloadPath { get; set; } = string.Empty; + + [ToggleableConfig] public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } = false; + + public int? Timeout { get; set; } = -1; + + [ToggleableConfig] public bool FolderPerPaidPost { get; set; } = false; + + [ToggleableConfig] public bool FolderPerPost { get; set; } = false; + + [ToggleableConfig] public bool FolderPerPaidMessage { get; set; } = false; + + [ToggleableConfig] public bool FolderPerMessage { get; set; } = false; + + [ToggleableConfig] public bool LimitDownloadRate { get; set; } = false; + + public int DownloadLimitInMbPerSec { get; set; } = 4; + + // Indicates if you want to download only on specific dates. + [ToggleableConfig] public bool DownloadOnlySpecificDates { get; set; } = false; + + // This enum will define if we want data from before or after the CustomDate. + [JsonConverter(typeof(StringEnumConverter))] + public DownloadDateSelection DownloadDateSelection { get; set; } = DownloadDateSelection.before; + // This is the specific date used in combination with the above enum. + + [JsonConverter(typeof(ShortDateConverter))] + public DateTime? CustomDate { get; set; } = null; + + [ToggleableConfig] public bool ShowScrapeSize { get; set; } = false; + + [ToggleableConfig] public bool DownloadPostsIncrementally { get; set; } = false; + + public bool NonInteractiveMode { get; set; } = false; + public string NonInteractiveModeListName { get; set; } = string.Empty; + + [ToggleableConfig] public bool NonInteractiveModePurchasedTab { get; set; } = false; + + public string? FFmpegPath { get; set; } = string.Empty; + + [ToggleableConfig] public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } = false; + + public Dictionary CreatorConfigs { get; set; } = new(); + + [ToggleableConfig] public bool DownloadDuplicatedMedia { get; set; } = false; + + public string IgnoredUsersListName { get; set; } = string.Empty; + + [JsonConverter(typeof(StringEnumConverter))] + public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error; + + [ToggleableConfig] public bool IgnoreOwnMessages { get; set; } = false; + + [ToggleableConfig] public bool DisableBrowserAuth { get; set; } = false; + + [JsonConverter(typeof(StringEnumConverter))] + public VideoResolution DownloadVideoResolution { get; set; } = VideoResolution.source; + + // When enabled, post/message text is stored as-is without XML stripping. + [ToggleableConfig] public bool DisableTextSanitization { get; set; } = false; + + public string? PaidPostFileNameFormat { get; set; } = string.Empty; + public string? PostFileNameFormat { get; set; } = string.Empty; + public string? PaidMessageFileNameFormat { get; set; } = string.Empty; + public string? MessageFileNameFormat { get; set; } = string.Empty; + + public IFileNameFormatConfig GetCreatorFileNameFormatConfig(string username) { - [ToggleableConfig] - public bool DownloadAvatarHeaderPhoto { get; set; } = true; - [ToggleableConfig] - public bool DownloadPaidPosts { get; set; } = true; - [ToggleableConfig] - public bool DownloadPosts { get; set; } = true; - [ToggleableConfig] - public bool DownloadArchived { get; set; } = true; - [ToggleableConfig] - public bool DownloadStreams { get; set; } = true; - [ToggleableConfig] - public bool DownloadStories { get; set; } = true; - [ToggleableConfig] - public bool DownloadHighlights { get; set; } = true; - [ToggleableConfig] - public bool DownloadMessages { get; set; } = true; - [ToggleableConfig] - public bool DownloadPaidMessages { get; set; } = true; - [ToggleableConfig] - public bool DownloadImages { get; set; } = true; - [ToggleableConfig] - public bool DownloadVideos { get; set; } = true; - [ToggleableConfig] - public bool DownloadAudios { get; set; } = true; - [ToggleableConfig] - public bool IncludeExpiredSubscriptions { get; set; } = false; - [ToggleableConfig] - public bool IncludeRestrictedSubscriptions { get; set; } = false; - [ToggleableConfig] - public bool SkipAds { get; set; } = false; + FileNameFormatConfig createFileNameFormatConfig = new(); - public string? DownloadPath { get; set; } = string.Empty; - public string? PaidPostFileNameFormat { get; set; } = string.Empty; - public string? PostFileNameFormat { get; set; } = string.Empty; - public string? PaidMessageFileNameFormat { get; set; } = string.Empty; - public string? MessageFileNameFormat { get; set; } = string.Empty; - [ToggleableConfig] - public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } = false; - public int? Timeout { get; set; } = -1; - [ToggleableConfig] - public bool FolderPerPaidPost { get; set; } = false; - [ToggleableConfig] - public bool FolderPerPost { get; set; } = false; - [ToggleableConfig] - public bool FolderPerPaidMessage { get; set; } = false; - [ToggleableConfig] - public bool FolderPerMessage { get; set; } = false; - [ToggleableConfig] - public bool LimitDownloadRate { get; set; } = false; - public int DownloadLimitInMbPerSec { get; set; } = 4; - - // Indicates if you want to download only on specific dates. - [ToggleableConfig] - public bool DownloadOnlySpecificDates { get; set; } = false; - - // This enum will define if we want data from before or after the CustomDate. - [JsonConverter(typeof(StringEnumConverter))] - public DownloadDateSelection DownloadDateSelection { get; set; } = DownloadDateSelection.before; - // This is the specific date used in combination with the above enum. - - [JsonConverter(typeof(ShortDateConverter))] - public DateTime? CustomDate { get; set; } = null; - - [ToggleableConfig] - public bool ShowScrapeSize { get; set; } = false; - - [ToggleableConfig] - public bool DownloadPostsIncrementally { get; set; } = false; - - public bool NonInteractiveMode { get; set; } = false; - public string NonInteractiveModeListName { get; set; } = string.Empty; - [ToggleableConfig] - public bool NonInteractiveModePurchasedTab { get; set; } = false; - public string? FFmpegPath { get; set; } = string.Empty; - - [ToggleableConfig] - public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } = false; - - public Dictionary CreatorConfigs { get; set; } = new Dictionary(); - - [ToggleableConfig] - public bool DownloadDuplicatedMedia { get; set; } = false; - - public string IgnoredUsersListName { get; set; } = string.Empty; - - [JsonConverter(typeof(StringEnumConverter))] - public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error; - - [ToggleableConfig] - public bool IgnoreOwnMessages { get; set; } = false; - - [ToggleableConfig] - public bool DisableBrowserAuth { get; set; } = false; - - [JsonConverter(typeof(StringEnumConverter))] - public VideoResolution DownloadVideoResolution { get; set; } = VideoResolution.source; - - // When enabled, post/message text is stored as-is without XML stripping. - [ToggleableConfig] - public bool DisableTextSanitization { get; set; } = false; - - public IFileNameFormatConfig GetCreatorFileNameFormatConfig(string username) + Func func = (val1, val2) => { - FileNameFormatConfig createFileNameFormatConfig = new FileNameFormatConfig(); - - Func func = (val1, val2) => + if (string.IsNullOrEmpty(val1)) { - if (string.IsNullOrEmpty(val1)) - return val2; - else - return val1; - }; - - if(CreatorConfigs.TryGetValue(username, out CreatorConfig? creatorConfig)) - { - createFileNameFormatConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat; - createFileNameFormatConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat; - createFileNameFormatConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat; - createFileNameFormatConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat; + return val2; } - createFileNameFormatConfig.PaidMessageFileNameFormat = func(createFileNameFormatConfig.PaidMessageFileNameFormat, PaidMessageFileNameFormat); - createFileNameFormatConfig.PostFileNameFormat = func(createFileNameFormatConfig.PostFileNameFormat, PostFileNameFormat); - createFileNameFormatConfig.MessageFileNameFormat = func(createFileNameFormatConfig.MessageFileNameFormat, MessageFileNameFormat); - createFileNameFormatConfig.PaidPostFileNameFormat = func(createFileNameFormatConfig.PaidPostFileNameFormat, PaidPostFileNameFormat); + return val1; + }; - Log.Debug("PaidMessageFilenameFormat: {CombinedConfigPaidMessageFileNameFormat}", createFileNameFormatConfig.PaidMessageFileNameFormat); - Log.Debug("PostFileNameFormat: {CombinedConfigPostFileNameFormat}", createFileNameFormatConfig.PostFileNameFormat); - Log.Debug("MessageFileNameFormat: {CombinedConfigMessageFileNameFormat}", createFileNameFormatConfig.MessageFileNameFormat); - Log.Debug("PaidPostFileNameFormat: {CombinedConfigPaidPostFileNameFormat}", createFileNameFormatConfig.PaidPostFileNameFormat); - - return createFileNameFormatConfig; + if (CreatorConfigs.TryGetValue(username, out CreatorConfig? creatorConfig)) + { + createFileNameFormatConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat; + createFileNameFormatConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat; + createFileNameFormatConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat; + createFileNameFormatConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat; } - } - public class CreatorConfig : IFileNameFormatConfig - { - public string? PaidPostFileNameFormat { get; set; } - public string? PostFileNameFormat { get; set; } - public string? PaidMessageFileNameFormat { get; set; } - public string? MessageFileNameFormat { get; set; } - } + createFileNameFormatConfig.PaidMessageFileNameFormat = + func(createFileNameFormatConfig.PaidMessageFileNameFormat, PaidMessageFileNameFormat); + createFileNameFormatConfig.PostFileNameFormat = + func(createFileNameFormatConfig.PostFileNameFormat, PostFileNameFormat); + createFileNameFormatConfig.MessageFileNameFormat = + func(createFileNameFormatConfig.MessageFileNameFormat, MessageFileNameFormat); + createFileNameFormatConfig.PaidPostFileNameFormat = + func(createFileNameFormatConfig.PaidPostFileNameFormat, PaidPostFileNameFormat); + Log.Debug("PaidMessageFilenameFormat: {CombinedConfigPaidMessageFileNameFormat}", + createFileNameFormatConfig.PaidMessageFileNameFormat); + Log.Debug("PostFileNameFormat: {CombinedConfigPostFileNameFormat}", + createFileNameFormatConfig.PostFileNameFormat); + Log.Debug("MessageFileNameFormat: {CombinedConfigMessageFileNameFormat}", + createFileNameFormatConfig.MessageFileNameFormat); + Log.Debug("PaidPostFileNameFormat: {CombinedConfigPaidPostFileNameFormat}", + createFileNameFormatConfig.PaidPostFileNameFormat); + + return createFileNameFormatConfig; + } +} + +public class CreatorConfig : IFileNameFormatConfig +{ + public string? PaidPostFileNameFormat { get; set; } + public string? PostFileNameFormat { get; set; } + public string? PaidMessageFileNameFormat { get; set; } + public string? MessageFileNameFormat { get; set; } } diff --git a/OF DL/Entities/DownloadResult.cs b/OF DL/Entities/DownloadResult.cs index f44d26d..0bfc589 100644 --- a/OF DL/Entities/DownloadResult.cs +++ b/OF DL/Entities/DownloadResult.cs @@ -1,37 +1,37 @@ namespace OF_DL.Entities; /// -/// Represents the result of a download operation. +/// Represents the result of a download operation. /// public class DownloadResult { /// - /// Total number of media items processed. + /// Total number of media items processed. /// public int TotalCount { get; set; } /// - /// Number of newly downloaded media items. + /// Number of newly downloaded media items. /// public int NewDownloads { get; set; } /// - /// Number of media items that were already downloaded. + /// Number of media items that were already downloaded. /// public int ExistingDownloads { get; set; } /// - /// The type of media downloaded (e.g., "Posts", "Messages", "Stories", etc.). + /// The type of media downloaded (e.g., "Posts", "Messages", "Stories", etc.). /// public string MediaType { get; set; } = string.Empty; /// - /// Indicates whether the download operation was successful. + /// Indicates whether the download operation was successful. /// public bool Success { get; set; } = true; /// - /// Optional error message if the download failed. + /// Optional error message if the download failed. /// public string? ErrorMessage { get; set; } } diff --git a/OF DL/Entities/DynamicRules.cs b/OF DL/Entities/DynamicRules.cs index eb41a7c..fc7684a 100644 --- a/OF DL/Entities/DynamicRules.cs +++ b/OF DL/Entities/DynamicRules.cs @@ -1,28 +1,30 @@ using Newtonsoft.Json; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class DynamicRules { - public class DynamicRules + [JsonProperty(PropertyName = "app-token")] + public string? AppToken { get; set; } + + [JsonProperty(PropertyName = "app_token")] + private string AppToken2 { - [JsonProperty(PropertyName="app-token")] - public string? AppToken { get; set; } - - [JsonProperty(PropertyName="app_token")] - private string AppToken2 { set { AppToken = value; } } - - [JsonProperty(PropertyName="static_param")] - public string? StaticParam { get; set; } - - [JsonProperty(PropertyName="prefix")] - public string? Prefix { get; set; } - - [JsonProperty(PropertyName="suffix")] - public string? Suffix { get; set; } - - [JsonProperty(PropertyName="checksum_constant")] - public int? ChecksumConstant { get; set; } - - [JsonProperty(PropertyName = "checksum_indexes")] - public List ChecksumIndexes { get; set; } + set => AppToken = value; } + + [JsonProperty(PropertyName = "static_param")] + public string? StaticParam { get; set; } + + [JsonProperty(PropertyName = "prefix")] + public string? Prefix { get; set; } + + [JsonProperty(PropertyName = "suffix")] + public string? Suffix { get; set; } + + [JsonProperty(PropertyName = "checksum_constant")] + public int? ChecksumConstant { get; set; } + + [JsonProperty(PropertyName = "checksum_indexes")] + public List ChecksumIndexes { get; set; } } diff --git a/OF DL/Entities/FileNameFormatConfig.cs b/OF DL/Entities/FileNameFormatConfig.cs index 73b7b73..46e36d5 100644 --- a/OF DL/Entities/FileNameFormatConfig.cs +++ b/OF DL/Entities/FileNameFormatConfig.cs @@ -1,11 +1,9 @@ -namespace OF_DL.Entities -{ - public class FileNameFormatConfig : IFileNameFormatConfig - { - public string? PaidPostFileNameFormat { get; set; } - public string? PostFileNameFormat { get; set; } - public string? PaidMessageFileNameFormat { get; set; } - public string? MessageFileNameFormat { get; set; } - } +namespace OF_DL.Entities; +public class FileNameFormatConfig : IFileNameFormatConfig +{ + public string? PaidPostFileNameFormat { get; set; } + public string? PostFileNameFormat { get; set; } + public string? PaidMessageFileNameFormat { get; set; } + public string? MessageFileNameFormat { get; set; } } diff --git a/OF DL/Entities/Highlights/HighlightMedia.cs b/OF DL/Entities/Highlights/HighlightMedia.cs index 12b4ccf..c75e01a 100644 --- a/OF DL/Entities/Highlights/HighlightMedia.cs +++ b/OF DL/Entities/Highlights/HighlightMedia.cs @@ -1,103 +1,102 @@ using Newtonsoft.Json; -namespace OF_DL.Entities.Highlights +namespace OF_DL.Entities.Highlights; + +public class HighlightMedia { - public class HighlightMedia - { - public long id { get; set; } - public long userId { get; set; } - public string title { get; set; } - public long coverStoryId { get; set; } - public string cover { get; set; } - public int storiesCount { get; set; } - public DateTime? createdAt { get; set; } - public List stories { get; set; } - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - } + public long id { get; set; } + public long userId { get; set; } + public string title { get; set; } + public long coverStoryId { get; set; } + public string cover { get; set; } + public int storiesCount { get; set; } + public DateTime? createdAt { get; set; } + public List stories { get; set; } - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + } - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Files files { get; set; } - } + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } - public class Preview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } + public class Medium + { + public long id { get; set; } + public string type { get; set; } + public bool convertedToVideo { get; set; } + public bool canView { get; set; } + public bool hasError { get; set; } + public DateTime? createdAt { get; set; } + public Files files { get; set; } + } - public class Source - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public int duration { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } + public class Preview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } - public class Sources - { - [JsonProperty("720")] - public string _720 { get; set; } + public class Source + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public int duration { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } - [JsonProperty("240")] - public string _240 { get; set; } - public string w150 { get; set; } - public string w480 { get; set; } - } + public class Sources + { + [JsonProperty("720")] public string _720 { get; set; } - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } + [JsonProperty("240")] public string _240 { get; set; } - public class Story - { - public long id { get; set; } - public long userId { get; set; } - public bool isWatched { get; set; } - public bool isReady { get; set; } - public List media { get; set; } - public DateTime? createdAt { get; set; } - public object question { get; set; } - public bool canLike { get; set; } - public bool isLiked { get; set; } - } + public string w150 { get; set; } + public string w480 { get; set; } + } - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - } + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } + + public class Story + { + public long id { get; set; } + public long userId { get; set; } + public bool isWatched { get; set; } + public bool isReady { get; set; } + public List media { get; set; } + public DateTime? createdAt { get; set; } + public object question { get; set; } + public bool canLike { get; set; } + public bool isLiked { get; set; } + } + + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } } diff --git a/OF DL/Entities/Highlights/Highlights.cs b/OF DL/Entities/Highlights/Highlights.cs index 8f3df8a..a5fa1b0 100644 --- a/OF DL/Entities/Highlights/Highlights.cs +++ b/OF DL/Entities/Highlights/Highlights.cs @@ -1,18 +1,18 @@ -namespace OF_DL.Entities.Highlights +namespace OF_DL.Entities.Highlights; + +public class Highlights { - public class Highlights - { - public List list { get; set; } - public bool hasMore { get; set; } - public class List - { - public long id { get; set; } - public long userId { get; set; } - public string title { get; set; } - public long coverStoryId { get; set; } - public string cover { get; set; } - public int storiesCount { get; set; } - public DateTime? createdAt { get; set; } - } - } + public List list { get; set; } + public bool hasMore { get; set; } + + public class List + { + public long id { get; set; } + public long userId { get; set; } + public string title { get; set; } + public long coverStoryId { get; set; } + public string cover { get; set; } + public int storiesCount { get; set; } + public DateTime? createdAt { get; set; } + } } diff --git a/OF DL/Entities/IFileNameFormatConfig.cs b/OF DL/Entities/IFileNameFormatConfig.cs index 1c3bc3f..663cc47 100644 --- a/OF DL/Entities/IFileNameFormatConfig.cs +++ b/OF DL/Entities/IFileNameFormatConfig.cs @@ -1,11 +1,9 @@ -namespace OF_DL.Entities -{ - public interface IFileNameFormatConfig - { - string? PaidPostFileNameFormat { get; set; } - string? PostFileNameFormat { get; set; } - string? PaidMessageFileNameFormat { get; set; } - string? MessageFileNameFormat { get; set; } - } +namespace OF_DL.Entities; +public interface IFileNameFormatConfig +{ + string? PaidPostFileNameFormat { get; set; } + string? PostFileNameFormat { get; set; } + string? PaidMessageFileNameFormat { get; set; } + string? MessageFileNameFormat { get; set; } } diff --git a/OF DL/Entities/Lists/UserList.cs b/OF DL/Entities/Lists/UserList.cs index 28a60d6..0c833a8 100644 --- a/OF DL/Entities/Lists/UserList.cs +++ b/OF DL/Entities/Lists/UserList.cs @@ -1,35 +1,35 @@ -namespace OF_DL.Entities.Lists -{ - public class UserList - { - public List list { get; set; } - public bool? hasMore { get; set; } - public class List - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public int? usersCount { get; set; } - public int? postsCount { get; set; } - public bool? canUpdate { get; set; } - public bool? canDelete { get; set; } - public bool? canManageUsers { get; set; } - public bool? canAddUsers { get; set; } - public bool? canPinnedToFeed { get; set; } - public bool? isPinnedToFeed { get; set; } - public bool? canPinnedToChat { get; set; } - public bool? isPinnedToChat { get; set; } - public string order { get; set; } - public string direction { get; set; } - public List users { get; set; } - public List customOrderUsersIds { get; set; } - public List posts { get; set; } - } +namespace OF_DL.Entities.Lists; - public class User - { - public long? id { get; set; } - public string _view { get; set; } - } - } +public class UserList +{ + public List list { get; set; } + public bool? hasMore { get; set; } + + public class List + { + public string id { get; set; } + public string type { get; set; } + public string name { get; set; } + public int? usersCount { get; set; } + public int? postsCount { get; set; } + public bool? canUpdate { get; set; } + public bool? canDelete { get; set; } + public bool? canManageUsers { get; set; } + public bool? canAddUsers { get; set; } + public bool? canPinnedToFeed { get; set; } + public bool? isPinnedToFeed { get; set; } + public bool? canPinnedToChat { get; set; } + public bool? isPinnedToChat { get; set; } + public string order { get; set; } + public string direction { get; set; } + public List users { get; set; } + public List customOrderUsersIds { get; set; } + public List posts { get; set; } + } + + public class User + { + public long? id { get; set; } + public string _view { get; set; } + } } diff --git a/OF DL/Entities/Lists/UsersList.cs b/OF DL/Entities/Lists/UsersList.cs index acd4c2a..77c0095 100644 --- a/OF DL/Entities/Lists/UsersList.cs +++ b/OF DL/Entities/Lists/UsersList.cs @@ -1,165 +1,164 @@ -namespace OF_DL.Entities.Lists +namespace OF_DL.Entities.Lists; + +public class UsersList { - public class UsersList - { - public string view { get; set; } - public string avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long? id { get; set; } - public string name { get; set; } - public string username { get; set; } - public bool? canLookStory { get; set; } - public bool? canCommentStory { get; set; } - public bool? hasNotViewedStory { get; set; } - public bool? isVerified { get; set; } - public bool? canPayInternal { get; set; } - public bool? hasScheduledStream { get; set; } - public bool? hasStream { get; set; } - public bool? hasStories { get; set; } - public bool? tipsEnabled { get; set; } - public bool? tipsTextEnabled { get; set; } - public int? tipsMin { get; set; } - public int? tipsMinInternal { get; set; } - public int? tipsMax { get; set; } - public bool? canEarn { get; set; } - public bool? canAddSubscriber { get; set; } - public string? subscribePrice { get; set; } - public List subscriptionBundles { get; set; } - public string displayName { get; set; } - public string notice { get; set; } - public bool? isPaywallRequired { get; set; } - public bool? unprofitable { get; set; } - public List listsStates { get; set; } - public bool? isMuted { get; set; } - public bool? isRestricted { get; set; } - public bool? canRestrict { get; set; } - public bool? subscribedBy { get; set; } - public bool? subscribedByExpire { get; set; } - public DateTime? subscribedByExpireDate { get; set; } - public bool? subscribedByAutoprolong { get; set; } - public bool? subscribedIsExpiredNow { get; set; } - public string? currentSubscribePrice { get; set; } - public bool? subscribedOn { get; set; } - public bool? subscribedOnExpiredNow { get; set; } - public string subscribedOnDuration { get; set; } - public bool? canReport { get; set; } - public bool? canReceiveChatMessage { get; set; } - public bool? hideChat { get; set; } - public DateTime? lastSeen { get; set; } - public bool? isPerformer { get; set; } - public bool? isRealPerformer { get; set; } - public SubscribedByData subscribedByData { get; set; } - public SubscribedOnData subscribedOnData { get; set; } - public bool? canTrialSend { get; set; } - public bool? isBlocked { get; set; } - public List promoOffers { get; set; } - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } + public string view { get; set; } + public string avatar { get; set; } + public AvatarThumbs avatarThumbs { get; set; } + public string header { get; set; } + public HeaderSize headerSize { get; set; } + public HeaderThumbs headerThumbs { get; set; } + public long? id { get; set; } + public string name { get; set; } + public string username { get; set; } + public bool? canLookStory { get; set; } + public bool? canCommentStory { get; set; } + public bool? hasNotViewedStory { get; set; } + public bool? isVerified { get; set; } + public bool? canPayInternal { get; set; } + public bool? hasScheduledStream { get; set; } + public bool? hasStream { get; set; } + public bool? hasStories { get; set; } + public bool? tipsEnabled { get; set; } + public bool? tipsTextEnabled { get; set; } + public int? tipsMin { get; set; } + public int? tipsMinInternal { get; set; } + public int? tipsMax { get; set; } + public bool? canEarn { get; set; } + public bool? canAddSubscriber { get; set; } + public string? subscribePrice { get; set; } + public List subscriptionBundles { get; set; } + public string displayName { get; set; } + public string notice { get; set; } + public bool? isPaywallRequired { get; set; } + public bool? unprofitable { get; set; } + public List listsStates { get; set; } + public bool? isMuted { get; set; } + public bool? isRestricted { get; set; } + public bool? canRestrict { get; set; } + public bool? subscribedBy { get; set; } + public bool? subscribedByExpire { get; set; } + public DateTime? subscribedByExpireDate { get; set; } + public bool? subscribedByAutoprolong { get; set; } + public bool? subscribedIsExpiredNow { get; set; } + public string? currentSubscribePrice { get; set; } + public bool? subscribedOn { get; set; } + public bool? subscribedOnExpiredNow { get; set; } + public string subscribedOnDuration { get; set; } + public bool? canReport { get; set; } + public bool? canReceiveChatMessage { get; set; } + public bool? hideChat { get; set; } + public DateTime? lastSeen { get; set; } + public bool? isPerformer { get; set; } + public bool? isRealPerformer { get; set; } + public SubscribedByData subscribedByData { get; set; } + public SubscribedOnData subscribedOnData { get; set; } + public bool? canTrialSend { get; set; } + public bool? isBlocked { get; set; } + public List promoOffers { get; set; } - public class HeaderSize - { - public int? width { get; set; } - public int? height { get; set; } - } + public class AvatarThumbs + { + public string c50 { get; set; } + public string c144 { get; set; } + } - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } + public class HeaderSize + { + public int? width { get; set; } + public int? height { get; set; } + } - public class ListsState - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool hasUser { get; set; } - public bool canAddUser { get; set; } - } + public class HeaderThumbs + { + public string w480 { get; set; } + public string w760 { get; set; } + } - public class Subscribe - { - public object id { get; set; } - public long? userId { get; set; } - public int? subscriberId { get; set; } - public DateTime? date { get; set; } - public int? duration { get; set; } - public DateTime? startDate { get; set; } - public DateTime? expireDate { get; set; } - public object cancelDate { get; set; } - public string? price { get; set; } - public string? regularPrice { get; set; } - public string? discount { get; set; } - public string action { get; set; } - public string type { get; set; } - public object offerStart { get; set; } - public object offerEnd { get; set; } - public bool? isCurrent { get; set; } - } + public class ListsState + { + public string id { get; set; } + public string type { get; set; } + public string name { get; set; } + public bool hasUser { get; set; } + public bool canAddUser { get; set; } + } - public class SubscribedByData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public string? discountPercent { get; set; } - public string? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public object renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public string status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public bool? showPostsInFeed { get; set; } - public List subscribes { get; set; } - } + public class Subscribe + { + public object id { get; set; } + public long? userId { get; set; } + public int? subscriberId { get; set; } + public DateTime? date { get; set; } + public int? duration { get; set; } + public DateTime? startDate { get; set; } + public DateTime? expireDate { get; set; } + public object cancelDate { get; set; } + public string? price { get; set; } + public string? regularPrice { get; set; } + public string? discount { get; set; } + public string action { get; set; } + public string type { get; set; } + public object offerStart { get; set; } + public object offerEnd { get; set; } + public bool? isCurrent { get; set; } + } - public class SubscribedOnData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public string? discountPercent { get; set; } - public string? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public object renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public object status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public string? tipsSumm { get; set; } - public string? subscribesSumm { get; set; } - public string? messagesSumm { get; set; } - public string? postsSumm { get; set; } - public string? streamsSumm { get; set; } - public string? totalSumm { get; set; } - public DateTime? lastActivity { get; set; } - public int? recommendations { get; set; } - public List subscribes { get; set; } - } + public class SubscribedByData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } + public string? subscribePrice { get; set; } + public string? discountPercent { get; set; } + public string? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public object renewedAt { get; set; } + public object discountFinishedAt { get; set; } + public object discountStartedAt { get; set; } + public string status { get; set; } + public bool? isMuted { get; set; } + public string unsubscribeReason { get; set; } + public string duration { get; set; } + public bool? showPostsInFeed { get; set; } + public List subscribes { get; set; } + } - public class SubscriptionBundle - { - public long? id { get; set; } - public string? discount { get; set; } - public string? duration { get; set; } - public string? price { get; set; } - public bool? canBuy { get; set; } - } + public class SubscribedOnData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } + public string? subscribePrice { get; set; } + public string? discountPercent { get; set; } + public string? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public object renewedAt { get; set; } + public object discountFinishedAt { get; set; } + public object discountStartedAt { get; set; } + public object status { get; set; } + public bool? isMuted { get; set; } + public string unsubscribeReason { get; set; } + public string duration { get; set; } + public string? tipsSumm { get; set; } + public string? subscribesSumm { get; set; } + public string? messagesSumm { get; set; } + public string? postsSumm { get; set; } + public string? streamsSumm { get; set; } + public string? totalSumm { get; set; } + public DateTime? lastActivity { get; set; } + public int? recommendations { get; set; } + public List subscribes { get; set; } + } - } + public class SubscriptionBundle + { + public long? id { get; set; } + public string? discount { get; set; } + public string? duration { get; set; } + public string? price { get; set; } + public bool? canBuy { get; set; } + } } diff --git a/OF DL/Entities/Messages/MessageCollection.cs b/OF DL/Entities/Messages/MessageCollection.cs index 4766fc1..c12cfcb 100644 --- a/OF DL/Entities/Messages/MessageCollection.cs +++ b/OF DL/Entities/Messages/MessageCollection.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Entities.Messages +namespace OF_DL.Entities.Messages; + +public class MessageCollection { - public class MessageCollection - { - public Dictionary Messages = new Dictionary(); - public List MessageObjects = new List(); - public List MessageMedia = new List(); - } + public List MessageMedia = new(); + public List MessageObjects = new(); + public Dictionary Messages = new(); } diff --git a/OF DL/Entities/Messages/Messages.cs b/OF DL/Entities/Messages/Messages.cs index 0809d57..ce05661 100644 --- a/OF DL/Entities/Messages/Messages.cs +++ b/OF DL/Entities/Messages/Messages.cs @@ -1,179 +1,173 @@ using Newtonsoft.Json; -namespace OF_DL.Entities.Messages +namespace OF_DL.Entities.Messages; + +public class Messages { - public class Messages - { - public List list { get; set; } - public bool hasMore { get; set; } - public class Dash - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public List list { get; set; } + public bool hasMore { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + public class Dash + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } + public class Drm + { + public Manifest manifest { get; set; } + public Signature signature { get; set; } + } - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + public Drm drm { get; set; } + } - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class FromUser - { - public long? id { get; set; } - public string _view { get; set; } - } + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class Hls - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class FromUser + { + public long? id { get; set; } + public string _view { get; set; } + } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + public class Hls + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } - public class List - { - public string responseType { get; set; } - public string text { get; set; } - public object giphyId { get; set; } - public bool? lockedText { get; set; } - public bool? isFree { get; set; } - public string? price { get; set; } - public bool? isMediaReady { get; set; } - public int? mediaCount { get; set; } - public List media { get; set; } - public List previews { get; set; } - public bool? isTip { get; set; } - public bool? isReportedByMe { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public object queueId { get; set; } - public FromUser fromUser { get; set; } - public bool? isFromQueue { get; set; } - public bool? canUnsendQueue { get; set; } - public int? unsendSecondsQueue { get; set; } - public long id { get; set; } - public bool? isOpened { get; set; } - public bool? isNew { get; set; } - public DateTime? createdAt { get; set; } - public DateTime? changedAt { get; set; } - public int? cancelSeconds { get; set; } - public bool? isLiked { get; set; } - public bool? canPurchase { get; set; } - public string canPurchaseReason { get; set; } - public bool? canReport { get; set; } - public bool? canBePinned { get; set; } - public bool? isPinned { get; set; } - } + public class Info + { + public Source source { get; set; } + public Preview preview { get; set; } + } - public class Manifest - { - public string hls { get; set; } - public string dash { get; set; } - } + public class List + { + public string responseType { get; set; } + public string text { get; set; } + public object giphyId { get; set; } + public bool? lockedText { get; set; } + public bool? isFree { get; set; } + public string? price { get; set; } + public bool? isMediaReady { get; set; } + public int? mediaCount { get; set; } + public List media { get; set; } + public List previews { get; set; } + public bool? isTip { get; set; } + public bool? isReportedByMe { get; set; } + public bool? isCouplePeopleMedia { get; set; } + public object queueId { get; set; } + public FromUser fromUser { get; set; } + public bool? isFromQueue { get; set; } + public bool? canUnsendQueue { get; set; } + public int? unsendSecondsQueue { get; set; } + public long id { get; set; } + public bool? isOpened { get; set; } + public bool? isNew { get; set; } + public DateTime? createdAt { get; set; } + public DateTime? changedAt { get; set; } + public int? cancelSeconds { get; set; } + public bool? isLiked { get; set; } + public bool? canPurchase { get; set; } + public string canPurchaseReason { get; set; } + public bool? canReport { get; set; } + public bool? canBePinned { get; set; } + public bool? isPinned { get; set; } + } - public class Medium - { - public long id { get; set; } - public bool canView { get; set; } - public string type { get; set; } - public string src { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public object locked { get; set; } - public int? duration { get; set; } - public bool? hasError { get; set; } - public string squarePreview { get; set; } - public Video video { get; set; } - public VideoSources videoSources { get; set; } - public Source source { get; set; } - public Info info { get; set; } - public Files files { get; set; } - } + public class Manifest + { + public string hls { get; set; } + public string dash { get; set; } + } - public class Preview - { - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - } + public class Medium + { + public long id { get; set; } + public bool canView { get; set; } + public string type { get; set; } + public string src { get; set; } + public string preview { get; set; } + public string thumb { get; set; } + public object locked { get; set; } + public int? duration { get; set; } + public bool? hasError { get; set; } + public string squarePreview { get; set; } + public Video video { get; set; } + public VideoSources videoSources { get; set; } + public Source source { get; set; } + public Info info { get; set; } + public Files files { get; set; } + } - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } + public class Preview + { + public int? width { get; set; } + public int? height { get; set; } + public int? size { get; set; } + } - public class Source - { - public string source { get; set; } - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - } + public class Signature + { + public Hls hls { get; set; } + public Dash dash { get; set; } + } - public class Video - { - public string mp4 { get; set; } - } + public class Source + { + public string source { get; set; } + public int? width { get; set; } + public int? height { get; set; } + public int? size { get; set; } + } - public class VideoSources - { - [JsonProperty("720")] - public string _720 { get; set; } + public class Video + { + public string mp4 { get; set; } + } - [JsonProperty("240")] - public string _240 { get; set; } - } - } + public class VideoSources + { + [JsonProperty("720")] public string _720 { get; set; } + + [JsonProperty("240")] public string _240 { get; set; } + } } diff --git a/OF DL/Entities/Messages/SingleMessage.cs b/OF DL/Entities/Messages/SingleMessage.cs index 8b7be53..69e43f9 100644 --- a/OF DL/Entities/Messages/SingleMessage.cs +++ b/OF DL/Entities/Messages/SingleMessage.cs @@ -1,118 +1,115 @@ using static OF_DL.Entities.Messages.Messages; -namespace OF_DL.Entities.Messages +namespace OF_DL.Entities.Messages; + +public class AvatarThumbs { - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } - - public class FromUser - { - public string view { get; set; } - public string avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long? id { get; set; } - public string name { get; set; } - public string username { get; set; } - public bool canLookStory { get; set; } - public bool canCommentStory { get; set; } - public bool hasNotViewedStory { get; set; } - public bool isVerified { get; set; } - public bool canPayInternal { get; set; } - public bool hasScheduledStream { get; set; } - public bool hasStream { get; set; } - public bool hasStories { get; set; } - public bool tipsEnabled { get; set; } - public bool tipsTextEnabled { get; set; } - public int tipsMin { get; set; } - public int tipsMinInternal { get; set; } - public int tipsMax { get; set; } - public bool canEarn { get; set; } - public bool canAddSubscriber { get; set; } - public string? subscribePrice { get; set; } - public List subscriptionBundles { get; set; } - public bool isPaywallRequired { get; set; } - public List listsStates { get; set; } - public bool isRestricted { get; set; } - public bool canRestrict { get; set; } - public object subscribedBy { get; set; } - public object subscribedByExpire { get; set; } - public DateTime subscribedByExpireDate { get; set; } - public object subscribedByAutoprolong { get; set; } - public bool subscribedIsExpiredNow { get; set; } - public object currentSubscribePrice { get; set; } - public object subscribedOn { get; set; } - public object subscribedOnExpiredNow { get; set; } - public object subscribedOnDuration { get; set; } - public int callPrice { get; set; } - public DateTime? lastSeen { get; set; } - public bool canReport { get; set; } - } - - public class HeaderSize - { - public int width { get; set; } - public int height { get; set; } - } - - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } - - public class ListsState - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool hasUser { get; set; } - public bool canAddUser { get; set; } - public string cannotAddUserReason { get; set; } - } - - public class Preview - { - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class SingleMessage - { - public string responseType { get; set; } - public string text { get; set; } - public object giphyId { get; set; } - public bool lockedText { get; set; } - public bool isFree { get; set; } - public double price { get; set; } - public bool isMediaReady { get; set; } - public int mediaCount { get; set; } - public List media { get; set; } - public List previews { get; set; } - public bool isTip { get; set; } - public bool isReportedByMe { get; set; } - public bool isCouplePeopleMedia { get; set; } - public long queueId { get; set; } - public FromUser fromUser { get; set; } - public bool isFromQueue { get; set; } - public bool canUnsendQueue { get; set; } - public int unsendSecondsQueue { get; set; } - public long id { get; set; } - public bool isOpened { get; set; } - public bool isNew { get; set; } - public DateTime? createdAt { get; set; } - public DateTime? changedAt { get; set; } - public int cancelSeconds { get; set; } - public bool isLiked { get; set; } - public bool canPurchase { get; set; } - public bool canReport { get; set; } - } - + public string c50 { get; set; } + public string c144 { get; set; } } +public class FromUser +{ + public string view { get; set; } + public string avatar { get; set; } + public AvatarThumbs avatarThumbs { get; set; } + public string header { get; set; } + public HeaderSize headerSize { get; set; } + public HeaderThumbs headerThumbs { get; set; } + public long? id { get; set; } + public string name { get; set; } + public string username { get; set; } + public bool canLookStory { get; set; } + public bool canCommentStory { get; set; } + public bool hasNotViewedStory { get; set; } + public bool isVerified { get; set; } + public bool canPayInternal { get; set; } + public bool hasScheduledStream { get; set; } + public bool hasStream { get; set; } + public bool hasStories { get; set; } + public bool tipsEnabled { get; set; } + public bool tipsTextEnabled { get; set; } + public int tipsMin { get; set; } + public int tipsMinInternal { get; set; } + public int tipsMax { get; set; } + public bool canEarn { get; set; } + public bool canAddSubscriber { get; set; } + public string? subscribePrice { get; set; } + public List subscriptionBundles { get; set; } + public bool isPaywallRequired { get; set; } + public List listsStates { get; set; } + public bool isRestricted { get; set; } + public bool canRestrict { get; set; } + public object subscribedBy { get; set; } + public object subscribedByExpire { get; set; } + public DateTime subscribedByExpireDate { get; set; } + public object subscribedByAutoprolong { get; set; } + public bool subscribedIsExpiredNow { get; set; } + public object currentSubscribePrice { get; set; } + public object subscribedOn { get; set; } + public object subscribedOnExpiredNow { get; set; } + public object subscribedOnDuration { get; set; } + public int callPrice { get; set; } + public DateTime? lastSeen { get; set; } + public bool canReport { get; set; } +} + +public class HeaderSize +{ + public int width { get; set; } + public int height { get; set; } +} + +public class HeaderThumbs +{ + public string w480 { get; set; } + public string w760 { get; set; } +} + +public class ListsState +{ + public string id { get; set; } + public string type { get; set; } + public string name { get; set; } + public bool hasUser { get; set; } + public bool canAddUser { get; set; } + public string cannotAddUserReason { get; set; } +} + +public class Preview +{ + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } +} + +public class SingleMessage +{ + public string responseType { get; set; } + public string text { get; set; } + public object giphyId { get; set; } + public bool lockedText { get; set; } + public bool isFree { get; set; } + public double price { get; set; } + public bool isMediaReady { get; set; } + public int mediaCount { get; set; } + public List media { get; set; } + public List previews { get; set; } + public bool isTip { get; set; } + public bool isReportedByMe { get; set; } + public bool isCouplePeopleMedia { get; set; } + public long queueId { get; set; } + public FromUser fromUser { get; set; } + public bool isFromQueue { get; set; } + public bool canUnsendQueue { get; set; } + public int unsendSecondsQueue { get; set; } + public long id { get; set; } + public bool isOpened { get; set; } + public bool isNew { get; set; } + public DateTime? createdAt { get; set; } + public DateTime? changedAt { get; set; } + public int cancelSeconds { get; set; } + public bool isLiked { get; set; } + public bool canPurchase { get; set; } + public bool canReport { get; set; } +} diff --git a/OF DL/Entities/OFDLRequest.cs b/OF DL/Entities/OFDLRequest.cs index 718fac4..b5d4184 100644 --- a/OF DL/Entities/OFDLRequest.cs +++ b/OF DL/Entities/OFDLRequest.cs @@ -1,16 +1,12 @@ using Newtonsoft.Json; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class OFDLRequest { - public class OFDLRequest - { - [JsonProperty("pssh")] - public string PSSH { get; set; } = ""; + [JsonProperty("pssh")] public string PSSH { get; set; } = ""; - [JsonProperty("licenceURL")] - public string LicenseURL { get; set; } = ""; + [JsonProperty("licenceURL")] public string LicenseURL { get; set; } = ""; - [JsonProperty("headers")] - public string Headers { get; set; } = ""; - } + [JsonProperty("headers")] public string Headers { get; set; } = ""; } diff --git a/OF DL/Entities/Post/Post.cs b/OF DL/Entities/Post/Post.cs index 13613fd..de711d4 100644 --- a/OF DL/Entities/Post/Post.cs +++ b/OF DL/Entities/Post/Post.cs @@ -12,6 +12,7 @@ public class Post public string headMarker { get; set; } public string tailMarker { get; set; } + public class Author { public long id { get; set; } @@ -20,11 +21,9 @@ public class Post public class Dash { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } [JsonProperty("CloudFront-Key-Pair-Id")] public string CloudFrontKeyPairId { get; set; } @@ -72,11 +71,9 @@ public class Post public class Hls { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } [JsonProperty("CloudFront-Key-Pair-Id")] public string CloudFrontKeyPairId { get; set; } @@ -90,6 +87,7 @@ public class Post public class List { + private string _rawText; public string responseType { get; set; } public long id { get; set; } public DateTime postedAt { get; set; } @@ -98,23 +96,20 @@ public class Post public Author author { get; set; } public string text { get; set; } - private string _rawText; public string rawText { get { - if(string.IsNullOrEmpty(_rawText)) + if (string.IsNullOrEmpty(_rawText)) { _rawText = XmlUtils.EvaluateInnerText(text); } return _rawText; } - set - { - _rawText = value; - } + set => _rawText = value; } + public bool? lockedText { get; set; } public bool? isFavorite { get; set; } public bool? canReport { get; set; } @@ -197,11 +192,9 @@ public class Post public class VideoSources { - [JsonProperty("720")] - public object _720 { get; set; } + [JsonProperty("720")] public object _720 { get; set; } - [JsonProperty("240")] - public object _240 { get; set; } + [JsonProperty("240")] public object _240 { get; set; } } #pragma warning restore IDE1006 // Naming Styles } diff --git a/OF DL/Entities/Post/PostCollection.cs b/OF DL/Entities/Post/PostCollection.cs index bc2acbf..541c728 100644 --- a/OF DL/Entities/Post/PostCollection.cs +++ b/OF DL/Entities/Post/PostCollection.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Entities.Post +namespace OF_DL.Entities.Post; + +public class PostCollection { - public class PostCollection - { - public Dictionary Posts = new Dictionary(); - public List PostObjects = new List(); - public List PostMedia = new List(); - } + public List PostMedia = new(); + public List PostObjects = new(); + public Dictionary Posts = new(); } diff --git a/OF DL/Entities/Post/SinglePost.cs b/OF DL/Entities/Post/SinglePost.cs index 909e3e8..7f137fc 100644 --- a/OF DL/Entities/Post/SinglePost.cs +++ b/OF DL/Entities/Post/SinglePost.cs @@ -1,191 +1,188 @@ using Newtonsoft.Json; using OF_DL.Utils; -namespace OF_DL.Entities.Post +namespace OF_DL.Entities.Post; + +public class SinglePost { - public class SinglePost + private string _rawText; + public string responseType { get; set; } + public long id { get; set; } + public DateTime postedAt { get; set; } + public string postedAtPrecise { get; set; } + public object expiredAt { get; set; } + public Author author { get; set; } + public string text { get; set; } + + public string rawText + { + get + { + if (string.IsNullOrEmpty(_rawText)) + { + _rawText = XmlUtils.EvaluateInnerText(text); + } + + return _rawText; + } + set => _rawText = value; + } + + public bool lockedText { get; set; } + public bool isFavorite { get; set; } + public bool canReport { get; set; } + public bool canDelete { get; set; } + public bool canComment { get; set; } + public bool canEdit { get; set; } + public bool isPinned { get; set; } + public int favoritesCount { get; set; } + public int mediaCount { get; set; } + public bool isMediaReady { get; set; } + public object voting { get; set; } + public bool isOpened { get; set; } + public bool canToggleFavorite { get; set; } + public string streamId { get; set; } + public string price { get; set; } + public bool hasVoting { get; set; } + public bool isAddedToBookmarks { get; set; } + public bool isArchived { get; set; } + public bool isPrivateArchived { get; set; } + public bool isDeleted { get; set; } + public bool hasUrl { get; set; } + public bool isCouplePeopleMedia { get; set; } + public int commentsCount { get; set; } + public List mentionedUsers { get; set; } + public List linkedUsers { get; set; } + public string tipsAmount { get; set; } + public string tipsAmountRaw { get; set; } + public List media { get; set; } + public bool canViewMedia { get; set; } + public List preview { get; set; } + + public class Author { - public string responseType { get; set; } public long id { get; set; } - public DateTime postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - private string _rawText; - public string rawText - { - get - { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } + public string _view { get; set; } + } - return _rawText; - } - set - { - _rawText = value; - } - } - public bool lockedText { get; set; } - public bool isFavorite { get; set; } - public bool canReport { get; set; } - public bool canDelete { get; set; } - public bool canComment { get; set; } - public bool canEdit { get; set; } - public bool isPinned { get; set; } - public int favoritesCount { get; set; } - public int mediaCount { get; set; } - public bool isMediaReady { get; set; } - public object voting { get; set; } - public bool isOpened { get; set; } - public bool canToggleFavorite { get; set; } - public string streamId { get; set; } - public string price { get; set; } - public bool hasVoting { get; set; } - public bool isAddedToBookmarks { get; set; } - public bool isArchived { get; set; } - public bool isPrivateArchived { get; set; } - public bool isDeleted { get; set; } - public bool hasUrl { get; set; } - public bool isCouplePeopleMedia { get; set; } - public int commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public string tipsAmount { get; set; } - public string tipsAmountRaw { get; set; } - public List media { get; set; } - public bool canViewMedia { get; set; } - public List preview { get; set; } - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + public Drm drm { get; set; } + } - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class Info + { + public Source source { get; set; } + public Preview preview { get; set; } + } - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } + public class Medium + { + public long id { get; set; } + public string type { get; set; } + public bool convertedToVideo { get; set; } + public bool canView { get; set; } + public bool hasError { get; set; } + public DateTime? createdAt { get; set; } + public Info info { get; set; } + public Source source { get; set; } + public string squarePreview { get; set; } + public string full { get; set; } + public string preview { get; set; } + public string thumb { get; set; } + public bool hasCustomPreview { get; set; } + public Files files { get; set; } + public VideoSources videoSources { get; set; } + } - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Info info { get; set; } - public Source source { get; set; } - public string squarePreview { get; set; } - public string full { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public bool hasCustomPreview { get; set; } - public Files files { get; set; } - public VideoSources videoSources { get; set; } - } + public class Preview + { + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public string url { get; set; } + } - public class Preview - { - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public string url { get; set; } - } + public class Source + { + public string source { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public int duration { get; set; } + } - public class Source - { - public string source { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public int duration { get; set; } - } + public class VideoSources + { + [JsonProperty("720")] public string _720 { get; set; } - public class VideoSources - { - [JsonProperty("720")] - public string _720 { get; set; } + [JsonProperty("240")] public string _240 { get; set; } + } - [JsonProperty("240")] - public string _240 { get; set; } - } - public class Dash - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class Dash + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - public class Hls - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class Drm + { + public Manifest manifest { get; set; } + public Signature signature { get; set; } + } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + public class Hls + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - public class Manifest - { - public string? hls { get; set; } - public string? dash { get; set; } - } - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } + + public class Manifest + { + public string? hls { get; set; } + public string? dash { get; set; } + } + + public class Signature + { + public Hls hls { get; set; } + public Dash dash { get; set; } } } diff --git a/OF DL/Entities/Post/SinglePostCollection.cs b/OF DL/Entities/Post/SinglePostCollection.cs index 6d374e2..272dda8 100644 --- a/OF DL/Entities/Post/SinglePostCollection.cs +++ b/OF DL/Entities/Post/SinglePostCollection.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Entities.Post +namespace OF_DL.Entities.Post; + +public class SinglePostCollection { - public class SinglePostCollection - { - public Dictionary SinglePosts = new Dictionary(); - public List SinglePostObjects = new List(); - public List SinglePostMedia = new List(); - } + public List SinglePostMedia = new(); + public List SinglePostObjects = new(); + public Dictionary SinglePosts = new(); } diff --git a/OF DL/Entities/Purchased/PaidMessageCollection.cs b/OF DL/Entities/Purchased/PaidMessageCollection.cs index 09111eb..c75fc2c 100644 --- a/OF DL/Entities/Purchased/PaidMessageCollection.cs +++ b/OF DL/Entities/Purchased/PaidMessageCollection.cs @@ -1,11 +1,10 @@ using static OF_DL.Entities.Messages.Messages; -namespace OF_DL.Entities.Purchased +namespace OF_DL.Entities.Purchased; + +public class PaidMessageCollection { - public class PaidMessageCollection - { - public Dictionary PaidMessages = new Dictionary(); - public List PaidMessageObjects = new List(); - public List PaidMessageMedia = new List(); - } + public List PaidMessageMedia = new(); + public List PaidMessageObjects = new(); + public Dictionary PaidMessages = new(); } diff --git a/OF DL/Entities/Purchased/PaidPostCollection.cs b/OF DL/Entities/Purchased/PaidPostCollection.cs index 2e86d97..d0cfccf 100644 --- a/OF DL/Entities/Purchased/PaidPostCollection.cs +++ b/OF DL/Entities/Purchased/PaidPostCollection.cs @@ -1,11 +1,10 @@ using static OF_DL.Entities.Messages.Messages; -namespace OF_DL.Entities.Purchased +namespace OF_DL.Entities.Purchased; + +public class PaidPostCollection { - public class PaidPostCollection - { - public Dictionary PaidPosts = new Dictionary(); - public List PaidPostObjects = new List(); - public List PaidPostMedia = new List(); - } + public List PaidPostMedia = new(); + public List PaidPostObjects = new(); + public Dictionary PaidPosts = new(); } diff --git a/OF DL/Entities/Purchased/Purchased.cs b/OF DL/Entities/Purchased/Purchased.cs index 2e0c087..8b470b7 100644 --- a/OF DL/Entities/Purchased/Purchased.cs +++ b/OF DL/Entities/Purchased/Purchased.cs @@ -1,77 +1,75 @@ using Newtonsoft.Json; using static OF_DL.Entities.Messages.Messages; -namespace OF_DL.Entities.Purchased +namespace OF_DL.Entities.Purchased; + +public class Purchased { - public class Purchased + public List list { get; set; } + public bool hasMore { get; set; } + + public class FromUser { - public List list { get; set; } - public bool hasMore { get; set; } + public long id { get; set; } + public string _view { get; set; } + } - public class FromUser - { - public long id { get; set; } - public string _view { get; set; } - } - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } + public class Author + { + public long id { get; set; } + public string _view { get; set; } + } - public class Hls - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class Hls + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } - public class List - { - public string responseType { get; set; } - public string text { get; set; } - public object giphyId { get; set; } - public bool? lockedText { get; set; } - public bool? isFree { get; set; } - public string? price { get; set; } - public bool? isMediaReady { get; set; } - public int? mediaCount { get; set; } - public List media { get; set; } - public List previews { get; set; } - public List preview { get; set; } - public bool? isTip { get; set; } - public bool? isReportedByMe { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public object queueId { get; set; } - public FromUser fromUser { get; set; } - public Author author { get; set; } - public bool? isFromQueue { get; set; } - public bool? canUnsendQueue { get; set; } - public int? unsendSecondsQueue { get; set; } - public long id { get; set; } - public bool isOpened { get; set; } - public bool? isNew { get; set; } - public DateTime? createdAt { get; set; } - public DateTime? postedAt { get; set; } - public DateTime? changedAt { get; set; } - public int? cancelSeconds { get; set; } - public bool? isLiked { get; set; } - public bool? canPurchase { get; set; } - public bool? canReport { get; set; } - public bool? isCanceled { get; set; } - public bool? isArchived { get; set; } - } + public class List + { + public string responseType { get; set; } + public string text { get; set; } + public object giphyId { get; set; } + public bool? lockedText { get; set; } + public bool? isFree { get; set; } + public string? price { get; set; } + public bool? isMediaReady { get; set; } + public int? mediaCount { get; set; } + public List media { get; set; } + public List previews { get; set; } + public List preview { get; set; } + public bool? isTip { get; set; } + public bool? isReportedByMe { get; set; } + public bool? isCouplePeopleMedia { get; set; } + public object queueId { get; set; } + public FromUser fromUser { get; set; } + public Author author { get; set; } + public bool? isFromQueue { get; set; } + public bool? canUnsendQueue { get; set; } + public int? unsendSecondsQueue { get; set; } + public long id { get; set; } + public bool isOpened { get; set; } + public bool? isNew { get; set; } + public DateTime? createdAt { get; set; } + public DateTime? postedAt { get; set; } + public DateTime? changedAt { get; set; } + public int? cancelSeconds { get; set; } + public bool? isLiked { get; set; } + public bool? canPurchase { get; set; } + public bool? canReport { get; set; } + public bool? isCanceled { get; set; } + public bool? isArchived { get; set; } + } - public class Manifest - { - public string hls { get; set; } - public string dash { get; set; } - } + public class Manifest + { + public string hls { get; set; } + public string dash { get; set; } } } diff --git a/OF DL/Entities/Purchased/PurchasedTabCollection.cs b/OF DL/Entities/Purchased/PurchasedTabCollection.cs index 822c0dd..4d39a33 100644 --- a/OF DL/Entities/Purchased/PurchasedTabCollection.cs +++ b/OF DL/Entities/Purchased/PurchasedTabCollection.cs @@ -1,10 +1,9 @@ -namespace OF_DL.Entities.Purchased +namespace OF_DL.Entities.Purchased; + +public class PurchasedTabCollection { - public class PurchasedTabCollection - { - public long UserId { get; set; } - public string Username { get; set; } = string.Empty; - public PaidPostCollection PaidPosts { get; set; } = new PaidPostCollection(); - public PaidMessageCollection PaidMessages { get; set; } = new PaidMessageCollection(); - } + public long UserId { get; set; } + public string Username { get; set; } = string.Empty; + public PaidPostCollection PaidPosts { get; set; } = new(); + public PaidMessageCollection PaidMessages { get; set; } = new(); } diff --git a/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs b/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs index ba9515e..1e5c4dc 100644 --- a/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs +++ b/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs @@ -1,15 +1,14 @@ using OF_DL.Entities.Messages; using static OF_DL.Entities.Messages.Messages; -namespace OF_DL.Entities.Purchased -{ - public class SinglePaidMessageCollection - { - public Dictionary SingleMessages = new Dictionary(); - public List SingleMessageObjects = new List(); - public List SingleMessageMedia = new List(); +namespace OF_DL.Entities.Purchased; - public Dictionary PreviewSingleMessages = new Dictionary(); - public List PreviewSingleMessageMedia = new List(); - } +public class SinglePaidMessageCollection +{ + public List PreviewSingleMessageMedia = new(); + + public Dictionary PreviewSingleMessages = new(); + public List SingleMessageMedia = new(); + public List SingleMessageObjects = new(); + public Dictionary SingleMessages = new(); } diff --git a/OF DL/Entities/ShortDateConverter.cs b/OF DL/Entities/ShortDateConverter.cs index 93278f3..11ac1d6 100644 --- a/OF DL/Entities/ShortDateConverter.cs +++ b/OF DL/Entities/ShortDateConverter.cs @@ -1,12 +1,8 @@ using Newtonsoft.Json.Converters; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class ShortDateConverter : IsoDateTimeConverter { - public class ShortDateConverter : IsoDateTimeConverter - { - public ShortDateConverter() - { - DateTimeFormat = "yyyy-MM-dd"; - } - } + public ShortDateConverter() => DateTimeFormat = "yyyy-MM-dd"; } diff --git a/OF DL/Entities/Stories/Stories.cs b/OF DL/Entities/Stories/Stories.cs index 90f0291..dc03628 100644 --- a/OF DL/Entities/Stories/Stories.cs +++ b/OF DL/Entities/Stories/Stories.cs @@ -1,91 +1,90 @@ using Newtonsoft.Json; -namespace OF_DL.Entities.Stories +namespace OF_DL.Entities.Stories; + +public class Stories { - public class Stories + public long id { get; set; } + public long userId { get; set; } + public bool isWatched { get; set; } + public bool isReady { get; set; } + public List media { get; set; } + public DateTime? createdAt { get; set; } + public object question { get; set; } + public bool canLike { get; set; } + public bool isLiked { get; set; } + + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + } + + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } + + public class Medium { public long id { get; set; } - public long userId { get; set; } - public bool isWatched { get; set; } - public bool isReady { get; set; } - public List media { get; set; } + public string type { get; set; } + public bool convertedToVideo { get; set; } + public bool canView { get; set; } + public bool hasError { get; set; } public DateTime? createdAt { get; set; } - public object question { get; set; } - public bool canLike { get; set; } - public bool isLiked { get; set; } - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - } + public Files files { get; set; } + } - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } + public class Preview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Files files { get; set; } - } + public class Source + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public int duration { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } - public class Preview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } + public class Sources + { + [JsonProperty("720")] public object _720 { get; set; } - public class Source - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public int duration { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } + [JsonProperty("240")] public object _240 { get; set; } - public class Sources - { - [JsonProperty("720")] - public object _720 { get; set; } + public string w150 { get; set; } + public string w480 { get; set; } + } - [JsonProperty("240")] - public object _240 { get; set; } - public string w150 { get; set; } - public string w480 { get; set; } - } + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } } } diff --git a/OF DL/Entities/Streams/Streams.cs b/OF DL/Entities/Streams/Streams.cs index 13bc4b1..872d191 100644 --- a/OF DL/Entities/Streams/Streams.cs +++ b/OF DL/Entities/Streams/Streams.cs @@ -1,211 +1,209 @@ using Newtonsoft.Json; using OF_DL.Utils; -namespace OF_DL.Entities.Streams +namespace OF_DL.Entities.Streams; + +public class Streams { - public class Streams + public List list { get; set; } + public bool hasMore { get; set; } + public string headMarker { get; set; } + public string tailMarker { get; set; } + public Counters counters { get; set; } + + public class Author { - public List list { get; set; } - public bool hasMore { get; set; } - public string headMarker { get; set; } - public string tailMarker { get; set; } - public Counters counters { get; set; } - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } + public long id { get; set; } + public string _view { get; set; } + } - public class Counters - { - public int audiosCount { get; set; } - public int photosCount { get; set; } - public int videosCount { get; set; } - public int mediasCount { get; set; } - public int postsCount { get; set; } - public int streamsCount { get; set; } - public int archivedPostsCount { get; set; } - } + public class Counters + { + public int audiosCount { get; set; } + public int photosCount { get; set; } + public int videosCount { get; set; } + public int mediasCount { get; set; } + public int postsCount { get; set; } + public int streamsCount { get; set; } + public int archivedPostsCount { get; set; } + } - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + public Drm drm { get; set; } + } - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } + public class Info + { + public Source source { get; set; } + public Preview preview { get; set; } + } - public class List + public class List + { + private string _rawText; + public string responseType { get; set; } + public long id { get; set; } + public DateTime postedAt { get; set; } + public string postedAtPrecise { get; set; } + public object expiredAt { get; set; } + public Author author { get; set; } + public string text { get; set; } + + public string rawText { - public string responseType { get; set; } - public long id { get; set; } - public DateTime postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - private string _rawText; - public string rawText + get { - get + if (string.IsNullOrEmpty(_rawText)) { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } + _rawText = XmlUtils.EvaluateInnerText(text); + } - return _rawText; - } - set - { - _rawText = value; - } + return _rawText; } - public bool lockedText { get; set; } - public bool isFavorite { get; set; } - public bool canReport { get; set; } - public bool canDelete { get; set; } - public bool canComment { get; set; } - public bool canEdit { get; set; } - public bool isPinned { get; set; } - public int favoritesCount { get; set; } - public int mediaCount { get; set; } - public bool isMediaReady { get; set; } - public object voting { get; set; } - public bool isOpened { get; set; } - public bool canToggleFavorite { get; set; } - public int streamId { get; set; } - public string price { get; set; } - public bool hasVoting { get; set; } - public bool isAddedToBookmarks { get; set; } - public bool isArchived { get; set; } - public bool isPrivateArchived { get; set; } - public bool isDeleted { get; set; } - public bool hasUrl { get; set; } - public bool isCouplePeopleMedia { get; set; } - public string cantCommentReason { get; set; } - public int commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public string tipsAmount { get; set; } - public string tipsAmountRaw { get; set; } - public List media { get; set; } - public bool canViewMedia { get; set; } - public List preview { get; set; } + set => _rawText = value; } - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Info info { get; set; } - public Source source { get; set; } - public string squarePreview { get; set; } - public string full { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public bool hasCustomPreview { get; set; } - public Files files { get; set; } - public VideoSources videoSources { get; set; } - } + public bool lockedText { get; set; } + public bool isFavorite { get; set; } + public bool canReport { get; set; } + public bool canDelete { get; set; } + public bool canComment { get; set; } + public bool canEdit { get; set; } + public bool isPinned { get; set; } + public int favoritesCount { get; set; } + public int mediaCount { get; set; } + public bool isMediaReady { get; set; } + public object voting { get; set; } + public bool isOpened { get; set; } + public bool canToggleFavorite { get; set; } + public int streamId { get; set; } + public string price { get; set; } + public bool hasVoting { get; set; } + public bool isAddedToBookmarks { get; set; } + public bool isArchived { get; set; } + public bool isPrivateArchived { get; set; } + public bool isDeleted { get; set; } + public bool hasUrl { get; set; } + public bool isCouplePeopleMedia { get; set; } + public string cantCommentReason { get; set; } + public int commentsCount { get; set; } + public List mentionedUsers { get; set; } + public List linkedUsers { get; set; } + public string tipsAmount { get; set; } + public string tipsAmountRaw { get; set; } + public List media { get; set; } + public bool canViewMedia { get; set; } + public List preview { get; set; } + } - public class Preview - { - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public string url { get; set; } - } + public class Medium + { + public long id { get; set; } + public string type { get; set; } + public bool convertedToVideo { get; set; } + public bool canView { get; set; } + public bool hasError { get; set; } + public DateTime? createdAt { get; set; } + public Info info { get; set; } + public Source source { get; set; } + public string squarePreview { get; set; } + public string full { get; set; } + public string preview { get; set; } + public string thumb { get; set; } + public bool hasCustomPreview { get; set; } + public Files files { get; set; } + public VideoSources videoSources { get; set; } + } - public class Source - { - public string source { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public int duration { get; set; } - } + public class Preview + { + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public string url { get; set; } + } - public class VideoSources - { - [JsonProperty("720")] - public object _720 { get; set; } + public class Source + { + public string source { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public int duration { get; set; } + } - [JsonProperty("240")] - public object _240 { get; set; } - } - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - public class Manifest - { - public string? hls { get; set; } - public string? dash { get; set; } - } - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } - public class Hls - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class VideoSources + { + [JsonProperty("720")] public object _720 { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + [JsonProperty("240")] public object _240 { get; set; } + } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - public class Dash - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class Drm + { + public Manifest manifest { get; set; } + public Signature signature { get; set; } + } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + public class Manifest + { + public string? hls { get; set; } + public string? dash { get; set; } + } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } + public class Signature + { + public Hls hls { get; set; } + public Dash dash { get; set; } + } + + public class Hls + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } + + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } + + public class Dash + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } + + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } } } diff --git a/OF DL/Entities/Streams/StreamsCollection.cs b/OF DL/Entities/Streams/StreamsCollection.cs index c9a76f2..bc895a3 100644 --- a/OF DL/Entities/Streams/StreamsCollection.cs +++ b/OF DL/Entities/Streams/StreamsCollection.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Entities.Streams +namespace OF_DL.Entities.Streams; + +public class StreamsCollection { - public class StreamsCollection - { - public Dictionary Streams = new Dictionary(); - public List StreamObjects = new List(); - public List StreamMedia = new List(); - } + public List StreamMedia = new(); + public List StreamObjects = new(); + public Dictionary Streams = new(); } diff --git a/OF DL/Entities/Subscriptions.cs b/OF DL/Entities/Subscriptions.cs index b5da541..22973d6 100644 --- a/OF DL/Entities/Subscriptions.cs +++ b/OF DL/Entities/Subscriptions.cs @@ -1,159 +1,159 @@ -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class Subscriptions { - public class Subscriptions + public List list { get; set; } + public bool hasMore { get; set; } + + public class AvatarThumbs { - public List list { get; set; } - public bool hasMore { get; set; } - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } + public string c50 { get; set; } + public string c144 { get; set; } + } - public class HeaderSize - { - public int? width { get; set; } - public int? height { get; set; } - } + public class HeaderSize + { + public int? width { get; set; } + public int? height { get; set; } + } - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } + public class HeaderThumbs + { + public string w480 { get; set; } + public string w760 { get; set; } + } - public class List - { - public string view { get; set; } - public string avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long id { get; set; } - public string name { get; set; } - public string username { get; set; } - public bool? canLookStory { get; set; } - public bool? canCommentStory { get; set; } - public bool? hasNotViewedStory { get; set; } - public bool? isVerified { get; set; } - public bool? canPayInternal { get; set; } - public bool? hasScheduledStream { get; set; } - public bool? hasStream { get; set; } - public bool? hasStories { get; set; } - public bool? tipsEnabled { get; set; } - public bool? tipsTextEnabled { get; set; } - public int? tipsMin { get; set; } - public int? tipsMinInternal { get; set; } - public int? tipsMax { get; set; } - public bool? canEarn { get; set; } - public bool? canAddSubscriber { get; set; } - public string? subscribePrice { get; set; } - public bool? isPaywallRequired { get; set; } - public bool? unprofitable { get; set; } - public List listsStates { get; set; } - public bool? isMuted { get; set; } - public bool? isRestricted { get; set; } - public bool? canRestrict { get; set; } - public bool? subscribedBy { get; set; } - public bool? subscribedByExpire { get; set; } - public DateTime? subscribedByExpireDate { get; set; } - public bool? subscribedByAutoprolong { get; set; } - public bool? subscribedIsExpiredNow { get; set; } - public string? currentSubscribePrice { get; set; } - public bool? subscribedOn { get; set; } - public bool? subscribedOnExpiredNow { get; set; } - public string subscribedOnDuration { get; set; } - public bool? canReport { get; set; } - public bool? canReceiveChatMessage { get; set; } - public bool? hideChat { get; set; } - public DateTime? lastSeen { get; set; } - public bool? isPerformer { get; set; } - public bool? isRealPerformer { get; set; } - public SubscribedByData subscribedByData { get; set; } - public SubscribedOnData subscribedOnData { get; set; } - public bool? canTrialSend { get; set; } - public bool? isBlocked { get; set; } - public string displayName { get; set; } - public string notice { get; set; } - } + public class List + { + public string view { get; set; } + public string avatar { get; set; } + public AvatarThumbs avatarThumbs { get; set; } + public string header { get; set; } + public HeaderSize headerSize { get; set; } + public HeaderThumbs headerThumbs { get; set; } + public long id { get; set; } + public string name { get; set; } + public string username { get; set; } + public bool? canLookStory { get; set; } + public bool? canCommentStory { get; set; } + public bool? hasNotViewedStory { get; set; } + public bool? isVerified { get; set; } + public bool? canPayInternal { get; set; } + public bool? hasScheduledStream { get; set; } + public bool? hasStream { get; set; } + public bool? hasStories { get; set; } + public bool? tipsEnabled { get; set; } + public bool? tipsTextEnabled { get; set; } + public int? tipsMin { get; set; } + public int? tipsMinInternal { get; set; } + public int? tipsMax { get; set; } + public bool? canEarn { get; set; } + public bool? canAddSubscriber { get; set; } + public string? subscribePrice { get; set; } + public bool? isPaywallRequired { get; set; } + public bool? unprofitable { get; set; } + public List listsStates { get; set; } + public bool? isMuted { get; set; } + public bool? isRestricted { get; set; } + public bool? canRestrict { get; set; } + public bool? subscribedBy { get; set; } + public bool? subscribedByExpire { get; set; } + public DateTime? subscribedByExpireDate { get; set; } + public bool? subscribedByAutoprolong { get; set; } + public bool? subscribedIsExpiredNow { get; set; } + public string? currentSubscribePrice { get; set; } + public bool? subscribedOn { get; set; } + public bool? subscribedOnExpiredNow { get; set; } + public string subscribedOnDuration { get; set; } + public bool? canReport { get; set; } + public bool? canReceiveChatMessage { get; set; } + public bool? hideChat { get; set; } + public DateTime? lastSeen { get; set; } + public bool? isPerformer { get; set; } + public bool? isRealPerformer { get; set; } + public SubscribedByData subscribedByData { get; set; } + public SubscribedOnData subscribedOnData { get; set; } + public bool? canTrialSend { get; set; } + public bool? isBlocked { get; set; } + public string displayName { get; set; } + public string notice { get; set; } + } - public class ListsState - { - public object id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool? hasUser { get; set; } - public bool? canAddUser { get; set; } - } + public class ListsState + { + public object id { get; set; } + public string type { get; set; } + public string name { get; set; } + public bool? hasUser { get; set; } + public bool? canAddUser { get; set; } + } - public class Subscribe - { - public object id { get; set; } - public long? userId { get; set; } - public int? subscriberId { get; set; } - public DateTime? date { get; set; } - public int? duration { get; set; } - public DateTime? startDate { get; set; } - public DateTime? expireDate { get; set; } - public object cancelDate { get; set; } - public string? price { get; set; } - public string? regularPrice { get; set; } - public string? discount { get; set; } - public string action { get; set; } - public string type { get; set; } - public object offerStart { get; set; } - public object offerEnd { get; set; } - public bool? isCurrent { get; set; } - } + public class Subscribe + { + public object id { get; set; } + public long? userId { get; set; } + public int? subscriberId { get; set; } + public DateTime? date { get; set; } + public int? duration { get; set; } + public DateTime? startDate { get; set; } + public DateTime? expireDate { get; set; } + public object cancelDate { get; set; } + public string? price { get; set; } + public string? regularPrice { get; set; } + public string? discount { get; set; } + public string action { get; set; } + public string type { get; set; } + public object offerStart { get; set; } + public object offerEnd { get; set; } + public bool? isCurrent { get; set; } + } - public class SubscribedByData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public DateTime? renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public string status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public bool? showPostsInFeed { get; set; } - public List subscribes { get; set; } - public bool? hasActivePaidSubscriptions { get; set; } - } + public class SubscribedByData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } + public string? subscribePrice { get; set; } + public int? discountPercent { get; set; } + public int? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public DateTime? renewedAt { get; set; } + public object discountFinishedAt { get; set; } + public object discountStartedAt { get; set; } + public string status { get; set; } + public bool? isMuted { get; set; } + public string unsubscribeReason { get; set; } + public string duration { get; set; } + public bool? showPostsInFeed { get; set; } + public List subscribes { get; set; } + public bool? hasActivePaidSubscriptions { get; set; } + } - public class SubscribedOnData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public DateTime? renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public object status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public string? tipsSumm { get; set; } - public string? subscribesSumm { get; set; } - public string? messagesSumm { get; set; } - public string? postsSumm { get; set; } - public string? streamsSumm { get; set; } - public string? totalSumm { get; set; } - public List subscribes { get; set; } - public bool? hasActivePaidSubscriptions { get; set; } - } + public class SubscribedOnData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } + public string? subscribePrice { get; set; } + public int? discountPercent { get; set; } + public int? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public DateTime? renewedAt { get; set; } + public object discountFinishedAt { get; set; } + public object discountStartedAt { get; set; } + public object status { get; set; } + public bool? isMuted { get; set; } + public string unsubscribeReason { get; set; } + public string duration { get; set; } + public string? tipsSumm { get; set; } + public string? subscribesSumm { get; set; } + public string? messagesSumm { get; set; } + public string? postsSumm { get; set; } + public string? streamsSumm { get; set; } + public string? totalSumm { get; set; } + public List subscribes { get; set; } + public bool? hasActivePaidSubscriptions { get; set; } } } diff --git a/OF DL/Entities/ToggleableConfigAttribute.cs b/OF DL/Entities/ToggleableConfigAttribute.cs index ff0a4e8..62d413f 100644 --- a/OF DL/Entities/ToggleableConfigAttribute.cs +++ b/OF DL/Entities/ToggleableConfigAttribute.cs @@ -1,8 +1,6 @@ -namespace OF_DL.Entities -{ - [AttributeUsage(AttributeTargets.Property)] - internal class ToggleableConfigAttribute : Attribute - { - } +namespace OF_DL.Entities; +[AttributeUsage(AttributeTargets.Property)] +internal class ToggleableConfigAttribute : Attribute +{ } diff --git a/OF DL/Entities/User.cs b/OF DL/Entities/User.cs index 1273c2b..606f2ac 100644 --- a/OF DL/Entities/User.cs +++ b/OF DL/Entities/User.cs @@ -1,185 +1,185 @@ -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class User { - public class User + public string view { get; set; } + public string? avatar { get; set; } + public AvatarThumbs avatarThumbs { get; set; } + public string? header { get; set; } + public HeaderSize headerSize { get; set; } + public HeaderThumbs headerThumbs { get; set; } + public long? id { get; set; } + public string name { get; set; } + public string username { get; set; } + public bool? canLookStory { get; set; } + public bool? canCommentStory { get; set; } + public bool? hasNotViewedStory { get; set; } + public bool? isVerified { get; set; } + public bool? canPayInternal { get; set; } + public bool? hasScheduledStream { get; set; } + public bool? hasStream { get; set; } + public bool? hasStories { get; set; } + public bool? tipsEnabled { get; set; } + public bool? tipsTextEnabled { get; set; } + public int? tipsMin { get; set; } + public int? tipsMinInternal { get; set; } + public int? tipsMax { get; set; } + public bool? canEarn { get; set; } + public bool? canAddSubscriber { get; set; } + public string? subscribePrice { get; set; } + public string displayName { get; set; } + public string notice { get; set; } + public bool? isPaywallRequired { get; set; } + public bool? unprofitable { get; set; } + public List listsStates { get; set; } + public bool? isMuted { get; set; } + public bool? isRestricted { get; set; } + public bool? canRestrict { get; set; } + public bool? subscribedBy { get; set; } + public bool? subscribedByExpire { get; set; } + public DateTime? subscribedByExpireDate { get; set; } + public bool? subscribedByAutoprolong { get; set; } + public bool? subscribedIsExpiredNow { get; set; } + public string? currentSubscribePrice { get; set; } + public bool? subscribedOn { get; set; } + public bool? subscribedOnExpiredNow { get; set; } + public string subscribedOnDuration { get; set; } + public DateTime? joinDate { get; set; } + public bool? isReferrerAllowed { get; set; } + public string about { get; set; } + public string rawAbout { get; set; } + public object website { get; set; } + public object wishlist { get; set; } + public object location { get; set; } + public int? postsCount { get; set; } + public int? archivedPostsCount { get; set; } + public int? privateArchivedPostsCount { get; set; } + public int? photosCount { get; set; } + public int? videosCount { get; set; } + public int? audiosCount { get; set; } + public int? mediasCount { get; set; } + public DateTime? lastSeen { get; set; } + public int? favoritesCount { get; set; } + public int? favoritedCount { get; set; } + public bool? showPostsInFeed { get; set; } + public bool? canReceiveChatMessage { get; set; } + public bool? isPerformer { get; set; } + public bool? isRealPerformer { get; set; } + public bool? isSpotifyConnected { get; set; } + public int? subscribersCount { get; set; } + public bool? hasPinnedPosts { get; set; } + public bool? hasLabels { get; set; } + public bool? canChat { get; set; } + public string? callPrice { get; set; } + public bool? isPrivateRestriction { get; set; } + public bool? showSubscribersCount { get; set; } + public bool? showMediaCount { get; set; } + public SubscribedByData subscribedByData { get; set; } + public SubscribedOnData subscribedOnData { get; set; } + public bool? canPromotion { get; set; } + public bool? canCreatePromotion { get; set; } + public bool? canCreateTrial { get; set; } + public bool? isAdultContent { get; set; } + public bool? canTrialSend { get; set; } + public bool? hadEnoughLastPhotos { get; set; } + public bool? hasLinks { get; set; } + public DateTime? firstPublishedPostDate { get; set; } + public bool? isSpringConnected { get; set; } + public bool? isFriend { get; set; } + public bool? isBlocked { get; set; } + public bool? canReport { get; set; } + + public class AvatarThumbs { - public string view { get; set; } - public string? avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string? header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long? id { get; set; } + public string c50 { get; set; } + public string c144 { get; set; } + } + + public class HeaderSize + { + public int? width { get; set; } + public int? height { get; set; } + } + + public class HeaderThumbs + { + public string w480 { get; set; } + public string w760 { get; set; } + } + + public class ListsState + { + public string id { get; set; } + public string type { get; set; } public string name { get; set; } - public string username { get; set; } - public bool? canLookStory { get; set; } - public bool? canCommentStory { get; set; } - public bool? hasNotViewedStory { get; set; } - public bool? isVerified { get; set; } - public bool? canPayInternal { get; set; } - public bool? hasScheduledStream { get; set; } - public bool? hasStream { get; set; } - public bool? hasStories { get; set; } - public bool? tipsEnabled { get; set; } - public bool? tipsTextEnabled { get; set; } - public int? tipsMin { get; set; } - public int? tipsMinInternal { get; set; } - public int? tipsMax { get; set; } - public bool? canEarn { get; set; } - public bool? canAddSubscriber { get; set; } + public bool hasUser { get; set; } + public bool canAddUser { get; set; } + } + + public class Subscribe + { + public long? id { get; set; } + public long? userId { get; set; } + public int? subscriberId { get; set; } + public DateTime? date { get; set; } + public int? duration { get; set; } + public DateTime? startDate { get; set; } + public DateTime? expireDate { get; set; } + public object cancelDate { get; set; } + public string? price { get; set; } + public string? regularPrice { get; set; } + public int? discount { get; set; } + public string action { get; set; } + public string type { get; set; } + public object offerStart { get; set; } + public object offerEnd { get; set; } + public bool? isCurrent { get; set; } + } + + public class SubscribedByData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } public string? subscribePrice { get; set; } - public string displayName { get; set; } - public string notice { get; set; } - public bool? isPaywallRequired { get; set; } - public bool? unprofitable { get; set; } - public List listsStates { get; set; } + public int? discountPercent { get; set; } + public int? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public object? renewedAt { get; set; } + public object? discountFinishedAt { get; set; } + public object? discountStartedAt { get; set; } + public string? status { get; set; } public bool? isMuted { get; set; } - public bool? isRestricted { get; set; } - public bool? canRestrict { get; set; } - public bool? subscribedBy { get; set; } - public bool? subscribedByExpire { get; set; } - public DateTime? subscribedByExpireDate { get; set; } - public bool? subscribedByAutoprolong { get; set; } - public bool? subscribedIsExpiredNow { get; set; } - public string? currentSubscribePrice { get; set; } - public bool? subscribedOn { get; set; } - public bool? subscribedOnExpiredNow { get; set; } - public string subscribedOnDuration { get; set; } - public DateTime? joinDate { get; set; } - public bool? isReferrerAllowed { get; set; } - public string about { get; set; } - public string rawAbout { get; set; } - public object website { get; set; } - public object wishlist { get; set; } - public object location { get; set; } - public int? postsCount { get; set; } - public int? archivedPostsCount { get; set; } - public int? privateArchivedPostsCount { get; set; } - public int? photosCount { get; set; } - public int? videosCount { get; set; } - public int? audiosCount { get; set; } - public int? mediasCount { get; set; } - public DateTime? lastSeen { get; set; } - public int? favoritesCount { get; set; } - public int? favoritedCount { get; set; } + public string? unsubscribeReason { get; set; } + public string? duration { get; set; } public bool? showPostsInFeed { get; set; } - public bool? canReceiveChatMessage { get; set; } - public bool? isPerformer { get; set; } - public bool? isRealPerformer { get; set; } - public bool? isSpotifyConnected { get; set; } - public int? subscribersCount { get; set; } - public bool? hasPinnedPosts { get; set; } - public bool? hasLabels { get; set; } - public bool? canChat { get; set; } - public string? callPrice { get; set; } - public bool? isPrivateRestriction { get; set; } - public bool? showSubscribersCount { get; set; } - public bool? showMediaCount { get; set; } - public SubscribedByData subscribedByData { get; set; } - public SubscribedOnData subscribedOnData { get; set; } - public bool? canPromotion { get; set; } - public bool? canCreatePromotion { get; set; } - public bool? canCreateTrial { get; set; } - public bool? isAdultContent { get; set; } - public bool? canTrialSend { get; set; } - public bool? hadEnoughLastPhotos { get; set; } - public bool? hasLinks { get; set; } - public DateTime? firstPublishedPostDate { get; set; } - public bool? isSpringConnected { get; set; } - public bool? isFriend { get; set; } - public bool? isBlocked { get; set; } - public bool? canReport { get; set; } - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } + public List? subscribes { get; set; } + } - public class HeaderSize - { - public int? width { get; set; } - public int? height { get; set; } - } - - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } - - public class ListsState - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool hasUser { get; set; } - public bool canAddUser { get; set; } - } - - public class Subscribe - { - public long? id { get; set; } - public long? userId { get; set; } - public int? subscriberId { get; set; } - public DateTime? date { get; set; } - public int? duration { get; set; } - public DateTime? startDate { get; set; } - public DateTime? expireDate { get; set; } - public object cancelDate { get; set; } - public string? price { get; set; } - public string? regularPrice { get; set; } - public int? discount { get; set; } - public string action { get; set; } - public string type { get; set; } - public object offerStart { get; set; } - public object offerEnd { get; set; } - public bool? isCurrent { get; set; } - } - - public class SubscribedByData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public object? renewedAt { get; set; } - public object? discountFinishedAt { get; set; } - public object? discountStartedAt { get; set; } - public string? status { get; set; } - public bool? isMuted { get; set; } - public string? unsubscribeReason { get; set; } - public string? duration { get; set; } - public bool? showPostsInFeed { get; set; } - public List? subscribes { get; set; } - } - - public class SubscribedOnData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public DateTime? renewedAt { get; set; } - public object? discountFinishedAt { get; set; } - public object? discountStartedAt { get; set; } - public object? status { get; set; } - public bool? isMuted { get; set; } - public string? unsubscribeReason { get; set; } - public string? duration { get; set; } - public string? tipsSumm { get; set; } - public string? subscribesSumm { get; set; } - public string? messagesSumm { get; set; } - public string? postsSumm { get; set; } - public string? streamsSumm { get; set; } - public string? totalSumm { get; set; } - public List? subscribes { get; set; } - } + public class SubscribedOnData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } + public string? subscribePrice { get; set; } + public int? discountPercent { get; set; } + public int? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public DateTime? renewedAt { get; set; } + public object? discountFinishedAt { get; set; } + public object? discountStartedAt { get; set; } + public object? status { get; set; } + public bool? isMuted { get; set; } + public string? unsubscribeReason { get; set; } + public string? duration { get; set; } + public string? tipsSumm { get; set; } + public string? subscribesSumm { get; set; } + public string? messagesSumm { get; set; } + public string? postsSumm { get; set; } + public string? streamsSumm { get; set; } + public string? totalSumm { get; set; } + public List? subscribes { get; set; } } } diff --git a/OF DL/Enumerations/CustomFileNameOption.cs b/OF DL/Enumerations/CustomFileNameOption.cs index 4e9659f..6656873 100644 --- a/OF DL/Enumerations/CustomFileNameOption.cs +++ b/OF DL/Enumerations/CustomFileNameOption.cs @@ -3,5 +3,5 @@ namespace OF_DL.Enumerations; public enum CustomFileNameOption { ReturnOriginal, - ReturnEmpty, + ReturnEmpty } diff --git a/OF DL/Enumerations/DownloadDateSelection.cs b/OF DL/Enumerations/DownloadDateSelection.cs index e40c38f..2f1d949 100644 --- a/OF DL/Enumerations/DownloadDateSelection.cs +++ b/OF DL/Enumerations/DownloadDateSelection.cs @@ -1,8 +1,7 @@ -namespace OF_DL.Enumerations +namespace OF_DL.Enumerations; + +public enum DownloadDateSelection { - public enum DownloadDateSelection - { - before, - after, - } + before, + after } diff --git a/OF DL/Enumerations/LoggingLevel.cs b/OF DL/Enumerations/LoggingLevel.cs index 876c527..f9c1d35 100644 --- a/OF DL/Enumerations/LoggingLevel.cs +++ b/OF DL/Enumerations/LoggingLevel.cs @@ -1,30 +1,34 @@ -namespace OF_DL.Enumerations +namespace OF_DL.Enumerations; + +public enum LoggingLevel { - public enum LoggingLevel - { - // - // Summary: - // Anything and everything you might want to know about a running block of code. - Verbose, - // - // Summary: - // Internal system events that aren't necessarily observable from the outside. - Debug, - // - // Summary: - // The lifeblood of operational intelligence - things happen. - Information, - // - // Summary: - // Service is degraded or endangered. - Warning, - // - // Summary: - // Functionality is unavailable, invariants are broken or data is lost. - Error, - // - // Summary: - // If you have a pager, it goes off when one of these occurs. - Fatal - } + // + // Summary: + // Anything and everything you might want to know about a running block of code. + Verbose, + + // + // Summary: + // Internal system events that aren't necessarily observable from the outside. + Debug, + + // + // Summary: + // The lifeblood of operational intelligence - things happen. + Information, + + // + // Summary: + // Service is degraded or endangered. + Warning, + + // + // Summary: + // Functionality is unavailable, invariants are broken or data is lost. + Error, + + // + // Summary: + // If you have a pager, it goes off when one of these occurs. + Fatal } diff --git a/OF DL/Enumerations/MediaType.cs b/OF DL/Enumerations/MediaType.cs index 38e3e81..da579c6 100644 --- a/OF DL/Enumerations/MediaType.cs +++ b/OF DL/Enumerations/MediaType.cs @@ -1,13 +1,12 @@ -namespace OF_DL.Enumerations +namespace OF_DL.Enumerations; + +public enum MediaType { - public enum MediaType - { - PaidPosts = 10, - Posts = 20, - Archived = 30, - Stories = 40, - Highlights = 50, - Messages = 60, - PaidMessages = 70 - } + PaidPosts = 10, + Posts = 20, + Archived = 30, + Stories = 40, + Highlights = 50, + Messages = 60, + PaidMessages = 70 } diff --git a/OF DL/Enumerations/VideoResolution.cs b/OF DL/Enumerations/VideoResolution.cs index 6c2f340..d07a1fd 100644 --- a/OF DL/Enumerations/VideoResolution.cs +++ b/OF DL/Enumerations/VideoResolution.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Enumerations +namespace OF_DL.Enumerations; + +public enum VideoResolution { - public enum VideoResolution - { - _240, - _720, - source - } + _240, + _720, + source } diff --git a/OF DL/Helpers/DownloadContext.cs b/OF DL/Helpers/DownloadContext.cs index cae7598..00f41f1 100644 --- a/OF DL/Helpers/DownloadContext.cs +++ b/OF DL/Helpers/DownloadContext.cs @@ -1,26 +1,25 @@ using OF_DL.Entities; using OF_DL.Services; -namespace OF_DL.Helpers -{ - internal interface IDownloadContext - { - public IFileNameFormatConfig FileNameFormatConfig { get; } - public IAPIService ApiService { get; } - public IDBService DBService { get; } - public IDownloadService DownloadService { get; } - } +namespace OF_DL.Helpers; - internal class DownloadContext( - IFileNameFormatConfig fileNameFormatConfig, - IAPIService apiService, - IDBService dBService, - IDownloadService downloadService) - : IDownloadContext - { - public IAPIService ApiService { get; } = apiService; - public IDBService DBService { get; } = dBService; - public IDownloadService DownloadService { get; } = downloadService; - public IFileNameFormatConfig FileNameFormatConfig { get; } = fileNameFormatConfig; - } +internal interface IDownloadContext +{ + public IFileNameFormatConfig FileNameFormatConfig { get; } + public IAPIService ApiService { get; } + public IDBService DBService { get; } + public IDownloadService DownloadService { get; } +} + +internal class DownloadContext( + IFileNameFormatConfig fileNameFormatConfig, + IAPIService apiService, + IDBService dBService, + IDownloadService downloadService) + : IDownloadContext +{ + public IAPIService ApiService { get; } = apiService; + public IDBService DBService { get; } = dBService; + public IDownloadService DownloadService { get; } = downloadService; + public IFileNameFormatConfig FileNameFormatConfig { get; } = fileNameFormatConfig; } diff --git a/OF DL/Helpers/IFileNameHelper.cs b/OF DL/Helpers/IFileNameHelper.cs index a3cc701..6454742 100644 --- a/OF DL/Helpers/IFileNameHelper.cs +++ b/OF DL/Helpers/IFileNameHelper.cs @@ -1,8 +1,9 @@ -namespace OF_DL.Helpers +namespace OF_DL.Helpers; + +public interface IFileNameHelper { - public interface IFileNameHelper - { - Task BuildFilename(string fileFormat, Dictionary values); - Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, string username, Dictionary users = null); - } + Task BuildFilename(string fileFormat, Dictionary values); + + Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, + string username, Dictionary users = null); } diff --git a/OF DL/Helpers/VersionHelper.cs b/OF DL/Helpers/VersionHelper.cs index dc2b163..8d6913a 100644 --- a/OF DL/Helpers/VersionHelper.cs +++ b/OF DL/Helpers/VersionHelper.cs @@ -6,15 +6,15 @@ namespace OF_DL.Helpers; public static class VersionHelper { - private static readonly HttpClient httpClient = new HttpClient(); private const string url = "https://git.ofdl.tools/api/v1/repos/sim0n00ps/OF-DL/releases/latest"; + private static readonly HttpClient httpClient = new(); public static async Task GetLatestReleaseTag(CancellationToken cancellationToken = default) { Log.Debug("Calling GetLatestReleaseTag"); try { - var response = await httpClient.GetAsync(url, cancellationToken); + HttpResponseMessage response = await httpClient.GetAsync(url, cancellationToken); if (!response.IsSuccessStatusCode) { @@ -22,12 +22,13 @@ public static class VersionHelper return null; } - var body = await response.Content.ReadAsStringAsync(); + string body = await response.Content.ReadAsStringAsync(); Log.Debug("GetLatestReleaseTag API Response: "); Log.Debug(body); - var versionCheckResponse = JsonConvert.DeserializeObject(body); + LatestReleaseAPIResponse? versionCheckResponse = + JsonConvert.DeserializeObject(body); if (versionCheckResponse == null || versionCheckResponse.TagName == "") { @@ -48,10 +49,13 @@ public static class VersionHelper 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); + 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); } } + return null; } } diff --git a/OF DL/OF DL.csproj b/OF DL/OF DL.csproj index be35c42..a57dfe5 100644 --- a/OF DL/OF DL.csproj +++ b/OF DL/OF DL.csproj @@ -10,23 +10,23 @@ - + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 54d6934..754e8c4 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -1,5 +1,11 @@ +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OF_DL.CLI; using OF_DL.Entities; using OF_DL.Entities.Archived; using OF_DL.Entities.Messages; @@ -7,88 +13,90 @@ using OF_DL.Entities.Post; using OF_DL.Entities.Purchased; using OF_DL.Entities.Streams; using OF_DL.Enumerations; -using OF_DL.CLI; using OF_DL.Helpers; using OF_DL.Services; using Serilog; using Spectre.Console; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using static OF_DL.Entities.Messages.Messages; -using Microsoft.Extensions.DependencyInjection; using Constants = OF_DL.Widevine.Constants; namespace OF_DL; public class Program(IServiceProvider serviceProvider) { - public static List paid_post_ids = new(); + public static List paid_post_ids = new(); - private static bool clientIdBlobMissing = false; - private static bool devicePrivateKeyMissing = false; + private static bool clientIdBlobMissing; + private static bool devicePrivateKeyMissing; private async Task LoadAuthFromBrowser() - { - var authService = serviceProvider.GetRequiredService(); - bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; + { + IAuthService authService = serviceProvider.GetRequiredService(); + bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; - // Show initial message - AnsiConsole.MarkupLine($"[yellow]Downloading dependencies. Please wait ...[/]"); + // Show 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]Note: Some users have reported that \"Sign in with Google\" has not been working with the new authentication method.[/]"); - 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[/]"); - } + // 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]Note: Some users have reported that \"Sign in with Google\" has not been working with the new authentication method.[/]"); + 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(); + // 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"); + 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); - } + Environment.Exit(2); + } - await authService.SaveToFileAsync(); - } + 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"); + 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); - var serviceProvider = services.BuildServiceProvider(); + ServiceProvider serviceProvider = services.BuildServiceProvider(); - // Get the Program instance and run - var program = serviceProvider.GetRequiredService(); - await program.RunAsync(); - } + // Get the Program instance and run + Program program = serviceProvider.GetRequiredService(); + await program.RunAsync(); + } - private static async Task ConfigureServices(string[] args) - { + private static async Task ConfigureServices(string[] args) + { // Set up dependency injection with LoggingService and ConfigService ServiceCollection services = new(); services.AddSingleton(); @@ -101,12 +109,13 @@ public class Program(IServiceProvider serviceProvider) if (!await configService.LoadConfigurationAsync(args)) { - AnsiConsole.MarkupLine($"\n[red]config.conf is not valid, check your syntax![/]\n"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); + AnsiConsole.MarkupLine("\n[red]config.conf is not valid, check your syntax![/]\n"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); if (!configService.IsCliNonInteractive) { Console.ReadKey(); } + Environment.Exit(3); } @@ -114,388 +123,422 @@ public class Program(IServiceProvider serviceProvider) // 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(loggingService); + services.AddSingleton(configService); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); return services; } - private async Task RunAsync() - { + private async Task RunAsync() + { IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); IAPIService apiService = serviceProvider.GetRequiredService(); - try - { - var os = Environment.OSVersion; + try + { + OperatingSystem os = Environment.OSVersion; - Log.Debug($"Operating system information: {os.VersionString}"); + Log.Debug($"Operating system information: {os.VersionString}"); - if (os.Platform == PlatformID.Win32NT) - { - // check if this is windows 10+ - if (os.Version.Major < 10) - { - 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", os.VersionString); - Console.Write("Press any key to continue.\n"); - Log.Error("Windows version prior to 10.x: {0}", os.VersionString); + if (os.Platform == PlatformID.Win32NT) + { + // check if this is windows 10+ + if (os.Version.Major < 10) + { + 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", + os.VersionString); + Console.Write("Press any key to continue.\n"); + Log.Error("Windows version prior to 10.x: {0}", os.VersionString); - if (!configService.CurrentConfig.NonInteractiveMode) - { - Console.ReadKey(); - } - Environment.Exit(1); - } - else - { - AnsiConsole.Markup("[green]Valid version of Windows found.\n[/]"); - } - } + if (!configService.CurrentConfig.NonInteractiveMode) + { + Console.ReadKey(); + } + + Environment.Exit(1); + } + else + { + AnsiConsole.Markup("[green]Valid version of Windows found.\n[/]"); + } + } try - { - // Only run the version check if not in DEBUG mode - #if !DEBUG - Version localVersion = Assembly.GetEntryAssembly()?.GetName().Version; //Only tested with numeric values. + { + // Only run the version check if not in DEBUG mode +#if !DEBUG + Version localVersion = + Assembly.GetEntryAssembly()?.GetName().Version; //Only tested with numeric values. - // Create a cancellation token with 30 second timeout - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - String? latestReleaseTag = null; + // Create a cancellation token with 30 second timeout + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + string? latestReleaseTag = null; - try - { - latestReleaseTag = await VersionHelper.GetLatestReleaseTag(cts.Token); - } - catch (OperationCanceledException) - { - AnsiConsole.Markup("[yellow]Version check timed out after 30 seconds.\n[/]"); - Log.Warning("Version check timed out after 30 seconds"); - latestReleaseTag = null; - } + try + { + latestReleaseTag = await VersionHelper.GetLatestReleaseTag(cts.Token); + } + catch (OperationCanceledException) + { + AnsiConsole.Markup("[yellow]Version check timed out after 30 seconds.\n[/]"); + Log.Warning("Version check timed out after 30 seconds"); + latestReleaseTag = null; + } - if (latestReleaseTag == null) - { - AnsiConsole.Markup("[yellow]Failed to verify that OF-DL is up-to-date.\n[/]"); - Log.Error("Failed to get the latest release tag."); - } - else - { - Version latestGiteaRelease = new Version(latestReleaseTag.Replace("OFDLV", "")); + if (latestReleaseTag == null) + { + AnsiConsole.Markup("[yellow]Failed to verify that OF-DL is up-to-date.\n[/]"); + Log.Error("Failed to get the latest release tag."); + } + else + { + Version latestGiteaRelease = new(latestReleaseTag.Replace("OFDLV", "")); - // Compare the Versions - int versionComparison = localVersion.CompareTo(latestGiteaRelease); - if (versionComparison < 0) - { - // The version on GitHub is more up to date than this local release. - AnsiConsole.Markup("[red]You are running OF-DL version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]"); - AnsiConsole.Markup("[red]Please update to the current release, " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}: [link=https://git.ofdl.tools/sim0n00ps/OF-DL/releases]https://git.ofdl.tools/sim0n00ps/OF-DL/releases[/]\n[/]"); - Log.Debug("Detected outdated client running version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}"); - Log.Debug("Latest release version " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}"); - } - else - { - // This local version is greater than the release version on GitHub. - AnsiConsole.Markup("[green]You are running OF-DL version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]"); - AnsiConsole.Markup("[green]Latest Release version: " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}\n[/]"); - Log.Debug("Detected client running version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}"); - Log.Debug("Latest release version " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}"); - } - } + // Compare the Versions + int versionComparison = localVersion.CompareTo(latestGiteaRelease); + if (versionComparison < 0) + { + // The version on GitHub is more up to date than this local release. + AnsiConsole.Markup("[red]You are running OF-DL version " + + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]"); + AnsiConsole.Markup("[red]Please update to the current release, " + + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}: [link=https://git.ofdl.tools/sim0n00ps/OF-DL/releases]https://git.ofdl.tools/sim0n00ps/OF-DL/releases[/]\n[/]"); + Log.Debug("Detected outdated client running version " + + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}"); + Log.Debug("Latest release version " + + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}"); + } + else + { + // This local version is greater than the release version on GitHub. + AnsiConsole.Markup("[green]You are running OF-DL version " + + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]"); + AnsiConsole.Markup("[green]Latest Release version: " + + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}\n[/]"); + Log.Debug("Detected client running version " + + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}"); + Log.Debug("Latest release version " + + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}"); + } + } - #else +#else AnsiConsole.Markup("[yellow]Running in Debug/Local mode. Version check skipped.\n[/]"); Log.Debug("Running in Debug/Local mode. Version check skipped."); - #endif - } - catch (Exception e) - { - AnsiConsole.Markup("[red]Error checking latest release on GitHub:\n[/]"); - Console.WriteLine(e); - Log.Error("Error checking latest release on GitHub.", e.Message); - } +#endif + } + catch (Exception e) + { + AnsiConsole.Markup("[red]Error checking latest release on GitHub:\n[/]"); + Console.WriteLine(e); + Log.Error("Error checking latest release on GitHub.", e.Message); + } if (await authService.LoadFromFileAsync()) - { - AnsiConsole.Markup("[green]auth.json located successfully!\n[/]"); - } - else if (File.Exists("auth.json")) - { - // File exists but failed to load - Log.Information("Auth file found but could not be deserialized"); - if (!configService.CurrentConfig!.DisableBrowserAuth) - { - Log.Debug("Deleting auth.json"); - File.Delete("auth.json"); - } + { + AnsiConsole.Markup("[green]auth.json located successfully!\n[/]"); + } + else if (File.Exists("auth.json")) + { + // File exists but failed to load + 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[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); + 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[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - Console.ReadKey(); - Environment.Exit(2); - } + Console.ReadKey(); + Environment.Exit(2); + } - if (!configService.CurrentConfig!.DisableBrowserAuth) - { - await LoadAuthFromBrowser(); - } - else - { - 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[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - - Console.ReadKey(); - Environment.Exit(2); - } - } - else - { - if (configService.CurrentConfig.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[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - - Console.ReadKey(); - Environment.Exit(2); - } - if (!configService.CurrentConfig!.DisableBrowserAuth) { await LoadAuthFromBrowser(); } else { - 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[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); + 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[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); Console.ReadKey(); Environment.Exit(2); } - } + } + else + { + if (configService.CurrentConfig.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[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - //Added to stop cookie being filled with un-needed headers - ValidateCookieString(authService.CurrentAuth!); + Console.ReadKey(); + Environment.Exit(2); + } - if (File.Exists("rules.json")) - { - AnsiConsole.Markup("[green]rules.json located successfully!\n[/]"); - try - { - JsonConvert.DeserializeObject(File.ReadAllText("rules.json")); - Log.Debug($"Rules.json: "); - Log.Debug(JsonConvert.SerializeObject(File.ReadAllText("rules.json"), Formatting.Indented)); - } - catch (Exception e) - { - Console.WriteLine(e); - 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"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - Log.Error("rules.json processing failed.", e.Message); + if (!configService.CurrentConfig!.DisableBrowserAuth) + { + await LoadAuthFromBrowser(); + } + else + { + 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[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - if (!configService.CurrentConfig.NonInteractiveMode) - { - Console.ReadKey(); - } - Environment.Exit(2); - } - } + Console.ReadKey(); + Environment.Exit(2); + } + } - if(configService.CurrentConfig.NonInteractiveMode) - { - // CLI argument overrides configuration - configService.CurrentConfig!.NonInteractiveMode = true; - Log.Debug("NonInteractiveMode = true"); - } + //Added to stop cookie being filled with un-needed headers + ValidateCookieString(authService.CurrentAuth!); - if(configService.CurrentConfig!.NonInteractiveMode) - { - configService.CurrentConfig.NonInteractiveMode = true; // If it was set in the config, reset the cli value so exception handling works - Log.Debug("NonInteractiveMode = true (set via config)"); - } + if (File.Exists("rules.json")) + { + AnsiConsole.Markup("[green]rules.json located successfully!\n[/]"); + try + { + JsonConvert.DeserializeObject(File.ReadAllText("rules.json")); + Log.Debug("Rules.json: "); + Log.Debug(JsonConvert.SerializeObject(File.ReadAllText("rules.json"), Formatting.Indented)); + } + catch (Exception e) + { + Console.WriteLine(e); + 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"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); + Log.Error("rules.json processing failed.", e.Message); - var ffmpegFound = false; - var pathAutoDetected = false; - if (!string.IsNullOrEmpty(configService.CurrentConfig!.FFmpegPath) && ValidateFilePath(configService.CurrentConfig.FFmpegPath)) - { - // FFmpeg path is set in config.json and is valid - ffmpegFound = true; - Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); - Log.Debug("FFMPEG path set in config.conf"); - } - else if (!string.IsNullOrEmpty(authService.CurrentAuth!.FFMPEG_PATH) && ValidateFilePath(authService.CurrentAuth.FFMPEG_PATH)) - { - // FFmpeg path is set in auth.json and is valid (config.conf takes precedence and auth.json is only available for backward compatibility) - ffmpegFound = true; - configService.CurrentConfig.FFmpegPath = authService.CurrentAuth.FFMPEG_PATH; - Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); - Log.Debug("FFMPEG path set in auth.json"); - } - else if (string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath)) - { - // FFmpeg path is not set in config.conf, so we will try to locate it in the PATH or current directory - var ffmpegPath = GetFullPath("ffmpeg"); - if (ffmpegPath != null) - { - // FFmpeg is found in the PATH or current directory - ffmpegFound = true; - pathAutoDetected = true; - configService.CurrentConfig.FFmpegPath = ffmpegPath; - Log.Debug($"FFMPEG found: {ffmpegPath}"); - Log.Debug("FFMPEG path found via PATH or current directory"); - } - else - { - // FFmpeg is not found in the PATH or current directory, so we will try to locate the windows executable - ffmpegPath = GetFullPath("ffmpeg.exe"); - if (ffmpegPath != null) - { - // FFmpeg windows executable is found in the PATH or current directory - ffmpegFound = true; - pathAutoDetected = true; - configService.CurrentConfig.FFmpegPath = ffmpegPath; - Log.Debug($"FFMPEG found: {ffmpegPath}"); - Log.Debug("FFMPEG path found in windows excutable directory"); - } - } - } + if (!configService.CurrentConfig.NonInteractiveMode) + { + Console.ReadKey(); + } - if (ffmpegFound) - { - if (pathAutoDetected) - { - AnsiConsole.Markup($"[green]FFmpeg located successfully. Path auto-detected: {configService.CurrentConfig.FFmpegPath}\n[/]"); - } - else - { - AnsiConsole.Markup($"[green]FFmpeg located successfully\n[/]"); - } + Environment.Exit(2); + } + } - // Escape backslashes in the path for Windows - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && configService.CurrentConfig.FFmpegPath!.Contains(@":\") && !configService.CurrentConfig.FFmpegPath.Contains(@":\\")) - { - configService.CurrentConfig.FFmpegPath = configService.CurrentConfig.FFmpegPath.Replace(@"\", @"\\"); - } + if (configService.CurrentConfig.NonInteractiveMode) + { + // CLI argument overrides configuration + configService.CurrentConfig!.NonInteractiveMode = true; + Log.Debug("NonInteractiveMode = true"); + } - // Get FFmpeg version - try - { - var processStartInfo = new System.Diagnostics.ProcessStartInfo - { - FileName = configService.CurrentConfig.FFmpegPath, - Arguments = "-version", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; + if (configService.CurrentConfig!.NonInteractiveMode) + { + configService.CurrentConfig.NonInteractiveMode = + true; // If it was set in the config, reset the cli value so exception handling works + Log.Debug("NonInteractiveMode = true (set via config)"); + } - using (var process = System.Diagnostics.Process.Start(processStartInfo)) - { - if (process != null) - { - string output = await process.StandardOutput.ReadToEndAsync(); - await process.WaitForExitAsync(); + bool ffmpegFound = false; + bool pathAutoDetected = false; + if (!string.IsNullOrEmpty(configService.CurrentConfig!.FFmpegPath) && + ValidateFilePath(configService.CurrentConfig.FFmpegPath)) + { + // FFmpeg path is set in config.json and is valid + ffmpegFound = true; + Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); + Log.Debug("FFMPEG path set in config.conf"); + } + else if (!string.IsNullOrEmpty(authService.CurrentAuth!.FFMPEG_PATH) && + ValidateFilePath(authService.CurrentAuth.FFMPEG_PATH)) + { + // FFmpeg path is set in auth.json and is valid (config.conf takes precedence and auth.json is only available for backward compatibility) + ffmpegFound = true; + configService.CurrentConfig.FFmpegPath = authService.CurrentAuth.FFMPEG_PATH; + Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); + Log.Debug("FFMPEG path set in auth.json"); + } + else if (string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath)) + { + // FFmpeg path is not set in config.conf, so we will try to locate it in the PATH or current directory + string? ffmpegPath = GetFullPath("ffmpeg"); + if (ffmpegPath != null) + { + // FFmpeg is found in the PATH or current directory + ffmpegFound = true; + pathAutoDetected = true; + configService.CurrentConfig.FFmpegPath = ffmpegPath; + Log.Debug($"FFMPEG found: {ffmpegPath}"); + Log.Debug("FFMPEG path found via PATH or current directory"); + } + else + { + // FFmpeg is not found in the PATH or current directory, so we will try to locate the windows executable + ffmpegPath = GetFullPath("ffmpeg.exe"); + if (ffmpegPath != null) + { + // FFmpeg windows executable is found in the PATH or current directory + ffmpegFound = true; + pathAutoDetected = true; + configService.CurrentConfig.FFmpegPath = ffmpegPath; + Log.Debug($"FFMPEG found: {ffmpegPath}"); + Log.Debug("FFMPEG path found in windows excutable directory"); + } + } + } - // Log full output - Log.Information("FFmpeg version output:\n{Output}", output); + if (ffmpegFound) + { + if (pathAutoDetected) + { + AnsiConsole.Markup( + $"[green]FFmpeg located successfully. Path auto-detected: {configService.CurrentConfig.FFmpegPath}\n[/]"); + } + else + { + AnsiConsole.Markup("[green]FFmpeg located successfully\n[/]"); + } - // Parse first line for console output - string firstLine = output.Split('\n')[0].Trim(); - if (firstLine.StartsWith("ffmpeg version")) - { - // Extract version string (text between "ffmpeg version " and " Copyright") - int versionStart = "ffmpeg version ".Length; - int copyrightIndex = firstLine.IndexOf(" Copyright"); - if (copyrightIndex > versionStart) - { - string version = firstLine.Substring(versionStart, copyrightIndex - versionStart); - AnsiConsole.Markup($"[green]ffmpeg version detected as {version}[/]\n"); - } - else - { - // Fallback if Copyright not found - string version = firstLine.Substring(versionStart); - AnsiConsole.Markup($"[green]ffmpeg version detected as {version}[/]\n"); - } - } - else - { - AnsiConsole.Markup($"[yellow]ffmpeg version could not be parsed[/]\n"); - } - } - } - } - catch (Exception ex) - { - Log.Warning(ex, "Failed to get FFmpeg version"); - AnsiConsole.Markup($"[yellow]Could not retrieve ffmpeg version[/]\n"); - } - } - else - { - AnsiConsole.Markup("[red]Cannot locate FFmpeg; please modify config.conf with the correct path. Press any key to exit.[/]"); - Log.Error($"Cannot locate FFmpeg with path: {configService.CurrentConfig.FFmpegPath}"); - if (!configService.CurrentConfig.NonInteractiveMode) - { - Console.ReadKey(); - } - Environment.Exit(4); - } + // Escape backslashes in the path for Windows + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + configService.CurrentConfig.FFmpegPath!.Contains(@":\") && + !configService.CurrentConfig.FFmpegPath.Contains(@":\\")) + { + configService.CurrentConfig.FFmpegPath = + configService.CurrentConfig.FFmpegPath.Replace(@"\", @"\\"); + } - if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_client_id_blob"))) - { - clientIdBlobMissing = true; - Log.Debug("clientIdBlobMissing missing"); - } - else - { - AnsiConsole.Markup($"[green]device_client_id_blob located successfully![/]\n"); - Log.Debug("clientIdBlobMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_client_id_blob"))); - } + // Get FFmpeg version + try + { + ProcessStartInfo processStartInfo = new() + { + FileName = configService.CurrentConfig.FFmpegPath, + Arguments = "-version", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; - if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_private_key"))) - { - devicePrivateKeyMissing = true; - Log.Debug("devicePrivateKeyMissing missing"); - } - else - { - AnsiConsole.Markup($"[green]device_private_key located successfully![/]\n"); - Log.Debug("devicePrivateKeyMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_private_key"))); - } + using (Process? process = Process.Start(processStartInfo)) + { + if (process != null) + { + string output = await process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync(); - if (clientIdBlobMissing || 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[/]"); - } + // Log full output + Log.Information("FFmpeg version output:\n{Output}", output); - Entities.User? validate = await apiService.GetUserInfo($"/users/me"); - if (validate == null || (validate?.name == null && validate?.username == null)) - { - Log.Error("Auth failed"); + // Parse first line for console output + string firstLine = output.Split('\n')[0].Trim(); + if (firstLine.StartsWith("ffmpeg version")) + { + // Extract version string (text between "ffmpeg version " and " Copyright") + int versionStart = "ffmpeg version ".Length; + int copyrightIndex = firstLine.IndexOf(" Copyright"); + if (copyrightIndex > versionStart) + { + string version = firstLine.Substring(versionStart, copyrightIndex - versionStart); + AnsiConsole.Markup($"[green]ffmpeg version detected as {version}[/]\n"); + } + else + { + // Fallback if Copyright not found + string version = firstLine.Substring(versionStart); + AnsiConsole.Markup($"[green]ffmpeg version detected as {version}[/]\n"); + } + } + else + { + AnsiConsole.Markup("[yellow]ffmpeg version could not be parsed[/]\n"); + } + } + } + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to get FFmpeg version"); + AnsiConsole.Markup("[yellow]Could not retrieve ffmpeg version[/]\n"); + } + } + else + { + AnsiConsole.Markup( + "[red]Cannot locate FFmpeg; please modify config.conf with the correct path. Press any key to exit.[/]"); + Log.Error($"Cannot locate FFmpeg with path: {configService.CurrentConfig.FFmpegPath}"); + if (!configService.CurrentConfig.NonInteractiveMode) + { + Console.ReadKey(); + } - authService.CurrentAuth = null; + Environment.Exit(4); + } + + if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_client_id_blob"))) + { + clientIdBlobMissing = true; + Log.Debug("clientIdBlobMissing missing"); + } + else + { + AnsiConsole.Markup("[green]device_client_id_blob located successfully![/]\n"); + Log.Debug("clientIdBlobMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, + Constants.DEVICE_NAME, "device_client_id_blob"))); + } + + if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_private_key"))) + { + devicePrivateKeyMissing = true; + Log.Debug("devicePrivateKeyMissing missing"); + } + else + { + AnsiConsole.Markup("[green]device_private_key located successfully![/]\n"); + Log.Debug("devicePrivateKeyMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, + Constants.DEVICE_NAME, "device_private_key"))); + } + + if (clientIdBlobMissing || 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[/]"); + } + + User? validate = await apiService.GetUserInfo("/users/me"); + 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")) @@ -504,1871 +547,2107 @@ public class Program(IServiceProvider serviceProvider) } } - if (!configService.CurrentConfig.NonInteractiveMode && !configService.CurrentConfig!.DisableBrowserAuth) - { + 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"); - Console.ReadKey(); - Environment.Exit(2); - } - } + 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"); + Console.ReadKey(); + Environment.Exit(2); + } + } - AnsiConsole.Markup($"[green]Logged In successfully as {validate.name} {validate.username}\n[/]"); - await DownloadAllData(); - } - 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); - } - Console.WriteLine("\nPress any key to exit."); - if (!configService.CurrentConfig.NonInteractiveMode) - { - Console.ReadKey(); - } - Environment.Exit(5); - } - } - - private async Task DownloadAllData() - { - IDBService dbService = serviceProvider.GetRequiredService(); - IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - Entities.Config Config = configService.CurrentConfig!; - - Log.Debug("Calling DownloadAllData"); - - do - { - DateTime startTime = DateTime.Now; - Dictionary users = new(); - Dictionary activeSubs = await apiService.GetActiveSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions); - - Log.Debug("Subscriptions: "); - - foreach (KeyValuePair activeSub in activeSubs) - { - if (!users.ContainsKey(activeSub.Key)) - { - users.Add(activeSub.Key, activeSub.Value); - Log.Debug($"Name: {activeSub.Key} ID: {activeSub.Value}"); - } - } - if (Config!.IncludeExpiredSubscriptions) - { - Log.Debug("Inactive Subscriptions: "); - - Dictionary expiredSubs = await apiService.GetExpiredSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions); - foreach (KeyValuePair expiredSub in expiredSubs) - { - if (!users.ContainsKey(expiredSub.Key)) - { - users.Add(expiredSub.Key, expiredSub.Value); - Log.Debug($"Name: {expiredSub.Key} ID: {expiredSub.Value}"); - } - } - } - - Dictionary lists = await apiService.GetLists("/lists"); - - // Remove users from the list if they are in the ignored list - if (!string.IsNullOrEmpty(Config.IgnoredUsersListName)) - { - if (!lists.TryGetValue(Config.IgnoredUsersListName, out var ignoredUsersListId)) - { - AnsiConsole.Markup($"[red]Ignored users list '{Config.IgnoredUsersListName}' not found\n[/]"); - Log.Error($"Ignored users list '{Config.IgnoredUsersListName}' not found"); - } - else - { - var ignoredUsernames = await apiService.GetListUsers($"/lists/{ignoredUsersListId}/users") ?? []; - users = users.Where(x => !ignoredUsernames.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value); - } - } - - await dbService.CreateUsersDB(users); - 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)) - { - var listId = lists[Config.NonInteractiveModeListName]; - var listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; - var selectedUsers = users.Where(x => listUsernames.Contains(x.Key)).Distinct().ToDictionary(x => x.Key, x => x.Value); - hasSelectedUsersKVP = new KeyValuePair>(true, selectedUsers); - } - else - { - var loggingService = serviceProvider.GetRequiredService(); - var userSelectionResult = await HandleUserSelection(users, lists); - - Config = configService.CurrentConfig!; - hasSelectedUsersKVP = new KeyValuePair>(userSelectionResult.IsExit, userSelectionResult.selectedUsers); - } - - if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && hasSelectedUsersKVP.Value.ContainsKey("SinglePost")) - { - 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 Regex("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 post_id = Convert.ToInt64(postUrl.Split("/")[3]); - string username = postUrl.Split("/")[4]; - - Log.Debug($"Single Post ID: {post_id.ToString()}"); - Log.Debug($"Single Post Creator: {username}"); - - if (users.ContainsKey(username)) - { - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = System.IO.Path.Combine(Config.DownloadPath, username); - } - else - { - path = $"__user_data__/sites/OnlyFans/{username}"; - } - - Log.Debug($"Download path: {path}"); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {username}\n[/]"); - Log.Debug($"Created folder for {username}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); - } - - await dbService.CreateDB(path); - - await DownloadSinglePost(username, post_id, path, users); - } - } - } - else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && hasSelectedUsersKVP.Value.ContainsKey("PurchasedTab")) - { - Dictionary purchasedTabUsers = await apiService.GetPurchasedTabUsers("/posts/paid/all", users); - AnsiConsole.Markup($"[red]Checking folders for Users in Purchased Tab\n[/]"); - foreach (KeyValuePair user in purchasedTabUsers) - { - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = System.IO.Path.Combine(Config.DownloadPath, user.Key); - } - else - { - path = $"__user_data__/sites/OnlyFans/{user.Key}"; - } - - Log.Debug($"Download path: {path}"); - - await dbService.CheckUsername(user, path); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {user.Key}\n[/]"); - Log.Debug($"Created folder for {user.Key}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {user.Key} already created\n[/]"); - Log.Debug($"Folder for {user.Key} already created"); - } - - Entities.User user_info = await apiService.GetUserInfo($"/users/{user.Key}"); - - await dbService.CreateDB(path); - } - - string p = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - p = Config.DownloadPath; - } - else - { - p = $"__user_data__/sites/OnlyFans/"; - } - - Log.Debug($"Download path: {p}"); - - List purchasedTabCollections = await apiService.GetPurchasedTab("/posts/paid/all", p, users); - foreach(PurchasedTabCollection purchasedTabCollection in purchasedTabCollections) - { - AnsiConsole.Markup($"[red]\nScraping Data for {purchasedTabCollection.Username}\n[/]"); - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = System.IO.Path.Combine(Config.DownloadPath, purchasedTabCollection.Username); - } - else - { - path = $"__user_data__/sites/OnlyFans/{purchasedTabCollection.Username}"; - } - - - Log.Debug($"Download path: {path}"); - - int paidPostCount = 0; - int paidMessagesCount = 0; - paidPostCount = await DownloadPaidPostsPurchasedTab(purchasedTabCollection.Username, purchasedTabCollection.PaidPosts, users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidPostCount, path, users); - paidMessagesCount = await DownloadPaidMessagesPurchasedTab(purchasedTabCollection.Username, purchasedTabCollection.PaidMessages, users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidMessagesCount, path, users); - - AnsiConsole.Markup("\n"); - AnsiConsole.Write(new BreakdownChart() - .FullSize() - .AddItem("Paid Posts", paidPostCount, Color.Red) - .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); - AnsiConsole.Markup("\n"); - } - DateTime endTime = DateTime.Now; - TimeSpan totalTime = endTime - startTime; - AnsiConsole.Markup($"[green]Scrape Completed in {totalTime.TotalMinutes:0.00} minutes\n[/]"); - Log.Debug($"Scrape Completed in {totalTime.TotalMinutes:0.00} minutes"); - } - else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && hasSelectedUsersKVP.Value.ContainsKey("SingleMessage")) - { - 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 Regex("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 message_id = Convert.ToInt64(messageUrl.Split("?firstId=")[1]); - long user_id = Convert.ToInt64(messageUrl.Split("/")[6]); - JObject user = await apiService.GetUserInfoById($"/users/list?x[]={user_id.ToString()}"); - string username = string.Empty; - - Log.Debug($"Message ID: {message_id}"); - Log.Debug($"User ID: {user_id}"); - - if (user is null) - { - username = $"Deleted User - {user_id.ToString()}"; - Log.Debug("Content creator not longer exists - ", user_id.ToString()); - } - else if (!string.IsNullOrEmpty(user[user_id.ToString()]["username"].ToString())) - { - username = user[user_id.ToString()]["username"].ToString(); - Log.Debug("Content creator: ", username); - } - - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = System.IO.Path.Combine(Config.DownloadPath, username); - } - else - { - path = $"__user_data__/sites/OnlyFans/{username}"; - } - - Log.Debug("Download path: ", path); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {username}\n[/]"); - Log.Debug($"Created folder for {username}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); - Log.Debug($"Folder for {username} already created"); - } - - await dbService.CreateDB(path); - - await DownloadPaidMessage(username, hasSelectedUsersKVP, 1, path, message_id); - } - } - else if (hasSelectedUsersKVP.Key && !hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) - { - //Iterate over each user in the list of users - foreach (KeyValuePair user in hasSelectedUsersKVP.Value) - { - int paidPostCount = 0; - int postCount = 0; - int archivedCount = 0; - int streamsCount = 0; - int storiesCount = 0; - int highlightsCount = 0; - int messagesCount = 0; - int paidMessagesCount = 0; - AnsiConsole.Markup($"[red]\nScraping Data for {user.Key}\n[/]"); - - Log.Debug($"Scraping Data for {user.Key}"); - - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = System.IO.Path.Combine(Config.DownloadPath, user.Key); - } - else - { - path = $"__user_data__/sites/OnlyFans/{user.Key}"; - } - - Log.Debug("Download path: ", path); - - await dbService.CheckUsername(user, path); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {user.Key}\n[/]"); - Log.Debug($"Created folder for {user.Key}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {user.Key} already created\n[/]"); - Log.Debug($"Folder for {user.Key} already created"); - } - - await dbService.CreateDB(path); - - if (Config.DownloadAvatarHeaderPhoto) - { - Entities.User? user_info = await apiService.GetUserInfo($"/users/{user.Key}"); - if (user_info != null) - { - await downloadService.DownloadAvatarHeader(user_info.avatar, user_info.header, path, user.Key); - } - } - - if (Config.DownloadPaidPosts) - { - paidPostCount = await DownloadPaidPosts(user.Key, hasSelectedUsersKVP, user, paidPostCount, path); - } - - if (Config.DownloadPosts) - { - postCount = await DownloadFreePosts(user.Key, hasSelectedUsersKVP, user, postCount, path); - } - - if (Config.DownloadArchived) - { - archivedCount = await DownloadArchived(user.Key, hasSelectedUsersKVP, user, archivedCount, path); - } - - if (Config.DownloadStreams) - { - streamsCount = await DownloadStreams(user.Key, hasSelectedUsersKVP, user, streamsCount, path); - } - - if (Config.DownloadStories) - { - storiesCount = await DownloadStories(user.Key, user, storiesCount, path); - } - - if (Config.DownloadHighlights) - { - highlightsCount = await DownloadHighlights(user.Key, user, highlightsCount, path); - } - - if (Config.DownloadMessages) - { - messagesCount = await DownloadMessages(user.Key, hasSelectedUsersKVP, user, messagesCount, path); - } - - if (Config.DownloadPaidMessages) - { - paidMessagesCount = await DownloadPaidMessages(user.Key, hasSelectedUsersKVP, user, paidMessagesCount, path); - } - - AnsiConsole.Markup("\n"); - AnsiConsole.Write(new BreakdownChart() - .FullSize() - .AddItem("Paid Posts", paidPostCount, Color.Red) - .AddItem("Posts", postCount, Color.Blue) - .AddItem("Archived", archivedCount, Color.Green) - .AddItem("Streams", streamsCount, Color.Purple) - .AddItem("Stories", storiesCount, Color.Yellow) - .AddItem("Highlights", highlightsCount, Color.Orange1) - .AddItem("Messages", messagesCount, Color.LightGreen) - .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); - AnsiConsole.Markup("\n"); - } - DateTime endTime = DateTime.Now; - TimeSpan totalTime = endTime - startTime; - AnsiConsole.Markup($"[green]Scrape Completed in {totalTime.TotalMinutes:0.00} minutes\n[/]"); - } - else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) - { - continue; - } - else - { - break; - } - } while (!Config.NonInteractiveMode); - } - - private async Task DownloadPaidMessages(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int paidMessagesCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - PaidMessageCollection paidMessageCollection = new PaidMessageCollection(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Paid Messages[/]", async ctx => + AnsiConsole.Markup($"[green]Logged In successfully as {validate.name} {validate.username}\n[/]"); + await DownloadAllData(); + } + catch (Exception ex) { - paidMessageCollection = await apiService.GetPaidMessages("/posts/paid/chat", path, user.Key, ctx); - }); + 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 (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) - { - AnsiConsole.Markup($"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); + Console.WriteLine("\nPress any key to exit."); + if (!configService.CurrentConfig.NonInteractiveMode) + { + Console.ReadKey(); + } - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values.ToList()); - } - else - { - totalSize = paidMessageCollection.PaidMessages.Count; - } + Environment.Exit(5); + } + } - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadPaidMessages(username, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, paidMessageCollection, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Paid Messages Already Downloaded: {result.ExistingDownloads} New Paid Messages Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - else - { - AnsiConsole.Markup($"[red]Found 0 Paid Messages\n[/]"); - return 0; - } - } - private async Task DownloadMessages(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int messagesCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - MessageCollection messages = new MessageCollection(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Messages[/]", async ctx => - { - messages = await apiService.GetMessages($"/chats/{user.Value}/messages", path, ctx); - }); - - if (messages != null && messages.Messages.Count > 0) - { - AnsiConsole.Markup($"[red]Found {messages.Messages.Count} Media from {messages.MessageObjects.Count} Messages\n[/]"); - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(messages.Messages.Values.ToList()); - } - else - { - totalSize = messages.Messages.Count; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {messages.Messages.Count} Messages[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadMessages(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, messages, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Messages Already Downloaded: {result.ExistingDownloads} New Messages Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - else - { - AnsiConsole.Markup($"[red]Found 0 Messages\n[/]"); - return 0; - } - } - - private async Task DownloadHighlights(string username, KeyValuePair user, int highlightsCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup($"[red]Getting Highlights\n[/]"); - - // Calculate total size for progress bar - long totalSize = 0; - var tempHighlights = await apiService.GetMedia(MediaType.Highlights, $"/users/{user.Value}/stories/highlights", null, path, paid_post_ids); - if (tempHighlights != null && tempHighlights.Count > 0) - { - AnsiConsole.Markup($"[red]Found {tempHighlights.Count} Highlights\n[/]"); - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(tempHighlights.Values.ToList()); - } - else - { - totalSize = tempHighlights.Count; - } - } - else - { - AnsiConsole.Markup($"[red]Found 0 Highlights\n[/]"); - return 0; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {tempHighlights.Count} Highlights[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadHighlights(username, user.Value, path, paid_post_ids.ToHashSet(), progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Highlights Already Downloaded: {result.ExistingDownloads} New Highlights Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - - private async Task DownloadStories(string username, KeyValuePair user, int storiesCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup($"[red]Getting Stories\n[/]"); - - // Calculate total size for progress bar - long totalSize = 0; - var tempStories = await serviceProvider.GetRequiredService().GetMedia(MediaType.Stories, $"/users/{user.Value}/stories", null, path, paid_post_ids); - if (tempStories != null && tempStories.Count > 0) - { - AnsiConsole.Markup($"[red]Found {tempStories.Count} Stories\n[/]"); - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(tempStories.Values.ToList()); - } - else - { - totalSize = tempStories.Count; - } - } - else - { - AnsiConsole.Markup($"[red]Found 0 Stories\n[/]"); - return 0; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {tempStories.Count} Stories[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadStories(username, user.Value, path, paid_post_ids.ToHashSet(), progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Stories Already Downloaded: {result.ExistingDownloads} New Stories Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - - private async Task DownloadArchived(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int archivedCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - ArchivedCollection archived = new ArchivedCollection(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Archived Posts[/]", async ctx => - { - archived = await apiService.GetArchived($"/users/{user.Value}/posts", path, ctx); - }); - - if (archived != null && archived.ArchivedPosts.Count > 0) - { - AnsiConsole.Markup($"[red]Found {archived.ArchivedPosts.Count} Media from {archived.ArchivedPostObjects.Count} Archived Posts\n[/]"); - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(archived.ArchivedPosts.Values.ToList()); - } - else - { - totalSize = archived.ArchivedPosts.Count; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {archived.ArchivedPosts.Count} Archived Posts[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadArchived(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, archived, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Archived Posts Already Downloaded: {result.ExistingDownloads} New Archived Posts Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - else - { - AnsiConsole.Markup($"[red]Found 0 Archived Posts\n[/]"); - return 0; - } - } - - private async Task DownloadFreePosts(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int postCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup($"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/]"); - Log.Debug($"Calling DownloadFreePosts - {user.Key}"); - - PostCollection posts = new PostCollection(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Posts[/]", async ctx => - { - posts = await apiService.GetPosts($"/users/{user.Value}/posts", path, paid_post_ids, ctx); - }); - - if (posts == null || posts.Posts.Count <= 0) - { - AnsiConsole.Markup($"[red]Found 0 Posts\n[/]"); - Log.Debug($"Found 0 Posts"); - return 0; - } - - AnsiConsole.Markup($"[red]Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts\n[/]"); - Log.Debug($"Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts"); - - long totalSize = configService.CurrentConfig.ShowScrapeSize - ? await downloadService.CalculateTotalFileSize(posts.Posts.Values.ToList()) - : posts.Posts.Count; - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {posts.Posts.Count} Posts[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadFreePosts(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, posts, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}[/]\n"); - Log.Debug($"Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}"); - - return result.TotalCount; - } - - private async Task DownloadPaidPosts(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int paidPostCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup($"[red]Getting Paid Posts\n[/]"); - Log.Debug($"Calling DownloadPaidPosts - {user.Key}"); - - PaidPostCollection purchasedPosts = new PaidPostCollection(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Paid Posts[/]", async ctx => - { - purchasedPosts = await apiService.GetPaidPosts("/posts/paid/post", path, user.Key, paid_post_ids, ctx); - }); - - if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) - { - AnsiConsole.Markup($"[red]Found 0 Paid Posts\n[/]"); - Log.Debug("Found 0 Paid Posts"); - return 0; - } - - AnsiConsole.Markup($"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); - Log.Debug($"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); - - long totalSize = configService.CurrentConfig.ShowScrapeSize - ? await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()) - : purchasedPosts.PaidPosts.Count; - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadPaidPosts(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, purchasedPosts, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}[/]\n"); - Log.Debug($"Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}"); - - return result.TotalCount; - } - - private async Task DownloadPaidPostsPurchasedTab(string username, PaidPostCollection purchasedPosts, KeyValuePair user, int paidPostCount, string path, Dictionary users) - { + private async Task DownloadAllData() + { IDBService dbService = serviceProvider.GetRequiredService(); IConfigService configService = serviceProvider.GetRequiredService(); IAuthService authService = serviceProvider.GetRequiredService(); IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - int oldPaidPostCount = 0; - int newPaidPostCount = 0; - if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) - { - AnsiConsole.Markup($"[red]Found 0 Paid Posts\n[/]"); - Log.Debug("Found 0 Paid Posts"); - return 0; - } + Config Config = configService.CurrentConfig!; - AnsiConsole.Markup($"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); - Log.Debug($"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); + Log.Debug("Calling DownloadAllData"); - paidPostCount = purchasedPosts.PaidPosts.Count; - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()); - } - else - { - totalSize = paidPostCount; - } - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - var task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", autoStart: false); - Log.Debug($"Downloading {purchasedPosts.PaidPosts.Count} Paid Posts"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair purchasedPostKVP in purchasedPosts.PaidPosts) - { - bool isNew; - if (purchasedPostKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = purchasedPostKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh == null) - { - continue; - } + do + { + DateTime startTime = DateTime.Now; + Dictionary users = new(); + Dictionary activeSubs = + await apiService.GetActiveSubscriptions("/subscriptions/subscribes", + Config.IncludeRestrictedSubscriptions); - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - Medium? mediaInfo = purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); - Purchased.List? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true) : null; + Log.Debug("Subscriptions: "); - isNew = await downloadService.DownloadPurchasedPostDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: purchasedPostKVP.Key, - api_type: "Posts", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidPostFileNameFormat ?? string.Empty, - postInfo: postInfo, - postMedia: mediaInfo, - fromUser: postInfo?.fromUser, - users: users); - if (isNew) - { - newPaidPostCount++; - } - else - { - oldPaidPostCount++; - } - } - else - { - Medium? mediaInfo = purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); - Purchased.List? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true) : null; + foreach (KeyValuePair activeSub in activeSubs) + { + if (!users.ContainsKey(activeSub.Key)) + { + users.Add(activeSub.Key, activeSub.Value); + Log.Debug($"Name: {activeSub.Key} ID: {activeSub.Value}"); + } + } - isNew = await downloadService.DownloadPurchasedPostMedia( - url: purchasedPostKVP.Value, - folder: path, - media_id: purchasedPostKVP.Key, - api_type: "Posts", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidPostFileNameFormat ?? string.Empty, - messageInfo: postInfo, - messageMedia: mediaInfo, - fromUser: postInfo?.fromUser, - users: users); - if (isNew) - { - newPaidPostCount++; - } - else - { - oldPaidPostCount++; - } - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}[/]\n"); - Log.Debug($"Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}"); - return paidPostCount; - } + if (Config!.IncludeExpiredSubscriptions) + { + Log.Debug("Inactive Subscriptions: "); - private async Task DownloadPaidMessagesPurchasedTab(string username, PaidMessageCollection paidMessageCollection, KeyValuePair user, int paidMessagesCount, string path, Dictionary users) - { - IDBService dbService = serviceProvider.GetRequiredService(); - IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); + Dictionary expiredSubs = + await apiService.GetExpiredSubscriptions("/subscriptions/subscribes", + Config.IncludeRestrictedSubscriptions); + foreach (KeyValuePair expiredSub in expiredSubs) + { + if (!users.ContainsKey(expiredSub.Key)) + { + users.Add(expiredSub.Key, expiredSub.Value); + Log.Debug($"Name: {expiredSub.Key} ID: {expiredSub.Value}"); + } + } + } - int oldPaidMessagesCount = 0; - int newPaidMessagesCount = 0; - if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) - { - AnsiConsole.Markup($"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); - Log.Debug($"Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages"); - paidMessagesCount = paidMessageCollection.PaidMessages.Count; - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values.ToList()); - } - else - { - totalSize = paidMessagesCount; - } - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - var task = ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", autoStart: false); - Log.Debug($"Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair paidMessageKVP in paidMessageCollection.PaidMessages) - { - bool isNew; - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = paidMessageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) - { - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } + Dictionary lists = await apiService.GetLists("/lists"); - Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + // Remove users from the list if they are in the ignored list + if (!string.IsNullOrEmpty(Config.IgnoredUsersListName)) + { + if (!lists.TryGetValue(Config.IgnoredUsersListName, out long ignoredUsersListId)) + { + AnsiConsole.Markup($"[red]Ignored users list '{Config.IgnoredUsersListName}' not found\n[/]"); + Log.Error($"Ignored users list '{Config.IgnoredUsersListName}' not found"); + } + else + { + List ignoredUsernames = + await apiService.GetListUsers($"/lists/{ignoredUsersListId}/users") ?? []; + users = users.Where(x => !ignoredUsernames.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value); + } + } - isNew = await downloadService.DownloadPurchasedMessageDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: users); + await dbService.CreateUsersDB(users); + 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)) + { + long listId = lists[Config.NonInteractiveModeListName]; + List listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; + Dictionary selectedUsers = users.Where(x => listUsernames.Contains(x.Key)).Distinct() + .ToDictionary(x => x.Key, x => x.Value); + hasSelectedUsersKVP = new KeyValuePair>(true, selectedUsers); + } + else + { + ILoggingService loggingService = serviceProvider.GetRequiredService(); + (bool IsExit, Dictionary? selectedUsers) userSelectionResult = + await HandleUserSelection(users, lists); - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } - else - { - Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - Purchased.List messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + Config = configService.CurrentConfig!; + hasSelectedUsersKVP = new KeyValuePair>(userSelectionResult.IsExit, + userSelectionResult.selectedUsers); + } - isNew = await downloadService.DownloadPurchasedMedia( - url: paidMessageKVP.Value, - folder: path, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: users); - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); - Log.Debug($"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}"); - } - else - { - AnsiConsole.Markup($"[red]Found 0 Paid Messages\n[/]"); - Log.Debug($"Found 0 Paid Messages"); - } + if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + hasSelectedUsersKVP.Value.ContainsKey("SinglePost")) + { + 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(); + } - return paidMessagesCount; - } + if (url == "" || url == "exit" || url == "back") + { + return ValidationResult.Success(); + } - private async Task DownloadStreams(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int streamsCount, string path) - { + Log.Error("Post URL invalid"); + return ValidationResult.Error("[red]Please enter a valid post URL[/]"); + })); + + if (postUrl != "" && postUrl != "exit" && postUrl != "back") + { + long post_id = Convert.ToInt64(postUrl.Split("/")[3]); + string username = postUrl.Split("/")[4]; + + Log.Debug($"Single Post ID: {post_id.ToString()}"); + Log.Debug($"Single Post Creator: {username}"); + + if (users.ContainsKey(username)) + { + string path = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + path = Path.Combine(Config.DownloadPath, username); + } + else + { + path = $"__user_data__/sites/OnlyFans/{username}"; + } + + Log.Debug($"Download path: {path}"); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + AnsiConsole.Markup($"[red]Created folder for {username}\n[/]"); + Log.Debug($"Created folder for {username}"); + } + else + { + AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); + } + + await dbService.CreateDB(path); + + await DownloadSinglePost(username, post_id, path, users); + } + } + } + else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + hasSelectedUsersKVP.Value.ContainsKey("PurchasedTab")) + { + Dictionary purchasedTabUsers = + await apiService.GetPurchasedTabUsers("/posts/paid/all", users); + AnsiConsole.Markup("[red]Checking folders for Users in Purchased Tab\n[/]"); + foreach (KeyValuePair user in purchasedTabUsers) + { + string path = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + path = Path.Combine(Config.DownloadPath, user.Key); + } + else + { + path = $"__user_data__/sites/OnlyFans/{user.Key}"; + } + + Log.Debug($"Download path: {path}"); + + await dbService.CheckUsername(user, path); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + AnsiConsole.Markup($"[red]Created folder for {user.Key}\n[/]"); + Log.Debug($"Created folder for {user.Key}"); + } + else + { + AnsiConsole.Markup($"[red]Folder for {user.Key} already created\n[/]"); + Log.Debug($"Folder for {user.Key} already created"); + } + + User user_info = await apiService.GetUserInfo($"/users/{user.Key}"); + + await dbService.CreateDB(path); + } + + string p = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + p = Config.DownloadPath; + } + else + { + p = "__user_data__/sites/OnlyFans/"; + } + + Log.Debug($"Download path: {p}"); + + List purchasedTabCollections = + await apiService.GetPurchasedTab("/posts/paid/all", p, users); + foreach (PurchasedTabCollection purchasedTabCollection in purchasedTabCollections) + { + AnsiConsole.Markup($"[red]\nScraping Data for {purchasedTabCollection.Username}\n[/]"); + string path = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + path = Path.Combine(Config.DownloadPath, purchasedTabCollection.Username); + } + else + { + path = $"__user_data__/sites/OnlyFans/{purchasedTabCollection.Username}"; + } + + + Log.Debug($"Download path: {path}"); + + int paidPostCount = 0; + int paidMessagesCount = 0; + paidPostCount = await DownloadPaidPostsPurchasedTab(purchasedTabCollection.Username, + purchasedTabCollection.PaidPosts, + users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidPostCount, path, + users); + paidMessagesCount = await DownloadPaidMessagesPurchasedTab(purchasedTabCollection.Username, + purchasedTabCollection.PaidMessages, + users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidMessagesCount, path, + users); + + AnsiConsole.Markup("\n"); + AnsiConsole.Write(new BreakdownChart() + .FullSize() + .AddItem("Paid Posts", paidPostCount, Color.Red) + .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); + AnsiConsole.Markup("\n"); + } + + DateTime endTime = DateTime.Now; + TimeSpan totalTime = endTime - startTime; + AnsiConsole.Markup($"[green]Scrape Completed in {totalTime.TotalMinutes:0.00} minutes\n[/]"); + Log.Debug($"Scrape Completed in {totalTime.TotalMinutes:0.00} minutes"); + } + else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + hasSelectedUsersKVP.Value.ContainsKey("SingleMessage")) + { + 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 message_id = Convert.ToInt64(messageUrl.Split("?firstId=")[1]); + long user_id = Convert.ToInt64(messageUrl.Split("/")[6]); + JObject user = await apiService.GetUserInfoById($"/users/list?x[]={user_id.ToString()}"); + string username = string.Empty; + + Log.Debug($"Message ID: {message_id}"); + Log.Debug($"User ID: {user_id}"); + + if (user is null) + { + username = $"Deleted User - {user_id.ToString()}"; + Log.Debug("Content creator not longer exists - ", user_id.ToString()); + } + else if (!string.IsNullOrEmpty(user[user_id.ToString()]["username"].ToString())) + { + username = user[user_id.ToString()]["username"].ToString(); + Log.Debug("Content creator: ", username); + } + + string path = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + path = Path.Combine(Config.DownloadPath, username); + } + else + { + path = $"__user_data__/sites/OnlyFans/{username}"; + } + + Log.Debug("Download path: ", path); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + AnsiConsole.Markup($"[red]Created folder for {username}\n[/]"); + Log.Debug($"Created folder for {username}"); + } + else + { + AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); + Log.Debug($"Folder for {username} already created"); + } + + await dbService.CreateDB(path); + + await DownloadPaidMessage(username, hasSelectedUsersKVP, 1, path, message_id); + } + } + else if (hasSelectedUsersKVP.Key && !hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) + { + //Iterate over each user in the list of users + foreach (KeyValuePair user in hasSelectedUsersKVP.Value) + { + int paidPostCount = 0; + int postCount = 0; + int archivedCount = 0; + int streamsCount = 0; + int storiesCount = 0; + int highlightsCount = 0; + int messagesCount = 0; + int paidMessagesCount = 0; + AnsiConsole.Markup($"[red]\nScraping Data for {user.Key}\n[/]"); + + Log.Debug($"Scraping Data for {user.Key}"); + + string path = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + path = Path.Combine(Config.DownloadPath, user.Key); + } + else + { + path = $"__user_data__/sites/OnlyFans/{user.Key}"; + } + + Log.Debug("Download path: ", path); + + await dbService.CheckUsername(user, path); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + AnsiConsole.Markup($"[red]Created folder for {user.Key}\n[/]"); + Log.Debug($"Created folder for {user.Key}"); + } + else + { + AnsiConsole.Markup($"[red]Folder for {user.Key} already created\n[/]"); + Log.Debug($"Folder for {user.Key} already created"); + } + + await dbService.CreateDB(path); + + if (Config.DownloadAvatarHeaderPhoto) + { + User? user_info = await apiService.GetUserInfo($"/users/{user.Key}"); + if (user_info != null) + { + await downloadService.DownloadAvatarHeader(user_info.avatar, user_info.header, path, + user.Key); + } + } + + if (Config.DownloadPaidPosts) + { + paidPostCount = + await DownloadPaidPosts(user.Key, hasSelectedUsersKVP, user, paidPostCount, path); + } + + if (Config.DownloadPosts) + { + postCount = await DownloadFreePosts(user.Key, hasSelectedUsersKVP, user, postCount, path); + } + + if (Config.DownloadArchived) + { + archivedCount = + await DownloadArchived(user.Key, hasSelectedUsersKVP, user, archivedCount, path); + } + + if (Config.DownloadStreams) + { + streamsCount = await DownloadStreams(user.Key, hasSelectedUsersKVP, user, streamsCount, path); + } + + if (Config.DownloadStories) + { + storiesCount = await DownloadStories(user.Key, user, storiesCount, path); + } + + if (Config.DownloadHighlights) + { + highlightsCount = await DownloadHighlights(user.Key, user, highlightsCount, path); + } + + if (Config.DownloadMessages) + { + messagesCount = + await DownloadMessages(user.Key, hasSelectedUsersKVP, user, messagesCount, path); + } + + if (Config.DownloadPaidMessages) + { + paidMessagesCount = await DownloadPaidMessages(user.Key, hasSelectedUsersKVP, user, + paidMessagesCount, path); + } + + AnsiConsole.Markup("\n"); + AnsiConsole.Write(new BreakdownChart() + .FullSize() + .AddItem("Paid Posts", paidPostCount, Color.Red) + .AddItem("Posts", postCount, Color.Blue) + .AddItem("Archived", archivedCount, Color.Green) + .AddItem("Streams", streamsCount, Color.Purple) + .AddItem("Stories", storiesCount, Color.Yellow) + .AddItem("Highlights", highlightsCount, Color.Orange1) + .AddItem("Messages", messagesCount, Color.LightGreen) + .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); + AnsiConsole.Markup("\n"); + } + + DateTime endTime = DateTime.Now; + TimeSpan totalTime = endTime - startTime; + AnsiConsole.Markup($"[green]Scrape Completed in {totalTime.TotalMinutes:0.00} minutes\n[/]"); + } + else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) + { + } + else + { + break; + } + } while (!Config.NonInteractiveMode); + } + + private async Task DownloadPaidMessages(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int paidMessagesCount, string path) + { IConfigService configService = serviceProvider.GetRequiredService(); IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - StreamsCollection streams = new StreamsCollection(); + PaidMessageCollection paidMessageCollection = new(); await AnsiConsole.Status() - .StartAsync("[red]Getting Streams[/]", async ctx => + .StartAsync("[red]Getting Paid Messages[/]", + async ctx => + { + paidMessageCollection = await apiService.GetPaidMessages("/posts/paid/chat", path, user.Key, ctx); + }); + + if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) { - streams = await apiService.GetStreams($"/users/{user.Value}/posts/streams", path, paid_post_ids, ctx); - }); + AnsiConsole.Markup( + $"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); - if (streams != null && streams.Streams.Count > 0) - { - AnsiConsole.Markup($"[red]Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams\n[/]"); - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(streams.Streams.Values.ToList()); - } - else - { - totalSize = streams.Streams.Count; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {streams.Streams.Count} Streams[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadStreams(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, streams, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Streams Already Downloaded: {result.ExistingDownloads} New Streams Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - else - { - AnsiConsole.Markup($"[red]Found 0 Streams\n[/]"); - return 0; - } - } - private async Task DownloadPaidMessage(string username, KeyValuePair> hasSelectedUsersKVP, int paidMessagesCount, string path, long message_id) - { - IDBService dbService = serviceProvider.GetRequiredService(); - IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - Log.Debug($"Calling DownloadPaidMessage - {username}"); - - AnsiConsole.Markup($"[red]Getting Paid Message\n[/]"); - - SinglePaidMessageCollection singlePaidMessageCollection = await apiService.GetPaidMessage($"/messages/{message_id.ToString()}", path); - int oldPreviewPaidMessagesCount = 0; - int newPreviewPaidMessagesCount = 0; - int oldPaidMessagesCount = 0; - int newPaidMessagesCount = 0; - if (singlePaidMessageCollection != null && singlePaidMessageCollection.PreviewSingleMessages.Count > 0) - { - AnsiConsole.Markup($"[red]Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages\n[/]"); - Log.Debug($"Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); - paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; long totalSize = 0; if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection.PreviewSingleMessages.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values + .ToList()); + } + else + { + totalSize = paidMessageCollection.PaidMessages.Count; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = + ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", + false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadPaidMessages(username, path, hasSelectedUsersKVP.Value, + clientIdBlobMissing, devicePrivateKeyMissing, paidMessageCollection, progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Paid Messages Already Downloaded: {result.ExistingDownloads} New Paid Messages Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + AnsiConsole.Markup("[red]Found 0 Paid Messages\n[/]"); + return 0; + } + + private async Task DownloadMessages(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int messagesCount, string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + MessageCollection messages = new(); + + await AnsiConsole.Status() + .StartAsync("[red]Getting Messages[/]", + async ctx => { messages = await apiService.GetMessages($"/chats/{user.Value}/messages", path, ctx); }); + + if (messages != null && messages.Messages.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {messages.Messages.Count} Media from {messages.MessageObjects.Count} Messages\n[/]"); + + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(messages.Messages.Values.ToList()); + } + else + { + totalSize = messages.Messages.Count; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {messages.Messages.Count} Messages[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadMessages(username, user.Value, path, + hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, messages, + progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Messages Already Downloaded: {result.ExistingDownloads} New Messages Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + AnsiConsole.Markup("[red]Found 0 Messages\n[/]"); + return 0; + } + + private async Task DownloadHighlights(string username, KeyValuePair user, int highlightsCount, + string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + + AnsiConsole.Markup("[red]Getting Highlights\n[/]"); + + // Calculate total size for progress bar + long totalSize = 0; + Dictionary? tempHighlights = await apiService.GetMedia(MediaType.Highlights, + $"/users/{user.Value}/stories/highlights", null, path, paid_post_ids); + if (tempHighlights != null && tempHighlights.Count > 0) + { + AnsiConsole.Markup($"[red]Found {tempHighlights.Count} Highlights\n[/]"); + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(tempHighlights.Values.ToList()); + } + else + { + totalSize = tempHighlights.Count; + } + } + else + { + AnsiConsole.Markup("[red]Found 0 Highlights\n[/]"); + return 0; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {tempHighlights.Count} Highlights[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadHighlights(username, user.Value, path, paid_post_ids.ToHashSet(), + progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Highlights Already Downloaded: {result.ExistingDownloads} New Highlights Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + private async Task DownloadStories(string username, KeyValuePair user, int storiesCount, + string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + AnsiConsole.Markup("[red]Getting Stories\n[/]"); + + // Calculate total size for progress bar + long totalSize = 0; + Dictionary? tempStories = await serviceProvider.GetRequiredService() + .GetMedia(MediaType.Stories, $"/users/{user.Value}/stories", null, path, paid_post_ids); + if (tempStories != null && tempStories.Count > 0) + { + AnsiConsole.Markup($"[red]Found {tempStories.Count} Stories\n[/]"); + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(tempStories.Values.ToList()); + } + else + { + totalSize = tempStories.Count; + } + } + else + { + AnsiConsole.Markup("[red]Found 0 Stories\n[/]"); + return 0; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {tempStories.Count} Stories[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadStories(username, user.Value, path, paid_post_ids.ToHashSet(), + progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Stories Already Downloaded: {result.ExistingDownloads} New Stories Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + private async Task DownloadArchived(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int archivedCount, string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + ArchivedCollection archived = new(); + + await AnsiConsole.Status() + .StartAsync("[red]Getting Archived Posts[/]", + async ctx => { archived = await apiService.GetArchived($"/users/{user.Value}/posts", path, ctx); }); + + if (archived != null && archived.ArchivedPosts.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {archived.ArchivedPosts.Count} Media from {archived.ArchivedPostObjects.Count} Archived Posts\n[/]"); + + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(archived.ArchivedPosts.Values.ToList()); + } + else + { + totalSize = archived.ArchivedPosts.Count; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = + ctx.AddTask($"[red]Downloading {archived.ArchivedPosts.Count} Archived Posts[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadArchived(username, user.Value, path, + hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, archived, + progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Archived Posts Already Downloaded: {result.ExistingDownloads} New Archived Posts Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + AnsiConsole.Markup("[red]Found 0 Archived Posts\n[/]"); + return 0; + } + + private async Task DownloadFreePosts(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int postCount, string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + AnsiConsole.Markup( + "[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/]"); + Log.Debug($"Calling DownloadFreePosts - {user.Key}"); + + PostCollection posts = new(); + + await AnsiConsole.Status() + .StartAsync("[red]Getting Posts[/]", + async ctx => + { + posts = await apiService.GetPosts($"/users/{user.Value}/posts", path, paid_post_ids, ctx); + }); + + if (posts == null || posts.Posts.Count <= 0) + { + AnsiConsole.Markup("[red]Found 0 Posts\n[/]"); + Log.Debug("Found 0 Posts"); + return 0; + } + + AnsiConsole.Markup($"[red]Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts\n[/]"); + Log.Debug($"Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts"); + + long totalSize = configService.CurrentConfig.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize(posts.Posts.Values.ToList()) + : posts.Posts.Count; + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {posts.Posts.Count} Posts[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadFreePosts(username, user.Value, path, hasSelectedUsersKVP.Value, + clientIdBlobMissing, devicePrivateKeyMissing, posts, progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}[/]\n"); + Log.Debug($"Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}"); + + return result.TotalCount; + } + + private async Task DownloadPaidPosts(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int paidPostCount, string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + AnsiConsole.Markup("[red]Getting Paid Posts\n[/]"); + Log.Debug($"Calling DownloadPaidPosts - {user.Key}"); + + PaidPostCollection purchasedPosts = new(); + + await AnsiConsole.Status() + .StartAsync("[red]Getting Paid Posts[/]", + async ctx => + { + purchasedPosts = + await apiService.GetPaidPosts("/posts/paid/post", path, user.Key, paid_post_ids, ctx); + }); + + if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) + { + AnsiConsole.Markup("[red]Found 0 Paid Posts\n[/]"); + Log.Debug("Found 0 Paid Posts"); + return 0; + } + + AnsiConsole.Markup( + $"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); + Log.Debug( + $"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); + + long totalSize = configService.CurrentConfig.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()) + : purchasedPosts.PaidPosts.Count; + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", + false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadPaidPosts(username, user.Value, path, hasSelectedUsersKVP.Value, + clientIdBlobMissing, devicePrivateKeyMissing, purchasedPosts, progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}[/]\n"); + Log.Debug( + $"Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}"); + + return result.TotalCount; + } + + private async Task DownloadPaidPostsPurchasedTab(string username, PaidPostCollection purchasedPosts, + KeyValuePair user, int paidPostCount, string path, Dictionary users) + { + IDBService dbService = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + int oldPaidPostCount = 0; + int newPaidPostCount = 0; + if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) + { + AnsiConsole.Markup("[red]Found 0 Paid Posts\n[/]"); + Log.Debug("Found 0 Paid Posts"); + return 0; + } + + AnsiConsole.Markup( + $"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); + Log.Debug( + $"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); + + paidPostCount = purchasedPosts.PaidPosts.Count; + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()); + } + else + { + totalSize = paidPostCount; + } + + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + // Define tasks + ProgressTask task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", + false); + Log.Debug($"Downloading {purchasedPosts.PaidPosts.Count} Paid Posts"); + task.MaxValue = totalSize; + task.StartTask(); + foreach (KeyValuePair purchasedPostKVP in purchasedPosts.PaidPosts) + { + bool isNew; + if (purchasedPostKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = purchasedPostKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string postId = messageUrlParsed[5]; + string? licenseURL = null; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh == null) + { + continue; + } + + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", + "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", + pssh); + } + else + { + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", + pssh); + } + + Medium? mediaInfo = + purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); + Purchased.List? postInfo = mediaInfo != null + ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true) + : null; + + isNew = await downloadService.DownloadPurchasedPostDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + purchasedPostKVP.Key, + "Posts", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidPostFileNameFormat ?? string.Empty, + postInfo, + mediaInfo, + postInfo?.fromUser, + users); + if (isNew) + { + newPaidPostCount++; + } + else + { + oldPaidPostCount++; + } + } + else + { + Medium? mediaInfo = + purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); + Purchased.List? postInfo = mediaInfo != null + ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true) + : null; + + isNew = await downloadService.DownloadPurchasedPostMedia( + purchasedPostKVP.Value, + path, + purchasedPostKVP.Key, + "Posts", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidPostFileNameFormat ?? string.Empty, + postInfo, + mediaInfo, + postInfo?.fromUser, + users); + if (isNew) + { + newPaidPostCount++; + } + else + { + oldPaidPostCount++; + } + } + } + + task.StopTask(); + }); + AnsiConsole.Markup( + $"[red]Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}[/]\n"); + Log.Debug($"Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}"); + return paidPostCount; + } + + private async Task DownloadPaidMessagesPurchasedTab(string username, + PaidMessageCollection paidMessageCollection, KeyValuePair user, int paidMessagesCount, + string path, Dictionary users) + { + IDBService dbService = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + int oldPaidMessagesCount = 0; + int newPaidMessagesCount = 0; + if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); + Log.Debug( + $"Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages"); + paidMessagesCount = paidMessageCollection.PaidMessages.Count; + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values + .ToList()); } else { totalSize = paidMessagesCount; } + await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - var task = ctx.AddTask($"[red]Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Paid Messages[/]", autoStart: false); - Log.Debug($"Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Paid Messages"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.PreviewSingleMessages) - { - bool isNew; - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = paidMessageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) - { - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + // Define tasks + ProgressTask task = + ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", + false); + Log.Debug($"Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages"); + task.MaxValue = totalSize; + task.StartTask(); + foreach (KeyValuePair paidMessageKVP in paidMessageCollection.PaidMessages) + { + bool isNew; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = paidMessageKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string messageId = messageUrlParsed[5]; + string? licenseURL = null; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh != null) + { + DateTime lastModified = + await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders( + $"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } + else + { + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } - Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => + m.id == paidMessageKVP.Key); + Purchased.List? messageInfo = + paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); - isNew = await downloadService.DownloadSingleMessagePreviewDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); + isNew = await downloadService.DownloadPurchasedMessageDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + users); - if (isNew) - { - newPreviewPaidMessagesCount++; - } - else - { - oldPreviewPaidMessagesCount++; - } - } - } - else - { - Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + if (isNew) + { + newPaidMessagesCount++; + } + else + { + oldPaidMessagesCount++; + } + } + } + else + { + Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); + Purchased.List messageInfo = + paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); - isNew = await downloadService.DownloadMessagePreviewMedia( - url: paidMessageKVP.Value, - folder: path, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newPreviewPaidMessagesCount++; - } - else - { - oldPreviewPaidMessagesCount++; - } - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Preview Paid Messages Already Downloaded: {oldPreviewPaidMessagesCount} New Preview Paid Messages Downloaded: {newPreviewPaidMessagesCount}[/]\n"); - Log.Debug($"Preview Paid Messages Already Downloaded: {oldPreviewPaidMessagesCount} New Preview Paid Messages Downloaded: {newPreviewPaidMessagesCount}"); + isNew = await downloadService.DownloadPurchasedMedia( + paidMessageKVP.Value, + path, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + users); + if (isNew) + { + newPaidMessagesCount++; + } + else + { + oldPaidMessagesCount++; + } + } + } + + task.StopTask(); + }); + AnsiConsole.Markup( + $"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); + Log.Debug( + $"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}"); + } + else + { + AnsiConsole.Markup("[red]Found 0 Paid Messages\n[/]"); + Log.Debug("Found 0 Paid Messages"); } - if (singlePaidMessageCollection != null && singlePaidMessageCollection.SingleMessages.Count > 0) - { - AnsiConsole.Markup($"[red]Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages\n[/]"); - Log.Debug($"Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); - paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection.SingleMessages.Values.ToList()); - } - else - { - totalSize = paidMessagesCount; - } - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - var task = ctx.AddTask($"[red]Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages[/]", autoStart: false); - Log.Debug($"Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.SingleMessages) - { - bool isNew; - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = paidMessageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) - { - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + return paidMessagesCount; + } - isNew = await downloadService.DownloadSinglePurchasedMessageDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } - else - { - Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - - isNew = await downloadService.DownloadSinglePurchasedMedia( - url: paidMessageKVP.Value, - folder: path, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); - Log.Debug($"Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}"); - } - else - { - AnsiConsole.Markup($"[red]Found 0 Paid Messages\n[/]"); - Log.Debug($"Found 0 Paid Messages"); - } - - return paidMessagesCount; - } - - private async Task DownloadSinglePost(string username, long post_id, string path, Dictionary users) - { + private async Task DownloadStreams(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int streamsCount, string path) + { IConfigService configService = serviceProvider.GetRequiredService(); IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - Log.Debug($"Calling DownloadSinglePost - {post_id.ToString()}"); + StreamsCollection streams = new(); - AnsiConsole.Markup($"[red]Getting Post\n[/]"); - SinglePostCollection post = await apiService.GetPost($"/posts/{post_id.ToString()}", path); - if (post == null) - { - AnsiConsole.Markup($"[red]Couldn't find post\n[/]"); - Log.Debug($"Couldn't find post"); - return; - } + await AnsiConsole.Status() + .StartAsync("[red]Getting Streams[/]", + async ctx => + { + streams = await apiService.GetStreams($"/users/{user.Value}/posts/streams", path, paid_post_ids, + ctx); + }); - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(post.SinglePosts.Values.ToList()); - } - else - { - totalSize = post.SinglePosts.Count; - } - bool isNew = false; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading Post[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair postKVP in post.SinglePosts) - { - if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = postKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh == null) - { - continue; - } + if (streams != null && streams.Streams.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams\n[/]"); - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - SinglePost.Medium mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.id == postKVP.Key); - SinglePost postInfo = post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(streams.Streams.Values.ToList()); + } + else + { + totalSize = streams.Streams.Count; + } - isNew = await downloadService.DownloadPostDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: postKVP.Key, - api_type: "Posts", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? string.Empty, - postInfo: postInfo, - postMedia: mediaInfo, - author: postInfo?.author, - users: users); - } - else - { - try - { - SinglePost.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => (m?.id == postKVP.Key) == true); - SinglePost? postInfo = post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {streams.Streams.Count} Streams[/]", false); + task.MaxValue = totalSize; + task.StartTask(); - isNew = await downloadService.DownloadPostMedia( - url: postKVP.Value, - folder: path, - media_id: postKVP.Key, - api_type: "Posts", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? string.Empty, - postInfo: postInfo, - postMedia: mediaInfo, - author: postInfo?.author, - users: users); - } - catch - { - Console.WriteLine("Media was null"); - } - } - } - task.StopTask(); - }); - if (isNew) - { - AnsiConsole.Markup($"[red]Post {post_id} downloaded\n[/]"); - Log.Debug($"Post {post_id} downloaded"); - } - else - { - AnsiConsole.Markup($"[red]Post {post_id} already downloaded\n[/]"); - Log.Debug($"Post {post_id} already downloaded"); - } - } + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadStreams(username, user.Value, path, + hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, streams, + progressReporter); - public async Task<(bool IsExit, Dictionary? selectedUsers)> HandleUserSelection(Dictionary users, Dictionary lists) - { + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Streams Already Downloaded: {result.ExistingDownloads} New Streams Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + AnsiConsole.Markup("[red]Found 0 Streams\n[/]"); + return 0; + } + + private async Task DownloadPaidMessage(string username, + KeyValuePair> hasSelectedUsersKVP, int paidMessagesCount, string path, + long message_id) + { + IDBService dbService = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + Log.Debug($"Calling DownloadPaidMessage - {username}"); + + AnsiConsole.Markup("[red]Getting Paid Message\n[/]"); + + SinglePaidMessageCollection singlePaidMessageCollection = + await apiService.GetPaidMessage($"/messages/{message_id.ToString()}", path); + int oldPreviewPaidMessagesCount = 0; + int newPreviewPaidMessagesCount = 0; + int oldPaidMessagesCount = 0; + int newPaidMessagesCount = 0; + if (singlePaidMessageCollection != null && singlePaidMessageCollection.PreviewSingleMessages.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages\n[/]"); + Log.Debug( + $"Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); + paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection + .PreviewSingleMessages.Values.ToList()); + } + else + { + totalSize = paidMessagesCount; + } + + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + // Define tasks + ProgressTask task = + ctx.AddTask( + $"[red]Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Paid Messages[/]", + false); + Log.Debug($"Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Paid Messages"); + task.MaxValue = totalSize; + task.StartTask(); + foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection + .PreviewSingleMessages) + { + bool isNew; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = paidMessageKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string messageId = messageUrlParsed[5]; + string? licenseURL = null; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh != null) + { + DateTime lastModified = + await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders( + $"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } + else + { + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } + + Medium? mediaInfo = + singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => + m.id == paidMessageKVP.Key); + SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadSingleMessagePreviewDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + hasSelectedUsersKVP.Value); + + if (isNew) + { + newPreviewPaidMessagesCount++; + } + else + { + oldPreviewPaidMessagesCount++; + } + } + } + else + { + Medium? mediaInfo = + singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => + m.id == paidMessageKVP.Key); + SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadMessagePreviewMedia( + paidMessageKVP.Value, + path, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + hasSelectedUsersKVP.Value); + if (isNew) + { + newPreviewPaidMessagesCount++; + } + else + { + oldPreviewPaidMessagesCount++; + } + } + } + + task.StopTask(); + }); + AnsiConsole.Markup( + $"[red]Preview Paid Messages Already Downloaded: {oldPreviewPaidMessagesCount} New Preview Paid Messages Downloaded: {newPreviewPaidMessagesCount}[/]\n"); + Log.Debug( + $"Preview Paid Messages Already Downloaded: {oldPreviewPaidMessagesCount} New Preview Paid Messages Downloaded: {newPreviewPaidMessagesCount}"); + } + + if (singlePaidMessageCollection != null && singlePaidMessageCollection.SingleMessages.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages\n[/]"); + Log.Debug( + $"Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); + paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection.SingleMessages + .Values.ToList()); + } + else + { + totalSize = paidMessagesCount; + } + + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + // Define tasks + ProgressTask task = + ctx.AddTask( + $"[red]Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages[/]", + false); + Log.Debug($"Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages"); + task.MaxValue = totalSize; + task.StartTask(); + foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.SingleMessages) + { + bool isNew; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = paidMessageKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string messageId = messageUrlParsed[5]; + string? licenseURL = null; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh != null) + { + DateTime lastModified = + await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders( + $"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } + else + { + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } + + Medium? mediaInfo = + singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => + m.id == paidMessageKVP.Key); + SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadSinglePurchasedMessageDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + hasSelectedUsersKVP.Value); + + if (isNew) + { + newPaidMessagesCount++; + } + else + { + oldPaidMessagesCount++; + } + } + } + else + { + Medium? mediaInfo = + singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => + m.id == paidMessageKVP.Key); + SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadSinglePurchasedMedia( + paidMessageKVP.Value, + path, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + hasSelectedUsersKVP.Value); + if (isNew) + { + newPaidMessagesCount++; + } + else + { + oldPaidMessagesCount++; + } + } + } + + task.StopTask(); + }); + AnsiConsole.Markup( + $"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); + Log.Debug( + $"Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}"); + } + else + { + AnsiConsole.Markup("[red]Found 0 Paid Messages\n[/]"); + Log.Debug("Found 0 Paid Messages"); + } + + return paidMessagesCount; + } + + private async Task DownloadSinglePost(string username, long post_id, string path, Dictionary users) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + Log.Debug($"Calling DownloadSinglePost - {post_id.ToString()}"); + + AnsiConsole.Markup("[red]Getting Post\n[/]"); + SinglePostCollection post = await apiService.GetPost($"/posts/{post_id.ToString()}", path); + if (post == null) + { + AnsiConsole.Markup("[red]Couldn't find post\n[/]"); + Log.Debug("Couldn't find post"); + return; + } + + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(post.SinglePosts.Values.ToList()); + } + else + { + totalSize = post.SinglePosts.Count; + } + + bool isNew = false; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask("[red]Downloading Post[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + foreach (KeyValuePair postKVP in post.SinglePosts) + { + if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = postKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string postId = messageUrlParsed[5]; + string? licenseURL = null; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh == null) + { + continue; + } + + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", + "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", + pssh); + } + else + { + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", + pssh); + } + + SinglePost.Medium mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.id == postKVP.Key); + SinglePost postInfo = + post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadPostDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + postKVP.Key, + "Posts", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? + string.Empty, + postInfo, + mediaInfo, + postInfo?.author, + users); + } + else + { + try + { + SinglePost.Medium? mediaInfo = + post.SinglePostMedia.FirstOrDefault(m => m?.id == postKVP.Key); + SinglePost? postInfo = + post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadPostMedia( + postKVP.Value, + path, + postKVP.Key, + "Posts", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PostFileNameFormat ?? string.Empty, + postInfo, + mediaInfo, + postInfo?.author, + users); + } + catch + { + Console.WriteLine("Media was null"); + } + } + } + + task.StopTask(); + }); + if (isNew) + { + AnsiConsole.Markup($"[red]Post {post_id} downloaded\n[/]"); + Log.Debug($"Post {post_id} downloaded"); + } + else + { + AnsiConsole.Markup($"[red]Post {post_id} already downloaded\n[/]"); + Log.Debug($"Post {post_id} already downloaded"); + } + } + + 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 Dictionary(); - Entities.Config currentConfig = configService.CurrentConfig!; + bool hasSelectedUsers = false; + Dictionary selectedUsers = new(); + Config currentConfig = configService.CurrentConfig!; - while (!hasSelectedUsers) - { - var mainMenuOptions = GetMainMenuOptions(users, lists); + while (!hasSelectedUsers) + { + List mainMenuOptions = GetMainMenuOptions(users, lists); - var 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) - ); + 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) - { - var listSelectionPrompt = new MultiSelectionPrompt(); - 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); - } - var listSelection = AnsiConsole.Prompt(listSelectionPrompt); + 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); + } - if (listSelection.Contains("[red]Go Back[/]")) - { - break; // Go back to the main menu - } - else - { - hasSelectedUsers = true; - List listUsernames = new(); - foreach (var 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}")).Distinct().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) - { - var selectedNamesPrompt = new MultiSelectionPrompt(); - 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); - } - var userSelection = AnsiConsole.Prompt(selectedNamesPrompt); - if (userSelection.Contains("[red]Go Back[/]")) - { - break; // Go back to the main menu - } - else - { - 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) - { - if (currentConfig == null) - currentConfig = new Entities.Config(); + List listSelection = AnsiConsole.Prompt(listSelectionPrompt); - var choices = new List<(string choice, bool isSelected)> - { - ("[red]Go Back[/]", false) - }; + if (listSelection.Contains("[red]Go Back[/]")) + { + break; // Go back to the main menu + } - foreach(var propInfo in typeof(Entities.Config).GetProperties()) - { - var attr = propInfo.GetCustomAttribute(); - if(attr != null) - { - string itemLabel = $"[red]{propInfo.Name}[/]"; - choices.Add(new(itemLabel, (bool)propInfo.GetValue(currentConfig)!)); - } - } + 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); + } + } - MultiSelectionPrompt multiSelectionPrompt = new MultiSelectionPrompt() - .Title("[red]Edit config.conf[/]") - .PageSize(25); + selectedUsers = users.Where(x => listUsernames.Contains($"{x.Key}")).Distinct() + .ToDictionary(x => x.Key, x => x.Value); + AnsiConsole.Markup(string.Format("[red]Downloading from List(s): {0}[/]", + string.Join(", ", listSelection))); + break; + } - foreach (var choice in choices) - { - multiSelectionPrompt.AddChoices(choice.choice, (selectionItem) => { if (choice.isSelected) selectionItem.Select(); }); - } + 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); + } - var configOptions = AnsiConsole.Prompt(multiSelectionPrompt); + List userSelection = AnsiConsole.Prompt(selectedNamesPrompt); + if (userSelection.Contains("[red]Go Back[/]")) + { + break; // Go back to the main menu + } - if (configOptions.Contains("[red]Go Back[/]")) - { - break; - } + hasSelectedUsers = true; + selectedUsers = users.Where(x => userSelection.Contains($"[red]{x.Key}[/]")) + .ToDictionary(x => x.Key, x => x.Value); + break; + } - bool configChanged = false; + 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) + { + if (currentConfig == null) + { + currentConfig = new Config(); + } - Entities.Config newConfig = new Entities.Config(); - foreach (var propInfo in typeof(Entities.Config).GetProperties()) - { - var attr = propInfo.GetCustomAttribute(); - if (attr != null) - { - // - // Get the new choice from the selection - // - string itemLabel = $"[red]{propInfo.Name}[/]"; - var newValue = configOptions.Contains(itemLabel); - var oldValue = choices.Where(c => c.choice == itemLabel).Select(c => c.isSelected).First(); - propInfo.SetValue(newConfig, newValue); + List<(string choice, bool isSelected)> choices = new() { ("[red]Go Back[/]", false) }; - if (newValue != oldValue) - configChanged = true; - } - else - { - // - // Reassign any non toggleable values - // - propInfo.SetValue(newConfig, propInfo.GetValue(currentConfig)); - } - } + foreach (PropertyInfo propInfo in typeof(Config).GetProperties()) + { + ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute(); + if (attr != null) + { + string itemLabel = $"[red]{propInfo.Name}[/]"; + choices.Add(new ValueTuple(itemLabel, + (bool)propInfo.GetValue(currentConfig)!)); + } + } - configService.UpdateConfig(newConfig); - await configService.SaveConfigurationAsync(); + MultiSelectionPrompt multiSelectionPrompt = new MultiSelectionPrompt() + .Title("[red]Edit config.conf[/]") + .PageSize(25); - currentConfig = newConfig; - if (configChanged) - { - return (true, new Dictionary { { "ConfigChanged", 0 } }); - } - break; - } - break; - case "[red]Change logging level[/]": - while (true) - { - var choices = new List<(string choice, bool isSelected)> - { - ("[red]Go Back[/]", false) - }; + foreach ((string choice, bool isSelected) choice in choices) + { + multiSelectionPrompt.AddChoices(choice.choice, selectionItem => + { + if (choice.isSelected) + { + selectionItem.Select(); + } + }); + } - foreach (string name in typeof(LoggingLevel).GetEnumNames()) - { - string itemLabel = $"[red]{name}[/]"; - choices.Add(new(itemLabel, name == loggingService.GetCurrentLoggingLevel().ToString())); - } + List configOptions = AnsiConsole.Prompt(multiSelectionPrompt); - SelectionPrompt selectionPrompt = new SelectionPrompt() - .Title("[red]Select logging level[/]") - .PageSize(25); + if (configOptions.Contains("[red]Go Back[/]")) + { + break; + } - foreach (var choice in choices) - { - selectionPrompt.AddChoice(choice.choice); - } + bool configChanged = false; - string levelOption = AnsiConsole.Prompt(selectionPrompt); + Config newConfig = new(); + foreach (PropertyInfo propInfo in typeof(Config).GetProperties()) + { + ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute(); + if (attr != null) + { + // + // Get the new choice from the selection + // + string itemLabel = $"[red]{propInfo.Name}[/]"; + bool newValue = configOptions.Contains(itemLabel); + bool oldValue = choices.Where(c => c.choice == itemLabel).Select(c => c.isSelected) + .First(); + propInfo.SetValue(newConfig, newValue); - if (levelOption.Contains("[red]Go Back[/]")) - { - break; - } + if (newValue != oldValue) + { + configChanged = true; + } + } + else + { + // + // Reassign any non toggleable values + // + propInfo.SetValue(newConfig, propInfo.GetValue(currentConfig)); + } + } - levelOption = levelOption.Replace("[red]", "").Replace("[/]", ""); - LoggingLevel newLogLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), levelOption, true); + configService.UpdateConfig(newConfig); + await configService.SaveConfigurationAsync(); - Log.Debug($"Logging level changed to: {levelOption}"); + currentConfig = newConfig; + if (configChanged) + { + return (true, new Dictionary { { "ConfigChanged", 0 } }); + } - bool configChanged = false; + break; + } - Entities.Config newConfig = new Entities.Config(); + break; + case "[red]Change logging level[/]": + while (true) + { + List<(string choice, bool isSelected)> choices = new() { ("[red]Go Back[/]", false) }; - newConfig = currentConfig; + foreach (string name in typeof(LoggingLevel).GetEnumNames()) + { + string itemLabel = $"[red]{name}[/]"; + choices.Add(new ValueTuple(itemLabel, + name == loggingService.GetCurrentLoggingLevel().ToString())); + } - newConfig.LoggingLevel = newLogLevel; + SelectionPrompt selectionPrompt = new SelectionPrompt() + .Title("[red]Select logging level[/]") + .PageSize(25); - currentConfig = newConfig; + foreach ((string choice, bool isSelected) choice in choices) + { + selectionPrompt.AddChoice(choice.choice); + } - configService.UpdateConfig(newConfig); - await configService.SaveConfigurationAsync(); + string levelOption = AnsiConsole.Prompt(selectionPrompt); - if (configChanged) - { - return (true, new Dictionary { { "ConfigChanged", 0 } }); - } + if (levelOption.Contains("[red]Go Back[/]")) + { + break; + } - break; - } - break; - case "[red]Logout and Exit[/]": - if (Directory.Exists("chrome-data")) - { - Log.Information("Deleting chrome-data folder"); - Directory.Delete("chrome-data", true); - } - if (File.Exists("auth.json")) - { - Log.Information("Deleting auth.json"); - File.Delete("auth.json"); - } - return (false, null); // Return false to indicate exit - case "[red]Exit[/]": - return (false, null); // Return false to indicate exit - } - } + levelOption = levelOption.Replace("[red]", "").Replace("[/]", ""); + LoggingLevel newLogLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), levelOption, true); - return (true, selectedUsers); // Return true to indicate selected users - } + Log.Debug($"Logging level changed to: {levelOption}"); - 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[/]" - }; - } - else - { - 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[/]" - }; - } - } + bool configChanged = false; - static bool ValidateFilePath(string path) - { - char[] invalidChars = System.IO.Path.GetInvalidPathChars(); - char[] foundInvalidChars = path.Where(c => invalidChars.Contains(c)).ToArray(); + Config newConfig = new(); - if (foundInvalidChars.Any()) - { - AnsiConsole.Markup($"[red]Invalid characters found in path {path}:[/] {string.Join(", ", foundInvalidChars)}\n"); - return false; - } + newConfig = currentConfig; - if (!System.IO.File.Exists(path)) - { - if (System.IO.Directory.Exists(path)) - { - AnsiConsole.Markup($"[red]The provided path {path} improperly points to a directory and not a file.[/]\n"); - } - else - { - AnsiConsole.Markup($"[red]The provided path {path} does not exist or is not accessible.[/]\n"); - } - return false; - } + newConfig.LoggingLevel = newLogLevel; - return true; - } + currentConfig = newConfig; - static ProgressColumn[] GetProgressColumns(bool showScrapeSize) - { - List progressColumns; - if (showScrapeSize) - { - progressColumns = new List() - { - new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn(), new DownloadedColumn(), new RemainingTimeColumn() - }; - } - else - { - progressColumns = new List() - { - new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn() - }; - } - return progressColumns.ToArray(); - } + configService.UpdateConfig(newConfig); + await configService.SaveConfigurationAsync(); - public static string? GetFullPath(string filename) - { - if (File.Exists(filename)) - { - return Path.GetFullPath(filename); - } + if (configChanged) + { + return (true, new Dictionary { { "ConfigChanged", 0 } }); + } - var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - foreach (var path in pathEnv.Split(Path.PathSeparator)) - { - var fullPath = Path.Combine(path, filename); - if (File.Exists(fullPath)) - { - return fullPath; - } - } - return null; - } + break; + } - public static void ValidateCookieString(Auth auth) - { - string pattern = @"(auth_id=\d+)|(sess=[^;]+)"; - var matches = Regex.Matches(auth.COOKIE, pattern); + break; + case "[red]Logout and Exit[/]": + if (Directory.Exists("chrome-data")) + { + Log.Information("Deleting chrome-data folder"); + Directory.Delete("chrome-data", true); + } - string output = string.Join("; ", matches); + if (File.Exists("auth.json")) + { + Log.Information("Deleting auth.json"); + File.Delete("auth.json"); + } - if (!output.EndsWith(";")) - { - output += ";"; - } + return (false, null); // Return false to indicate exit + case "[red]Exit[/]": + return (false, null); // Return false to indicate exit + } + } - if(auth.COOKIE.Trim() != output.Trim()) - { - auth.COOKIE = output; - string newAuthString = JsonConvert.SerializeObject(auth, Formatting.Indented); - File.WriteAllText("auth.json", newAuthString); - } - } + return (true, selectedUsers); // Return true to indicate selected users + } + + 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 static bool ValidateFilePath(string path) + { + char[] invalidChars = Path.GetInvalidPathChars(); + char[] foundInvalidChars = path.Where(c => invalidChars.Contains(c)).ToArray(); + + if (foundInvalidChars.Any()) + { + AnsiConsole.Markup( + $"[red]Invalid characters found in path {path}:[/] {string.Join(", ", foundInvalidChars)}\n"); + return false; + } + + if (!File.Exists(path)) + { + if (Directory.Exists(path)) + { + AnsiConsole.Markup( + $"[red]The provided path {path} improperly points to a directory and not a file.[/]\n"); + } + else + { + AnsiConsole.Markup($"[red]The provided path {path} does not exist or is not accessible.[/]\n"); + } + + return false; + } + + return true; + } + + private static ProgressColumn[] GetProgressColumns(bool showScrapeSize) + { + List progressColumns; + if (showScrapeSize) + { + progressColumns = new List + { + new TaskDescriptionColumn(), + new ProgressBarColumn(), + new PercentageColumn(), + new DownloadedColumn(), + new RemainingTimeColumn() + }; + } + else + { + progressColumns = new List + { + new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn() + }; + } + + return progressColumns.ToArray(); + } + + public static string? GetFullPath(string filename) + { + if (File.Exists(filename)) + { + return Path.GetFullPath(filename); + } + + string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + foreach (string path in pathEnv.Split(Path.PathSeparator)) + { + string fullPath = Path.Combine(path, filename); + if (File.Exists(fullPath)) + { + return fullPath; + } + } + + return null; + } + + public static void ValidateCookieString(Auth auth) + { + string pattern = @"(auth_id=\d+)|(sess=[^;]+)"; + MatchCollection matches = Regex.Matches(auth.COOKIE, pattern); + + string output = string.Join("; ", matches); + + if (!output.EndsWith(";")) + { + output += ";"; + } + + if (auth.COOKIE.Trim() != output.Trim()) + { + auth.COOKIE = output; + string newAuthString = JsonConvert.SerializeObject(auth, Formatting.Indented); + File.WriteAllText("auth.json", newAuthString); + } + } } diff --git a/OF DL/References/Spectre.Console.deps.json b/OF DL/References/Spectre.Console.deps.json index 5470288..3b1ab90 100644 --- a/OF DL/References/Spectre.Console.deps.json +++ b/OF DL/References/Spectre.Console.deps.json @@ -1,112 +1,112 @@ { - "runtimeTarget": { - "name": ".NETCoreApp,Version=v7.0", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETCoreApp,Version=v7.0": { - "Spectre.Console/0.0.0-preview.0": { - "dependencies": { - "Microsoft.SourceLink.GitHub": "1.1.1", - "MinVer": "4.2.0", - "Roslynator.Analyzers": "4.1.2", - "StyleCop.Analyzers": "1.2.0-beta.435", - "System.Memory": "4.5.5", - "Wcwidth.Sources": "1.0.0" + "runtimeTarget": { + "name": ".NETCoreApp,Version=v7.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v7.0": { + "Spectre.Console/0.0.0-preview.0": { + "dependencies": { + "Microsoft.SourceLink.GitHub": "1.1.1", + "MinVer": "4.2.0", + "Roslynator.Analyzers": "4.1.2", + "StyleCop.Analyzers": "1.2.0-beta.435", + "System.Memory": "4.5.5", + "Wcwidth.Sources": "1.0.0" + }, + "runtime": { + "Spectre.Console.dll": {} + } + }, + "Microsoft.Build.Tasks.Git/1.1.1": {}, + "Microsoft.SourceLink.Common/1.1.1": {}, + "Microsoft.SourceLink.GitHub/1.1.1": { + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.1", + "Microsoft.SourceLink.Common": "1.1.1" + } + }, + "MinVer/4.2.0": {}, + "Roslynator.Analyzers/4.1.2": {}, + "StyleCop.Analyzers/1.2.0-beta.435": { + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.435" + } + }, + "StyleCop.Analyzers.Unstable/1.2.0.435": {}, + "System.Memory/4.5.5": {}, + "Wcwidth.Sources/1.0.0": {} + } + }, + "libraries": { + "Spectre.Console/0.0.0-preview.0": { + "type": "project", + "serviceable": false, + "sha512": "" }, - "runtime": { - "Spectre.Console.dll": {} + "Microsoft.Build.Tasks.Git/1.1.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==", + "path": "microsoft.build.tasks.git/1.1.1", + "hashPath": "microsoft.build.tasks.git.1.1.1.nupkg.sha512" + }, + "Microsoft.SourceLink.Common/1.1.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==", + "path": "microsoft.sourcelink.common/1.1.1", + "hashPath": "microsoft.sourcelink.common.1.1.1.nupkg.sha512" + }, + "Microsoft.SourceLink.GitHub/1.1.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==", + "path": "microsoft.sourcelink.github/1.1.1", + "hashPath": "microsoft.sourcelink.github.1.1.1.nupkg.sha512" + }, + "MinVer/4.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Po4tv+sri1jsaebQ8F6+yD5ru9Gas5mR111F6HR2ULqwflvjjZXMstpeOc1GHMJeQa3g4E3b8MX8K2cShkuUAg==", + "path": "minver/4.2.0", + "hashPath": "minver.4.2.0.nupkg.sha512" + }, + "Roslynator.Analyzers/4.1.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-bNl3GRSBFjJymYnwq/IRDD9MOSZz9VKdGk9RsN0MWIXoSRnVKQv84f6s9nLE13y20lZgMZKlDqGw2uInBH4JgA==", + "path": "roslynator.analyzers/4.1.2", + "hashPath": "roslynator.analyzers.4.1.2.nupkg.sha512" + }, + "StyleCop.Analyzers/1.2.0-beta.435": { + "type": "package", + "serviceable": true, + "sha512": "sha512-TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==", + "path": "stylecop.analyzers/1.2.0-beta.435", + "hashPath": "stylecop.analyzers.1.2.0-beta.435.nupkg.sha512" + }, + "StyleCop.Analyzers.Unstable/1.2.0.435": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg==", + "path": "stylecop.analyzers.unstable/1.2.0.435", + "hashPath": "stylecop.analyzers.unstable.1.2.0.435.nupkg.sha512" + }, + "System.Memory/4.5.5": { + "type": "package", + "serviceable": true, + "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "path": "system.memory/4.5.5", + "hashPath": "system.memory.4.5.5.nupkg.sha512" + }, + "Wcwidth.Sources/1.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-86tmwfGXRz7GJQXBnoTFoMvqSqd6irfkEkRzQFR54W/nweaR8cUvzY8x++z+B/+eUPSuqD2Ah1iPJHgthy4pzg==", + "path": "wcwidth.sources/1.0.0", + "hashPath": "wcwidth.sources.1.0.0.nupkg.sha512" } - }, - "Microsoft.Build.Tasks.Git/1.1.1": {}, - "Microsoft.SourceLink.Common/1.1.1": {}, - "Microsoft.SourceLink.GitHub/1.1.1": { - "dependencies": { - "Microsoft.Build.Tasks.Git": "1.1.1", - "Microsoft.SourceLink.Common": "1.1.1" - } - }, - "MinVer/4.2.0": {}, - "Roslynator.Analyzers/4.1.2": {}, - "StyleCop.Analyzers/1.2.0-beta.435": { - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.435" - } - }, - "StyleCop.Analyzers.Unstable/1.2.0.435": {}, - "System.Memory/4.5.5": {}, - "Wcwidth.Sources/1.0.0": {} } - }, - "libraries": { - "Spectre.Console/0.0.0-preview.0": { - "type": "project", - "serviceable": false, - "sha512": "" - }, - "Microsoft.Build.Tasks.Git/1.1.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==", - "path": "microsoft.build.tasks.git/1.1.1", - "hashPath": "microsoft.build.tasks.git.1.1.1.nupkg.sha512" - }, - "Microsoft.SourceLink.Common/1.1.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==", - "path": "microsoft.sourcelink.common/1.1.1", - "hashPath": "microsoft.sourcelink.common.1.1.1.nupkg.sha512" - }, - "Microsoft.SourceLink.GitHub/1.1.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==", - "path": "microsoft.sourcelink.github/1.1.1", - "hashPath": "microsoft.sourcelink.github.1.1.1.nupkg.sha512" - }, - "MinVer/4.2.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-Po4tv+sri1jsaebQ8F6+yD5ru9Gas5mR111F6HR2ULqwflvjjZXMstpeOc1GHMJeQa3g4E3b8MX8K2cShkuUAg==", - "path": "minver/4.2.0", - "hashPath": "minver.4.2.0.nupkg.sha512" - }, - "Roslynator.Analyzers/4.1.2": { - "type": "package", - "serviceable": true, - "sha512": "sha512-bNl3GRSBFjJymYnwq/IRDD9MOSZz9VKdGk9RsN0MWIXoSRnVKQv84f6s9nLE13y20lZgMZKlDqGw2uInBH4JgA==", - "path": "roslynator.analyzers/4.1.2", - "hashPath": "roslynator.analyzers.4.1.2.nupkg.sha512" - }, - "StyleCop.Analyzers/1.2.0-beta.435": { - "type": "package", - "serviceable": true, - "sha512": "sha512-TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==", - "path": "stylecop.analyzers/1.2.0-beta.435", - "hashPath": "stylecop.analyzers.1.2.0-beta.435.nupkg.sha512" - }, - "StyleCop.Analyzers.Unstable/1.2.0.435": { - "type": "package", - "serviceable": true, - "sha512": "sha512-ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg==", - "path": "stylecop.analyzers.unstable/1.2.0.435", - "hashPath": "stylecop.analyzers.unstable.1.2.0.435.nupkg.sha512" - }, - "System.Memory/4.5.5": { - "type": "package", - "serviceable": true, - "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", - "path": "system.memory/4.5.5", - "hashPath": "system.memory.4.5.5.nupkg.sha512" - }, - "Wcwidth.Sources/1.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-86tmwfGXRz7GJQXBnoTFoMvqSqd6irfkEkRzQFR54W/nweaR8cUvzY8x++z+B/+eUPSuqD2Ah1iPJHgthy4pzg==", - "path": "wcwidth.sources/1.0.0", - "hashPath": "wcwidth.sources.1.0.0.nupkg.sha512" - } - } -} \ No newline at end of file +} diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 00a4870..8132cd7 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -1,3 +1,8 @@ +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Entities; @@ -10,14 +15,9 @@ using OF_DL.Entities.Purchased; using OF_DL.Entities.Stories; using OF_DL.Entities.Streams; using OF_DL.Enumerations; +using OF_DL.Widevine; using Serilog; using Spectre.Console; -using System.Globalization; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Xml.Linq; -using OF_DL.Widevine; using static OF_DL.Utils.HttpUtil; using Constants = OF_DL.Helpers.Constants; @@ -26,19 +26,14 @@ namespace OF_DL.Services; public class APIService(IAuthService authService, IConfigService configService, IDBService dbService) : IAPIService { + private const int MaxAttempts = 30; + private const int DelayBetweenAttempts = 3000; private static readonly JsonSerializerSettings m_JsonSerializerSettings; private static DateTime? cachedDynamicRulesExpiration; private static DynamicRules? cachedDynamicRules; - private const int MaxAttempts = 30; - private const int DelayBetweenAttempts = 3000; - static APIService() - { - m_JsonSerializerSettings = new() - { - MissingMemberHandling = MissingMemberHandling.Ignore - }; - } + static APIService() => + m_JsonSerializerSettings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }; public Dictionary GetDynamicHeaders(string path, string queryParams) @@ -83,7 +78,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } - DateTimeOffset dto = (DateTimeOffset)DateTime.UtcNow; + DateTimeOffset dto = DateTime.UtcNow; long timestamp = dto.ToUnixTimeMilliseconds(); string input = $"{root!.StaticParam}\n{timestamp}\n{path + queryParams}\n{authService.CurrentAuth.USER_ID}"; @@ -91,8 +86,9 @@ public class APIService(IAuthService authService, IConfigService configService, byte[] hashBytes = SHA1.HashData(inputBytes); string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); - var checksum = root.ChecksumIndexes.Aggregate(0, (current, number) => current + hashString[number]) + root.ChecksumConstant!.Value; - var sign = $"{root.Prefix}:{hashString}:{checksum.ToString("X").ToLower()}:{root.Suffix}"; + int checksum = root.ChecksumIndexes.Aggregate(0, (current, number) => current + hashString[number]) + + root.ChecksumConstant!.Value; + string sign = $"{root.Prefix}:{hashString}:{checksum.ToString("X").ToLower()}:{root.Suffix}"; Dictionary headers = new() { @@ -109,121 +105,23 @@ public class APIService(IAuthService authService, IConfigService configService, } - private async Task BuildHeaderAndExecuteRequests(Dictionary getParams, string endpoint, HttpClient client) - { - Log.Debug("Calling BuildHeaderAndExecuteRequests"); - - HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint); - using var response = await client.SendAsync(request); - response.EnsureSuccessStatusCode(); - string body = await response.Content.ReadAsStringAsync(); - - Log.Debug(body); - - return body; - } - - - private async Task BuildHttpRequestMessage(Dictionary getParams, string endpoint) - { - Log.Debug("Calling BuildHttpRequestMessage"); - - string queryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); - - Dictionary headers = GetDynamicHeaders($"/api2/v2{endpoint}", queryParams); - - HttpRequestMessage request = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{queryParams}"); - - Log.Debug($"Full request URL: {Constants.API_URL}{endpoint}{queryParams}"); - - foreach (KeyValuePair keyValuePair in headers) - { - request.Headers.Add(keyValuePair.Key, keyValuePair.Value); - } - - return request; - } - - private static double ConvertToUnixTimestampWithMicrosecondPrecision(DateTime date) - { - DateTime origin = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - TimeSpan diff = date.ToUniversalTime() - origin; - - return diff.TotalSeconds; // This gives the number of seconds. If you need milliseconds, use diff.TotalMilliseconds - } - - public static bool IsStringOnlyDigits(string input) - { - return input.All(char.IsDigit); - } - - - private HttpClient GetHttpClient() - { - var client = new HttpClient(); - if (configService.CurrentConfig?.Timeout != null && configService.CurrentConfig.Timeout > 0) - { - client.Timeout = TimeSpan.FromSeconds(configService.CurrentConfig.Timeout.Value); - } - return client; - } - - - /// - /// this one is used during initialization only - /// if the config option is not available then no modificatiotns will be done on the getParams - /// - /// - /// - /// - private static void UpdateGetParamsForDateSelection(Enumerations.DownloadDateSelection downloadDateSelection, ref Dictionary getParams, DateTime? dt) - { - //if (config.DownloadOnlySpecificDates && dt.HasValue) - //{ - if (dt.HasValue) - { - UpdateGetParamsForDateSelection( - downloadDateSelection, - ref getParams, - ConvertToUnixTimestampWithMicrosecondPrecision(dt.Value).ToString("0.000000", CultureInfo.InvariantCulture) - ); - } - //} - } - - private static void UpdateGetParamsForDateSelection(Enumerations.DownloadDateSelection downloadDateSelection, ref Dictionary getParams, string unixTimeStampInMicrosec) - { - switch (downloadDateSelection) - { - case Enumerations.DownloadDateSelection.before: - getParams["beforePublishTime"] = unixTimeStampInMicrosec; - break; - case Enumerations.DownloadDateSelection.after: - getParams["order"] = "publish_date_asc"; - getParams["afterPublishTime"] = unixTimeStampInMicrosec; - break; - } - } - - public async Task GetUserInfo(string endpoint) { Log.Debug($"Calling GetUserInfo: {endpoint}"); try { - Entities.User? user = new(); + User? user = new(); int post_limit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, - { "order", "publish_date_asc" } + { "limit", post_limit.ToString() }, { "order", "publish_date_asc" } }; HttpClient client = new(); HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint); - using var response = await client.SendAsync(request); + using HttpResponseMessage response = await client.SendAsync(request); if (!response.IsSuccessStatusCode) { @@ -231,8 +129,8 @@ public class APIService(IAuthService authService, IConfigService configService, } response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); - user = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + string body = await response.Content.ReadAsStringAsync(); + user = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); return user; } catch (Exception ex) @@ -242,10 +140,13 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } @@ -256,16 +157,18 @@ public class APIService(IAuthService authService, IConfigService configService, HttpClient client = new(); HttpRequestMessage request = await BuildHttpRequestMessage(new Dictionary(), endpoint); - using var response = await client.SendAsync(request); + using HttpResponseMessage response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); + string body = await response.Content.ReadAsStringAsync(); //if the content creator doesnt exist, we get a 200 response, but the content isnt usable //so let's not throw an exception, since "content creator no longer exists" is handled elsewhere //which means we wont get loads of exceptions if (body.Equals("[]")) + { return null; + } JObject jObject = JObject.Parse(body); @@ -278,75 +181,13 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } - return null; - } - - public async Task?> GetAllSubscriptions(Dictionary getParams, string endpoint, bool includeRestricted) - { - try - { - Dictionary users = new(); - Subscriptions subscriptions = new(); - - Log.Debug("Calling GetAllSubscrptions"); - - string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); - - subscriptions = JsonConvert.DeserializeObject(body); - if (subscriptions != null && subscriptions.hasMore) - { - getParams["offset"] = subscriptions.list.Count.ToString(); - - while (true) - { - Subscriptions newSubscriptions = new(); - string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); - - if (!string.IsNullOrEmpty(loopbody) && (!loopbody.Contains("[]") || loopbody.Trim() != "[]")) - { - newSubscriptions = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); - } - else - { - break; - } - - subscriptions.list.AddRange(newSubscriptions.list); - if (!newSubscriptions.hasMore) - { - break; - } - getParams["offset"] = subscriptions.list.Count.ToString(); - } - } - - foreach (Subscriptions.List subscription in subscriptions.list) - { - if ((!(subscription.isRestricted ?? false) || ((subscription.isRestricted ?? false) && includeRestricted)) - && !users.ContainsKey(subscription.username)) - { - users.Add(subscription.username, subscription.id); - } - } - - return users; - } - 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); - } - } return null; } @@ -354,10 +195,7 @@ public class APIService(IAuthService authService, IConfigService configService, { Dictionary getParams = new() { - { "offset", "0" }, - { "limit", "50" }, - { "type", "active" }, - { "format", "infinite"} + { "offset", "0" }, { "limit", "50" }, { "type", "active" }, { "format", "infinite" } }; return await GetAllSubscriptions(getParams, endpoint, includeRestricted); @@ -366,13 +204,9 @@ public class APIService(IAuthService authService, IConfigService configService, public async Task?> GetExpiredSubscriptions(string endpoint, bool includeRestricted) { - Dictionary getParams = new() { - { "offset", "0" }, - { "limit", "50" }, - { "type", "expired" }, - { "format", "infinite"} + { "offset", "0" }, { "limit", "50" }, { "type", "expired" }, { "format", "infinite" } }; Log.Debug("Calling GetExpiredSubscriptions"); @@ -428,8 +262,8 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } - } + return lists; } catch (Exception ex) @@ -439,10 +273,13 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } @@ -454,16 +291,12 @@ public class APIService(IAuthService authService, IConfigService configService, try { int offset = 0; - Dictionary getParams = new() - { - { "offset", offset.ToString() }, - { "limit", "50" }, - }; + Dictionary getParams = new() { { "offset", offset.ToString() }, { "limit", "50" } }; List users = new(); while (true) { - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); if (body == null) { break; @@ -488,8 +321,8 @@ public class APIService(IAuthService authService, IConfigService configService, offset += 50; getParams["offset"] = Convert.ToString(offset); - } + return users; } catch (Exception ex) @@ -499,21 +332,23 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } public async Task> GetMedia(MediaType mediatype, - string endpoint, - string? username, - string folder, - List paid_post_ids) + string endpoint, + string? username, + string folder, + List paid_post_ids) { - Log.Debug($"Calling GetMedia - {username}"); try @@ -527,7 +362,6 @@ public class APIService(IAuthService authService, IConfigService configService, switch (mediatype) { - case MediaType.Stories: getParams = new Dictionary { @@ -540,57 +374,67 @@ public class APIService(IAuthService authService, IConfigService configService, case MediaType.Highlights: getParams = new Dictionary { - { "limit", limit.ToString() }, - { "offset", offset.ToString() }, - { "skip_users", "all" } + { "limit", limit.ToString() }, { "offset", offset.ToString() }, { "skip_users", "all" } }; break; } - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); if (mediatype == MediaType.Stories) { Log.Debug("Media Stories - " + endpoint); - var stories = JsonConvert.DeserializeObject>(body, m_JsonSerializerSettings) ?? new List(); + List stories = JsonConvert.DeserializeObject>(body, m_JsonSerializerSettings) ?? + new List(); foreach (Stories story in stories) { if (story.media[0].createdAt.HasValue) { - await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, story.media[0].createdAt.Value); + await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, + story.media[0].createdAt.Value); } else if (story.createdAt.HasValue) { - await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, story.createdAt.Value); + await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, + story.createdAt.Value); } else { await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, DateTime.Now); } + if (story.media != null && story.media.Count > 0) { foreach (Stories.Medium medium in story.media) { - await dbService.AddMedia(folder, medium.id, story.id, medium.files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); + await dbService.AddMedia(folder, medium.id, story.id, medium.files.full.url, null, null, + null, "Stories", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, false, false, null); if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (medium.canView) { if (!return_urls.ContainsKey(medium.id)) @@ -605,7 +449,8 @@ public class APIService(IAuthService authService, IConfigService configService, else if (mediatype == MediaType.Highlights) { List highlight_ids = new(); - var highlights = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings) ?? new Highlights(); + Highlights highlights = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings) ?? + new Highlights(); if (highlights.hasMore) { @@ -617,7 +462,7 @@ public class APIService(IAuthService authService, IConfigService configService, Log.Debug("Media Highlights - " + endpoint); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newhighlights = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); highlights.list.AddRange(newhighlights.list); @@ -625,10 +470,12 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } + offset += 5; getParams["offset"] = offset.ToString(); } } + foreach (Highlights.List list in highlights.list) { if (!highlight_ids.Contains(list.id.ToString())) @@ -640,58 +487,73 @@ public class APIService(IAuthService authService, IConfigService configService, foreach (string highlight_id in highlight_ids) { HighlightMedia highlightMedia = new(); - Dictionary highlight_headers = GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, string.Empty); + Dictionary highlight_headers = + GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, string.Empty); HttpClient highlight_client = GetHttpClient(); - HttpRequestMessage highlight_request = new(HttpMethod.Get, $"https://onlyfans.com/api2/v2/stories/highlights/{highlight_id}"); + HttpRequestMessage highlight_request = new(HttpMethod.Get, + $"https://onlyfans.com/api2/v2/stories/highlights/{highlight_id}"); foreach (KeyValuePair keyValuePair in highlight_headers) { highlight_request.Headers.Add(keyValuePair.Key, keyValuePair.Value); } - using var highlightResponse = await highlight_client.SendAsync(highlight_request); + using HttpResponseMessage highlightResponse = await highlight_client.SendAsync(highlight_request); highlightResponse.EnsureSuccessStatusCode(); - var highlightBody = await highlightResponse.Content.ReadAsStringAsync(); - highlightMedia = JsonConvert.DeserializeObject(highlightBody, m_JsonSerializerSettings); + string highlightBody = await highlightResponse.Content.ReadAsStringAsync(); + highlightMedia = + JsonConvert.DeserializeObject(highlightBody, m_JsonSerializerSettings); if (highlightMedia != null) { foreach (HighlightMedia.Story item in highlightMedia.stories) { if (item.media[0].createdAt.HasValue) { - await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, item.media[0].createdAt.Value); + await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, + item.media[0].createdAt.Value); } else if (item.createdAt.HasValue) { - await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, item.createdAt.Value); + await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, + item.createdAt.Value); } else { - await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, DateTime.Now); + await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, + DateTime.Now); } + if (item.media.Count > 0 && item.media[0].canView) { foreach (HighlightMedia.Medium medium in item.media) { - await dbService.AddMedia(folder, medium.id, item.id, item.media[0].files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); + await dbService.AddMedia(folder, medium.id, item.id, item.media[0].files.full.url, + null, null, null, "Stories", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, false, false, null); if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!return_urls.ContainsKey(medium.id)) { return_urls.Add(medium.id, item.media[0].files.full.url); @@ -712,15 +574,19 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } - public async Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, StatusContext ctx) + public async Task GetPaidPosts(string endpoint, string folder, string username, + List paid_post_ids, StatusContext ctx) { Log.Debug($"Calling GetPaidPosts - {username}"); @@ -738,7 +604,7 @@ public class APIService(IAuthService authService, IConfigService configService, { "author", username } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); paidPosts = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -748,10 +614,9 @@ public class APIService(IAuthService authService, IConfigService configService, getParams["offset"] = paidPosts.list.Count.ToString(); while (true) { - Purchased newPaidPosts = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newPaidPosts = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); paidPosts.list.AddRange(newPaidPosts.list); @@ -762,9 +627,9 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); } - } foreach (Purchased.List purchase in paidPosts.list) @@ -798,7 +663,12 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); + + await dbService.AddPost(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", + purchase.price != null && purchase.isOpened ? true : false, + purchase.isArchived.HasValue ? purchase.isArchived.Value : false, + purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); paidPostCollection.PaidPostObjects.Add(purchase); foreach (Messages.Medium medium in purchase.media) { @@ -811,49 +681,69 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (previewids.Count > 0) { bool has = previewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!has && medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, medium.files.full.url); paidPostCollection.PaidPostMedia.Add(medium); } } else if (!has && medium.canView && medium.files != null && medium.files.drm != null) { - if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - paidPostCollection.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + paidPostCollection.PaidPosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidPostCollection.PaidPostMedia.Add(medium); } - } } else { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, medium.files.full.url); paidPostCollection.PaidPostMedia.Add(medium); } @@ -862,8 +752,14 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - paidPostCollection.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + paidPostCollection.PaidPosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidPostCollection.PaidPostMedia.Add(medium); } } @@ -871,6 +767,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } } + return paidPostCollection; } catch (Exception ex) @@ -880,15 +777,19 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } - public async Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx) + public async Task GetPosts(string endpoint, string folder, List paid_post_ids, + StatusContext ctx) { Log.Debug($"Calling GetPosts - {endpoint}"); @@ -905,20 +806,21 @@ public class APIService(IAuthService authService, IConfigService configService, { "skip_users", "all" } }; - Enumerations.DownloadDateSelection downloadDateSelection = Enumerations.DownloadDateSelection.before; + DownloadDateSelection downloadDateSelection = DownloadDateSelection.before; DateTime? downloadAsOf = null; - if (configService.CurrentConfig.DownloadOnlySpecificDates && configService.CurrentConfig.CustomDate.HasValue) + if (configService.CurrentConfig.DownloadOnlySpecificDates && + configService.CurrentConfig.CustomDate.HasValue) { downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; downloadAsOf = configService.CurrentConfig.CustomDate; } else if (configService.CurrentConfig.DownloadPostsIncrementally) { - var mostRecentPostDate = await dbService.GetMostRecentPostDate(folder); + DateTime? mostRecentPostDate = await dbService.GetMostRecentPostDate(folder); if (mostRecentPostDate.HasValue) { - downloadDateSelection = Enumerations.DownloadDateSelection.after; + downloadDateSelection = DownloadDateSelection.after; downloadAsOf = mostRecentPostDate.Value.AddMinutes(-5); // Back track a little for a margin of error } } @@ -928,9 +830,10 @@ public class APIService(IAuthService authService, IConfigService configService, ref getParams, downloadAsOf); - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); posts = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - ctx.Status($"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]"); + ctx.Status( + $"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); if (posts != null && posts.hasMore) @@ -944,11 +847,12 @@ public class APIService(IAuthService authService, IConfigService configService, { Post newposts = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newposts = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); posts.list.AddRange(newposts.list); - ctx.Status($"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]"); + ctx.Status( + $"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); if (!newposts.hasMore) @@ -967,16 +871,19 @@ public class APIService(IAuthService authService, IConfigService configService, { if (configService.CurrentConfig.SkipAds) { - if (post.rawText != null && (post.rawText.Contains("#ad") || post.rawText.Contains("/trial/") || post.rawText.Contains("#announcement"))) + if (post.rawText != null && (post.rawText.Contains("#ad") || post.rawText.Contains("/trial/") || + post.rawText.Contains("#announcement"))) { continue; } - if (post.text != null && (post.text.Contains("#ad") || post.text.Contains("/trial/") || post.text.Contains("#announcement"))) + if (post.text != null && (post.text.Contains("#ad") || post.text.Contains("/trial/") || + post.text.Contains("#announcement"))) { continue; } } + List postPreviewIds = new(); if (post.preview != null && post.preview.Count > 0) { @@ -991,7 +898,10 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(folder, post.id, post.rawText != null ? post.rawText : string.Empty, post.price != null ? post.price.ToString() : "0", post.price != null && post.isOpened ? true : false, post.isArchived, post.postedAt); + + await dbService.AddPost(folder, post.id, post.rawText != null ? post.rawText : string.Empty, + post.price != null ? post.price : "0", post.price != null && post.isOpened ? true : false, + post.isArchived, post.postedAt); postCollection.PostObjects.Add(post); if (post.media != null && post.media.Count > 0) { @@ -1001,18 +911,22 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (medium.canView && medium.files?.drm == null) { bool has = paid_post_ids.Any(cus => cus.Equals(medium.id)); @@ -1022,7 +936,12 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!postCollection.Posts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, post.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, post.id, medium.files!.full.url, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); postCollection.Posts.Add(medium.id, medium.files!.full.url); postCollection.PostMedia.Add(medium); } @@ -1034,7 +953,12 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!postCollection.Posts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, post.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, post.id, medium.files.preview.url, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); postCollection.Posts.Add(medium.id, medium.files.preview.url); postCollection.PostMedia.Add(medium); } @@ -1048,8 +972,14 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!postCollection.Posts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, post.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); - postCollection.Posts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{post.id}"); + await dbService.AddMedia(folder, medium.id, post.id, medium.files.drm.manifest.dash, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); + postCollection.Posts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{post.id}"); postCollection.PostMedia.Add(medium); } } @@ -1067,12 +997,16 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } + public async Task GetPost(string endpoint, string folder) { Log.Debug($"Calling GetPost - {endpoint}"); @@ -1081,12 +1015,9 @@ public class APIService(IAuthService authService, IConfigService configService, { SinglePost singlePost = new(); SinglePostCollection singlePostCollection = new(); - Dictionary getParams = new() - { - { "skip_users", "all" } - }; + Dictionary getParams = new() { { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); singlePost = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); if (singlePost != null) @@ -1105,7 +1036,11 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(folder, singlePost.id, singlePost.text != null ? singlePost.text : string.Empty, singlePost.price != null ? singlePost.price.ToString() : "0", singlePost.price != null && singlePost.isOpened ? true : false, singlePost.isArchived, singlePost.postedAt); + + await dbService.AddPost(folder, singlePost.id, singlePost.text != null ? singlePost.text : string.Empty, + singlePost.price != null ? singlePost.price : "0", + singlePost.price != null && singlePost.isOpened ? true : false, singlePost.isArchived, + singlePost.postedAt); singlePostCollection.SinglePostObjects.Add(singlePost); if (singlePost.media != null && singlePost.media.Count > 0) { @@ -1115,18 +1050,22 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (medium.canView && medium.files?.drm == null) { switch (configService.CurrentConfig.DownloadVideoResolution) @@ -1136,25 +1075,38 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, singlePost.id, + medium.files!.full.url, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.files!.full.url); singlePostCollection.SinglePostMedia.Add(medium); } } + break; case VideoResolution._240: - if(medium.videoSources != null) + if (medium.videoSources != null) { if (!string.IsNullOrEmpty(medium.videoSources._240)) { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._240, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, medium.videoSources._240); + await dbService.AddMedia(folder, medium.id, singlePost.id, + medium.videoSources._240, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.id, + medium.videoSources._240); singlePostCollection.SinglePostMedia.Add(medium); } } } + break; case VideoResolution._720: if (medium.videoSources != null) @@ -1163,14 +1115,20 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._720, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, medium.videoSources._720); + await dbService.AddMedia(folder, medium.id, singlePost.id, + medium.videoSources._720, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.id, + medium.videoSources._720); singlePostCollection.SinglePostMedia.Add(medium); } } } - break; + break; } } else if (medium.canView && medium.files != null && medium.files.drm != null) @@ -1179,8 +1137,14 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{singlePost.id}"); + await dbService.AddMedia(folder, medium.id, singlePost.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{singlePost.id}"); singlePostCollection.SinglePostMedia.Add(medium); } } @@ -1189,7 +1153,12 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files.preview.url, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.files.preview.url); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1207,14 +1176,18 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } - public async Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx) + public async Task GetStreams(string endpoint, string folder, List paid_post_ids, + StatusContext ctx) { Log.Debug($"Calling GetStreams - {endpoint}"); @@ -1231,8 +1204,9 @@ public class APIService(IAuthService authService, IConfigService configService, { "skip_users", "all" } }; - Enumerations.DownloadDateSelection downloadDateSelection = Enumerations.DownloadDateSelection.before; - if (configService.CurrentConfig.DownloadOnlySpecificDates && configService.CurrentConfig.CustomDate.HasValue) + DownloadDateSelection downloadDateSelection = DownloadDateSelection.before; + if (configService.CurrentConfig.DownloadOnlySpecificDates && + configService.CurrentConfig.CustomDate.HasValue) { downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; } @@ -1242,14 +1216,13 @@ public class APIService(IAuthService authService, IConfigService configService, ref getParams, configService.CurrentConfig.CustomDate); - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); streams = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); if (streams != null && streams.hasMore) { - UpdateGetParamsForDateSelection( downloadDateSelection, ref getParams, @@ -1259,7 +1232,7 @@ public class APIService(IAuthService authService, IConfigService configService, { Streams newstreams = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newstreams = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); streams.list.AddRange(newstreams.list); @@ -1294,7 +1267,10 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(folder, stream.id, stream.text != null ? stream.text : string.Empty, stream.price != null ? stream.price.ToString() : "0", stream.price != null && stream.isOpened ? true : false, stream.isArchived, stream.postedAt); + + await dbService.AddPost(folder, stream.id, stream.text != null ? stream.text : string.Empty, + stream.price != null ? stream.price : "0", stream.price != null && stream.isOpened ? true : false, + stream.isArchived, stream.postedAt); streamsCollection.StreamObjects.Add(stream); if (stream.media != null && stream.media.Count > 0) { @@ -1304,26 +1280,36 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (medium.canView && medium.files?.drm == null) { bool has = paid_post_ids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!has && medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (!streamsCollection.Streams.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, stream.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, stream.id, medium.files.full.url, null, + null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + streamPreviewIds.Contains(medium.id) ? true : false, false, null); streamsCollection.Streams.Add(medium.id, medium.files.full.url); streamsCollection.StreamMedia.Add(medium); } @@ -1336,8 +1322,14 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!streamsCollection.Streams.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, stream.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); - streamsCollection.Streams.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{stream.id}"); + await dbService.AddMedia(folder, medium.id, stream.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + streamPreviewIds.Contains(medium.id) ? true : false, false, null); + streamsCollection.Streams.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{stream.id}"); streamsCollection.StreamMedia.Add(medium); } } @@ -1355,10 +1347,13 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } @@ -1382,8 +1377,9 @@ public class APIService(IAuthService authService, IConfigService configService, { "counters", "1" } }; - Enumerations.DownloadDateSelection downloadDateSelection = Enumerations.DownloadDateSelection.before; - if (configService.CurrentConfig.DownloadOnlySpecificDates && configService.CurrentConfig.CustomDate.HasValue) + DownloadDateSelection downloadDateSelection = DownloadDateSelection.before; + if (configService.CurrentConfig.DownloadOnlySpecificDates && + configService.CurrentConfig.CustomDate.HasValue) { downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; } @@ -1393,7 +1389,7 @@ public class APIService(IAuthService authService, IConfigService configService, ref getParams, configService.CurrentConfig.CustomDate); - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); archived = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1401,14 +1397,14 @@ public class APIService(IAuthService authService, IConfigService configService, if (archived != null && archived.hasMore) { UpdateGetParamsForDateSelection( - downloadDateSelection, - ref getParams, - archived.tailMarker); + downloadDateSelection, + ref getParams, + archived.tailMarker); while (true) { Archived newarchived = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newarchived = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); archived.list.AddRange(newarchived.list); @@ -1419,10 +1415,11 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } + UpdateGetParamsForDateSelection( - downloadDateSelection, - ref getParams, - newarchived.tailMarker); + downloadDateSelection, + ref getParams, + newarchived.tailMarker); } } @@ -1442,7 +1439,10 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(folder, archive.id, archive.text != null ? archive.text : string.Empty, archive.price != null ? archive.price.ToString() : "0", archive.price != null && archive.isOpened ? true : false, archive.isArchived, archive.postedAt); + + await dbService.AddPost(folder, archive.id, archive.text != null ? archive.text : string.Empty, + archive.price != null ? archive.price : "0", + archive.price != null && archive.isOpened ? true : false, archive.isArchived, archive.postedAt); archivedCollection.ArchivedPostObjects.Add(archive); if (archive.media != null && archive.media.Count > 0) { @@ -1452,23 +1452,33 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (!archivedCollection.ArchivedPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, archive.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, archive.id, medium.files.full.url, null, + null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); archivedCollection.ArchivedPosts.Add(medium.id, medium.files.full.url); archivedCollection.ArchivedPostMedia.Add(medium); } @@ -1477,8 +1487,14 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!archivedCollection.ArchivedPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, archive.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - archivedCollection.ArchivedPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{archive.id}"); + await dbService.AddMedia(folder, medium.id, archive.id, medium.files.drm.manifest.dash, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + archivedCollection.ArchivedPosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{archive.id}"); archivedCollection.ArchivedPostMedia.Add(medium); } } @@ -1495,10 +1511,13 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } @@ -1514,12 +1533,10 @@ public class APIService(IAuthService authService, IConfigService configService, int post_limit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, - { "order", "desc" }, - { "skip_users", "all" } + { "limit", post_limit.ToString() }, { "order", "desc" }, { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); messages = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1531,7 +1548,7 @@ public class APIService(IAuthService authService, IConfigService configService, { Messages newmessages = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newmessages = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); messages.list.AddRange(newmessages.list); @@ -1542,6 +1559,7 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } + getParams["id"] = newmessages.list[newmessages.list.Count - 1].id.ToString(); } } @@ -1555,6 +1573,7 @@ public class APIService(IAuthService authService, IConfigService configService, continue; } } + List messagePreviewIds = new(); if (list.previews != null && list.previews.Count > 0) { @@ -1569,35 +1588,52 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - if (!configService.CurrentConfig.IgnoreOwnMessages || list.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + + if (!configService.CurrentConfig.IgnoreOwnMessages || + list.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - await dbService.AddMessage(folder, list.id, list.text != null ? list.text : string.Empty, list.price != null ? list.price.ToString() : "0", list.canPurchaseReason == "opened" ? true : list.canPurchaseReason != "opened" ? false : (bool?)null ?? false, false, list.createdAt.HasValue ? list.createdAt.Value : DateTime.Now, list.fromUser != null && list.fromUser.id != null ? list.fromUser.id.Value : int.MinValue); + await dbService.AddMessage(folder, list.id, list.text != null ? list.text : string.Empty, + list.price != null ? list.price : "0", + list.canPurchaseReason == "opened" ? true : + list.canPurchaseReason != "opened" ? false : (bool?)null ?? false, false, + list.createdAt.HasValue ? list.createdAt.Value : DateTime.Now, + list.fromUser != null && list.fromUser.id != null ? list.fromUser.id.Value : int.MinValue); messageCollection.MessageObjects.Add(list); if (list.canPurchaseReason != "opened" && list.media != null && list.media.Count > 0) { foreach (Messages.Medium medium in list.media) { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, + null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, medium.files.full.url); messageCollection.MessageMedia.Add(medium); } @@ -1608,22 +1644,32 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - messageCollection.Messages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, + null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + messageCollection.Messages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); messageCollection.MessageMedia.Add(medium); } } @@ -1633,53 +1679,74 @@ public class APIService(IAuthService authService, IConfigService configService, { foreach (Messages.Medium medium in list.media) { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url) && messagePreviewIds.Contains(medium.id)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url) && messagePreviewIds.Contains(medium.id)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, + null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, medium.files.full.url); messageCollection.MessageMedia.Add(medium); } } - else if (medium.canView && medium.files != null && medium.files.drm != null && messagePreviewIds.Contains(medium.id)) + else if (medium.canView && medium.files != null && medium.files.drm != null && + messagePreviewIds.Contains(medium.id)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - messageCollection.Messages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, + null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + messageCollection.Messages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); messageCollection.MessageMedia.Add(medium); } } @@ -1697,10 +1764,13 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } @@ -1713,18 +1783,18 @@ public class APIService(IAuthService authService, IConfigService configService, SingleMessage message = new(); SinglePaidMessageCollection singlePaidMessageCollection = new(); int post_limit = 50; - Dictionary getParams = new() - { - { "limit", post_limit.ToString() }, - { "order", "desc" } - }; + Dictionary getParams = new() { { "limit", post_limit.ToString() }, { "order", "desc" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); message = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - if (!configService.CurrentConfig.IgnoreOwnMessages || message.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + if (!configService.CurrentConfig.IgnoreOwnMessages || + message.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - await dbService.AddMessage(folder, message.id, message.text != null ? message.text : string.Empty, message.price != null ? message.price.ToString() : "0", true, false, message.createdAt.HasValue ? message.createdAt.Value : DateTime.Now, message.fromUser != null && message.fromUser.id != null ? message.fromUser.id.Value : int.MinValue); + await dbService.AddMessage(folder, message.id, message.text != null ? message.text : string.Empty, + message.price != null ? message.price.ToString() : "0", true, false, + message.createdAt.HasValue ? message.createdAt.Value : DateTime.Now, + message.fromUser != null && message.fromUser.id != null ? message.fromUser.id.Value : int.MinValue); singlePaidMessageCollection.SingleMessageObjects.Add(message); List messagePreviewIds = new(); if (message.previews != null && message.previews.Count > 0) @@ -1745,20 +1815,24 @@ public class APIService(IAuthService authService, IConfigService configService, { foreach (Messages.Medium medium in message.media) { - if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && + medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; @@ -1766,25 +1840,34 @@ public class APIService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.SingleMessages.Add(medium.id, medium.files.full.url.ToString()); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, + null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + singlePaidMessageCollection.SingleMessages.Add(medium.id, medium.files.full.url); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } } - else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && + medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; @@ -1792,25 +1875,34 @@ public class APIService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, medium.files.full.url.ToString()); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, + null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, medium.files.full.url); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } } - else if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.drm != null) + else if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && + medium.files.drm != null) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; @@ -1818,25 +1910,35 @@ public class APIService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.SingleMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, + null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + singlePaidMessageCollection.SingleMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } } - else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.drm != null) + else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && + medium.files.drm != null) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; @@ -1844,8 +1946,14 @@ public class APIService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, + null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } } @@ -1862,15 +1970,19 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } - public async Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx) + public async Task GetPaidMessages(string endpoint, string folder, string username, + StatusContext ctx) { Log.Debug($"Calling GetPaidMessages - {username}"); @@ -1888,7 +2000,7 @@ public class APIService(IAuthService authService, IConfigService configService, { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); paidMessages = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1903,18 +2015,21 @@ public class APIService(IAuthService authService, IConfigService configService, Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); - HttpRequestMessage looprequest = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); + HttpRequestMessage looprequest = + new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); foreach (KeyValuePair keyValuePair in loopheaders) { looprequest.Headers.Add(keyValuePair.Key, keyValuePair.Value); } - using (var loopresponse = await loopclient.SendAsync(looprequest)) + + using (HttpResponseMessage loopresponse = await loopclient.SendAsync(looprequest)) { loopresponse.EnsureSuccessStatusCode(); - var loopbody = await loopresponse.Content.ReadAsStringAsync(); + string loopbody = await loopresponse.Content.ReadAsStringAsync(); newpaidMessages = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); } + paidMessages.list.AddRange(newpaidMessages.list); ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1923,24 +2038,34 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); } } if (paidMessages.list != null && paidMessages.list.Count > 0) { - foreach (Purchased.List purchase in paidMessages.list.Where(p => p.responseType == "message").OrderByDescending(p => p.postedAt ?? p.createdAt)) + foreach (Purchased.List purchase in paidMessages.list.Where(p => p.responseType == "message") + .OrderByDescending(p => p.postedAt ?? p.createdAt)) { - if (!configService.CurrentConfig.IgnoreOwnMessages || purchase.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + if (!configService.CurrentConfig.IgnoreOwnMessages || + purchase.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { if (purchase.postedAt != null) { - await dbService.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); + await dbService.AddMessage(folder, purchase.id, + purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, + purchase.fromUser.id); } else { - await dbService.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); + await dbService.AddMessage(folder, purchase.id, + purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, + purchase.fromUser.id); } + paidMessageCollection.PaidMessageObjects.Add(purchase); if (purchase.media != null && purchase.media.Count > 0) { @@ -1977,27 +2102,37 @@ public class APIService(IAuthService authService, IConfigService configService, if (previewids.Count > 0) { bool has = previewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!has && medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, medium.files.full.url); paidMessageCollection.PaidMessageMedia.Add(medium); } @@ -2008,49 +2143,69 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - paidMessageCollection.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + paidMessageCollection.PaidMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidMessageCollection.PaidMessageMedia.Add(medium); } } } else { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, medium.files.full.url); paidMessageCollection.PaidMessageMedia.Add(medium); } @@ -2061,22 +2216,32 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - paidMessageCollection.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + paidMessageCollection.PaidMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidMessageCollection.PaidMessageMedia.Add(medium); } } @@ -2096,10 +2261,13 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } @@ -2120,7 +2288,7 @@ public class APIService(IAuthService authService, IConfigService configService, { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); purchased = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); if (purchased != null && purchased.hasMore) { @@ -2132,23 +2300,27 @@ public class APIService(IAuthService authService, IConfigService configService, Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); - HttpRequestMessage looprequest = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); + HttpRequestMessage looprequest = + new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); foreach (KeyValuePair keyValuePair in loopheaders) { looprequest.Headers.Add(keyValuePair.Key, keyValuePair.Value); } - using (var loopresponse = await loopclient.SendAsync(looprequest)) + + using (HttpResponseMessage loopresponse = await loopclient.SendAsync(looprequest)) { loopresponse.EnsureSuccessStatusCode(); - var loopbody = await loopresponse.Content.ReadAsStringAsync(); + string loopbody = await loopresponse.Content.ReadAsStringAsync(); newPurchased = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); } + purchased.list.AddRange(newPurchased.list); if (!newPurchased.hasMore) { break; } + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); } } @@ -2163,16 +2335,20 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!string.IsNullOrEmpty(users.FirstOrDefault(x => x.Value == purchase.fromUser.id).Key)) { - if (!purchasedTabUsers.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.fromUser.id).Key)) + if (!purchasedTabUsers.ContainsKey(users + .FirstOrDefault(x => x.Value == purchase.fromUser.id).Key)) { - purchasedTabUsers.Add(users.FirstOrDefault(x => x.Value == purchase.fromUser.id).Key, purchase.fromUser.id); + purchasedTabUsers.Add( + users.FirstOrDefault(x => x.Value == purchase.fromUser.id).Key, + purchase.fromUser.id); } } else { if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", purchase.fromUser.id); + purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", + purchase.fromUser.id); } } } @@ -2180,29 +2356,35 @@ public class APIService(IAuthService authService, IConfigService configService, { JObject user = await GetUserInfoById($"/users/list?x[]={purchase.fromUser.id}"); - if(user is null) + if (user is null) { if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) { - if(!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) + if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", purchase.fromUser.id); + purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", + purchase.fromUser.id); } } + Log.Debug("Content creator not longer exists - {0}", purchase.fromUser.id); } - else if (!string.IsNullOrEmpty(user[purchase.fromUser.id.ToString()]["username"].ToString())) + else if (!string.IsNullOrEmpty(user[purchase.fromUser.id.ToString()]["username"] + .ToString())) { - if (!purchasedTabUsers.ContainsKey(user[purchase.fromUser.id.ToString()]["username"].ToString())) + if (!purchasedTabUsers.ContainsKey(user[purchase.fromUser.id.ToString()]["username"] + .ToString())) { - purchasedTabUsers.Add(user[purchase.fromUser.id.ToString()]["username"].ToString(), purchase.fromUser.id); + purchasedTabUsers.Add(user[purchase.fromUser.id.ToString()]["username"].ToString(), + purchase.fromUser.id); } } else { if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", purchase.fromUser.id); + purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", + purchase.fromUser.id); } } } @@ -2213,9 +2395,12 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!string.IsNullOrEmpty(users.FirstOrDefault(x => x.Value == purchase.author.id).Key)) { - if (!purchasedTabUsers.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.author.id).Key) && users.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.author.id).Key)) + if (!purchasedTabUsers.ContainsKey(users + .FirstOrDefault(x => x.Value == purchase.author.id).Key) && + users.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.author.id).Key)) { - purchasedTabUsers.Add(users.FirstOrDefault(x => x.Value == purchase.author.id).Key, purchase.author.id); + purchasedTabUsers.Add(users.FirstOrDefault(x => x.Value == purchase.author.id).Key, + purchase.author.id); } } else @@ -2234,18 +2419,23 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) { - if(!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.author.id}")) + if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.author.id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.author.id}", purchase.author.id); + purchasedTabUsers.Add($"Deleted User - {purchase.author.id}", + purchase.author.id); } } + Log.Debug("Content creator not longer exists - {0}", purchase.author.id); } else if (!string.IsNullOrEmpty(user[purchase.author.id.ToString()]["username"].ToString())) { - if (!purchasedTabUsers.ContainsKey(user[purchase.author.id.ToString()]["username"].ToString()) && users.ContainsKey(user[purchase.author.id.ToString()]["username"].ToString())) + if (!purchasedTabUsers.ContainsKey(user[purchase.author.id.ToString()]["username"] + .ToString()) && + users.ContainsKey(user[purchase.author.id.ToString()]["username"].ToString())) { - purchasedTabUsers.Add(user[purchase.author.id.ToString()]["username"].ToString(), purchase.author.id); + purchasedTabUsers.Add(user[purchase.author.id.ToString()]["username"].ToString(), + purchase.author.id); } } else @@ -2269,20 +2459,24 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } - public async Task> GetPurchasedTab(string endpoint, string folder, Dictionary users) + public async Task> GetPurchasedTab(string endpoint, string folder, + Dictionary users) { Log.Debug($"Calling GetPurchasedTab - {endpoint}"); try { - Dictionary> userPurchases = new Dictionary>(); + Dictionary> userPurchases = new(); List purchasedTabCollections = new(); Purchased purchased = new(); int post_limit = 50; @@ -2294,7 +2488,7 @@ public class APIService(IAuthService authService, IConfigService configService, { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); purchased = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); if (purchased != null && purchased.hasMore) { @@ -2306,23 +2500,27 @@ public class APIService(IAuthService authService, IConfigService configService, Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); - HttpRequestMessage looprequest = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); + HttpRequestMessage looprequest = + new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); foreach (KeyValuePair keyValuePair in loopheaders) { looprequest.Headers.Add(keyValuePair.Key, keyValuePair.Value); } - using (var loopresponse = await loopclient.SendAsync(looprequest)) + + using (HttpResponseMessage loopresponse = await loopclient.SendAsync(looprequest)) { loopresponse.EnsureSuccessStatusCode(); - var loopbody = await loopresponse.Content.ReadAsStringAsync(); + string loopbody = await loopresponse.Content.ReadAsStringAsync(); newPurchased = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); } + purchased.list.AddRange(newPurchased.list); if (!newPurchased.hasMore) { break; } + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); } } @@ -2337,6 +2535,7 @@ public class APIService(IAuthService authService, IConfigService configService, { userPurchases.Add(purchase.fromUser.id, new List()); } + userPurchases[purchase.fromUser.id].Add(purchase); } else if (purchase.author != null) @@ -2345,6 +2544,7 @@ public class APIService(IAuthService authService, IConfigService configService, { userPurchases.Add(purchase.author.id, new List()); } + userPurchases[purchase.author.id].Add(purchase); } } @@ -2352,20 +2552,28 @@ public class APIService(IAuthService authService, IConfigService configService, foreach (KeyValuePair> user in userPurchases) { - PurchasedTabCollection purchasedTabCollection = new PurchasedTabCollection(); + PurchasedTabCollection purchasedTabCollection = new(); JObject userObject = await GetUserInfoById($"/users/list?x[]={user.Key}"); purchasedTabCollection.UserId = user.Key; - purchasedTabCollection.Username = userObject is not null && !string.IsNullOrEmpty(userObject[user.Key.ToString()]["username"].ToString()) ? userObject[user.Key.ToString()]["username"].ToString() : $"Deleted User - {user.Key}"; - string path = System.IO.Path.Combine(folder, purchasedTabCollection.Username); + purchasedTabCollection.Username = + userObject is not null && + !string.IsNullOrEmpty(userObject[user.Key.ToString()]["username"].ToString()) + ? userObject[user.Key.ToString()]["username"].ToString() + : $"Deleted User - {user.Key}"; + string path = Path.Combine(folder, purchasedTabCollection.Username); if (Path.Exists(path)) { foreach (Purchased.List purchase in user.Value) { if (purchase.media == null) { - Log.Warning("PurchasedTab purchase media null, setting empty list | userId={UserId} username={Username} purchaseId={PurchaseId} responseType={ResponseType} createdAt={CreatedAt} postedAt={PostedAt}", user.Key, purchasedTabCollection.Username, purchase.id, purchase.responseType, purchase.createdAt, purchase.postedAt); + Log.Warning( + "PurchasedTab purchase media null, setting empty list | userId={UserId} username={Username} purchaseId={PurchaseId} responseType={ResponseType} createdAt={CreatedAt} postedAt={PostedAt}", + user.Key, purchasedTabCollection.Username, purchase.id, purchase.responseType, + purchase.createdAt, purchase.postedAt); purchase.media = new List(); } + switch (purchase.responseType) { case "post": @@ -2396,7 +2604,13 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); + + await dbService.AddPost(path, purchase.id, + purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", + purchase.price != null && purchase.isOpened ? true : false, + purchase.isArchived.HasValue ? purchase.isArchived.Value : false, + purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); purchasedTabCollection.PaidPosts.PaidPostObjects.Add(purchase); foreach (Messages.Medium medium in purchase.media) { @@ -2404,51 +2618,73 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (previewids.Count > 0) { bool has = previewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!has && medium.canView && medium.files != null && + medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { - if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, medium.files.full.url); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, + medium.files.full.url); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } - else if (!has && medium.canView && medium.files != null && medium.files.drm != null) + else if (!has && medium.canView && medium.files != null && + medium.files.drm != null) { - if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } - } } else { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, medium.files.full.url); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, + medium.files.full.url); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } @@ -2456,23 +2692,37 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } } } + break; case "message": if (purchase.postedAt != null) { - await dbService.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); + await dbService.AddMessage(path, purchase.id, + purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", true, false, + purchase.postedAt.Value, purchase.fromUser.id); } else { - await dbService.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); + await dbService.AddMessage(path, purchase.id, + purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", true, false, + purchase.createdAt.Value, purchase.fromUser.id); } + purchasedTabCollection.PaidMessages.PaidMessageObjects.Add(purchase); if (purchase.media != null && purchase.media.Count > 0) { @@ -2509,118 +2759,185 @@ public class APIService(IAuthService authService, IConfigService configService, if (paidMessagePreviewids.Count > 0) { bool has = paidMessagePreviewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!has && medium.canView && medium.files != null && + medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.type == "photo" && + !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + + if (medium.type == "video" && + !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + + if (medium.type == "audio" && + !configService.CurrentConfig.DownloadAudios) { continue; } - if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) + + if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( + medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, medium.files.full.url); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.id) ? true : false, false, + null); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, + medium.files.full.url); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } } - else if (!has && medium.canView && medium.files != null && medium.files.drm != null) + else if (!has && medium.canView && medium.files != null && + medium.files.drm != null) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.type == "photo" && + !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + + if (medium.type == "video" && + !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + + if (medium.type == "audio" && + !configService.CurrentConfig.DownloadAudios) { continue; } - if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) + + if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( + medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.id) ? true : false, false, + null); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } } } else { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.type == "photo" && + !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + + if (medium.type == "video" && + !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + + if (medium.type == "audio" && + !configService.CurrentConfig.DownloadAudios) { continue; } - if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) + + if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( + medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, medium.files.full.url); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.id) ? true : false, false, + null); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, + medium.files.full.url); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } } else if (medium.canView && medium.files != null && medium.files.drm != null) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.type == "photo" && + !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + + if (medium.type == "video" && + !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + + if (medium.type == "audio" && + !configService.CurrentConfig.DownloadAudios) { continue; } - if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) + + if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( + medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.id) ? true : false, false, + null); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } } } } } + break; } } + purchasedTabCollections.Add(purchasedTabCollection); } } + return purchasedTabCollections; } catch (Exception ex) @@ -2630,10 +2947,13 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } @@ -2648,15 +2968,16 @@ public class APIService(IAuthService authService, IConfigService configService, HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); request.Headers.Add("user-agent", authService.CurrentAuth.USER_AGENT); request.Headers.Add("Accept", "*/*"); - request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); - using (var response = await client.SendAsync(request)) + request.Headers.Add("Cookie", + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); + using (HttpResponseMessage response = await client.SendAsync(request)) { response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); + string body = await response.Content.ReadAsStringAsync(); XNamespace ns = "urn:mpeg:dash:schema:mpd:2011"; XNamespace cenc = "urn:mpeg:cenc:2013"; XDocument xmlDoc = XDocument.Parse(body); - var psshElements = xmlDoc.Descendants(cenc + "pssh"); + IEnumerable psshElements = xmlDoc.Descendants(cenc + "pssh"); pssh = psshElements.ElementAt(1).Value; } @@ -2669,10 +2990,13 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } @@ -2693,14 +3017,17 @@ public class APIService(IAuthService authService, IConfigService configService, HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); request.Headers.Add("user-agent", authService.CurrentAuth.USER_AGENT); request.Headers.Add("Accept", "*/*"); - request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); - using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) + request.Headers.Add("Cookie", + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); + using (HttpResponseMessage response = + await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); lastmodified = response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now; Log.Debug($"Last modified: {lastmodified}"); } + return lastmodified; } catch (Exception ex) @@ -2710,14 +3037,18 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return DateTime.Now; } - public async Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, string pssh) + public async Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, + string pssh) { Log.Debug("Calling GetDecryptionKey"); @@ -2728,7 +3059,7 @@ public class APIService(IAuthService authService, IConfigService configService, string dcValue = string.Empty; HttpClient client = new(); - CDRMProjectRequest cdrmProjectRequest = new CDRMProjectRequest + CDRMProjectRequest cdrmProjectRequest = new() { PSSH = pssh, LicenseURL = licenceURL, @@ -2750,13 +3081,13 @@ public class APIService(IAuthService authService, IConfigService configService, Content = new StringContent(json, Encoding.UTF8, "application/json") }; - using var response = await client.SendAsync(request); + using HttpResponseMessage response = await client.SendAsync(request); Log.Debug($"CDRM Project Response (Attempt {attempt}): {response.Content.ReadAsStringAsync().Result}"); response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); - var doc = JsonDocument.Parse(body); + string body = await response.Content.ReadAsStringAsync(); + JsonDocument doc = JsonDocument.Parse(body); if (doc.RootElement.TryGetProperty("status", out JsonElement status)) { @@ -2765,13 +3096,11 @@ public class APIService(IAuthService authService, IConfigService configService, dcValue = doc.RootElement.GetProperty("message").GetString().Trim(); return dcValue; } - else + + Log.Debug($"CDRM response status not successful. Retrying... Attempt {attempt} of {MaxAttempts}"); + if (attempt < MaxAttempts) { - Log.Debug($"CDRM response status not successful. Retrying... Attempt {attempt} of {MaxAttempts}"); - if (attempt < MaxAttempts) - { - await Task.Delay(DelayBetweenAttempts); - } + await Task.Delay(DelayBetweenAttempts); } } else @@ -2793,14 +3122,18 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } - public async Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, string pssh) + public async Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, + string pssh) { Log.Debug("Calling GetDecryptionOFDL"); @@ -2809,11 +3142,9 @@ public class APIService(IAuthService authService, IConfigService configService, HttpClient client = new(); int attempt = 0; - OFDLRequest ofdlRequest = new OFDLRequest + OFDLRequest ofdlRequest = new() { - PSSH = pssh, - LicenseURL = licenceURL, - Headers = JsonConvert.SerializeObject(drmHeaders) + PSSH = pssh, LicenseURL = licenceURL, Headers = JsonConvert.SerializeObject(drmHeaders) }; string json = JsonConvert.SerializeObject(ofdlRequest); @@ -2829,15 +3160,19 @@ public class APIService(IAuthService authService, IConfigService configService, Content = new StringContent(json, Encoding.UTF8, "application/json") }; - using var response = await client.SendAsync(request); + using HttpResponseMessage response = await client.SendAsync(request); if (!response.IsSuccessStatusCode) + { continue; + } string body = await response.Content.ReadAsStringAsync(); if (!body.TrimStart().StartsWith('{')) + { return body; + } Log.Debug($"Received JSON object instead of string. Retrying... Attempt {attempt} of {MaxAttempts}"); await Task.Delay(DelayBetweenAttempts); @@ -2850,8 +3185,10 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } @@ -2864,12 +3201,12 @@ public class APIService(IAuthService authService, IConfigService configService, try { - var resp1 = await PostData(licenceURL, drmHeaders, new byte[] { 0x08, 0x04 }); - var certDataB64 = Convert.ToBase64String(resp1); - var cdm = new CDMApi(); - var challenge = cdm.GetChallenge(pssh, certDataB64, false, false); - var resp2 = await PostData(licenceURL, drmHeaders, challenge); - var licenseB64 = Convert.ToBase64String(resp2); + byte[] resp1 = await PostData(licenceURL, drmHeaders, new byte[] { 0x08, 0x04 }); + string certDataB64 = Convert.ToBase64String(resp1); + CDMApi cdm = new(); + byte[] challenge = cdm.GetChallenge(pssh, certDataB64); + byte[] resp2 = await PostData(licenceURL, drmHeaders, challenge); + string licenseB64 = Convert.ToBase64String(resp2); Log.Debug($"resp1: {resp1}"); Log.Debug($"certDataB64: {certDataB64}"); Log.Debug($"challenge: {challenge}"); @@ -2879,7 +3216,7 @@ public class APIService(IAuthService authService, IConfigService configService, List keys = cdm.GetKeys(); if (keys.Count > 0) { - Log.Debug($"GetDecryptionKeyCDM Key: {keys[0].ToString()}"); + Log.Debug($"GetDecryptionKeyCDM Key: {keys[0]}"); return keys[0].ToString(); } } @@ -2890,10 +3227,186 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + + return null; + } + + + private async Task BuildHeaderAndExecuteRequests(Dictionary getParams, string endpoint, + HttpClient client) + { + Log.Debug("Calling BuildHeaderAndExecuteRequests"); + + HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint); + using HttpResponseMessage response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + string body = await response.Content.ReadAsStringAsync(); + + Log.Debug(body); + + return body; + } + + + private async Task BuildHttpRequestMessage(Dictionary getParams, + string endpoint) + { + Log.Debug("Calling BuildHttpRequestMessage"); + + string queryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); + + Dictionary headers = GetDynamicHeaders($"/api2/v2{endpoint}", queryParams); + + HttpRequestMessage request = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{queryParams}"); + + Log.Debug($"Full request URL: {Constants.API_URL}{endpoint}{queryParams}"); + + foreach (KeyValuePair keyValuePair in headers) + { + request.Headers.Add(keyValuePair.Key, keyValuePair.Value); + } + + return request; + } + + private static double ConvertToUnixTimestampWithMicrosecondPrecision(DateTime date) + { + DateTime origin = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + TimeSpan diff = date.ToUniversalTime() - origin; + + return + diff.TotalSeconds; // This gives the number of seconds. If you need milliseconds, use diff.TotalMilliseconds + } + + public static bool IsStringOnlyDigits(string input) => input.All(char.IsDigit); + + + private HttpClient GetHttpClient() + { + HttpClient client = new(); + if (configService.CurrentConfig?.Timeout != null && configService.CurrentConfig.Timeout > 0) + { + client.Timeout = TimeSpan.FromSeconds(configService.CurrentConfig.Timeout.Value); + } + + return client; + } + + + /// + /// this one is used during initialization only + /// if the config option is not available then no modificatiotns will be done on the getParams + /// + /// + /// + /// + private static void UpdateGetParamsForDateSelection(DownloadDateSelection downloadDateSelection, + ref Dictionary getParams, DateTime? dt) + { + //if (config.DownloadOnlySpecificDates && dt.HasValue) + //{ + if (dt.HasValue) + { + UpdateGetParamsForDateSelection( + downloadDateSelection, + ref getParams, + ConvertToUnixTimestampWithMicrosecondPrecision(dt.Value) + .ToString("0.000000", CultureInfo.InvariantCulture) + ); + } + //} + } + + private static void UpdateGetParamsForDateSelection(DownloadDateSelection downloadDateSelection, + ref Dictionary getParams, string unixTimeStampInMicrosec) + { + switch (downloadDateSelection) + { + case DownloadDateSelection.before: + getParams["beforePublishTime"] = unixTimeStampInMicrosec; + break; + case DownloadDateSelection.after: + getParams["order"] = "publish_date_asc"; + getParams["afterPublishTime"] = unixTimeStampInMicrosec; + break; + } + } + + + public async Task?> GetAllSubscriptions(Dictionary getParams, + string endpoint, bool includeRestricted) + { + try + { + Dictionary users = new(); + Subscriptions subscriptions = new(); + + Log.Debug("Calling GetAllSubscrptions"); + + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + + subscriptions = JsonConvert.DeserializeObject(body); + if (subscriptions != null && subscriptions.hasMore) + { + getParams["offset"] = subscriptions.list.Count.ToString(); + + while (true) + { + Subscriptions newSubscriptions = new(); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + + if (!string.IsNullOrEmpty(loopbody) && (!loopbody.Contains("[]") || loopbody.Trim() != "[]")) + { + newSubscriptions = + JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + } + else + { + break; + } + + subscriptions.list.AddRange(newSubscriptions.list); + if (!newSubscriptions.hasMore) + { + break; + } + + getParams["offset"] = subscriptions.list.Count.ToString(); + } + } + + foreach (Subscriptions.List subscription in subscriptions.list) + { + if ((!(subscription.isRestricted ?? false) || + ((subscription.isRestricted ?? false) && includeRestricted)) + && !users.ContainsKey(subscription.username)) + { + users.Add(subscription.username, subscription.id); + } + } + + return users; + } + 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); + } + } + return null; } @@ -2902,9 +3415,10 @@ public class APIService(IAuthService authService, IConfigService configService, Log.Debug("Calling GetDynamicRules"); try { - HttpClient client = new HttpClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://git.ofdl.tools/sim0n00ps/dynamic-rules/raw/branch/main/rules.json"); - using var response = client.Send(request); + HttpClient client = new(); + HttpRequestMessage request = new(HttpMethod.Get, + "https://git.ofdl.tools/sim0n00ps/dynamic-rules/raw/branch/main/rules.json"); + using HttpResponseMessage response = client.Send(request); if (!response.IsSuccessStatusCode) { @@ -2912,7 +3426,7 @@ public class APIService(IAuthService authService, IConfigService configService, return null; } - var body = response.Content.ReadAsStringAsync().Result; + string body = response.Content.ReadAsStringAsync().Result; Log.Debug("GetDynamicRules Response: "); Log.Debug(body); @@ -2926,10 +3440,13 @@ public class APIService(IAuthService authService, IConfigService configService, 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); + 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); } } + return null; } } diff --git a/OF DL/Services/AuthService.cs b/OF DL/Services/AuthService.cs index c705b2f..17dfd81 100644 --- a/OF DL/Services/AuthService.cs +++ b/OF DL/Services/AuthService.cs @@ -4,227 +4,220 @@ using PuppeteerSharp; using PuppeteerSharp.BrowserData; using Serilog; -namespace OF_DL.Services +namespace OF_DL.Services; + +public class AuthService : IAuthService { - public class AuthService : IAuthService + private const int LoginTimeout = 600000; // 10 minutes + private const int FeedLoadTimeout = 60000; // 1 minute + + private readonly string[] _desiredCookies = + [ + "auth_id", + "sess" + ]; + + private readonly LaunchOptions _options = new() { - private readonly LaunchOptions _options = new() + Headless = false, + Channel = ChromeReleaseChannel.Stable, + DefaultViewport = null, + Args = ["--no-sandbox", "--disable-setuid-sandbox"], + UserDataDir = Path.GetFullPath("chrome-data") + }; + + public Auth? CurrentAuth { get; set; } + + public async Task LoadFromFileAsync(string filePath = "auth.json") + { + try { - Headless = false, - Channel = ChromeReleaseChannel.Stable, - DefaultViewport = null, - Args = ["--no-sandbox", "--disable-setuid-sandbox"], - UserDataDir = Path.GetFullPath("chrome-data") - }; - - private readonly string[] _desiredCookies = - [ - "auth_id", - "sess" - ]; - - private const int LoginTimeout = 600000; // 10 minutes - private const int FeedLoadTimeout = 60000; // 1 minute - - public Auth? CurrentAuth { get; set; } - - public async Task LoadFromFileAsync(string filePath = "auth.json") - { - try + if (!File.Exists(filePath)) { - if (!File.Exists(filePath)) - { - Log.Debug("Auth file not found: {FilePath}", filePath); - return false; - } - - var json = await File.ReadAllTextAsync(filePath); - CurrentAuth = JsonConvert.DeserializeObject(json); - Log.Debug("Auth file loaded and deserialized successfully"); - return CurrentAuth != null; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to load auth from file"); + Log.Debug("Auth file not found: {FilePath}", filePath); return false; } + + string json = await File.ReadAllTextAsync(filePath); + CurrentAuth = JsonConvert.DeserializeObject(json); + Log.Debug("Auth file loaded and deserialized successfully"); + return CurrentAuth != null; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load auth from file"); + return false; + } + } + + public async Task LoadFromBrowserAsync() + { + try + { + bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; + + await SetupBrowser(runningInDocker); + CurrentAuth = await GetAuthFromBrowser(); + + return CurrentAuth != null; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load auth from browser"); + return false; + } + } + + public async Task SaveToFileAsync(string filePath = "auth.json") + { + if (CurrentAuth == null) + { + Log.Warning("Attempted to save null auth to file"); + return; } - public async Task LoadFromBrowserAsync() + try { - try - { - bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; - - await SetupBrowser(runningInDocker); - CurrentAuth = await GetAuthFromBrowser(); - - return CurrentAuth != null; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to load auth from browser"); - return false; - } + string json = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented); + await File.WriteAllTextAsync(filePath, json); + Log.Debug($"Auth saved to file: {filePath}"); } - - public async Task SaveToFileAsync(string filePath = "auth.json") + catch (Exception ex) { - if (CurrentAuth == null) - { - Log.Warning("Attempted to save null auth to file"); - return; - } - - try - { - var json = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented); - await File.WriteAllTextAsync(filePath, json); - Log.Debug($"Auth saved to file: {filePath}"); - } - catch (Exception ex) - { - Log.Error(ex, "Failed to save auth to file"); - } + Log.Error(ex, "Failed to save auth to file"); } + } - private async Task SetupBrowser(bool runningInDocker) + private async Task SetupBrowser(bool runningInDocker) + { + string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH"); + if (executablePath != null) { - string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH"); - if (executablePath != null) + Log.Information( + "OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}", + executablePath); + _options.ExecutablePath = executablePath; + } + else + { + BrowserFetcher browserFetcher = new(); + List installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList(); + if (installedBrowsers.Count == 0) { - Log.Information("OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}", executablePath); - _options.ExecutablePath = executablePath; + Log.Information("Downloading browser."); + InstalledBrowser? downloadedBrowser = await browserFetcher.DownloadAsync(); + Log.Information("Browser downloaded. Path: {executablePath}", + downloadedBrowser.GetExecutablePath()); + _options.ExecutablePath = downloadedBrowser.GetExecutablePath(); } else { - var browserFetcher = new BrowserFetcher(); - var installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList(); - if (installedBrowsers.Count == 0) + _options.ExecutablePath = installedBrowsers.First().GetExecutablePath(); + } + } + + if (runningInDocker) + { + Log.Information("Running in Docker. Disabling sandbox and GPU."); + _options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]; + } + } + + private async Task GetBcToken(IPage page) => + await page.EvaluateExpressionAsync("window.localStorage.getItem('bcTokenSha') || ''"); + + private async Task GetAuthFromBrowser(bool isDocker = false) + { + try + { + IBrowser? browser; + try + { + browser = await Puppeteer.LaunchAsync(_options); + } + catch (ProcessException e) + { + if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir)) { - Log.Information("Downloading browser."); - var downloadedBrowser = await browserFetcher.DownloadAsync(); - Log.Information("Browser downloaded. Path: {executablePath}", - downloadedBrowser.GetExecutablePath()); - _options.ExecutablePath = downloadedBrowser.GetExecutablePath(); + Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again."); + Directory.Delete(_options.UserDataDir, true); + browser = await Puppeteer.LaunchAsync(_options); } else { - _options.ExecutablePath = installedBrowsers.First().GetExecutablePath(); + throw; } } - if (runningInDocker) + if (browser == null) { - Log.Information("Running in Docker. Disabling sandbox and GPU."); - _options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]; + throw new Exception("Could not get browser"); } - } - private async Task GetBcToken(IPage page) - { - return await page.EvaluateExpressionAsync("window.localStorage.getItem('bcTokenSha') || ''"); - } + IPage[]? pages = await browser.PagesAsync(); + IPage? page = pages.First(); - private async Task GetAuthFromBrowser(bool isDocker = false) - { + if (page == null) + { + throw new Exception("Could not get page"); + } + + Log.Debug("Navigating to OnlyFans."); + await page.GoToAsync("https://onlyfans.com"); + + Log.Debug("Waiting for user to login"); + await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout }); + Log.Debug("Feed element detected (user logged in)"); + + await page.ReloadAsync(); + + await page.WaitForNavigationAsync(new NavigationOptions + { + WaitUntil = [WaitUntilNavigation.Networkidle2], Timeout = FeedLoadTimeout + }); + Log.Debug("DOM loaded. Getting BC token and cookies ..."); + + string xBc; try { - IBrowser? browser; - try - { - browser = await Puppeteer.LaunchAsync(_options); - } - catch (ProcessException e) - { - if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir)) - { - Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again."); - Directory.Delete(_options.UserDataDir, true); - browser = await Puppeteer.LaunchAsync(_options); - } - else - { - throw; - } - } - - if (browser == null) - { - throw new Exception("Could not get browser"); - } - - IPage[]? pages = await browser.PagesAsync(); - IPage? page = pages.First(); - - if (page == null) - { - throw new Exception("Could not get page"); - } - - Log.Debug("Navigating to OnlyFans."); - await page.GoToAsync("https://onlyfans.com"); - - Log.Debug("Waiting for user to login"); - await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout }); - Log.Debug("Feed element detected (user logged in)"); - - await page.ReloadAsync(); - - await page.WaitForNavigationAsync(new NavigationOptions { - WaitUntil = [WaitUntilNavigation.Networkidle2], - Timeout = FeedLoadTimeout - }); - Log.Debug("DOM loaded. Getting BC token and cookies ..."); - - string xBc; - try - { - xBc = await GetBcToken(page); - } - catch (Exception e) - { - throw new Exception("Error getting bcToken"); - } - - Dictionary mappedCookies = (await page.GetCookiesAsync()) - .Where(cookie => cookie.Domain.Contains("onlyfans.com")) - .ToDictionary(cookie => cookie.Name, cookie => cookie.Value); - - mappedCookies.TryGetValue("auth_id", out string? userId); - if (userId == null) - { - throw new Exception("Could not find 'auth_id' cookie"); - } - - mappedCookies.TryGetValue("sess", out string? sess); - if (sess == null) - { - throw new Exception("Could not find 'sess' cookie"); - } - - string? userAgent = await browser.GetUserAgentAsync(); - if (userAgent == null) - { - throw new Exception("Could not get user agent"); - } - - string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key)) - .Select(key => $"${key}={mappedCookies[key]};")); - - return new Auth() - { - COOKIE = cookies, - USER_AGENT = userAgent, - USER_ID = userId, - X_BC = xBc - }; + xBc = await GetBcToken(page); } catch (Exception e) { - Log.Error(e, "Error getting auth from browser"); - return null; + throw new Exception("Error getting bcToken"); } + + Dictionary mappedCookies = (await page.GetCookiesAsync()) + .Where(cookie => cookie.Domain.Contains("onlyfans.com")) + .ToDictionary(cookie => cookie.Name, cookie => cookie.Value); + + mappedCookies.TryGetValue("auth_id", out string? userId); + if (userId == null) + { + throw new Exception("Could not find 'auth_id' cookie"); + } + + mappedCookies.TryGetValue("sess", out string? sess); + if (sess == null) + { + throw new Exception("Could not find 'sess' cookie"); + } + + string? userAgent = await browser.GetUserAgentAsync(); + if (userAgent == null) + { + throw new Exception("Could not get user agent"); + } + + string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key)) + .Select(key => $"${key}={mappedCookies[key]};")); + + return new Auth { COOKIE = cookies, USER_AGENT = userAgent, USER_ID = userId, X_BC = xBc }; + } + catch (Exception e) + { + Log.Error(e, "Error getting auth from browser"); + return null; } } } diff --git a/OF DL/Services/ConfigService.cs b/OF DL/Services/ConfigService.cs index b2c6a28..0b175da 100644 --- a/OF DL/Services/ConfigService.cs +++ b/OF DL/Services/ConfigService.cs @@ -1,223 +1,241 @@ +using System.Text; using Akka.Configuration; +using Akka.Configuration.Hocon; using Newtonsoft.Json; using OF_DL.Entities; -using Serilog; -using System.Text; using OF_DL.Enumerations; +using OF_DL.Utils; +using Serilog; using Config = OF_DL.Entities.Config; -namespace OF_DL.Services +namespace OF_DL.Services; + +public class ConfigService(ILoggingService loggingService) : IConfigService { - public class ConfigService(ILoggingService loggingService) : IConfigService + public Config CurrentConfig { get; private set; } = new(); + public bool IsCliNonInteractive { get; private set; } + + public async Task LoadConfigurationAsync(string[] args) { - public Config CurrentConfig { get; private set; } = new(); - public bool IsCliNonInteractive { get; private set; } - - public async Task LoadConfigurationAsync(string[] args) + try { - try + IsCliNonInteractive = false; + + // Migrate from config.json to config.conf if needed + await MigrateFromJsonToConfAsync(); + + // Load config.conf or create default + if (File.Exists("config.conf")) { - IsCliNonInteractive = false; - - // Migrate from config.json to config.conf if needed - await MigrateFromJsonToConfAsync(); - - // Load config.conf or create default - if (File.Exists("config.conf")) + Log.Debug("config.conf located successfully"); + if (!await LoadConfigFromFileAsync("config.conf")) { - Log.Debug("config.conf located successfully"); - if (!await LoadConfigFromFileAsync("config.conf")) - { - return false; - } - } - else - { - Log.Debug("config.conf not found, creating default"); - await CreateDefaultConfigFileAsync(); - if (!await LoadConfigFromFileAsync("config.conf")) - { - return false; - } - } - - // Check for command-line arguments - if (args != null && args.Length > 0) - { - const string NON_INTERACTIVE_ARG = "--non-interactive"; - if (args.Any(a => a.Equals(NON_INTERACTIVE_ARG, StringComparison.OrdinalIgnoreCase))) - { - IsCliNonInteractive = true; - Log.Debug("NonInteractiveMode set via command line"); - } - - Log.Debug("Additional arguments:"); - foreach (string argument in args) - { - Log.Debug(argument); - } - } - - return true; - } - catch (Exception ex) - { - Log.Error(ex, "Configuration loading failed"); - return false; - } - } - - public async Task SaveConfigurationAsync(string filePath = "config.conf") - { - if (CurrentConfig == null) - { - Log.Warning("Attempted to save null config to file"); - return; - } - - try - { - var hoconConfig = BuildHoconFromConfig(CurrentConfig); - await File.WriteAllTextAsync(filePath, hoconConfig); - Log.Debug($"Config saved to file: {filePath}"); - } - catch (Exception ex) - { - Log.Error(ex, "Failed to save config to file"); - } - } - - public void UpdateConfig(Config newConfig) - { - CurrentConfig = newConfig; - - // Update logging level - loggingService.UpdateLoggingLevel(newConfig.LoggingLevel); - - // Apply text sanitization preference globally - OF_DL.Utils.XmlUtils.Passthrough = newConfig.DisableTextSanitization; - - Log.Debug("Configuration updated"); - string configString = JsonConvert.SerializeObject(newConfig, Formatting.Indented); - Log.Debug(configString); - } - - private async Task MigrateFromJsonToConfAsync() - { - if (!File.Exists("config.json")) - return; - - try - { - Log.Debug("config.json found, migrating to config.conf"); - string jsonText = await File.ReadAllTextAsync("config.json"); - var jsonConfig = JsonConvert.DeserializeObject(jsonText); - - if (jsonConfig != null) - { - var hoconConfig = BuildHoconFromConfig(jsonConfig); - await File.WriteAllTextAsync("config.conf", hoconConfig); - File.Delete("config.json"); - Log.Information("config.conf created successfully from config.json"); + return false; } } - catch (Exception ex) + else { - Log.Error(ex, "Failed to migrate config.json to config.conf"); - throw; + Log.Debug("config.conf not found, creating default"); + await CreateDefaultConfigFileAsync(); + if (!await LoadConfigFromFileAsync("config.conf")) + { + return false; + } } + + // Check for command-line arguments + if (args != null && args.Length > 0) + { + const string NON_INTERACTIVE_ARG = "--non-interactive"; + if (args.Any(a => a.Equals(NON_INTERACTIVE_ARG, StringComparison.OrdinalIgnoreCase))) + { + IsCliNonInteractive = true; + Log.Debug("NonInteractiveMode set via command line"); + } + + Log.Debug("Additional arguments:"); + foreach (string argument in args) + { + Log.Debug(argument); + } + } + + return true; + } + catch (Exception ex) + { + Log.Error(ex, "Configuration loading failed"); + return false; + } + } + + public async Task SaveConfigurationAsync(string filePath = "config.conf") + { + if (CurrentConfig == null) + { + Log.Warning("Attempted to save null config to file"); + return; } - private async Task LoadConfigFromFileAsync(string filePath) + try { - try + string hoconConfig = BuildHoconFromConfig(CurrentConfig); + await File.WriteAllTextAsync(filePath, hoconConfig); + Log.Debug($"Config saved to file: {filePath}"); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to save config to file"); + } + } + + public void UpdateConfig(Config newConfig) + { + CurrentConfig = newConfig; + + // Update logging level + loggingService.UpdateLoggingLevel(newConfig.LoggingLevel); + + // Apply text sanitization preference globally + XmlUtils.Passthrough = newConfig.DisableTextSanitization; + + Log.Debug("Configuration updated"); + string configString = JsonConvert.SerializeObject(newConfig, Formatting.Indented); + Log.Debug(configString); + } + + private async Task MigrateFromJsonToConfAsync() + { + if (!File.Exists("config.json")) + { + return; + } + + try + { + Log.Debug("config.json found, migrating to config.conf"); + string jsonText = await File.ReadAllTextAsync("config.json"); + Config? jsonConfig = JsonConvert.DeserializeObject(jsonText); + + if (jsonConfig != null) { - string hoconText = await File.ReadAllTextAsync(filePath); - var hoconConfig = ConfigurationFactory.ParseString(hoconText); + string hoconConfig = BuildHoconFromConfig(jsonConfig); + await File.WriteAllTextAsync("config.conf", hoconConfig); + File.Delete("config.json"); + Log.Information("config.conf created successfully from config.json"); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to migrate config.json to config.conf"); + throw; + } + } - CurrentConfig = new Config + private async Task LoadConfigFromFileAsync(string filePath) + { + try + { + string hoconText = await File.ReadAllTextAsync(filePath); + Akka.Configuration.Config? hoconConfig = ConfigurationFactory.ParseString(hoconText); + + CurrentConfig = new Config + { + // Auth + DisableBrowserAuth = hoconConfig.GetBoolean("Auth.DisableBrowserAuth"), + + // FFmpeg Settings + FFmpegPath = hoconConfig.GetString("External.FFmpegPath"), + + // Download Settings + DownloadAvatarHeaderPhoto = hoconConfig.GetBoolean("Download.Media.DownloadAvatarHeaderPhoto"), + DownloadPaidPosts = hoconConfig.GetBoolean("Download.Media.DownloadPaidPosts"), + DownloadPosts = hoconConfig.GetBoolean("Download.Media.DownloadPosts"), + DownloadArchived = hoconConfig.GetBoolean("Download.Media.DownloadArchived"), + DownloadStreams = hoconConfig.GetBoolean("Download.Media.DownloadStreams"), + DownloadStories = hoconConfig.GetBoolean("Download.Media.DownloadStories"), + DownloadHighlights = hoconConfig.GetBoolean("Download.Media.DownloadHighlights"), + DownloadMessages = hoconConfig.GetBoolean("Download.Media.DownloadMessages"), + DownloadPaidMessages = hoconConfig.GetBoolean("Download.Media.DownloadPaidMessages"), + DownloadImages = hoconConfig.GetBoolean("Download.Media.DownloadImages"), + DownloadVideos = hoconConfig.GetBoolean("Download.Media.DownloadVideos"), + DownloadAudios = hoconConfig.GetBoolean("Download.Media.DownloadAudios"), + IgnoreOwnMessages = hoconConfig.GetBoolean("Download.IgnoreOwnMessages"), + DownloadPostsIncrementally = hoconConfig.GetBoolean("Download.DownloadPostsIncrementally"), + BypassContentForCreatorsWhoNoLongerExist = + hoconConfig.GetBoolean("Download.BypassContentForCreatorsWhoNoLongerExist"), + DownloadDuplicatedMedia = hoconConfig.GetBoolean("Download.DownloadDuplicatedMedia"), + SkipAds = hoconConfig.GetBoolean("Download.SkipAds"), + DownloadPath = hoconConfig.GetString("Download.DownloadPath"), + DownloadOnlySpecificDates = hoconConfig.GetBoolean("Download.DownloadOnlySpecificDates"), + DownloadDateSelection = + Enum.Parse(hoconConfig.GetString("Download.DownloadDateSelection"), true), + CustomDate = + !string.IsNullOrWhiteSpace(hoconConfig.GetString("Download.CustomDate")) + ? DateTime.Parse(hoconConfig.GetString("Download.CustomDate")) + : null, + ShowScrapeSize = hoconConfig.GetBoolean("Download.ShowScrapeSize"), + DisableTextSanitization = + bool.TryParse(hoconConfig.GetString("Download.DisableTextSanitization", "false"), out bool dts) + ? dts + : false, + DownloadVideoResolution = + ParseVideoResolution(hoconConfig.GetString("Download.DownloadVideoResolution", "source")), + + // File Settings + PaidPostFileNameFormat = hoconConfig.GetString("File.PaidPostFileNameFormat"), + PostFileNameFormat = hoconConfig.GetString("File.PostFileNameFormat"), + PaidMessageFileNameFormat = hoconConfig.GetString("File.PaidMessageFileNameFormat"), + MessageFileNameFormat = hoconConfig.GetString("File.MessageFileNameFormat"), + RenameExistingFilesWhenCustomFormatIsSelected = + hoconConfig.GetBoolean("File.RenameExistingFilesWhenCustomFormatIsSelected"), + + // Folder Settings + FolderPerPaidPost = hoconConfig.GetBoolean("Folder.FolderPerPaidPost"), + FolderPerPost = hoconConfig.GetBoolean("Folder.FolderPerPost"), + FolderPerPaidMessage = hoconConfig.GetBoolean("Folder.FolderPerPaidMessage"), + FolderPerMessage = hoconConfig.GetBoolean("Folder.FolderPerMessage"), + + // Subscription Settings + IncludeExpiredSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeExpiredSubscriptions"), + IncludeRestrictedSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeRestrictedSubscriptions"), + IgnoredUsersListName = hoconConfig.GetString("Subscriptions.IgnoredUsersListName"), + + // Interaction Settings + NonInteractiveMode = hoconConfig.GetBoolean("Interaction.NonInteractiveMode"), + NonInteractiveModeListName = hoconConfig.GetString("Interaction.NonInteractiveModeListName"), + NonInteractiveModePurchasedTab = hoconConfig.GetBoolean("Interaction.NonInteractiveModePurchasedTab"), + + // Performance Settings + Timeout = + string.IsNullOrWhiteSpace(hoconConfig.GetString("Performance.Timeout")) + ? -1 + : hoconConfig.GetInt("Performance.Timeout"), + LimitDownloadRate = hoconConfig.GetBoolean("Performance.LimitDownloadRate"), + DownloadLimitInMbPerSec = hoconConfig.GetInt("Performance.DownloadLimitInMbPerSec"), + + // Logging/Debug Settings + LoggingLevel = Enum.Parse(hoconConfig.GetString("Logging.LoggingLevel"), true) + }; + + // Validate file name formats + ValidateFileNameFormat(CurrentConfig.PaidPostFileNameFormat, "PaidPostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.PostFileNameFormat, "PostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.PaidMessageFileNameFormat, "PaidMessageFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.MessageFileNameFormat, "MessageFileNameFormat"); + + // Load creator-specific configs + Akka.Configuration.Config? creatorConfigsSection = hoconConfig.GetConfig("CreatorConfigs"); + if (creatorConfigsSection != null) + { + foreach (KeyValuePair key in creatorConfigsSection.AsEnumerable()) { - // Auth - DisableBrowserAuth = hoconConfig.GetBoolean("Auth.DisableBrowserAuth"), - - // FFmpeg Settings - FFmpegPath = hoconConfig.GetString("External.FFmpegPath"), - - // Download Settings - DownloadAvatarHeaderPhoto = hoconConfig.GetBoolean("Download.Media.DownloadAvatarHeaderPhoto"), - DownloadPaidPosts = hoconConfig.GetBoolean("Download.Media.DownloadPaidPosts"), - DownloadPosts = hoconConfig.GetBoolean("Download.Media.DownloadPosts"), - DownloadArchived = hoconConfig.GetBoolean("Download.Media.DownloadArchived"), - DownloadStreams = hoconConfig.GetBoolean("Download.Media.DownloadStreams"), - DownloadStories = hoconConfig.GetBoolean("Download.Media.DownloadStories"), - DownloadHighlights = hoconConfig.GetBoolean("Download.Media.DownloadHighlights"), - DownloadMessages = hoconConfig.GetBoolean("Download.Media.DownloadMessages"), - DownloadPaidMessages = hoconConfig.GetBoolean("Download.Media.DownloadPaidMessages"), - DownloadImages = hoconConfig.GetBoolean("Download.Media.DownloadImages"), - DownloadVideos = hoconConfig.GetBoolean("Download.Media.DownloadVideos"), - DownloadAudios = hoconConfig.GetBoolean("Download.Media.DownloadAudios"), - IgnoreOwnMessages = hoconConfig.GetBoolean("Download.IgnoreOwnMessages"), - DownloadPostsIncrementally = hoconConfig.GetBoolean("Download.DownloadPostsIncrementally"), - BypassContentForCreatorsWhoNoLongerExist = hoconConfig.GetBoolean("Download.BypassContentForCreatorsWhoNoLongerExist"), - DownloadDuplicatedMedia = hoconConfig.GetBoolean("Download.DownloadDuplicatedMedia"), - SkipAds = hoconConfig.GetBoolean("Download.SkipAds"), - DownloadPath = hoconConfig.GetString("Download.DownloadPath"), - DownloadOnlySpecificDates = hoconConfig.GetBoolean("Download.DownloadOnlySpecificDates"), - DownloadDateSelection = Enum.Parse(hoconConfig.GetString("Download.DownloadDateSelection"), true), - CustomDate = !string.IsNullOrWhiteSpace(hoconConfig.GetString("Download.CustomDate")) ? DateTime.Parse(hoconConfig.GetString("Download.CustomDate")) : null, - ShowScrapeSize = hoconConfig.GetBoolean("Download.ShowScrapeSize"), - DisableTextSanitization = bool.TryParse(hoconConfig.GetString("Download.DisableTextSanitization", "false"), out var dts) ? dts : false, - DownloadVideoResolution = ParseVideoResolution(hoconConfig.GetString("Download.DownloadVideoResolution", "source")), - - // File Settings - PaidPostFileNameFormat = hoconConfig.GetString("File.PaidPostFileNameFormat"), - PostFileNameFormat = hoconConfig.GetString("File.PostFileNameFormat"), - PaidMessageFileNameFormat = hoconConfig.GetString("File.PaidMessageFileNameFormat"), - MessageFileNameFormat = hoconConfig.GetString("File.MessageFileNameFormat"), - RenameExistingFilesWhenCustomFormatIsSelected = hoconConfig.GetBoolean("File.RenameExistingFilesWhenCustomFormatIsSelected"), - - // Folder Settings - FolderPerPaidPost = hoconConfig.GetBoolean("Folder.FolderPerPaidPost"), - FolderPerPost = hoconConfig.GetBoolean("Folder.FolderPerPost"), - FolderPerPaidMessage = hoconConfig.GetBoolean("Folder.FolderPerPaidMessage"), - FolderPerMessage = hoconConfig.GetBoolean("Folder.FolderPerMessage"), - - // Subscription Settings - IncludeExpiredSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeExpiredSubscriptions"), - IncludeRestrictedSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeRestrictedSubscriptions"), - IgnoredUsersListName = hoconConfig.GetString("Subscriptions.IgnoredUsersListName"), - - // Interaction Settings - NonInteractiveMode = hoconConfig.GetBoolean("Interaction.NonInteractiveMode"), - NonInteractiveModeListName = hoconConfig.GetString("Interaction.NonInteractiveModeListName"), - NonInteractiveModePurchasedTab = hoconConfig.GetBoolean("Interaction.NonInteractiveModePurchasedTab"), - - // Performance Settings - Timeout = string.IsNullOrWhiteSpace(hoconConfig.GetString("Performance.Timeout")) ? -1 : hoconConfig.GetInt("Performance.Timeout"), - LimitDownloadRate = hoconConfig.GetBoolean("Performance.LimitDownloadRate"), - DownloadLimitInMbPerSec = hoconConfig.GetInt("Performance.DownloadLimitInMbPerSec"), - - // Logging/Debug Settings - LoggingLevel = Enum.Parse(hoconConfig.GetString("Logging.LoggingLevel"), true) - }; - - // Validate file name formats - ValidateFileNameFormat(CurrentConfig.PaidPostFileNameFormat, "PaidPostFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.PostFileNameFormat, "PostFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.PaidMessageFileNameFormat, "PaidMessageFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.MessageFileNameFormat, "MessageFileNameFormat"); - - // Load creator-specific configs - var creatorConfigsSection = hoconConfig.GetConfig("CreatorConfigs"); - if (creatorConfigsSection != null) - { - foreach (var key in creatorConfigsSection.AsEnumerable()) + string creatorKey = key.Key; + Akka.Configuration.Config? creatorHocon = creatorConfigsSection.GetConfig(creatorKey); + if (!CurrentConfig.CreatorConfigs.ContainsKey(creatorKey) && creatorHocon != null) { - var creatorKey = key.Key; - var creatorHocon = creatorConfigsSection.GetConfig(creatorKey); - if (!CurrentConfig.CreatorConfigs.ContainsKey(creatorKey) && creatorHocon != null) - { - CurrentConfig.CreatorConfigs.Add(key.Key, new CreatorConfig + CurrentConfig.CreatorConfigs.Add(key.Key, + new CreatorConfig { PaidPostFileNameFormat = creatorHocon.GetString("PaidPostFileNameFormat"), PostFileNameFormat = creatorHocon.GetString("PostFileNameFormat"), @@ -225,161 +243,172 @@ namespace OF_DL.Services MessageFileNameFormat = creatorHocon.GetString("MessageFileNameFormat") }); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidPostFileNameFormat, $"{key.Key}.PaidPostFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PostFileNameFormat, $"{key.Key}.PostFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidMessageFileNameFormat, $"{key.Key}.PaidMessageFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].MessageFileNameFormat, $"{key.Key}.MessageFileNameFormat"); - } + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidPostFileNameFormat, + $"{key.Key}.PaidPostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PostFileNameFormat, + $"{key.Key}.PostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidMessageFileNameFormat, + $"{key.Key}.PaidMessageFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].MessageFileNameFormat, + $"{key.Key}.MessageFileNameFormat"); } } - - // Update logging level - loggingService.UpdateLoggingLevel(CurrentConfig.LoggingLevel); - - // Apply text sanitization preference globally - OF_DL.Utils.XmlUtils.Passthrough = CurrentConfig.DisableTextSanitization; - - Log.Debug("Configuration loaded successfully"); - string configString = JsonConvert.SerializeObject(CurrentConfig, Formatting.Indented); - Log.Debug(configString); - - return true; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to parse config file"); - return false; } + + // Update logging level + loggingService.UpdateLoggingLevel(CurrentConfig.LoggingLevel); + + // Apply text sanitization preference globally + XmlUtils.Passthrough = CurrentConfig.DisableTextSanitization; + + Log.Debug("Configuration loaded successfully"); + string configString = JsonConvert.SerializeObject(CurrentConfig, Formatting.Indented); + Log.Debug(configString); + + return true; } - - private async Task CreateDefaultConfigFileAsync() + catch (Exception ex) { - Config defaultConfig = new Config(); - var hoconConfig = BuildHoconFromConfig(defaultConfig); - await File.WriteAllTextAsync("config.conf", hoconConfig); - Log.Information("Created default config.conf file"); - } - - private string BuildHoconFromConfig(Config config) - { - var hocon = new StringBuilder(); - - hocon.AppendLine("# Auth"); - hocon.AppendLine("Auth {"); - hocon.AppendLine($" DisableBrowserAuth = {config.DisableBrowserAuth.ToString().ToLower()}"); - hocon.AppendLine("}"); - - hocon.AppendLine("# External Tools"); - hocon.AppendLine("External {"); - hocon.AppendLine($" FFmpegPath = \"{config.FFmpegPath}\""); - hocon.AppendLine("}"); - - hocon.AppendLine("# Download Settings"); - hocon.AppendLine("Download {"); - hocon.AppendLine(" Media {"); - hocon.AppendLine($" DownloadAvatarHeaderPhoto = {config.DownloadAvatarHeaderPhoto.ToString().ToLower()}"); - hocon.AppendLine($" DownloadPaidPosts = {config.DownloadPaidPosts.ToString().ToLower()}"); - hocon.AppendLine($" DownloadPosts = {config.DownloadPosts.ToString().ToLower()}"); - hocon.AppendLine($" DownloadArchived = {config.DownloadArchived.ToString().ToLower()}"); - hocon.AppendLine($" DownloadStreams = {config.DownloadStreams.ToString().ToLower()}"); - hocon.AppendLine($" DownloadStories = {config.DownloadStories.ToString().ToLower()}"); - hocon.AppendLine($" DownloadHighlights = {config.DownloadHighlights.ToString().ToLower()}"); - hocon.AppendLine($" DownloadMessages = {config.DownloadMessages.ToString().ToLower()}"); - hocon.AppendLine($" DownloadPaidMessages = {config.DownloadPaidMessages.ToString().ToLower()}"); - hocon.AppendLine($" DownloadImages = {config.DownloadImages.ToString().ToLower()}"); - hocon.AppendLine($" DownloadVideos = {config.DownloadVideos.ToString().ToLower()}"); - hocon.AppendLine($" DownloadAudios = {config.DownloadAudios.ToString().ToLower()}"); - hocon.AppendLine(" }"); - hocon.AppendLine($" IgnoreOwnMessages = {config.IgnoreOwnMessages.ToString().ToLower()}"); - hocon.AppendLine($" DownloadPostsIncrementally = {config.DownloadPostsIncrementally.ToString().ToLower()}"); - hocon.AppendLine($" BypassContentForCreatorsWhoNoLongerExist = {config.BypassContentForCreatorsWhoNoLongerExist.ToString().ToLower()}"); - hocon.AppendLine($" DownloadDuplicatedMedia = {config.DownloadDuplicatedMedia.ToString().ToLower()}"); - hocon.AppendLine($" SkipAds = {config.SkipAds.ToString().ToLower()}"); - hocon.AppendLine($" DownloadPath = \"{config.DownloadPath}\""); - hocon.AppendLine($" DownloadOnlySpecificDates = {config.DownloadOnlySpecificDates.ToString().ToLower()}"); - hocon.AppendLine($" DownloadDateSelection = \"{config.DownloadDateSelection.ToString().ToLower()}\""); - hocon.AppendLine($" CustomDate = \"{config.CustomDate?.ToString("yyyy-MM-dd")}\""); - hocon.AppendLine($" ShowScrapeSize = {config.ShowScrapeSize.ToString().ToLower()}"); - hocon.AppendLine($" DisableTextSanitization = {config.DisableTextSanitization.ToString().ToLower()}"); - hocon.AppendLine($" DownloadVideoResolution = \"{(config.DownloadVideoResolution == VideoResolution.source ? "source" : config.DownloadVideoResolution.ToString().TrimStart('_'))}\""); - hocon.AppendLine("}"); - - hocon.AppendLine("# File Settings"); - hocon.AppendLine("File {"); - hocon.AppendLine($" PaidPostFileNameFormat = \"{config.PaidPostFileNameFormat}\""); - hocon.AppendLine($" PostFileNameFormat = \"{config.PostFileNameFormat}\""); - hocon.AppendLine($" PaidMessageFileNameFormat = \"{config.PaidMessageFileNameFormat}\""); - hocon.AppendLine($" MessageFileNameFormat = \"{config.MessageFileNameFormat}\""); - hocon.AppendLine($" RenameExistingFilesWhenCustomFormatIsSelected = {config.RenameExistingFilesWhenCustomFormatIsSelected.ToString().ToLower()}"); - hocon.AppendLine("}"); - - hocon.AppendLine("# Creator-Specific Configurations"); - hocon.AppendLine("CreatorConfigs {"); - foreach (var creatorConfig in config.CreatorConfigs) - { - hocon.AppendLine($" \"{creatorConfig.Key}\" {{"); - hocon.AppendLine($" PaidPostFileNameFormat = \"{creatorConfig.Value.PaidPostFileNameFormat}\""); - hocon.AppendLine($" PostFileNameFormat = \"{creatorConfig.Value.PostFileNameFormat}\""); - hocon.AppendLine($" PaidMessageFileNameFormat = \"{creatorConfig.Value.PaidMessageFileNameFormat}\""); - hocon.AppendLine($" MessageFileNameFormat = \"{creatorConfig.Value.MessageFileNameFormat}\""); - hocon.AppendLine(" }"); - } - hocon.AppendLine("}"); - - hocon.AppendLine("# Folder Settings"); - hocon.AppendLine("Folder {"); - hocon.AppendLine($" FolderPerPaidPost = {config.FolderPerPaidPost.ToString().ToLower()}"); - hocon.AppendLine($" FolderPerPost = {config.FolderPerPost.ToString().ToLower()}"); - hocon.AppendLine($" FolderPerPaidMessage = {config.FolderPerPaidMessage.ToString().ToLower()}"); - hocon.AppendLine($" FolderPerMessage = {config.FolderPerMessage.ToString().ToLower()}"); - hocon.AppendLine("}"); - - hocon.AppendLine("# Subscription Settings"); - hocon.AppendLine("Subscriptions {"); - hocon.AppendLine($" IncludeExpiredSubscriptions = {config.IncludeExpiredSubscriptions.ToString().ToLower()}"); - hocon.AppendLine($" IncludeRestrictedSubscriptions = {config.IncludeRestrictedSubscriptions.ToString().ToLower()}"); - hocon.AppendLine($" IgnoredUsersListName = \"{config.IgnoredUsersListName}\""); - hocon.AppendLine("}"); - - hocon.AppendLine("# Interaction Settings"); - hocon.AppendLine("Interaction {"); - hocon.AppendLine($" NonInteractiveMode = {config.NonInteractiveMode.ToString().ToLower()}"); - hocon.AppendLine($" NonInteractiveModeListName = \"{config.NonInteractiveModeListName}\""); - hocon.AppendLine($" NonInteractiveModePurchasedTab = {config.NonInteractiveModePurchasedTab.ToString().ToLower()}"); - hocon.AppendLine("}"); - - hocon.AppendLine("# Performance Settings"); - hocon.AppendLine("Performance {"); - hocon.AppendLine($" Timeout = {(config.Timeout.HasValue ? config.Timeout.Value : -1)}"); - hocon.AppendLine($" LimitDownloadRate = {config.LimitDownloadRate.ToString().ToLower()}"); - hocon.AppendLine($" DownloadLimitInMbPerSec = {config.DownloadLimitInMbPerSec}"); - hocon.AppendLine("}"); - - hocon.AppendLine("# Logging/Debug Settings"); - hocon.AppendLine("Logging {"); - hocon.AppendLine($" LoggingLevel = \"{config.LoggingLevel.ToString().ToLower()}\""); - hocon.AppendLine("}"); - - return hocon.ToString(); - } - - private void ValidateFileNameFormat(string? format, string settingName) - { - if (!string.IsNullOrEmpty(format) && - !format.Contains("{mediaId}", StringComparison.OrdinalIgnoreCase) && - !format.Contains("{filename}", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException( - $"{settingName} is not unique enough. Please include either '{{mediaId}}' or '{{filename}}' to ensure files are not overwritten."); - } - } - - private VideoResolution ParseVideoResolution(string value) - { - if (value.Equals("source", StringComparison.OrdinalIgnoreCase)) - return VideoResolution.source; - - return Enum.Parse("_" + value, ignoreCase: true); + Log.Error(ex, "Failed to parse config file"); + return false; } } + + private async Task CreateDefaultConfigFileAsync() + { + Config defaultConfig = new(); + string hoconConfig = BuildHoconFromConfig(defaultConfig); + await File.WriteAllTextAsync("config.conf", hoconConfig); + Log.Information("Created default config.conf file"); + } + + private string BuildHoconFromConfig(Config config) + { + StringBuilder hocon = new(); + + hocon.AppendLine("# Auth"); + hocon.AppendLine("Auth {"); + hocon.AppendLine($" DisableBrowserAuth = {config.DisableBrowserAuth.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# External Tools"); + hocon.AppendLine("External {"); + hocon.AppendLine($" FFmpegPath = \"{config.FFmpegPath}\""); + hocon.AppendLine("}"); + + hocon.AppendLine("# Download Settings"); + hocon.AppendLine("Download {"); + hocon.AppendLine(" Media {"); + hocon.AppendLine($" DownloadAvatarHeaderPhoto = {config.DownloadAvatarHeaderPhoto.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPaidPosts = {config.DownloadPaidPosts.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPosts = {config.DownloadPosts.ToString().ToLower()}"); + hocon.AppendLine($" DownloadArchived = {config.DownloadArchived.ToString().ToLower()}"); + hocon.AppendLine($" DownloadStreams = {config.DownloadStreams.ToString().ToLower()}"); + hocon.AppendLine($" DownloadStories = {config.DownloadStories.ToString().ToLower()}"); + hocon.AppendLine($" DownloadHighlights = {config.DownloadHighlights.ToString().ToLower()}"); + hocon.AppendLine($" DownloadMessages = {config.DownloadMessages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPaidMessages = {config.DownloadPaidMessages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadImages = {config.DownloadImages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadVideos = {config.DownloadVideos.ToString().ToLower()}"); + hocon.AppendLine($" DownloadAudios = {config.DownloadAudios.ToString().ToLower()}"); + hocon.AppendLine(" }"); + hocon.AppendLine($" IgnoreOwnMessages = {config.IgnoreOwnMessages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPostsIncrementally = {config.DownloadPostsIncrementally.ToString().ToLower()}"); + hocon.AppendLine( + $" BypassContentForCreatorsWhoNoLongerExist = {config.BypassContentForCreatorsWhoNoLongerExist.ToString().ToLower()}"); + hocon.AppendLine($" DownloadDuplicatedMedia = {config.DownloadDuplicatedMedia.ToString().ToLower()}"); + hocon.AppendLine($" SkipAds = {config.SkipAds.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPath = \"{config.DownloadPath}\""); + hocon.AppendLine($" DownloadOnlySpecificDates = {config.DownloadOnlySpecificDates.ToString().ToLower()}"); + hocon.AppendLine($" DownloadDateSelection = \"{config.DownloadDateSelection.ToString().ToLower()}\""); + hocon.AppendLine($" CustomDate = \"{config.CustomDate?.ToString("yyyy-MM-dd")}\""); + hocon.AppendLine($" ShowScrapeSize = {config.ShowScrapeSize.ToString().ToLower()}"); + hocon.AppendLine($" DisableTextSanitization = {config.DisableTextSanitization.ToString().ToLower()}"); + hocon.AppendLine( + $" DownloadVideoResolution = \"{(config.DownloadVideoResolution == VideoResolution.source ? "source" : config.DownloadVideoResolution.ToString().TrimStart('_'))}\""); + hocon.AppendLine("}"); + + hocon.AppendLine("# File Settings"); + hocon.AppendLine("File {"); + hocon.AppendLine($" PaidPostFileNameFormat = \"{config.PaidPostFileNameFormat}\""); + hocon.AppendLine($" PostFileNameFormat = \"{config.PostFileNameFormat}\""); + hocon.AppendLine($" PaidMessageFileNameFormat = \"{config.PaidMessageFileNameFormat}\""); + hocon.AppendLine($" MessageFileNameFormat = \"{config.MessageFileNameFormat}\""); + hocon.AppendLine( + $" RenameExistingFilesWhenCustomFormatIsSelected = {config.RenameExistingFilesWhenCustomFormatIsSelected.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Creator-Specific Configurations"); + hocon.AppendLine("CreatorConfigs {"); + foreach (KeyValuePair creatorConfig in config.CreatorConfigs) + { + hocon.AppendLine($" \"{creatorConfig.Key}\" {{"); + hocon.AppendLine($" PaidPostFileNameFormat = \"{creatorConfig.Value.PaidPostFileNameFormat}\""); + hocon.AppendLine($" PostFileNameFormat = \"{creatorConfig.Value.PostFileNameFormat}\""); + hocon.AppendLine($" PaidMessageFileNameFormat = \"{creatorConfig.Value.PaidMessageFileNameFormat}\""); + hocon.AppendLine($" MessageFileNameFormat = \"{creatorConfig.Value.MessageFileNameFormat}\""); + hocon.AppendLine(" }"); + } + + hocon.AppendLine("}"); + + hocon.AppendLine("# Folder Settings"); + hocon.AppendLine("Folder {"); + hocon.AppendLine($" FolderPerPaidPost = {config.FolderPerPaidPost.ToString().ToLower()}"); + hocon.AppendLine($" FolderPerPost = {config.FolderPerPost.ToString().ToLower()}"); + hocon.AppendLine($" FolderPerPaidMessage = {config.FolderPerPaidMessage.ToString().ToLower()}"); + hocon.AppendLine($" FolderPerMessage = {config.FolderPerMessage.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Subscription Settings"); + hocon.AppendLine("Subscriptions {"); + hocon.AppendLine($" IncludeExpiredSubscriptions = {config.IncludeExpiredSubscriptions.ToString().ToLower()}"); + hocon.AppendLine( + $" IncludeRestrictedSubscriptions = {config.IncludeRestrictedSubscriptions.ToString().ToLower()}"); + hocon.AppendLine($" IgnoredUsersListName = \"{config.IgnoredUsersListName}\""); + hocon.AppendLine("}"); + + hocon.AppendLine("# Interaction Settings"); + hocon.AppendLine("Interaction {"); + hocon.AppendLine($" NonInteractiveMode = {config.NonInteractiveMode.ToString().ToLower()}"); + hocon.AppendLine($" NonInteractiveModeListName = \"{config.NonInteractiveModeListName}\""); + hocon.AppendLine( + $" NonInteractiveModePurchasedTab = {config.NonInteractiveModePurchasedTab.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Performance Settings"); + hocon.AppendLine("Performance {"); + hocon.AppendLine($" Timeout = {(config.Timeout.HasValue ? config.Timeout.Value : -1)}"); + hocon.AppendLine($" LimitDownloadRate = {config.LimitDownloadRate.ToString().ToLower()}"); + hocon.AppendLine($" DownloadLimitInMbPerSec = {config.DownloadLimitInMbPerSec}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Logging/Debug Settings"); + hocon.AppendLine("Logging {"); + hocon.AppendLine($" LoggingLevel = \"{config.LoggingLevel.ToString().ToLower()}\""); + hocon.AppendLine("}"); + + return hocon.ToString(); + } + + private void ValidateFileNameFormat(string? format, string settingName) + { + if (!string.IsNullOrEmpty(format) && + !format.Contains("{mediaId}", StringComparison.OrdinalIgnoreCase) && + !format.Contains("{filename}", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + $"{settingName} is not unique enough. Please include either '{{mediaId}}' or '{{filename}}' to ensure files are not overwritten."); + } + } + + private VideoResolution ParseVideoResolution(string value) + { + if (value.Equals("source", StringComparison.OrdinalIgnoreCase)) + { + return VideoResolution.source; + } + + return Enum.Parse("_" + value, true); + } } diff --git a/OF DL/Services/DBService.cs b/OF DL/Services/DBService.cs index 70efd80..2ff6c29 100644 --- a/OF DL/Services/DBService.cs +++ b/OF DL/Services/DBService.cs @@ -2,38 +2,41 @@ using System.Text; using Microsoft.Data.Sqlite; using Serilog; -namespace OF_DL.Services +namespace OF_DL.Services; + +public class DBService(IConfigService configService) : IDBService { - public class DBService(IConfigService configService) : IDBService + public async Task CreateDB(string folder) { - public async Task CreateDB(string folder) + try { - try + if (!Directory.Exists(folder + "/Metadata")) { - if (!Directory.Exists(folder + "/Metadata")) - { - Directory.CreateDirectory(folder + "/Metadata"); - } + Directory.CreateDirectory(folder + "/Metadata"); + } - string dbFilePath = $"{folder}/Metadata/user_data.db"; + string dbFilePath = $"{folder}/Metadata/user_data.db"; - // connect to the new database file - using SqliteConnection connection = new($"Data Source={dbFilePath}"); - // open the connection - connection.Open(); + // connect to the new database file + using SqliteConnection connection = new($"Data Source={dbFilePath}"); + // open the connection + connection.Open(); - // create the 'medias' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS medias (id INTEGER NOT NULL, media_id INTEGER, post_id INTEGER NOT NULL, link VARCHAR, directory VARCHAR, filename VARCHAR, size INTEGER, api_type VARCHAR, media_type VARCHAR, preview INTEGER, linked VARCHAR, downloaded INTEGER, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(media_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } + // create the 'medias' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS medias (id INTEGER NOT NULL, media_id INTEGER, post_id INTEGER NOT NULL, link VARCHAR, directory VARCHAR, filename VARCHAR, size INTEGER, api_type VARCHAR, media_type VARCHAR, preview INTEGER, linked VARCHAR, downloaded INTEGER, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(media_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } - await EnsureCreatedAtColumnExists(connection, "medias"); + await EnsureCreatedAtColumnExists(connection, "medias"); - // - // Alter existing databases to create unique constraint on `medias` - // - using (SqliteCommand cmd = new(@" + // + // Alter existing databases to create unique constraint on `medias` + // + using (SqliteCommand cmd = new(@" PRAGMA foreign_keys=off; BEGIN TRANSACTION; @@ -66,408 +69,473 @@ namespace OF_DL.Services COMMIT; PRAGMA foreign_keys=on;", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'messages' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS messages (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, user_id INTEGER, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'posts' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS posts (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'stories' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS stories (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'others' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS others (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'products' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS products (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, title VARCHAR, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'profiles' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS profiles (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(username));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - connection.Close(); - } - 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); - } + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'messages' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS messages (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, user_id INTEGER, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'posts' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS posts (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'stories' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS stories (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'others' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS others (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'products' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS products (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, title VARCHAR, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'profiles' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS profiles (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(username));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + connection.Close(); + } + 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); } } + } - public async Task CreateUsersDB(Dictionary users) + public async Task CreateUsersDB(Dictionary users) + { + try { - try + using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); + Log.Debug("Database data source: " + connection.DataSource); + + connection.Open(); + + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS users (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, PRIMARY KEY(id), UNIQUE(username));", + connection)) { - using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); - Log.Debug("Database data source: " + connection.DataSource); - - connection.Open(); - - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS users (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, PRIMARY KEY(id), UNIQUE(username));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - Log.Debug("Adding missing creators"); - foreach (KeyValuePair user in users) - { - using (SqliteCommand checkCmd = new($"SELECT user_id, username FROM users WHERE user_id = @userId;", connection)) - { - checkCmd.Parameters.AddWithValue("@userId", user.Value); - using (var reader = await checkCmd.ExecuteReaderAsync()) - { - if (!reader.Read()) - { - using (SqliteCommand insertCmd = new($"INSERT INTO users (user_id, username) VALUES (@userId, @username);", connection)) - { - insertCmd.Parameters.AddWithValue("@userId", user.Value); - insertCmd.Parameters.AddWithValue("@username", user.Key); - await insertCmd.ExecuteNonQueryAsync(); - Log.Debug("Inserted new creator: " + user.Key); - } - } - else - { - Log.Debug("Creator " + user.Key + " already exists"); - } - } - } - } - - connection.Close(); + await cmd.ExecuteNonQueryAsync(); } - catch (Exception ex) + + Log.Debug("Adding missing creators"); + foreach (KeyValuePair user in users) { - 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); - } - } - } - - public async Task CheckUsername(KeyValuePair user, string path) - { - try - { - using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); - - connection.Open(); - - using (SqliteCommand checkCmd = new($"SELECT user_id, username FROM users WHERE user_id = @userId;", connection)) + using (SqliteCommand checkCmd = new("SELECT user_id, username FROM users WHERE user_id = @userId;", + connection)) { checkCmd.Parameters.AddWithValue("@userId", user.Value); - using (var reader = await checkCmd.ExecuteReaderAsync()) + using (SqliteDataReader reader = await checkCmd.ExecuteReaderAsync()) { - if (reader.Read()) + if (!reader.Read()) { - long storedUserId = reader.GetInt64(0); - string storedUsername = reader.GetString(1); - - if (storedUsername != user.Key) + using (SqliteCommand insertCmd = + new("INSERT INTO users (user_id, username) VALUES (@userId, @username);", + connection)) { - using (SqliteCommand updateCmd = new($"UPDATE users SET username = @newUsername WHERE user_id = @userId;", connection)) - { - updateCmd.Parameters.AddWithValue("@newUsername", user.Key); - updateCmd.Parameters.AddWithValue("@userId", user.Value); - await updateCmd.ExecuteNonQueryAsync(); - } + insertCmd.Parameters.AddWithValue("@userId", user.Value); + insertCmd.Parameters.AddWithValue("@username", user.Key); + await insertCmd.ExecuteNonQueryAsync(); + Log.Debug("Inserted new creator: " + user.Key); + } + } + else + { + Log.Debug("Creator " + user.Key + " already exists"); + } + } + } + } - string oldPath = path.Replace(path.Split("/")[^1], storedUsername); + connection.Close(); + } + 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 (Directory.Exists(oldPath)) - { - Directory.Move(path.Replace(path.Split("/")[^1], storedUsername), path); - } + public async Task CheckUsername(KeyValuePair user, string path) + { + try + { + using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); + + connection.Open(); + + using (SqliteCommand checkCmd = new("SELECT user_id, username FROM users WHERE user_id = @userId;", + connection)) + { + checkCmd.Parameters.AddWithValue("@userId", user.Value); + using (SqliteDataReader reader = await checkCmd.ExecuteReaderAsync()) + { + if (reader.Read()) + { + long storedUserId = reader.GetInt64(0); + string storedUsername = reader.GetString(1); + + if (storedUsername != user.Key) + { + using (SqliteCommand updateCmd = + new("UPDATE users SET username = @newUsername WHERE user_id = @userId;", connection)) + { + updateCmd.Parameters.AddWithValue("@newUsername", user.Key); + updateCmd.Parameters.AddWithValue("@userId", user.Value); + await updateCmd.ExecuteNonQueryAsync(); + } + + string oldPath = path.Replace(path.Split("/")[^1], storedUsername); + + if (Directory.Exists(oldPath)) + { + Directory.Move(path.Replace(path.Split("/")[^1], storedUsername), path); } } } } + } - connection.Close(); - } - 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); - } - } + connection.Close(); } - - public async Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at, long user_id) + catch (Exception ex) { - try + 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) { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); - connection.Open(); - await EnsureCreatedAtColumnExists(connection, "messages"); - using SqliteCommand cmd = new($"SELECT COUNT(*) FROM messages WHERE post_id=@post_id", connection); - cmd.Parameters.AddWithValue("@post_id", post_id); - int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); - if (count == 0) - { - // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = new("INSERT INTO messages(post_id, text, price, paid, archived, created_at, user_id, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @user_id, @record_created_at)", connection); - insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@is_paid", is_paid); - insertCmd.Parameters.AddWithValue("@is_archived", is_archived); - insertCmd.Parameters.AddWithValue("@created_at", created_at); - insertCmd.Parameters.AddWithValue("@user_id", user_id); - insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); - await insertCmd.ExecuteNonQueryAsync(); - } - } - 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); - } + 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); } } + } - - public async Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at) - { - try - { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); - connection.Open(); - await EnsureCreatedAtColumnExists(connection, "posts"); - using SqliteCommand cmd = new($"SELECT COUNT(*) FROM posts WHERE post_id=@post_id", connection); - cmd.Parameters.AddWithValue("@post_id", post_id); - int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); - if (count == 0) - { - // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = new("INSERT INTO posts(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", connection); - insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@is_paid", is_paid); - insertCmd.Parameters.AddWithValue("@is_archived", is_archived); - insertCmd.Parameters.AddWithValue("@created_at", created_at); - insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); - await insertCmd.ExecuteNonQueryAsync(); - } - } - 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); - } - } - } - - - public async Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at) - { - try - { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); - connection.Open(); - await EnsureCreatedAtColumnExists(connection, "stories"); - using SqliteCommand cmd = new($"SELECT COUNT(*) FROM stories WHERE post_id=@post_id", connection); - cmd.Parameters.AddWithValue("@post_id", post_id); - int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); - if (count == 0) - { - // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = new("INSERT INTO stories(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", connection); - insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@is_paid", is_paid); - insertCmd.Parameters.AddWithValue("@is_archived", is_archived); - insertCmd.Parameters.AddWithValue("@created_at", created_at); - insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); - await insertCmd.ExecuteNonQueryAsync(); - } - } - 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); - } - } - } - - - public async Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, string? filename, long? size, string api_type, string media_type, bool preview, bool downloaded, DateTime? created_at) - { - try - { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); - connection.Open(); - await EnsureCreatedAtColumnExists(connection, "medias"); - StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM medias WHERE media_id=@media_id"); - if (configService.CurrentConfig.DownloadDuplicatedMedia) - { - sql.Append(" and api_type=@api_type"); - } - - using SqliteCommand cmd = new(sql.ToString(), connection); - cmd.Parameters.AddWithValue("@media_id", media_id); - cmd.Parameters.AddWithValue("@api_type", api_type); - int count = Convert.ToInt32(cmd.ExecuteScalar()); - if (count == 0) - { - // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = new($"INSERT INTO medias(media_id, post_id, link, directory, filename, size, api_type, media_type, preview, downloaded, created_at, record_created_at) VALUES({media_id}, {post_id}, '{link}', '{directory?.ToString() ?? "NULL"}', '{filename?.ToString() ?? "NULL"}', {size?.ToString() ?? "NULL"}, '{api_type}', '{media_type}', {Convert.ToInt32(preview)}, {Convert.ToInt32(downloaded)}, '{created_at?.ToString("yyyy-MM-dd HH:mm:ss")}', '{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}')", connection); - await insertCmd.ExecuteNonQueryAsync(); - } - } - 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); - } - } - } - - - public async Task CheckDownloaded(string folder, long media_id, string api_type) - { - try - { - bool downloaded = false; - - using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) - { - StringBuilder sql = new StringBuilder("SELECT downloaded FROM medias WHERE media_id=@media_id"); - if(configService.CurrentConfig.DownloadDuplicatedMedia) - { - sql.Append(" and api_type=@api_type"); - } - - connection.Open(); - using SqliteCommand cmd = new (sql.ToString(), connection); - cmd.Parameters.AddWithValue("@media_id", media_id); - cmd.Parameters.AddWithValue("@api_type", api_type); - downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync()); - } - return downloaded; - } - 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); - } - } - return false; - } - - - public async Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, long size, bool downloaded, DateTime created_at) + public async Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, + bool is_archived, DateTime created_at, long user_id) + { + try { using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); connection.Open(); + await EnsureCreatedAtColumnExists(connection, "messages"); + using SqliteCommand cmd = new("SELECT COUNT(*) FROM messages WHERE post_id=@post_id", connection); + cmd.Parameters.AddWithValue("@post_id", post_id); + int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + if (count == 0) + { + // If the record doesn't exist, insert a new one + using SqliteCommand insertCmd = + new( + "INSERT INTO messages(post_id, text, price, paid, archived, created_at, user_id, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @user_id, @record_created_at)", + connection); + insertCmd.Parameters.AddWithValue("@post_id", post_id); + insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@is_paid", is_paid); + insertCmd.Parameters.AddWithValue("@is_archived", is_archived); + insertCmd.Parameters.AddWithValue("@created_at", created_at); + insertCmd.Parameters.AddWithValue("@user_id", user_id); + insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + await insertCmd.ExecuteNonQueryAsync(); + } + } + 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); + } + } + } - // Construct the update command - StringBuilder sql = new StringBuilder("UPDATE medias SET directory=@directory, filename=@filename, size=@size, downloaded=@downloaded, created_at=@created_at WHERE media_id=@media_id"); + + public async Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, + bool is_archived, DateTime created_at) + { + try + { + using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + connection.Open(); + await EnsureCreatedAtColumnExists(connection, "posts"); + using SqliteCommand cmd = new("SELECT COUNT(*) FROM posts WHERE post_id=@post_id", connection); + cmd.Parameters.AddWithValue("@post_id", post_id); + int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + if (count == 0) + { + // If the record doesn't exist, insert a new one + using SqliteCommand insertCmd = + new( + "INSERT INTO posts(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", + connection); + insertCmd.Parameters.AddWithValue("@post_id", post_id); + insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@is_paid", is_paid); + insertCmd.Parameters.AddWithValue("@is_archived", is_archived); + insertCmd.Parameters.AddWithValue("@created_at", created_at); + insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + await insertCmd.ExecuteNonQueryAsync(); + } + } + 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); + } + } + } + + + public async Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, + bool is_archived, DateTime created_at) + { + try + { + using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + connection.Open(); + await EnsureCreatedAtColumnExists(connection, "stories"); + using SqliteCommand cmd = new("SELECT COUNT(*) FROM stories WHERE post_id=@post_id", connection); + cmd.Parameters.AddWithValue("@post_id", post_id); + int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + if (count == 0) + { + // If the record doesn't exist, insert a new one + using SqliteCommand insertCmd = + new( + "INSERT INTO stories(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", + connection); + insertCmd.Parameters.AddWithValue("@post_id", post_id); + insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@is_paid", is_paid); + insertCmd.Parameters.AddWithValue("@is_archived", is_archived); + insertCmd.Parameters.AddWithValue("@created_at", created_at); + insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + await insertCmd.ExecuteNonQueryAsync(); + } + } + 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); + } + } + } + + + public async Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, + string? filename, long? size, string api_type, string media_type, bool preview, bool downloaded, + DateTime? created_at) + { + try + { + using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + connection.Open(); + await EnsureCreatedAtColumnExists(connection, "medias"); + StringBuilder sql = new("SELECT COUNT(*) FROM medias WHERE media_id=@media_id"); if (configService.CurrentConfig.DownloadDuplicatedMedia) { sql.Append(" and api_type=@api_type"); } - // Create a new command object - using SqliteCommand command = new(sql.ToString(), connection); - // Add parameters to the command object - command.Parameters.AddWithValue("@directory", directory); - command.Parameters.AddWithValue("@filename", filename); - command.Parameters.AddWithValue("@size", size); - command.Parameters.AddWithValue("@downloaded", downloaded ? 1 : 0); - command.Parameters.AddWithValue("@created_at", created_at); - command.Parameters.AddWithValue("@media_id", media_id); - command.Parameters.AddWithValue("@api_type", api_type); - - // Execute the command - await command.ExecuteNonQueryAsync(); + using SqliteCommand cmd = new(sql.ToString(), connection); + cmd.Parameters.AddWithValue("@media_id", media_id); + cmd.Parameters.AddWithValue("@api_type", api_type); + int count = Convert.ToInt32(cmd.ExecuteScalar()); + if (count == 0) + { + // If the record doesn't exist, insert a new one + using SqliteCommand insertCmd = new( + $"INSERT INTO medias(media_id, post_id, link, directory, filename, size, api_type, media_type, preview, downloaded, created_at, record_created_at) VALUES({media_id}, {post_id}, '{link}', '{directory ?? "NULL"}', '{filename ?? "NULL"}', {size?.ToString() ?? "NULL"}, '{api_type}', '{media_type}', {Convert.ToInt32(preview)}, {Convert.ToInt32(downloaded)}, '{created_at?.ToString("yyyy-MM-dd HH:mm:ss")}', '{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}')", + connection); + await insertCmd.ExecuteNonQueryAsync(); + } } - - - public async Task GetStoredFileSize(string folder, long media_id, string api_type) + catch (Exception ex) { - long size; + 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); + } + } + } + + + public async Task CheckDownloaded(string folder, long media_id, string api_type) + { + try + { + bool downloaded = false; + using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) { + StringBuilder sql = new("SELECT downloaded FROM medias WHERE media_id=@media_id"); + if (configService.CurrentConfig.DownloadDuplicatedMedia) + { + sql.Append(" and api_type=@api_type"); + } + connection.Open(); - using SqliteCommand cmd = new($"SELECT size FROM medias WHERE media_id=@media_id and api_type=@api_type", connection); + using SqliteCommand cmd = new(sql.ToString(), connection); cmd.Parameters.AddWithValue("@media_id", media_id); cmd.Parameters.AddWithValue("@api_type", api_type); - size = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync()); + } + + return downloaded; + } + 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); } - return size; } - public async Task GetMostRecentPostDate(string folder) + return false; + } + + + public async Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, + long size, bool downloaded, DateTime created_at) + { + using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + connection.Open(); + + // Construct the update command + StringBuilder sql = + new( + "UPDATE medias SET directory=@directory, filename=@filename, size=@size, downloaded=@downloaded, created_at=@created_at WHERE media_id=@media_id"); + if (configService.CurrentConfig.DownloadDuplicatedMedia) { - DateTime? mostRecentDate = null; - using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) - { - connection.Open(); - using SqliteCommand cmd = new(@" + sql.Append(" and api_type=@api_type"); + } + + // Create a new command object + using SqliteCommand command = new(sql.ToString(), connection); + // Add parameters to the command object + command.Parameters.AddWithValue("@directory", directory); + command.Parameters.AddWithValue("@filename", filename); + command.Parameters.AddWithValue("@size", size); + command.Parameters.AddWithValue("@downloaded", downloaded ? 1 : 0); + command.Parameters.AddWithValue("@created_at", created_at); + command.Parameters.AddWithValue("@media_id", media_id); + command.Parameters.AddWithValue("@api_type", api_type); + + // Execute the command + await command.ExecuteNonQueryAsync(); + } + + + public async Task GetStoredFileSize(string folder, long media_id, string api_type) + { + long size; + using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) + { + connection.Open(); + using SqliteCommand cmd = new("SELECT size FROM medias WHERE media_id=@media_id and api_type=@api_type", + connection); + cmd.Parameters.AddWithValue("@media_id", media_id); + cmd.Parameters.AddWithValue("@api_type", api_type); + size = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + } + + return size; + } + + public async Task GetMostRecentPostDate(string folder) + { + DateTime? mostRecentDate = null; + using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) + { + connection.Open(); + using SqliteCommand cmd = new(@" SELECT MIN(created_at) AS created_at FROM ( @@ -483,35 +551,36 @@ namespace OF_DL.Services ON P.post_id = m.post_id WHERE m.downloaded = 0 )", connection); - var scalarValue = await cmd.ExecuteScalarAsync(); - if(scalarValue != null && scalarValue != DBNull.Value) - { - mostRecentDate = Convert.ToDateTime(scalarValue); - } + object? scalarValue = await cmd.ExecuteScalarAsync(); + if (scalarValue != null && scalarValue != DBNull.Value) + { + mostRecentDate = Convert.ToDateTime(scalarValue); } - return mostRecentDate; } - private async Task EnsureCreatedAtColumnExists(SqliteConnection connection, string tableName) + return mostRecentDate; + } + + private async Task EnsureCreatedAtColumnExists(SqliteConnection connection, string tableName) + { + using SqliteCommand cmd = new($"PRAGMA table_info({tableName});", connection); + using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); + bool columnExists = false; + + while (await reader.ReadAsync()) { - using SqliteCommand cmd = new($"PRAGMA table_info({tableName});", connection); - using var reader = await cmd.ExecuteReaderAsync(); - bool columnExists = false; - - while (await reader.ReadAsync()) + if (reader["name"].ToString() == "record_created_at") { - if (reader["name"].ToString() == "record_created_at") - { - columnExists = true; - break; - } + columnExists = true; + break; } + } - if (!columnExists) - { - using SqliteCommand alterCmd = new($"ALTER TABLE {tableName} ADD COLUMN record_created_at TIMESTAMP;", connection); - await alterCmd.ExecuteNonQueryAsync(); - } + if (!columnExists) + { + using SqliteCommand alterCmd = new($"ALTER TABLE {tableName} ADD COLUMN record_created_at TIMESTAMP;", + connection); + await alterCmd.ExecuteNonQueryAsync(); } } } diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index ab6cd7e..9fe84c4 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -34,12 +34,18 @@ public class DownloadService( { string path = "/Profile"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(avatarUrl)) { string avatarpath = $"{path}/Avatars"; - if (!Directory.Exists(folder + avatarpath)) Directory.CreateDirectory(folder + avatarpath); + if (!Directory.Exists(folder + avatarpath)) + { + Directory.CreateDirectory(folder + avatarpath); + } List avatarMD5Hashes = CalculateFolderMD5(folder + avatarpath); @@ -48,11 +54,7 @@ public class DownloadService( HttpClient client = new(); - HttpRequestMessage request = new() - { - Method = HttpMethod.Get, - RequestUri = uri - }; + HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = uri }; using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); @@ -84,7 +86,10 @@ public class DownloadService( if (!string.IsNullOrEmpty(headerUrl)) { string headerpath = $"{path}/Headers"; - if (!Directory.Exists(folder + headerpath)) Directory.CreateDirectory(folder + headerpath); + if (!Directory.Exists(folder + headerpath)) + { + Directory.CreateDirectory(folder + headerpath); + } List headerMD5Hashes = CalculateFolderMD5(folder + headerpath); @@ -93,11 +98,7 @@ public class DownloadService( HttpClient client = new(); - HttpRequestMessage request = new() - { - Method = HttpMethod.Get, - RequestUri = uri - }; + HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = uri }; using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); @@ -151,7 +152,10 @@ public class DownloadService( int pos1 = decryptionKey.IndexOf(':'); string decKey = ""; - if (pos1 >= 0) decKey = decryptionKey.Substring(pos1 + 1); + if (pos1 >= 0) + { + decKey = decryptionKey.Substring(pos1 + 1); + } int streamIndex = 0; string tempFilename = $"{folder}{path}/{filename}_source.mp4"; @@ -269,18 +273,28 @@ public class DownloadService( { try { - if (File.Exists(tempFilename)) File.SetLastWriteTime(tempFilename, lastModified); + if (File.Exists(tempFilename)) + { + File.SetLastWriteTime(tempFilename, lastModified); + } + if (!string.IsNullOrEmpty(customFileName)) + { File.Move(tempFilename, $"{folder + path + "/" + customFileName + ".mp4"}"); + } // Cleanup Files long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : tempFilename).Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4", @@ -337,7 +351,9 @@ public class DownloadService( .FirstOrDefault(e => (string)e.Attribute("mimeType") == "video/mp4"); if (videoAdaptationSet == null) + { return null; + } string targetHeight = resolution switch { @@ -350,13 +366,45 @@ public class DownloadService( List representations = videoAdaptationSet.Elements(ns + "Representation").ToList(); for (int i = 0; i < representations.Count; i++) + { if ((string)representations[i].Attribute("height") == targetHeight) + { return i; // this is the index FFmpeg will use for `-map 0:v:{i}` + } + } } return null; } + private static List CalculateFolderMD5(string folder) + { + List md5Hashes = new(); + if (Directory.Exists(folder)) + { + string[] files = Directory.GetFiles(folder); + + foreach (string file in files) + { + md5Hashes.Add(CalculateMD5(file)); + } + } + + return md5Hashes; + } + + private static string CalculateMD5(string filePath) + { + using (MD5 md5 = MD5.Create()) + { + using (FileStream stream = File.OpenRead(filePath)) + { + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } + #region common /// @@ -384,7 +432,11 @@ public class DownloadService( try { string customFileName = string.Empty; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } + string extension = Path.GetExtension(url.Split("?")[0]); path = UpdatePathBasedOnExtension(folder, path, extension); @@ -445,7 +497,10 @@ public class DownloadService( path += subdirectory; string fullPath = folder + path; - if (!Directory.Exists(fullPath)) Directory.CreateDirectory(fullPath); + if (!Directory.Exists(fullPath)) + { + Directory.CreateDirectory(fullPath); + } } return path; @@ -473,12 +528,14 @@ public class DownloadService( CustomFileNameOption option) { if (string.IsNullOrEmpty(filenameFormat) || postInfo == null || postMedia == null || author == null) + { return option switch { CustomFileNameOption.ReturnOriginal => filename, CustomFileNameOption.ReturnEmpty => string.Empty, _ => filename }; + } List properties = new(); string pattern = @"\{(.*?)\}"; @@ -516,7 +573,10 @@ public class DownloadService( using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) fileSize = response.Content.Headers.ContentLength ?? 0; + if (response.IsSuccessStatusCode) + { + fileSize = response.Content.Headers.ContentLength ?? 0; + } } else { @@ -524,7 +584,10 @@ public class DownloadService( client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.USER_AGENT); using HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) fileSize = response.Content.Headers.ContentLength ?? 0; + if (response.IsSuccessStatusCode) + { + fileSize = response.Content.Headers.ContentLength ?? 0; + } } } catch (Exception ex) @@ -553,7 +616,11 @@ public class DownloadService( client.DefaultRequestHeaders.Add("User-Agent", auth.USER_AGENT); using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) return response.Content.Headers.LastModified.Value.DateTime; + if (response.IsSuccessStatusCode) + { + return response.Content.Headers.LastModified.Value.DateTime; + } + return DateTime.Now; } @@ -562,7 +629,11 @@ public class DownloadService( using HttpClient client = new(); using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) return response.Content.Headers.LastModified.Value.DateTime; + if (response.IsSuccessStatusCode) + { + return response.Content.Headers.LastModified.Value.DateTime; + } + return DateTime.Now; } @@ -591,6 +662,7 @@ public class DownloadService( try { if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { return await HandleNewMedia(folder, media_id, api_type, @@ -600,12 +672,16 @@ public class DownloadService( resolvedFilename, extension, progressReporter); + } bool status = await HandlePreviouslyDownloadedMediaAsync(folder, media_id, api_type, progressReporter); if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && serverFilename != resolvedFilename) + { await HandleRenamingOfExistingFilesAsync(folder, media_id, api_type, path, serverFilename, resolvedFilename, extension); + } + return status; } catch (Exception ex) @@ -627,7 +703,10 @@ public class DownloadService( { string fullPathWithTheServerFileName = $"{folder}{path}/{serverFilename}{extension}"; string fullPathWithTheNewFileName = $"{folder}{path}/{resolvedFilename}{extension}"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } try { @@ -711,9 +790,14 @@ public class DownloadService( fileSizeInBytes = GetLocalFileSize(finalPath); lastModified = File.GetLastWriteTime(finalPath); if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + status = false; } // Handle the case where the file has been downloaded in the past with a custom filename. @@ -724,9 +808,14 @@ public class DownloadService( fileSizeInBytes = GetLocalFileSize(fullPathWithTheNewFileName); lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName); if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + status = false; } else //file doesn't exist and we should download it. @@ -775,10 +864,7 @@ public class DownloadService( /// /// The path to the file. /// The file size in bytes. - private long GetLocalFileSize(string filePath) - { - return new FileInfo(filePath).Length; - } + private long GetLocalFileSize(string filePath) => new FileInfo(filePath).Length; /// @@ -791,11 +877,7 @@ public class DownloadService( private async Task DownloadFile(string url, string destinationPath, IProgressReporter progressReporter) { using HttpClient client = new(); - HttpRequestMessage request = new() - { - Method = HttpMethod.Get, - RequestUri = new Uri(url) - }; + HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = new Uri(url) }; using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); @@ -812,13 +894,21 @@ public class DownloadService( int read; while ((read = await throttledStream.ReadAsync(buffer, CancellationToken.None)) > 0) { - if (configService.CurrentConfig.ShowScrapeSize) progressReporter.ReportProgress(read); + if (configService.CurrentConfig.ShowScrapeSize) + { + progressReporter.ReportProgress(read); + } + await fileStream.WriteAsync(buffer.AsMemory(0, read), CancellationToken.None); } } File.SetLastWriteTime(destinationPath, response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); - if (!configService.CurrentConfig.ShowScrapeSize) progressReporter.ReportProgress(1); + if (!configService.CurrentConfig.ShowScrapeSize) + { + progressReporter.ReportProgress(1); + } + return response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now; } @@ -844,16 +934,25 @@ public class DownloadService( } long[] fileSizes = await Task.WhenAll(tasks); - foreach (long fileSize in fileSizes) totalFileSize += fileSize; + foreach (long fileSize in fileSizes) + { + totalFileSize += fileSize; + } } else { List> tasks = new(); - foreach (string url in urls) tasks.Add(GetFileSizeAsync(url)); + foreach (string url in urls) + { + tasks.Add(GetFileSizeAsync(url)); + } long[] fileSizes = await Task.WhenAll(tasks); - foreach (long fileSize in fileSizes) totalFileSize += fileSize; + foreach (long fileSize in fileSizes) + { + totalFileSize += fileSize; + } } return totalFileSize; @@ -870,9 +969,13 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) + { path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Posts/Free"; + } Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); @@ -890,9 +993,13 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) + { path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Posts/Free"; + } Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); @@ -910,9 +1017,13 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.id is not null && streamInfo?.postedAt is not null) + { path = $"/Posts/Free/{streamInfo.id} {streamInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Posts/Free"; + } Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); @@ -931,9 +1042,14 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Messages/Free"; + } + Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, @@ -949,9 +1065,14 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Messages/Free"; + } + Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, @@ -992,9 +1113,14 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Messages/Paid"; + } + Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, @@ -1010,9 +1136,14 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Messages/Paid"; + } + Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, @@ -1035,9 +1166,14 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPaidPost && messageInfo != null && messageInfo?.id is not null && messageInfo?.postedAt is not null) + { path = $"/Posts/Paid/{messageInfo.id} {messageInfo.postedAt.Value:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Posts/Paid"; + } + Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, @@ -1064,10 +1200,18 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Messages/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) @@ -1075,7 +1219,11 @@ public class DownloadService( List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1086,17 +1234,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1104,12 +1259,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1124,6 +1284,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1168,10 +1329,18 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Messages/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) @@ -1179,7 +1348,11 @@ public class DownloadService( List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1190,17 +1363,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1208,12 +1388,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1228,6 +1413,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1273,17 +1459,29 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Messages/Paid/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1294,17 +1492,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1312,12 +1517,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1332,6 +1542,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1376,17 +1587,29 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Messages/Paid/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1397,17 +1620,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1415,12 +1645,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1435,6 +1670,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1480,17 +1716,29 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) + { path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Posts/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1501,17 +1749,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1519,12 +1774,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1539,6 +1799,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1583,17 +1844,29 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) + { path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Posts/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1604,17 +1877,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1622,12 +1902,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1642,6 +1927,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1686,17 +1972,29 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.id is not null && streamInfo?.postedAt is not null) + { path = $"/Posts/Free/{streamInfo.id} {streamInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Posts/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && streamInfo != null && streamMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(streamInfo, streamMedia, author, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1707,17 +2005,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1725,12 +2030,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1745,6 +2055,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1789,10 +2100,18 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPaidPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) + { path = $"/Posts/Paid/{postInfo.id} {postInfo.postedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Posts/Paid/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) @@ -1800,7 +2119,11 @@ public class DownloadService( List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, fromUser, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1811,17 +2134,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1829,12 +2159,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1849,6 +2184,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1892,14 +2228,21 @@ public class DownloadService( Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; string path = "/Archived/Posts/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1910,17 +2253,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1928,12 +2278,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1948,6 +2303,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -2014,9 +2370,13 @@ public class DownloadService( bool isNew = await DownloadStoryMedia(highlightKVP.Value, path, highlightKVP.Key, "Stories", progressReporter); if (isNew) + { newHighlightsCount++; + } else + { oldHighlightsCount++; + } } Log.Debug( @@ -2062,9 +2422,13 @@ public class DownloadService( { bool isNew = await DownloadStoryMedia(storyKVP.Value, path, storyKVP.Key, "Stories", progressReporter); if (isNew) + { newStoriesCount++; + } else + { oldStoriesCount++; + } } Log.Debug($"Stories Already Downloaded: {oldStoriesCount} New Stories Downloaded: {newStoriesCount}"); @@ -2126,13 +2490,18 @@ public class DownloadService( "?type=widevine"); string decryptionKey; if (clientIdBlobMissing || devicePrivateKeyMissing) + { decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); + } else + { decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); + } + Archived.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.id == archivedKVP.Key); Archived.List? postInfo = @@ -2182,9 +2551,13 @@ public class DownloadService( } if (isNew) + { newArchivedCount++; + } else + { oldArchivedCount++; + } } Log.Debug( @@ -2246,13 +2619,18 @@ public class DownloadService( "?type=widevine"); string decryptionKey; if (clientIdBlobMissing || devicePrivateKeyMissing) + { decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); + } else + { decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); + } + Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); List? messageInfo = messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); @@ -2300,9 +2678,13 @@ public class DownloadService( } if (isNew) + { newMessagesCount++; + } else + { oldMessagesCount++; + } } Log.Debug($"Messages Already Downloaded: {oldMessagesCount} New Messages Downloaded: {newMessagesCount}"); @@ -2329,7 +2711,11 @@ public class DownloadService( Log.Debug("Found 0 Paid Messages"); return new DownloadResult { - TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Paid Messages", Success = true + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Paid Messages", + Success = true }; } @@ -2386,15 +2772,24 @@ public class DownloadService( string.Empty, messageInfo, mediaInfo, messageInfo?.fromUser, users); } - if (isNew) newCount++; - else oldCount++; + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } } Log.Debug($"Paid Messages Already Downloaded: {oldCount} New Paid Messages Downloaded: {newCount}"); return new DownloadResult { - TotalCount = paidMessageCollection.PaidMessages.Count, NewDownloads = newCount, - ExistingDownloads = oldCount, MediaType = "Paid Messages", Success = true + TotalCount = paidMessageCollection.PaidMessages.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Paid Messages", + Success = true }; } @@ -2408,7 +2803,13 @@ public class DownloadService( { Log.Debug("Found 0 Streams"); return new DownloadResult - { TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Streams", Success = true }; + { + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Streams", + Success = true + }; } Log.Debug($"Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams"); @@ -2462,15 +2863,24 @@ public class DownloadService( string.Empty, streamInfo, mediaInfo, streamInfo?.author, users); } - if (isNew) newCount++; - else oldCount++; + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } } Log.Debug($"Streams Already Downloaded: {oldCount} New Streams Downloaded: {newCount}"); return new DownloadResult { - TotalCount = streams.Streams.Count, NewDownloads = newCount, ExistingDownloads = oldCount, - MediaType = "Streams", Success = true + TotalCount = streams.Streams.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Streams", + Success = true }; } @@ -2486,7 +2896,13 @@ public class DownloadService( { Log.Debug("Found 0 Posts"); return new DownloadResult - { TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Posts", Success = true }; + { + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Posts", + Success = true + }; } Log.Debug($"Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts"); @@ -2537,14 +2953,23 @@ public class DownloadService( string.Empty, postInfo, mediaInfo, postInfo?.author, users); } - if (isNew) newCount++; - else oldCount++; + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } } Log.Debug($"Posts Already Downloaded: {oldCount} New Posts Downloaded: {newCount}"); return new DownloadResult { - TotalCount = posts.Posts.Count, NewDownloads = newCount, ExistingDownloads = oldCount, MediaType = "Posts", + TotalCount = posts.Posts.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Posts", Success = true }; } @@ -2559,7 +2984,13 @@ public class DownloadService( { Log.Debug("Found 0 Paid Posts"); return new DownloadResult - { TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Paid Posts", Success = true }; + { + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Paid Posts", + Success = true + }; } Log.Debug( @@ -2613,46 +3044,26 @@ public class DownloadService( string.Empty, postInfo, mediaInfo, postInfo?.fromUser, users); } - if (isNew) newCount++; - else oldCount++; + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } } Log.Debug($"Paid Posts Already Downloaded: {oldCount} New Paid Posts Downloaded: {newCount}"); return new DownloadResult { - TotalCount = purchasedPosts.PaidPosts.Count, NewDownloads = newCount, ExistingDownloads = oldCount, - MediaType = "Paid Posts", Success = true + TotalCount = purchasedPosts.PaidPosts.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Paid Posts", + Success = true }; } #endregion - - private static List CalculateFolderMD5(string folder) - { - List md5Hashes = new List(); - if (Directory.Exists(folder)) - { - string[] files = Directory.GetFiles(folder); - - foreach (string file in files) - { - md5Hashes.Add(CalculateMD5(file)); - } - } - - return md5Hashes; - } - - private static string CalculateMD5(string filePath) - { - using (var md5 = MD5.Create()) - { - using (var stream = File.OpenRead(filePath)) - { - byte[] hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - } - } diff --git a/OF DL/Services/FileNameService.cs b/OF DL/Services/FileNameService.cs index f97a1d5..a692874 100644 --- a/OF DL/Services/FileNameService.cs +++ b/OF DL/Services/FileNameService.cs @@ -1,181 +1,190 @@ -using HtmlAgilityPack; using System.Reflection; +using HtmlAgilityPack; -namespace OF_DL.Services +namespace OF_DL.Services; + +public class FileNameService(IAuthService authService) : IFileNameService { - public class FileNameService(IAuthService authService) : IFileNameService + public async Task> GetFilename(object obj1, object obj2, object obj3, + List selectedProperties, string username, Dictionary users = null) { - public async Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, string username, Dictionary users = null) + Dictionary values = new(); + Type type1 = obj1.GetType(); + Type type2 = obj2.GetType(); + PropertyInfo[] properties1 = type1.GetProperties(); + PropertyInfo[] properties2 = type2.GetProperties(); + + foreach (string propertyName in selectedProperties) { - Dictionary values = new(); - Type type1 = obj1.GetType(); - Type type2 = obj2.GetType(); - PropertyInfo[] properties1 = type1.GetProperties(); - PropertyInfo[] properties2 = type2.GetProperties(); - - foreach (string propertyName in selectedProperties) + if (propertyName.Contains("media")) { - if (propertyName.Contains("media")) + object drmProperty = null; + object fileProperty = GetNestedPropertyValue(obj2, "files"); + if (fileProperty != null) { - object drmProperty = null; - object fileProperty = GetNestedPropertyValue(obj2, "files"); - if(fileProperty != null) - { - drmProperty = GetNestedPropertyValue(obj2, "files.drm"); - } + drmProperty = GetNestedPropertyValue(obj2, "files.drm"); + } - if(fileProperty != null && drmProperty != null && propertyName == "mediaCreatedAt") + if (fileProperty != null && drmProperty != null && propertyName == "mediaCreatedAt") + { + object mpdurl = GetNestedPropertyValue(obj2, "files.drm.manifest.dash"); + object policy = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontPolicy"); + object signature = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontSignature"); + object kvp = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontKeyPairId"); + DateTime lastModified = + await DownloadService.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), + authService.CurrentAuth); + values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); + continue; + } + + if ((fileProperty == null || drmProperty == null) && propertyName == "mediaCreatedAt") + { + object source = GetNestedPropertyValue(obj2, "files.full.url"); + if (source != null) { - object mpdurl = GetNestedPropertyValue(obj2, "files.drm.manifest.dash"); - object policy = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontPolicy"); - object signature = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontSignature"); - object kvp = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontKeyPairId"); - DateTime lastModified = await DownloadService.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), authService.CurrentAuth); + DateTime lastModified = await DownloadService.GetMediaLastModified(source.ToString()); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); continue; } - else if((fileProperty == null || drmProperty == null) && propertyName == "mediaCreatedAt") + + object preview = GetNestedPropertyValue(obj2, "preview"); + if (preview != null) { - object source = GetNestedPropertyValue(obj2, "files.full.url"); - if(source != null) + DateTime lastModified = await DownloadService.GetMediaLastModified(preview.ToString()); + values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); + continue; + } + } + + PropertyInfo? property = Array.Find(properties2, + p => p.Name.Equals(propertyName.Replace("media", ""), StringComparison.OrdinalIgnoreCase)); + if (property != null) + { + object? propertyValue = property.GetValue(obj2); + if (propertyValue != null) + { + if (propertyValue is DateTime dateTimeValue) { - DateTime lastModified = await DownloadService.GetMediaLastModified(source.ToString()); - values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); - continue; + values.Add(propertyName, dateTimeValue.ToString("yyyy-MM-dd")); } else { - object preview = GetNestedPropertyValue(obj2, "preview"); - if(preview != null) - { - DateTime lastModified = await DownloadService.GetMediaLastModified(preview.ToString()); - values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); - continue; - } - } - - } - PropertyInfo? property = Array.Find(properties2, p => p.Name.Equals(propertyName.Replace("media", ""), StringComparison.OrdinalIgnoreCase)); - if (property != null) - { - object? propertyValue = property.GetValue(obj2); - if (propertyValue != null) - { - if (propertyValue is DateTime dateTimeValue) - { - values.Add(propertyName, dateTimeValue.ToString("yyyy-MM-dd")); - } - else - { - values.Add(propertyName, propertyValue.ToString()); - } + values.Add(propertyName, propertyValue.ToString()); } } } - else if (propertyName.Contains("filename")) + } + else if (propertyName.Contains("filename")) + { + string sourcePropertyPath = "files.full.url"; + object sourcePropertyValue = GetNestedPropertyValue(obj2, sourcePropertyPath); + if (sourcePropertyValue != null) { - string sourcePropertyPath = "files.full.url"; - object sourcePropertyValue = GetNestedPropertyValue(obj2, sourcePropertyPath); - if (sourcePropertyValue != null) - { - Uri uri = new(sourcePropertyValue.ToString()); - string filename = System.IO.Path.GetFileName(uri.LocalPath); - values.Add(propertyName, filename.Split(".")[0]); - } - else - { - string propertyPath = "files.drm.manifest.dash"; - object nestedPropertyValue = GetNestedPropertyValue(obj2, propertyPath); - if (nestedPropertyValue != null) - { - Uri uri = new(nestedPropertyValue.ToString()); - string filename = System.IO.Path.GetFileName(uri.LocalPath); - values.Add(propertyName, filename.Split(".")[0] + "_source"); - } - } - } - else if (propertyName.Contains("username")) - { - if(!string.IsNullOrEmpty(username)) - { - values.Add(propertyName, username); - } - else - { - string propertyPath = "id"; - object nestedPropertyValue = GetNestedPropertyValue(obj3, propertyPath); - if (nestedPropertyValue != null) - { - values.Add(propertyName, users.FirstOrDefault(u => u.Value == Convert.ToInt32(nestedPropertyValue.ToString())).Key); - } - } - } - else if (propertyName.Contains("text", StringComparison.OrdinalIgnoreCase)) - { - PropertyInfo property = Array.Find(properties1, p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); - if (property != null) - { - object propertyValue = property.GetValue(obj1); - if (propertyValue != null) - { - var pageDoc = new HtmlDocument(); - pageDoc.LoadHtml(propertyValue.ToString()); - var str = pageDoc.DocumentNode.InnerText; - if (str.Length > 100) // todo: add length limit to config - str = str.Substring(0, 100); - values.Add(propertyName, str); - } - } + Uri uri = new(sourcePropertyValue.ToString()); + string filename = Path.GetFileName(uri.LocalPath); + values.Add(propertyName, filename.Split(".")[0]); } else { - PropertyInfo property = Array.Find(properties1, p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); - if (property != null) + string propertyPath = "files.drm.manifest.dash"; + object nestedPropertyValue = GetNestedPropertyValue(obj2, propertyPath); + if (nestedPropertyValue != null) { - object propertyValue = property.GetValue(obj1); - if (propertyValue != null) + Uri uri = new(nestedPropertyValue.ToString()); + string filename = Path.GetFileName(uri.LocalPath); + values.Add(propertyName, filename.Split(".")[0] + "_source"); + } + } + } + else if (propertyName.Contains("username")) + { + if (!string.IsNullOrEmpty(username)) + { + values.Add(propertyName, username); + } + else + { + string propertyPath = "id"; + object nestedPropertyValue = GetNestedPropertyValue(obj3, propertyPath); + if (nestedPropertyValue != null) + { + values.Add(propertyName, + users.FirstOrDefault(u => u.Value == Convert.ToInt32(nestedPropertyValue.ToString())).Key); + } + } + } + else if (propertyName.Contains("text", StringComparison.OrdinalIgnoreCase)) + { + PropertyInfo property = Array.Find(properties1, + p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); + if (property != null) + { + object propertyValue = property.GetValue(obj1); + if (propertyValue != null) + { + HtmlDocument pageDoc = new(); + pageDoc.LoadHtml(propertyValue.ToString()); + string str = pageDoc.DocumentNode.InnerText; + if (str.Length > 100) // todo: add length limit to config { - if (propertyValue is DateTime dateTimeValue) - { - values.Add(propertyName, dateTimeValue.ToString("yyyy-MM-dd")); - } - else - { - values.Add(propertyName, propertyValue.ToString()); - } + str = str.Substring(0, 100); + } + + values.Add(propertyName, str); + } + } + } + else + { + PropertyInfo property = Array.Find(properties1, + p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); + if (property != null) + { + object propertyValue = property.GetValue(obj1); + if (propertyValue != null) + { + if (propertyValue is DateTime dateTimeValue) + { + values.Add(propertyName, dateTimeValue.ToString("yyyy-MM-dd")); + } + else + { + values.Add(propertyName, propertyValue.ToString()); } } } } - return values; - } - - static object GetNestedPropertyValue(object source, string propertyPath) - { - object value = source; - foreach (var propertyName in propertyPath.Split('.')) - { - PropertyInfo property = value.GetType().GetProperty(propertyName) ?? throw new ArgumentException($"Property '{propertyName}' not found."); - value = property.GetValue(value); - } - return value; - } - - public async Task BuildFilename(string fileFormat, Dictionary values) - { - foreach (var kvp in values) - { - string placeholder = "{" + kvp.Key + "}"; - fileFormat = fileFormat.Replace(placeholder, kvp.Value); - } - - return RemoveInvalidFileNameChars($"{fileFormat}"); - } - - private static string RemoveInvalidFileNameChars(string fileName) - { - return string.IsNullOrEmpty(fileName) ? fileName : string.Concat(fileName.Split(Path.GetInvalidFileNameChars())); } + return values; } + + public async Task BuildFilename(string fileFormat, Dictionary values) + { + foreach (KeyValuePair kvp in values) + { + string placeholder = "{" + kvp.Key + "}"; + fileFormat = fileFormat.Replace(placeholder, kvp.Value); + } + + return RemoveInvalidFileNameChars($"{fileFormat}"); + } + + private static object GetNestedPropertyValue(object source, string propertyPath) + { + object value = source; + foreach (string propertyName in propertyPath.Split('.')) + { + PropertyInfo property = value.GetType().GetProperty(propertyName) ?? + throw new ArgumentException($"Property '{propertyName}' not found."); + value = property.GetValue(value); + } + + return value; + } + + private static string RemoveInvalidFileNameChars(string fileName) => string.IsNullOrEmpty(fileName) + ? fileName + : string.Concat(fileName.Split(Path.GetInvalidFileNameChars())); } diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index a24c088..3163062 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -8,32 +8,36 @@ using OF_DL.Entities.Streams; using OF_DL.Enumerations; using Spectre.Console; -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IAPIService { - public interface IAPIService - { - Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, string pssh); - Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, string pssh); - Task GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp); - Task GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp); - Task> GetLists(string endpoint); - Task> GetListUsers(string endpoint); - Task> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, List paid_post_ids); - Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, StatusContext ctx); - Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx); - Task GetPost(string endpoint, string folder); - Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx); - Task GetArchived(string endpoint, string folder, StatusContext ctx); - Task GetMessages(string endpoint, string folder, StatusContext ctx); - Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx); - Task GetPaidMessage(string endpoint, string folder); - Task> GetPurchasedTabUsers(string endpoint, Dictionary users); - Task> GetPurchasedTab(string endpoint, string folder, Dictionary users); - Task GetUserInfo(string endpoint); - Task GetUserInfoById(string endpoint); - Dictionary GetDynamicHeaders(string path, string queryParam); - Task> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions); - Task> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions); - Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, string pssh); - } + Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, string pssh); + Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, string pssh); + Task GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp); + Task GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp); + Task> GetLists(string endpoint); + Task> GetListUsers(string endpoint); + + Task> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, + List paid_post_ids); + + Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, + StatusContext ctx); + + Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx); + Task GetPost(string endpoint, string folder); + Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx); + Task GetArchived(string endpoint, string folder, StatusContext ctx); + Task GetMessages(string endpoint, string folder, StatusContext ctx); + Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx); + Task GetPaidMessage(string endpoint, string folder); + Task> GetPurchasedTabUsers(string endpoint, Dictionary users); + Task> GetPurchasedTab(string endpoint, string folder, Dictionary users); + Task GetUserInfo(string endpoint); + Task GetUserInfoById(string endpoint); + Dictionary GetDynamicHeaders(string path, string queryParam); + Task> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions); + Task> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions); + Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, string pssh); } diff --git a/OF DL/Services/IAuthService.cs b/OF DL/Services/IAuthService.cs index 8d14486..e79ad43 100644 --- a/OF DL/Services/IAuthService.cs +++ b/OF DL/Services/IAuthService.cs @@ -1,12 +1,11 @@ using OF_DL.Entities; -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IAuthService { - public interface IAuthService - { - Auth? CurrentAuth { get; set; } - Task LoadFromFileAsync(string filePath = "auth.json"); - Task LoadFromBrowserAsync(); - Task SaveToFileAsync(string filePath = "auth.json"); - } + Auth? CurrentAuth { get; set; } + Task LoadFromFileAsync(string filePath = "auth.json"); + Task LoadFromBrowserAsync(); + Task SaveToFileAsync(string filePath = "auth.json"); } diff --git a/OF DL/Services/IConfigService.cs b/OF DL/Services/IConfigService.cs index 57c639a..fd7c35d 100644 --- a/OF DL/Services/IConfigService.cs +++ b/OF DL/Services/IConfigService.cs @@ -1,13 +1,12 @@ using OF_DL.Entities; -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IConfigService { - public interface IConfigService - { - Config CurrentConfig { get; } - bool IsCliNonInteractive { get; } - Task LoadConfigurationAsync(string[] args); - Task SaveConfigurationAsync(string filePath = "config.conf"); - void UpdateConfig(Config newConfig); - } + Config CurrentConfig { get; } + bool IsCliNonInteractive { get; } + Task LoadConfigurationAsync(string[] args); + Task SaveConfigurationAsync(string filePath = "config.conf"); + void UpdateConfig(Config newConfig); } diff --git a/OF DL/Services/IDBService.cs b/OF DL/Services/IDBService.cs index 5a32c03..9a14484 100644 --- a/OF DL/Services/IDBService.cs +++ b/OF DL/Services/IDBService.cs @@ -1,17 +1,27 @@ -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IDBService { - public interface IDBService - { - Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at, long user_id); - Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at); - Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at); - Task CreateDB(string folder); - Task CreateUsersDB(Dictionary users); - Task CheckUsername(KeyValuePair user, string path); - Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, string? filename, long? size, string api_type, string media_type, bool preview, bool downloaded, DateTime? created_at); - Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, long size, bool downloaded, DateTime created_at); - Task GetStoredFileSize(string folder, long media_id, string api_type); - Task CheckDownloaded(string folder, long media_id, string api_type); - Task GetMostRecentPostDate(string folder); - } + Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, + DateTime created_at, long user_id); + + Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, + DateTime created_at); + + Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, + DateTime created_at); + + Task CreateDB(string folder); + Task CreateUsersDB(Dictionary users); + Task CheckUsername(KeyValuePair user, string path); + + Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, string? filename, + long? size, string api_type, string media_type, bool preview, bool downloaded, DateTime? created_at); + + Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, long size, + bool downloaded, DateTime created_at); + + Task GetStoredFileSize(string folder, long media_id, string api_type); + Task CheckDownloaded(string folder, long media_id, string api_type); + Task GetMostRecentPostDate(string folder); } diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index 696eaee..11d7c96 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -7,44 +7,128 @@ using OF_DL.Entities.Streams; using static OF_DL.Entities.Messages.Messages; using FromUser = OF_DL.Entities.Messages.FromUser; -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IDownloadService { - public interface IDownloadService - { - Task CalculateTotalFileSize(List urls); - Task ProcessMediaDownload(string folder, long media_id, string api_type, string url, string path, string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); - Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Archived.List? messageInfo, Archived.Medium? messageMedia, Archived.Author? author, Dictionary users); - Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Archived.List? postInfo, Archived.Medium? postMedia, Archived.Author? author, Dictionary users); - Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, Dictionary users); - Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary users); - Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username); - Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Messages.List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users); - Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Messages.List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users); - Task DownloadPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary users); - Task DownloadPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, SinglePost.Author? author, Dictionary users); - Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); - Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary users); - Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); - Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary users); - Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, Medium? postMedia, Purchased.FromUser? fromUser, Dictionary users); - Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); - Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter); - Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary users); - Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary users); - Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, - FromUser? fromUser, Dictionary users); - Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, - FromUser? fromUser, Dictionary users); - Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); - Task DownloadStories(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); - Task DownloadArchived(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedCollection archived, IProgressReporter progressReporter); - Task DownloadMessages(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageCollection messages, IProgressReporter progressReporter); - Task DownloadPaidMessages(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter); - Task DownloadStreams(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamsCollection streams, IProgressReporter progressReporter); - Task DownloadFreePosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostCollection posts, IProgressReporter progressReporter); - Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidPostCollection purchasedPosts, IProgressReporter progressReporter); - } + Task CalculateTotalFileSize(List urls); + + Task ProcessMediaDownload(string folder, long media_id, string api_type, string url, string path, + string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); + + Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Archived.List? messageInfo, + Archived.Medium? messageMedia, Archived.Author? author, Dictionary users); + + Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Archived.List? postInfo, Archived.Medium? postMedia, + Archived.Author? author, Dictionary users); + + Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, + string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, + string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, + Dictionary users); + + Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, + string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, + string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, + Dictionary users); + + Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username); + + Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, + string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, + string? filenameFormat, List? messageInfo, Medium? messageMedia, Messages.FromUser? fromUser, + Dictionary users); + + Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, + Messages.FromUser? fromUser, Dictionary users); + + Task DownloadPostMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, + Post.Author? author, Dictionary users); + + Task DownloadPostMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, + SinglePost.Author? author, Dictionary users); + + Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + Purchased.FromUser? fromUser, Dictionary users); + + Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users); + + Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + Purchased.FromUser? fromUser, Dictionary users); + + Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users); + + Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, Medium? postMedia, + Purchased.FromUser? fromUser, Dictionary users); + + Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + Purchased.FromUser? fromUser, Dictionary users); + + Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter); + + Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, + Streams.Medium? streamMedia, Streams.Author? author, Dictionary users); + + Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, + string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, + string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, + Dictionary users); + + Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users); + + Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users); + + Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, + IProgressReporter progressReporter); + + Task DownloadStories(string username, long userId, string path, HashSet paidPostIds, + IProgressReporter progressReporter); + + Task DownloadArchived(string username, long userId, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedCollection archived, + IProgressReporter progressReporter); + + Task DownloadMessages(string username, long userId, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageCollection messages, + IProgressReporter progressReporter); + + Task DownloadPaidMessages(string username, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidMessageCollection paidMessageCollection, + IProgressReporter progressReporter); + + Task DownloadStreams(string username, long userId, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamsCollection streams, + IProgressReporter progressReporter); + + Task DownloadFreePosts(string username, long userId, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostCollection posts, + IProgressReporter progressReporter); + + Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidPostCollection purchasedPosts, + IProgressReporter progressReporter); } diff --git a/OF DL/Services/IFileNameService.cs b/OF DL/Services/IFileNameService.cs index d692f30..4e4a026 100644 --- a/OF DL/Services/IFileNameService.cs +++ b/OF DL/Services/IFileNameService.cs @@ -1,8 +1,9 @@ -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IFileNameService { - public interface IFileNameService - { - Task BuildFilename(string fileFormat, Dictionary values); - Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, string username, Dictionary users = null); - } + Task BuildFilename(string fileFormat, Dictionary values); + + Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, + string username, Dictionary users = null); } diff --git a/OF DL/Services/ILoggingService.cs b/OF DL/Services/ILoggingService.cs index b0516ef..7c6a8a3 100644 --- a/OF DL/Services/ILoggingService.cs +++ b/OF DL/Services/ILoggingService.cs @@ -1,12 +1,11 @@ using OF_DL.Enumerations; using Serilog.Core; -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface ILoggingService { - public interface ILoggingService - { - LoggingLevelSwitch LevelSwitch { get; } - void UpdateLoggingLevel(LoggingLevel newLevel); - LoggingLevel GetCurrentLoggingLevel(); - } + LoggingLevelSwitch LevelSwitch { get; } + void UpdateLoggingLevel(LoggingLevel newLevel); + LoggingLevel GetCurrentLoggingLevel(); } diff --git a/OF DL/Services/IProgressReporter.cs b/OF DL/Services/IProgressReporter.cs index d312059..d36d1f3 100644 --- a/OF DL/Services/IProgressReporter.cs +++ b/OF DL/Services/IProgressReporter.cs @@ -1,19 +1,19 @@ namespace OF_DL.Services; /// -/// Interface for reporting download progress in a UI-agnostic way. -/// This allows the download service to report progress without being coupled to any specific UI framework. +/// Interface for reporting download progress in a UI-agnostic way. +/// This allows the download service to report progress without being coupled to any specific UI framework. /// public interface IProgressReporter { /// - /// Reports progress increment. The value represents either bytes downloaded or file count depending on configuration. + /// Reports progress increment. The value represents either bytes downloaded or file count depending on configuration. /// /// The amount to increment progress by void ReportProgress(long increment); /// - /// Reports a status message (optional for implementations). + /// Reports a status message (optional for implementations). /// /// The status message to report void ReportStatus(string message); diff --git a/OF DL/Services/LoggingService.cs b/OF DL/Services/LoggingService.cs index c65511a..b4c47fa 100644 --- a/OF DL/Services/LoggingService.cs +++ b/OF DL/Services/LoggingService.cs @@ -3,40 +3,36 @@ using Serilog; using Serilog.Core; using Serilog.Events; -namespace OF_DL.Services +namespace OF_DL.Services; + +public class LoggingService : ILoggingService { - public class LoggingService : ILoggingService + public LoggingService() { - public LoggingLevelSwitch LevelSwitch { get; } + LevelSwitch = new LoggingLevelSwitch(); + InitializeLogger(); + } - public LoggingService() - { - LevelSwitch = new LoggingLevelSwitch(); - InitializeLogger(); - } + public LoggingLevelSwitch LevelSwitch { get; } - private void InitializeLogger() - { - // Set the initial level to Error (until we've read from config) - LevelSwitch.MinimumLevel = LogEventLevel.Error; + public void UpdateLoggingLevel(LoggingLevel newLevel) + { + LevelSwitch.MinimumLevel = (LogEventLevel)newLevel; + Log.Debug("Logging level updated to: {LoggingLevel}", newLevel); + } - Log.Logger = new LoggerConfiguration() - .MinimumLevel.ControlledBy(LevelSwitch) - .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day) - .CreateLogger(); + public LoggingLevel GetCurrentLoggingLevel() => (LoggingLevel)LevelSwitch.MinimumLevel; - Log.Debug("Logging service initialized"); - } + private void InitializeLogger() + { + // Set the initial level to Error (until we've read from config) + LevelSwitch.MinimumLevel = LogEventLevel.Error; - public void UpdateLoggingLevel(LoggingLevel newLevel) - { - LevelSwitch.MinimumLevel = (LogEventLevel)newLevel; - Log.Debug("Logging level updated to: {LoggingLevel}", newLevel); - } + Log.Logger = new LoggerConfiguration() + .MinimumLevel.ControlledBy(LevelSwitch) + .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); - public LoggingLevel GetCurrentLoggingLevel() - { - return (LoggingLevel)LevelSwitch.MinimumLevel; - } + Log.Debug("Logging service initialized"); } } diff --git a/OF DL/Utils/HttpUtil.cs b/OF DL/Utils/HttpUtil.cs index a239547..eaa7ebb 100644 --- a/OF DL/Utils/HttpUtil.cs +++ b/OF DL/Utils/HttpUtil.cs @@ -1,146 +1,141 @@ -using OF_DL.Helpers; +using System.Net; using System.Text; +using OF_DL.Helpers; -namespace OF_DL.Utils +namespace OF_DL.Utils; + +internal class HttpUtil { - class HttpUtil + public static HttpClient Client { get; set; } = new(new HttpClientHandler { - public static HttpClient Client { get; set; } = new HttpClient(new HttpClientHandler + AllowAutoRedirect = true + //Proxy = null + }); + + public static async Task PostData(string URL, Dictionary headers, string postData) + { + string mediaType = postData.StartsWith("{") ? "application/json" : "application/x-www-form-urlencoded"; + HttpResponseMessage response = await PerformOperation(async () => { - AllowAutoRedirect = true, - //Proxy = null + StringContent content = new(postData, Encoding.UTF8, mediaType); + //ByteArrayContent content = new ByteArrayContent(postData); + + return await Post(URL, headers, content); }); - public static async Task PostData(string URL, Dictionary headers, string postData) + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + return bytes; + } + + public static async Task PostData(string URL, Dictionary headers, byte[] postData) + { + HttpResponseMessage response = await PerformOperation(async () => { - var mediaType = postData.StartsWith("{") ? "application/json" : "application/x-www-form-urlencoded"; - var response = await PerformOperation(async () => + ByteArrayContent content = new(postData); + + return await Post(URL, headers, content); + }); + + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + return bytes; + } + + public static async Task PostData(string URL, Dictionary headers, + Dictionary postData) + { + HttpResponseMessage response = await PerformOperation(async () => + { + FormUrlEncodedContent content = new(postData); + + return await Post(URL, headers, content); + }); + + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + return bytes; + } + + public static async Task GetWebSource(string URL, Dictionary headers = null) + { + HttpResponseMessage response = await PerformOperation(async () => { return await Get(URL, headers); }); + + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + return Encoding.UTF8.GetString(bytes); + } + + public static async Task GetBinary(string URL, Dictionary headers = null) + { + HttpResponseMessage response = await PerformOperation(async () => { return await Get(URL, headers); }); + + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + return bytes; + } + + public static string GetString(byte[] bytes) => Encoding.UTF8.GetString(bytes); + + private static async Task Get(string URL, Dictionary headers = null) + { + HttpRequestMessage request = new() { RequestUri = new Uri(URL), Method = HttpMethod.Get }; + + if (headers != null) + { + foreach (KeyValuePair header in headers) { - StringContent content = new StringContent(postData, Encoding.UTF8, mediaType); - //ByteArrayContent content = new ByteArrayContent(postData); - - return await Post(URL, headers, content); - }); - - byte[] bytes = await response.Content.ReadAsByteArrayAsync(); - return bytes; + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } } - public static async Task PostData(string URL, Dictionary headers, byte[] postData) + return await Send(request); + } + + private static async Task Post(string URL, Dictionary headers, + HttpContent content) + { + HttpRequestMessage request = new() { RequestUri = new Uri(URL), Method = HttpMethod.Post, Content = content }; + + if (headers != null) { - var response = await PerformOperation(async () => + foreach (KeyValuePair header in headers) { - ByteArrayContent content = new ByteArrayContent(postData); - - return await Post(URL, headers, content); - }); - - byte[] bytes = await response.Content.ReadAsByteArrayAsync(); - return bytes; + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } } - public static async Task PostData(string URL, Dictionary headers, Dictionary postData) + return await Send(request); + } + + private static async Task Send(HttpRequestMessage request) => await Client.SendAsync(request); + + private static async Task PerformOperation(Func> operation) + { + HttpResponseMessage response = await operation(); + + int retryCount = 0; + + while (retryCount < Constants.WIDEVINE_MAX_RETRIES && response.StatusCode == HttpStatusCode.TooManyRequests) { - var response = await PerformOperation(async () => + // + // We've hit a rate limit, so we should wait before retrying. + // + int retryAfterSeconds = + Constants.WIDEVINE_RETRY_DELAY * (retryCount + 1); // Default retry time. Increases with each retry. + if (response.Headers.RetryAfter != null && response.Headers.RetryAfter.Delta.HasValue) { - FormUrlEncodedContent content = new FormUrlEncodedContent(postData); - - return await Post(URL, headers, content); - }); - - byte[] bytes = await response.Content.ReadAsByteArrayAsync(); - return bytes; - } - - public static async Task GetWebSource(string URL, Dictionary headers = null) - { - var response = await PerformOperation(async () => - { - return await Get(URL, headers); - }); - - byte[] bytes = await response.Content.ReadAsByteArrayAsync(); - return Encoding.UTF8.GetString(bytes); - } - - public static async Task GetBinary(string URL, Dictionary headers = null) - { - var response = await PerformOperation(async () => - { - return await Get(URL, headers); - }); - - byte[] bytes = await response.Content.ReadAsByteArrayAsync(); - return bytes; - } - public static string GetString(byte[] bytes) - { - return Encoding.UTF8.GetString(bytes); - } - - private static async Task Get(string URL, Dictionary headers = null) - { - HttpRequestMessage request = new HttpRequestMessage() - { - RequestUri = new Uri(URL), - Method = HttpMethod.Get - }; - - if (headers != null) - foreach (KeyValuePair header in headers) - request.Headers.TryAddWithoutValidation(header.Key, header.Value); - - return await Send(request); - } - - private static async Task Post(string URL, Dictionary headers, HttpContent content) - { - HttpRequestMessage request = new HttpRequestMessage() - { - RequestUri = new Uri(URL), - Method = HttpMethod.Post, - Content = content - }; - - if (headers != null) - foreach (KeyValuePair header in headers) - request.Headers.TryAddWithoutValidation(header.Key, header.Value); - - return await Send(request); - } - - private static async Task Send(HttpRequestMessage request) - { - return await Client.SendAsync(request); - } - - private static async Task PerformOperation(Func> operation) - { - var response = await operation(); - - var retryCount = 0; - - while (retryCount < Constants.WIDEVINE_MAX_RETRIES && response.StatusCode == System.Net.HttpStatusCode.TooManyRequests) - { - // - // We've hit a rate limit, so we should wait before retrying. - // - var retryAfterSeconds = Constants.WIDEVINE_RETRY_DELAY * (retryCount + 1); // Default retry time. Increases with each retry. - if (response.Headers.RetryAfter != null && response.Headers.RetryAfter.Delta.HasValue) + if (response.Headers.RetryAfter.Delta.Value.TotalSeconds > 0) { - if (response.Headers.RetryAfter.Delta.Value.TotalSeconds > 0) - retryAfterSeconds = (int)response.Headers.RetryAfter.Delta.Value.TotalSeconds + 1; // Add 1 second to ensure we wait a bit longer than the suggested time + retryAfterSeconds = + (int)response.Headers.RetryAfter.Delta.Value.TotalSeconds + + 1; // Add 1 second to ensure we wait a bit longer than the suggested time } - - await Task.Delay(retryAfterSeconds * 1000); // Peform the delay - - response = await operation(); - retryCount++; } - response.EnsureSuccessStatusCode(); // Throw an exception if the response is not successful + await Task.Delay(retryAfterSeconds * 1000); // Peform the delay - return response; + response = await operation(); + retryCount++; } + + response.EnsureSuccessStatusCode(); // Throw an exception if the response is not successful + + return response; } } diff --git a/OF DL/Utils/ThrottledStream.cs b/OF DL/Utils/ThrottledStream.cs index b52b413..b53b712 100644 --- a/OF DL/Utils/ThrottledStream.cs +++ b/OF DL/Utils/ThrottledStream.cs @@ -2,15 +2,14 @@ using System.Reactive.Concurrency; namespace OF_DL.Utils; - public class ThrottledStream : Stream { - private readonly Stream parent; private readonly int maxBytesPerSecond; + private readonly Stream parent; private readonly IScheduler scheduler; - private readonly IStopwatch stopwatch; private readonly bool shouldThrottle; + private readonly IStopwatch stopwatch; private long processed; @@ -30,16 +29,39 @@ public class ThrottledStream : Stream } + public override bool CanRead => parent.CanRead; + + + public override bool CanSeek => parent.CanSeek; + + + public override bool CanWrite => parent.CanWrite; + + + public override long Length => parent.Length; + + + public override long Position + { + get => parent.Position; + set => parent.Position = value; + } + + protected void Throttle(int bytes) { - if (!shouldThrottle) return; + if (!shouldThrottle) + { + return; + } + processed += bytes; - var targetTime = TimeSpan.FromSeconds((double)processed / maxBytesPerSecond); - var actualTime = stopwatch.Elapsed; - var sleep = targetTime - actualTime; + TimeSpan targetTime = TimeSpan.FromSeconds((double)processed / maxBytesPerSecond); + TimeSpan actualTime = stopwatch.Elapsed; + TimeSpan sleep = targetTime - actualTime; if (sleep > TimeSpan.Zero) { - using var waitHandle = new AutoResetEvent(initialState: false); + using AutoResetEvent waitHandle = new(false); scheduler.Sleep(sleep).GetAwaiter().OnCompleted(() => waitHandle.Set()); waitHandle.WaitOne(); } @@ -47,11 +69,15 @@ public class ThrottledStream : Stream protected async Task ThrottleAsync(int bytes) { - if (!shouldThrottle) return; + if (!shouldThrottle) + { + return; + } + processed += bytes; - var targetTime = TimeSpan.FromSeconds((double)processed / maxBytesPerSecond); - var actualTime = stopwatch.Elapsed; - var sleep = targetTime - actualTime; + TimeSpan targetTime = TimeSpan.FromSeconds((double)processed / maxBytesPerSecond); + TimeSpan actualTime = stopwatch.Elapsed; + TimeSpan sleep = targetTime - actualTime; if (sleep > TimeSpan.Zero) { @@ -61,7 +87,7 @@ public class ThrottledStream : Stream public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - var read = await parent.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); + int read = await parent.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); await ThrottleAsync(read).ConfigureAwait(false); return read; } @@ -79,71 +105,26 @@ public class ThrottledStream : Stream await parent.WriteAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); } - public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, + CancellationToken cancellationToken = default) { await ThrottleAsync(buffer.Length).ConfigureAwait(false); await parent.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); } - public override bool CanRead - { - get { return parent.CanRead; } - } - - - public override bool CanSeek - { - get { return parent.CanSeek; } - } - - - public override bool CanWrite - { - get { return parent.CanWrite; } - } - - - public override void Flush() - { - parent.Flush(); - } - - - public override long Length - { - get { return parent.Length; } - } - - - public override long Position - { - get - { - return parent.Position; - } - set - { - parent.Position = value; - } - } + public override void Flush() => parent.Flush(); public override int Read(byte[] buffer, int offset, int count) { - var read = parent.Read(buffer, offset, count); + int read = parent.Read(buffer, offset, count); Throttle(read); return read; } - public override long Seek(long offset, SeekOrigin origin) - { - return parent.Seek(offset, origin); - } + public override long Seek(long offset, SeekOrigin origin) => parent.Seek(offset, origin); - public override void SetLength(long value) - { - parent.SetLength(value); - } + public override void SetLength(long value) => parent.SetLength(value); public override void Write(byte[] buffer, int offset, int count) { diff --git a/OF DL/Utils/XmlUtils.cs b/OF DL/Utils/XmlUtils.cs index 940ebef..eef2073 100644 --- a/OF DL/Utils/XmlUtils.cs +++ b/OF DL/Utils/XmlUtils.cs @@ -1,28 +1,28 @@ using System.Xml.Linq; -namespace OF_DL.Utils +namespace OF_DL.Utils; + +internal static class XmlUtils { - internal static class XmlUtils + // When true, return original text without parsing/stripping. + public static bool Passthrough { get; set; } = false; + + public static string EvaluateInnerText(string xmlValue) { - // When true, return original text without parsing/stripping. - public static bool Passthrough { get; set; } = false; - - public static string EvaluateInnerText(string xmlValue) + if (Passthrough) { - if (Passthrough) - { - return xmlValue ?? string.Empty; - } - - try - { - var parsedText = XElement.Parse($"{xmlValue}"); - return parsedText.Value; - } - catch - { } - - return string.Empty; + return xmlValue ?? string.Empty; } + + try + { + XElement parsedText = XElement.Parse($"{xmlValue}"); + return parsedText.Value; + } + catch + { + } + + return string.Empty; } } diff --git a/OF DL/Widevine/CDM.cs b/OF DL/Widevine/CDM.cs index ffe25cc..d005cd5 100644 --- a/OF DL/Widevine/CDM.cs +++ b/OF DL/Widevine/CDM.cs @@ -4,579 +4,571 @@ using System.Text; using OF_DL.Crypto; using ProtoBuf; -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +public class CDM { - public class CDM + private static Dictionary Devices { get; } = new() { - static Dictionary Devices { get; } = new Dictionary() + [Constants.DEVICE_NAME] = new CDMDevice(Constants.DEVICE_NAME) + }; + + private static Dictionary Sessions { get; } = new(); + + private static byte[] CheckPSSH(string psshB64) + { + byte[] systemID = new byte[] { 237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237 }; + + if (psshB64.Length % 4 != 0) { - [Constants.DEVICE_NAME] = new CDMDevice(Constants.DEVICE_NAME, null, null, null) - }; - static Dictionary Sessions { get; set; } = new Dictionary(); - - static byte[] CheckPSSH(string psshB64) - { - byte[] systemID = new byte[] { 237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237 }; - - if (psshB64.Length % 4 != 0) - { - psshB64 = psshB64.PadRight(psshB64.Length + (4 - (psshB64.Length % 4)), '='); - } - - byte[] pssh = Convert.FromBase64String(psshB64); - - if (pssh.Length < 30) - return pssh; - - if (!pssh[12..28].SequenceEqual(systemID)) - { - List newPssh = new List() { 0, 0, 0 }; - newPssh.Add((byte)(32 + pssh.Length)); - newPssh.AddRange(Encoding.UTF8.GetBytes("pssh")); - newPssh.AddRange(new byte[] { 0, 0, 0, 0 }); - newPssh.AddRange(systemID); - newPssh.AddRange(new byte[] { 0, 0, 0, 0 }); - newPssh[31] = (byte)(pssh.Length); - newPssh.AddRange(pssh); - - return newPssh.ToArray(); - } - else - { - return pssh; - } + psshB64 = psshB64.PadRight(psshB64.Length + (4 - psshB64.Length % 4), '='); } - public static string OpenSession(string initDataB64, string deviceName, bool offline = false, bool raw = false) + byte[] pssh = Convert.FromBase64String(psshB64); + + if (pssh.Length < 30) { - byte[] initData = CheckPSSH(initDataB64); + return pssh; + } - var device = Devices[deviceName]; + if (!pssh[12..28].SequenceEqual(systemID)) + { + List newPssh = new() { 0, 0, 0 }; + newPssh.Add((byte)(32 + pssh.Length)); + newPssh.AddRange(Encoding.UTF8.GetBytes("pssh")); + newPssh.AddRange(new byte[] { 0, 0, 0, 0 }); + newPssh.AddRange(systemID); + newPssh.AddRange(new byte[] { 0, 0, 0, 0 }); + newPssh[31] = (byte)pssh.Length; + newPssh.AddRange(pssh); - byte[] sessionId = new byte[16]; + return newPssh.ToArray(); + } - if (device.IsAndroid) + return pssh; + } + + public static string OpenSession(string initDataB64, string deviceName, bool offline = false, bool raw = false) + { + byte[] initData = CheckPSSH(initDataB64); + + CDMDevice device = Devices[deviceName]; + + byte[] sessionId = new byte[16]; + + if (device.IsAndroid) + { + string randHex = ""; + + Random rand = new(); + string choice = "ABCDEF0123456789"; + for (int i = 0; i < 16; i++) { - string randHex = ""; - - Random rand = new Random(); - string choice = "ABCDEF0123456789"; - for (int i = 0; i < 16; i++) - randHex += choice[rand.Next(16)]; - - string counter = "01"; - string rest = "00000000000000"; - sessionId = Encoding.ASCII.GetBytes(randHex + counter + rest); - } - else - { - Random rand = new Random(); - rand.NextBytes(sessionId); + randHex += choice[rand.Next(16)]; } - Session session; - dynamic parsedInitData = ParseInitData(initData); + string counter = "01"; + string rest = "00000000000000"; + sessionId = Encoding.ASCII.GetBytes(randHex + counter + rest); + } + else + { + Random rand = new(); + rand.NextBytes(sessionId); + } - if (parsedInitData != null) + Session session; + dynamic parsedInitData = ParseInitData(initData); + + if (parsedInitData != null) + { + session = new Session(sessionId, parsedInitData, device, offline); + } + else if (raw) + { + session = new Session(sessionId, initData, device, offline); + } + else + { + return null; + } + + Sessions.Add(BytesToHex(sessionId), session); + + return BytesToHex(sessionId); + } + + private static WidevineCencHeader ParseInitData(byte[] initData) + { + WidevineCencHeader cencHeader; + + try + { + cencHeader = Serializer.Deserialize(new MemoryStream(initData[32..])); + } + catch + { + try { - session = new Session(sessionId, parsedInitData, device, offline); + //needed for HBO Max + + PSSHBox psshBox = PSSHBox.FromByteArray(initData); + cencHeader = Serializer.Deserialize(new MemoryStream(psshBox.Data)); } - else if (raw) - { - session = new Session(sessionId, initData, device, offline); - } - else + catch { + //Logger.Verbose("Unable to parse, unsupported init data format"); return null; } - - Sessions.Add(BytesToHex(sessionId), session); - - return BytesToHex(sessionId); } - static WidevineCencHeader ParseInitData(byte[] initData) + return cencHeader; + } + + public static bool CloseSession(string sessionId) + { + //Logger.Debug($"CloseSession(session_id={BytesToHex(sessionId)})"); + //Logger.Verbose("Closing CDM session"); + + if (Sessions.ContainsKey(sessionId)) { - WidevineCencHeader cencHeader; - - try - { - cencHeader = Serializer.Deserialize(new MemoryStream(initData[32..])); - } - catch - { - try - { - //needed for HBO Max - - PSSHBox psshBox = PSSHBox.FromByteArray(initData); - cencHeader = Serializer.Deserialize(new MemoryStream(psshBox.Data)); - } - catch - { - //Logger.Verbose("Unable to parse, unsupported init data format"); - return null; - } - } - - return cencHeader; - } - - public static bool CloseSession(string sessionId) - { - //Logger.Debug($"CloseSession(session_id={BytesToHex(sessionId)})"); - //Logger.Verbose("Closing CDM session"); - - if (Sessions.ContainsKey(sessionId)) - { - Sessions.Remove(sessionId); - //Logger.Verbose("CDM session closed"); - return true; - } - else - { - //Logger.Info($"Session {sessionId} not found"); - return false; - } - } - - public static bool SetServiceCertificate(string sessionId, byte[] certData) - { - //Logger.Debug($"SetServiceCertificate(sessionId={BytesToHex(sessionId)}, cert={certB64})"); - //Logger.Verbose($"Setting service certificate"); - - if (!Sessions.ContainsKey(sessionId)) - { - //Logger.Error("Session ID doesn't exist"); - return false; - } - - SignedMessage signedMessage = new SignedMessage(); - - try - { - signedMessage = Serializer.Deserialize(new MemoryStream(certData)); - } - catch - { - //Logger.Warn("Failed to parse cert as SignedMessage"); - } - - SignedDeviceCertificate serviceCertificate; - try - { - try - { - //Logger.Debug("Service cert provided as signedmessage"); - serviceCertificate = Serializer.Deserialize(new MemoryStream(signedMessage.Msg)); - } - catch - { - //Logger.Debug("Service cert provided as signeddevicecertificate"); - serviceCertificate = Serializer.Deserialize(new MemoryStream(certData)); - } - } - catch - { - //Logger.Error("Failed to parse service certificate"); - return false; - } - - Sessions[sessionId].ServiceCertificate = serviceCertificate; - Sessions[sessionId].PrivacyMode = true; - + Sessions.Remove(sessionId); + //Logger.Verbose("CDM session closed"); return true; } - public static byte[] GetLicenseRequest(string sessionId) + //Logger.Info($"Session {sessionId} not found"); + return false; + } + + public static bool SetServiceCertificate(string sessionId, byte[] certData) + { + //Logger.Debug($"SetServiceCertificate(sessionId={BytesToHex(sessionId)}, cert={certB64})"); + //Logger.Verbose($"Setting service certificate"); + + if (!Sessions.ContainsKey(sessionId)) { - //Logger.Debug($"GetLicenseRequest(sessionId={BytesToHex(sessionId)})"); - //Logger.Verbose($"Getting license request"); - - if (!Sessions.ContainsKey(sessionId)) - { - //Logger.Error("Session ID doesn't exist"); - return null; - } - - var session = Sessions[sessionId]; - - //Logger.Debug("Building license request"); - - dynamic licenseRequest; - - if (session.InitData is WidevineCencHeader) - { - licenseRequest = new SignedLicenseRequest - { - Type = SignedLicenseRequest.MessageType.LicenseRequest, - Msg = new LicenseRequest - { - Type = LicenseRequest.RequestType.New, - KeyControlNonce = 1093602366, - ProtocolVersion = ProtocolVersion.Current, - ContentId = new LicenseRequest.ContentIdentification - { - CencId = new LicenseRequest.ContentIdentification.Cenc - { - LicenseType = session.Offline ? LicenseType.Offline : LicenseType.Default, - RequestId = session.SessionId, - Pssh = session.InitData - } - } - } - }; - } - else - { - licenseRequest = new SignedLicenseRequestRaw - { - Type = SignedLicenseRequestRaw.MessageType.LicenseRequest, - Msg = new LicenseRequestRaw - { - Type = LicenseRequestRaw.RequestType.New, - KeyControlNonce = 1093602366, - ProtocolVersion = ProtocolVersion.Current, - ContentId = new LicenseRequestRaw.ContentIdentification - { - CencId = new LicenseRequestRaw.ContentIdentification.Cenc - { - LicenseType = session.Offline ? LicenseType.Offline : LicenseType.Default, - RequestId = session.SessionId, - Pssh = session.InitData - } - } - } - }; - } - - if (session.PrivacyMode) - { - //Logger.Debug("Privacy mode & serivce certificate loaded, encrypting client id"); - - EncryptedClientIdentification encryptedClientIdProto = new EncryptedClientIdentification(); - - //Logger.Debug("Unencrypted client id " + Utils.SerializeToString(clientId)); - - using var memoryStream = new MemoryStream(); - Serializer.Serialize(memoryStream, session.Device.ClientID); - byte[] data = Padding.AddPKCS7Padding(memoryStream.ToArray(), 16); - - using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider - { - BlockSize = 128, - Padding = PaddingMode.PKCS7, - Mode = CipherMode.CBC - }; - aesProvider.GenerateKey(); - aesProvider.GenerateIV(); - - using MemoryStream mstream = new MemoryStream(); - using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateEncryptor(aesProvider.Key, aesProvider.IV), CryptoStreamMode.Write); - cryptoStream.Write(data, 0, data.Length); - encryptedClientIdProto.EncryptedClientId = mstream.ToArray(); - - using RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); - RSA.ImportRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey, out int bytesRead); - encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aesProvider.Key, RSAEncryptionPadding.OaepSHA1); - encryptedClientIdProto.EncryptedClientIdIv = aesProvider.IV; - encryptedClientIdProto.ServiceId = Encoding.UTF8.GetString(session.ServiceCertificate.DeviceCertificate.ServiceId); - encryptedClientIdProto.ServiceCertificateSerialNumber = session.ServiceCertificate.DeviceCertificate.SerialNumber; - - licenseRequest.Msg.EncryptedClientId = encryptedClientIdProto; - } - else - { - licenseRequest.Msg.ClientId = session.Device.ClientID; - } - - //Logger.Debug("Signing license request"); - - using (var memoryStream = new MemoryStream()) - { - Serializer.Serialize(memoryStream, licenseRequest.Msg); - byte[] data = memoryStream.ToArray(); - session.LicenseRequest = data; - - licenseRequest.Signature = session.Device.Sign(data); - } - - //Logger.Verbose("License request created"); - - byte[] requestBytes; - using (var memoryStream = new MemoryStream()) - { - Serializer.Serialize(memoryStream, licenseRequest); - requestBytes = memoryStream.ToArray(); - } - - Sessions[sessionId] = session; - - //Logger.Debug($"license request b64: {Convert.ToBase64String(requestBytes)}"); - return requestBytes; + //Logger.Error("Session ID doesn't exist"); + return false; } - public static void ProvideLicense(string sessionId, byte[] license) + SignedMessage signedMessage = new(); + + try { - //Logger.Debug($"ProvideLicense(sessionId={BytesToHex(sessionId)}, licenseB64={licenseB64})"); - //Logger.Verbose("Decrypting provided license"); + signedMessage = Serializer.Deserialize(new MemoryStream(certData)); + } + catch + { + //Logger.Warn("Failed to parse cert as SignedMessage"); + } - if (!Sessions.ContainsKey(sessionId)) - { - throw new Exception("Session ID doesn't exist"); - } - - var session = Sessions[sessionId]; - - if (session.LicenseRequest == null) - { - throw new Exception("Generate a license request first"); - } - - SignedLicense signedLicense; + SignedDeviceCertificate serviceCertificate; + try + { try { - signedLicense = Serializer.Deserialize(new MemoryStream(license)); + //Logger.Debug("Service cert provided as signedmessage"); + serviceCertificate = + Serializer.Deserialize(new MemoryStream(signedMessage.Msg)); } catch { - throw new Exception("Unable to parse license"); + //Logger.Debug("Service cert provided as signeddevicecertificate"); + serviceCertificate = Serializer.Deserialize(new MemoryStream(certData)); } + } + catch + { + //Logger.Error("Failed to parse service certificate"); + return false; + } - //Logger.Debug("License: " + Utils.SerializeToString(signedLicense)); + Sessions[sessionId].ServiceCertificate = serviceCertificate; + Sessions[sessionId].PrivacyMode = true; - session.License = signedLicense; + return true; + } - //Logger.Debug($"Deriving keys from session key"); + public static byte[] GetLicenseRequest(string sessionId) + { + //Logger.Debug($"GetLicenseRequest(sessionId={BytesToHex(sessionId)})"); + //Logger.Verbose($"Getting license request"); - try + if (!Sessions.ContainsKey(sessionId)) + { + //Logger.Error("Session ID doesn't exist"); + return null; + } + + Session session = Sessions[sessionId]; + + //Logger.Debug("Building license request"); + + dynamic licenseRequest; + + if (session.InitData is WidevineCencHeader) + { + licenseRequest = new SignedLicenseRequest { - var sessionKey = session.Device.Decrypt(session.License.SessionKey); - - if (sessionKey.Length != 16) + Type = SignedLicenseRequest.MessageType.LicenseRequest, + Msg = new LicenseRequest { - throw new Exception("Unable to decrypt session key"); + Type = LicenseRequest.RequestType.New, + KeyControlNonce = 1093602366, + ProtocolVersion = ProtocolVersion.Current, + ContentId = new LicenseRequest.ContentIdentification + { + CencId = new LicenseRequest.ContentIdentification.Cenc + { + LicenseType = session.Offline ? LicenseType.Offline : LicenseType.Default, + RequestId = session.SessionId, + Pssh = session.InitData + } + } } + }; + } + else + { + licenseRequest = new SignedLicenseRequestRaw + { + Type = SignedLicenseRequestRaw.MessageType.LicenseRequest, + Msg = new LicenseRequestRaw + { + Type = LicenseRequestRaw.RequestType.New, + KeyControlNonce = 1093602366, + ProtocolVersion = ProtocolVersion.Current, + ContentId = new LicenseRequestRaw.ContentIdentification + { + CencId = new LicenseRequestRaw.ContentIdentification.Cenc + { + LicenseType = session.Offline ? LicenseType.Offline : LicenseType.Default, + RequestId = session.SessionId, + Pssh = session.InitData + } + } + } + }; + } - session.SessionKey = sessionKey; - } - catch + if (session.PrivacyMode) + { + //Logger.Debug("Privacy mode & serivce certificate loaded, encrypting client id"); + + EncryptedClientIdentification encryptedClientIdProto = new(); + + //Logger.Debug("Unencrypted client id " + Utils.SerializeToString(clientId)); + + using MemoryStream memoryStream = new(); + Serializer.Serialize(memoryStream, session.Device.ClientID); + byte[] data = Padding.AddPKCS7Padding(memoryStream.ToArray(), 16); + + using AesCryptoServiceProvider aesProvider = new() + { + BlockSize = 128, Padding = PaddingMode.PKCS7, Mode = CipherMode.CBC + }; + aesProvider.GenerateKey(); + aesProvider.GenerateIV(); + + using MemoryStream mstream = new(); + using CryptoStream cryptoStream = new(mstream, aesProvider.CreateEncryptor(aesProvider.Key, aesProvider.IV), + CryptoStreamMode.Write); + cryptoStream.Write(data, 0, data.Length); + encryptedClientIdProto.EncryptedClientId = mstream.ToArray(); + + using RSACryptoServiceProvider RSA = new(); + RSA.ImportRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey, out int bytesRead); + encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aesProvider.Key, RSAEncryptionPadding.OaepSHA1); + encryptedClientIdProto.EncryptedClientIdIv = aesProvider.IV; + encryptedClientIdProto.ServiceId = + Encoding.UTF8.GetString(session.ServiceCertificate.DeviceCertificate.ServiceId); + encryptedClientIdProto.ServiceCertificateSerialNumber = + session.ServiceCertificate.DeviceCertificate.SerialNumber; + + licenseRequest.Msg.EncryptedClientId = encryptedClientIdProto; + } + else + { + licenseRequest.Msg.ClientId = session.Device.ClientID; + } + + //Logger.Debug("Signing license request"); + + using (MemoryStream memoryStream = new()) + { + Serializer.Serialize(memoryStream, licenseRequest.Msg); + byte[] data = memoryStream.ToArray(); + session.LicenseRequest = data; + + licenseRequest.Signature = session.Device.Sign(data); + } + + //Logger.Verbose("License request created"); + + byte[] requestBytes; + using (MemoryStream memoryStream = new()) + { + Serializer.Serialize(memoryStream, licenseRequest); + requestBytes = memoryStream.ToArray(); + } + + Sessions[sessionId] = session; + + //Logger.Debug($"license request b64: {Convert.ToBase64String(requestBytes)}"); + return requestBytes; + } + + public static void ProvideLicense(string sessionId, byte[] license) + { + //Logger.Debug($"ProvideLicense(sessionId={BytesToHex(sessionId)}, licenseB64={licenseB64})"); + //Logger.Verbose("Decrypting provided license"); + + if (!Sessions.ContainsKey(sessionId)) + { + throw new Exception("Session ID doesn't exist"); + } + + Session session = Sessions[sessionId]; + + if (session.LicenseRequest == null) + { + throw new Exception("Generate a license request first"); + } + + SignedLicense signedLicense; + try + { + signedLicense = Serializer.Deserialize(new MemoryStream(license)); + } + catch + { + throw new Exception("Unable to parse license"); + } + + //Logger.Debug("License: " + Utils.SerializeToString(signedLicense)); + + session.License = signedLicense; + + //Logger.Debug($"Deriving keys from session key"); + + try + { + byte[] sessionKey = session.Device.Decrypt(session.License.SessionKey); + + if (sessionKey.Length != 16) { throw new Exception("Unable to decrypt session key"); } - //Logger.Debug("Session key: " + BytesToHex(session.SessionKey)); + session.SessionKey = sessionKey; + } + catch + { + throw new Exception("Unable to decrypt session key"); + } - session.DerivedKeys = DeriveKeys(session.LicenseRequest, session.SessionKey); + //Logger.Debug("Session key: " + BytesToHex(session.SessionKey)); - //Logger.Debug("Verifying license signature"); + session.DerivedKeys = DeriveKeys(session.LicenseRequest, session.SessionKey); - byte[] licenseBytes; - using (var memoryStream = new MemoryStream()) + //Logger.Debug("Verifying license signature"); + + byte[] licenseBytes; + using (MemoryStream memoryStream = new()) + { + Serializer.Serialize(memoryStream, signedLicense.Msg); + licenseBytes = memoryStream.ToArray(); + } + + byte[] hmacHash = CryptoUtils.GetHMACSHA256Digest(licenseBytes, session.DerivedKeys.Auth1); + + if (!hmacHash.SequenceEqual(signedLicense.Signature)) + { + throw new Exception("License signature mismatch"); + } + + foreach (License.KeyContainer key in signedLicense.Msg.Keys) + { + string type = key.Type.ToString(); + + if (type == "Signing") { - Serializer.Serialize(memoryStream, signedLicense.Msg); - licenseBytes = memoryStream.ToArray(); - } - byte[] hmacHash = CryptoUtils.GetHMACSHA256Digest(licenseBytes, session.DerivedKeys.Auth1); - - if (!hmacHash.SequenceEqual(signedLicense.Signature)) - { - throw new Exception("License signature mismatch"); + continue; } - foreach (License.KeyContainer key in signedLicense.Msg.Keys) + byte[] keyId; + byte[] encryptedKey = key.Key; + byte[] iv = key.Iv; + keyId = key.Id; + if (keyId == null) { - string type = key.Type.ToString(); + keyId = Encoding.ASCII.GetBytes(key.Type.ToString()); + } - if (type == "Signing") - continue; + byte[] decryptedKey; - byte[] keyId; - byte[] encryptedKey = key.Key; - byte[] iv = key.Iv; - keyId = key.Id; - if (keyId == null) + using MemoryStream mstream = new(); + using AesCryptoServiceProvider aesProvider = new() { Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 }; + using CryptoStream cryptoStream = new(mstream, aesProvider.CreateDecryptor(session.DerivedKeys.Enc, iv), + CryptoStreamMode.Write); + cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); + decryptedKey = mstream.ToArray(); + + List permissions = new(); + if (type == "OperatorSession") + { + foreach (PropertyInfo perm in key._OperatorSessionKeyPermissions.GetType().GetProperties()) { - keyId = Encoding.ASCII.GetBytes(key.Type.ToString()); - } - - byte[] decryptedKey; - - using MemoryStream mstream = new MemoryStream(); - using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider - { - Mode = CipherMode.CBC, - Padding = PaddingMode.PKCS7 - }; - using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateDecryptor(session.DerivedKeys.Enc, iv), CryptoStreamMode.Write); - cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); - decryptedKey = mstream.ToArray(); - - List permissions = new List(); - if (type == "OperatorSession") - { - foreach (PropertyInfo perm in key._OperatorSessionKeyPermissions.GetType().GetProperties()) + if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1) { - if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1) - { - permissions.Add(perm.Name); - } + permissions.Add(perm.Name); } } - session.ContentKeys.Add(new ContentKey - { - KeyID = keyId, - Type = type, - Bytes = decryptedKey, - Permissions = permissions - }); } - //Logger.Debug($"Key count: {session.Keys.Count}"); - - Sessions[sessionId] = session; - - //Logger.Verbose("Decrypted all keys"); - } - - public static DerivedKeys DeriveKeys(byte[] message, byte[] key) - { - byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[] { 0x0, }).Concat(message).Concat(new byte[] { 0x0, 0x0, 0x0, 0x80 }).ToArray(); - byte[] authKeyBase = Encoding.UTF8.GetBytes("AUTHENTICATION").Concat(new byte[] { 0x0, }).Concat(message).Concat(new byte[] { 0x0, 0x0, 0x2, 0x0 }).ToArray(); - - byte[] encKey = new byte[] { 0x01 }.Concat(encKeyBase).ToArray(); - byte[] authKey1 = new byte[] { 0x01 }.Concat(authKeyBase).ToArray(); - byte[] authKey2 = new byte[] { 0x02 }.Concat(authKeyBase).ToArray(); - byte[] authKey3 = new byte[] { 0x03 }.Concat(authKeyBase).ToArray(); - byte[] authKey4 = new byte[] { 0x04 }.Concat(authKeyBase).ToArray(); - - byte[] encCmacKey = CryptoUtils.GetCMACDigest(encKey, key); - byte[] authCmacKey1 = CryptoUtils.GetCMACDigest(authKey1, key); - byte[] authCmacKey2 = CryptoUtils.GetCMACDigest(authKey2, key); - byte[] authCmacKey3 = CryptoUtils.GetCMACDigest(authKey3, key); - byte[] authCmacKey4 = CryptoUtils.GetCMACDigest(authKey4, key); - - byte[] authCmacCombined1 = authCmacKey1.Concat(authCmacKey2).ToArray(); - byte[] authCmacCombined2 = authCmacKey3.Concat(authCmacKey4).ToArray(); - - return new DerivedKeys + session.ContentKeys.Add(new ContentKey { - Auth1 = authCmacCombined1, - Auth2 = authCmacCombined2, - Enc = encCmacKey - }; + KeyID = keyId, Type = type, Bytes = decryptedKey, Permissions = permissions + }); } - public static List GetKeys(string sessionId) - { - if (Sessions.ContainsKey(sessionId)) - return Sessions[sessionId].ContentKeys; - else - { - throw new Exception("Session not found"); - } - } + //Logger.Debug($"Key count: {session.Keys.Count}"); - private static string BytesToHex(byte[] data) - { - return BitConverter.ToString(data).Replace("-", ""); - } + Sessions[sessionId] = session; + + //Logger.Verbose("Decrypted all keys"); } + + public static DerivedKeys DeriveKeys(byte[] message, byte[] key) + { + byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[] { 0x0 }).Concat(message) + .Concat(new byte[] { 0x0, 0x0, 0x0, 0x80 }).ToArray(); + byte[] authKeyBase = Encoding.UTF8.GetBytes("AUTHENTICATION").Concat(new byte[] { 0x0 }).Concat(message) + .Concat(new byte[] { 0x0, 0x0, 0x2, 0x0 }).ToArray(); + + byte[] encKey = new byte[] { 0x01 }.Concat(encKeyBase).ToArray(); + byte[] authKey1 = new byte[] { 0x01 }.Concat(authKeyBase).ToArray(); + byte[] authKey2 = new byte[] { 0x02 }.Concat(authKeyBase).ToArray(); + byte[] authKey3 = new byte[] { 0x03 }.Concat(authKeyBase).ToArray(); + byte[] authKey4 = new byte[] { 0x04 }.Concat(authKeyBase).ToArray(); + + byte[] encCmacKey = CryptoUtils.GetCMACDigest(encKey, key); + byte[] authCmacKey1 = CryptoUtils.GetCMACDigest(authKey1, key); + byte[] authCmacKey2 = CryptoUtils.GetCMACDigest(authKey2, key); + byte[] authCmacKey3 = CryptoUtils.GetCMACDigest(authKey3, key); + byte[] authCmacKey4 = CryptoUtils.GetCMACDigest(authKey4, key); + + byte[] authCmacCombined1 = authCmacKey1.Concat(authCmacKey2).ToArray(); + byte[] authCmacCombined2 = authCmacKey3.Concat(authCmacKey4).ToArray(); + + return new DerivedKeys { Auth1 = authCmacCombined1, Auth2 = authCmacCombined2, Enc = encCmacKey }; + } + + public static List GetKeys(string sessionId) + { + if (Sessions.ContainsKey(sessionId)) + { + return Sessions[sessionId].ContentKeys; + } + + throw new Exception("Session not found"); + } + + private static string BytesToHex(byte[] data) => BitConverter.ToString(data).Replace("-", ""); } - - /* - public static List ProvideLicense(string requestB64, string licenseB64) + public static List ProvideLicense(string requestB64, string licenseB64) + { + byte[] licenseRequest; + + var request = Serializer.Deserialize(new MemoryStream(Convert.FromBase64String(requestB64))); + + using (var ms = new MemoryStream()) { - byte[] licenseRequest; + Serializer.Serialize(ms, request.Msg); + licenseRequest = ms.ToArray(); + } - var request = Serializer.Deserialize(new MemoryStream(Convert.FromBase64String(requestB64))); + SignedLicense signedLicense; + try + { + signedLicense = Serializer.Deserialize(new MemoryStream(Convert.FromBase64String(licenseB64))); + } + catch + { + return null; + } - using (var ms = new MemoryStream()) - { - Serializer.Serialize(ms, request.Msg); - licenseRequest = ms.ToArray(); - } + byte[] sessionKey; + try + { - SignedLicense signedLicense; - try - { - signedLicense = Serializer.Deserialize(new MemoryStream(Convert.FromBase64String(licenseB64))); - } - catch + sessionKey = Controllers.Adapter.OaepDecrypt(Convert.ToBase64String(signedLicense.SessionKey)); + + if (sessionKey.Length != 16) { return null; } + } + catch + { + return null; + } - byte[] sessionKey; - try + byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[] { 0x0, }).Concat(licenseRequest).Concat(new byte[] { 0x0, 0x0, 0x0, 0x80 }).ToArray(); + + byte[] encKey = new byte[] { 0x01 }.Concat(encKeyBase).ToArray(); + + byte[] encCmacKey = GetCmacDigest(encKey, sessionKey); + + byte[] encryptionKey = encCmacKey; + + List keys = new List(); + + foreach (License.KeyContainer key in signedLicense.Msg.Keys) + { + string type = key.Type.ToString(); + if (type == "Signing") { - - sessionKey = Controllers.Adapter.OaepDecrypt(Convert.ToBase64String(signedLicense.SessionKey)); - - if (sessionKey.Length != 16) - { - return null; - } - } - catch - { - return null; + continue; } - byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[] { 0x0, }).Concat(licenseRequest).Concat(new byte[] { 0x0, 0x0, 0x0, 0x80 }).ToArray(); - - byte[] encKey = new byte[] { 0x01 }.Concat(encKeyBase).ToArray(); - - byte[] encCmacKey = GetCmacDigest(encKey, sessionKey); - - byte[] encryptionKey = encCmacKey; - - List keys = new List(); - - foreach (License.KeyContainer key in signedLicense.Msg.Keys) + byte[] keyId; + byte[] encryptedKey = key.Key; + byte[] iv = key.Iv; + keyId = key.Id; + if (keyId == null) { - string type = key.Type.ToString(); - if (type == "Signing") - { - continue; - } + keyId = Encoding.ASCII.GetBytes(key.Type.ToString()); + } - byte[] keyId; - byte[] encryptedKey = key.Key; - byte[] iv = key.Iv; - keyId = key.Id; - if (keyId == null) - { - keyId = Encoding.ASCII.GetBytes(key.Type.ToString()); - } + byte[] decryptedKey; - byte[] decryptedKey; + using MemoryStream mstream = new MemoryStream(); + using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider + { + Mode = CipherMode.CBC, + Padding = PaddingMode.PKCS7 + }; + using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateDecryptor(encryptionKey, iv), CryptoStreamMode.Write); + cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); + decryptedKey = mstream.ToArray(); - using MemoryStream mstream = new MemoryStream(); - using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider + List permissions = new List(); + if (type == "OPERATOR_SESSION") + { + foreach (FieldInfo perm in key._OperatorSessionKeyPermissions.GetType().GetFields()) { - Mode = CipherMode.CBC, - Padding = PaddingMode.PKCS7 - }; - using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateDecryptor(encryptionKey, iv), CryptoStreamMode.Write); - cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); - decryptedKey = mstream.ToArray(); - - List permissions = new List(); - if (type == "OPERATOR_SESSION") - { - foreach (FieldInfo perm in key._OperatorSessionKeyPermissions.GetType().GetFields()) + if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1) { - if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1) - { - permissions.Add(perm.Name); - } + permissions.Add(perm.Name); } } - keys.Add(BitConverter.ToString(keyId).Replace("-","").ToLower() + ":" + BitConverter.ToString(decryptedKey).Replace("-", "").ToLower()); } + keys.Add(BitConverter.ToString(keyId).Replace("-","").ToLower() + ":" + BitConverter.ToString(decryptedKey).Replace("-", "").ToLower()); + } - return keys; - }*/ + return keys; + }*/ diff --git a/OF DL/Widevine/CDMApi.cs b/OF DL/Widevine/CDMApi.cs index 4e0f1d9..e6d7315 100644 --- a/OF DL/Widevine/CDMApi.cs +++ b/OF DL/Widevine/CDMApi.cs @@ -1,25 +1,21 @@ -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +public class CDMApi { - public class CDMApi + private string SessionId { get; set; } + + public byte[] GetChallenge(string initDataB64, string certDataB64, bool offline = false, bool raw = false) { - string SessionId { get; set; } - - public byte[] GetChallenge(string initDataB64, string certDataB64, bool offline = false, bool raw = false) - { - SessionId = CDM.OpenSession(initDataB64, Constants.DEVICE_NAME, offline, raw); - CDM.SetServiceCertificate(SessionId, Convert.FromBase64String(certDataB64)); - return CDM.GetLicenseRequest(SessionId); - } - - public bool ProvideLicense(string licenseB64) - { - CDM.ProvideLicense(SessionId, Convert.FromBase64String(licenseB64)); - return true; - } - - public List GetKeys() - { - return CDM.GetKeys(SessionId); - } + SessionId = CDM.OpenSession(initDataB64, Constants.DEVICE_NAME, offline, raw); + CDM.SetServiceCertificate(SessionId, Convert.FromBase64String(certDataB64)); + return CDM.GetLicenseRequest(SessionId); } + + public bool ProvideLicense(string licenseB64) + { + CDM.ProvideLicense(SessionId, Convert.FromBase64String(licenseB64)); + return true; + } + + public List GetKeys() => CDM.GetKeys(SessionId); } diff --git a/OF DL/Widevine/CDMDevice.cs b/OF DL/Widevine/CDMDevice.cs index 7dc4023..09fc4ef 100644 --- a/OF DL/Widevine/CDMDevice.cs +++ b/OF DL/Widevine/CDMDevice.cs @@ -7,84 +7,86 @@ using Org.BouncyCastle.Crypto.Signers; using Org.BouncyCastle.OpenSsl; using ProtoBuf; -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +public class CDMDevice { - public class CDMDevice + public CDMDevice(string deviceName, byte[] clientIdBlobBytes = null, byte[] privateKeyBytes = null, + byte[] vmpBytes = null) { - public string DeviceName { get; set; } - public ClientIdentification ClientID { get; set; } - AsymmetricCipherKeyPair DeviceKeys { get; set; } + DeviceName = deviceName; - public virtual bool IsAndroid { get; set; } = true; + string privateKeyPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_private_key"); + string vmpPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_vmp_blob"); - public CDMDevice(string deviceName, byte[] clientIdBlobBytes = null, byte[] privateKeyBytes = null, byte[] vmpBytes = null) + if (clientIdBlobBytes == null) { - DeviceName = deviceName; + string clientIDPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_client_id_blob"); - string privateKeyPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_private_key"); - string vmpPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_vmp_blob"); - - if (clientIdBlobBytes == null) + if (!File.Exists(clientIDPath)) { - string clientIDPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_client_id_blob"); - - if (!File.Exists(clientIDPath)) - throw new Exception("No client id blob found"); - - clientIdBlobBytes = File.ReadAllBytes(clientIDPath); + throw new Exception("No client id blob found"); } - ClientID = Serializer.Deserialize(new MemoryStream(clientIdBlobBytes)); - - if (privateKeyBytes != null) - { - using var reader = new StringReader(Encoding.UTF8.GetString(privateKeyBytes)); - DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject(); - } - else if (File.Exists(privateKeyPath)) - { - using var reader = File.OpenText(privateKeyPath); - DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject(); - } - - if (vmpBytes != null) - { - var vmp = Serializer.Deserialize(new MemoryStream(vmpBytes)); - ClientID.FileHashes = vmp; - } - else if (File.Exists(vmpPath)) - { - var vmp = Serializer.Deserialize(new MemoryStream(File.ReadAllBytes(vmpPath))); - ClientID.FileHashes = vmp; - } + clientIdBlobBytes = File.ReadAllBytes(clientIDPath); } - public virtual byte[] Decrypt(byte[] data) + ClientID = Serializer.Deserialize(new MemoryStream(clientIdBlobBytes)); + + if (privateKeyBytes != null) { - OaepEncoding eng = new OaepEncoding(new RsaEngine()); - eng.Init(false, DeviceKeys.Private); - - int length = data.Length; - int blockSize = eng.GetInputBlockSize(); - - List plainText = new List(); - - for (int chunkPosition = 0; chunkPosition < length; chunkPosition += blockSize) - { - int chunkSize = Math.Min(blockSize, length - chunkPosition); - plainText.AddRange(eng.ProcessBlock(data, chunkPosition, chunkSize)); - } - - return plainText.ToArray(); + using StringReader reader = new(Encoding.UTF8.GetString(privateKeyBytes)); + DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject(); + } + else if (File.Exists(privateKeyPath)) + { + using StreamReader reader = File.OpenText(privateKeyPath); + DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject(); } - public virtual byte[] Sign(byte[] data) + if (vmpBytes != null) { - PssSigner eng = new PssSigner(new RsaEngine(), new Sha1Digest()); - - eng.Init(true, DeviceKeys.Private); - eng.BlockUpdate(data, 0, data.Length); - return eng.GenerateSignature(); + FileHashes? vmp = Serializer.Deserialize(new MemoryStream(vmpBytes)); + ClientID.FileHashes = vmp; + } + else if (File.Exists(vmpPath)) + { + FileHashes? vmp = Serializer.Deserialize(new MemoryStream(File.ReadAllBytes(vmpPath))); + ClientID.FileHashes = vmp; } } + + public string DeviceName { get; set; } + public ClientIdentification ClientID { get; set; } + private AsymmetricCipherKeyPair DeviceKeys { get; } + + public virtual bool IsAndroid { get; set; } = true; + + public virtual byte[] Decrypt(byte[] data) + { + OaepEncoding eng = new(new RsaEngine()); + eng.Init(false, DeviceKeys.Private); + + int length = data.Length; + int blockSize = eng.GetInputBlockSize(); + + List plainText = new(); + + for (int chunkPosition = 0; chunkPosition < length; chunkPosition += blockSize) + { + int chunkSize = Math.Min(blockSize, length - chunkPosition); + plainText.AddRange(eng.ProcessBlock(data, chunkPosition, chunkSize)); + } + + return plainText.ToArray(); + } + + public virtual byte[] Sign(byte[] data) + { + PssSigner eng = new(new RsaEngine(), new Sha1Digest()); + + eng.Init(true, DeviceKeys.Private); + eng.BlockUpdate(data, 0, data.Length); + return eng.GenerateSignature(); + } } diff --git a/OF DL/Widevine/Constants.cs b/OF DL/Widevine/Constants.cs index a692eb2..280dcaa 100644 --- a/OF DL/Widevine/Constants.cs +++ b/OF DL/Widevine/Constants.cs @@ -1,9 +1,10 @@ -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +public class Constants { - public class Constants - { - public static string WORKING_FOLDER { get; set; } = System.IO.Path.GetFullPath(System.IO.Path.Join(System.IO.Directory.GetCurrentDirectory(), "cdm")); - public static string DEVICES_FOLDER { get; set; } = System.IO.Path.GetFullPath(System.IO.Path.Join(WORKING_FOLDER, "devices")); - public static string DEVICE_NAME { get; set; } = "chrome_1610"; - } + public static string WORKING_FOLDER { get; set; } = + Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "cdm")); + + public static string DEVICES_FOLDER { get; set; } = Path.GetFullPath(Path.Join(WORKING_FOLDER, "devices")); + public static string DEVICE_NAME { get; set; } = "chrome_1610"; } diff --git a/OF DL/Widevine/ContentKey.cs b/OF DL/Widevine/ContentKey.cs index 8b611dd..1ad7507 100644 --- a/OF DL/Widevine/ContentKey.cs +++ b/OF DL/Widevine/ContentKey.cs @@ -1,39 +1,27 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +[Serializable] +public class ContentKey { - [Serializable] - public class ContentKey + [JsonPropertyName("key_id")] public byte[] KeyID { get; set; } + + [JsonPropertyName("type")] public string Type { get; set; } + + [JsonPropertyName("bytes")] public byte[] Bytes { get; set; } + + [NotMapped] + [JsonPropertyName("permissions")] + public List Permissions { - [JsonPropertyName("key_id")] - public byte[] KeyID { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("bytes")] - public byte[] Bytes { get; set; } - - [NotMapped] - [JsonPropertyName("permissions")] - public List Permissions { - get - { - return PermissionsString.Split(",").ToList(); - } - set - { - PermissionsString = string.Join(",", value); - } - } - - [JsonIgnore] - public string PermissionsString { get; set; } - - public override string ToString() - { - return $"{BitConverter.ToString(KeyID).Replace("-", "").ToLower()}:{BitConverter.ToString(Bytes).Replace("-", "").ToLower()}"; - } + get => PermissionsString.Split(",").ToList(); + set => PermissionsString = string.Join(",", value); } + + [JsonIgnore] public string PermissionsString { get; set; } + + public override string ToString() => + $"{BitConverter.ToString(KeyID).Replace("-", "").ToLower()}:{BitConverter.ToString(Bytes).Replace("-", "").ToLower()}"; } diff --git a/OF DL/Widevine/DerivedKeys.cs b/OF DL/Widevine/DerivedKeys.cs index fdcebec..5a61705 100644 --- a/OF DL/Widevine/DerivedKeys.cs +++ b/OF DL/Widevine/DerivedKeys.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +public class DerivedKeys { - public class DerivedKeys - { - public byte[] Auth1 { get; set; } - public byte[] Auth2 { get; set; } - public byte[] Enc { get; set; } - } + public byte[] Auth1 { get; set; } + public byte[] Auth2 { get; set; } + public byte[] Enc { get; set; } } diff --git a/OF DL/Widevine/PSSHBox.cs b/OF DL/Widevine/PSSHBox.cs index 55b16de..a61f694 100644 --- a/OF DL/Widevine/PSSHBox.cs +++ b/OF DL/Widevine/PSSHBox.cs @@ -1,59 +1,68 @@ -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +internal class PSSHBox { - class PSSHBox + private static readonly byte[] PSSH_HEADER = new byte[] { 0x70, 0x73, 0x73, 0x68 }; + + private PSSHBox(List kids, byte[] data) { - static readonly byte[] PSSH_HEADER = new byte[] { 0x70, 0x73, 0x73, 0x68 }; + KIDs = kids; + Data = data; + } - public List KIDs { get; set; } = new List(); - public byte[] Data { get; set; } + public List KIDs { get; set; } = new(); + public byte[] Data { get; set; } - PSSHBox(List kids, byte[] data) + public static PSSHBox FromByteArray(byte[] psshbox) + { + using MemoryStream stream = new(psshbox); + + stream.Seek(4, SeekOrigin.Current); + byte[] header = new byte[4]; + stream.Read(header, 0, 4); + + if (!header.SequenceEqual(PSSH_HEADER)) { - KIDs = kids; - Data = data; + throw new Exception("Not a pssh box"); } - public static PSSHBox FromByteArray(byte[] psshbox) + stream.Seek(20, SeekOrigin.Current); + byte[] kidCountBytes = new byte[4]; + stream.Read(kidCountBytes, 0, 4); + + if (BitConverter.IsLittleEndian) { - using var stream = new System.IO.MemoryStream(psshbox); - - stream.Seek(4, System.IO.SeekOrigin.Current); - byte[] header = new byte[4]; - stream.Read(header, 0, 4); - - if (!header.SequenceEqual(PSSH_HEADER)) - throw new Exception("Not a pssh box"); - - stream.Seek(20, System.IO.SeekOrigin.Current); - byte[] kidCountBytes = new byte[4]; - stream.Read(kidCountBytes, 0, 4); - - if (BitConverter.IsLittleEndian) - Array.Reverse(kidCountBytes); - uint kidCount = BitConverter.ToUInt32(kidCountBytes); - - List kids = new List(); - for (int i = 0; i < kidCount; i++) - { - byte[] kid = new byte[16]; - stream.Read(kid); - kids.Add(kid); - } - - byte[] dataLengthBytes = new byte[4]; - stream.Read(dataLengthBytes); - - if (BitConverter.IsLittleEndian) - Array.Reverse(dataLengthBytes); - uint dataLength = BitConverter.ToUInt32(dataLengthBytes); - - if (dataLength == 0) - return new PSSHBox(kids, null); - - byte[] data = new byte[dataLength]; - stream.Read(data); - - return new PSSHBox(kids, data); + Array.Reverse(kidCountBytes); } + + uint kidCount = BitConverter.ToUInt32(kidCountBytes); + + List kids = new(); + for (int i = 0; i < kidCount; i++) + { + byte[] kid = new byte[16]; + stream.Read(kid); + kids.Add(kid); + } + + byte[] dataLengthBytes = new byte[4]; + stream.Read(dataLengthBytes); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(dataLengthBytes); + } + + uint dataLength = BitConverter.ToUInt32(dataLengthBytes); + + if (dataLength == 0) + { + return new PSSHBox(kids, null); + } + + byte[] data = new byte[dataLength]; + stream.Read(data); + + return new PSSHBox(kids, data); } } diff --git a/OF DL/Widevine/Session.cs b/OF DL/Widevine/Session.cs index 0c6a515..5e16afc 100644 --- a/OF DL/Widevine/Session.cs +++ b/OF DL/Widevine/Session.cs @@ -1,25 +1,24 @@ -namespace OF_DL.Widevine -{ - class Session - { - public byte[] SessionId { get; set; } - public dynamic InitData { get; set; } - public bool Offline { get; set; } - public CDMDevice Device { get; set; } - public byte[] SessionKey { get; set; } - public DerivedKeys DerivedKeys { get; set; } - public byte[] LicenseRequest { get; set; } - public SignedLicense License { get; set; } - public SignedDeviceCertificate ServiceCertificate { get; set; } - public bool PrivacyMode { get; set; } - public List ContentKeys { get; set; } = new List(); +namespace OF_DL.Widevine; - public Session(byte[] sessionId, dynamic initData, CDMDevice device, bool offline) - { - SessionId = sessionId; - InitData = initData; - Offline = offline; - Device = device; - } +internal class Session +{ + public Session(byte[] sessionId, dynamic initData, CDMDevice device, bool offline) + { + SessionId = sessionId; + InitData = initData; + Offline = offline; + Device = device; } + + public byte[] SessionId { get; set; } + public dynamic InitData { get; set; } + public bool Offline { get; set; } + public CDMDevice Device { get; set; } + public byte[] SessionKey { get; set; } + public DerivedKeys DerivedKeys { get; set; } + public byte[] LicenseRequest { get; set; } + public SignedLicense License { get; set; } + public SignedDeviceCertificate ServiceCertificate { get; set; } + public bool PrivacyMode { get; set; } + public List ContentKeys { get; set; } = new(); } diff --git a/OF DL/rules.json b/OF DL/rules.json index 9cb9785..d524235 100644 --- a/OF DL/rules.json +++ b/OF DL/rules.json @@ -4,5 +4,38 @@ "prefix": "30586", "suffix": "67000213", "checksum_constant": 521, - "checksum_indexes": [ 0, 2, 3, 7, 7, 8, 8, 10, 11, 13, 14, 16, 17, 17, 17, 19, 19, 20, 21, 21, 23, 23, 24, 24, 27, 27, 29, 30, 31, 34, 35, 39 ] + "checksum_indexes": [ + 0, + 2, + 3, + 7, + 7, + 8, + 8, + 10, + 11, + 13, + 14, + 16, + 17, + 17, + 17, + 19, + 19, + 20, + 21, + 21, + 23, + 23, + 24, + 24, + 27, + 27, + 29, + 30, + 31, + 34, + 35, + 39 + ] } From 50217a7642cca783a0dbfb60a6f150f1588521ef Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Sun, 8 Feb 2026 14:06:13 -0600 Subject: [PATCH 11/55] Rename Entities to Models --- OF DL/Helpers/DownloadContext.cs | 2 +- OF DL/Helpers/VersionHelper.cs | 2 +- .../{Entities => Models}/Archived/Archived.cs | 2 +- .../Archived/ArchivedCollection.cs | 2 +- OF DL/{Entities => Models}/Auth.cs | 2 +- .../{Entities => Models}/CDRMProjectRequest.cs | 2 +- OF DL/{Entities => Models}/Config.cs | 2 +- OF DL/{Entities => Models}/DownloadResult.cs | 2 +- OF DL/{Entities => Models}/DynamicRules.cs | 2 +- .../FileNameFormatConfig.cs | 2 +- .../Highlights/HighlightMedia.cs | 2 +- .../Highlights/Highlights.cs | 2 +- .../IFileNameFormatConfig.cs | 2 +- .../LatestReleaseAPIResponse.cs | 2 +- OF DL/{Entities => Models}/Lists/UserList.cs | 2 +- OF DL/{Entities => Models}/Lists/UsersList.cs | 2 +- .../Messages/MessageCollection.cs | 2 +- .../{Entities => Models}/Messages/Messages.cs | 2 +- .../Messages/SingleMessage.cs | 4 ++-- OF DL/{Entities => Models}/OFDLRequest.cs | 2 +- OF DL/{Entities => Models}/Post/Post.cs | 2 +- .../Post/PostCollection.cs | 2 +- OF DL/{Entities => Models}/Post/SinglePost.cs | 2 +- .../Post/SinglePostCollection.cs | 2 +- .../Purchased/PaidMessageCollection.cs | 4 ++-- .../Purchased/PaidPostCollection.cs | 4 ++-- .../Purchased/Purchased.cs | 4 ++-- .../Purchased/PurchasedTabCollection.cs | 2 +- .../Purchased/SinglePaidMessageCollection.cs | 6 +++--- .../{Entities => Models}/ShortDateConverter.cs | 2 +- OF DL/{Entities => Models}/Stories/Stories.cs | 2 +- OF DL/{Entities => Models}/Streams/Streams.cs | 2 +- .../Streams/StreamsCollection.cs | 2 +- OF DL/{Entities => Models}/Subscriptions.cs | 2 +- .../ToggleableConfigAttribute.cs | 2 +- OF DL/{Entities => Models}/User.cs | 2 +- OF DL/Program.cs | 14 +++++++------- OF DL/Services/APIService.cs | 18 +++++++++--------- OF DL/Services/AuthService.cs | 2 +- OF DL/Services/ConfigService.cs | 4 ++-- OF DL/Services/DownloadService.cs | 16 ++++++++-------- OF DL/Services/IAPIService.cs | 12 ++++++------ OF DL/Services/IAuthService.cs | 2 +- OF DL/Services/IConfigService.cs | 2 +- OF DL/Services/IDownloadService.cs | 16 ++++++++-------- 45 files changed, 85 insertions(+), 85 deletions(-) rename OF DL/{Entities => Models}/Archived/Archived.cs (99%) rename OF DL/{Entities => Models}/Archived/ArchivedCollection.cs (86%) rename OF DL/{Entities => Models}/Auth.cs (93%) rename OF DL/{Entities => Models}/CDRMProjectRequest.cs (94%) rename OF DL/{Entities => Models}/Config.cs (99%) rename OF DL/{Entities => Models}/DownloadResult.cs (97%) rename OF DL/{Entities => Models}/DynamicRules.cs (96%) rename OF DL/{Entities => Models}/FileNameFormatConfig.cs (90%) rename OF DL/{Entities => Models}/Highlights/HighlightMedia.cs (98%) rename OF DL/{Entities => Models}/Highlights/Highlights.cs (92%) rename OF DL/{Entities => Models}/IFileNameFormatConfig.cs (89%) rename OF DL/{Entities => Models}/LatestReleaseAPIResponse.cs (86%) rename OF DL/{Entities => Models}/Lists/UserList.cs (96%) rename OF DL/{Entities => Models}/Lists/UsersList.cs (99%) rename OF DL/{Entities => Models}/Messages/MessageCollection.cs (85%) rename OF DL/{Entities => Models}/Messages/Messages.cs (99%) rename OF DL/{Entities => Models}/Messages/SingleMessage.cs (97%) rename OF DL/{Entities => Models}/OFDLRequest.cs (91%) rename OF DL/{Entities => Models}/Post/Post.cs (99%) rename OF DL/{Entities => Models}/Post/PostCollection.cs (85%) rename OF DL/{Entities => Models}/Post/SinglePost.cs (99%) rename OF DL/{Entities => Models}/Post/SinglePostCollection.cs (87%) rename OF DL/{Entities => Models}/Purchased/PaidMessageCollection.cs (71%) rename OF DL/{Entities => Models}/Purchased/PaidPostCollection.cs (70%) rename OF DL/{Entities => Models}/Purchased/Purchased.cs (96%) rename OF DL/{Entities => Models}/Purchased/PurchasedTabCollection.cs (88%) rename OF DL/{Entities => Models}/Purchased/SinglePaidMessageCollection.cs (75%) rename OF DL/{Entities => Models}/ShortDateConverter.cs (86%) rename OF DL/{Entities => Models}/Stories/Stories.cs (98%) rename OF DL/{Entities => Models}/Streams/Streams.cs (99%) rename OF DL/{Entities => Models}/Streams/StreamsCollection.cs (85%) rename OF DL/{Entities => Models}/Subscriptions.cs (99%) rename OF DL/{Entities => Models}/ToggleableConfigAttribute.cs (77%) rename OF DL/{Entities => Models}/User.cs (99%) diff --git a/OF DL/Helpers/DownloadContext.cs b/OF DL/Helpers/DownloadContext.cs index 00f41f1..0ba77f4 100644 --- a/OF DL/Helpers/DownloadContext.cs +++ b/OF DL/Helpers/DownloadContext.cs @@ -1,4 +1,4 @@ -using OF_DL.Entities; +using OF_DL.Models; using OF_DL.Services; namespace OF_DL.Helpers; diff --git a/OF DL/Helpers/VersionHelper.cs b/OF DL/Helpers/VersionHelper.cs index 8d6913a..1d28844 100644 --- a/OF DL/Helpers/VersionHelper.cs +++ b/OF DL/Helpers/VersionHelper.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json; -using OF_DL.Entities; +using OF_DL.Models; using Serilog; namespace OF_DL.Helpers; diff --git a/OF DL/Entities/Archived/Archived.cs b/OF DL/Models/Archived/Archived.cs similarity index 99% rename from OF DL/Entities/Archived/Archived.cs rename to OF DL/Models/Archived/Archived.cs index a8ff9dd..2ec4d68 100644 --- a/OF DL/Entities/Archived/Archived.cs +++ b/OF DL/Models/Archived/Archived.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using OF_DL.Utils; -namespace OF_DL.Entities.Archived; +namespace OF_DL.Models.Archived; public class Archived { diff --git a/OF DL/Entities/Archived/ArchivedCollection.cs b/OF DL/Models/Archived/ArchivedCollection.cs similarity index 86% rename from OF DL/Entities/Archived/ArchivedCollection.cs rename to OF DL/Models/Archived/ArchivedCollection.cs index c61931a..b4f6d6d 100644 --- a/OF DL/Entities/Archived/ArchivedCollection.cs +++ b/OF DL/Models/Archived/ArchivedCollection.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities.Archived; +namespace OF_DL.Models.Archived; public class ArchivedCollection { diff --git a/OF DL/Entities/Auth.cs b/OF DL/Models/Auth.cs similarity index 93% rename from OF DL/Entities/Auth.cs rename to OF DL/Models/Auth.cs index 58b9da5..e718a53 100644 --- a/OF DL/Entities/Auth.cs +++ b/OF DL/Models/Auth.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Entities; +namespace OF_DL.Models; public class Auth { diff --git a/OF DL/Entities/CDRMProjectRequest.cs b/OF DL/Models/CDRMProjectRequest.cs similarity index 94% rename from OF DL/Entities/CDRMProjectRequest.cs rename to OF DL/Models/CDRMProjectRequest.cs index 6f92897..182e827 100644 --- a/OF DL/Entities/CDRMProjectRequest.cs +++ b/OF DL/Models/CDRMProjectRequest.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Entities; +namespace OF_DL.Models; public class CDRMProjectRequest { diff --git a/OF DL/Entities/Config.cs b/OF DL/Models/Config.cs similarity index 99% rename from OF DL/Entities/Config.cs rename to OF DL/Models/Config.cs index 814cc5c..05aa188 100644 --- a/OF DL/Entities/Config.cs +++ b/OF DL/Models/Config.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json.Converters; using OF_DL.Enumerations; using Serilog; -namespace OF_DL.Entities; +namespace OF_DL.Models; public class Config : IFileNameFormatConfig { diff --git a/OF DL/Entities/DownloadResult.cs b/OF DL/Models/DownloadResult.cs similarity index 97% rename from OF DL/Entities/DownloadResult.cs rename to OF DL/Models/DownloadResult.cs index 0bfc589..1f2a6bc 100644 --- a/OF DL/Entities/DownloadResult.cs +++ b/OF DL/Models/DownloadResult.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities; +namespace OF_DL.Models; /// /// Represents the result of a download operation. diff --git a/OF DL/Entities/DynamicRules.cs b/OF DL/Models/DynamicRules.cs similarity index 96% rename from OF DL/Entities/DynamicRules.cs rename to OF DL/Models/DynamicRules.cs index fc7684a..d1e8968 100644 --- a/OF DL/Entities/DynamicRules.cs +++ b/OF DL/Models/DynamicRules.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Entities; +namespace OF_DL.Models; public class DynamicRules { diff --git a/OF DL/Entities/FileNameFormatConfig.cs b/OF DL/Models/FileNameFormatConfig.cs similarity index 90% rename from OF DL/Entities/FileNameFormatConfig.cs rename to OF DL/Models/FileNameFormatConfig.cs index 46e36d5..9b706af 100644 --- a/OF DL/Entities/FileNameFormatConfig.cs +++ b/OF DL/Models/FileNameFormatConfig.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities; +namespace OF_DL.Models; public class FileNameFormatConfig : IFileNameFormatConfig { diff --git a/OF DL/Entities/Highlights/HighlightMedia.cs b/OF DL/Models/Highlights/HighlightMedia.cs similarity index 98% rename from OF DL/Entities/Highlights/HighlightMedia.cs rename to OF DL/Models/Highlights/HighlightMedia.cs index c75e01a..93c4c92 100644 --- a/OF DL/Entities/Highlights/HighlightMedia.cs +++ b/OF DL/Models/Highlights/HighlightMedia.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Entities.Highlights; +namespace OF_DL.Models.Highlights; public class HighlightMedia { diff --git a/OF DL/Entities/Highlights/Highlights.cs b/OF DL/Models/Highlights/Highlights.cs similarity index 92% rename from OF DL/Entities/Highlights/Highlights.cs rename to OF DL/Models/Highlights/Highlights.cs index a5fa1b0..7057a8a 100644 --- a/OF DL/Entities/Highlights/Highlights.cs +++ b/OF DL/Models/Highlights/Highlights.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities.Highlights; +namespace OF_DL.Models.Highlights; public class Highlights { diff --git a/OF DL/Entities/IFileNameFormatConfig.cs b/OF DL/Models/IFileNameFormatConfig.cs similarity index 89% rename from OF DL/Entities/IFileNameFormatConfig.cs rename to OF DL/Models/IFileNameFormatConfig.cs index 663cc47..ac774dd 100644 --- a/OF DL/Entities/IFileNameFormatConfig.cs +++ b/OF DL/Models/IFileNameFormatConfig.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities; +namespace OF_DL.Models; public interface IFileNameFormatConfig { diff --git a/OF DL/Entities/LatestReleaseAPIResponse.cs b/OF DL/Models/LatestReleaseAPIResponse.cs similarity index 86% rename from OF DL/Entities/LatestReleaseAPIResponse.cs rename to OF DL/Models/LatestReleaseAPIResponse.cs index 1081765..be2322d 100644 --- a/OF DL/Entities/LatestReleaseAPIResponse.cs +++ b/OF DL/Models/LatestReleaseAPIResponse.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Entities; +namespace OF_DL.Models; public class LatestReleaseAPIResponse { diff --git a/OF DL/Entities/Lists/UserList.cs b/OF DL/Models/Lists/UserList.cs similarity index 96% rename from OF DL/Entities/Lists/UserList.cs rename to OF DL/Models/Lists/UserList.cs index 0c833a8..60248cf 100644 --- a/OF DL/Entities/Lists/UserList.cs +++ b/OF DL/Models/Lists/UserList.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities.Lists; +namespace OF_DL.Models.Lists; public class UserList { diff --git a/OF DL/Entities/Lists/UsersList.cs b/OF DL/Models/Lists/UsersList.cs similarity index 99% rename from OF DL/Entities/Lists/UsersList.cs rename to OF DL/Models/Lists/UsersList.cs index 77c0095..806b4aa 100644 --- a/OF DL/Entities/Lists/UsersList.cs +++ b/OF DL/Models/Lists/UsersList.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities.Lists; +namespace OF_DL.Models.Lists; public class UsersList { diff --git a/OF DL/Entities/Messages/MessageCollection.cs b/OF DL/Models/Messages/MessageCollection.cs similarity index 85% rename from OF DL/Entities/Messages/MessageCollection.cs rename to OF DL/Models/Messages/MessageCollection.cs index c12cfcb..00321c0 100644 --- a/OF DL/Entities/Messages/MessageCollection.cs +++ b/OF DL/Models/Messages/MessageCollection.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities.Messages; +namespace OF_DL.Models.Messages; public class MessageCollection { diff --git a/OF DL/Entities/Messages/Messages.cs b/OF DL/Models/Messages/Messages.cs similarity index 99% rename from OF DL/Entities/Messages/Messages.cs rename to OF DL/Models/Messages/Messages.cs index ce05661..b0c72ef 100644 --- a/OF DL/Entities/Messages/Messages.cs +++ b/OF DL/Models/Messages/Messages.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Entities.Messages; +namespace OF_DL.Models.Messages; public class Messages { diff --git a/OF DL/Entities/Messages/SingleMessage.cs b/OF DL/Models/Messages/SingleMessage.cs similarity index 97% rename from OF DL/Entities/Messages/SingleMessage.cs rename to OF DL/Models/Messages/SingleMessage.cs index 69e43f9..ffaee05 100644 --- a/OF DL/Entities/Messages/SingleMessage.cs +++ b/OF DL/Models/Messages/SingleMessage.cs @@ -1,6 +1,6 @@ -using static OF_DL.Entities.Messages.Messages; +using static OF_DL.Models.Messages.Messages; -namespace OF_DL.Entities.Messages; +namespace OF_DL.Models.Messages; public class AvatarThumbs { diff --git a/OF DL/Entities/OFDLRequest.cs b/OF DL/Models/OFDLRequest.cs similarity index 91% rename from OF DL/Entities/OFDLRequest.cs rename to OF DL/Models/OFDLRequest.cs index b5d4184..5af7a25 100644 --- a/OF DL/Entities/OFDLRequest.cs +++ b/OF DL/Models/OFDLRequest.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Entities; +namespace OF_DL.Models; public class OFDLRequest { diff --git a/OF DL/Entities/Post/Post.cs b/OF DL/Models/Post/Post.cs similarity index 99% rename from OF DL/Entities/Post/Post.cs rename to OF DL/Models/Post/Post.cs index de711d4..e61a48c 100644 --- a/OF DL/Entities/Post/Post.cs +++ b/OF DL/Models/Post/Post.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using OF_DL.Utils; -namespace OF_DL.Entities.Post; +namespace OF_DL.Models.Post; #pragma warning disable IDE1006 // Naming Styles public class Post diff --git a/OF DL/Entities/Post/PostCollection.cs b/OF DL/Models/Post/PostCollection.cs similarity index 85% rename from OF DL/Entities/Post/PostCollection.cs rename to OF DL/Models/Post/PostCollection.cs index 541c728..861a20c 100644 --- a/OF DL/Entities/Post/PostCollection.cs +++ b/OF DL/Models/Post/PostCollection.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities.Post; +namespace OF_DL.Models.Post; public class PostCollection { diff --git a/OF DL/Entities/Post/SinglePost.cs b/OF DL/Models/Post/SinglePost.cs similarity index 99% rename from OF DL/Entities/Post/SinglePost.cs rename to OF DL/Models/Post/SinglePost.cs index 7f137fc..d3b2fba 100644 --- a/OF DL/Entities/Post/SinglePost.cs +++ b/OF DL/Models/Post/SinglePost.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using OF_DL.Utils; -namespace OF_DL.Entities.Post; +namespace OF_DL.Models.Post; public class SinglePost { diff --git a/OF DL/Entities/Post/SinglePostCollection.cs b/OF DL/Models/Post/SinglePostCollection.cs similarity index 87% rename from OF DL/Entities/Post/SinglePostCollection.cs rename to OF DL/Models/Post/SinglePostCollection.cs index 272dda8..d1992b8 100644 --- a/OF DL/Entities/Post/SinglePostCollection.cs +++ b/OF DL/Models/Post/SinglePostCollection.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities.Post; +namespace OF_DL.Models.Post; public class SinglePostCollection { diff --git a/OF DL/Entities/Purchased/PaidMessageCollection.cs b/OF DL/Models/Purchased/PaidMessageCollection.cs similarity index 71% rename from OF DL/Entities/Purchased/PaidMessageCollection.cs rename to OF DL/Models/Purchased/PaidMessageCollection.cs index c75fc2c..0ca1548 100644 --- a/OF DL/Entities/Purchased/PaidMessageCollection.cs +++ b/OF DL/Models/Purchased/PaidMessageCollection.cs @@ -1,6 +1,6 @@ -using static OF_DL.Entities.Messages.Messages; +using static OF_DL.Models.Messages.Messages; -namespace OF_DL.Entities.Purchased; +namespace OF_DL.Models.Purchased; public class PaidMessageCollection { diff --git a/OF DL/Entities/Purchased/PaidPostCollection.cs b/OF DL/Models/Purchased/PaidPostCollection.cs similarity index 70% rename from OF DL/Entities/Purchased/PaidPostCollection.cs rename to OF DL/Models/Purchased/PaidPostCollection.cs index d0cfccf..4def769 100644 --- a/OF DL/Entities/Purchased/PaidPostCollection.cs +++ b/OF DL/Models/Purchased/PaidPostCollection.cs @@ -1,6 +1,6 @@ -using static OF_DL.Entities.Messages.Messages; +using static OF_DL.Models.Messages.Messages; -namespace OF_DL.Entities.Purchased; +namespace OF_DL.Models.Purchased; public class PaidPostCollection { diff --git a/OF DL/Entities/Purchased/Purchased.cs b/OF DL/Models/Purchased/Purchased.cs similarity index 96% rename from OF DL/Entities/Purchased/Purchased.cs rename to OF DL/Models/Purchased/Purchased.cs index 8b470b7..671ed3b 100644 --- a/OF DL/Entities/Purchased/Purchased.cs +++ b/OF DL/Models/Purchased/Purchased.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; -using static OF_DL.Entities.Messages.Messages; +using static OF_DL.Models.Messages.Messages; -namespace OF_DL.Entities.Purchased; +namespace OF_DL.Models.Purchased; public class Purchased { diff --git a/OF DL/Entities/Purchased/PurchasedTabCollection.cs b/OF DL/Models/Purchased/PurchasedTabCollection.cs similarity index 88% rename from OF DL/Entities/Purchased/PurchasedTabCollection.cs rename to OF DL/Models/Purchased/PurchasedTabCollection.cs index 4d39a33..f41710c 100644 --- a/OF DL/Entities/Purchased/PurchasedTabCollection.cs +++ b/OF DL/Models/Purchased/PurchasedTabCollection.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities.Purchased; +namespace OF_DL.Models.Purchased; public class PurchasedTabCollection { diff --git a/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs b/OF DL/Models/Purchased/SinglePaidMessageCollection.cs similarity index 75% rename from OF DL/Entities/Purchased/SinglePaidMessageCollection.cs rename to OF DL/Models/Purchased/SinglePaidMessageCollection.cs index 1e5c4dc..db60540 100644 --- a/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs +++ b/OF DL/Models/Purchased/SinglePaidMessageCollection.cs @@ -1,7 +1,7 @@ -using OF_DL.Entities.Messages; -using static OF_DL.Entities.Messages.Messages; +using OF_DL.Models.Messages; +using static OF_DL.Models.Messages.Messages; -namespace OF_DL.Entities.Purchased; +namespace OF_DL.Models.Purchased; public class SinglePaidMessageCollection { diff --git a/OF DL/Entities/ShortDateConverter.cs b/OF DL/Models/ShortDateConverter.cs similarity index 86% rename from OF DL/Entities/ShortDateConverter.cs rename to OF DL/Models/ShortDateConverter.cs index 11ac1d6..7a9f62c 100644 --- a/OF DL/Entities/ShortDateConverter.cs +++ b/OF DL/Models/ShortDateConverter.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json.Converters; -namespace OF_DL.Entities; +namespace OF_DL.Models; public class ShortDateConverter : IsoDateTimeConverter { diff --git a/OF DL/Entities/Stories/Stories.cs b/OF DL/Models/Stories/Stories.cs similarity index 98% rename from OF DL/Entities/Stories/Stories.cs rename to OF DL/Models/Stories/Stories.cs index dc03628..9aede74 100644 --- a/OF DL/Entities/Stories/Stories.cs +++ b/OF DL/Models/Stories/Stories.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Entities.Stories; +namespace OF_DL.Models.Stories; public class Stories { diff --git a/OF DL/Entities/Streams/Streams.cs b/OF DL/Models/Streams/Streams.cs similarity index 99% rename from OF DL/Entities/Streams/Streams.cs rename to OF DL/Models/Streams/Streams.cs index 872d191..de993ef 100644 --- a/OF DL/Entities/Streams/Streams.cs +++ b/OF DL/Models/Streams/Streams.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using OF_DL.Utils; -namespace OF_DL.Entities.Streams; +namespace OF_DL.Models.Streams; public class Streams { diff --git a/OF DL/Entities/Streams/StreamsCollection.cs b/OF DL/Models/Streams/StreamsCollection.cs similarity index 85% rename from OF DL/Entities/Streams/StreamsCollection.cs rename to OF DL/Models/Streams/StreamsCollection.cs index bc895a3..126c112 100644 --- a/OF DL/Entities/Streams/StreamsCollection.cs +++ b/OF DL/Models/Streams/StreamsCollection.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities.Streams; +namespace OF_DL.Models.Streams; public class StreamsCollection { diff --git a/OF DL/Entities/Subscriptions.cs b/OF DL/Models/Subscriptions.cs similarity index 99% rename from OF DL/Entities/Subscriptions.cs rename to OF DL/Models/Subscriptions.cs index 22973d6..5ae8a43 100644 --- a/OF DL/Entities/Subscriptions.cs +++ b/OF DL/Models/Subscriptions.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities; +namespace OF_DL.Models; public class Subscriptions { diff --git a/OF DL/Entities/ToggleableConfigAttribute.cs b/OF DL/Models/ToggleableConfigAttribute.cs similarity index 77% rename from OF DL/Entities/ToggleableConfigAttribute.cs rename to OF DL/Models/ToggleableConfigAttribute.cs index 62d413f..f04dcc0 100644 --- a/OF DL/Entities/ToggleableConfigAttribute.cs +++ b/OF DL/Models/ToggleableConfigAttribute.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities; +namespace OF_DL.Models; [AttributeUsage(AttributeTargets.Property)] internal class ToggleableConfigAttribute : Attribute diff --git a/OF DL/Entities/User.cs b/OF DL/Models/User.cs similarity index 99% rename from OF DL/Entities/User.cs rename to OF DL/Models/User.cs index 606f2ac..5ea7d48 100644 --- a/OF DL/Entities/User.cs +++ b/OF DL/Models/User.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Entities; +namespace OF_DL.Models; public class User { diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 754e8c4..1e14abe 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -6,18 +6,18 @@ using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.CLI; -using OF_DL.Entities; -using OF_DL.Entities.Archived; -using OF_DL.Entities.Messages; -using OF_DL.Entities.Post; -using OF_DL.Entities.Purchased; -using OF_DL.Entities.Streams; +using OF_DL.Models; +using OF_DL.Models.Archived; +using OF_DL.Models.Messages; +using OF_DL.Models.Post; +using OF_DL.Models.Purchased; +using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Helpers; using OF_DL.Services; using Serilog; using Spectre.Console; -using static OF_DL.Entities.Messages.Messages; +using static OF_DL.Models.Messages.Messages; using Constants = OF_DL.Widevine.Constants; namespace OF_DL; diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 8132cd7..a02f8f7 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -5,15 +5,15 @@ using System.Text.Json; using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OF_DL.Entities; -using OF_DL.Entities.Archived; -using OF_DL.Entities.Highlights; -using OF_DL.Entities.Lists; -using OF_DL.Entities.Messages; -using OF_DL.Entities.Post; -using OF_DL.Entities.Purchased; -using OF_DL.Entities.Stories; -using OF_DL.Entities.Streams; +using OF_DL.Models; +using OF_DL.Models.Archived; +using OF_DL.Models.Highlights; +using OF_DL.Models.Lists; +using OF_DL.Models.Messages; +using OF_DL.Models.Post; +using OF_DL.Models.Purchased; +using OF_DL.Models.Stories; +using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Widevine; using Serilog; diff --git a/OF DL/Services/AuthService.cs b/OF DL/Services/AuthService.cs index 17dfd81..8887731 100644 --- a/OF DL/Services/AuthService.cs +++ b/OF DL/Services/AuthService.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json; -using OF_DL.Entities; +using OF_DL.Models; using PuppeteerSharp; using PuppeteerSharp.BrowserData; using Serilog; diff --git a/OF DL/Services/ConfigService.cs b/OF DL/Services/ConfigService.cs index 0b175da..d22c617 100644 --- a/OF DL/Services/ConfigService.cs +++ b/OF DL/Services/ConfigService.cs @@ -2,11 +2,11 @@ using System.Text; using Akka.Configuration; using Akka.Configuration.Hocon; using Newtonsoft.Json; -using OF_DL.Entities; +using OF_DL.Models; using OF_DL.Enumerations; using OF_DL.Utils; using Serilog; -using Config = OF_DL.Entities.Config; +using Config = OF_DL.Models.Config; namespace OF_DL.Services; diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 9fe84c4..2867dc0 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -3,18 +3,18 @@ using System.Text.RegularExpressions; using System.Xml.Linq; using FFmpeg.NET; using FFmpeg.NET.Events; -using OF_DL.Entities; -using OF_DL.Entities.Archived; -using OF_DL.Entities.Messages; -using OF_DL.Entities.Post; -using OF_DL.Entities.Purchased; -using OF_DL.Entities.Streams; +using OF_DL.Models; +using OF_DL.Models.Archived; +using OF_DL.Models.Messages; +using OF_DL.Models.Post; +using OF_DL.Models.Purchased; +using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Utils; using Serilog; using Serilog.Events; -using static OF_DL.Entities.Messages.Messages; -using FromUser = OF_DL.Entities.Messages.FromUser; +using static OF_DL.Models.Messages.Messages; +using FromUser = OF_DL.Models.Messages.FromUser; namespace OF_DL.Services; diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index 3163062..869f50e 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -1,10 +1,10 @@ using Newtonsoft.Json.Linq; -using OF_DL.Entities; -using OF_DL.Entities.Archived; -using OF_DL.Entities.Messages; -using OF_DL.Entities.Post; -using OF_DL.Entities.Purchased; -using OF_DL.Entities.Streams; +using OF_DL.Models; +using OF_DL.Models.Archived; +using OF_DL.Models.Messages; +using OF_DL.Models.Post; +using OF_DL.Models.Purchased; +using OF_DL.Models.Streams; using OF_DL.Enumerations; using Spectre.Console; diff --git a/OF DL/Services/IAuthService.cs b/OF DL/Services/IAuthService.cs index e79ad43..485cfdc 100644 --- a/OF DL/Services/IAuthService.cs +++ b/OF DL/Services/IAuthService.cs @@ -1,4 +1,4 @@ -using OF_DL.Entities; +using OF_DL.Models; namespace OF_DL.Services; diff --git a/OF DL/Services/IConfigService.cs b/OF DL/Services/IConfigService.cs index fd7c35d..abb535b 100644 --- a/OF DL/Services/IConfigService.cs +++ b/OF DL/Services/IConfigService.cs @@ -1,4 +1,4 @@ -using OF_DL.Entities; +using OF_DL.Models; namespace OF_DL.Services; diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index 11d7c96..399702b 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -1,11 +1,11 @@ -using OF_DL.Entities; -using OF_DL.Entities.Archived; -using OF_DL.Entities.Messages; -using OF_DL.Entities.Post; -using OF_DL.Entities.Purchased; -using OF_DL.Entities.Streams; -using static OF_DL.Entities.Messages.Messages; -using FromUser = OF_DL.Entities.Messages.FromUser; +using OF_DL.Models; +using OF_DL.Models.Archived; +using OF_DL.Models.Messages; +using OF_DL.Models.Post; +using OF_DL.Models.Purchased; +using OF_DL.Models.Streams; +using static OF_DL.Models.Messages.Messages; +using FromUser = OF_DL.Models.Messages.FromUser; namespace OF_DL.Services; From 35712da12d7ab28175a64731b8db9aaa05b0e92d Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Sun, 8 Feb 2026 15:29:42 -0600 Subject: [PATCH 12/55] Refactor Archived entities into DTOs and application entities with standardized naming conventions and default values --- OF DL/Models/Archived/Archived.cs | 262 ------------------ OF DL/Models/Archived/ArchivedCollection.cs | 8 - OF DL/Models/Dtos/Archived/ArchivedDto.cs | 16 ++ OF DL/Models/Dtos/Archived/CountersDto.cs | 20 ++ OF DL/Models/Dtos/Archived/InfoDto.cs | 11 + OF DL/Models/Dtos/Archived/LinkedPostDto.cs | 96 +++++++ OF DL/Models/Dtos/Archived/ListItemDto.cs | 97 +++++++ OF DL/Models/Dtos/Archived/MediumDto.cs | 35 +++ OF DL/Models/Dtos/Common/AuthorDto.cs | 10 + OF DL/Models/Dtos/Common/DashDto.cs | 13 + OF DL/Models/Dtos/Common/DrmDto.cs | 11 + OF DL/Models/Dtos/Common/FilesDto.cs | 17 ++ OF DL/Models/Dtos/Common/FullDto.cs | 16 ++ OF DL/Models/Dtos/Common/HlsDto.cs | 13 + OF DL/Models/Dtos/Common/ManifestDto.cs | 10 + OF DL/Models/Dtos/Common/PreviewDto.cs | 14 + OF DL/Models/Dtos/Common/SignatureDto.cs | 10 + OF DL/Models/Dtos/Common/SourceDto.cs | 18 ++ OF DL/Models/Dtos/Common/SourcesDto.cs | 14 + OF DL/Models/Dtos/Common/SquarePreviewDto.cs | 14 + OF DL/Models/Dtos/Common/ThumbDto.cs | 14 + OF DL/Models/Dtos/Common/VideoSourcesDto.cs | 10 + OF DL/Models/Entities/Archived/Archived.cs | 10 + .../Entities/Archived/ArchivedCollection.cs | 10 + OF DL/Models/Entities/Archived/ListItem.cs | 24 ++ OF DL/Models/Entities/Archived/Medium.cs | 16 ++ OF DL/Models/Entities/Common/Author.cs | 6 + OF DL/Models/Entities/Common/Dash.cs | 10 + OF DL/Models/Entities/Common/Drm.cs | 8 + OF DL/Models/Entities/Common/Files.cs | 8 + OF DL/Models/Entities/Common/Full.cs | 6 + OF DL/Models/Entities/Common/Manifest.cs | 6 + OF DL/Models/Entities/Common/Signature.cs | 6 + OF DL/Models/Mappers/ArchivedMapper.cs | 137 +++++++++ OF DL/Program.cs | 19 +- OF DL/Services/APIService.cs | 88 +++--- OF DL/Services/DownloadService.cs | 73 +++-- OF DL/Services/IAPIService.cs | 2 +- OF DL/Services/IDownloadService.cs | 40 ++- 39 files changed, 831 insertions(+), 367 deletions(-) delete mode 100644 OF DL/Models/Archived/Archived.cs delete mode 100644 OF DL/Models/Archived/ArchivedCollection.cs create mode 100644 OF DL/Models/Dtos/Archived/ArchivedDto.cs create mode 100644 OF DL/Models/Dtos/Archived/CountersDto.cs create mode 100644 OF DL/Models/Dtos/Archived/InfoDto.cs create mode 100644 OF DL/Models/Dtos/Archived/LinkedPostDto.cs create mode 100644 OF DL/Models/Dtos/Archived/ListItemDto.cs create mode 100644 OF DL/Models/Dtos/Archived/MediumDto.cs create mode 100644 OF DL/Models/Dtos/Common/AuthorDto.cs create mode 100644 OF DL/Models/Dtos/Common/DashDto.cs create mode 100644 OF DL/Models/Dtos/Common/DrmDto.cs create mode 100644 OF DL/Models/Dtos/Common/FilesDto.cs create mode 100644 OF DL/Models/Dtos/Common/FullDto.cs create mode 100644 OF DL/Models/Dtos/Common/HlsDto.cs create mode 100644 OF DL/Models/Dtos/Common/ManifestDto.cs create mode 100644 OF DL/Models/Dtos/Common/PreviewDto.cs create mode 100644 OF DL/Models/Dtos/Common/SignatureDto.cs create mode 100644 OF DL/Models/Dtos/Common/SourceDto.cs create mode 100644 OF DL/Models/Dtos/Common/SourcesDto.cs create mode 100644 OF DL/Models/Dtos/Common/SquarePreviewDto.cs create mode 100644 OF DL/Models/Dtos/Common/ThumbDto.cs create mode 100644 OF DL/Models/Dtos/Common/VideoSourcesDto.cs create mode 100644 OF DL/Models/Entities/Archived/Archived.cs create mode 100644 OF DL/Models/Entities/Archived/ArchivedCollection.cs create mode 100644 OF DL/Models/Entities/Archived/ListItem.cs create mode 100644 OF DL/Models/Entities/Archived/Medium.cs create mode 100644 OF DL/Models/Entities/Common/Author.cs create mode 100644 OF DL/Models/Entities/Common/Dash.cs create mode 100644 OF DL/Models/Entities/Common/Drm.cs create mode 100644 OF DL/Models/Entities/Common/Files.cs create mode 100644 OF DL/Models/Entities/Common/Full.cs create mode 100644 OF DL/Models/Entities/Common/Manifest.cs create mode 100644 OF DL/Models/Entities/Common/Signature.cs create mode 100644 OF DL/Models/Mappers/ArchivedMapper.cs diff --git a/OF DL/Models/Archived/Archived.cs b/OF DL/Models/Archived/Archived.cs deleted file mode 100644 index 2ec4d68..0000000 --- a/OF DL/Models/Archived/Archived.cs +++ /dev/null @@ -1,262 +0,0 @@ -using Newtonsoft.Json; -using OF_DL.Utils; - -namespace OF_DL.Models.Archived; - -public class Archived -{ - public List list { get; set; } - public bool hasMore { get; set; } - public string headMarker { get; set; } - public string tailMarker { get; set; } - public Counters counters { get; set; } - - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } - - public class Counters - { - public int? audiosCount { get; set; } - public int? photosCount { get; set; } - public int? videosCount { get; set; } - public int? mediasCount { get; set; } - public int? postsCount { get; set; } - public int? streamsCount { get; set; } - public int? archivedPostsCount { get; set; } - } - - public class Dash - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } - - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } - - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Hls - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } - - public class LinkedPost - { - private string _rawText; - public string responseType { get; set; } - public long? id { get; set; } - public DateTime? postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - - public string rawText - { - get - { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } - - return _rawText; - } - set => _rawText = value; - } - - public bool? lockedText { get; set; } - public bool? isFavorite { get; set; } - public bool? canReport { get; set; } - public bool? canDelete { get; set; } - public bool? canComment { get; set; } - public bool? canEdit { get; set; } - public bool? isPinned { get; set; } - public int? favoritesCount { get; set; } - public int? mediaCount { get; set; } - public bool? isMediaReady { get; set; } - public object voting { get; set; } - public bool? isOpened { get; set; } - public bool? canToggleFavorite { get; set; } - public object streamId { get; set; } - public string? price { get; set; } - public bool? hasVoting { get; set; } - public bool? isAddedToBookmarks { get; set; } - public bool? isArchived { get; set; } - public bool? isPrivateArchived { get; set; } - public bool? isDeleted { get; set; } - public bool? hasUrl { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public string cantCommentReason { get; set; } - public int? commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public List media { get; set; } - public bool? canViewMedia { get; set; } - public List preview { get; set; } - } - - public class List - { - private string _rawText; - public string responseType { get; set; } - public long id { get; set; } - public DateTime postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - - public string rawText - { - get - { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } - - return _rawText; - } - set => _rawText = value; - } - - public bool? lockedText { get; set; } - public bool? isFavorite { get; set; } - public bool? canReport { get; set; } - public bool? canDelete { get; set; } - public bool? canComment { get; set; } - public bool? canEdit { get; set; } - public bool? isPinned { get; set; } - public int? favoritesCount { get; set; } - public int? mediaCount { get; set; } - public bool? isMediaReady { get; set; } - public object voting { get; set; } - public bool isOpened { get; set; } - public bool? canToggleFavorite { get; set; } - public object streamId { get; set; } - public string price { get; set; } - public bool? hasVoting { get; set; } - public bool? isAddedToBookmarks { get; set; } - public bool isArchived { get; set; } - public bool? isPrivateArchived { get; set; } - public bool? isDeleted { get; set; } - public bool? hasUrl { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public int? commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public List media { get; set; } - public bool? canViewMedia { get; set; } - public List preview { get; set; } - public string cantCommentReason { get; set; } - } - - public class Manifest - { - public string hls { get; set; } - public string dash { get; set; } - } - - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool? convertedToVideo { get; set; } - public bool canView { get; set; } - public bool? hasError { get; set; } - public DateTime? createdAt { get; set; } - public Info info { get; set; } - public Source source { get; set; } - public string squarePreview { get; set; } - public string full { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public Files files { get; set; } - public VideoSources videoSources { get; set; } - } - - public class Preview - { - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - public string url { get; set; } - } - - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } - - public class Source - { - public string source { get; set; } - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - public int? duration { get; set; } - } - - public class VideoSources - { - [JsonProperty("720")] public string _720 { get; set; } - - [JsonProperty("240")] public string _240 { get; set; } - } -} diff --git a/OF DL/Models/Archived/ArchivedCollection.cs b/OF DL/Models/Archived/ArchivedCollection.cs deleted file mode 100644 index b4f6d6d..0000000 --- a/OF DL/Models/Archived/ArchivedCollection.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OF_DL.Models.Archived; - -public class ArchivedCollection -{ - public List ArchivedPostMedia = new(); - public List ArchivedPostObjects = new(); - public Dictionary ArchivedPosts = new(); -} diff --git a/OF DL/Models/Dtos/Archived/ArchivedDto.cs b/OF DL/Models/Dtos/Archived/ArchivedDto.cs new file mode 100644 index 0000000..80d373a --- /dev/null +++ b/OF DL/Models/Dtos/Archived/ArchivedDto.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Archived; + +public class ArchivedDto +{ + [JsonProperty("list")] public List List { get; set; } = []; + + [JsonProperty("hasMore")] public bool HasMore { get; set; } + + [JsonProperty("headMarker")] public string HeadMarker { get; set; } = ""; + + [JsonProperty("tailMarker")] public string TailMarker { get; set; } = ""; + + [JsonProperty("counters")] public CountersDto Counters { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Archived/CountersDto.cs b/OF DL/Models/Dtos/Archived/CountersDto.cs new file mode 100644 index 0000000..40e2500 --- /dev/null +++ b/OF DL/Models/Dtos/Archived/CountersDto.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Archived; + +public class CountersDto +{ + [JsonProperty("audiosCount")] public int? AudiosCount { get; set; } + + [JsonProperty("photosCount")] public int? PhotosCount { get; set; } + + [JsonProperty("videosCount")] public int? VideosCount { get; set; } + + [JsonProperty("mediasCount")] public int? MediasCount { get; set; } + + [JsonProperty("postsCount")] public int? PostsCount { get; set; } + + [JsonProperty("streamsCount")] public int? StreamsCount { get; set; } + + [JsonProperty("archivedPostsCount")] public int? ArchivedPostsCount { get; set; } +} diff --git a/OF DL/Models/Dtos/Archived/InfoDto.cs b/OF DL/Models/Dtos/Archived/InfoDto.cs new file mode 100644 index 0000000..1e4b13d --- /dev/null +++ b/OF DL/Models/Dtos/Archived/InfoDto.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Archived; + +public class InfoDto +{ + [JsonProperty("source")] public SourceDto Source { get; set; } = new(); + + [JsonProperty("preview")] public PreviewDto Preview { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Archived/LinkedPostDto.cs b/OF DL/Models/Dtos/Archived/LinkedPostDto.cs new file mode 100644 index 0000000..3bedcae --- /dev/null +++ b/OF DL/Models/Dtos/Archived/LinkedPostDto.cs @@ -0,0 +1,96 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; +using OF_DL.Utils; + +namespace OF_DL.Models.Dtos.Archived; + +public class LinkedPostDto +{ + private string _rawText = ""; + [JsonProperty("responseType")] public string ResponseType { get; set; } = ""; + + [JsonProperty("id")] public long? Id { get; set; } + + [JsonProperty("postedAt")] public DateTime? PostedAt { get; set; } + + [JsonProperty("postedAtPrecise")] public string PostedAtPrecise { get; set; } = ""; + + [JsonProperty("expiredAt")] public object ExpiredAt { get; set; } = new(); + + [JsonProperty("author")] public AuthorDto Author { get; set; } = new(); + + [JsonProperty("text")] public string Text { get; set; } = ""; + + [JsonProperty("rawText")] + public string RawText + { + get + { + if (string.IsNullOrEmpty(_rawText)) + { + _rawText = XmlUtils.EvaluateInnerText(Text); + } + + return _rawText; + } + set => _rawText = value; + } + + [JsonProperty("lockedText")] public bool? LockedText { get; set; } + + [JsonProperty("isFavorite")] public bool? IsFavorite { get; set; } + + [JsonProperty("canReport")] public bool? CanReport { get; set; } + + [JsonProperty("canDelete")] public bool? CanDelete { get; set; } + + [JsonProperty("canComment")] public bool? CanComment { get; set; } + + [JsonProperty("canEdit")] public bool? CanEdit { get; set; } + + [JsonProperty("isPinned")] public bool? IsPinned { get; set; } + + [JsonProperty("favoritesCount")] public int? FavoritesCount { get; set; } + + [JsonProperty("mediaCount")] public int? MediaCount { get; set; } + + [JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; } + + [JsonProperty("voting")] public object Voting { get; set; } = new(); + + [JsonProperty("isOpened")] public bool? IsOpened { get; set; } + + [JsonProperty("canToggleFavorite")] public bool? CanToggleFavorite { get; set; } + + [JsonProperty("streamId")] public object StreamId { get; set; } = new(); + + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("hasVoting")] public bool? HasVoting { get; set; } + + [JsonProperty("isAddedToBookmarks")] public bool? IsAddedToBookmarks { get; set; } + + [JsonProperty("isArchived")] public bool? IsArchived { get; set; } + + [JsonProperty("isPrivateArchived")] public bool? IsPrivateArchived { get; set; } + + [JsonProperty("isDeleted")] public bool? IsDeleted { get; set; } + + [JsonProperty("hasUrl")] public bool? HasUrl { get; set; } + + [JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; } + + [JsonProperty("cantCommentReason")] public string CantCommentReason { get; set; } = ""; + + [JsonProperty("commentsCount")] public int? CommentsCount { get; set; } + + [JsonProperty("mentionedUsers")] public List MentionedUsers { get; set; } = []; + + [JsonProperty("linkedUsers")] public List LinkedUsers { get; set; } = []; + + [JsonProperty("media")] public List Media { get; set; } = []; + + [JsonProperty("canViewMedia")] public bool? CanViewMedia { get; set; } + + [JsonProperty("preview")] public List Preview { get; set; } = []; +} diff --git a/OF DL/Models/Dtos/Archived/ListItemDto.cs b/OF DL/Models/Dtos/Archived/ListItemDto.cs new file mode 100644 index 0000000..d2106ed --- /dev/null +++ b/OF DL/Models/Dtos/Archived/ListItemDto.cs @@ -0,0 +1,97 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; +using OF_DL.Utils; + +namespace OF_DL.Models.Dtos.Archived; + +public class ListItemDto +{ + private string _rawText = ""; + + [JsonProperty("responseType")] public string ResponseType { get; set; } = ""; + + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("postedAt")] public DateTime PostedAt { get; set; } + + [JsonProperty("postedAtPrecise")] public string PostedAtPrecise { get; set; } = ""; + + [JsonProperty("expiredAt")] public object ExpiredAt { get; set; } = new(); + + [JsonProperty("author")] public AuthorDto Author { get; set; } = new(); + + [JsonProperty("text")] public string Text { get; set; } = ""; + + [JsonProperty("rawText")] + public string RawText + { + get + { + if (string.IsNullOrEmpty(_rawText)) + { + _rawText = XmlUtils.EvaluateInnerText(Text); + } + + return _rawText; + } + set => _rawText = value; + } + + [JsonProperty("lockedText")] public bool? LockedText { get; set; } + + [JsonProperty("isFavorite")] public bool? IsFavorite { get; set; } + + [JsonProperty("canReport")] public bool? CanReport { get; set; } + + [JsonProperty("canDelete")] public bool? CanDelete { get; set; } + + [JsonProperty("canComment")] public bool? CanComment { get; set; } + + [JsonProperty("canEdit")] public bool? CanEdit { get; set; } + + [JsonProperty("isPinned")] public bool? IsPinned { get; set; } + + [JsonProperty("favoritesCount")] public int? FavoritesCount { get; set; } + + [JsonProperty("mediaCount")] public int? MediaCount { get; set; } + + [JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; } + + [JsonProperty("voting")] public object Voting { get; set; } = new(); + + [JsonProperty("isOpened")] public bool IsOpened { get; set; } + + [JsonProperty("canToggleFavorite")] public bool? CanToggleFavorite { get; set; } + + [JsonProperty("streamId")] public object StreamId { get; set; } = new(); + + [JsonProperty("price")] public string Price { get; set; } = ""; + + [JsonProperty("hasVoting")] public bool? HasVoting { get; set; } + + [JsonProperty("isAddedToBookmarks")] public bool? IsAddedToBookmarks { get; set; } + + [JsonProperty("isArchived")] public bool IsArchived { get; set; } + + [JsonProperty("isPrivateArchived")] public bool? IsPrivateArchived { get; set; } + + [JsonProperty("isDeleted")] public bool? IsDeleted { get; set; } + + [JsonProperty("hasUrl")] public bool? HasUrl { get; set; } + + [JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; } + + [JsonProperty("commentsCount")] public int? CommentsCount { get; set; } + + [JsonProperty("mentionedUsers")] public List MentionedUsers { get; set; } = []; + + [JsonProperty("linkedUsers")] public List LinkedUsers { get; set; } = []; + + [JsonProperty("media")] public List Media { get; set; } = []; + + [JsonProperty("canViewMedia")] public bool? CanViewMedia { get; set; } + + [JsonProperty("preview")] public List Preview { get; set; } = []; + + [JsonProperty("cantCommentReason")] public string CantCommentReason { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Archived/MediumDto.cs b/OF DL/Models/Dtos/Archived/MediumDto.cs new file mode 100644 index 0000000..de6570b --- /dev/null +++ b/OF DL/Models/Dtos/Archived/MediumDto.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Archived; + +public class MediumDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("convertedToVideo")] public bool? ConvertedToVideo { get; set; } + + [JsonProperty("canView")] public bool CanView { get; set; } + + [JsonProperty("hasError")] public bool? HasError { get; set; } + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } = new(); + + [JsonProperty("info")] public InfoDto Info { get; set; } = new(); + + [JsonProperty("source")] public SourceDto Source { get; set; } = new(); + + [JsonProperty("squarePreview")] public string SquarePreview { get; set; } = ""; + + [JsonProperty("full")] public string Full { get; set; } = ""; + + [JsonProperty("preview")] public string Preview { get; set; } = ""; + + [JsonProperty("thumb")] public string Thumb { get; set; } = ""; + + [JsonProperty("files")] public FilesDto Files { get; set; } = new(); + + [JsonProperty("videoSources")] public VideoSourcesDto VideoSources { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Common/AuthorDto.cs b/OF DL/Models/Dtos/Common/AuthorDto.cs new file mode 100644 index 0000000..ab91233 --- /dev/null +++ b/OF DL/Models/Dtos/Common/AuthorDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class AuthorDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("_view")] public string View { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Common/DashDto.cs b/OF DL/Models/Dtos/Common/DashDto.cs new file mode 100644 index 0000000..1a430b2 --- /dev/null +++ b/OF DL/Models/Dtos/Common/DashDto.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class DashDto +{ + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } = ""; + + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } = ""; + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Common/DrmDto.cs b/OF DL/Models/Dtos/Common/DrmDto.cs new file mode 100644 index 0000000..a5a2d86 --- /dev/null +++ b/OF DL/Models/Dtos/Common/DrmDto.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Archived; + +namespace OF_DL.Models.Dtos.Common; + +public class DrmDto +{ + [JsonProperty("manifest")] public ManifestDto Manifest { get; set; } = new(); + + [JsonProperty("signature")] public SignatureDto Signature { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Common/FilesDto.cs b/OF DL/Models/Dtos/Common/FilesDto.cs new file mode 100644 index 0000000..12a4a6c --- /dev/null +++ b/OF DL/Models/Dtos/Common/FilesDto.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + + +namespace OF_DL.Models.Dtos.Common; + +public class FilesDto +{ + [JsonProperty("full")] public FullDto Full { get; set; } = new(); + + [JsonProperty("thumb")] public ThumbDto Thumb { get; set; } = new(); + + [JsonProperty("preview")] public PreviewDto Preview { get; set; } = new(); + + [JsonProperty("squarePreview")] public SquarePreviewDto SquarePreview { get; set; } = new(); + + [JsonProperty("drm")] public DrmDto Drm { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Common/FullDto.cs b/OF DL/Models/Dtos/Common/FullDto.cs new file mode 100644 index 0000000..1fda737 --- /dev/null +++ b/OF DL/Models/Dtos/Common/FullDto.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class FullDto +{ + [JsonProperty("url")] public string Url { get; set; } = ""; + + [JsonProperty("width")] public int Width { get; set; } + + [JsonProperty("height")] public int Height { get; set; } + + [JsonProperty("size")] public long Size { get; set; } + + [JsonProperty("sources")] public List Sources { get; set; } = []; +} diff --git a/OF DL/Models/Dtos/Common/HlsDto.cs b/OF DL/Models/Dtos/Common/HlsDto.cs new file mode 100644 index 0000000..d580efb --- /dev/null +++ b/OF DL/Models/Dtos/Common/HlsDto.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class HlsDto +{ + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } = ""; + + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } = ""; + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Common/ManifestDto.cs b/OF DL/Models/Dtos/Common/ManifestDto.cs new file mode 100644 index 0000000..89f39c3 --- /dev/null +++ b/OF DL/Models/Dtos/Common/ManifestDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class ManifestDto +{ + [JsonProperty("hls")] public string Hls { get; set; } = ""; + + [JsonProperty("dash")] public string Dash { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Common/PreviewDto.cs b/OF DL/Models/Dtos/Common/PreviewDto.cs new file mode 100644 index 0000000..b1f96a2 --- /dev/null +++ b/OF DL/Models/Dtos/Common/PreviewDto.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class PreviewDto +{ + [JsonProperty("width")] public int? Width { get; set; } + + [JsonProperty("height")] public int? Height { get; set; } + + [JsonProperty("size")] public int? Size { get; set; } + + [JsonProperty("url")] public string Url { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Common/SignatureDto.cs b/OF DL/Models/Dtos/Common/SignatureDto.cs new file mode 100644 index 0000000..42ccaaf --- /dev/null +++ b/OF DL/Models/Dtos/Common/SignatureDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class SignatureDto +{ + [JsonProperty("hls")] public HlsDto Hls { get; set; } = new(); + + [JsonProperty("dash")] public DashDto Dash { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Common/SourceDto.cs b/OF DL/Models/Dtos/Common/SourceDto.cs new file mode 100644 index 0000000..ae37216 --- /dev/null +++ b/OF DL/Models/Dtos/Common/SourceDto.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class SourceDto +{ + [JsonProperty("url")] public string Url { get; set; } = ""; + + [JsonProperty("width")] public int Width { get; set; } + + [JsonProperty("height")] public int Height { get; set; } + + [JsonProperty("duration")] public int Duration { get; set; } + + [JsonProperty("size")] public long Size { get; set; } + + [JsonProperty("sources")] public SourcesDto Sources { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Common/SourcesDto.cs b/OF DL/Models/Dtos/Common/SourcesDto.cs new file mode 100644 index 0000000..a7d151f --- /dev/null +++ b/OF DL/Models/Dtos/Common/SourcesDto.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class SourcesDto +{ + [JsonProperty("720")] public string _720 { get; set; } = ""; + + [JsonProperty("240")] public string _240 { get; set; } = ""; + + [JsonProperty("w150")] public string W150 { get; set; } = ""; + + [JsonProperty("w480")] public string W480 { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Common/SquarePreviewDto.cs b/OF DL/Models/Dtos/Common/SquarePreviewDto.cs new file mode 100644 index 0000000..fc67f0a --- /dev/null +++ b/OF DL/Models/Dtos/Common/SquarePreviewDto.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class SquarePreviewDto +{ + [JsonProperty("url")] public string Url { get; set; } = ""; + + [JsonProperty("width")] public int Width { get; set; } + + [JsonProperty("height")] public int Height { get; set; } + + [JsonProperty("size")] public long Size { get; set; } +} diff --git a/OF DL/Models/Dtos/Common/ThumbDto.cs b/OF DL/Models/Dtos/Common/ThumbDto.cs new file mode 100644 index 0000000..b18f9c1 --- /dev/null +++ b/OF DL/Models/Dtos/Common/ThumbDto.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class ThumbDto +{ + [JsonProperty("url")] public string Url { get; set; } = ""; + + [JsonProperty("width")] public int Width { get; set; } + + [JsonProperty("height")] public int Height { get; set; } + + [JsonProperty("size")] public long Size { get; set; } +} diff --git a/OF DL/Models/Dtos/Common/VideoSourcesDto.cs b/OF DL/Models/Dtos/Common/VideoSourcesDto.cs new file mode 100644 index 0000000..3f22ccf --- /dev/null +++ b/OF DL/Models/Dtos/Common/VideoSourcesDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class VideoSourcesDto +{ + [JsonProperty("720")] public string _720 { get; set; } = ""; + + [JsonProperty("240")] public string _240 { get; set; } = ""; +} diff --git a/OF DL/Models/Entities/Archived/Archived.cs b/OF DL/Models/Entities/Archived/Archived.cs new file mode 100644 index 0000000..80bd9f6 --- /dev/null +++ b/OF DL/Models/Entities/Archived/Archived.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Archived; + +public class Archived +{ + public List List { get; set; } = []; + + public bool HasMore { get; set; } + + public string? TailMarker { get; set; } +} diff --git a/OF DL/Models/Entities/Archived/ArchivedCollection.cs b/OF DL/Models/Entities/Archived/ArchivedCollection.cs new file mode 100644 index 0000000..d4662ed --- /dev/null +++ b/OF DL/Models/Entities/Archived/ArchivedCollection.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Archived; + +public class ArchivedCollection +{ + public List ArchivedPostMedia { get; set; } = []; + + public List ArchivedPostObjects { get; set; } = []; + + public Dictionary ArchivedPosts { get; set; } = new(); +} diff --git a/OF DL/Models/Entities/Archived/ListItem.cs b/OF DL/Models/Entities/Archived/ListItem.cs new file mode 100644 index 0000000..b7dd2d1 --- /dev/null +++ b/OF DL/Models/Entities/Archived/ListItem.cs @@ -0,0 +1,24 @@ +using OF_DL.Models.Entities.Common; + +namespace OF_DL.Models.Entities.Archived; + +public class ListItem +{ + public long Id { get; set; } + + public DateTime PostedAt { get; set; } + + public Author? Author { get; set; } + + public string? Text { get; set; } + + public string? Price { get; set; } + + public bool IsOpened { get; set; } + + public bool IsArchived { get; set; } + + public List? Media { get; set; } + + public List? Preview { get; set; } +} diff --git a/OF DL/Models/Entities/Archived/Medium.cs b/OF DL/Models/Entities/Archived/Medium.cs new file mode 100644 index 0000000..bebf52d --- /dev/null +++ b/OF DL/Models/Entities/Archived/Medium.cs @@ -0,0 +1,16 @@ +using OF_DL.Models.Entities.Common; + +namespace OF_DL.Models.Entities.Archived; + +public class Medium +{ + public long Id { get; set; } + + public string? Type { get; set; } + + public bool CanView { get; set; } + + public Files? Files { get; set; } + + public string? Preview { get; set; } +} diff --git a/OF DL/Models/Entities/Common/Author.cs b/OF DL/Models/Entities/Common/Author.cs new file mode 100644 index 0000000..50983e2 --- /dev/null +++ b/OF DL/Models/Entities/Common/Author.cs @@ -0,0 +1,6 @@ +namespace OF_DL.Models.Entities.Common; + +public class Author +{ + public long Id { get; set; } +} diff --git a/OF DL/Models/Entities/Common/Dash.cs b/OF DL/Models/Entities/Common/Dash.cs new file mode 100644 index 0000000..380f375 --- /dev/null +++ b/OF DL/Models/Entities/Common/Dash.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Common; + +public class Dash +{ + public string? CloudFrontPolicy { get; set; } + + public string? CloudFrontSignature { get; set; } + + public string? CloudFrontKeyPairId { get; set; } +} diff --git a/OF DL/Models/Entities/Common/Drm.cs b/OF DL/Models/Entities/Common/Drm.cs new file mode 100644 index 0000000..2ccdbbb --- /dev/null +++ b/OF DL/Models/Entities/Common/Drm.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Models.Entities.Common; + +public class Drm +{ + public Manifest? Manifest { get; set; } + + public Signature? Signature { get; set; } +} diff --git a/OF DL/Models/Entities/Common/Files.cs b/OF DL/Models/Entities/Common/Files.cs new file mode 100644 index 0000000..4dec6a1 --- /dev/null +++ b/OF DL/Models/Entities/Common/Files.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Models.Entities.Common; + +public class Files +{ + public Full? Full { get; set; } + + public Drm? Drm { get; set; } +} diff --git a/OF DL/Models/Entities/Common/Full.cs b/OF DL/Models/Entities/Common/Full.cs new file mode 100644 index 0000000..38032dc --- /dev/null +++ b/OF DL/Models/Entities/Common/Full.cs @@ -0,0 +1,6 @@ +namespace OF_DL.Models.Entities.Common; + +public class Full +{ + public string? Url { get; set; } +} diff --git a/OF DL/Models/Entities/Common/Manifest.cs b/OF DL/Models/Entities/Common/Manifest.cs new file mode 100644 index 0000000..dbb5f1f --- /dev/null +++ b/OF DL/Models/Entities/Common/Manifest.cs @@ -0,0 +1,6 @@ +namespace OF_DL.Models.Entities.Common; + +public class Manifest +{ + public string? Dash { get; set; } +} diff --git a/OF DL/Models/Entities/Common/Signature.cs b/OF DL/Models/Entities/Common/Signature.cs new file mode 100644 index 0000000..cb174ab --- /dev/null +++ b/OF DL/Models/Entities/Common/Signature.cs @@ -0,0 +1,6 @@ +namespace OF_DL.Models.Entities.Common; + +public class Signature +{ + public Dash? Dash { get; set; } +} diff --git a/OF DL/Models/Mappers/ArchivedMapper.cs b/OF DL/Models/Mappers/ArchivedMapper.cs new file mode 100644 index 0000000..22666b8 --- /dev/null +++ b/OF DL/Models/Mappers/ArchivedMapper.cs @@ -0,0 +1,137 @@ +using OF_DL.Models.Dtos.Archived; +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Entities.Archived; + +namespace OF_DL.Models.Mappers; + +public static class ArchivedMapper +{ + public static Archived FromDto(ArchivedDto? dto) + { + Archived mapped = new() { HasMore = dto?.HasMore ?? false, TailMarker = dto?.TailMarker }; + + if (dto?.List == null) + { + return mapped; + } + + foreach (ListItemDto entry in dto.List) + { + mapped.List.Add(MapList(entry)); + } + + return mapped; + } + + private static ListItem MapList(ListItemDto dto) => + new() + { + Id = dto.Id, + PostedAt = dto.PostedAt, + Author = MapAuthor(dto.Author), + Text = dto.Text, + Price = dto.Price, + IsOpened = dto.IsOpened, + IsArchived = dto.IsArchived, + Media = MapMedia(dto.Media), + Preview = dto.Preview + }; + + private static Entities.Common.Author? MapAuthor(AuthorDto? dto) + { + if (dto == null) + { + return null; + } + + return new Entities.Common.Author { Id = dto.Id }; + } + + private static List? MapMedia(List? media) + { + if (media == null) + { + return null; + } + + return media.Select(MapMedium).ToList(); + } + + private static Medium MapMedium(MediumDto dto) => + new() + { + Id = dto.Id, + Type = dto.Type, + CanView = dto.CanView, + Files = MapFiles(dto.Files), + Preview = dto.Preview + }; + + private static Entities.Common.Files? MapFiles(FilesDto? dto) + { + if (dto == null) + { + return null; + } + + return new Entities.Common.Files { Full = MapFull(dto.Full), Drm = MapDrm(dto.Drm) }; + } + + private static Entities.Common.Full? MapFull(FullDto? dto) + { + if (dto == null) + { + return null; + } + + return new Entities.Common.Full { Url = dto.Url }; + } + + private static Entities.Common.Drm? MapDrm(DrmDto? dto) + { + if (dto == null) + { + return null; + } + + return new Entities.Common.Drm + { + Manifest = MapManifest(dto.Manifest), Signature = MapSignature(dto.Signature) + }; + } + + private static Entities.Common.Manifest? MapManifest(ManifestDto? dto) + { + if (dto == null) + { + return null; + } + + return new Entities.Common.Manifest { Dash = dto.Dash }; + } + + private static Entities.Common.Signature? MapSignature(SignatureDto? dto) + { + if (dto == null) + { + return null; + } + + return new Entities.Common.Signature { Dash = MapDash(dto.Dash) }; + } + + private static Entities.Common.Dash? MapDash(DashDto? dto) + { + if (dto == null) + { + return null; + } + + return new Entities.Common.Dash + { + CloudFrontPolicy = dto.CloudFrontPolicy, + CloudFrontSignature = dto.CloudFrontSignature, + CloudFrontKeyPairId = dto.CloudFrontKeyPairId + }; + } +} diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 1e14abe..1cbe8d0 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -7,17 +7,16 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.CLI; using OF_DL.Models; -using OF_DL.Models.Archived; using OF_DL.Models.Messages; using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Helpers; +using OF_DL.Models.Entities.Archived; using OF_DL.Services; using Serilog; using Spectre.Console; -using static OF_DL.Models.Messages.Messages; using Constants = OF_DL.Widevine.Constants; namespace OF_DL; @@ -1522,7 +1521,7 @@ public class Program(IServiceProvider serviceProvider) pssh); } - Medium? mediaInfo = + Messages.Medium? mediaInfo = purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); Purchased.List? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => @@ -1557,7 +1556,7 @@ public class Program(IServiceProvider serviceProvider) } else { - Medium? mediaInfo = + Messages.Medium? mediaInfo = purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); Purchased.List? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => @@ -1671,7 +1670,7 @@ public class Program(IServiceProvider serviceProvider) pssh); } - Medium? mediaInfo = + Messages.Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); Purchased.List? messageInfo = @@ -1708,7 +1707,7 @@ public class Program(IServiceProvider serviceProvider) } else { - Medium? mediaInfo = + Messages.Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); Purchased.List messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => @@ -1898,7 +1897,7 @@ public class Program(IServiceProvider serviceProvider) pssh); } - Medium? mediaInfo = + Messages.Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); SingleMessage? messageInfo = @@ -1935,7 +1934,7 @@ public class Program(IServiceProvider serviceProvider) } else { - Medium? mediaInfo = + Messages.Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); SingleMessage? messageInfo = @@ -2038,7 +2037,7 @@ public class Program(IServiceProvider serviceProvider) pssh); } - Medium? mediaInfo = + Messages.Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); SingleMessage? messageInfo = @@ -2075,7 +2074,7 @@ public class Program(IServiceProvider serviceProvider) } else { - Medium? mediaInfo = + Messages.Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); SingleMessage? messageInfo = diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index a02f8f7..03cee60 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -6,7 +6,6 @@ using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Archived; using OF_DL.Models.Highlights; using OF_DL.Models.Lists; using OF_DL.Models.Messages; @@ -15,6 +14,10 @@ using OF_DL.Models.Purchased; using OF_DL.Models.Stories; using OF_DL.Models.Streams; using OF_DL.Enumerations; +using OF_DL.Models.Dtos.Archived; +using OF_DL.Models.Entities; +using OF_DL.Models.Entities.Archived; +using OF_DL.Models.Mappers; using OF_DL.Widevine; using Serilog; using Spectre.Console; @@ -1390,28 +1393,31 @@ public class APIService(IAuthService authService, IConfigService configService, configService.CurrentConfig.CustomDate); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - archived = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.list.Count}[/]"); + ArchivedDto archivedDto = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + archived = ArchivedMapper.FromDto(archivedDto); + ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (archived != null && archived.hasMore) + if (archived != null && archived.HasMore) { UpdateGetParamsForDateSelection( downloadDateSelection, ref getParams, - archived.tailMarker); + archived.TailMarker); while (true) { Archived newarchived = new(); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - newarchived = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + ArchivedDto newarchivedDto = + JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + newarchived = ArchivedMapper.FromDto(newarchivedDto); - archived.list.AddRange(newarchived.list); - ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.list.Count}[/]"); + archived.List.AddRange(newarchived.List); + ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (!newarchived.hasMore) + if (!newarchived.HasMore) { break; } @@ -1419,18 +1425,18 @@ public class APIService(IAuthService authService, IConfigService configService, UpdateGetParamsForDateSelection( downloadDateSelection, ref getParams, - newarchived.tailMarker); + newarchived.TailMarker); } } - foreach (Archived.List archive in archived.list) + foreach (ListItem archive in archived.List) { List previewids = new(); - if (archive.preview != null) + if (archive.Preview != null) { - for (int i = 0; i < archive.preview.Count; i++) + for (int i = 0; i < archive.Preview.Count; i++) { - if (archive.preview[i] is long previewId) + if (archive.Preview[i] is long previewId) { if (!previewids.Contains(previewId)) { @@ -1440,61 +1446,61 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, archive.id, archive.text != null ? archive.text : string.Empty, - archive.price != null ? archive.price : "0", - archive.price != null && archive.isOpened ? true : false, archive.isArchived, archive.postedAt); + await dbService.AddPost(folder, archive.Id, archive.Text != null ? archive.Text : string.Empty, + archive.Price != null ? archive.Price : "0", + archive.Price != null && archive.IsOpened ? true : false, archive.IsArchived, archive.PostedAt); archivedCollection.ArchivedPostObjects.Add(archive); - if (archive.media != null && archive.media.Count > 0) + if (archive.Media != null && archive.Media.Count > 0) { - foreach (Archived.Medium medium in archive.media) + foreach (Medium medium in archive.Media) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (medium.canView && medium.files != null && medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.CanView && medium.Files != null && medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (!archivedCollection.ArchivedPosts.ContainsKey(medium.id)) + if (!archivedCollection.ArchivedPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, archive.id, medium.files.full.url, null, + await dbService.AddMedia(folder, medium.Id, archive.Id, medium.Files.Full.Url, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - archivedCollection.ArchivedPosts.Add(medium.id, medium.files.full.url); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id) ? true : false, false, null); + archivedCollection.ArchivedPosts.Add(medium.Id, medium.Files.Full.Url); archivedCollection.ArchivedPostMedia.Add(medium); } } - else if (medium.canView && medium.files != null && medium.files.drm != null) + else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { - if (!archivedCollection.ArchivedPosts.ContainsKey(medium.id)) + if (!archivedCollection.ArchivedPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, archive.id, medium.files.drm.manifest.dash, + await dbService.AddMedia(folder, medium.Id, archive.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - archivedCollection.ArchivedPosts.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{archive.id}"); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id) ? true : false, false, null); + archivedCollection.ArchivedPosts.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{archive.Id}"); archivedCollection.ArchivedPostMedia.Add(medium); } } diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 2867dc0..5495d8a 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -4,12 +4,13 @@ using System.Xml.Linq; using FFmpeg.NET; using FFmpeg.NET.Events; using OF_DL.Models; -using OF_DL.Models.Archived; using OF_DL.Models.Messages; using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; +using OF_DL.Models.Entities; +using OF_DL.Models.Entities.Archived; using OF_DL.Utils; using Serilog; using Serilog.Events; @@ -1036,7 +1037,7 @@ public class DownloadService( public async Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users) { string path; @@ -1059,7 +1060,8 @@ public class DownloadService( } public async Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, + Messages.Medium? messageMedia, FromUser? fromUser, Dictionary users) { string path; @@ -1083,8 +1085,9 @@ public class DownloadService( public async Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Archived.List? messageInfo, - Archived.Medium? messageMedia, Archived.Author? author, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, ListItem? messageInfo, + OF_DL.Models.Entities.Archived.Medium? messageMedia, Models.Entities.Common.Author? author, + Dictionary users) { string path = "/Archived/Posts/Free"; Uri uri = new(url); @@ -1107,7 +1110,8 @@ public class DownloadService( } public async Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, + Messages.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) { string path; @@ -1130,7 +1134,8 @@ public class DownloadService( } public async Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, + Messages.Medium? messageMedia, FromUser? fromUser, Dictionary users) { string path; @@ -1159,7 +1164,7 @@ public class DownloadService( IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Medium? messageMedia, + Messages.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) { @@ -1189,7 +1194,7 @@ public class DownloadService( public async Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users) { try @@ -1318,7 +1323,8 @@ public class DownloadService( public async Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, + Messages.Medium? messageMedia, FromUser? fromUser, Dictionary users) { try @@ -1448,7 +1454,8 @@ public class DownloadService( public async Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, + Messages.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) { try @@ -1576,7 +1583,8 @@ public class DownloadService( public async Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, + Messages.Medium? messageMedia, FromUser? fromUser, Dictionary users) { try @@ -2089,7 +2097,8 @@ public class DownloadService( public async Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, Medium? postMedia, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, + Messages.Medium? postMedia, Purchased.FromUser? fromUser, Dictionary users) { try @@ -2219,8 +2228,9 @@ public class DownloadService( public async Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Archived.List? postInfo, Archived.Medium? postMedia, - Archived.Author? author, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, ListItem? postInfo, + OF_DL.Models.Entities.Archived.Medium? postMedia, + Models.Entities.Common.Author? author, Dictionary users) { try { @@ -2502,10 +2512,10 @@ public class DownloadService( pssh); } - Archived.Medium? mediaInfo = - archived.ArchivedPostMedia.FirstOrDefault(m => m.id == archivedKVP.Key); - Archived.List? postInfo = - archived.ArchivedPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + OF_DL.Models.Entities.Archived.Medium? mediaInfo = + archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); + ListItem? postInfo = + archived.ArchivedPostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadArchivedPostDRMVideo( policy, @@ -2522,7 +2532,7 @@ public class DownloadService( string.Empty, postInfo, mediaInfo, - postInfo?.author, + postInfo?.Author, users); } else @@ -2532,9 +2542,10 @@ public class DownloadService( } else { - Archived.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.id == archivedKVP.Key); - Archived.List? postInfo = - archived.ArchivedPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + OF_DL.Models.Entities.Archived.Medium? mediaInfo = + archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); + ListItem? postInfo = + archived.ArchivedPostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadArchivedMedia( archivedKVP.Value, @@ -2546,7 +2557,7 @@ public class DownloadService( string.Empty, postInfo, mediaInfo, - postInfo?.author, + postInfo?.Author, users); } @@ -2631,7 +2642,7 @@ public class DownloadService( pssh); } - Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); + Messages.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); List? messageInfo = messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); @@ -2660,7 +2671,7 @@ public class DownloadService( } else { - Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); + Messages.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); List? messageInfo = messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); isNew = await DownloadMessageMedia( @@ -2747,7 +2758,8 @@ public class DownloadService( $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/message/{parsed[5]}?type=widevine", pssh); - Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == kvp.Key); + Messages.Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == kvp.Key); Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); @@ -2764,7 +2776,8 @@ public class DownloadService( } else { - Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == kvp.Key); + Messages.Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == kvp.Key); Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); isNew = await DownloadPurchasedMedia(kvp.Value, path, kvp.Key, "Messages", progressReporter, @@ -3020,7 +3033,7 @@ public class DownloadService( $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", pssh); - Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); + Messages.Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); Purchased.List? postInfo = purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); @@ -3036,7 +3049,7 @@ public class DownloadService( } else { - Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); + Messages.Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); Purchased.List? postInfo = purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); isNew = await DownloadPurchasedPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index 869f50e..5606b5c 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -1,11 +1,11 @@ using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Archived; using OF_DL.Models.Messages; using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; +using OF_DL.Models.Entities.Archived; using Spectre.Console; namespace OF_DL.Services; diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index 399702b..63916c3 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -1,5 +1,5 @@ using OF_DL.Models; -using OF_DL.Models.Archived; +using OF_DL.Models.Entities.Archived; using OF_DL.Models.Messages; using OF_DL.Models.Post; using OF_DL.Models.Purchased; @@ -17,13 +17,15 @@ public interface IDownloadService string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Archived.List? messageInfo, - Archived.Medium? messageMedia, Archived.Author? author, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, ListItem? messageInfo, + OF_DL.Models.Entities.Archived.Medium? messageMedia, Models.Entities.Common.Author? author, + Dictionary users); Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Archived.List? postInfo, Archived.Medium? postMedia, - Archived.Author? author, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, ListItem? postInfo, + OF_DL.Models.Entities.Archived.Medium? postMedia, + Models.Entities.Common.Author? author, Dictionary users); Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, @@ -39,11 +41,11 @@ public interface IDownloadService Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, - string? filenameFormat, List? messageInfo, Medium? messageMedia, Messages.FromUser? fromUser, + string? filenameFormat, List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users); Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users); Task DownloadPostMedia(string url, string folder, long media_id, string api_type, @@ -55,30 +57,36 @@ public interface IDownloadService SinglePost.Author? author, Dictionary users); Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, + Messages.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, + Messages.Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, + Messages.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, + Messages.Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, Medium? postMedia, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, + Messages.Medium? postMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, + Messages.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, @@ -95,11 +103,13 @@ public interface IDownloadService Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, + Messages.Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, + Messages.Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, From f243471b29596b7ed3aa6c17b5adc096ca2ba895 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Sun, 8 Feb 2026 15:45:40 -0600 Subject: [PATCH 13/55] Remove BOM from files --- OF DL/Crypto/CryptoUtils.cs | 2 +- OF DL/Crypto/Padding.cs | 2 +- OF DL/Enumerations/MediaType.cs | 2 +- OF DL/Helpers/DownloadContext.cs | 2 +- OF DL/Helpers/VersionHelper.cs | 2 +- OF DL/Models/Dtos/Archived/CountersDto.cs | 2 +- OF DL/Models/Dtos/Archived/InfoDto.cs | 2 +- OF DL/Models/Dtos/Archived/LinkedPostDto.cs | 2 +- OF DL/Models/Dtos/Archived/ListItemDto.cs | 2 +- OF DL/Models/Dtos/Archived/MediumDto.cs | 2 +- OF DL/Models/Dtos/Common/AuthorDto.cs | 2 +- OF DL/Models/Dtos/Common/DashDto.cs | 2 +- OF DL/Models/Dtos/Common/DrmDto.cs | 2 +- OF DL/Models/Dtos/Common/FilesDto.cs | 2 +- OF DL/Models/Dtos/Common/FullDto.cs | 2 +- OF DL/Models/Dtos/Common/HlsDto.cs | 2 +- OF DL/Models/Dtos/Common/ManifestDto.cs | 2 +- OF DL/Models/Dtos/Common/PreviewDto.cs | 2 +- OF DL/Models/Dtos/Common/SignatureDto.cs | 2 +- OF DL/Models/Dtos/Common/SourceDto.cs | 2 +- OF DL/Models/Dtos/Common/SourcesDto.cs | 2 +- OF DL/Models/Dtos/Common/SquarePreviewDto.cs | 2 +- OF DL/Models/Dtos/Common/ThumbDto.cs | 2 +- OF DL/Models/Dtos/Common/VideoSourcesDto.cs | 2 +- OF DL/Models/Entities/Archived/ListItem.cs | 2 +- OF DL/Models/Entities/Archived/Medium.cs | 2 +- OF DL/Models/Entities/Common/Author.cs | 2 +- OF DL/Models/Entities/Common/Dash.cs | 2 +- OF DL/Models/Entities/Common/Drm.cs | 2 +- OF DL/Models/Entities/Common/Files.cs | 2 +- OF DL/Models/Entities/Common/Full.cs | 2 +- OF DL/Models/Entities/Common/Manifest.cs | 2 +- OF DL/Models/Entities/Common/Signature.cs | 2 +- OF DL/Models/FileNameFormatConfig.cs | 2 +- OF DL/Models/IFileNameFormatConfig.cs | 2 +- OF DL/Models/LatestReleaseAPIResponse.cs | 2 +- OF DL/Models/Lists/UserList.cs | 2 +- OF DL/Models/ToggleableConfigAttribute.cs | 2 +- OF DL/OF DL.csproj | 2 +- OF DL/Widevine/CDMApi.cs | 2 +- OF DL/Widevine/CDMDevice.cs | 2 +- OF DL/Widevine/ContentKey.cs | 2 +- OF DL/Widevine/DerivedKeys.cs | 2 +- OF DL/Widevine/PSSHBox.cs | 2 +- OF DL/Widevine/Session.cs | 2 +- OF DL/Widevine/WvProto2.cs | 2 +- 46 files changed, 46 insertions(+), 46 deletions(-) diff --git a/OF DL/Crypto/CryptoUtils.cs b/OF DL/Crypto/CryptoUtils.cs index 624dce0..3c519c8 100644 --- a/OF DL/Crypto/CryptoUtils.cs +++ b/OF DL/Crypto/CryptoUtils.cs @@ -1,4 +1,4 @@ -using System.Security.Cryptography; +using System.Security.Cryptography; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Macs; diff --git a/OF DL/Crypto/Padding.cs b/OF DL/Crypto/Padding.cs index e3075e4..1225e1e 100644 --- a/OF DL/Crypto/Padding.cs +++ b/OF DL/Crypto/Padding.cs @@ -1,4 +1,4 @@ -using System.Security.Cryptography; +using System.Security.Cryptography; namespace OF_DL.Crypto; diff --git a/OF DL/Enumerations/MediaType.cs b/OF DL/Enumerations/MediaType.cs index da579c6..ccdd25d 100644 --- a/OF DL/Enumerations/MediaType.cs +++ b/OF DL/Enumerations/MediaType.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Enumerations; +namespace OF_DL.Enumerations; public enum MediaType { diff --git a/OF DL/Helpers/DownloadContext.cs b/OF DL/Helpers/DownloadContext.cs index 0ba77f4..1a32612 100644 --- a/OF DL/Helpers/DownloadContext.cs +++ b/OF DL/Helpers/DownloadContext.cs @@ -1,4 +1,4 @@ -using OF_DL.Models; +using OF_DL.Models; using OF_DL.Services; namespace OF_DL.Helpers; diff --git a/OF DL/Helpers/VersionHelper.cs b/OF DL/Helpers/VersionHelper.cs index 1d28844..1343f3d 100644 --- a/OF DL/Helpers/VersionHelper.cs +++ b/OF DL/Helpers/VersionHelper.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using OF_DL.Models; using Serilog; diff --git a/OF DL/Models/Dtos/Archived/CountersDto.cs b/OF DL/Models/Dtos/Archived/CountersDto.cs index 40e2500..32a515e 100644 --- a/OF DL/Models/Dtos/Archived/CountersDto.cs +++ b/OF DL/Models/Dtos/Archived/CountersDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Archived; diff --git a/OF DL/Models/Dtos/Archived/InfoDto.cs b/OF DL/Models/Dtos/Archived/InfoDto.cs index 1e4b13d..36c077a 100644 --- a/OF DL/Models/Dtos/Archived/InfoDto.cs +++ b/OF DL/Models/Dtos/Archived/InfoDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using OF_DL.Models.Dtos.Common; namespace OF_DL.Models.Dtos.Archived; diff --git a/OF DL/Models/Dtos/Archived/LinkedPostDto.cs b/OF DL/Models/Dtos/Archived/LinkedPostDto.cs index 3bedcae..ac610fa 100644 --- a/OF DL/Models/Dtos/Archived/LinkedPostDto.cs +++ b/OF DL/Models/Dtos/Archived/LinkedPostDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using OF_DL.Models.Dtos.Common; using OF_DL.Utils; diff --git a/OF DL/Models/Dtos/Archived/ListItemDto.cs b/OF DL/Models/Dtos/Archived/ListItemDto.cs index d2106ed..808202f 100644 --- a/OF DL/Models/Dtos/Archived/ListItemDto.cs +++ b/OF DL/Models/Dtos/Archived/ListItemDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using OF_DL.Models.Dtos.Common; using OF_DL.Utils; diff --git a/OF DL/Models/Dtos/Archived/MediumDto.cs b/OF DL/Models/Dtos/Archived/MediumDto.cs index de6570b..bcbd99a 100644 --- a/OF DL/Models/Dtos/Archived/MediumDto.cs +++ b/OF DL/Models/Dtos/Archived/MediumDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using OF_DL.Models.Dtos.Common; namespace OF_DL.Models.Dtos.Archived; diff --git a/OF DL/Models/Dtos/Common/AuthorDto.cs b/OF DL/Models/Dtos/Common/AuthorDto.cs index ab91233..2857aca 100644 --- a/OF DL/Models/Dtos/Common/AuthorDto.cs +++ b/OF DL/Models/Dtos/Common/AuthorDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/DashDto.cs b/OF DL/Models/Dtos/Common/DashDto.cs index 1a430b2..a3f8a1a 100644 --- a/OF DL/Models/Dtos/Common/DashDto.cs +++ b/OF DL/Models/Dtos/Common/DashDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/DrmDto.cs b/OF DL/Models/Dtos/Common/DrmDto.cs index a5a2d86..25271be 100644 --- a/OF DL/Models/Dtos/Common/DrmDto.cs +++ b/OF DL/Models/Dtos/Common/DrmDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using OF_DL.Models.Dtos.Archived; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/FilesDto.cs b/OF DL/Models/Dtos/Common/FilesDto.cs index 12a4a6c..13692a8 100644 --- a/OF DL/Models/Dtos/Common/FilesDto.cs +++ b/OF DL/Models/Dtos/Common/FilesDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/FullDto.cs b/OF DL/Models/Dtos/Common/FullDto.cs index 1fda737..fd21dff 100644 --- a/OF DL/Models/Dtos/Common/FullDto.cs +++ b/OF DL/Models/Dtos/Common/FullDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/HlsDto.cs b/OF DL/Models/Dtos/Common/HlsDto.cs index d580efb..36e65b1 100644 --- a/OF DL/Models/Dtos/Common/HlsDto.cs +++ b/OF DL/Models/Dtos/Common/HlsDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/ManifestDto.cs b/OF DL/Models/Dtos/Common/ManifestDto.cs index 89f39c3..9e19ad4 100644 --- a/OF DL/Models/Dtos/Common/ManifestDto.cs +++ b/OF DL/Models/Dtos/Common/ManifestDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/PreviewDto.cs b/OF DL/Models/Dtos/Common/PreviewDto.cs index b1f96a2..ad19606 100644 --- a/OF DL/Models/Dtos/Common/PreviewDto.cs +++ b/OF DL/Models/Dtos/Common/PreviewDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/SignatureDto.cs b/OF DL/Models/Dtos/Common/SignatureDto.cs index 42ccaaf..bfc0374 100644 --- a/OF DL/Models/Dtos/Common/SignatureDto.cs +++ b/OF DL/Models/Dtos/Common/SignatureDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/SourceDto.cs b/OF DL/Models/Dtos/Common/SourceDto.cs index ae37216..a61fd1d 100644 --- a/OF DL/Models/Dtos/Common/SourceDto.cs +++ b/OF DL/Models/Dtos/Common/SourceDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/SourcesDto.cs b/OF DL/Models/Dtos/Common/SourcesDto.cs index a7d151f..f2d5d17 100644 --- a/OF DL/Models/Dtos/Common/SourcesDto.cs +++ b/OF DL/Models/Dtos/Common/SourcesDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/SquarePreviewDto.cs b/OF DL/Models/Dtos/Common/SquarePreviewDto.cs index fc67f0a..1457778 100644 --- a/OF DL/Models/Dtos/Common/SquarePreviewDto.cs +++ b/OF DL/Models/Dtos/Common/SquarePreviewDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/ThumbDto.cs b/OF DL/Models/Dtos/Common/ThumbDto.cs index b18f9c1..2257499 100644 --- a/OF DL/Models/Dtos/Common/ThumbDto.cs +++ b/OF DL/Models/Dtos/Common/ThumbDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Dtos/Common/VideoSourcesDto.cs b/OF DL/Models/Dtos/Common/VideoSourcesDto.cs index 3f22ccf..0596204 100644 --- a/OF DL/Models/Dtos/Common/VideoSourcesDto.cs +++ b/OF DL/Models/Dtos/Common/VideoSourcesDto.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL/Models/Entities/Archived/ListItem.cs b/OF DL/Models/Entities/Archived/ListItem.cs index b7dd2d1..15333db 100644 --- a/OF DL/Models/Entities/Archived/ListItem.cs +++ b/OF DL/Models/Entities/Archived/ListItem.cs @@ -1,4 +1,4 @@ -using OF_DL.Models.Entities.Common; +using OF_DL.Models.Entities.Common; namespace OF_DL.Models.Entities.Archived; diff --git a/OF DL/Models/Entities/Archived/Medium.cs b/OF DL/Models/Entities/Archived/Medium.cs index bebf52d..9b8b537 100644 --- a/OF DL/Models/Entities/Archived/Medium.cs +++ b/OF DL/Models/Entities/Archived/Medium.cs @@ -1,4 +1,4 @@ -using OF_DL.Models.Entities.Common; +using OF_DL.Models.Entities.Common; namespace OF_DL.Models.Entities.Archived; diff --git a/OF DL/Models/Entities/Common/Author.cs b/OF DL/Models/Entities/Common/Author.cs index 50983e2..5f6b9d9 100644 --- a/OF DL/Models/Entities/Common/Author.cs +++ b/OF DL/Models/Entities/Common/Author.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models.Entities.Common; +namespace OF_DL.Models.Entities.Common; public class Author { diff --git a/OF DL/Models/Entities/Common/Dash.cs b/OF DL/Models/Entities/Common/Dash.cs index 380f375..5d746d6 100644 --- a/OF DL/Models/Entities/Common/Dash.cs +++ b/OF DL/Models/Entities/Common/Dash.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models.Entities.Common; +namespace OF_DL.Models.Entities.Common; public class Dash { diff --git a/OF DL/Models/Entities/Common/Drm.cs b/OF DL/Models/Entities/Common/Drm.cs index 2ccdbbb..fe97d3c 100644 --- a/OF DL/Models/Entities/Common/Drm.cs +++ b/OF DL/Models/Entities/Common/Drm.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models.Entities.Common; +namespace OF_DL.Models.Entities.Common; public class Drm { diff --git a/OF DL/Models/Entities/Common/Files.cs b/OF DL/Models/Entities/Common/Files.cs index 4dec6a1..97bdadb 100644 --- a/OF DL/Models/Entities/Common/Files.cs +++ b/OF DL/Models/Entities/Common/Files.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models.Entities.Common; +namespace OF_DL.Models.Entities.Common; public class Files { diff --git a/OF DL/Models/Entities/Common/Full.cs b/OF DL/Models/Entities/Common/Full.cs index 38032dc..517e09d 100644 --- a/OF DL/Models/Entities/Common/Full.cs +++ b/OF DL/Models/Entities/Common/Full.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models.Entities.Common; +namespace OF_DL.Models.Entities.Common; public class Full { diff --git a/OF DL/Models/Entities/Common/Manifest.cs b/OF DL/Models/Entities/Common/Manifest.cs index dbb5f1f..b3502b1 100644 --- a/OF DL/Models/Entities/Common/Manifest.cs +++ b/OF DL/Models/Entities/Common/Manifest.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models.Entities.Common; +namespace OF_DL.Models.Entities.Common; public class Manifest { diff --git a/OF DL/Models/Entities/Common/Signature.cs b/OF DL/Models/Entities/Common/Signature.cs index cb174ab..bf10deb 100644 --- a/OF DL/Models/Entities/Common/Signature.cs +++ b/OF DL/Models/Entities/Common/Signature.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models.Entities.Common; +namespace OF_DL.Models.Entities.Common; public class Signature { diff --git a/OF DL/Models/FileNameFormatConfig.cs b/OF DL/Models/FileNameFormatConfig.cs index 9b706af..8b51a05 100644 --- a/OF DL/Models/FileNameFormatConfig.cs +++ b/OF DL/Models/FileNameFormatConfig.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models; +namespace OF_DL.Models; public class FileNameFormatConfig : IFileNameFormatConfig { diff --git a/OF DL/Models/IFileNameFormatConfig.cs b/OF DL/Models/IFileNameFormatConfig.cs index ac774dd..85b1d60 100644 --- a/OF DL/Models/IFileNameFormatConfig.cs +++ b/OF DL/Models/IFileNameFormatConfig.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models; +namespace OF_DL.Models; public interface IFileNameFormatConfig { diff --git a/OF DL/Models/LatestReleaseAPIResponse.cs b/OF DL/Models/LatestReleaseAPIResponse.cs index be2322d..ec77eb7 100644 --- a/OF DL/Models/LatestReleaseAPIResponse.cs +++ b/OF DL/Models/LatestReleaseAPIResponse.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace OF_DL.Models; diff --git a/OF DL/Models/Lists/UserList.cs b/OF DL/Models/Lists/UserList.cs index 60248cf..f499edb 100644 --- a/OF DL/Models/Lists/UserList.cs +++ b/OF DL/Models/Lists/UserList.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models.Lists; +namespace OF_DL.Models.Lists; public class UserList { diff --git a/OF DL/Models/ToggleableConfigAttribute.cs b/OF DL/Models/ToggleableConfigAttribute.cs index f04dcc0..98880a5 100644 --- a/OF DL/Models/ToggleableConfigAttribute.cs +++ b/OF DL/Models/ToggleableConfigAttribute.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models; +namespace OF_DL.Models; [AttributeUsage(AttributeTargets.Property)] internal class ToggleableConfigAttribute : Attribute diff --git a/OF DL/OF DL.csproj b/OF DL/OF DL.csproj index a57dfe5..8bc38a9 100644 --- a/OF DL/OF DL.csproj +++ b/OF DL/OF DL.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/OF DL/Widevine/CDMApi.cs b/OF DL/Widevine/CDMApi.cs index e6d7315..c1d3193 100644 --- a/OF DL/Widevine/CDMApi.cs +++ b/OF DL/Widevine/CDMApi.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Widevine; +namespace OF_DL.Widevine; public class CDMApi { diff --git a/OF DL/Widevine/CDMDevice.cs b/OF DL/Widevine/CDMDevice.cs index 09fc4ef..887a91f 100644 --- a/OF DL/Widevine/CDMDevice.cs +++ b/OF DL/Widevine/CDMDevice.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Encodings; diff --git a/OF DL/Widevine/ContentKey.cs b/OF DL/Widevine/ContentKey.cs index 1ad7507..3a25f9d 100644 --- a/OF DL/Widevine/ContentKey.cs +++ b/OF DL/Widevine/ContentKey.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; namespace OF_DL.Widevine; diff --git a/OF DL/Widevine/DerivedKeys.cs b/OF DL/Widevine/DerivedKeys.cs index 5a61705..9aca670 100644 --- a/OF DL/Widevine/DerivedKeys.cs +++ b/OF DL/Widevine/DerivedKeys.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Widevine; +namespace OF_DL.Widevine; public class DerivedKeys { diff --git a/OF DL/Widevine/PSSHBox.cs b/OF DL/Widevine/PSSHBox.cs index a61f694..64363d6 100644 --- a/OF DL/Widevine/PSSHBox.cs +++ b/OF DL/Widevine/PSSHBox.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Widevine; +namespace OF_DL.Widevine; internal class PSSHBox { diff --git a/OF DL/Widevine/Session.cs b/OF DL/Widevine/Session.cs index 5e16afc..e9177ef 100644 --- a/OF DL/Widevine/Session.cs +++ b/OF DL/Widevine/Session.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Widevine; +namespace OF_DL.Widevine; internal class Session { diff --git a/OF DL/Widevine/WvProto2.cs b/OF DL/Widevine/WvProto2.cs index 2a5c0b4..452430d 100644 --- a/OF DL/Widevine/WvProto2.cs +++ b/OF DL/Widevine/WvProto2.cs @@ -1,4 +1,4 @@ -// +// // This file was generated by a tool; you should avoid making direct changes. // Consider using 'partial classes' to extend these types // Input: my.proto From 2e3f17945e231a4eaf66b161a4daae37e133a004 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Sun, 8 Feb 2026 16:16:38 -0600 Subject: [PATCH 14/55] Refactor Highlight entities into DTOs and application entities with standardized naming conventions and default values --- OF DL/Models/Dtos/Common/PreviewDto.cs | 2 + OF DL/Models/Dtos/Common/SquarePreviewDto.cs | 2 + .../Dtos/Highlights/HighlightMediaDto.cs | 22 +++ OF DL/Models/Dtos/Highlights/HighlightsDto.cs | 10 ++ OF DL/Models/Dtos/Highlights/ListItemDto.cs | 20 +++ OF DL/Models/Dtos/Highlights/MediumDto.cs | 21 +++ OF DL/Models/Dtos/Highlights/StoryDto.cs | 24 ++++ .../Entities/Highlights/HighlightMedia.cs | 6 + .../Models/Entities/Highlights/Highlights.cs | 8 ++ OF DL/Models/Entities/Highlights/ListItem.cs | 6 + OF DL/Models/Entities/Highlights/Medium.cs | 16 +++ OF DL/Models/Entities/Highlights/Story.cs | 10 ++ OF DL/Models/Highlights/HighlightMedia.cs | 102 ------------- OF DL/Models/Highlights/Highlights.cs | 18 --- OF DL/Models/Mappers/HighlightsMapper.cs | 66 +++++++++ OF DL/Program.cs | 20 +-- OF DL/Services/APIService.cs | 134 +++++++++--------- OF DL/Services/DownloadService.cs | 53 ++++--- OF DL/Services/IAPIService.cs | 4 +- OF DL/Services/IDownloadService.cs | 32 ++--- 20 files changed, 335 insertions(+), 241 deletions(-) create mode 100644 OF DL/Models/Dtos/Highlights/HighlightMediaDto.cs create mode 100644 OF DL/Models/Dtos/Highlights/HighlightsDto.cs create mode 100644 OF DL/Models/Dtos/Highlights/ListItemDto.cs create mode 100644 OF DL/Models/Dtos/Highlights/MediumDto.cs create mode 100644 OF DL/Models/Dtos/Highlights/StoryDto.cs create mode 100644 OF DL/Models/Entities/Highlights/HighlightMedia.cs create mode 100644 OF DL/Models/Entities/Highlights/Highlights.cs create mode 100644 OF DL/Models/Entities/Highlights/ListItem.cs create mode 100644 OF DL/Models/Entities/Highlights/Medium.cs create mode 100644 OF DL/Models/Entities/Highlights/Story.cs delete mode 100644 OF DL/Models/Highlights/HighlightMedia.cs delete mode 100644 OF DL/Models/Highlights/Highlights.cs create mode 100644 OF DL/Models/Mappers/HighlightsMapper.cs diff --git a/OF DL/Models/Dtos/Common/PreviewDto.cs b/OF DL/Models/Dtos/Common/PreviewDto.cs index ad19606..c33df6b 100644 --- a/OF DL/Models/Dtos/Common/PreviewDto.cs +++ b/OF DL/Models/Dtos/Common/PreviewDto.cs @@ -11,4 +11,6 @@ public class PreviewDto [JsonProperty("size")] public int? Size { get; set; } [JsonProperty("url")] public string Url { get; set; } = ""; + + [JsonProperty("sources")] public SourcesDto Sources { get; set; } = new(); } diff --git a/OF DL/Models/Dtos/Common/SquarePreviewDto.cs b/OF DL/Models/Dtos/Common/SquarePreviewDto.cs index 1457778..d5f8f82 100644 --- a/OF DL/Models/Dtos/Common/SquarePreviewDto.cs +++ b/OF DL/Models/Dtos/Common/SquarePreviewDto.cs @@ -11,4 +11,6 @@ public class SquarePreviewDto [JsonProperty("height")] public int Height { get; set; } [JsonProperty("size")] public long Size { get; set; } + + [JsonProperty("sources")] public SourcesDto Sources { get; set; } = new(); } diff --git a/OF DL/Models/Dtos/Highlights/HighlightMediaDto.cs b/OF DL/Models/Dtos/Highlights/HighlightMediaDto.cs new file mode 100644 index 0000000..1ceba1d --- /dev/null +++ b/OF DL/Models/Dtos/Highlights/HighlightMediaDto.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Highlights; + +public class HighlightMediaDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("userId")] public long UserId { get; set; } + + [JsonProperty("title")] public string Title { get; set; } = ""; + + [JsonProperty("coverStoryId")] public long CoverStoryId { get; set; } + + [JsonProperty("cover")] public string Cover { get; set; } = ""; + + [JsonProperty("storiesCount")] public int StoriesCount { get; set; } + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } + + [JsonProperty("stories")] public List Stories { get; set; } = []; +} diff --git a/OF DL/Models/Dtos/Highlights/HighlightsDto.cs b/OF DL/Models/Dtos/Highlights/HighlightsDto.cs new file mode 100644 index 0000000..1178828 --- /dev/null +++ b/OF DL/Models/Dtos/Highlights/HighlightsDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Highlights; + +public class HighlightsDto +{ + [JsonProperty("list")] public List List { get; set; } = []; + + [JsonProperty("hasMore")] public bool HasMore { get; set; } +} diff --git a/OF DL/Models/Dtos/Highlights/ListItemDto.cs b/OF DL/Models/Dtos/Highlights/ListItemDto.cs new file mode 100644 index 0000000..ffeecd7 --- /dev/null +++ b/OF DL/Models/Dtos/Highlights/ListItemDto.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Highlights; + +public class ListItemDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("userId")] public long UserId { get; set; } + + [JsonProperty("title")] public string Title { get; set; } = ""; + + [JsonProperty("coverStoryId")] public long CoverStoryId { get; set; } + + [JsonProperty("cover")] public string Cover { get; set; } = ""; + + [JsonProperty("storiesCount")] public int StoriesCount { get; set; } + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } +} diff --git a/OF DL/Models/Dtos/Highlights/MediumDto.cs b/OF DL/Models/Dtos/Highlights/MediumDto.cs new file mode 100644 index 0000000..cf6bc4c --- /dev/null +++ b/OF DL/Models/Dtos/Highlights/MediumDto.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Highlights; + +public class MediumDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("convertedToVideo")] public bool ConvertedToVideo { get; set; } + + [JsonProperty("canView")] public bool CanView { get; set; } + + [JsonProperty("hasError")] public bool HasError { get; set; } + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } + + [JsonProperty("files")] public FilesDto Files { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Highlights/StoryDto.cs b/OF DL/Models/Dtos/Highlights/StoryDto.cs new file mode 100644 index 0000000..053215a --- /dev/null +++ b/OF DL/Models/Dtos/Highlights/StoryDto.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Highlights; + +public class StoryDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("userId")] public long UserId { get; set; } + + [JsonProperty("isWatched")] public bool IsWatched { get; set; } + + [JsonProperty("isReady")] public bool IsReady { get; set; } + + [JsonProperty("media")] public List Media { get; set; } = []; + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } + + [JsonProperty("question")] public object Question { get; set; } = new(); + + [JsonProperty("canLike")] public bool CanLike { get; set; } + + [JsonProperty("isLiked")] public bool IsLiked { get; set; } +} diff --git a/OF DL/Models/Entities/Highlights/HighlightMedia.cs b/OF DL/Models/Entities/Highlights/HighlightMedia.cs new file mode 100644 index 0000000..e082a0a --- /dev/null +++ b/OF DL/Models/Entities/Highlights/HighlightMedia.cs @@ -0,0 +1,6 @@ +namespace OF_DL.Models.Entities.Highlights; + +public class HighlightMedia +{ + public List Stories { get; set; } = []; +} diff --git a/OF DL/Models/Entities/Highlights/Highlights.cs b/OF DL/Models/Entities/Highlights/Highlights.cs new file mode 100644 index 0000000..a0a916c --- /dev/null +++ b/OF DL/Models/Entities/Highlights/Highlights.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Models.Entities.Highlights; + +public class Highlights +{ + public List List { get; set; } = []; + + public bool HasMore { get; set; } +} diff --git a/OF DL/Models/Entities/Highlights/ListItem.cs b/OF DL/Models/Entities/Highlights/ListItem.cs new file mode 100644 index 0000000..6d6a407 --- /dev/null +++ b/OF DL/Models/Entities/Highlights/ListItem.cs @@ -0,0 +1,6 @@ +namespace OF_DL.Models.Entities.Highlights; + +public class ListItem +{ + public long Id { get; set; } +} diff --git a/OF DL/Models/Entities/Highlights/Medium.cs b/OF DL/Models/Entities/Highlights/Medium.cs new file mode 100644 index 0000000..e3855eb --- /dev/null +++ b/OF DL/Models/Entities/Highlights/Medium.cs @@ -0,0 +1,16 @@ +using OF_DL.Models.Entities.Common; + +namespace OF_DL.Models.Entities.Highlights; + +public class Medium +{ + public long Id { get; set; } + + public string? Type { get; set; } + + public bool CanView { get; set; } + + public DateTime? CreatedAt { get; set; } + + public Files? Files { get; set; } +} diff --git a/OF DL/Models/Entities/Highlights/Story.cs b/OF DL/Models/Entities/Highlights/Story.cs new file mode 100644 index 0000000..ef2fb81 --- /dev/null +++ b/OF DL/Models/Entities/Highlights/Story.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Highlights; + +public class Story +{ + public long Id { get; set; } + + public DateTime? CreatedAt { get; set; } + + public List? Media { get; set; } +} diff --git a/OF DL/Models/Highlights/HighlightMedia.cs b/OF DL/Models/Highlights/HighlightMedia.cs deleted file mode 100644 index 93c4c92..0000000 --- a/OF DL/Models/Highlights/HighlightMedia.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Newtonsoft.Json; - -namespace OF_DL.Models.Highlights; - -public class HighlightMedia -{ - public long id { get; set; } - public long userId { get; set; } - public string title { get; set; } - public long coverStoryId { get; set; } - public string cover { get; set; } - public int storiesCount { get; set; } - public DateTime? createdAt { get; set; } - public List stories { get; set; } - - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - } - - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } - - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Files files { get; set; } - } - - public class Preview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } - - public class Source - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public int duration { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } - - public class Sources - { - [JsonProperty("720")] public string _720 { get; set; } - - [JsonProperty("240")] public string _240 { get; set; } - - public string w150 { get; set; } - public string w480 { get; set; } - } - - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } - - public class Story - { - public long id { get; set; } - public long userId { get; set; } - public bool isWatched { get; set; } - public bool isReady { get; set; } - public List media { get; set; } - public DateTime? createdAt { get; set; } - public object question { get; set; } - public bool canLike { get; set; } - public bool isLiked { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } -} diff --git a/OF DL/Models/Highlights/Highlights.cs b/OF DL/Models/Highlights/Highlights.cs deleted file mode 100644 index 7057a8a..0000000 --- a/OF DL/Models/Highlights/Highlights.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace OF_DL.Models.Highlights; - -public class Highlights -{ - public List list { get; set; } - public bool hasMore { get; set; } - - public class List - { - public long id { get; set; } - public long userId { get; set; } - public string title { get; set; } - public long coverStoryId { get; set; } - public string cover { get; set; } - public int storiesCount { get; set; } - public DateTime? createdAt { get; set; } - } -} diff --git a/OF DL/Models/Mappers/HighlightsMapper.cs b/OF DL/Models/Mappers/HighlightsMapper.cs new file mode 100644 index 0000000..450ea00 --- /dev/null +++ b/OF DL/Models/Mappers/HighlightsMapper.cs @@ -0,0 +1,66 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Highlights; +using OF_DL.Models.Entities.Common; +using OF_DL.Models.Entities.Highlights; + +namespace OF_DL.Models.Mappers; + +public static class HighlightsMapper +{ + public static Highlights FromDto(HighlightsDto? dto) + { + Highlights mapped = new() { HasMore = dto?.HasMore ?? false }; + + if (dto?.List == null) + { + return mapped; + } + + foreach (ListItemDto entry in dto.List) + { + mapped.List.Add(MapListItem(entry)); + } + + return mapped; + } + + public static HighlightMedia FromDto(HighlightMediaDto? dto) + { + HighlightMedia mapped = new(); + + if (dto?.Stories == null) + { + return mapped; + } + + foreach (StoryDto story in dto.Stories) + { + mapped.Stories.Add(MapStory(story)); + } + + return mapped; + } + + private static ListItem MapListItem(ListItemDto dto) => new() { Id = dto.Id }; + + private static Story MapStory(StoryDto dto) => + new() { Id = dto.Id, CreatedAt = dto.CreatedAt, Media = MapMedia(dto.Media) }; + + private static List? MapMedia(List? media) => + media?.Select(MapMedium).ToList(); + + private static Medium MapMedium(MediumDto dto) => + new() + { + Id = dto.Id, + Type = dto.Type, + CanView = dto.CanView, + CreatedAt = dto.CreatedAt, + Files = MapFiles(dto.Files) + }; + + private static Files? MapFiles(FilesDto? dto) => dto == null ? null : new Files { Full = MapFull(dto.Full) }; + + private static Full? MapFull(FullDto? dto) => + dto == null ? null : new Full { Url = dto.Url }; +} diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 1cbe8d0..18250ce 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -13,11 +13,11 @@ using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Helpers; -using OF_DL.Models.Entities.Archived; +using ArchivedModels = OF_DL.Models.Entities.Archived; using OF_DL.Services; using Serilog; using Spectre.Console; -using Constants = OF_DL.Widevine.Constants; +using WidevineConstants = OF_DL.Widevine.Constants; namespace OF_DL; @@ -502,7 +502,8 @@ public class Program(IServiceProvider serviceProvider) Environment.Exit(4); } - if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_client_id_blob"))) + if (!File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, WidevineConstants.DEVICE_NAME, + "device_client_id_blob"))) { clientIdBlobMissing = true; Log.Debug("clientIdBlobMissing missing"); @@ -510,11 +511,12 @@ public class Program(IServiceProvider serviceProvider) else { AnsiConsole.Markup("[green]device_client_id_blob located successfully![/]\n"); - Log.Debug("clientIdBlobMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, - Constants.DEVICE_NAME, "device_client_id_blob"))); + Log.Debug("clientIdBlobMissing found: " + File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, + WidevineConstants.DEVICE_NAME, "device_client_id_blob"))); } - if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_private_key"))) + if (!File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, WidevineConstants.DEVICE_NAME, + "device_private_key"))) { devicePrivateKeyMissing = true; Log.Debug("devicePrivateKeyMissing missing"); @@ -522,8 +524,8 @@ public class Program(IServiceProvider serviceProvider) else { AnsiConsole.Markup("[green]device_private_key located successfully![/]\n"); - Log.Debug("devicePrivateKeyMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, - Constants.DEVICE_NAME, "device_private_key"))); + Log.Debug("devicePrivateKeyMissing found: " + File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, + WidevineConstants.DEVICE_NAME, "device_private_key"))); } if (clientIdBlobMissing || devicePrivateKeyMissing) @@ -1272,7 +1274,7 @@ public class Program(IServiceProvider serviceProvider) IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - ArchivedCollection archived = new(); + ArchivedModels.ArchivedCollection archived = new(); await AnsiConsole.Status() .StartAsync("[red]Getting Archived Posts[/]", diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 03cee60..04beede 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -6,7 +6,6 @@ using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Highlights; using OF_DL.Models.Lists; using OF_DL.Models.Messages; using OF_DL.Models.Post; @@ -15,8 +14,9 @@ using OF_DL.Models.Stories; using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Models.Dtos.Archived; -using OF_DL.Models.Entities; -using OF_DL.Models.Entities.Archived; +using OF_DL.Models.Dtos.Highlights; +using ArchivedModels = OF_DL.Models.Entities.Archived; +using HighlightEntities = OF_DL.Models.Entities.Highlights; using OF_DL.Models.Mappers; using OF_DL.Widevine; using Serilog; @@ -452,24 +452,25 @@ public class APIService(IAuthService authService, IConfigService configService, else if (mediatype == MediaType.Highlights) { List highlight_ids = new(); - Highlights highlights = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings) ?? - new Highlights(); + HighlightsDto? highlightsDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + HighlightEntities.Highlights highlights = HighlightsMapper.FromDto(highlightsDto); - if (highlights.hasMore) + if (highlights.HasMore) { offset += 5; getParams["offset"] = offset.ToString(); while (true) { - Highlights newhighlights = new(); - Log.Debug("Media Highlights - " + endpoint); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - newhighlights = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + HighlightsDto? newHighlightsDto = + JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + HighlightEntities.Highlights newHighlights = HighlightsMapper.FromDto(newHighlightsDto); - highlights.list.AddRange(newhighlights.list); - if (!newhighlights.hasMore) + highlights.List.AddRange(newHighlights.List); + if (!newHighlights.HasMore) { break; } @@ -479,17 +480,16 @@ public class APIService(IAuthService authService, IConfigService configService, } } - foreach (Highlights.List list in highlights.list) + foreach (HighlightEntities.ListItem list in highlights.List) { - if (!highlight_ids.Contains(list.id.ToString())) + if (!highlight_ids.Contains(list.Id.ToString())) { - highlight_ids.Add(list.id.ToString()); + highlight_ids.Add(list.Id.ToString()); } } foreach (string highlight_id in highlight_ids) { - HighlightMedia highlightMedia = new(); Dictionary highlight_headers = GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, string.Empty); @@ -506,61 +506,61 @@ public class APIService(IAuthService authService, IConfigService configService, using HttpResponseMessage highlightResponse = await highlight_client.SendAsync(highlight_request); highlightResponse.EnsureSuccessStatusCode(); string highlightBody = await highlightResponse.Content.ReadAsStringAsync(); - highlightMedia = - JsonConvert.DeserializeObject(highlightBody, m_JsonSerializerSettings); - if (highlightMedia != null) + HighlightMediaDto? highlightMediaDto = + JsonConvert.DeserializeObject(highlightBody, m_JsonSerializerSettings); + HighlightEntities.HighlightMedia highlightMedia = HighlightsMapper.FromDto(highlightMediaDto); + + foreach (HighlightEntities.Story item in highlightMedia.Stories) { - foreach (HighlightMedia.Story item in highlightMedia.stories) + if (item.Media != null && item.Media.Count > 0 && item.Media[0].CreatedAt.HasValue) { - if (item.media[0].createdAt.HasValue) - { - await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, - item.media[0].createdAt.Value); - } - else if (item.createdAt.HasValue) - { - await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, - item.createdAt.Value); - } - else - { - await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, - DateTime.Now); - } + await dbService.AddStory(folder, item.Id, string.Empty, "0", false, false, + item.Media[0].CreatedAt.Value); + } + else if (item.CreatedAt.HasValue) + { + await dbService.AddStory(folder, item.Id, string.Empty, "0", false, false, + item.CreatedAt.Value); + } + else + { + await dbService.AddStory(folder, item.Id, string.Empty, "0", false, false, + DateTime.Now); + } - if (item.media.Count > 0 && item.media[0].canView) + if (item.Media != null && item.Media.Count > 0 && item.Media[0].CanView) + { + foreach (HighlightEntities.Medium medium in item.Media) { - foreach (HighlightMedia.Medium medium in item.media) + string storyUrl = item.Media[0].Files?.Full?.Url ?? string.Empty; + await dbService.AddMedia(folder, medium.Id, item.Id, storyUrl, null, null, null, + "Stories", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, false, false, null); + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { - await dbService.AddMedia(folder, medium.id, item.id, item.media[0].files.full.url, - null, null, null, "Stories", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, false, false, null); - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } + continue; + } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) + { + continue; + } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) + { + continue; + } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) - { - continue; - } + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + { + continue; + } - if (!return_urls.ContainsKey(medium.id)) - { - return_urls.Add(medium.id, item.media[0].files.full.url); - } + if (!return_urls.ContainsKey(medium.Id) && !string.IsNullOrEmpty(storyUrl)) + { + return_urls.Add(medium.Id, storyUrl); } } } @@ -1361,14 +1361,14 @@ public class APIService(IAuthService authService, IConfigService configService, } - public async Task GetArchived(string endpoint, string folder, StatusContext ctx) + public async Task GetArchived(string endpoint, string folder, StatusContext ctx) { Log.Debug($"Calling GetArchived - {endpoint}"); try { - Archived archived = new(); - ArchivedCollection archivedCollection = new(); + ArchivedModels.Archived archived = new(); + ArchivedModels.ArchivedCollection archivedCollection = new(); int post_limit = 50; Dictionary getParams = new() { @@ -1406,7 +1406,7 @@ public class APIService(IAuthService authService, IConfigService configService, archived.TailMarker); while (true) { - Archived newarchived = new(); + ArchivedModels.Archived newarchived = new(); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); ArchivedDto newarchivedDto = @@ -1429,7 +1429,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } - foreach (ListItem archive in archived.List) + foreach (ArchivedModels.ListItem archive in archived.List) { List previewids = new(); if (archive.Preview != null) @@ -1452,7 +1452,7 @@ public class APIService(IAuthService authService, IConfigService configService, archivedCollection.ArchivedPostObjects.Add(archive); if (archive.Media != null && archive.Media.Count > 0) { - foreach (Medium medium in archive.Media) + foreach (ArchivedModels.Medium medium in archive.Media) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 5495d8a..509433b 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -9,8 +9,7 @@ using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; -using OF_DL.Models.Entities; -using OF_DL.Models.Entities.Archived; +using ArchivedModels = OF_DL.Models.Entities.Archived; using OF_DL.Utils; using Serilog; using Serilog.Events; @@ -1037,7 +1036,7 @@ public class DownloadService( public async Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Messages.Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users) { string path; @@ -1061,7 +1060,7 @@ public class DownloadService( public async Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, FromUser? fromUser, Dictionary users) { string path; @@ -1085,8 +1084,8 @@ public class DownloadService( public async Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ListItem? messageInfo, - OF_DL.Models.Entities.Archived.Medium? messageMedia, Models.Entities.Common.Author? author, + IProgressReporter progressReporter, string? filenameFormat, ArchivedModels.ListItem? messageInfo, + ArchivedModels.Medium? messageMedia, Models.Entities.Common.Author? author, Dictionary users) { string path = "/Archived/Posts/Free"; @@ -1111,7 +1110,7 @@ public class DownloadService( public async Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) { string path; @@ -1135,7 +1134,7 @@ public class DownloadService( public async Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, FromUser? fromUser, Dictionary users) { string path; @@ -1164,7 +1163,7 @@ public class DownloadService( IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) { @@ -1194,7 +1193,7 @@ public class DownloadService( public async Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Messages.Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users) { try @@ -1324,7 +1323,7 @@ public class DownloadService( public async Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, FromUser? fromUser, Dictionary users) { try @@ -1455,7 +1454,7 @@ public class DownloadService( public async Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) { try @@ -1584,7 +1583,7 @@ public class DownloadService( public async Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, FromUser? fromUser, Dictionary users) { try @@ -2098,7 +2097,7 @@ public class DownloadService( public async Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, - Messages.Medium? postMedia, + Medium? postMedia, Purchased.FromUser? fromUser, Dictionary users) { try @@ -2228,8 +2227,8 @@ public class DownloadService( public async Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ListItem? postInfo, - OF_DL.Models.Entities.Archived.Medium? postMedia, + IProgressReporter progressReporter, string? filenameFormat, ArchivedModels.ListItem? postInfo, + ArchivedModels.Medium? postMedia, Models.Entities.Common.Author? author, Dictionary users) { try @@ -2455,7 +2454,7 @@ public class DownloadService( public async Task DownloadArchived(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, - ArchivedCollection archived, IProgressReporter progressReporter) + ArchivedModels.ArchivedCollection archived, IProgressReporter progressReporter) { Log.Debug($"Calling DownloadArchived - {username}"); @@ -2512,9 +2511,9 @@ public class DownloadService( pssh); } - OF_DL.Models.Entities.Archived.Medium? mediaInfo = + ArchivedModels.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); - ListItem? postInfo = + ArchivedModels.ListItem? postInfo = archived.ArchivedPostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadArchivedPostDRMVideo( @@ -2542,9 +2541,9 @@ public class DownloadService( } else { - OF_DL.Models.Entities.Archived.Medium? mediaInfo = + ArchivedModels.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); - ListItem? postInfo = + ArchivedModels.ListItem? postInfo = archived.ArchivedPostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadArchivedMedia( @@ -2642,7 +2641,7 @@ public class DownloadService( pssh); } - Messages.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); + Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); List? messageInfo = messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); @@ -2671,7 +2670,7 @@ public class DownloadService( } else { - Messages.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); + Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); List? messageInfo = messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); isNew = await DownloadMessageMedia( @@ -2758,7 +2757,7 @@ public class DownloadService( $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/message/{parsed[5]}?type=widevine", pssh); - Messages.Medium? mediaInfo = + Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == kvp.Key); Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => @@ -2776,7 +2775,7 @@ public class DownloadService( } else { - Messages.Medium? mediaInfo = + Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == kvp.Key); Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); @@ -3033,7 +3032,7 @@ public class DownloadService( $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", pssh); - Messages.Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); + Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); Purchased.List? postInfo = purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); @@ -3049,7 +3048,7 @@ public class DownloadService( } else { - Messages.Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); + Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); Purchased.List? postInfo = purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); isNew = await DownloadPurchasedPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index 5606b5c..ae00a9f 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -5,7 +5,7 @@ using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; -using OF_DL.Models.Entities.Archived; +using ArchivedModels = OF_DL.Models.Entities.Archived; using Spectre.Console; namespace OF_DL.Services; @@ -28,7 +28,7 @@ public interface IAPIService Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx); Task GetPost(string endpoint, string folder); Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx); - Task GetArchived(string endpoint, string folder, StatusContext ctx); + Task GetArchived(string endpoint, string folder, StatusContext ctx); Task GetMessages(string endpoint, string folder, StatusContext ctx); Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx); Task GetPaidMessage(string endpoint, string folder); diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index 63916c3..b334e91 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -1,5 +1,5 @@ using OF_DL.Models; -using OF_DL.Models.Entities.Archived; +using ArchivedModels = OF_DL.Models.Entities.Archived; using OF_DL.Models.Messages; using OF_DL.Models.Post; using OF_DL.Models.Purchased; @@ -17,14 +17,14 @@ public interface IDownloadService string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ListItem? messageInfo, - OF_DL.Models.Entities.Archived.Medium? messageMedia, Models.Entities.Common.Author? author, + IProgressReporter progressReporter, string? filenameFormat, ArchivedModels.ListItem? messageInfo, + ArchivedModels.Medium? messageMedia, Models.Entities.Common.Author? author, Dictionary users); Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ListItem? postInfo, - OF_DL.Models.Entities.Archived.Medium? postMedia, + IProgressReporter progressReporter, string? filenameFormat, ArchivedModels.ListItem? postInfo, + ArchivedModels.Medium? postMedia, Models.Entities.Common.Author? author, Dictionary users); Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, @@ -41,11 +41,11 @@ public interface IDownloadService Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, - string? filenameFormat, List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, + string? filenameFormat, List? messageInfo, Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users); Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Messages.Medium? messageMedia, + IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users); Task DownloadPostMedia(string url, string folder, long media_id, string api_type, @@ -58,35 +58,35 @@ public interface IDownloadService Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, - Messages.Medium? postMedia, + Medium? postMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, @@ -104,12 +104,12 @@ public interface IDownloadService Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Messages.Medium? messageMedia, + Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, @@ -119,7 +119,7 @@ public interface IDownloadService IProgressReporter progressReporter); Task DownloadArchived(string username, long userId, string path, Dictionary users, - bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedCollection archived, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedModels.ArchivedCollection archived, IProgressReporter progressReporter); Task DownloadMessages(string username, long userId, string path, Dictionary users, From 911f98bc250e87c1803c1cd366fd7b71f12da05c Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Sun, 8 Feb 2026 17:44:50 -0600 Subject: [PATCH 15/55] Refactor List entities into DTOs and application entities with standardized naming conventions and default values --- OF DL/Models/Dtos/Lists/AvatarThumbsDto.cs | 10 ++ OF DL/Models/Dtos/Lists/HeaderSizeDto.cs | 10 ++ OF DL/Models/Dtos/Lists/HeaderThumbsDto.cs | 10 ++ OF DL/Models/Dtos/Lists/ListsStateDto.cs | 16 ++ OF DL/Models/Dtos/Lists/SubscribeDto.cs | 38 ++++ .../Models/Dtos/Lists/SubscribedByDataDto.cs | 40 +++++ .../Models/Dtos/Lists/SubscribedOnDataDto.cs | 54 ++++++ .../Dtos/Lists/SubscriptionBundleDto.cs | 16 ++ OF DL/Models/Dtos/Lists/UserListDto.cs | 10 ++ OF DL/Models/Dtos/Lists/UserListItemDto.cs | 42 +++++ OF DL/Models/Dtos/Lists/UserListUserDto.cs | 10 ++ OF DL/Models/Dtos/Lists/UsersListDto.cs | 115 ++++++++++++ OF DL/Models/Entities/Lists/UserList.cs | 8 + OF DL/Models/Entities/Lists/UserListItem.cs | 8 + OF DL/Models/Entities/Lists/UsersList.cs | 6 + OF DL/Models/Lists/UserList.cs | 35 ---- OF DL/Models/Lists/UsersList.cs | 164 ------------------ OF DL/Models/Mappers/UserListsMapper.cs | 40 +++++ OF DL/Services/APIService.cs | 27 ++- 19 files changed, 446 insertions(+), 213 deletions(-) create mode 100644 OF DL/Models/Dtos/Lists/AvatarThumbsDto.cs create mode 100644 OF DL/Models/Dtos/Lists/HeaderSizeDto.cs create mode 100644 OF DL/Models/Dtos/Lists/HeaderThumbsDto.cs create mode 100644 OF DL/Models/Dtos/Lists/ListsStateDto.cs create mode 100644 OF DL/Models/Dtos/Lists/SubscribeDto.cs create mode 100644 OF DL/Models/Dtos/Lists/SubscribedByDataDto.cs create mode 100644 OF DL/Models/Dtos/Lists/SubscribedOnDataDto.cs create mode 100644 OF DL/Models/Dtos/Lists/SubscriptionBundleDto.cs create mode 100644 OF DL/Models/Dtos/Lists/UserListDto.cs create mode 100644 OF DL/Models/Dtos/Lists/UserListItemDto.cs create mode 100644 OF DL/Models/Dtos/Lists/UserListUserDto.cs create mode 100644 OF DL/Models/Dtos/Lists/UsersListDto.cs create mode 100644 OF DL/Models/Entities/Lists/UserList.cs create mode 100644 OF DL/Models/Entities/Lists/UserListItem.cs create mode 100644 OF DL/Models/Entities/Lists/UsersList.cs delete mode 100644 OF DL/Models/Lists/UserList.cs delete mode 100644 OF DL/Models/Lists/UsersList.cs create mode 100644 OF DL/Models/Mappers/UserListsMapper.cs diff --git a/OF DL/Models/Dtos/Lists/AvatarThumbsDto.cs b/OF DL/Models/Dtos/Lists/AvatarThumbsDto.cs new file mode 100644 index 0000000..62ece7c --- /dev/null +++ b/OF DL/Models/Dtos/Lists/AvatarThumbsDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class AvatarThumbsDto +{ + [JsonProperty("c50")] public string C50 { get; set; } = ""; + + [JsonProperty("c144")] public string C144 { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Lists/HeaderSizeDto.cs b/OF DL/Models/Dtos/Lists/HeaderSizeDto.cs new file mode 100644 index 0000000..4e37f8c --- /dev/null +++ b/OF DL/Models/Dtos/Lists/HeaderSizeDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class HeaderSizeDto +{ + [JsonProperty("width")] public int? Width { get; set; } + + [JsonProperty("height")] public int? Height { get; set; } +} diff --git a/OF DL/Models/Dtos/Lists/HeaderThumbsDto.cs b/OF DL/Models/Dtos/Lists/HeaderThumbsDto.cs new file mode 100644 index 0000000..8ddbbe3 --- /dev/null +++ b/OF DL/Models/Dtos/Lists/HeaderThumbsDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class HeaderThumbsDto +{ + [JsonProperty("w480")] public string W480 { get; set; } = ""; + + [JsonProperty("w760")] public string W760 { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Lists/ListsStateDto.cs b/OF DL/Models/Dtos/Lists/ListsStateDto.cs new file mode 100644 index 0000000..375e9a2 --- /dev/null +++ b/OF DL/Models/Dtos/Lists/ListsStateDto.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class ListsStateDto +{ + [JsonProperty("id")] public string Id { get; set; } = ""; + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("name")] public string Name { get; set; } = ""; + + [JsonProperty("hasUser")] public bool HasUser { get; set; } + + [JsonProperty("canAddUser")] public bool CanAddUser { get; set; } +} diff --git a/OF DL/Models/Dtos/Lists/SubscribeDto.cs b/OF DL/Models/Dtos/Lists/SubscribeDto.cs new file mode 100644 index 0000000..bb7c0b7 --- /dev/null +++ b/OF DL/Models/Dtos/Lists/SubscribeDto.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class SubscribeDto +{ + [JsonProperty("id")] public object Id { get; set; } = new(); + + [JsonProperty("userId")] public long? UserId { get; set; } + + [JsonProperty("subscriberId")] public int? SubscriberId { get; set; } + + [JsonProperty("date")] public DateTime? Date { get; set; } + + [JsonProperty("duration")] public int? Duration { get; set; } + + [JsonProperty("startDate")] public DateTime? StartDate { get; set; } + + [JsonProperty("expireDate")] public DateTime? ExpireDate { get; set; } + + [JsonProperty("cancelDate")] public object CancelDate { get; set; } = new(); + + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("regularPrice")] public string? RegularPrice { get; set; } + + [JsonProperty("discount")] public string? Discount { get; set; } + + [JsonProperty("action")] public string Action { get; set; } = ""; + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("offerStart")] public object OfferStart { get; set; } = new(); + + [JsonProperty("offerEnd")] public object OfferEnd { get; set; } = new(); + + [JsonProperty("isCurrent")] public bool? IsCurrent { get; set; } +} diff --git a/OF DL/Models/Dtos/Lists/SubscribedByDataDto.cs b/OF DL/Models/Dtos/Lists/SubscribedByDataDto.cs new file mode 100644 index 0000000..8e73fad --- /dev/null +++ b/OF DL/Models/Dtos/Lists/SubscribedByDataDto.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class SubscribedByDataDto +{ + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("newPrice")] public string? NewPrice { get; set; } + + [JsonProperty("regularPrice")] public string? RegularPrice { get; set; } + + [JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; } + + [JsonProperty("discountPercent")] public string? DiscountPercent { get; set; } + + [JsonProperty("discountPeriod")] public string? DiscountPeriod { get; set; } + + [JsonProperty("subscribeAt")] public DateTime? SubscribeAt { get; set; } + + [JsonProperty("expiredAt")] public DateTime? ExpiredAt { get; set; } + + [JsonProperty("renewedAt")] public object RenewedAt { get; set; } = new(); + + [JsonProperty("discountFinishedAt")] public object DiscountFinishedAt { get; set; } = new(); + + [JsonProperty("discountStartedAt")] public object DiscountStartedAt { get; set; } = new(); + + [JsonProperty("status")] public string Status { get; set; } = ""; + + [JsonProperty("isMuted")] public bool? IsMuted { get; set; } + + [JsonProperty("unsubscribeReason")] public string UnsubscribeReason { get; set; } = ""; + + [JsonProperty("duration")] public string Duration { get; set; } = ""; + + [JsonProperty("showPostsInFeed")] public bool? ShowPostsInFeed { get; set; } + + [JsonProperty("subscribes")] public List Subscribes { get; set; } = []; +} diff --git a/OF DL/Models/Dtos/Lists/SubscribedOnDataDto.cs b/OF DL/Models/Dtos/Lists/SubscribedOnDataDto.cs new file mode 100644 index 0000000..ab1ed2f --- /dev/null +++ b/OF DL/Models/Dtos/Lists/SubscribedOnDataDto.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class SubscribedOnDataDto +{ + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("newPrice")] public string? NewPrice { get; set; } + + [JsonProperty("regularPrice")] public string? RegularPrice { get; set; } + + [JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; } + + [JsonProperty("discountPercent")] public string? DiscountPercent { get; set; } + + [JsonProperty("discountPeriod")] public string? DiscountPeriod { get; set; } + + [JsonProperty("subscribeAt")] public DateTime? SubscribeAt { get; set; } + + [JsonProperty("expiredAt")] public DateTime? ExpiredAt { get; set; } + + [JsonProperty("renewedAt")] public object RenewedAt { get; set; } = new(); + + [JsonProperty("discountFinishedAt")] public object DiscountFinishedAt { get; set; } = new(); + + [JsonProperty("discountStartedAt")] public object DiscountStartedAt { get; set; } = new(); + + [JsonProperty("status")] public object Status { get; set; } = new(); + + [JsonProperty("isMuted")] public bool? IsMuted { get; set; } + + [JsonProperty("unsubscribeReason")] public string UnsubscribeReason { get; set; } = ""; + + [JsonProperty("duration")] public string Duration { get; set; } = ""; + + [JsonProperty("tipsSumm")] public string? TipsSumm { get; set; } + + [JsonProperty("subscribesSumm")] public string? SubscribesSumm { get; set; } + + [JsonProperty("messagesSumm")] public string? MessagesSumm { get; set; } + + [JsonProperty("postsSumm")] public string? PostsSumm { get; set; } + + [JsonProperty("streamsSumm")] public string? StreamsSumm { get; set; } + + [JsonProperty("totalSumm")] public string? TotalSumm { get; set; } + + [JsonProperty("lastActivity")] public DateTime? LastActivity { get; set; } + + [JsonProperty("recommendations")] public int? Recommendations { get; set; } + + [JsonProperty("subscribes")] public List Subscribes { get; set; } = []; +} diff --git a/OF DL/Models/Dtos/Lists/SubscriptionBundleDto.cs b/OF DL/Models/Dtos/Lists/SubscriptionBundleDto.cs new file mode 100644 index 0000000..38e9c62 --- /dev/null +++ b/OF DL/Models/Dtos/Lists/SubscriptionBundleDto.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class SubscriptionBundleDto +{ + [JsonProperty("id")] public long? Id { get; set; } + + [JsonProperty("discount")] public string? Discount { get; set; } + + [JsonProperty("duration")] public string? Duration { get; set; } + + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("canBuy")] public bool? CanBuy { get; set; } +} diff --git a/OF DL/Models/Dtos/Lists/UserListDto.cs b/OF DL/Models/Dtos/Lists/UserListDto.cs new file mode 100644 index 0000000..ded01cb --- /dev/null +++ b/OF DL/Models/Dtos/Lists/UserListDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class UserListDto +{ + [JsonProperty("list")] public List List { get; set; } = []; + + [JsonProperty("hasMore")] public bool? HasMore { get; set; } +} diff --git a/OF DL/Models/Dtos/Lists/UserListItemDto.cs b/OF DL/Models/Dtos/Lists/UserListItemDto.cs new file mode 100644 index 0000000..9b5f7a8 --- /dev/null +++ b/OF DL/Models/Dtos/Lists/UserListItemDto.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class UserListItemDto +{ + [JsonProperty("id")] public string Id { get; set; } = ""; + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("name")] public string Name { get; set; } = ""; + + [JsonProperty("usersCount")] public int? UsersCount { get; set; } + + [JsonProperty("postsCount")] public int? PostsCount { get; set; } + + [JsonProperty("canUpdate")] public bool? CanUpdate { get; set; } + + [JsonProperty("canDelete")] public bool? CanDelete { get; set; } + + [JsonProperty("canManageUsers")] public bool? CanManageUsers { get; set; } + + [JsonProperty("canAddUsers")] public bool? CanAddUsers { get; set; } + + [JsonProperty("canPinnedToFeed")] public bool? CanPinnedToFeed { get; set; } + + [JsonProperty("isPinnedToFeed")] public bool? IsPinnedToFeed { get; set; } + + [JsonProperty("canPinnedToChat")] public bool? CanPinnedToChat { get; set; } + + [JsonProperty("isPinnedToChat")] public bool? IsPinnedToChat { get; set; } + + [JsonProperty("order")] public string Order { get; set; } = ""; + + [JsonProperty("direction")] public string Direction { get; set; } = ""; + + [JsonProperty("users")] public List Users { get; set; } = []; + + [JsonProperty("customOrderUsersIds")] public List CustomOrderUsersIds { get; set; } = []; + + [JsonProperty("posts")] public List Posts { get; set; } = []; +} diff --git a/OF DL/Models/Dtos/Lists/UserListUserDto.cs b/OF DL/Models/Dtos/Lists/UserListUserDto.cs new file mode 100644 index 0000000..aaca253 --- /dev/null +++ b/OF DL/Models/Dtos/Lists/UserListUserDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class UserListUserDto +{ + [JsonProperty("id")] public long? Id { get; set; } + + [JsonProperty("_view")] public string View { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Lists/UsersListDto.cs b/OF DL/Models/Dtos/Lists/UsersListDto.cs new file mode 100644 index 0000000..2154096 --- /dev/null +++ b/OF DL/Models/Dtos/Lists/UsersListDto.cs @@ -0,0 +1,115 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Lists; + +public class UsersListDto +{ + [JsonProperty("view")] public string View { get; set; } = ""; + + [JsonProperty("avatar")] public string Avatar { get; set; } = ""; + + [JsonProperty("avatarThumbs")] public AvatarThumbsDto AvatarThumbs { get; set; } = new(); + + [JsonProperty("header")] public string Header { get; set; } = ""; + + [JsonProperty("headerSize")] public HeaderSizeDto HeaderSize { get; set; } = new(); + + [JsonProperty("headerThumbs")] public HeaderThumbsDto HeaderThumbs { get; set; } = new(); + + [JsonProperty("id")] public long? Id { get; set; } + + [JsonProperty("name")] public string Name { get; set; } = ""; + + [JsonProperty("username")] public string Username { get; set; } = ""; + + [JsonProperty("canLookStory")] public bool? CanLookStory { get; set; } + + [JsonProperty("canCommentStory")] public bool? CanCommentStory { get; set; } + + [JsonProperty("hasNotViewedStory")] public bool? HasNotViewedStory { get; set; } + + [JsonProperty("isVerified")] public bool? IsVerified { get; set; } + + [JsonProperty("canPayInternal")] public bool? CanPayInternal { get; set; } + + [JsonProperty("hasScheduledStream")] public bool? HasScheduledStream { get; set; } + + [JsonProperty("hasStream")] public bool? HasStream { get; set; } + + [JsonProperty("hasStories")] public bool? HasStories { get; set; } + + [JsonProperty("tipsEnabled")] public bool? TipsEnabled { get; set; } + + [JsonProperty("tipsTextEnabled")] public bool? TipsTextEnabled { get; set; } + + [JsonProperty("tipsMin")] public int? TipsMin { get; set; } + + [JsonProperty("tipsMinInternal")] public int? TipsMinInternal { get; set; } + + [JsonProperty("tipsMax")] public int? TipsMax { get; set; } + + [JsonProperty("canEarn")] public bool? CanEarn { get; set; } + + [JsonProperty("canAddSubscriber")] public bool? CanAddSubscriber { get; set; } + + [JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; } + + [JsonProperty("subscriptionBundles")] + public List SubscriptionBundles { get; set; } = []; + + [JsonProperty("displayName")] public string DisplayName { get; set; } = ""; + + [JsonProperty("notice")] public string Notice { get; set; } = ""; + + [JsonProperty("isPaywallRequired")] public bool? IsPaywallRequired { get; set; } + + [JsonProperty("unprofitable")] public bool? Unprofitable { get; set; } + + [JsonProperty("listsStates")] public List ListsStates { get; set; } = []; + + [JsonProperty("isMuted")] public bool? IsMuted { get; set; } + + [JsonProperty("isRestricted")] public bool? IsRestricted { get; set; } + + [JsonProperty("canRestrict")] public bool? CanRestrict { get; set; } + + [JsonProperty("subscribedBy")] public bool? SubscribedBy { get; set; } + + [JsonProperty("subscribedByExpire")] public bool? SubscribedByExpire { get; set; } + + [JsonProperty("subscribedByExpireDate")] public DateTime? SubscribedByExpireDate { get; set; } + + [JsonProperty("subscribedByAutoprolong")] public bool? SubscribedByAutoprolong { get; set; } + + [JsonProperty("subscribedIsExpiredNow")] public bool? SubscribedIsExpiredNow { get; set; } + + [JsonProperty("currentSubscribePrice")] public string? CurrentSubscribePrice { get; set; } + + [JsonProperty("subscribedOn")] public bool? SubscribedOn { get; set; } + + [JsonProperty("subscribedOnExpiredNow")] public bool? SubscribedOnExpiredNow { get; set; } + + [JsonProperty("subscribedOnDuration")] public string SubscribedOnDuration { get; set; } = ""; + + [JsonProperty("canReport")] public bool? CanReport { get; set; } + + [JsonProperty("canReceiveChatMessage")] public bool? CanReceiveChatMessage { get; set; } + + [JsonProperty("hideChat")] public bool? HideChat { get; set; } + + [JsonProperty("lastSeen")] public DateTime? LastSeen { get; set; } + + [JsonProperty("isPerformer")] public bool? IsPerformer { get; set; } + + [JsonProperty("isRealPerformer")] public bool? IsRealPerformer { get; set; } + + [JsonProperty("subscribedByData")] public SubscribedByDataDto SubscribedByData { get; set; } = new(); + + [JsonProperty("subscribedOnData")] public SubscribedOnDataDto SubscribedOnData { get; set; } = new(); + + [JsonProperty("canTrialSend")] public bool? CanTrialSend { get; set; } + + [JsonProperty("isBlocked")] public bool? IsBlocked { get; set; } + + [JsonProperty("promoOffers")] public List PromoOffers { get; set; } = []; +} diff --git a/OF DL/Models/Entities/Lists/UserList.cs b/OF DL/Models/Entities/Lists/UserList.cs new file mode 100644 index 0000000..69babb3 --- /dev/null +++ b/OF DL/Models/Entities/Lists/UserList.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Models.Entities.Lists; + +public class UserList +{ + public List List { get; set; } = []; + + public bool HasMore { get; set; } +} diff --git a/OF DL/Models/Entities/Lists/UserListItem.cs b/OF DL/Models/Entities/Lists/UserListItem.cs new file mode 100644 index 0000000..ad57069 --- /dev/null +++ b/OF DL/Models/Entities/Lists/UserListItem.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Models.Entities.Lists; + +public class UserListItem +{ + public string Id { get; set; } = ""; + + public string Name { get; set; } = ""; +} diff --git a/OF DL/Models/Entities/Lists/UsersList.cs b/OF DL/Models/Entities/Lists/UsersList.cs new file mode 100644 index 0000000..877b982 --- /dev/null +++ b/OF DL/Models/Entities/Lists/UsersList.cs @@ -0,0 +1,6 @@ +namespace OF_DL.Models.Entities.Lists; + +public class UsersList +{ + public string Username { get; set; } = ""; +} diff --git a/OF DL/Models/Lists/UserList.cs b/OF DL/Models/Lists/UserList.cs deleted file mode 100644 index f499edb..0000000 --- a/OF DL/Models/Lists/UserList.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace OF_DL.Models.Lists; - -public class UserList -{ - public List list { get; set; } - public bool? hasMore { get; set; } - - public class List - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public int? usersCount { get; set; } - public int? postsCount { get; set; } - public bool? canUpdate { get; set; } - public bool? canDelete { get; set; } - public bool? canManageUsers { get; set; } - public bool? canAddUsers { get; set; } - public bool? canPinnedToFeed { get; set; } - public bool? isPinnedToFeed { get; set; } - public bool? canPinnedToChat { get; set; } - public bool? isPinnedToChat { get; set; } - public string order { get; set; } - public string direction { get; set; } - public List users { get; set; } - public List customOrderUsersIds { get; set; } - public List posts { get; set; } - } - - public class User - { - public long? id { get; set; } - public string _view { get; set; } - } -} diff --git a/OF DL/Models/Lists/UsersList.cs b/OF DL/Models/Lists/UsersList.cs deleted file mode 100644 index 806b4aa..0000000 --- a/OF DL/Models/Lists/UsersList.cs +++ /dev/null @@ -1,164 +0,0 @@ -namespace OF_DL.Models.Lists; - -public class UsersList -{ - public string view { get; set; } - public string avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long? id { get; set; } - public string name { get; set; } - public string username { get; set; } - public bool? canLookStory { get; set; } - public bool? canCommentStory { get; set; } - public bool? hasNotViewedStory { get; set; } - public bool? isVerified { get; set; } - public bool? canPayInternal { get; set; } - public bool? hasScheduledStream { get; set; } - public bool? hasStream { get; set; } - public bool? hasStories { get; set; } - public bool? tipsEnabled { get; set; } - public bool? tipsTextEnabled { get; set; } - public int? tipsMin { get; set; } - public int? tipsMinInternal { get; set; } - public int? tipsMax { get; set; } - public bool? canEarn { get; set; } - public bool? canAddSubscriber { get; set; } - public string? subscribePrice { get; set; } - public List subscriptionBundles { get; set; } - public string displayName { get; set; } - public string notice { get; set; } - public bool? isPaywallRequired { get; set; } - public bool? unprofitable { get; set; } - public List listsStates { get; set; } - public bool? isMuted { get; set; } - public bool? isRestricted { get; set; } - public bool? canRestrict { get; set; } - public bool? subscribedBy { get; set; } - public bool? subscribedByExpire { get; set; } - public DateTime? subscribedByExpireDate { get; set; } - public bool? subscribedByAutoprolong { get; set; } - public bool? subscribedIsExpiredNow { get; set; } - public string? currentSubscribePrice { get; set; } - public bool? subscribedOn { get; set; } - public bool? subscribedOnExpiredNow { get; set; } - public string subscribedOnDuration { get; set; } - public bool? canReport { get; set; } - public bool? canReceiveChatMessage { get; set; } - public bool? hideChat { get; set; } - public DateTime? lastSeen { get; set; } - public bool? isPerformer { get; set; } - public bool? isRealPerformer { get; set; } - public SubscribedByData subscribedByData { get; set; } - public SubscribedOnData subscribedOnData { get; set; } - public bool? canTrialSend { get; set; } - public bool? isBlocked { get; set; } - public List promoOffers { get; set; } - - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } - - public class HeaderSize - { - public int? width { get; set; } - public int? height { get; set; } - } - - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } - - public class ListsState - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool hasUser { get; set; } - public bool canAddUser { get; set; } - } - - public class Subscribe - { - public object id { get; set; } - public long? userId { get; set; } - public int? subscriberId { get; set; } - public DateTime? date { get; set; } - public int? duration { get; set; } - public DateTime? startDate { get; set; } - public DateTime? expireDate { get; set; } - public object cancelDate { get; set; } - public string? price { get; set; } - public string? regularPrice { get; set; } - public string? discount { get; set; } - public string action { get; set; } - public string type { get; set; } - public object offerStart { get; set; } - public object offerEnd { get; set; } - public bool? isCurrent { get; set; } - } - - public class SubscribedByData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public string? discountPercent { get; set; } - public string? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public object renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public string status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public bool? showPostsInFeed { get; set; } - public List subscribes { get; set; } - } - - public class SubscribedOnData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public string? discountPercent { get; set; } - public string? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public object renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public object status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public string? tipsSumm { get; set; } - public string? subscribesSumm { get; set; } - public string? messagesSumm { get; set; } - public string? postsSumm { get; set; } - public string? streamsSumm { get; set; } - public string? totalSumm { get; set; } - public DateTime? lastActivity { get; set; } - public int? recommendations { get; set; } - public List subscribes { get; set; } - } - - public class SubscriptionBundle - { - public long? id { get; set; } - public string? discount { get; set; } - public string? duration { get; set; } - public string? price { get; set; } - public bool? canBuy { get; set; } - } -} diff --git a/OF DL/Models/Mappers/UserListsMapper.cs b/OF DL/Models/Mappers/UserListsMapper.cs new file mode 100644 index 0000000..1ff4e32 --- /dev/null +++ b/OF DL/Models/Mappers/UserListsMapper.cs @@ -0,0 +1,40 @@ +using OF_DL.Models.Dtos.Lists; +using OF_DL.Models.Entities.Lists; + +namespace OF_DL.Models.Mappers; + +public static class UserListsMapper +{ + public static UserList FromDto(UserListDto? dto) + { + UserList mapped = new() { HasMore = dto?.HasMore ?? false }; + + if (dto?.List == null) + { + return mapped; + } + + foreach (UserListItemDto entry in dto.List) + { + mapped.List.Add(MapListItem(entry)); + } + + return mapped; + } + + public static List FromDto(List? dto) + { + if (dto == null) + { + return []; + } + + return dto.Select(MapUsersList).ToList(); + } + + private static UserListItem MapListItem(UserListItemDto dto) => + new() { Id = dto.Id, Name = dto.Name }; + + private static UsersList MapUsersList(UsersListDto dto) => + new() { Username = dto.Username }; +} diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 04beede..149a35f 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -6,7 +6,6 @@ using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Lists; using OF_DL.Models.Messages; using OF_DL.Models.Post; using OF_DL.Models.Purchased; @@ -14,9 +13,11 @@ using OF_DL.Models.Stories; using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Models.Dtos.Archived; +using OF_DL.Models.Dtos.Lists; using OF_DL.Models.Dtos.Highlights; using ArchivedModels = OF_DL.Models.Entities.Archived; using HighlightEntities = OF_DL.Models.Entities.Highlights; +using ListEntities = OF_DL.Models.Entities.Lists; using OF_DL.Models.Mappers; using OF_DL.Widevine; using Serilog; @@ -242,21 +243,18 @@ public class APIService(IAuthService authService, IConfigService configService, break; } - UserList userList = JsonConvert.DeserializeObject(body); - if (userList == null) - { - break; - } + UserListDto? userListDto = JsonConvert.DeserializeObject(body); + ListEntities.UserList userList = UserListsMapper.FromDto(userListDto); - foreach (UserList.List l in userList.list) + foreach (ListEntities.UserListItem listItem in userList.List) { - if (IsStringOnlyDigits(l.id) && !lists.ContainsKey(l.name)) + if (IsStringOnlyDigits(listItem.Id) && !lists.ContainsKey(listItem.Name)) { - lists.Add(l.name, Convert.ToInt32(l.id)); + lists.Add(listItem.Name, Convert.ToInt32(listItem.Id)); } } - if (userList.hasMore.Value) + if (userList.HasMore) { offset += 50; getParams["offset"] = Convert.ToString(offset); @@ -305,16 +303,17 @@ public class APIService(IAuthService authService, IConfigService configService, break; } - List? usersList = JsonConvert.DeserializeObject>(body); + List? usersListDto = JsonConvert.DeserializeObject>(body); + List usersList = UserListsMapper.FromDto(usersListDto); - if (usersList == null || usersList.Count <= 0) + if (usersList.Count <= 0) { break; } - foreach (UsersList ul in usersList) + foreach (ListEntities.UsersList ul in usersList) { - users.Add(ul.username); + users.Add(ul.Username); } if (users.Count < 50) From d8794ee219f45191b8af3019b21ad92fa33f2b4f Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Sun, 8 Feb 2026 19:15:32 -0600 Subject: [PATCH 16/55] Refactor Message entities into DTOs and application entities with standardized naming conventions and default values --- .../Dtos/{Lists => Common}/AvatarThumbsDto.cs | 2 +- OF DL/Models/Dtos/Common/HeaderSizeDto.cs | 10 + OF DL/Models/Dtos/Common/HeaderThumbsDto.cs | 10 + OF DL/Models/Dtos/Common/VideoDto.cs | 8 + OF DL/Models/Dtos/Lists/UsersListDto.cs | 22 +- OF DL/Models/Dtos/Messages/FromUserDto.cs | 98 +++ OF DL/Models/Dtos/Messages/InfoDto.cs | 11 + OF DL/Models/Dtos/Messages/ListItemDto.cs | 66 ++ OF DL/Models/Dtos/Messages/ListsStateDto.cs | 18 + OF DL/Models/Dtos/Messages/MediumDto.cs | 37 + OF DL/Models/Dtos/Messages/MessagesDto.cs | 10 + .../Models/Dtos/Messages/SingleMessageDto.cs | 60 ++ OF DL/Models/Entities/Common/FromUser.cs | 6 + OF DL/Models/Entities/Messages/ListItem.cs | 22 + OF DL/Models/Entities/Messages/Medium.cs | 14 + .../Entities/Messages/MessageCollection.cs | 10 + OF DL/Models/Entities/Messages/Messages.cs | 8 + .../Models/Entities/Messages/SingleMessage.cs | 20 + OF DL/Models/Mappers/MessagesMapper.cs | 110 +++ OF DL/Models/Messages/MessageCollection.cs | 8 - OF DL/Models/Messages/Messages.cs | 173 ---- OF DL/Models/Messages/SingleMessage.cs | 115 --- .../Models/Purchased/PaidMessageCollection.cs | 4 +- OF DL/Models/Purchased/PaidPostCollection.cs | 4 +- OF DL/Models/Purchased/Purchased.cs | 4 +- .../Purchased/SinglePaidMessageCollection.cs | 9 +- OF DL/Program.cs | 72 +- OF DL/Services/APIService.cs | 824 +++++++++--------- OF DL/Services/DownloadService.cs | 140 +-- OF DL/Services/IAPIService.cs | 8 +- OF DL/Services/IDownloadService.cs | 56 +- 31 files changed, 1102 insertions(+), 857 deletions(-) rename OF DL/Models/Dtos/{Lists => Common}/AvatarThumbsDto.cs (84%) create mode 100644 OF DL/Models/Dtos/Common/HeaderSizeDto.cs create mode 100644 OF DL/Models/Dtos/Common/HeaderThumbsDto.cs create mode 100644 OF DL/Models/Dtos/Common/VideoDto.cs create mode 100644 OF DL/Models/Dtos/Messages/FromUserDto.cs create mode 100644 OF DL/Models/Dtos/Messages/InfoDto.cs create mode 100644 OF DL/Models/Dtos/Messages/ListItemDto.cs create mode 100644 OF DL/Models/Dtos/Messages/ListsStateDto.cs create mode 100644 OF DL/Models/Dtos/Messages/MediumDto.cs create mode 100644 OF DL/Models/Dtos/Messages/MessagesDto.cs create mode 100644 OF DL/Models/Dtos/Messages/SingleMessageDto.cs create mode 100644 OF DL/Models/Entities/Common/FromUser.cs create mode 100644 OF DL/Models/Entities/Messages/ListItem.cs create mode 100644 OF DL/Models/Entities/Messages/Medium.cs create mode 100644 OF DL/Models/Entities/Messages/MessageCollection.cs create mode 100644 OF DL/Models/Entities/Messages/Messages.cs create mode 100644 OF DL/Models/Entities/Messages/SingleMessage.cs create mode 100644 OF DL/Models/Mappers/MessagesMapper.cs delete mode 100644 OF DL/Models/Messages/MessageCollection.cs delete mode 100644 OF DL/Models/Messages/Messages.cs delete mode 100644 OF DL/Models/Messages/SingleMessage.cs diff --git a/OF DL/Models/Dtos/Lists/AvatarThumbsDto.cs b/OF DL/Models/Dtos/Common/AvatarThumbsDto.cs similarity index 84% rename from OF DL/Models/Dtos/Lists/AvatarThumbsDto.cs rename to OF DL/Models/Dtos/Common/AvatarThumbsDto.cs index 62ece7c..ab37032 100644 --- a/OF DL/Models/Dtos/Lists/AvatarThumbsDto.cs +++ b/OF DL/Models/Dtos/Common/AvatarThumbsDto.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Models.Dtos.Lists; +namespace OF_DL.Models.Dtos.Common; public class AvatarThumbsDto { diff --git a/OF DL/Models/Dtos/Common/HeaderSizeDto.cs b/OF DL/Models/Dtos/Common/HeaderSizeDto.cs new file mode 100644 index 0000000..3e09784 --- /dev/null +++ b/OF DL/Models/Dtos/Common/HeaderSizeDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class HeaderSizeDto +{ + [JsonProperty("width")] public int Width { get; set; } + + [JsonProperty("height")] public int Height { get; set; } +} diff --git a/OF DL/Models/Dtos/Common/HeaderThumbsDto.cs b/OF DL/Models/Dtos/Common/HeaderThumbsDto.cs new file mode 100644 index 0000000..9e4b763 --- /dev/null +++ b/OF DL/Models/Dtos/Common/HeaderThumbsDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class HeaderThumbsDto +{ + [JsonProperty("w480")] public string W480 { get; set; } = ""; + + [JsonProperty("w760")] public string W760 { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Common/VideoDto.cs b/OF DL/Models/Dtos/Common/VideoDto.cs new file mode 100644 index 0000000..003ea73 --- /dev/null +++ b/OF DL/Models/Dtos/Common/VideoDto.cs @@ -0,0 +1,8 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Common; + +public class VideoDto +{ + [JsonProperty("mp4")] public string Mp4 { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Lists/UsersListDto.cs b/OF DL/Models/Dtos/Lists/UsersListDto.cs index 2154096..3885214 100644 --- a/OF DL/Models/Dtos/Lists/UsersListDto.cs +++ b/OF DL/Models/Dtos/Lists/UsersListDto.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; namespace OF_DL.Models.Dtos.Lists; @@ -54,8 +55,7 @@ public class UsersListDto [JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; } - [JsonProperty("subscriptionBundles")] - public List SubscriptionBundles { get; set; } = []; + [JsonProperty("subscriptionBundles")] public List SubscriptionBundles { get; set; } = []; [JsonProperty("displayName")] public string DisplayName { get; set; } = ""; @@ -77,23 +77,29 @@ public class UsersListDto [JsonProperty("subscribedByExpire")] public bool? SubscribedByExpire { get; set; } - [JsonProperty("subscribedByExpireDate")] public DateTime? SubscribedByExpireDate { get; set; } + [JsonProperty("subscribedByExpireDate")] + public DateTime? SubscribedByExpireDate { get; set; } - [JsonProperty("subscribedByAutoprolong")] public bool? SubscribedByAutoprolong { get; set; } + [JsonProperty("subscribedByAutoprolong")] + public bool? SubscribedByAutoprolong { get; set; } - [JsonProperty("subscribedIsExpiredNow")] public bool? SubscribedIsExpiredNow { get; set; } + [JsonProperty("subscribedIsExpiredNow")] + public bool? SubscribedIsExpiredNow { get; set; } - [JsonProperty("currentSubscribePrice")] public string? CurrentSubscribePrice { get; set; } + [JsonProperty("currentSubscribePrice")] + public string? CurrentSubscribePrice { get; set; } [JsonProperty("subscribedOn")] public bool? SubscribedOn { get; set; } - [JsonProperty("subscribedOnExpiredNow")] public bool? SubscribedOnExpiredNow { get; set; } + [JsonProperty("subscribedOnExpiredNow")] + public bool? SubscribedOnExpiredNow { get; set; } [JsonProperty("subscribedOnDuration")] public string SubscribedOnDuration { get; set; } = ""; [JsonProperty("canReport")] public bool? CanReport { get; set; } - [JsonProperty("canReceiveChatMessage")] public bool? CanReceiveChatMessage { get; set; } + [JsonProperty("canReceiveChatMessage")] + public bool? CanReceiveChatMessage { get; set; } [JsonProperty("hideChat")] public bool? HideChat { get; set; } diff --git a/OF DL/Models/Dtos/Messages/FromUserDto.cs b/OF DL/Models/Dtos/Messages/FromUserDto.cs new file mode 100644 index 0000000..999b7f6 --- /dev/null +++ b/OF DL/Models/Dtos/Messages/FromUserDto.cs @@ -0,0 +1,98 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Messages; + +public class FromUserDto +{ + [JsonProperty("_view")] public string ViewRaw { get; set; } = ""; + + [JsonProperty("view")] public string View { get; set; } = ""; + + [JsonProperty("avatar")] public string Avatar { get; set; } = ""; + + [JsonProperty("avatarThumbs")] public AvatarThumbsDto AvatarThumbs { get; set; } = new(); + + [JsonProperty("header")] public string Header { get; set; } = ""; + + [JsonProperty("headerSize")] public HeaderSizeDto HeaderSize { get; set; } = new(); + + [JsonProperty("headerThumbs")] public HeaderThumbsDto HeaderThumbs { get; set; } = new(); + + [JsonProperty("id")] public long? Id { get; set; } + + [JsonProperty("name")] public string Name { get; set; } = ""; + + [JsonProperty("username")] public string Username { get; set; } = ""; + + [JsonProperty("canLookStory")] public bool CanLookStory { get; set; } + + [JsonProperty("canCommentStory")] public bool CanCommentStory { get; set; } + + [JsonProperty("hasNotViewedStory")] public bool HasNotViewedStory { get; set; } + + [JsonProperty("isVerified")] public bool IsVerified { get; set; } + + [JsonProperty("canPayInternal")] public bool CanPayInternal { get; set; } + + [JsonProperty("hasScheduledStream")] public bool HasScheduledStream { get; set; } + + [JsonProperty("hasStream")] public bool HasStream { get; set; } + + [JsonProperty("hasStories")] public bool HasStories { get; set; } + + [JsonProperty("tipsEnabled")] public bool TipsEnabled { get; set; } + + [JsonProperty("tipsTextEnabled")] public bool TipsTextEnabled { get; set; } + + [JsonProperty("tipsMin")] public int TipsMin { get; set; } + + [JsonProperty("tipsMinInternal")] public int TipsMinInternal { get; set; } + + [JsonProperty("tipsMax")] public int TipsMax { get; set; } + + [JsonProperty("canEarn")] public bool CanEarn { get; set; } + + [JsonProperty("canAddSubscriber")] public bool CanAddSubscriber { get; set; } + + [JsonProperty("subscribePrice")] public string SubscribePrice { get; set; } = ""; + + [JsonProperty("subscriptionBundles")] public List SubscriptionBundles { get; set; } = []; + + [JsonProperty("isPaywallRequired")] public bool IsPaywallRequired { get; set; } + + [JsonProperty("listsStates")] public List ListsStates { get; set; } = []; + + [JsonProperty("isRestricted")] public bool IsRestricted { get; set; } + + [JsonProperty("canRestrict")] public bool CanRestrict { get; set; } + + [JsonProperty("subscribedBy")] public object SubscribedBy { get; set; } = new(); + + [JsonProperty("subscribedByExpire")] public object SubscribedByExpire { get; set; } = new(); + + [JsonProperty("subscribedByExpireDate")] + public DateTime? SubscribedByExpireDate { get; set; } + + [JsonProperty("subscribedByAutoprolong")] + public object SubscribedByAutoprolong { get; set; } = new(); + + [JsonProperty("subscribedIsExpiredNow")] + public bool SubscribedIsExpiredNow { get; set; } + + [JsonProperty("currentSubscribePrice")] + public object CurrentSubscribePrice { get; set; } = new(); + + [JsonProperty("subscribedOn")] public object SubscribedOn { get; set; } = new(); + + [JsonProperty("subscribedOnExpiredNow")] + public object SubscribedOnExpiredNow { get; set; } = new(); + + [JsonProperty("subscribedOnDuration")] public object SubscribedOnDuration { get; set; } = new(); + + [JsonProperty("callPrice")] public int CallPrice { get; set; } + + [JsonProperty("lastSeen")] public DateTime? LastSeen { get; set; } + + [JsonProperty("canReport")] public bool CanReport { get; set; } +} diff --git a/OF DL/Models/Dtos/Messages/InfoDto.cs b/OF DL/Models/Dtos/Messages/InfoDto.cs new file mode 100644 index 0000000..30b3ceb --- /dev/null +++ b/OF DL/Models/Dtos/Messages/InfoDto.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Messages; + +public class InfoDto +{ + [JsonProperty("source")] public SourceDto Source { get; set; } = new(); + + [JsonProperty("preview")] public PreviewDto Preview { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Messages/ListItemDto.cs b/OF DL/Models/Dtos/Messages/ListItemDto.cs new file mode 100644 index 0000000..6b414d7 --- /dev/null +++ b/OF DL/Models/Dtos/Messages/ListItemDto.cs @@ -0,0 +1,66 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Messages; + +public class ListItemDto +{ + [JsonProperty("responseType")] public string ResponseType { get; set; } = ""; + + [JsonProperty("text")] public string Text { get; set; } = ""; + + [JsonProperty("giphyId")] public object GiphyId { get; set; } = new(); + + [JsonProperty("lockedText")] public bool? LockedText { get; set; } + + [JsonProperty("isFree")] public bool? IsFree { get; set; } + + [JsonProperty("price")] public string Price { get; set; } = ""; + + [JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; } + + [JsonProperty("mediaCount")] public int? MediaCount { get; set; } + + [JsonProperty("media")] public List Media { get; set; } = []; + + [JsonProperty("previews")] public List Previews { get; set; } = []; + + [JsonProperty("isTip")] public bool? IsTip { get; set; } + + [JsonProperty("isReportedByMe")] public bool? IsReportedByMe { get; set; } + + [JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; } + + [JsonProperty("queueId")] public object QueueId { get; set; } = new(); + + [JsonProperty("fromUser")] public FromUserDto FromUser { get; set; } = new(); + + [JsonProperty("isFromQueue")] public bool? IsFromQueue { get; set; } + + [JsonProperty("canUnsendQueue")] public bool? CanUnsendQueue { get; set; } + + [JsonProperty("unsendSecondsQueue")] public int? UnsendSecondsQueue { get; set; } + + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("isOpened")] public bool? IsOpened { get; set; } + + [JsonProperty("isNew")] public bool? IsNew { get; set; } + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } + + [JsonProperty("changedAt")] public DateTime? ChangedAt { get; set; } + + [JsonProperty("cancelSeconds")] public int? CancelSeconds { get; set; } + + [JsonProperty("isLiked")] public bool? IsLiked { get; set; } + + [JsonProperty("canPurchase")] public bool? CanPurchase { get; set; } + + [JsonProperty("canPurchaseReason")] public string CanPurchaseReason { get; set; } = ""; + + [JsonProperty("canReport")] public bool? CanReport { get; set; } + + [JsonProperty("canBePinned")] public bool? CanBePinned { get; set; } + + [JsonProperty("isPinned")] public bool? IsPinned { get; set; } +} diff --git a/OF DL/Models/Dtos/Messages/ListsStateDto.cs b/OF DL/Models/Dtos/Messages/ListsStateDto.cs new file mode 100644 index 0000000..dd4524a --- /dev/null +++ b/OF DL/Models/Dtos/Messages/ListsStateDto.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Messages; + +public class ListsStateDto +{ + [JsonProperty("id")] public string Id { get; set; } = ""; + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("name")] public string Name { get; set; } = ""; + + [JsonProperty("hasUser")] public bool HasUser { get; set; } + + [JsonProperty("canAddUser")] public bool CanAddUser { get; set; } + + [JsonProperty("cannotAddUserReason")] public string CannotAddUserReason { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Messages/MediumDto.cs b/OF DL/Models/Dtos/Messages/MediumDto.cs new file mode 100644 index 0000000..58052f1 --- /dev/null +++ b/OF DL/Models/Dtos/Messages/MediumDto.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Messages; + +public class MediumDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("canView")] public bool CanView { get; set; } + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("src")] public string Src { get; set; } = ""; + + [JsonProperty("preview")] public string Preview { get; set; } = ""; + + [JsonProperty("thumb")] public string Thumb { get; set; } = ""; + + [JsonProperty("locked")] public object Locked { get; set; } = new(); + + [JsonProperty("duration")] public int? Duration { get; set; } + + [JsonProperty("hasError")] public bool? HasError { get; set; } + + [JsonProperty("squarePreview")] public string SquarePreview { get; set; } = ""; + + [JsonProperty("video")] public VideoDto Video { get; set; } = new(); + + [JsonProperty("videoSources")] public VideoSourcesDto VideoSources { get; set; } = new(); + + [JsonProperty("source")] public SourceDto Source { get; set; } = new(); + + [JsonProperty("info")] public InfoDto Info { get; set; } = new(); + + [JsonProperty("files")] public FilesDto Files { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Messages/MessagesDto.cs b/OF DL/Models/Dtos/Messages/MessagesDto.cs new file mode 100644 index 0000000..50673b5 --- /dev/null +++ b/OF DL/Models/Dtos/Messages/MessagesDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Messages; + +public class MessagesDto +{ + [JsonProperty("list")] public List List { get; set; } = []; + + [JsonProperty("hasMore")] public bool HasMore { get; set; } +} diff --git a/OF DL/Models/Dtos/Messages/SingleMessageDto.cs b/OF DL/Models/Dtos/Messages/SingleMessageDto.cs new file mode 100644 index 0000000..252c59b --- /dev/null +++ b/OF DL/Models/Dtos/Messages/SingleMessageDto.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Messages; + +public class SingleMessageDto +{ + [JsonProperty("responseType")] public string ResponseType { get; set; } = ""; + + [JsonProperty("text")] public string Text { get; set; } = ""; + + [JsonProperty("giphyId")] public object GiphyId { get; set; } = new(); + + [JsonProperty("lockedText")] public bool LockedText { get; set; } + + [JsonProperty("isFree")] public bool IsFree { get; set; } + + [JsonProperty("price")] public double Price { get; set; } + + [JsonProperty("isMediaReady")] public bool IsMediaReady { get; set; } + + [JsonProperty("mediaCount")] public int MediaCount { get; set; } + + [JsonProperty("media")] public List Media { get; set; } = []; + + [JsonProperty("previews")] public List Previews { get; set; } = []; + + [JsonProperty("isTip")] public bool IsTip { get; set; } + + [JsonProperty("isReportedByMe")] public bool IsReportedByMe { get; set; } + + [JsonProperty("isCouplePeopleMedia")] public bool IsCouplePeopleMedia { get; set; } + + [JsonProperty("queueId")] public long QueueId { get; set; } + + [JsonProperty("fromUser")] public FromUserDto FromUser { get; set; } = new(); + + [JsonProperty("isFromQueue")] public bool IsFromQueue { get; set; } + + [JsonProperty("canUnsendQueue")] public bool CanUnsendQueue { get; set; } + + [JsonProperty("unsendSecondsQueue")] public int UnsendSecondsQueue { get; set; } + + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("isOpened")] public bool IsOpened { get; set; } + + [JsonProperty("isNew")] public bool IsNew { get; set; } + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } + + [JsonProperty("changedAt")] public DateTime? ChangedAt { get; set; } + + [JsonProperty("cancelSeconds")] public int CancelSeconds { get; set; } + + [JsonProperty("isLiked")] public bool IsLiked { get; set; } + + [JsonProperty("canPurchase")] public bool CanPurchase { get; set; } + + [JsonProperty("canReport")] public bool CanReport { get; set; } +} diff --git a/OF DL/Models/Entities/Common/FromUser.cs b/OF DL/Models/Entities/Common/FromUser.cs new file mode 100644 index 0000000..bc09e0b --- /dev/null +++ b/OF DL/Models/Entities/Common/FromUser.cs @@ -0,0 +1,6 @@ +namespace OF_DL.Models.Entities.Common; + +public class FromUser +{ + public long? Id { get; set; } +} diff --git a/OF DL/Models/Entities/Messages/ListItem.cs b/OF DL/Models/Entities/Messages/ListItem.cs new file mode 100644 index 0000000..e48f1f5 --- /dev/null +++ b/OF DL/Models/Entities/Messages/ListItem.cs @@ -0,0 +1,22 @@ +using OF_DL.Models.Entities.Common; + +namespace OF_DL.Models.Entities.Messages; + +public class ListItem +{ + public long Id { get; set; } + + public string? Text { get; set; } + + public string? Price { get; set; } + + public string? CanPurchaseReason { get; set; } + + public DateTime? CreatedAt { get; set; } + + public List? Previews { get; set; } + + public List? Media { get; set; } + + public FromUser? FromUser { get; set; } +} diff --git a/OF DL/Models/Entities/Messages/Medium.cs b/OF DL/Models/Entities/Messages/Medium.cs new file mode 100644 index 0000000..7f6fa1a --- /dev/null +++ b/OF DL/Models/Entities/Messages/Medium.cs @@ -0,0 +1,14 @@ +using OF_DL.Models.Entities.Common; + +namespace OF_DL.Models.Entities.Messages; + +public class Medium +{ + public long Id { get; set; } + + public bool CanView { get; set; } + + public string? Type { get; set; } + + public Files? Files { get; set; } +} diff --git a/OF DL/Models/Entities/Messages/MessageCollection.cs b/OF DL/Models/Entities/Messages/MessageCollection.cs new file mode 100644 index 0000000..c5ac0c2 --- /dev/null +++ b/OF DL/Models/Entities/Messages/MessageCollection.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Messages; + +public class MessageCollection +{ + public List MessageMedia { get; set; } = []; + + public List MessageObjects { get; set; } = []; + + public Dictionary Messages { get; set; } = new(); +} diff --git a/OF DL/Models/Entities/Messages/Messages.cs b/OF DL/Models/Entities/Messages/Messages.cs new file mode 100644 index 0000000..9a965e2 --- /dev/null +++ b/OF DL/Models/Entities/Messages/Messages.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Models.Entities.Messages; + +public class Messages +{ + public List List { get; set; } = []; + + public bool HasMore { get; set; } +} diff --git a/OF DL/Models/Entities/Messages/SingleMessage.cs b/OF DL/Models/Entities/Messages/SingleMessage.cs new file mode 100644 index 0000000..b41b555 --- /dev/null +++ b/OF DL/Models/Entities/Messages/SingleMessage.cs @@ -0,0 +1,20 @@ +using OF_DL.Models.Entities.Common; + +namespace OF_DL.Models.Entities.Messages; + +public class SingleMessage +{ + public long Id { get; set; } + + public string? Text { get; set; } + + public double? Price { get; set; } + + public DateTime? CreatedAt { get; set; } + + public List? Media { get; set; } + + public List? Previews { get; set; } + + public FromUser? FromUser { get; set; } +} diff --git a/OF DL/Models/Mappers/MessagesMapper.cs b/OF DL/Models/Mappers/MessagesMapper.cs new file mode 100644 index 0000000..bedf505 --- /dev/null +++ b/OF DL/Models/Mappers/MessagesMapper.cs @@ -0,0 +1,110 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Messages; +using OF_DL.Models.Entities.Common; +using MessageEntities = OF_DL.Models.Entities.Messages; + +namespace OF_DL.Models.Mappers; + +public static class MessagesMapper +{ + public static MessageEntities.Messages FromDto(MessagesDto? dto) + { + MessageEntities.Messages mapped = new() { HasMore = dto?.HasMore ?? false }; + + if (dto?.List == null) + { + return mapped; + } + + foreach (ListItemDto entry in dto.List) + { + mapped.List.Add(MapListItem(entry)); + } + + return mapped; + } + + public static MessageEntities.SingleMessage FromDto(SingleMessageDto? dto) + { + MessageEntities.SingleMessage mapped = new(); + + if (dto == null) + { + return mapped; + } + + mapped.Id = dto.Id; + mapped.Text = dto.Text; + mapped.Price = dto.Price; + mapped.CreatedAt = dto.CreatedAt; + mapped.Media = MapMedia(dto.Media); + mapped.Previews = dto.Previews; + mapped.FromUser = MapFromUser(dto.FromUser); + + return mapped; + } + + public static MessageEntities.Medium MapMedium(MediumDto dto) => + new() { Id = dto.Id, Type = dto.Type, CanView = dto.CanView, Files = MapFiles(dto.Files) }; + + private static MessageEntities.ListItem MapListItem(ListItemDto dto) => + new() + { + Id = dto.Id, + Text = dto.Text, + Price = dto.Price, + CanPurchaseReason = dto.CanPurchaseReason, + CreatedAt = dto.CreatedAt, + Media = MapMedia(dto.Media), + Previews = dto.Previews, + FromUser = MapFromUser(dto.FromUser) + }; + + private static List? MapMedia(List? media) => + media?.Select(MapMedium).ToList(); + + private static FromUser? MapFromUser(FromUserDto? dto) => + dto == null ? null : new FromUser { Id = dto.Id }; + + private static Files? MapFiles(FilesDto? dto) + { + if (dto == null) + { + return null; + } + + return new Files { Full = MapFull(dto.Full), Drm = MapDrm(dto.Drm) }; + } + + private static Full? MapFull(FullDto? dto) => dto == null ? null : new Full { Url = dto.Url }; + + private static Drm? MapDrm(DrmDto? dto) + { + if (dto == null) + { + return null; + } + + return new Drm { Manifest = MapManifest(dto.Manifest), Signature = MapSignature(dto.Signature) }; + } + + private static Manifest? MapManifest(ManifestDto? dto) => dto == null ? null : new Manifest { Dash = dto.Dash }; + + private static Signature? MapSignature(SignatureDto? dto) => + dto == null ? null : new Signature { Dash = MapDash(dto.Dash) }; + + private static Dash? MapDash(DashDto? dto) + { + if (dto == null) + { + return null; + } + + return new Dash + { + CloudFrontPolicy = dto.CloudFrontPolicy, + CloudFrontSignature = dto.CloudFrontSignature, + CloudFrontKeyPairId = dto.CloudFrontKeyPairId + }; + } +} diff --git a/OF DL/Models/Messages/MessageCollection.cs b/OF DL/Models/Messages/MessageCollection.cs deleted file mode 100644 index 00321c0..0000000 --- a/OF DL/Models/Messages/MessageCollection.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OF_DL.Models.Messages; - -public class MessageCollection -{ - public List MessageMedia = new(); - public List MessageObjects = new(); - public Dictionary Messages = new(); -} diff --git a/OF DL/Models/Messages/Messages.cs b/OF DL/Models/Messages/Messages.cs deleted file mode 100644 index b0c72ef..0000000 --- a/OF DL/Models/Messages/Messages.cs +++ /dev/null @@ -1,173 +0,0 @@ -using Newtonsoft.Json; - -namespace OF_DL.Models.Messages; - -public class Messages -{ - public List list { get; set; } - public bool hasMore { get; set; } - - public class Dash - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } - - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } - - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class FromUser - { - public long? id { get; set; } - public string _view { get; set; } - } - - public class Hls - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } - - public class List - { - public string responseType { get; set; } - public string text { get; set; } - public object giphyId { get; set; } - public bool? lockedText { get; set; } - public bool? isFree { get; set; } - public string? price { get; set; } - public bool? isMediaReady { get; set; } - public int? mediaCount { get; set; } - public List media { get; set; } - public List previews { get; set; } - public bool? isTip { get; set; } - public bool? isReportedByMe { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public object queueId { get; set; } - public FromUser fromUser { get; set; } - public bool? isFromQueue { get; set; } - public bool? canUnsendQueue { get; set; } - public int? unsendSecondsQueue { get; set; } - public long id { get; set; } - public bool? isOpened { get; set; } - public bool? isNew { get; set; } - public DateTime? createdAt { get; set; } - public DateTime? changedAt { get; set; } - public int? cancelSeconds { get; set; } - public bool? isLiked { get; set; } - public bool? canPurchase { get; set; } - public string canPurchaseReason { get; set; } - public bool? canReport { get; set; } - public bool? canBePinned { get; set; } - public bool? isPinned { get; set; } - } - - public class Manifest - { - public string hls { get; set; } - public string dash { get; set; } - } - - public class Medium - { - public long id { get; set; } - public bool canView { get; set; } - public string type { get; set; } - public string src { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public object locked { get; set; } - public int? duration { get; set; } - public bool? hasError { get; set; } - public string squarePreview { get; set; } - public Video video { get; set; } - public VideoSources videoSources { get; set; } - public Source source { get; set; } - public Info info { get; set; } - public Files files { get; set; } - } - - public class Preview - { - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - } - - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } - - public class Source - { - public string source { get; set; } - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - } - - public class Video - { - public string mp4 { get; set; } - } - - public class VideoSources - { - [JsonProperty("720")] public string _720 { get; set; } - - [JsonProperty("240")] public string _240 { get; set; } - } -} diff --git a/OF DL/Models/Messages/SingleMessage.cs b/OF DL/Models/Messages/SingleMessage.cs deleted file mode 100644 index ffaee05..0000000 --- a/OF DL/Models/Messages/SingleMessage.cs +++ /dev/null @@ -1,115 +0,0 @@ -using static OF_DL.Models.Messages.Messages; - -namespace OF_DL.Models.Messages; - -public class AvatarThumbs -{ - public string c50 { get; set; } - public string c144 { get; set; } -} - -public class FromUser -{ - public string view { get; set; } - public string avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long? id { get; set; } - public string name { get; set; } - public string username { get; set; } - public bool canLookStory { get; set; } - public bool canCommentStory { get; set; } - public bool hasNotViewedStory { get; set; } - public bool isVerified { get; set; } - public bool canPayInternal { get; set; } - public bool hasScheduledStream { get; set; } - public bool hasStream { get; set; } - public bool hasStories { get; set; } - public bool tipsEnabled { get; set; } - public bool tipsTextEnabled { get; set; } - public int tipsMin { get; set; } - public int tipsMinInternal { get; set; } - public int tipsMax { get; set; } - public bool canEarn { get; set; } - public bool canAddSubscriber { get; set; } - public string? subscribePrice { get; set; } - public List subscriptionBundles { get; set; } - public bool isPaywallRequired { get; set; } - public List listsStates { get; set; } - public bool isRestricted { get; set; } - public bool canRestrict { get; set; } - public object subscribedBy { get; set; } - public object subscribedByExpire { get; set; } - public DateTime subscribedByExpireDate { get; set; } - public object subscribedByAutoprolong { get; set; } - public bool subscribedIsExpiredNow { get; set; } - public object currentSubscribePrice { get; set; } - public object subscribedOn { get; set; } - public object subscribedOnExpiredNow { get; set; } - public object subscribedOnDuration { get; set; } - public int callPrice { get; set; } - public DateTime? lastSeen { get; set; } - public bool canReport { get; set; } -} - -public class HeaderSize -{ - public int width { get; set; } - public int height { get; set; } -} - -public class HeaderThumbs -{ - public string w480 { get; set; } - public string w760 { get; set; } -} - -public class ListsState -{ - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool hasUser { get; set; } - public bool canAddUser { get; set; } - public string cannotAddUserReason { get; set; } -} - -public class Preview -{ - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } -} - -public class SingleMessage -{ - public string responseType { get; set; } - public string text { get; set; } - public object giphyId { get; set; } - public bool lockedText { get; set; } - public bool isFree { get; set; } - public double price { get; set; } - public bool isMediaReady { get; set; } - public int mediaCount { get; set; } - public List media { get; set; } - public List previews { get; set; } - public bool isTip { get; set; } - public bool isReportedByMe { get; set; } - public bool isCouplePeopleMedia { get; set; } - public long queueId { get; set; } - public FromUser fromUser { get; set; } - public bool isFromQueue { get; set; } - public bool canUnsendQueue { get; set; } - public int unsendSecondsQueue { get; set; } - public long id { get; set; } - public bool isOpened { get; set; } - public bool isNew { get; set; } - public DateTime? createdAt { get; set; } - public DateTime? changedAt { get; set; } - public int cancelSeconds { get; set; } - public bool isLiked { get; set; } - public bool canPurchase { get; set; } - public bool canReport { get; set; } -} diff --git a/OF DL/Models/Purchased/PaidMessageCollection.cs b/OF DL/Models/Purchased/PaidMessageCollection.cs index 0ca1548..26bf6ec 100644 --- a/OF DL/Models/Purchased/PaidMessageCollection.cs +++ b/OF DL/Models/Purchased/PaidMessageCollection.cs @@ -1,10 +1,10 @@ -using static OF_DL.Models.Messages.Messages; +using MessageEntities = OF_DL.Models.Entities.Messages; namespace OF_DL.Models.Purchased; public class PaidMessageCollection { - public List PaidMessageMedia = new(); + public List PaidMessageMedia = new(); public List PaidMessageObjects = new(); public Dictionary PaidMessages = new(); } diff --git a/OF DL/Models/Purchased/PaidPostCollection.cs b/OF DL/Models/Purchased/PaidPostCollection.cs index 4def769..24bce7d 100644 --- a/OF DL/Models/Purchased/PaidPostCollection.cs +++ b/OF DL/Models/Purchased/PaidPostCollection.cs @@ -1,10 +1,10 @@ -using static OF_DL.Models.Messages.Messages; +using MessageEntities = OF_DL.Models.Entities.Messages; namespace OF_DL.Models.Purchased; public class PaidPostCollection { - public List PaidPostMedia = new(); + public List PaidPostMedia = new(); public List PaidPostObjects = new(); public Dictionary PaidPosts = new(); } diff --git a/OF DL/Models/Purchased/Purchased.cs b/OF DL/Models/Purchased/Purchased.cs index 671ed3b..cae8e66 100644 --- a/OF DL/Models/Purchased/Purchased.cs +++ b/OF DL/Models/Purchased/Purchased.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json; -using static OF_DL.Models.Messages.Messages; +using OF_DL.Models.Dtos.Messages; namespace OF_DL.Models.Purchased; @@ -41,7 +41,7 @@ public class Purchased public string? price { get; set; } public bool? isMediaReady { get; set; } public int? mediaCount { get; set; } - public List media { get; set; } + public List media { get; set; } public List previews { get; set; } public List preview { get; set; } public bool? isTip { get; set; } diff --git a/OF DL/Models/Purchased/SinglePaidMessageCollection.cs b/OF DL/Models/Purchased/SinglePaidMessageCollection.cs index db60540..1f2e33f 100644 --- a/OF DL/Models/Purchased/SinglePaidMessageCollection.cs +++ b/OF DL/Models/Purchased/SinglePaidMessageCollection.cs @@ -1,14 +1,13 @@ -using OF_DL.Models.Messages; -using static OF_DL.Models.Messages.Messages; +using MessageEntities = OF_DL.Models.Entities.Messages; namespace OF_DL.Models.Purchased; public class SinglePaidMessageCollection { - public List PreviewSingleMessageMedia = new(); + public List PreviewSingleMessageMedia = new(); public Dictionary PreviewSingleMessages = new(); - public List SingleMessageMedia = new(); - public List SingleMessageObjects = new(); + public List SingleMessageMedia = new(); + public List SingleMessageObjects = new(); public Dictionary SingleMessages = new(); } diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 18250ce..287f50e 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -7,13 +7,13 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.CLI; using OF_DL.Models; -using OF_DL.Models.Messages; using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Helpers; -using ArchivedModels = OF_DL.Models.Entities.Archived; +using ArchivedEntities = OF_DL.Models.Entities.Archived; +using MessageEntities = OF_DL.Models.Entities.Messages; using OF_DL.Services; using Serilog; using Spectre.Console; @@ -1116,7 +1116,7 @@ public class Program(IServiceProvider serviceProvider) IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - MessageCollection messages = new(); + MessageEntities.MessageCollection messages = new(); await AnsiConsole.Status() .StartAsync("[red]Getting Messages[/]", @@ -1274,7 +1274,7 @@ public class Program(IServiceProvider serviceProvider) IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - ArchivedModels.ArchivedCollection archived = new(); + ArchivedEntities.ArchivedCollection archived = new(); await AnsiConsole.Status() .StartAsync("[red]Getting Archived Posts[/]", @@ -1523,11 +1523,11 @@ public class Program(IServiceProvider serviceProvider) pssh); } - Messages.Medium? mediaInfo = - purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); + MessageEntities.Medium? mediaInfo = + purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); Purchased.List? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => - p?.media?.Contains(mediaInfo) == true) + p?.media?.Any(m => m.Id == purchasedPostKVP.Key) == true) : null; isNew = await downloadService.DownloadPurchasedPostDRMVideo( @@ -1558,11 +1558,11 @@ public class Program(IServiceProvider serviceProvider) } else { - Messages.Medium? mediaInfo = - purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); + MessageEntities.Medium? mediaInfo = + purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); Purchased.List? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => - p?.media?.Contains(mediaInfo) == true) + p?.media?.Any(m => m.Id == purchasedPostKVP.Key) == true) : null; isNew = await downloadService.DownloadPurchasedPostMedia( @@ -1672,12 +1672,12 @@ public class Program(IServiceProvider serviceProvider) pssh); } - Messages.Medium? mediaInfo = + MessageEntities.Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => - m.id == paidMessageKVP.Key); + m.Id == paidMessageKVP.Key); Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.media?.Contains(mediaInfo) == true); + p?.media?.Any(m => m.Id == paidMessageKVP.Key) == true); isNew = await downloadService.DownloadPurchasedMessageDRMVideo( policy, @@ -1709,11 +1709,11 @@ public class Program(IServiceProvider serviceProvider) } else { - Messages.Medium? mediaInfo = - paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); + MessageEntities.Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKVP.Key); Purchased.List messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.media?.Contains(mediaInfo) == true); + p?.media?.Any(m => m.Id == paidMessageKVP.Key) == true); isNew = await downloadService.DownloadPurchasedMedia( paidMessageKVP.Value, @@ -1899,12 +1899,12 @@ public class Program(IServiceProvider serviceProvider) pssh); } - Messages.Medium? mediaInfo = + MessageEntities.Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => - m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = + m.Id == paidMessageKVP.Key); + MessageEntities.SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.media?.Contains(mediaInfo) == true); + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); isNew = await downloadService.DownloadSingleMessagePreviewDRMVideo( policy, @@ -1921,7 +1921,7 @@ public class Program(IServiceProvider serviceProvider) .PaidMessageFileNameFormat ?? string.Empty, messageInfo, mediaInfo, - messageInfo?.fromUser, + messageInfo?.FromUser, hasSelectedUsersKVP.Value); if (isNew) @@ -1936,12 +1936,12 @@ public class Program(IServiceProvider serviceProvider) } else { - Messages.Medium? mediaInfo = + MessageEntities.Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => - m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = + m.Id == paidMessageKVP.Key); + MessageEntities.SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.media?.Contains(mediaInfo) == true); + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); isNew = await downloadService.DownloadMessagePreviewMedia( paidMessageKVP.Value, @@ -1953,7 +1953,7 @@ public class Program(IServiceProvider serviceProvider) .PaidMessageFileNameFormat ?? string.Empty, messageInfo, mediaInfo, - messageInfo?.fromUser, + messageInfo?.FromUser, hasSelectedUsersKVP.Value); if (isNew) { @@ -2039,12 +2039,12 @@ public class Program(IServiceProvider serviceProvider) pssh); } - Messages.Medium? mediaInfo = + MessageEntities.Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => - m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = + m.Id == paidMessageKVP.Key); + MessageEntities.SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.media?.Contains(mediaInfo) == true); + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); isNew = await downloadService.DownloadSinglePurchasedMessageDRMVideo( policy, @@ -2061,7 +2061,7 @@ public class Program(IServiceProvider serviceProvider) .PaidMessageFileNameFormat ?? string.Empty, messageInfo, mediaInfo, - messageInfo?.fromUser, + messageInfo?.FromUser, hasSelectedUsersKVP.Value); if (isNew) @@ -2076,12 +2076,12 @@ public class Program(IServiceProvider serviceProvider) } else { - Messages.Medium? mediaInfo = + MessageEntities.Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => - m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = + m.Id == paidMessageKVP.Key); + MessageEntities.SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.media?.Contains(mediaInfo) == true); + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); isNew = await downloadService.DownloadSinglePurchasedMedia( paidMessageKVP.Value, @@ -2093,7 +2093,7 @@ public class Program(IServiceProvider serviceProvider) .PaidMessageFileNameFormat ?? string.Empty, messageInfo, mediaInfo, - messageInfo?.fromUser, + messageInfo?.FromUser, hasSelectedUsersKVP.Value); if (isNew) { diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 149a35f..576d06a 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -6,18 +6,19 @@ using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Messages; using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Stories; using OF_DL.Models.Streams; using OF_DL.Enumerations; -using OF_DL.Models.Dtos.Archived; -using OF_DL.Models.Dtos.Lists; -using OF_DL.Models.Dtos.Highlights; -using ArchivedModels = OF_DL.Models.Entities.Archived; +using ArchivedDtos = OF_DL.Models.Dtos.Archived; +using MessageDtos = OF_DL.Models.Dtos.Messages; +using ListDtos = OF_DL.Models.Dtos.Lists; +using HighlightDtos = OF_DL.Models.Dtos.Highlights; +using ArchivedEntities = OF_DL.Models.Entities.Archived; using HighlightEntities = OF_DL.Models.Entities.Highlights; using ListEntities = OF_DL.Models.Entities.Lists; +using MessageEntities = OF_DL.Models.Entities.Messages; using OF_DL.Models.Mappers; using OF_DL.Widevine; using Serilog; @@ -243,7 +244,7 @@ public class APIService(IAuthService authService, IConfigService configService, break; } - UserListDto? userListDto = JsonConvert.DeserializeObject(body); + ListDtos.UserListDto? userListDto = JsonConvert.DeserializeObject(body); ListEntities.UserList userList = UserListsMapper.FromDto(userListDto); foreach (ListEntities.UserListItem listItem in userList.List) @@ -303,7 +304,8 @@ public class APIService(IAuthService authService, IConfigService configService, break; } - List? usersListDto = JsonConvert.DeserializeObject>(body); + List? usersListDto = + JsonConvert.DeserializeObject>(body); List usersList = UserListsMapper.FromDto(usersListDto); if (usersList.Count <= 0) @@ -451,8 +453,8 @@ public class APIService(IAuthService authService, IConfigService configService, else if (mediatype == MediaType.Highlights) { List highlight_ids = new(); - HighlightsDto? highlightsDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + HighlightDtos.HighlightsDto? highlightsDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); HighlightEntities.Highlights highlights = HighlightsMapper.FromDto(highlightsDto); if (highlights.HasMore) @@ -464,8 +466,9 @@ public class APIService(IAuthService authService, IConfigService configService, Log.Debug("Media Highlights - " + endpoint); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - HighlightsDto? newHighlightsDto = - JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + HighlightDtos.HighlightsDto? newHighlightsDto = + JsonConvert.DeserializeObject(loopbody, + m_JsonSerializerSettings); HighlightEntities.Highlights newHighlights = HighlightsMapper.FromDto(newHighlightsDto); highlights.List.AddRange(newHighlights.List); @@ -505,8 +508,9 @@ public class APIService(IAuthService authService, IConfigService configService, using HttpResponseMessage highlightResponse = await highlight_client.SendAsync(highlight_request); highlightResponse.EnsureSuccessStatusCode(); string highlightBody = await highlightResponse.Content.ReadAsStringAsync(); - HighlightMediaDto? highlightMediaDto = - JsonConvert.DeserializeObject(highlightBody, m_JsonSerializerSettings); + HighlightDtos.HighlightMediaDto? highlightMediaDto = + JsonConvert.DeserializeObject(highlightBody, + m_JsonSerializerSettings); HighlightEntities.HighlightMedia highlightMedia = HighlightsMapper.FromDto(highlightMediaDto); foreach (HighlightEntities.Story item in highlightMedia.Stories) @@ -672,97 +676,98 @@ public class APIService(IAuthService authService, IConfigService configService, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); paidPostCollection.PaidPostObjects.Add(purchase); - foreach (Messages.Medium medium in purchase.media) + foreach (MessageDtos.MediumDto medium in purchase.media) { - if (!previewids.Contains(medium.id)) + MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); + if (!previewids.Contains(medium.Id)) { - paid_post_ids.Add(medium.id); + paid_post_ids.Add(medium.Id); } - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (previewids.Count > 0) { - bool has = previewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url)) + bool has = previewids.Any(cus => cus.Equals(medium.Id)); + if (!has && medium.CanView && medium.Files != null && medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) + if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, + await dbService.AddMedia(folder, medium.Id, purchase.id, medium.Files.Full.Url, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - paidPostCollection.PaidPosts.Add(medium.id, medium.files.full.url); - paidPostCollection.PaidPostMedia.Add(medium); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + paidPostCollection.PaidPosts.Add(medium.Id, medium.Files.Full.Url); + paidPostCollection.PaidPostMedia.Add(mappedMedium); } } - else if (!has && medium.canView && medium.files != null && medium.files.drm != null) + else if (!has && medium.CanView && medium.Files != null && medium.Files.Drm != null) { - if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) + if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, - medium.files.drm.manifest.dash, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - paidPostCollection.PaidPosts.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); - paidPostCollection.PaidPostMedia.Add(medium); + await dbService.AddMedia(folder, medium.Id, purchase.id, + medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + paidPostCollection.PaidPosts.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); + paidPostCollection.PaidPostMedia.Add(mappedMedium); } } } else { - if (medium.canView && medium.files != null && medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.CanView && medium.Files != null && medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) + if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, + await dbService.AddMedia(folder, medium.Id, purchase.id, medium.Files.Full.Url, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - paidPostCollection.PaidPosts.Add(medium.id, medium.files.full.url); - paidPostCollection.PaidPostMedia.Add(medium); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + paidPostCollection.PaidPosts.Add(medium.Id, medium.Files.Full.Url); + paidPostCollection.PaidPostMedia.Add(mappedMedium); } } - else if (medium.canView && medium.files != null && medium.files.drm != null) + else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { - if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) + if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, - medium.files.drm.manifest.dash, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - paidPostCollection.PaidPosts.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); - paidPostCollection.PaidPostMedia.Add(medium); + await dbService.AddMedia(folder, medium.Id, purchase.id, + medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + paidPostCollection.PaidPosts.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); + paidPostCollection.PaidPostMedia.Add(mappedMedium); } } } @@ -1360,14 +1365,15 @@ public class APIService(IAuthService authService, IConfigService configService, } - public async Task GetArchived(string endpoint, string folder, StatusContext ctx) + public async Task GetArchived(string endpoint, string folder, + StatusContext ctx) { Log.Debug($"Calling GetArchived - {endpoint}"); try { - ArchivedModels.Archived archived = new(); - ArchivedModels.ArchivedCollection archivedCollection = new(); + ArchivedEntities.Archived archived = new(); + ArchivedEntities.ArchivedCollection archivedCollection = new(); int post_limit = 50; Dictionary getParams = new() { @@ -1392,7 +1398,8 @@ public class APIService(IAuthService authService, IConfigService configService, configService.CurrentConfig.CustomDate); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - ArchivedDto archivedDto = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + ArchivedDtos.ArchivedDto archivedDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); archived = ArchivedMapper.FromDto(archivedDto); ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1405,11 +1412,11 @@ public class APIService(IAuthService authService, IConfigService configService, archived.TailMarker); while (true) { - ArchivedModels.Archived newarchived = new(); + ArchivedEntities.Archived newarchived = new(); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - ArchivedDto newarchivedDto = - JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + ArchivedDtos.ArchivedDto newarchivedDto = + JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); newarchived = ArchivedMapper.FromDto(newarchivedDto); archived.List.AddRange(newarchived.List); @@ -1428,7 +1435,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } - foreach (ArchivedModels.ListItem archive in archived.List) + foreach (ArchivedEntities.ListItem archive in archived.List) { List previewids = new(); if (archive.Preview != null) @@ -1451,7 +1458,7 @@ public class APIService(IAuthService authService, IConfigService configService, archivedCollection.ArchivedPostObjects.Add(archive); if (archive.Media != null && archive.Media.Count > 0) { - foreach (ArchivedModels.Medium medium in archive.Media) + foreach (ArchivedEntities.Medium medium in archive.Media) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -1527,14 +1534,14 @@ public class APIService(IAuthService authService, IConfigService configService, } - public async Task GetMessages(string endpoint, string folder, StatusContext ctx) + public async Task GetMessages(string endpoint, string folder, StatusContext ctx) { Log.Debug($"Calling GetMessages - {endpoint}"); try { - Messages messages = new(); - MessageCollection messageCollection = new(); + MessageEntities.Messages messages = new(); + MessageEntities.MessageCollection messageCollection = new(); int post_limit = 50; Dictionary getParams = new() { @@ -1542,49 +1549,54 @@ public class APIService(IAuthService authService, IConfigService configService, }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - messages = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.list.Count}[/]"); + MessageDtos.MessagesDto? messagesDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + messages = MessagesMapper.FromDto(messagesDto); + ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (messages.hasMore) + if (messages.HasMore) { - getParams["id"] = messages.list[^1].id.ToString(); + getParams["id"] = messages.List[^1].Id.ToString(); while (true) { - Messages newmessages = new(); + MessageEntities.Messages newMessages = new(); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - newmessages = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + MessageDtos.MessagesDto? newMessagesDto = + JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + newMessages = MessagesMapper.FromDto(newMessagesDto); - messages.list.AddRange(newmessages.list); - ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.list.Count}[/]"); + messages.List.AddRange(newMessages.List); + ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (!newmessages.hasMore) + if (!newMessages.HasMore) { break; } - getParams["id"] = newmessages.list[newmessages.list.Count - 1].id.ToString(); + getParams["id"] = newMessages.List[newMessages.List.Count - 1].Id.ToString(); } } - foreach (Messages.List list in messages.list) + foreach (MessageEntities.ListItem list in messages.List) { if (configService.CurrentConfig.SkipAds) { - if (list.text != null && (list.text.Contains("#ad") || list.text.Contains("/trial/"))) + if (!string.IsNullOrEmpty(list.Text) && + (list.Text.Contains("#ad") || list.Text.Contains("/trial/"))) { continue; } } List messagePreviewIds = new(); - if (list.previews != null && list.previews.Count > 0) + if (list.Previews != null && list.Previews.Count > 0) { - for (int i = 0; i < list.previews.Count; i++) + for (int i = 0; i < list.Previews.Count; i++) { - if (list.previews[i] is long previewId) + if (list.Previews[i] is long previewId) { if (!messagePreviewIds.Contains(previewId)) { @@ -1595,86 +1607,85 @@ public class APIService(IAuthService authService, IConfigService configService, } if (!configService.CurrentConfig.IgnoreOwnMessages || - list.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + list.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - await dbService.AddMessage(folder, list.id, list.text != null ? list.text : string.Empty, - list.price != null ? list.price : "0", - list.canPurchaseReason == "opened" ? true : - list.canPurchaseReason != "opened" ? false : (bool?)null ?? false, false, - list.createdAt.HasValue ? list.createdAt.Value : DateTime.Now, - list.fromUser != null && list.fromUser.id != null ? list.fromUser.id.Value : int.MinValue); + await dbService.AddMessage(folder, list.Id, list.Text ?? string.Empty, list.Price ?? "0", + list.CanPurchaseReason == "opened" ? true : + list.CanPurchaseReason != "opened" ? false : (bool?)null ?? false, false, + list.CreatedAt.HasValue ? list.CreatedAt.Value : DateTime.Now, + list.FromUser?.Id ?? int.MinValue); messageCollection.MessageObjects.Add(list); - if (list.canPurchaseReason != "opened" && list.media != null && list.media.Count > 0) + if (list.CanPurchaseReason != "opened" && list.Media != null && list.Media.Count > 0) { - foreach (Messages.Medium medium in list.media) + foreach (MessageEntities.Medium medium in list.Media) { - if (medium.canView && medium.files != null && medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.CanView && medium.Files != null && medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!messageCollection.Messages.ContainsKey(medium.id)) + if (!messageCollection.Messages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, + await dbService.AddMedia(folder, medium.Id, list.Id, medium.Files.Full.Url, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.id) ? true : false, false, null); - messageCollection.Messages.Add(medium.id, medium.files.full.url); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.Id), false, null); + messageCollection.Messages.Add(medium.Id, medium.Files.Full.Url); messageCollection.MessageMedia.Add(medium); } } - else if (medium.canView && medium.files != null && medium.files.drm != null) + else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!messageCollection.Messages.ContainsKey(medium.id)) + if (!messageCollection.Messages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, + await dbService.AddMedia(folder, medium.Id, list.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.id) ? true : false, false, null); - messageCollection.Messages.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.Id), false, null); + messageCollection.Messages.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{list.Id}"); messageCollection.MessageMedia.Add(medium); } } @@ -1682,76 +1693,76 @@ public class APIService(IAuthService authService, IConfigService configService, } else if (messagePreviewIds.Count > 0) { - foreach (Messages.Medium medium in list.media) + foreach (MessageEntities.Medium medium in list.Media) { - if (medium.canView && medium.files != null && medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url) && messagePreviewIds.Contains(medium.id)) + if (medium.CanView && medium.Files != null && medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url) && messagePreviewIds.Contains(medium.Id)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!messageCollection.Messages.ContainsKey(medium.id)) + if (!messageCollection.Messages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, + await dbService.AddMedia(folder, medium.Id, list.Id, medium.Files.Full.Url, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.id) ? true : false, false, null); - messageCollection.Messages.Add(medium.id, medium.files.full.url); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.Id), false, null); + messageCollection.Messages.Add(medium.Id, medium.Files.Full.Url); messageCollection.MessageMedia.Add(medium); } } - else if (medium.canView && medium.files != null && medium.files.drm != null && - messagePreviewIds.Contains(medium.id)) + else if (medium.CanView && medium.Files != null && medium.Files.Drm != null && + messagePreviewIds.Contains(medium.Id)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!messageCollection.Messages.ContainsKey(medium.id)) + if (!messageCollection.Messages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, + await dbService.AddMedia(folder, medium.Id, list.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.id) ? true : false, false, null); - messageCollection.Messages.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.Id), false, null); + messageCollection.Messages.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{list.Id}"); messageCollection.MessageMedia.Add(medium); } } @@ -1785,28 +1796,30 @@ public class APIService(IAuthService authService, IConfigService configService, try { - SingleMessage message = new(); + MessageEntities.SingleMessage message = new(); SinglePaidMessageCollection singlePaidMessageCollection = new(); int post_limit = 50; Dictionary getParams = new() { { "limit", post_limit.ToString() }, { "order", "desc" } }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - message = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + MessageDtos.SingleMessageDto? messageDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + message = MessagesMapper.FromDto(messageDto); if (!configService.CurrentConfig.IgnoreOwnMessages || - message.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + message.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - await dbService.AddMessage(folder, message.id, message.text != null ? message.text : string.Empty, - message.price != null ? message.price.ToString() : "0", true, false, - message.createdAt.HasValue ? message.createdAt.Value : DateTime.Now, - message.fromUser != null && message.fromUser.id != null ? message.fromUser.id.Value : int.MinValue); + await dbService.AddMessage(folder, message.Id, message.Text ?? string.Empty, + message.Price != null ? message.Price.ToString() : "0", true, false, + message.CreatedAt.HasValue ? message.CreatedAt.Value : DateTime.Now, + message.FromUser?.Id ?? int.MinValue); singlePaidMessageCollection.SingleMessageObjects.Add(message); List messagePreviewIds = new(); - if (message.previews != null && message.previews.Count > 0) + if (message.Previews != null && message.Previews.Count > 0) { - for (int i = 0; i < message.previews.Count; i++) + for (int i = 0; i < message.Previews.Count; i++) { - if (message.previews[i] is long previewId) + if (message.Previews[i] is long previewId) { if (!messagePreviewIds.Contains(previewId)) { @@ -1816,149 +1829,149 @@ public class APIService(IAuthService authService, IConfigService configService, } } - if (message.media != null && message.media.Count > 0) + if (message.Media != null && message.Media.Count > 0) { - foreach (Messages.Medium medium in message.media) + foreach (MessageEntities.Medium medium in message.Media) { - if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && - medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!messagePreviewIds.Contains(medium.Id) && medium.CanView && medium.Files != null && + medium.Files.Full != null && !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.id)) + if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, + await dbService.AddMedia(folder, medium.Id, message.Id, medium.Files.Full.Url, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.SingleMessages.Add(medium.id, medium.files.full.url); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.Id), false, null); + singlePaidMessageCollection.SingleMessages.Add(medium.Id, medium.Files.Full.Url); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } } - else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && - medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + else if (messagePreviewIds.Contains(medium.Id) && medium.CanView && medium.Files != null && + medium.Files.Full != null && !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.id)) + if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, + await dbService.AddMedia(folder, medium.Id, message.Id, medium.Files.Full.Url, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, medium.files.full.url); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.Id), false, null); + singlePaidMessageCollection.PreviewSingleMessages.Add(medium.Id, medium.Files.Full.Url); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } } - else if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && - medium.files.drm != null) + else if (!messagePreviewIds.Contains(medium.Id) && medium.CanView && medium.Files != null && + medium.Files.Drm != null) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.id)) + if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, + await dbService.AddMedia(folder, medium.Id, message.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.SingleMessages.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.Id), false, null); + singlePaidMessageCollection.SingleMessages.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{message.Id}"); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } } - else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && - medium.files.drm != null) + else if (messagePreviewIds.Contains(medium.Id) && medium.CanView && medium.Files != null && + medium.Files.Drm != null) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.id)) + if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, + await dbService.AddMedia(folder, medium.Id, message.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.Id), false, null); + singlePaidMessageCollection.PreviewSingleMessages.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{message.Id}"); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } } @@ -2102,152 +2115,154 @@ public class APIService(IAuthService authService, IConfigService configService, } } - foreach (Messages.Medium medium in purchase.media) + foreach (MessageDtos.MediumDto medium in purchase.media) { if (previewids.Count > 0) { - bool has = previewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url)) + MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); + bool has = previewids.Any(cus => cus.Equals(medium.Id)); + if (!has && medium.CanView && medium.Files != null && medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, - medium.files.full.url, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - paidMessageCollection.PaidMessages.Add(medium.id, medium.files.full.url); - paidMessageCollection.PaidMessageMedia.Add(medium); + await dbService.AddMedia(folder, medium.Id, purchase.id, + medium.Files.Full.Url, null, null, null, "Messages", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + paidMessageCollection.PaidMessages.Add(medium.Id, medium.Files.Full.Url); + paidMessageCollection.PaidMessageMedia.Add(mappedMedium); } } - else if (!has && medium.canView && medium.files != null && medium.files.drm != null) + else if (!has && medium.CanView && medium.Files != null && medium.Files.Drm != null) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, - medium.files.drm.manifest.dash, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - paidMessageCollection.PaidMessages.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); - paidMessageCollection.PaidMessageMedia.Add(medium); + await dbService.AddMedia(folder, medium.Id, purchase.id, + medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + paidMessageCollection.PaidMessages.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); + paidMessageCollection.PaidMessageMedia.Add(mappedMedium); } } } else { - if (medium.canView && medium.files != null && medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url)) + MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); + if (medium.CanView && medium.Files != null && medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, - medium.files.full.url, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - paidMessageCollection.PaidMessages.Add(medium.id, medium.files.full.url); - paidMessageCollection.PaidMessageMedia.Add(medium); + await dbService.AddMedia(folder, medium.Id, purchase.id, + medium.Files.Full.Url, null, null, null, "Messages", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + paidMessageCollection.PaidMessages.Add(medium.Id, medium.Files.Full.Url); + paidMessageCollection.PaidMessageMedia.Add(mappedMedium); } } - else if (medium.canView && medium.files != null && medium.files.drm != null) + else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, - medium.files.drm.manifest.dash, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - paidMessageCollection.PaidMessages.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); - paidMessageCollection.PaidMessageMedia.Add(medium); + await dbService.AddMedia(folder, medium.Id, purchase.id, + medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + paidMessageCollection.PaidMessages.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); + paidMessageCollection.PaidMessageMedia.Add(mappedMedium); } } } @@ -2576,7 +2591,7 @@ public class APIService(IAuthService authService, IConfigService configService, "PurchasedTab purchase media null, setting empty list | userId={UserId} username={Username} purchaseId={PurchaseId} responseType={ResponseType} createdAt={CreatedAt} postedAt={PostedAt}", user.Key, purchasedTabCollection.Username, purchase.id, purchase.responseType, purchase.createdAt, purchase.postedAt); - purchase.media = new List(); + purchase.media = new List(); } switch (purchase.responseType) @@ -2617,95 +2632,96 @@ public class APIService(IAuthService authService, IConfigService configService, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); purchasedTabCollection.PaidPosts.PaidPostObjects.Add(purchase); - foreach (Messages.Medium medium in purchase.media) + foreach (MessageDtos.MediumDto medium in purchase.media) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (previewids.Count > 0) { - bool has = previewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && - medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + bool has = previewids.Any(cus => cus.Equals(medium.Id)); + if (!has && medium.CanView && medium.Files != null && + medium.Files.Full != null && !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) + if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.id, purchase.id, - medium.files.full.url, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, - medium.files.full.url); - purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); + await dbService.AddMedia(path, medium.Id, purchase.id, + medium.Files.Full.Url, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, + medium.Files.Full.Url); + purchasedTabCollection.PaidPosts.PaidPostMedia.Add(mappedMedium); } } - else if (!has && medium.canView && medium.files != null && - medium.files.drm != null) + else if (!has && medium.CanView && medium.Files != null && + medium.Files.Drm != null) { - if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) + if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.id, purchase.id, - medium.files.drm.manifest.dash, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); - purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); + await dbService.AddMedia(path, medium.Id, purchase.id, + medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); + purchasedTabCollection.PaidPosts.PaidPostMedia.Add(mappedMedium); } } } else { - if (medium.canView && medium.files != null && medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.CanView && medium.Files != null && medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) + if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.id, purchase.id, - medium.files.full.url, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, - medium.files.full.url); - purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); + await dbService.AddMedia(path, medium.Id, purchase.id, + medium.Files.Full.Url, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, + medium.Files.Full.Url); + purchasedTabCollection.PaidPosts.PaidPostMedia.Add(mappedMedium); } } - else if (medium.canView && medium.files != null && medium.files.drm != null) + else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { - if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) + if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.id, purchase.id, - medium.files.drm.manifest.dash, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); - purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); + await dbService.AddMedia(path, medium.Id, purchase.id, + medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + previewids.Contains(medium.Id), false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); + purchasedTabCollection.PaidPosts.PaidPostMedia.Add(mappedMedium); } } } @@ -2759,176 +2775,182 @@ public class APIService(IAuthService authService, IConfigService configService, } } - foreach (Messages.Medium medium in purchase.media) + foreach (MessageDtos.MediumDto medium in purchase.media) { if (paidMessagePreviewids.Count > 0) { - bool has = paidMessagePreviewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && - medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url)) + MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); + bool has = paidMessagePreviewids.Any(cus => cus.Equals(medium.Id)); + if (!has && medium.CanView && medium.Files != null && + medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (medium.type == "photo" && + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( - medium.id)) + medium.Id)) { - await dbService.AddMedia(path, medium.id, purchase.id, - medium.files.full.url, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - paidMessagePreviewids.Contains(medium.id) ? true : false, false, + await dbService.AddMedia(path, medium.Id, purchase.id, + medium.Files.Full.Url, null, null, null, "Messages", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.Id), false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, - medium.files.full.url); - purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, + medium.Files.Full.Url); + purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( + mappedMedium); } } - else if (!has && medium.canView && medium.files != null && - medium.files.drm != null) + else if (!has && medium.CanView && medium.Files != null && + medium.Files.Drm != null) { - if (medium.type == "photo" && + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( - medium.id)) + medium.Id)) { - await dbService.AddMedia(path, medium.id, purchase.id, - medium.files.drm.manifest.dash, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - paidMessagePreviewids.Contains(medium.id) ? true : false, false, + await dbService.AddMedia(path, medium.Id, purchase.id, + medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.Id), false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); - purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); + purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( + mappedMedium); } } } else { - if (medium.canView && medium.files != null && medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url)) + MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); + if (medium.CanView && medium.Files != null && medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (medium.type == "photo" && + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( - medium.id)) + medium.Id)) { - await dbService.AddMedia(path, medium.id, purchase.id, - medium.files.full.url, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - paidMessagePreviewids.Contains(medium.id) ? true : false, false, + await dbService.AddMedia(path, medium.Id, purchase.id, + medium.Files.Full.Url, null, null, null, "Messages", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.Id), false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, - medium.files.full.url); - purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, + medium.Files.Full.Url); + purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( + mappedMedium); } } - else if (medium.canView && medium.files != null && medium.files.drm != null) + else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { - if (medium.type == "photo" && + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( - medium.id)) + medium.Id)) { - await dbService.AddMedia(path, medium.id, purchase.id, - medium.files.drm.manifest.dash, null, null, null, "Messages", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - paidMessagePreviewids.Contains(medium.id) ? true : false, false, + await dbService.AddMedia(path, medium.Id, purchase.id, + medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.Id), false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); - purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); + purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( + mappedMedium); } } } diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 509433b..577b4c2 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -4,17 +4,16 @@ using System.Xml.Linq; using FFmpeg.NET; using FFmpeg.NET.Events; using OF_DL.Models; -using OF_DL.Models.Messages; using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; -using ArchivedModels = OF_DL.Models.Entities.Archived; +using OF_DL.Models.Entities.Common; +using ArchivedEntities = OF_DL.Models.Entities.Archived; using OF_DL.Utils; using Serilog; using Serilog.Events; -using static OF_DL.Models.Messages.Messages; -using FromUser = OF_DL.Models.Messages.FromUser; +using MessageEntities = OF_DL.Models.Entities.Messages; namespace OF_DL.Services; @@ -1036,14 +1035,14 @@ public class DownloadService( public async Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, - Messages.FromUser? fromUser, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.ListItem? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) { string path; - if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && - messageInfo?.createdAt is not null) + if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.Id is not null && + messageInfo?.CreatedAt is not null) { - path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + path = $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"; } else { @@ -1059,15 +1058,14 @@ public class DownloadService( } public async Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Medium? messageMedia, - FromUser? fromUser, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) { string path; - if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && - messageInfo?.createdAt is not null) + if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.Id is not null && + messageInfo?.CreatedAt is not null) { - path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + path = $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"; } else { @@ -1084,8 +1082,8 @@ public class DownloadService( public async Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ArchivedModels.ListItem? messageInfo, - ArchivedModels.Medium? messageMedia, Models.Entities.Common.Author? author, + IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? messageInfo, + ArchivedEntities.Medium? messageMedia, Author? author, Dictionary users) { string path = "/Archived/Posts/Free"; @@ -1110,7 +1108,7 @@ public class DownloadService( public async Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Medium? messageMedia, + MessageEntities.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) { string path; @@ -1133,15 +1131,14 @@ public class DownloadService( } public async Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Medium? messageMedia, - FromUser? fromUser, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) { string path; - if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && - messageInfo?.createdAt is not null) + if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.Id is not null && + messageInfo?.CreatedAt is not null) { - path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + path = $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"; } else { @@ -1163,7 +1160,7 @@ public class DownloadService( IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Medium? messageMedia, + MessageEntities.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) { @@ -1193,8 +1190,8 @@ public class DownloadService( public async Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, - Messages.FromUser? fromUser, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.ListItem? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) { try { @@ -1202,10 +1199,10 @@ public class DownloadService( string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && - messageInfo?.createdAt is not null) + if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.Id is not null && + messageInfo?.CreatedAt is not null) { - path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + path = $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; } else { @@ -1322,9 +1319,8 @@ public class DownloadService( public async Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Medium? messageMedia, - FromUser? fromUser, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) { try { @@ -1332,10 +1328,10 @@ public class DownloadService( string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && - messageInfo?.createdAt is not null) + if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.Id is not null && + messageInfo?.CreatedAt is not null) { - path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + path = $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; } else { @@ -1454,7 +1450,7 @@ public class DownloadService( public async Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Medium? messageMedia, + MessageEntities.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users) { try @@ -1582,9 +1578,8 @@ public class DownloadService( public async Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Medium? messageMedia, - FromUser? fromUser, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) { try { @@ -1593,9 +1588,9 @@ public class DownloadService( Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && - messageInfo?.id is not null && messageInfo?.createdAt is not null) + messageInfo?.Id is not null && messageInfo?.CreatedAt is not null) { - path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + path = $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; } else { @@ -2097,7 +2092,7 @@ public class DownloadService( public async Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, - Medium? postMedia, + MessageEntities.Medium? postMedia, Purchased.FromUser? fromUser, Dictionary users) { try @@ -2227,9 +2222,9 @@ public class DownloadService( public async Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ArchivedModels.ListItem? postInfo, - ArchivedModels.Medium? postMedia, - Models.Entities.Common.Author? author, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? postInfo, + ArchivedEntities.Medium? postMedia, + Author? author, Dictionary users) { try { @@ -2454,7 +2449,7 @@ public class DownloadService( public async Task DownloadArchived(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, - ArchivedModels.ArchivedCollection archived, IProgressReporter progressReporter) + ArchivedEntities.ArchivedCollection archived, IProgressReporter progressReporter) { Log.Debug($"Calling DownloadArchived - {username}"); @@ -2511,9 +2506,9 @@ public class DownloadService( pssh); } - ArchivedModels.Medium? mediaInfo = + ArchivedEntities.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); - ArchivedModels.ListItem? postInfo = + ArchivedEntities.ListItem? postInfo = archived.ArchivedPostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadArchivedPostDRMVideo( @@ -2541,9 +2536,9 @@ public class DownloadService( } else { - ArchivedModels.Medium? mediaInfo = + ArchivedEntities.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); - ArchivedModels.ListItem? postInfo = + ArchivedEntities.ListItem? postInfo = archived.ArchivedPostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadArchivedMedia( @@ -2585,7 +2580,7 @@ public class DownloadService( public async Task DownloadMessages(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, - MessageCollection messages, IProgressReporter progressReporter) + MessageEntities.MessageCollection messages, IProgressReporter progressReporter) { Log.Debug($"Calling DownloadMessages - {username}"); @@ -2641,9 +2636,10 @@ public class DownloadService( pssh); } - Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); - List? messageInfo = - messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + MessageEntities.Medium? mediaInfo = + messages.MessageMedia.FirstOrDefault(m => m.Id == messageKVP.Key); + MessageEntities.ListItem? messageInfo = messages.MessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == messageKVP.Key) == true); isNew = await DownloadMessageDRMVideo( policy, @@ -2660,7 +2656,7 @@ public class DownloadService( string.Empty, messageInfo, mediaInfo, - messageInfo?.fromUser, + messageInfo?.FromUser, users); } else @@ -2670,8 +2666,9 @@ public class DownloadService( } else { - Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); - List? messageInfo = messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + MessageEntities.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.Id == messageKVP.Key); + MessageEntities.ListItem? messageInfo = messages.MessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == messageKVP.Key) == true); isNew = await DownloadMessageMedia( messageKVP.Value, @@ -2683,7 +2680,7 @@ public class DownloadService( string.Empty, messageInfo, mediaInfo, - messageInfo?.fromUser, + messageInfo?.FromUser, users); } @@ -2757,11 +2754,11 @@ public class DownloadService( $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/message/{parsed[5]}?type=widevine", pssh); - Medium? mediaInfo = - paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == kvp.Key); + MessageEntities.Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == kvp.Key); Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.media?.Contains(mediaInfo) == true); + p?.media?.Any(m => m.Id == kvp.Key) == true); isNew = await DownloadPurchasedMessageDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, lastModified, kvp.Key, "Messages", progressReporter, @@ -2775,10 +2772,10 @@ public class DownloadService( } else { - Medium? mediaInfo = - paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == kvp.Key); - Purchased.List? messageInfo = - paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + MessageEntities.Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == kvp.Key); + Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => + p?.media?.Any(m => m.Id == kvp.Key) == true); isNew = await DownloadPurchasedMedia(kvp.Value, path, kvp.Key, "Messages", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? string.Empty, messageInfo, mediaInfo, messageInfo?.fromUser, users); @@ -3032,9 +3029,11 @@ public class DownloadService( $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", pssh); - Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); + MessageEntities.Medium? mediaInfo = + purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKVP.Key); Purchased.List? postInfo = - purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + purchasedPosts.PaidPostObjects.FirstOrDefault(p => + p?.media?.Any(m => m.Id == postKVP.Key) == true); isNew = await DownloadPurchasedPostDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, lastModified, postKVP.Key, "Posts", progressReporter, @@ -3048,9 +3047,10 @@ public class DownloadService( } else { - Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.id == postKVP.Key); + MessageEntities.Medium? mediaInfo = + purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKVP.Key); Purchased.List? postInfo = - purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Any(m => m.Id == postKVP.Key) == true); isNew = await DownloadPurchasedPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? string.Empty, postInfo, mediaInfo, postInfo?.fromUser, users); diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index ae00a9f..d8e7e52 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -1,11 +1,11 @@ using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Messages; using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; -using ArchivedModels = OF_DL.Models.Entities.Archived; +using ArchivedEntities = OF_DL.Models.Entities.Archived; +using MessageEntities = OF_DL.Models.Entities.Messages; using Spectre.Console; namespace OF_DL.Services; @@ -28,8 +28,8 @@ public interface IAPIService Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx); Task GetPost(string endpoint, string folder); Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx); - Task GetArchived(string endpoint, string folder, StatusContext ctx); - Task GetMessages(string endpoint, string folder, StatusContext ctx); + Task GetArchived(string endpoint, string folder, StatusContext ctx); + Task GetMessages(string endpoint, string folder, StatusContext ctx); Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx); Task GetPaidMessage(string endpoint, string folder); Task> GetPurchasedTabUsers(string endpoint, Dictionary users); diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index b334e91..1f90736 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -1,11 +1,10 @@ using OF_DL.Models; -using ArchivedModels = OF_DL.Models.Entities.Archived; -using OF_DL.Models.Messages; +using OF_DL.Models.Entities.Common; +using ArchivedEntities = OF_DL.Models.Entities.Archived; using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; -using static OF_DL.Models.Messages.Messages; -using FromUser = OF_DL.Models.Messages.FromUser; +using MessageEntities = OF_DL.Models.Entities.Messages; namespace OF_DL.Services; @@ -17,15 +16,15 @@ public interface IDownloadService string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ArchivedModels.ListItem? messageInfo, - ArchivedModels.Medium? messageMedia, Models.Entities.Common.Author? author, + IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? messageInfo, + ArchivedEntities.Medium? messageMedia, Author? author, Dictionary users); Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ArchivedModels.ListItem? postInfo, - ArchivedModels.Medium? postMedia, - Models.Entities.Common.Author? author, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? postInfo, + ArchivedEntities.Medium? postMedia, + Author? author, Dictionary users); Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, @@ -41,12 +40,13 @@ public interface IDownloadService Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, - string? filenameFormat, List? messageInfo, Medium? messageMedia, Messages.FromUser? fromUser, + string? filenameFormat, MessageEntities.ListItem? messageInfo, MessageEntities.Medium? messageMedia, + FromUser? fromUser, Dictionary users); Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, - Messages.FromUser? fromUser, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.ListItem? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, @@ -58,35 +58,33 @@ public interface IDownloadService Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Medium? messageMedia, + MessageEntities.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Medium? messageMedia, - FromUser? fromUser, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Medium? messageMedia, + MessageEntities.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Medium? messageMedia, - FromUser? fromUser, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, - Medium? postMedia, + MessageEntities.Medium? postMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, - Medium? messageMedia, + MessageEntities.Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, @@ -103,14 +101,12 @@ public interface IDownloadService Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Medium? messageMedia, - FromUser? fromUser, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, - Medium? messageMedia, - FromUser? fromUser, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, + MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users); Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); @@ -119,11 +115,11 @@ public interface IDownloadService IProgressReporter progressReporter); Task DownloadArchived(string username, long userId, string path, Dictionary users, - bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedModels.ArchivedCollection archived, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedEntities.ArchivedCollection archived, IProgressReporter progressReporter); Task DownloadMessages(string username, long userId, string path, Dictionary users, - bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageCollection messages, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageEntities.MessageCollection messages, IProgressReporter progressReporter); Task DownloadPaidMessages(string username, string path, Dictionary users, From 3c307bf7de7dfb3a9af7607220b5534d14bdce59 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Sun, 8 Feb 2026 21:04:09 -0600 Subject: [PATCH 17/55] Refactor Posts entities into DTOs and application entities with standardized naming conventions and default values --- OF DL/Models/Dtos/Posts/InfoDto.cs | 11 + OF DL/Models/Dtos/Posts/ListItemDto.cs | 101 +++++++ OF DL/Models/Dtos/Posts/MediumDto.cs | 37 +++ OF DL/Models/Dtos/Posts/PostDto.cs | 14 + OF DL/Models/Dtos/Posts/SinglePostDto.cs | 99 +++++++ OF DL/Models/Entities/Common/Files.cs | 2 + OF DL/Models/Entities/Common/Preview.cs | 6 + OF DL/Models/Entities/Common/VideoSources.cs | 8 + OF DL/Models/Entities/Posts/ListItem.cs | 41 +++ OF DL/Models/Entities/Posts/Medium.cs | 18 ++ OF DL/Models/Entities/Posts/Post.cs | 10 + OF DL/Models/Entities/Posts/PostCollection.cs | 10 + OF DL/Models/Entities/Posts/SinglePost.cs | 41 +++ .../Entities/Posts/SinglePostCollection.cs | 10 + OF DL/Models/Mappers/ArchivedMapper.cs | 33 +-- OF DL/Models/Mappers/MessagesMapper.cs | 23 +- OF DL/Models/Mappers/PostMapper.cs | 182 ++++++++++++ OF DL/Models/Post/Post.cs | 200 ------------- OF DL/Models/Post/PostCollection.cs | 8 - OF DL/Models/Post/SinglePost.cs | 188 ------------- OF DL/Models/Post/SinglePostCollection.cs | 8 - OF DL/Program.cs | 26 +- OF DL/Services/APIService.cs | 262 +++++++++--------- OF DL/Services/DownloadService.cs | 83 +++--- OF DL/Services/IAPIService.cs | 8 +- OF DL/Services/IDownloadService.cs | 40 +-- 26 files changed, 821 insertions(+), 648 deletions(-) create mode 100644 OF DL/Models/Dtos/Posts/InfoDto.cs create mode 100644 OF DL/Models/Dtos/Posts/ListItemDto.cs create mode 100644 OF DL/Models/Dtos/Posts/MediumDto.cs create mode 100644 OF DL/Models/Dtos/Posts/PostDto.cs create mode 100644 OF DL/Models/Dtos/Posts/SinglePostDto.cs create mode 100644 OF DL/Models/Entities/Common/Preview.cs create mode 100644 OF DL/Models/Entities/Common/VideoSources.cs create mode 100644 OF DL/Models/Entities/Posts/ListItem.cs create mode 100644 OF DL/Models/Entities/Posts/Medium.cs create mode 100644 OF DL/Models/Entities/Posts/Post.cs create mode 100644 OF DL/Models/Entities/Posts/PostCollection.cs create mode 100644 OF DL/Models/Entities/Posts/SinglePost.cs create mode 100644 OF DL/Models/Entities/Posts/SinglePostCollection.cs create mode 100644 OF DL/Models/Mappers/PostMapper.cs delete mode 100644 OF DL/Models/Post/Post.cs delete mode 100644 OF DL/Models/Post/PostCollection.cs delete mode 100644 OF DL/Models/Post/SinglePost.cs delete mode 100644 OF DL/Models/Post/SinglePostCollection.cs diff --git a/OF DL/Models/Dtos/Posts/InfoDto.cs b/OF DL/Models/Dtos/Posts/InfoDto.cs new file mode 100644 index 0000000..6a7bcf3 --- /dev/null +++ b/OF DL/Models/Dtos/Posts/InfoDto.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Posts; + +public class InfoDto +{ + [JsonProperty("source")] public SourceDto Source { get; set; } = new(); + + [JsonProperty("preview")] public PreviewDto Preview { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Posts/ListItemDto.cs b/OF DL/Models/Dtos/Posts/ListItemDto.cs new file mode 100644 index 0000000..d4196e8 --- /dev/null +++ b/OF DL/Models/Dtos/Posts/ListItemDto.cs @@ -0,0 +1,101 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; +using OF_DL.Utils; + +namespace OF_DL.Models.Dtos.Posts; + +public class ListItemDto +{ + private string _rawText = ""; + + [JsonProperty("responseType")] public string ResponseType { get; set; } = ""; + + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("postedAt")] public DateTime PostedAt { get; set; } + + [JsonProperty("postedAtPrecise")] public string PostedAtPrecise { get; set; } = ""; + + [JsonProperty("expiredAt")] public object ExpiredAt { get; set; } = new(); + + [JsonProperty("author")] public AuthorDto Author { get; set; } = new(); + + [JsonProperty("text")] public string Text { get; set; } = ""; + + [JsonProperty("rawText")] + public string RawText + { + get + { + if (string.IsNullOrEmpty(_rawText)) + { + _rawText = XmlUtils.EvaluateInnerText(Text); + } + + return _rawText; + } + set => _rawText = value; + } + + [JsonProperty("lockedText")] public bool? LockedText { get; set; } + + [JsonProperty("isFavorite")] public bool? IsFavorite { get; set; } + + [JsonProperty("canReport")] public bool? CanReport { get; set; } + + [JsonProperty("canDelete")] public bool? CanDelete { get; set; } + + [JsonProperty("canComment")] public bool? CanComment { get; set; } + + [JsonProperty("canEdit")] public bool? CanEdit { get; set; } + + [JsonProperty("isPinned")] public bool? IsPinned { get; set; } + + [JsonProperty("favoritesCount")] public int? FavoritesCount { get; set; } + + [JsonProperty("mediaCount")] public int? MediaCount { get; set; } + + [JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; } + + [JsonProperty("voting")] public object Voting { get; set; } = new(); + + [JsonProperty("isOpened")] public bool IsOpened { get; set; } + + [JsonProperty("canToggleFavorite")] public bool? CanToggleFavorite { get; set; } + + [JsonProperty("streamId")] public object StreamId { get; set; } = new(); + + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("hasVoting")] public bool? HasVoting { get; set; } + + [JsonProperty("isAddedToBookmarks")] public bool? IsAddedToBookmarks { get; set; } + + [JsonProperty("isArchived")] public bool IsArchived { get; set; } + + [JsonProperty("isPrivateArchived")] public bool? IsPrivateArchived { get; set; } + + [JsonProperty("isDeleted")] public bool? IsDeleted { get; set; } + + [JsonProperty("hasUrl")] public bool? HasUrl { get; set; } + + [JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; } + + [JsonProperty("cantCommentReason")] public string CantCommentReason { get; set; } = ""; + + [JsonProperty("votingType")] public int? VotingType { get; set; } + + [JsonProperty("commentsCount")] public int? CommentsCount { get; set; } + + [JsonProperty("mentionedUsers")] public List MentionedUsers { get; set; } = []; + + [JsonProperty("linkedUsers")] public List LinkedUsers { get; set; } = []; + + [JsonProperty("canVote")] public bool? CanVote { get; set; } + + [JsonProperty("media")] public List Media { get; set; } = []; + + [JsonProperty("canViewMedia")] public bool? CanViewMedia { get; set; } + + [JsonProperty("preview")] public List Preview { get; set; } = []; +} diff --git a/OF DL/Models/Dtos/Posts/MediumDto.cs b/OF DL/Models/Dtos/Posts/MediumDto.cs new file mode 100644 index 0000000..39d785c --- /dev/null +++ b/OF DL/Models/Dtos/Posts/MediumDto.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Posts; + +public class MediumDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("convertedToVideo")] public bool? ConvertedToVideo { get; set; } + + [JsonProperty("canView")] public bool CanView { get; set; } + + [JsonProperty("hasError")] public bool? HasError { get; set; } + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } + + [JsonProperty("info")] public InfoDto? Info { get; set; } + + [JsonProperty("source")] public SourceDto? Source { get; set; } + + [JsonProperty("squarePreview")] public string? SquarePreview { get; set; } + + [JsonProperty("full")] public string? Full { get; set; } + + [JsonProperty("preview")] public string? Preview { get; set; } + + [JsonProperty("thumb")] public string? Thumb { get; set; } + + [JsonProperty("hasCustomPreview")] public bool? HasCustomPreview { get; set; } + + [JsonProperty("files")] public FilesDto Files { get; set; } = new(); + + [JsonProperty("videoSources")] public VideoSourcesDto VideoSources { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Posts/PostDto.cs b/OF DL/Models/Dtos/Posts/PostDto.cs new file mode 100644 index 0000000..8b7fbeb --- /dev/null +++ b/OF DL/Models/Dtos/Posts/PostDto.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Posts; + +public class PostDto +{ + [JsonProperty("list")] public List List { get; set; } = []; + + [JsonProperty("hasMore")] public bool HasMore { get; set; } + + [JsonProperty("headMarker")] public string HeadMarker { get; set; } = ""; + + [JsonProperty("tailMarker")] public string TailMarker { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Posts/SinglePostDto.cs b/OF DL/Models/Dtos/Posts/SinglePostDto.cs new file mode 100644 index 0000000..d37073e --- /dev/null +++ b/OF DL/Models/Dtos/Posts/SinglePostDto.cs @@ -0,0 +1,99 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; +using OF_DL.Utils; + +namespace OF_DL.Models.Dtos.Posts; + +public class SinglePostDto +{ + private string _rawText = ""; + + [JsonProperty("responseType")] public string ResponseType { get; set; } = ""; + + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("postedAt")] public DateTime PostedAt { get; set; } + + [JsonProperty("postedAtPrecise")] public string PostedAtPrecise { get; set; } = ""; + + [JsonProperty("expiredAt")] public object ExpiredAt { get; set; } = new(); + + [JsonProperty("author")] public AuthorDto Author { get; set; } = new(); + + [JsonProperty("text")] public string Text { get; set; } = ""; + + [JsonProperty("rawText")] + public string RawText + { + get + { + if (string.IsNullOrEmpty(_rawText)) + { + _rawText = XmlUtils.EvaluateInnerText(Text); + } + + return _rawText; + } + set => _rawText = value; + } + + [JsonProperty("lockedText")] public bool LockedText { get; set; } + + [JsonProperty("isFavorite")] public bool IsFavorite { get; set; } + + [JsonProperty("canReport")] public bool CanReport { get; set; } + + [JsonProperty("canDelete")] public bool CanDelete { get; set; } + + [JsonProperty("canComment")] public bool CanComment { get; set; } + + [JsonProperty("canEdit")] public bool CanEdit { get; set; } + + [JsonProperty("isPinned")] public bool IsPinned { get; set; } + + [JsonProperty("favoritesCount")] public int FavoritesCount { get; set; } + + [JsonProperty("mediaCount")] public int MediaCount { get; set; } + + [JsonProperty("isMediaReady")] public bool IsMediaReady { get; set; } + + [JsonProperty("voting")] public object Voting { get; set; } = new(); + + [JsonProperty("isOpened")] public bool IsOpened { get; set; } + + [JsonProperty("canToggleFavorite")] public bool CanToggleFavorite { get; set; } + + [JsonProperty("streamId")] public string StreamId { get; set; } = ""; + + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("hasVoting")] public bool HasVoting { get; set; } + + [JsonProperty("isAddedToBookmarks")] public bool IsAddedToBookmarks { get; set; } + + [JsonProperty("isArchived")] public bool IsArchived { get; set; } + + [JsonProperty("isPrivateArchived")] public bool IsPrivateArchived { get; set; } + + [JsonProperty("isDeleted")] public bool IsDeleted { get; set; } + + [JsonProperty("hasUrl")] public bool HasUrl { get; set; } + + [JsonProperty("isCouplePeopleMedia")] public bool IsCouplePeopleMedia { get; set; } + + [JsonProperty("commentsCount")] public int CommentsCount { get; set; } + + [JsonProperty("mentionedUsers")] public List MentionedUsers { get; set; } = []; + + [JsonProperty("linkedUsers")] public List LinkedUsers { get; set; } = []; + + [JsonProperty("tipsAmount")] public string TipsAmount { get; set; } = ""; + + [JsonProperty("tipsAmountRaw")] public string TipsAmountRaw { get; set; } = ""; + + [JsonProperty("media")] public List Media { get; set; } = []; + + [JsonProperty("canViewMedia")] public bool CanViewMedia { get; set; } + + [JsonProperty("preview")] public List Preview { get; set; } = []; +} diff --git a/OF DL/Models/Entities/Common/Files.cs b/OF DL/Models/Entities/Common/Files.cs index 97bdadb..4f50965 100644 --- a/OF DL/Models/Entities/Common/Files.cs +++ b/OF DL/Models/Entities/Common/Files.cs @@ -4,5 +4,7 @@ public class Files { public Full? Full { get; set; } + public Preview? Preview { get; set; } + public Drm? Drm { get; set; } } diff --git a/OF DL/Models/Entities/Common/Preview.cs b/OF DL/Models/Entities/Common/Preview.cs new file mode 100644 index 0000000..a3d6edb --- /dev/null +++ b/OF DL/Models/Entities/Common/Preview.cs @@ -0,0 +1,6 @@ +namespace OF_DL.Models.Entities.Common; + +public class Preview +{ + public string? Url { get; set; } +} diff --git a/OF DL/Models/Entities/Common/VideoSources.cs b/OF DL/Models/Entities/Common/VideoSources.cs new file mode 100644 index 0000000..92422c0 --- /dev/null +++ b/OF DL/Models/Entities/Common/VideoSources.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Models.Entities.Common; + +public class VideoSources +{ + public string? _720 { get; set; } + + public string? _240 { get; set; } +} diff --git a/OF DL/Models/Entities/Posts/ListItem.cs b/OF DL/Models/Entities/Posts/ListItem.cs new file mode 100644 index 0000000..e5a2f5b --- /dev/null +++ b/OF DL/Models/Entities/Posts/ListItem.cs @@ -0,0 +1,41 @@ +using OF_DL.Models.Entities.Common; +using OF_DL.Utils; + +namespace OF_DL.Models.Entities.Posts; + +public class ListItem +{ + private string _rawText = ""; + + public long Id { get; set; } + + public DateTime PostedAt { get; set; } + + public Author? Author { get; set; } + + public string Text { get; set; } = ""; + + public string RawText + { + get + { + if (string.IsNullOrEmpty(_rawText)) + { + _rawText = XmlUtils.EvaluateInnerText(Text); + } + + return _rawText; + } + set => _rawText = value; + } + + public bool IsOpened { get; set; } + + public string? Price { get; set; } + + public bool IsArchived { get; set; } + + public List? Media { get; set; } + + public List? Preview { get; set; } +} diff --git a/OF DL/Models/Entities/Posts/Medium.cs b/OF DL/Models/Entities/Posts/Medium.cs new file mode 100644 index 0000000..88eab10 --- /dev/null +++ b/OF DL/Models/Entities/Posts/Medium.cs @@ -0,0 +1,18 @@ +using OF_DL.Models.Entities.Common; + +namespace OF_DL.Models.Entities.Posts; + +public class Medium +{ + public long Id { get; set; } + + public string Type { get; set; } = ""; + + public bool CanView { get; set; } + + public string? Preview { get; set; } + + public Files? Files { get; set; } + + public VideoSources? VideoSources { get; set; } +} diff --git a/OF DL/Models/Entities/Posts/Post.cs b/OF DL/Models/Entities/Posts/Post.cs new file mode 100644 index 0000000..d914824 --- /dev/null +++ b/OF DL/Models/Entities/Posts/Post.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Posts; + +public class Post +{ + public List List { get; set; } = []; + + public bool HasMore { get; set; } + + public string? TailMarker { get; set; } +} diff --git a/OF DL/Models/Entities/Posts/PostCollection.cs b/OF DL/Models/Entities/Posts/PostCollection.cs new file mode 100644 index 0000000..c5cecba --- /dev/null +++ b/OF DL/Models/Entities/Posts/PostCollection.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Posts; + +public class PostCollection +{ + public List PostMedia { get; set; } = []; + + public List PostObjects { get; set; } = []; + + public Dictionary Posts { get; set; } = new(); +} diff --git a/OF DL/Models/Entities/Posts/SinglePost.cs b/OF DL/Models/Entities/Posts/SinglePost.cs new file mode 100644 index 0000000..9875864 --- /dev/null +++ b/OF DL/Models/Entities/Posts/SinglePost.cs @@ -0,0 +1,41 @@ +using OF_DL.Models.Entities.Common; +using OF_DL.Utils; + +namespace OF_DL.Models.Entities.Posts; + +public class SinglePost +{ + private string _rawText = ""; + + public long Id { get; set; } + + public DateTime PostedAt { get; set; } + + public Author? Author { get; set; } + + public string Text { get; set; } = ""; + + public string RawText + { + get + { + if (string.IsNullOrEmpty(_rawText)) + { + _rawText = XmlUtils.EvaluateInnerText(Text); + } + + return _rawText; + } + set => _rawText = value; + } + + public bool IsOpened { get; set; } + + public string? Price { get; set; } + + public bool IsArchived { get; set; } + + public List? Media { get; set; } + + public List? Preview { get; set; } +} diff --git a/OF DL/Models/Entities/Posts/SinglePostCollection.cs b/OF DL/Models/Entities/Posts/SinglePostCollection.cs new file mode 100644 index 0000000..9afc43c --- /dev/null +++ b/OF DL/Models/Entities/Posts/SinglePostCollection.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Posts; + +public class SinglePostCollection +{ + public List SinglePostMedia { get; set; } = []; + + public List SinglePostObjects { get; set; } = []; + + public Dictionary SinglePosts { get; set; } = new(); +} diff --git a/OF DL/Models/Mappers/ArchivedMapper.cs b/OF DL/Models/Mappers/ArchivedMapper.cs index 22666b8..2ef0d09 100644 --- a/OF DL/Models/Mappers/ArchivedMapper.cs +++ b/OF DL/Models/Mappers/ArchivedMapper.cs @@ -77,15 +77,8 @@ public static class ArchivedMapper return new Entities.Common.Files { Full = MapFull(dto.Full), Drm = MapDrm(dto.Drm) }; } - private static Entities.Common.Full? MapFull(FullDto? dto) - { - if (dto == null) - { - return null; - } - - return new Entities.Common.Full { Url = dto.Url }; - } + private static Entities.Common.Full? MapFull(FullDto? dto) => + dto == null ? null : new Entities.Common.Full { Url = dto.Url }; private static Entities.Common.Drm? MapDrm(DrmDto? dto) { @@ -100,25 +93,11 @@ public static class ArchivedMapper }; } - private static Entities.Common.Manifest? MapManifest(ManifestDto? dto) - { - if (dto == null) - { - return null; - } + private static Entities.Common.Manifest? MapManifest(ManifestDto? dto) => + dto == null ? null : new Entities.Common.Manifest { Dash = dto.Dash }; - return new Entities.Common.Manifest { Dash = dto.Dash }; - } - - private static Entities.Common.Signature? MapSignature(SignatureDto? dto) - { - if (dto == null) - { - return null; - } - - return new Entities.Common.Signature { Dash = MapDash(dto.Dash) }; - } + private static Entities.Common.Signature? MapSignature(SignatureDto? dto) => + dto == null ? null : new Entities.Common.Signature { Dash = MapDash(dto.Dash) }; private static Entities.Common.Dash? MapDash(DashDto? dto) { diff --git a/OF DL/Models/Mappers/MessagesMapper.cs b/OF DL/Models/Mappers/MessagesMapper.cs index bedf505..3f3f750 100644 --- a/OF DL/Models/Mappers/MessagesMapper.cs +++ b/OF DL/Models/Mappers/MessagesMapper.cs @@ -66,27 +66,14 @@ public static class MessagesMapper private static FromUser? MapFromUser(FromUserDto? dto) => dto == null ? null : new FromUser { Id = dto.Id }; - private static Files? MapFiles(FilesDto? dto) - { - if (dto == null) - { - return null; - } - - return new Files { Full = MapFull(dto.Full), Drm = MapDrm(dto.Drm) }; - } + private static Files? MapFiles(FilesDto? dto) => + dto == null ? null : new Files { Full = MapFull(dto.Full), Drm = MapDrm(dto.Drm) }; private static Full? MapFull(FullDto? dto) => dto == null ? null : new Full { Url = dto.Url }; - private static Drm? MapDrm(DrmDto? dto) - { - if (dto == null) - { - return null; - } - - return new Drm { Manifest = MapManifest(dto.Manifest), Signature = MapSignature(dto.Signature) }; - } + private static Drm? MapDrm(DrmDto? dto) => dto == null + ? null + : new Drm { Manifest = MapManifest(dto.Manifest), Signature = MapSignature(dto.Signature) }; private static Manifest? MapManifest(ManifestDto? dto) => dto == null ? null : new Manifest { Dash = dto.Dash }; diff --git a/OF DL/Models/Mappers/PostMapper.cs b/OF DL/Models/Mappers/PostMapper.cs new file mode 100644 index 0000000..00c1b8f --- /dev/null +++ b/OF DL/Models/Mappers/PostMapper.cs @@ -0,0 +1,182 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Posts; +using OF_DL.Models.Entities.Common; +using OF_DL.Models.Entities.Posts; + + +namespace OF_DL.Models.Mappers; + +public static class PostMapper +{ + public static Post FromDto(PostDto? dto) + { + Post mapped = new() { HasMore = dto?.HasMore ?? false, TailMarker = dto?.TailMarker }; + + if (dto?.List == null) + { + return mapped; + } + + foreach (ListItemDto entry in dto.List) + { + mapped.List.Add(MapList(entry)); + } + + return mapped; + } + + public static SinglePost FromDto(SinglePostDto? dto) + { + SinglePost mapped = new(); + + if (dto == null) + { + return mapped; + } + + mapped.Id = dto.Id; + mapped.PostedAt = dto.PostedAt; + mapped.Author = MapSingleAuthor(dto.Author); + mapped.Text = dto.Text; + mapped.RawText = dto.RawText; + mapped.IsOpened = dto.IsOpened; + mapped.Price = dto.Price; + mapped.IsArchived = dto.IsArchived; + mapped.Media = MapSingleMedia(dto.Media); + mapped.Preview = dto.Preview; + + return mapped; + } + + private static ListItem MapList(ListItemDto dto) => + new() + { + Id = dto.Id, + PostedAt = dto.PostedAt, + Author = MapAuthor(dto.Author), + Text = dto.Text, + RawText = dto.RawText, + IsOpened = dto.IsOpened, + Price = dto.Price, + IsArchived = dto.IsArchived, + Media = MapMedia(dto.Media), + Preview = dto.Preview + }; + + private static Author? MapAuthor(AuthorDto? dto) => + dto == null ? null : new Author { Id = dto.Id }; + + private static Author? MapSingleAuthor(AuthorDto? dto) => + dto == null ? null : new Author { Id = dto.Id }; + + private static List? MapMedia(List? media) => + media?.Select(MapMedium).ToList(); + + private static Medium MapMedium(MediumDto dto) => + new() + { + Id = dto.Id, + Type = dto.Type, + CanView = dto.CanView, + Preview = dto.Preview, + Files = MapFiles(dto.Files) + }; + + private static Files? MapFiles(FilesDto? dto) => dto == null + ? null + : new Files { Full = MapFull(dto.Full), Preview = MapPreview(dto.Preview), Drm = MapDrm(dto.Drm) }; + + private static Full? MapFull(FullDto? dto) => + dto == null || string.IsNullOrEmpty(dto.Url) ? null : new Full { Url = dto.Url }; + + private static Preview? MapPreview(PreviewDto? dto) => + dto == null || string.IsNullOrEmpty(dto.Url) ? null : new Preview { Url = dto.Url }; + + private static Drm MapDrm(DrmDto? dto) => new() + { + Manifest = MapManifest(dto?.Manifest), Signature = MapSignature(dto?.Signature) + }; + + private static Manifest? MapManifest(ManifestDto? dto) => + dto == null ? null : new Manifest { Dash = dto.Dash }; + + private static Signature? MapSignature(SignatureDto? dto) => + dto == null ? null : new Signature { Dash = MapDash(dto.Dash) }; + + private static Dash? MapDash(DashDto? dto) + { + if (dto == null) + { + return null; + } + + return new Dash + { + CloudFrontPolicy = dto.CloudFrontPolicy, + CloudFrontSignature = dto.CloudFrontSignature, + CloudFrontKeyPairId = dto.CloudFrontKeyPairId + }; + } + + private static List? MapSingleMedia(List? media) => + media?.Select(MapSingleMedium).ToList(); + + private static Medium MapSingleMedium(MediumDto dto) => + new() + { + Id = dto.Id, + Type = dto.Type, + CanView = dto.CanView, + Preview = dto.Preview, + Files = MapSingleFiles(dto.Files), + VideoSources = MapVideoSources(dto.VideoSources) + }; + + private static Files? MapSingleFiles(FilesDto? dto) + { + if (dto == null) + { + return null; + } + + return new Files + { + Full = MapSingleFull(dto.Full), Preview = MapSinglePreview(dto.Preview), Drm = MapSingleDrm(dto.Drm) + }; + } + + private static Full? MapSingleFull(FullDto? dto) => + dto == null || string.IsNullOrEmpty(dto.Url) ? null : new Full { Url = dto.Url }; + + private static Preview? MapSinglePreview(PreviewDto? dto) => + dto == null || string.IsNullOrEmpty(dto.Url) ? null : new Preview { Url = dto.Url }; + + private static Drm? MapSingleDrm(DrmDto? dto) => dto == null + ? null + : new Drm { Manifest = MapSingleManifest(dto.Manifest), Signature = MapSingleSignature(dto.Signature) }; + + private static Manifest? MapSingleManifest(ManifestDto? dto) => + dto == null ? null : new Manifest { Dash = dto.Dash }; + + private static Signature? MapSingleSignature(SignatureDto? dto) => + dto == null ? null : new Signature { Dash = MapSingleDash(dto.Dash) }; + + private static Dash? MapSingleDash(DashDto? dto) + { + if (dto == null) + { + return null; + } + + return new Dash + { + CloudFrontPolicy = dto.CloudFrontPolicy, + CloudFrontSignature = dto.CloudFrontSignature, + CloudFrontKeyPairId = dto.CloudFrontKeyPairId + }; + } + + private static VideoSources? MapVideoSources(VideoSourcesDto? dto) => dto == null + ? null + : new VideoSources { _240 = dto._240, _720 = dto._720 }; +} diff --git a/OF DL/Models/Post/Post.cs b/OF DL/Models/Post/Post.cs deleted file mode 100644 index e61a48c..0000000 --- a/OF DL/Models/Post/Post.cs +++ /dev/null @@ -1,200 +0,0 @@ -using Newtonsoft.Json; -using OF_DL.Utils; - -namespace OF_DL.Models.Post; - -#pragma warning disable IDE1006 // Naming Styles -public class Post -{ - public List list { get; set; } - public bool hasMore { get; set; } - - public string headMarker { get; set; } - - public string tailMarker { get; set; } - - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } - - public class Dash - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } - - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } - - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Hls - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } - - public class List - { - private string _rawText; - public string responseType { get; set; } - public long id { get; set; } - public DateTime postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - - public string rawText - { - get - { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } - - return _rawText; - } - set => _rawText = value; - } - - public bool? lockedText { get; set; } - public bool? isFavorite { get; set; } - public bool? canReport { get; set; } - public bool? canDelete { get; set; } - public bool? canComment { get; set; } - public bool? canEdit { get; set; } - public bool? isPinned { get; set; } - public int? favoritesCount { get; set; } - public int? mediaCount { get; set; } - public bool? isMediaReady { get; set; } - public object voting { get; set; } - public bool isOpened { get; set; } - public bool? canToggleFavorite { get; set; } - public object streamId { get; set; } - public string? price { get; set; } - public bool? hasVoting { get; set; } - public bool? isAddedToBookmarks { get; set; } - public bool isArchived { get; set; } - public bool? isPrivateArchived { get; set; } - public bool? isDeleted { get; set; } - public bool? hasUrl { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public string cantCommentReason { get; set; } - public int? votingType { get; set; } - public int? commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public bool? canVote { get; set; } - public List media { get; set; } - public bool? canViewMedia { get; set; } - public List preview { get; set; } - } - - public class Manifest - { - public string? hls { get; set; } - public string? dash { get; set; } - } - - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool? convertedToVideo { get; set; } - public bool canView { get; set; } - public bool? hasError { get; set; } - public DateTime? createdAt { get; set; } - public Info info { get; set; } - public Source source { get; set; } - public string squarePreview { get; set; } - public string full { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public Files files { get; set; } - public VideoSources videoSources { get; set; } - } - - public class Preview - { - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - public string url { get; set; } - } - - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } - - public class Source - { - public string? source { get; set; } - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - public int? duration { get; set; } - } - - public class VideoSources - { - [JsonProperty("720")] public object _720 { get; set; } - - [JsonProperty("240")] public object _240 { get; set; } - } -#pragma warning restore IDE1006 // Naming Styles -} diff --git a/OF DL/Models/Post/PostCollection.cs b/OF DL/Models/Post/PostCollection.cs deleted file mode 100644 index 861a20c..0000000 --- a/OF DL/Models/Post/PostCollection.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OF_DL.Models.Post; - -public class PostCollection -{ - public List PostMedia = new(); - public List PostObjects = new(); - public Dictionary Posts = new(); -} diff --git a/OF DL/Models/Post/SinglePost.cs b/OF DL/Models/Post/SinglePost.cs deleted file mode 100644 index d3b2fba..0000000 --- a/OF DL/Models/Post/SinglePost.cs +++ /dev/null @@ -1,188 +0,0 @@ -using Newtonsoft.Json; -using OF_DL.Utils; - -namespace OF_DL.Models.Post; - -public class SinglePost -{ - private string _rawText; - public string responseType { get; set; } - public long id { get; set; } - public DateTime postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - - public string rawText - { - get - { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } - - return _rawText; - } - set => _rawText = value; - } - - public bool lockedText { get; set; } - public bool isFavorite { get; set; } - public bool canReport { get; set; } - public bool canDelete { get; set; } - public bool canComment { get; set; } - public bool canEdit { get; set; } - public bool isPinned { get; set; } - public int favoritesCount { get; set; } - public int mediaCount { get; set; } - public bool isMediaReady { get; set; } - public object voting { get; set; } - public bool isOpened { get; set; } - public bool canToggleFavorite { get; set; } - public string streamId { get; set; } - public string price { get; set; } - public bool hasVoting { get; set; } - public bool isAddedToBookmarks { get; set; } - public bool isArchived { get; set; } - public bool isPrivateArchived { get; set; } - public bool isDeleted { get; set; } - public bool hasUrl { get; set; } - public bool isCouplePeopleMedia { get; set; } - public int commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public string tipsAmount { get; set; } - public string tipsAmountRaw { get; set; } - public List media { get; set; } - public bool canViewMedia { get; set; } - public List preview { get; set; } - - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } - - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } - - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } - - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } - - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Info info { get; set; } - public Source source { get; set; } - public string squarePreview { get; set; } - public string full { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public bool hasCustomPreview { get; set; } - public Files files { get; set; } - public VideoSources videoSources { get; set; } - } - - public class Preview - { - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public string url { get; set; } - } - - public class Source - { - public string source { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public int duration { get; set; } - } - - public class VideoSources - { - [JsonProperty("720")] public string _720 { get; set; } - - [JsonProperty("240")] public string _240 { get; set; } - } - - public class Dash - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - - public class Hls - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Manifest - { - public string? hls { get; set; } - public string? dash { get; set; } - } - - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } -} diff --git a/OF DL/Models/Post/SinglePostCollection.cs b/OF DL/Models/Post/SinglePostCollection.cs deleted file mode 100644 index d1992b8..0000000 --- a/OF DL/Models/Post/SinglePostCollection.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OF_DL.Models.Post; - -public class SinglePostCollection -{ - public List SinglePostMedia = new(); - public List SinglePostObjects = new(); - public Dictionary SinglePosts = new(); -} diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 287f50e..3f71c3e 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -7,13 +7,13 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.CLI; using OF_DL.Models; -using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Helpers; using ArchivedEntities = OF_DL.Models.Entities.Archived; using MessageEntities = OF_DL.Models.Entities.Messages; +using PostEntities = OF_DL.Models.Entities.Posts; using OF_DL.Services; using Serilog; using Spectre.Console; @@ -33,7 +33,7 @@ public class Program(IServiceProvider serviceProvider) IAuthService authService = serviceProvider.GetRequiredService(); bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; - // Show initial message + // Show the initial message AnsiConsole.MarkupLine("[yellow]Downloading dependencies. Please wait ...[/]"); // Show instructions based on the environment @@ -1334,7 +1334,7 @@ public class Program(IServiceProvider serviceProvider) "[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/]"); Log.Debug($"Calling DownloadFreePosts - {user.Key}"); - PostCollection posts = new(); + PostEntities.PostCollection posts = new(); await AnsiConsole.Status() .StartAsync("[red]Getting Posts[/]", @@ -2131,7 +2131,7 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Calling DownloadSinglePost - {post_id.ToString()}"); AnsiConsole.Markup("[red]Getting Post\n[/]"); - SinglePostCollection post = await apiService.GetPost($"/posts/{post_id.ToString()}", path); + PostEntities.SinglePostCollection post = await apiService.GetPost($"/posts/{post_id.ToString()}", path); if (post == null) { AnsiConsole.Markup("[red]Couldn't find post\n[/]"); @@ -2193,9 +2193,9 @@ public class Program(IServiceProvider serviceProvider) pssh); } - SinglePost.Medium mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.id == postKVP.Key); - SinglePost postInfo = - post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + PostEntities.Medium mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.Id == postKVP.Key); + PostEntities.SinglePost postInfo = + post.SinglePostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await downloadService.DownloadPostDRMVideo( policy, @@ -2212,17 +2212,17 @@ public class Program(IServiceProvider serviceProvider) string.Empty, postInfo, mediaInfo, - postInfo?.author, + postInfo?.Author, users); } else { try { - SinglePost.Medium? mediaInfo = - post.SinglePostMedia.FirstOrDefault(m => m?.id == postKVP.Key); - SinglePost? postInfo = - post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + PostEntities.Medium? mediaInfo = + post.SinglePostMedia.FirstOrDefault(m => m.Id == postKVP.Key); + PostEntities.SinglePost? postInfo = + post.SinglePostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await downloadService.DownloadPostMedia( postKVP.Value, @@ -2234,7 +2234,7 @@ public class Program(IServiceProvider serviceProvider) .PostFileNameFormat ?? string.Empty, postInfo, mediaInfo, - postInfo?.author, + postInfo?.Author, users); } catch diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 576d06a..e48dc3c 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -6,12 +6,12 @@ using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Stories; using OF_DL.Models.Streams; using OF_DL.Enumerations; using ArchivedDtos = OF_DL.Models.Dtos.Archived; +using PostDtos = OF_DL.Models.Dtos.Posts; using MessageDtos = OF_DL.Models.Dtos.Messages; using ListDtos = OF_DL.Models.Dtos.Lists; using HighlightDtos = OF_DL.Models.Dtos.Highlights; @@ -19,12 +19,14 @@ using ArchivedEntities = OF_DL.Models.Entities.Archived; using HighlightEntities = OF_DL.Models.Entities.Highlights; using ListEntities = OF_DL.Models.Entities.Lists; using MessageEntities = OF_DL.Models.Entities.Messages; +using PostEntities = OF_DL.Models.Entities.Posts; using OF_DL.Models.Mappers; using OF_DL.Widevine; using Serilog; using Spectre.Console; using static OF_DL.Utils.HttpUtil; using Constants = OF_DL.Helpers.Constants; +using SinglePostCollection = OF_DL.Models.Entities.Posts.SinglePostCollection; namespace OF_DL.Services; @@ -795,15 +797,15 @@ public class APIService(IAuthService authService, IConfigService configService, } - public async Task GetPosts(string endpoint, string folder, List paid_post_ids, + public async Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx) { Log.Debug($"Calling GetPosts - {endpoint}"); try { - Post posts = new(); - PostCollection postCollection = new(); + PostEntities.Post posts = new(); + PostEntities.PostCollection postCollection = new(); int post_limit = 50; Dictionary getParams = new() { @@ -838,31 +840,35 @@ public class APIService(IAuthService authService, IConfigService configService, downloadAsOf); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); - posts = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + PostDtos.PostDto? postsDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + posts = PostMapper.FromDto(postsDto); ctx.Status( - $"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]"); + $"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (posts != null && posts.hasMore) + if (posts != null && posts.HasMore) { UpdateGetParamsForDateSelection( downloadDateSelection, ref getParams, - posts.tailMarker); + posts.TailMarker); while (true) { - Post newposts = new(); + PostEntities.Post newposts = new(); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - newposts = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + PostDtos.PostDto? newPostsDto = + JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + newposts = PostMapper.FromDto(newPostsDto); - posts.list.AddRange(newposts.list); + posts.List.AddRange(newposts.List); ctx.Status( - $"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]"); + $"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (!newposts.hasMore) + if (!newposts.HasMore) { break; } @@ -870,33 +876,33 @@ public class APIService(IAuthService authService, IConfigService configService, UpdateGetParamsForDateSelection( downloadDateSelection, ref getParams, - newposts.tailMarker); + newposts.TailMarker); } } - foreach (Post.List post in posts.list) + foreach (PostEntities.ListItem post in posts.List) { if (configService.CurrentConfig.SkipAds) { - if (post.rawText != null && (post.rawText.Contains("#ad") || post.rawText.Contains("/trial/") || - post.rawText.Contains("#announcement"))) + if (post.RawText != null && (post.RawText.Contains("#ad") || post.RawText.Contains("/trial/") || + post.RawText.Contains("#announcement"))) { continue; } - if (post.text != null && (post.text.Contains("#ad") || post.text.Contains("/trial/") || - post.text.Contains("#announcement"))) + if (post.Text != null && (post.Text.Contains("#ad") || post.Text.Contains("/trial/") || + post.Text.Contains("#announcement"))) { continue; } } List postPreviewIds = new(); - if (post.preview != null && post.preview.Count > 0) + if (post.Preview != null && post.Preview.Count > 0) { - for (int i = 0; i < post.preview.Count; i++) + for (int i = 0; i < post.Preview.Count; i++) { - if (post.preview[i] is long previewId) + if (post.Preview[i] is long previewId) { if (!postPreviewIds.Contains(previewId)) { @@ -906,87 +912,87 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, post.id, post.rawText != null ? post.rawText : string.Empty, - post.price != null ? post.price : "0", post.price != null && post.isOpened ? true : false, - post.isArchived, post.postedAt); + await dbService.AddPost(folder, post.Id, post.RawText != null ? post.RawText : string.Empty, + post.Price != null ? post.Price : "0", post.Price != null && post.IsOpened ? true : false, + post.IsArchived, post.PostedAt); postCollection.PostObjects.Add(post); - if (post.media != null && post.media.Count > 0) + if (post.Media != null && post.Media.Count > 0) { - foreach (Post.Medium medium in post.media) + foreach (PostEntities.Medium medium in post.Media) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (medium.canView && medium.files?.drm == null) + if (medium.CanView && medium.Files?.Drm == null) { - bool has = paid_post_ids.Any(cus => cus.Equals(medium.id)); - if (medium.files!.full != null && !string.IsNullOrEmpty(medium.files!.full.url)) + bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); + if (medium.Files!.Full != null && !string.IsNullOrEmpty(medium.Files!.Full.Url)) { if (!has) { - if (!postCollection.Posts.ContainsKey(medium.id)) + if (!postCollection.Posts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, post.id, medium.files!.full.url, + await dbService.AddMedia(folder, medium.Id, post.Id, medium.Files!.Full.Url, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.id) ? true : false, false, null); - postCollection.Posts.Add(medium.id, medium.files!.full.url); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.Id) ? true : false, false, null); + postCollection.Posts.Add(medium.Id, medium.Files!.Full.Url); postCollection.PostMedia.Add(medium); } } } - else if (medium.files.preview != null && medium.files!.full == null) + else if (medium.Files.Preview != null && medium.Files!.Full == null) { if (!has) { - if (!postCollection.Posts.ContainsKey(medium.id)) + if (!postCollection.Posts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, post.id, medium.files.preview.url, + await dbService.AddMedia(folder, medium.Id, post.Id, medium.Files.Preview.Url, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.id) ? true : false, false, null); - postCollection.Posts.Add(medium.id, medium.files.preview.url); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.Id) ? true : false, false, null); + postCollection.Posts.Add(medium.Id, medium.Files.Preview.Url); postCollection.PostMedia.Add(medium); } } } } - else if (medium.canView && medium.files != null && medium.files.drm != null) + else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { - bool has = paid_post_ids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.files != null && medium.files.drm != null) + bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); + if (!has && medium.Files != null && medium.Files.Drm != null) { - if (!postCollection.Posts.ContainsKey(medium.id)) + if (!postCollection.Posts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, post.id, medium.files.drm.manifest.dash, + await dbService.AddMedia(folder, medium.Id, post.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.id) ? true : false, false, null); - postCollection.Posts.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{post.id}"); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.Id) ? true : false, false, null); + postCollection.Posts.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{post.Id}"); postCollection.PostMedia.Add(medium); } } @@ -1020,21 +1026,23 @@ public class APIService(IAuthService authService, IConfigService configService, try { - SinglePost singlePost = new(); + PostEntities.SinglePost singlePost = new(); SinglePostCollection singlePostCollection = new(); Dictionary getParams = new() { { "skip_users", "all" } }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); - singlePost = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + PostDtos.SinglePostDto? singlePostDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + singlePost = PostMapper.FromDto(singlePostDto); - if (singlePost != null) + if (singlePostDto != null) { List postPreviewIds = new(); - if (singlePost.preview != null && singlePost.preview.Count > 0) + if (singlePost.Preview != null && singlePost.Preview.Count > 0) { - for (int i = 0; i < singlePost.preview.Count; i++) + for (int i = 0; i < singlePost.Preview.Count; i++) { - if (singlePost.preview[i] is long previewId) + if (singlePost.Preview[i] is long previewId) { if (!postPreviewIds.Contains(previewId)) { @@ -1044,71 +1052,71 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, singlePost.id, singlePost.text != null ? singlePost.text : string.Empty, - singlePost.price != null ? singlePost.price : "0", - singlePost.price != null && singlePost.isOpened ? true : false, singlePost.isArchived, - singlePost.postedAt); + await dbService.AddPost(folder, singlePost.Id, singlePost.Text != null ? singlePost.Text : string.Empty, + singlePost.Price != null ? singlePost.Price : "0", + singlePost.Price != null && singlePost.IsOpened ? true : false, singlePost.IsArchived, + singlePost.PostedAt); singlePostCollection.SinglePostObjects.Add(singlePost); - if (singlePost.media != null && singlePost.media.Count > 0) + if (singlePost.Media != null && singlePost.Media.Count > 0) { - foreach (SinglePost.Medium medium in singlePost.media) + foreach (PostEntities.Medium medium in singlePost.Media) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (medium.canView && medium.files?.drm == null) + if (medium.CanView && medium.Files?.Drm == null) { switch (configService.CurrentConfig.DownloadVideoResolution) { case VideoResolution.source: - if (medium.files!.full != null && !string.IsNullOrEmpty(medium.files!.full.url)) + if (medium.Files!.Full != null && !string.IsNullOrEmpty(medium.Files!.Full.Url)) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, - medium.files!.full.url, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, medium.files!.full.url); + await dbService.AddMedia(folder, medium.Id, singlePost.Id, + medium.Files!.Full.Url, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.Id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, medium.Files!.Full.Url); singlePostCollection.SinglePostMedia.Add(medium); } } break; case VideoResolution._240: - if (medium.videoSources != null) + if (medium.VideoSources != null) { - if (!string.IsNullOrEmpty(medium.videoSources._240)) + if (!string.IsNullOrEmpty(medium.VideoSources._240)) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, - medium.videoSources._240, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, - medium.videoSources._240); + await dbService.AddMedia(folder, medium.Id, singlePost.Id, + medium.VideoSources._240, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.Id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, + medium.VideoSources._240); singlePostCollection.SinglePostMedia.Add(medium); } } @@ -1116,20 +1124,20 @@ public class APIService(IAuthService authService, IConfigService configService, break; case VideoResolution._720: - if (medium.videoSources != null) + if (medium.VideoSources != null) { - if (!string.IsNullOrEmpty(medium.videoSources._720)) + if (!string.IsNullOrEmpty(medium.VideoSources._720)) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, - medium.videoSources._720, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, - medium.videoSources._720); + await dbService.AddMedia(folder, medium.Id, singlePost.Id, + medium.VideoSources._720, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.Id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, + medium.VideoSources._720); singlePostCollection.SinglePostMedia.Add(medium); } } @@ -1138,35 +1146,35 @@ public class APIService(IAuthService authService, IConfigService configService, break; } } - else if (medium.canView && medium.files != null && medium.files.drm != null) + else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { - if (medium.files != null && medium.files.drm != null) + if (medium.Files != null && medium.Files.Drm != null) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, - medium.files.drm.manifest.dash, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{singlePost.id}"); + await dbService.AddMedia(folder, medium.Id, singlePost.Id, + medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.Id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{singlePost.Id}"); singlePostCollection.SinglePostMedia.Add(medium); } } } - else if (medium.files.preview != null && medium.files!.full == null) + else if (medium.Files.Preview != null && medium.Files!.Full == null) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files.preview.url, + await dbService.AddMedia(folder, medium.Id, singlePost.Id, medium.Files.Preview.Url, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, medium.files.preview.url); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.Id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, medium.Files.Preview.Url); singlePostCollection.SinglePostMedia.Add(medium); } } diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 577b4c2..b138344 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -4,16 +4,16 @@ using System.Xml.Linq; using FFmpeg.NET; using FFmpeg.NET.Events; using OF_DL.Models; -using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; -using OF_DL.Models.Entities.Common; using ArchivedEntities = OF_DL.Models.Entities.Archived; +using CommonEntities = OF_DL.Models.Entities.Common; +using MessageEntities = OF_DL.Models.Entities.Messages; +using PostEntities = OF_DL.Models.Entities.Posts; using OF_DL.Utils; using Serilog; using Serilog.Events; -using MessageEntities = OF_DL.Models.Entities.Messages; namespace OF_DL.Services; @@ -962,14 +962,15 @@ public class DownloadService( #region normal posts public async Task DownloadPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, - Post.Author? author, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, PostEntities.ListItem? postInfo, + PostEntities.Medium? postMedia, + CommonEntities.Author? author, Dictionary users) { string path; - if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && - postInfo?.postedAt is not null) + if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.Id is not null && + postInfo?.PostedAt is not null) { - path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + path = $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}"; } else { @@ -986,14 +987,15 @@ public class DownloadService( } public async Task DownloadPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, - SinglePost.Author? author, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, PostEntities.SinglePost? postInfo, + PostEntities.Medium? postMedia, + CommonEntities.Author? author, Dictionary users) { string path; - if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && - postInfo?.postedAt is not null) + if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.Id is not null && + postInfo?.PostedAt is not null) { - path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + path = $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}"; } else { @@ -1036,7 +1038,7 @@ public class DownloadService( public async Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) { string path; if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.Id is not null && @@ -1059,7 +1061,7 @@ public class DownloadService( public async Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) { string path; if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.Id is not null && @@ -1083,7 +1085,7 @@ public class DownloadService( public async Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? messageInfo, - ArchivedEntities.Medium? messageMedia, Author? author, + ArchivedEntities.Medium? messageMedia, CommonEntities.Author? author, Dictionary users) { string path = "/Archived/Posts/Free"; @@ -1132,7 +1134,7 @@ public class DownloadService( public async Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) { string path; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.Id is not null && @@ -1191,7 +1193,7 @@ public class DownloadService( public async Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) { try { @@ -1320,7 +1322,7 @@ public class DownloadService( public async Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) { try { @@ -1579,7 +1581,7 @@ public class DownloadService( public async Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users) + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) { try { @@ -1707,8 +1709,9 @@ public class DownloadService( public async Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, - Post.Author? author, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, PostEntities.ListItem? postInfo, + PostEntities.Medium? postMedia, + CommonEntities.Author? author, Dictionary users) { try { @@ -1716,10 +1719,10 @@ public class DownloadService( string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && - postInfo?.postedAt is not null) + if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.Id is not null && + postInfo?.PostedAt is not null) { - path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + path = $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}/Videos"; } else { @@ -1835,8 +1838,9 @@ public class DownloadService( public async Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, - SinglePost.Author author, Dictionary users) + IProgressReporter progressReporter, string filenameFormat, PostEntities.SinglePost postInfo, + PostEntities.Medium postMedia, + CommonEntities.Author author, Dictionary users) { try { @@ -1844,10 +1848,10 @@ public class DownloadService( string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && - postInfo?.postedAt is not null) + if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.Id is not null && + postInfo?.PostedAt is not null) { - path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + path = $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}/Videos"; } else { @@ -2224,7 +2228,7 @@ public class DownloadService( string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? postInfo, ArchivedEntities.Medium? postMedia, - Author? author, Dictionary users) + CommonEntities.Author? author, Dictionary users) { try { @@ -2896,7 +2900,8 @@ public class DownloadService( // Add these methods to DownloadService.cs before the #endregion line public async Task DownloadFreePosts(string username, long userId, string path, - Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostCollection posts, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PostEntities.PostCollection posts, IProgressReporter progressReporter) { Log.Debug($"Calling DownloadFreePosts - {username}"); @@ -2940,13 +2945,14 @@ public class DownloadService( $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", pssh); - Post.Medium mediaInfo = posts.PostMedia.FirstOrDefault(m => m.id == postKVP.Key); - Post.List postInfo = posts.PostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + PostEntities.Medium mediaInfo = posts.PostMedia.FirstOrDefault(m => m.Id == postKVP.Key); + PostEntities.ListItem postInfo = + posts.PostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadPostDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, lastModified, postKVP.Key, "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, postInfo, mediaInfo, postInfo?.author, users); + string.Empty, postInfo, mediaInfo, postInfo?.Author, users); } else { @@ -2955,11 +2961,12 @@ public class DownloadService( } else { - Post.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m?.id == postKVP.Key); - Post.List? postInfo = posts.PostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + PostEntities.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m?.Id == postKVP.Key); + PostEntities.ListItem? postInfo = + posts.PostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, postInfo, mediaInfo, postInfo?.author, users); + string.Empty, postInfo, mediaInfo, postInfo?.Author, users); } if (isNew) diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index d8e7e52..1c77129 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -1,11 +1,11 @@ using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; using ArchivedEntities = OF_DL.Models.Entities.Archived; using MessageEntities = OF_DL.Models.Entities.Messages; +using PostEntities = OF_DL.Models.Entities.Posts; using Spectre.Console; namespace OF_DL.Services; @@ -25,8 +25,10 @@ public interface IAPIService Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, StatusContext ctx); - Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx); - Task GetPost(string endpoint, string folder); + Task GetPosts(string endpoint, string folder, List paid_post_ids, + StatusContext ctx); + + Task GetPost(string endpoint, string folder); Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx); Task GetArchived(string endpoint, string folder, StatusContext ctx); Task GetMessages(string endpoint, string folder, StatusContext ctx); diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index 1f90736..b1ff68e 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -1,10 +1,10 @@ using OF_DL.Models; -using OF_DL.Models.Entities.Common; -using ArchivedEntities = OF_DL.Models.Entities.Archived; -using OF_DL.Models.Post; using OF_DL.Models.Purchased; using OF_DL.Models.Streams; +using ArchivedEntities = OF_DL.Models.Entities.Archived; +using CommonEntities = OF_DL.Models.Entities.Common; using MessageEntities = OF_DL.Models.Entities.Messages; +using PostEntities = OF_DL.Models.Entities.Posts; namespace OF_DL.Services; @@ -17,23 +17,25 @@ public interface IDownloadService Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? messageInfo, - ArchivedEntities.Medium? messageMedia, Author? author, + ArchivedEntities.Medium? messageMedia, CommonEntities.Author? author, Dictionary users); Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? postInfo, ArchivedEntities.Medium? postMedia, - Author? author, Dictionary users); + CommonEntities.Author? author, Dictionary users); Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, - string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, + string filenameFormat, PostEntities.SinglePost postInfo, PostEntities.Medium postMedia, + CommonEntities.Author author, Dictionary users); Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, - string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, + string? filenameFormat, PostEntities.ListItem? postInfo, PostEntities.Medium? postMedia, + CommonEntities.Author? author, Dictionary users); Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username); @@ -41,20 +43,22 @@ public interface IDownloadService Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.ListItem? messageInfo, MessageEntities.Medium? messageMedia, - FromUser? fromUser, + CommonEntities.FromUser? fromUser, Dictionary users); Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users); + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users); Task DownloadPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, - Post.Author? author, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, PostEntities.ListItem? postInfo, + PostEntities.Medium? postMedia, + CommonEntities.Author? author, Dictionary users); Task DownloadPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, - SinglePost.Author? author, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, PostEntities.SinglePost? postInfo, + PostEntities.Medium? postMedia, + CommonEntities.Author? author, Dictionary users); Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, @@ -63,7 +67,7 @@ public interface IDownloadService Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users); + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users); Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, @@ -74,7 +78,7 @@ public interface IDownloadService Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users); + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users); Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, @@ -102,11 +106,11 @@ public interface IDownloadService Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users); + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users); Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, FromUser? fromUser, Dictionary users); + MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users); Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); @@ -131,7 +135,7 @@ public interface IDownloadService IProgressReporter progressReporter); Task DownloadFreePosts(string username, long userId, string path, Dictionary users, - bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostCollection posts, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostEntities.PostCollection posts, IProgressReporter progressReporter); Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, From 6c60509398bb76ac8b6e8158c2782ca1dc8f27a7 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Sun, 8 Feb 2026 22:58:05 -0600 Subject: [PATCH 18/55] Refactor Purchased entities into DTOs and application entities with standardized naming conventions and default values --- OF DL/Models/Dtos/Purchased/FromUserDto.cs | 10 + OF DL/Models/Dtos/Purchased/ListItemDto.cs | 72 +++ OF DL/Models/Dtos/Purchased/PurchasedDto.cs | 10 + OF DL/Models/Entities/Common/FromUser.cs | 2 +- OF DL/Models/Entities/Purchased/ListItem.cs | 33 ++ .../Purchased/PaidMessageCollection.cs | 12 + .../Entities/Purchased/PaidPostCollection.cs | 12 + OF DL/Models/Entities/Purchased/Purchased.cs | 8 + .../Purchased/PurchasedTabCollection.cs | 7 +- .../Purchased/SinglePaidMessageCollection.cs | 17 + OF DL/Models/Mappers/MessagesMapper.cs | 2 +- OF DL/Models/Mappers/PurchasedMapper.cs | 56 +++ .../Models/Purchased/PaidMessageCollection.cs | 10 - OF DL/Models/Purchased/PaidPostCollection.cs | 10 - OF DL/Models/Purchased/Purchased.cs | 75 --- .../Purchased/SinglePaidMessageCollection.cs | 13 - OF DL/Program.cs | 42 +- OF DL/Services/APIService.cs | 457 +++++++++--------- OF DL/Services/DownloadService.cs | 69 +-- OF DL/Services/IAPIService.cs | 2 +- OF DL/Services/IDownloadService.cs | 23 +- 21 files changed, 545 insertions(+), 397 deletions(-) create mode 100644 OF DL/Models/Dtos/Purchased/FromUserDto.cs create mode 100644 OF DL/Models/Dtos/Purchased/ListItemDto.cs create mode 100644 OF DL/Models/Dtos/Purchased/PurchasedDto.cs create mode 100644 OF DL/Models/Entities/Purchased/ListItem.cs create mode 100644 OF DL/Models/Entities/Purchased/PaidMessageCollection.cs create mode 100644 OF DL/Models/Entities/Purchased/PaidPostCollection.cs create mode 100644 OF DL/Models/Entities/Purchased/Purchased.cs rename OF DL/Models/{ => Entities}/Purchased/PurchasedTabCollection.cs (69%) create mode 100644 OF DL/Models/Entities/Purchased/SinglePaidMessageCollection.cs create mode 100644 OF DL/Models/Mappers/PurchasedMapper.cs delete mode 100644 OF DL/Models/Purchased/PaidMessageCollection.cs delete mode 100644 OF DL/Models/Purchased/PaidPostCollection.cs delete mode 100644 OF DL/Models/Purchased/Purchased.cs delete mode 100644 OF DL/Models/Purchased/SinglePaidMessageCollection.cs diff --git a/OF DL/Models/Dtos/Purchased/FromUserDto.cs b/OF DL/Models/Dtos/Purchased/FromUserDto.cs new file mode 100644 index 0000000..fffd69f --- /dev/null +++ b/OF DL/Models/Dtos/Purchased/FromUserDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Purchased; + +public class FromUserDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("_view")] public string View { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Purchased/ListItemDto.cs b/OF DL/Models/Dtos/Purchased/ListItemDto.cs new file mode 100644 index 0000000..2104490 --- /dev/null +++ b/OF DL/Models/Dtos/Purchased/ListItemDto.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; +using MessageDtos = OF_DL.Models.Dtos.Messages; + +namespace OF_DL.Models.Dtos.Purchased; + +public class ListItemDto +{ + [JsonProperty("responseType")] public string ResponseType { get; set; } = ""; + + [JsonProperty("text")] public string Text { get; set; } = ""; + + [JsonProperty("giphyId")] public object GiphyId { get; set; } = new(); + + [JsonProperty("lockedText")] public bool? LockedText { get; set; } + + [JsonProperty("isFree")] public bool? IsFree { get; set; } + + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; } + + [JsonProperty("mediaCount")] public int? MediaCount { get; set; } + + [JsonProperty("media")] public List? Media { get; set; } + + [JsonProperty("previews")] public List? Previews { get; set; } + + [JsonProperty("preview")] public List? Preview { get; set; } + + [JsonProperty("isTip")] public bool? IsTip { get; set; } + + [JsonProperty("isReportedByMe")] public bool? IsReportedByMe { get; set; } + + [JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; } + + [JsonProperty("queueId")] public object QueueId { get; set; } = new(); + + [JsonProperty("fromUser")] public FromUserDto? FromUser { get; set; } + + [JsonProperty("author")] public AuthorDto? Author { get; set; } + + [JsonProperty("isFromQueue")] public bool? IsFromQueue { get; set; } + + [JsonProperty("canUnsendQueue")] public bool? CanUnsendQueue { get; set; } + + [JsonProperty("unsendSecondsQueue")] public int? UnsendSecondsQueue { get; set; } + + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("isOpened")] public bool IsOpened { get; set; } + + [JsonProperty("isNew")] public bool? IsNew { get; set; } + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } + + [JsonProperty("postedAt")] public DateTime? PostedAt { get; set; } + + [JsonProperty("changedAt")] public DateTime? ChangedAt { get; set; } + + [JsonProperty("cancelSeconds")] public int? CancelSeconds { get; set; } + + [JsonProperty("isLiked")] public bool? IsLiked { get; set; } + + [JsonProperty("canPurchase")] public bool? CanPurchase { get; set; } + + [JsonProperty("canReport")] public bool? CanReport { get; set; } + + [JsonProperty("isCanceled")] public bool? IsCanceled { get; set; } + + [JsonProperty("isArchived")] public bool? IsArchived { get; set; } +} diff --git a/OF DL/Models/Dtos/Purchased/PurchasedDto.cs b/OF DL/Models/Dtos/Purchased/PurchasedDto.cs new file mode 100644 index 0000000..7be06c7 --- /dev/null +++ b/OF DL/Models/Dtos/Purchased/PurchasedDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Purchased; + +public class PurchasedDto +{ + [JsonProperty("list")] public List List { get; set; } = []; + + [JsonProperty("hasMore")] public bool HasMore { get; set; } +} diff --git a/OF DL/Models/Entities/Common/FromUser.cs b/OF DL/Models/Entities/Common/FromUser.cs index bc09e0b..fe435b2 100644 --- a/OF DL/Models/Entities/Common/FromUser.cs +++ b/OF DL/Models/Entities/Common/FromUser.cs @@ -2,5 +2,5 @@ namespace OF_DL.Models.Entities.Common; public class FromUser { - public long? Id { get; set; } + public long Id { get; set; } } diff --git a/OF DL/Models/Entities/Purchased/ListItem.cs b/OF DL/Models/Entities/Purchased/ListItem.cs new file mode 100644 index 0000000..45561be --- /dev/null +++ b/OF DL/Models/Entities/Purchased/ListItem.cs @@ -0,0 +1,33 @@ +using OF_DL.Models.Entities.Common; +using MessageEntities = OF_DL.Models.Entities.Messages; + +namespace OF_DL.Models.Entities.Purchased; + +public class ListItem +{ + public string ResponseType { get; set; } = ""; + + public string? Text { get; set; } + + public string? Price { get; set; } + + public List? Media { get; set; } + + public List? Previews { get; set; } + + public List? Preview { get; set; } + + public FromUser FromUser { get; set; } = new(); + + public Author Author { get; set; } = new(); + + public long Id { get; set; } + + public bool IsOpened { get; set; } + + public DateTime? CreatedAt { get; set; } + + public DateTime? PostedAt { get; set; } + + public bool? IsArchived { get; set; } +} diff --git a/OF DL/Models/Entities/Purchased/PaidMessageCollection.cs b/OF DL/Models/Entities/Purchased/PaidMessageCollection.cs new file mode 100644 index 0000000..dd1435f --- /dev/null +++ b/OF DL/Models/Entities/Purchased/PaidMessageCollection.cs @@ -0,0 +1,12 @@ +using MessageEntities = OF_DL.Models.Entities.Messages; + +namespace OF_DL.Models.Entities.Purchased; + +public class PaidMessageCollection +{ + public List PaidMessageMedia { get; set; } = []; + + public List PaidMessageObjects { get; set; } = []; + + public Dictionary PaidMessages { get; set; } = new(); +} diff --git a/OF DL/Models/Entities/Purchased/PaidPostCollection.cs b/OF DL/Models/Entities/Purchased/PaidPostCollection.cs new file mode 100644 index 0000000..55b8e7b --- /dev/null +++ b/OF DL/Models/Entities/Purchased/PaidPostCollection.cs @@ -0,0 +1,12 @@ +using MessageEntities = OF_DL.Models.Entities.Messages; + +namespace OF_DL.Models.Entities.Purchased; + +public class PaidPostCollection +{ + public List PaidPostMedia { get; set; } = []; + + public List PaidPostObjects { get; set; } = []; + + public Dictionary PaidPosts { get; set; } = new(); +} diff --git a/OF DL/Models/Entities/Purchased/Purchased.cs b/OF DL/Models/Entities/Purchased/Purchased.cs new file mode 100644 index 0000000..fc80ec9 --- /dev/null +++ b/OF DL/Models/Entities/Purchased/Purchased.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Models.Entities.Purchased; + +public class Purchased +{ + public List List { get; set; } = []; + + public bool HasMore { get; set; } +} diff --git a/OF DL/Models/Purchased/PurchasedTabCollection.cs b/OF DL/Models/Entities/Purchased/PurchasedTabCollection.cs similarity index 69% rename from OF DL/Models/Purchased/PurchasedTabCollection.cs rename to OF DL/Models/Entities/Purchased/PurchasedTabCollection.cs index f41710c..e64eb86 100644 --- a/OF DL/Models/Purchased/PurchasedTabCollection.cs +++ b/OF DL/Models/Entities/Purchased/PurchasedTabCollection.cs @@ -1,9 +1,12 @@ -namespace OF_DL.Models.Purchased; +namespace OF_DL.Models.Entities.Purchased; public class PurchasedTabCollection { public long UserId { get; set; } - public string Username { get; set; } = string.Empty; + + public string Username { get; set; } = ""; + public PaidPostCollection PaidPosts { get; set; } = new(); + public PaidMessageCollection PaidMessages { get; set; } = new(); } diff --git a/OF DL/Models/Entities/Purchased/SinglePaidMessageCollection.cs b/OF DL/Models/Entities/Purchased/SinglePaidMessageCollection.cs new file mode 100644 index 0000000..66e0f5a --- /dev/null +++ b/OF DL/Models/Entities/Purchased/SinglePaidMessageCollection.cs @@ -0,0 +1,17 @@ +using MessageEntities = OF_DL.Models.Entities.Messages; + +namespace OF_DL.Models.Entities.Purchased; + +public class SinglePaidMessageCollection +{ + public List PreviewSingleMessageMedia { get; set; } = []; + + + public Dictionary PreviewSingleMessages { get; set; } = new(); + + public List SingleMessageMedia { get; set; } = []; + + public List SingleMessageObjects { get; set; } = []; + + public Dictionary SingleMessages { get; set; } = new(); +} diff --git a/OF DL/Models/Mappers/MessagesMapper.cs b/OF DL/Models/Mappers/MessagesMapper.cs index 3f3f750..52b9a8e 100644 --- a/OF DL/Models/Mappers/MessagesMapper.cs +++ b/OF DL/Models/Mappers/MessagesMapper.cs @@ -64,7 +64,7 @@ public static class MessagesMapper media?.Select(MapMedium).ToList(); private static FromUser? MapFromUser(FromUserDto? dto) => - dto == null ? null : new FromUser { Id = dto.Id }; + dto?.Id == null ? null : new FromUser { Id = (long)dto.Id }; private static Files? MapFiles(FilesDto? dto) => dto == null ? null : new Files { Full = MapFull(dto.Full), Drm = MapDrm(dto.Drm) }; diff --git a/OF DL/Models/Mappers/PurchasedMapper.cs b/OF DL/Models/Mappers/PurchasedMapper.cs new file mode 100644 index 0000000..43b5830 --- /dev/null +++ b/OF DL/Models/Mappers/PurchasedMapper.cs @@ -0,0 +1,56 @@ +using OF_DL.Models.Dtos.Purchased; +using OF_DL.Models.Dtos.Common; +using MessageDtos = OF_DL.Models.Dtos.Messages; +using CommonEntities = OF_DL.Models.Entities.Common; +using MessageEntities = OF_DL.Models.Entities.Messages; +using PurchasedEntities = OF_DL.Models.Entities.Purchased; + + +namespace OF_DL.Models.Mappers; + +public static class PurchasedMapper +{ + public static PurchasedEntities.Purchased FromDto(PurchasedDto? dto) + { + PurchasedEntities.Purchased mapped = new() { HasMore = dto?.HasMore ?? false }; + + if (dto?.List == null) + { + return mapped; + } + + foreach (ListItemDto entry in dto.List) + { + mapped.List.Add(MapList(entry)); + } + + return mapped; + } + + private static PurchasedEntities.ListItem MapList(ListItemDto dto) => + new() + { + ResponseType = dto.ResponseType, + Text = dto.Text, + Price = dto.Price, + IsOpened = dto.IsOpened, + IsArchived = dto.IsArchived, + CreatedAt = dto.CreatedAt, + PostedAt = dto.PostedAt, + Id = dto.Id, + Media = MapMedia(dto.Media), + Previews = dto.Previews, + Preview = dto.Preview, + FromUser = MapFromUser(dto.FromUser), + Author = MapAuthor(dto.Author) + }; + + private static CommonEntities.FromUser MapFromUser(FromUserDto? dto) => + dto == null ? new CommonEntities.FromUser() : new CommonEntities.FromUser { Id = dto.Id }; + + private static CommonEntities.Author MapAuthor(AuthorDto? dto) => + dto == null ? new CommonEntities.Author() : new CommonEntities.Author { Id = dto.Id }; + + private static List? MapMedia(List? media) => + media?.Select(MessagesMapper.MapMedium).ToList(); +} diff --git a/OF DL/Models/Purchased/PaidMessageCollection.cs b/OF DL/Models/Purchased/PaidMessageCollection.cs deleted file mode 100644 index 26bf6ec..0000000 --- a/OF DL/Models/Purchased/PaidMessageCollection.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MessageEntities = OF_DL.Models.Entities.Messages; - -namespace OF_DL.Models.Purchased; - -public class PaidMessageCollection -{ - public List PaidMessageMedia = new(); - public List PaidMessageObjects = new(); - public Dictionary PaidMessages = new(); -} diff --git a/OF DL/Models/Purchased/PaidPostCollection.cs b/OF DL/Models/Purchased/PaidPostCollection.cs deleted file mode 100644 index 24bce7d..0000000 --- a/OF DL/Models/Purchased/PaidPostCollection.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MessageEntities = OF_DL.Models.Entities.Messages; - -namespace OF_DL.Models.Purchased; - -public class PaidPostCollection -{ - public List PaidPostMedia = new(); - public List PaidPostObjects = new(); - public Dictionary PaidPosts = new(); -} diff --git a/OF DL/Models/Purchased/Purchased.cs b/OF DL/Models/Purchased/Purchased.cs deleted file mode 100644 index cae8e66..0000000 --- a/OF DL/Models/Purchased/Purchased.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Newtonsoft.Json; -using OF_DL.Models.Dtos.Messages; - -namespace OF_DL.Models.Purchased; - -public class Purchased -{ - public List list { get; set; } - public bool hasMore { get; set; } - - public class FromUser - { - public long id { get; set; } - public string _view { get; set; } - } - - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } - - public class Hls - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - - public class List - { - public string responseType { get; set; } - public string text { get; set; } - public object giphyId { get; set; } - public bool? lockedText { get; set; } - public bool? isFree { get; set; } - public string? price { get; set; } - public bool? isMediaReady { get; set; } - public int? mediaCount { get; set; } - public List media { get; set; } - public List previews { get; set; } - public List preview { get; set; } - public bool? isTip { get; set; } - public bool? isReportedByMe { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public object queueId { get; set; } - public FromUser fromUser { get; set; } - public Author author { get; set; } - public bool? isFromQueue { get; set; } - public bool? canUnsendQueue { get; set; } - public int? unsendSecondsQueue { get; set; } - public long id { get; set; } - public bool isOpened { get; set; } - public bool? isNew { get; set; } - public DateTime? createdAt { get; set; } - public DateTime? postedAt { get; set; } - public DateTime? changedAt { get; set; } - public int? cancelSeconds { get; set; } - public bool? isLiked { get; set; } - public bool? canPurchase { get; set; } - public bool? canReport { get; set; } - public bool? isCanceled { get; set; } - public bool? isArchived { get; set; } - } - - public class Manifest - { - public string hls { get; set; } - public string dash { get; set; } - } -} diff --git a/OF DL/Models/Purchased/SinglePaidMessageCollection.cs b/OF DL/Models/Purchased/SinglePaidMessageCollection.cs deleted file mode 100644 index 1f2e33f..0000000 --- a/OF DL/Models/Purchased/SinglePaidMessageCollection.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MessageEntities = OF_DL.Models.Entities.Messages; - -namespace OF_DL.Models.Purchased; - -public class SinglePaidMessageCollection -{ - public List PreviewSingleMessageMedia = new(); - - public Dictionary PreviewSingleMessages = new(); - public List SingleMessageMedia = new(); - public List SingleMessageObjects = new(); - public Dictionary SingleMessages = new(); -} diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 3f71c3e..7cb09cc 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -7,13 +7,13 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.CLI; using OF_DL.Models; -using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Helpers; using ArchivedEntities = OF_DL.Models.Entities.Archived; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; +using PurchasedEntities = OF_DL.Models.Entities.Purchased; using OF_DL.Services; using Serilog; using Spectre.Console; @@ -801,9 +801,9 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Download path: {p}"); - List purchasedTabCollections = + List purchasedTabCollections = await apiService.GetPurchasedTab("/posts/paid/all", p, users); - foreach (PurchasedTabCollection purchasedTabCollection in purchasedTabCollections) + foreach (PurchasedEntities.PurchasedTabCollection purchasedTabCollection in purchasedTabCollections) { AnsiConsole.Markup($"[red]\nScraping Data for {purchasedTabCollection.Username}\n[/]"); string path = ""; @@ -1056,7 +1056,7 @@ public class Program(IServiceProvider serviceProvider) IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - PaidMessageCollection paidMessageCollection = new(); + PurchasedEntities.PaidMessageCollection paidMessageCollection = new(); await AnsiConsole.Status() .StartAsync("[red]Getting Paid Messages[/]", @@ -1391,7 +1391,7 @@ public class Program(IServiceProvider serviceProvider) AnsiConsole.Markup("[red]Getting Paid Posts\n[/]"); Log.Debug($"Calling DownloadPaidPosts - {user.Key}"); - PaidPostCollection purchasedPosts = new(); + PurchasedEntities.PaidPostCollection purchasedPosts = new(); await AnsiConsole.Status() .StartAsync("[red]Getting Paid Posts[/]", @@ -1442,7 +1442,8 @@ public class Program(IServiceProvider serviceProvider) return result.TotalCount; } - private async Task DownloadPaidPostsPurchasedTab(string username, PaidPostCollection purchasedPosts, + private async Task DownloadPaidPostsPurchasedTab(string username, + PurchasedEntities.PaidPostCollection purchasedPosts, KeyValuePair user, int paidPostCount, string path, Dictionary users) { IDBService dbService = serviceProvider.GetRequiredService(); @@ -1525,9 +1526,9 @@ public class Program(IServiceProvider serviceProvider) MessageEntities.Medium? mediaInfo = purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); - Purchased.List? postInfo = mediaInfo != null + PurchasedEntities.ListItem? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => - p?.media?.Any(m => m.Id == purchasedPostKVP.Key) == true) + p?.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true) : null; isNew = await downloadService.DownloadPurchasedPostDRMVideo( @@ -1545,7 +1546,7 @@ public class Program(IServiceProvider serviceProvider) .PaidPostFileNameFormat ?? string.Empty, postInfo, mediaInfo, - postInfo?.fromUser, + postInfo?.FromUser, users); if (isNew) { @@ -1560,9 +1561,9 @@ public class Program(IServiceProvider serviceProvider) { MessageEntities.Medium? mediaInfo = purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); - Purchased.List? postInfo = mediaInfo != null + PurchasedEntities.ListItem? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => - p?.media?.Any(m => m.Id == purchasedPostKVP.Key) == true) + p?.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true) : null; isNew = await downloadService.DownloadPurchasedPostMedia( @@ -1575,7 +1576,7 @@ public class Program(IServiceProvider serviceProvider) .PaidPostFileNameFormat ?? string.Empty, postInfo, mediaInfo, - postInfo?.fromUser, + postInfo?.FromUser, users); if (isNew) { @@ -1597,7 +1598,8 @@ public class Program(IServiceProvider serviceProvider) } private async Task DownloadPaidMessagesPurchasedTab(string username, - PaidMessageCollection paidMessageCollection, KeyValuePair user, int paidMessagesCount, + PurchasedEntities.PaidMessageCollection paidMessageCollection, KeyValuePair user, + int paidMessagesCount, string path, Dictionary users) { IDBService dbService = serviceProvider.GetRequiredService(); @@ -1675,9 +1677,9 @@ public class Program(IServiceProvider serviceProvider) MessageEntities.Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKVP.Key); - Purchased.List? messageInfo = + PurchasedEntities.ListItem? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.media?.Any(m => m.Id == paidMessageKVP.Key) == true); + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); isNew = await downloadService.DownloadPurchasedMessageDRMVideo( policy, @@ -1694,7 +1696,7 @@ public class Program(IServiceProvider serviceProvider) .PaidMessageFileNameFormat ?? string.Empty, messageInfo, mediaInfo, - messageInfo?.fromUser, + messageInfo?.FromUser, users); if (isNew) @@ -1711,9 +1713,9 @@ public class Program(IServiceProvider serviceProvider) { MessageEntities.Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKVP.Key); - Purchased.List messageInfo = + PurchasedEntities.ListItem messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.media?.Any(m => m.Id == paidMessageKVP.Key) == true); + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); isNew = await downloadService.DownloadPurchasedMedia( paidMessageKVP.Value, @@ -1725,7 +1727,7 @@ public class Program(IServiceProvider serviceProvider) .PaidMessageFileNameFormat ?? string.Empty, messageInfo, mediaInfo, - messageInfo?.fromUser, + messageInfo?.FromUser, users); if (isNew) { @@ -1827,7 +1829,7 @@ public class Program(IServiceProvider serviceProvider) AnsiConsole.Markup("[red]Getting Paid Message\n[/]"); - SinglePaidMessageCollection singlePaidMessageCollection = + PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection = await apiService.GetPaidMessage($"/messages/{message_id.ToString()}", path); int oldPreviewPaidMessagesCount = 0; int newPreviewPaidMessagesCount = 0; diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index e48dc3c..069929c 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -6,13 +6,13 @@ using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Purchased; using OF_DL.Models.Stories; using OF_DL.Models.Streams; using OF_DL.Enumerations; using ArchivedDtos = OF_DL.Models.Dtos.Archived; using PostDtos = OF_DL.Models.Dtos.Posts; using MessageDtos = OF_DL.Models.Dtos.Messages; +using PurchasedDtos = OF_DL.Models.Dtos.Purchased; using ListDtos = OF_DL.Models.Dtos.Lists; using HighlightDtos = OF_DL.Models.Dtos.Highlights; using ArchivedEntities = OF_DL.Models.Entities.Archived; @@ -20,6 +20,7 @@ using HighlightEntities = OF_DL.Models.Entities.Highlights; using ListEntities = OF_DL.Models.Entities.Lists; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; +using PurchasedEntities = OF_DL.Models.Entities.Purchased; using OF_DL.Models.Mappers; using OF_DL.Widevine; using Serilog; @@ -593,15 +594,16 @@ public class APIService(IAuthService authService, IConfigService configService, } - public async Task GetPaidPosts(string endpoint, string folder, string username, + public async Task GetPaidPosts(string endpoint, string folder, + string username, List paid_post_ids, StatusContext ctx) { Log.Debug($"Calling GetPaidPosts - {username}"); try { - Purchased paidPosts = new(); - PaidPostCollection paidPostCollection = new(); + PurchasedEntities.Purchased paidPosts = new(); + PurchasedEntities.PaidPostCollection paidPostCollection = new(); int post_limit = 50; Dictionary getParams = new() { @@ -613,25 +615,29 @@ public class APIService(IAuthService authService, IConfigService configService, }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - paidPosts = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.list.Count}[/]"); + PurchasedDtos.PurchasedDto? paidPostsDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + paidPosts = PurchasedMapper.FromDto(paidPostsDto); + ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (paidPosts != null && paidPosts.hasMore) + if (paidPosts != null && paidPosts.HasMore) { - getParams["offset"] = paidPosts.list.Count.ToString(); + getParams["offset"] = paidPosts.List.Count.ToString(); while (true) { - Purchased newPaidPosts = new(); + PurchasedEntities.Purchased newPaidPosts = new(); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - newPaidPosts = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + PurchasedDtos.PurchasedDto? newPaidPostsDto = + JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + newPaidPosts = PurchasedMapper.FromDto(newPaidPostsDto); - paidPosts.list.AddRange(newPaidPosts.list); - ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.list.Count}[/]"); + paidPosts.List.AddRange(newPaidPosts.List); + ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (!newPaidPosts.hasMore) + if (!newPaidPosts.HasMore) { break; } @@ -640,16 +646,16 @@ public class APIService(IAuthService authService, IConfigService configService, } } - foreach (Purchased.List purchase in paidPosts.list) + foreach (PurchasedEntities.ListItem purchase in paidPosts.List) { - if (purchase.responseType == "post" && purchase.media != null && purchase.media.Count > 0) + if (purchase.ResponseType == "post" && purchase.Media != null && purchase.Media.Count > 0) { List previewids = new(); - if (purchase.previews != null) + if (purchase.Previews != null) { - for (int i = 0; i < purchase.previews.Count; i++) + for (int i = 0; i < purchase.Previews.Count; i++) { - if (purchase.previews[i] is long previewId) + if (purchase.Previews[i] is long previewId) { if (!previewids.Contains(previewId)) { @@ -658,11 +664,11 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - else if (purchase.preview != null) + else if (purchase.Preview != null) { - for (int i = 0; i < purchase.preview.Count; i++) + for (int i = 0; i < purchase.Preview.Count; i++) { - if (purchase.preview[i] is long previewId) + if (purchase.Preview[i] is long previewId) { if (!previewids.Contains(previewId)) { @@ -672,15 +678,14 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, - purchase.price != null ? purchase.price : "0", - purchase.price != null && purchase.isOpened ? true : false, - purchase.isArchived.HasValue ? purchase.isArchived.Value : false, - purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); + await dbService.AddPost(folder, purchase.Id, purchase.Text != null ? purchase.Text : string.Empty, + purchase.Price != null ? purchase.Price : "0", + purchase.Price != null && purchase.IsOpened ? true : false, + purchase.IsArchived.HasValue ? purchase.IsArchived.Value : false, + purchase.CreatedAt != null ? purchase.CreatedAt.Value : purchase.PostedAt.Value); paidPostCollection.PaidPostObjects.Add(purchase); - foreach (MessageDtos.MediumDto medium in purchase.media) + foreach (MessageEntities.Medium medium in purchase.Media) { - MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); if (!previewids.Contains(medium.Id)) { paid_post_ids.Add(medium.Id); @@ -714,29 +719,29 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, purchase.id, medium.Files.Full.Url, + await dbService.AddMedia(folder, medium.Id, purchase.Id, medium.Files.Full.Url, null, null, null, "Posts", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : medium.Type == "audio" ? "Audios" : null, previewids.Contains(medium.Id), false, null); paidPostCollection.PaidPosts.Add(medium.Id, medium.Files.Full.Url); - paidPostCollection.PaidPostMedia.Add(mappedMedium); + paidPostCollection.PaidPostMedia.Add(medium); } } else if (!has && medium.CanView && medium.Files != null && medium.Files.Drm != null) { if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, purchase.id, + await dbService.AddMedia(folder, medium.Id, purchase.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : medium.Type == "audio" ? "Audios" : null, previewids.Contains(medium.Id), false, null); paidPostCollection.PaidPosts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); - paidPostCollection.PaidPostMedia.Add(mappedMedium); + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + paidPostCollection.PaidPostMedia.Add(medium); } } } @@ -747,29 +752,29 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, purchase.id, medium.Files.Full.Url, + await dbService.AddMedia(folder, medium.Id, purchase.Id, medium.Files.Full.Url, null, null, null, "Posts", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : medium.Type == "audio" ? "Audios" : null, previewids.Contains(medium.Id), false, null); paidPostCollection.PaidPosts.Add(medium.Id, medium.Files.Full.Url); - paidPostCollection.PaidPostMedia.Add(mappedMedium); + paidPostCollection.PaidPostMedia.Add(medium); } } else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, purchase.id, + await dbService.AddMedia(folder, medium.Id, purchase.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : medium.Type == "audio" ? "Audios" : null, previewids.Contains(medium.Id), false, null); paidPostCollection.PaidPosts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); - paidPostCollection.PaidPostMedia.Add(mappedMedium); + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + paidPostCollection.PaidPostMedia.Add(medium); } } } @@ -1798,14 +1803,14 @@ public class APIService(IAuthService authService, IConfigService configService, return null; } - public async Task GetPaidMessage(string endpoint, string folder) + public async Task GetPaidMessage(string endpoint, string folder) { Log.Debug($"Calling GetPaidMessage - {endpoint}"); try { MessageEntities.SingleMessage message = new(); - SinglePaidMessageCollection singlePaidMessageCollection = new(); + PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection = new(); int post_limit = 50; Dictionary getParams = new() { { "limit", post_limit.ToString() }, { "order", "desc" } }; @@ -2007,15 +2012,16 @@ public class APIService(IAuthService authService, IConfigService configService, } - public async Task GetPaidMessages(string endpoint, string folder, string username, + public async Task GetPaidMessages(string endpoint, string folder, + string username, StatusContext ctx) { Log.Debug($"Calling GetPaidMessages - {username}"); try { - Purchased paidMessages = new(); - PaidMessageCollection paidMessageCollection = new(); + PurchasedEntities.Purchased paidMessages = new(); + PurchasedEntities.PaidMessageCollection paidMessageCollection = new(); int post_limit = 50; Dictionary getParams = new() { @@ -2027,17 +2033,19 @@ public class APIService(IAuthService authService, IConfigService configService, }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - paidMessages = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.list.Count}[/]"); + PurchasedDtos.PurchasedDto? paidMessagesDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + paidMessages = PurchasedMapper.FromDto(paidMessagesDto); + ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (paidMessages != null && paidMessages.hasMore) + if (paidMessages != null && paidMessages.HasMore) { - getParams["offset"] = paidMessages.list.Count.ToString(); + getParams["offset"] = paidMessages.List.Count.ToString(); while (true) { string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); - Purchased newpaidMessages = new(); + PurchasedEntities.Purchased newpaidMessages = new(); Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); @@ -2053,14 +2061,17 @@ public class APIService(IAuthService authService, IConfigService configService, { loopresponse.EnsureSuccessStatusCode(); string loopbody = await loopresponse.Content.ReadAsStringAsync(); - newpaidMessages = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + PurchasedDtos.PurchasedDto? newPaidMessagesDto = + JsonConvert.DeserializeObject(loopbody, + m_JsonSerializerSettings); + newpaidMessages = PurchasedMapper.FromDto(newPaidMessagesDto); } - paidMessages.list.AddRange(newpaidMessages.list); - ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.list.Count}[/]"); + paidMessages.List.AddRange(newpaidMessages.List); + ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (!newpaidMessages.hasMore) + if (!newpaidMessages.HasMore) { break; } @@ -2069,38 +2080,39 @@ public class APIService(IAuthService authService, IConfigService configService, } } - if (paidMessages.list != null && paidMessages.list.Count > 0) + if (paidMessages.List != null && paidMessages.List.Count > 0) { - foreach (Purchased.List purchase in paidMessages.list.Where(p => p.responseType == "message") - .OrderByDescending(p => p.postedAt ?? p.createdAt)) + foreach (PurchasedEntities.ListItem purchase in paidMessages.List + .Where(p => p.ResponseType == "message") + .OrderByDescending(p => p.PostedAt ?? p.CreatedAt)) { if (!configService.CurrentConfig.IgnoreOwnMessages || - purchase.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + purchase.FromUser.Id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - if (purchase.postedAt != null) + if (purchase.PostedAt != null) { - await dbService.AddMessage(folder, purchase.id, - purchase.text != null ? purchase.text : string.Empty, - purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, - purchase.fromUser.id); + await dbService.AddMessage(folder, purchase.Id, + purchase.Text != null ? purchase.Text : string.Empty, + purchase.Price != null ? purchase.Price : "0", true, false, purchase.PostedAt.Value, + purchase.FromUser.Id); } else { - await dbService.AddMessage(folder, purchase.id, - purchase.text != null ? purchase.text : string.Empty, - purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, - purchase.fromUser.id); + await dbService.AddMessage(folder, purchase.Id, + purchase.Text != null ? purchase.Text : string.Empty, + purchase.Price != null ? purchase.Price : "0", true, false, purchase.CreatedAt.Value, + purchase.FromUser.Id); } paidMessageCollection.PaidMessageObjects.Add(purchase); - if (purchase.media != null && purchase.media.Count > 0) + if (purchase.Media != null && purchase.Media.Count > 0) { List previewids = new(); - if (purchase.previews != null) + if (purchase.Previews != null) { - for (int i = 0; i < purchase.previews.Count; i++) + for (int i = 0; i < purchase.Previews.Count; i++) { - if (purchase.previews[i] is long previewId) + if (purchase.Previews[i] is long previewId) { if (!previewids.Contains(previewId)) { @@ -2109,11 +2121,11 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - else if (purchase.preview != null) + else if (purchase.Preview != null) { - for (int i = 0; i < purchase.preview.Count; i++) + for (int i = 0; i < purchase.Preview.Count; i++) { - if (purchase.preview[i] is long previewId) + if (purchase.Preview[i] is long previewId) { if (!previewids.Contains(previewId)) { @@ -2123,11 +2135,10 @@ public class APIService(IAuthService authService, IConfigService configService, } } - foreach (MessageDtos.MediumDto medium in purchase.media) + foreach (MessageEntities.Medium medium in purchase.Media) { if (previewids.Count > 0) { - MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); bool has = previewids.Any(cus => cus.Equals(medium.Id)); if (!has && medium.CanView && medium.Files != null && medium.Files.Full != null && !string.IsNullOrEmpty(medium.Files.Full.Url)) @@ -2154,14 +2165,14 @@ public class APIService(IAuthService authService, IConfigService configService, if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, purchase.id, + await dbService.AddMedia(folder, medium.Id, purchase.Id, medium.Files.Full.Url, null, null, null, "Messages", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : medium.Type == "audio" ? "Audios" : null, previewids.Contains(medium.Id), false, null); paidMessageCollection.PaidMessages.Add(medium.Id, medium.Files.Full.Url); - paidMessageCollection.PaidMessageMedia.Add(mappedMedium); + paidMessageCollection.PaidMessageMedia.Add(medium); } } else if (!has && medium.CanView && medium.Files != null && medium.Files.Drm != null) @@ -2188,21 +2199,20 @@ public class APIService(IAuthService authService, IConfigService configService, if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, purchase.id, + await dbService.AddMedia(folder, medium.Id, purchase.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : medium.Type == "audio" ? "Audios" : null, previewids.Contains(medium.Id), false, null); paidMessageCollection.PaidMessages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); - paidMessageCollection.PaidMessageMedia.Add(mappedMedium); + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + paidMessageCollection.PaidMessageMedia.Add(medium); } } } else { - MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); if (medium.CanView && medium.Files != null && medium.Files.Full != null && !string.IsNullOrEmpty(medium.Files.Full.Url)) { @@ -2228,14 +2238,14 @@ public class APIService(IAuthService authService, IConfigService configService, if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, purchase.id, + await dbService.AddMedia(folder, medium.Id, purchase.Id, medium.Files.Full.Url, null, null, null, "Messages", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : medium.Type == "audio" ? "Audios" : null, previewids.Contains(medium.Id), false, null); paidMessageCollection.PaidMessages.Add(medium.Id, medium.Files.Full.Url); - paidMessageCollection.PaidMessageMedia.Add(mappedMedium); + paidMessageCollection.PaidMessageMedia.Add(medium); } } else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) @@ -2262,15 +2272,15 @@ public class APIService(IAuthService authService, IConfigService configService, if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, purchase.id, + await dbService.AddMedia(folder, medium.Id, purchase.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : medium.Type == "audio" ? "Audios" : null, previewids.Contains(medium.Id), false, null); paidMessageCollection.PaidMessages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); - paidMessageCollection.PaidMessageMedia.Add(mappedMedium); + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + paidMessageCollection.PaidMessageMedia.Add(medium); } } } @@ -2306,7 +2316,7 @@ public class APIService(IAuthService authService, IConfigService configService, try { Dictionary purchasedTabUsers = new(); - Purchased purchased = new(); + PurchasedEntities.Purchased purchased = new(); int post_limit = 50; Dictionary getParams = new() { @@ -2317,14 +2327,16 @@ public class APIService(IAuthService authService, IConfigService configService, }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - purchased = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - if (purchased != null && purchased.hasMore) + PurchasedDtos.PurchasedDto? purchasedDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + purchased = PurchasedMapper.FromDto(purchasedDto); + if (purchased != null && purchased.HasMore) { - getParams["offset"] = purchased.list.Count.ToString(); + getParams["offset"] = purchased.List.Count.ToString(); while (true) { string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); - Purchased newPurchased = new(); + PurchasedEntities.Purchased newPurchased = new(); Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); @@ -2340,11 +2352,14 @@ public class APIService(IAuthService authService, IConfigService configService, { loopresponse.EnsureSuccessStatusCode(); string loopbody = await loopresponse.Content.ReadAsStringAsync(); - newPurchased = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + PurchasedDtos.PurchasedDto? newPurchasedDto = + JsonConvert.DeserializeObject(loopbody, + m_JsonSerializerSettings); + newPurchased = PurchasedMapper.FromDto(newPurchasedDto); } - purchased.list.AddRange(newPurchased.list); - if (!newPurchased.hasMore) + purchased.List.AddRange(newPurchased.List); + if (!newPurchased.HasMore) { break; } @@ -2353,124 +2368,125 @@ public class APIService(IAuthService authService, IConfigService configService, } } - if (purchased.list != null && purchased.list.Count > 0) + if (purchased.List != null && purchased.List.Count > 0) { - foreach (Purchased.List purchase in purchased.list.OrderByDescending(p => p.postedAt ?? p.createdAt)) + foreach (PurchasedEntities.ListItem purchase in + purchased.List.OrderByDescending(p => p.PostedAt ?? p.CreatedAt)) { - if (purchase.fromUser != null) + if (purchase.FromUser != null) { - if (users.Values.Contains(purchase.fromUser.id)) + if (users.Values.Contains(purchase.FromUser.Id)) { - if (!string.IsNullOrEmpty(users.FirstOrDefault(x => x.Value == purchase.fromUser.id).Key)) + if (!string.IsNullOrEmpty(users.FirstOrDefault(x => x.Value == purchase.FromUser.Id).Key)) { if (!purchasedTabUsers.ContainsKey(users - .FirstOrDefault(x => x.Value == purchase.fromUser.id).Key)) + .FirstOrDefault(x => x.Value == purchase.FromUser.Id).Key)) { purchasedTabUsers.Add( - users.FirstOrDefault(x => x.Value == purchase.fromUser.id).Key, - purchase.fromUser.id); + users.FirstOrDefault(x => x.Value == purchase.FromUser.Id).Key, + purchase.FromUser.Id); } } else { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) + if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.FromUser.Id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", - purchase.fromUser.id); + purchasedTabUsers.Add($"Deleted User - {purchase.FromUser.Id}", + purchase.FromUser.Id); } } } else { - JObject user = await GetUserInfoById($"/users/list?x[]={purchase.fromUser.id}"); + JObject user = await GetUserInfoById($"/users/list?x[]={purchase.FromUser.Id}"); if (user is null) { if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) + if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.FromUser.Id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", - purchase.fromUser.id); + purchasedTabUsers.Add($"Deleted User - {purchase.FromUser.Id}", + purchase.FromUser.Id); } } - Log.Debug("Content creator not longer exists - {0}", purchase.fromUser.id); + Log.Debug("Content creator not longer exists - {0}", purchase.FromUser.Id); } - else if (!string.IsNullOrEmpty(user[purchase.fromUser.id.ToString()]["username"] + else if (!string.IsNullOrEmpty(user[purchase.FromUser.Id.ToString()]["username"] .ToString())) { - if (!purchasedTabUsers.ContainsKey(user[purchase.fromUser.id.ToString()]["username"] + if (!purchasedTabUsers.ContainsKey(user[purchase.FromUser.Id.ToString()]["username"] .ToString())) { - purchasedTabUsers.Add(user[purchase.fromUser.id.ToString()]["username"].ToString(), - purchase.fromUser.id); + purchasedTabUsers.Add(user[purchase.FromUser.Id.ToString()]["username"].ToString(), + purchase.FromUser.Id); } } else { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) + if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.FromUser.Id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", - purchase.fromUser.id); + purchasedTabUsers.Add($"Deleted User - {purchase.FromUser.Id}", + purchase.FromUser.Id); } } } } - else if (purchase.author != null) + else if (purchase.Author != null) { - if (users.Values.Contains(purchase.author.id)) + if (users.Values.Contains(purchase.Author.Id)) { - if (!string.IsNullOrEmpty(users.FirstOrDefault(x => x.Value == purchase.author.id).Key)) + if (!string.IsNullOrEmpty(users.FirstOrDefault(x => x.Value == purchase.Author.Id).Key)) { if (!purchasedTabUsers.ContainsKey(users - .FirstOrDefault(x => x.Value == purchase.author.id).Key) && - users.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.author.id).Key)) + .FirstOrDefault(x => x.Value == purchase.Author.Id).Key) && + users.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.Author.Id).Key)) { - purchasedTabUsers.Add(users.FirstOrDefault(x => x.Value == purchase.author.id).Key, - purchase.author.id); + purchasedTabUsers.Add(users.FirstOrDefault(x => x.Value == purchase.Author.Id).Key, + purchase.Author.Id); } } else { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.author.id}")) + if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.Author.Id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.author.id}", purchase.author.id); + purchasedTabUsers.Add($"Deleted User - {purchase.Author.Id}", purchase.Author.Id); } } } else { - JObject user = await GetUserInfoById($"/users/list?x[]={purchase.author.id}"); + JObject user = await GetUserInfoById($"/users/list?x[]={purchase.Author.Id}"); if (user is null) { if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.author.id}")) + if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.Author.Id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.author.id}", - purchase.author.id); + purchasedTabUsers.Add($"Deleted User - {purchase.Author.Id}", + purchase.Author.Id); } } - Log.Debug("Content creator not longer exists - {0}", purchase.author.id); + Log.Debug("Content creator not longer exists - {0}", purchase.Author.Id); } - else if (!string.IsNullOrEmpty(user[purchase.author.id.ToString()]["username"].ToString())) + else if (!string.IsNullOrEmpty(user[purchase.Author.Id.ToString()]["username"].ToString())) { - if (!purchasedTabUsers.ContainsKey(user[purchase.author.id.ToString()]["username"] + if (!purchasedTabUsers.ContainsKey(user[purchase.Author.Id.ToString()]["username"] .ToString()) && - users.ContainsKey(user[purchase.author.id.ToString()]["username"].ToString())) + users.ContainsKey(user[purchase.Author.Id.ToString()]["username"].ToString())) { - purchasedTabUsers.Add(user[purchase.author.id.ToString()]["username"].ToString(), - purchase.author.id); + purchasedTabUsers.Add(user[purchase.Author.Id.ToString()]["username"].ToString(), + purchase.Author.Id); } } else { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.author.id}")) + if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.Author.Id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.author.id}", purchase.author.id); + purchasedTabUsers.Add($"Deleted User - {purchase.Author.Id}", purchase.Author.Id); } } } @@ -2497,16 +2513,16 @@ public class APIService(IAuthService authService, IConfigService configService, return null; } - public async Task> GetPurchasedTab(string endpoint, string folder, + public async Task> GetPurchasedTab(string endpoint, string folder, Dictionary users) { Log.Debug($"Calling GetPurchasedTab - {endpoint}"); try { - Dictionary> userPurchases = new(); - List purchasedTabCollections = new(); - Purchased purchased = new(); + Dictionary> userPurchases = new(); + List purchasedTabCollections = []; + PurchasedEntities.Purchased purchased = new(); int post_limit = 50; Dictionary getParams = new() { @@ -2517,14 +2533,16 @@ public class APIService(IAuthService authService, IConfigService configService, }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - purchased = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - if (purchased != null && purchased.hasMore) + PurchasedDtos.PurchasedDto? purchasedDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + purchased = PurchasedMapper.FromDto(purchasedDto); + if (purchased != null && purchased.HasMore) { - getParams["offset"] = purchased.list.Count.ToString(); + getParams["offset"] = purchased.List.Count.ToString(); while (true) { string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); - Purchased newPurchased = new(); + PurchasedEntities.Purchased newPurchased = new(); Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); @@ -2540,11 +2558,14 @@ public class APIService(IAuthService authService, IConfigService configService, { loopresponse.EnsureSuccessStatusCode(); string loopbody = await loopresponse.Content.ReadAsStringAsync(); - newPurchased = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + PurchasedDtos.PurchasedDto? newPurchasedDto = + JsonConvert.DeserializeObject(loopbody, + m_JsonSerializerSettings); + newPurchased = PurchasedMapper.FromDto(newPurchasedDto); } - purchased.list.AddRange(newPurchased.list); - if (!newPurchased.hasMore) + purchased.List.AddRange(newPurchased.List); + if (!newPurchased.HasMore) { break; } @@ -2553,34 +2574,35 @@ public class APIService(IAuthService authService, IConfigService configService, } } - if (purchased.list != null && purchased.list.Count > 0) + if (purchased.List != null && purchased.List.Count > 0) { - foreach (Purchased.List purchase in purchased.list.OrderByDescending(p => p.postedAt ?? p.createdAt)) + foreach (PurchasedEntities.ListItem purchase in + purchased.List.OrderByDescending(p => p.PostedAt ?? p.CreatedAt)) { - if (purchase.fromUser != null) + if (purchase.FromUser != null) { - if (!userPurchases.ContainsKey(purchase.fromUser.id)) + if (!userPurchases.ContainsKey(purchase.FromUser.Id)) { - userPurchases.Add(purchase.fromUser.id, new List()); + userPurchases.Add(purchase.FromUser.Id, new List()); } - userPurchases[purchase.fromUser.id].Add(purchase); + userPurchases[purchase.FromUser.Id].Add(purchase); } - else if (purchase.author != null) + else if (purchase.Author != null) { - if (!userPurchases.ContainsKey(purchase.author.id)) + if (!userPurchases.ContainsKey(purchase.Author.Id)) { - userPurchases.Add(purchase.author.id, new List()); + userPurchases.Add(purchase.Author.Id, new List()); } - userPurchases[purchase.author.id].Add(purchase); + userPurchases[purchase.Author.Id].Add(purchase); } } } - foreach (KeyValuePair> user in userPurchases) + foreach (KeyValuePair> user in userPurchases) { - PurchasedTabCollection purchasedTabCollection = new(); + PurchasedEntities.PurchasedTabCollection purchasedTabCollection = new(); JObject userObject = await GetUserInfoById($"/users/list?x[]={user.Key}"); purchasedTabCollection.UserId = user.Key; purchasedTabCollection.Username = @@ -2591,26 +2613,26 @@ public class APIService(IAuthService authService, IConfigService configService, string path = Path.Combine(folder, purchasedTabCollection.Username); if (Path.Exists(path)) { - foreach (Purchased.List purchase in user.Value) + foreach (PurchasedEntities.ListItem purchase in user.Value) { - if (purchase.media == null) + if (purchase.Media == null) { Log.Warning( "PurchasedTab purchase media null, setting empty list | userId={UserId} username={Username} purchaseId={PurchaseId} responseType={ResponseType} createdAt={CreatedAt} postedAt={PostedAt}", - user.Key, purchasedTabCollection.Username, purchase.id, purchase.responseType, - purchase.createdAt, purchase.postedAt); - purchase.media = new List(); + user.Key, purchasedTabCollection.Username, purchase.Id, purchase.ResponseType, + purchase.CreatedAt, purchase.PostedAt); + purchase.Media = new List(); } - switch (purchase.responseType) + switch (purchase.ResponseType) { case "post": List previewids = new(); - if (purchase.previews != null) + if (purchase.Previews != null) { - for (int i = 0; i < purchase.previews.Count; i++) + for (int i = 0; i < purchase.Previews.Count; i++) { - if (purchase.previews[i] is long previewId) + if (purchase.Previews[i] is long previewId) { if (!previewids.Contains(previewId)) { @@ -2619,11 +2641,11 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - else if (purchase.preview != null) + else if (purchase.Preview != null) { - for (int i = 0; i < purchase.preview.Count; i++) + for (int i = 0; i < purchase.Preview.Count; i++) { - if (purchase.preview[i] is long previewId) + if (purchase.Preview[i] is long previewId) { if (!previewids.Contains(previewId)) { @@ -2633,16 +2655,15 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(path, purchase.id, - purchase.text != null ? purchase.text : string.Empty, - purchase.price != null ? purchase.price : "0", - purchase.price != null && purchase.isOpened ? true : false, - purchase.isArchived.HasValue ? purchase.isArchived.Value : false, - purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); + await dbService.AddPost(path, purchase.Id, + purchase.Text != null ? purchase.Text : string.Empty, + purchase.Price != null ? purchase.Price : "0", + purchase.Price != null && purchase.IsOpened ? true : false, + purchase.IsArchived.HasValue ? purchase.IsArchived.Value : false, + purchase.CreatedAt != null ? purchase.CreatedAt.Value : purchase.PostedAt.Value); purchasedTabCollection.PaidPosts.PaidPostObjects.Add(purchase); - foreach (MessageDtos.MediumDto medium in purchase.media) + foreach (MessageEntities.Medium medium in purchase.Media) { - MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; @@ -2671,7 +2692,7 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.id, + await dbService.AddMedia(path, medium.Id, purchase.Id, medium.Files.Full.Url, null, null, null, "Posts", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : @@ -2679,7 +2700,7 @@ public class APIService(IAuthService authService, IConfigService configService, previewids.Contains(medium.Id), false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, medium.Files.Full.Url); - purchasedTabCollection.PaidPosts.PaidPostMedia.Add(mappedMedium); + purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } else if (!has && medium.CanView && medium.Files != null && @@ -2687,15 +2708,15 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.id, + await dbService.AddMedia(path, medium.Id, purchase.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : medium.Type == "audio" ? "Audios" : null, previewids.Contains(medium.Id), false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); - purchasedTabCollection.PaidPosts.PaidPostMedia.Add(mappedMedium); + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } } @@ -2706,7 +2727,7 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.id, + await dbService.AddMedia(path, medium.Id, purchase.Id, medium.Files.Full.Url, null, null, null, "Posts", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : @@ -2714,22 +2735,22 @@ public class APIService(IAuthService authService, IConfigService configService, previewids.Contains(medium.Id), false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, medium.Files.Full.Url); - purchasedTabCollection.PaidPosts.PaidPostMedia.Add(mappedMedium); + purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.id, + await dbService.AddMedia(path, medium.Id, purchase.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : medium.Type == "audio" ? "Audios" : null, previewids.Contains(medium.Id), false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); - purchasedTabCollection.PaidPosts.PaidPostMedia.Add(mappedMedium); + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } } @@ -2737,30 +2758,30 @@ public class APIService(IAuthService authService, IConfigService configService, break; case "message": - if (purchase.postedAt != null) + if (purchase.PostedAt != null) { - await dbService.AddMessage(path, purchase.id, - purchase.text != null ? purchase.text : string.Empty, - purchase.price != null ? purchase.price : "0", true, false, - purchase.postedAt.Value, purchase.fromUser.id); + await dbService.AddMessage(path, purchase.Id, + purchase.Text != null ? purchase.Text : string.Empty, + purchase.Price != null ? purchase.Price : "0", true, false, + purchase.PostedAt.Value, purchase.FromUser.Id); } else { - await dbService.AddMessage(path, purchase.id, - purchase.text != null ? purchase.text : string.Empty, - purchase.price != null ? purchase.price : "0", true, false, - purchase.createdAt.Value, purchase.fromUser.id); + await dbService.AddMessage(path, purchase.Id, + purchase.Text != null ? purchase.Text : string.Empty, + purchase.Price != null ? purchase.Price : "0", true, false, + purchase.CreatedAt.Value, purchase.FromUser.Id); } purchasedTabCollection.PaidMessages.PaidMessageObjects.Add(purchase); - if (purchase.media != null && purchase.media.Count > 0) + if (purchase.Media != null && purchase.Media.Count > 0) { List paidMessagePreviewids = new(); - if (purchase.previews != null) + if (purchase.Previews != null) { - for (int i = 0; i < purchase.previews.Count; i++) + for (int i = 0; i < purchase.Previews.Count; i++) { - if (purchase.previews[i] is long previewId) + if (purchase.Previews[i] is long previewId) { if (!paidMessagePreviewids.Contains(previewId)) { @@ -2769,11 +2790,11 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - else if (purchase.preview != null) + else if (purchase.Preview != null) { - for (int i = 0; i < purchase.preview.Count; i++) + for (int i = 0; i < purchase.Preview.Count; i++) { - if (purchase.preview[i] is long previewId) + if (purchase.Preview[i] is long previewId) { if (!paidMessagePreviewids.Contains(previewId)) { @@ -2783,11 +2804,10 @@ public class APIService(IAuthService authService, IConfigService configService, } } - foreach (MessageDtos.MediumDto medium in purchase.media) + foreach (MessageEntities.Medium medium in purchase.Media) { if (paidMessagePreviewids.Count > 0) { - MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); bool has = paidMessagePreviewids.Any(cus => cus.Equals(medium.Id)); if (!has && medium.CanView && medium.Files != null && medium.Files.Full != null && @@ -2819,7 +2839,7 @@ public class APIService(IAuthService authService, IConfigService configService, if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.id, + await dbService.AddMedia(path, medium.Id, purchase.Id, medium.Files.Full.Url, null, null, null, "Messages", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : @@ -2829,7 +2849,7 @@ public class APIService(IAuthService authService, IConfigService configService, purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, medium.Files.Full.Url); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( - mappedMedium); + medium); } } else if (!has && medium.CanView && medium.Files != null && @@ -2861,7 +2881,7 @@ public class APIService(IAuthService authService, IConfigService configService, if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.id, + await dbService.AddMedia(path, medium.Id, purchase.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : @@ -2869,15 +2889,14 @@ public class APIService(IAuthService authService, IConfigService configService, paidMessagePreviewids.Contains(medium.Id), false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( - mappedMedium); + medium); } } } else { - MessageEntities.Medium mappedMedium = MessagesMapper.MapMedium(medium); if (medium.CanView && medium.Files != null && medium.Files.Full != null && !string.IsNullOrEmpty(medium.Files.Full.Url)) { @@ -2907,7 +2926,7 @@ public class APIService(IAuthService authService, IConfigService configService, if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.id, + await dbService.AddMedia(path, medium.Id, purchase.Id, medium.Files.Full.Url, null, null, null, "Messages", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : @@ -2917,7 +2936,7 @@ public class APIService(IAuthService authService, IConfigService configService, purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, medium.Files.Full.Url); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( - mappedMedium); + medium); } } else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) @@ -2948,7 +2967,7 @@ public class APIService(IAuthService authService, IConfigService configService, if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.id, + await dbService.AddMedia(path, medium.Id, purchase.Id, medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", medium.Type == "photo" ? "Images" : medium.Type == "video" || medium.Type == "gif" ? "Videos" : @@ -2956,9 +2975,9 @@ public class APIService(IAuthService authService, IConfigService configService, paidMessagePreviewids.Contains(medium.Id), false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.id}"); + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( - mappedMedium); + medium); } } } diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index b138344..9f5e7d8 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -4,13 +4,13 @@ using System.Xml.Linq; using FFmpeg.NET; using FFmpeg.NET.Events; using OF_DL.Models; -using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; using ArchivedEntities = OF_DL.Models.Entities.Archived; using CommonEntities = OF_DL.Models.Entities.Common; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; +using PurchasedEntities = OF_DL.Models.Entities.Purchased; using OF_DL.Utils; using Serilog; using Serilog.Events; @@ -1109,15 +1109,15 @@ public class DownloadService( } public async Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, + IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? messageInfo, MessageEntities.Medium? messageMedia, - Purchased.FromUser? fromUser, Dictionary users) + CommonEntities.FromUser? fromUser, Dictionary users) { string path; - if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && - messageInfo?.createdAt is not null) + if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.Id is not null && + messageInfo?.CreatedAt is not null) { - path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + path = $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"; } else { @@ -1161,16 +1161,16 @@ public class DownloadService( string api_type, IProgressReporter progressReporter, string? filenameFormat, - Purchased.List? messageInfo, + PurchasedEntities.ListItem? messageInfo, MessageEntities.Medium? messageMedia, - Purchased.FromUser? fromUser, + CommonEntities.FromUser? fromUser, Dictionary users) { string path; - if (configService.CurrentConfig.FolderPerPaidPost && messageInfo != null && messageInfo?.id is not null && - messageInfo?.postedAt is not null) + if (configService.CurrentConfig.FolderPerPaidPost && messageInfo != null && messageInfo?.Id is not null && + messageInfo?.PostedAt is not null) { - path = $"/Posts/Paid/{messageInfo.id} {messageInfo.postedAt.Value:yyyy-MM-dd HH-mm-ss}"; + path = $"/Posts/Paid/{messageInfo.Id} {messageInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}"; } else { @@ -1451,9 +1451,9 @@ public class DownloadService( public async Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, + IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? messageInfo, MessageEntities.Medium? messageMedia, - Purchased.FromUser? fromUser, Dictionary users) + CommonEntities.FromUser? fromUser, Dictionary users) { try { @@ -1462,9 +1462,9 @@ public class DownloadService( Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && - messageInfo?.id is not null && messageInfo?.createdAt is not null) + messageInfo?.Id is not null && messageInfo?.CreatedAt is not null) { - path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + path = $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; } else { @@ -2095,9 +2095,9 @@ public class DownloadService( public async Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, + IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? postInfo, MessageEntities.Medium? postMedia, - Purchased.FromUser? fromUser, Dictionary users) + CommonEntities.FromUser? fromUser, Dictionary users) { try { @@ -2105,10 +2105,10 @@ public class DownloadService( string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerPaidPost && postInfo != null && postInfo?.id is not null && - postInfo?.postedAt is not null) + if (configService.CurrentConfig.FolderPerPaidPost && postInfo != null && postInfo?.Id is not null && + postInfo?.PostedAt is not null) { - path = $"/Posts/Paid/{postInfo.id} {postInfo.postedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + path = $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; } else { @@ -2712,7 +2712,8 @@ public class DownloadService( public async Task DownloadPaidMessages(string username, string path, Dictionary users, - bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidMessageCollection paidMessageCollection, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter) { Log.Debug($"Calling DownloadPaidMessages - {username}"); @@ -2760,14 +2761,14 @@ public class DownloadService( MessageEntities.Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == kvp.Key); - Purchased.List? messageInfo = + PurchasedEntities.ListItem? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.media?.Any(m => m.Id == kvp.Key) == true); + p?.Media?.Any(m => m.Id == kvp.Key) == true); isNew = await DownloadPurchasedMessageDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, lastModified, kvp.Key, "Messages", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? - string.Empty, messageInfo, mediaInfo, messageInfo?.fromUser, users); + string.Empty, messageInfo, mediaInfo, messageInfo?.FromUser, users); } else { @@ -2778,11 +2779,11 @@ public class DownloadService( { MessageEntities.Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == kvp.Key); - Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.media?.Any(m => m.Id == kvp.Key) == true); + PurchasedEntities.ListItem? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == kvp.Key) == true); isNew = await DownloadPurchasedMedia(kvp.Value, path, kvp.Key, "Messages", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? - string.Empty, messageInfo, mediaInfo, messageInfo?.fromUser, users); + string.Empty, messageInfo, mediaInfo, messageInfo?.FromUser, users); } if (isNew) @@ -2992,7 +2993,7 @@ public class DownloadService( public async Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, - PaidPostCollection purchasedPosts, IProgressReporter progressReporter) + PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter) { Log.Debug($"Calling DownloadPaidPosts - {username}"); @@ -3038,14 +3039,14 @@ public class DownloadService( MessageEntities.Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKVP.Key); - Purchased.List? postInfo = + PurchasedEntities.ListItem? postInfo = purchasedPosts.PaidPostObjects.FirstOrDefault(p => - p?.media?.Any(m => m.Id == postKVP.Key) == true); + p?.Media?.Any(m => m.Id == postKVP.Key) == true); isNew = await DownloadPurchasedPostDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, lastModified, postKVP.Key, "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, postInfo, mediaInfo, postInfo?.fromUser, users); + string.Empty, postInfo, mediaInfo, postInfo?.FromUser, users); } else { @@ -3056,11 +3057,11 @@ public class DownloadService( { MessageEntities.Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKVP.Key); - Purchased.List? postInfo = - purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.media?.Any(m => m.Id == postKVP.Key) == true); + PurchasedEntities.ListItem? postInfo = + purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.Media?.Any(m => m.Id == postKVP.Key) == true); isNew = await DownloadPurchasedPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, postInfo, mediaInfo, postInfo?.fromUser, users); + string.Empty, postInfo, mediaInfo, postInfo?.FromUser, users); } if (isNew) diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index 1c77129..14a487c 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -1,8 +1,8 @@ using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using OF_DL.Enumerations; +using OF_DL.Models.Entities.Purchased; using ArchivedEntities = OF_DL.Models.Entities.Archived; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index b1ff68e..d0979ba 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -1,10 +1,10 @@ using OF_DL.Models; -using OF_DL.Models.Purchased; using OF_DL.Models.Streams; using ArchivedEntities = OF_DL.Models.Entities.Archived; using CommonEntities = OF_DL.Models.Entities.Common; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; +using PurchasedEntities = OF_DL.Models.Entities.Purchased; namespace OF_DL.Services; @@ -61,9 +61,9 @@ public interface IDownloadService CommonEntities.Author? author, Dictionary users); Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, + IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? messageInfo, MessageEntities.Medium? messageMedia, - Purchased.FromUser? fromUser, Dictionary users); + CommonEntities.FromUser? fromUser, Dictionary users); Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, @@ -71,9 +71,9 @@ public interface IDownloadService Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, + IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? messageInfo, MessageEntities.Medium? messageMedia, - Purchased.FromUser? fromUser, Dictionary users); + CommonEntities.FromUser? fromUser, Dictionary users); Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, @@ -82,14 +82,14 @@ public interface IDownloadService Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, + IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? postInfo, MessageEntities.Medium? postMedia, - Purchased.FromUser? fromUser, Dictionary users); + CommonEntities.FromUser? fromUser, Dictionary users); Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, + IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? messageInfo, MessageEntities.Medium? messageMedia, - Purchased.FromUser? fromUser, Dictionary users); + CommonEntities.FromUser? fromUser, Dictionary users); Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter); @@ -127,7 +127,8 @@ public interface IDownloadService IProgressReporter progressReporter); Task DownloadPaidMessages(string username, string path, Dictionary users, - bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidMessageCollection paidMessageCollection, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter); Task DownloadStreams(string username, long userId, string path, Dictionary users, @@ -139,6 +140,6 @@ public interface IDownloadService IProgressReporter progressReporter); Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, - bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidPostCollection purchasedPosts, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter); } From 849fbbc919820bbddb2101bf9713a66c47e5afaa Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Sun, 8 Feb 2026 23:27:36 -0600 Subject: [PATCH 19/55] Refactor Stories entities into DTOs and application entities with standardized naming conventions and default values --- OF DL/Models/Dtos/Stories/MediumDto.cs | 21 ++++++ OF DL/Models/Dtos/Stories/StoryDto.cs | 24 +++++++ OF DL/Models/Entities/Stories/Medium.cs | 16 +++++ OF DL/Models/Entities/Stories/Stories.cs | 10 +++ OF DL/Models/Mappers/StoriesMapper.cs | 32 +++++++++ OF DL/Models/Stories/Stories.cs | 90 ------------------------ OF DL/Services/APIService.cs | 58 +++++++-------- 7 files changed, 133 insertions(+), 118 deletions(-) create mode 100644 OF DL/Models/Dtos/Stories/MediumDto.cs create mode 100644 OF DL/Models/Dtos/Stories/StoryDto.cs create mode 100644 OF DL/Models/Entities/Stories/Medium.cs create mode 100644 OF DL/Models/Entities/Stories/Stories.cs create mode 100644 OF DL/Models/Mappers/StoriesMapper.cs delete mode 100644 OF DL/Models/Stories/Stories.cs diff --git a/OF DL/Models/Dtos/Stories/MediumDto.cs b/OF DL/Models/Dtos/Stories/MediumDto.cs new file mode 100644 index 0000000..6971f50 --- /dev/null +++ b/OF DL/Models/Dtos/Stories/MediumDto.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Stories; + +public class MediumDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("convertedToVideo")] public bool ConvertedToVideo { get; set; } + + [JsonProperty("canView")] public bool CanView { get; set; } + + [JsonProperty("hasError")] public bool HasError { get; set; } + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } + + [JsonProperty("files")] public FilesDto Files { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Stories/StoryDto.cs b/OF DL/Models/Dtos/Stories/StoryDto.cs new file mode 100644 index 0000000..1a11fa5 --- /dev/null +++ b/OF DL/Models/Dtos/Stories/StoryDto.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Stories; + +public class StoryDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("userId")] public long UserId { get; set; } + + [JsonProperty("isWatched")] public bool IsWatched { get; set; } + + [JsonProperty("isReady")] public bool IsReady { get; set; } + + [JsonProperty("media")] public List Media { get; set; } = []; + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } + + [JsonProperty("question")] public object Question { get; set; } = new(); + + [JsonProperty("canLike")] public bool CanLike { get; set; } + + [JsonProperty("isLiked")] public bool IsLiked { get; set; } +} diff --git a/OF DL/Models/Entities/Stories/Medium.cs b/OF DL/Models/Entities/Stories/Medium.cs new file mode 100644 index 0000000..20e3f83 --- /dev/null +++ b/OF DL/Models/Entities/Stories/Medium.cs @@ -0,0 +1,16 @@ +using OF_DL.Models.Entities.Common; + +namespace OF_DL.Models.Entities.Stories; + +public class Medium +{ + public long Id { get; set; } + + public string? Type { get; set; } + + public bool CanView { get; set; } + + public DateTime? CreatedAt { get; set; } + + public Files Files { get; set; } = new(); +} diff --git a/OF DL/Models/Entities/Stories/Stories.cs b/OF DL/Models/Entities/Stories/Stories.cs new file mode 100644 index 0000000..65d06d5 --- /dev/null +++ b/OF DL/Models/Entities/Stories/Stories.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Stories; + +public class Stories +{ + public long Id { get; set; } + + public DateTime? CreatedAt { get; set; } + + public List Media { get; set; } = []; +} diff --git a/OF DL/Models/Mappers/StoriesMapper.cs b/OF DL/Models/Mappers/StoriesMapper.cs new file mode 100644 index 0000000..4ccdd03 --- /dev/null +++ b/OF DL/Models/Mappers/StoriesMapper.cs @@ -0,0 +1,32 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Stories; +using OF_DL.Models.Entities.Common; +using OF_DL.Models.Entities.Stories; + +namespace OF_DL.Models.Mappers; + +public static class StoriesMapper +{ + public static List FromDto(List? dto) => + dto == null ? [] : dto.Select(MapStory).ToList(); + + private static Stories MapStory(StoryDto dto) => + new() { Id = dto.Id, CreatedAt = dto.CreatedAt, Media = MapMedia(dto.Media) }; + + private static List MapMedia(List? media) => + media == null ? [] : media.Select(MapMedium).ToList(); + + private static Medium MapMedium(MediumDto dto) => + new() + { + Id = dto.Id, + Type = dto.Type, + CanView = dto.CanView, + CreatedAt = dto.CreatedAt, + Files = MapFiles(dto.Files) + }; + + private static Files MapFiles(FilesDto? dto) => new() { Full = MapFull(dto?.Full) }; + + private static Full MapFull(FullDto? dto) => new() { Url = dto?.Url }; +} diff --git a/OF DL/Models/Stories/Stories.cs b/OF DL/Models/Stories/Stories.cs deleted file mode 100644 index 9aede74..0000000 --- a/OF DL/Models/Stories/Stories.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Newtonsoft.Json; - -namespace OF_DL.Models.Stories; - -public class Stories -{ - public long id { get; set; } - public long userId { get; set; } - public bool isWatched { get; set; } - public bool isReady { get; set; } - public List media { get; set; } - public DateTime? createdAt { get; set; } - public object question { get; set; } - public bool canLike { get; set; } - public bool isLiked { get; set; } - - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - } - - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } - - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Files files { get; set; } - } - - public class Preview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } - - public class Source - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public int duration { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } - - public class Sources - { - [JsonProperty("720")] public object _720 { get; set; } - - [JsonProperty("240")] public object _240 { get; set; } - - public string w150 { get; set; } - public string w480 { get; set; } - } - - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } -} diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 069929c..93983f5 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -6,21 +6,22 @@ using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Stories; using OF_DL.Models.Streams; using OF_DL.Enumerations; using ArchivedDtos = OF_DL.Models.Dtos.Archived; -using PostDtos = OF_DL.Models.Dtos.Posts; -using MessageDtos = OF_DL.Models.Dtos.Messages; -using PurchasedDtos = OF_DL.Models.Dtos.Purchased; -using ListDtos = OF_DL.Models.Dtos.Lists; using HighlightDtos = OF_DL.Models.Dtos.Highlights; +using ListDtos = OF_DL.Models.Dtos.Lists; +using MessageDtos = OF_DL.Models.Dtos.Messages; +using PostDtos = OF_DL.Models.Dtos.Posts; +using PurchasedDtos = OF_DL.Models.Dtos.Purchased; +using StoriesDtos = OF_DL.Models.Dtos.Stories; using ArchivedEntities = OF_DL.Models.Entities.Archived; using HighlightEntities = OF_DL.Models.Entities.Highlights; using ListEntities = OF_DL.Models.Entities.Lists; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; +using StoriesEntities = OF_DL.Models.Entities.Stories; using OF_DL.Models.Mappers; using OF_DL.Widevine; using Serilog; @@ -393,60 +394,61 @@ public class APIService(IAuthService authService, IConfigService configService, { Log.Debug("Media Stories - " + endpoint); - List stories = JsonConvert.DeserializeObject>(body, m_JsonSerializerSettings) ?? - new List(); + List? storiesDto = + JsonConvert.DeserializeObject>(body, m_JsonSerializerSettings); + List stories = StoriesMapper.FromDto(storiesDto); - foreach (Stories story in stories) + foreach (StoriesEntities.Stories story in stories) { - if (story.media[0].createdAt.HasValue) + if (story.Media[0].CreatedAt.HasValue) { - await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, - story.media[0].createdAt.Value); + await dbService.AddStory(folder, story.Id, string.Empty, "0", false, false, + story.Media[0].CreatedAt.Value); } - else if (story.createdAt.HasValue) + else if (story.CreatedAt.HasValue) { - await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, - story.createdAt.Value); + await dbService.AddStory(folder, story.Id, string.Empty, "0", false, false, + story.CreatedAt.Value); } else { - await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, DateTime.Now); + await dbService.AddStory(folder, story.Id, string.Empty, "0", false, false, DateTime.Now); } - if (story.media != null && story.media.Count > 0) + if (story.Media != null && story.Media.Count > 0) { - foreach (Stories.Medium medium in story.media) + foreach (StoriesEntities.Medium medium in story.Media) { - await dbService.AddMedia(folder, medium.id, story.id, medium.files.full.url, null, null, + await dbService.AddMedia(folder, medium.Id, story.Id, medium.Files.Full.Url, null, null, null, "Stories", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, false, false, null); - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, false, false, null); + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (medium.canView) + if (medium.CanView) { - if (!return_urls.ContainsKey(medium.id)) + if (!return_urls.ContainsKey(medium.Id)) { - return_urls.Add(medium.id, medium.files.full.url); + return_urls.Add(medium.Id, medium.Files.Full.Url); } } } From 40ccf7aa625bcf6020223e6ffd2904313ac059fa Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 00:10:09 -0600 Subject: [PATCH 20/55] Refactor Streams entities into DTOs and application entities with standardized naming conventions and default values --- OF DL/Models/Dtos/Archived/ArchivedDto.cs | 1 + .../Dtos/{Archived => Common}/CountersDto.cs | 2 +- OF DL/Models/Dtos/Streams/InfoDto.cs | 11 + OF DL/Models/Dtos/Streams/ListItemDto.cs | 101 +++++++++ OF DL/Models/Dtos/Streams/MediumDto.cs | 37 ++++ OF DL/Models/Dtos/Streams/StreamsDto.cs | 17 ++ OF DL/Models/Entities/Streams/ListItem.cs | 41 ++++ OF DL/Models/Entities/Streams/Medium.cs | 14 ++ OF DL/Models/Entities/Streams/Streams.cs | 10 + .../Entities/Streams/StreamsCollection.cs | 8 + OF DL/Models/Mappers/StreamsMapper.cs | 108 +++++++++ OF DL/Models/Streams/Streams.cs | 209 ------------------ OF DL/Models/Streams/StreamsCollection.cs | 8 - OF DL/Program.cs | 4 +- OF DL/Services/APIService.cs | 114 +++++----- OF DL/Services/DownloadService.cs | 42 ++-- OF DL/Services/IAPIService.cs | 24 +- OF DL/Services/IDownloadService.cs | 12 +- 18 files changed, 457 insertions(+), 306 deletions(-) rename OF DL/Models/Dtos/{Archived => Common}/CountersDto.cs (93%) create mode 100644 OF DL/Models/Dtos/Streams/InfoDto.cs create mode 100644 OF DL/Models/Dtos/Streams/ListItemDto.cs create mode 100644 OF DL/Models/Dtos/Streams/MediumDto.cs create mode 100644 OF DL/Models/Dtos/Streams/StreamsDto.cs create mode 100644 OF DL/Models/Entities/Streams/ListItem.cs create mode 100644 OF DL/Models/Entities/Streams/Medium.cs create mode 100644 OF DL/Models/Entities/Streams/Streams.cs create mode 100644 OF DL/Models/Entities/Streams/StreamsCollection.cs create mode 100644 OF DL/Models/Mappers/StreamsMapper.cs delete mode 100644 OF DL/Models/Streams/Streams.cs delete mode 100644 OF DL/Models/Streams/StreamsCollection.cs diff --git a/OF DL/Models/Dtos/Archived/ArchivedDto.cs b/OF DL/Models/Dtos/Archived/ArchivedDto.cs index 80d373a..b9968d1 100644 --- a/OF DL/Models/Dtos/Archived/ArchivedDto.cs +++ b/OF DL/Models/Dtos/Archived/ArchivedDto.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; namespace OF_DL.Models.Dtos.Archived; diff --git a/OF DL/Models/Dtos/Archived/CountersDto.cs b/OF DL/Models/Dtos/Common/CountersDto.cs similarity index 93% rename from OF DL/Models/Dtos/Archived/CountersDto.cs rename to OF DL/Models/Dtos/Common/CountersDto.cs index 32a515e..fe3c3b4 100644 --- a/OF DL/Models/Dtos/Archived/CountersDto.cs +++ b/OF DL/Models/Dtos/Common/CountersDto.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Models.Dtos.Archived; +namespace OF_DL.Models.Dtos.Common; public class CountersDto { diff --git a/OF DL/Models/Dtos/Streams/InfoDto.cs b/OF DL/Models/Dtos/Streams/InfoDto.cs new file mode 100644 index 0000000..bd1e9b1 --- /dev/null +++ b/OF DL/Models/Dtos/Streams/InfoDto.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Streams; + +public class InfoDto +{ + [JsonProperty("source")] public SourceDto Source { get; set; } = new(); + + [JsonProperty("preview")] public PreviewDto Preview { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Streams/ListItemDto.cs b/OF DL/Models/Dtos/Streams/ListItemDto.cs new file mode 100644 index 0000000..151ece4 --- /dev/null +++ b/OF DL/Models/Dtos/Streams/ListItemDto.cs @@ -0,0 +1,101 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; +using OF_DL.Utils; + +namespace OF_DL.Models.Dtos.Streams; + +public class ListItemDto +{ + private string _rawText = ""; + + [JsonProperty("responseType")] public string ResponseType { get; set; } = ""; + + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("postedAt")] public DateTime PostedAt { get; set; } + + [JsonProperty("postedAtPrecise")] public string PostedAtPrecise { get; set; } = ""; + + [JsonProperty("expiredAt")] public object ExpiredAt { get; set; } = new(); + + [JsonProperty("author")] public AuthorDto Author { get; set; } = new(); + + [JsonProperty("text")] public string Text { get; set; } = ""; + + [JsonProperty("rawText")] + public string RawText + { + get + { + if (string.IsNullOrEmpty(_rawText)) + { + _rawText = XmlUtils.EvaluateInnerText(Text); + } + + return _rawText; + } + set => _rawText = value; + } + + [JsonProperty("lockedText")] public bool? LockedText { get; set; } + + [JsonProperty("isFavorite")] public bool? IsFavorite { get; set; } + + [JsonProperty("canReport")] public bool? CanReport { get; set; } + + [JsonProperty("canDelete")] public bool? CanDelete { get; set; } + + [JsonProperty("canComment")] public bool? CanComment { get; set; } + + [JsonProperty("canEdit")] public bool? CanEdit { get; set; } + + [JsonProperty("isPinned")] public bool? IsPinned { get; set; } + + [JsonProperty("favoritesCount")] public int? FavoritesCount { get; set; } + + [JsonProperty("mediaCount")] public int? MediaCount { get; set; } + + [JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; } + + [JsonProperty("voting")] public object Voting { get; set; } = new(); + + [JsonProperty("isOpened")] public bool? IsOpened { get; set; } + + [JsonProperty("canToggleFavorite")] public bool? CanToggleFavorite { get; set; } + + [JsonProperty("streamId")] public int? StreamId { get; set; } + + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("hasVoting")] public bool? HasVoting { get; set; } + + [JsonProperty("isAddedToBookmarks")] public bool? IsAddedToBookmarks { get; set; } + + [JsonProperty("isArchived")] public bool? IsArchived { get; set; } + + [JsonProperty("isPrivateArchived")] public bool? IsPrivateArchived { get; set; } + + [JsonProperty("isDeleted")] public bool? IsDeleted { get; set; } + + [JsonProperty("hasUrl")] public bool? HasUrl { get; set; } + + [JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; } + + [JsonProperty("cantCommentReason")] public string CantCommentReason { get; set; } = ""; + + [JsonProperty("commentsCount")] public int? CommentsCount { get; set; } + + [JsonProperty("mentionedUsers")] public List MentionedUsers { get; set; } = []; + + [JsonProperty("linkedUsers")] public List LinkedUsers { get; set; } = []; + + [JsonProperty("tipsAmount")] public string TipsAmount { get; set; } = ""; + + [JsonProperty("tipsAmountRaw")] public string TipsAmountRaw { get; set; } = ""; + + [JsonProperty("media")] public List Media { get; set; } = []; + + [JsonProperty("canViewMedia")] public bool? CanViewMedia { get; set; } + + [JsonProperty("preview")] public List Preview { get; set; } = []; +} diff --git a/OF DL/Models/Dtos/Streams/MediumDto.cs b/OF DL/Models/Dtos/Streams/MediumDto.cs new file mode 100644 index 0000000..61040f1 --- /dev/null +++ b/OF DL/Models/Dtos/Streams/MediumDto.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Streams; + +public class MediumDto +{ + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("convertedToVideo")] public bool ConvertedToVideo { get; set; } + + [JsonProperty("canView")] public bool CanView { get; set; } + + [JsonProperty("hasError")] public bool HasError { get; set; } + + [JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } + + [JsonProperty("info")] public InfoDto Info { get; set; } = new(); + + [JsonProperty("source")] public SourceDto Source { get; set; } = new(); + + [JsonProperty("squarePreview")] public string SquarePreview { get; set; } = ""; + + [JsonProperty("full")] public string Full { get; set; } = ""; + + [JsonProperty("preview")] public string Preview { get; set; } = ""; + + [JsonProperty("thumb")] public string Thumb { get; set; } = ""; + + [JsonProperty("hasCustomPreview")] public bool HasCustomPreview { get; set; } + + [JsonProperty("files")] public FilesDto Files { get; set; } = new(); + + [JsonProperty("videoSources")] public VideoSourcesDto VideoSources { get; set; } = new(); +} diff --git a/OF DL/Models/Dtos/Streams/StreamsDto.cs b/OF DL/Models/Dtos/Streams/StreamsDto.cs new file mode 100644 index 0000000..480a494 --- /dev/null +++ b/OF DL/Models/Dtos/Streams/StreamsDto.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Streams; + +public class StreamsDto +{ + [JsonProperty("list")] public List List { get; set; } = []; + + [JsonProperty("hasMore")] public bool HasMore { get; set; } + + [JsonProperty("headMarker")] public string HeadMarker { get; set; } = ""; + + [JsonProperty("tailMarker")] public string TailMarker { get; set; } = ""; + + [JsonProperty("counters")] public CountersDto Counters { get; set; } = new(); +} diff --git a/OF DL/Models/Entities/Streams/ListItem.cs b/OF DL/Models/Entities/Streams/ListItem.cs new file mode 100644 index 0000000..05bd254 --- /dev/null +++ b/OF DL/Models/Entities/Streams/ListItem.cs @@ -0,0 +1,41 @@ +using OF_DL.Models.Entities.Common; +using OF_DL.Utils; + +namespace OF_DL.Models.Entities.Streams; + +public class ListItem +{ + private string _rawText = ""; + + public long Id { get; set; } + + public DateTime PostedAt { get; set; } + + public Author? Author { get; set; } + + public string Text { get; set; } = ""; + + public string RawText + { + get + { + if (string.IsNullOrEmpty(_rawText)) + { + _rawText = XmlUtils.EvaluateInnerText(Text); + } + + return _rawText; + } + set => _rawText = value; + } + + public bool IsOpened { get; set; } + + public string? Price { get; set; } + + public bool IsArchived { get; set; } + + public List? Media { get; set; } + + public List? Preview { get; set; } +} diff --git a/OF DL/Models/Entities/Streams/Medium.cs b/OF DL/Models/Entities/Streams/Medium.cs new file mode 100644 index 0000000..0dbb768 --- /dev/null +++ b/OF DL/Models/Entities/Streams/Medium.cs @@ -0,0 +1,14 @@ +using OF_DL.Models.Entities.Common; + +namespace OF_DL.Models.Entities.Streams; + +public class Medium +{ + public long Id { get; set; } + + public string? Type { get; set; } + + public bool CanView { get; set; } + + public Files? Files { get; set; } +} diff --git a/OF DL/Models/Entities/Streams/Streams.cs b/OF DL/Models/Entities/Streams/Streams.cs new file mode 100644 index 0000000..55646fb --- /dev/null +++ b/OF DL/Models/Entities/Streams/Streams.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Streams; + +public class Streams +{ + public List List { get; set; } = []; + + public bool HasMore { get; set; } + + public string? TailMarker { get; set; } +} diff --git a/OF DL/Models/Entities/Streams/StreamsCollection.cs b/OF DL/Models/Entities/Streams/StreamsCollection.cs new file mode 100644 index 0000000..3355d01 --- /dev/null +++ b/OF DL/Models/Entities/Streams/StreamsCollection.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Models.Entities.Streams; + +public class StreamsCollection +{ + public List StreamMedia { get; set; } = []; + public List StreamObjects { get; set; } = []; + public Dictionary Streams { get; set; } = new(); +} diff --git a/OF DL/Models/Mappers/StreamsMapper.cs b/OF DL/Models/Mappers/StreamsMapper.cs new file mode 100644 index 0000000..3b3e30d --- /dev/null +++ b/OF DL/Models/Mappers/StreamsMapper.cs @@ -0,0 +1,108 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Streams; +using OF_DL.Models.Entities.Common; +using OF_DL.Models.Entities.Streams; + +namespace OF_DL.Models.Mappers; + +public static class StreamsMapper +{ + public static Streams FromDto(StreamsDto? dto) + { + Streams mapped = new() { HasMore = dto?.HasMore ?? false, TailMarker = dto?.TailMarker }; + + if (dto?.List == null) + { + return mapped; + } + + foreach (ListItemDto entry in dto.List) + { + mapped.List.Add(MapList(entry)); + } + + return mapped; + } + + private static ListItem MapList(ListItemDto dto) => + new() + { + Id = dto.Id, + PostedAt = dto.PostedAt, + Author = MapAuthor(dto.Author), + Text = dto.Text, + RawText = dto.RawText, + Price = dto.Price, + IsOpened = dto.IsOpened ?? false, + IsArchived = dto.IsArchived ?? false, + Media = MapMedia(dto.Media), + Preview = dto.Preview + }; + + private static Author? MapAuthor(AuthorDto? dto) => + dto == null ? null : new Author { Id = dto.Id }; + + private static List? MapMedia(List? media) => + media?.Select(MapMedium).ToList(); + + private static Medium MapMedium(MediumDto dto) => + new() { Id = dto.Id, Type = dto.Type, CanView = dto.CanView, Files = MapFiles(dto.Files) }; + + private static Files? MapFiles(FilesDto? dto) + { + if (dto == null) + { + return null; + } + + Full? full = MapFull(dto.Full); + Drm? drm = MapDrm(dto.Drm); + + if (full == null && drm == null) + { + return null; + } + + return new Files { Full = full, Drm = drm }; + } + + private static Full? MapFull(FullDto? dto) => + dto == null || string.IsNullOrEmpty(dto.Url) ? null : new Full { Url = dto.Url }; + + private static Drm? MapDrm(DrmDto? dto) + { + if (dto?.Manifest == null || string.IsNullOrEmpty(dto.Manifest.Dash)) + { + return null; + } + + Dash? dash = MapDash(dto.Signature.Dash); + if (dash == null) + { + return null; + } + + return new Drm + { + Manifest = new Manifest { Dash = dto.Manifest.Dash }, Signature = new Signature { Dash = dash } + }; + } + + private static Dash? MapDash(DashDto? dto) + { + if (dto == null || + string.IsNullOrEmpty(dto.CloudFrontPolicy) || + string.IsNullOrEmpty(dto.CloudFrontSignature) || + string.IsNullOrEmpty(dto.CloudFrontKeyPairId)) + { + return null; + } + + return new Dash + { + CloudFrontPolicy = dto.CloudFrontPolicy, + CloudFrontSignature = dto.CloudFrontSignature, + CloudFrontKeyPairId = dto.CloudFrontKeyPairId + }; + } +} diff --git a/OF DL/Models/Streams/Streams.cs b/OF DL/Models/Streams/Streams.cs deleted file mode 100644 index de993ef..0000000 --- a/OF DL/Models/Streams/Streams.cs +++ /dev/null @@ -1,209 +0,0 @@ -using Newtonsoft.Json; -using OF_DL.Utils; - -namespace OF_DL.Models.Streams; - -public class Streams -{ - public List list { get; set; } - public bool hasMore { get; set; } - public string headMarker { get; set; } - public string tailMarker { get; set; } - public Counters counters { get; set; } - - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } - - public class Counters - { - public int audiosCount { get; set; } - public int photosCount { get; set; } - public int videosCount { get; set; } - public int mediasCount { get; set; } - public int postsCount { get; set; } - public int streamsCount { get; set; } - public int archivedPostsCount { get; set; } - } - - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } - - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } - - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } - - public class List - { - private string _rawText; - public string responseType { get; set; } - public long id { get; set; } - public DateTime postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - - public string rawText - { - get - { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } - - return _rawText; - } - set => _rawText = value; - } - - public bool lockedText { get; set; } - public bool isFavorite { get; set; } - public bool canReport { get; set; } - public bool canDelete { get; set; } - public bool canComment { get; set; } - public bool canEdit { get; set; } - public bool isPinned { get; set; } - public int favoritesCount { get; set; } - public int mediaCount { get; set; } - public bool isMediaReady { get; set; } - public object voting { get; set; } - public bool isOpened { get; set; } - public bool canToggleFavorite { get; set; } - public int streamId { get; set; } - public string price { get; set; } - public bool hasVoting { get; set; } - public bool isAddedToBookmarks { get; set; } - public bool isArchived { get; set; } - public bool isPrivateArchived { get; set; } - public bool isDeleted { get; set; } - public bool hasUrl { get; set; } - public bool isCouplePeopleMedia { get; set; } - public string cantCommentReason { get; set; } - public int commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public string tipsAmount { get; set; } - public string tipsAmountRaw { get; set; } - public List media { get; set; } - public bool canViewMedia { get; set; } - public List preview { get; set; } - } - - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Info info { get; set; } - public Source source { get; set; } - public string squarePreview { get; set; } - public string full { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public bool hasCustomPreview { get; set; } - public Files files { get; set; } - public VideoSources videoSources { get; set; } - } - - public class Preview - { - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public string url { get; set; } - } - - public class Source - { - public string source { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public int duration { get; set; } - } - - public class VideoSources - { - [JsonProperty("720")] public object _720 { get; set; } - - [JsonProperty("240")] public object _240 { get; set; } - } - - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - - public class Manifest - { - public string? hls { get; set; } - public string? dash { get; set; } - } - - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } - - public class Hls - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Dash - { - [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } -} diff --git a/OF DL/Models/Streams/StreamsCollection.cs b/OF DL/Models/Streams/StreamsCollection.cs deleted file mode 100644 index 126c112..0000000 --- a/OF DL/Models/Streams/StreamsCollection.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OF_DL.Models.Streams; - -public class StreamsCollection -{ - public List StreamMedia = new(); - public List StreamObjects = new(); - public Dictionary Streams = new(); -} diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 7cb09cc..629037f 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -7,13 +7,13 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.CLI; using OF_DL.Models; -using OF_DL.Models.Streams; using OF_DL.Enumerations; using OF_DL.Helpers; using ArchivedEntities = OF_DL.Models.Entities.Archived; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; +using StreamEntities = OF_DL.Models.Entities.Streams; using OF_DL.Services; using Serilog; using Spectre.Console; @@ -1764,7 +1764,7 @@ public class Program(IServiceProvider serviceProvider) IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - StreamsCollection streams = new(); + StreamEntities.StreamsCollection streams = new(); await AnsiConsole.Status() .StartAsync("[red]Getting Streams[/]", diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 93983f5..f2e20c0 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -6,7 +6,6 @@ using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Streams; using OF_DL.Enumerations; using ArchivedDtos = OF_DL.Models.Dtos.Archived; using HighlightDtos = OF_DL.Models.Dtos.Highlights; @@ -15,13 +14,15 @@ using MessageDtos = OF_DL.Models.Dtos.Messages; using PostDtos = OF_DL.Models.Dtos.Posts; using PurchasedDtos = OF_DL.Models.Dtos.Purchased; using StoriesDtos = OF_DL.Models.Dtos.Stories; +using StreamsDtos = OF_DL.Models.Dtos.Streams; using ArchivedEntities = OF_DL.Models.Entities.Archived; using HighlightEntities = OF_DL.Models.Entities.Highlights; using ListEntities = OF_DL.Models.Entities.Lists; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; -using StoriesEntities = OF_DL.Models.Entities.Stories; +using StoryEntities = OF_DL.Models.Entities.Stories; +using StreamEntities = OF_DL.Models.Entities.Streams; using OF_DL.Models.Mappers; using OF_DL.Widevine; using Serilog; @@ -396,9 +397,9 @@ public class APIService(IAuthService authService, IConfigService configService, List? storiesDto = JsonConvert.DeserializeObject>(body, m_JsonSerializerSettings); - List stories = StoriesMapper.FromDto(storiesDto); + List stories = StoriesMapper.FromDto(storiesDto); - foreach (StoriesEntities.Stories story in stories) + foreach (StoryEntities.Stories story in stories) { if (story.Media[0].CreatedAt.HasValue) { @@ -417,7 +418,7 @@ public class APIService(IAuthService authService, IConfigService configService, if (story.Media != null && story.Media.Count > 0) { - foreach (StoriesEntities.Medium medium in story.Media) + foreach (StoryEntities.Medium medium in story.Media) { await dbService.AddMedia(folder, medium.Id, story.Id, medium.Files.Full.Url, null, null, null, "Stories", @@ -1208,15 +1209,16 @@ public class APIService(IAuthService authService, IConfigService configService, return null; } - public async Task GetStreams(string endpoint, string folder, List paid_post_ids, + public async Task GetStreams(string endpoint, string folder, + List paid_post_ids, StatusContext ctx) { Log.Debug($"Calling GetStreams - {endpoint}"); try { - Streams streams = new(); - StreamsCollection streamsCollection = new(); + StreamEntities.Streams streams = new(); + StreamEntities.StreamsCollection streamsCollection = new(); int post_limit = 50; Dictionary getParams = new() { @@ -1239,29 +1241,33 @@ public class APIService(IAuthService authService, IConfigService configService, configService.CurrentConfig.CustomDate); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); - streams = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.list.Count}[/]"); + StreamsDtos.StreamsDto? streamsDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + streams = StreamsMapper.FromDto(streamsDto); + ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (streams != null && streams.hasMore) + if (streams != null && streams.HasMore) { UpdateGetParamsForDateSelection( downloadDateSelection, ref getParams, - streams.tailMarker); + streams.TailMarker); while (true) { - Streams newstreams = new(); + StreamEntities.Streams newstreams = new(); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - newstreams = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + StreamsDtos.StreamsDto? newStreamsDto = + JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + newstreams = StreamsMapper.FromDto(newStreamsDto); - streams.list.AddRange(newstreams.list); - ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.list.Count}[/]"); + streams.List.AddRange(newstreams.List); + ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.List.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); - if (!newstreams.hasMore) + if (!newstreams.HasMore) { break; } @@ -1269,18 +1275,18 @@ public class APIService(IAuthService authService, IConfigService configService, UpdateGetParamsForDateSelection( downloadDateSelection, ref getParams, - newstreams.tailMarker); + newstreams.TailMarker); } } - foreach (Streams.List stream in streams.list) + foreach (StreamEntities.ListItem stream in streams.List) { List streamPreviewIds = new(); - if (stream.preview != null && stream.preview.Count > 0) + if (stream.Preview != null && stream.Preview.Count > 0) { - for (int i = 0; i < stream.preview.Count; i++) + for (int i = 0; i < stream.Preview.Count; i++) { - if (stream.preview[i] is long previewId) + if (stream.Preview[i] is long previewId) { if (!streamPreviewIds.Contains(previewId)) { @@ -1290,68 +1296,68 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, stream.id, stream.text != null ? stream.text : string.Empty, - stream.price != null ? stream.price : "0", stream.price != null && stream.isOpened ? true : false, - stream.isArchived, stream.postedAt); + await dbService.AddPost(folder, stream.Id, stream.Text != null ? stream.Text : string.Empty, + stream.Price != null ? stream.Price : "0", stream.Price != null && stream.IsOpened ? true : false, + stream.IsArchived, stream.PostedAt); streamsCollection.StreamObjects.Add(stream); - if (stream.media != null && stream.media.Count > 0) + if (stream.Media != null && stream.Media.Count > 0) { - foreach (Streams.Medium medium in stream.media) + foreach (StreamEntities.Medium medium in stream.Media) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) + if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (medium.canView && medium.files?.drm == null) + if (medium.CanView && medium.Files?.Drm == null) { - bool has = paid_post_ids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && - !string.IsNullOrEmpty(medium.files.full.url)) + bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); + if (!has && medium.CanView && medium.Files != null && medium.Files.Full != null && + !string.IsNullOrEmpty(medium.Files.Full.Url)) { - if (!streamsCollection.Streams.ContainsKey(medium.id)) + if (!streamsCollection.Streams.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, stream.id, medium.files.full.url, null, + await dbService.AddMedia(folder, medium.Id, stream.Id, medium.Files.Full.Url, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - streamPreviewIds.Contains(medium.id) ? true : false, false, null); - streamsCollection.Streams.Add(medium.id, medium.files.full.url); + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + streamPreviewIds.Contains(medium.Id) ? true : false, false, null); + streamsCollection.Streams.Add(medium.Id, medium.Files.Full.Url); streamsCollection.StreamMedia.Add(medium); } } } - else if (medium.canView && medium.files != null && medium.files.drm != null) + else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) { - bool has = paid_post_ids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.files != null && medium.files.drm != null) + bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); + if (!has && medium.Files != null && medium.Files.Drm != null) { - if (!streamsCollection.Streams.ContainsKey(medium.id)) + if (!streamsCollection.Streams.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.id, stream.id, - medium.files.drm.manifest.dash, null, null, null, "Posts", - medium.type == "photo" ? "Images" : - medium.type == "video" || medium.type == "gif" ? "Videos" : - medium.type == "audio" ? "Audios" : null, - streamPreviewIds.Contains(medium.id) ? true : false, false, null); - streamsCollection.Streams.Add(medium.id, - $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{stream.id}"); + await dbService.AddMedia(folder, medium.Id, stream.Id, + medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", + medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null, + streamPreviewIds.Contains(medium.Id) ? true : false, false, null); + streamsCollection.Streams.Add(medium.Id, + $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{stream.Id}"); streamsCollection.StreamMedia.Add(medium); } } diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 9f5e7d8..91cffc2 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -4,13 +4,13 @@ using System.Xml.Linq; using FFmpeg.NET; using FFmpeg.NET.Events; using OF_DL.Models; -using OF_DL.Models.Streams; using OF_DL.Enumerations; using ArchivedEntities = OF_DL.Models.Entities.Archived; using CommonEntities = OF_DL.Models.Entities.Common; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; +using StreamEntities = OF_DL.Models.Entities.Streams; using OF_DL.Utils; using Serilog; using Serilog.Events; @@ -1012,14 +1012,15 @@ public class DownloadService( } public async Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, - Streams.Medium? streamMedia, Streams.Author? author, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, StreamEntities.ListItem? streamInfo, + StreamEntities.Medium? streamMedia, CommonEntities.Author? author, + Dictionary users) { string path; - if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.id is not null && - streamInfo?.postedAt is not null) + if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.Id is not null && + streamInfo?.PostedAt is not null) { - path = $"/Posts/Free/{streamInfo.id} {streamInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + path = $"/Posts/Free/{streamInfo.Id} {streamInfo.PostedAt:yyyy-MM-dd HH-mm-ss}"; } else { @@ -1967,8 +1968,9 @@ public class DownloadService( public async Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, - Streams.Medium? streamMedia, Streams.Author? author, Dictionary users) + IProgressReporter progressReporter, string? filenameFormat, StreamEntities.ListItem? streamInfo, + StreamEntities.Medium? streamMedia, CommonEntities.Author? author, + Dictionary users) { try { @@ -1976,10 +1978,10 @@ public class DownloadService( string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.id is not null && - streamInfo?.postedAt is not null) + if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.Id is not null && + streamInfo?.PostedAt is not null) { - path = $"/Posts/Free/{streamInfo.id} {streamInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + path = $"/Posts/Free/{streamInfo.Id} {streamInfo.PostedAt:yyyy-MM-dd HH-mm-ss}/Videos"; } else { @@ -2809,7 +2811,7 @@ public class DownloadService( public async Task DownloadStreams(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, - StreamsCollection streams, IProgressReporter progressReporter) + StreamEntities.StreamsCollection streams, IProgressReporter progressReporter) { Log.Debug($"Calling DownloadStreams - {username}"); @@ -2853,14 +2855,14 @@ public class DownloadService( $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", pssh); - Streams.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.id == kvp.Key); - Streams.List? streamInfo = - streams.StreamObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + StreamEntities.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.Id == kvp.Key); + StreamEntities.ListItem? streamInfo = + streams.StreamObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadStreamsDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, lastModified, kvp.Key, "Streams", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, streamInfo, mediaInfo, streamInfo?.author, users); + string.Empty, streamInfo, mediaInfo, streamInfo?.Author, users); } else { @@ -2869,12 +2871,12 @@ public class DownloadService( } else { - Streams.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.id == kvp.Key); - Streams.List? streamInfo = - streams.StreamObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + StreamEntities.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.Id == kvp.Key); + StreamEntities.ListItem? streamInfo = + streams.StreamObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadStreamMedia(kvp.Value, path, kvp.Key, "Streams", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, streamInfo, mediaInfo, streamInfo?.author, users); + string.Empty, streamInfo, mediaInfo, streamInfo?.Author, users); } if (isNew) diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index 14a487c..83671fa 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -1,11 +1,11 @@ using Newtonsoft.Json.Linq; using OF_DL.Models; -using OF_DL.Models.Streams; using OF_DL.Enumerations; -using OF_DL.Models.Entities.Purchased; using ArchivedEntities = OF_DL.Models.Entities.Archived; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; +using PurchasedEntities = OF_DL.Models.Entities.Purchased; +using StreamEntities = OF_DL.Models.Entities.Streams; using Spectre.Console; namespace OF_DL.Services; @@ -22,20 +22,30 @@ public interface IAPIService Task> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, List paid_post_ids); - Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, + Task GetPaidPosts(string endpoint, string folder, string username, + List paid_post_ids, StatusContext ctx); Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx); Task GetPost(string endpoint, string folder); - Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx); + + Task GetStreams(string endpoint, string folder, List paid_post_ids, + StatusContext ctx); + Task GetArchived(string endpoint, string folder, StatusContext ctx); Task GetMessages(string endpoint, string folder, StatusContext ctx); - Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx); - Task GetPaidMessage(string endpoint, string folder); + + Task GetPaidMessages(string endpoint, string folder, string username, + StatusContext ctx); + + Task GetPaidMessage(string endpoint, string folder); Task> GetPurchasedTabUsers(string endpoint, Dictionary users); - Task> GetPurchasedTab(string endpoint, string folder, Dictionary users); + + Task> GetPurchasedTab(string endpoint, string folder, + Dictionary users); + Task GetUserInfo(string endpoint); Task GetUserInfoById(string endpoint); Dictionary GetDynamicHeaders(string path, string queryParam); diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index d0979ba..14f5f58 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -1,10 +1,10 @@ using OF_DL.Models; -using OF_DL.Models.Streams; using ArchivedEntities = OF_DL.Models.Entities.Archived; using CommonEntities = OF_DL.Models.Entities.Common; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; +using StreamEntities = OF_DL.Models.Entities.Streams; namespace OF_DL.Services; @@ -95,12 +95,14 @@ public interface IDownloadService IProgressReporter progressReporter); Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, - Streams.Medium? streamMedia, Streams.Author? author, Dictionary users); + IProgressReporter progressReporter, string? filenameFormat, StreamEntities.ListItem? streamInfo, + StreamEntities.Medium? streamMedia, CommonEntities.Author? author, + Dictionary users); Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, - string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, + string? filenameFormat, StreamEntities.ListItem? streamInfo, StreamEntities.Medium? streamMedia, + CommonEntities.Author? author, Dictionary users); Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, @@ -132,7 +134,7 @@ public interface IDownloadService IProgressReporter progressReporter); Task DownloadStreams(string username, long userId, string path, Dictionary users, - bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamsCollection streams, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamEntities.StreamsCollection streams, IProgressReporter progressReporter); Task DownloadFreePosts(string username, long userId, string path, Dictionary users, From a9b135636b71c00047cc279c6779714419541c50 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 00:31:52 -0600 Subject: [PATCH 21/55] Refactor Users entities into DTOs and application entities with standardized naming conventions and default values --- OF DL/Models/Dtos/Users/ListsStateDto.cs | 16 ++ OF DL/Models/Dtos/Users/SubscribeDto.cs | 38 ++++ .../Models/Dtos/Users/SubscribedByDataDto.cs | 40 ++++ .../Models/Dtos/Users/SubscribedOnDataDto.cs | 50 +++++ OF DL/Models/Dtos/Users/UserDto.cs | 179 +++++++++++++++++ OF DL/Models/Entities/Users/User.cs | 12 ++ OF DL/Models/Mappers/UserMapper.cs | 11 ++ .../{ => Subscriptions}/Subscriptions.cs | 2 +- OF DL/Models/User.cs | 185 ------------------ OF DL/Program.cs | 13 +- OF DL/Services/APIService.cs | 11 +- OF DL/Services/IAPIService.cs | 4 +- 12 files changed, 364 insertions(+), 197 deletions(-) create mode 100644 OF DL/Models/Dtos/Users/ListsStateDto.cs create mode 100644 OF DL/Models/Dtos/Users/SubscribeDto.cs create mode 100644 OF DL/Models/Dtos/Users/SubscribedByDataDto.cs create mode 100644 OF DL/Models/Dtos/Users/SubscribedOnDataDto.cs create mode 100644 OF DL/Models/Dtos/Users/UserDto.cs create mode 100644 OF DL/Models/Entities/Users/User.cs create mode 100644 OF DL/Models/Mappers/UserMapper.cs rename OF DL/Models/{ => Subscriptions}/Subscriptions.cs (99%) delete mode 100644 OF DL/Models/User.cs diff --git a/OF DL/Models/Dtos/Users/ListsStateDto.cs b/OF DL/Models/Dtos/Users/ListsStateDto.cs new file mode 100644 index 0000000..ce6952f --- /dev/null +++ b/OF DL/Models/Dtos/Users/ListsStateDto.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Users; + +public class ListsStateDto +{ + [JsonProperty("id")] public string Id { get; set; } = ""; + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("name")] public string Name { get; set; } = ""; + + [JsonProperty("hasUser")] public bool HasUser { get; set; } + + [JsonProperty("canAddUser")] public bool CanAddUser { get; set; } +} diff --git a/OF DL/Models/Dtos/Users/SubscribeDto.cs b/OF DL/Models/Dtos/Users/SubscribeDto.cs new file mode 100644 index 0000000..d6ff5a9 --- /dev/null +++ b/OF DL/Models/Dtos/Users/SubscribeDto.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Users; + +public class SubscribeDto +{ + [JsonProperty("id")] public long? Id { get; set; } + + [JsonProperty("userId")] public long? UserId { get; set; } + + [JsonProperty("subscriberId")] public int? SubscriberId { get; set; } + + [JsonProperty("date")] public DateTime? Date { get; set; } + + [JsonProperty("duration")] public int? Duration { get; set; } + + [JsonProperty("startDate")] public DateTime? StartDate { get; set; } + + [JsonProperty("expireDate")] public DateTime? ExpireDate { get; set; } + + [JsonProperty("cancelDate")] public object CancelDate { get; set; } = new(); + + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("regularPrice")] public string? RegularPrice { get; set; } + + [JsonProperty("discount")] public int? Discount { get; set; } + + [JsonProperty("action")] public string Action { get; set; } = ""; + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("offerStart")] public object OfferStart { get; set; } = new(); + + [JsonProperty("offerEnd")] public object OfferEnd { get; set; } = new(); + + [JsonProperty("isCurrent")] public bool? IsCurrent { get; set; } +} diff --git a/OF DL/Models/Dtos/Users/SubscribedByDataDto.cs b/OF DL/Models/Dtos/Users/SubscribedByDataDto.cs new file mode 100644 index 0000000..1ec6f04 --- /dev/null +++ b/OF DL/Models/Dtos/Users/SubscribedByDataDto.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Users; + +public class SubscribedByDataDto +{ + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("newPrice")] public string? NewPrice { get; set; } + + [JsonProperty("regularPrice")] public string? RegularPrice { get; set; } + + [JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; } + + [JsonProperty("discountPercent")] public int? DiscountPercent { get; set; } + + [JsonProperty("discountPeriod")] public int? DiscountPeriod { get; set; } + + [JsonProperty("subscribeAt")] public DateTime? SubscribeAt { get; set; } + + [JsonProperty("expiredAt")] public DateTime? ExpiredAt { get; set; } + + [JsonProperty("renewedAt")] public object? RenewedAt { get; set; } + + [JsonProperty("discountFinishedAt")] public object? DiscountFinishedAt { get; set; } + + [JsonProperty("discountStartedAt")] public object? DiscountStartedAt { get; set; } + + [JsonProperty("status")] public string? Status { get; set; } + + [JsonProperty("isMuted")] public bool? IsMuted { get; set; } + + [JsonProperty("unsubscribeReason")] public string? UnsubscribeReason { get; set; } + + [JsonProperty("duration")] public string? Duration { get; set; } + + [JsonProperty("showPostsInFeed")] public bool? ShowPostsInFeed { get; set; } + + [JsonProperty("subscribes")] public List? Subscribes { get; set; } +} diff --git a/OF DL/Models/Dtos/Users/SubscribedOnDataDto.cs b/OF DL/Models/Dtos/Users/SubscribedOnDataDto.cs new file mode 100644 index 0000000..d373156 --- /dev/null +++ b/OF DL/Models/Dtos/Users/SubscribedOnDataDto.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Users; + +public class SubscribedOnDataDto +{ + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("newPrice")] public string? NewPrice { get; set; } + + [JsonProperty("regularPrice")] public string? RegularPrice { get; set; } + + [JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; } + + [JsonProperty("discountPercent")] public int? DiscountPercent { get; set; } + + [JsonProperty("discountPeriod")] public int? DiscountPeriod { get; set; } + + [JsonProperty("subscribeAt")] public DateTime? SubscribeAt { get; set; } + + [JsonProperty("expiredAt")] public DateTime? ExpiredAt { get; set; } + + [JsonProperty("renewedAt")] public DateTime? RenewedAt { get; set; } + + [JsonProperty("discountFinishedAt")] public object? DiscountFinishedAt { get; set; } + + [JsonProperty("discountStartedAt")] public object? DiscountStartedAt { get; set; } + + [JsonProperty("status")] public object? Status { get; set; } + + [JsonProperty("isMuted")] public bool? IsMuted { get; set; } + + [JsonProperty("unsubscribeReason")] public string? UnsubscribeReason { get; set; } + + [JsonProperty("duration")] public string? Duration { get; set; } + + [JsonProperty("tipsSumm")] public string? TipsSumm { get; set; } + + [JsonProperty("subscribesSumm")] public string? SubscribesSumm { get; set; } + + [JsonProperty("messagesSumm")] public string? MessagesSumm { get; set; } + + [JsonProperty("postsSumm")] public string? PostsSumm { get; set; } + + [JsonProperty("streamsSumm")] public string? StreamsSumm { get; set; } + + [JsonProperty("totalSumm")] public string? TotalSumm { get; set; } + + [JsonProperty("subscribes")] public List? Subscribes { get; set; } +} diff --git a/OF DL/Models/Dtos/Users/UserDto.cs b/OF DL/Models/Dtos/Users/UserDto.cs new file mode 100644 index 0000000..cc1ca3b --- /dev/null +++ b/OF DL/Models/Dtos/Users/UserDto.cs @@ -0,0 +1,179 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Users; + +public class UserDto +{ + [JsonProperty("view")] public string View { get; set; } = ""; + + [JsonProperty("avatar")] public string? Avatar { get; set; } + + [JsonProperty("avatarThumbs")] public AvatarThumbsDto AvatarThumbs { get; set; } = new(); + + [JsonProperty("header")] public string? Header { get; set; } + + [JsonProperty("headerSize")] public HeaderSizeDto HeaderSize { get; set; } = new(); + + [JsonProperty("headerThumbs")] public HeaderThumbsDto HeaderThumbs { get; set; } = new(); + + [JsonProperty("id")] public long? Id { get; set; } + + [JsonProperty("name")] public string? Name { get; set; } + + [JsonProperty("username")] public string? Username { get; set; } + + [JsonProperty("canLookStory")] public bool? CanLookStory { get; set; } + + [JsonProperty("canCommentStory")] public bool? CanCommentStory { get; set; } + + [JsonProperty("hasNotViewedStory")] public bool? HasNotViewedStory { get; set; } + + [JsonProperty("isVerified")] public bool? IsVerified { get; set; } + + [JsonProperty("canPayInternal")] public bool? CanPayInternal { get; set; } + + [JsonProperty("hasScheduledStream")] public bool? HasScheduledStream { get; set; } + + [JsonProperty("hasStream")] public bool? HasStream { get; set; } + + [JsonProperty("hasStories")] public bool? HasStories { get; set; } + + [JsonProperty("tipsEnabled")] public bool? TipsEnabled { get; set; } + + [JsonProperty("tipsTextEnabled")] public bool? TipsTextEnabled { get; set; } + + [JsonProperty("tipsMin")] public int? TipsMin { get; set; } + + [JsonProperty("tipsMinInternal")] public int? TipsMinInternal { get; set; } + + [JsonProperty("tipsMax")] public int? TipsMax { get; set; } + + [JsonProperty("canEarn")] public bool? CanEarn { get; set; } + + [JsonProperty("canAddSubscriber")] public bool? CanAddSubscriber { get; set; } + + [JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; } + + [JsonProperty("displayName")] public string DisplayName { get; set; } = ""; + + [JsonProperty("notice")] public string Notice { get; set; } = ""; + + [JsonProperty("isPaywallRequired")] public bool? IsPaywallRequired { get; set; } + + [JsonProperty("unprofitable")] public bool? Unprofitable { get; set; } + + [JsonProperty("listsStates")] public List ListsStates { get; set; } = []; + + [JsonProperty("isMuted")] public bool? IsMuted { get; set; } + + [JsonProperty("isRestricted")] public bool? IsRestricted { get; set; } + + [JsonProperty("canRestrict")] public bool? CanRestrict { get; set; } + + [JsonProperty("subscribedBy")] public bool? SubscribedBy { get; set; } + + [JsonProperty("subscribedByExpire")] public bool? SubscribedByExpire { get; set; } + + [JsonProperty("subscribedByExpireDate")] public DateTime? SubscribedByExpireDate { get; set; } + + [JsonProperty("subscribedByAutoprolong")] public bool? SubscribedByAutoprolong { get; set; } + + [JsonProperty("subscribedIsExpiredNow")] public bool? SubscribedIsExpiredNow { get; set; } + + [JsonProperty("currentSubscribePrice")] public string? CurrentSubscribePrice { get; set; } + + [JsonProperty("subscribedOn")] public bool? SubscribedOn { get; set; } + + [JsonProperty("subscribedOnExpiredNow")] public bool? SubscribedOnExpiredNow { get; set; } + + [JsonProperty("subscribedOnDuration")] public string SubscribedOnDuration { get; set; } = ""; + + [JsonProperty("joinDate")] public DateTime? JoinDate { get; set; } + + [JsonProperty("isReferrerAllowed")] public bool? IsReferrerAllowed { get; set; } + + [JsonProperty("about")] public string About { get; set; } = ""; + + [JsonProperty("rawAbout")] public string RawAbout { get; set; } = ""; + + [JsonProperty("website")] public object Website { get; set; } = new(); + + [JsonProperty("wishlist")] public object Wishlist { get; set; } = new(); + + [JsonProperty("location")] public object Location { get; set; } = new(); + + [JsonProperty("postsCount")] public int? PostsCount { get; set; } + + [JsonProperty("archivedPostsCount")] public int? ArchivedPostsCount { get; set; } + + [JsonProperty("privateArchivedPostsCount")] public int? PrivateArchivedPostsCount { get; set; } + + [JsonProperty("photosCount")] public int? PhotosCount { get; set; } + + [JsonProperty("videosCount")] public int? VideosCount { get; set; } + + [JsonProperty("audiosCount")] public int? AudiosCount { get; set; } + + [JsonProperty("mediasCount")] public int? MediasCount { get; set; } + + [JsonProperty("lastSeen")] public DateTime? LastSeen { get; set; } + + [JsonProperty("favoritesCount")] public int? FavoritesCount { get; set; } + + [JsonProperty("favoritedCount")] public int? FavoritedCount { get; set; } + + [JsonProperty("showPostsInFeed")] public bool? ShowPostsInFeed { get; set; } + + [JsonProperty("canReceiveChatMessage")] public bool? CanReceiveChatMessage { get; set; } + + [JsonProperty("isPerformer")] public bool? IsPerformer { get; set; } + + [JsonProperty("isRealPerformer")] public bool? IsRealPerformer { get; set; } + + [JsonProperty("isSpotifyConnected")] public bool? IsSpotifyConnected { get; set; } + + [JsonProperty("subscribersCount")] public int? SubscribersCount { get; set; } + + [JsonProperty("hasPinnedPosts")] public bool? HasPinnedPosts { get; set; } + + [JsonProperty("hasLabels")] public bool? HasLabels { get; set; } + + [JsonProperty("canChat")] public bool? CanChat { get; set; } + + [JsonProperty("callPrice")] public string? CallPrice { get; set; } + + [JsonProperty("isPrivateRestriction")] public bool? IsPrivateRestriction { get; set; } + + [JsonProperty("showSubscribersCount")] public bool? ShowSubscribersCount { get; set; } + + [JsonProperty("showMediaCount")] public bool? ShowMediaCount { get; set; } + + [JsonProperty("subscribedByData")] public SubscribedByDataDto SubscribedByData { get; set; } = new(); + + [JsonProperty("subscribedOnData")] public SubscribedOnDataDto SubscribedOnData { get; set; } = new(); + + [JsonProperty("canPromotion")] public bool? CanPromotion { get; set; } + + [JsonProperty("canCreatePromotion")] public bool? CanCreatePromotion { get; set; } + + [JsonProperty("canCreateTrial")] public bool? CanCreateTrial { get; set; } + + [JsonProperty("isAdultContent")] public bool? IsAdultContent { get; set; } + + [JsonProperty("canTrialSend")] public bool? CanTrialSend { get; set; } + + [JsonProperty("hadEnoughLastPhotos")] public bool? HadEnoughLastPhotos { get; set; } + + [JsonProperty("hasLinks")] public bool? HasLinks { get; set; } + + [JsonProperty("firstPublishedPostDate")] public DateTime? FirstPublishedPostDate { get; set; } + + [JsonProperty("isSpringConnected")] public bool? IsSpringConnected { get; set; } + + [JsonProperty("isFriend")] public bool? IsFriend { get; set; } + + [JsonProperty("isBlocked")] public bool? IsBlocked { get; set; } + + [JsonProperty("canReport")] public bool? CanReport { get; set; } +} diff --git a/OF DL/Models/Entities/Users/User.cs b/OF DL/Models/Entities/Users/User.cs new file mode 100644 index 0000000..270cf25 --- /dev/null +++ b/OF DL/Models/Entities/Users/User.cs @@ -0,0 +1,12 @@ +namespace OF_DL.Models.Entities.Users; + +public class User +{ + public string? Avatar { get; set; } + + public string? Header { get; set; } + + public string? Name { get; set; } + + public string? Username { get; set; } +} diff --git a/OF DL/Models/Mappers/UserMapper.cs b/OF DL/Models/Mappers/UserMapper.cs new file mode 100644 index 0000000..c724b20 --- /dev/null +++ b/OF DL/Models/Mappers/UserMapper.cs @@ -0,0 +1,11 @@ +using OF_DL.Models.Dtos.Users; +using OF_DL.Models.Entities.Users; + +namespace OF_DL.Models.Mappers; + +public static class UserMapper +{ + public static User? FromDto(UserDto? dto) => dto == null + ? null + : new User { Avatar = dto.Avatar, Header = dto.Header, Name = dto.Name, Username = dto.Username }; +} diff --git a/OF DL/Models/Subscriptions.cs b/OF DL/Models/Subscriptions/Subscriptions.cs similarity index 99% rename from OF DL/Models/Subscriptions.cs rename to OF DL/Models/Subscriptions/Subscriptions.cs index 5ae8a43..6903743 100644 --- a/OF DL/Models/Subscriptions.cs +++ b/OF DL/Models/Subscriptions/Subscriptions.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models; +namespace OF_DL.Models.Subscriptions; public class Subscriptions { diff --git a/OF DL/Models/User.cs b/OF DL/Models/User.cs deleted file mode 100644 index 5ea7d48..0000000 --- a/OF DL/Models/User.cs +++ /dev/null @@ -1,185 +0,0 @@ -namespace OF_DL.Models; - -public class User -{ - public string view { get; set; } - public string? avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string? header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long? id { get; set; } - public string name { get; set; } - public string username { get; set; } - public bool? canLookStory { get; set; } - public bool? canCommentStory { get; set; } - public bool? hasNotViewedStory { get; set; } - public bool? isVerified { get; set; } - public bool? canPayInternal { get; set; } - public bool? hasScheduledStream { get; set; } - public bool? hasStream { get; set; } - public bool? hasStories { get; set; } - public bool? tipsEnabled { get; set; } - public bool? tipsTextEnabled { get; set; } - public int? tipsMin { get; set; } - public int? tipsMinInternal { get; set; } - public int? tipsMax { get; set; } - public bool? canEarn { get; set; } - public bool? canAddSubscriber { get; set; } - public string? subscribePrice { get; set; } - public string displayName { get; set; } - public string notice { get; set; } - public bool? isPaywallRequired { get; set; } - public bool? unprofitable { get; set; } - public List listsStates { get; set; } - public bool? isMuted { get; set; } - public bool? isRestricted { get; set; } - public bool? canRestrict { get; set; } - public bool? subscribedBy { get; set; } - public bool? subscribedByExpire { get; set; } - public DateTime? subscribedByExpireDate { get; set; } - public bool? subscribedByAutoprolong { get; set; } - public bool? subscribedIsExpiredNow { get; set; } - public string? currentSubscribePrice { get; set; } - public bool? subscribedOn { get; set; } - public bool? subscribedOnExpiredNow { get; set; } - public string subscribedOnDuration { get; set; } - public DateTime? joinDate { get; set; } - public bool? isReferrerAllowed { get; set; } - public string about { get; set; } - public string rawAbout { get; set; } - public object website { get; set; } - public object wishlist { get; set; } - public object location { get; set; } - public int? postsCount { get; set; } - public int? archivedPostsCount { get; set; } - public int? privateArchivedPostsCount { get; set; } - public int? photosCount { get; set; } - public int? videosCount { get; set; } - public int? audiosCount { get; set; } - public int? mediasCount { get; set; } - public DateTime? lastSeen { get; set; } - public int? favoritesCount { get; set; } - public int? favoritedCount { get; set; } - public bool? showPostsInFeed { get; set; } - public bool? canReceiveChatMessage { get; set; } - public bool? isPerformer { get; set; } - public bool? isRealPerformer { get; set; } - public bool? isSpotifyConnected { get; set; } - public int? subscribersCount { get; set; } - public bool? hasPinnedPosts { get; set; } - public bool? hasLabels { get; set; } - public bool? canChat { get; set; } - public string? callPrice { get; set; } - public bool? isPrivateRestriction { get; set; } - public bool? showSubscribersCount { get; set; } - public bool? showMediaCount { get; set; } - public SubscribedByData subscribedByData { get; set; } - public SubscribedOnData subscribedOnData { get; set; } - public bool? canPromotion { get; set; } - public bool? canCreatePromotion { get; set; } - public bool? canCreateTrial { get; set; } - public bool? isAdultContent { get; set; } - public bool? canTrialSend { get; set; } - public bool? hadEnoughLastPhotos { get; set; } - public bool? hasLinks { get; set; } - public DateTime? firstPublishedPostDate { get; set; } - public bool? isSpringConnected { get; set; } - public bool? isFriend { get; set; } - public bool? isBlocked { get; set; } - public bool? canReport { get; set; } - - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } - - public class HeaderSize - { - public int? width { get; set; } - public int? height { get; set; } - } - - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } - - public class ListsState - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool hasUser { get; set; } - public bool canAddUser { get; set; } - } - - public class Subscribe - { - public long? id { get; set; } - public long? userId { get; set; } - public int? subscriberId { get; set; } - public DateTime? date { get; set; } - public int? duration { get; set; } - public DateTime? startDate { get; set; } - public DateTime? expireDate { get; set; } - public object cancelDate { get; set; } - public string? price { get; set; } - public string? regularPrice { get; set; } - public int? discount { get; set; } - public string action { get; set; } - public string type { get; set; } - public object offerStart { get; set; } - public object offerEnd { get; set; } - public bool? isCurrent { get; set; } - } - - public class SubscribedByData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public object? renewedAt { get; set; } - public object? discountFinishedAt { get; set; } - public object? discountStartedAt { get; set; } - public string? status { get; set; } - public bool? isMuted { get; set; } - public string? unsubscribeReason { get; set; } - public string? duration { get; set; } - public bool? showPostsInFeed { get; set; } - public List? subscribes { get; set; } - } - - public class SubscribedOnData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public DateTime? renewedAt { get; set; } - public object? discountFinishedAt { get; set; } - public object? discountStartedAt { get; set; } - public object? status { get; set; } - public bool? isMuted { get; set; } - public string? unsubscribeReason { get; set; } - public string? duration { get; set; } - public string? tipsSumm { get; set; } - public string? subscribesSumm { get; set; } - public string? messagesSumm { get; set; } - public string? postsSumm { get; set; } - public string? streamsSumm { get; set; } - public string? totalSumm { get; set; } - public List? subscribes { get; set; } - } -} diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 629037f..1e2e770 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -14,6 +14,7 @@ using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; using StreamEntities = OF_DL.Models.Entities.Streams; +using UserEntities = OF_DL.Models.Entities.Users; using OF_DL.Services; using Serilog; using Spectre.Console; @@ -534,8 +535,8 @@ public class Program(IServiceProvider serviceProvider) "[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[/]"); } - User? validate = await apiService.GetUserInfo("/users/me"); - if (validate == null || (validate?.name == null && validate?.username == null)) + UserEntities.User? validate = await apiService.GetUserInfo("/users/me"); + if (validate == null || (validate?.Name == null && validate?.Username == null)) { Log.Error("Auth failed"); @@ -563,7 +564,7 @@ public class Program(IServiceProvider serviceProvider) } } - AnsiConsole.Markup($"[green]Logged In successfully as {validate.name} {validate.username}\n[/]"); + AnsiConsole.Markup($"[green]Logged In successfully as {validate.Name} {validate.Username}\n[/]"); await DownloadAllData(); } catch (Exception ex) @@ -784,7 +785,7 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Folder for {user.Key} already created"); } - User user_info = await apiService.GetUserInfo($"/users/{user.Key}"); + UserEntities.User user_info = await apiService.GetUserInfo($"/users/{user.Key}"); await dbService.CreateDB(path); } @@ -967,10 +968,10 @@ public class Program(IServiceProvider serviceProvider) if (Config.DownloadAvatarHeaderPhoto) { - User? user_info = await apiService.GetUserInfo($"/users/{user.Key}"); + UserEntities.User? user_info = await apiService.GetUserInfo($"/users/{user.Key}"); if (user_info != null) { - await downloadService.DownloadAvatarHeader(user_info.avatar, user_info.header, path, + await downloadService.DownloadAvatarHeader(user_info.Avatar, user_info.Header, path, user.Key); } } diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index f2e20c0..4260832 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -15,6 +15,7 @@ using PostDtos = OF_DL.Models.Dtos.Posts; using PurchasedDtos = OF_DL.Models.Dtos.Purchased; using StoriesDtos = OF_DL.Models.Dtos.Stories; using StreamsDtos = OF_DL.Models.Dtos.Streams; +using UserDtos = OF_DL.Models.Dtos.Users; using ArchivedEntities = OF_DL.Models.Entities.Archived; using HighlightEntities = OF_DL.Models.Entities.Highlights; using ListEntities = OF_DL.Models.Entities.Lists; @@ -23,7 +24,9 @@ using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; using StoryEntities = OF_DL.Models.Entities.Stories; using StreamEntities = OF_DL.Models.Entities.Streams; +using UserEntities = OF_DL.Models.Entities.Users; using OF_DL.Models.Mappers; +using OF_DL.Models.Subscriptions; using OF_DL.Widevine; using Serilog; using Spectre.Console; @@ -115,13 +118,13 @@ public class APIService(IAuthService authService, IConfigService configService, } - public async Task GetUserInfo(string endpoint) + public async Task GetUserInfo(string endpoint) { Log.Debug($"Calling GetUserInfo: {endpoint}"); try { - User? user = new(); + UserEntities.User? user = new(); int post_limit = 50; Dictionary getParams = new() { @@ -140,7 +143,9 @@ public class APIService(IAuthService authService, IConfigService configService, response.EnsureSuccessStatusCode(); string body = await response.Content.ReadAsStringAsync(); - user = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + UserDtos.UserDto? userDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + user = UserMapper.FromDto(userDto) ?? new UserEntities.User(); return user; } catch (Exception ex) diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index 83671fa..a14c024 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -1,11 +1,11 @@ using Newtonsoft.Json.Linq; -using OF_DL.Models; using OF_DL.Enumerations; using ArchivedEntities = OF_DL.Models.Entities.Archived; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; using StreamEntities = OF_DL.Models.Entities.Streams; +using UserEntities = OF_DL.Models.Entities.Users; using Spectre.Console; namespace OF_DL.Services; @@ -46,7 +46,7 @@ public interface IAPIService Task> GetPurchasedTab(string endpoint, string folder, Dictionary users); - Task GetUserInfo(string endpoint); + Task GetUserInfo(string endpoint); Task GetUserInfoById(string endpoint); Dictionary GetDynamicHeaders(string path, string queryParam); Task> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions); From cd5c22d86234929bc82bd17f0ed75e24ed4dbb5c Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 00:46:19 -0600 Subject: [PATCH 22/55] Refactor Subscriptions entities into DTOs and application entities with standardized naming conventions and default values --- .../{Users => Common}/SubscribedByDataDto.cs | 20 ++- .../{Users => Common}/SubscribedOnDataDto.cs | 16 +- .../Models/Dtos/Subscriptions/ListItemDto.cs | 111 ++++++++++++ .../Dtos/Subscriptions/ListsStateDto.cs | 16 ++ .../Models/Dtos/Subscriptions/SubscribeDto.cs | 38 +++++ .../Dtos/Subscriptions/SubscriptionsDto.cs | 10 ++ .../Models/Entities/Subscriptions/ListItem.cs | 10 ++ .../Entities/Subscriptions/Subscriptions.cs | 8 + OF DL/Models/Mappers/SubscriptionsMapper.cs | 27 +++ OF DL/Models/Subscriptions/Subscriptions.cs | 159 ------------------ OF DL/Services/APIService.cs | 37 ++-- 11 files changed, 263 insertions(+), 189 deletions(-) rename OF DL/Models/Dtos/{Users => Common}/SubscribedByDataDto.cs (59%) rename OF DL/Models/Dtos/{Users => Common}/SubscribedOnDataDto.cs (77%) create mode 100644 OF DL/Models/Dtos/Subscriptions/ListItemDto.cs create mode 100644 OF DL/Models/Dtos/Subscriptions/ListsStateDto.cs create mode 100644 OF DL/Models/Dtos/Subscriptions/SubscribeDto.cs create mode 100644 OF DL/Models/Dtos/Subscriptions/SubscriptionsDto.cs create mode 100644 OF DL/Models/Entities/Subscriptions/ListItem.cs create mode 100644 OF DL/Models/Entities/Subscriptions/Subscriptions.cs create mode 100644 OF DL/Models/Mappers/SubscriptionsMapper.cs delete mode 100644 OF DL/Models/Subscriptions/Subscriptions.cs diff --git a/OF DL/Models/Dtos/Users/SubscribedByDataDto.cs b/OF DL/Models/Dtos/Common/SubscribedByDataDto.cs similarity index 59% rename from OF DL/Models/Dtos/Users/SubscribedByDataDto.cs rename to OF DL/Models/Dtos/Common/SubscribedByDataDto.cs index 1ec6f04..f1bc4d9 100644 --- a/OF DL/Models/Dtos/Users/SubscribedByDataDto.cs +++ b/OF DL/Models/Dtos/Common/SubscribedByDataDto.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; +using OF_DL.Models.Dtos.Subscriptions; -namespace OF_DL.Models.Dtos.Users; +namespace OF_DL.Models.Dtos.Common; public class SubscribedByDataDto { @@ -20,21 +21,24 @@ public class SubscribedByDataDto [JsonProperty("expiredAt")] public DateTime? ExpiredAt { get; set; } - [JsonProperty("renewedAt")] public object? RenewedAt { get; set; } + [JsonProperty("renewedAt")] public DateTime? RenewedAt { get; set; } - [JsonProperty("discountFinishedAt")] public object? DiscountFinishedAt { get; set; } + [JsonProperty("discountFinishedAt")] public object? DiscountFinishedAt { get; set; } = new(); - [JsonProperty("discountStartedAt")] public object? DiscountStartedAt { get; set; } + [JsonProperty("discountStartedAt")] public object? DiscountStartedAt { get; set; } = new(); - [JsonProperty("status")] public string? Status { get; set; } + [JsonProperty("status")] public string Status { get; set; } = ""; [JsonProperty("isMuted")] public bool? IsMuted { get; set; } - [JsonProperty("unsubscribeReason")] public string? UnsubscribeReason { get; set; } + [JsonProperty("unsubscribeReason")] public string UnsubscribeReason { get; set; } = ""; - [JsonProperty("duration")] public string? Duration { get; set; } + [JsonProperty("duration")] public string Duration { get; set; } = ""; [JsonProperty("showPostsInFeed")] public bool? ShowPostsInFeed { get; set; } - [JsonProperty("subscribes")] public List? Subscribes { get; set; } + [JsonProperty("subscribes")] public List Subscribes { get; set; } = []; + + [JsonProperty("hasActivePaidSubscriptions")] + public bool? HasActivePaidSubscriptions { get; set; } } diff --git a/OF DL/Models/Dtos/Users/SubscribedOnDataDto.cs b/OF DL/Models/Dtos/Common/SubscribedOnDataDto.cs similarity index 77% rename from OF DL/Models/Dtos/Users/SubscribedOnDataDto.cs rename to OF DL/Models/Dtos/Common/SubscribedOnDataDto.cs index d373156..b0c0cee 100644 --- a/OF DL/Models/Dtos/Users/SubscribedOnDataDto.cs +++ b/OF DL/Models/Dtos/Common/SubscribedOnDataDto.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; +using OF_DL.Models.Dtos.Subscriptions; -namespace OF_DL.Models.Dtos.Users; +namespace OF_DL.Models.Dtos.Common; public class SubscribedOnDataDto { @@ -22,17 +23,17 @@ public class SubscribedOnDataDto [JsonProperty("renewedAt")] public DateTime? RenewedAt { get; set; } - [JsonProperty("discountFinishedAt")] public object? DiscountFinishedAt { get; set; } + [JsonProperty("discountFinishedAt")] public object? DiscountFinishedAt { get; set; } = new(); - [JsonProperty("discountStartedAt")] public object? DiscountStartedAt { get; set; } + [JsonProperty("discountStartedAt")] public object? DiscountStartedAt { get; set; } = new(); [JsonProperty("status")] public object? Status { get; set; } [JsonProperty("isMuted")] public bool? IsMuted { get; set; } - [JsonProperty("unsubscribeReason")] public string? UnsubscribeReason { get; set; } + [JsonProperty("unsubscribeReason")] public string? UnsubscribeReason { get; set; } = ""; - [JsonProperty("duration")] public string? Duration { get; set; } + [JsonProperty("duration")] public string Duration { get; set; } = ""; [JsonProperty("tipsSumm")] public string? TipsSumm { get; set; } @@ -46,5 +47,8 @@ public class SubscribedOnDataDto [JsonProperty("totalSumm")] public string? TotalSumm { get; set; } - [JsonProperty("subscribes")] public List? Subscribes { get; set; } + [JsonProperty("subscribes")] public List Subscribes { get; set; } = []; + + [JsonProperty("hasActivePaidSubscriptions")] + public bool? HasActivePaidSubscriptions { get; set; } } diff --git a/OF DL/Models/Dtos/Subscriptions/ListItemDto.cs b/OF DL/Models/Dtos/Subscriptions/ListItemDto.cs new file mode 100644 index 0000000..8b60c38 --- /dev/null +++ b/OF DL/Models/Dtos/Subscriptions/ListItemDto.cs @@ -0,0 +1,111 @@ +using Newtonsoft.Json; +using OF_DL.Models.Dtos.Common; + +namespace OF_DL.Models.Dtos.Subscriptions; + +public class ListItemDto +{ + [JsonProperty("view")] public string View { get; set; } = ""; + + [JsonProperty("avatar")] public string? Avatar { get; set; } + + [JsonProperty("avatarThumbs")] public AvatarThumbsDto AvatarThumbs { get; set; } = new(); + + [JsonProperty("header")] public string? Header { get; set; } + + [JsonProperty("headerSize")] public HeaderSizeDto HeaderSize { get; set; } = new(); + + [JsonProperty("headerThumbs")] public HeaderThumbsDto HeaderThumbs { get; set; } = new(); + + [JsonProperty("id")] public long Id { get; set; } + + [JsonProperty("name")] public string Name { get; set; } = ""; + + [JsonProperty("username")] public string? Username { get; set; } + + [JsonProperty("canLookStory")] public bool? CanLookStory { get; set; } + + [JsonProperty("canCommentStory")] public bool? CanCommentStory { get; set; } + + [JsonProperty("hasNotViewedStory")] public bool? HasNotViewedStory { get; set; } + + [JsonProperty("isVerified")] public bool? IsVerified { get; set; } + + [JsonProperty("canPayInternal")] public bool? CanPayInternal { get; set; } + + [JsonProperty("hasScheduledStream")] public bool? HasScheduledStream { get; set; } + + [JsonProperty("hasStream")] public bool? HasStream { get; set; } + + [JsonProperty("hasStories")] public bool? HasStories { get; set; } + + [JsonProperty("tipsEnabled")] public bool? TipsEnabled { get; set; } + + [JsonProperty("tipsTextEnabled")] public bool? TipsTextEnabled { get; set; } + + [JsonProperty("tipsMin")] public int? TipsMin { get; set; } + + [JsonProperty("tipsMinInternal")] public int? TipsMinInternal { get; set; } + + [JsonProperty("tipsMax")] public int? TipsMax { get; set; } + + [JsonProperty("canEarn")] public bool? CanEarn { get; set; } + + [JsonProperty("canAddSubscriber")] public bool? CanAddSubscriber { get; set; } + + [JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; } + + [JsonProperty("isPaywallRequired")] public bool? IsPaywallRequired { get; set; } + + [JsonProperty("unprofitable")] public bool? Unprofitable { get; set; } + + [JsonProperty("listsStates")] public List ListsStates { get; set; } = []; + + [JsonProperty("isMuted")] public bool? IsMuted { get; set; } + + [JsonProperty("isRestricted")] public bool? IsRestricted { get; set; } + + [JsonProperty("canRestrict")] public bool? CanRestrict { get; set; } + + [JsonProperty("subscribedBy")] public bool? SubscribedBy { get; set; } + + [JsonProperty("subscribedByExpire")] public bool? SubscribedByExpire { get; set; } + + [JsonProperty("subscribedByExpireDate")] public DateTime? SubscribedByExpireDate { get; set; } + + [JsonProperty("subscribedByAutoprolong")] public bool? SubscribedByAutoprolong { get; set; } + + [JsonProperty("subscribedIsExpiredNow")] public bool? SubscribedIsExpiredNow { get; set; } + + [JsonProperty("currentSubscribePrice")] public string? CurrentSubscribePrice { get; set; } + + [JsonProperty("subscribedOn")] public bool? SubscribedOn { get; set; } + + [JsonProperty("subscribedOnExpiredNow")] public bool? SubscribedOnExpiredNow { get; set; } + + [JsonProperty("subscribedOnDuration")] public string SubscribedOnDuration { get; set; } = ""; + + [JsonProperty("canReport")] public bool? CanReport { get; set; } + + [JsonProperty("canReceiveChatMessage")] public bool? CanReceiveChatMessage { get; set; } + + [JsonProperty("hideChat")] public bool? HideChat { get; set; } + + [JsonProperty("lastSeen")] public DateTime? LastSeen { get; set; } + + [JsonProperty("isPerformer")] public bool? IsPerformer { get; set; } + + [JsonProperty("isRealPerformer")] public bool? IsRealPerformer { get; set; } + + [JsonProperty("subscribedByData")] public SubscribedByDataDto SubscribedByData { get; set; } = new(); + + [JsonProperty("subscribedOnData")] public SubscribedOnDataDto SubscribedOnData { get; set; } = new(); + + [JsonProperty("canTrialSend")] public bool? CanTrialSend { get; set; } + + [JsonProperty("isBlocked")] public bool? IsBlocked { get; set; } + + [JsonProperty("displayName")] public string DisplayName { get; set; } = ""; + + [JsonProperty("notice")] public string Notice { get; set; } = ""; +} diff --git a/OF DL/Models/Dtos/Subscriptions/ListsStateDto.cs b/OF DL/Models/Dtos/Subscriptions/ListsStateDto.cs new file mode 100644 index 0000000..fc4929f --- /dev/null +++ b/OF DL/Models/Dtos/Subscriptions/ListsStateDto.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Subscriptions; + +public class ListsStateDto +{ + [JsonProperty("id")] public object Id { get; set; } = new(); + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("name")] public string Name { get; set; } = ""; + + [JsonProperty("hasUser")] public bool? HasUser { get; set; } + + [JsonProperty("canAddUser")] public bool? CanAddUser { get; set; } +} diff --git a/OF DL/Models/Dtos/Subscriptions/SubscribeDto.cs b/OF DL/Models/Dtos/Subscriptions/SubscribeDto.cs new file mode 100644 index 0000000..2dfeef8 --- /dev/null +++ b/OF DL/Models/Dtos/Subscriptions/SubscribeDto.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Subscriptions; + +public class SubscribeDto +{ + [JsonProperty("id")] public object Id { get; set; } = new(); + + [JsonProperty("userId")] public long? UserId { get; set; } + + [JsonProperty("subscriberId")] public int? SubscriberId { get; set; } + + [JsonProperty("date")] public DateTime? Date { get; set; } + + [JsonProperty("duration")] public int? Duration { get; set; } + + [JsonProperty("startDate")] public DateTime? StartDate { get; set; } + + [JsonProperty("expireDate")] public DateTime? ExpireDate { get; set; } + + [JsonProperty("cancelDate")] public object CancelDate { get; set; } = new(); + + [JsonProperty("price")] public string? Price { get; set; } + + [JsonProperty("regularPrice")] public string? RegularPrice { get; set; } + + [JsonProperty("discount")] public string? Discount { get; set; } + + [JsonProperty("action")] public string Action { get; set; } = ""; + + [JsonProperty("type")] public string Type { get; set; } = ""; + + [JsonProperty("offerStart")] public object OfferStart { get; set; } = new(); + + [JsonProperty("offerEnd")] public object OfferEnd { get; set; } = new(); + + [JsonProperty("isCurrent")] public bool? IsCurrent { get; set; } +} diff --git a/OF DL/Models/Dtos/Subscriptions/SubscriptionsDto.cs b/OF DL/Models/Dtos/Subscriptions/SubscriptionsDto.cs new file mode 100644 index 0000000..3743c8a --- /dev/null +++ b/OF DL/Models/Dtos/Subscriptions/SubscriptionsDto.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OF_DL.Models.Dtos.Subscriptions; + +public class SubscriptionsDto +{ + [JsonProperty("list")] public List List { get; set; } = []; + + [JsonProperty("hasMore")] public bool HasMore { get; set; } +} diff --git a/OF DL/Models/Entities/Subscriptions/ListItem.cs b/OF DL/Models/Entities/Subscriptions/ListItem.cs new file mode 100644 index 0000000..a9fe450 --- /dev/null +++ b/OF DL/Models/Entities/Subscriptions/ListItem.cs @@ -0,0 +1,10 @@ +namespace OF_DL.Models.Entities.Subscriptions; + +public class ListItem +{ + public string Username { get; set; } = ""; + + public bool? IsRestricted { get; set; } + + public long Id { get; set; } +} diff --git a/OF DL/Models/Entities/Subscriptions/Subscriptions.cs b/OF DL/Models/Entities/Subscriptions/Subscriptions.cs new file mode 100644 index 0000000..4b8dfb2 --- /dev/null +++ b/OF DL/Models/Entities/Subscriptions/Subscriptions.cs @@ -0,0 +1,8 @@ +namespace OF_DL.Models.Entities.Subscriptions; + +public class Subscriptions +{ + public List List { get; set; } = []; + + public bool HasMore { get; set; } +} diff --git a/OF DL/Models/Mappers/SubscriptionsMapper.cs b/OF DL/Models/Mappers/SubscriptionsMapper.cs new file mode 100644 index 0000000..29859d5 --- /dev/null +++ b/OF DL/Models/Mappers/SubscriptionsMapper.cs @@ -0,0 +1,27 @@ +using OF_DL.Models.Dtos.Subscriptions; +using OF_DL.Models.Entities.Subscriptions; + +namespace OF_DL.Models.Mappers; + +public static class SubscriptionsMapper +{ + public static Subscriptions FromDto(SubscriptionsDto? dto) + { + Subscriptions mapped = new() { HasMore = dto?.HasMore ?? false }; + + if (dto?.List == null) + { + return mapped; + } + + foreach (ListItemDto entry in dto.List) + { + mapped.List.Add(MapList(entry)); + } + + return mapped; + } + + private static ListItem MapList(ListItemDto dto) => + new() { Id = dto.Id, Username = dto.Username ?? string.Empty, IsRestricted = dto.IsRestricted }; +} diff --git a/OF DL/Models/Subscriptions/Subscriptions.cs b/OF DL/Models/Subscriptions/Subscriptions.cs deleted file mode 100644 index 6903743..0000000 --- a/OF DL/Models/Subscriptions/Subscriptions.cs +++ /dev/null @@ -1,159 +0,0 @@ -namespace OF_DL.Models.Subscriptions; - -public class Subscriptions -{ - public List list { get; set; } - public bool hasMore { get; set; } - - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } - - public class HeaderSize - { - public int? width { get; set; } - public int? height { get; set; } - } - - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } - - public class List - { - public string view { get; set; } - public string avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long id { get; set; } - public string name { get; set; } - public string username { get; set; } - public bool? canLookStory { get; set; } - public bool? canCommentStory { get; set; } - public bool? hasNotViewedStory { get; set; } - public bool? isVerified { get; set; } - public bool? canPayInternal { get; set; } - public bool? hasScheduledStream { get; set; } - public bool? hasStream { get; set; } - public bool? hasStories { get; set; } - public bool? tipsEnabled { get; set; } - public bool? tipsTextEnabled { get; set; } - public int? tipsMin { get; set; } - public int? tipsMinInternal { get; set; } - public int? tipsMax { get; set; } - public bool? canEarn { get; set; } - public bool? canAddSubscriber { get; set; } - public string? subscribePrice { get; set; } - public bool? isPaywallRequired { get; set; } - public bool? unprofitable { get; set; } - public List listsStates { get; set; } - public bool? isMuted { get; set; } - public bool? isRestricted { get; set; } - public bool? canRestrict { get; set; } - public bool? subscribedBy { get; set; } - public bool? subscribedByExpire { get; set; } - public DateTime? subscribedByExpireDate { get; set; } - public bool? subscribedByAutoprolong { get; set; } - public bool? subscribedIsExpiredNow { get; set; } - public string? currentSubscribePrice { get; set; } - public bool? subscribedOn { get; set; } - public bool? subscribedOnExpiredNow { get; set; } - public string subscribedOnDuration { get; set; } - public bool? canReport { get; set; } - public bool? canReceiveChatMessage { get; set; } - public bool? hideChat { get; set; } - public DateTime? lastSeen { get; set; } - public bool? isPerformer { get; set; } - public bool? isRealPerformer { get; set; } - public SubscribedByData subscribedByData { get; set; } - public SubscribedOnData subscribedOnData { get; set; } - public bool? canTrialSend { get; set; } - public bool? isBlocked { get; set; } - public string displayName { get; set; } - public string notice { get; set; } - } - - public class ListsState - { - public object id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool? hasUser { get; set; } - public bool? canAddUser { get; set; } - } - - public class Subscribe - { - public object id { get; set; } - public long? userId { get; set; } - public int? subscriberId { get; set; } - public DateTime? date { get; set; } - public int? duration { get; set; } - public DateTime? startDate { get; set; } - public DateTime? expireDate { get; set; } - public object cancelDate { get; set; } - public string? price { get; set; } - public string? regularPrice { get; set; } - public string? discount { get; set; } - public string action { get; set; } - public string type { get; set; } - public object offerStart { get; set; } - public object offerEnd { get; set; } - public bool? isCurrent { get; set; } - } - - public class SubscribedByData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public DateTime? renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public string status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public bool? showPostsInFeed { get; set; } - public List subscribes { get; set; } - public bool? hasActivePaidSubscriptions { get; set; } - } - - public class SubscribedOnData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public DateTime? renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public object status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public string? tipsSumm { get; set; } - public string? subscribesSumm { get; set; } - public string? messagesSumm { get; set; } - public string? postsSumm { get; set; } - public string? streamsSumm { get; set; } - public string? totalSumm { get; set; } - public List subscribes { get; set; } - public bool? hasActivePaidSubscriptions { get; set; } - } -} diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 4260832..e3b6e88 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -16,6 +16,7 @@ using PurchasedDtos = OF_DL.Models.Dtos.Purchased; using StoriesDtos = OF_DL.Models.Dtos.Stories; using StreamsDtos = OF_DL.Models.Dtos.Streams; using UserDtos = OF_DL.Models.Dtos.Users; +using SubscriptionsDtos = OF_DL.Models.Dtos.Subscriptions; using ArchivedEntities = OF_DL.Models.Entities.Archived; using HighlightEntities = OF_DL.Models.Entities.Highlights; using ListEntities = OF_DL.Models.Entities.Lists; @@ -24,9 +25,9 @@ using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; using StoryEntities = OF_DL.Models.Entities.Stories; using StreamEntities = OF_DL.Models.Entities.Streams; +using SubscriptionEntities = OF_DL.Models.Entities.Subscriptions; using UserEntities = OF_DL.Models.Entities.Users; using OF_DL.Models.Mappers; -using OF_DL.Models.Subscriptions; using OF_DL.Widevine; using Serilog; using Spectre.Console; @@ -3412,49 +3413,53 @@ public class APIService(IAuthService authService, IConfigService configService, try { Dictionary users = new(); - Subscriptions subscriptions = new(); + SubscriptionEntities.Subscriptions subscriptions = new(); Log.Debug("Calling GetAllSubscrptions"); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); - subscriptions = JsonConvert.DeserializeObject(body); - if (subscriptions != null && subscriptions.hasMore) + SubscriptionsDtos.SubscriptionsDto? subscriptionsDto = + JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + subscriptions = SubscriptionsMapper.FromDto(subscriptionsDto); + if (subscriptions.HasMore) { - getParams["offset"] = subscriptions.list.Count.ToString(); + getParams["offset"] = subscriptions.List.Count.ToString(); while (true) { - Subscriptions newSubscriptions = new(); + SubscriptionEntities.Subscriptions newSubscriptions = new(); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); if (!string.IsNullOrEmpty(loopbody) && (!loopbody.Contains("[]") || loopbody.Trim() != "[]")) { - newSubscriptions = - JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + SubscriptionsDtos.SubscriptionsDto? newSubscriptionsDto = + JsonConvert.DeserializeObject(loopbody, + m_JsonSerializerSettings); + newSubscriptions = SubscriptionsMapper.FromDto(newSubscriptionsDto); } else { break; } - subscriptions.list.AddRange(newSubscriptions.list); - if (!newSubscriptions.hasMore) + subscriptions.List.AddRange(newSubscriptions.List); + if (!newSubscriptions.HasMore) { break; } - getParams["offset"] = subscriptions.list.Count.ToString(); + getParams["offset"] = subscriptions.List.Count.ToString(); } } - foreach (Subscriptions.List subscription in subscriptions.list) + foreach (SubscriptionEntities.ListItem subscription in subscriptions.List) { - if ((!(subscription.isRestricted ?? false) || - ((subscription.isRestricted ?? false) && includeRestricted)) - && !users.ContainsKey(subscription.username)) + if ((!(subscription.IsRestricted ?? false) || + ((subscription.IsRestricted ?? false) && includeRestricted)) + && !users.ContainsKey(subscription.Username)) { - users.Add(subscription.username, subscription.id); + users.Add(subscription.Username, subscription.Id); } } From 407419a819c91506ddf8da807846d4bb449cd38f Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 00:55:28 -0600 Subject: [PATCH 23/55] Replace string.Empty with "" --- OF DL/Models/Auth.cs | 10 ++--- OF DL/Models/Config.cs | 16 +++---- OF DL/Models/Mappers/SubscriptionsMapper.cs | 2 +- OF DL/Services/APIService.cs | 42 ++++++++--------- OF DL/Services/DownloadService.cs | 50 ++++++++++----------- OF DL/Utils/XmlUtils.cs | 4 +- 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/OF DL/Models/Auth.cs b/OF DL/Models/Auth.cs index e718a53..7369d34 100644 --- a/OF DL/Models/Auth.cs +++ b/OF DL/Models/Auth.cs @@ -4,10 +4,10 @@ namespace OF_DL.Models; public class Auth { - public string? USER_ID { get; set; } = string.Empty; - public string? USER_AGENT { get; set; } = string.Empty; - public string? X_BC { get; set; } = string.Empty; - public string? COOKIE { get; set; } = string.Empty; + public string? USER_ID { get; set; } = ""; + public string? USER_AGENT { get; set; } = ""; + public string? X_BC { get; set; } = ""; + public string? COOKIE { get; set; } = ""; - [JsonIgnore] public string? FFMPEG_PATH { get; set; } = string.Empty; + [JsonIgnore] public string? FFMPEG_PATH { get; set; } = ""; } diff --git a/OF DL/Models/Config.cs b/OF DL/Models/Config.cs index 05aa188..aeb77b7 100644 --- a/OF DL/Models/Config.cs +++ b/OF DL/Models/Config.cs @@ -37,7 +37,7 @@ public class Config : IFileNameFormatConfig [ToggleableConfig] public bool SkipAds { get; set; } = false; - public string? DownloadPath { get; set; } = string.Empty; + public string? DownloadPath { get; set; } = ""; [ToggleableConfig] public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } = false; @@ -71,11 +71,11 @@ public class Config : IFileNameFormatConfig [ToggleableConfig] public bool DownloadPostsIncrementally { get; set; } = false; public bool NonInteractiveMode { get; set; } = false; - public string NonInteractiveModeListName { get; set; } = string.Empty; + public string NonInteractiveModeListName { get; set; } = ""; [ToggleableConfig] public bool NonInteractiveModePurchasedTab { get; set; } = false; - public string? FFmpegPath { get; set; } = string.Empty; + public string? FFmpegPath { get; set; } = ""; [ToggleableConfig] public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } = false; @@ -83,7 +83,7 @@ public class Config : IFileNameFormatConfig [ToggleableConfig] public bool DownloadDuplicatedMedia { get; set; } = false; - public string IgnoredUsersListName { get; set; } = string.Empty; + public string IgnoredUsersListName { get; set; } = ""; [JsonConverter(typeof(StringEnumConverter))] public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error; @@ -98,10 +98,10 @@ public class Config : IFileNameFormatConfig // When enabled, post/message text is stored as-is without XML stripping. [ToggleableConfig] public bool DisableTextSanitization { get; set; } = false; - public string? PaidPostFileNameFormat { get; set; } = string.Empty; - public string? PostFileNameFormat { get; set; } = string.Empty; - public string? PaidMessageFileNameFormat { get; set; } = string.Empty; - public string? MessageFileNameFormat { get; set; } = string.Empty; + public string? PaidPostFileNameFormat { get; set; } = ""; + public string? PostFileNameFormat { get; set; } = ""; + public string? PaidMessageFileNameFormat { get; set; } = ""; + public string? MessageFileNameFormat { get; set; } = ""; public IFileNameFormatConfig GetCreatorFileNameFormatConfig(string username) { diff --git a/OF DL/Models/Mappers/SubscriptionsMapper.cs b/OF DL/Models/Mappers/SubscriptionsMapper.cs index 29859d5..9c9f2fd 100644 --- a/OF DL/Models/Mappers/SubscriptionsMapper.cs +++ b/OF DL/Models/Mappers/SubscriptionsMapper.cs @@ -23,5 +23,5 @@ public static class SubscriptionsMapper } private static ListItem MapList(ListItemDto dto) => - new() { Id = dto.Id, Username = dto.Username ?? string.Empty, IsRestricted = dto.IsRestricted }; + new() { Id = dto.Id, Username = dto.Username ?? "", IsRestricted = dto.IsRestricted }; } diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index e3b6e88..0872e93 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -409,17 +409,17 @@ public class APIService(IAuthService authService, IConfigService configService, { if (story.Media[0].CreatedAt.HasValue) { - await dbService.AddStory(folder, story.Id, string.Empty, "0", false, false, + await dbService.AddStory(folder, story.Id, "", "0", false, false, story.Media[0].CreatedAt.Value); } else if (story.CreatedAt.HasValue) { - await dbService.AddStory(folder, story.Id, string.Empty, "0", false, false, + await dbService.AddStory(folder, story.Id, "", "0", false, false, story.CreatedAt.Value); } else { - await dbService.AddStory(folder, story.Id, string.Empty, "0", false, false, DateTime.Now); + await dbService.AddStory(folder, story.Id, "", "0", false, false, DateTime.Now); } if (story.Media != null && story.Media.Count > 0) @@ -505,7 +505,7 @@ public class APIService(IAuthService authService, IConfigService configService, foreach (string highlight_id in highlight_ids) { Dictionary highlight_headers = - GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, string.Empty); + GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, ""); HttpClient highlight_client = GetHttpClient(); @@ -529,17 +529,17 @@ public class APIService(IAuthService authService, IConfigService configService, { if (item.Media != null && item.Media.Count > 0 && item.Media[0].CreatedAt.HasValue) { - await dbService.AddStory(folder, item.Id, string.Empty, "0", false, false, + await dbService.AddStory(folder, item.Id, "", "0", false, false, item.Media[0].CreatedAt.Value); } else if (item.CreatedAt.HasValue) { - await dbService.AddStory(folder, item.Id, string.Empty, "0", false, false, + await dbService.AddStory(folder, item.Id, "", "0", false, false, item.CreatedAt.Value); } else { - await dbService.AddStory(folder, item.Id, string.Empty, "0", false, false, + await dbService.AddStory(folder, item.Id, "", "0", false, false, DateTime.Now); } @@ -547,7 +547,7 @@ public class APIService(IAuthService authService, IConfigService configService, { foreach (HighlightEntities.Medium medium in item.Media) { - string storyUrl = item.Media[0].Files?.Full?.Url ?? string.Empty; + string storyUrl = item.Media[0].Files?.Full?.Url ?? ""; await dbService.AddMedia(folder, medium.Id, item.Id, storyUrl, null, null, null, "Stories", medium.Type == "photo" ? "Images" : @@ -687,7 +687,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, purchase.Id, purchase.Text != null ? purchase.Text : string.Empty, + await dbService.AddPost(folder, purchase.Id, purchase.Text != null ? purchase.Text : "", purchase.Price != null ? purchase.Price : "0", purchase.Price != null && purchase.IsOpened ? true : false, purchase.IsArchived.HasValue ? purchase.IsArchived.Value : false, @@ -926,7 +926,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, post.Id, post.RawText != null ? post.RawText : string.Empty, + await dbService.AddPost(folder, post.Id, post.RawText != null ? post.RawText : "", post.Price != null ? post.Price : "0", post.Price != null && post.IsOpened ? true : false, post.IsArchived, post.PostedAt); postCollection.PostObjects.Add(post); @@ -1066,7 +1066,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, singlePost.Id, singlePost.Text != null ? singlePost.Text : string.Empty, + await dbService.AddPost(folder, singlePost.Id, singlePost.Text != null ? singlePost.Text : "", singlePost.Price != null ? singlePost.Price : "0", singlePost.Price != null && singlePost.IsOpened ? true : false, singlePost.IsArchived, singlePost.PostedAt); @@ -1302,7 +1302,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, stream.Id, stream.Text != null ? stream.Text : string.Empty, + await dbService.AddPost(folder, stream.Id, stream.Text != null ? stream.Text : "", stream.Price != null ? stream.Price : "0", stream.Price != null && stream.IsOpened ? true : false, stream.IsArchived, stream.PostedAt); streamsCollection.StreamObjects.Add(stream); @@ -1479,7 +1479,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, archive.Id, archive.Text != null ? archive.Text : string.Empty, + await dbService.AddPost(folder, archive.Id, archive.Text != null ? archive.Text : "", archive.Price != null ? archive.Price : "0", archive.Price != null && archive.IsOpened ? true : false, archive.IsArchived, archive.PostedAt); archivedCollection.ArchivedPostObjects.Add(archive); @@ -1636,7 +1636,7 @@ public class APIService(IAuthService authService, IConfigService configService, if (!configService.CurrentConfig.IgnoreOwnMessages || list.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - await dbService.AddMessage(folder, list.Id, list.Text ?? string.Empty, list.Price ?? "0", + await dbService.AddMessage(folder, list.Id, list.Text ?? "", list.Price ?? "0", list.CanPurchaseReason == "opened" ? true : list.CanPurchaseReason != "opened" ? false : (bool?)null ?? false, false, list.CreatedAt.HasValue ? list.CreatedAt.Value : DateTime.Now, @@ -1836,7 +1836,7 @@ public class APIService(IAuthService authService, IConfigService configService, if (!configService.CurrentConfig.IgnoreOwnMessages || message.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - await dbService.AddMessage(folder, message.Id, message.Text ?? string.Empty, + await dbService.AddMessage(folder, message.Id, message.Text ?? "", message.Price != null ? message.Price.ToString() : "0", true, false, message.CreatedAt.HasValue ? message.CreatedAt.Value : DateTime.Now, message.FromUser?.Id ?? int.MinValue); @@ -2106,14 +2106,14 @@ public class APIService(IAuthService authService, IConfigService configService, if (purchase.PostedAt != null) { await dbService.AddMessage(folder, purchase.Id, - purchase.Text != null ? purchase.Text : string.Empty, + purchase.Text != null ? purchase.Text : "", purchase.Price != null ? purchase.Price : "0", true, false, purchase.PostedAt.Value, purchase.FromUser.Id); } else { await dbService.AddMessage(folder, purchase.Id, - purchase.Text != null ? purchase.Text : string.Empty, + purchase.Text != null ? purchase.Text : "", purchase.Price != null ? purchase.Price : "0", true, false, purchase.CreatedAt.Value, purchase.FromUser.Id); } @@ -2670,7 +2670,7 @@ public class APIService(IAuthService authService, IConfigService configService, } await dbService.AddPost(path, purchase.Id, - purchase.Text != null ? purchase.Text : string.Empty, + purchase.Text != null ? purchase.Text : "", purchase.Price != null ? purchase.Price : "0", purchase.Price != null && purchase.IsOpened ? true : false, purchase.IsArchived.HasValue ? purchase.IsArchived.Value : false, @@ -2775,14 +2775,14 @@ public class APIService(IAuthService authService, IConfigService configService, if (purchase.PostedAt != null) { await dbService.AddMessage(path, purchase.Id, - purchase.Text != null ? purchase.Text : string.Empty, + purchase.Text != null ? purchase.Text : "", purchase.Price != null ? purchase.Price : "0", true, false, purchase.PostedAt.Value, purchase.FromUser.Id); } else { await dbService.AddMessage(path, purchase.Id, - purchase.Text != null ? purchase.Text : string.Empty, + purchase.Text != null ? purchase.Text : "", purchase.Price != null ? purchase.Price : "0", true, false, purchase.CreatedAt.Value, purchase.FromUser.Id); } @@ -3124,7 +3124,7 @@ public class APIService(IAuthService authService, IConfigService configService, try { - string dcValue = string.Empty; + string dcValue = ""; HttpClient client = new(); CDRMProjectRequest cdrmProjectRequest = new() diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 91cffc2..aa9d4c9 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -194,7 +194,7 @@ public class DownloadService( LoggingLevel.Warning => "-loglevel warning", LoggingLevel.Error => "-loglevel error", LoggingLevel.Fatal => "-loglevel fatal", - _ => string.Empty + _ => "" }; if (logLevelArgs.Contains("-report", StringComparison.OrdinalIgnoreCase)) @@ -430,7 +430,7 @@ public class DownloadService( { try { - string customFileName = string.Empty; + string customFileName = ""; if (!Directory.Exists(folder + path)) { Directory.CreateDirectory(folder + path); @@ -468,7 +468,7 @@ public class DownloadService( /// A string that represents the updated path based on the file extension. private string UpdatePathBasedOnExtension(string folder, string path, string extension) { - string subdirectory = string.Empty; + string subdirectory = ""; switch (extension.ToLower()) { @@ -531,7 +531,7 @@ public class DownloadService( return option switch { CustomFileNameOption.ReturnOriginal => filename, - CustomFileNameOption.ReturnEmpty => string.Empty, + CustomFileNameOption.ReturnEmpty => "", _ => filename }; } @@ -1198,7 +1198,7 @@ public class DownloadService( { try { - string customFileName = string.Empty; + string customFileName = ""; string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; @@ -1327,7 +1327,7 @@ public class DownloadService( { try { - string customFileName = string.Empty; + string customFileName = ""; string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; @@ -1458,7 +1458,7 @@ public class DownloadService( { try { - string customFileName = string.Empty; + string customFileName = ""; string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; @@ -1586,7 +1586,7 @@ public class DownloadService( { try { - string customFileName = string.Empty; + string customFileName = ""; string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; @@ -1716,7 +1716,7 @@ public class DownloadService( { try { - string customFileName = string.Empty; + string customFileName = ""; string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; @@ -1845,7 +1845,7 @@ public class DownloadService( { try { - string customFileName = string.Empty; + string customFileName = ""; string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; @@ -1974,7 +1974,7 @@ public class DownloadService( { try { - string customFileName = string.Empty; + string customFileName = ""; string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; @@ -2103,7 +2103,7 @@ public class DownloadService( { try { - string customFileName = string.Empty; + string customFileName = ""; string path; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; @@ -2234,7 +2234,7 @@ public class DownloadService( { try { - string customFileName = string.Empty; + string customFileName = ""; Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; string path = "/Archived/Posts/Free/Videos"; @@ -2529,7 +2529,7 @@ public class DownloadService( "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, + "", postInfo, mediaInfo, postInfo?.Author, @@ -2554,7 +2554,7 @@ public class DownloadService( "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, + "", postInfo, mediaInfo, postInfo?.Author, @@ -2659,7 +2659,7 @@ public class DownloadService( "Messages", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? - string.Empty, + "", messageInfo, mediaInfo, messageInfo?.FromUser, @@ -2683,7 +2683,7 @@ public class DownloadService( "Messages", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? - string.Empty, + "", messageInfo, mediaInfo, messageInfo?.FromUser, @@ -2770,7 +2770,7 @@ public class DownloadService( isNew = await DownloadPurchasedMessageDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, lastModified, kvp.Key, "Messages", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? - string.Empty, messageInfo, mediaInfo, messageInfo?.FromUser, users); + "", messageInfo, mediaInfo, messageInfo?.FromUser, users); } else { @@ -2785,7 +2785,7 @@ public class DownloadService( p?.Media?.Any(m => m.Id == kvp.Key) == true); isNew = await DownloadPurchasedMedia(kvp.Value, path, kvp.Key, "Messages", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? - string.Empty, messageInfo, mediaInfo, messageInfo?.FromUser, users); + "", messageInfo, mediaInfo, messageInfo?.FromUser, users); } if (isNew) @@ -2862,7 +2862,7 @@ public class DownloadService( isNew = await DownloadStreamsDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, lastModified, kvp.Key, "Streams", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, streamInfo, mediaInfo, streamInfo?.Author, users); + "", streamInfo, mediaInfo, streamInfo?.Author, users); } else { @@ -2876,7 +2876,7 @@ public class DownloadService( streams.StreamObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadStreamMedia(kvp.Value, path, kvp.Key, "Streams", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, streamInfo, mediaInfo, streamInfo?.Author, users); + "", streamInfo, mediaInfo, streamInfo?.Author, users); } if (isNew) @@ -2955,7 +2955,7 @@ public class DownloadService( isNew = await DownloadPostDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, lastModified, postKVP.Key, "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, postInfo, mediaInfo, postInfo?.Author, users); + "", postInfo, mediaInfo, postInfo?.Author, users); } else { @@ -2969,7 +2969,7 @@ public class DownloadService( posts.PostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); isNew = await DownloadPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, postInfo, mediaInfo, postInfo?.Author, users); + "", postInfo, mediaInfo, postInfo?.Author, users); } if (isNew) @@ -3048,7 +3048,7 @@ public class DownloadService( isNew = await DownloadPurchasedPostDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, lastModified, postKVP.Key, "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, postInfo, mediaInfo, postInfo?.FromUser, users); + "", postInfo, mediaInfo, postInfo?.FromUser, users); } else { @@ -3063,7 +3063,7 @@ public class DownloadService( purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.Media?.Any(m => m.Id == postKVP.Key) == true); isNew = await DownloadPurchasedPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, postInfo, mediaInfo, postInfo?.FromUser, users); + "", postInfo, mediaInfo, postInfo?.FromUser, users); } if (isNew) diff --git a/OF DL/Utils/XmlUtils.cs b/OF DL/Utils/XmlUtils.cs index eef2073..08d3864 100644 --- a/OF DL/Utils/XmlUtils.cs +++ b/OF DL/Utils/XmlUtils.cs @@ -11,7 +11,7 @@ internal static class XmlUtils { if (Passthrough) { - return xmlValue ?? string.Empty; + return xmlValue; } try @@ -23,6 +23,6 @@ internal static class XmlUtils { } - return string.Empty; + return ""; } } From fee9ca1e9726fd287250b63b3e427e384b6bc1c4 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 00:55:54 -0600 Subject: [PATCH 24/55] Update XML comments --- OF DL/Program.cs | 24 ++++++++++---------- OF DL/Services/DownloadService.cs | 37 +++++++++++++++++-------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 1e2e770..44b1bd7 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -876,7 +876,7 @@ public class Program(IServiceProvider serviceProvider) long message_id = Convert.ToInt64(messageUrl.Split("?firstId=")[1]); long user_id = Convert.ToInt64(messageUrl.Split("/")[6]); JObject user = await apiService.GetUserInfoById($"/users/list?x[]={user_id.ToString()}"); - string username = string.Empty; + string username = ""; Log.Debug($"Message ID: {message_id}"); Log.Debug($"User ID: {user_id}"); @@ -1544,7 +1544,7 @@ public class Program(IServiceProvider serviceProvider) "Posts", new SpectreProgressReporter(task), configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidPostFileNameFormat ?? string.Empty, + .PaidPostFileNameFormat ?? "", postInfo, mediaInfo, postInfo?.FromUser, @@ -1574,7 +1574,7 @@ public class Program(IServiceProvider serviceProvider) "Posts", new SpectreProgressReporter(task), configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidPostFileNameFormat ?? string.Empty, + .PaidPostFileNameFormat ?? "", postInfo, mediaInfo, postInfo?.FromUser, @@ -1694,7 +1694,7 @@ public class Program(IServiceProvider serviceProvider) "Messages", new SpectreProgressReporter(task), configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? string.Empty, + .PaidMessageFileNameFormat ?? "", messageInfo, mediaInfo, messageInfo?.FromUser, @@ -1725,7 +1725,7 @@ public class Program(IServiceProvider serviceProvider) "Messages", new SpectreProgressReporter(task), configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? string.Empty, + .PaidMessageFileNameFormat ?? "", messageInfo, mediaInfo, messageInfo?.FromUser, @@ -1921,7 +1921,7 @@ public class Program(IServiceProvider serviceProvider) "Messages", new SpectreProgressReporter(task), configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? string.Empty, + .PaidMessageFileNameFormat ?? "", messageInfo, mediaInfo, messageInfo?.FromUser, @@ -1953,7 +1953,7 @@ public class Program(IServiceProvider serviceProvider) "Messages", new SpectreProgressReporter(task), configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? string.Empty, + .PaidMessageFileNameFormat ?? "", messageInfo, mediaInfo, messageInfo?.FromUser, @@ -2061,7 +2061,7 @@ public class Program(IServiceProvider serviceProvider) "Messages", new SpectreProgressReporter(task), configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? string.Empty, + .PaidMessageFileNameFormat ?? "", messageInfo, mediaInfo, messageInfo?.FromUser, @@ -2093,7 +2093,7 @@ public class Program(IServiceProvider serviceProvider) "Messages", new SpectreProgressReporter(task), configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? string.Empty, + .PaidMessageFileNameFormat ?? "", messageInfo, mediaInfo, messageInfo?.FromUser, @@ -2212,7 +2212,7 @@ public class Program(IServiceProvider serviceProvider) "Posts", new SpectreProgressReporter(task), configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - string.Empty, + "", postInfo, mediaInfo, postInfo?.Author, @@ -2234,7 +2234,7 @@ public class Program(IServiceProvider serviceProvider) "Posts", new SpectreProgressReporter(task), configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PostFileNameFormat ?? string.Empty, + .PostFileNameFormat ?? "", postInfo, mediaInfo, postInfo?.Author, @@ -2622,7 +2622,7 @@ public class Program(IServiceProvider serviceProvider) return Path.GetFullPath(filename); } - string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? ""; foreach (string path in pathEnv.Split(Path.PathSeparator)) { string fullPath = Path.Combine(path, filename); diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index aa9d4c9..254169d 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -412,12 +412,10 @@ public class DownloadService( /// /// /// - /// - /// - /// - /// - /// - /// + /// + /// + /// + /// /// protected async Task CreateDirectoriesAndDownloadMedia(string path, string url, @@ -466,7 +464,7 @@ public class DownloadService( /// The initial relative path. /// The file extension. /// A string that represents the updated path based on the file extension. - private string UpdatePathBasedOnExtension(string folder, string path, string extension) + private static string UpdatePathBasedOnExtension(string folder, string path, string extension) { string subdirectory = ""; @@ -509,14 +507,17 @@ public class DownloadService( /// /// Generates a custom filename based on the given format and properties. /// + /// /// The format string for the filename. /// General information about the post. /// Media associated with the post. /// Author of the post. + /// /// Dictionary containing user-related data. - /// Helper class for filename operations. + /// + /// /// A Task resulting in a string that represents the custom filename. - private async Task GenerateCustomFileName(string filename, + private static async Task GenerateCustomFileName(string filename, string? filenameFormat, object? postInfo, object? postMedia, @@ -641,12 +642,13 @@ public class DownloadService( /// /// The folder where the media is stored. /// The ID of the media. - /// The full path to the media. + /// /// The URL from where to download the media. /// The relative path to the media. + /// /// The filename after any required manipulations. /// The file extension. - /// The task object for tracking progress. + /// /// A Task resulting in a boolean indicating whether the media is newly downloaded or not. public async Task ProcessMediaDownload(string folder, long media_id, @@ -730,12 +732,13 @@ public class DownloadService( /// /// /// + /// /// /// + /// /// /// - /// - /// + /// /// A Task resulting in a boolean indicating whether the media is newly downloaded or not. private async Task HandleNewMedia(string folder, long media_id, @@ -838,8 +841,8 @@ public class DownloadService( /// /// /// - /// - /// + /// + /// /// A boolean indicating whether the media is newly downloaded or not. private async Task HandlePreviouslyDownloadedMediaAsync(string folder, long media_id, string api_type, IProgressReporter progressReporter) @@ -863,7 +866,7 @@ public class DownloadService( /// /// The path to the file. /// The file size in bytes. - private long GetLocalFileSize(string filePath) => new FileInfo(filePath).Length; + private static long GetLocalFileSize(string filePath) => new FileInfo(filePath).Length; /// @@ -871,7 +874,7 @@ public class DownloadService( /// /// The URL to download the file from. /// The path where the downloaded file will be saved. - /// Progress tracking object. + /// /// A Task resulting in a DateTime indicating the last modified date of the downloaded file. private async Task DownloadFile(string url, string destinationPath, IProgressReporter progressReporter) { From a57af4042ff37fae74c33a6a239823b2c632e351 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 01:10:05 -0600 Subject: [PATCH 25/55] Refactor remaining entities --- OF DL/Helpers/VersionHelper.cs | 4 +- OF DL/Models/Auth.cs | 18 ++++-- OF DL/Models/CDRMProjectRequest.cs | 5 +- OF DL/Models/Config.cs | 61 ++++++++----------- OF DL/Models/CreatorConfig.cs | 12 ++++ OF DL/Models/DynamicRules.cs | 2 +- OF DL/Models/FileNameFormatConfig.cs | 3 + OF DL/Models/IFileNameFormatConfig.cs | 3 + ...esponse.cs => LatestReleaseApiResponse.cs} | 2 +- OF DL/Models/OFDLRequest.cs | 5 +- OF DL/Models/ToggleableConfigAttribute.cs | 4 +- OF DL/Program.cs | 12 ++-- OF DL/Services/APIService.cs | 30 ++++----- OF DL/Services/AuthService.cs | 2 +- OF DL/Services/DownloadService.cs | 50 +++++++-------- 15 files changed, 113 insertions(+), 100 deletions(-) create mode 100644 OF DL/Models/CreatorConfig.cs rename OF DL/Models/{LatestReleaseAPIResponse.cs => LatestReleaseApiResponse.cs} (79%) diff --git a/OF DL/Helpers/VersionHelper.cs b/OF DL/Helpers/VersionHelper.cs index 1343f3d..2b3903b 100644 --- a/OF DL/Helpers/VersionHelper.cs +++ b/OF DL/Helpers/VersionHelper.cs @@ -27,8 +27,8 @@ public static class VersionHelper Log.Debug("GetLatestReleaseTag API Response: "); Log.Debug(body); - LatestReleaseAPIResponse? versionCheckResponse = - JsonConvert.DeserializeObject(body); + LatestReleaseApiResponse? versionCheckResponse = + JsonConvert.DeserializeObject(body); if (versionCheckResponse == null || versionCheckResponse.TagName == "") { diff --git a/OF DL/Models/Auth.cs b/OF DL/Models/Auth.cs index 7369d34..0788fbd 100644 --- a/OF DL/Models/Auth.cs +++ b/OF DL/Models/Auth.cs @@ -4,10 +4,18 @@ namespace OF_DL.Models; public class Auth { - public string? USER_ID { get; set; } = ""; - public string? USER_AGENT { get; set; } = ""; - public string? X_BC { get; set; } = ""; - public string? COOKIE { get; set; } = ""; + [JsonProperty(PropertyName = "USER_ID")] + public string? UserId { get; set; } = ""; - [JsonIgnore] public string? FFMPEG_PATH { get; set; } = ""; + [JsonProperty(PropertyName = "USER_AGENT")] + public string? UserAgent { get; set; } = ""; + + [JsonProperty(PropertyName = "X_BC")] public string? XBc { get; set; } = ""; + + [JsonProperty(PropertyName = "COOKIE")] + public string? Cookie { get; set; } = ""; + + [JsonIgnore] + [JsonProperty(PropertyName = "FFMPEG_PATH")] + public string? FfmpegPath { get; set; } = ""; } diff --git a/OF DL/Models/CDRMProjectRequest.cs b/OF DL/Models/CDRMProjectRequest.cs index 182e827..07ae8fe 100644 --- a/OF DL/Models/CDRMProjectRequest.cs +++ b/OF DL/Models/CDRMProjectRequest.cs @@ -2,11 +2,12 @@ using Newtonsoft.Json; namespace OF_DL.Models; +// ReSharper disable once InconsistentNaming public class CDRMProjectRequest { - [JsonProperty("pssh")] public string PSSH { get; set; } = ""; + [JsonProperty("pssh")] public string Pssh { get; set; } = ""; - [JsonProperty("licurl")] public string LicenseURL { get; set; } = ""; + [JsonProperty("licurl")] public string LicenseUrl { get; set; } = ""; [JsonProperty("headers")] public string Headers { get; set; } = ""; diff --git a/OF DL/Models/Config.cs b/OF DL/Models/Config.cs index aeb77b7..4e45631 100644 --- a/OF DL/Models/Config.cs +++ b/OF DL/Models/Config.cs @@ -105,52 +105,39 @@ public class Config : IFileNameFormatConfig public IFileNameFormatConfig GetCreatorFileNameFormatConfig(string username) { - FileNameFormatConfig createFileNameFormatConfig = new(); - - Func func = (val1, val2) => - { - if (string.IsNullOrEmpty(val1)) - { - return val2; - } - - return val1; - }; + FileNameFormatConfig combinedFilenameFormatConfig = new(); if (CreatorConfigs.TryGetValue(username, out CreatorConfig? creatorConfig)) { - createFileNameFormatConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat; - createFileNameFormatConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat; - createFileNameFormatConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat; - createFileNameFormatConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat; + combinedFilenameFormatConfig.PaidMessageFileNameFormat = + !string.IsNullOrEmpty(creatorConfig.PaidMessageFileNameFormat) + ? creatorConfig.PaidMessageFileNameFormat + : PaidMessageFileNameFormat; + + combinedFilenameFormatConfig.PostFileNameFormat = !string.IsNullOrEmpty(creatorConfig.PostFileNameFormat) + ? creatorConfig.PostFileNameFormat + : PostFileNameFormat; + + combinedFilenameFormatConfig.MessageFileNameFormat = + !string.IsNullOrEmpty(creatorConfig.MessageFileNameFormat) + ? creatorConfig.MessageFileNameFormat + : MessageFileNameFormat; + + combinedFilenameFormatConfig.PaidPostFileNameFormat = + !string.IsNullOrEmpty(creatorConfig.PaidPostFileNameFormat) + ? creatorConfig.PaidPostFileNameFormat + : PaidPostFileNameFormat; } - createFileNameFormatConfig.PaidMessageFileNameFormat = - func(createFileNameFormatConfig.PaidMessageFileNameFormat, PaidMessageFileNameFormat); - createFileNameFormatConfig.PostFileNameFormat = - func(createFileNameFormatConfig.PostFileNameFormat, PostFileNameFormat); - createFileNameFormatConfig.MessageFileNameFormat = - func(createFileNameFormatConfig.MessageFileNameFormat, MessageFileNameFormat); - createFileNameFormatConfig.PaidPostFileNameFormat = - func(createFileNameFormatConfig.PaidPostFileNameFormat, PaidPostFileNameFormat); - Log.Debug("PaidMessageFilenameFormat: {CombinedConfigPaidMessageFileNameFormat}", - createFileNameFormatConfig.PaidMessageFileNameFormat); + combinedFilenameFormatConfig.PaidMessageFileNameFormat); Log.Debug("PostFileNameFormat: {CombinedConfigPostFileNameFormat}", - createFileNameFormatConfig.PostFileNameFormat); + combinedFilenameFormatConfig.PostFileNameFormat); Log.Debug("MessageFileNameFormat: {CombinedConfigMessageFileNameFormat}", - createFileNameFormatConfig.MessageFileNameFormat); + combinedFilenameFormatConfig.MessageFileNameFormat); Log.Debug("PaidPostFileNameFormat: {CombinedConfigPaidPostFileNameFormat}", - createFileNameFormatConfig.PaidPostFileNameFormat); + combinedFilenameFormatConfig.PaidPostFileNameFormat); - return createFileNameFormatConfig; + return combinedFilenameFormatConfig; } } - -public class CreatorConfig : IFileNameFormatConfig -{ - public string? PaidPostFileNameFormat { get; set; } - public string? PostFileNameFormat { get; set; } - public string? PaidMessageFileNameFormat { get; set; } - public string? MessageFileNameFormat { get; set; } -} diff --git a/OF DL/Models/CreatorConfig.cs b/OF DL/Models/CreatorConfig.cs new file mode 100644 index 0000000..6c7ad40 --- /dev/null +++ b/OF DL/Models/CreatorConfig.cs @@ -0,0 +1,12 @@ +namespace OF_DL.Models; + +public class CreatorConfig : IFileNameFormatConfig +{ + public string? PaidPostFileNameFormat { get; set; } + + public string? PostFileNameFormat { get; set; } + + public string? PaidMessageFileNameFormat { get; set; } + + public string? MessageFileNameFormat { get; set; } +} diff --git a/OF DL/Models/DynamicRules.cs b/OF DL/Models/DynamicRules.cs index d1e8968..412547a 100644 --- a/OF DL/Models/DynamicRules.cs +++ b/OF DL/Models/DynamicRules.cs @@ -26,5 +26,5 @@ public class DynamicRules public int? ChecksumConstant { get; set; } [JsonProperty(PropertyName = "checksum_indexes")] - public List ChecksumIndexes { get; set; } + public List ChecksumIndexes { get; set; } = []; } diff --git a/OF DL/Models/FileNameFormatConfig.cs b/OF DL/Models/FileNameFormatConfig.cs index 8b51a05..2b1b0b2 100644 --- a/OF DL/Models/FileNameFormatConfig.cs +++ b/OF DL/Models/FileNameFormatConfig.cs @@ -3,7 +3,10 @@ namespace OF_DL.Models; public class FileNameFormatConfig : IFileNameFormatConfig { public string? PaidPostFileNameFormat { get; set; } + public string? PostFileNameFormat { get; set; } + public string? PaidMessageFileNameFormat { get; set; } + public string? MessageFileNameFormat { get; set; } } diff --git a/OF DL/Models/IFileNameFormatConfig.cs b/OF DL/Models/IFileNameFormatConfig.cs index 85b1d60..7622eee 100644 --- a/OF DL/Models/IFileNameFormatConfig.cs +++ b/OF DL/Models/IFileNameFormatConfig.cs @@ -3,7 +3,10 @@ namespace OF_DL.Models; public interface IFileNameFormatConfig { string? PaidPostFileNameFormat { get; set; } + string? PostFileNameFormat { get; set; } + string? PaidMessageFileNameFormat { get; set; } + string? MessageFileNameFormat { get; set; } } diff --git a/OF DL/Models/LatestReleaseAPIResponse.cs b/OF DL/Models/LatestReleaseApiResponse.cs similarity index 79% rename from OF DL/Models/LatestReleaseAPIResponse.cs rename to OF DL/Models/LatestReleaseApiResponse.cs index ec77eb7..3cf66ee 100644 --- a/OF DL/Models/LatestReleaseAPIResponse.cs +++ b/OF DL/Models/LatestReleaseApiResponse.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace OF_DL.Models; -public class LatestReleaseAPIResponse +public class LatestReleaseApiResponse { [JsonProperty(PropertyName = "tag_name")] public string TagName { get; set; } = ""; diff --git a/OF DL/Models/OFDLRequest.cs b/OF DL/Models/OFDLRequest.cs index 5af7a25..52fef56 100644 --- a/OF DL/Models/OFDLRequest.cs +++ b/OF DL/Models/OFDLRequest.cs @@ -2,11 +2,12 @@ using Newtonsoft.Json; namespace OF_DL.Models; +// ReSharper disable once InconsistentNaming public class OFDLRequest { - [JsonProperty("pssh")] public string PSSH { get; set; } = ""; + [JsonProperty("pssh")] public string Pssh { get; set; } = ""; - [JsonProperty("licenceURL")] public string LicenseURL { get; set; } = ""; + [JsonProperty("licenceURL")] public string LicenseUrl { get; set; } = ""; [JsonProperty("headers")] public string Headers { get; set; } = ""; } diff --git a/OF DL/Models/ToggleableConfigAttribute.cs b/OF DL/Models/ToggleableConfigAttribute.cs index 98880a5..c0e6c83 100644 --- a/OF DL/Models/ToggleableConfigAttribute.cs +++ b/OF DL/Models/ToggleableConfigAttribute.cs @@ -1,6 +1,4 @@ namespace OF_DL.Models; [AttributeUsage(AttributeTargets.Property)] -internal class ToggleableConfigAttribute : Attribute -{ -} +internal class ToggleableConfigAttribute : Attribute; diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 44b1bd7..b37abd5 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -376,12 +376,12 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); Log.Debug("FFMPEG path set in config.conf"); } - else if (!string.IsNullOrEmpty(authService.CurrentAuth!.FFMPEG_PATH) && - ValidateFilePath(authService.CurrentAuth.FFMPEG_PATH)) + else if (!string.IsNullOrEmpty(authService.CurrentAuth!.FfmpegPath) && + ValidateFilePath(authService.CurrentAuth.FfmpegPath)) { // FFmpeg path is set in auth.json and is valid (config.conf takes precedence and auth.json is only available for backward compatibility) ffmpegFound = true; - configService.CurrentConfig.FFmpegPath = authService.CurrentAuth.FFMPEG_PATH; + configService.CurrentConfig.FFmpegPath = authService.CurrentAuth.FfmpegPath; Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); Log.Debug("FFMPEG path set in auth.json"); } @@ -2638,7 +2638,7 @@ public class Program(IServiceProvider serviceProvider) public static void ValidateCookieString(Auth auth) { string pattern = @"(auth_id=\d+)|(sess=[^;]+)"; - MatchCollection matches = Regex.Matches(auth.COOKIE, pattern); + MatchCollection matches = Regex.Matches(auth.Cookie, pattern); string output = string.Join("; ", matches); @@ -2647,9 +2647,9 @@ public class Program(IServiceProvider serviceProvider) output += ";"; } - if (auth.COOKIE.Trim() != output.Trim()) + if (auth.Cookie.Trim() != output.Trim()) { - auth.COOKIE = output; + auth.Cookie = output; string newAuthString = JsonConvert.SerializeObject(auth, Formatting.Indented); File.WriteAllText("auth.json", newAuthString); } diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 0872e93..80edeb4 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -95,7 +95,7 @@ public class APIService(IAuthService authService, IConfigService configService, DateTimeOffset dto = DateTime.UtcNow; long timestamp = dto.ToUnixTimeMilliseconds(); - string input = $"{root!.StaticParam}\n{timestamp}\n{path + queryParams}\n{authService.CurrentAuth.USER_ID}"; + string input = $"{root!.StaticParam}\n{timestamp}\n{path + queryParams}\n{authService.CurrentAuth.UserId}"; byte[] inputBytes = Encoding.UTF8.GetBytes(input); byte[] hashBytes = SHA1.HashData(inputBytes); string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); @@ -108,12 +108,12 @@ public class APIService(IAuthService authService, IConfigService configService, { { "accept", "application/json, text/plain" }, { "app-token", root.AppToken! }, - { "cookie", authService.CurrentAuth!.COOKIE! }, + { "cookie", authService.CurrentAuth!.Cookie! }, { "sign", sign }, { "time", timestamp.ToString() }, - { "user-id", authService.CurrentAuth!.USER_ID! }, - { "user-agent", authService.CurrentAuth!.USER_AGENT! }, - { "x-bc", authService.CurrentAuth!.X_BC! } + { "user-id", authService.CurrentAuth!.UserId! }, + { "user-agent", authService.CurrentAuth!.UserAgent! }, + { "x-bc", authService.CurrentAuth!.XBc! } }; return headers; } @@ -1634,7 +1634,7 @@ public class APIService(IAuthService authService, IConfigService configService, } if (!configService.CurrentConfig.IgnoreOwnMessages || - list.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + list.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.UserId)) { await dbService.AddMessage(folder, list.Id, list.Text ?? "", list.Price ?? "0", list.CanPurchaseReason == "opened" ? true : @@ -1834,7 +1834,7 @@ public class APIService(IAuthService authService, IConfigService configService, message = MessagesMapper.FromDto(messageDto); if (!configService.CurrentConfig.IgnoreOwnMessages || - message.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + message.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.UserId)) { await dbService.AddMessage(folder, message.Id, message.Text ?? "", message.Price != null ? message.Price.ToString() : "0", true, false, @@ -2101,7 +2101,7 @@ public class APIService(IAuthService authService, IConfigService configService, .OrderByDescending(p => p.PostedAt ?? p.CreatedAt)) { if (!configService.CurrentConfig.IgnoreOwnMessages || - purchase.FromUser.Id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + purchase.FromUser.Id != Convert.ToInt32(authService.CurrentAuth.UserId)) { if (purchase.PostedAt != null) { @@ -3034,10 +3034,10 @@ public class APIService(IAuthService authService, IConfigService configService, HttpClient client = new(); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); - request.Headers.Add("user-agent", authService.CurrentAuth.USER_AGENT); + request.Headers.Add("user-agent", authService.CurrentAuth.UserAgent); request.Headers.Add("Accept", "*/*"); request.Headers.Add("Cookie", - $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.Cookie};"); using (HttpResponseMessage response = await client.SendAsync(request)) { response.EnsureSuccessStatusCode(); @@ -3083,10 +3083,10 @@ public class APIService(IAuthService authService, IConfigService configService, HttpClient client = new(); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); - request.Headers.Add("user-agent", authService.CurrentAuth.USER_AGENT); + request.Headers.Add("user-agent", authService.CurrentAuth.UserAgent); request.Headers.Add("Accept", "*/*"); request.Headers.Add("Cookie", - $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.Cookie};"); using (HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { @@ -3129,8 +3129,8 @@ public class APIService(IAuthService authService, IConfigService configService, CDRMProjectRequest cdrmProjectRequest = new() { - PSSH = pssh, - LicenseURL = licenceURL, + Pssh = pssh, + LicenseUrl = licenceURL, Headers = JsonConvert.SerializeObject(drmHeaders), Cookies = "", Data = "" @@ -3212,7 +3212,7 @@ public class APIService(IAuthService authService, IConfigService configService, OFDLRequest ofdlRequest = new() { - PSSH = pssh, LicenseURL = licenceURL, Headers = JsonConvert.SerializeObject(drmHeaders) + Pssh = pssh, LicenseUrl = licenceURL, Headers = JsonConvert.SerializeObject(drmHeaders) }; string json = JsonConvert.SerializeObject(ofdlRequest); diff --git a/OF DL/Services/AuthService.cs b/OF DL/Services/AuthService.cs index 8887731..3f9f654 100644 --- a/OF DL/Services/AuthService.cs +++ b/OF DL/Services/AuthService.cs @@ -212,7 +212,7 @@ public class AuthService : IAuthService string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key)) .Select(key => $"${key}={mappedCookies[key]};")); - return new Auth { COOKIE = cookies, USER_AGENT = userAgent, USER_ID = userId, X_BC = xBc }; + return new Auth { Cookie = cookies, UserAgent = userAgent, UserId = userId, XBc = xBc }; } catch (Exception e) { diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 254169d..1e6c74d 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -334,10 +334,10 @@ public class DownloadService( { HttpClient client = new(); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); - request.Headers.Add("user-agent", authService.CurrentAuth.USER_AGENT); + request.Headers.Add("user-agent", authService.CurrentAuth.UserAgent); request.Headers.Add("Accept", "*/*"); request.Headers.Add("Cookie", - $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.Cookie};"); using (HttpResponseMessage response = await client.SendAsync(request)) { response.EnsureSuccessStatusCode(); @@ -568,8 +568,8 @@ public class DownloadService( using HttpClient client = new(); client.DefaultRequestHeaders.Add("Cookie", - $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE}"); - client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.USER_AGENT); + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.Cookie}"); + client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.UserAgent); using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); @@ -581,7 +581,7 @@ public class DownloadService( else { using HttpClient client = new(); - client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.USER_AGENT); + client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.UserAgent); using HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead); if (response.IsSuccessStatusCode) @@ -612,8 +612,8 @@ public class DownloadService( using HttpClient client = new(); client.DefaultRequestHeaders.Add("Cookie", - $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {auth.COOKIE}"); - client.DefaultRequestHeaders.Add("User-Agent", auth.USER_AGENT); + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {auth.Cookie}"); + client.DefaultRequestHeaders.Add("User-Agent", auth.UserAgent); using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); if (response.IsSuccessStatusCode) @@ -1242,8 +1242,8 @@ public class DownloadService( ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { - return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, - authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); } @@ -1371,8 +1371,8 @@ public class DownloadService( ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { - return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, - authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); } @@ -1501,8 +1501,8 @@ public class DownloadService( ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { - return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, - authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); } @@ -1629,8 +1629,8 @@ public class DownloadService( ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { - return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, - authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); } @@ -1759,8 +1759,8 @@ public class DownloadService( ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { - return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, - authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); } @@ -1888,8 +1888,8 @@ public class DownloadService( ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { - return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, - authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); } @@ -2017,8 +2017,8 @@ public class DownloadService( ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { - return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, - authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); } @@ -2147,8 +2147,8 @@ public class DownloadService( ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { - return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, - authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); } @@ -2267,8 +2267,8 @@ public class DownloadService( ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { - return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, - authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, + return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); } From a8b2acaad62a60a125980f27355338b3e10ec39f Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 04:31:27 -0600 Subject: [PATCH 26/55] Fix custom filename format configs --- OF DL/Models/Config.cs | 39 ++++++++------ OF DL/Models/Dtos/Common/FilesDto.cs | 2 +- OF DL/Services/FileNameService.cs | 78 +++++++++++++++------------- OF DL/Services/IFileNameService.cs | 5 +- 4 files changed, 70 insertions(+), 54 deletions(-) diff --git a/OF DL/Models/Config.cs b/OF DL/Models/Config.cs index 4e45631..7e83618 100644 --- a/OF DL/Models/Config.cs +++ b/OF DL/Models/Config.cs @@ -105,28 +105,35 @@ public class Config : IFileNameFormatConfig public IFileNameFormatConfig GetCreatorFileNameFormatConfig(string username) { - FileNameFormatConfig combinedFilenameFormatConfig = new(); + FileNameFormatConfig combinedFilenameFormatConfig = new() + { + PaidPostFileNameFormat = PaidPostFileNameFormat, + PostFileNameFormat = PostFileNameFormat, + PaidMessageFileNameFormat = PaidMessageFileNameFormat, + MessageFileNameFormat = MessageFileNameFormat + }; if (CreatorConfigs.TryGetValue(username, out CreatorConfig? creatorConfig)) { - combinedFilenameFormatConfig.PaidMessageFileNameFormat = - !string.IsNullOrEmpty(creatorConfig.PaidMessageFileNameFormat) - ? creatorConfig.PaidMessageFileNameFormat - : PaidMessageFileNameFormat; + if (creatorConfig?.PaidPostFileNameFormat != null) + { + combinedFilenameFormatConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat; + } - combinedFilenameFormatConfig.PostFileNameFormat = !string.IsNullOrEmpty(creatorConfig.PostFileNameFormat) - ? creatorConfig.PostFileNameFormat - : PostFileNameFormat; + if (creatorConfig?.PostFileNameFormat != null) + { + combinedFilenameFormatConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat; + } - combinedFilenameFormatConfig.MessageFileNameFormat = - !string.IsNullOrEmpty(creatorConfig.MessageFileNameFormat) - ? creatorConfig.MessageFileNameFormat - : MessageFileNameFormat; + if (creatorConfig?.PaidMessageFileNameFormat != null) + { + combinedFilenameFormatConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat; + } - combinedFilenameFormatConfig.PaidPostFileNameFormat = - !string.IsNullOrEmpty(creatorConfig.PaidPostFileNameFormat) - ? creatorConfig.PaidPostFileNameFormat - : PaidPostFileNameFormat; + if (creatorConfig?.MessageFileNameFormat != null) + { + combinedFilenameFormatConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat; + } } Log.Debug("PaidMessageFilenameFormat: {CombinedConfigPaidMessageFileNameFormat}", diff --git a/OF DL/Models/Dtos/Common/FilesDto.cs b/OF DL/Models/Dtos/Common/FilesDto.cs index 13692a8..c7b9056 100644 --- a/OF DL/Models/Dtos/Common/FilesDto.cs +++ b/OF DL/Models/Dtos/Common/FilesDto.cs @@ -13,5 +13,5 @@ public class FilesDto [JsonProperty("squarePreview")] public SquarePreviewDto SquarePreview { get; set; } = new(); - [JsonProperty("drm")] public DrmDto Drm { get; set; } = new(); + [JsonProperty("drm")] public DrmDto? Drm { get; set; } } diff --git a/OF DL/Services/FileNameService.cs b/OF DL/Services/FileNameService.cs index a692874..4643637 100644 --- a/OF DL/Services/FileNameService.cs +++ b/OF DL/Services/FileNameService.cs @@ -5,12 +5,12 @@ namespace OF_DL.Services; public class FileNameService(IAuthService authService) : IFileNameService { - public async Task> GetFilename(object obj1, object obj2, object obj3, - List selectedProperties, string username, Dictionary users = null) + public async Task> GetFilename(object info, object media, object author, + List selectedProperties, string username, Dictionary? users = null) { Dictionary values = new(); - Type type1 = obj1.GetType(); - Type type2 = obj2.GetType(); + Type type1 = info.GetType(); + Type type2 = media.GetType(); PropertyInfo[] properties1 = type1.GetProperties(); PropertyInfo[] properties2 = type2.GetProperties(); @@ -18,19 +18,30 @@ public class FileNameService(IAuthService authService) : IFileNameService { if (propertyName.Contains("media")) { - object drmProperty = null; - object fileProperty = GetNestedPropertyValue(obj2, "files"); + object? drmProperty = null; + object? fileProperty = GetNestedPropertyValue(media, "Files"); if (fileProperty != null) { - drmProperty = GetNestedPropertyValue(obj2, "files.drm"); + drmProperty = GetNestedPropertyValue(media, "Files.Drm"); } if (fileProperty != null && drmProperty != null && propertyName == "mediaCreatedAt") { - object mpdurl = GetNestedPropertyValue(obj2, "files.drm.manifest.dash"); - object policy = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontPolicy"); - object signature = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontSignature"); - object kvp = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontKeyPairId"); + string? mpdurl = GetNestedPropertyValue(media, "Files.Drm.Manifest.Dash") as string; + string? policy = + GetNestedPropertyValue(media, "Files.Drm.Signature.Dash.CloudFrontPolicy") as string; + string? signature = + GetNestedPropertyValue(media, "Files.Drm.Signature.Dash.CloudFrontSignature") as string; + string? kvp = + GetNestedPropertyValue(media, "Files.Drm.Signature.Dash.CloudFrontKeyPairId") as string; + + if (string.IsNullOrEmpty(mpdurl) || string.IsNullOrEmpty(policy) || + string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(kvp) || + authService.CurrentAuth == null) + { + continue; + } + DateTime lastModified = await DownloadService.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), authService.CurrentAuth); @@ -40,18 +51,18 @@ public class FileNameService(IAuthService authService) : IFileNameService if ((fileProperty == null || drmProperty == null) && propertyName == "mediaCreatedAt") { - object source = GetNestedPropertyValue(obj2, "files.full.url"); + object? source = GetNestedPropertyValue(media, "Files.Full.Url"); if (source != null) { - DateTime lastModified = await DownloadService.GetMediaLastModified(source.ToString()); + DateTime lastModified = await DownloadService.GetMediaLastModified(source.ToString() ?? ""); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); continue; } - object preview = GetNestedPropertyValue(obj2, "preview"); + object? preview = GetNestedPropertyValue(media, "Preview"); if (preview != null) { - DateTime lastModified = await DownloadService.GetMediaLastModified(preview.ToString()); + DateTime lastModified = await DownloadService.GetMediaLastModified(preview.ToString() ?? ""); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); continue; } @@ -61,7 +72,7 @@ public class FileNameService(IAuthService authService) : IFileNameService p => p.Name.Equals(propertyName.Replace("media", ""), StringComparison.OrdinalIgnoreCase)); if (property != null) { - object? propertyValue = property.GetValue(obj2); + object? propertyValue = property.GetValue(media); if (propertyValue != null) { if (propertyValue is DateTime dateTimeValue) @@ -70,28 +81,26 @@ public class FileNameService(IAuthService authService) : IFileNameService } else { - values.Add(propertyName, propertyValue.ToString()); + values.Add(propertyName, propertyValue.ToString() ?? ""); } } } } else if (propertyName.Contains("filename")) { - string sourcePropertyPath = "files.full.url"; - object sourcePropertyValue = GetNestedPropertyValue(obj2, sourcePropertyPath); + object? sourcePropertyValue = GetNestedPropertyValue(media, "Files.Full.Url"); if (sourcePropertyValue != null) { - Uri uri = new(sourcePropertyValue.ToString()); + Uri uri = new(sourcePropertyValue.ToString() ?? ""); string filename = Path.GetFileName(uri.LocalPath); values.Add(propertyName, filename.Split(".")[0]); } else { - string propertyPath = "files.drm.manifest.dash"; - object nestedPropertyValue = GetNestedPropertyValue(obj2, propertyPath); + object? nestedPropertyValue = GetNestedPropertyValue(media, "Files.Drm.Manifest.Dash"); if (nestedPropertyValue != null) { - Uri uri = new(nestedPropertyValue.ToString()); + Uri uri = new(nestedPropertyValue.ToString() ?? ""); string filename = Path.GetFileName(uri.LocalPath); values.Add(propertyName, filename.Split(".")[0] + "_source"); } @@ -105,8 +114,7 @@ public class FileNameService(IAuthService authService) : IFileNameService } else { - string propertyPath = "id"; - object nestedPropertyValue = GetNestedPropertyValue(obj3, propertyPath); + object? nestedPropertyValue = GetNestedPropertyValue(author, "Id"); if (nestedPropertyValue != null) { values.Add(propertyName, @@ -116,11 +124,11 @@ public class FileNameService(IAuthService authService) : IFileNameService } else if (propertyName.Contains("text", StringComparison.OrdinalIgnoreCase)) { - PropertyInfo property = Array.Find(properties1, + PropertyInfo? property = Array.Find(properties1, p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); if (property != null) { - object propertyValue = property.GetValue(obj1); + object? propertyValue = property.GetValue(info); if (propertyValue != null) { HtmlDocument pageDoc = new(); @@ -137,11 +145,11 @@ public class FileNameService(IAuthService authService) : IFileNameService } else { - PropertyInfo property = Array.Find(properties1, + PropertyInfo? property = Array.Find(properties1, p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); if (property != null) { - object propertyValue = property.GetValue(obj1); + object? propertyValue = property.GetValue(info); if (propertyValue != null) { if (propertyValue is DateTime dateTimeValue) @@ -150,7 +158,7 @@ public class FileNameService(IAuthService authService) : IFileNameService } else { - values.Add(propertyName, propertyValue.ToString()); + values.Add(propertyName, propertyValue.ToString() ?? ""); } } } @@ -160,7 +168,7 @@ public class FileNameService(IAuthService authService) : IFileNameService return values; } - public async Task BuildFilename(string fileFormat, Dictionary values) + public Task BuildFilename(string fileFormat, Dictionary values) { foreach (KeyValuePair kvp in values) { @@ -168,15 +176,15 @@ public class FileNameService(IAuthService authService) : IFileNameService fileFormat = fileFormat.Replace(placeholder, kvp.Value); } - return RemoveInvalidFileNameChars($"{fileFormat}"); + return Task.FromResult(RemoveInvalidFileNameChars($"{fileFormat}")); } - private static object GetNestedPropertyValue(object source, string propertyPath) + private static object? GetNestedPropertyValue(object source, string propertyPath) { - object value = source; + object? value = source; foreach (string propertyName in propertyPath.Split('.')) { - PropertyInfo property = value.GetType().GetProperty(propertyName) ?? + PropertyInfo property = value?.GetType().GetProperty(propertyName) ?? throw new ArgumentException($"Property '{propertyName}' not found."); value = property.GetValue(value); } diff --git a/OF DL/Services/IFileNameService.cs b/OF DL/Services/IFileNameService.cs index 4e4a026..35d5c2f 100644 --- a/OF DL/Services/IFileNameService.cs +++ b/OF DL/Services/IFileNameService.cs @@ -4,6 +4,7 @@ public interface IFileNameService { Task BuildFilename(string fileFormat, Dictionary values); - Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, - string username, Dictionary users = null); + Task> GetFilename(object info, object media, object author, + List selectedProperties, + string username, Dictionary? users = null); } From 44a9fb1fcde74165fa26dbebd391bf74ab573e00 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 04:34:38 -0600 Subject: [PATCH 27/55] Reduce duplicated code and simplify download media methods --- OF DL/Program.cs | 564 +++----- OF DL/Services/DownloadService.cs | 2049 +++++----------------------- OF DL/Services/IDownloadService.cs | 106 +- 3 files changed, 480 insertions(+), 2239 deletions(-) diff --git a/OF DL/Program.cs b/OF DL/Program.cs index b37abd5..19dd492 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -1491,102 +1491,50 @@ public class Program(IServiceProvider serviceProvider) foreach (KeyValuePair purchasedPostKVP in purchasedPosts.PaidPosts) { bool isNew; + MessageEntities.Medium? mediaInfo = + purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); + PurchasedEntities.ListItem? postInfo = mediaInfo != null + ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true) + : null; + string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidPostFileNameFormat ?? ""; + string paidPostPath = configService.CurrentConfig.FolderPerPaidPost && postInfo != null && + postInfo?.Id is not null && postInfo?.PostedAt is not null + ? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Posts/Paid"; + if (purchasedPostKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] messageUrlParsed = purchasedPostKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh == null) + string[] parsed = purchasedPostKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = + await downloadService.GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], + parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { continue; } - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = - apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", - "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", - pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", - pssh); - } - - MessageEntities.Medium? mediaInfo = - purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); - PurchasedEntities.ListItem? postInfo = mediaInfo != null - ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true) - : null; - - isNew = await downloadService.DownloadPurchasedPostDRMVideo( - policy, - signature, - kvp, - mpdURL, - decryptionKey, - path, - lastModified, - purchasedPostKVP.Key, + isNew = await downloadService.DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, purchasedPostKVP.Key, "Posts", - new SpectreProgressReporter(task), - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidPostFileNameFormat ?? "", - postInfo, - mediaInfo, - postInfo?.FromUser, - users); - if (isNew) - { - newPaidPostCount++; - } - else - { - oldPaidPostCount++; - } + new SpectreProgressReporter(task), paidPostPath + "/Videos", filenameFormat, + postInfo, mediaInfo, postInfo?.FromUser, users); } else { - MessageEntities.Medium? mediaInfo = - purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); - PurchasedEntities.ListItem? postInfo = mediaInfo != null - ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true) - : null; + isNew = await downloadService.DownloadMedia(purchasedPostKVP.Value, path, + purchasedPostKVP.Key, "Posts", new SpectreProgressReporter(task), + paidPostPath, filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users); + } - isNew = await downloadService.DownloadPurchasedPostMedia( - purchasedPostKVP.Value, - path, - purchasedPostKVP.Key, - "Posts", - new SpectreProgressReporter(task), - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidPostFileNameFormat ?? "", - postInfo, - mediaInfo, - postInfo?.FromUser, - users); - if (isNew) - { - newPaidPostCount++; - } - else - { - oldPaidPostCount++; - } + if (isNew) + { + newPaidPostCount++; + } + else + { + oldPaidPostCount++; } } @@ -1643,101 +1591,49 @@ public class Program(IServiceProvider serviceProvider) foreach (KeyValuePair paidMessageKVP in paidMessageCollection.PaidMessages) { bool isNew; + MessageEntities.Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKVP.Key); + PurchasedEntities.ListItem? messageInfo = + paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? ""; + string paidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && + messageInfo?.Id is not null && messageInfo?.CreatedAt is not null + ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Messages/Paid"; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] messageUrlParsed = paidMessageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) + string[] parsed = paidMessageKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = + await downloadService.GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], + parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { - DateTime lastModified = - await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = - apiService.GetDynamicHeaders( - $"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", - pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", - pssh); - } - - MessageEntities.Medium? mediaInfo = - paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => - m.Id == paidMessageKVP.Key); - PurchasedEntities.ListItem? messageInfo = - paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); - - isNew = await downloadService.DownloadPurchasedMessageDRMVideo( - policy, - signature, - kvp, - mpdURL, - decryptionKey, - path, - lastModified, - paidMessageKVP.Key, - "Messages", - new SpectreProgressReporter(task), - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? "", - messageInfo, - mediaInfo, - messageInfo?.FromUser, - users); - - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } + continue; } + + isNew = await downloadService.DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), paidMsgPath + "/Videos", filenameFormat, + messageInfo, mediaInfo, messageInfo?.FromUser, users); } else { - MessageEntities.Medium? mediaInfo = - paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKVP.Key); - PurchasedEntities.ListItem messageInfo = - paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + isNew = await downloadService.DownloadMedia(paidMessageKVP.Value, path, + paidMessageKVP.Key, "Messages", new SpectreProgressReporter(task), + paidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); + } - isNew = await downloadService.DownloadPurchasedMedia( - paidMessageKVP.Value, - path, - paidMessageKVP.Key, - "Messages", - new SpectreProgressReporter(task), - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? "", - messageInfo, - mediaInfo, - messageInfo?.FromUser, - users); - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } + if (isNew) + { + newPaidMessagesCount++; + } + else + { + oldPaidMessagesCount++; } } @@ -1826,6 +1722,9 @@ public class Program(IServiceProvider serviceProvider) IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); + string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? ""; + Log.Debug($"Calling DownloadPaidMessage - {username}"); AnsiConsole.Markup("[red]Getting Paid Message\n[/]"); @@ -1870,102 +1769,50 @@ public class Program(IServiceProvider serviceProvider) .PreviewSingleMessages) { bool isNew; + MessageEntities.Medium? mediaInfo = + singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => + m.Id == paidMessageKVP.Key); + MessageEntities.SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + + string previewMsgPath = configService.CurrentConfig.FolderPerMessage && messageInfo != null && + messageInfo?.Id is not null && messageInfo?.CreatedAt is not null + ? $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Messages/Free"; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] messageUrlParsed = paidMessageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) + string[] parsed = paidMessageKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = + await downloadService.GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], + parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { - DateTime lastModified = - await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = - apiService.GetDynamicHeaders( - $"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", - pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", - pssh); - } - - MessageEntities.Medium? mediaInfo = - singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => - m.Id == paidMessageKVP.Key); - MessageEntities.SingleMessage? messageInfo = - singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); - - isNew = await downloadService.DownloadSingleMessagePreviewDRMVideo( - policy, - signature, - kvp, - mpdURL, - decryptionKey, - path, - lastModified, - paidMessageKVP.Key, - "Messages", - new SpectreProgressReporter(task), - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? "", - messageInfo, - mediaInfo, - messageInfo?.FromUser, - hasSelectedUsersKVP.Value); - - if (isNew) - { - newPreviewPaidMessagesCount++; - } - else - { - oldPreviewPaidMessagesCount++; - } + continue; } + + isNew = await downloadService.DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), previewMsgPath + "/Videos", filenameFormat, + messageInfo, mediaInfo, messageInfo?.FromUser, hasSelectedUsersKVP.Value); } else { - MessageEntities.Medium? mediaInfo = - singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => - m.Id == paidMessageKVP.Key); - MessageEntities.SingleMessage? messageInfo = - singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); - - isNew = await downloadService.DownloadMessagePreviewMedia( - paidMessageKVP.Value, - path, - paidMessageKVP.Key, - "Messages", - new SpectreProgressReporter(task), - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? "", - messageInfo, - mediaInfo, - messageInfo?.FromUser, + isNew = await downloadService.DownloadMedia(paidMessageKVP.Value, path, + paidMessageKVP.Key, "Messages", new SpectreProgressReporter(task), + previewMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, hasSelectedUsersKVP.Value); - if (isNew) - { - newPreviewPaidMessagesCount++; - } - else - { - oldPreviewPaidMessagesCount++; - } + } + + if (isNew) + { + newPreviewPaidMessagesCount++; + } + else + { + oldPreviewPaidMessagesCount++; } } @@ -2010,93 +1857,42 @@ public class Program(IServiceProvider serviceProvider) foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.SingleMessages) { bool isNew; + MessageEntities.Medium? mediaInfo = + singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => + m.Id == paidMessageKVP.Key); + MessageEntities.SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? ""; + string singlePaidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && + messageInfo != null && messageInfo?.Id is not null && + messageInfo?.CreatedAt is not null + ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Messages/Paid"; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] messageUrlParsed = paidMessageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) + string[] parsed = paidMessageKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = + await downloadService.GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], + parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { - DateTime lastModified = - await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = - apiService.GetDynamicHeaders( - $"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", - pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", - pssh); - } - - MessageEntities.Medium? mediaInfo = - singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => - m.Id == paidMessageKVP.Key); - MessageEntities.SingleMessage? messageInfo = - singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); - - isNew = await downloadService.DownloadSinglePurchasedMessageDRMVideo( - policy, - signature, - kvp, - mpdURL, - decryptionKey, - path, - lastModified, - paidMessageKVP.Key, - "Messages", - new SpectreProgressReporter(task), - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? "", - messageInfo, - mediaInfo, - messageInfo?.FromUser, - hasSelectedUsersKVP.Value); - - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } + continue; } + + isNew = await downloadService.DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), singlePaidMsgPath + "/Videos", filenameFormat, + messageInfo, mediaInfo, messageInfo?.FromUser, hasSelectedUsersKVP.Value); } else { - MessageEntities.Medium? mediaInfo = - singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => - m.Id == paidMessageKVP.Key); - MessageEntities.SingleMessage? messageInfo = - singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); - - isNew = await downloadService.DownloadSinglePurchasedMedia( - paidMessageKVP.Value, - path, - paidMessageKVP.Key, - "Messages", - new SpectreProgressReporter(task), - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? "", - messageInfo, - mediaInfo, - messageInfo?.FromUser, + isNew = await downloadService.DownloadMedia(paidMessageKVP.Value, path, + paidMessageKVP.Key, "Messages", new SpectreProgressReporter(task), + singlePaidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, hasSelectedUsersKVP.Value); if (isNew) { @@ -2162,83 +1958,39 @@ public class Program(IServiceProvider serviceProvider) task.StartTask(); foreach (KeyValuePair postKVP in post.SinglePosts) { + PostEntities.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.Id == postKVP.Key); + PostEntities.SinglePost? postInfo = + post.SinglePostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); + string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PostFileNameFormat ?? ""; + string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && + postInfo?.Id is not null && postInfo?.PostedAt is not null + ? $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}" + : "/Posts/Free"; + if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] messageUrlParsed = postKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh == null) + string[] parsed = postKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = + await downloadService.GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], + parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { continue; } - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = - apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", - "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", - pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", - pssh); - } - - PostEntities.Medium mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.Id == postKVP.Key); - PostEntities.SinglePost postInfo = - post.SinglePostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); - - isNew = await downloadService.DownloadPostDRMVideo( - policy, - signature, - kvp, - mpdURL, - decryptionKey, - path, - lastModified, - postKVP.Key, - "Posts", - new SpectreProgressReporter(task), - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - "", - postInfo, - mediaInfo, - postInfo?.Author, - users); + isNew = await downloadService.DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKVP.Key, "Posts", + new SpectreProgressReporter(task), postPath + "/Videos", filenameFormat, + postInfo, mediaInfo, postInfo?.Author, users); } else { try { - PostEntities.Medium? mediaInfo = - post.SinglePostMedia.FirstOrDefault(m => m.Id == postKVP.Key); - PostEntities.SinglePost? postInfo = - post.SinglePostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); - - isNew = await downloadService.DownloadPostMedia( - postKVP.Value, - path, - postKVP.Key, - "Posts", - new SpectreProgressReporter(task), - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PostFileNameFormat ?? "", - postInfo, - mediaInfo, - postInfo?.Author, - users); + isNew = await downloadService.DownloadMedia(postKVP.Value, path, + postKVP.Key, "Posts", new SpectreProgressReporter(task), + postPath, filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); } catch { diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index 1e6c74d..da1b941 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -6,7 +6,6 @@ using FFmpeg.NET.Events; using OF_DL.Models; using OF_DL.Enumerations; using ArchivedEntities = OF_DL.Models.Entities.Archived; -using CommonEntities = OF_DL.Models.Entities.Common; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; @@ -40,90 +39,12 @@ public class DownloadService( if (!string.IsNullOrEmpty(avatarUrl)) { - string avatarpath = $"{path}/Avatars"; - if (!Directory.Exists(folder + avatarpath)) - { - Directory.CreateDirectory(folder + avatarpath); - } - - List avatarMD5Hashes = CalculateFolderMD5(folder + avatarpath); - - Uri uri = new(avatarUrl); - string destinationPath = $"{folder}{avatarpath}/"; - - HttpClient client = new(); - - HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = uri }; - using HttpResponseMessage response = - await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - response.EnsureSuccessStatusCode(); - - using MemoryStream memoryStream = new(); - await response.Content.CopyToAsync(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - - MD5 md5 = MD5.Create(); - byte[] hash = md5.ComputeHash(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - if (!avatarMD5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant())) - { - destinationPath = destinationPath + string.Format("{0} {1}.jpg", username, - response.Content.Headers.LastModified.HasValue - ? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy") - : DateTime.Now.ToString("dd-MM-yyyy")); - - using (FileStream fileStream = File.Create(destinationPath)) - { - await memoryStream.CopyToAsync(fileStream); - } - - File.SetLastWriteTime(destinationPath, - response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); - } + await DownloadProfileImage(avatarUrl, folder, $"{path}/Avatars", username); } if (!string.IsNullOrEmpty(headerUrl)) { - string headerpath = $"{path}/Headers"; - if (!Directory.Exists(folder + headerpath)) - { - Directory.CreateDirectory(folder + headerpath); - } - - List headerMD5Hashes = CalculateFolderMD5(folder + headerpath); - - Uri uri = new(headerUrl); - string destinationPath = $"{folder}{headerpath}/"; - - HttpClient client = new(); - - HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = uri }; - using HttpResponseMessage response = - await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - response.EnsureSuccessStatusCode(); - - using MemoryStream memoryStream = new(); - await response.Content.CopyToAsync(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - - MD5 md5 = MD5.Create(); - byte[] hash = md5.ComputeHash(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - if (!headerMD5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant())) - { - destinationPath = destinationPath + string.Format("{0} {1}.jpg", username, - response.Content.Headers.LastModified.HasValue - ? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy") - : DateTime.Now.ToString("dd-MM-yyyy")); - - using (FileStream fileStream = File.Create(destinationPath)) - { - await memoryStream.CopyToAsync(fileStream); - } - - File.SetLastWriteTime(destinationPath, - response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); - } + await DownloadProfileImage(headerUrl, folder, $"{path}/Headers", username); } } catch (Exception ex) @@ -139,7 +60,48 @@ public class DownloadService( } } - #region drm common + private async Task DownloadProfileImage(string url, string folder, string subFolder, string username) + { + if (!Directory.Exists(folder + subFolder)) + { + Directory.CreateDirectory(folder + subFolder); + } + + List md5Hashes = CalculateFolderMD5(folder + subFolder); + + Uri uri = new(url); + string destinationPath = $"{folder}{subFolder}/"; + + HttpClient client = new(); + + HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = uri }; + using HttpResponseMessage response = + await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + using MemoryStream memoryStream = new(); + await response.Content.CopyToAsync(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + + MD5 md5 = MD5.Create(); + byte[] hash = md5.ComputeHash(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + if (!md5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant())) + { + destinationPath = destinationPath + string.Format("{0} {1}.jpg", username, + response.Content.Headers.LastModified.HasValue + ? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy") + : DateTime.Now.ToString("dd-MM-yyyy")); + + using (FileStream fileStream = File.Create(destinationPath)) + { + await memoryStream.CopyToAsync(fileStream); + } + + File.SetLastWriteTime(destinationPath, + response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); + } + } private async Task DownloadDrmMedia(string user_agent, string policy, string signature, string kvp, string sess, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, @@ -265,8 +227,6 @@ public class DownloadService( return false; } - #endregion - private async Task OnFFMPEGDownloadComplete(string tempFilename, DateTime lastModified, string folder, string path, string customFileName, string filename, long media_id, string api_type, IProgressReporter progressReporter) { @@ -404,8 +364,6 @@ public class DownloadService( } } - #region common - /// /// /// @@ -631,7 +589,10 @@ public class DownloadService( using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); if (response.IsSuccessStatusCode) { - return response.Content.Headers.LastModified.Value.DateTime; + if (response.Content.Headers.LastModified != null) + { + return response.Content.Headers.LastModified.Value.DateTime; + } } return DateTime.Now; @@ -960,26 +921,11 @@ public class DownloadService( return totalFileSize; } - #endregion - - #region normal posts - - public async Task DownloadPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PostEntities.ListItem? postInfo, - PostEntities.Medium? postMedia, - CommonEntities.Author? author, Dictionary users) + public async Task DownloadMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string path, + string? filenameFormat, object? postInfo, object? postMedia, + object? author, Dictionary users) { - string path; - if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.Id is not null && - postInfo?.PostedAt is not null) - { - path = $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Posts/Free"; - } - Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, @@ -989,1369 +935,142 @@ public class DownloadService( filename, resolvedFilename); } - public async Task DownloadPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PostEntities.SinglePost? postInfo, - PostEntities.Medium? postMedia, - CommonEntities.Author? author, Dictionary users) + public async Task DownloadDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string path, + string? filenameFormat, object? postInfo, object? postMedia, + object? author, Dictionary users) { - string path; - if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.Id is not null && - postInfo?.PostedAt is not null) + try { - path = $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}"; + Uri uri = new(url); + string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } + + string customFileName = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, + folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnEmpty); + + if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { + if (!string.IsNullOrEmpty(customFileName) + ? !File.Exists(folder + path + "/" + customFileName + ".mp4") + : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { + return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, + progressReporter, customFileName, filename, path); + } + + long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) + ? folder + path + "/" + customFileName + ".mp4" + : folder + path + "/" + filename + "_source.mp4").Length; + ReportProgress(progressReporter, fileSizeInBytes); + + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + !string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4", + fileSizeInBytes, true, lastModified); + } + else + { + if (!string.IsNullOrEmpty(customFileName)) + { + if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && + filename + "_source" != customFileName) + { + string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; + string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + + try + { + File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + size, true, lastModified); + } + } + + if (configService.CurrentConfig.ShowScrapeSize) + { + long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + progressReporter.ReportProgress(size); + } + else + { + progressReporter.ReportProgress(1); + } + } + + return false; + } + 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); + } + } + + return false; + } + + private void ReportProgress(IProgressReporter reporter, long sizeOrCount) + { + if (configService.CurrentConfig.ShowScrapeSize) + { + reporter.ReportProgress(sizeOrCount); } else { - path = "/Posts/Free"; + reporter.ReportProgress(1); } - - Uri uri = new(url); - string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, - folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); - - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, - filename, resolvedFilename); } - public async Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, StreamEntities.ListItem? streamInfo, - StreamEntities.Medium? streamMedia, CommonEntities.Author? author, - Dictionary users) + public async Task<(string decryptionKey, DateTime lastModified)?> GetDecryptionInfo( + string mpdURL, string policy, string signature, string kvp, + string mediaId, string contentId, string drmType, + bool clientIdBlobMissing, bool devicePrivateKeyMissing) { - string path; - if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.Id is not null && - streamInfo?.PostedAt is not null) + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh == null) { - path = $"/Posts/Free/{streamInfo.Id} {streamInfo.PostedAt:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Posts/Free"; + return null; } - Uri uri = new(url); - string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, streamInfo, streamMedia, - author, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/{drmType}/{contentId}", + "?type=widevine"); + string licenseUrl = + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/{drmType}/{contentId}?type=widevine"; - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, - filename, resolvedFilename); + string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing + ? await apiService.GetDecryptionKeyOFDL(drmHeaders, licenseUrl, pssh) + : await apiService.GetDecryptionKeyCDM(drmHeaders, licenseUrl, pssh); + + return (decryptionKey, lastModified); } - - public async Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) - { - string path; - if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.Id is not null && - messageInfo?.CreatedAt is not null) - { - path = $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Messages/Free"; - } - - Uri uri = new(url); - string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, - fromUser, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, - filename, resolvedFilename); - } - - public async Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) - { - string path; - if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.Id is not null && - messageInfo?.CreatedAt is not null) - { - path = $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Messages/Free"; - } - - Uri uri = new(url); - string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, - fromUser, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, - filename, resolvedFilename); - } - - - public async Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? messageInfo, - ArchivedEntities.Medium? messageMedia, CommonEntities.Author? author, - Dictionary users) - { - string path = "/Archived/Posts/Free"; - Uri uri = new(url); - string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, - author, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, - filename, resolvedFilename); - } - - - public async Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter) - { - string path = "/Stories/Free"; - Uri uri = new(url); - string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, - filename, filename); - } - - public async Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, - CommonEntities.FromUser? fromUser, Dictionary users) - { - string path; - if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.Id is not null && - messageInfo?.CreatedAt is not null) - { - path = $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Messages/Paid"; - } - - Uri uri = new(url); - string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, - fromUser, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, - filename, resolvedFilename); - } - - public async Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) - { - string path; - if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.Id is not null && - messageInfo?.CreatedAt is not null) - { - path = $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Messages/Paid"; - } - - Uri uri = new(url); - string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, - fromUser, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, - filename, resolvedFilename); - } - - public async Task DownloadPurchasedPostMedia(string url, - string folder, - long media_id, - string api_type, - IProgressReporter progressReporter, - string? filenameFormat, - PurchasedEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, - CommonEntities.FromUser? fromUser, - Dictionary users) - { - string path; - if (configService.CurrentConfig.FolderPerPaidPost && messageInfo != null && messageInfo?.Id is not null && - messageInfo?.PostedAt is not null) - { - path = $"/Posts/Paid/{messageInfo.Id} {messageInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}"; - } - else - { - path = "/Posts/Paid"; - } - - Uri uri = new(url); - string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); - string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, - fromUser, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, - filename, resolvedFilename); - } - - #endregion - - - #region drm posts - - public async Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) - { - try - { - string customFileName = ""; - string path; - Uri uri = new(url); - string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.Id is not null && - messageInfo?.CreatedAt is not null) - { - path = $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Messages/Free/Videos"; - } - - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - - if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - - Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, - fromUser, properties, folder.Split("/")[^1], users); - customFileName = await fileNameService.BuildFilename(filenameFormat, values); - } - - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) - ? !File.Exists(folder + path + "/" + customFileName + ".mp4") - : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, - authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, - progressReporter, customFileName, filename, path); - } - - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) - ? folder + path + "/" + customFileName + ".mp4" - : folder + path + "/" + filename + "_source.mp4").Length; - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } - - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, - !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", - fileSizeInBytes, true, lastModified); - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && - filename + "_source" != customFileName) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", - size, true, lastModified); - } - } - - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } - } - - return false; - } - 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); - } - } - - return false; - } - - public async Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, - string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) - { - try - { - string customFileName = ""; - string path; - Uri uri = new(url); - string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.Id is not null && - messageInfo?.CreatedAt is not null) - { - path = $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Messages/Free/Videos"; - } - - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - - if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - - Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, - fromUser, properties, folder.Split("/")[^1], users); - customFileName = await fileNameService.BuildFilename(filenameFormat, values); - } - - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) - ? !File.Exists(folder + path + "/" + customFileName + ".mp4") - : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, - authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, - progressReporter, customFileName, filename, path); - } - - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) - ? folder + path + "/" + customFileName + ".mp4" - : folder + path + "/" + filename + "_source.mp4").Length; - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } - - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, - !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", - fileSizeInBytes, true, lastModified); - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && - filename + "_source" != customFileName) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", - size, true, lastModified); - } - } - - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } - } - - return false; - } - 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); - } - } - - return false; - } - - - public async Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, - CommonEntities.FromUser? fromUser, Dictionary users) - { - try - { - string customFileName = ""; - string path; - Uri uri = new(url); - string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && - messageInfo?.Id is not null && messageInfo?.CreatedAt is not null) - { - path = $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Messages/Paid/Videos"; - } - - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - - Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, - fromUser, properties, folder.Split("/")[^1], users); - customFileName = await fileNameService.BuildFilename(filenameFormat, values); - } - - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) - ? !File.Exists(folder + path + "/" + customFileName + ".mp4") - : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, - authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, - progressReporter, customFileName, filename, path); - } - - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) - ? folder + path + "/" + customFileName + ".mp4" - : folder + path + "/" + filename + "_source.mp4").Length; - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } - - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, - !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", - fileSizeInBytes, true, lastModified); - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && - filename + "_source" != customFileName) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", - size, true, lastModified); - } - } - - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } - } - - return false; - } - 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); - } - } - - return false; - } - - public async Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, - string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users) - { - try - { - string customFileName = ""; - string path; - Uri uri = new(url); - string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && - messageInfo?.Id is not null && messageInfo?.CreatedAt is not null) - { - path = $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Messages/Paid/Videos"; - } - - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - - Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, - fromUser, properties, folder.Split("/")[^1], users); - customFileName = await fileNameService.BuildFilename(filenameFormat, values); - } - - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) - ? !File.Exists(folder + path + "/" + customFileName + ".mp4") - : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, - authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, - progressReporter, customFileName, filename, path); - } - - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) - ? folder + path + "/" + customFileName + ".mp4" - : folder + path + "/" + filename + "_source.mp4").Length; - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } - - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, - !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", - fileSizeInBytes, true, lastModified); - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && - filename + "_source" != customFileName) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", - size, true, lastModified); - } - } - - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } - } - - return false; - } - 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); - } - } - - return false; - } - - - public async Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PostEntities.ListItem? postInfo, - PostEntities.Medium? postMedia, - CommonEntities.Author? author, Dictionary users) - { - try - { - string customFileName = ""; - string path; - Uri uri = new(url); - string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.Id is not null && - postInfo?.PostedAt is not null) - { - path = $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Posts/Free/Videos"; - } - - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - - Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, - properties, folder.Split("/")[^1], users); - customFileName = await fileNameService.BuildFilename(filenameFormat, values); - } - - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) - ? !File.Exists(folder + path + "/" + customFileName + ".mp4") - : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, - authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, - progressReporter, customFileName, filename, path); - } - - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) - ? folder + path + "/" + customFileName + ".mp4" - : folder + path + "/" + filename + "_source.mp4").Length; - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } - - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, - !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", - fileSizeInBytes, true, lastModified); - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && - filename + "_source" != customFileName) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", - size, true, lastModified); - } - } - - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } - } - - return false; - } - 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); - } - } - - return false; - } - - public async Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string filenameFormat, PostEntities.SinglePost postInfo, - PostEntities.Medium postMedia, - CommonEntities.Author author, Dictionary users) - { - try - { - string customFileName = ""; - string path; - Uri uri = new(url); - string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.Id is not null && - postInfo?.PostedAt is not null) - { - path = $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Posts/Free/Videos"; - } - - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - - Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, - properties, folder.Split("/")[^1], users); - customFileName = await fileNameService.BuildFilename(filenameFormat, values); - } - - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) - ? !File.Exists(folder + path + "/" + customFileName + ".mp4") - : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, - authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, - progressReporter, customFileName, filename, path); - } - - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) - ? folder + path + "/" + customFileName + ".mp4" - : folder + path + "/" + filename + "_source.mp4").Length; - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } - - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, - !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", - fileSizeInBytes, true, lastModified); - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && - filename + "_source" != customFileName) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", - size, true, lastModified); - } - } - - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } - } - - return false; - } - 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); - } - } - - return false; - } - - public async Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, StreamEntities.ListItem? streamInfo, - StreamEntities.Medium? streamMedia, CommonEntities.Author? author, - Dictionary users) - { - try - { - string customFileName = ""; - string path; - Uri uri = new(url); - string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.Id is not null && - streamInfo?.PostedAt is not null) - { - path = $"/Posts/Free/{streamInfo.Id} {streamInfo.PostedAt:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Posts/Free/Videos"; - } - - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && streamInfo != null && streamMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - - Dictionary values = await fileNameService.GetFilename(streamInfo, streamMedia, author, - properties, folder.Split("/")[^1], users); - customFileName = await fileNameService.BuildFilename(filenameFormat, values); - } - - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) - ? !File.Exists(folder + path + "/" + customFileName + ".mp4") - : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, - authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, - progressReporter, customFileName, filename, path); - } - - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) - ? folder + path + "/" + customFileName + ".mp4" - : folder + path + "/" + filename + "_source.mp4").Length; - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } - - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, - !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", - fileSizeInBytes, true, lastModified); - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && - filename + "_source" != customFileName) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", - size, true, lastModified); - } - } - - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } - } - - return false; - } - 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); - } - } - - return false; - } - - public async Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? postInfo, - MessageEntities.Medium? postMedia, - CommonEntities.FromUser? fromUser, Dictionary users) - { - try - { - string customFileName = ""; - string path; - Uri uri = new(url); - string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - if (configService.CurrentConfig.FolderPerPaidPost && postInfo != null && postInfo?.Id is not null && - postInfo?.PostedAt is not null) - { - path = $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; - } - else - { - path = "/Posts/Paid/Videos"; - } - - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - - if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - - Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, fromUser, - properties, folder.Split("/")[^1], users); - customFileName = await fileNameService.BuildFilename(filenameFormat, values); - } - - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) - ? !File.Exists(folder + path + "/" + customFileName + ".mp4") - : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, - authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, - progressReporter, customFileName, filename, path); - } - - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) - ? folder + path + "/" + customFileName + ".mp4" - : folder + path + "/" + filename + "_source.mp4").Length; - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } - - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, - !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", - fileSizeInBytes, true, lastModified); - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && - filename + "_source" != customFileName) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", - size, true, lastModified); - } - } - - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } - } - - return false; - } - 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); - } - } - - return false; - } - - - public async Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? postInfo, - ArchivedEntities.Medium? postMedia, - CommonEntities.Author? author, Dictionary users) - { - try - { - string customFileName = ""; - Uri uri = new(url); - string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; - string path = "/Archived/Posts/Free/Videos"; - if (!Directory.Exists(folder + path)) - { - Directory.CreateDirectory(folder + path); - } - - if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) - { - List properties = new(); - string pattern = @"\{(.*?)\}"; - MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) - { - properties.Add(match.Groups[1].Value); - } - - Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, - properties, folder.Split("/")[^1], users); - customFileName = await fileNameService.BuildFilename(filenameFormat, values); - } - - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) - { - if (!string.IsNullOrEmpty(customFileName) - ? !File.Exists(folder + path + "/" + customFileName + ".mp4") - : !File.Exists(folder + path + "/" + filename + "_source.mp4")) - { - return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, - authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, - progressReporter, customFileName, filename, path); - } - - long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) - ? folder + path + "/" + customFileName + ".mp4" - : folder + path + "/" + filename + "_source.mp4").Length; - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } - - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, - !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", - fileSizeInBytes, true, lastModified); - } - else - { - if (!string.IsNullOrEmpty(customFileName)) - { - if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && - filename + "_source" != customFileName) - { - string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; - string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) - { - return false; - } - - try - { - File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - return false; - } - - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", - size, true, lastModified); - } - } - - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } - } - - return false; - } - 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); - } - } - - return false; - } - - #endregion - - #region Collection Download Methods - public async Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter) { @@ -2381,7 +1100,8 @@ public class DownloadService( foreach (KeyValuePair highlightKVP in highlights) { bool isNew = - await DownloadStoryMedia(highlightKVP.Value, path, highlightKVP.Key, "Stories", progressReporter); + await DownloadMedia(highlightKVP.Value, path, highlightKVP.Key, "Stories", progressReporter, + "/Stories/Free", null, null, null, null, new Dictionary()); if (isNew) { newHighlightsCount++; @@ -2433,7 +1153,8 @@ public class DownloadService( foreach (KeyValuePair storyKVP in stories) { - bool isNew = await DownloadStoryMedia(storyKVP.Value, path, storyKVP.Key, "Stories", progressReporter); + bool isNew = await DownloadMedia(storyKVP.Value, path, storyKVP.Key, "Stories", progressReporter, + "/Stories/Free", null, null, null, null, new Dictionary()); if (isNew) { newStoriesCount++; @@ -2484,84 +1205,33 @@ public class DownloadService( foreach (KeyValuePair archivedKVP in archived.ArchivedPosts) { bool isNew; + ArchivedEntities.Medium? mediaInfo = + archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); + ArchivedEntities.ListItem? postInfo = + archived.ArchivedPostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); + string filenameFormat = + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; + if (archivedKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] messageUrlParsed = archivedKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - - if (pssh != null) - { - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = - apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", - "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", - pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", - pssh); - } - - ArchivedEntities.Medium? mediaInfo = - archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); - ArchivedEntities.ListItem? postInfo = - archived.ArchivedPostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); - - isNew = await DownloadArchivedPostDRMVideo( - policy, - signature, - kvp, - mpdURL, - decryptionKey, - path, - lastModified, - archivedKVP.Key, - "Posts", - progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - "", - postInfo, - mediaInfo, - postInfo?.Author, - users); - } - else + string[] parsed = archivedKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], + parsed[2], parsed[3], + parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { continue; } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, archivedKVP.Key, "Posts", + progressReporter, "/Archived/Posts/Free/Videos", filenameFormat, + postInfo, mediaInfo, postInfo?.Author, users); } else { - ArchivedEntities.Medium? mediaInfo = - archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); - ArchivedEntities.ListItem? postInfo = - archived.ArchivedPostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); - - isNew = await DownloadArchivedMedia( - archivedKVP.Value, - path, - archivedKVP.Key, - "Posts", - progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - "", - postInfo, - mediaInfo, - postInfo?.Author, - users); + isNew = await DownloadMedia(archivedKVP.Value, path, archivedKVP.Key, "Posts", progressReporter, + "/Archived/Posts/Free", filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); } if (isNew) @@ -2614,83 +1284,36 @@ public class DownloadService( foreach (KeyValuePair messageKVP in messages.Messages) { bool isNew; + MessageEntities.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.Id == messageKVP.Key); + MessageEntities.ListItem? messageInfo = messages.MessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == messageKVP.Key) == true); + string filenameFormat = + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? ""; + string messagePath = configService.CurrentConfig.FolderPerMessage && messageInfo != null && + messageInfo?.Id is not null && messageInfo?.CreatedAt is not null + ? $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Messages/Free"; + if (messageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] messageUrlParsed = messageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - - if (pssh != null) - { - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = - apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", - "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", - pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", - pssh); - } - - MessageEntities.Medium? mediaInfo = - messages.MessageMedia.FirstOrDefault(m => m.Id == messageKVP.Key); - MessageEntities.ListItem? messageInfo = messages.MessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == messageKVP.Key) == true); - - isNew = await DownloadMessageDRMVideo( - policy, - signature, - kvp, - mpdURL, - decryptionKey, - path, - lastModified, - messageKVP.Key, - "Messages", - progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? - "", - messageInfo, - mediaInfo, - messageInfo?.FromUser, - users); - } - else + string[] parsed = messageKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], + parsed[2], parsed[3], + parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { continue; } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, messageKVP.Key, "Messages", + progressReporter, messagePath + "/Videos", filenameFormat, + messageInfo, mediaInfo, messageInfo?.FromUser, users); } else { - MessageEntities.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.Id == messageKVP.Key); - MessageEntities.ListItem? messageInfo = messages.MessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == messageKVP.Key) == true); - - isNew = await DownloadMessageMedia( - messageKVP.Value, - path, - messageKVP.Key, - "Messages", - progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? - "", - messageInfo, - mediaInfo, - messageInfo?.FromUser, - users); + isNew = await DownloadMedia(messageKVP.Value, path, messageKVP.Key, "Messages", progressReporter, + messagePath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); } if (isNew) @@ -2742,53 +1365,40 @@ public class DownloadService( int oldCount = 0; int newCount = 0; - foreach (KeyValuePair kvp in paidMessageCollection.PaidMessages) + foreach (KeyValuePair kvpEntry in paidMessageCollection.PaidMessages) { bool isNew; - if (kvp.Value.Contains("cdn3.onlyfans.com/dash/files")) + MessageEntities.Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == kvpEntry.Key); + PurchasedEntities.ListItem? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == kvpEntry.Key) == true); + string filenameFormat = + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? ""; + string paidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && + messageInfo?.Id is not null && messageInfo?.CreatedAt is not null + ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Messages/Paid"; + + if (kvpEntry.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = kvp.Value.Split(','); - string? pssh = await apiService.GetDRMMPDPSSH(parsed[0], parsed[1], parsed[2], parsed[3]); - if (pssh != null) - { - DateTime lastModified = - await apiService.GetDRMMPDLastModified(parsed[0], parsed[1], parsed[2], parsed[3]); - Dictionary drmHeaders = - apiService.GetDynamicHeaders($"/api2/v2/users/media/{parsed[4]}/drm/message/{parsed[5]}", - "?type=widevine"); - string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing - ? await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/message/{parsed[5]}?type=widevine", - pssh) - : await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/message/{parsed[5]}?type=widevine", - pssh); - - MessageEntities.Medium? mediaInfo = - paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == kvp.Key); - PurchasedEntities.ListItem? messageInfo = - paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == kvp.Key) == true); - - isNew = await DownloadPurchasedMessageDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - decryptionKey, path, lastModified, kvp.Key, "Messages", progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? - "", messageInfo, mediaInfo, messageInfo?.FromUser, users); - } - else + string[] parsed = kvpEntry.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], + parsed[2], parsed[3], + parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { continue; } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, kvpEntry.Key, "Messages", + progressReporter, paidMsgPath + "/Videos", filenameFormat, + messageInfo, mediaInfo, messageInfo?.FromUser, users); } else { - MessageEntities.Medium? mediaInfo = - paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == kvp.Key); - PurchasedEntities.ListItem? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == kvp.Key) == true); - isNew = await DownloadPurchasedMedia(kvp.Value, path, kvp.Key, "Messages", progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? - "", messageInfo, mediaInfo, messageInfo?.FromUser, users); + isNew = await DownloadMedia(kvpEntry.Value, path, kvpEntry.Key, "Messages", progressReporter, + paidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); } if (isNew) @@ -2836,50 +1446,39 @@ public class DownloadService( int oldCount = 0; int newCount = 0; - foreach (KeyValuePair kvp in streams.Streams) + foreach (KeyValuePair kvpEntry in streams.Streams) { bool isNew; - if (kvp.Value.Contains("cdn3.onlyfans.com/dash/files")) + StreamEntities.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.Id == kvpEntry.Key); + StreamEntities.ListItem? streamInfo = + streams.StreamObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); + string filenameFormat = + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; + string streamPath = configService.CurrentConfig.FolderPerPost && streamInfo != null && + streamInfo?.Id is not null && streamInfo?.PostedAt is not null + ? $"/Posts/Free/{streamInfo.Id} {streamInfo.PostedAt:yyyy-MM-dd HH-mm-ss}" + : "/Posts/Free"; + + if (kvpEntry.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = kvp.Value.Split(','); - string? pssh = await apiService.GetDRMMPDPSSH(parsed[0], parsed[1], parsed[2], parsed[3]); - if (pssh != null) - { - DateTime lastModified = - await apiService.GetDRMMPDLastModified(parsed[0], parsed[1], parsed[2], parsed[3]); - Dictionary drmHeaders = - apiService.GetDynamicHeaders($"/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}", - "?type=widevine"); - string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing - ? await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", - pssh) - : await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", - pssh); - - StreamEntities.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.Id == kvp.Key); - StreamEntities.ListItem? streamInfo = - streams.StreamObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); - - isNew = await DownloadStreamsDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, - path, lastModified, kvp.Key, "Streams", progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - "", streamInfo, mediaInfo, streamInfo?.Author, users); - } - else + string[] parsed = kvpEntry.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], + parsed[2], parsed[3], + parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { continue; } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, kvpEntry.Key, "Streams", + progressReporter, streamPath + "/Videos", filenameFormat, + streamInfo, mediaInfo, streamInfo?.Author, users); } else { - StreamEntities.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.Id == kvp.Key); - StreamEntities.ListItem? streamInfo = - streams.StreamObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); - isNew = await DownloadStreamMedia(kvp.Value, path, kvp.Key, "Streams", progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - "", streamInfo, mediaInfo, streamInfo?.Author, users); + isNew = await DownloadMedia(kvpEntry.Value, path, kvpEntry.Key, "Streams", progressReporter, + streamPath, filenameFormat, streamInfo, mediaInfo, streamInfo?.Author, users); } if (isNew) @@ -2903,8 +1502,6 @@ public class DownloadService( }; } -// Add these methods to DownloadService.cs before the #endregion line - public async Task DownloadFreePosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostEntities.PostCollection posts, @@ -2932,47 +1529,36 @@ public class DownloadService( foreach (KeyValuePair postKVP in posts.Posts) { bool isNew; + PostEntities.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m?.Id == postKVP.Key); + PostEntities.ListItem? postInfo = + posts.PostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); + string filenameFormat = + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; + string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && + postInfo?.Id is not null && postInfo?.PostedAt is not null + ? $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}" + : "/Posts/Free"; + if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) { string[] parsed = postKVP.Value.Split(','); - string? pssh = await apiService.GetDRMMPDPSSH(parsed[0], parsed[1], parsed[2], parsed[3]); - if (pssh != null) - { - DateTime lastModified = - await apiService.GetDRMMPDLastModified(parsed[0], parsed[1], parsed[2], parsed[3]); - Dictionary drmHeaders = - apiService.GetDynamicHeaders($"/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}", - "?type=widevine"); - string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing - ? await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", - pssh) - : await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", - pssh); - - PostEntities.Medium mediaInfo = posts.PostMedia.FirstOrDefault(m => m.Id == postKVP.Key); - PostEntities.ListItem postInfo = - posts.PostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); - - isNew = await DownloadPostDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], decryptionKey, path, - lastModified, postKVP.Key, "Posts", progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - "", postInfo, mediaInfo, postInfo?.Author, users); - } - else + (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], + parsed[2], parsed[3], + parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { continue; } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKVP.Key, "Posts", + progressReporter, postPath + "/Videos", filenameFormat, + postInfo, mediaInfo, postInfo?.Author, users); } else { - PostEntities.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m?.Id == postKVP.Key); - PostEntities.ListItem? postInfo = - posts.PostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); - isNew = await DownloadPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - "", postInfo, mediaInfo, postInfo?.Author, users); + isNew = await DownloadMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, + postPath, filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); } if (isNew) @@ -3023,50 +1609,37 @@ public class DownloadService( foreach (KeyValuePair postKVP in purchasedPosts.PaidPosts) { bool isNew; + MessageEntities.Medium? mediaInfo = + purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKVP.Key); + PurchasedEntities.ListItem? postInfo = + purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.Media?.Any(m => m.Id == postKVP.Key) == true); + string filenameFormat = + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; + string paidPostPath = configService.CurrentConfig.FolderPerPaidPost && postInfo != null && + postInfo?.Id is not null && postInfo?.PostedAt is not null + ? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Posts/Paid"; + if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) { string[] parsed = postKVP.Value.Split(','); - string? pssh = await apiService.GetDRMMPDPSSH(parsed[0], parsed[1], parsed[2], parsed[3]); - if (pssh != null) - { - DateTime lastModified = - await apiService.GetDRMMPDLastModified(parsed[0], parsed[1], parsed[2], parsed[3]); - Dictionary drmHeaders = - apiService.GetDynamicHeaders($"/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}", - "?type=widevine"); - string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing - ? await apiService.GetDecryptionKeyOFDL(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", - pssh) - : await apiService.GetDecryptionKeyCDM(drmHeaders, - $"https://onlyfans.com/api2/v2/users/media/{parsed[4]}/drm/post/{parsed[5]}?type=widevine", - pssh); - - MessageEntities.Medium? mediaInfo = - purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKVP.Key); - PurchasedEntities.ListItem? postInfo = - purchasedPosts.PaidPostObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == postKVP.Key) == true); - - isNew = await DownloadPurchasedPostDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - decryptionKey, path, lastModified, postKVP.Key, "Posts", progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - "", postInfo, mediaInfo, postInfo?.FromUser, users); - } - else + (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], + parsed[2], parsed[3], + parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) { continue; } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKVP.Key, "Posts", + progressReporter, paidPostPath + "/Videos", filenameFormat, + postInfo, mediaInfo, postInfo?.FromUser, users); } else { - MessageEntities.Medium? mediaInfo = - purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKVP.Key); - PurchasedEntities.ListItem? postInfo = - purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.Media?.Any(m => m.Id == postKVP.Key) == true); - isNew = await DownloadPurchasedPostMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, - configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? - "", postInfo, mediaInfo, postInfo?.FromUser, users); + isNew = await DownloadMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, + paidPostPath, filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users); } if (isNew) @@ -3089,6 +1662,4 @@ public class DownloadService( Success = true }; } - - #endregion } diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index 14f5f58..c46996a 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -1,6 +1,5 @@ using OF_DL.Models; using ArchivedEntities = OF_DL.Models.Entities.Archived; -using CommonEntities = OF_DL.Models.Entities.Common; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; @@ -15,105 +14,24 @@ public interface IDownloadService Task ProcessMediaDownload(string folder, long media_id, string api_type, string url, string path, string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); - Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? messageInfo, - ArchivedEntities.Medium? messageMedia, CommonEntities.Author? author, - Dictionary users); + Task DownloadMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string path, + string? filenameFormat, object? postInfo, object? postMedia, + object? author, Dictionary users); - Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, + Task DownloadDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, ArchivedEntities.ListItem? postInfo, - ArchivedEntities.Medium? postMedia, - CommonEntities.Author? author, Dictionary users); + IProgressReporter progressReporter, string path, + string? filenameFormat, object? postInfo, object? postMedia, + object? author, Dictionary users); - Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, - string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, - string filenameFormat, PostEntities.SinglePost postInfo, PostEntities.Medium postMedia, - CommonEntities.Author author, - Dictionary users); - - Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, - string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, - string? filenameFormat, PostEntities.ListItem? postInfo, PostEntities.Medium? postMedia, - CommonEntities.Author? author, - Dictionary users); + Task<(string decryptionKey, DateTime lastModified)?> GetDecryptionInfo( + string mpdURL, string policy, string signature, string kvp, + string mediaId, string contentId, string drmType, + bool clientIdBlobMissing, bool devicePrivateKeyMissing); Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username); - Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, - string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, - string? filenameFormat, MessageEntities.ListItem? messageInfo, MessageEntities.Medium? messageMedia, - CommonEntities.FromUser? fromUser, - Dictionary users); - - Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users); - - Task DownloadPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PostEntities.ListItem? postInfo, - PostEntities.Medium? postMedia, - CommonEntities.Author? author, Dictionary users); - - Task DownloadPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PostEntities.SinglePost? postInfo, - PostEntities.Medium? postMedia, - CommonEntities.Author? author, Dictionary users); - - Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, - CommonEntities.FromUser? fromUser, Dictionary users); - - Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users); - - Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, - CommonEntities.FromUser? fromUser, Dictionary users); - - Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users); - - Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? postInfo, - MessageEntities.Medium? postMedia, - CommonEntities.FromUser? fromUser, Dictionary users); - - Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, PurchasedEntities.ListItem? messageInfo, - MessageEntities.Medium? messageMedia, - CommonEntities.FromUser? fromUser, Dictionary users); - - Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter); - - Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, StreamEntities.ListItem? streamInfo, - StreamEntities.Medium? streamMedia, CommonEntities.Author? author, - Dictionary users); - - Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, - string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, - string? filenameFormat, StreamEntities.ListItem? streamInfo, StreamEntities.Medium? streamMedia, - CommonEntities.Author? author, - Dictionary users); - - Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users); - - Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, MessageEntities.SingleMessage? messageInfo, - MessageEntities.Medium? messageMedia, CommonEntities.FromUser? fromUser, Dictionary users); - Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); From 17af1e8dfe32be38abb8c5787479c8d5e11b4843 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 04:48:21 -0600 Subject: [PATCH 28/55] Address compiler warnings --- OF DL/Crypto/Padding.cs | 2 +- OF DL/Helpers/IFileNameHelper.cs | 2 +- OF DL/Services/APIService.cs | 8 ++++---- OF DL/Services/DownloadService.cs | 5 ++++- OF DL/Services/IAPIService.cs | 14 +++++++------- OF DL/Utils/HttpUtil.cs | 10 +++++----- OF DL/Widevine/CDM.cs | 6 +++--- OF DL/Widevine/CDMDevice.cs | 4 ++-- OF DL/Widevine/ContentKey.cs | 8 ++++---- OF DL/Widevine/DerivedKeys.cs | 6 +++--- OF DL/Widevine/PSSHBox.cs | 2 +- OF DL/Widevine/Session.cs | 30 +++++++++++------------------- 12 files changed, 46 insertions(+), 51 deletions(-) diff --git a/OF DL/Crypto/Padding.cs b/OF DL/Crypto/Padding.cs index 1225e1e..5f4641b 100644 --- a/OF DL/Crypto/Padding.cs +++ b/OF DL/Crypto/Padding.cs @@ -37,7 +37,7 @@ public class Padding return result; } - public static byte[] AddPSSPadding(byte[] hash) + public static byte[]? AddPSSPadding(byte[] hash) { int modBits = 2048; int hLen = 20; diff --git a/OF DL/Helpers/IFileNameHelper.cs b/OF DL/Helpers/IFileNameHelper.cs index 6454742..0ea39f1 100644 --- a/OF DL/Helpers/IFileNameHelper.cs +++ b/OF DL/Helpers/IFileNameHelper.cs @@ -5,5 +5,5 @@ public interface IFileNameHelper Task BuildFilename(string fileFormat, Dictionary values); Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, - string username, Dictionary users = null); + string username, Dictionary? users); } diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 80edeb4..9c64c1d 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -125,7 +125,7 @@ public class APIService(IAuthService authService, IConfigService configService, try { - UserEntities.User? user = new(); + UserEntities.User user = new(); int post_limit = 50; Dictionary getParams = new() { @@ -166,7 +166,7 @@ public class APIService(IAuthService authService, IConfigService configService, return null; } - public async Task GetUserInfoById(string endpoint) + public async Task GetUserInfoById(string endpoint) { try { @@ -231,7 +231,7 @@ public class APIService(IAuthService authService, IConfigService configService, } - public async Task> GetLists(string endpoint) + public async Task?> GetLists(string endpoint) { Log.Debug("Calling GetLists"); @@ -358,7 +358,7 @@ public class APIService(IAuthService authService, IConfigService configService, } - public async Task> GetMedia(MediaType mediatype, + public async Task?> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index da1b941..eb2c698 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -576,7 +576,10 @@ public class DownloadService( using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); if (response.IsSuccessStatusCode) { - return response.Content.Headers.LastModified.Value.DateTime; + if (response.Content.Headers.LastModified != null) + { + return response.Content.Headers.LastModified.Value.DateTime; + } } return DateTime.Now; diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index a14c024..fe55d1f 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -16,10 +16,10 @@ public interface IAPIService Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, string pssh); Task GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp); Task GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp); - Task> GetLists(string endpoint); - Task> GetListUsers(string endpoint); + Task?> GetLists(string endpoint); + Task?> GetListUsers(string endpoint); - Task> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, + Task?> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, List paid_post_ids); Task GetPaidPosts(string endpoint, string folder, string username, @@ -46,10 +46,10 @@ public interface IAPIService Task> GetPurchasedTab(string endpoint, string folder, Dictionary users); - Task GetUserInfo(string endpoint); - Task GetUserInfoById(string endpoint); + Task GetUserInfo(string endpoint); + Task GetUserInfoById(string endpoint); Dictionary GetDynamicHeaders(string path, string queryParam); - Task> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions); - Task> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions); + Task?> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions); + Task?> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions); Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, string pssh); } diff --git a/OF DL/Utils/HttpUtil.cs b/OF DL/Utils/HttpUtil.cs index eaa7ebb..bd0203a 100644 --- a/OF DL/Utils/HttpUtil.cs +++ b/OF DL/Utils/HttpUtil.cs @@ -54,7 +54,7 @@ internal class HttpUtil return bytes; } - public static async Task GetWebSource(string URL, Dictionary headers = null) + public static async Task GetWebSource(string URL, Dictionary? headers = null) { HttpResponseMessage response = await PerformOperation(async () => { return await Get(URL, headers); }); @@ -62,7 +62,7 @@ internal class HttpUtil return Encoding.UTF8.GetString(bytes); } - public static async Task GetBinary(string URL, Dictionary headers = null) + public static async Task GetBinary(string URL, Dictionary? headers = null) { HttpResponseMessage response = await PerformOperation(async () => { return await Get(URL, headers); }); @@ -72,7 +72,7 @@ internal class HttpUtil public static string GetString(byte[] bytes) => Encoding.UTF8.GetString(bytes); - private static async Task Get(string URL, Dictionary headers = null) + private static async Task Get(string URL, Dictionary? headers = null) { HttpRequestMessage request = new() { RequestUri = new Uri(URL), Method = HttpMethod.Get }; @@ -87,8 +87,8 @@ internal class HttpUtil return await Send(request); } - private static async Task Post(string URL, Dictionary headers, - HttpContent content) + private static async Task Post(string URL, Dictionary? headers = null, + HttpContent? content = null) { HttpRequestMessage request = new() { RequestUri = new Uri(URL), Method = HttpMethod.Post, Content = content }; diff --git a/OF DL/Widevine/CDM.cs b/OF DL/Widevine/CDM.cs index d005cd5..64e352c 100644 --- a/OF DL/Widevine/CDM.cs +++ b/OF DL/Widevine/CDM.cs @@ -48,7 +48,7 @@ public class CDM return pssh; } - public static string OpenSession(string initDataB64, string deviceName, bool offline = false, bool raw = false) + public static string? OpenSession(string initDataB64, string deviceName, bool offline = false, bool raw = false) { byte[] initData = CheckPSSH(initDataB64); @@ -98,7 +98,7 @@ public class CDM return BytesToHex(sessionId); } - private static WidevineCencHeader ParseInitData(byte[] initData) + private static WidevineCencHeader? ParseInitData(byte[] initData) { WidevineCencHeader cencHeader; @@ -190,7 +190,7 @@ public class CDM return true; } - public static byte[] GetLicenseRequest(string sessionId) + public static byte[]? GetLicenseRequest(string sessionId) { //Logger.Debug($"GetLicenseRequest(sessionId={BytesToHex(sessionId)})"); //Logger.Verbose($"Getting license request"); diff --git a/OF DL/Widevine/CDMDevice.cs b/OF DL/Widevine/CDMDevice.cs index 887a91f..61ba112 100644 --- a/OF DL/Widevine/CDMDevice.cs +++ b/OF DL/Widevine/CDMDevice.cs @@ -11,8 +11,8 @@ namespace OF_DL.Widevine; public class CDMDevice { - public CDMDevice(string deviceName, byte[] clientIdBlobBytes = null, byte[] privateKeyBytes = null, - byte[] vmpBytes = null) + public CDMDevice(string deviceName, byte[]? clientIdBlobBytes = null, byte[]? privateKeyBytes = null, + byte[]? vmpBytes = null) { DeviceName = deviceName; diff --git a/OF DL/Widevine/ContentKey.cs b/OF DL/Widevine/ContentKey.cs index 3a25f9d..2487f56 100644 --- a/OF DL/Widevine/ContentKey.cs +++ b/OF DL/Widevine/ContentKey.cs @@ -6,11 +6,11 @@ namespace OF_DL.Widevine; [Serializable] public class ContentKey { - [JsonPropertyName("key_id")] public byte[] KeyID { get; set; } + [JsonPropertyName("key_id")] public byte[] KeyID { get; set; } = []; - [JsonPropertyName("type")] public string Type { get; set; } + [JsonPropertyName("type")] public string Type { get; set; } = ""; - [JsonPropertyName("bytes")] public byte[] Bytes { get; set; } + [JsonPropertyName("bytes")] public byte[] Bytes { get; set; } = []; [NotMapped] [JsonPropertyName("permissions")] @@ -20,7 +20,7 @@ public class ContentKey set => PermissionsString = string.Join(",", value); } - [JsonIgnore] public string PermissionsString { get; set; } + [JsonIgnore] public string PermissionsString { get; set; } = ""; public override string ToString() => $"{BitConverter.ToString(KeyID).Replace("-", "").ToLower()}:{BitConverter.ToString(Bytes).Replace("-", "").ToLower()}"; diff --git a/OF DL/Widevine/DerivedKeys.cs b/OF DL/Widevine/DerivedKeys.cs index 9aca670..2ca9d1d 100644 --- a/OF DL/Widevine/DerivedKeys.cs +++ b/OF DL/Widevine/DerivedKeys.cs @@ -2,7 +2,7 @@ namespace OF_DL.Widevine; public class DerivedKeys { - public byte[] Auth1 { get; set; } - public byte[] Auth2 { get; set; } - public byte[] Enc { get; set; } + public byte[] Auth1 { get; set; } = []; + public byte[] Auth2 { get; set; } = []; + public byte[] Enc { get; set; } = []; } diff --git a/OF DL/Widevine/PSSHBox.cs b/OF DL/Widevine/PSSHBox.cs index 64363d6..0e59099 100644 --- a/OF DL/Widevine/PSSHBox.cs +++ b/OF DL/Widevine/PSSHBox.cs @@ -57,7 +57,7 @@ internal class PSSHBox if (dataLength == 0) { - return new PSSHBox(kids, null); + return new PSSHBox(kids, []); } byte[] data = new byte[dataLength]; diff --git a/OF DL/Widevine/Session.cs b/OF DL/Widevine/Session.cs index e9177ef..d879aa5 100644 --- a/OF DL/Widevine/Session.cs +++ b/OF DL/Widevine/Session.cs @@ -1,24 +1,16 @@ namespace OF_DL.Widevine; -internal class Session +internal class Session(byte[] sessionId, dynamic initData, CDMDevice device, bool offline) { - public Session(byte[] sessionId, dynamic initData, CDMDevice device, bool offline) - { - SessionId = sessionId; - InitData = initData; - Offline = offline; - Device = device; - } - - public byte[] SessionId { get; set; } - public dynamic InitData { get; set; } - public bool Offline { get; set; } - public CDMDevice Device { get; set; } - public byte[] SessionKey { get; set; } - public DerivedKeys DerivedKeys { get; set; } - public byte[] LicenseRequest { get; set; } - public SignedLicense License { get; set; } - public SignedDeviceCertificate ServiceCertificate { get; set; } + public byte[] SessionId { get; set; } = sessionId; + public dynamic InitData { get; set; } = initData; + public bool Offline { get; set; } = offline; + public CDMDevice Device { get; set; } = device; + public byte[] SessionKey { get; set; } = []; + public DerivedKeys DerivedKeys { get; set; } = new(); + public byte[] LicenseRequest { get; set; } = []; + public SignedLicense License { get; set; } = new(); + public SignedDeviceCertificate ServiceCertificate { get; set; } = new(); public bool PrivacyMode { get; set; } - public List ContentKeys { get; set; } = new(); + public List ContentKeys { get; set; } = []; } From 4c680a40b553393df48ae29a666fb894f96c8b00 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 13:59:41 -0600 Subject: [PATCH 29/55] Remove application logic from Program and continue to fix compiler warnings --- OF DL/CLI/SpectreDownloadEventHandler.cs | 127 + OF DL/CLI/SpectreProgressReporter.cs | 6 +- OF DL/CLI/SpectreStatusReporter.cs | 17 + OF DL/Models/CreatorDownloadResult.cs | 29 + OF DL/Models/StartupResult.cs | 39 + OF DL/Program.cs | 2243 +++-------------- .../Services/{APIService.cs => ApiService.cs} | 479 ++-- OF DL/Services/AuthService.cs | 53 +- OF DL/Services/ConfigService.cs | 66 +- .../Services/DownloadOrchestrationService.cs | 550 ++++ OF DL/Services/DownloadService.cs | 408 ++- OF DL/Services/FileNameService.cs | 8 +- OF DL/Services/IAPIService.cs | 16 +- OF DL/Services/IAuthService.cs | 19 + OF DL/Services/IConfigService.cs | 14 + OF DL/Services/IDownloadEventHandler.cs | 63 + .../Services/IDownloadOrchestrationService.cs | 73 + OF DL/Services/IDownloadService.cs | 17 + OF DL/Services/IStartupService.cs | 10 + OF DL/Services/IStatusReporter.cs | 14 + OF DL/Services/StartupService.cs | 254 ++ OF DL/Widevine/CDM.cs | 12 +- OF DL/Widevine/CDMApi.cs | 20 +- 23 files changed, 2346 insertions(+), 2191 deletions(-) create mode 100644 OF DL/CLI/SpectreDownloadEventHandler.cs create mode 100644 OF DL/CLI/SpectreStatusReporter.cs create mode 100644 OF DL/Models/CreatorDownloadResult.cs create mode 100644 OF DL/Models/StartupResult.cs rename OF DL/Services/{APIService.cs => ApiService.cs} (92%) create mode 100644 OF DL/Services/DownloadOrchestrationService.cs create mode 100644 OF DL/Services/IDownloadEventHandler.cs create mode 100644 OF DL/Services/IDownloadOrchestrationService.cs create mode 100644 OF DL/Services/IStartupService.cs create mode 100644 OF DL/Services/IStatusReporter.cs create mode 100644 OF DL/Services/StartupService.cs diff --git a/OF DL/CLI/SpectreDownloadEventHandler.cs b/OF DL/CLI/SpectreDownloadEventHandler.cs new file mode 100644 index 0000000..a34cd8a --- /dev/null +++ b/OF DL/CLI/SpectreDownloadEventHandler.cs @@ -0,0 +1,127 @@ +using OF_DL.Models; +using OF_DL.Services; +using Spectre.Console; + +namespace OF_DL.CLI; + +/// +/// Spectre.Console implementation of IDownloadEventHandler. +/// Handles all CLI-specific display logic for downloads. +/// +public class SpectreDownloadEventHandler : IDownloadEventHandler +{ + public async Task WithStatusAsync(string statusMessage, Func> work) + { + T result = default!; + await AnsiConsole.Status() + .StartAsync($"[red]{Markup.Escape(statusMessage)}[/]", + async ctx => + { + SpectreStatusReporter reporter = new(ctx); + result = await work(reporter); + }); + return result; + } + + public async Task WithProgressAsync(string description, long maxValue, bool showSize, + Func> work) + { + T result = default!; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(showSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]{Markup.Escape(description)}[/]", false); + task.MaxValue = maxValue; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await work(progressReporter); + + task.StopTask(); + }); + return result; + } + + public void OnContentFound(string contentType, int mediaCount, int objectCount) + { + AnsiConsole.Markup($"[red]Found {mediaCount} Media from {objectCount} {Markup.Escape(contentType)}\n[/]"); + } + + public void OnNoContentFound(string contentType) + { + AnsiConsole.Markup($"[red]Found 0 {Markup.Escape(contentType)}\n[/]"); + } + + public void OnDownloadComplete(string contentType, DownloadResult result) + { + AnsiConsole.Markup( + $"[red]{Markup.Escape(contentType)} Already Downloaded: {result.ExistingDownloads} New {Markup.Escape(contentType)} Downloaded: {result.NewDownloads}[/]\n"); + } + + public void OnUserStarting(string username) + { + AnsiConsole.Markup($"[red]\nScraping Data for {Markup.Escape(username)}\n[/]"); + } + + public void OnUserComplete(string username, CreatorDownloadResult result) + { + AnsiConsole.Markup("\n"); + AnsiConsole.Write(new BreakdownChart() + .FullSize() + .AddItem("Paid Posts", result.PaidPostCount, Color.Red) + .AddItem("Posts", result.PostCount, Color.Blue) + .AddItem("Archived", result.ArchivedCount, Color.Green) + .AddItem("Streams", result.StreamsCount, Color.Purple) + .AddItem("Stories", result.StoriesCount, Color.Yellow) + .AddItem("Highlights", result.HighlightsCount, Color.Orange1) + .AddItem("Messages", result.MessagesCount, Color.LightGreen) + .AddItem("Paid Messages", result.PaidMessagesCount, Color.Aqua)); + AnsiConsole.Markup("\n"); + } + + public void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount) + { + AnsiConsole.Markup("\n"); + AnsiConsole.Write(new BreakdownChart() + .FullSize() + .AddItem("Paid Posts", paidPostCount, Color.Red) + .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); + AnsiConsole.Markup("\n"); + } + + public void OnScrapeComplete(TimeSpan elapsed) + { + AnsiConsole.Markup($"[green]Scrape Completed in {elapsed.TotalMinutes:0.00} minutes\n[/]"); + } + + public void OnMessage(string message) + { + AnsiConsole.Markup($"[red]{Markup.Escape(message)}\n[/]"); + } + + private static ProgressColumn[] GetProgressColumns(bool showScrapeSize) + { + List progressColumns; + if (showScrapeSize) + { + progressColumns = + [ + new TaskDescriptionColumn(), + new ProgressBarColumn(), + new PercentageColumn(), + new DownloadedColumn(), + new RemainingTimeColumn() + ]; + } + else + { + progressColumns = + [ + new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn() + ]; + } + + return progressColumns.ToArray(); + } +} diff --git a/OF DL/CLI/SpectreProgressReporter.cs b/OF DL/CLI/SpectreProgressReporter.cs index 49cf12b..a9fbd4a 100644 --- a/OF DL/CLI/SpectreProgressReporter.cs +++ b/OF DL/CLI/SpectreProgressReporter.cs @@ -6,11 +6,9 @@ namespace OF_DL.CLI; /// /// Implementation of IProgressReporter that uses Spectre.Console's ProgressTask for CLI output. /// -public class SpectreProgressReporter : IProgressReporter +public class SpectreProgressReporter(ProgressTask task) : IProgressReporter { - private readonly ProgressTask _task; - - public SpectreProgressReporter(ProgressTask task) => _task = task ?? throw new ArgumentNullException(nameof(task)); + private readonly ProgressTask _task = task ?? throw new ArgumentNullException(nameof(task)); public void ReportProgress(long increment) => _task.Increment(increment); diff --git a/OF DL/CLI/SpectreStatusReporter.cs b/OF DL/CLI/SpectreStatusReporter.cs new file mode 100644 index 0000000..7c965fd --- /dev/null +++ b/OF DL/CLI/SpectreStatusReporter.cs @@ -0,0 +1,17 @@ +using OF_DL.Services; +using Spectre.Console; + +namespace OF_DL.CLI; + +/// +/// Implementation of IStatusReporter that uses Spectre.Console's StatusContext for CLI output. +/// +public class SpectreStatusReporter(StatusContext ctx) : IStatusReporter +{ + public void ReportStatus(string message) + { + ctx.Status($"[red]{message}[/]"); + ctx.Spinner(Spinner.Known.Dots); + ctx.SpinnerStyle(Style.Parse("blue")); + } +} diff --git a/OF DL/Models/CreatorDownloadResult.cs b/OF DL/Models/CreatorDownloadResult.cs new file mode 100644 index 0000000..16de4cb --- /dev/null +++ b/OF DL/Models/CreatorDownloadResult.cs @@ -0,0 +1,29 @@ +namespace OF_DL.Models; + +public class CreatorDownloadResult +{ + public int PaidPostCount { get; set; } + + public int PostCount { get; set; } + + public int ArchivedCount { get; set; } + + public int StreamsCount { get; set; } + + public int StoriesCount { get; set; } + + public int HighlightsCount { get; set; } + + public int MessagesCount { get; set; } + + public int PaidMessagesCount { get; set; } +} + +public class UserListResult +{ + public Dictionary Users { get; set; } = new(); + + public Dictionary Lists { get; set; } = new(); + + public string? IgnoredListError { get; set; } +} diff --git a/OF DL/Models/StartupResult.cs b/OF DL/Models/StartupResult.cs new file mode 100644 index 0000000..6b67cd0 --- /dev/null +++ b/OF DL/Models/StartupResult.cs @@ -0,0 +1,39 @@ +namespace OF_DL.Models; + +public class StartupResult +{ + public bool IsWindowsVersionValid { get; set; } = true; + + public string? OsVersionString { get; set; } + + public bool FfmpegFound { get; set; } + + public bool FfmpegPathAutoDetected { get; set; } + + public string? FfmpegPath { get; set; } + + public string? FfmpegVersion { get; set; } + + public bool ClientIdBlobMissing { get; set; } + + public bool DevicePrivateKeyMissing { get; set; } + + public bool RulesJsonValid { get; set; } + + public bool RulesJsonExists { get; set; } + + public string? RulesJsonError { get; set; } +} + +public class VersionCheckResult +{ + public Version? LocalVersion { get; set; } + + public Version? LatestVersion { get; set; } + + public bool IsUpToDate { get; set; } + + public bool CheckFailed { get; set; } + + public bool TimedOut { get; set; } +} diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 19dd492..550ab6f 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -1,34 +1,17 @@ -using System.Diagnostics; -using System.Reflection; -using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using OF_DL.CLI; using OF_DL.Models; using OF_DL.Enumerations; -using OF_DL.Helpers; -using ArchivedEntities = OF_DL.Models.Entities.Archived; -using MessageEntities = OF_DL.Models.Entities.Messages; -using PostEntities = OF_DL.Models.Entities.Posts; -using PurchasedEntities = OF_DL.Models.Entities.Purchased; -using StreamEntities = OF_DL.Models.Entities.Streams; -using UserEntities = OF_DL.Models.Entities.Users; +using OF_DL.Models.Entities.Users; using OF_DL.Services; using Serilog; using Spectre.Console; -using WidevineConstants = OF_DL.Widevine.Constants; namespace OF_DL; public class Program(IServiceProvider serviceProvider) { - public static List paid_post_ids = new(); - - private static bool clientIdBlobMissing; - private static bool devicePrivateKeyMissing; - private async Task LoadAuthFromBrowser() { IAuthService authService = serviceProvider.GetRequiredService(); @@ -86,7 +69,6 @@ public class Program(IServiceProvider serviceProvider) 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(); @@ -126,10 +108,12 @@ public class Program(IServiceProvider serviceProvider) services.AddSingleton(loggingService); services.AddSingleton(configService); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); return services; @@ -139,362 +123,41 @@ public class Program(IServiceProvider serviceProvider) { IConfigService configService = serviceProvider.GetRequiredService(); IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); + IStartupService startupService = serviceProvider.GetRequiredService(); + IDownloadOrchestrationService orchestrationService = + serviceProvider.GetRequiredService(); try { - OperatingSystem os = Environment.OSVersion; + // Version check + VersionCheckResult versionResult = await startupService.CheckVersionAsync(); + DisplayVersionResult(versionResult); - Log.Debug($"Operating system information: {os.VersionString}"); + // Environment validation + StartupResult startupResult = await startupService.ValidateEnvironmentAsync(); + DisplayStartupResult(startupResult); - if (os.Platform == PlatformID.Win32NT) + if (!startupResult.IsWindowsVersionValid) { - // check if this is windows 10+ - if (os.Version.Major < 10) + 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); + Console.Write("Press any key to continue.\n"); + + if (!configService.CurrentConfig.NonInteractiveMode) { - 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", - os.VersionString); - Console.Write("Press any key to continue.\n"); - Log.Error("Windows version prior to 10.x: {0}", os.VersionString); - - if (!configService.CurrentConfig.NonInteractiveMode) - { - Console.ReadKey(); - } - - Environment.Exit(1); - } - else - { - AnsiConsole.Markup("[green]Valid version of Windows found.\n[/]"); - } - } - - try - { - // Only run the version check if not in DEBUG mode -#if !DEBUG - Version localVersion = - Assembly.GetEntryAssembly()?.GetName().Version; //Only tested with numeric values. - - // Create a cancellation token with 30 second timeout - using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); - string? latestReleaseTag = null; - - try - { - latestReleaseTag = await VersionHelper.GetLatestReleaseTag(cts.Token); - } - catch (OperationCanceledException) - { - AnsiConsole.Markup("[yellow]Version check timed out after 30 seconds.\n[/]"); - Log.Warning("Version check timed out after 30 seconds"); - latestReleaseTag = null; - } - - if (latestReleaseTag == null) - { - AnsiConsole.Markup("[yellow]Failed to verify that OF-DL is up-to-date.\n[/]"); - Log.Error("Failed to get the latest release tag."); - } - else - { - Version latestGiteaRelease = new(latestReleaseTag.Replace("OFDLV", "")); - - // Compare the Versions - int versionComparison = localVersion.CompareTo(latestGiteaRelease); - if (versionComparison < 0) - { - // The version on GitHub is more up to date than this local release. - AnsiConsole.Markup("[red]You are running OF-DL version " + - $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]"); - AnsiConsole.Markup("[red]Please update to the current release, " + - $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}: [link=https://git.ofdl.tools/sim0n00ps/OF-DL/releases]https://git.ofdl.tools/sim0n00ps/OF-DL/releases[/]\n[/]"); - Log.Debug("Detected outdated client running version " + - $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}"); - Log.Debug("Latest release version " + - $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}"); - } - else - { - // This local version is greater than the release version on GitHub. - AnsiConsole.Markup("[green]You are running OF-DL version " + - $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]"); - AnsiConsole.Markup("[green]Latest Release version: " + - $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}\n[/]"); - Log.Debug("Detected client running version " + - $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}"); - Log.Debug("Latest release version " + - $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}"); - } - } - -#else - AnsiConsole.Markup("[yellow]Running in Debug/Local mode. Version check skipped.\n[/]"); - Log.Debug("Running in Debug/Local mode. Version check skipped."); -#endif - } - catch (Exception e) - { - AnsiConsole.Markup("[red]Error checking latest release on GitHub:\n[/]"); - Console.WriteLine(e); - Log.Error("Error checking latest release on GitHub.", e.Message); - } - - - if (await authService.LoadFromFileAsync()) - { - AnsiConsole.Markup("[green]auth.json located successfully!\n[/]"); - } - else if (File.Exists("auth.json")) - { - // File exists but failed to load - 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[/]"); - AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - Console.ReadKey(); - Environment.Exit(2); } - - if (!configService.CurrentConfig!.DisableBrowserAuth) - { - await LoadAuthFromBrowser(); - } - else - { - 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[/]"); - AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - - Console.ReadKey(); - Environment.Exit(2); - } - } - else - { - if (configService.CurrentConfig.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[/]"); - AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - - Console.ReadKey(); - Environment.Exit(2); - } - - if (!configService.CurrentConfig!.DisableBrowserAuth) - { - await LoadAuthFromBrowser(); - } - else - { - 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[/]"); - AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - - Console.ReadKey(); - Environment.Exit(2); - } + Environment.Exit(1); } - //Added to stop cookie being filled with un-needed headers - ValidateCookieString(authService.CurrentAuth!); - - if (File.Exists("rules.json")) - { - AnsiConsole.Markup("[green]rules.json located successfully!\n[/]"); - try - { - JsonConvert.DeserializeObject(File.ReadAllText("rules.json")); - Log.Debug("Rules.json: "); - Log.Debug(JsonConvert.SerializeObject(File.ReadAllText("rules.json"), Formatting.Indented)); - } - catch (Exception e) - { - Console.WriteLine(e); - 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"); - AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - Log.Error("rules.json processing failed.", e.Message); - - if (!configService.CurrentConfig.NonInteractiveMode) - { - Console.ReadKey(); - } - - Environment.Exit(2); - } - } - - if (configService.CurrentConfig.NonInteractiveMode) - { - // CLI argument overrides configuration - configService.CurrentConfig!.NonInteractiveMode = true; - Log.Debug("NonInteractiveMode = true"); - } - - if (configService.CurrentConfig!.NonInteractiveMode) - { - configService.CurrentConfig.NonInteractiveMode = - true; // If it was set in the config, reset the cli value so exception handling works - Log.Debug("NonInteractiveMode = true (set via config)"); - } - - bool ffmpegFound = false; - bool pathAutoDetected = false; - if (!string.IsNullOrEmpty(configService.CurrentConfig!.FFmpegPath) && - ValidateFilePath(configService.CurrentConfig.FFmpegPath)) - { - // FFmpeg path is set in config.json and is valid - ffmpegFound = true; - Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); - Log.Debug("FFMPEG path set in config.conf"); - } - else if (!string.IsNullOrEmpty(authService.CurrentAuth!.FfmpegPath) && - ValidateFilePath(authService.CurrentAuth.FfmpegPath)) - { - // FFmpeg path is set in auth.json and is valid (config.conf takes precedence and auth.json is only available for backward compatibility) - ffmpegFound = true; - configService.CurrentConfig.FFmpegPath = authService.CurrentAuth.FfmpegPath; - Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); - Log.Debug("FFMPEG path set in auth.json"); - } - else if (string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath)) - { - // FFmpeg path is not set in config.conf, so we will try to locate it in the PATH or current directory - string? ffmpegPath = GetFullPath("ffmpeg"); - if (ffmpegPath != null) - { - // FFmpeg is found in the PATH or current directory - ffmpegFound = true; - pathAutoDetected = true; - configService.CurrentConfig.FFmpegPath = ffmpegPath; - Log.Debug($"FFMPEG found: {ffmpegPath}"); - Log.Debug("FFMPEG path found via PATH or current directory"); - } - else - { - // FFmpeg is not found in the PATH or current directory, so we will try to locate the windows executable - ffmpegPath = GetFullPath("ffmpeg.exe"); - if (ffmpegPath != null) - { - // FFmpeg windows executable is found in the PATH or current directory - ffmpegFound = true; - pathAutoDetected = true; - configService.CurrentConfig.FFmpegPath = ffmpegPath; - Log.Debug($"FFMPEG found: {ffmpegPath}"); - Log.Debug("FFMPEG path found in windows excutable directory"); - } - } - } - - if (ffmpegFound) - { - if (pathAutoDetected) - { - AnsiConsole.Markup( - $"[green]FFmpeg located successfully. Path auto-detected: {configService.CurrentConfig.FFmpegPath}\n[/]"); - } - else - { - AnsiConsole.Markup("[green]FFmpeg located successfully\n[/]"); - } - - // Escape backslashes in the path for Windows - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && - configService.CurrentConfig.FFmpegPath!.Contains(@":\") && - !configService.CurrentConfig.FFmpegPath.Contains(@":\\")) - { - configService.CurrentConfig.FFmpegPath = - configService.CurrentConfig.FFmpegPath.Replace(@"\", @"\\"); - } - - // Get FFmpeg version - try - { - ProcessStartInfo processStartInfo = new() - { - FileName = configService.CurrentConfig.FFmpegPath, - 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 full output - Log.Information("FFmpeg version output:\n{Output}", output); - - // Parse first line for console output - string firstLine = output.Split('\n')[0].Trim(); - if (firstLine.StartsWith("ffmpeg version")) - { - // Extract version string (text between "ffmpeg version " and " Copyright") - int versionStart = "ffmpeg version ".Length; - int copyrightIndex = firstLine.IndexOf(" Copyright"); - if (copyrightIndex > versionStart) - { - string version = firstLine.Substring(versionStart, copyrightIndex - versionStart); - AnsiConsole.Markup($"[green]ffmpeg version detected as {version}[/]\n"); - } - else - { - // Fallback if Copyright not found - string version = firstLine.Substring(versionStart); - AnsiConsole.Markup($"[green]ffmpeg version detected as {version}[/]\n"); - } - } - else - { - AnsiConsole.Markup("[yellow]ffmpeg version could not be parsed[/]\n"); - } - } - } - } - catch (Exception ex) - { - Log.Warning(ex, "Failed to get FFmpeg version"); - AnsiConsole.Markup("[yellow]Could not retrieve ffmpeg version[/]\n"); - } - } - else + if (!startupResult.FfmpegFound) { AnsiConsole.Markup( "[red]Cannot locate FFmpeg; please modify config.conf with the correct path. Press any key to exit.[/]"); - Log.Error($"Cannot locate FFmpeg with path: {configService.CurrentConfig.FFmpegPath}"); if (!configService.CurrentConfig.NonInteractiveMode) { Console.ReadKey(); @@ -503,45 +166,30 @@ public class Program(IServiceProvider serviceProvider) Environment.Exit(4); } - if (!File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, WidevineConstants.DEVICE_NAME, - "device_client_id_blob"))) + // Auth flow + await HandleAuthFlow(authService, configService); + + // Validate cookie string + authService.ValidateCookieString(); + + // rules.json validation + DisplayRulesJsonResult(startupResult, configService); + + // NonInteractiveMode + if (configService.CurrentConfig.NonInteractiveMode) { - clientIdBlobMissing = true; - Log.Debug("clientIdBlobMissing missing"); - } - else - { - AnsiConsole.Markup("[green]device_client_id_blob located successfully![/]\n"); - Log.Debug("clientIdBlobMissing found: " + File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, - WidevineConstants.DEVICE_NAME, "device_client_id_blob"))); + configService.CurrentConfig.NonInteractiveMode = true; + Log.Debug("NonInteractiveMode = true"); } - if (!File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, WidevineConstants.DEVICE_NAME, - "device_private_key"))) - { - devicePrivateKeyMissing = true; - Log.Debug("devicePrivateKeyMissing missing"); - } - else - { - AnsiConsole.Markup("[green]device_private_key located successfully![/]\n"); - Log.Debug("devicePrivateKeyMissing found: " + File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, - WidevineConstants.DEVICE_NAME, "device_private_key"))); - } - - if (clientIdBlobMissing || 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[/]"); - } - - UserEntities.User? validate = await apiService.GetUserInfo("/users/me"); - if (validate == null || (validate?.Name == null && validate?.Username == null)) + // 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 (!configService.CurrentConfig.DisableBrowserAuth) { if (File.Exists("auth.json")) { @@ -549,7 +197,8 @@ public class Program(IServiceProvider serviceProvider) } } - if (!configService.CurrentConfig.NonInteractiveMode && !configService.CurrentConfig!.DisableBrowserAuth) + if (!configService.CurrentConfig.NonInteractiveMode && + !configService.CurrentConfig.DisableBrowserAuth) { await LoadAuthFromBrowser(); } @@ -564,8 +213,11 @@ public class Program(IServiceProvider serviceProvider) } } - AnsiConsole.Markup($"[green]Logged In successfully as {validate.Name} {validate.Username}\n[/]"); - await DownloadAllData(); + 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) { @@ -590,1426 +242,233 @@ public class Program(IServiceProvider serviceProvider) } } - private async Task DownloadAllData() + private async Task DownloadAllData( + IDownloadOrchestrationService orchestrationService, + IConfigService configService, + StartupResult startupResult) { - IDBService dbService = serviceProvider.GetRequiredService(); - IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - Config Config = configService.CurrentConfig!; + Config config = configService.CurrentConfig; + SpectreDownloadEventHandler eventHandler = new(); Log.Debug("Calling DownloadAllData"); do { DateTime startTime = DateTime.Now; - Dictionary users = new(); - Dictionary activeSubs = - await apiService.GetActiveSubscriptions("/subscriptions/subscribes", - Config.IncludeRestrictedSubscriptions); - Log.Debug("Subscriptions: "); + UserListResult userListResult = await orchestrationService.GetAvailableUsersAsync(); + Dictionary users = userListResult.Users; + Dictionary lists = userListResult.Lists; - foreach (KeyValuePair activeSub in activeSubs) + if (userListResult.IgnoredListError != null) { - if (!users.ContainsKey(activeSub.Key)) - { - users.Add(activeSub.Key, activeSub.Value); - Log.Debug($"Name: {activeSub.Key} ID: {activeSub.Value}"); - } + AnsiConsole.Markup($"[red]{Markup.Escape(userListResult.IgnoredListError)}\n[/]"); } - if (Config!.IncludeExpiredSubscriptions) - { - Log.Debug("Inactive Subscriptions: "); - - Dictionary expiredSubs = - await apiService.GetExpiredSubscriptions("/subscriptions/subscribes", - Config.IncludeRestrictedSubscriptions); - foreach (KeyValuePair expiredSub in expiredSubs) - { - if (!users.ContainsKey(expiredSub.Key)) - { - users.Add(expiredSub.Key, expiredSub.Value); - Log.Debug($"Name: {expiredSub.Key} ID: {expiredSub.Value}"); - } - } - } - - Dictionary lists = await apiService.GetLists("/lists"); - - // Remove users from the list if they are in the ignored list - if (!string.IsNullOrEmpty(Config.IgnoredUsersListName)) - { - if (!lists.TryGetValue(Config.IgnoredUsersListName, out long ignoredUsersListId)) - { - AnsiConsole.Markup($"[red]Ignored users list '{Config.IgnoredUsersListName}' not found\n[/]"); - Log.Error($"Ignored users list '{Config.IgnoredUsersListName}' not found"); - } - else - { - List ignoredUsernames = - await apiService.GetListUsers($"/lists/{ignoredUsersListId}/users") ?? []; - users = users.Where(x => !ignoredUsernames.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value); - } - } - - await dbService.CreateUsersDB(users); KeyValuePair> hasSelectedUsersKVP; - if (Config.NonInteractiveMode && Config.NonInteractiveModePurchasedTab) + if (config.NonInteractiveMode && config.NonInteractiveModePurchasedTab) { hasSelectedUsersKVP = new KeyValuePair>(true, new Dictionary { { "PurchasedTab", 0 } }); } - else if (Config.NonInteractiveMode && string.IsNullOrEmpty(Config.NonInteractiveModeListName)) + else if (config.NonInteractiveMode && string.IsNullOrEmpty(config.NonInteractiveModeListName)) { hasSelectedUsersKVP = new KeyValuePair>(true, users); } - else if (Config.NonInteractiveMode && !string.IsNullOrEmpty(Config.NonInteractiveModeListName)) + else if (config.NonInteractiveMode && !string.IsNullOrEmpty(config.NonInteractiveModeListName)) { - long listId = lists[Config.NonInteractiveModeListName]; - List listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; - Dictionary selectedUsers = users.Where(x => listUsernames.Contains(x.Key)).Distinct() - .ToDictionary(x => x.Key, x => x.Value); + Dictionary selectedUsers = + await orchestrationService.GetUsersForListAsync(config.NonInteractiveModeListName, users, lists); hasSelectedUsersKVP = new KeyValuePair>(true, selectedUsers); } else { - ILoggingService loggingService = serviceProvider.GetRequiredService(); (bool IsExit, Dictionary? selectedUsers) userSelectionResult = await HandleUserSelection(users, lists); - Config = configService.CurrentConfig!; + config = configService.CurrentConfig; hasSelectedUsersKVP = new KeyValuePair>(userSelectionResult.IsExit, - userSelectionResult.selectedUsers); + userSelectionResult.selectedUsers ?? []); } - if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value.ContainsKey("SinglePost")) { - 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 post_id = Convert.ToInt64(postUrl.Split("/")[3]); - string username = postUrl.Split("/")[4]; - - Log.Debug($"Single Post ID: {post_id.ToString()}"); - Log.Debug($"Single Post Creator: {username}"); - - if (users.ContainsKey(username)) - { - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = Path.Combine(Config.DownloadPath, username); - } - else - { - path = $"__user_data__/sites/OnlyFans/{username}"; - } - - Log.Debug($"Download path: {path}"); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {username}\n[/]"); - Log.Debug($"Created folder for {username}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); - } - - await dbService.CreateDB(path); - - await DownloadSinglePost(username, post_id, path, users); - } - } + await HandleSinglePostDownload(orchestrationService, users, startupResult, eventHandler); } - else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value.ContainsKey("PurchasedTab")) { - Dictionary purchasedTabUsers = - await apiService.GetPurchasedTabUsers("/posts/paid/all", users); - AnsiConsole.Markup("[red]Checking folders for Users in Purchased Tab\n[/]"); - foreach (KeyValuePair user in purchasedTabUsers) - { - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = Path.Combine(Config.DownloadPath, user.Key); - } - else - { - path = $"__user_data__/sites/OnlyFans/{user.Key}"; - } - - Log.Debug($"Download path: {path}"); - - await dbService.CheckUsername(user, path); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {user.Key}\n[/]"); - Log.Debug($"Created folder for {user.Key}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {user.Key} already created\n[/]"); - Log.Debug($"Folder for {user.Key} already created"); - } - - UserEntities.User user_info = await apiService.GetUserInfo($"/users/{user.Key}"); - - await dbService.CreateDB(path); - } - - string p = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - p = Config.DownloadPath; - } - else - { - p = "__user_data__/sites/OnlyFans/"; - } - - Log.Debug($"Download path: {p}"); - - List purchasedTabCollections = - await apiService.GetPurchasedTab("/posts/paid/all", p, users); - foreach (PurchasedEntities.PurchasedTabCollection purchasedTabCollection in purchasedTabCollections) - { - AnsiConsole.Markup($"[red]\nScraping Data for {purchasedTabCollection.Username}\n[/]"); - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = Path.Combine(Config.DownloadPath, purchasedTabCollection.Username); - } - else - { - path = $"__user_data__/sites/OnlyFans/{purchasedTabCollection.Username}"; - } - - - Log.Debug($"Download path: {path}"); - - int paidPostCount = 0; - int paidMessagesCount = 0; - paidPostCount = await DownloadPaidPostsPurchasedTab(purchasedTabCollection.Username, - purchasedTabCollection.PaidPosts, - users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidPostCount, path, - users); - paidMessagesCount = await DownloadPaidMessagesPurchasedTab(purchasedTabCollection.Username, - purchasedTabCollection.PaidMessages, - users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidMessagesCount, path, - users); - - AnsiConsole.Markup("\n"); - AnsiConsole.Write(new BreakdownChart() - .FullSize() - .AddItem("Paid Posts", paidPostCount, Color.Red) - .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); - AnsiConsole.Markup("\n"); - } + await orchestrationService.DownloadPurchasedTabAsync(users, + startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler); DateTime endTime = DateTime.Now; - TimeSpan totalTime = endTime - startTime; - AnsiConsole.Markup($"[green]Scrape Completed in {totalTime.TotalMinutes:0.00} minutes\n[/]"); - Log.Debug($"Scrape Completed in {totalTime.TotalMinutes:0.00} minutes"); + eventHandler.OnScrapeComplete(endTime - startTime); } - else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value.ContainsKey("SingleMessage")) { - 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 message_id = Convert.ToInt64(messageUrl.Split("?firstId=")[1]); - long user_id = Convert.ToInt64(messageUrl.Split("/")[6]); - JObject user = await apiService.GetUserInfoById($"/users/list?x[]={user_id.ToString()}"); - string username = ""; - - Log.Debug($"Message ID: {message_id}"); - Log.Debug($"User ID: {user_id}"); - - if (user is null) - { - username = $"Deleted User - {user_id.ToString()}"; - Log.Debug("Content creator not longer exists - ", user_id.ToString()); - } - else if (!string.IsNullOrEmpty(user[user_id.ToString()]["username"].ToString())) - { - username = user[user_id.ToString()]["username"].ToString(); - Log.Debug("Content creator: ", username); - } - - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = Path.Combine(Config.DownloadPath, username); - } - else - { - path = $"__user_data__/sites/OnlyFans/{username}"; - } - - Log.Debug("Download path: ", path); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {username}\n[/]"); - Log.Debug($"Created folder for {username}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); - Log.Debug($"Folder for {username} already created"); - } - - await dbService.CreateDB(path); - - await DownloadPaidMessage(username, hasSelectedUsersKVP, 1, path, message_id); - } + await HandleSingleMessageDownload(orchestrationService, users, startupResult, eventHandler); } - else if (hasSelectedUsersKVP.Key && !hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) + else if (hasSelectedUsersKVP.Key && + !hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) { - //Iterate over each user in the list of users foreach (KeyValuePair user in hasSelectedUsersKVP.Value) { - int paidPostCount = 0; - int postCount = 0; - int archivedCount = 0; - int streamsCount = 0; - int storiesCount = 0; - int highlightsCount = 0; - int messagesCount = 0; - int paidMessagesCount = 0; - AnsiConsole.Markup($"[red]\nScraping Data for {user.Key}\n[/]"); + string path = orchestrationService.ResolveDownloadPath(user.Key); + Log.Debug($"Download path: {path}"); - Log.Debug($"Scraping Data for {user.Key}"); - - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = Path.Combine(Config.DownloadPath, user.Key); - } - else - { - path = $"__user_data__/sites/OnlyFans/{user.Key}"; - } - - Log.Debug("Download path: ", path); - - await dbService.CheckUsername(user, path); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {user.Key}\n[/]"); - Log.Debug($"Created folder for {user.Key}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {user.Key} already created\n[/]"); - Log.Debug($"Folder for {user.Key} already created"); - } - - await dbService.CreateDB(path); - - if (Config.DownloadAvatarHeaderPhoto) - { - UserEntities.User? user_info = await apiService.GetUserInfo($"/users/{user.Key}"); - if (user_info != null) - { - await downloadService.DownloadAvatarHeader(user_info.Avatar, user_info.Header, path, - user.Key); - } - } - - if (Config.DownloadPaidPosts) - { - paidPostCount = - await DownloadPaidPosts(user.Key, hasSelectedUsersKVP, user, paidPostCount, path); - } - - if (Config.DownloadPosts) - { - postCount = await DownloadFreePosts(user.Key, hasSelectedUsersKVP, user, postCount, path); - } - - if (Config.DownloadArchived) - { - archivedCount = - await DownloadArchived(user.Key, hasSelectedUsersKVP, user, archivedCount, path); - } - - if (Config.DownloadStreams) - { - streamsCount = await DownloadStreams(user.Key, hasSelectedUsersKVP, user, streamsCount, path); - } - - if (Config.DownloadStories) - { - storiesCount = await DownloadStories(user.Key, user, storiesCount, path); - } - - if (Config.DownloadHighlights) - { - highlightsCount = await DownloadHighlights(user.Key, user, highlightsCount, path); - } - - if (Config.DownloadMessages) - { - messagesCount = - await DownloadMessages(user.Key, hasSelectedUsersKVP, user, messagesCount, path); - } - - if (Config.DownloadPaidMessages) - { - paidMessagesCount = await DownloadPaidMessages(user.Key, hasSelectedUsersKVP, user, - paidMessagesCount, path); - } - - AnsiConsole.Markup("\n"); - AnsiConsole.Write(new BreakdownChart() - .FullSize() - .AddItem("Paid Posts", paidPostCount, Color.Red) - .AddItem("Posts", postCount, Color.Blue) - .AddItem("Archived", archivedCount, Color.Green) - .AddItem("Streams", streamsCount, Color.Purple) - .AddItem("Stories", storiesCount, Color.Yellow) - .AddItem("Highlights", highlightsCount, Color.Orange1) - .AddItem("Messages", messagesCount, Color.LightGreen) - .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); - AnsiConsole.Markup("\n"); + await orchestrationService.DownloadCreatorContentAsync( + user.Key, user.Value, path, users, + startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, + eventHandler); } DateTime endTime = DateTime.Now; - TimeSpan totalTime = endTime - startTime; - AnsiConsole.Markup($"[green]Scrape Completed in {totalTime.TotalMinutes:0.00} minutes\n[/]"); + eventHandler.OnScrapeComplete(endTime - startTime); } - else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) { + // Config was changed, loop will re-read } else { break; } - } while (!Config.NonInteractiveMode); + } while (!config.NonInteractiveMode); } - private async Task DownloadPaidMessages(string username, - KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, - int paidMessagesCount, string path) + private async Task HandleSinglePostDownload( + IDownloadOrchestrationService orchestrationService, + Dictionary users, + StartupResult startupResult, + IDownloadEventHandler eventHandler) { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - PurchasedEntities.PaidMessageCollection paidMessageCollection = new(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Paid Messages[/]", - async ctx => - { - paidMessageCollection = await apiService.GetPaidMessages("/posts/paid/chat", path, user.Key, ctx); - }); - - if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) - { - AnsiConsole.Markup( - $"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values - .ToList()); - } - else - { - totalSize = paidMessageCollection.PaidMessages.Count; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - ProgressTask task = - ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", - false); - task.MaxValue = totalSize; - task.StartTask(); - - SpectreProgressReporter progressReporter = new(task); - result = await downloadService.DownloadPaidMessages(username, path, hasSelectedUsersKVP.Value, - clientIdBlobMissing, devicePrivateKeyMissing, paidMessageCollection, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup( - $"[red]Paid Messages Already Downloaded: {result.ExistingDownloads} New Paid Messages Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - - AnsiConsole.Markup("[red]Found 0 Paid Messages\n[/]"); - return 0; - } - - private async Task DownloadMessages(string username, - KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, - int messagesCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - MessageEntities.MessageCollection messages = new(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Messages[/]", - async ctx => { messages = await apiService.GetMessages($"/chats/{user.Value}/messages", path, ctx); }); - - if (messages != null && messages.Messages.Count > 0) - { - AnsiConsole.Markup( - $"[red]Found {messages.Messages.Count} Media from {messages.MessageObjects.Count} Messages\n[/]"); - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(messages.Messages.Values.ToList()); - } - else - { - totalSize = messages.Messages.Count; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - ProgressTask task = ctx.AddTask($"[red]Downloading {messages.Messages.Count} Messages[/]", false); - task.MaxValue = totalSize; - task.StartTask(); - - SpectreProgressReporter progressReporter = new(task); - result = await downloadService.DownloadMessages(username, user.Value, path, - hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, messages, - progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup( - $"[red]Messages Already Downloaded: {result.ExistingDownloads} New Messages Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - - AnsiConsole.Markup("[red]Found 0 Messages\n[/]"); - return 0; - } - - private async Task DownloadHighlights(string username, KeyValuePair user, int highlightsCount, - string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup("[red]Getting Highlights\n[/]"); - - // Calculate total size for progress bar - long totalSize = 0; - Dictionary? tempHighlights = await apiService.GetMedia(MediaType.Highlights, - $"/users/{user.Value}/stories/highlights", null, path, paid_post_ids); - if (tempHighlights != null && tempHighlights.Count > 0) - { - AnsiConsole.Markup($"[red]Found {tempHighlights.Count} Highlights\n[/]"); - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(tempHighlights.Values.ToList()); - } - else - { - totalSize = tempHighlights.Count; - } - } - else - { - AnsiConsole.Markup("[red]Found 0 Highlights\n[/]"); - return 0; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - ProgressTask task = ctx.AddTask($"[red]Downloading {tempHighlights.Count} Highlights[/]", false); - task.MaxValue = totalSize; - task.StartTask(); - - SpectreProgressReporter progressReporter = new(task); - result = await downloadService.DownloadHighlights(username, user.Value, path, paid_post_ids.ToHashSet(), - progressReporter); - - task.StopTask(); - }); - AnsiConsole.Markup( - $"[red]Highlights Already Downloaded: {result.ExistingDownloads} New Highlights Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - - private async Task DownloadStories(string username, KeyValuePair user, int storiesCount, - string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup("[red]Getting Stories\n[/]"); - - // Calculate total size for progress bar - long totalSize = 0; - Dictionary? tempStories = await serviceProvider.GetRequiredService() - .GetMedia(MediaType.Stories, $"/users/{user.Value}/stories", null, path, paid_post_ids); - if (tempStories != null && tempStories.Count > 0) - { - AnsiConsole.Markup($"[red]Found {tempStories.Count} Stories\n[/]"); - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(tempStories.Values.ToList()); - } - else - { - totalSize = tempStories.Count; - } - } - else - { - AnsiConsole.Markup("[red]Found 0 Stories\n[/]"); - return 0; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - ProgressTask task = ctx.AddTask($"[red]Downloading {tempStories.Count} Stories[/]", false); - task.MaxValue = totalSize; - task.StartTask(); - - SpectreProgressReporter progressReporter = new(task); - result = await downloadService.DownloadStories(username, user.Value, path, paid_post_ids.ToHashSet(), - progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup( - $"[red]Stories Already Downloaded: {result.ExistingDownloads} New Stories Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - - private async Task DownloadArchived(string username, - KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, - int archivedCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - ArchivedEntities.ArchivedCollection archived = new(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Archived Posts[/]", - async ctx => { archived = await apiService.GetArchived($"/users/{user.Value}/posts", path, ctx); }); - - if (archived != null && archived.ArchivedPosts.Count > 0) - { - AnsiConsole.Markup( - $"[red]Found {archived.ArchivedPosts.Count} Media from {archived.ArchivedPostObjects.Count} Archived Posts\n[/]"); - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(archived.ArchivedPosts.Values.ToList()); - } - else - { - totalSize = archived.ArchivedPosts.Count; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => + "[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 => { - ProgressTask task = - ctx.AddTask($"[red]Downloading {archived.ArchivedPosts.Count} Archived Posts[/]", false); - task.MaxValue = totalSize; - task.StartTask(); - - SpectreProgressReporter progressReporter = new(task); - result = await downloadService.DownloadArchived(username, user.Value, path, - hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, archived, - progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup( - $"[red]Archived Posts Already Downloaded: {result.ExistingDownloads} New Archived Posts Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - - AnsiConsole.Markup("[red]Found 0 Archived Posts\n[/]"); - return 0; - } - - private async Task DownloadFreePosts(string username, - KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, - int postCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup( - "[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/]"); - Log.Debug($"Calling DownloadFreePosts - {user.Key}"); - - PostEntities.PostCollection posts = new(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Posts[/]", - async ctx => - { - posts = await apiService.GetPosts($"/users/{user.Value}/posts", path, paid_post_ids, ctx); - }); - - if (posts == null || posts.Posts.Count <= 0) - { - AnsiConsole.Markup("[red]Found 0 Posts\n[/]"); - Log.Debug("Found 0 Posts"); - return 0; - } - - AnsiConsole.Markup($"[red]Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts\n[/]"); - Log.Debug($"Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts"); - - long totalSize = configService.CurrentConfig.ShowScrapeSize - ? await downloadService.CalculateTotalFileSize(posts.Posts.Values.ToList()) - : posts.Posts.Count; - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - ProgressTask task = ctx.AddTask($"[red]Downloading {posts.Posts.Count} Posts[/]", false); - task.MaxValue = totalSize; - task.StartTask(); - - SpectreProgressReporter progressReporter = new(task); - result = await downloadService.DownloadFreePosts(username, user.Value, path, hasSelectedUsersKVP.Value, - clientIdBlobMissing, devicePrivateKeyMissing, posts, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup( - $"[red]Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}[/]\n"); - Log.Debug($"Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}"); - - return result.TotalCount; - } - - private async Task DownloadPaidPosts(string username, - KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, - int paidPostCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup("[red]Getting Paid Posts\n[/]"); - Log.Debug($"Calling DownloadPaidPosts - {user.Key}"); - - PurchasedEntities.PaidPostCollection purchasedPosts = new(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Paid Posts[/]", - async ctx => - { - purchasedPosts = - await apiService.GetPaidPosts("/posts/paid/post", path, user.Key, paid_post_ids, ctx); - }); - - if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) - { - AnsiConsole.Markup("[red]Found 0 Paid Posts\n[/]"); - Log.Debug("Found 0 Paid Posts"); - return 0; - } - - AnsiConsole.Markup( - $"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); - Log.Debug( - $"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); - - long totalSize = configService.CurrentConfig.ShowScrapeSize - ? await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()) - : purchasedPosts.PaidPosts.Count; - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - ProgressTask task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", - false); - task.MaxValue = totalSize; - task.StartTask(); - - SpectreProgressReporter progressReporter = new(task); - result = await downloadService.DownloadPaidPosts(username, user.Value, path, hasSelectedUsersKVP.Value, - clientIdBlobMissing, devicePrivateKeyMissing, purchasedPosts, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup( - $"[red]Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}[/]\n"); - Log.Debug( - $"Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}"); - - return result.TotalCount; - } - - private async Task DownloadPaidPostsPurchasedTab(string username, - PurchasedEntities.PaidPostCollection purchasedPosts, - KeyValuePair user, int paidPostCount, string path, Dictionary users) - { - IDBService dbService = serviceProvider.GetRequiredService(); - IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - int oldPaidPostCount = 0; - int newPaidPostCount = 0; - if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) - { - AnsiConsole.Markup("[red]Found 0 Paid Posts\n[/]"); - Log.Debug("Found 0 Paid Posts"); - return 0; - } - - AnsiConsole.Markup( - $"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); - Log.Debug( - $"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); - - paidPostCount = purchasedPosts.PaidPosts.Count; - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()); - } - else - { - totalSize = paidPostCount; - } - - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - ProgressTask task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", - false); - Log.Debug($"Downloading {purchasedPosts.PaidPosts.Count} Paid Posts"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair purchasedPostKVP in purchasedPosts.PaidPosts) - { - bool isNew; - MessageEntities.Medium? mediaInfo = - purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); - PurchasedEntities.ListItem? postInfo = mediaInfo != null - ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true) - : null; - string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidPostFileNameFormat ?? ""; - string paidPostPath = configService.CurrentConfig.FolderPerPaidPost && postInfo != null && - postInfo?.Id is not null && postInfo?.PostedAt is not null - ? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}" - : "/Posts/Paid"; - - if (purchasedPostKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + Log.Debug($"Single Post URL: {url}"); + Regex regex = new("https://onlyfans\\.com/[0-9]+/[A-Za-z0-9]+", RegexOptions.IgnoreCase); + if (regex.IsMatch(url)) { - string[] parsed = purchasedPostKVP.Value.Split(','); - (string decryptionKey, DateTime lastModified)? drmInfo = - await downloadService.GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], - parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); - if (drmInfo == null) - { - continue; - } - - isNew = await downloadService.DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, purchasedPostKVP.Key, - "Posts", - new SpectreProgressReporter(task), paidPostPath + "/Videos", filenameFormat, - postInfo, mediaInfo, postInfo?.FromUser, users); - } - else - { - isNew = await downloadService.DownloadMedia(purchasedPostKVP.Value, path, - purchasedPostKVP.Key, "Posts", new SpectreProgressReporter(task), - paidPostPath, filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users); + return ValidationResult.Success(); } - if (isNew) + if (url == "" || url == "exit" || url == "back") { - newPaidPostCount++; - } - else - { - oldPaidPostCount++; + 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[/]"); } - task.StopTask(); - }); + 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]Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}[/]\n"); - Log.Debug($"Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}"); - return paidPostCount; - } + "[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(); + } - private async Task DownloadPaidMessagesPurchasedTab(string username, - PurchasedEntities.PaidMessageCollection paidMessageCollection, KeyValuePair user, - int paidMessagesCount, - string path, Dictionary users) - { - IDBService dbService = serviceProvider.GetRequiredService(); - IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); + if (url == "" || url == "back" || url == "exit") + { + return ValidationResult.Success(); + } - int oldPaidMessagesCount = 0; - int newPaidMessagesCount = 0; - if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) + Log.Error("Message URL invalid"); + return ValidationResult.Error("[red]Please enter a valid message URL[/]"); + })); + + if (messageUrl != "" && messageUrl != "exit" && messageUrl != "back") { - AnsiConsole.Markup( - $"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); - Log.Debug( - $"Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages"); - paidMessagesCount = paidMessageCollection.PaidMessages.Count; - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) + 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) { - totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values - .ToList()); + 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 { - totalSize = paidMessagesCount; + AnsiConsole.Markup($"[red]Folder for {Markup.Escape(username)} already created\n[/]"); + Log.Debug($"Folder for {username} already created"); } - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - ProgressTask task = - ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", - false); - Log.Debug($"Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair paidMessageKVP in paidMessageCollection.PaidMessages) - { - bool isNew; - MessageEntities.Medium? mediaInfo = - paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKVP.Key); - PurchasedEntities.ListItem? messageInfo = - paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); - string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? ""; - string paidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && - messageInfo?.Id is not null && messageInfo?.CreatedAt is not null - ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" - : "/Messages/Paid"; + IDBService dbService = serviceProvider.GetRequiredService(); + await dbService.CreateDB(path); - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] parsed = paidMessageKVP.Value.Split(','); - (string decryptionKey, DateTime lastModified)? drmInfo = - await downloadService.GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], - parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); - if (drmInfo == null) - { - continue; - } - - isNew = await downloadService.DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, - "Messages", - new SpectreProgressReporter(task), paidMsgPath + "/Videos", filenameFormat, - messageInfo, mediaInfo, messageInfo?.FromUser, users); - } - else - { - isNew = await downloadService.DownloadMedia(paidMessageKVP.Value, path, - paidMessageKVP.Key, "Messages", new SpectreProgressReporter(task), - paidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); - } - - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - - task.StopTask(); - }); - AnsiConsole.Markup( - $"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); - Log.Debug( - $"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}"); - } - else - { - AnsiConsole.Markup("[red]Found 0 Paid Messages\n[/]"); - Log.Debug("Found 0 Paid Messages"); - } - - return paidMessagesCount; - } - - private async Task DownloadStreams(string username, - KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, - int streamsCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - StreamEntities.StreamsCollection streams = new(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Streams[/]", - async ctx => - { - streams = await apiService.GetStreams($"/users/{user.Value}/posts/streams", path, paid_post_ids, - ctx); - }); - - if (streams != null && streams.Streams.Count > 0) - { - AnsiConsole.Markup( - $"[red]Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams\n[/]"); - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(streams.Streams.Values.ToList()); - } - else - { - totalSize = streams.Streams.Count; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - ProgressTask task = ctx.AddTask($"[red]Downloading {streams.Streams.Count} Streams[/]", false); - task.MaxValue = totalSize; - task.StartTask(); - - SpectreProgressReporter progressReporter = new(task); - result = await downloadService.DownloadStreams(username, user.Value, path, - hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, streams, - progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup( - $"[red]Streams Already Downloaded: {result.ExistingDownloads} New Streams Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - - AnsiConsole.Markup("[red]Found 0 Streams\n[/]"); - return 0; - } - - private async Task DownloadPaidMessage(string username, - KeyValuePair> hasSelectedUsersKVP, int paidMessagesCount, string path, - long message_id) - { - IDBService dbService = serviceProvider.GetRequiredService(); - IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? ""; - - Log.Debug($"Calling DownloadPaidMessage - {username}"); - - AnsiConsole.Markup("[red]Getting Paid Message\n[/]"); - - PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection = - await apiService.GetPaidMessage($"/messages/{message_id.ToString()}", path); - int oldPreviewPaidMessagesCount = 0; - int newPreviewPaidMessagesCount = 0; - int oldPaidMessagesCount = 0; - int newPaidMessagesCount = 0; - if (singlePaidMessageCollection != null && singlePaidMessageCollection.PreviewSingleMessages.Count > 0) - { - AnsiConsole.Markup( - $"[red]Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages\n[/]"); - Log.Debug( - $"Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); - paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection - .PreviewSingleMessages.Values.ToList()); - } - else - { - totalSize = paidMessagesCount; - } - - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - ProgressTask task = - ctx.AddTask( - $"[red]Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Paid Messages[/]", - false); - Log.Debug($"Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Paid Messages"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection - .PreviewSingleMessages) - { - bool isNew; - MessageEntities.Medium? mediaInfo = - singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => - m.Id == paidMessageKVP.Key); - MessageEntities.SingleMessage? messageInfo = - singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); - - string previewMsgPath = configService.CurrentConfig.FolderPerMessage && messageInfo != null && - messageInfo?.Id is not null && messageInfo?.CreatedAt is not null - ? $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" - : "/Messages/Free"; - - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] parsed = paidMessageKVP.Value.Split(','); - (string decryptionKey, DateTime lastModified)? drmInfo = - await downloadService.GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], - parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); - if (drmInfo == null) - { - continue; - } - - isNew = await downloadService.DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, - "Messages", - new SpectreProgressReporter(task), previewMsgPath + "/Videos", filenameFormat, - messageInfo, mediaInfo, messageInfo?.FromUser, hasSelectedUsersKVP.Value); - } - else - { - isNew = await downloadService.DownloadMedia(paidMessageKVP.Value, path, - paidMessageKVP.Key, "Messages", new SpectreProgressReporter(task), - previewMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, - hasSelectedUsersKVP.Value); - } - - if (isNew) - { - newPreviewPaidMessagesCount++; - } - else - { - oldPreviewPaidMessagesCount++; - } - } - - task.StopTask(); - }); - AnsiConsole.Markup( - $"[red]Preview Paid Messages Already Downloaded: {oldPreviewPaidMessagesCount} New Preview Paid Messages Downloaded: {newPreviewPaidMessagesCount}[/]\n"); - Log.Debug( - $"Preview Paid Messages Already Downloaded: {oldPreviewPaidMessagesCount} New Preview Paid Messages Downloaded: {newPreviewPaidMessagesCount}"); - } - - if (singlePaidMessageCollection != null && singlePaidMessageCollection.SingleMessages.Count > 0) - { - AnsiConsole.Markup( - $"[red]Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages\n[/]"); - Log.Debug( - $"Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); - paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection.SingleMessages - .Values.ToList()); - } - else - { - totalSize = paidMessagesCount; - } - - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - ProgressTask task = - ctx.AddTask( - $"[red]Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages[/]", - false); - Log.Debug($"Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.SingleMessages) - { - bool isNew; - MessageEntities.Medium? mediaInfo = - singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => - m.Id == paidMessageKVP.Key); - MessageEntities.SingleMessage? messageInfo = - singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); - string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PaidMessageFileNameFormat ?? ""; - string singlePaidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && - messageInfo != null && messageInfo?.Id is not null && - messageInfo?.CreatedAt is not null - ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" - : "/Messages/Paid"; - - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] parsed = paidMessageKVP.Value.Split(','); - (string decryptionKey, DateTime lastModified)? drmInfo = - await downloadService.GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], - parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); - if (drmInfo == null) - { - continue; - } - - isNew = await downloadService.DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, - "Messages", - new SpectreProgressReporter(task), singlePaidMsgPath + "/Videos", filenameFormat, - messageInfo, mediaInfo, messageInfo?.FromUser, hasSelectedUsersKVP.Value); - } - else - { - isNew = await downloadService.DownloadMedia(paidMessageKVP.Value, path, - paidMessageKVP.Key, "Messages", new SpectreProgressReporter(task), - singlePaidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, - hasSelectedUsersKVP.Value); - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } - - task.StopTask(); - }); - AnsiConsole.Markup( - $"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); - Log.Debug( - $"Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}"); - } - else - { - AnsiConsole.Markup("[red]Found 0 Paid Messages\n[/]"); - Log.Debug("Found 0 Paid Messages"); - } - - return paidMessagesCount; - } - - private async Task DownloadSinglePost(string username, long post_id, string path, Dictionary users) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - Log.Debug($"Calling DownloadSinglePost - {post_id.ToString()}"); - - AnsiConsole.Markup("[red]Getting Post\n[/]"); - PostEntities.SinglePostCollection post = await apiService.GetPost($"/posts/{post_id.ToString()}", path); - if (post == null) - { - AnsiConsole.Markup("[red]Couldn't find post\n[/]"); - Log.Debug("Couldn't find post"); - return; - } - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(post.SinglePosts.Values.ToList()); - } - else - { - totalSize = post.SinglePosts.Count; - } - - bool isNew = false; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - ProgressTask task = ctx.AddTask("[red]Downloading Post[/]", false); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair postKVP in post.SinglePosts) - { - PostEntities.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.Id == postKVP.Key); - PostEntities.SinglePost? postInfo = - post.SinglePostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); - string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) - .PostFileNameFormat ?? ""; - string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && - postInfo?.Id is not null && postInfo?.PostedAt is not null - ? $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}" - : "/Posts/Free"; - - if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] parsed = postKVP.Value.Split(','); - (string decryptionKey, DateTime lastModified)? drmInfo = - await downloadService.GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], - parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); - if (drmInfo == null) - { - continue; - } - - isNew = await downloadService.DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKVP.Key, "Posts", - new SpectreProgressReporter(task), postPath + "/Videos", filenameFormat, - postInfo, mediaInfo, postInfo?.Author, users); - } - else - { - try - { - isNew = await downloadService.DownloadMedia(postKVP.Value, path, - postKVP.Key, "Posts", new SpectreProgressReporter(task), - postPath, filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); - } - catch - { - Console.WriteLine("Media was null"); - } - } - } - - task.StopTask(); - }); - if (isNew) - { - AnsiConsole.Markup($"[red]Post {post_id} downloaded\n[/]"); - Log.Debug($"Post {post_id} downloaded"); - } - else - { - AnsiConsole.Markup($"[red]Post {post_id} already downloaded\n[/]"); - Log.Debug($"Post {post_id} already downloaded"); + await orchestrationService.DownloadSinglePaidMessageAsync(username, messageId, path, users, + startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler); } } @@ -2023,7 +482,7 @@ public class Program(IServiceProvider serviceProvider) bool hasSelectedUsers = false; Dictionary selectedUsers = new(); - Config currentConfig = configService.CurrentConfig!; + Config currentConfig = configService.CurrentConfig; while (!hasSelectedUsers) { @@ -2058,7 +517,7 @@ public class Program(IServiceProvider serviceProvider) if (listSelection.Contains("[red]Go Back[/]")) { - break; // Go back to the main menu + break; } hasSelectedUsers = true; @@ -2066,7 +525,7 @@ public class Program(IServiceProvider serviceProvider) foreach (string item in listSelection) { long listId = lists[item.Replace("[red]", "").Replace("[/]", "")]; - List usernames = await apiService.GetListUsers($"/lists/{listId}/users"); + List usernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; foreach (string user in usernames) { listUsernames.Add(user); @@ -2099,7 +558,7 @@ public class Program(IServiceProvider serviceProvider) List userSelection = AnsiConsole.Prompt(selectedNamesPrompt); if (userSelection.Contains("[red]Go Back[/]")) { - break; // Go back to the main menu + break; } hasSelectedUsers = true; @@ -2118,22 +577,11 @@ public class Program(IServiceProvider serviceProvider) case "[red]Edit config.conf[/]": while (true) { - if (currentConfig == null) - { - currentConfig = new Config(); - } - + List<(string Name, bool Value)> toggleableProps = configService.GetToggleableProperties(); List<(string choice, bool isSelected)> choices = new() { ("[red]Go Back[/]", false) }; - - foreach (PropertyInfo propInfo in typeof(Config).GetProperties()) + foreach ((string Name, bool Value) prop in toggleableProps) { - ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute(); - if (attr != null) - { - string itemLabel = $"[red]{propInfo.Name}[/]"; - choices.Add(new ValueTuple(itemLabel, - (bool)propInfo.GetValue(currentConfig)!)); - } + choices.Add(($"[red]{prop.Name}[/]", prop.Value)); } MultiSelectionPrompt multiSelectionPrompt = new MultiSelectionPrompt() @@ -2158,41 +606,15 @@ public class Program(IServiceProvider serviceProvider) break; } - bool configChanged = false; + // Extract plain names from selections + List selectedNames = configOptions + .Select(o => o.Replace("[red]", "").Replace("[/]", "")) + .ToList(); - Config newConfig = new(); - foreach (PropertyInfo propInfo in typeof(Config).GetProperties()) - { - ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute(); - if (attr != null) - { - // - // Get the new choice from the selection - // - string itemLabel = $"[red]{propInfo.Name}[/]"; - bool newValue = configOptions.Contains(itemLabel); - bool oldValue = choices.Where(c => c.choice == itemLabel).Select(c => c.isSelected) - .First(); - propInfo.SetValue(newConfig, newValue); - - if (newValue != oldValue) - { - configChanged = true; - } - } - else - { - // - // Reassign any non toggleable values - // - propInfo.SetValue(newConfig, propInfo.GetValue(currentConfig)); - } - } - - configService.UpdateConfig(newConfig); + bool configChanged = configService.ApplyToggleableSelections(selectedNames); await configService.SaveConfigurationAsync(); + currentConfig = configService.CurrentConfig; - currentConfig = newConfig; if (configChanged) { return (true, new Dictionary { { "ConfigChanged", 0 } }); @@ -2205,7 +627,7 @@ public class Program(IServiceProvider serviceProvider) case "[red]Change logging level[/]": while (true) { - List<(string choice, bool isSelected)> choices = new() { ("[red]Go Back[/]", false) }; + List<(string choice, bool isSelected)> choices = [("[red]Go Back[/]", false)]; foreach (string name in typeof(LoggingLevel).GetEnumNames()) { @@ -2231,52 +653,31 @@ public class Program(IServiceProvider serviceProvider) } levelOption = levelOption.Replace("[red]", "").Replace("[/]", ""); - LoggingLevel newLogLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), levelOption, true); + LoggingLevel newLogLevel = + (LoggingLevel)Enum.Parse(typeof(LoggingLevel), levelOption, true); Log.Debug($"Logging level changed to: {levelOption}"); - bool configChanged = false; - - Config newConfig = new(); - - newConfig = currentConfig; - + Config newConfig = currentConfig; newConfig.LoggingLevel = newLogLevel; - currentConfig = newConfig; configService.UpdateConfig(newConfig); await configService.SaveConfigurationAsync(); - if (configChanged) - { - return (true, new Dictionary { { "ConfigChanged", 0 } }); - } - break; } break; case "[red]Logout and Exit[/]": - if (Directory.Exists("chrome-data")) - { - Log.Information("Deleting chrome-data folder"); - Directory.Delete("chrome-data", true); - } - - if (File.Exists("auth.json")) - { - Log.Information("Deleting auth.json"); - File.Delete("auth.json"); - } - - return (false, null); // Return false to indicate exit + authService.Logout(); + return (false, null); case "[red]Exit[/]": - return (false, null); // Return false to indicate exit + return (false, null); } } - return (true, selectedUsers); // Return true to indicate selected users + return (true, selectedUsers); } public static List GetMainMenuOptions(Dictionary users, Dictionary lists) @@ -2312,98 +713,182 @@ public class Program(IServiceProvider serviceProvider) }; } - private static bool ValidateFilePath(string path) + private async Task HandleAuthFlow(IAuthService authService, IConfigService configService) { - char[] invalidChars = Path.GetInvalidPathChars(); - char[] foundInvalidChars = path.Where(c => invalidChars.Contains(c)).ToArray(); - - if (foundInvalidChars.Any()) + if (await authService.LoadFromFileAsync()) { - AnsiConsole.Markup( - $"[red]Invalid characters found in path {path}:[/] {string.Join(", ", foundInvalidChars)}\n"); - return false; + AnsiConsole.Markup("[green]auth.json located successfully!\n[/]"); } - - if (!File.Exists(path)) + else if (File.Exists("auth.json")) { - if (Directory.Exists(path)) + Log.Information("Auth file found but could not be deserialized"); + if (!configService.CurrentConfig.DisableBrowserAuth) { - AnsiConsole.Markup( - $"[red]The provided path {path} improperly points to a directory and not a file.[/]\n"); + 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[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); + + Console.ReadKey(); + Environment.Exit(2); + } + + if (!configService.CurrentConfig.DisableBrowserAuth) + { + await LoadAuthFromBrowser(); } else { - AnsiConsole.Markup($"[red]The provided path {path} does not exist or is not accessible.[/]\n"); + ShowAuthMissingError(); } - - return false; - } - - return true; - } - - private static ProgressColumn[] GetProgressColumns(bool showScrapeSize) - { - List progressColumns; - if (showScrapeSize) - { - progressColumns = new List - { - new TaskDescriptionColumn(), - new ProgressBarColumn(), - new PercentageColumn(), - new DownloadedColumn(), - new RemainingTimeColumn() - }; } else { - progressColumns = new List + if (configService.CurrentConfig.NonInteractiveMode) { - new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn() - }; + ShowAuthMissingError(); + } + else if (!configService.CurrentConfig.DisableBrowserAuth) + { + await LoadAuthFromBrowser(); + } + else + { + ShowAuthMissingError(); + } } - - return progressColumns.ToArray(); } - public static string? GetFullPath(string filename) + private static void ShowAuthMissingError() { - if (File.Exists(filename)) + 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[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); + + Console.ReadKey(); + Environment.Exit(2); + } + + private static void DisplayVersionResult(VersionCheckResult result) + { + if (result.TimedOut) { - return Path.GetFullPath(filename); + AnsiConsole.Markup("[yellow]Version check timed out after 30 seconds.\n[/]"); + return; } - string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? ""; - foreach (string path in pathEnv.Split(Path.PathSeparator)) + if (result.CheckFailed) { - string fullPath = Path.Combine(path, filename); - if (File.Exists(fullPath)) + 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) + { + if (result.FfmpegPathAutoDetected && result.FfmpegPath != null) { - return fullPath; + AnsiConsole.Markup( + $"[green]FFmpeg located successfully. Path auto-detected: {Markup.Escape(result.FfmpegPath)}\n[/]"); + } + else + { + AnsiConsole.Markup("[green]FFmpeg located successfully\n[/]"); + } + + if (result.FfmpegVersion != null) + { + AnsiConsole.Markup($"[green]ffmpeg version detected as {Markup.Escape(result.FfmpegVersion)}[/]\n"); + } + else + { + AnsiConsole.Markup("[yellow]ffmpeg version could not be parsed[/]\n"); } } - return null; - } - - public static void ValidateCookieString(Auth auth) - { - string pattern = @"(auth_id=\d+)|(sess=[^;]+)"; - MatchCollection matches = Regex.Matches(auth.Cookie, pattern); - - string output = string.Join("; ", matches); - - if (!output.EndsWith(";")) + // Widevine + if (!result.ClientIdBlobMissing) { - output += ";"; + AnsiConsole.Markup("[green]device_client_id_blob located successfully![/]\n"); } - if (auth.Cookie.Trim() != output.Trim()) + if (!result.DevicePrivateKeyMissing) { - auth.Cookie = output; - string newAuthString = JsonConvert.SerializeObject(auth, Formatting.Indented); - File.WriteAllText("auth.json", newAuthString); + 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"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); + Log.Error("rules.json processing failed: {Error}", result.RulesJsonError); + + if (!configService.CurrentConfig.NonInteractiveMode) + { + Console.ReadKey(); + } + + Environment.Exit(2); + } } } } diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/ApiService.cs similarity index 92% rename from OF DL/Services/APIService.cs rename to OF DL/Services/ApiService.cs index 9c64c1d..c460b17 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/ApiService.cs @@ -30,53 +30,52 @@ using UserEntities = OF_DL.Models.Entities.Users; using OF_DL.Models.Mappers; using OF_DL.Widevine; using Serilog; -using Spectre.Console; using static OF_DL.Utils.HttpUtil; using Constants = OF_DL.Helpers.Constants; using SinglePostCollection = OF_DL.Models.Entities.Posts.SinglePostCollection; namespace OF_DL.Services; -public class APIService(IAuthService authService, IConfigService configService, IDBService dbService) +public class ApiService(IAuthService authService, IConfigService configService, IDBService dbService) : IAPIService { private const int MaxAttempts = 30; private const int DelayBetweenAttempts = 3000; - private static readonly JsonSerializerSettings m_JsonSerializerSettings; - private static DateTime? cachedDynamicRulesExpiration; - private static DynamicRules? cachedDynamicRules; + private static readonly JsonSerializerSettings s_mJsonSerializerSettings; + private static DateTime? s_cachedDynamicRulesExpiration; + private static DynamicRules? s_cachedDynamicRules; - static APIService() => - m_JsonSerializerSettings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }; + static ApiService() => + s_mJsonSerializerSettings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }; public Dictionary GetDynamicHeaders(string path, string queryParams) { Log.Debug("Calling GetDynamicHeaders"); - Log.Debug($"Path: {path}"); - Log.Debug($"Query Params: {queryParams}"); + Log.Debug("Path: {Path}", path); + Log.Debug("Query Params: {QueryParams}", queryParams); DynamicRules? root; //Check if we have a cached version of the dynamic rules - if (cachedDynamicRules != null && cachedDynamicRulesExpiration.HasValue && - DateTime.UtcNow < cachedDynamicRulesExpiration) + if (s_cachedDynamicRules != null && s_cachedDynamicRulesExpiration.HasValue && + DateTime.UtcNow < s_cachedDynamicRulesExpiration) { Log.Debug("Using cached dynamic rules"); - root = cachedDynamicRules; + root = s_cachedDynamicRules; } else { //Get rules from GitHub and fallback to local file - string? dynamicRulesJSON = GetDynamicRules(); - if (!string.IsNullOrEmpty(dynamicRulesJSON)) + string? dynamicRulesJson = GetDynamicRules(); + if (!string.IsNullOrEmpty(dynamicRulesJson)) { Log.Debug("Using dynamic rules from GitHub"); - root = JsonConvert.DeserializeObject(dynamicRulesJSON); + root = JsonConvert.DeserializeObject(dynamicRulesJson); // Cache the GitHub response for 15 minutes - cachedDynamicRules = root; - cachedDynamicRulesExpiration = DateTime.UtcNow.AddMinutes(15); + s_cachedDynamicRules = root; + s_cachedDynamicRulesExpiration = DateTime.UtcNow.AddMinutes(15); } else { @@ -87,33 +86,55 @@ public class APIService(IAuthService authService, IConfigService configService, // operations and frequent call to GitHub. Since the GitHub dynamic rules // are preferred to the local file, the cache time is shorter than when dynamic rules // are successfully retrieved from GitHub. - cachedDynamicRules = root; - cachedDynamicRulesExpiration = DateTime.UtcNow.AddMinutes(5); + s_cachedDynamicRules = root; + s_cachedDynamicRulesExpiration = DateTime.UtcNow.AddMinutes(5); } } + if (root == null) + { + throw new Exception("Unable to parse dynamic rules. Root is null"); + } + + if (root.ChecksumConstant == null || root.ChecksumIndexes.Count == 0 || root.Prefix == null || + root.Suffix == null || root.AppToken == null) + { + throw new Exception("Invalid dynamic rules. Missing required fields"); + } + + if (authService.CurrentAuth == null) + { + throw new Exception("Auth service is null"); + } + + if (authService.CurrentAuth.UserId == null || authService.CurrentAuth.Cookie == null || + authService.CurrentAuth.UserAgent == null || authService.CurrentAuth.XBc == null) + { + throw new Exception("Auth service is missing required fields"); + } + DateTimeOffset dto = DateTime.UtcNow; long timestamp = dto.ToUnixTimeMilliseconds(); - string input = $"{root!.StaticParam}\n{timestamp}\n{path + queryParams}\n{authService.CurrentAuth.UserId}"; + string input = $"{root.StaticParam}\n{timestamp}\n{path + queryParams}\n{authService.CurrentAuth.UserId}"; byte[] inputBytes = Encoding.UTF8.GetBytes(input); byte[] hashBytes = SHA1.HashData(inputBytes); string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); int checksum = root.ChecksumIndexes.Aggregate(0, (current, number) => current + hashString[number]) + - root.ChecksumConstant!.Value; + root.ChecksumConstant.Value; string sign = $"{root.Prefix}:{hashString}:{checksum.ToString("X").ToLower()}:{root.Suffix}"; Dictionary headers = new() { { "accept", "application/json, text/plain" }, - { "app-token", root.AppToken! }, - { "cookie", authService.CurrentAuth!.Cookie! }, + { "app-token", root.AppToken }, + { "cookie", authService.CurrentAuth.Cookie }, { "sign", sign }, { "time", timestamp.ToString() }, - { "user-id", authService.CurrentAuth!.UserId! }, - { "user-agent", authService.CurrentAuth!.UserAgent! }, - { "x-bc", authService.CurrentAuth!.XBc! } + { "user-id", authService.CurrentAuth.UserId }, + { "user-agent", authService.CurrentAuth.UserAgent }, + { "x-bc", authService.CurrentAuth.XBc } }; return headers; } @@ -126,10 +147,10 @@ public class APIService(IAuthService authService, IConfigService configService, try { UserEntities.User user = new(); - int post_limit = 50; + const int postLimit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, { "order", "publish_date_asc" } + { "limit", postLimit.ToString() }, { "order", "publish_date_asc" } }; HttpClient client = new(); @@ -145,7 +166,7 @@ public class APIService(IAuthService authService, IConfigService configService, response.EnsureSuccessStatusCode(); string body = await response.Content.ReadAsStringAsync(); UserDtos.UserDto? userDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); user = UserMapper.FromDto(userDto) ?? new UserEntities.User(); return user; } @@ -369,7 +390,7 @@ public class APIService(IAuthService authService, IConfigService configService, try { Dictionary return_urls = new(); - int post_limit = 50; + const int postLimit = 50; int limit = 5; int offset = 0; @@ -380,9 +401,7 @@ public class APIService(IAuthService authService, IConfigService configService, case MediaType.Stories: getParams = new Dictionary { - { "limit", post_limit.ToString() }, - { "order", "publish_date_desc" }, - { "skip_users", "all" } + { "limit", postLimit.ToString() }, { "order", "publish_date_desc" }, { "skip_users", "all" } }; break; @@ -402,7 +421,7 @@ public class APIService(IAuthService authService, IConfigService configService, Log.Debug("Media Stories - " + endpoint); List? storiesDto = - JsonConvert.DeserializeObject>(body, m_JsonSerializerSettings); + JsonConvert.DeserializeObject>(body, s_mJsonSerializerSettings); List stories = StoriesMapper.FromDto(storiesDto); foreach (StoryEntities.Stories story in stories) @@ -422,15 +441,27 @@ public class APIService(IAuthService authService, IConfigService configService, await dbService.AddStory(folder, story.Id, "", "0", false, false, DateTime.Now); } - if (story.Media != null && story.Media.Count > 0) + if (story.Media.Count > 0) { foreach (StoryEntities.Medium medium in story.Media) { + if (medium.Files.Full == null || medium.Files.Full.Url == null) + { + continue; + } + + string? mediaType = medium.Type == "photo" ? "Images" : + medium.Type == "video" || medium.Type == "gif" ? "Videos" : + medium.Type == "audio" ? "Audios" : null; + if (mediaType == null) + { + continue; + } + await dbService.AddMedia(folder, medium.Id, story.Id, medium.Files.Full.Url, null, null, null, "Stories", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, false, false, null); + mediaType, false, false, null); + if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; @@ -464,9 +495,9 @@ public class APIService(IAuthService authService, IConfigService configService, } else if (mediatype == MediaType.Highlights) { - List highlight_ids = new(); + List highlightIds = []; HighlightDtos.HighlightsDto? highlightsDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); HighlightEntities.Highlights highlights = HighlightsMapper.FromDto(highlightsDto); if (highlights.HasMore) @@ -480,7 +511,7 @@ public class APIService(IAuthService authService, IConfigService configService, string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); HighlightDtos.HighlightsDto? newHighlightsDto = JsonConvert.DeserializeObject(loopbody, - m_JsonSerializerSettings); + s_mJsonSerializerSettings); HighlightEntities.Highlights newHighlights = HighlightsMapper.FromDto(newHighlightsDto); highlights.List.AddRange(newHighlights.List); @@ -496,13 +527,13 @@ public class APIService(IAuthService authService, IConfigService configService, foreach (HighlightEntities.ListItem list in highlights.List) { - if (!highlight_ids.Contains(list.Id.ToString())) + if (!highlightIds.Contains(list.Id.ToString())) { - highlight_ids.Add(list.Id.ToString()); + highlightIds.Add(list.Id.ToString()); } } - foreach (string highlight_id in highlight_ids) + foreach (string highlight_id in highlightIds) { Dictionary highlight_headers = GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, ""); @@ -522,15 +553,20 @@ public class APIService(IAuthService authService, IConfigService configService, string highlightBody = await highlightResponse.Content.ReadAsStringAsync(); HighlightDtos.HighlightMediaDto? highlightMediaDto = JsonConvert.DeserializeObject(highlightBody, - m_JsonSerializerSettings); + s_mJsonSerializerSettings); HighlightEntities.HighlightMedia highlightMedia = HighlightsMapper.FromDto(highlightMediaDto); foreach (HighlightEntities.Story item in highlightMedia.Stories) { - if (item.Media != null && item.Media.Count > 0 && item.Media[0].CreatedAt.HasValue) + if (item.Media != null && item.Media.Count > 0 && item.Media[0].CreatedAt.HasValue && + item.Media[0].CreatedAt != null) { - await dbService.AddStory(folder, item.Id, "", "0", false, false, - item.Media[0].CreatedAt.Value); + DateTime? createdAt = item.Media[0].CreatedAt; + if (createdAt != null) + { + await dbService.AddStory(folder, item.Id, "", "0", false, false, + createdAt.Value); + } } else if (item.CreatedAt.HasValue) { @@ -605,18 +641,17 @@ public class APIService(IAuthService authService, IConfigService configService, public async Task GetPaidPosts(string endpoint, string folder, string username, - List paid_post_ids, StatusContext ctx) + List paid_post_ids, IStatusReporter statusReporter) { Log.Debug($"Calling GetPaidPosts - {username}"); try { - PurchasedEntities.Purchased paidPosts = new(); PurchasedEntities.PaidPostCollection paidPostCollection = new(); - int post_limit = 50; + const int postLimit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, + { "limit", postLimit.ToString() }, { "skip_users", "all" }, { "order", "publish_date_desc" }, { "format", "infinite" }, @@ -625,11 +660,9 @@ public class APIService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); PurchasedDtos.PurchasedDto? paidPostsDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - paidPosts = PurchasedMapper.FromDto(paidPostsDto); - ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + PurchasedEntities.Purchased paidPosts = PurchasedMapper.FromDto(paidPostsDto); + statusReporter.ReportStatus($"Getting Paid Posts - Found {paidPosts.List.Count}"); if (paidPosts != null && paidPosts.HasMore) { getParams["offset"] = paidPosts.List.Count.ToString(); @@ -639,19 +672,17 @@ public class APIService(IAuthService authService, IConfigService configService, string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); PurchasedDtos.PurchasedDto? newPaidPostsDto = - JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + JsonConvert.DeserializeObject(loopbody, s_mJsonSerializerSettings); newPaidPosts = PurchasedMapper.FromDto(newPaidPostsDto); paidPosts.List.AddRange(newPaidPosts.List); - ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + statusReporter.ReportStatus($"Getting Paid Posts - Found {paidPosts.List.Count}"); if (!newPaidPosts.HasMore) { break; } - getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit); } } @@ -812,18 +843,17 @@ public class APIService(IAuthService authService, IConfigService configService, public async Task GetPosts(string endpoint, string folder, List paid_post_ids, - StatusContext ctx) + IStatusReporter statusReporter) { Log.Debug($"Calling GetPosts - {endpoint}"); try { - PostEntities.Post posts = new(); PostEntities.PostCollection postCollection = new(); - int post_limit = 50; + const int postLimit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, + { "limit", postLimit.ToString() }, { "order", "publish_date_desc" }, { "format", "infinite" }, { "skip_users", "all" } @@ -855,12 +885,9 @@ public class APIService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); PostDtos.PostDto? postsDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - posts = PostMapper.FromDto(postsDto); - ctx.Status( - $"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + PostEntities.Post posts = PostMapper.FromDto(postsDto); + statusReporter.ReportStatus($"Getting Posts - Found {posts.List.Count}"); if (posts != null && posts.HasMore) { UpdateGetParamsForDateSelection( @@ -874,14 +901,11 @@ public class APIService(IAuthService authService, IConfigService configService, string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); PostDtos.PostDto? newPostsDto = - JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + JsonConvert.DeserializeObject(loopbody, s_mJsonSerializerSettings); newposts = PostMapper.FromDto(newPostsDto); posts.List.AddRange(newposts.List); - ctx.Status( - $"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + statusReporter.ReportStatus($"Getting Posts - Found {posts.List.Count}"); if (!newposts.HasMore) { break; @@ -1046,7 +1070,7 @@ public class APIService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); PostDtos.SinglePostDto? singlePostDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); singlePost = PostMapper.FromDto(singlePostDto); if (singlePostDto != null) @@ -1217,18 +1241,17 @@ public class APIService(IAuthService authService, IConfigService configService, public async Task GetStreams(string endpoint, string folder, List paid_post_ids, - StatusContext ctx) + IStatusReporter statusReporter) { Log.Debug($"Calling GetStreams - {endpoint}"); try { - StreamEntities.Streams streams = new(); StreamEntities.StreamsCollection streamsCollection = new(); - int post_limit = 50; + const int postLimit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, + { "limit", postLimit.ToString() }, { "order", "publish_date_desc" }, { "format", "infinite" }, { "skip_users", "all" } @@ -1248,11 +1271,9 @@ public class APIService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); StreamsDtos.StreamsDto? streamsDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - streams = StreamsMapper.FromDto(streamsDto); - ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + StreamEntities.Streams streams = StreamsMapper.FromDto(streamsDto); + statusReporter.ReportStatus($"Getting Streams - Found {streams.List.Count}"); if (streams != null && streams.HasMore) { UpdateGetParamsForDateSelection( @@ -1266,13 +1287,11 @@ public class APIService(IAuthService authService, IConfigService configService, string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); StreamsDtos.StreamsDto? newStreamsDto = - JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + JsonConvert.DeserializeObject(loopbody, s_mJsonSerializerSettings); newstreams = StreamsMapper.FromDto(newStreamsDto); streams.List.AddRange(newstreams.List); - ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + statusReporter.ReportStatus($"Getting Streams - Found {streams.List.Count}"); if (!newstreams.HasMore) { break; @@ -1393,18 +1412,17 @@ public class APIService(IAuthService authService, IConfigService configService, public async Task GetArchived(string endpoint, string folder, - StatusContext ctx) + IStatusReporter statusReporter) { Log.Debug($"Calling GetArchived - {endpoint}"); try { - ArchivedEntities.Archived archived = new(); ArchivedEntities.ArchivedCollection archivedCollection = new(); - int post_limit = 50; + const int postLimit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, + { "limit", postLimit.ToString() }, { "order", "publish_date_desc" }, { "skip_users", "all" }, { "format", "infinite" }, @@ -1425,13 +1443,16 @@ public class APIService(IAuthService authService, IConfigService configService, configService.CurrentConfig.CustomDate); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - ArchivedDtos.ArchivedDto archivedDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - archived = ArchivedMapper.FromDto(archivedDto); - ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); - if (archived != null && archived.HasMore) + if (body == null) + { + throw new Exception("Failed to retrieve archived posts. Received null response."); + } + + ArchivedDtos.ArchivedDto? archivedDto = + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + ArchivedEntities.Archived archived = ArchivedMapper.FromDto(archivedDto); + statusReporter.ReportStatus($"Getting Archived Posts - Found {archived.List.Count}"); + if (archived.HasMore) { UpdateGetParamsForDateSelection( downloadDateSelection, @@ -1439,17 +1460,18 @@ public class APIService(IAuthService authService, IConfigService configService, archived.TailMarker); while (true) { - ArchivedEntities.Archived newarchived = new(); - string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); - ArchivedDtos.ArchivedDto newarchivedDto = - JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); - newarchived = ArchivedMapper.FromDto(newarchivedDto); + if (loopbody == null) + { + throw new Exception("Failed to retrieve archived posts. Received null response."); + } + + ArchivedDtos.ArchivedDto? newarchivedDto = + JsonConvert.DeserializeObject(loopbody, s_mJsonSerializerSettings); + ArchivedEntities.Archived newarchived = ArchivedMapper.FromDto(newarchivedDto); archived.List.AddRange(newarchived.List); - ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + statusReporter.ReportStatus($"Getting Archived Posts - Found {archived.List.Count}"); if (!newarchived.HasMore) { break; @@ -1561,27 +1583,25 @@ public class APIService(IAuthService authService, IConfigService configService, } - public async Task GetMessages(string endpoint, string folder, StatusContext ctx) + public async Task GetMessages(string endpoint, string folder, + IStatusReporter statusReporter) { Log.Debug($"Calling GetMessages - {endpoint}"); try { - MessageEntities.Messages messages = new(); MessageEntities.MessageCollection messageCollection = new(); - int post_limit = 50; + const int postLimit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, { "order", "desc" }, { "skip_users", "all" } + { "limit", postLimit.ToString() }, { "order", "desc" }, { "skip_users", "all" } }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); MessageDtos.MessagesDto? messagesDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - messages = MessagesMapper.FromDto(messagesDto); - ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + MessageEntities.Messages messages = MessagesMapper.FromDto(messagesDto); + statusReporter.ReportStatus($"Getting Messages - Found {messages.List.Count}"); if (messages.HasMore) { getParams["id"] = messages.List[^1].Id.ToString(); @@ -1591,13 +1611,11 @@ public class APIService(IAuthService authService, IConfigService configService, string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); MessageDtos.MessagesDto? newMessagesDto = - JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + JsonConvert.DeserializeObject(loopbody, s_mJsonSerializerSettings); newMessages = MessagesMapper.FromDto(newMessagesDto); messages.List.AddRange(newMessages.List); - ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + statusReporter.ReportStatus($"Getting Messages - Found {messages.List.Count}"); if (!newMessages.HasMore) { break; @@ -1823,15 +1841,14 @@ public class APIService(IAuthService authService, IConfigService configService, try { - MessageEntities.SingleMessage message = new(); PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection = new(); - int post_limit = 50; - Dictionary getParams = new() { { "limit", post_limit.ToString() }, { "order", "desc" } }; + const int postLimit = 50; + Dictionary getParams = new() { { "limit", postLimit.ToString() }, { "order", "desc" } }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); MessageDtos.SingleMessageDto? messageDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - message = MessagesMapper.FromDto(messageDto); + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + MessageEntities.SingleMessage message = MessagesMapper.FromDto(messageDto); if (!configService.CurrentConfig.IgnoreOwnMessages || message.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.UserId)) @@ -2028,18 +2045,17 @@ public class APIService(IAuthService authService, IConfigService configService, public async Task GetPaidMessages(string endpoint, string folder, string username, - StatusContext ctx) + IStatusReporter statusReporter) { Log.Debug($"Calling GetPaidMessages - {username}"); try { - PurchasedEntities.Purchased paidMessages = new(); PurchasedEntities.PaidMessageCollection paidMessageCollection = new(); - int post_limit = 50; + const int postLimit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, + { "limit", postLimit.ToString() }, { "order", "publish_date_desc" }, { "format", "infinite" }, { "author", username }, @@ -2048,11 +2064,9 @@ public class APIService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); PurchasedDtos.PurchasedDto? paidMessagesDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - paidMessages = PurchasedMapper.FromDto(paidMessagesDto); - ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + PurchasedEntities.Purchased paidMessages = PurchasedMapper.FromDto(paidMessagesDto); + statusReporter.ReportStatus($"Getting Paid Messages - Found {paidMessages.List.Count}"); if (paidMessages != null && paidMessages.HasMore) { getParams["offset"] = paidMessages.List.Count.ToString(); @@ -2077,20 +2091,18 @@ public class APIService(IAuthService authService, IConfigService configService, string loopbody = await loopresponse.Content.ReadAsStringAsync(); PurchasedDtos.PurchasedDto? newPaidMessagesDto = JsonConvert.DeserializeObject(loopbody, - m_JsonSerializerSettings); + s_mJsonSerializerSettings); newpaidMessages = PurchasedMapper.FromDto(newPaidMessagesDto); } paidMessages.List.AddRange(newpaidMessages.List); - ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.List.Count}[/]"); - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("blue")); + statusReporter.ReportStatus($"Getting Paid Messages - Found {paidMessages.List.Count}"); if (!newpaidMessages.HasMore) { break; } - getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit); } } @@ -2330,27 +2342,31 @@ public class APIService(IAuthService authService, IConfigService configService, try { Dictionary purchasedTabUsers = new(); - PurchasedEntities.Purchased purchased = new(); - int post_limit = 50; + const int postLimit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, + { "limit", postLimit.ToString() }, { "order", "publish_date_desc" }, { "format", "infinite" }, { "skip_users", "all" } }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + if (body == null) + { + throw new Exception("Failed to get purchased tab users. null body returned."); + } + PurchasedDtos.PurchasedDto? purchasedDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - purchased = PurchasedMapper.FromDto(purchasedDto); - if (purchased != null && purchased.HasMore) + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + PurchasedEntities.Purchased purchased = PurchasedMapper.FromDto(purchasedDto); + if (purchased.HasMore) { getParams["offset"] = purchased.List.Count.ToString(); while (true) { string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); - PurchasedEntities.Purchased newPurchased = new(); + PurchasedEntities.Purchased newPurchased; Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); @@ -2368,7 +2384,7 @@ public class APIService(IAuthService authService, IConfigService configService, string loopbody = await loopresponse.Content.ReadAsStringAsync(); PurchasedDtos.PurchasedDto? newPurchasedDto = JsonConvert.DeserializeObject(loopbody, - m_JsonSerializerSettings); + s_mJsonSerializerSettings); newPurchased = PurchasedMapper.FromDto(newPurchasedDto); } @@ -2378,16 +2394,17 @@ public class APIService(IAuthService authService, IConfigService configService, break; } - getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit); } } - if (purchased.List != null && purchased.List.Count > 0) + if (purchased.List.Count > 0) { foreach (PurchasedEntities.ListItem purchase in purchased.List.OrderByDescending(p => p.PostedAt ?? p.CreatedAt)) { - if (purchase.FromUser != null) + // purchase.FromUser.Id is not nullable, so the default value is 0 + if (purchase.FromUser.Id != 0) { if (users.Values.Contains(purchase.FromUser.Id)) { @@ -2412,9 +2429,9 @@ public class APIService(IAuthService authService, IConfigService configService, } else { - JObject user = await GetUserInfoById($"/users/list?x[]={purchase.FromUser.Id}"); + JObject? user = await GetUserInfoById($"/users/list?x[]={purchase.FromUser.Id}"); - if (user is null) + if (user == null) { if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) { @@ -2447,7 +2464,8 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - else if (purchase.Author != null) + // purchase.Author is not nullable, so we check against the Author's Id (default value 0) + else if (purchase.Author.Id != 0) { if (users.Values.Contains(purchase.Author.Id)) { @@ -2471,7 +2489,7 @@ public class APIService(IAuthService authService, IConfigService configService, } else { - JObject user = await GetUserInfoById($"/users/list?x[]={purchase.Author.Id}"); + JObject? user = await GetUserInfoById($"/users/list?x[]={purchase.Author.Id}"); if (user is null) { @@ -2536,11 +2554,10 @@ public class APIService(IAuthService authService, IConfigService configService, { Dictionary> userPurchases = new(); List purchasedTabCollections = []; - PurchasedEntities.Purchased purchased = new(); - int post_limit = 50; + const int postLimit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, + { "limit", postLimit.ToString() }, { "order", "publish_date_desc" }, { "format", "infinite" }, { "skip_users", "all" } @@ -2548,9 +2565,9 @@ public class APIService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); PurchasedDtos.PurchasedDto? purchasedDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - purchased = PurchasedMapper.FromDto(purchasedDto); - if (purchased != null && purchased.HasMore) + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + PurchasedEntities.Purchased purchased = PurchasedMapper.FromDto(purchasedDto); + if (purchased.HasMore) { getParams["offset"] = purchased.List.Count.ToString(); while (true) @@ -2574,7 +2591,7 @@ public class APIService(IAuthService authService, IConfigService configService, string loopbody = await loopresponse.Content.ReadAsStringAsync(); PurchasedDtos.PurchasedDto? newPurchasedDto = JsonConvert.DeserializeObject(loopbody, - m_JsonSerializerSettings); + s_mJsonSerializerSettings); newPurchased = PurchasedMapper.FromDto(newPurchasedDto); } @@ -2584,7 +2601,7 @@ public class APIService(IAuthService authService, IConfigService configService, break; } - getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit); } } @@ -2617,7 +2634,7 @@ public class APIService(IAuthService authService, IConfigService configService, foreach (KeyValuePair> user in userPurchases) { PurchasedEntities.PurchasedTabCollection purchasedTabCollection = new(); - JObject userObject = await GetUserInfoById($"/users/list?x[]={user.Key}"); + JObject? userObject = await GetUserInfoById($"/users/list?x[]={user.Key}"); purchasedTabCollection.UserId = user.Key; purchasedTabCollection.Username = userObject is not null && @@ -3030,7 +3047,12 @@ public class APIService(IAuthService authService, IConfigService configService, { try { - string pssh = null; + if (authService.CurrentAuth == null) + { + throw new Exception("No current authentication available"); + } + + string? pssh; HttpClient client = new(); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); @@ -3115,92 +3137,7 @@ public class APIService(IAuthService authService, IConfigService configService, return DateTime.Now; } - public async Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, - string pssh) - { - Log.Debug("Calling GetDecryptionKey"); - - int attempt = 0; - - try - { - string dcValue = ""; - HttpClient client = new(); - - CDRMProjectRequest cdrmProjectRequest = new() - { - Pssh = pssh, - LicenseUrl = licenceURL, - Headers = JsonConvert.SerializeObject(drmHeaders), - Cookies = "", - Data = "" - }; - - string json = JsonConvert.SerializeObject(cdrmProjectRequest); - - Log.Debug($"Posting to CDRM Project: {json}"); - - while (attempt < MaxAttempts) - { - attempt++; - - HttpRequestMessage request = new(HttpMethod.Post, "https://cdrm-project.com/api/decrypt") - { - Content = new StringContent(json, Encoding.UTF8, "application/json") - }; - - using HttpResponseMessage response = await client.SendAsync(request); - - Log.Debug($"CDRM Project Response (Attempt {attempt}): {response.Content.ReadAsStringAsync().Result}"); - - response.EnsureSuccessStatusCode(); - string body = await response.Content.ReadAsStringAsync(); - JsonDocument doc = JsonDocument.Parse(body); - - if (doc.RootElement.TryGetProperty("status", out JsonElement status)) - { - if (status.ToString().Trim().Equals("success", StringComparison.OrdinalIgnoreCase)) - { - dcValue = doc.RootElement.GetProperty("message").GetString().Trim(); - return dcValue; - } - - Log.Debug($"CDRM response status not successful. Retrying... Attempt {attempt} of {MaxAttempts}"); - if (attempt < MaxAttempts) - { - await Task.Delay(DelayBetweenAttempts); - } - } - else - { - Log.Debug($"Status not in CDRM response. Retrying... Attempt {attempt} of {MaxAttempts}"); - if (attempt < MaxAttempts) - { - await Task.Delay(DelayBetweenAttempts); - } - } - } - - throw new Exception("Maximum retry attempts reached. Unable to get a valid decryption key."); - } - 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); - } - } - - return null; - } - - public async Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, + public async Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceUrl, string pssh) { Log.Debug("Calling GetDecryptionOFDL"); @@ -3212,12 +3149,12 @@ public class APIService(IAuthService authService, IConfigService configService, OFDLRequest ofdlRequest = new() { - Pssh = pssh, LicenseUrl = licenceURL, Headers = JsonConvert.SerializeObject(drmHeaders) + Pssh = pssh, LicenseUrl = licenceUrl, Headers = JsonConvert.SerializeObject(drmHeaders) }; string json = JsonConvert.SerializeObject(ofdlRequest); - Log.Debug($"Posting to ofdl.tools: {json}"); + Log.Debug("Posting to ofdl.tools: {Json}", json); while (attempt < MaxAttempts) { @@ -3263,28 +3200,34 @@ public class APIService(IAuthService authService, IConfigService configService, return null; } - public async Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, string pssh) + public async Task? GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, + string pssh) { Log.Debug("Calling GetDecryptionKeyCDM"); try { - byte[] resp1 = await PostData(licenceURL, drmHeaders, new byte[] { 0x08, 0x04 }); + byte[] resp1 = await PostData(licenceURL, drmHeaders, [0x08, 0x04]); string certDataB64 = Convert.ToBase64String(resp1); CDMApi cdm = new(); - byte[] challenge = cdm.GetChallenge(pssh, certDataB64); + byte[]? challenge = cdm.GetChallenge(pssh, certDataB64); + if (challenge == null) + { + throw new Exception("Failed to get challenge from CDM"); + } + byte[] resp2 = await PostData(licenceURL, drmHeaders, challenge); string licenseB64 = Convert.ToBase64String(resp2); - Log.Debug($"resp1: {resp1}"); - Log.Debug($"certDataB64: {certDataB64}"); - Log.Debug($"challenge: {challenge}"); - Log.Debug($"resp2: {resp2}"); - Log.Debug($"licenseB64: {licenseB64}"); + Log.Debug("resp1: {Resp1}", resp1); + Log.Debug("certDataB64: {CertDataB64}", certDataB64); + Log.Debug("challenge: {Challenge}", challenge); + Log.Debug("resp2: {Resp2}", resp2); + Log.Debug("licenseB64: {LicenseB64}", licenseB64); cdm.ProvideLicense(licenseB64); List keys = cdm.GetKeys(); if (keys.Count > 0) { - Log.Debug($"GetDecryptionKeyCDM Key: {keys[0]}"); + Log.Debug("GetDecryptionKeyCDM Key: {ContentKey}", keys[0]); return keys[0].ToString(); } } @@ -3322,7 +3265,7 @@ public class APIService(IAuthService authService, IConfigService configService, } - private async Task BuildHttpRequestMessage(Dictionary getParams, + private Task BuildHttpRequestMessage(Dictionary getParams, string endpoint) { Log.Debug("Calling BuildHttpRequestMessage"); @@ -3340,7 +3283,7 @@ public class APIService(IAuthService authService, IConfigService configService, request.Headers.Add(keyValuePair.Key, keyValuePair.Value); } - return request; + return Task.FromResult(request); } private static double ConvertToUnixTimestampWithMicrosecondPrecision(DateTime date) @@ -3420,7 +3363,7 @@ public class APIService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); SubscriptionsDtos.SubscriptionsDto? subscriptionsDto = - JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); subscriptions = SubscriptionsMapper.FromDto(subscriptionsDto); if (subscriptions.HasMore) { @@ -3435,7 +3378,7 @@ public class APIService(IAuthService authService, IConfigService configService, { SubscriptionsDtos.SubscriptionsDto? newSubscriptionsDto = JsonConvert.DeserializeObject(loopbody, - m_JsonSerializerSettings); + s_mJsonSerializerSettings); newSubscriptions = SubscriptionsMapper.FromDto(newSubscriptionsDto); } else diff --git a/OF DL/Services/AuthService.cs b/OF DL/Services/AuthService.cs index 3f9f654..2755bfb 100644 --- a/OF DL/Services/AuthService.cs +++ b/OF DL/Services/AuthService.cs @@ -1,12 +1,15 @@ +using System.Text.RegularExpressions; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using OF_DL.Models; using PuppeteerSharp; using PuppeteerSharp.BrowserData; using Serilog; +using UserEntities = OF_DL.Models.Entities.Users; namespace OF_DL.Services; -public class AuthService : IAuthService +public class AuthService(IServiceProvider serviceProvider) : IAuthService { private const int LoginTimeout = 600000; // 10 minutes private const int FeedLoadTimeout = 60000; // 1 minute @@ -126,6 +129,53 @@ public class AuthService : IAuthService private async Task GetBcToken(IPage page) => await page.EvaluateExpressionAsync("window.localStorage.getItem('bcTokenSha') || ''"); + public void ValidateCookieString() + { + if (CurrentAuth == null) + { + return; + } + + string pattern = @"(auth_id=\d+)|(sess=[^;]+)"; + MatchCollection matches = Regex.Matches(CurrentAuth.Cookie, pattern); + + string output = string.Join("; ", matches); + + if (!output.EndsWith(";")) + { + output += ";"; + } + + if (CurrentAuth.Cookie.Trim() != output.Trim()) + { + CurrentAuth.Cookie = output; + string newAuthString = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented); + File.WriteAllText("auth.json", newAuthString); + } + } + + public async Task ValidateAuthAsync() + { + // Resolve IAPIService lazily to avoid circular dependency + IAPIService apiService = serviceProvider.GetRequiredService(); + return await apiService.GetUserInfo("/users/me"); + } + + public void Logout() + { + if (Directory.Exists("chrome-data")) + { + Log.Information("Deleting chrome-data folder"); + Directory.Delete("chrome-data", true); + } + + if (File.Exists("auth.json")) + { + Log.Information("Deleting auth.json"); + File.Delete("auth.json"); + } + } + private async Task GetAuthFromBrowser(bool isDocker = false) { try @@ -184,6 +234,7 @@ public class AuthService : IAuthService } catch (Exception e) { + Log.Error(e, "Error getting bcToken"); throw new Exception("Error getting bcToken"); } diff --git a/OF DL/Services/ConfigService.cs b/OF DL/Services/ConfigService.cs index d22c617..9c43b0a 100644 --- a/OF DL/Services/ConfigService.cs +++ b/OF DL/Services/ConfigService.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Text; using Akka.Configuration; using Akka.Configuration.Hocon; @@ -228,13 +229,12 @@ public class ConfigService(ILoggingService loggingService) : IConfigService Akka.Configuration.Config? creatorConfigsSection = hoconConfig.GetConfig("CreatorConfigs"); if (creatorConfigsSection != null) { - foreach (KeyValuePair key in creatorConfigsSection.AsEnumerable()) + foreach ((string? creatorKey, _) in creatorConfigsSection.AsEnumerable()) { - string creatorKey = key.Key; Akka.Configuration.Config? creatorHocon = creatorConfigsSection.GetConfig(creatorKey); if (!CurrentConfig.CreatorConfigs.ContainsKey(creatorKey) && creatorHocon != null) { - CurrentConfig.CreatorConfigs.Add(key.Key, + CurrentConfig.CreatorConfigs.Add(creatorKey, new CreatorConfig { PaidPostFileNameFormat = creatorHocon.GetString("PaidPostFileNameFormat"), @@ -243,14 +243,14 @@ public class ConfigService(ILoggingService loggingService) : IConfigService MessageFileNameFormat = creatorHocon.GetString("MessageFileNameFormat") }); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidPostFileNameFormat, - $"{key.Key}.PaidPostFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PostFileNameFormat, - $"{key.Key}.PostFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidMessageFileNameFormat, - $"{key.Key}.PaidMessageFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].MessageFileNameFormat, - $"{key.Key}.MessageFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[creatorKey].PaidPostFileNameFormat, + $"{creatorKey}.PaidPostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[creatorKey].PostFileNameFormat, + $"{creatorKey}.PostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[creatorKey].PaidMessageFileNameFormat, + $"{creatorKey}.PaidMessageFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[creatorKey].MessageFileNameFormat, + $"{creatorKey}.MessageFileNameFormat"); } } } @@ -402,6 +402,50 @@ public class ConfigService(ILoggingService loggingService) : IConfigService } } + public List<(string Name, bool Value)> GetToggleableProperties() + { + List<(string Name, bool Value)> result = []; + foreach (PropertyInfo propInfo in typeof(Config).GetProperties()) + { + ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute(); + if (attr != null) + { + result.Add((propInfo.Name, (bool)propInfo.GetValue(CurrentConfig)!)); + } + } + + return result; + } + + public bool ApplyToggleableSelections(List selectedNames) + { + bool configChanged = false; + Config newConfig = new(); + + foreach (PropertyInfo propInfo in typeof(Config).GetProperties()) + { + ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute(); + if (attr != null) + { + bool newValue = selectedNames.Contains(propInfo.Name); + bool oldValue = (bool)propInfo.GetValue(CurrentConfig)!; + propInfo.SetValue(newConfig, newValue); + + if (newValue != oldValue) + { + configChanged = true; + } + } + else + { + propInfo.SetValue(newConfig, propInfo.GetValue(CurrentConfig)); + } + } + + UpdateConfig(newConfig); + return configChanged; + } + private VideoResolution ParseVideoResolution(string value) { if (value.Equals("source", StringComparison.OrdinalIgnoreCase)) diff --git a/OF DL/Services/DownloadOrchestrationService.cs b/OF DL/Services/DownloadOrchestrationService.cs new file mode 100644 index 0000000..cc8902c --- /dev/null +++ b/OF DL/Services/DownloadOrchestrationService.cs @@ -0,0 +1,550 @@ +using Newtonsoft.Json.Linq; +using OF_DL.Enumerations; +using OF_DL.Models; +using Serilog; +using PostEntities = OF_DL.Models.Entities.Posts; +using PurchasedEntities = OF_DL.Models.Entities.Purchased; +using UserEntities = OF_DL.Models.Entities.Users; + +namespace OF_DL.Services; + +public class DownloadOrchestrationService( + IAPIService apiService, + IConfigService configService, + IDownloadService downloadService, + IDBService dbService) : IDownloadOrchestrationService +{ + public List PaidPostIds { get; } = new(); + + public async Task GetAvailableUsersAsync() + { + UserListResult result = new(); + Config config = configService.CurrentConfig; + + Dictionary? activeSubs = + await apiService.GetActiveSubscriptions("/subscriptions/subscribes", + config.IncludeRestrictedSubscriptions); + + if (activeSubs != null) + { + Log.Debug("Subscriptions: "); + foreach (KeyValuePair activeSub in activeSubs) + { + if (!result.Users.ContainsKey(activeSub.Key)) + { + result.Users.Add(activeSub.Key, activeSub.Value); + Log.Debug($"Name: {activeSub.Key} ID: {activeSub.Value}"); + } + } + } + else + { + Log.Error("Couldn't get active subscriptions. Received null response."); + } + + + if (config.IncludeExpiredSubscriptions) + { + Log.Debug("Inactive Subscriptions: "); + Dictionary? expiredSubs = + await apiService.GetExpiredSubscriptions("/subscriptions/subscribes", + config.IncludeRestrictedSubscriptions); + + if (expiredSubs != null) + { + foreach (KeyValuePair expiredSub in expiredSubs.Where(expiredSub => + !result.Users.ContainsKey(expiredSub.Key))) + { + result.Users.Add(expiredSub.Key, expiredSub.Value); + Log.Debug("Name: {ExpiredSubKey} ID: {ExpiredSubValue}", expiredSub.Key, expiredSub.Value); + } + } + else + { + Log.Error("Couldn't get expired subscriptions. Received null response."); + } + } + + result.Lists = await apiService.GetLists("/lists") ?? new Dictionary(); + + // Remove users from the list if they are in the ignored list + if (!string.IsNullOrEmpty(config.IgnoredUsersListName)) + { + if (!result.Lists.TryGetValue(config.IgnoredUsersListName, out long ignoredUsersListId)) + { + result.IgnoredListError = $"Ignored users list '{config.IgnoredUsersListName}' not found"; + Log.Error(result.IgnoredListError); + } + else + { + List ignoredUsernames = + await apiService.GetListUsers($"/lists/{ignoredUsersListId}/users") ?? []; + result.Users = result.Users.Where(x => !ignoredUsernames.Contains(x.Key)) + .ToDictionary(x => x.Key, x => x.Value); + } + } + + await dbService.CreateUsersDB(result.Users); + return result; + } + + public async Task> GetUsersForListAsync( + string listName, Dictionary allUsers, Dictionary lists) + { + long listId = lists[listName]; + List listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; + return allUsers.Where(x => listUsernames.Contains(x.Key)).Distinct() + .ToDictionary(x => x.Key, x => x.Value); + } + + public string ResolveDownloadPath(string username) => + !string.IsNullOrEmpty(configService.CurrentConfig.DownloadPath) + ? Path.Combine(configService.CurrentConfig.DownloadPath, username) + : $"__user_data__/sites/OnlyFans/{username}"; + + public async Task PrepareUserFolderAsync(string username, long userId, string path) + { + await dbService.CheckUsername(new KeyValuePair(username, userId), path); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + Log.Debug($"Created folder for {username}"); + } + + await dbService.CreateDB(path); + } + + public async Task DownloadCreatorContentAsync( + string username, long userId, string path, + Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + IDownloadEventHandler eventHandler) + { + Config config = configService.CurrentConfig; + CreatorDownloadResult counts = new(); + + eventHandler.OnUserStarting(username); + Log.Debug($"Scraping Data for {username}"); + + await PrepareUserFolderAsync(username, userId, path); + + if (config.DownloadAvatarHeaderPhoto) + { + UserEntities.User? userInfo = await apiService.GetUserInfo($"/users/{username}"); + if (userInfo != null) + { + await downloadService.DownloadAvatarHeader(userInfo.Avatar, userInfo.Header, path, username); + } + } + + if (config.DownloadPaidPosts) + { + counts.PaidPostCount = await DownloadContentTypeAsync("Paid Posts", + async statusReporter => + await apiService.GetPaidPosts("/posts/paid/post", path, username, PaidPostIds, statusReporter), + posts => posts?.PaidPosts?.Count ?? 0, + posts => posts?.PaidPostObjects?.Count ?? 0, + posts => posts?.PaidPosts?.Values?.ToList(), + async (posts, reporter) => await downloadService.DownloadPaidPosts(username, userId, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, posts, reporter), + eventHandler); + } + + if (config.DownloadPosts) + { + eventHandler.OnMessage( + "Getting Posts (this may take a long time, depending on the number of Posts the creator has)"); + Log.Debug($"Calling DownloadFreePosts - {username}"); + + counts.PostCount = await DownloadContentTypeAsync("Posts", + async statusReporter => + await apiService.GetPosts($"/users/{userId}/posts", path, PaidPostIds, statusReporter), + posts => posts?.Posts?.Count ?? 0, + posts => posts?.PostObjects?.Count ?? 0, + posts => posts?.Posts?.Values?.ToList(), + async (posts, reporter) => await downloadService.DownloadFreePosts(username, userId, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, posts, reporter), + eventHandler); + } + + if (config.DownloadArchived) + { + counts.ArchivedCount = await DownloadContentTypeAsync("Archived Posts", + async statusReporter => + await apiService.GetArchived($"/users/{userId}/posts", path, statusReporter), + archived => archived?.ArchivedPosts?.Count ?? 0, + archived => archived?.ArchivedPostObjects?.Count ?? 0, + archived => archived?.ArchivedPosts?.Values?.ToList(), + async (archived, reporter) => await downloadService.DownloadArchived(username, userId, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, archived, reporter), + eventHandler); + } + + if (config.DownloadStreams) + { + counts.StreamsCount = await DownloadContentTypeAsync("Streams", + async statusReporter => + await apiService.GetStreams($"/users/{userId}/posts/streams", path, PaidPostIds, statusReporter), + streams => streams?.Streams?.Count ?? 0, + streams => streams?.StreamObjects?.Count ?? 0, + streams => streams?.Streams?.Values?.ToList(), + async (streams, reporter) => await downloadService.DownloadStreams(username, userId, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, streams, reporter), + eventHandler); + } + + if (config.DownloadStories) + { + eventHandler.OnMessage("Getting Stories"); + Dictionary? tempStories = await apiService.GetMedia(MediaType.Stories, + $"/users/{userId}/stories", null, path, PaidPostIds); + + if (tempStories != null && tempStories.Count > 0) + { + eventHandler.OnContentFound("Stories", tempStories.Count, tempStories.Count); + + long totalSize = config.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize(tempStories.Values.ToList()) + : tempStories.Count; + + DownloadResult result = await eventHandler.WithProgressAsync( + $"Downloading {tempStories.Count} Stories", totalSize, config.ShowScrapeSize, + async reporter => await downloadService.DownloadStories(username, userId, path, + PaidPostIds.ToHashSet(), reporter)); + + eventHandler.OnDownloadComplete("Stories", result); + counts.StoriesCount = result.TotalCount; + } + else + { + eventHandler.OnNoContentFound("Stories"); + } + } + + if (config.DownloadHighlights) + { + eventHandler.OnMessage("Getting Highlights"); + Dictionary? tempHighlights = await apiService.GetMedia(MediaType.Highlights, + $"/users/{userId}/stories/highlights", null, path, PaidPostIds); + + if (tempHighlights != null && tempHighlights.Count > 0) + { + eventHandler.OnContentFound("Highlights", tempHighlights.Count, tempHighlights.Count); + + long totalSize = config.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize(tempHighlights.Values.ToList()) + : tempHighlights.Count; + + DownloadResult result = await eventHandler.WithProgressAsync( + $"Downloading {tempHighlights.Count} Highlights", totalSize, config.ShowScrapeSize, + async reporter => await downloadService.DownloadHighlights(username, userId, path, + PaidPostIds.ToHashSet(), reporter)); + + eventHandler.OnDownloadComplete("Highlights", result); + counts.HighlightsCount = result.TotalCount; + } + else + { + eventHandler.OnNoContentFound("Highlights"); + } + } + + if (config.DownloadMessages) + { + counts.MessagesCount = await DownloadContentTypeAsync("Messages", + async statusReporter => + await apiService.GetMessages($"/chats/{userId}/messages", path, statusReporter), + messages => messages.Messages?.Count ?? 0, + messages => messages.MessageObjects?.Count ?? 0, + messages => messages?.Messages.Values.ToList(), + async (messages, reporter) => await downloadService.DownloadMessages(username, userId, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, messages, reporter), + eventHandler); + } + + if (config.DownloadPaidMessages) + { + counts.PaidMessagesCount = await DownloadContentTypeAsync("Paid Messages", + async statusReporter => + await apiService.GetPaidMessages("/posts/paid/chat", path, username, statusReporter), + paidMessages => paidMessages?.PaidMessages?.Count ?? 0, + paidMessages => paidMessages?.PaidMessageObjects?.Count ?? 0, + paidMessages => paidMessages?.PaidMessages?.Values?.ToList(), + async (paidMessages, reporter) => await downloadService.DownloadPaidMessages(username, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, paidMessages, reporter), + eventHandler); + } + + eventHandler.OnUserComplete(username, counts); + return counts; + } + + public async Task DownloadSinglePostAsync( + string username, long postId, string path, + Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + IDownloadEventHandler eventHandler) + { + Log.Debug($"Calling DownloadSinglePost - {postId}"); + eventHandler.OnMessage("Getting Post"); + + PostEntities.SinglePostCollection post = await apiService.GetPost($"/posts/{postId}", path); + if (post.SinglePosts.Count == 0) + { + eventHandler.OnMessage("Couldn't find post"); + Log.Debug("Couldn't find post"); + return; + } + + Config config = configService.CurrentConfig; + long totalSize = config.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize(post.SinglePosts.Values.ToList()) + : post.SinglePosts.Count; + + DownloadResult result = await eventHandler.WithProgressAsync( + "Downloading Post", totalSize, config.ShowScrapeSize, + async reporter => await downloadService.DownloadSinglePost(username, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, post, reporter)); + + if (result.NewDownloads > 0) + { + eventHandler.OnMessage($"Post {postId} downloaded"); + Log.Debug($"Post {postId} downloaded"); + } + else + { + eventHandler.OnMessage($"Post {postId} already downloaded"); + Log.Debug($"Post {postId} already downloaded"); + } + } + + public async Task DownloadPurchasedTabAsync( + Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + IDownloadEventHandler eventHandler) + { + Config config = configService.CurrentConfig; + + Dictionary purchasedTabUsers = + await apiService.GetPurchasedTabUsers("/posts/paid/all", users); + + eventHandler.OnMessage("Checking folders for Users in Purchased Tab"); + + foreach (KeyValuePair user in purchasedTabUsers) + { + string path = ResolveDownloadPath(user.Key); + Log.Debug($"Download path: {path}"); + + await dbService.CheckUsername(user, path); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + Log.Debug($"Created folder for {user.Key}"); + } + + await apiService.GetUserInfo($"/users/{user.Key}"); + await dbService.CreateDB(path); + } + + string basePath = !string.IsNullOrEmpty(config.DownloadPath) + ? config.DownloadPath + : "__user_data__/sites/OnlyFans/"; + + Log.Debug($"Download path: {basePath}"); + + List purchasedTabCollections = + await apiService.GetPurchasedTab("/posts/paid/all", basePath, users); + + foreach (PurchasedEntities.PurchasedTabCollection purchasedTabCollection in purchasedTabCollections) + { + eventHandler.OnUserStarting(purchasedTabCollection.Username); + string path = ResolveDownloadPath(purchasedTabCollection.Username); + Log.Debug($"Download path: {path}"); + + int paidPostCount = 0; + int paidMessagesCount = 0; + + // Download paid posts + if (purchasedTabCollection.PaidPosts?.PaidPosts?.Count > 0) + { + eventHandler.OnContentFound("Paid Posts", + purchasedTabCollection.PaidPosts.PaidPosts.Count, + purchasedTabCollection.PaidPosts.PaidPostObjects.Count); + + long totalSize = config.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize( + purchasedTabCollection.PaidPosts.PaidPosts.Values.ToList()) + : purchasedTabCollection.PaidPosts.PaidPosts.Count; + + DownloadResult postResult = await eventHandler.WithProgressAsync( + $"Downloading {purchasedTabCollection.PaidPosts.PaidPosts.Count} Paid Posts", + totalSize, config.ShowScrapeSize, + async reporter => await downloadService.DownloadPaidPostsPurchasedTab( + purchasedTabCollection.Username, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, + purchasedTabCollection.PaidPosts, reporter)); + + eventHandler.OnDownloadComplete("Paid Posts", postResult); + paidPostCount = postResult.TotalCount; + } + else + { + eventHandler.OnNoContentFound("Paid Posts"); + } + + // Download paid messages + if (purchasedTabCollection.PaidMessages?.PaidMessages?.Count > 0) + { + eventHandler.OnContentFound("Paid Messages", + purchasedTabCollection.PaidMessages.PaidMessages.Count, + purchasedTabCollection.PaidMessages.PaidMessageObjects.Count); + + long totalSize = config.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize( + purchasedTabCollection.PaidMessages.PaidMessages.Values.ToList()) + : purchasedTabCollection.PaidMessages.PaidMessages.Count; + + DownloadResult msgResult = await eventHandler.WithProgressAsync( + $"Downloading {purchasedTabCollection.PaidMessages.PaidMessages.Count} Paid Messages", + totalSize, config.ShowScrapeSize, + async reporter => await downloadService.DownloadPaidMessagesPurchasedTab( + purchasedTabCollection.Username, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, + purchasedTabCollection.PaidMessages, reporter)); + + eventHandler.OnDownloadComplete("Paid Messages", msgResult); + paidMessagesCount = msgResult.TotalCount; + } + else + { + eventHandler.OnNoContentFound("Paid Messages"); + } + + eventHandler.OnPurchasedTabUserComplete(purchasedTabCollection.Username, paidPostCount, paidMessagesCount); + } + } + + public async Task DownloadSinglePaidMessageAsync( + string username, long messageId, string path, + Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + IDownloadEventHandler eventHandler) + { + Log.Debug($"Calling DownloadSinglePaidMessage - {username}"); + eventHandler.OnMessage("Getting Paid Message"); + + PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection = + await apiService.GetPaidMessage($"/messages/{messageId}", path); + + if (singlePaidMessageCollection.SingleMessages.Count == 0) + { + eventHandler.OnNoContentFound("Paid Messages"); + return; + } + + Config config = configService.CurrentConfig; + + // Handle preview messages + if (singlePaidMessageCollection.PreviewSingleMessages.Count > 0) + { + eventHandler.OnContentFound("Preview Paid Messages", + singlePaidMessageCollection.PreviewSingleMessages.Count, + singlePaidMessageCollection.SingleMessageObjects.Count); + + long previewSize = config.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize( + singlePaidMessageCollection.PreviewSingleMessages.Values.ToList()) + : singlePaidMessageCollection.PreviewSingleMessages.Count; + + DownloadResult previewResult = await eventHandler.WithProgressAsync( + $"Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Paid Messages", + previewSize, config.ShowScrapeSize, + async reporter => await downloadService.DownloadSinglePaidMessage(username, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, singlePaidMessageCollection, reporter)); + + eventHandler.OnDownloadComplete("Paid Messages", previewResult); + } + else if (singlePaidMessageCollection.SingleMessages.Count > 0) + { + // Only actual paid messages, no preview + eventHandler.OnContentFound("Paid Messages", + singlePaidMessageCollection.SingleMessages.Count, + singlePaidMessageCollection.SingleMessageObjects.Count); + + long totalSize = config.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize( + singlePaidMessageCollection.SingleMessages.Values.ToList()) + : singlePaidMessageCollection.SingleMessages.Count; + + DownloadResult result = await eventHandler.WithProgressAsync( + $"Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages", + totalSize, config.ShowScrapeSize, + async reporter => await downloadService.DownloadSinglePaidMessage(username, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, singlePaidMessageCollection, reporter)); + + eventHandler.OnDownloadComplete("Paid Messages", result); + } + else + { + eventHandler.OnNoContentFound("Paid Messages"); + } + } + + public async Task ResolveUsernameAsync(long userId) + { + JObject? user = await apiService.GetUserInfoById($"/users/list?x[]={userId}"); + if (user == null) + { + return $"Deleted User - {userId}"; + } + + string? username = user[userId.ToString()]?["username"]?.ToString(); + return !string.IsNullOrEmpty(username) ? username : $"Deleted User - {userId}"; + } + + /// + /// Generic helper for the common pattern: fetch with status -> check count -> download with progress. + /// + private async Task DownloadContentTypeAsync( + string contentType, + Func> fetchData, + Func getMediaCount, + Func getObjectCount, + Func?> getUrls, + Func> downloadData, + IDownloadEventHandler eventHandler) + { + T data = await eventHandler.WithStatusAsync($"Getting {contentType}", + async statusReporter => await fetchData(statusReporter)); + + int mediaCount = getMediaCount(data); + if (mediaCount <= 0) + { + eventHandler.OnNoContentFound(contentType); + Log.Debug($"Found 0 {contentType}"); + return 0; + } + + int objectCount = getObjectCount(data); + eventHandler.OnContentFound(contentType, mediaCount, objectCount); + Log.Debug($"Found {mediaCount} Media from {objectCount} {contentType}"); + + Config config = configService.CurrentConfig; + List? urls = getUrls(data); + long totalSize = config.ShowScrapeSize && urls != null + ? await downloadService.CalculateTotalFileSize(urls) + : mediaCount; + + DownloadResult result = await eventHandler.WithProgressAsync( + $"Downloading {mediaCount} {contentType}", totalSize, config.ShowScrapeSize, + async reporter => await downloadData(data, reporter)); + + eventHandler.OnDownloadComplete(contentType, result); + Log.Debug( + $"{contentType} Already Downloaded: {result.ExistingDownloads} New {contentType} Downloaded: {result.NewDownloads}"); + + return result.TotalCount; + } +} diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index eb2c698..b9cc5ac 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -292,6 +292,11 @@ public class DownloadService( private async Task GetVideoStreamIndexFromMpd(string mpdUrl, string policy, string signature, string kvp, VideoResolution resolution) { + if (authService.CurrentAuth == null) + { + return null; + } + HttpClient client = new(); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); request.Headers.Add("user-agent", authService.CurrentAuth.UserAgent); @@ -307,7 +312,7 @@ public class DownloadService( XNamespace cenc = "urn:mpeg:cenc:2013"; XElement? videoAdaptationSet = doc .Descendants(ns + "AdaptationSet") - .FirstOrDefault(e => (string)e.Attribute("mimeType") == "video/mp4"); + .FirstOrDefault(e => (string?)e.Attribute("mimeType") == "video/mp4"); if (videoAdaptationSet == null) { @@ -326,7 +331,7 @@ public class DownloadService( for (int i = 0; i < representations.Count; i++) { - if ((string)representations[i].Attribute("height") == targetHeight) + if ((string?)representations[i].Attribute("height") == targetHeight) { return i; // this is the index FFmpeg will use for `-map 0:v:{i}` } @@ -512,6 +517,21 @@ public class DownloadService( try { + if (authService.CurrentAuth == null) + { + throw new Exception("No authentication information available."); + } + + if (authService.CurrentAuth.Cookie == null) + { + throw new Exception("No authentication cookie available."); + } + + if (authService.CurrentAuth.UserAgent == null) + { + throw new Exception("No user agent available."); + } + Uri uri = new(url); if (uri.Host == "cdn3.onlyfans.com" && uri.LocalPath.Contains("/dash/files")) @@ -558,8 +578,6 @@ public class DownloadService( public static async Task GetDRMVideoLastModified(string url, Auth auth) { - Uri uri = new(url); - string[] messageUrlParsed = url.Split(','); string mpdURL = messageUrlParsed[0]; string policy = messageUrlParsed[1]; @@ -946,6 +964,21 @@ public class DownloadService( { try { + if (authService.CurrentAuth == null) + { + throw new Exception("No authentication information available."); + } + + if (authService.CurrentAuth.Cookie == null) + { + throw new Exception("No authentication cookie available."); + } + + if (authService.CurrentAuth.UserAgent == null) + { + throw new Exception("No user agent available."); + } + Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; @@ -1079,7 +1112,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadHighlights - {username}"); - Dictionary highlights = await apiService.GetMedia(MediaType.Highlights, + Dictionary? highlights = await apiService.GetMedia(MediaType.Highlights, $"/users/{userId}/stories/highlights", null, path, paidPostIds.ToList()); if (highlights == null || highlights.Count == 0) @@ -1133,7 +1166,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadStories - {username}"); - Dictionary stories = await apiService.GetMedia(MediaType.Stories, $"/users/{userId}/stories", + Dictionary? stories = await apiService.GetMedia(MediaType.Stories, $"/users/{userId}/stories", null, path, paidPostIds.ToList()); if (stories == null || stories.Count == 0) @@ -1454,7 +1487,7 @@ public class DownloadService( bool isNew; StreamEntities.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.Id == kvpEntry.Key); StreamEntities.ListItem? streamInfo = - streams.StreamObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); + streams.StreamObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; string streamPath = configService.CurrentConfig.FolderPerPost && streamInfo != null && @@ -1534,7 +1567,7 @@ public class DownloadService( bool isNew; PostEntities.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m?.Id == postKVP.Key); PostEntities.ListItem? postInfo = - posts.PostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); + posts.PostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && @@ -1665,4 +1698,363 @@ public class DownloadService( Success = true }; } + + public async Task DownloadPaidPostsPurchasedTab(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadPaidPostsPurchasedTab - {username}"); + + if (purchasedPosts == null || purchasedPosts.PaidPosts.Count == 0) + { + Log.Debug("Found 0 Paid Posts"); + return new DownloadResult { TotalCount = 0, MediaType = "Paid Posts", Success = true }; + } + + int oldCount = 0, newCount = 0; + + foreach (KeyValuePair purchasedPostKVP in purchasedPosts.PaidPosts) + { + bool isNew; + MessageEntities.Medium? mediaInfo = + purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); + PurchasedEntities.ListItem? postInfo = mediaInfo != null + ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true) + : null; + string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidPostFileNameFormat ?? ""; + string paidPostPath = configService.CurrentConfig.FolderPerPaidPost && postInfo != null && + postInfo?.Id is not null && postInfo?.PostedAt is not null + ? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Posts/Paid"; + + if (purchasedPostKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] parsed = purchasedPostKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = + await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], + parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) + { + continue; + } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, purchasedPostKVP.Key, + "Posts", progressReporter, paidPostPath + "/Videos", filenameFormat, + postInfo, mediaInfo, postInfo?.FromUser, users); + } + else + { + isNew = await DownloadMedia(purchasedPostKVP.Value, path, + purchasedPostKVP.Key, "Posts", progressReporter, + paidPostPath, filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users); + } + + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } + } + + Log.Debug($"Paid Posts Already Downloaded: {oldCount} New Paid Posts Downloaded: {newCount}"); + return new DownloadResult + { + TotalCount = purchasedPosts.PaidPosts.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Paid Posts", + Success = true + }; + } + + public async Task DownloadPaidMessagesPurchasedTab(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadPaidMessagesPurchasedTab - {username}"); + + if (paidMessageCollection == null || paidMessageCollection.PaidMessages.Count == 0) + { + Log.Debug("Found 0 Paid Messages"); + return new DownloadResult { TotalCount = 0, MediaType = "Paid Messages", Success = true }; + } + + int oldCount = 0, newCount = 0; + + foreach (KeyValuePair paidMessageKVP in paidMessageCollection.PaidMessages) + { + bool isNew; + MessageEntities.Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKVP.Key); + PurchasedEntities.ListItem? messageInfo = + paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? ""; + string paidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && + messageInfo?.Id is not null && messageInfo?.CreatedAt is not null + ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Messages/Paid"; + + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] parsed = paidMessageKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = + await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], + parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) + { + continue; + } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, + "Messages", progressReporter, paidMsgPath + "/Videos", filenameFormat, + messageInfo, mediaInfo, messageInfo?.FromUser, users); + } + else + { + isNew = await DownloadMedia(paidMessageKVP.Value, path, + paidMessageKVP.Key, "Messages", progressReporter, + paidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); + } + + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } + } + + Log.Debug($"Paid Messages Already Downloaded: {oldCount} New Paid Messages Downloaded: {newCount}"); + return new DownloadResult + { + TotalCount = paidMessageCollection.PaidMessages.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Paid Messages", + Success = true + }; + } + + public async Task DownloadSinglePost(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PostEntities.SinglePostCollection post, IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadSinglePost - {username}"); + + if (post == null || post.SinglePosts.Count == 0) + { + Log.Debug("Couldn't find post"); + return new DownloadResult { TotalCount = 0, MediaType = "Posts", Success = true }; + } + + int oldCount = 0, newCount = 0; + + foreach (KeyValuePair postKVP in post.SinglePosts) + { + PostEntities.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.Id == postKVP.Key); + PostEntities.SinglePost? postInfo = + post.SinglePostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); + string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PostFileNameFormat ?? ""; + string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && + postInfo?.Id is not null && postInfo?.PostedAt is not null + ? $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}" + : "/Posts/Free"; + + bool isNew; + if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] parsed = postKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = + await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], + parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) + { + continue; + } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKVP.Key, "Posts", + progressReporter, postPath + "/Videos", filenameFormat, + postInfo, mediaInfo, postInfo?.Author, users); + } + else + { + try + { + isNew = await DownloadMedia(postKVP.Value, path, + postKVP.Key, "Posts", progressReporter, + postPath, filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); + } + catch + { + Log.Warning("Media was null"); + continue; + } + } + + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } + } + + return new DownloadResult + { + TotalCount = post.SinglePosts.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Posts", + Success = true + }; + } + + public async Task DownloadSinglePaidMessage(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection, + IProgressReporter progressReporter) + { + Log.Debug($"Calling DownloadSinglePaidMessage - {username}"); + + if (singlePaidMessageCollection == null) + { + return new DownloadResult { TotalCount = 0, MediaType = "Paid Messages", Success = true }; + } + + int totalNew = 0, totalOld = 0; + + // Download preview messages + if (singlePaidMessageCollection.PreviewSingleMessages.Count > 0) + { + foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.PreviewSingleMessages) + { + MessageEntities.Medium? mediaInfo = + singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => + m.Id == paidMessageKVP.Key); + MessageEntities.SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? ""; + string previewMsgPath = configService.CurrentConfig.FolderPerMessage && messageInfo != null && + messageInfo?.Id is not null && messageInfo?.CreatedAt is not null + ? $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Messages/Free"; + + bool isNew; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] parsed = paidMessageKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = + await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], + parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) + { + continue; + } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, + "Messages", progressReporter, previewMsgPath + "/Videos", filenameFormat, + messageInfo, mediaInfo, messageInfo?.FromUser, users); + } + else + { + isNew = await DownloadMedia(paidMessageKVP.Value, path, + paidMessageKVP.Key, "Messages", progressReporter, + previewMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); + } + + if (isNew) + { + totalNew++; + } + else + { + totalOld++; + } + } + } + + // Download actual paid messages + if (singlePaidMessageCollection.SingleMessages.Count > 0) + { + foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.SingleMessages) + { + MessageEntities.Medium? mediaInfo = + singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => + m.Id == paidMessageKVP.Key); + MessageEntities.SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? ""; + string singlePaidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && + messageInfo != null && messageInfo?.Id is not null && + messageInfo?.CreatedAt is not null + ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" + : "/Messages/Paid"; + + bool isNew; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] parsed = paidMessageKVP.Value.Split(','); + (string decryptionKey, DateTime lastModified)? drmInfo = + await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], + parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); + if (drmInfo == null) + { + continue; + } + + isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, + "Messages", progressReporter, singlePaidMsgPath + "/Videos", filenameFormat, + messageInfo, mediaInfo, messageInfo?.FromUser, users); + } + else + { + isNew = await DownloadMedia(paidMessageKVP.Value, path, + paidMessageKVP.Key, "Messages", progressReporter, + singlePaidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); + } + + if (isNew) + { + totalNew++; + } + else + { + totalOld++; + } + } + } + + int totalCount = singlePaidMessageCollection.PreviewSingleMessages.Count + + singlePaidMessageCollection.SingleMessages.Count; + Log.Debug($"Paid Messages Already Downloaded: {totalOld} New Paid Messages Downloaded: {totalNew}"); + return new DownloadResult + { + TotalCount = totalCount, + NewDownloads = totalNew, + ExistingDownloads = totalOld, + MediaType = "Paid Messages", + Success = true + }; + } } diff --git a/OF DL/Services/FileNameService.cs b/OF DL/Services/FileNameService.cs index 4643637..cd8732e 100644 --- a/OF DL/Services/FileNameService.cs +++ b/OF DL/Services/FileNameService.cs @@ -115,7 +115,7 @@ public class FileNameService(IAuthService authService) : IFileNameService else { object? nestedPropertyValue = GetNestedPropertyValue(author, "Id"); - if (nestedPropertyValue != null) + if (nestedPropertyValue != null && users != null) { values.Add(propertyName, users.FirstOrDefault(u => u.Value == Convert.ToInt32(nestedPropertyValue.ToString())).Key); @@ -132,11 +132,11 @@ public class FileNameService(IAuthService authService) : IFileNameService if (propertyValue != null) { HtmlDocument pageDoc = new(); - pageDoc.LoadHtml(propertyValue.ToString()); + pageDoc.LoadHtml(propertyValue.ToString() ?? ""); string str = pageDoc.DocumentNode.InnerText; - if (str.Length > 100) // todo: add length limit to config + if (str.Length > 100) // TODO: add length limit to config { - str = str.Substring(0, 100); + str = str[..100]; } values.Add(propertyName, str); diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index fe55d1f..9fa9349 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -6,13 +6,11 @@ using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; using StreamEntities = OF_DL.Models.Entities.Streams; using UserEntities = OF_DL.Models.Entities.Users; -using Spectre.Console; namespace OF_DL.Services; public interface IAPIService { - Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, string pssh); Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, string pssh); Task GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp); Task GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp); @@ -24,21 +22,23 @@ public interface IAPIService Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, - StatusContext ctx); + IStatusReporter statusReporter); Task GetPosts(string endpoint, string folder, List paid_post_ids, - StatusContext ctx); + IStatusReporter statusReporter); Task GetPost(string endpoint, string folder); Task GetStreams(string endpoint, string folder, List paid_post_ids, - StatusContext ctx); + IStatusReporter statusReporter); - Task GetArchived(string endpoint, string folder, StatusContext ctx); - Task GetMessages(string endpoint, string folder, StatusContext ctx); + Task GetArchived(string endpoint, string folder, + IStatusReporter statusReporter); + + Task GetMessages(string endpoint, string folder, IStatusReporter statusReporter); Task GetPaidMessages(string endpoint, string folder, string username, - StatusContext ctx); + IStatusReporter statusReporter); Task GetPaidMessage(string endpoint, string folder); Task> GetPurchasedTabUsers(string endpoint, Dictionary users); diff --git a/OF DL/Services/IAuthService.cs b/OF DL/Services/IAuthService.cs index 485cfdc..c990bfd 100644 --- a/OF DL/Services/IAuthService.cs +++ b/OF DL/Services/IAuthService.cs @@ -1,11 +1,30 @@ using OF_DL.Models; +using UserEntities = OF_DL.Models.Entities.Users; namespace OF_DL.Services; public interface IAuthService { Auth? CurrentAuth { get; set; } + Task LoadFromFileAsync(string filePath = "auth.json"); + Task LoadFromBrowserAsync(); + Task SaveToFileAsync(string filePath = "auth.json"); + + /// + /// Cleans up the cookie string to only contain auth_id and sess cookies. + /// + void ValidateCookieString(); + + /// + /// Validates auth by calling the API and returns the user info if valid. + /// + Task ValidateAuthAsync(); + + /// + /// Logs out by deleting chrome-data and auth.json. + /// + void Logout(); } diff --git a/OF DL/Services/IConfigService.cs b/OF DL/Services/IConfigService.cs index abb535b..6a31ba3 100644 --- a/OF DL/Services/IConfigService.cs +++ b/OF DL/Services/IConfigService.cs @@ -5,8 +5,22 @@ namespace OF_DL.Services; public interface IConfigService { Config CurrentConfig { get; } + bool IsCliNonInteractive { get; } + Task LoadConfigurationAsync(string[] args); + Task SaveConfigurationAsync(string filePath = "config.conf"); + void UpdateConfig(Config newConfig); + + /// + /// Returns property names and current values for toggleable config properties. + /// + List<(string Name, bool Value)> GetToggleableProperties(); + + /// + /// Applies selected toggleable properties. Returns true if any changed. + /// + bool ApplyToggleableSelections(List selectedNames); } diff --git a/OF DL/Services/IDownloadEventHandler.cs b/OF DL/Services/IDownloadEventHandler.cs new file mode 100644 index 0000000..d40788c --- /dev/null +++ b/OF DL/Services/IDownloadEventHandler.cs @@ -0,0 +1,63 @@ +using OF_DL.Models; + +namespace OF_DL.Services; + +/// +/// UI callback contract for download orchestration. Implementations handle +/// status display, progress bars, and notifications in a UI-framework-specific way. +/// +public interface IDownloadEventHandler +{ + /// + /// Wraps work in a status indicator (spinner) during API fetching. + /// The implementation controls how the status is displayed. + /// + Task WithStatusAsync(string statusMessage, Func> work); + + /// + /// Wraps work in a progress bar during downloading. + /// The implementation controls how progress is displayed. + /// + Task WithProgressAsync(string description, long maxValue, bool showSize, + Func> work); + + /// + /// Called when content of a specific type is found for a creator. + /// + void OnContentFound(string contentType, int mediaCount, int objectCount); + + /// + /// Called when no content of a specific type is found for a creator. + /// + void OnNoContentFound(string contentType); + + /// + /// Called when downloading of a content type completes. + /// + void OnDownloadComplete(string contentType, DownloadResult result); + + /// + /// Called when starting to process a specific user/creator. + /// + void OnUserStarting(string username); + + /// + /// Called when all downloads for a user/creator are complete. + /// + void OnUserComplete(string username, CreatorDownloadResult result); + + /// + /// Called when a purchased tab user's downloads are complete. + /// + void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount); + + /// + /// Called when the entire scrape operation completes. + /// + void OnScrapeComplete(TimeSpan elapsed); + + /// + /// General status message display. + /// + void OnMessage(string message); +} diff --git a/OF DL/Services/IDownloadOrchestrationService.cs b/OF DL/Services/IDownloadOrchestrationService.cs new file mode 100644 index 0000000..44e48c6 --- /dev/null +++ b/OF DL/Services/IDownloadOrchestrationService.cs @@ -0,0 +1,73 @@ +using OF_DL.Models; +using UserEntities = OF_DL.Models.Entities.Users; + +namespace OF_DL.Services; + +public interface IDownloadOrchestrationService +{ + /// + /// Fetch subscriptions, lists, filter ignored users. + /// + Task GetAvailableUsersAsync(); + + /// + /// Get users for a specific list by name. + /// + Task> GetUsersForListAsync( + string listName, Dictionary allUsers, Dictionary lists); + + /// + /// Resolve download path for a username based on config. + /// + string ResolveDownloadPath(string username); + + /// + /// Prepare user folder (create dir, check username, create DB). + /// + Task PrepareUserFolderAsync(string username, long userId, string path); + + /// + /// Download all configured content types for a single creator. + /// + Task DownloadCreatorContentAsync( + string username, long userId, string path, + Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + IDownloadEventHandler eventHandler); + + /// + /// Download a single post by ID. + /// + Task DownloadSinglePostAsync( + string username, long postId, string path, + Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + IDownloadEventHandler eventHandler); + + /// + /// Download purchased tab content for all users. + /// + Task DownloadPurchasedTabAsync( + Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + IDownloadEventHandler eventHandler); + + /// + /// Download a single paid message by message ID. + /// + Task DownloadSinglePaidMessageAsync( + string username, long messageId, string path, + Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + IDownloadEventHandler eventHandler); + + /// + /// Resolve username from user ID via API. + /// + Task ResolveUsernameAsync(long userId); + + /// + /// Tracks paid post IDs across downloads. + /// + List PaidPostIds { get; } +} diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index c46996a..5f95a14 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -62,4 +62,21 @@ public interface IDownloadService Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter); + + Task DownloadPaidPostsPurchasedTab(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter); + + Task DownloadPaidMessagesPurchasedTab(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter); + + Task DownloadSinglePost(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PostEntities.SinglePostCollection post, IProgressReporter progressReporter); + + Task DownloadSinglePaidMessage(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection, + IProgressReporter progressReporter); } diff --git a/OF DL/Services/IStartupService.cs b/OF DL/Services/IStartupService.cs new file mode 100644 index 0000000..3b54466 --- /dev/null +++ b/OF DL/Services/IStartupService.cs @@ -0,0 +1,10 @@ +using OF_DL.Models; + +namespace OF_DL.Services; + +public interface IStartupService +{ + Task ValidateEnvironmentAsync(); + + Task CheckVersionAsync(); +} diff --git a/OF DL/Services/IStatusReporter.cs b/OF DL/Services/IStatusReporter.cs new file mode 100644 index 0000000..fe693c5 --- /dev/null +++ b/OF DL/Services/IStatusReporter.cs @@ -0,0 +1,14 @@ +namespace OF_DL.Services; + +/// +/// Interface for reporting status updates in a UI-agnostic way. +/// This replaces Spectre.Console's StatusContext in the service layer. +/// +public interface IStatusReporter +{ + /// + /// Reports a status message (e.g., "Getting Posts\n Found 42"). + /// The reporter implementation decides how to format and display the message. + /// + void ReportStatus(string message); +} diff --git a/OF DL/Services/StartupService.cs b/OF DL/Services/StartupService.cs new file mode 100644 index 0000000..6c0cfad --- /dev/null +++ b/OF DL/Services/StartupService.cs @@ -0,0 +1,254 @@ +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using OF_DL.Helpers; +using OF_DL.Models; +using Serilog; +using WidevineConstants = OF_DL.Widevine.Constants; + +namespace OF_DL.Services; + +public class StartupService(IConfigService configService, IAuthService authService) : IStartupService +{ + public async Task ValidateEnvironmentAsync() + { + StartupResult result = new(); + + // OS validation + OperatingSystem os = Environment.OSVersion; + result.OsVersionString = os.VersionString; + Log.Debug($"Operating system information: {os.VersionString}"); + + if (os.Platform == PlatformID.Win32NT && os.Version.Major < 10) + { + result.IsWindowsVersionValid = false; + Log.Error("Windows version prior to 10.x: {0}", os.VersionString); + } + + // FFmpeg detection + DetectFfmpeg(result); + + if (result.FfmpegFound) + { + // Escape backslashes for Windows + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + result.FfmpegPath!.Contains(@":\") && + !result.FfmpegPath.Contains(@":\\")) + { + result.FfmpegPath = result.FfmpegPath.Replace(@"\", @"\\"); + configService.CurrentConfig!.FFmpegPath = result.FfmpegPath; + } + + // Get FFmpeg version + result.FfmpegVersion = await GetFfmpegVersionAsync(result.FfmpegPath!); + } + + // 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")); + + if (!result.ClientIdBlobMissing) + { + Log.Debug("device_client_id_blob found"); + } + else + { + Log.Debug("device_client_id_blob missing"); + } + + if (!result.DevicePrivateKeyMissing) + { + Log.Debug("device_private_key found"); + } + else + { + Log.Debug("device_private_key missing"); + } + + // rules.json validation + if (File.Exists("rules.json")) + { + result.RulesJsonExists = true; + try + { + JsonConvert.DeserializeObject(File.ReadAllText("rules.json")); + Log.Debug("Rules.json: "); + Log.Debug(JsonConvert.SerializeObject(File.ReadAllText("rules.json"), Formatting.Indented)); + result.RulesJsonValid = true; + } + catch (Exception e) + { + result.RulesJsonError = e.Message; + Log.Error("rules.json processing failed.", e.Message); + } + } + + return result; + } + + 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 = null; + + 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.", e.Message); + } +#else + 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 static async Task GetFfmpegVersionAsync(string ffmpegPath) + { + try + { + ProcessStartInfo processStartInfo = new() + { + FileName = ffmpegPath, + 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("FFmpeg version output:\n{Output}", output); + + string firstLine = output.Split('\n')[0].Trim(); + if (firstLine.StartsWith("ffmpeg version")) + { + int versionStart = "ffmpeg version ".Length; + int copyrightIndex = firstLine.IndexOf(" Copyright"); + return copyrightIndex > versionStart + ? firstLine.Substring(versionStart, copyrightIndex - versionStart) + : firstLine.Substring(versionStart); + } + } + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to get FFmpeg version"); + } + + return null; + } + + private static bool ValidateFilePath(string path) + { + char[] invalidChars = Path.GetInvalidPathChars(); + if (path.Any(c => invalidChars.Contains(c))) + { + return false; + } + + return File.Exists(path); + } + + public static string? GetFullPath(string filename) + { + if (File.Exists(filename)) + { + return Path.GetFullPath(filename); + } + + string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? ""; + foreach (string path in pathEnv.Split(Path.PathSeparator)) + { + string fullPath = Path.Combine(path, filename); + if (File.Exists(fullPath)) + { + return fullPath; + } + } + + return null; + } +} diff --git a/OF DL/Widevine/CDM.cs b/OF DL/Widevine/CDM.cs index 64e352c..9cb20c3 100644 --- a/OF DL/Widevine/CDM.cs +++ b/OF DL/Widevine/CDM.cs @@ -78,7 +78,7 @@ public class CDM } Session session; - dynamic parsedInitData = ParseInitData(initData); + dynamic? parsedInitData = ParseInitData(initData); if (parsedInitData != null) { @@ -409,18 +409,22 @@ public class CDM byte[] decryptedKey; using MemoryStream mstream = new(); - using AesCryptoServiceProvider aesProvider = new() { Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 }; + + using AesCryptoServiceProvider aesProvider = new(); + aesProvider.Mode = CipherMode.CBC; + aesProvider.Padding = PaddingMode.PKCS7; + using CryptoStream cryptoStream = new(mstream, aesProvider.CreateDecryptor(session.DerivedKeys.Enc, iv), CryptoStreamMode.Write); cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); decryptedKey = mstream.ToArray(); - List permissions = new(); + List permissions = []; if (type == "OperatorSession") { foreach (PropertyInfo perm in key._OperatorSessionKeyPermissions.GetType().GetProperties()) { - if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1) + if ((uint?)perm.GetValue(key._OperatorSessionKeyPermissions) == 1) { permissions.Add(perm.Name); } diff --git a/OF DL/Widevine/CDMApi.cs b/OF DL/Widevine/CDMApi.cs index c1d3193..f00c83b 100644 --- a/OF DL/Widevine/CDMApi.cs +++ b/OF DL/Widevine/CDMApi.cs @@ -1,20 +1,32 @@ +using Serilog; + namespace OF_DL.Widevine; public class CDMApi { - private string SessionId { get; set; } + private string? SessionId { get; set; } - public byte[] GetChallenge(string initDataB64, string certDataB64, bool offline = false, bool raw = false) + public byte[]? GetChallenge(string initDataB64, string certDataB64, bool offline = false, bool raw = false) { SessionId = CDM.OpenSession(initDataB64, Constants.DEVICE_NAME, offline, raw); + if (SessionId == null) + { + Log.Debug("CDM.OpenSession returned null, unable to proceed with challenge generation"); + return null; + } + CDM.SetServiceCertificate(SessionId, Convert.FromBase64String(certDataB64)); return CDM.GetLicenseRequest(SessionId); } - public bool ProvideLicense(string licenseB64) + public void ProvideLicense(string licenseB64) { + if (SessionId == null) + { + throw new Exception("No session ID set. Could not provide license"); + } + CDM.ProvideLicense(SessionId, Convert.FromBase64String(licenseB64)); - return true; } public List GetKeys() => CDM.GetKeys(SessionId); From 9fe84e9d9f24b7319a3a3848686896816edcae4e Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 22:27:34 -0600 Subject: [PATCH 30/55] Add AGENTS.md --- AGENTS.md | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d70d7b8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,193 @@ +# AGENTS.md + +This repo is **OF DL** (also known as OF-DL), a C# console app that downloads media from a user's OnlyFans account(s). +This document is for AI agents helping developers modify the project. It focuses on architecture, data flow, and the +most important change points. + +**Quick Flow** + +1. `Program.Main` builds DI, loads `config.conf`, and runs the interactive flow. +2. `StartupService.CheckVersionAsync` checks the latest release tag (`OFDLV*`) from `git.ofdl.tools` when not in DEBUG. +3. `StartupService.ValidateEnvironmentAsync` validates OS, FFmpeg, `rules.json`, and Widevine device files. +4. `AuthService` loads `auth.json` or opens a browser login (PuppeteerSharp) and persists auth data. +5. `ApiService` signs every API request with dynamic rules and the current auth. +6. `DownloadOrchestrationService` selects creators, prepares folders/DBs, and calls `DownloadService` per media type. +7. `DownloadService` downloads media, handles DRM, and records metadata in SQLite. + +**Project Layout** + +- `OF DL/Program.cs` orchestrates startup, config/auth loading, and the interactive flow. +- `OF DL/Services/` contains application services (API, auth, download, config, DB, startup, logging, filenames). +- `OF DL/Models/` holds configuration, auth, DTOs, entities, and mapping helpers. +- `OF DL/Widevine/` implements Widevine CDM handling and key derivation. +- `OF DL/CLI/` contains Spectre.Console UI helpers and progress reporting. +- `docs/` and `mkdocs.yml` define the documentation site. +- `site/` is generated MkDocs output and should not be edited by hand. +- `docker/` contains container entrypoint and supervisor configuration. + +**Key Services** + +- `ApiService` (`OF DL/Services/ApiService.cs`) builds signed headers, performs HTTP requests, and maps DTOs to + entities. It also handles DRM-related calls like MPD/PSSH extraction and license requests. +- `AuthService` (`OF DL/Services/AuthService.cs`) loads `auth.json` or performs browser-based login with PuppeteerSharp, + then persists auth. It also normalizes cookies. +- `ConfigService` (`OF DL/Services/ConfigService.cs`) loads `config.conf` (HOCON), migrates legacy `config.json`, and + updates global settings (logging, text sanitization). +- `DownloadService` (`OF DL/Services/DownloadService.cs`) downloads all media (images, video, audio) and handles DRM + video decryption and FFmpeg execution. +- `DownloadOrchestrationService` (`OF DL/Services/DownloadOrchestrationService.cs`) coordinates user selection, + subscription lists, per-user folder prep, and per-media-type download execution. +- `DBService` (`OF DL/Services/DBService.cs`) manages SQLite metadata DBs for downloaded media and a `users.db` index. +- `StartupService` (`OF DL/Services/StartupService.cs`) validates FFmpeg, rules.json, Widevine device files, and + performs release version checks. +- `LoggingService` (`OF DL/Services/LoggingService.cs`) writes logs to `logs/OFDL.txt` and updates log level based on + config. +- `FileNameService` (`OF DL/Services/FileNameService.cs`) formats filenames using the custom format rules from config. + +**Models: DTOs vs Entities** + +- DTOs live under `OF DL/Models/Dtos/` and mirror API response JSON. +- Entities live under `OF DL/Models/Entities/` and represent the internal domain used by download logic. +- Mappers in `OF DL/Models/Mappers/` convert DTOs into entities to isolate API changes from downstream logic. + +**Configuration** + +- Primary config file is `config.conf` (HOCON). `ConfigService` migrates legacy `config.json` if found and creates a + default `config.conf` if missing. +- `Config` lives in `OF DL/Models/Config.cs` and is populated by `ConfigService.LoadConfigFromFileAsync`. +- `ConfigService.UpdateConfig` is the central place where runtime config changes are applied (logging level and text + sanitization). +- CLI flag `--non-interactive` forces non-interactive mode; `ConfigService.IsCliNonInteractive` and + `Config.NonInteractiveMode` both gate prompts. +- FFmpeg path is read from `config.conf`, `auth.json`, or auto-detected from PATH/current directory. + +**Runtime Files (relative to the working directory)** + +- `config.conf`, `auth.json`, and `rules.json` are loaded from the current working directory. +- `cdm/` (Widevine device files), `chrome-data/` (Puppeteer profile), and `logs/` are created under the working + directory. +- `users.db` is stored at the working directory root. + +**Authentication Flow** + +- Auth data is stored in `auth.json` using the `Auth` model in `OF DL/Models/Auth.cs`. +- `AuthService.LoadFromBrowserAsync` launches Chrome via PuppeteerSharp, waits for login, then extracts `auth_id` and + `sess` cookies, `bcTokenSha` from localStorage (used as `X_BC`), and `USER_AGENT` from the browser. +- `AuthService.ValidateCookieString` rewrites the cookie string so it contains only `auth_id` and `sess` and ensures a + trailing `;`. +- `AuthService` uses `chrome-data/` as its user data directory; `Logout` deletes `chrome-data` and `auth.json`. + +Environment variables used by auth: + +- `OFDL_DOCKER=true` toggles Docker-specific instructions and browser flags. +- `OFDL_PUPPETEER_EXECUTABLE_PATH` overrides the Chromium path for PuppeteerSharp. + +**Dynamic Rules and Signature Headers** + +- All OnlyFans API requests use dynamic headers from `ApiService.GetDynamicHeaders`. +- Dynamic rules are fetched from `https://git.ofdl.tools/sim0n00ps/dynamic-rules/raw/branch/main/rules.json` with + fallback to local `rules.json` in the current working directory. The repo ships `OF DL/rules.json` as the default + rules file. +- Cache durations: 15 minutes for remote rules, 5 minutes for local rules. +- `DynamicRules` shape is defined in `OF DL/Models/DynamicRules.cs` and includes `app-token`, `static_param`, `prefix`, + `suffix`, `checksum_constant`, and `checksum_indexes`. + +Signature algorithm in `GetDynamicHeaders`: + +- `input = "{static_param}\n{timestamp_ms}\n{path+query}\n{user_id}"` +- `hash = SHA1(input)` lower-case hex string +- `checksum = sum(hashString[index] char values) + checksum_constant` +- `sign = "{prefix}:{hash}:{checksum_hex}:{suffix}"` + +Headers included in signed requests: + +- `app-token`, `sign`, `time`, `user-id`, `user-agent`, `x-bc`, `cookie`. + +**Widevine CDM and DRM Decryption** + +- Runtime Widevine device files are expected at `cdm/devices/chrome_1610/device_client_id_blob` and + `cdm/devices/chrome_1610/device_private_key` (relative to the working directory). Paths are defined in + `OF DL/Widevine/Constants.cs` and validated in `StartupService`. + +DRM flow is primarily in `DownloadService.GetDecryptionInfo` and `ApiService` DRM helpers: + +- `ApiService.GetDRMMPDPSSH` downloads the MPD manifest and extracts the `cenc:pssh` value. +- `ApiService.GetDRMMPDLastModified` uses CloudFront signed cookies and returns MPD `Last-Modified`. +- `DownloadService.GetDecryptionInfo` builds DRM headers (via `GetDynamicHeaders`) and hits the license endpoint. + +Two decryption paths exist: + +- If CDM device files exist, `ApiService.GetDecryptionKeyCDM` uses `Widevine/CDMApi`. +- If missing, `ApiService.GetDecryptionKeyOFDL` calls `https://ofdl.tools/WV` as a fallback. + +`DownloadService.DownloadDrmMedia` runs FFmpeg with `-cenc_decryption_key`, CloudFront cookies, and auth +cookies/user-agent. Output is written to `{filename}_source.mp4`, then moved and recorded in SQLite. + +**Download Paths, Data, and Logs** + +- Default download root is `__user_data__/sites/OnlyFans/{username}` when `DownloadPath` is blank. This is computed in + `DownloadOrchestrationService.ResolveDownloadPath`. +- Each creator folder gets a `Metadata/user_data.db` (SQLite) managed by `DBService`. +- A global `users.db` in the working directory tracks subscribed creators and user IDs. +- Logs are written to `logs/OFDL.txt` (rolling daily); FFmpeg report files are also written under `logs/` when debug + logging is enabled. + +**Docs (MkDocs)** + +- Docs source lives under `docs/` and configuration is in `mkdocs.yml`. +- Build the site with `mkdocs build --clean` (outputs to `site/`). +- Preview locally with `mkdocs serve`. + +**CI/CD (Gitea Workflows)** + +- `/.gitea/workflows/publish-docs.yml` builds and deploys docs on tag pushes matching `OFDLV*` and on manual dispatch. +- `/.gitea/workflows/publish-docker.yml` builds multi-arch Docker images on `OFDLV*` tags and pushes to the Gitea + registry. +- `/.gitea/workflows/publish-release.yml` publishes Windows and Linux builds on `OFDLV*` tags and creates a draft + release. + +**Docker Image** + +- Built via `/.gitea/workflows/publish-docker.yml` on tag pushes `OFDLV*`. +- Image tags: `git.ofdl.tools/sim0n00ps/of-dl:latest` and `git.ofdl.tools/sim0n00ps/of-dl:{version}`. +- Build args include `VERSION` (tag name with `OFDLV` stripped). +- Platforms: `linux/amd64` and `linux/arm64`. +- Runtime uses `/config` as the working directory and `/data` for downloads; `docker/entrypoint.sh` seeds + `/config/config.conf` and `/config/rules.json` from `/default-config`. + +**Release Checklist** + +1. Update docs under `docs/` and verify locally with `mkdocs build --clean` or `mkdocs serve`. +2. Tag the release as `OFDLV{version}` and push the tag. +3. Verify the draft release artifact and publish the release in Gitea. + +**Coding Style (from .editorconfig)** + +- Indentation: 4 spaces by default, 2 spaces for XML/YAML/project files. No tabs. +- Line endings: LF for `*.sh`, CRLF for `*.cmd`/`*.bat`. +- C# braces on new lines (`csharp_new_line_before_open_brace = all`). +- Prefer predefined types (`int`, `string`) and avoid `var` except when type is apparent. +- `using` directives go outside the namespace and `System` namespaces are sorted first. +- Private/internal fields use `_camelCase`; private/internal static fields use `s_` prefix. +- `const` fields should be PascalCase. +- Prefer braces for control blocks and expression-bodied members (silent preferences). + +**Where to Look First** + +- `OF DL/Program.cs` for the execution path and menu flow. +- `OF DL/Services/ApiService.cs` for OF API calls and header signing. +- `OF DL/Services/DownloadService.cs` for downloads and DRM handling. +- `OF DL/Services/DownloadOrchestrationService.cs` for creator selection and flow control. +- `OF DL/Widevine/` for CDM key generation and license parsing. +- `OF DL/Models/Config.cs` and `OF DL/Services/ConfigService.cs` for config shape and parsing. +- `OF DL/Services/AuthService.cs` for user-facing authentication behavior and browser login flow. +- `docs/` for public documentation; update docs whenever user-facing behavior or configuration changes. + +Documentation updates for common changes: + +- Config option added/removed/changed in `Config` or `config.conf`: update `docs/config/all-configuration-options.md` ( + full spec), `docs/config/configuration.md` (organized list), and `docs/config/custom-filename-formats.md` if filename + tokens or formats are affected. +- Authentication flow changes (browser login, legacy methods, required fields): update `docs/config/auth.md`. +- CLI/menu flow or download workflow changes: update `docs/running-the-program.md`. +- Docker runtime or container flags/paths change: update `docs/installation/docker.md`. From ff431a377d37764c1cd0dc7f4c228547bd5651fd Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 22:39:23 -0600 Subject: [PATCH 31/55] Create OF DL.Core project to contain all the application logic for future GUI development --- AGENTS.md | 57 ++++++++++--------- {OF DL => OF DL.Core}/Crypto/CryptoUtils.cs | 0 {OF DL => OF DL.Core}/Crypto/Padding.cs | 0 .../Enumerations/CustomFileNameOption.cs | 0 .../Enumerations/DownloadDateSelection.cs | 0 .../Enumerations/LoggingLevel.cs | 0 .../Enumerations/MediaType.cs | 0 .../Enumerations/VideoResolution.cs | 0 {OF DL => OF DL.Core}/Helpers/Constants.cs | 0 .../Helpers/DownloadContext.cs | 0 .../Helpers/IFileNameHelper.cs | 0 .../Helpers/VersionHelper.cs | 0 {OF DL => OF DL.Core}/Models/Auth.cs | 0 .../Models/CDRMProjectRequest.cs | 0 {OF DL => OF DL.Core}/Models/Config.cs | 0 {OF DL => OF DL.Core}/Models/CreatorConfig.cs | 0 .../Models/CreatorDownloadResult.cs | 0 .../Models/DownloadResult.cs | 0 .../Models/Dtos/Archived/ArchivedDto.cs | 0 .../Models/Dtos/Archived/InfoDto.cs | 0 .../Models/Dtos/Archived/LinkedPostDto.cs | 0 .../Models/Dtos/Archived/ListItemDto.cs | 0 .../Models/Dtos/Archived/MediumDto.cs | 0 .../Models/Dtos/Common/AuthorDto.cs | 0 .../Models/Dtos/Common/AvatarThumbsDto.cs | 0 .../Models/Dtos/Common/CountersDto.cs | 0 .../Models/Dtos/Common/DashDto.cs | 0 .../Models/Dtos/Common/DrmDto.cs | 0 .../Models/Dtos/Common/FilesDto.cs | 0 .../Models/Dtos/Common/FullDto.cs | 0 .../Models/Dtos/Common/HeaderSizeDto.cs | 0 .../Models/Dtos/Common/HeaderThumbsDto.cs | 0 .../Models/Dtos/Common/HlsDto.cs | 0 .../Models/Dtos/Common/ManifestDto.cs | 0 .../Models/Dtos/Common/PreviewDto.cs | 0 .../Models/Dtos/Common/SignatureDto.cs | 0 .../Models/Dtos/Common/SourceDto.cs | 0 .../Models/Dtos/Common/SourcesDto.cs | 0 .../Models/Dtos/Common/SquarePreviewDto.cs | 0 .../Models/Dtos/Common/SubscribedByDataDto.cs | 0 .../Models/Dtos/Common/SubscribedOnDataDto.cs | 0 .../Models/Dtos/Common/ThumbDto.cs | 0 .../Models/Dtos/Common/VideoDto.cs | 0 .../Models/Dtos/Common/VideoSourcesDto.cs | 0 .../Dtos/Highlights/HighlightMediaDto.cs | 0 .../Models/Dtos/Highlights/HighlightsDto.cs | 0 .../Models/Dtos/Highlights/ListItemDto.cs | 0 .../Models/Dtos/Highlights/MediumDto.cs | 0 .../Models/Dtos/Highlights/StoryDto.cs | 0 .../Models/Dtos/Lists/HeaderSizeDto.cs | 0 .../Models/Dtos/Lists/HeaderThumbsDto.cs | 0 .../Models/Dtos/Lists/ListsStateDto.cs | 0 .../Models/Dtos/Lists/SubscribeDto.cs | 0 .../Models/Dtos/Lists/SubscribedByDataDto.cs | 0 .../Models/Dtos/Lists/SubscribedOnDataDto.cs | 0 .../Dtos/Lists/SubscriptionBundleDto.cs | 0 .../Models/Dtos/Lists/UserListDto.cs | 0 .../Models/Dtos/Lists/UserListItemDto.cs | 0 .../Models/Dtos/Lists/UserListUserDto.cs | 0 .../Models/Dtos/Lists/UsersListDto.cs | 0 .../Models/Dtos/Messages/FromUserDto.cs | 0 .../Models/Dtos/Messages/InfoDto.cs | 0 .../Models/Dtos/Messages/ListItemDto.cs | 0 .../Models/Dtos/Messages/ListsStateDto.cs | 0 .../Models/Dtos/Messages/MediumDto.cs | 0 .../Models/Dtos/Messages/MessagesDto.cs | 0 .../Models/Dtos/Messages/SingleMessageDto.cs | 0 .../Models/Dtos/Posts/InfoDto.cs | 0 .../Models/Dtos/Posts/ListItemDto.cs | 0 .../Models/Dtos/Posts/MediumDto.cs | 0 .../Models/Dtos/Posts/PostDto.cs | 0 .../Models/Dtos/Posts/SinglePostDto.cs | 0 .../Models/Dtos/Purchased/FromUserDto.cs | 0 .../Models/Dtos/Purchased/ListItemDto.cs | 0 .../Models/Dtos/Purchased/PurchasedDto.cs | 0 .../Models/Dtos/Stories/MediumDto.cs | 0 .../Models/Dtos/Stories/StoryDto.cs | 0 .../Models/Dtos/Streams/InfoDto.cs | 0 .../Models/Dtos/Streams/ListItemDto.cs | 0 .../Models/Dtos/Streams/MediumDto.cs | 0 .../Models/Dtos/Streams/StreamsDto.cs | 0 .../Models/Dtos/Subscriptions/ListItemDto.cs | 0 .../Dtos/Subscriptions/ListsStateDto.cs | 0 .../Models/Dtos/Subscriptions/SubscribeDto.cs | 0 .../Dtos/Subscriptions/SubscriptionsDto.cs | 0 .../Models/Dtos/Users/ListsStateDto.cs | 0 .../Models/Dtos/Users/SubscribeDto.cs | 0 .../Models/Dtos/Users/UserDto.cs | 0 {OF DL => OF DL.Core}/Models/DynamicRules.cs | 0 .../Models/Entities/Archived/Archived.cs | 0 .../Entities/Archived/ArchivedCollection.cs | 0 .../Models/Entities/Archived/ListItem.cs | 0 .../Models/Entities/Archived/Medium.cs | 0 .../Models/Entities/Common/Author.cs | 0 .../Models/Entities/Common/Dash.cs | 0 .../Models/Entities/Common/Drm.cs | 0 .../Models/Entities/Common/Files.cs | 0 .../Models/Entities/Common/FromUser.cs | 0 .../Models/Entities/Common/Full.cs | 0 .../Models/Entities/Common/Manifest.cs | 0 .../Models/Entities/Common/Preview.cs | 0 .../Models/Entities/Common/Signature.cs | 0 .../Models/Entities/Common/VideoSources.cs | 0 .../Entities/Highlights/HighlightMedia.cs | 0 .../Models/Entities/Highlights/Highlights.cs | 0 .../Models/Entities/Highlights/ListItem.cs | 0 .../Models/Entities/Highlights/Medium.cs | 0 .../Models/Entities/Highlights/Story.cs | 0 .../Models/Entities/Lists/UserList.cs | 0 .../Models/Entities/Lists/UserListItem.cs | 0 .../Models/Entities/Lists/UsersList.cs | 0 .../Models/Entities/Messages/ListItem.cs | 0 .../Models/Entities/Messages/Medium.cs | 0 .../Entities/Messages/MessageCollection.cs | 0 .../Models/Entities/Messages/Messages.cs | 0 .../Models/Entities/Messages/SingleMessage.cs | 0 .../Models/Entities/Posts/ListItem.cs | 0 .../Models/Entities/Posts/Medium.cs | 0 .../Models/Entities/Posts/Post.cs | 0 .../Models/Entities/Posts/PostCollection.cs | 0 .../Models/Entities/Posts/SinglePost.cs | 0 .../Entities/Posts/SinglePostCollection.cs | 0 .../Models/Entities/Purchased/ListItem.cs | 0 .../Purchased/PaidMessageCollection.cs | 0 .../Entities/Purchased/PaidPostCollection.cs | 0 .../Models/Entities/Purchased/Purchased.cs | 0 .../Purchased/PurchasedTabCollection.cs | 0 .../Purchased/SinglePaidMessageCollection.cs | 0 .../Models/Entities/Stories/Medium.cs | 0 .../Models/Entities/Stories/Stories.cs | 0 .../Models/Entities/Streams/ListItem.cs | 0 .../Models/Entities/Streams/Medium.cs | 0 .../Models/Entities/Streams/Streams.cs | 0 .../Entities/Streams/StreamsCollection.cs | 0 .../Models/Entities/Subscriptions/ListItem.cs | 0 .../Entities/Subscriptions/Subscriptions.cs | 0 .../Models/Entities/Users/User.cs | 0 .../Models/FileNameFormatConfig.cs | 0 .../Models/IFileNameFormatConfig.cs | 0 .../Models/LatestReleaseApiResponse.cs | 0 .../Models/Mappers/ArchivedMapper.cs | 0 .../Models/Mappers/HighlightsMapper.cs | 0 .../Models/Mappers/MessagesMapper.cs | 0 .../Models/Mappers/PostMapper.cs | 0 .../Models/Mappers/PurchasedMapper.cs | 0 .../Models/Mappers/StoriesMapper.cs | 0 .../Models/Mappers/StreamsMapper.cs | 0 .../Models/Mappers/SubscriptionsMapper.cs | 0 .../Models/Mappers/UserListsMapper.cs | 0 .../Models/Mappers/UserMapper.cs | 0 {OF DL => OF DL.Core}/Models/OFDLRequest.cs | 0 .../Models/ShortDateConverter.cs | 0 {OF DL => OF DL.Core}/Models/StartupResult.cs | 0 .../Models/ToggleableConfigAttribute.cs | 0 OF DL.Core/OF DL.Core.csproj | 26 +++++++++ {OF DL => OF DL.Core}/Services/ApiService.cs | 0 {OF DL => OF DL.Core}/Services/AuthService.cs | 0 .../Services/ConfigService.cs | 0 {OF DL => OF DL.Core}/Services/DBService.cs | 0 .../Services/DownloadOrchestrationService.cs | 0 .../Services/DownloadService.cs | 0 .../Services/FileNameService.cs | 0 {OF DL => OF DL.Core}/Services/IAPIService.cs | 0 .../Services/IAuthService.cs | 0 .../Services/IConfigService.cs | 0 {OF DL => OF DL.Core}/Services/IDBService.cs | 0 .../Services/IDownloadEventHandler.cs | 0 .../Services/IDownloadOrchestrationService.cs | 0 .../Services/IDownloadService.cs | 0 .../Services/IFileNameService.cs | 0 .../Services/ILoggingService.cs | 0 .../Services/IProgressReporter.cs | 0 .../Services/IStartupService.cs | 0 .../Services/IStatusReporter.cs | 0 .../Services/LoggingService.cs | 0 .../Services/StartupService.cs | 0 {OF DL => OF DL.Core}/Utils/HttpUtil.cs | 0 .../Utils/ThrottledStream.cs | 0 {OF DL => OF DL.Core}/Utils/XmlUtils.cs | 0 {OF DL => OF DL.Core}/Widevine/CDM.cs | 0 {OF DL => OF DL.Core}/Widevine/CDMApi.cs | 0 {OF DL => OF DL.Core}/Widevine/CDMDevice.cs | 0 {OF DL => OF DL.Core}/Widevine/Constants.cs | 0 {OF DL => OF DL.Core}/Widevine/ContentKey.cs | 0 {OF DL => OF DL.Core}/Widevine/DerivedKeys.cs | 0 {OF DL => OF DL.Core}/Widevine/PSSHBox.cs | 0 {OF DL => OF DL.Core}/Widevine/Session.cs | 0 {OF DL => OF DL.Core}/Widevine/WvProto2.cs | 0 OF DL.sln | 6 ++ OF DL/OF DL.csproj | 14 +---- 190 files changed, 63 insertions(+), 40 deletions(-) rename {OF DL => OF DL.Core}/Crypto/CryptoUtils.cs (100%) rename {OF DL => OF DL.Core}/Crypto/Padding.cs (100%) rename {OF DL => OF DL.Core}/Enumerations/CustomFileNameOption.cs (100%) rename {OF DL => OF DL.Core}/Enumerations/DownloadDateSelection.cs (100%) rename {OF DL => OF DL.Core}/Enumerations/LoggingLevel.cs (100%) rename {OF DL => OF DL.Core}/Enumerations/MediaType.cs (100%) rename {OF DL => OF DL.Core}/Enumerations/VideoResolution.cs (100%) rename {OF DL => OF DL.Core}/Helpers/Constants.cs (100%) rename {OF DL => OF DL.Core}/Helpers/DownloadContext.cs (100%) rename {OF DL => OF DL.Core}/Helpers/IFileNameHelper.cs (100%) rename {OF DL => OF DL.Core}/Helpers/VersionHelper.cs (100%) rename {OF DL => OF DL.Core}/Models/Auth.cs (100%) rename {OF DL => OF DL.Core}/Models/CDRMProjectRequest.cs (100%) rename {OF DL => OF DL.Core}/Models/Config.cs (100%) rename {OF DL => OF DL.Core}/Models/CreatorConfig.cs (100%) rename {OF DL => OF DL.Core}/Models/CreatorDownloadResult.cs (100%) rename {OF DL => OF DL.Core}/Models/DownloadResult.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Archived/ArchivedDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Archived/InfoDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Archived/LinkedPostDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Archived/ListItemDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Archived/MediumDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/AuthorDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/AvatarThumbsDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/CountersDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/DashDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/DrmDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/FilesDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/FullDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/HeaderSizeDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/HeaderThumbsDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/HlsDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/ManifestDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/PreviewDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/SignatureDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/SourceDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/SourcesDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/SquarePreviewDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/SubscribedByDataDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/SubscribedOnDataDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/ThumbDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/VideoDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Common/VideoSourcesDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Highlights/HighlightMediaDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Highlights/HighlightsDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Highlights/ListItemDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Highlights/MediumDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Highlights/StoryDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/HeaderSizeDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/HeaderThumbsDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/ListsStateDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/SubscribeDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/SubscribedByDataDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/SubscribedOnDataDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/SubscriptionBundleDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/UserListDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/UserListItemDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/UserListUserDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Lists/UsersListDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Messages/FromUserDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Messages/InfoDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Messages/ListItemDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Messages/ListsStateDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Messages/MediumDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Messages/MessagesDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Messages/SingleMessageDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Posts/InfoDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Posts/ListItemDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Posts/MediumDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Posts/PostDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Posts/SinglePostDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Purchased/FromUserDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Purchased/ListItemDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Purchased/PurchasedDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Stories/MediumDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Stories/StoryDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Streams/InfoDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Streams/ListItemDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Streams/MediumDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Streams/StreamsDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Subscriptions/ListItemDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Subscriptions/ListsStateDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Subscriptions/SubscribeDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Subscriptions/SubscriptionsDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Users/ListsStateDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Users/SubscribeDto.cs (100%) rename {OF DL => OF DL.Core}/Models/Dtos/Users/UserDto.cs (100%) rename {OF DL => OF DL.Core}/Models/DynamicRules.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Archived/Archived.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Archived/ArchivedCollection.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Archived/ListItem.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Archived/Medium.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Common/Author.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Common/Dash.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Common/Drm.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Common/Files.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Common/FromUser.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Common/Full.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Common/Manifest.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Common/Preview.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Common/Signature.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Common/VideoSources.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Highlights/HighlightMedia.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Highlights/Highlights.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Highlights/ListItem.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Highlights/Medium.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Highlights/Story.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Lists/UserList.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Lists/UserListItem.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Lists/UsersList.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Messages/ListItem.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Messages/Medium.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Messages/MessageCollection.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Messages/Messages.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Messages/SingleMessage.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Posts/ListItem.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Posts/Medium.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Posts/Post.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Posts/PostCollection.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Posts/SinglePost.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Posts/SinglePostCollection.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Purchased/ListItem.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Purchased/PaidMessageCollection.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Purchased/PaidPostCollection.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Purchased/Purchased.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Purchased/PurchasedTabCollection.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Purchased/SinglePaidMessageCollection.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Stories/Medium.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Stories/Stories.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Streams/ListItem.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Streams/Medium.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Streams/Streams.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Streams/StreamsCollection.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Subscriptions/ListItem.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Subscriptions/Subscriptions.cs (100%) rename {OF DL => OF DL.Core}/Models/Entities/Users/User.cs (100%) rename {OF DL => OF DL.Core}/Models/FileNameFormatConfig.cs (100%) rename {OF DL => OF DL.Core}/Models/IFileNameFormatConfig.cs (100%) rename {OF DL => OF DL.Core}/Models/LatestReleaseApiResponse.cs (100%) rename {OF DL => OF DL.Core}/Models/Mappers/ArchivedMapper.cs (100%) rename {OF DL => OF DL.Core}/Models/Mappers/HighlightsMapper.cs (100%) rename {OF DL => OF DL.Core}/Models/Mappers/MessagesMapper.cs (100%) rename {OF DL => OF DL.Core}/Models/Mappers/PostMapper.cs (100%) rename {OF DL => OF DL.Core}/Models/Mappers/PurchasedMapper.cs (100%) rename {OF DL => OF DL.Core}/Models/Mappers/StoriesMapper.cs (100%) rename {OF DL => OF DL.Core}/Models/Mappers/StreamsMapper.cs (100%) rename {OF DL => OF DL.Core}/Models/Mappers/SubscriptionsMapper.cs (100%) rename {OF DL => OF DL.Core}/Models/Mappers/UserListsMapper.cs (100%) rename {OF DL => OF DL.Core}/Models/Mappers/UserMapper.cs (100%) rename {OF DL => OF DL.Core}/Models/OFDLRequest.cs (100%) rename {OF DL => OF DL.Core}/Models/ShortDateConverter.cs (100%) rename {OF DL => OF DL.Core}/Models/StartupResult.cs (100%) rename {OF DL => OF DL.Core}/Models/ToggleableConfigAttribute.cs (100%) create mode 100644 OF DL.Core/OF DL.Core.csproj rename {OF DL => OF DL.Core}/Services/ApiService.cs (100%) rename {OF DL => OF DL.Core}/Services/AuthService.cs (100%) rename {OF DL => OF DL.Core}/Services/ConfigService.cs (100%) rename {OF DL => OF DL.Core}/Services/DBService.cs (100%) rename {OF DL => OF DL.Core}/Services/DownloadOrchestrationService.cs (100%) rename {OF DL => OF DL.Core}/Services/DownloadService.cs (100%) rename {OF DL => OF DL.Core}/Services/FileNameService.cs (100%) rename {OF DL => OF DL.Core}/Services/IAPIService.cs (100%) rename {OF DL => OF DL.Core}/Services/IAuthService.cs (100%) rename {OF DL => OF DL.Core}/Services/IConfigService.cs (100%) rename {OF DL => OF DL.Core}/Services/IDBService.cs (100%) rename {OF DL => OF DL.Core}/Services/IDownloadEventHandler.cs (100%) rename {OF DL => OF DL.Core}/Services/IDownloadOrchestrationService.cs (100%) rename {OF DL => OF DL.Core}/Services/IDownloadService.cs (100%) rename {OF DL => OF DL.Core}/Services/IFileNameService.cs (100%) rename {OF DL => OF DL.Core}/Services/ILoggingService.cs (100%) rename {OF DL => OF DL.Core}/Services/IProgressReporter.cs (100%) rename {OF DL => OF DL.Core}/Services/IStartupService.cs (100%) rename {OF DL => OF DL.Core}/Services/IStatusReporter.cs (100%) rename {OF DL => OF DL.Core}/Services/LoggingService.cs (100%) rename {OF DL => OF DL.Core}/Services/StartupService.cs (100%) rename {OF DL => OF DL.Core}/Utils/HttpUtil.cs (100%) rename {OF DL => OF DL.Core}/Utils/ThrottledStream.cs (100%) rename {OF DL => OF DL.Core}/Utils/XmlUtils.cs (100%) rename {OF DL => OF DL.Core}/Widevine/CDM.cs (100%) rename {OF DL => OF DL.Core}/Widevine/CDMApi.cs (100%) rename {OF DL => OF DL.Core}/Widevine/CDMDevice.cs (100%) rename {OF DL => OF DL.Core}/Widevine/Constants.cs (100%) rename {OF DL => OF DL.Core}/Widevine/ContentKey.cs (100%) rename {OF DL => OF DL.Core}/Widevine/DerivedKeys.cs (100%) rename {OF DL => OF DL.Core}/Widevine/PSSHBox.cs (100%) rename {OF DL => OF DL.Core}/Widevine/Session.cs (100%) rename {OF DL => OF DL.Core}/Widevine/WvProto2.cs (100%) diff --git a/AGENTS.md b/AGENTS.md index d70d7b8..0b780fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # AGENTS.md +Note: Keep AGENTS.md updated as project structure, key services, or workflows change. + This repo is **OF DL** (also known as OF-DL), a C# console app that downloads media from a user's OnlyFans account(s). This document is for AI agents helping developers modify the project. It focuses on architecture, data flow, and the most important change points. @@ -16,45 +18,46 @@ most important change points. **Project Layout** -- `OF DL/Program.cs` orchestrates startup, config/auth loading, and the interactive flow. -- `OF DL/Services/` contains application services (API, auth, download, config, DB, startup, logging, filenames). -- `OF DL/Models/` holds configuration, auth, DTOs, entities, and mapping helpers. -- `OF DL/Widevine/` implements Widevine CDM handling and key derivation. -- `OF DL/CLI/` contains Spectre.Console UI helpers and progress reporting. +- `OF DL/Program.cs` orchestrates startup, config/auth loading, and the interactive flow (CLI entrypoint). +- `OF DL/CLI/` contains Spectre.Console UI helpers and progress reporting (CLI-only). +- `OF DL.Core/Services/` contains application services (API, auth, download, config, DB, startup, logging, filenames). +- `OF DL.Core/Models/` holds configuration, auth, DTOs, entities, and mapping helpers. +- `OF DL.Core/Widevine/` implements Widevine CDM handling and key derivation. +- `OF DL.Core/Helpers/`, `OF DL.Core/Utils/`, `OF DL.Core/Crypto/`, `OF DL.Core/Enumerations/` contain shared core logic. - `docs/` and `mkdocs.yml` define the documentation site. - `site/` is generated MkDocs output and should not be edited by hand. - `docker/` contains container entrypoint and supervisor configuration. **Key Services** -- `ApiService` (`OF DL/Services/ApiService.cs`) builds signed headers, performs HTTP requests, and maps DTOs to +- `ApiService` (`OF DL.Core/Services/ApiService.cs`) builds signed headers, performs HTTP requests, and maps DTOs to entities. It also handles DRM-related calls like MPD/PSSH extraction and license requests. -- `AuthService` (`OF DL/Services/AuthService.cs`) loads `auth.json` or performs browser-based login with PuppeteerSharp, +- `AuthService` (`OF DL.Core/Services/AuthService.cs`) loads `auth.json` or performs browser-based login with PuppeteerSharp, then persists auth. It also normalizes cookies. -- `ConfigService` (`OF DL/Services/ConfigService.cs`) loads `config.conf` (HOCON), migrates legacy `config.json`, and +- `ConfigService` (`OF DL.Core/Services/ConfigService.cs`) loads `config.conf` (HOCON), migrates legacy `config.json`, and updates global settings (logging, text sanitization). -- `DownloadService` (`OF DL/Services/DownloadService.cs`) downloads all media (images, video, audio) and handles DRM +- `DownloadService` (`OF DL.Core/Services/DownloadService.cs`) downloads all media (images, video, audio) and handles DRM video decryption and FFmpeg execution. -- `DownloadOrchestrationService` (`OF DL/Services/DownloadOrchestrationService.cs`) coordinates user selection, +- `DownloadOrchestrationService` (`OF DL.Core/Services/DownloadOrchestrationService.cs`) coordinates user selection, subscription lists, per-user folder prep, and per-media-type download execution. -- `DBService` (`OF DL/Services/DBService.cs`) manages SQLite metadata DBs for downloaded media and a `users.db` index. -- `StartupService` (`OF DL/Services/StartupService.cs`) validates FFmpeg, rules.json, Widevine device files, and +- `DBService` (`OF DL.Core/Services/DBService.cs`) manages SQLite metadata DBs for downloaded media and a `users.db` index. +- `StartupService` (`OF DL.Core/Services/StartupService.cs`) validates FFmpeg, rules.json, Widevine device files, and performs release version checks. -- `LoggingService` (`OF DL/Services/LoggingService.cs`) writes logs to `logs/OFDL.txt` and updates log level based on +- `LoggingService` (`OF DL.Core/Services/LoggingService.cs`) writes logs to `logs/OFDL.txt` and updates log level based on config. -- `FileNameService` (`OF DL/Services/FileNameService.cs`) formats filenames using the custom format rules from config. +- `FileNameService` (`OF DL.Core/Services/FileNameService.cs`) formats filenames using the custom format rules from config. **Models: DTOs vs Entities** -- DTOs live under `OF DL/Models/Dtos/` and mirror API response JSON. -- Entities live under `OF DL/Models/Entities/` and represent the internal domain used by download logic. -- Mappers in `OF DL/Models/Mappers/` convert DTOs into entities to isolate API changes from downstream logic. +- DTOs live under `OF DL.Core/Models/Dtos/` and mirror API response JSON. +- Entities live under `OF DL.Core/Models/Entities/` and represent the internal domain used by download logic. +- Mappers in `OF DL.Core/Models/Mappers/` convert DTOs into entities to isolate API changes from downstream logic. **Configuration** - Primary config file is `config.conf` (HOCON). `ConfigService` migrates legacy `config.json` if found and creates a default `config.conf` if missing. -- `Config` lives in `OF DL/Models/Config.cs` and is populated by `ConfigService.LoadConfigFromFileAsync`. +- `Config` lives in `OF DL.Core/Models/Config.cs` and is populated by `ConfigService.LoadConfigFromFileAsync`. - `ConfigService.UpdateConfig` is the central place where runtime config changes are applied (logging level and text sanitization). - CLI flag `--non-interactive` forces non-interactive mode; `ConfigService.IsCliNonInteractive` and @@ -70,7 +73,7 @@ most important change points. **Authentication Flow** -- Auth data is stored in `auth.json` using the `Auth` model in `OF DL/Models/Auth.cs`. +- Auth data is stored in `auth.json` using the `Auth` model in `OF DL.Core/Models/Auth.cs`. - `AuthService.LoadFromBrowserAsync` launches Chrome via PuppeteerSharp, waits for login, then extracts `auth_id` and `sess` cookies, `bcTokenSha` from localStorage (used as `X_BC`), and `USER_AGENT` from the browser. - `AuthService.ValidateCookieString` rewrites the cookie string so it contains only `auth_id` and `sess` and ensures a @@ -89,7 +92,7 @@ Environment variables used by auth: fallback to local `rules.json` in the current working directory. The repo ships `OF DL/rules.json` as the default rules file. - Cache durations: 15 minutes for remote rules, 5 minutes for local rules. -- `DynamicRules` shape is defined in `OF DL/Models/DynamicRules.cs` and includes `app-token`, `static_param`, `prefix`, +- `DynamicRules` shape is defined in `OF DL.Core/Models/DynamicRules.cs` and includes `app-token`, `static_param`, `prefix`, `suffix`, `checksum_constant`, and `checksum_indexes`. Signature algorithm in `GetDynamicHeaders`: @@ -107,7 +110,7 @@ Headers included in signed requests: - Runtime Widevine device files are expected at `cdm/devices/chrome_1610/device_client_id_blob` and `cdm/devices/chrome_1610/device_private_key` (relative to the working directory). Paths are defined in - `OF DL/Widevine/Constants.cs` and validated in `StartupService`. + `OF DL.Core/Widevine/Constants.cs` and validated in `StartupService`. DRM flow is primarily in `DownloadService.GetDecryptionInfo` and `ApiService` DRM helpers: @@ -175,12 +178,12 @@ cookies/user-agent. Output is written to `{filename}_source.mp4`, then moved and **Where to Look First** - `OF DL/Program.cs` for the execution path and menu flow. -- `OF DL/Services/ApiService.cs` for OF API calls and header signing. -- `OF DL/Services/DownloadService.cs` for downloads and DRM handling. -- `OF DL/Services/DownloadOrchestrationService.cs` for creator selection and flow control. -- `OF DL/Widevine/` for CDM key generation and license parsing. -- `OF DL/Models/Config.cs` and `OF DL/Services/ConfigService.cs` for config shape and parsing. -- `OF DL/Services/AuthService.cs` for user-facing authentication behavior and browser login flow. +- `OF DL.Core/Services/ApiService.cs` for OF API calls and header signing. +- `OF DL.Core/Services/DownloadService.cs` for downloads and DRM handling. +- `OF DL.Core/Services/DownloadOrchestrationService.cs` for creator selection and flow control. +- `OF DL.Core/Widevine/` for CDM key generation and license parsing. +- `OF DL.Core/Models/Config.cs` and `OF DL.Core/Services/ConfigService.cs` for config shape and parsing. +- `OF DL.Core/Services/AuthService.cs` for user-facing authentication behavior and browser login flow. - `docs/` for public documentation; update docs whenever user-facing behavior or configuration changes. Documentation updates for common changes: diff --git a/OF DL/Crypto/CryptoUtils.cs b/OF DL.Core/Crypto/CryptoUtils.cs similarity index 100% rename from OF DL/Crypto/CryptoUtils.cs rename to OF DL.Core/Crypto/CryptoUtils.cs diff --git a/OF DL/Crypto/Padding.cs b/OF DL.Core/Crypto/Padding.cs similarity index 100% rename from OF DL/Crypto/Padding.cs rename to OF DL.Core/Crypto/Padding.cs diff --git a/OF DL/Enumerations/CustomFileNameOption.cs b/OF DL.Core/Enumerations/CustomFileNameOption.cs similarity index 100% rename from OF DL/Enumerations/CustomFileNameOption.cs rename to OF DL.Core/Enumerations/CustomFileNameOption.cs diff --git a/OF DL/Enumerations/DownloadDateSelection.cs b/OF DL.Core/Enumerations/DownloadDateSelection.cs similarity index 100% rename from OF DL/Enumerations/DownloadDateSelection.cs rename to OF DL.Core/Enumerations/DownloadDateSelection.cs diff --git a/OF DL/Enumerations/LoggingLevel.cs b/OF DL.Core/Enumerations/LoggingLevel.cs similarity index 100% rename from OF DL/Enumerations/LoggingLevel.cs rename to OF DL.Core/Enumerations/LoggingLevel.cs diff --git a/OF DL/Enumerations/MediaType.cs b/OF DL.Core/Enumerations/MediaType.cs similarity index 100% rename from OF DL/Enumerations/MediaType.cs rename to OF DL.Core/Enumerations/MediaType.cs diff --git a/OF DL/Enumerations/VideoResolution.cs b/OF DL.Core/Enumerations/VideoResolution.cs similarity index 100% rename from OF DL/Enumerations/VideoResolution.cs rename to OF DL.Core/Enumerations/VideoResolution.cs diff --git a/OF DL/Helpers/Constants.cs b/OF DL.Core/Helpers/Constants.cs similarity index 100% rename from OF DL/Helpers/Constants.cs rename to OF DL.Core/Helpers/Constants.cs diff --git a/OF DL/Helpers/DownloadContext.cs b/OF DL.Core/Helpers/DownloadContext.cs similarity index 100% rename from OF DL/Helpers/DownloadContext.cs rename to OF DL.Core/Helpers/DownloadContext.cs diff --git a/OF DL/Helpers/IFileNameHelper.cs b/OF DL.Core/Helpers/IFileNameHelper.cs similarity index 100% rename from OF DL/Helpers/IFileNameHelper.cs rename to OF DL.Core/Helpers/IFileNameHelper.cs diff --git a/OF DL/Helpers/VersionHelper.cs b/OF DL.Core/Helpers/VersionHelper.cs similarity index 100% rename from OF DL/Helpers/VersionHelper.cs rename to OF DL.Core/Helpers/VersionHelper.cs diff --git a/OF DL/Models/Auth.cs b/OF DL.Core/Models/Auth.cs similarity index 100% rename from OF DL/Models/Auth.cs rename to OF DL.Core/Models/Auth.cs diff --git a/OF DL/Models/CDRMProjectRequest.cs b/OF DL.Core/Models/CDRMProjectRequest.cs similarity index 100% rename from OF DL/Models/CDRMProjectRequest.cs rename to OF DL.Core/Models/CDRMProjectRequest.cs diff --git a/OF DL/Models/Config.cs b/OF DL.Core/Models/Config.cs similarity index 100% rename from OF DL/Models/Config.cs rename to OF DL.Core/Models/Config.cs diff --git a/OF DL/Models/CreatorConfig.cs b/OF DL.Core/Models/CreatorConfig.cs similarity index 100% rename from OF DL/Models/CreatorConfig.cs rename to OF DL.Core/Models/CreatorConfig.cs diff --git a/OF DL/Models/CreatorDownloadResult.cs b/OF DL.Core/Models/CreatorDownloadResult.cs similarity index 100% rename from OF DL/Models/CreatorDownloadResult.cs rename to OF DL.Core/Models/CreatorDownloadResult.cs diff --git a/OF DL/Models/DownloadResult.cs b/OF DL.Core/Models/DownloadResult.cs similarity index 100% rename from OF DL/Models/DownloadResult.cs rename to OF DL.Core/Models/DownloadResult.cs diff --git a/OF DL/Models/Dtos/Archived/ArchivedDto.cs b/OF DL.Core/Models/Dtos/Archived/ArchivedDto.cs similarity index 100% rename from OF DL/Models/Dtos/Archived/ArchivedDto.cs rename to OF DL.Core/Models/Dtos/Archived/ArchivedDto.cs diff --git a/OF DL/Models/Dtos/Archived/InfoDto.cs b/OF DL.Core/Models/Dtos/Archived/InfoDto.cs similarity index 100% rename from OF DL/Models/Dtos/Archived/InfoDto.cs rename to OF DL.Core/Models/Dtos/Archived/InfoDto.cs diff --git a/OF DL/Models/Dtos/Archived/LinkedPostDto.cs b/OF DL.Core/Models/Dtos/Archived/LinkedPostDto.cs similarity index 100% rename from OF DL/Models/Dtos/Archived/LinkedPostDto.cs rename to OF DL.Core/Models/Dtos/Archived/LinkedPostDto.cs diff --git a/OF DL/Models/Dtos/Archived/ListItemDto.cs b/OF DL.Core/Models/Dtos/Archived/ListItemDto.cs similarity index 100% rename from OF DL/Models/Dtos/Archived/ListItemDto.cs rename to OF DL.Core/Models/Dtos/Archived/ListItemDto.cs diff --git a/OF DL/Models/Dtos/Archived/MediumDto.cs b/OF DL.Core/Models/Dtos/Archived/MediumDto.cs similarity index 100% rename from OF DL/Models/Dtos/Archived/MediumDto.cs rename to OF DL.Core/Models/Dtos/Archived/MediumDto.cs diff --git a/OF DL/Models/Dtos/Common/AuthorDto.cs b/OF DL.Core/Models/Dtos/Common/AuthorDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/AuthorDto.cs rename to OF DL.Core/Models/Dtos/Common/AuthorDto.cs diff --git a/OF DL/Models/Dtos/Common/AvatarThumbsDto.cs b/OF DL.Core/Models/Dtos/Common/AvatarThumbsDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/AvatarThumbsDto.cs rename to OF DL.Core/Models/Dtos/Common/AvatarThumbsDto.cs diff --git a/OF DL/Models/Dtos/Common/CountersDto.cs b/OF DL.Core/Models/Dtos/Common/CountersDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/CountersDto.cs rename to OF DL.Core/Models/Dtos/Common/CountersDto.cs diff --git a/OF DL/Models/Dtos/Common/DashDto.cs b/OF DL.Core/Models/Dtos/Common/DashDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/DashDto.cs rename to OF DL.Core/Models/Dtos/Common/DashDto.cs diff --git a/OF DL/Models/Dtos/Common/DrmDto.cs b/OF DL.Core/Models/Dtos/Common/DrmDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/DrmDto.cs rename to OF DL.Core/Models/Dtos/Common/DrmDto.cs diff --git a/OF DL/Models/Dtos/Common/FilesDto.cs b/OF DL.Core/Models/Dtos/Common/FilesDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/FilesDto.cs rename to OF DL.Core/Models/Dtos/Common/FilesDto.cs diff --git a/OF DL/Models/Dtos/Common/FullDto.cs b/OF DL.Core/Models/Dtos/Common/FullDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/FullDto.cs rename to OF DL.Core/Models/Dtos/Common/FullDto.cs diff --git a/OF DL/Models/Dtos/Common/HeaderSizeDto.cs b/OF DL.Core/Models/Dtos/Common/HeaderSizeDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/HeaderSizeDto.cs rename to OF DL.Core/Models/Dtos/Common/HeaderSizeDto.cs diff --git a/OF DL/Models/Dtos/Common/HeaderThumbsDto.cs b/OF DL.Core/Models/Dtos/Common/HeaderThumbsDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/HeaderThumbsDto.cs rename to OF DL.Core/Models/Dtos/Common/HeaderThumbsDto.cs diff --git a/OF DL/Models/Dtos/Common/HlsDto.cs b/OF DL.Core/Models/Dtos/Common/HlsDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/HlsDto.cs rename to OF DL.Core/Models/Dtos/Common/HlsDto.cs diff --git a/OF DL/Models/Dtos/Common/ManifestDto.cs b/OF DL.Core/Models/Dtos/Common/ManifestDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/ManifestDto.cs rename to OF DL.Core/Models/Dtos/Common/ManifestDto.cs diff --git a/OF DL/Models/Dtos/Common/PreviewDto.cs b/OF DL.Core/Models/Dtos/Common/PreviewDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/PreviewDto.cs rename to OF DL.Core/Models/Dtos/Common/PreviewDto.cs diff --git a/OF DL/Models/Dtos/Common/SignatureDto.cs b/OF DL.Core/Models/Dtos/Common/SignatureDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/SignatureDto.cs rename to OF DL.Core/Models/Dtos/Common/SignatureDto.cs diff --git a/OF DL/Models/Dtos/Common/SourceDto.cs b/OF DL.Core/Models/Dtos/Common/SourceDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/SourceDto.cs rename to OF DL.Core/Models/Dtos/Common/SourceDto.cs diff --git a/OF DL/Models/Dtos/Common/SourcesDto.cs b/OF DL.Core/Models/Dtos/Common/SourcesDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/SourcesDto.cs rename to OF DL.Core/Models/Dtos/Common/SourcesDto.cs diff --git a/OF DL/Models/Dtos/Common/SquarePreviewDto.cs b/OF DL.Core/Models/Dtos/Common/SquarePreviewDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/SquarePreviewDto.cs rename to OF DL.Core/Models/Dtos/Common/SquarePreviewDto.cs diff --git a/OF DL/Models/Dtos/Common/SubscribedByDataDto.cs b/OF DL.Core/Models/Dtos/Common/SubscribedByDataDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/SubscribedByDataDto.cs rename to OF DL.Core/Models/Dtos/Common/SubscribedByDataDto.cs diff --git a/OF DL/Models/Dtos/Common/SubscribedOnDataDto.cs b/OF DL.Core/Models/Dtos/Common/SubscribedOnDataDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/SubscribedOnDataDto.cs rename to OF DL.Core/Models/Dtos/Common/SubscribedOnDataDto.cs diff --git a/OF DL/Models/Dtos/Common/ThumbDto.cs b/OF DL.Core/Models/Dtos/Common/ThumbDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/ThumbDto.cs rename to OF DL.Core/Models/Dtos/Common/ThumbDto.cs diff --git a/OF DL/Models/Dtos/Common/VideoDto.cs b/OF DL.Core/Models/Dtos/Common/VideoDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/VideoDto.cs rename to OF DL.Core/Models/Dtos/Common/VideoDto.cs diff --git a/OF DL/Models/Dtos/Common/VideoSourcesDto.cs b/OF DL.Core/Models/Dtos/Common/VideoSourcesDto.cs similarity index 100% rename from OF DL/Models/Dtos/Common/VideoSourcesDto.cs rename to OF DL.Core/Models/Dtos/Common/VideoSourcesDto.cs diff --git a/OF DL/Models/Dtos/Highlights/HighlightMediaDto.cs b/OF DL.Core/Models/Dtos/Highlights/HighlightMediaDto.cs similarity index 100% rename from OF DL/Models/Dtos/Highlights/HighlightMediaDto.cs rename to OF DL.Core/Models/Dtos/Highlights/HighlightMediaDto.cs diff --git a/OF DL/Models/Dtos/Highlights/HighlightsDto.cs b/OF DL.Core/Models/Dtos/Highlights/HighlightsDto.cs similarity index 100% rename from OF DL/Models/Dtos/Highlights/HighlightsDto.cs rename to OF DL.Core/Models/Dtos/Highlights/HighlightsDto.cs diff --git a/OF DL/Models/Dtos/Highlights/ListItemDto.cs b/OF DL.Core/Models/Dtos/Highlights/ListItemDto.cs similarity index 100% rename from OF DL/Models/Dtos/Highlights/ListItemDto.cs rename to OF DL.Core/Models/Dtos/Highlights/ListItemDto.cs diff --git a/OF DL/Models/Dtos/Highlights/MediumDto.cs b/OF DL.Core/Models/Dtos/Highlights/MediumDto.cs similarity index 100% rename from OF DL/Models/Dtos/Highlights/MediumDto.cs rename to OF DL.Core/Models/Dtos/Highlights/MediumDto.cs diff --git a/OF DL/Models/Dtos/Highlights/StoryDto.cs b/OF DL.Core/Models/Dtos/Highlights/StoryDto.cs similarity index 100% rename from OF DL/Models/Dtos/Highlights/StoryDto.cs rename to OF DL.Core/Models/Dtos/Highlights/StoryDto.cs diff --git a/OF DL/Models/Dtos/Lists/HeaderSizeDto.cs b/OF DL.Core/Models/Dtos/Lists/HeaderSizeDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/HeaderSizeDto.cs rename to OF DL.Core/Models/Dtos/Lists/HeaderSizeDto.cs diff --git a/OF DL/Models/Dtos/Lists/HeaderThumbsDto.cs b/OF DL.Core/Models/Dtos/Lists/HeaderThumbsDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/HeaderThumbsDto.cs rename to OF DL.Core/Models/Dtos/Lists/HeaderThumbsDto.cs diff --git a/OF DL/Models/Dtos/Lists/ListsStateDto.cs b/OF DL.Core/Models/Dtos/Lists/ListsStateDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/ListsStateDto.cs rename to OF DL.Core/Models/Dtos/Lists/ListsStateDto.cs diff --git a/OF DL/Models/Dtos/Lists/SubscribeDto.cs b/OF DL.Core/Models/Dtos/Lists/SubscribeDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/SubscribeDto.cs rename to OF DL.Core/Models/Dtos/Lists/SubscribeDto.cs diff --git a/OF DL/Models/Dtos/Lists/SubscribedByDataDto.cs b/OF DL.Core/Models/Dtos/Lists/SubscribedByDataDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/SubscribedByDataDto.cs rename to OF DL.Core/Models/Dtos/Lists/SubscribedByDataDto.cs diff --git a/OF DL/Models/Dtos/Lists/SubscribedOnDataDto.cs b/OF DL.Core/Models/Dtos/Lists/SubscribedOnDataDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/SubscribedOnDataDto.cs rename to OF DL.Core/Models/Dtos/Lists/SubscribedOnDataDto.cs diff --git a/OF DL/Models/Dtos/Lists/SubscriptionBundleDto.cs b/OF DL.Core/Models/Dtos/Lists/SubscriptionBundleDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/SubscriptionBundleDto.cs rename to OF DL.Core/Models/Dtos/Lists/SubscriptionBundleDto.cs diff --git a/OF DL/Models/Dtos/Lists/UserListDto.cs b/OF DL.Core/Models/Dtos/Lists/UserListDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/UserListDto.cs rename to OF DL.Core/Models/Dtos/Lists/UserListDto.cs diff --git a/OF DL/Models/Dtos/Lists/UserListItemDto.cs b/OF DL.Core/Models/Dtos/Lists/UserListItemDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/UserListItemDto.cs rename to OF DL.Core/Models/Dtos/Lists/UserListItemDto.cs diff --git a/OF DL/Models/Dtos/Lists/UserListUserDto.cs b/OF DL.Core/Models/Dtos/Lists/UserListUserDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/UserListUserDto.cs rename to OF DL.Core/Models/Dtos/Lists/UserListUserDto.cs diff --git a/OF DL/Models/Dtos/Lists/UsersListDto.cs b/OF DL.Core/Models/Dtos/Lists/UsersListDto.cs similarity index 100% rename from OF DL/Models/Dtos/Lists/UsersListDto.cs rename to OF DL.Core/Models/Dtos/Lists/UsersListDto.cs diff --git a/OF DL/Models/Dtos/Messages/FromUserDto.cs b/OF DL.Core/Models/Dtos/Messages/FromUserDto.cs similarity index 100% rename from OF DL/Models/Dtos/Messages/FromUserDto.cs rename to OF DL.Core/Models/Dtos/Messages/FromUserDto.cs diff --git a/OF DL/Models/Dtos/Messages/InfoDto.cs b/OF DL.Core/Models/Dtos/Messages/InfoDto.cs similarity index 100% rename from OF DL/Models/Dtos/Messages/InfoDto.cs rename to OF DL.Core/Models/Dtos/Messages/InfoDto.cs diff --git a/OF DL/Models/Dtos/Messages/ListItemDto.cs b/OF DL.Core/Models/Dtos/Messages/ListItemDto.cs similarity index 100% rename from OF DL/Models/Dtos/Messages/ListItemDto.cs rename to OF DL.Core/Models/Dtos/Messages/ListItemDto.cs diff --git a/OF DL/Models/Dtos/Messages/ListsStateDto.cs b/OF DL.Core/Models/Dtos/Messages/ListsStateDto.cs similarity index 100% rename from OF DL/Models/Dtos/Messages/ListsStateDto.cs rename to OF DL.Core/Models/Dtos/Messages/ListsStateDto.cs diff --git a/OF DL/Models/Dtos/Messages/MediumDto.cs b/OF DL.Core/Models/Dtos/Messages/MediumDto.cs similarity index 100% rename from OF DL/Models/Dtos/Messages/MediumDto.cs rename to OF DL.Core/Models/Dtos/Messages/MediumDto.cs diff --git a/OF DL/Models/Dtos/Messages/MessagesDto.cs b/OF DL.Core/Models/Dtos/Messages/MessagesDto.cs similarity index 100% rename from OF DL/Models/Dtos/Messages/MessagesDto.cs rename to OF DL.Core/Models/Dtos/Messages/MessagesDto.cs diff --git a/OF DL/Models/Dtos/Messages/SingleMessageDto.cs b/OF DL.Core/Models/Dtos/Messages/SingleMessageDto.cs similarity index 100% rename from OF DL/Models/Dtos/Messages/SingleMessageDto.cs rename to OF DL.Core/Models/Dtos/Messages/SingleMessageDto.cs diff --git a/OF DL/Models/Dtos/Posts/InfoDto.cs b/OF DL.Core/Models/Dtos/Posts/InfoDto.cs similarity index 100% rename from OF DL/Models/Dtos/Posts/InfoDto.cs rename to OF DL.Core/Models/Dtos/Posts/InfoDto.cs diff --git a/OF DL/Models/Dtos/Posts/ListItemDto.cs b/OF DL.Core/Models/Dtos/Posts/ListItemDto.cs similarity index 100% rename from OF DL/Models/Dtos/Posts/ListItemDto.cs rename to OF DL.Core/Models/Dtos/Posts/ListItemDto.cs diff --git a/OF DL/Models/Dtos/Posts/MediumDto.cs b/OF DL.Core/Models/Dtos/Posts/MediumDto.cs similarity index 100% rename from OF DL/Models/Dtos/Posts/MediumDto.cs rename to OF DL.Core/Models/Dtos/Posts/MediumDto.cs diff --git a/OF DL/Models/Dtos/Posts/PostDto.cs b/OF DL.Core/Models/Dtos/Posts/PostDto.cs similarity index 100% rename from OF DL/Models/Dtos/Posts/PostDto.cs rename to OF DL.Core/Models/Dtos/Posts/PostDto.cs diff --git a/OF DL/Models/Dtos/Posts/SinglePostDto.cs b/OF DL.Core/Models/Dtos/Posts/SinglePostDto.cs similarity index 100% rename from OF DL/Models/Dtos/Posts/SinglePostDto.cs rename to OF DL.Core/Models/Dtos/Posts/SinglePostDto.cs diff --git a/OF DL/Models/Dtos/Purchased/FromUserDto.cs b/OF DL.Core/Models/Dtos/Purchased/FromUserDto.cs similarity index 100% rename from OF DL/Models/Dtos/Purchased/FromUserDto.cs rename to OF DL.Core/Models/Dtos/Purchased/FromUserDto.cs diff --git a/OF DL/Models/Dtos/Purchased/ListItemDto.cs b/OF DL.Core/Models/Dtos/Purchased/ListItemDto.cs similarity index 100% rename from OF DL/Models/Dtos/Purchased/ListItemDto.cs rename to OF DL.Core/Models/Dtos/Purchased/ListItemDto.cs diff --git a/OF DL/Models/Dtos/Purchased/PurchasedDto.cs b/OF DL.Core/Models/Dtos/Purchased/PurchasedDto.cs similarity index 100% rename from OF DL/Models/Dtos/Purchased/PurchasedDto.cs rename to OF DL.Core/Models/Dtos/Purchased/PurchasedDto.cs diff --git a/OF DL/Models/Dtos/Stories/MediumDto.cs b/OF DL.Core/Models/Dtos/Stories/MediumDto.cs similarity index 100% rename from OF DL/Models/Dtos/Stories/MediumDto.cs rename to OF DL.Core/Models/Dtos/Stories/MediumDto.cs diff --git a/OF DL/Models/Dtos/Stories/StoryDto.cs b/OF DL.Core/Models/Dtos/Stories/StoryDto.cs similarity index 100% rename from OF DL/Models/Dtos/Stories/StoryDto.cs rename to OF DL.Core/Models/Dtos/Stories/StoryDto.cs diff --git a/OF DL/Models/Dtos/Streams/InfoDto.cs b/OF DL.Core/Models/Dtos/Streams/InfoDto.cs similarity index 100% rename from OF DL/Models/Dtos/Streams/InfoDto.cs rename to OF DL.Core/Models/Dtos/Streams/InfoDto.cs diff --git a/OF DL/Models/Dtos/Streams/ListItemDto.cs b/OF DL.Core/Models/Dtos/Streams/ListItemDto.cs similarity index 100% rename from OF DL/Models/Dtos/Streams/ListItemDto.cs rename to OF DL.Core/Models/Dtos/Streams/ListItemDto.cs diff --git a/OF DL/Models/Dtos/Streams/MediumDto.cs b/OF DL.Core/Models/Dtos/Streams/MediumDto.cs similarity index 100% rename from OF DL/Models/Dtos/Streams/MediumDto.cs rename to OF DL.Core/Models/Dtos/Streams/MediumDto.cs diff --git a/OF DL/Models/Dtos/Streams/StreamsDto.cs b/OF DL.Core/Models/Dtos/Streams/StreamsDto.cs similarity index 100% rename from OF DL/Models/Dtos/Streams/StreamsDto.cs rename to OF DL.Core/Models/Dtos/Streams/StreamsDto.cs diff --git a/OF DL/Models/Dtos/Subscriptions/ListItemDto.cs b/OF DL.Core/Models/Dtos/Subscriptions/ListItemDto.cs similarity index 100% rename from OF DL/Models/Dtos/Subscriptions/ListItemDto.cs rename to OF DL.Core/Models/Dtos/Subscriptions/ListItemDto.cs diff --git a/OF DL/Models/Dtos/Subscriptions/ListsStateDto.cs b/OF DL.Core/Models/Dtos/Subscriptions/ListsStateDto.cs similarity index 100% rename from OF DL/Models/Dtos/Subscriptions/ListsStateDto.cs rename to OF DL.Core/Models/Dtos/Subscriptions/ListsStateDto.cs diff --git a/OF DL/Models/Dtos/Subscriptions/SubscribeDto.cs b/OF DL.Core/Models/Dtos/Subscriptions/SubscribeDto.cs similarity index 100% rename from OF DL/Models/Dtos/Subscriptions/SubscribeDto.cs rename to OF DL.Core/Models/Dtos/Subscriptions/SubscribeDto.cs diff --git a/OF DL/Models/Dtos/Subscriptions/SubscriptionsDto.cs b/OF DL.Core/Models/Dtos/Subscriptions/SubscriptionsDto.cs similarity index 100% rename from OF DL/Models/Dtos/Subscriptions/SubscriptionsDto.cs rename to OF DL.Core/Models/Dtos/Subscriptions/SubscriptionsDto.cs diff --git a/OF DL/Models/Dtos/Users/ListsStateDto.cs b/OF DL.Core/Models/Dtos/Users/ListsStateDto.cs similarity index 100% rename from OF DL/Models/Dtos/Users/ListsStateDto.cs rename to OF DL.Core/Models/Dtos/Users/ListsStateDto.cs diff --git a/OF DL/Models/Dtos/Users/SubscribeDto.cs b/OF DL.Core/Models/Dtos/Users/SubscribeDto.cs similarity index 100% rename from OF DL/Models/Dtos/Users/SubscribeDto.cs rename to OF DL.Core/Models/Dtos/Users/SubscribeDto.cs diff --git a/OF DL/Models/Dtos/Users/UserDto.cs b/OF DL.Core/Models/Dtos/Users/UserDto.cs similarity index 100% rename from OF DL/Models/Dtos/Users/UserDto.cs rename to OF DL.Core/Models/Dtos/Users/UserDto.cs diff --git a/OF DL/Models/DynamicRules.cs b/OF DL.Core/Models/DynamicRules.cs similarity index 100% rename from OF DL/Models/DynamicRules.cs rename to OF DL.Core/Models/DynamicRules.cs diff --git a/OF DL/Models/Entities/Archived/Archived.cs b/OF DL.Core/Models/Entities/Archived/Archived.cs similarity index 100% rename from OF DL/Models/Entities/Archived/Archived.cs rename to OF DL.Core/Models/Entities/Archived/Archived.cs diff --git a/OF DL/Models/Entities/Archived/ArchivedCollection.cs b/OF DL.Core/Models/Entities/Archived/ArchivedCollection.cs similarity index 100% rename from OF DL/Models/Entities/Archived/ArchivedCollection.cs rename to OF DL.Core/Models/Entities/Archived/ArchivedCollection.cs diff --git a/OF DL/Models/Entities/Archived/ListItem.cs b/OF DL.Core/Models/Entities/Archived/ListItem.cs similarity index 100% rename from OF DL/Models/Entities/Archived/ListItem.cs rename to OF DL.Core/Models/Entities/Archived/ListItem.cs diff --git a/OF DL/Models/Entities/Archived/Medium.cs b/OF DL.Core/Models/Entities/Archived/Medium.cs similarity index 100% rename from OF DL/Models/Entities/Archived/Medium.cs rename to OF DL.Core/Models/Entities/Archived/Medium.cs diff --git a/OF DL/Models/Entities/Common/Author.cs b/OF DL.Core/Models/Entities/Common/Author.cs similarity index 100% rename from OF DL/Models/Entities/Common/Author.cs rename to OF DL.Core/Models/Entities/Common/Author.cs diff --git a/OF DL/Models/Entities/Common/Dash.cs b/OF DL.Core/Models/Entities/Common/Dash.cs similarity index 100% rename from OF DL/Models/Entities/Common/Dash.cs rename to OF DL.Core/Models/Entities/Common/Dash.cs diff --git a/OF DL/Models/Entities/Common/Drm.cs b/OF DL.Core/Models/Entities/Common/Drm.cs similarity index 100% rename from OF DL/Models/Entities/Common/Drm.cs rename to OF DL.Core/Models/Entities/Common/Drm.cs diff --git a/OF DL/Models/Entities/Common/Files.cs b/OF DL.Core/Models/Entities/Common/Files.cs similarity index 100% rename from OF DL/Models/Entities/Common/Files.cs rename to OF DL.Core/Models/Entities/Common/Files.cs diff --git a/OF DL/Models/Entities/Common/FromUser.cs b/OF DL.Core/Models/Entities/Common/FromUser.cs similarity index 100% rename from OF DL/Models/Entities/Common/FromUser.cs rename to OF DL.Core/Models/Entities/Common/FromUser.cs diff --git a/OF DL/Models/Entities/Common/Full.cs b/OF DL.Core/Models/Entities/Common/Full.cs similarity index 100% rename from OF DL/Models/Entities/Common/Full.cs rename to OF DL.Core/Models/Entities/Common/Full.cs diff --git a/OF DL/Models/Entities/Common/Manifest.cs b/OF DL.Core/Models/Entities/Common/Manifest.cs similarity index 100% rename from OF DL/Models/Entities/Common/Manifest.cs rename to OF DL.Core/Models/Entities/Common/Manifest.cs diff --git a/OF DL/Models/Entities/Common/Preview.cs b/OF DL.Core/Models/Entities/Common/Preview.cs similarity index 100% rename from OF DL/Models/Entities/Common/Preview.cs rename to OF DL.Core/Models/Entities/Common/Preview.cs diff --git a/OF DL/Models/Entities/Common/Signature.cs b/OF DL.Core/Models/Entities/Common/Signature.cs similarity index 100% rename from OF DL/Models/Entities/Common/Signature.cs rename to OF DL.Core/Models/Entities/Common/Signature.cs diff --git a/OF DL/Models/Entities/Common/VideoSources.cs b/OF DL.Core/Models/Entities/Common/VideoSources.cs similarity index 100% rename from OF DL/Models/Entities/Common/VideoSources.cs rename to OF DL.Core/Models/Entities/Common/VideoSources.cs diff --git a/OF DL/Models/Entities/Highlights/HighlightMedia.cs b/OF DL.Core/Models/Entities/Highlights/HighlightMedia.cs similarity index 100% rename from OF DL/Models/Entities/Highlights/HighlightMedia.cs rename to OF DL.Core/Models/Entities/Highlights/HighlightMedia.cs diff --git a/OF DL/Models/Entities/Highlights/Highlights.cs b/OF DL.Core/Models/Entities/Highlights/Highlights.cs similarity index 100% rename from OF DL/Models/Entities/Highlights/Highlights.cs rename to OF DL.Core/Models/Entities/Highlights/Highlights.cs diff --git a/OF DL/Models/Entities/Highlights/ListItem.cs b/OF DL.Core/Models/Entities/Highlights/ListItem.cs similarity index 100% rename from OF DL/Models/Entities/Highlights/ListItem.cs rename to OF DL.Core/Models/Entities/Highlights/ListItem.cs diff --git a/OF DL/Models/Entities/Highlights/Medium.cs b/OF DL.Core/Models/Entities/Highlights/Medium.cs similarity index 100% rename from OF DL/Models/Entities/Highlights/Medium.cs rename to OF DL.Core/Models/Entities/Highlights/Medium.cs diff --git a/OF DL/Models/Entities/Highlights/Story.cs b/OF DL.Core/Models/Entities/Highlights/Story.cs similarity index 100% rename from OF DL/Models/Entities/Highlights/Story.cs rename to OF DL.Core/Models/Entities/Highlights/Story.cs diff --git a/OF DL/Models/Entities/Lists/UserList.cs b/OF DL.Core/Models/Entities/Lists/UserList.cs similarity index 100% rename from OF DL/Models/Entities/Lists/UserList.cs rename to OF DL.Core/Models/Entities/Lists/UserList.cs diff --git a/OF DL/Models/Entities/Lists/UserListItem.cs b/OF DL.Core/Models/Entities/Lists/UserListItem.cs similarity index 100% rename from OF DL/Models/Entities/Lists/UserListItem.cs rename to OF DL.Core/Models/Entities/Lists/UserListItem.cs diff --git a/OF DL/Models/Entities/Lists/UsersList.cs b/OF DL.Core/Models/Entities/Lists/UsersList.cs similarity index 100% rename from OF DL/Models/Entities/Lists/UsersList.cs rename to OF DL.Core/Models/Entities/Lists/UsersList.cs diff --git a/OF DL/Models/Entities/Messages/ListItem.cs b/OF DL.Core/Models/Entities/Messages/ListItem.cs similarity index 100% rename from OF DL/Models/Entities/Messages/ListItem.cs rename to OF DL.Core/Models/Entities/Messages/ListItem.cs diff --git a/OF DL/Models/Entities/Messages/Medium.cs b/OF DL.Core/Models/Entities/Messages/Medium.cs similarity index 100% rename from OF DL/Models/Entities/Messages/Medium.cs rename to OF DL.Core/Models/Entities/Messages/Medium.cs diff --git a/OF DL/Models/Entities/Messages/MessageCollection.cs b/OF DL.Core/Models/Entities/Messages/MessageCollection.cs similarity index 100% rename from OF DL/Models/Entities/Messages/MessageCollection.cs rename to OF DL.Core/Models/Entities/Messages/MessageCollection.cs diff --git a/OF DL/Models/Entities/Messages/Messages.cs b/OF DL.Core/Models/Entities/Messages/Messages.cs similarity index 100% rename from OF DL/Models/Entities/Messages/Messages.cs rename to OF DL.Core/Models/Entities/Messages/Messages.cs diff --git a/OF DL/Models/Entities/Messages/SingleMessage.cs b/OF DL.Core/Models/Entities/Messages/SingleMessage.cs similarity index 100% rename from OF DL/Models/Entities/Messages/SingleMessage.cs rename to OF DL.Core/Models/Entities/Messages/SingleMessage.cs diff --git a/OF DL/Models/Entities/Posts/ListItem.cs b/OF DL.Core/Models/Entities/Posts/ListItem.cs similarity index 100% rename from OF DL/Models/Entities/Posts/ListItem.cs rename to OF DL.Core/Models/Entities/Posts/ListItem.cs diff --git a/OF DL/Models/Entities/Posts/Medium.cs b/OF DL.Core/Models/Entities/Posts/Medium.cs similarity index 100% rename from OF DL/Models/Entities/Posts/Medium.cs rename to OF DL.Core/Models/Entities/Posts/Medium.cs diff --git a/OF DL/Models/Entities/Posts/Post.cs b/OF DL.Core/Models/Entities/Posts/Post.cs similarity index 100% rename from OF DL/Models/Entities/Posts/Post.cs rename to OF DL.Core/Models/Entities/Posts/Post.cs diff --git a/OF DL/Models/Entities/Posts/PostCollection.cs b/OF DL.Core/Models/Entities/Posts/PostCollection.cs similarity index 100% rename from OF DL/Models/Entities/Posts/PostCollection.cs rename to OF DL.Core/Models/Entities/Posts/PostCollection.cs diff --git a/OF DL/Models/Entities/Posts/SinglePost.cs b/OF DL.Core/Models/Entities/Posts/SinglePost.cs similarity index 100% rename from OF DL/Models/Entities/Posts/SinglePost.cs rename to OF DL.Core/Models/Entities/Posts/SinglePost.cs diff --git a/OF DL/Models/Entities/Posts/SinglePostCollection.cs b/OF DL.Core/Models/Entities/Posts/SinglePostCollection.cs similarity index 100% rename from OF DL/Models/Entities/Posts/SinglePostCollection.cs rename to OF DL.Core/Models/Entities/Posts/SinglePostCollection.cs diff --git a/OF DL/Models/Entities/Purchased/ListItem.cs b/OF DL.Core/Models/Entities/Purchased/ListItem.cs similarity index 100% rename from OF DL/Models/Entities/Purchased/ListItem.cs rename to OF DL.Core/Models/Entities/Purchased/ListItem.cs diff --git a/OF DL/Models/Entities/Purchased/PaidMessageCollection.cs b/OF DL.Core/Models/Entities/Purchased/PaidMessageCollection.cs similarity index 100% rename from OF DL/Models/Entities/Purchased/PaidMessageCollection.cs rename to OF DL.Core/Models/Entities/Purchased/PaidMessageCollection.cs diff --git a/OF DL/Models/Entities/Purchased/PaidPostCollection.cs b/OF DL.Core/Models/Entities/Purchased/PaidPostCollection.cs similarity index 100% rename from OF DL/Models/Entities/Purchased/PaidPostCollection.cs rename to OF DL.Core/Models/Entities/Purchased/PaidPostCollection.cs diff --git a/OF DL/Models/Entities/Purchased/Purchased.cs b/OF DL.Core/Models/Entities/Purchased/Purchased.cs similarity index 100% rename from OF DL/Models/Entities/Purchased/Purchased.cs rename to OF DL.Core/Models/Entities/Purchased/Purchased.cs diff --git a/OF DL/Models/Entities/Purchased/PurchasedTabCollection.cs b/OF DL.Core/Models/Entities/Purchased/PurchasedTabCollection.cs similarity index 100% rename from OF DL/Models/Entities/Purchased/PurchasedTabCollection.cs rename to OF DL.Core/Models/Entities/Purchased/PurchasedTabCollection.cs diff --git a/OF DL/Models/Entities/Purchased/SinglePaidMessageCollection.cs b/OF DL.Core/Models/Entities/Purchased/SinglePaidMessageCollection.cs similarity index 100% rename from OF DL/Models/Entities/Purchased/SinglePaidMessageCollection.cs rename to OF DL.Core/Models/Entities/Purchased/SinglePaidMessageCollection.cs diff --git a/OF DL/Models/Entities/Stories/Medium.cs b/OF DL.Core/Models/Entities/Stories/Medium.cs similarity index 100% rename from OF DL/Models/Entities/Stories/Medium.cs rename to OF DL.Core/Models/Entities/Stories/Medium.cs diff --git a/OF DL/Models/Entities/Stories/Stories.cs b/OF DL.Core/Models/Entities/Stories/Stories.cs similarity index 100% rename from OF DL/Models/Entities/Stories/Stories.cs rename to OF DL.Core/Models/Entities/Stories/Stories.cs diff --git a/OF DL/Models/Entities/Streams/ListItem.cs b/OF DL.Core/Models/Entities/Streams/ListItem.cs similarity index 100% rename from OF DL/Models/Entities/Streams/ListItem.cs rename to OF DL.Core/Models/Entities/Streams/ListItem.cs diff --git a/OF DL/Models/Entities/Streams/Medium.cs b/OF DL.Core/Models/Entities/Streams/Medium.cs similarity index 100% rename from OF DL/Models/Entities/Streams/Medium.cs rename to OF DL.Core/Models/Entities/Streams/Medium.cs diff --git a/OF DL/Models/Entities/Streams/Streams.cs b/OF DL.Core/Models/Entities/Streams/Streams.cs similarity index 100% rename from OF DL/Models/Entities/Streams/Streams.cs rename to OF DL.Core/Models/Entities/Streams/Streams.cs diff --git a/OF DL/Models/Entities/Streams/StreamsCollection.cs b/OF DL.Core/Models/Entities/Streams/StreamsCollection.cs similarity index 100% rename from OF DL/Models/Entities/Streams/StreamsCollection.cs rename to OF DL.Core/Models/Entities/Streams/StreamsCollection.cs diff --git a/OF DL/Models/Entities/Subscriptions/ListItem.cs b/OF DL.Core/Models/Entities/Subscriptions/ListItem.cs similarity index 100% rename from OF DL/Models/Entities/Subscriptions/ListItem.cs rename to OF DL.Core/Models/Entities/Subscriptions/ListItem.cs diff --git a/OF DL/Models/Entities/Subscriptions/Subscriptions.cs b/OF DL.Core/Models/Entities/Subscriptions/Subscriptions.cs similarity index 100% rename from OF DL/Models/Entities/Subscriptions/Subscriptions.cs rename to OF DL.Core/Models/Entities/Subscriptions/Subscriptions.cs diff --git a/OF DL/Models/Entities/Users/User.cs b/OF DL.Core/Models/Entities/Users/User.cs similarity index 100% rename from OF DL/Models/Entities/Users/User.cs rename to OF DL.Core/Models/Entities/Users/User.cs diff --git a/OF DL/Models/FileNameFormatConfig.cs b/OF DL.Core/Models/FileNameFormatConfig.cs similarity index 100% rename from OF DL/Models/FileNameFormatConfig.cs rename to OF DL.Core/Models/FileNameFormatConfig.cs diff --git a/OF DL/Models/IFileNameFormatConfig.cs b/OF DL.Core/Models/IFileNameFormatConfig.cs similarity index 100% rename from OF DL/Models/IFileNameFormatConfig.cs rename to OF DL.Core/Models/IFileNameFormatConfig.cs diff --git a/OF DL/Models/LatestReleaseApiResponse.cs b/OF DL.Core/Models/LatestReleaseApiResponse.cs similarity index 100% rename from OF DL/Models/LatestReleaseApiResponse.cs rename to OF DL.Core/Models/LatestReleaseApiResponse.cs diff --git a/OF DL/Models/Mappers/ArchivedMapper.cs b/OF DL.Core/Models/Mappers/ArchivedMapper.cs similarity index 100% rename from OF DL/Models/Mappers/ArchivedMapper.cs rename to OF DL.Core/Models/Mappers/ArchivedMapper.cs diff --git a/OF DL/Models/Mappers/HighlightsMapper.cs b/OF DL.Core/Models/Mappers/HighlightsMapper.cs similarity index 100% rename from OF DL/Models/Mappers/HighlightsMapper.cs rename to OF DL.Core/Models/Mappers/HighlightsMapper.cs diff --git a/OF DL/Models/Mappers/MessagesMapper.cs b/OF DL.Core/Models/Mappers/MessagesMapper.cs similarity index 100% rename from OF DL/Models/Mappers/MessagesMapper.cs rename to OF DL.Core/Models/Mappers/MessagesMapper.cs diff --git a/OF DL/Models/Mappers/PostMapper.cs b/OF DL.Core/Models/Mappers/PostMapper.cs similarity index 100% rename from OF DL/Models/Mappers/PostMapper.cs rename to OF DL.Core/Models/Mappers/PostMapper.cs diff --git a/OF DL/Models/Mappers/PurchasedMapper.cs b/OF DL.Core/Models/Mappers/PurchasedMapper.cs similarity index 100% rename from OF DL/Models/Mappers/PurchasedMapper.cs rename to OF DL.Core/Models/Mappers/PurchasedMapper.cs diff --git a/OF DL/Models/Mappers/StoriesMapper.cs b/OF DL.Core/Models/Mappers/StoriesMapper.cs similarity index 100% rename from OF DL/Models/Mappers/StoriesMapper.cs rename to OF DL.Core/Models/Mappers/StoriesMapper.cs diff --git a/OF DL/Models/Mappers/StreamsMapper.cs b/OF DL.Core/Models/Mappers/StreamsMapper.cs similarity index 100% rename from OF DL/Models/Mappers/StreamsMapper.cs rename to OF DL.Core/Models/Mappers/StreamsMapper.cs diff --git a/OF DL/Models/Mappers/SubscriptionsMapper.cs b/OF DL.Core/Models/Mappers/SubscriptionsMapper.cs similarity index 100% rename from OF DL/Models/Mappers/SubscriptionsMapper.cs rename to OF DL.Core/Models/Mappers/SubscriptionsMapper.cs diff --git a/OF DL/Models/Mappers/UserListsMapper.cs b/OF DL.Core/Models/Mappers/UserListsMapper.cs similarity index 100% rename from OF DL/Models/Mappers/UserListsMapper.cs rename to OF DL.Core/Models/Mappers/UserListsMapper.cs diff --git a/OF DL/Models/Mappers/UserMapper.cs b/OF DL.Core/Models/Mappers/UserMapper.cs similarity index 100% rename from OF DL/Models/Mappers/UserMapper.cs rename to OF DL.Core/Models/Mappers/UserMapper.cs diff --git a/OF DL/Models/OFDLRequest.cs b/OF DL.Core/Models/OFDLRequest.cs similarity index 100% rename from OF DL/Models/OFDLRequest.cs rename to OF DL.Core/Models/OFDLRequest.cs diff --git a/OF DL/Models/ShortDateConverter.cs b/OF DL.Core/Models/ShortDateConverter.cs similarity index 100% rename from OF DL/Models/ShortDateConverter.cs rename to OF DL.Core/Models/ShortDateConverter.cs diff --git a/OF DL/Models/StartupResult.cs b/OF DL.Core/Models/StartupResult.cs similarity index 100% rename from OF DL/Models/StartupResult.cs rename to OF DL.Core/Models/StartupResult.cs diff --git a/OF DL/Models/ToggleableConfigAttribute.cs b/OF DL.Core/Models/ToggleableConfigAttribute.cs similarity index 100% rename from OF DL/Models/ToggleableConfigAttribute.cs rename to OF DL.Core/Models/ToggleableConfigAttribute.cs diff --git a/OF DL.Core/OF DL.Core.csproj b/OF DL.Core/OF DL.Core.csproj new file mode 100644 index 0000000..88e3b0c --- /dev/null +++ b/OF DL.Core/OF DL.Core.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + OF_DL + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/OF DL/Services/ApiService.cs b/OF DL.Core/Services/ApiService.cs similarity index 100% rename from OF DL/Services/ApiService.cs rename to OF DL.Core/Services/ApiService.cs diff --git a/OF DL/Services/AuthService.cs b/OF DL.Core/Services/AuthService.cs similarity index 100% rename from OF DL/Services/AuthService.cs rename to OF DL.Core/Services/AuthService.cs diff --git a/OF DL/Services/ConfigService.cs b/OF DL.Core/Services/ConfigService.cs similarity index 100% rename from OF DL/Services/ConfigService.cs rename to OF DL.Core/Services/ConfigService.cs diff --git a/OF DL/Services/DBService.cs b/OF DL.Core/Services/DBService.cs similarity index 100% rename from OF DL/Services/DBService.cs rename to OF DL.Core/Services/DBService.cs diff --git a/OF DL/Services/DownloadOrchestrationService.cs b/OF DL.Core/Services/DownloadOrchestrationService.cs similarity index 100% rename from OF DL/Services/DownloadOrchestrationService.cs rename to OF DL.Core/Services/DownloadOrchestrationService.cs diff --git a/OF DL/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs similarity index 100% rename from OF DL/Services/DownloadService.cs rename to OF DL.Core/Services/DownloadService.cs diff --git a/OF DL/Services/FileNameService.cs b/OF DL.Core/Services/FileNameService.cs similarity index 100% rename from OF DL/Services/FileNameService.cs rename to OF DL.Core/Services/FileNameService.cs diff --git a/OF DL/Services/IAPIService.cs b/OF DL.Core/Services/IAPIService.cs similarity index 100% rename from OF DL/Services/IAPIService.cs rename to OF DL.Core/Services/IAPIService.cs diff --git a/OF DL/Services/IAuthService.cs b/OF DL.Core/Services/IAuthService.cs similarity index 100% rename from OF DL/Services/IAuthService.cs rename to OF DL.Core/Services/IAuthService.cs diff --git a/OF DL/Services/IConfigService.cs b/OF DL.Core/Services/IConfigService.cs similarity index 100% rename from OF DL/Services/IConfigService.cs rename to OF DL.Core/Services/IConfigService.cs diff --git a/OF DL/Services/IDBService.cs b/OF DL.Core/Services/IDBService.cs similarity index 100% rename from OF DL/Services/IDBService.cs rename to OF DL.Core/Services/IDBService.cs diff --git a/OF DL/Services/IDownloadEventHandler.cs b/OF DL.Core/Services/IDownloadEventHandler.cs similarity index 100% rename from OF DL/Services/IDownloadEventHandler.cs rename to OF DL.Core/Services/IDownloadEventHandler.cs diff --git a/OF DL/Services/IDownloadOrchestrationService.cs b/OF DL.Core/Services/IDownloadOrchestrationService.cs similarity index 100% rename from OF DL/Services/IDownloadOrchestrationService.cs rename to OF DL.Core/Services/IDownloadOrchestrationService.cs diff --git a/OF DL/Services/IDownloadService.cs b/OF DL.Core/Services/IDownloadService.cs similarity index 100% rename from OF DL/Services/IDownloadService.cs rename to OF DL.Core/Services/IDownloadService.cs diff --git a/OF DL/Services/IFileNameService.cs b/OF DL.Core/Services/IFileNameService.cs similarity index 100% rename from OF DL/Services/IFileNameService.cs rename to OF DL.Core/Services/IFileNameService.cs diff --git a/OF DL/Services/ILoggingService.cs b/OF DL.Core/Services/ILoggingService.cs similarity index 100% rename from OF DL/Services/ILoggingService.cs rename to OF DL.Core/Services/ILoggingService.cs diff --git a/OF DL/Services/IProgressReporter.cs b/OF DL.Core/Services/IProgressReporter.cs similarity index 100% rename from OF DL/Services/IProgressReporter.cs rename to OF DL.Core/Services/IProgressReporter.cs diff --git a/OF DL/Services/IStartupService.cs b/OF DL.Core/Services/IStartupService.cs similarity index 100% rename from OF DL/Services/IStartupService.cs rename to OF DL.Core/Services/IStartupService.cs diff --git a/OF DL/Services/IStatusReporter.cs b/OF DL.Core/Services/IStatusReporter.cs similarity index 100% rename from OF DL/Services/IStatusReporter.cs rename to OF DL.Core/Services/IStatusReporter.cs diff --git a/OF DL/Services/LoggingService.cs b/OF DL.Core/Services/LoggingService.cs similarity index 100% rename from OF DL/Services/LoggingService.cs rename to OF DL.Core/Services/LoggingService.cs diff --git a/OF DL/Services/StartupService.cs b/OF DL.Core/Services/StartupService.cs similarity index 100% rename from OF DL/Services/StartupService.cs rename to OF DL.Core/Services/StartupService.cs diff --git a/OF DL/Utils/HttpUtil.cs b/OF DL.Core/Utils/HttpUtil.cs similarity index 100% rename from OF DL/Utils/HttpUtil.cs rename to OF DL.Core/Utils/HttpUtil.cs diff --git a/OF DL/Utils/ThrottledStream.cs b/OF DL.Core/Utils/ThrottledStream.cs similarity index 100% rename from OF DL/Utils/ThrottledStream.cs rename to OF DL.Core/Utils/ThrottledStream.cs diff --git a/OF DL/Utils/XmlUtils.cs b/OF DL.Core/Utils/XmlUtils.cs similarity index 100% rename from OF DL/Utils/XmlUtils.cs rename to OF DL.Core/Utils/XmlUtils.cs diff --git a/OF DL/Widevine/CDM.cs b/OF DL.Core/Widevine/CDM.cs similarity index 100% rename from OF DL/Widevine/CDM.cs rename to OF DL.Core/Widevine/CDM.cs diff --git a/OF DL/Widevine/CDMApi.cs b/OF DL.Core/Widevine/CDMApi.cs similarity index 100% rename from OF DL/Widevine/CDMApi.cs rename to OF DL.Core/Widevine/CDMApi.cs diff --git a/OF DL/Widevine/CDMDevice.cs b/OF DL.Core/Widevine/CDMDevice.cs similarity index 100% rename from OF DL/Widevine/CDMDevice.cs rename to OF DL.Core/Widevine/CDMDevice.cs diff --git a/OF DL/Widevine/Constants.cs b/OF DL.Core/Widevine/Constants.cs similarity index 100% rename from OF DL/Widevine/Constants.cs rename to OF DL.Core/Widevine/Constants.cs diff --git a/OF DL/Widevine/ContentKey.cs b/OF DL.Core/Widevine/ContentKey.cs similarity index 100% rename from OF DL/Widevine/ContentKey.cs rename to OF DL.Core/Widevine/ContentKey.cs diff --git a/OF DL/Widevine/DerivedKeys.cs b/OF DL.Core/Widevine/DerivedKeys.cs similarity index 100% rename from OF DL/Widevine/DerivedKeys.cs rename to OF DL.Core/Widevine/DerivedKeys.cs diff --git a/OF DL/Widevine/PSSHBox.cs b/OF DL.Core/Widevine/PSSHBox.cs similarity index 100% rename from OF DL/Widevine/PSSHBox.cs rename to OF DL.Core/Widevine/PSSHBox.cs diff --git a/OF DL/Widevine/Session.cs b/OF DL.Core/Widevine/Session.cs similarity index 100% rename from OF DL/Widevine/Session.cs rename to OF DL.Core/Widevine/Session.cs diff --git a/OF DL/Widevine/WvProto2.cs b/OF DL.Core/Widevine/WvProto2.cs similarity index 100% rename from OF DL/Widevine/WvProto2.cs rename to OF DL.Core/Widevine/WvProto2.cs diff --git a/OF DL.sln b/OF DL.sln index 2434220..62e5364 100644 --- a/OF DL.sln +++ b/OF DL.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.5.33516.290 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OF DL", "OF DL\OF DL.csproj", "{318B2CE3-D046-4276-A2CF-89E6DF32F1B3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OF DL.Core", "OF DL.Core\OF DL.Core.csproj", "{7B8B6A26-6732-4B3A-AE62-1CE589DFF8F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {318B2CE3-D046-4276-A2CF-89E6DF32F1B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {318B2CE3-D046-4276-A2CF-89E6DF32F1B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {318B2CE3-D046-4276-A2CF-89E6DF32F1B3}.Release|Any CPU.Build.0 = Release|Any CPU + {7B8B6A26-6732-4B3A-AE62-1CE589DFF8F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B8B6A26-6732-4B3A-AE62-1CE589DFF8F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B8B6A26-6732-4B3A-AE62-1CE589DFF8F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B8B6A26-6732-4B3A-AE62-1CE589DFF8F2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/OF DL/OF DL.csproj b/OF DL/OF DL.csproj index 8bc38a9..63aeb86 100644 --- a/OF DL/OF DL.csproj +++ b/OF DL/OF DL.csproj @@ -14,19 +14,7 @@ - - - - - - - - - - - - - + From 9766636d04d0d197a06d5246db94f7a02aeec24d Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 23:14:52 -0600 Subject: [PATCH 32/55] Extract repeated mapper methods into a common mapper class --- .../Models/Entities/Purchased/ListItem.cs | 4 +- OF DL.Core/Models/Mappers/ArchivedMapper.cs | 72 +---------- OF DL.Core/Models/Mappers/CommonMapper.cs | 115 ++++++++++++++++++ OF DL.Core/Models/Mappers/HighlightsMapper.cs | 6 +- OF DL.Core/Models/Mappers/MessagesMapper.cs | 32 ++--- OF DL.Core/Models/Mappers/PostMapper.cs | 86 ++----------- OF DL.Core/Models/Mappers/PurchasedMapper.cs | 15 ++- OF DL.Core/Models/Mappers/StoriesMapper.cs | 5 +- OF DL.Core/Models/Mappers/StreamsMapper.cs | 43 +------ OF DL.Core/Models/Mappers/UserListsMapper.cs | 11 +- 10 files changed, 161 insertions(+), 228 deletions(-) create mode 100644 OF DL.Core/Models/Mappers/CommonMapper.cs diff --git a/OF DL.Core/Models/Entities/Purchased/ListItem.cs b/OF DL.Core/Models/Entities/Purchased/ListItem.cs index 45561be..9e21aca 100644 --- a/OF DL.Core/Models/Entities/Purchased/ListItem.cs +++ b/OF DL.Core/Models/Entities/Purchased/ListItem.cs @@ -17,9 +17,9 @@ public class ListItem public List? Preview { get; set; } - public FromUser FromUser { get; set; } = new(); + public FromUser? FromUser { get; set; } - public Author Author { get; set; } = new(); + public Author? Author { get; set; } public long Id { get; set; } diff --git a/OF DL.Core/Models/Mappers/ArchivedMapper.cs b/OF DL.Core/Models/Mappers/ArchivedMapper.cs index 2ef0d09..ab722c6 100644 --- a/OF DL.Core/Models/Mappers/ArchivedMapper.cs +++ b/OF DL.Core/Models/Mappers/ArchivedMapper.cs @@ -28,7 +28,7 @@ public static class ArchivedMapper { Id = dto.Id, PostedAt = dto.PostedAt, - Author = MapAuthor(dto.Author), + Author = CommonMapper.MapAuthor(dto.Author), Text = dto.Text, Price = dto.Price, IsOpened = dto.IsOpened, @@ -37,25 +37,8 @@ public static class ArchivedMapper Preview = dto.Preview }; - private static Entities.Common.Author? MapAuthor(AuthorDto? dto) - { - if (dto == null) - { - return null; - } - - return new Entities.Common.Author { Id = dto.Id }; - } - - private static List? MapMedia(List? media) - { - if (media == null) - { - return null; - } - - return media.Select(MapMedium).ToList(); - } + private static List? MapMedia(List? media) => + media == null ? null : media.Select(MapMedium).ToList(); private static Medium MapMedium(MediumDto dto) => new() @@ -67,50 +50,7 @@ public static class ArchivedMapper Preview = dto.Preview }; - private static Entities.Common.Files? MapFiles(FilesDto? dto) - { - if (dto == null) - { - return null; - } - - return new Entities.Common.Files { Full = MapFull(dto.Full), Drm = MapDrm(dto.Drm) }; - } - - private static Entities.Common.Full? MapFull(FullDto? dto) => - dto == null ? null : new Entities.Common.Full { Url = dto.Url }; - - private static Entities.Common.Drm? MapDrm(DrmDto? dto) - { - if (dto == null) - { - return null; - } - - return new Entities.Common.Drm - { - Manifest = MapManifest(dto.Manifest), Signature = MapSignature(dto.Signature) - }; - } - - private static Entities.Common.Manifest? MapManifest(ManifestDto? dto) => - dto == null ? null : new Entities.Common.Manifest { Dash = dto.Dash }; - - private static Entities.Common.Signature? MapSignature(SignatureDto? dto) => - dto == null ? null : new Entities.Common.Signature { Dash = MapDash(dto.Dash) }; - - private static Entities.Common.Dash? MapDash(DashDto? dto) - { - if (dto == null) - { - return null; - } - - return new Entities.Common.Dash - { - CloudFrontPolicy = dto.CloudFrontPolicy, - CloudFrontSignature = dto.CloudFrontSignature, - CloudFrontKeyPairId = dto.CloudFrontKeyPairId - }; - } + private static Entities.Common.Files? MapFiles(FilesDto? dto) => dto == null + ? null + : new Entities.Common.Files { Full = CommonMapper.MapFull(dto.Full), Drm = CommonMapper.MapDrm(dto.Drm) }; } diff --git a/OF DL.Core/Models/Mappers/CommonMapper.cs b/OF DL.Core/Models/Mappers/CommonMapper.cs new file mode 100644 index 0000000..3312a2f --- /dev/null +++ b/OF DL.Core/Models/Mappers/CommonMapper.cs @@ -0,0 +1,115 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Entities.Common; + +namespace OF_DL.Models.Mappers; + +public static class CommonMapper +{ + public static Author? MapAuthor(AuthorDto? dto) + { + if (dto == null || dto.Id == 0) + { + return null; + } + + return new Author { Id = dto.Id }; + } + + private static Dash? MapDash(DashDto? dto) + { + if (dto == null || ( + string.IsNullOrEmpty(dto.CloudFrontPolicy) && + string.IsNullOrEmpty(dto.CloudFrontSignature) && + string.IsNullOrEmpty(dto.CloudFrontKeyPairId))) + { + return null; + } + + return new Dash + { + CloudFrontPolicy = dto.CloudFrontPolicy, + CloudFrontSignature = dto.CloudFrontSignature, + CloudFrontKeyPairId = dto.CloudFrontKeyPairId + }; + } + + public static Drm? MapDrm(DrmDto? dto) + { + if (dto == null || (dto.Manifest == new ManifestDto() && dto.Signature == new SignatureDto())) + { + return null; + } + + return new Drm { Manifest = MapManifest(dto.Manifest), Signature = MapSignature(dto.Signature) }; + } + + public static Files? MapFiles(FilesDto? dto) + { + if (dto == null) + { + return null; + } + + Full? full = MapFull(dto.Full); + Preview? preview = MapPreview(dto.Preview); + Drm? drm = MapDrm(dto.Drm); + + if (full == null && preview == null && drm == null) + { + return null; + } + + return new Files { Full = MapFull(dto.Full), Preview = MapPreview(dto.Preview), Drm = MapDrm(dto.Drm) }; + } + + public static Full? MapFull(FullDto? dto) + { + if (dto == null || string.IsNullOrEmpty(dto.Url)) + { + return null; + } + + return new Full { Url = dto.Url }; + } + + public static Manifest? MapManifest(ManifestDto? dto) + { + if (dto == null || string.IsNullOrEmpty(dto.Dash)) + { + return null; + } + + return new Manifest { Dash = dto.Dash }; + } + + public static Preview? MapPreview(PreviewDto? dto) + { + if (dto == null || string.IsNullOrEmpty(dto.Url)) + { + return null; + } + + return new Preview { Url = dto.Url }; + } + + public static Signature? MapSignature(SignatureDto? dto) + { + if (dto == null) + { + return null; + } + + Dash? dash = MapDash(dto.Dash); + return dash == null ? null : new Signature { Dash = dash }; + } + + public static VideoSources? MapVideoSources(VideoSourcesDto? dto) + { + if (dto == null || (string.IsNullOrEmpty(dto._240) && string.IsNullOrEmpty(dto._720))) + { + return null; + } + + return new VideoSources { _240 = dto._240, _720 = dto._720 }; + } +} diff --git a/OF DL.Core/Models/Mappers/HighlightsMapper.cs b/OF DL.Core/Models/Mappers/HighlightsMapper.cs index 450ea00..60efa17 100644 --- a/OF DL.Core/Models/Mappers/HighlightsMapper.cs +++ b/OF DL.Core/Models/Mappers/HighlightsMapper.cs @@ -59,8 +59,6 @@ public static class HighlightsMapper Files = MapFiles(dto.Files) }; - private static Files? MapFiles(FilesDto? dto) => dto == null ? null : new Files { Full = MapFull(dto.Full) }; - - private static Full? MapFull(FullDto? dto) => - dto == null ? null : new Full { Url = dto.Url }; + private static Files? MapFiles(FilesDto? dto) => + dto == null ? null : new Files { Full = CommonMapper.MapFull(dto.Full) }; } diff --git a/OF DL.Core/Models/Mappers/MessagesMapper.cs b/OF DL.Core/Models/Mappers/MessagesMapper.cs index 52b9a8e..c416ab1 100644 --- a/OF DL.Core/Models/Mappers/MessagesMapper.cs +++ b/OF DL.Core/Models/Mappers/MessagesMapper.cs @@ -63,35 +63,17 @@ public static class MessagesMapper private static List? MapMedia(List? media) => media?.Select(MapMedium).ToList(); - private static FromUser? MapFromUser(FromUserDto? dto) => - dto?.Id == null ? null : new FromUser { Id = (long)dto.Id }; - - private static Files? MapFiles(FilesDto? dto) => - dto == null ? null : new Files { Full = MapFull(dto.Full), Drm = MapDrm(dto.Drm) }; - - private static Full? MapFull(FullDto? dto) => dto == null ? null : new Full { Url = dto.Url }; - - private static Drm? MapDrm(DrmDto? dto) => dto == null - ? null - : new Drm { Manifest = MapManifest(dto.Manifest), Signature = MapSignature(dto.Signature) }; - - private static Manifest? MapManifest(ManifestDto? dto) => dto == null ? null : new Manifest { Dash = dto.Dash }; - - private static Signature? MapSignature(SignatureDto? dto) => - dto == null ? null : new Signature { Dash = MapDash(dto.Dash) }; - - private static Dash? MapDash(DashDto? dto) + private static FromUser? MapFromUser(FromUserDto? dto) { - if (dto == null) + if (dto?.Id == null || dto.Id == 0) { return null; } - return new Dash - { - CloudFrontPolicy = dto.CloudFrontPolicy, - CloudFrontSignature = dto.CloudFrontSignature, - CloudFrontKeyPairId = dto.CloudFrontKeyPairId - }; + return new FromUser { Id = dto.Id.Value }; } + + private static Files? MapFiles(FilesDto? dto) => dto == null + ? null + : new Files { Full = CommonMapper.MapFull(dto.Full), Drm = CommonMapper.MapDrm(dto.Drm) }; } diff --git a/OF DL.Core/Models/Mappers/PostMapper.cs b/OF DL.Core/Models/Mappers/PostMapper.cs index 00c1b8f..1634ce8 100644 --- a/OF DL.Core/Models/Mappers/PostMapper.cs +++ b/OF DL.Core/Models/Mappers/PostMapper.cs @@ -36,7 +36,7 @@ public static class PostMapper mapped.Id = dto.Id; mapped.PostedAt = dto.PostedAt; - mapped.Author = MapSingleAuthor(dto.Author); + mapped.Author = CommonMapper.MapAuthor(dto.Author); mapped.Text = dto.Text; mapped.RawText = dto.RawText; mapped.IsOpened = dto.IsOpened; @@ -53,7 +53,7 @@ public static class PostMapper { Id = dto.Id, PostedAt = dto.PostedAt, - Author = MapAuthor(dto.Author), + Author = CommonMapper.MapAuthor(dto.Author), Text = dto.Text, RawText = dto.RawText, IsOpened = dto.IsOpened, @@ -63,12 +63,6 @@ public static class PostMapper Preview = dto.Preview }; - private static Author? MapAuthor(AuthorDto? dto) => - dto == null ? null : new Author { Id = dto.Id }; - - private static Author? MapSingleAuthor(AuthorDto? dto) => - dto == null ? null : new Author { Id = dto.Id }; - private static List? MapMedia(List? media) => media?.Select(MapMedium).ToList(); @@ -84,39 +78,12 @@ public static class PostMapper private static Files? MapFiles(FilesDto? dto) => dto == null ? null - : new Files { Full = MapFull(dto.Full), Preview = MapPreview(dto.Preview), Drm = MapDrm(dto.Drm) }; - - private static Full? MapFull(FullDto? dto) => - dto == null || string.IsNullOrEmpty(dto.Url) ? null : new Full { Url = dto.Url }; - - private static Preview? MapPreview(PreviewDto? dto) => - dto == null || string.IsNullOrEmpty(dto.Url) ? null : new Preview { Url = dto.Url }; - - private static Drm MapDrm(DrmDto? dto) => new() - { - Manifest = MapManifest(dto?.Manifest), Signature = MapSignature(dto?.Signature) - }; - - private static Manifest? MapManifest(ManifestDto? dto) => - dto == null ? null : new Manifest { Dash = dto.Dash }; - - private static Signature? MapSignature(SignatureDto? dto) => - dto == null ? null : new Signature { Dash = MapDash(dto.Dash) }; - - private static Dash? MapDash(DashDto? dto) - { - if (dto == null) + : new Files { - return null; - } - - return new Dash - { - CloudFrontPolicy = dto.CloudFrontPolicy, - CloudFrontSignature = dto.CloudFrontSignature, - CloudFrontKeyPairId = dto.CloudFrontKeyPairId + Full = CommonMapper.MapFull(dto.Full), + Preview = CommonMapper.MapPreview(dto.Preview), + Drm = CommonMapper.MapDrm(dto.Drm) }; - } private static List? MapSingleMedia(List? media) => media?.Select(MapSingleMedium).ToList(); @@ -129,7 +96,7 @@ public static class PostMapper CanView = dto.CanView, Preview = dto.Preview, Files = MapSingleFiles(dto.Files), - VideoSources = MapVideoSources(dto.VideoSources) + VideoSources = CommonMapper.MapVideoSources(dto.VideoSources) }; private static Files? MapSingleFiles(FilesDto? dto) @@ -141,42 +108,9 @@ public static class PostMapper return new Files { - Full = MapSingleFull(dto.Full), Preview = MapSinglePreview(dto.Preview), Drm = MapSingleDrm(dto.Drm) + Full = CommonMapper.MapFull(dto.Full), + Preview = CommonMapper.MapPreview(dto.Preview), + Drm = CommonMapper.MapDrm(dto.Drm) }; } - - private static Full? MapSingleFull(FullDto? dto) => - dto == null || string.IsNullOrEmpty(dto.Url) ? null : new Full { Url = dto.Url }; - - private static Preview? MapSinglePreview(PreviewDto? dto) => - dto == null || string.IsNullOrEmpty(dto.Url) ? null : new Preview { Url = dto.Url }; - - private static Drm? MapSingleDrm(DrmDto? dto) => dto == null - ? null - : new Drm { Manifest = MapSingleManifest(dto.Manifest), Signature = MapSingleSignature(dto.Signature) }; - - private static Manifest? MapSingleManifest(ManifestDto? dto) => - dto == null ? null : new Manifest { Dash = dto.Dash }; - - private static Signature? MapSingleSignature(SignatureDto? dto) => - dto == null ? null : new Signature { Dash = MapSingleDash(dto.Dash) }; - - private static Dash? MapSingleDash(DashDto? dto) - { - if (dto == null) - { - return null; - } - - return new Dash - { - CloudFrontPolicy = dto.CloudFrontPolicy, - CloudFrontSignature = dto.CloudFrontSignature, - CloudFrontKeyPairId = dto.CloudFrontKeyPairId - }; - } - - private static VideoSources? MapVideoSources(VideoSourcesDto? dto) => dto == null - ? null - : new VideoSources { _240 = dto._240, _720 = dto._720 }; } diff --git a/OF DL.Core/Models/Mappers/PurchasedMapper.cs b/OF DL.Core/Models/Mappers/PurchasedMapper.cs index 43b5830..9ace9e5 100644 --- a/OF DL.Core/Models/Mappers/PurchasedMapper.cs +++ b/OF DL.Core/Models/Mappers/PurchasedMapper.cs @@ -1,5 +1,4 @@ using OF_DL.Models.Dtos.Purchased; -using OF_DL.Models.Dtos.Common; using MessageDtos = OF_DL.Models.Dtos.Messages; using CommonEntities = OF_DL.Models.Entities.Common; using MessageEntities = OF_DL.Models.Entities.Messages; @@ -42,14 +41,18 @@ public static class PurchasedMapper Previews = dto.Previews, Preview = dto.Preview, FromUser = MapFromUser(dto.FromUser), - Author = MapAuthor(dto.Author) + Author = CommonMapper.MapAuthor(dto.Author) }; - private static CommonEntities.FromUser MapFromUser(FromUserDto? dto) => - dto == null ? new CommonEntities.FromUser() : new CommonEntities.FromUser { Id = dto.Id }; + private static CommonEntities.FromUser? MapFromUser(FromUserDto? dto) + { + if (dto == null || dto.Id == 0) + { + return null; + } - private static CommonEntities.Author MapAuthor(AuthorDto? dto) => - dto == null ? new CommonEntities.Author() : new CommonEntities.Author { Id = dto.Id }; + return new CommonEntities.FromUser { Id = dto.Id }; + } private static List? MapMedia(List? media) => media?.Select(MessagesMapper.MapMedium).ToList(); diff --git a/OF DL.Core/Models/Mappers/StoriesMapper.cs b/OF DL.Core/Models/Mappers/StoriesMapper.cs index 4ccdd03..70ef4aa 100644 --- a/OF DL.Core/Models/Mappers/StoriesMapper.cs +++ b/OF DL.Core/Models/Mappers/StoriesMapper.cs @@ -26,7 +26,6 @@ public static class StoriesMapper Files = MapFiles(dto.Files) }; - private static Files MapFiles(FilesDto? dto) => new() { Full = MapFull(dto?.Full) }; - - private static Full MapFull(FullDto? dto) => new() { Url = dto?.Url }; + private static Files MapFiles(FilesDto? dto) => + new() { Full = CommonMapper.MapFull(dto?.Full) }; } diff --git a/OF DL.Core/Models/Mappers/StreamsMapper.cs b/OF DL.Core/Models/Mappers/StreamsMapper.cs index 3b3e30d..caa61ee 100644 --- a/OF DL.Core/Models/Mappers/StreamsMapper.cs +++ b/OF DL.Core/Models/Mappers/StreamsMapper.cs @@ -29,7 +29,7 @@ public static class StreamsMapper { Id = dto.Id, PostedAt = dto.PostedAt, - Author = MapAuthor(dto.Author), + Author = CommonMapper.MapAuthor(dto.Author), Text = dto.Text, RawText = dto.RawText, Price = dto.Price, @@ -39,9 +39,6 @@ public static class StreamsMapper Preview = dto.Preview }; - private static Author? MapAuthor(AuthorDto? dto) => - dto == null ? null : new Author { Id = dto.Id }; - private static List? MapMedia(List? media) => media?.Select(MapMedium).ToList(); @@ -55,7 +52,7 @@ public static class StreamsMapper return null; } - Full? full = MapFull(dto.Full); + Full? full = CommonMapper.MapFull(dto.Full); Drm? drm = MapDrm(dto.Drm); if (full == null && drm == null) @@ -66,43 +63,15 @@ public static class StreamsMapper return new Files { Full = full, Drm = drm }; } - private static Full? MapFull(FullDto? dto) => - dto == null || string.IsNullOrEmpty(dto.Url) ? null : new Full { Url = dto.Url }; - private static Drm? MapDrm(DrmDto? dto) { - if (dto?.Manifest == null || string.IsNullOrEmpty(dto.Manifest.Dash)) + Manifest? manifest = CommonMapper.MapManifest(dto?.Manifest); + if (manifest == null) { return null; } - Dash? dash = MapDash(dto.Signature.Dash); - if (dash == null) - { - return null; - } - - return new Drm - { - Manifest = new Manifest { Dash = dto.Manifest.Dash }, Signature = new Signature { Dash = dash } - }; - } - - private static Dash? MapDash(DashDto? dto) - { - if (dto == null || - string.IsNullOrEmpty(dto.CloudFrontPolicy) || - string.IsNullOrEmpty(dto.CloudFrontSignature) || - string.IsNullOrEmpty(dto.CloudFrontKeyPairId)) - { - return null; - } - - return new Dash - { - CloudFrontPolicy = dto.CloudFrontPolicy, - CloudFrontSignature = dto.CloudFrontSignature, - CloudFrontKeyPairId = dto.CloudFrontKeyPairId - }; + Signature? signature = CommonMapper.MapSignature(dto?.Signature); + return signature == null ? null : new Drm { Manifest = manifest, Signature = signature }; } } diff --git a/OF DL.Core/Models/Mappers/UserListsMapper.cs b/OF DL.Core/Models/Mappers/UserListsMapper.cs index 1ff4e32..7f0557a 100644 --- a/OF DL.Core/Models/Mappers/UserListsMapper.cs +++ b/OF DL.Core/Models/Mappers/UserListsMapper.cs @@ -22,15 +22,8 @@ public static class UserListsMapper return mapped; } - public static List FromDto(List? dto) - { - if (dto == null) - { - return []; - } - - return dto.Select(MapUsersList).ToList(); - } + public static List FromDto(List? dto) => + dto == null ? [] : dto.Select(MapUsersList).ToList(); private static UserListItem MapListItem(UserListItemDto dto) => new() { Id = dto.Id, Name = dto.Name }; From 974b0d4d7a6c561254dbb8c42ef20ea76ca1482e Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 23:56:09 -0600 Subject: [PATCH 33/55] Organize remaining model classes into similar namespaces --- OF DL.Core/Helpers/VersionHelper.cs | 2 +- OF DL.Core/Models/CDRMProjectRequest.cs | 17 ------------ OF DL.Core/Models/{ => Config}/Config.cs | 7 ++++- .../Models/{ => Config}/CreatorConfig.cs | 2 +- .../{ => Config}/FileNameFormatConfig.cs | 2 +- .../{ => Config}/IFileNameFormatConfig.cs | 2 +- .../{ => Config}/ToggleableConfigAttribute.cs | 2 +- .../{ => Downloads}/CreatorDownloadResult.cs | 2 +- .../Models/{ => Downloads}/DownloadResult.cs | 2 +- .../Models/{ => OfdlApi}/DynamicRules.cs | 2 +- .../{ => OfdlApi}/LatestReleaseApiResponse.cs | 2 +- .../Models/{ => OfdlApi}/OFDLRequest.cs | 5 ++-- OF DL.Core/Models/ShortDateConverter.cs | 8 ------ OF DL.Core/Services/ApiService.cs | 3 ++- OF DL.Core/Services/ConfigService.cs | 5 ++-- .../Services/DownloadOrchestrationService.cs | 3 ++- OF DL.Core/Services/DownloadService.cs | 1 + OF DL.Core/Services/IConfigService.cs | 2 +- OF DL.Core/Services/IDownloadEventHandler.cs | 2 +- .../Services/IDownloadOrchestrationService.cs | 3 +-- OF DL.Core/Services/IDownloadService.cs | 2 +- OF DL.Core/Services/StartupService.cs | 1 + OF DL/CLI/SpectreDownloadEventHandler.cs | 27 +++++-------------- OF DL/Program.cs | 2 ++ 24 files changed, 38 insertions(+), 68 deletions(-) delete mode 100644 OF DL.Core/Models/CDRMProjectRequest.cs rename OF DL.Core/Models/{ => Config}/Config.cs (97%) rename OF DL.Core/Models/{ => Config}/CreatorConfig.cs (90%) rename OF DL.Core/Models/{ => Config}/FileNameFormatConfig.cs (90%) rename OF DL.Core/Models/{ => Config}/IFileNameFormatConfig.cs (88%) rename OF DL.Core/Models/{ => Config}/ToggleableConfigAttribute.cs (76%) rename OF DL.Core/Models/{ => Downloads}/CreatorDownloadResult.cs (94%) rename OF DL.Core/Models/{ => Downloads}/DownloadResult.cs (96%) rename OF DL.Core/Models/{ => OfdlApi}/DynamicRules.cs (95%) rename OF DL.Core/Models/{ => OfdlApi}/LatestReleaseApiResponse.cs (83%) rename OF DL.Core/Models/{ => OfdlApi}/OFDLRequest.cs (72%) delete mode 100644 OF DL.Core/Models/ShortDateConverter.cs diff --git a/OF DL.Core/Helpers/VersionHelper.cs b/OF DL.Core/Helpers/VersionHelper.cs index 2b3903b..17e01a1 100644 --- a/OF DL.Core/Helpers/VersionHelper.cs +++ b/OF DL.Core/Helpers/VersionHelper.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json; -using OF_DL.Models; +using OF_DL.Models.OfdlApi; using Serilog; namespace OF_DL.Helpers; diff --git a/OF DL.Core/Models/CDRMProjectRequest.cs b/OF DL.Core/Models/CDRMProjectRequest.cs deleted file mode 100644 index 07ae8fe..0000000 --- a/OF DL.Core/Models/CDRMProjectRequest.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Newtonsoft.Json; - -namespace OF_DL.Models; - -// ReSharper disable once InconsistentNaming -public class CDRMProjectRequest -{ - [JsonProperty("pssh")] public string Pssh { get; set; } = ""; - - [JsonProperty("licurl")] public string LicenseUrl { get; set; } = ""; - - [JsonProperty("headers")] public string Headers { get; set; } = ""; - - [JsonProperty("cookies")] public string Cookies { get; set; } = ""; - - [JsonProperty("data")] public string Data { get; set; } = ""; -} diff --git a/OF DL.Core/Models/Config.cs b/OF DL.Core/Models/Config/Config.cs similarity index 97% rename from OF DL.Core/Models/Config.cs rename to OF DL.Core/Models/Config/Config.cs index 7e83618..a655711 100644 --- a/OF DL.Core/Models/Config.cs +++ b/OF DL.Core/Models/Config/Config.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json.Converters; using OF_DL.Enumerations; using Serilog; -namespace OF_DL.Models; +namespace OF_DL.Models.Config; public class Config : IFileNameFormatConfig { @@ -147,4 +147,9 @@ public class Config : IFileNameFormatConfig return combinedFilenameFormatConfig; } + + private class ShortDateConverter : IsoDateTimeConverter + { + public ShortDateConverter() => DateTimeFormat = "yyyy-MM-dd"; + } } diff --git a/OF DL.Core/Models/CreatorConfig.cs b/OF DL.Core/Models/Config/CreatorConfig.cs similarity index 90% rename from OF DL.Core/Models/CreatorConfig.cs rename to OF DL.Core/Models/Config/CreatorConfig.cs index 6c7ad40..89ba399 100644 --- a/OF DL.Core/Models/CreatorConfig.cs +++ b/OF DL.Core/Models/Config/CreatorConfig.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models; +namespace OF_DL.Models.Config; public class CreatorConfig : IFileNameFormatConfig { diff --git a/OF DL.Core/Models/FileNameFormatConfig.cs b/OF DL.Core/Models/Config/FileNameFormatConfig.cs similarity index 90% rename from OF DL.Core/Models/FileNameFormatConfig.cs rename to OF DL.Core/Models/Config/FileNameFormatConfig.cs index 2b1b0b2..2a14123 100644 --- a/OF DL.Core/Models/FileNameFormatConfig.cs +++ b/OF DL.Core/Models/Config/FileNameFormatConfig.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models; +namespace OF_DL.Models.Config; public class FileNameFormatConfig : IFileNameFormatConfig { diff --git a/OF DL.Core/Models/IFileNameFormatConfig.cs b/OF DL.Core/Models/Config/IFileNameFormatConfig.cs similarity index 88% rename from OF DL.Core/Models/IFileNameFormatConfig.cs rename to OF DL.Core/Models/Config/IFileNameFormatConfig.cs index 7622eee..5800f88 100644 --- a/OF DL.Core/Models/IFileNameFormatConfig.cs +++ b/OF DL.Core/Models/Config/IFileNameFormatConfig.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models; +namespace OF_DL.Models.Config; public interface IFileNameFormatConfig { diff --git a/OF DL.Core/Models/ToggleableConfigAttribute.cs b/OF DL.Core/Models/Config/ToggleableConfigAttribute.cs similarity index 76% rename from OF DL.Core/Models/ToggleableConfigAttribute.cs rename to OF DL.Core/Models/Config/ToggleableConfigAttribute.cs index c0e6c83..4adeba0 100644 --- a/OF DL.Core/Models/ToggleableConfigAttribute.cs +++ b/OF DL.Core/Models/Config/ToggleableConfigAttribute.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models; +namespace OF_DL.Models.Config; [AttributeUsage(AttributeTargets.Property)] internal class ToggleableConfigAttribute : Attribute; diff --git a/OF DL.Core/Models/CreatorDownloadResult.cs b/OF DL.Core/Models/Downloads/CreatorDownloadResult.cs similarity index 94% rename from OF DL.Core/Models/CreatorDownloadResult.cs rename to OF DL.Core/Models/Downloads/CreatorDownloadResult.cs index 16de4cb..d686e1b 100644 --- a/OF DL.Core/Models/CreatorDownloadResult.cs +++ b/OF DL.Core/Models/Downloads/CreatorDownloadResult.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models; +namespace OF_DL.Models.Downloads; public class CreatorDownloadResult { diff --git a/OF DL.Core/Models/DownloadResult.cs b/OF DL.Core/Models/Downloads/DownloadResult.cs similarity index 96% rename from OF DL.Core/Models/DownloadResult.cs rename to OF DL.Core/Models/Downloads/DownloadResult.cs index 1f2a6bc..a38b314 100644 --- a/OF DL.Core/Models/DownloadResult.cs +++ b/OF DL.Core/Models/Downloads/DownloadResult.cs @@ -1,4 +1,4 @@ -namespace OF_DL.Models; +namespace OF_DL.Models.Downloads; /// /// Represents the result of a download operation. diff --git a/OF DL.Core/Models/DynamicRules.cs b/OF DL.Core/Models/OfdlApi/DynamicRules.cs similarity index 95% rename from OF DL.Core/Models/DynamicRules.cs rename to OF DL.Core/Models/OfdlApi/DynamicRules.cs index 412547a..ccdd635 100644 --- a/OF DL.Core/Models/DynamicRules.cs +++ b/OF DL.Core/Models/OfdlApi/DynamicRules.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Models; +namespace OF_DL.Models.OfdlApi; public class DynamicRules { diff --git a/OF DL.Core/Models/LatestReleaseApiResponse.cs b/OF DL.Core/Models/OfdlApi/LatestReleaseApiResponse.cs similarity index 83% rename from OF DL.Core/Models/LatestReleaseApiResponse.cs rename to OF DL.Core/Models/OfdlApi/LatestReleaseApiResponse.cs index 3cf66ee..4197a89 100644 --- a/OF DL.Core/Models/LatestReleaseApiResponse.cs +++ b/OF DL.Core/Models/OfdlApi/LatestReleaseApiResponse.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace OF_DL.Models; +namespace OF_DL.Models.OfdlApi; public class LatestReleaseApiResponse { diff --git a/OF DL.Core/Models/OFDLRequest.cs b/OF DL.Core/Models/OfdlApi/OFDLRequest.cs similarity index 72% rename from OF DL.Core/Models/OFDLRequest.cs rename to OF DL.Core/Models/OfdlApi/OFDLRequest.cs index 52fef56..0f943e2 100644 --- a/OF DL.Core/Models/OFDLRequest.cs +++ b/OF DL.Core/Models/OfdlApi/OFDLRequest.cs @@ -1,9 +1,8 @@ using Newtonsoft.Json; -namespace OF_DL.Models; +namespace OF_DL.Models.OfdlApi; -// ReSharper disable once InconsistentNaming -public class OFDLRequest +public class OfdlRequest { [JsonProperty("pssh")] public string Pssh { get; set; } = ""; diff --git a/OF DL.Core/Models/ShortDateConverter.cs b/OF DL.Core/Models/ShortDateConverter.cs deleted file mode 100644 index 7a9f62c..0000000 --- a/OF DL.Core/Models/ShortDateConverter.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Newtonsoft.Json.Converters; - -namespace OF_DL.Models; - -public class ShortDateConverter : IsoDateTimeConverter -{ - public ShortDateConverter() => DateTimeFormat = "yyyy-MM-dd"; -} diff --git a/OF DL.Core/Services/ApiService.cs b/OF DL.Core/Services/ApiService.cs index c460b17..89b6eab 100644 --- a/OF DL.Core/Services/ApiService.cs +++ b/OF DL.Core/Services/ApiService.cs @@ -28,6 +28,7 @@ using StreamEntities = OF_DL.Models.Entities.Streams; using SubscriptionEntities = OF_DL.Models.Entities.Subscriptions; using UserEntities = OF_DL.Models.Entities.Users; using OF_DL.Models.Mappers; +using OF_DL.Models.OfdlApi; using OF_DL.Widevine; using Serilog; using static OF_DL.Utils.HttpUtil; @@ -3147,7 +3148,7 @@ public class ApiService(IAuthService authService, IConfigService configService, HttpClient client = new(); int attempt = 0; - OFDLRequest ofdlRequest = new() + OfdlRequest ofdlRequest = new() { Pssh = pssh, LicenseUrl = licenceUrl, Headers = JsonConvert.SerializeObject(drmHeaders) }; diff --git a/OF DL.Core/Services/ConfigService.cs b/OF DL.Core/Services/ConfigService.cs index 9c43b0a..d34b3d3 100644 --- a/OF DL.Core/Services/ConfigService.cs +++ b/OF DL.Core/Services/ConfigService.cs @@ -1,13 +1,12 @@ using System.Reflection; using System.Text; using Akka.Configuration; -using Akka.Configuration.Hocon; using Newtonsoft.Json; -using OF_DL.Models; using OF_DL.Enumerations; +using OF_DL.Models.Config; using OF_DL.Utils; using Serilog; -using Config = OF_DL.Models.Config; +using Config = OF_DL.Models.Config.Config; namespace OF_DL.Services; diff --git a/OF DL.Core/Services/DownloadOrchestrationService.cs b/OF DL.Core/Services/DownloadOrchestrationService.cs index cc8902c..288d7c4 100644 --- a/OF DL.Core/Services/DownloadOrchestrationService.cs +++ b/OF DL.Core/Services/DownloadOrchestrationService.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json.Linq; using OF_DL.Enumerations; -using OF_DL.Models; +using OF_DL.Models.Config; +using OF_DL.Models.Downloads; using Serilog; using PostEntities = OF_DL.Models.Entities.Posts; using PurchasedEntities = OF_DL.Models.Entities.Purchased; diff --git a/OF DL.Core/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs index b9cc5ac..eaee01f 100644 --- a/OF DL.Core/Services/DownloadService.cs +++ b/OF DL.Core/Services/DownloadService.cs @@ -5,6 +5,7 @@ using FFmpeg.NET; using FFmpeg.NET.Events; using OF_DL.Models; using OF_DL.Enumerations; +using OF_DL.Models.Downloads; using ArchivedEntities = OF_DL.Models.Entities.Archived; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; diff --git a/OF DL.Core/Services/IConfigService.cs b/OF DL.Core/Services/IConfigService.cs index 6a31ba3..088e3fd 100644 --- a/OF DL.Core/Services/IConfigService.cs +++ b/OF DL.Core/Services/IConfigService.cs @@ -1,4 +1,4 @@ -using OF_DL.Models; +using OF_DL.Models.Config; namespace OF_DL.Services; diff --git a/OF DL.Core/Services/IDownloadEventHandler.cs b/OF DL.Core/Services/IDownloadEventHandler.cs index d40788c..602f6c2 100644 --- a/OF DL.Core/Services/IDownloadEventHandler.cs +++ b/OF DL.Core/Services/IDownloadEventHandler.cs @@ -1,4 +1,4 @@ -using OF_DL.Models; +using OF_DL.Models.Downloads; namespace OF_DL.Services; diff --git a/OF DL.Core/Services/IDownloadOrchestrationService.cs b/OF DL.Core/Services/IDownloadOrchestrationService.cs index 44e48c6..f471bfd 100644 --- a/OF DL.Core/Services/IDownloadOrchestrationService.cs +++ b/OF DL.Core/Services/IDownloadOrchestrationService.cs @@ -1,5 +1,4 @@ -using OF_DL.Models; -using UserEntities = OF_DL.Models.Entities.Users; +using OF_DL.Models.Downloads; namespace OF_DL.Services; diff --git a/OF DL.Core/Services/IDownloadService.cs b/OF DL.Core/Services/IDownloadService.cs index 5f95a14..b474f60 100644 --- a/OF DL.Core/Services/IDownloadService.cs +++ b/OF DL.Core/Services/IDownloadService.cs @@ -1,4 +1,4 @@ -using OF_DL.Models; +using OF_DL.Models.Downloads; using ArchivedEntities = OF_DL.Models.Entities.Archived; using MessageEntities = OF_DL.Models.Entities.Messages; using PostEntities = OF_DL.Models.Entities.Posts; diff --git a/OF DL.Core/Services/StartupService.cs b/OF DL.Core/Services/StartupService.cs index 6c0cfad..6769345 100644 --- a/OF DL.Core/Services/StartupService.cs +++ b/OF DL.Core/Services/StartupService.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using Newtonsoft.Json; using OF_DL.Helpers; using OF_DL.Models; +using OF_DL.Models.OfdlApi; using Serilog; using WidevineConstants = OF_DL.Widevine.Constants; diff --git a/OF DL/CLI/SpectreDownloadEventHandler.cs b/OF DL/CLI/SpectreDownloadEventHandler.cs index a34cd8a..8758324 100644 --- a/OF DL/CLI/SpectreDownloadEventHandler.cs +++ b/OF DL/CLI/SpectreDownloadEventHandler.cs @@ -1,4 +1,4 @@ -using OF_DL.Models; +using OF_DL.Models.Downloads; using OF_DL.Services; using Spectre.Console; @@ -43,26 +43,18 @@ public class SpectreDownloadEventHandler : IDownloadEventHandler return result; } - public void OnContentFound(string contentType, int mediaCount, int objectCount) - { + public void OnContentFound(string contentType, int mediaCount, int objectCount) => AnsiConsole.Markup($"[red]Found {mediaCount} Media from {objectCount} {Markup.Escape(contentType)}\n[/]"); - } - public void OnNoContentFound(string contentType) - { + public void OnNoContentFound(string contentType) => AnsiConsole.Markup($"[red]Found 0 {Markup.Escape(contentType)}\n[/]"); - } - public void OnDownloadComplete(string contentType, DownloadResult result) - { + public void OnDownloadComplete(string contentType, DownloadResult result) => AnsiConsole.Markup( $"[red]{Markup.Escape(contentType)} Already Downloaded: {result.ExistingDownloads} New {Markup.Escape(contentType)} Downloaded: {result.NewDownloads}[/]\n"); - } - public void OnUserStarting(string username) - { + public void OnUserStarting(string username) => AnsiConsole.Markup($"[red]\nScraping Data for {Markup.Escape(username)}\n[/]"); - } public void OnUserComplete(string username, CreatorDownloadResult result) { @@ -90,15 +82,10 @@ public class SpectreDownloadEventHandler : IDownloadEventHandler AnsiConsole.Markup("\n"); } - public void OnScrapeComplete(TimeSpan elapsed) - { + public void OnScrapeComplete(TimeSpan elapsed) => AnsiConsole.Markup($"[green]Scrape Completed in {elapsed.TotalMinutes:0.00} minutes\n[/]"); - } - public void OnMessage(string message) - { - AnsiConsole.Markup($"[red]{Markup.Escape(message)}\n[/]"); - } + public void OnMessage(string message) => AnsiConsole.Markup($"[red]{Markup.Escape(message)}\n[/]"); private static ProgressColumn[] GetProgressColumns(bool showScrapeSize) { diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 550ab6f..31339ba 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -3,6 +3,8 @@ 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; From ed06a5e514c213bc2cb942987069da88db73e9f1 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Mon, 9 Feb 2026 23:56:25 -0600 Subject: [PATCH 34/55] Delete unused classes --- OF DL.Core/Helpers/DownloadContext.cs | 25 ------------------------- OF DL.Core/Helpers/IFileNameHelper.cs | 9 --------- 2 files changed, 34 deletions(-) delete mode 100644 OF DL.Core/Helpers/DownloadContext.cs delete mode 100644 OF DL.Core/Helpers/IFileNameHelper.cs diff --git a/OF DL.Core/Helpers/DownloadContext.cs b/OF DL.Core/Helpers/DownloadContext.cs deleted file mode 100644 index 1a32612..0000000 --- a/OF DL.Core/Helpers/DownloadContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -using OF_DL.Models; -using OF_DL.Services; - -namespace OF_DL.Helpers; - -internal interface IDownloadContext -{ - public IFileNameFormatConfig FileNameFormatConfig { get; } - public IAPIService ApiService { get; } - public IDBService DBService { get; } - public IDownloadService DownloadService { get; } -} - -internal class DownloadContext( - IFileNameFormatConfig fileNameFormatConfig, - IAPIService apiService, - IDBService dBService, - IDownloadService downloadService) - : IDownloadContext -{ - public IAPIService ApiService { get; } = apiService; - public IDBService DBService { get; } = dBService; - public IDownloadService DownloadService { get; } = downloadService; - public IFileNameFormatConfig FileNameFormatConfig { get; } = fileNameFormatConfig; -} diff --git a/OF DL.Core/Helpers/IFileNameHelper.cs b/OF DL.Core/Helpers/IFileNameHelper.cs deleted file mode 100644 index 0ea39f1..0000000 --- a/OF DL.Core/Helpers/IFileNameHelper.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace OF_DL.Helpers; - -public interface IFileNameHelper -{ - Task BuildFilename(string fileFormat, Dictionary values); - - Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, - string username, Dictionary? users); -} From f7f1fad92ddc8da20bc5a7e54c157139491d3646 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 00:19:21 -0600 Subject: [PATCH 35/55] Update AGENTS.md with updated models namespaces --- AGENTS.md | 73 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0b780fd..1803141 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This repo is **OF DL** (also known as OF-DL), a C# console app that downloads me This document is for AI agents helping developers modify the project. It focuses on architecture, data flow, and the most important change points. -**Quick Flow** +## Quick Flow 1. `Program.Main` builds DI, loads `config.conf`, and runs the interactive flow. 2. `StartupService.CheckVersionAsync` checks the latest release tag (`OFDLV*`) from `git.ofdl.tools` when not in DEBUG. @@ -16,64 +16,77 @@ most important change points. 6. `DownloadOrchestrationService` selects creators, prepares folders/DBs, and calls `DownloadService` per media type. 7. `DownloadService` downloads media, handles DRM, and records metadata in SQLite. -**Project Layout** +## Project Layout - `OF DL/Program.cs` orchestrates startup, config/auth loading, and the interactive flow (CLI entrypoint). - `OF DL/CLI/` contains Spectre.Console UI helpers and progress reporting (CLI-only). - `OF DL.Core/Services/` contains application services (API, auth, download, config, DB, startup, logging, filenames). -- `OF DL.Core/Models/` holds configuration, auth, DTOs, entities, and mapping helpers. +- `OF DL.Core/Models/` holds configuration, auth, API request/response models, downloads/startup results, DTOs, + entities, and mapping helpers. - `OF DL.Core/Widevine/` implements Widevine CDM handling and key derivation. -- `OF DL.Core/Helpers/`, `OF DL.Core/Utils/`, `OF DL.Core/Crypto/`, `OF DL.Core/Enumerations/` contain shared core logic. +- `OF DL.Core/Helpers/`, `OF DL.Core/Utils/`, `OF DL.Core/Crypto/`, `OF DL.Core/Enumerations/` contain shared core + logic. - `docs/` and `mkdocs.yml` define the documentation site. - `site/` is generated MkDocs output and should not be edited by hand. - `docker/` contains container entrypoint and supervisor configuration. -**Key Services** +## Key Services - `ApiService` (`OF DL.Core/Services/ApiService.cs`) builds signed headers, performs HTTP requests, and maps DTOs to entities. It also handles DRM-related calls like MPD/PSSH extraction and license requests. -- `AuthService` (`OF DL.Core/Services/AuthService.cs`) loads `auth.json` or performs browser-based login with PuppeteerSharp, +- `AuthService` (`OF DL.Core/Services/AuthService.cs`) loads `auth.json` or performs browser-based login with + PuppeteerSharp, then persists auth. It also normalizes cookies. -- `ConfigService` (`OF DL.Core/Services/ConfigService.cs`) loads `config.conf` (HOCON), migrates legacy `config.json`, and +- `ConfigService` (`OF DL.Core/Services/ConfigService.cs`) loads `config.conf` (HOCON), migrates legacy `config.json`, + and updates global settings (logging, text sanitization). -- `DownloadService` (`OF DL.Core/Services/DownloadService.cs`) downloads all media (images, video, audio) and handles DRM +- `DownloadService` (`OF DL.Core/Services/DownloadService.cs`) downloads all media (images, video, audio) and handles + DRM video decryption and FFmpeg execution. - `DownloadOrchestrationService` (`OF DL.Core/Services/DownloadOrchestrationService.cs`) coordinates user selection, subscription lists, per-user folder prep, and per-media-type download execution. -- `DBService` (`OF DL.Core/Services/DBService.cs`) manages SQLite metadata DBs for downloaded media and a `users.db` index. +- `DBService` (`OF DL.Core/Services/DBService.cs`) manages SQLite metadata DBs for downloaded media and a `users.db` + index. - `StartupService` (`OF DL.Core/Services/StartupService.cs`) validates FFmpeg, rules.json, Widevine device files, and performs release version checks. -- `LoggingService` (`OF DL.Core/Services/LoggingService.cs`) writes logs to `logs/OFDL.txt` and updates log level based on +- `LoggingService` (`OF DL.Core/Services/LoggingService.cs`) writes logs to `logs/OFDL.txt` and updates log level based + on + config. +- `FileNameService` (`OF DL.Core/Services/FileNameService.cs`) formats filenames using the custom format rules from config. -- `FileNameService` (`OF DL.Core/Services/FileNameService.cs`) formats filenames using the custom format rules from config. -**Models: DTOs vs Entities** +## Models - DTOs live under `OF DL.Core/Models/Dtos/` and mirror API response JSON. - Entities live under `OF DL.Core/Models/Entities/` and represent the internal domain used by download logic. - Mappers in `OF DL.Core/Models/Mappers/` convert DTOs into entities to isolate API changes from downstream logic. +- Non-DTO/Entity models are grouped by concern under `OF DL.Core/Models/Api/`, `Auth/`, `Config/`, `Downloads/`, + and `Startup/`. +- Classes in `OF DL.Core/Models/OfdlApi/` mirror request and response JOSN OF DL APIs (custom and gitea) +- Classes in `OF DL.Core/Models/Config/` are used for reading and storing application configuration +- Classes in `OF DL.Core/Models/Downloads/` contain counts and application state for downloads -**Configuration** +## Configuration - Primary config file is `config.conf` (HOCON). `ConfigService` migrates legacy `config.json` if found and creates a default `config.conf` if missing. -- `Config` lives in `OF DL.Core/Models/Config.cs` and is populated by `ConfigService.LoadConfigFromFileAsync`. +- `Config` lives in `OF DL.Core/Models/Config/Config.cs` and is populated by `ConfigService.LoadConfigFromFileAsync`. - `ConfigService.UpdateConfig` is the central place where runtime config changes are applied (logging level and text sanitization). - CLI flag `--non-interactive` forces non-interactive mode; `ConfigService.IsCliNonInteractive` and `Config.NonInteractiveMode` both gate prompts. - FFmpeg path is read from `config.conf`, `auth.json`, or auto-detected from PATH/current directory. -**Runtime Files (relative to the working directory)** +## Runtime Files (relative to the working directory) - `config.conf`, `auth.json`, and `rules.json` are loaded from the current working directory. - `cdm/` (Widevine device files), `chrome-data/` (Puppeteer profile), and `logs/` are created under the working directory. - `users.db` is stored at the working directory root. -**Authentication Flow** +## Authentication Flow -- Auth data is stored in `auth.json` using the `Auth` model in `OF DL.Core/Models/Auth.cs`. +- Auth data is stored in `auth.json` using the `Auth` model in `OF DL.Core/Models/Auth/Auth.cs`. - `AuthService.LoadFromBrowserAsync` launches Chrome via PuppeteerSharp, waits for login, then extracts `auth_id` and `sess` cookies, `bcTokenSha` from localStorage (used as `X_BC`), and `USER_AGENT` from the browser. - `AuthService.ValidateCookieString` rewrites the cookie string so it contains only `auth_id` and `sess` and ensures a @@ -85,15 +98,15 @@ Environment variables used by auth: - `OFDL_DOCKER=true` toggles Docker-specific instructions and browser flags. - `OFDL_PUPPETEER_EXECUTABLE_PATH` overrides the Chromium path for PuppeteerSharp. -**Dynamic Rules and Signature Headers** +## Dynamic Rules and Signature Headers - All OnlyFans API requests use dynamic headers from `ApiService.GetDynamicHeaders`. - Dynamic rules are fetched from `https://git.ofdl.tools/sim0n00ps/dynamic-rules/raw/branch/main/rules.json` with fallback to local `rules.json` in the current working directory. The repo ships `OF DL/rules.json` as the default rules file. - Cache durations: 15 minutes for remote rules, 5 minutes for local rules. -- `DynamicRules` shape is defined in `OF DL.Core/Models/DynamicRules.cs` and includes `app-token`, `static_param`, `prefix`, - `suffix`, `checksum_constant`, and `checksum_indexes`. +- `DynamicRules` shape is defined in `OF DL.Core/Models/Api/DynamicRules.cs` and includes `app-token`, `static_param`, + `prefix`, `suffix`, `checksum_constant`, and `checksum_indexes`. Signature algorithm in `GetDynamicHeaders`: @@ -106,7 +119,7 @@ Headers included in signed requests: - `app-token`, `sign`, `time`, `user-id`, `user-agent`, `x-bc`, `cookie`. -**Widevine CDM and DRM Decryption** +## Widevine CDM and DRM Decryption - Runtime Widevine device files are expected at `cdm/devices/chrome_1610/device_client_id_blob` and `cdm/devices/chrome_1610/device_private_key` (relative to the working directory). Paths are defined in @@ -126,7 +139,7 @@ Two decryption paths exist: `DownloadService.DownloadDrmMedia` runs FFmpeg with `-cenc_decryption_key`, CloudFront cookies, and auth cookies/user-agent. Output is written to `{filename}_source.mp4`, then moved and recorded in SQLite. -**Download Paths, Data, and Logs** +## Download Paths, Data, and Logs - Default download root is `__user_data__/sites/OnlyFans/{username}` when `DownloadPath` is blank. This is computed in `DownloadOrchestrationService.ResolveDownloadPath`. @@ -135,13 +148,13 @@ cookies/user-agent. Output is written to `{filename}_source.mp4`, then moved and - Logs are written to `logs/OFDL.txt` (rolling daily); FFmpeg report files are also written under `logs/` when debug logging is enabled. -**Docs (MkDocs)** +## Docs (MkDocs) - Docs source lives under `docs/` and configuration is in `mkdocs.yml`. - Build the site with `mkdocs build --clean` (outputs to `site/`). - Preview locally with `mkdocs serve`. -**CI/CD (Gitea Workflows)** +## CI/CD (Gitea Workflows) - `/.gitea/workflows/publish-docs.yml` builds and deploys docs on tag pushes matching `OFDLV*` and on manual dispatch. - `/.gitea/workflows/publish-docker.yml` builds multi-arch Docker images on `OFDLV*` tags and pushes to the Gitea @@ -149,7 +162,7 @@ cookies/user-agent. Output is written to `{filename}_source.mp4`, then moved and - `/.gitea/workflows/publish-release.yml` publishes Windows and Linux builds on `OFDLV*` tags and creates a draft release. -**Docker Image** +## Docker Image - Built via `/.gitea/workflows/publish-docker.yml` on tag pushes `OFDLV*`. - Image tags: `git.ofdl.tools/sim0n00ps/of-dl:latest` and `git.ofdl.tools/sim0n00ps/of-dl:{version}`. @@ -158,13 +171,13 @@ cookies/user-agent. Output is written to `{filename}_source.mp4`, then moved and - Runtime uses `/config` as the working directory and `/data` for downloads; `docker/entrypoint.sh` seeds `/config/config.conf` and `/config/rules.json` from `/default-config`. -**Release Checklist** +## Release Checklist 1. Update docs under `docs/` and verify locally with `mkdocs build --clean` or `mkdocs serve`. 2. Tag the release as `OFDLV{version}` and push the tag. 3. Verify the draft release artifact and publish the release in Gitea. -**Coding Style (from .editorconfig)** +## Coding Style (from .editorconfig) - Indentation: 4 spaces by default, 2 spaces for XML/YAML/project files. No tabs. - Line endings: LF for `*.sh`, CRLF for `*.cmd`/`*.bat`. @@ -175,18 +188,18 @@ cookies/user-agent. Output is written to `{filename}_source.mp4`, then moved and - `const` fields should be PascalCase. - Prefer braces for control blocks and expression-bodied members (silent preferences). -**Where to Look First** +## Where to Look First - `OF DL/Program.cs` for the execution path and menu flow. - `OF DL.Core/Services/ApiService.cs` for OF API calls and header signing. - `OF DL.Core/Services/DownloadService.cs` for downloads and DRM handling. - `OF DL.Core/Services/DownloadOrchestrationService.cs` for creator selection and flow control. - `OF DL.Core/Widevine/` for CDM key generation and license parsing. -- `OF DL.Core/Models/Config.cs` and `OF DL.Core/Services/ConfigService.cs` for config shape and parsing. +- `OF DL.Core/Models/Config/Config.cs` and `OF DL.Core/Services/ConfigService.cs` for config shape and parsing. - `OF DL.Core/Services/AuthService.cs` for user-facing authentication behavior and browser login flow. - `docs/` for public documentation; update docs whenever user-facing behavior or configuration changes. -Documentation updates for common changes: +## Documentation updates for common changes: - Config option added/removed/changed in `Config` or `config.conf`: update `docs/config/all-configuration-options.md` ( full spec), `docs/config/configuration.md` (organized list), and `docs/config/custom-filename-formats.md` if filename From 487de58274f172dcba13a34a5848b0c91816b70e Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 02:08:51 -0600 Subject: [PATCH 36/55] Address compiler warning --- OF DL.Core/Crypto/Padding.cs | 13 +- OF DL.Core/Models/Config/Config.cs | 8 +- OF DL.Core/Models/Dtos/Common/DrmDto.cs | 1 - OF DL.Core/Services/ApiService.cs | 1156 ++++++++--------- OF DL.Core/Services/AuthService.cs | 7 +- OF DL.Core/Services/ConfigService.cs | 26 +- OF DL.Core/Services/DBService.cs | 46 +- .../Services/DownloadOrchestrationService.cs | 40 +- OF DL.Core/Services/DownloadService.cs | 156 ++- OF DL.Core/Services/StartupService.cs | 4 +- OF DL.Core/Utils/ThrottledStream.cs | 2 +- OF DL.Core/Utils/XmlUtils.cs | 1 + OF DL.Core/Widevine/CDM.cs | 4 +- OF DL.Core/Widevine/CDMApi.cs | 10 +- OF DL.Core/Widevine/CDMDevice.cs | 4 + .../Widevine/{PSSHBox.cs => PsshBox.cs} | 26 +- 16 files changed, 720 insertions(+), 784 deletions(-) rename OF DL.Core/Widevine/{PSSHBox.cs => PsshBox.cs} (64%) diff --git a/OF DL.Core/Crypto/Padding.cs b/OF DL.Core/Crypto/Padding.cs index 5f4641b..2927482 100644 --- a/OF DL.Core/Crypto/Padding.cs +++ b/OF DL.Core/Crypto/Padding.cs @@ -37,7 +37,7 @@ public class Padding return result; } - public static byte[]? AddPSSPadding(byte[] hash) + public static byte[] AddPssPadding(byte[] hash) { int modBits = 2048; int hLen = 20; @@ -49,10 +49,11 @@ public class Padding lmask = (lmask >> 1) | 0x80; } - if (emLen < hLen + hLen + 2) - { - return null; - } + // Commented out since the condition will always be false while emLen = 256 and hLen = 20 + // if (emLen < hLen + hLen + 2) + // { + // return null; + // } byte[] salt = new byte[hLen]; new Random().NextBytes(salt); @@ -102,7 +103,7 @@ public class Padding db[i] = (byte)(maskedDB[i] ^ dbMask[i]); } - int onePos = BitConverter.ToString(db[hLen..]).Replace("-", "").IndexOf("01") / 2; + int onePos = BitConverter.ToString(db[hLen..]).Replace("-", "").IndexOf("01", StringComparison.Ordinal) / 2; byte[] unpadded = db[(hLen + onePos + 1)..]; return unpadded; diff --git a/OF DL.Core/Models/Config/Config.cs b/OF DL.Core/Models/Config/Config.cs index a655711..50323d8 100644 --- a/OF DL.Core/Models/Config/Config.cs +++ b/OF DL.Core/Models/Config/Config.cs @@ -115,22 +115,22 @@ public class Config : IFileNameFormatConfig if (CreatorConfigs.TryGetValue(username, out CreatorConfig? creatorConfig)) { - if (creatorConfig?.PaidPostFileNameFormat != null) + if (creatorConfig.PaidPostFileNameFormat != null) { combinedFilenameFormatConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat; } - if (creatorConfig?.PostFileNameFormat != null) + if (creatorConfig.PostFileNameFormat != null) { combinedFilenameFormatConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat; } - if (creatorConfig?.PaidMessageFileNameFormat != null) + if (creatorConfig.PaidMessageFileNameFormat != null) { combinedFilenameFormatConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat; } - if (creatorConfig?.MessageFileNameFormat != null) + if (creatorConfig.MessageFileNameFormat != null) { combinedFilenameFormatConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat; } diff --git a/OF DL.Core/Models/Dtos/Common/DrmDto.cs b/OF DL.Core/Models/Dtos/Common/DrmDto.cs index 25271be..1290908 100644 --- a/OF DL.Core/Models/Dtos/Common/DrmDto.cs +++ b/OF DL.Core/Models/Dtos/Common/DrmDto.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using OF_DL.Models.Dtos.Archived; namespace OF_DL.Models.Dtos.Common; diff --git a/OF DL.Core/Services/ApiService.cs b/OF DL.Core/Services/ApiService.cs index 89b6eab..eb57808 100644 --- a/OF DL.Core/Services/ApiService.cs +++ b/OF DL.Core/Services/ApiService.cs @@ -1,11 +1,11 @@ using System.Globalization; using System.Security.Cryptography; using System.Text; -using System.Text.Json; using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Models; +using OF_DL.Models.Entities.Common; using OF_DL.Enumerations; using ArchivedDtos = OF_DL.Models.Dtos.Archived; using HighlightDtos = OF_DL.Models.Dtos.Highlights; @@ -416,26 +416,30 @@ public class ApiService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + if (string.IsNullOrWhiteSpace(body)) + { + Log.Warning("GetMedia returned empty response for {Endpoint}", endpoint); + return return_urls; + } if (mediatype == MediaType.Stories) { Log.Debug("Media Stories - " + endpoint); List? storiesDto = - JsonConvert.DeserializeObject>(body, s_mJsonSerializerSettings); + DeserializeJson>(body, s_mJsonSerializerSettings); List stories = StoriesMapper.FromDto(storiesDto); foreach (StoryEntities.Stories story in stories) { - if (story.Media[0].CreatedAt.HasValue) + DateTime? storyCreatedAt = story.Media.Count > 0 ? story.Media[0].CreatedAt : null; + if (storyCreatedAt.HasValue) { - await dbService.AddStory(folder, story.Id, "", "0", false, false, - story.Media[0].CreatedAt.Value); + await dbService.AddStory(folder, story.Id, "", "0", false, false, storyCreatedAt.Value); } else if (story.CreatedAt.HasValue) { - await dbService.AddStory(folder, story.Id, "", "0", false, false, - story.CreatedAt.Value); + await dbService.AddStory(folder, story.Id, "", "0", false, false, story.CreatedAt.Value); } else { @@ -446,22 +450,20 @@ public class ApiService(IAuthService authService, IConfigService configService, { foreach (StoryEntities.Medium medium in story.Media) { - if (medium.Files.Full == null || medium.Files.Full.Url == null) + string? mediaUrl = medium.Files.Full?.Url; + if (string.IsNullOrEmpty(mediaUrl)) { continue; } - string? mediaType = medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null; + string? mediaType = ResolveMediaType(medium.Type); if (mediaType == null) { continue; } - await dbService.AddMedia(folder, medium.Id, story.Id, medium.Files.Full.Url, null, null, - null, "Stories", - mediaType, false, false, null); + await dbService.AddMedia(folder, medium.Id, story.Id, mediaUrl, null, null, null, + "Stories", mediaType, false, false, null); if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -483,12 +485,9 @@ public class ApiService(IAuthService authService, IConfigService configService, continue; } - if (medium.CanView) + if (medium.CanView && !return_urls.ContainsKey(medium.Id)) { - if (!return_urls.ContainsKey(medium.Id)) - { - return_urls.Add(medium.Id, medium.Files.Full.Url); - } + return_urls.Add(medium.Id, mediaUrl); } } } @@ -498,7 +497,7 @@ public class ApiService(IAuthService authService, IConfigService configService, { List highlightIds = []; HighlightDtos.HighlightsDto? highlightsDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + DeserializeJson(body, s_mJsonSerializerSettings); HighlightEntities.Highlights highlights = HighlightsMapper.FromDto(highlightsDto); if (highlights.HasMore) @@ -510,9 +509,14 @@ public class ApiService(IAuthService authService, IConfigService configService, Log.Debug("Media Highlights - " + endpoint); string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + if (string.IsNullOrWhiteSpace(loopbody)) + { + Log.Warning("Received empty body from API"); + break; + } + HighlightDtos.HighlightsDto? newHighlightsDto = - JsonConvert.DeserializeObject(loopbody, - s_mJsonSerializerSettings); + DeserializeJson(loopbody, s_mJsonSerializerSettings); HighlightEntities.Highlights newHighlights = HighlightsMapper.FromDto(newHighlightsDto); highlights.List.AddRange(newHighlights.List); @@ -553,43 +557,37 @@ public class ApiService(IAuthService authService, IConfigService configService, highlightResponse.EnsureSuccessStatusCode(); string highlightBody = await highlightResponse.Content.ReadAsStringAsync(); HighlightDtos.HighlightMediaDto? highlightMediaDto = - JsonConvert.DeserializeObject(highlightBody, - s_mJsonSerializerSettings); + DeserializeJson(highlightBody, s_mJsonSerializerSettings); HighlightEntities.HighlightMedia highlightMedia = HighlightsMapper.FromDto(highlightMediaDto); foreach (HighlightEntities.Story item in highlightMedia.Stories) { - if (item.Media != null && item.Media.Count > 0 && item.Media[0].CreatedAt.HasValue && - item.Media[0].CreatedAt != null) + DateTime? createdAt = item.Media != null && item.Media.Count > 0 + ? item.Media[0].CreatedAt + : null; + + if (createdAt.HasValue) { - DateTime? createdAt = item.Media[0].CreatedAt; - if (createdAt != null) - { - await dbService.AddStory(folder, item.Id, "", "0", false, false, - createdAt.Value); - } + await dbService.AddStory(folder, item.Id, "", "0", false, false, createdAt.Value); } else if (item.CreatedAt.HasValue) { - await dbService.AddStory(folder, item.Id, "", "0", false, false, - item.CreatedAt.Value); + await dbService.AddStory(folder, item.Id, "", "0", false, false, item.CreatedAt.Value); } else { - await dbService.AddStory(folder, item.Id, "", "0", false, false, - DateTime.Now); + await dbService.AddStory(folder, item.Id, "", "0", false, false, DateTime.Now); } if (item.Media != null && item.Media.Count > 0 && item.Media[0].CanView) { + string? storyUrl = item.Media[0].Files?.Full?.Url; + string storyUrlValue = storyUrl ?? string.Empty; foreach (HighlightEntities.Medium medium in item.Media) { - string storyUrl = item.Media[0].Files?.Full?.Url ?? ""; - await dbService.AddMedia(folder, medium.Id, item.Id, storyUrl, null, null, null, - "Stories", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, false, false, null); + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + await dbService.AddMedia(folder, medium.Id, item.Id, storyUrlValue, null, null, null, + "Stories", mediaType, false, false, null); if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; @@ -661,20 +659,18 @@ public class ApiService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); PurchasedDtos.PurchasedDto? paidPostsDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + DeserializeJson(body, s_mJsonSerializerSettings); PurchasedEntities.Purchased paidPosts = PurchasedMapper.FromDto(paidPostsDto); statusReporter.ReportStatus($"Getting Paid Posts - Found {paidPosts.List.Count}"); - if (paidPosts != null && paidPosts.HasMore) + if (paidPosts.HasMore) { getParams["offset"] = paidPosts.List.Count.ToString(); while (true) { - PurchasedEntities.Purchased newPaidPosts = new(); - string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); PurchasedDtos.PurchasedDto? newPaidPostsDto = - JsonConvert.DeserializeObject(loopbody, s_mJsonSerializerSettings); - newPaidPosts = PurchasedMapper.FromDto(newPaidPostsDto); + DeserializeJson(loopbody, s_mJsonSerializerSettings); + PurchasedEntities.Purchased newPaidPosts = PurchasedMapper.FromDto(newPaidPostsDto); paidPosts.List.AddRange(newPaidPosts.List); statusReporter.ReportStatus($"Getting Paid Posts - Found {paidPosts.List.Count}"); @@ -687,7 +683,8 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - foreach (PurchasedEntities.ListItem purchase in paidPosts.List) + List paidPostList = paidPosts.List; + foreach (PurchasedEntities.ListItem purchase in paidPostList) { if (purchase.ResponseType == "post" && purchase.Media != null && purchase.Media.Count > 0) { @@ -719,11 +716,11 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, purchase.Id, purchase.Text != null ? purchase.Text : "", - purchase.Price != null ? purchase.Price : "0", - purchase.Price != null && purchase.IsOpened ? true : false, - purchase.IsArchived.HasValue ? purchase.IsArchived.Value : false, - purchase.CreatedAt != null ? purchase.CreatedAt.Value : purchase.PostedAt.Value); + DateTime createdAt = purchase.CreatedAt ?? purchase.PostedAt ?? DateTime.Now; + bool isArchived = purchase.IsArchived ?? false; + await dbService.AddPost(folder, purchase.Id, purchase.Text ?? "", + purchase.Price ?? "0", + purchase.Price != null && purchase.IsOpened, isArchived, createdAt); paidPostCollection.PaidPostObjects.Add(purchase); foreach (MessageEntities.Medium medium in purchase.Media) { @@ -752,69 +749,63 @@ public class ApiService(IAuthService authService, IConfigService configService, continue; } + string mediaType = ResolveMediaType(medium.Type) ?? ""; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = previewids.Contains(medium.Id); + if (previewids.Count > 0) { bool has = previewids.Any(cus => cus.Equals(medium.Id)); - if (!has && medium.CanView && medium.Files != null && medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url)) + if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, purchase.Id, medium.Files.Full.Url, + await dbService.AddMedia(folder, medium.Id, purchase.Id, fullUrl, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); - paidPostCollection.PaidPosts.Add(medium.Id, medium.Files.Full.Url); + mediaType, isPreview, false, null); + paidPostCollection.PaidPosts.Add(medium.Id, fullUrl); paidPostCollection.PaidPostMedia.Add(medium); } } - else if (!has && medium.CanView && medium.Files != null && medium.Files.Drm != null) + else if (!has && medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { await dbService.AddMedia(folder, medium.Id, purchase.Id, - medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); + manifestDash, null, null, null, "Posts", mediaType, isPreview, false, null); paidPostCollection.PaidPosts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); paidPostCollection.PaidPostMedia.Add(medium); } } } else { - if (medium.CanView && medium.Files != null && medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url)) + if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, purchase.Id, medium.Files.Full.Url, + await dbService.AddMedia(folder, medium.Id, purchase.Id, fullUrl, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); - paidPostCollection.PaidPosts.Add(medium.Id, medium.Files.Full.Url); + mediaType, isPreview, false, null); + paidPostCollection.PaidPosts.Add(medium.Id, fullUrl); paidPostCollection.PaidPostMedia.Add(medium); } } - else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { await dbService.AddMedia(folder, medium.Id, purchase.Id, - medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); + manifestDash, null, null, null, "Posts", mediaType, isPreview, false, null); paidPostCollection.PaidPosts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); paidPostCollection.PaidPostMedia.Add(medium); } } @@ -839,7 +830,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return new PurchasedEntities.PaidPostCollection(); } @@ -886,10 +877,10 @@ public class ApiService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); PostDtos.PostDto? postsDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + DeserializeJson(body, s_mJsonSerializerSettings); PostEntities.Post posts = PostMapper.FromDto(postsDto); statusReporter.ReportStatus($"Getting Posts - Found {posts.List.Count}"); - if (posts != null && posts.HasMore) + if (posts.HasMore) { UpdateGetParamsForDateSelection( downloadDateSelection, @@ -898,12 +889,10 @@ public class ApiService(IAuthService authService, IConfigService configService, while (true) { - PostEntities.Post newposts = new(); - string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); PostDtos.PostDto? newPostsDto = - JsonConvert.DeserializeObject(loopbody, s_mJsonSerializerSettings); - newposts = PostMapper.FromDto(newPostsDto); + DeserializeJson(loopbody, s_mJsonSerializerSettings); + PostEntities.Post newposts = PostMapper.FromDto(newPostsDto); posts.List.AddRange(newposts.List); statusReporter.ReportStatus($"Getting Posts - Found {posts.List.Count}"); @@ -919,18 +908,21 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - foreach (PostEntities.ListItem post in posts.List) + List postList = posts.List; + foreach (PostEntities.ListItem post in postList) { if (configService.CurrentConfig.SkipAds) { - if (post.RawText != null && (post.RawText.Contains("#ad") || post.RawText.Contains("/trial/") || - post.RawText.Contains("#announcement"))) + if (!string.IsNullOrEmpty(post.RawText) && + (post.RawText.Contains("#ad") || post.RawText.Contains("/trial/") || + post.RawText.Contains("#announcement"))) { continue; } - if (post.Text != null && (post.Text.Contains("#ad") || post.Text.Contains("/trial/") || - post.Text.Contains("#announcement"))) + if (!string.IsNullOrEmpty(post.Text) && + (post.Text.Contains("#ad") || post.Text.Contains("/trial/") || + post.Text.Contains("#announcement"))) { continue; } @@ -951,8 +943,8 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, post.Id, post.RawText != null ? post.RawText : "", - post.Price != null ? post.Price : "0", post.Price != null && post.IsOpened ? true : false, + await dbService.AddPost(folder, post.Id, !string.IsNullOrEmpty(post.RawText) ? post.RawText : "", + post.Price ?? "0", post.Price != null && post.IsOpened, post.IsArchived, post.PostedAt); postCollection.PostObjects.Add(post); if (post.Media != null && post.Media.Count > 0) @@ -979,62 +971,48 @@ public class ApiService(IAuthService authService, IConfigService configService, continue; } + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + string? previewUrl = medium.Files?.Preview?.Url; + bool isPreview = postPreviewIds.Contains(medium.Id); + if (medium.CanView && medium.Files?.Drm == null) { bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); - if (medium.Files!.Full != null && !string.IsNullOrEmpty(medium.Files!.Full.Url)) - { - if (!has) - { - if (!postCollection.Posts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, post.Id, medium.Files!.Full.Url, - null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.Id) ? true : false, false, null); - postCollection.Posts.Add(medium.Id, medium.Files!.Full.Url); - postCollection.PostMedia.Add(medium); - } - } - } - else if (medium.Files.Preview != null && medium.Files!.Full == null) - { - if (!has) - { - if (!postCollection.Posts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, post.Id, medium.Files.Preview.Url, - null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.Id) ? true : false, false, null); - postCollection.Posts.Add(medium.Id, medium.Files.Preview.Url); - postCollection.PostMedia.Add(medium); - } - } - } - } - else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) - { - bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); - if (!has && medium.Files != null && medium.Files.Drm != null) + if (!has && !string.IsNullOrEmpty(fullUrl)) { if (!postCollection.Posts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, post.Id, medium.Files.Drm.Manifest.Dash, - null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.Id) ? true : false, false, null); - postCollection.Posts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{post.Id}"); + await dbService.AddMedia(folder, medium.Id, post.Id, fullUrl, null, null, null, + "Posts", mediaType, isPreview, false, null); + postCollection.Posts.Add(medium.Id, fullUrl); postCollection.PostMedia.Add(medium); } } + else if (!has && string.IsNullOrEmpty(fullUrl) && !string.IsNullOrEmpty(previewUrl)) + { + if (!postCollection.Posts.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, post.Id, previewUrl, null, null, null, + "Posts", mediaType, isPreview, false, null); + postCollection.Posts.Add(medium.Id, previewUrl); + postCollection.PostMedia.Add(medium); + } + } + } + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId)) + { + bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); + if (!has && !postCollection.Posts.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, post.Id, manifestDash, null, null, null, + "Posts", mediaType, isPreview, false, null); + postCollection.Posts.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{post.Id}"); + postCollection.PostMedia.Add(medium); + } } } } @@ -1056,7 +1034,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return new PostEntities.PostCollection(); } public async Task GetPost(string endpoint, string folder) @@ -1065,14 +1043,13 @@ public class ApiService(IAuthService authService, IConfigService configService, try { - PostEntities.SinglePost singlePost = new(); SinglePostCollection singlePostCollection = new(); Dictionary getParams = new() { { "skip_users", "all" } }; string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); PostDtos.SinglePostDto? singlePostDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); - singlePost = PostMapper.FromDto(singlePostDto); + DeserializeJson(body, s_mJsonSerializerSettings); + PostEntities.SinglePost singlePost = PostMapper.FromDto(singlePostDto); if (singlePostDto != null) { @@ -1091,9 +1068,10 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, singlePost.Id, singlePost.Text != null ? singlePost.Text : "", - singlePost.Price != null ? singlePost.Price : "0", - singlePost.Price != null && singlePost.IsOpened ? true : false, singlePost.IsArchived, + await dbService.AddPost(folder, singlePost.Id, + !string.IsNullOrEmpty(singlePost.Text) ? singlePost.Text : "", + singlePost.Price ?? "0", + singlePost.Price != null && singlePost.IsOpened, singlePost.IsArchived, singlePost.PostedAt); singlePostCollection.SinglePostObjects.Add(singlePost); if (singlePost.Media != null && singlePost.Media.Count > 0) @@ -1120,100 +1098,78 @@ public class ApiService(IAuthService authService, IConfigService configService, continue; } + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + bool isPreview = postPreviewIds.Contains(medium.Id); + string? fullUrl = medium.Files?.Full?.Url; + string? previewUrl = medium.Files?.Preview?.Url; + if (medium.CanView && medium.Files?.Drm == null) { switch (configService.CurrentConfig.DownloadVideoResolution) { case VideoResolution.source: - if (medium.Files!.Full != null && !string.IsNullOrEmpty(medium.Files!.Full.Url)) + if (!string.IsNullOrEmpty(fullUrl)) { if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { await dbService.AddMedia(folder, medium.Id, singlePost.Id, - medium.Files!.Full.Url, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.Id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.Id, medium.Files!.Full.Url); + fullUrl, null, null, null, "Posts", mediaType, isPreview, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, fullUrl); singlePostCollection.SinglePostMedia.Add(medium); } } break; case VideoResolution._240: - if (medium.VideoSources != null) + string? video240 = medium.VideoSources?._240; + if (!string.IsNullOrEmpty(video240)) { - if (!string.IsNullOrEmpty(medium.VideoSources._240)) + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, singlePost.Id, - medium.VideoSources._240, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.Id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.Id, - medium.VideoSources._240); - singlePostCollection.SinglePostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, singlePost.Id, video240, null, + null, null, "Posts", mediaType, isPreview, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, video240); + singlePostCollection.SinglePostMedia.Add(medium); } } break; case VideoResolution._720: - if (medium.VideoSources != null) + string? video720 = medium.VideoSources?._720; + if (!string.IsNullOrEmpty(video720)) { - if (!string.IsNullOrEmpty(medium.VideoSources._720)) + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, singlePost.Id, - medium.VideoSources._720, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.Id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.Id, - medium.VideoSources._720); - singlePostCollection.SinglePostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, singlePost.Id, video720, null, + null, null, "Posts", mediaType, isPreview, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, video720); + singlePostCollection.SinglePostMedia.Add(medium); } } break; } } - else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) - { - if (medium.Files != null && medium.Files.Drm != null) - { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, singlePost.Id, - medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.Id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{singlePost.Id}"); - singlePostCollection.SinglePostMedia.Add(medium); - } - } - } - else if (medium.Files.Preview != null && medium.Files!.Full == null) + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId)) { if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, singlePost.Id, medium.Files.Preview.Url, - null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - postPreviewIds.Contains(medium.Id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.Id, medium.Files.Preview.Url); + await dbService.AddMedia(folder, medium.Id, singlePost.Id, manifestDash, null, null, + null, "Posts", mediaType, isPreview, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{singlePost.Id}"); + singlePostCollection.SinglePostMedia.Add(medium); + } + } + else if (!string.IsNullOrEmpty(previewUrl) && medium.Files?.Full == null) + { + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, singlePost.Id, previewUrl, null, null, null, + "Posts", mediaType, isPreview, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, previewUrl); singlePostCollection.SinglePostMedia.Add(medium); } } @@ -1237,7 +1193,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return new SinglePostCollection(); } public async Task GetStreams(string endpoint, string folder, @@ -1272,10 +1228,10 @@ public class ApiService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); StreamsDtos.StreamsDto? streamsDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + DeserializeJson(body, s_mJsonSerializerSettings); StreamEntities.Streams streams = StreamsMapper.FromDto(streamsDto); statusReporter.ReportStatus($"Getting Streams - Found {streams.List.Count}"); - if (streams != null && streams.HasMore) + if (streams.HasMore) { UpdateGetParamsForDateSelection( downloadDateSelection, @@ -1284,12 +1240,10 @@ public class ApiService(IAuthService authService, IConfigService configService, while (true) { - StreamEntities.Streams newstreams = new(); - string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); StreamsDtos.StreamsDto? newStreamsDto = - JsonConvert.DeserializeObject(loopbody, s_mJsonSerializerSettings); - newstreams = StreamsMapper.FromDto(newStreamsDto); + DeserializeJson(loopbody, s_mJsonSerializerSettings); + StreamEntities.Streams newstreams = StreamsMapper.FromDto(newStreamsDto); streams.List.AddRange(newstreams.List); statusReporter.ReportStatus($"Getting Streams - Found {streams.List.Count}"); @@ -1305,7 +1259,8 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - foreach (StreamEntities.ListItem stream in streams.List) + List streamList = streams.List; + foreach (StreamEntities.ListItem stream in streamList) { List streamPreviewIds = new(); if (stream.Preview != null && stream.Preview.Count > 0) @@ -1322,8 +1277,8 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, stream.Id, stream.Text != null ? stream.Text : "", - stream.Price != null ? stream.Price : "0", stream.Price != null && stream.IsOpened ? true : false, + await dbService.AddPost(folder, stream.Id, !string.IsNullOrEmpty(stream.Text) ? stream.Text : "", + stream.Price ?? "0", stream.Price != null && stream.IsOpened, stream.IsArchived, stream.PostedAt); streamsCollection.StreamObjects.Add(stream); if (stream.Media != null && stream.Media.Count > 0) @@ -1350,42 +1305,36 @@ public class ApiService(IAuthService authService, IConfigService configService, continue; } + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = streamPreviewIds.Contains(medium.Id); + if (medium.CanView && medium.Files?.Drm == null) { bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); - if (!has && medium.CanView && medium.Files != null && medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url)) + if (!has && !string.IsNullOrEmpty(fullUrl)) { if (!streamsCollection.Streams.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, stream.Id, medium.Files.Full.Url, null, - null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - streamPreviewIds.Contains(medium.Id) ? true : false, false, null); - streamsCollection.Streams.Add(medium.Id, medium.Files.Full.Url); + await dbService.AddMedia(folder, medium.Id, stream.Id, fullUrl, null, null, null, + "Posts", mediaType, isPreview, false, null); + streamsCollection.Streams.Add(medium.Id, fullUrl); streamsCollection.StreamMedia.Add(medium); } } } - else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId)) { bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); - if (!has && medium.Files != null && medium.Files.Drm != null) + if (!has && !streamsCollection.Streams.ContainsKey(medium.Id)) { - if (!streamsCollection.Streams.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, stream.Id, - medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - streamPreviewIds.Contains(medium.Id) ? true : false, false, null); - streamsCollection.Streams.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{stream.Id}"); - streamsCollection.StreamMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, stream.Id, manifestDash, null, null, null, + "Posts", mediaType, isPreview, false, null); + streamsCollection.Streams.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{stream.Id}"); + streamsCollection.StreamMedia.Add(medium); } } } @@ -1408,7 +1357,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return new StreamEntities.StreamsCollection(); } @@ -1450,7 +1399,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } ArchivedDtos.ArchivedDto? archivedDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + DeserializeJson(body, s_mJsonSerializerSettings); ArchivedEntities.Archived archived = ArchivedMapper.FromDto(archivedDto); statusReporter.ReportStatus($"Getting Archived Posts - Found {archived.List.Count}"); if (archived.HasMore) @@ -1468,7 +1417,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } ArchivedDtos.ArchivedDto? newarchivedDto = - JsonConvert.DeserializeObject(loopbody, s_mJsonSerializerSettings); + DeserializeJson(loopbody, s_mJsonSerializerSettings); ArchivedEntities.Archived newarchived = ArchivedMapper.FromDto(newarchivedDto); archived.List.AddRange(newarchived.List); @@ -1503,8 +1452,8 @@ public class ApiService(IAuthService authService, IConfigService configService, } await dbService.AddPost(folder, archive.Id, archive.Text != null ? archive.Text : "", - archive.Price != null ? archive.Price : "0", - archive.Price != null && archive.IsOpened ? true : false, archive.IsArchived, archive.PostedAt); + archive.Price ?? "0", + archive.Price != null && archive.IsOpened, archive.IsArchived, archive.PostedAt); archivedCollection.ArchivedPostObjects.Add(archive); if (archive.Media != null && archive.Media.Count > 0) { @@ -1530,33 +1479,30 @@ public class ApiService(IAuthService authService, IConfigService configService, continue; } - if (medium.CanView && medium.Files != null && medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url)) + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = previewids.Contains(medium.Id); + + if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (!archivedCollection.ArchivedPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, archive.Id, medium.Files.Full.Url, null, - null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id) ? true : false, false, null); - archivedCollection.ArchivedPosts.Add(medium.Id, medium.Files.Full.Url); + await dbService.AddMedia(folder, medium.Id, archive.Id, fullUrl, null, null, null, + "Posts", mediaType, isPreview, false, null); + archivedCollection.ArchivedPosts.Add(medium.Id, fullUrl); archivedCollection.ArchivedPostMedia.Add(medium); } } - else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId)) { if (!archivedCollection.ArchivedPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, archive.Id, medium.Files.Drm.Manifest.Dash, - null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.Id, archive.Id, manifestDash, null, null, null, + "Posts", mediaType, isPreview, false, null); archivedCollection.ArchivedPosts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{archive.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{archive.Id}"); archivedCollection.ArchivedPostMedia.Add(medium); } } @@ -1580,7 +1526,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return new ArchivedEntities.ArchivedCollection(); } @@ -1597,10 +1543,11 @@ public class ApiService(IAuthService authService, IConfigService configService, { { "limit", postLimit.ToString() }, { "order", "desc" }, { "skip_users", "all" } }; + int currentUserId = GetCurrentUserIdOrDefault(); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); MessageDtos.MessagesDto? messagesDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + DeserializeJson(body, s_mJsonSerializerSettings); MessageEntities.Messages messages = MessagesMapper.FromDto(messagesDto); statusReporter.ReportStatus($"Getting Messages - Found {messages.List.Count}"); if (messages.HasMore) @@ -1608,12 +1555,10 @@ public class ApiService(IAuthService authService, IConfigService configService, getParams["id"] = messages.List[^1].Id.ToString(); while (true) { - MessageEntities.Messages newMessages = new(); - string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); MessageDtos.MessagesDto? newMessagesDto = - JsonConvert.DeserializeObject(loopbody, s_mJsonSerializerSettings); - newMessages = MessagesMapper.FromDto(newMessagesDto); + DeserializeJson(loopbody, s_mJsonSerializerSettings); + MessageEntities.Messages newMessages = MessagesMapper.FromDto(newMessagesDto); messages.List.AddRange(newMessages.List); statusReporter.ReportStatus($"Getting Messages - Found {messages.List.Count}"); @@ -1652,21 +1597,24 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - if (!configService.CurrentConfig.IgnoreOwnMessages || - list.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.UserId)) + if (!configService.CurrentConfig.IgnoreOwnMessages || list.FromUser?.Id != currentUserId) { + DateTime createdAt = list.CreatedAt ?? DateTime.Now; await dbService.AddMessage(folder, list.Id, list.Text ?? "", list.Price ?? "0", list.CanPurchaseReason == "opened" ? true : list.CanPurchaseReason != "opened" ? false : (bool?)null ?? false, false, - list.CreatedAt.HasValue ? list.CreatedAt.Value : DateTime.Now, + createdAt, list.FromUser?.Id ?? int.MinValue); messageCollection.MessageObjects.Add(list); if (list.CanPurchaseReason != "opened" && list.Media != null && list.Media.Count > 0) { - foreach (MessageEntities.Medium medium in list.Media) + foreach (MessageEntities.Medium medium in list.Media ?? new List()) { - if (medium.CanView && medium.Files != null && medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url)) + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = messagePreviewIds.Contains(medium.Id); + + if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -1690,17 +1638,16 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!messageCollection.Messages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, list.Id, medium.Files.Full.Url, null, - null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.Id), false, null); - messageCollection.Messages.Add(medium.Id, medium.Files.Full.Url); + await dbService.AddMedia(folder, medium.Id, list.Id, fullUrl, null, null, null, + "Messages", mediaType, isPreview, false, null); + messageCollection.Messages.Add(medium.Id, fullUrl); messageCollection.MessageMedia.Add(medium); } } - else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -1724,14 +1671,10 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!messageCollection.Messages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, list.Id, medium.Files.Drm.Manifest.Dash, - null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.Id), false, null); + await dbService.AddMedia(folder, medium.Id, list.Id, manifestDash, null, null, null, + "Messages", mediaType, isPreview, false, null); messageCollection.Messages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{list.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{list.Id}"); messageCollection.MessageMedia.Add(medium); } } @@ -1739,10 +1682,13 @@ public class ApiService(IAuthService authService, IConfigService configService, } else if (messagePreviewIds.Count > 0) { - foreach (MessageEntities.Medium medium in list.Media) + foreach (MessageEntities.Medium medium in list.Media ?? new List()) { - if (medium.CanView && medium.Files != null && medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url) && messagePreviewIds.Contains(medium.Id)) + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = messagePreviewIds.Contains(medium.Id); + + if (medium.CanView && !string.IsNullOrEmpty(fullUrl) && isPreview) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -1766,18 +1712,16 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!messageCollection.Messages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, list.Id, medium.Files.Full.Url, null, - null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.Id), false, null); - messageCollection.Messages.Add(medium.Id, medium.Files.Full.Url); + await dbService.AddMedia(folder, medium.Id, list.Id, fullUrl, null, null, null, + "Messages", mediaType, isPreview, false, null); + messageCollection.Messages.Add(medium.Id, fullUrl); messageCollection.MessageMedia.Add(medium); } } - else if (medium.CanView && medium.Files != null && medium.Files.Drm != null && - messagePreviewIds.Contains(medium.Id)) + else if (medium.CanView && isPreview && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -1801,14 +1745,10 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!messageCollection.Messages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, list.Id, medium.Files.Drm.Manifest.Dash, - null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.Id), false, null); + await dbService.AddMedia(folder, medium.Id, list.Id, manifestDash, null, null, null, + "Messages", mediaType, isPreview, false, null); messageCollection.Messages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{list.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{list.Id}"); messageCollection.MessageMedia.Add(medium); } } @@ -1833,7 +1773,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return new MessageEntities.MessageCollection(); } public async Task GetPaidMessage(string endpoint, string folder) @@ -1845,18 +1785,19 @@ public class ApiService(IAuthService authService, IConfigService configService, PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection = new(); const int postLimit = 50; Dictionary getParams = new() { { "limit", postLimit.ToString() }, { "order", "desc" } }; + int currentUserId = GetCurrentUserIdOrDefault(); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); MessageDtos.SingleMessageDto? messageDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + DeserializeJson(body, s_mJsonSerializerSettings); MessageEntities.SingleMessage message = MessagesMapper.FromDto(messageDto); - if (!configService.CurrentConfig.IgnoreOwnMessages || - message.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.UserId)) + if (!configService.CurrentConfig.IgnoreOwnMessages || message.FromUser?.Id != currentUserId) { + DateTime createdAt = message.CreatedAt ?? DateTime.Now; await dbService.AddMessage(folder, message.Id, message.Text ?? "", - message.Price != null ? message.Price.ToString() : "0", true, false, - message.CreatedAt.HasValue ? message.CreatedAt.Value : DateTime.Now, + message.Price?.ToString() ?? "0", true, false, + createdAt, message.FromUser?.Id ?? int.MinValue); singlePaidMessageCollection.SingleMessageObjects.Add(message); List messagePreviewIds = new(); @@ -1878,8 +1819,11 @@ public class ApiService(IAuthService authService, IConfigService configService, { foreach (MessageEntities.Medium medium in message.Media) { - if (!messagePreviewIds.Contains(medium.Id) && medium.CanView && medium.Files != null && - medium.Files.Full != null && !string.IsNullOrEmpty(medium.Files.Full.Url)) + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = messagePreviewIds.Contains(medium.Id); + + if (!isPreview && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -1903,18 +1847,13 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, message.Id, medium.Files.Full.Url, null, - null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.Id), false, null); - singlePaidMessageCollection.SingleMessages.Add(medium.Id, medium.Files.Full.Url); + await dbService.AddMedia(folder, medium.Id, message.Id, fullUrl, null, null, null, + "Messages", mediaType, isPreview, false, null); + singlePaidMessageCollection.SingleMessages.Add(medium.Id, fullUrl); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } } - else if (messagePreviewIds.Contains(medium.Id) && medium.CanView && medium.Files != null && - medium.Files.Full != null && !string.IsNullOrEmpty(medium.Files.Full.Url)) + else if (isPreview && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -1938,18 +1877,15 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, message.Id, medium.Files.Full.Url, null, - null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.Id), false, null); - singlePaidMessageCollection.PreviewSingleMessages.Add(medium.Id, medium.Files.Full.Url); + await dbService.AddMedia(folder, medium.Id, message.Id, fullUrl, null, null, null, + "Messages", mediaType, isPreview, false, null); + singlePaidMessageCollection.PreviewSingleMessages.Add(medium.Id, fullUrl); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } } - else if (!messagePreviewIds.Contains(medium.Id) && medium.CanView && medium.Files != null && - medium.Files.Drm != null) + else if (!isPreview && medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -1973,19 +1909,17 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, message.Id, medium.Files.Drm.Manifest.Dash, - null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.Id), false, null); + await dbService.AddMedia(folder, medium.Id, message.Id, manifestDash, null, null, null, + "Messages", mediaType, isPreview, false, null); singlePaidMessageCollection.SingleMessages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{message.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{message.Id}"); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } } - else if (messagePreviewIds.Contains(medium.Id) && medium.CanView && medium.Files != null && - medium.Files.Drm != null) + else if (isPreview && medium.CanView && + TryGetDrmInfo(medium.Files, out string previewManifestDash, + out string previewCloudFrontPolicy, out string previewCloudFrontSignature, + out string previewCloudFrontKeyPairId)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -2009,14 +1943,10 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.Id)) { - await dbService.AddMedia(folder, medium.Id, message.Id, medium.Files.Drm.Manifest.Dash, - null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - messagePreviewIds.Contains(medium.Id), false, null); + await dbService.AddMedia(folder, medium.Id, message.Id, previewManifestDash, null, null, + null, "Messages", mediaType, isPreview, false, null); singlePaidMessageCollection.PreviewSingleMessages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{message.Id}"); + $"{previewManifestDash},{previewCloudFrontPolicy},{previewCloudFrontSignature},{previewCloudFrontKeyPairId},{medium.Id},{message.Id}"); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } } @@ -2040,7 +1970,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return new PurchasedEntities.SinglePaidMessageCollection(); } @@ -2062,19 +1992,20 @@ public class ApiService(IAuthService authService, IConfigService configService, { "author", username }, { "skip_users", "all" } }; + int currentUserId = GetCurrentUserIdOrDefault(); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); PurchasedDtos.PurchasedDto? paidMessagesDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + DeserializeJson(body, s_mJsonSerializerSettings); PurchasedEntities.Purchased paidMessages = PurchasedMapper.FromDto(paidMessagesDto); statusReporter.ReportStatus($"Getting Paid Messages - Found {paidMessages.List.Count}"); - if (paidMessages != null && paidMessages.HasMore) + if (paidMessages.HasMore) { getParams["offset"] = paidMessages.List.Count.ToString(); while (true) { string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); - PurchasedEntities.Purchased newpaidMessages = new(); + PurchasedEntities.Purchased newpaidMessages; Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); @@ -2091,8 +2022,7 @@ public class ApiService(IAuthService authService, IConfigService configService, loopresponse.EnsureSuccessStatusCode(); string loopbody = await loopresponse.Content.ReadAsStringAsync(); PurchasedDtos.PurchasedDto? newPaidMessagesDto = - JsonConvert.DeserializeObject(loopbody, - s_mJsonSerializerSettings); + DeserializeJson(loopbody, s_mJsonSerializerSettings); newpaidMessages = PurchasedMapper.FromDto(newPaidMessagesDto); } @@ -2107,29 +2037,22 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - if (paidMessages.List != null && paidMessages.List.Count > 0) + List paidMessageList = + paidMessages.List; + if (paidMessageList.Count > 0) { - foreach (PurchasedEntities.ListItem purchase in paidMessages.List + foreach (PurchasedEntities.ListItem purchase in paidMessageList .Where(p => p.ResponseType == "message") .OrderByDescending(p => p.PostedAt ?? p.CreatedAt)) { - if (!configService.CurrentConfig.IgnoreOwnMessages || - purchase.FromUser.Id != Convert.ToInt32(authService.CurrentAuth.UserId)) + long fromUserId = purchase.FromUser?.Id ?? long.MinValue; + if (!configService.CurrentConfig.IgnoreOwnMessages || fromUserId != currentUserId) { - if (purchase.PostedAt != null) - { - await dbService.AddMessage(folder, purchase.Id, - purchase.Text != null ? purchase.Text : "", - purchase.Price != null ? purchase.Price : "0", true, false, purchase.PostedAt.Value, - purchase.FromUser.Id); - } - else - { - await dbService.AddMessage(folder, purchase.Id, - purchase.Text != null ? purchase.Text : "", - purchase.Price != null ? purchase.Price : "0", true, false, purchase.CreatedAt.Value, - purchase.FromUser.Id); - } + DateTime createdAt = purchase.PostedAt ?? purchase.CreatedAt ?? DateTime.Now; + await dbService.AddMessage(folder, purchase.Id, + purchase.Text ?? "", + purchase.Price ?? "0", true, false, createdAt, + fromUserId); paidMessageCollection.PaidMessageObjects.Add(purchase); if (purchase.Media != null && purchase.Media.Count > 0) @@ -2164,11 +2087,14 @@ public class ApiService(IAuthService authService, IConfigService configService, foreach (MessageEntities.Medium medium in purchase.Media) { + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = previewids.Contains(medium.Id); + if (previewids.Count > 0) { bool has = previewids.Any(cus => cus.Equals(medium.Id)); - if (!has && medium.CanView && medium.Files != null && medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url)) + if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -2193,16 +2119,16 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { await dbService.AddMedia(folder, medium.Id, purchase.Id, - medium.Files.Full.Url, null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); - paidMessageCollection.PaidMessages.Add(medium.Id, medium.Files.Full.Url); + fullUrl, null, null, null, "Messages", mediaType, isPreview, false, + null); + paidMessageCollection.PaidMessages.Add(medium.Id, fullUrl); paidMessageCollection.PaidMessageMedia.Add(medium); } } - else if (!has && medium.CanView && medium.Files != null && medium.Files.Drm != null) + else if (!has && medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -2227,21 +2153,17 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { await dbService.AddMedia(folder, medium.Id, purchase.Id, - medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); + manifestDash, null, null, null, "Messages", mediaType, isPreview, false, + null); paidMessageCollection.PaidMessages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); paidMessageCollection.PaidMessageMedia.Add(medium); } } } else { - if (medium.CanView && medium.Files != null && medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url)) + if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -2266,16 +2188,16 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { await dbService.AddMedia(folder, medium.Id, purchase.Id, - medium.Files.Full.Url, null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); - paidMessageCollection.PaidMessages.Add(medium.Id, medium.Files.Full.Url); + fullUrl, null, null, null, "Messages", mediaType, isPreview, false, + null); + paidMessageCollection.PaidMessages.Add(medium.Id, fullUrl); paidMessageCollection.PaidMessageMedia.Add(medium); } } - else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) { @@ -2300,13 +2222,10 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) { await dbService.AddMedia(folder, medium.Id, purchase.Id, - medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); + manifestDash, null, null, null, "Messages", mediaType, isPreview, false, + null); paidMessageCollection.PaidMessages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); paidMessageCollection.PaidMessageMedia.Add(medium); } } @@ -2333,7 +2252,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return new PurchasedEntities.PaidMessageCollection(); } public async Task> GetPurchasedTabUsers(string endpoint, Dictionary users) @@ -2359,7 +2278,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } PurchasedDtos.PurchasedDto? purchasedDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + DeserializeJson(body, s_mJsonSerializerSettings); PurchasedEntities.Purchased purchased = PurchasedMapper.FromDto(purchasedDto); if (purchased.HasMore) { @@ -2384,8 +2303,7 @@ public class ApiService(IAuthService authService, IConfigService configService, loopresponse.EnsureSuccessStatusCode(); string loopbody = await loopresponse.Content.ReadAsStringAsync(); PurchasedDtos.PurchasedDto? newPurchasedDto = - JsonConvert.DeserializeObject(loopbody, - s_mJsonSerializerSettings); + DeserializeJson(loopbody, s_mJsonSerializerSettings); newPurchased = PurchasedMapper.FromDto(newPurchasedDto); } @@ -2404,123 +2322,84 @@ public class ApiService(IAuthService authService, IConfigService configService, foreach (PurchasedEntities.ListItem purchase in purchased.List.OrderByDescending(p => p.PostedAt ?? p.CreatedAt)) { - // purchase.FromUser.Id is not nullable, so the default value is 0 - if (purchase.FromUser.Id != 0) + long fromUserId = purchase.FromUser?.Id ?? 0; + long authorId = purchase.Author?.Id ?? 0; + + if (fromUserId != 0) { - if (users.Values.Contains(purchase.FromUser.Id)) + if (users.Values.Contains(fromUserId)) { - if (!string.IsNullOrEmpty(users.FirstOrDefault(x => x.Value == purchase.FromUser.Id).Key)) + string? matchedUsername = users.FirstOrDefault(x => x.Value == fromUserId).Key; + if (!string.IsNullOrEmpty(matchedUsername)) { - if (!purchasedTabUsers.ContainsKey(users - .FirstOrDefault(x => x.Value == purchase.FromUser.Id).Key)) + if (!purchasedTabUsers.ContainsKey(matchedUsername)) { - purchasedTabUsers.Add( - users.FirstOrDefault(x => x.Value == purchase.FromUser.Id).Key, - purchase.FromUser.Id); + purchasedTabUsers.Add(matchedUsername, fromUserId); } } - else + else if (!purchasedTabUsers.ContainsKey($"Deleted User - {fromUserId}")) { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.FromUser.Id}")) - { - purchasedTabUsers.Add($"Deleted User - {purchase.FromUser.Id}", - purchase.FromUser.Id); - } + purchasedTabUsers.Add($"Deleted User - {fromUserId}", fromUserId); } } else { - JObject? user = await GetUserInfoById($"/users/list?x[]={purchase.FromUser.Id}"); + JObject? user = await GetUserInfoById($"/users/list?x[]={fromUserId}"); + string? fetchedUsername = user?[fromUserId.ToString()]?["username"]?.ToString(); - if (user == null) + if (string.IsNullOrEmpty(fetchedUsername)) { - if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) + if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist && + !purchasedTabUsers.ContainsKey($"Deleted User - {fromUserId}")) { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.FromUser.Id}")) - { - purchasedTabUsers.Add($"Deleted User - {purchase.FromUser.Id}", - purchase.FromUser.Id); - } + purchasedTabUsers.Add($"Deleted User - {fromUserId}", fromUserId); } - Log.Debug("Content creator not longer exists - {0}", purchase.FromUser.Id); + Log.Debug("Content creator not longer exists - {0}", fromUserId); } - else if (!string.IsNullOrEmpty(user[purchase.FromUser.Id.ToString()]["username"] - .ToString())) + else if (!purchasedTabUsers.ContainsKey(fetchedUsername)) { - if (!purchasedTabUsers.ContainsKey(user[purchase.FromUser.Id.ToString()]["username"] - .ToString())) - { - purchasedTabUsers.Add(user[purchase.FromUser.Id.ToString()]["username"].ToString(), - purchase.FromUser.Id); - } - } - else - { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.FromUser.Id}")) - { - purchasedTabUsers.Add($"Deleted User - {purchase.FromUser.Id}", - purchase.FromUser.Id); - } + purchasedTabUsers.Add(fetchedUsername, fromUserId); } } } - // purchase.Author is not nullable, so we check against the Author's Id (default value 0) - else if (purchase.Author.Id != 0) + else if (authorId != 0) { - if (users.Values.Contains(purchase.Author.Id)) + if (users.ContainsValue(authorId)) { - if (!string.IsNullOrEmpty(users.FirstOrDefault(x => x.Value == purchase.Author.Id).Key)) + string? matchedUsername = users.FirstOrDefault(x => x.Value == authorId).Key; + if (!string.IsNullOrEmpty(matchedUsername)) { - if (!purchasedTabUsers.ContainsKey(users - .FirstOrDefault(x => x.Value == purchase.Author.Id).Key) && - users.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.Author.Id).Key)) + if (!purchasedTabUsers.ContainsKey(matchedUsername) && + users.ContainsKey(matchedUsername)) { - purchasedTabUsers.Add(users.FirstOrDefault(x => x.Value == purchase.Author.Id).Key, - purchase.Author.Id); + purchasedTabUsers.Add(matchedUsername, authorId); } } - else + else if (!purchasedTabUsers.ContainsKey($"Deleted User - {authorId}")) { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.Author.Id}")) - { - purchasedTabUsers.Add($"Deleted User - {purchase.Author.Id}", purchase.Author.Id); - } + purchasedTabUsers.Add($"Deleted User - {authorId}", authorId); } } else { - JObject? user = await GetUserInfoById($"/users/list?x[]={purchase.Author.Id}"); + JObject? user = await GetUserInfoById($"/users/list?x[]={authorId}"); + string? fetchedUsername = user?[authorId.ToString()]?["username"]?.ToString(); - if (user is null) + if (string.IsNullOrEmpty(fetchedUsername)) { - if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) + if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist && + !purchasedTabUsers.ContainsKey($"Deleted User - {authorId}")) { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.Author.Id}")) - { - purchasedTabUsers.Add($"Deleted User - {purchase.Author.Id}", - purchase.Author.Id); - } + purchasedTabUsers.Add($"Deleted User - {authorId}", authorId); } - Log.Debug("Content creator not longer exists - {0}", purchase.Author.Id); + Log.Debug("Content creator not longer exists - {0}", authorId); } - else if (!string.IsNullOrEmpty(user[purchase.Author.Id.ToString()]["username"].ToString())) + else if (!purchasedTabUsers.ContainsKey(fetchedUsername) && + users.ContainsKey(fetchedUsername)) { - if (!purchasedTabUsers.ContainsKey(user[purchase.Author.Id.ToString()]["username"] - .ToString()) && - users.ContainsKey(user[purchase.Author.Id.ToString()]["username"].ToString())) - { - purchasedTabUsers.Add(user[purchase.Author.Id.ToString()]["username"].ToString(), - purchase.Author.Id); - } - } - else - { - if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.Author.Id}")) - { - purchasedTabUsers.Add($"Deleted User - {purchase.Author.Id}", purchase.Author.Id); - } + purchasedTabUsers.Add(fetchedUsername, authorId); } } } @@ -2543,7 +2422,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return new Dictionary(); } public async Task> GetPurchasedTab(string endpoint, string folder, @@ -2566,7 +2445,7 @@ public class ApiService(IAuthService authService, IConfigService configService, string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); PurchasedDtos.PurchasedDto? purchasedDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + DeserializeJson(body, s_mJsonSerializerSettings); PurchasedEntities.Purchased purchased = PurchasedMapper.FromDto(purchasedDto); if (purchased.HasMore) { @@ -2574,7 +2453,7 @@ public class ApiService(IAuthService authService, IConfigService configService, while (true) { string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); - PurchasedEntities.Purchased newPurchased = new(); + PurchasedEntities.Purchased newPurchased; Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); @@ -2591,8 +2470,7 @@ public class ApiService(IAuthService authService, IConfigService configService, loopresponse.EnsureSuccessStatusCode(); string loopbody = await loopresponse.Content.ReadAsStringAsync(); PurchasedDtos.PurchasedDto? newPurchasedDto = - JsonConvert.DeserializeObject(loopbody, - s_mJsonSerializerSettings); + DeserializeJson(loopbody, s_mJsonSerializerSettings); newPurchased = PurchasedMapper.FromDto(newPurchasedDto); } @@ -2606,7 +2484,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - if (purchased.List != null && purchased.List.Count > 0) + if (purchased.List.Count > 0) { foreach (PurchasedEntities.ListItem purchase in purchased.List.OrderByDescending(p => p.PostedAt ?? p.CreatedAt)) @@ -2637,11 +2515,10 @@ public class ApiService(IAuthService authService, IConfigService configService, PurchasedEntities.PurchasedTabCollection purchasedTabCollection = new(); JObject? userObject = await GetUserInfoById($"/users/list?x[]={user.Key}"); purchasedTabCollection.UserId = user.Key; - purchasedTabCollection.Username = - userObject is not null && - !string.IsNullOrEmpty(userObject[user.Key.ToString()]["username"].ToString()) - ? userObject[user.Key.ToString()]["username"].ToString() - : $"Deleted User - {user.Key}"; + string? fetchedUsername = userObject?[user.Key.ToString()]?["username"]?.ToString(); + purchasedTabCollection.Username = !string.IsNullOrEmpty(fetchedUsername) + ? fetchedUsername + : $"Deleted User - {user.Key}"; string path = Path.Combine(folder, purchasedTabCollection.Username); if (Path.Exists(path)) { @@ -2687,12 +2564,14 @@ public class ApiService(IAuthService authService, IConfigService configService, } } + DateTime createdAt = purchase.CreatedAt ?? purchase.PostedAt ?? DateTime.Now; + bool isArchived = purchase.IsArchived ?? false; await dbService.AddPost(path, purchase.Id, - purchase.Text != null ? purchase.Text : "", - purchase.Price != null ? purchase.Price : "0", - purchase.Price != null && purchase.IsOpened ? true : false, - purchase.IsArchived.HasValue ? purchase.IsArchived.Value : false, - purchase.CreatedAt != null ? purchase.CreatedAt.Value : purchase.PostedAt.Value); + purchase.Text ?? "", + purchase.Price ?? "0", + purchase.Price != null && purchase.IsOpened, + isArchived, + createdAt); purchasedTabCollection.PaidPosts.PaidPostObjects.Add(purchase); foreach (MessageEntities.Medium medium in purchase.Media) { @@ -2716,72 +2595,61 @@ public class ApiService(IAuthService authService, IConfigService configService, continue; } + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = previewids.Contains(medium.Id); + if (previewids.Count > 0) { bool has = previewids.Any(cus => cus.Equals(medium.Id)); - if (!has && medium.CanView && medium.Files != null && - medium.Files.Full != null && !string.IsNullOrEmpty(medium.Files.Full.Url)) + if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.Id, - medium.Files.Full.Url, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, - medium.Files.Full.Url); + await dbService.AddMedia(path, medium.Id, purchase.Id, fullUrl, null, + null, null, "Posts", mediaType, isPreview, false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, fullUrl); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } - else if (!has && medium.CanView && medium.Files != null && - medium.Files.Drm != null) + else if (!has && medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.Id, - medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); + await dbService.AddMedia(path, medium.Id, purchase.Id, manifestDash, + null, null, null, "Posts", mediaType, isPreview, false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } } else { - if (medium.CanView && medium.Files != null && medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url)) + if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.Id, - medium.Files.Full.Url, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, - medium.Files.Full.Url); + await dbService.AddMedia(path, medium.Id, purchase.Id, fullUrl, null, + null, null, "Posts", mediaType, isPreview, false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, fullUrl); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } - else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.Id)) { - await dbService.AddMedia(path, medium.Id, purchase.Id, - medium.Files.Drm.Manifest.Dash, null, null, null, "Posts", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - previewids.Contains(medium.Id), false, null); + await dbService.AddMedia(path, medium.Id, purchase.Id, manifestDash, + null, null, null, "Posts", mediaType, isPreview, false, null); purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } @@ -2790,20 +2658,12 @@ public class ApiService(IAuthService authService, IConfigService configService, break; case "message": - if (purchase.PostedAt != null) - { - await dbService.AddMessage(path, purchase.Id, - purchase.Text != null ? purchase.Text : "", - purchase.Price != null ? purchase.Price : "0", true, false, - purchase.PostedAt.Value, purchase.FromUser.Id); - } - else - { - await dbService.AddMessage(path, purchase.Id, - purchase.Text != null ? purchase.Text : "", - purchase.Price != null ? purchase.Price : "0", true, false, - purchase.CreatedAt.Value, purchase.FromUser.Id); - } + DateTime messageCreatedAt = purchase.PostedAt ?? purchase.CreatedAt ?? DateTime.Now; + long fromUserId = purchase.FromUser?.Id ?? long.MinValue; + await dbService.AddMessage(path, purchase.Id, + purchase.Text ?? "", + purchase.Price ?? "0", true, false, + messageCreatedAt, fromUserId); purchasedTabCollection.PaidMessages.PaidMessageObjects.Add(purchase); if (purchase.Media != null && purchase.Media.Count > 0) @@ -2840,10 +2700,11 @@ public class ApiService(IAuthService authService, IConfigService configService, { if (paidMessagePreviewids.Count > 0) { + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = paidMessagePreviewids.Contains(medium.Id); bool has = paidMessagePreviewids.Any(cus => cus.Equals(medium.Id)); - if (!has && medium.CanView && medium.Files != null && - medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url)) + if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) @@ -2872,20 +2733,18 @@ public class ApiService(IAuthService authService, IConfigService configService, medium.Id)) { await dbService.AddMedia(path, medium.Id, purchase.Id, - medium.Files.Full.Url, null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - paidMessagePreviewids.Contains(medium.Id), false, - null); + fullUrl, null, null, null, "Messages", mediaType, + isPreview, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, - medium.Files.Full.Url); + fullUrl); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( medium); } } - else if (!has && medium.CanView && medium.Files != null && - medium.Files.Drm != null) + else if (!has && medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) @@ -2914,14 +2773,10 @@ public class ApiService(IAuthService authService, IConfigService configService, medium.Id)) { await dbService.AddMedia(path, medium.Id, purchase.Id, - medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - paidMessagePreviewids.Contains(medium.Id), false, - null); + manifestDash, null, null, null, "Messages", mediaType, + isPreview, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( medium); } @@ -2929,8 +2784,11 @@ public class ApiService(IAuthService authService, IConfigService configService, } else { - if (medium.CanView && medium.Files != null && medium.Files.Full != null && - !string.IsNullOrEmpty(medium.Files.Full.Url)) + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = paidMessagePreviewids.Contains(medium.Id); + + if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) @@ -2959,19 +2817,18 @@ public class ApiService(IAuthService authService, IConfigService configService, medium.Id)) { await dbService.AddMedia(path, medium.Id, purchase.Id, - medium.Files.Full.Url, null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - paidMessagePreviewids.Contains(medium.Id), false, - null); + fullUrl, null, null, null, "Messages", mediaType, + isPreview, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, - medium.Files.Full.Url); + fullUrl); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( medium); } } - else if (medium.CanView && medium.Files != null && medium.Files.Drm != null) + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) @@ -3000,14 +2857,10 @@ public class ApiService(IAuthService authService, IConfigService configService, medium.Id)) { await dbService.AddMedia(path, medium.Id, purchase.Id, - medium.Files.Drm.Manifest.Dash, null, null, null, "Messages", - medium.Type == "photo" ? "Images" : - medium.Type == "video" || medium.Type == "gif" ? "Videos" : - medium.Type == "audio" ? "Audios" : null, - paidMessagePreviewids.Contains(medium.Id), false, - null); + manifestDash, null, null, null, "Messages", mediaType, + isPreview, false, null); purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.Id, - $"{medium.Files.Drm.Manifest.Dash},{medium.Files.Drm.Signature.Dash.CloudFrontPolicy},{medium.Files.Drm.Signature.Dash.CloudFrontSignature},{medium.Files.Drm.Signature.Dash.CloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add( medium); } @@ -3040,7 +2893,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return []; } @@ -3048,19 +2901,20 @@ public class ApiService(IAuthService authService, IConfigService configService, { try { - if (authService.CurrentAuth == null) + Auth? currentAuth = authService.CurrentAuth; + if (currentAuth == null || currentAuth.UserAgent == null || currentAuth.Cookie == null) { - throw new Exception("No current authentication available"); + throw new Exception("Auth service is missing required fields"); } string? pssh; HttpClient client = new(); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); - request.Headers.Add("user-agent", authService.CurrentAuth.UserAgent); + request.Headers.Add("user-agent", currentAuth.UserAgent); request.Headers.Add("Accept", "*/*"); request.Headers.Add("Cookie", - $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.Cookie};"); + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {currentAuth.Cookie};"); using (HttpResponseMessage response = await client.SendAsync(request)) { response.EnsureSuccessStatusCode(); @@ -3088,7 +2942,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return string.Empty; } @@ -3104,12 +2958,18 @@ public class ApiService(IAuthService authService, IConfigService configService, { DateTime lastmodified; + Auth? currentAuth = authService.CurrentAuth; + if (currentAuth == null || currentAuth.UserAgent == null || currentAuth.Cookie == null) + { + throw new Exception("Auth service is missing required fields"); + } + HttpClient client = new(); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); - request.Headers.Add("user-agent", authService.CurrentAuth.UserAgent); + request.Headers.Add("user-agent", currentAuth.UserAgent); request.Headers.Add("Accept", "*/*"); request.Headers.Add("Cookie", - $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.Cookie};"); + $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {currentAuth.Cookie};"); using (HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { @@ -3198,10 +3058,10 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return string.Empty; } - public async Task? GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, + public async Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, string pssh) { Log.Debug("Calling GetDecryptionKeyCDM"); @@ -3246,7 +3106,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return null; + return string.Empty; } @@ -3302,7 +3162,7 @@ public class ApiService(IAuthService authService, IConfigService configService, private HttpClient GetHttpClient() { HttpClient client = new(); - if (configService.CurrentConfig?.Timeout != null && configService.CurrentConfig.Timeout > 0) + if (configService.CurrentConfig.Timeout != null && configService.CurrentConfig.Timeout > 0) { client.Timeout = TimeSpan.FromSeconds(configService.CurrentConfig.Timeout.Value); } @@ -3310,12 +3170,73 @@ public class ApiService(IAuthService authService, IConfigService configService, return client; } + private static T? DeserializeJson(string? body, JsonSerializerSettings? settings = null) + { + if (string.IsNullOrWhiteSpace(body)) + { + return default; + } + + return settings == null + ? JsonConvert.DeserializeObject(body) + : JsonConvert.DeserializeObject(body, settings); + } + + private static string? ResolveMediaType(string? type) => + type switch + { + "photo" => "Images", + "video" => "Videos", + "gif" => "Videos", + "audio" => "Audios", + _ => null + }; + + private static bool TryGetDrmInfo(Files? files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId) + { + manifestDash = string.Empty; + cloudFrontPolicy = string.Empty; + cloudFrontSignature = string.Empty; + cloudFrontKeyPairId = string.Empty; + + string? dash = files?.Drm?.Manifest?.Dash; + Dash? signatureDash = files?.Drm?.Signature?.Dash; + if (string.IsNullOrEmpty(dash) || signatureDash == null) + { + return false; + } + + if (string.IsNullOrEmpty(signatureDash.CloudFrontPolicy) || + string.IsNullOrEmpty(signatureDash.CloudFrontSignature) || + string.IsNullOrEmpty(signatureDash.CloudFrontKeyPairId)) + { + return false; + } + + manifestDash = dash; + cloudFrontPolicy = signatureDash.CloudFrontPolicy; + cloudFrontSignature = signatureDash.CloudFrontSignature; + cloudFrontKeyPairId = signatureDash.CloudFrontKeyPairId; + return true; + } + + private int GetCurrentUserIdOrDefault() + { + if (authService.CurrentAuth?.UserId == null) + { + return int.MinValue; + } + + return int.TryParse(authService.CurrentAuth.UserId, out int userId) ? userId : int.MinValue; + } + /// /// this one is used during initialization only - /// if the config option is not available then no modificatiotns will be done on the getParams + /// if the config option is not available, then no modifications will be done on the getParams /// - /// + /// /// /// private static void UpdateGetParamsForDateSelection(DownloadDateSelection downloadDateSelection, @@ -3336,8 +3257,13 @@ public class ApiService(IAuthService authService, IConfigService configService, } private static void UpdateGetParamsForDateSelection(DownloadDateSelection downloadDateSelection, - ref Dictionary getParams, string unixTimeStampInMicrosec) + ref Dictionary getParams, string? unixTimeStampInMicrosec) { + if (string.IsNullOrWhiteSpace(unixTimeStampInMicrosec)) + { + return; + } + switch (downloadDateSelection) { case DownloadDateSelection.before: @@ -3357,29 +3283,27 @@ public class ApiService(IAuthService authService, IConfigService configService, try { Dictionary users = new(); - SubscriptionEntities.Subscriptions subscriptions = new(); Log.Debug("Calling GetAllSubscrptions"); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); SubscriptionsDtos.SubscriptionsDto? subscriptionsDto = - JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); - subscriptions = SubscriptionsMapper.FromDto(subscriptionsDto); + DeserializeJson(body, s_mJsonSerializerSettings); + SubscriptionEntities.Subscriptions subscriptions = SubscriptionsMapper.FromDto(subscriptionsDto); if (subscriptions.HasMore) { getParams["offset"] = subscriptions.List.Count.ToString(); while (true) { - SubscriptionEntities.Subscriptions newSubscriptions = new(); + SubscriptionEntities.Subscriptions newSubscriptions; string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); if (!string.IsNullOrEmpty(loopbody) && (!loopbody.Contains("[]") || loopbody.Trim() != "[]")) { SubscriptionsDtos.SubscriptionsDto? newSubscriptionsDto = - JsonConvert.DeserializeObject(loopbody, - s_mJsonSerializerSettings); + DeserializeJson(loopbody, s_mJsonSerializerSettings); newSubscriptions = SubscriptionsMapper.FromDto(newSubscriptionsDto); } else diff --git a/OF DL.Core/Services/AuthService.cs b/OF DL.Core/Services/AuthService.cs index 2755bfb..45c7afc 100644 --- a/OF DL.Core/Services/AuthService.cs +++ b/OF DL.Core/Services/AuthService.cs @@ -136,6 +136,11 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService return; } + if (string.IsNullOrWhiteSpace(CurrentAuth.Cookie)) + { + return; + } + string pattern = @"(auth_id=\d+)|(sess=[^;]+)"; MatchCollection matches = Regex.Matches(CurrentAuth.Cookie, pattern); @@ -176,7 +181,7 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService } } - private async Task GetAuthFromBrowser(bool isDocker = false) + private async Task GetAuthFromBrowser() { try { diff --git a/OF DL.Core/Services/ConfigService.cs b/OF DL.Core/Services/ConfigService.cs index d34b3d3..d49a987 100644 --- a/OF DL.Core/Services/ConfigService.cs +++ b/OF DL.Core/Services/ConfigService.cs @@ -44,7 +44,7 @@ public class ConfigService(ILoggingService loggingService) : IConfigService } // Check for command-line arguments - if (args != null && args.Length > 0) + if (args.Length > 0) { const string NON_INTERACTIVE_ARG = "--non-interactive"; if (args.Any(a => a.Equals(NON_INTERACTIVE_ARG, StringComparison.OrdinalIgnoreCase))) @@ -71,12 +71,6 @@ public class ConfigService(ILoggingService loggingService) : IConfigService public async Task SaveConfigurationAsync(string filePath = "config.conf") { - if (CurrentConfig == null) - { - Log.Warning("Attempted to save null config to file"); - return; - } - try { string hoconConfig = BuildHoconFromConfig(CurrentConfig); @@ -409,7 +403,13 @@ public class ConfigService(ILoggingService loggingService) : IConfigService ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute(); if (attr != null) { - result.Add((propInfo.Name, (bool)propInfo.GetValue(CurrentConfig)!)); + bool? value = (bool?)propInfo.GetValue(CurrentConfig); + if (value == null) + { + continue; + } + + result.Add((propInfo.Name, value.Value)); } } @@ -427,10 +427,16 @@ public class ConfigService(ILoggingService loggingService) : IConfigService if (attr != null) { bool newValue = selectedNames.Contains(propInfo.Name); - bool oldValue = (bool)propInfo.GetValue(CurrentConfig)!; + bool? oldValue = (bool?)propInfo.GetValue(CurrentConfig); + + if (oldValue == null) + { + continue; + } + propInfo.SetValue(newConfig, newValue); - if (newValue != oldValue) + if (newValue != oldValue.Value) { configChanged = true; } diff --git a/OF DL.Core/Services/DBService.cs b/OF DL.Core/Services/DBService.cs index 2ff6c29..c9b6df0 100644 --- a/OF DL.Core/Services/DBService.cs +++ b/OF DL.Core/Services/DBService.cs @@ -219,17 +219,17 @@ public class DBService(IConfigService configService) : IDBService connection)) { checkCmd.Parameters.AddWithValue("@userId", user.Value); - using (SqliteDataReader reader = await checkCmd.ExecuteReaderAsync()) + await using (SqliteDataReader reader = await checkCmd.ExecuteReaderAsync()) { if (reader.Read()) { - long storedUserId = reader.GetInt64(0); string storedUsername = reader.GetString(1); if (storedUsername != user.Key) { - using (SqliteCommand updateCmd = - new("UPDATE users SET username = @newUsername WHERE user_id = @userId;", connection)) + await using (SqliteCommand updateCmd = + new("UPDATE users SET username = @newUsername WHERE user_id = @userId;", + connection)) { updateCmd.Parameters.AddWithValue("@newUsername", user.Key); updateCmd.Parameters.AddWithValue("@userId", user.Value); @@ -278,13 +278,13 @@ public class DBService(IConfigService configService) : IDBService if (count == 0) { // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = + await using SqliteCommand insertCmd = new( "INSERT INTO messages(post_id, text, price, paid, archived, created_at, user_id, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @user_id, @record_created_at)", connection); insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@message_text", message_text); + insertCmd.Parameters.AddWithValue("@price", price); insertCmd.Parameters.AddWithValue("@is_paid", is_paid); insertCmd.Parameters.AddWithValue("@is_archived", is_archived); insertCmd.Parameters.AddWithValue("@created_at", created_at); @@ -328,8 +328,8 @@ public class DBService(IConfigService configService) : IDBService "INSERT INTO posts(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", connection); insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@message_text", message_text); + insertCmd.Parameters.AddWithValue("@price", price); insertCmd.Parameters.AddWithValue("@is_paid", is_paid); insertCmd.Parameters.AddWithValue("@is_archived", is_archived); insertCmd.Parameters.AddWithValue("@created_at", created_at); @@ -372,8 +372,8 @@ public class DBService(IConfigService configService) : IDBService "INSERT INTO stories(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", connection); insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@message_text", message_text); + insertCmd.Parameters.AddWithValue("@price", price); insertCmd.Parameters.AddWithValue("@is_paid", is_paid); insertCmd.Parameters.AddWithValue("@is_archived", is_archived); insertCmd.Parameters.AddWithValue("@created_at", created_at); @@ -445,23 +445,21 @@ public class DBService(IConfigService configService) : IDBService { try { - bool downloaded = false; + bool downloaded; - using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) + await using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + StringBuilder sql = new("SELECT downloaded FROM medias WHERE media_id=@media_id"); + if (configService.CurrentConfig.DownloadDuplicatedMedia) { - StringBuilder sql = new("SELECT downloaded FROM medias WHERE media_id=@media_id"); - if (configService.CurrentConfig.DownloadDuplicatedMedia) - { - sql.Append(" and api_type=@api_type"); - } - - connection.Open(); - using SqliteCommand cmd = new(sql.ToString(), connection); - cmd.Parameters.AddWithValue("@media_id", media_id); - cmd.Parameters.AddWithValue("@api_type", api_type); - downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync()); + sql.Append(" and api_type=@api_type"); } + connection.Open(); + await using SqliteCommand cmd = new(sql.ToString(), connection); + cmd.Parameters.AddWithValue("@media_id", media_id); + cmd.Parameters.AddWithValue("@api_type", api_type); + downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync()); + return downloaded; } catch (Exception ex) diff --git a/OF DL.Core/Services/DownloadOrchestrationService.cs b/OF DL.Core/Services/DownloadOrchestrationService.cs index 288d7c4..ebf2d08 100644 --- a/OF DL.Core/Services/DownloadOrchestrationService.cs +++ b/OF DL.Core/Services/DownloadOrchestrationService.cs @@ -144,9 +144,9 @@ public class DownloadOrchestrationService( counts.PaidPostCount = await DownloadContentTypeAsync("Paid Posts", async statusReporter => await apiService.GetPaidPosts("/posts/paid/post", path, username, PaidPostIds, statusReporter), - posts => posts?.PaidPosts?.Count ?? 0, - posts => posts?.PaidPostObjects?.Count ?? 0, - posts => posts?.PaidPosts?.Values?.ToList(), + posts => posts.PaidPosts.Count, + posts => posts.PaidPostObjects.Count, + posts => posts.PaidPosts.Values.ToList(), async (posts, reporter) => await downloadService.DownloadPaidPosts(username, userId, path, users, clientIdBlobMissing, devicePrivateKeyMissing, posts, reporter), eventHandler); @@ -161,9 +161,9 @@ public class DownloadOrchestrationService( counts.PostCount = await DownloadContentTypeAsync("Posts", async statusReporter => await apiService.GetPosts($"/users/{userId}/posts", path, PaidPostIds, statusReporter), - posts => posts?.Posts?.Count ?? 0, - posts => posts?.PostObjects?.Count ?? 0, - posts => posts?.Posts?.Values?.ToList(), + posts => posts.Posts.Count, + posts => posts.PostObjects.Count, + posts => posts.Posts.Values.ToList(), async (posts, reporter) => await downloadService.DownloadFreePosts(username, userId, path, users, clientIdBlobMissing, devicePrivateKeyMissing, posts, reporter), eventHandler); @@ -174,9 +174,9 @@ public class DownloadOrchestrationService( counts.ArchivedCount = await DownloadContentTypeAsync("Archived Posts", async statusReporter => await apiService.GetArchived($"/users/{userId}/posts", path, statusReporter), - archived => archived?.ArchivedPosts?.Count ?? 0, - archived => archived?.ArchivedPostObjects?.Count ?? 0, - archived => archived?.ArchivedPosts?.Values?.ToList(), + archived => archived.ArchivedPosts.Count, + archived => archived.ArchivedPostObjects.Count, + archived => archived.ArchivedPosts.Values.ToList(), async (archived, reporter) => await downloadService.DownloadArchived(username, userId, path, users, clientIdBlobMissing, devicePrivateKeyMissing, archived, reporter), eventHandler); @@ -187,9 +187,9 @@ public class DownloadOrchestrationService( counts.StreamsCount = await DownloadContentTypeAsync("Streams", async statusReporter => await apiService.GetStreams($"/users/{userId}/posts/streams", path, PaidPostIds, statusReporter), - streams => streams?.Streams?.Count ?? 0, - streams => streams?.StreamObjects?.Count ?? 0, - streams => streams?.Streams?.Values?.ToList(), + streams => streams.Streams.Count, + streams => streams.StreamObjects.Count, + streams => streams.Streams.Values.ToList(), async (streams, reporter) => await downloadService.DownloadStreams(username, userId, path, users, clientIdBlobMissing, devicePrivateKeyMissing, streams, reporter), eventHandler); @@ -256,9 +256,9 @@ public class DownloadOrchestrationService( counts.MessagesCount = await DownloadContentTypeAsync("Messages", async statusReporter => await apiService.GetMessages($"/chats/{userId}/messages", path, statusReporter), - messages => messages.Messages?.Count ?? 0, - messages => messages.MessageObjects?.Count ?? 0, - messages => messages?.Messages.Values.ToList(), + messages => messages.Messages.Count, + messages => messages.MessageObjects.Count, + messages => messages.Messages.Values.ToList(), async (messages, reporter) => await downloadService.DownloadMessages(username, userId, path, users, clientIdBlobMissing, devicePrivateKeyMissing, messages, reporter), eventHandler); @@ -269,9 +269,9 @@ public class DownloadOrchestrationService( counts.PaidMessagesCount = await DownloadContentTypeAsync("Paid Messages", async statusReporter => await apiService.GetPaidMessages("/posts/paid/chat", path, username, statusReporter), - paidMessages => paidMessages?.PaidMessages?.Count ?? 0, - paidMessages => paidMessages?.PaidMessageObjects?.Count ?? 0, - paidMessages => paidMessages?.PaidMessages?.Values?.ToList(), + paidMessages => paidMessages.PaidMessages.Count, + paidMessages => paidMessages.PaidMessageObjects.Count, + paidMessages => paidMessages.PaidMessages.Values.ToList(), async (paidMessages, reporter) => await downloadService.DownloadPaidMessages(username, path, users, clientIdBlobMissing, devicePrivateKeyMissing, paidMessages, reporter), eventHandler); @@ -368,7 +368,7 @@ public class DownloadOrchestrationService( int paidMessagesCount = 0; // Download paid posts - if (purchasedTabCollection.PaidPosts?.PaidPosts?.Count > 0) + if (purchasedTabCollection.PaidPosts.PaidPosts.Count > 0) { eventHandler.OnContentFound("Paid Posts", purchasedTabCollection.PaidPosts.PaidPosts.Count, @@ -396,7 +396,7 @@ public class DownloadOrchestrationService( } // Download paid messages - if (purchasedTabCollection.PaidMessages?.PaidMessages?.Count > 0) + if (purchasedTabCollection.PaidMessages.PaidMessages.Count > 0) { eventHandler.OnContentFound("Paid Messages", purchasedTabCollection.PaidMessages.PaidMessages.Count, diff --git a/OF DL.Core/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs index eaee01f..0fb1f80 100644 --- a/OF DL.Core/Services/DownloadService.cs +++ b/OF DL.Core/Services/DownloadService.cs @@ -25,7 +25,7 @@ public class DownloadService( IAPIService apiService) : IDownloadService { - private TaskCompletionSource _completionSource; + private TaskCompletionSource _completionSource = new(); public async Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username) { @@ -201,7 +201,7 @@ public class DownloadService( Engine ffmpeg = new(configService.CurrentConfig.FFmpegPath); ffmpeg.Error += OnError; - ffmpeg.Complete += async (sender, args) => + ffmpeg.Complete += async (_, _) => { _completionSource.TrySetResult(true); await OnFFMPEGDownloadComplete(tempFilename, lastModified, folder, path, customFileName, filename, @@ -275,14 +275,14 @@ public class DownloadService( } } - private void OnError(object sender, ConversionErrorEventArgs e) + private void OnError(object? sender, ConversionErrorEventArgs e) { // Guard all fields to avoid NullReference exceptions from FFmpeg.NET - string input = e?.Input?.Name ?? ""; - string output = e?.Output?.Name ?? ""; - string exitCode = e?.Exception?.ExitCode.ToString() ?? ""; - string message = e?.Exception?.Message ?? ""; - string inner = e?.Exception?.InnerException?.Message ?? ""; + string input = e.Input?.Name ?? ""; + string output = e.Output?.Name ?? ""; + string exitCode = e.Exception?.ExitCode.ToString() ?? ""; + string message = e.Exception?.Message ?? ""; + string inner = e.Exception?.InnerException?.Message ?? ""; Log.Error("FFmpeg failed. Input={Input} Output={Output} ExitCode={ExitCode} Message={Message} Inner={Inner}", input, output, exitCode, message, inner); @@ -310,7 +310,7 @@ public class DownloadService( string body = await response.Content.ReadAsStringAsync(); XDocument doc = XDocument.Parse(body); XNamespace ns = "urn:mpeg:dash:schema:mpd:2011"; - XNamespace cenc = "urn:mpeg:cenc:2013"; + // XNamespace cenc = "urn:mpeg:cenc:2013"; XElement? videoAdaptationSet = doc .Descendants(ns + "AdaptationSet") .FirstOrDefault(e => (string?)e.Attribute("mimeType") == "video/mp4"); @@ -392,7 +392,6 @@ public class DownloadService( { try { - string customFileName = ""; if (!Directory.Exists(folder + path)) { Directory.CreateDirectory(folder + path); @@ -580,19 +579,19 @@ public class DownloadService( public static async Task GetDRMVideoLastModified(string url, Auth auth) { string[] messageUrlParsed = url.Split(','); - string mpdURL = messageUrlParsed[0]; + string mpdUrl = messageUrlParsed[0]; string policy = messageUrlParsed[1]; string signature = messageUrlParsed[2]; string kvp = messageUrlParsed[3]; - mpdURL = mpdURL.Replace(".mpd", "_source.mp4"); + mpdUrl = mpdUrl.Replace(".mpd", "_source.mp4"); using HttpClient client = new(); client.DefaultRequestHeaders.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {auth.Cookie}"); client.DefaultRequestHeaders.Add("User-Agent", auth.UserAgent); - using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); + using HttpResponseMessage response = await client.GetAsync(mpdUrl, HttpCompletionOption.ResponseHeadersRead); if (response.IsSuccessStatusCode) { if (response.Content.Headers.LastModified != null) @@ -624,8 +623,8 @@ public class DownloadService( /// Processes the download and database update of media. /// /// The folder where the media is stored. - /// The ID of the media. - /// + /// The ID of the media. + /// /// The URL from where to download the media. /// The relative path to the media. /// @@ -634,8 +633,8 @@ public class DownloadService( /// /// A Task resulting in a boolean indicating whether the media is newly downloaded or not. public async Task ProcessMediaDownload(string folder, - long media_id, - string api_type, + long mediaId, + string apiType, string url, string path, string serverFilename, @@ -645,11 +644,11 @@ public class DownloadService( { try { - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + if (!await dbService.CheckDownloaded(folder, mediaId, apiType)) { return await HandleNewMedia(folder, - media_id, - api_type, + mediaId, + apiType, url, path, serverFilename, @@ -658,11 +657,11 @@ public class DownloadService( progressReporter); } - bool status = await HandlePreviouslyDownloadedMediaAsync(folder, media_id, api_type, progressReporter); + bool status = await HandlePreviouslyDownloadedMediaAsync(folder, mediaId, apiType, progressReporter); if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && serverFilename != resolvedFilename) { - await HandleRenamingOfExistingFilesAsync(folder, media_id, api_type, path, serverFilename, + await HandleRenamingOfExistingFilesAsync(folder, mediaId, apiType, path, serverFilename, resolvedFilename, extension); } @@ -677,9 +676,9 @@ public class DownloadService( } - private async Task HandleRenamingOfExistingFilesAsync(string folder, - long media_id, - string api_type, + private async Task HandleRenamingOfExistingFilesAsync(string folder, + long mediaId, + string apiType, string path, string serverFilename, string resolvedFilename, @@ -689,7 +688,7 @@ public class DownloadService( string fullPathWithTheNewFileName = $"{folder}{path}/{resolvedFilename}{extension}"; if (!File.Exists(fullPathWithTheServerFileName)) { - return false; + return; } try @@ -699,14 +698,13 @@ public class DownloadService( catch (Exception ex) { Console.WriteLine($"An error occurred: {ex.Message}"); - return false; + return; } - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + long size = await dbService.GetStoredFileSize(folder, mediaId, apiType); DateTime lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, resolvedFilename + extension, size, true, + await dbService.UpdateMedia(folder, mediaId, apiType, folder + path, resolvedFilename + extension, size, true, lastModified); - return true; } @@ -1084,17 +1082,13 @@ public class DownloadService( } public async Task<(string decryptionKey, DateTime lastModified)?> GetDecryptionInfo( - string mpdURL, string policy, string signature, string kvp, + string mpdUrl, string policy, string signature, string kvp, string mediaId, string contentId, string drmType, bool clientIdBlobMissing, bool devicePrivateKeyMissing) { - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh == null) - { - return null; - } + string pssh = await apiService.GetDRMMPDPSSH(mpdUrl, policy, signature, kvp); - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdUrl, policy, signature, kvp); Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/{drmType}/{contentId}", "?type=widevine"); @@ -1220,7 +1214,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadArchived - {username}"); - if (archived == null || archived.ArchivedPosts.Count == 0) + if (archived.ArchivedPosts.Count == 0) { Log.Debug("Found 0 Archived Posts"); return new DownloadResult @@ -1244,8 +1238,9 @@ public class DownloadService( bool isNew; ArchivedEntities.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); - ArchivedEntities.ListItem? postInfo = - archived.ArchivedPostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); + ArchivedEntities.ListItem? postInfo = mediaInfo == null + ? null + : archived.ArchivedPostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; @@ -1300,7 +1295,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadMessages - {username}"); - if (messages == null || messages.Messages.Count == 0) + if (messages.Messages.Count == 0) { Log.Debug("Found 0 Messages"); return new DownloadResult @@ -1323,11 +1318,11 @@ public class DownloadService( bool isNew; MessageEntities.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.Id == messageKVP.Key); MessageEntities.ListItem? messageInfo = messages.MessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == messageKVP.Key) == true); + p.Media?.Any(m => m.Id == messageKVP.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? ""; string messagePath = configService.CurrentConfig.FolderPerMessage && messageInfo != null && - messageInfo?.Id is not null && messageInfo?.CreatedAt is not null + messageInfo.Id != 0 && messageInfo.CreatedAt is not null ? $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Messages/Free"; @@ -1383,7 +1378,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadPaidMessages - {username}"); - if (paidMessageCollection == null || paidMessageCollection.PaidMessages.Count == 0) + if (paidMessageCollection.PaidMessages.Count == 0) { Log.Debug("Found 0 Paid Messages"); return new DownloadResult @@ -1408,11 +1403,11 @@ public class DownloadService( MessageEntities.Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == kvpEntry.Key); PurchasedEntities.ListItem? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == kvpEntry.Key) == true); + p.Media?.Any(m => m.Id == kvpEntry.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? ""; string paidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && - messageInfo?.Id is not null && messageInfo?.CreatedAt is not null + messageInfo.Id != 0 && messageInfo.CreatedAt is not null ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Messages/Paid"; @@ -1465,7 +1460,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadStreams - {username}"); - if (streams == null || streams.Streams.Count == 0) + if (streams.Streams.Count == 0) { Log.Debug("Found 0 Streams"); return new DownloadResult @@ -1487,12 +1482,12 @@ public class DownloadService( { bool isNew; StreamEntities.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.Id == kvpEntry.Key); - StreamEntities.ListItem? streamInfo = - streams.StreamObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); + StreamEntities.ListItem? streamInfo = mediaInfo == null + ? null + : streams.StreamObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; - string streamPath = configService.CurrentConfig.FolderPerPost && streamInfo != null && - streamInfo?.Id is not null && streamInfo?.PostedAt is not null + string streamPath = configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo.Id != 0 ? $"/Posts/Free/{streamInfo.Id} {streamInfo.PostedAt:yyyy-MM-dd HH-mm-ss}" : "/Posts/Free"; @@ -1546,7 +1541,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadFreePosts - {username}"); - if (posts == null || posts.Posts.Count == 0) + if (posts.Posts.Count == 0) { Log.Debug("Found 0 Posts"); return new DownloadResult @@ -1566,13 +1561,13 @@ public class DownloadService( foreach (KeyValuePair postKVP in posts.Posts) { bool isNew; - PostEntities.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m?.Id == postKVP.Key); - PostEntities.ListItem? postInfo = - posts.PostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); + PostEntities.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m.Id == postKVP.Key); + PostEntities.ListItem? postInfo = mediaInfo == null + ? null + : posts.PostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; - string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && - postInfo?.Id is not null && postInfo?.PostedAt is not null + string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo.Id != 0 ? $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}" : "/Posts/Free"; @@ -1625,7 +1620,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadPaidPosts - {username}"); - if (purchasedPosts == null || purchasedPosts.PaidPosts.Count == 0) + if (purchasedPosts.PaidPosts.Count == 0) { Log.Debug("Found 0 Paid Posts"); return new DownloadResult @@ -1649,11 +1644,11 @@ public class DownloadService( MessageEntities.Medium? mediaInfo = purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKVP.Key); PurchasedEntities.ListItem? postInfo = - purchasedPosts.PaidPostObjects.FirstOrDefault(p => p?.Media?.Any(m => m.Id == postKVP.Key) == true); + purchasedPosts.PaidPostObjects.FirstOrDefault(p => p.Media?.Any(m => m.Id == postKVP.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; string paidPostPath = configService.CurrentConfig.FolderPerPaidPost && postInfo != null && - postInfo?.Id is not null && postInfo?.PostedAt is not null + postInfo.Id != 0 && postInfo.PostedAt is not null ? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Posts/Paid"; @@ -1706,7 +1701,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadPaidPostsPurchasedTab - {username}"); - if (purchasedPosts == null || purchasedPosts.PaidPosts.Count == 0) + if (purchasedPosts.PaidPosts.Count == 0) { Log.Debug("Found 0 Paid Posts"); return new DownloadResult { TotalCount = 0, MediaType = "Paid Posts", Success = true }; @@ -1721,12 +1716,12 @@ public class DownloadService( purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); PurchasedEntities.ListItem? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true) + p.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true) : null; string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) .PaidPostFileNameFormat ?? ""; string paidPostPath = configService.CurrentConfig.FolderPerPaidPost && postInfo != null && - postInfo?.Id is not null && postInfo?.PostedAt is not null + postInfo.Id != 0 && postInfo.PostedAt is not null ? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Posts/Paid"; @@ -1766,7 +1761,7 @@ public class DownloadService( Log.Debug($"Paid Posts Already Downloaded: {oldCount} New Paid Posts Downloaded: {newCount}"); return new DownloadResult { - TotalCount = purchasedPosts.PaidPosts.Count, + TotalCount = purchasedPosts?.PaidPosts.Count ?? 0, NewDownloads = newCount, ExistingDownloads = oldCount, MediaType = "Paid Posts", @@ -1780,7 +1775,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadPaidMessagesPurchasedTab - {username}"); - if (paidMessageCollection == null || paidMessageCollection.PaidMessages.Count == 0) + if (paidMessageCollection.PaidMessages.Count == 0) { Log.Debug("Found 0 Paid Messages"); return new DownloadResult { TotalCount = 0, MediaType = "Paid Messages", Success = true }; @@ -1795,11 +1790,11 @@ public class DownloadService( paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKVP.Key); PurchasedEntities.ListItem? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + p.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) .PaidMessageFileNameFormat ?? ""; string paidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && - messageInfo?.Id is not null && messageInfo?.CreatedAt is not null + messageInfo.Id != 0 && messageInfo.CreatedAt is not null ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Messages/Paid"; @@ -1853,7 +1848,7 @@ public class DownloadService( { Log.Debug($"Calling DownloadSinglePost - {username}"); - if (post == null || post.SinglePosts.Count == 0) + if (post.SinglePosts.Count == 0) { Log.Debug("Couldn't find post"); return new DownloadResult { TotalCount = 0, MediaType = "Posts", Success = true }; @@ -1864,12 +1859,12 @@ public class DownloadService( foreach (KeyValuePair postKVP in post.SinglePosts) { PostEntities.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.Id == postKVP.Key); - PostEntities.SinglePost? postInfo = - post.SinglePostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true); + PostEntities.SinglePost? postInfo = mediaInfo == null + ? null + : post.SinglePostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) .PostFileNameFormat ?? ""; - string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && - postInfo?.Id is not null && postInfo?.PostedAt is not null + string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo.Id != 0 ? $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}" : "/Posts/Free"; @@ -1930,12 +1925,7 @@ public class DownloadService( PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection, IProgressReporter progressReporter) { - Log.Debug($"Calling DownloadSinglePaidMessage - {username}"); - - if (singlePaidMessageCollection == null) - { - return new DownloadResult { TotalCount = 0, MediaType = "Paid Messages", Success = true }; - } + Log.Debug("Calling DownloadSinglePaidMessage - {Username}", username); int totalNew = 0, totalOld = 0; @@ -1949,11 +1939,11 @@ public class DownloadService( m.Id == paidMessageKVP.Key); MessageEntities.SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + p.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) .PaidMessageFileNameFormat ?? ""; string previewMsgPath = configService.CurrentConfig.FolderPerMessage && messageInfo != null && - messageInfo?.Id is not null && messageInfo?.CreatedAt is not null + messageInfo.Id != 0 && messageInfo.CreatedAt is not null ? $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Messages/Free"; @@ -2002,12 +1992,12 @@ public class DownloadService( m.Id == paidMessageKVP.Key); MessageEntities.SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + p.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) .PaidMessageFileNameFormat ?? ""; string singlePaidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && - messageInfo != null && messageInfo?.Id is not null && - messageInfo?.CreatedAt is not null + messageInfo != null && messageInfo.Id != 0 && + messageInfo.CreatedAt is not null ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Messages/Paid"; diff --git a/OF DL.Core/Services/StartupService.cs b/OF DL.Core/Services/StartupService.cs index 6769345..b33747d 100644 --- a/OF DL.Core/Services/StartupService.cs +++ b/OF DL.Core/Services/StartupService.cs @@ -38,7 +38,7 @@ public class StartupService(IConfigService configService, IAuthService authServi !result.FfmpegPath.Contains(@":\\")) { result.FfmpegPath = result.FfmpegPath.Replace(@"\", @"\\"); - configService.CurrentConfig!.FFmpegPath = result.FfmpegPath; + configService.CurrentConfig.FFmpegPath = result.FfmpegPath; } // Get FFmpeg version @@ -144,7 +144,7 @@ public class StartupService(IConfigService configService, IAuthService authServi private void DetectFfmpeg(StartupResult result) { - if (!string.IsNullOrEmpty(configService.CurrentConfig!.FFmpegPath) && + if (!string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath) && ValidateFilePath(configService.CurrentConfig.FFmpegPath)) { result.FfmpegFound = true; diff --git a/OF DL.Core/Utils/ThrottledStream.cs b/OF DL.Core/Utils/ThrottledStream.cs index b53b712..0e1b128 100644 --- a/OF DL.Core/Utils/ThrottledStream.cs +++ b/OF DL.Core/Utils/ThrottledStream.cs @@ -67,7 +67,7 @@ public class ThrottledStream : Stream } } - protected async Task ThrottleAsync(int bytes) + private async Task ThrottleAsync(int bytes) { if (!shouldThrottle) { diff --git a/OF DL.Core/Utils/XmlUtils.cs b/OF DL.Core/Utils/XmlUtils.cs index 08d3864..4f9bbf6 100644 --- a/OF DL.Core/Utils/XmlUtils.cs +++ b/OF DL.Core/Utils/XmlUtils.cs @@ -21,6 +21,7 @@ internal static class XmlUtils } catch { + // ignored } return ""; diff --git a/OF DL.Core/Widevine/CDM.cs b/OF DL.Core/Widevine/CDM.cs index 9cb20c3..4dc3cb0 100644 --- a/OF DL.Core/Widevine/CDM.cs +++ b/OF DL.Core/Widevine/CDM.cs @@ -112,7 +112,7 @@ public class CDM { //needed for HBO Max - PSSHBox psshBox = PSSHBox.FromByteArray(initData); + PsshBox psshBox = PsshBox.FromByteArray(initData); cencHeader = Serializer.Deserialize(new MemoryStream(psshBox.Data)); } catch @@ -278,7 +278,7 @@ public class CDM encryptedClientIdProto.EncryptedClientId = mstream.ToArray(); using RSACryptoServiceProvider RSA = new(); - RSA.ImportRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey, out int bytesRead); + RSA.ImportRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey, out int _); encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aesProvider.Key, RSAEncryptionPadding.OaepSHA1); encryptedClientIdProto.EncryptedClientIdIv = aesProvider.IV; encryptedClientIdProto.ServiceId = diff --git a/OF DL.Core/Widevine/CDMApi.cs b/OF DL.Core/Widevine/CDMApi.cs index f00c83b..ba952dc 100644 --- a/OF DL.Core/Widevine/CDMApi.cs +++ b/OF DL.Core/Widevine/CDMApi.cs @@ -29,5 +29,13 @@ public class CDMApi CDM.ProvideLicense(SessionId, Convert.FromBase64String(licenseB64)); } - public List GetKeys() => CDM.GetKeys(SessionId); + public List GetKeys() + { + if (SessionId == null) + { + throw new Exception("No session ID set. Could not get keys"); + } + + return CDM.GetKeys(SessionId); + } } diff --git a/OF DL.Core/Widevine/CDMDevice.cs b/OF DL.Core/Widevine/CDMDevice.cs index 61ba112..c17f5eb 100644 --- a/OF DL.Core/Widevine/CDMDevice.cs +++ b/OF DL.Core/Widevine/CDMDevice.cs @@ -43,6 +43,10 @@ public class CDMDevice using StreamReader reader = File.OpenText(privateKeyPath); DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject(); } + else + { + throw new Exception("No device private key found"); + } if (vmpBytes != null) { diff --git a/OF DL.Core/Widevine/PSSHBox.cs b/OF DL.Core/Widevine/PsshBox.cs similarity index 64% rename from OF DL.Core/Widevine/PSSHBox.cs rename to OF DL.Core/Widevine/PsshBox.cs index 0e59099..1291f0a 100644 --- a/OF DL.Core/Widevine/PSSHBox.cs +++ b/OF DL.Core/Widevine/PsshBox.cs @@ -1,34 +1,34 @@ namespace OF_DL.Widevine; -internal class PSSHBox +internal class PsshBox { - private static readonly byte[] PSSH_HEADER = new byte[] { 0x70, 0x73, 0x73, 0x68 }; + private static readonly byte[] s_psshHeader = [0x70, 0x73, 0x73, 0x68]; - private PSSHBox(List kids, byte[] data) + private PsshBox(List kids, byte[] data) { KIDs = kids; Data = data; } - public List KIDs { get; set; } = new(); + public List KIDs { get; set; } public byte[] Data { get; set; } - public static PSSHBox FromByteArray(byte[] psshbox) + public static PsshBox FromByteArray(byte[] psshbox) { using MemoryStream stream = new(psshbox); stream.Seek(4, SeekOrigin.Current); byte[] header = new byte[4]; - stream.Read(header, 0, 4); + stream.ReadExactly(header, 0, 4); - if (!header.SequenceEqual(PSSH_HEADER)) + if (!header.SequenceEqual(s_psshHeader)) { throw new Exception("Not a pssh box"); } stream.Seek(20, SeekOrigin.Current); byte[] kidCountBytes = new byte[4]; - stream.Read(kidCountBytes, 0, 4); + stream.ReadExactly(kidCountBytes, 0, 4); if (BitConverter.IsLittleEndian) { @@ -41,12 +41,12 @@ internal class PSSHBox for (int i = 0; i < kidCount; i++) { byte[] kid = new byte[16]; - stream.Read(kid); + stream.ReadExactly(kid); kids.Add(kid); } byte[] dataLengthBytes = new byte[4]; - stream.Read(dataLengthBytes); + stream.ReadExactly(dataLengthBytes); if (BitConverter.IsLittleEndian) { @@ -57,12 +57,12 @@ internal class PSSHBox if (dataLength == 0) { - return new PSSHBox(kids, []); + return new PsshBox(kids, []); } byte[] data = new byte[dataLength]; - stream.Read(data); + stream.ReadExactly(data); - return new PSSHBox(kids, data); + return new PsshBox(kids, data); } } From 85c9bc1f57d4d8c40d7506e1618bcd65bebd57cd Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 03:06:15 -0600 Subject: [PATCH 37/55] Fix docker builds --- .gitattributes | 5 ++++ Dockerfile | 3 +- OF DL.Core/Services/ConfigService.cs | 32 +++++++++++---------- OF DL/Program.cs | 42 +++++++++++++++++----------- 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/.gitattributes b/.gitattributes index 1ff0c42..06ec5e7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,11 @@ ############################################################################### * text=auto +############################################################################### +# Shell scripts should use LF line endings (avoid /bin/sh^M issues in containers) +############################################################################### +*.sh text eol=lf + ############################################################################### # Set default behavior for command prompt diff. # diff --git a/Dockerfile b/Dockerfile index 1307df3..013ab31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,12 @@ RUN apk --no-cache --repository community add \ # Copy source code COPY ["OF DL.sln", "/src/OF DL.sln"] COPY ["OF DL", "/src/OF DL"] +COPY ["OF DL.Core", "/src/OF DL.Core"] WORKDIR "/src" # Build release -RUN dotnet publish -p:WarningLevel=0 -p:Version=$VERSION -c Release --self-contained true -p:PublishSingleFile=true -o out +RUN dotnet publish "OF DL/OF DL.csproj" -p:WarningLevel=0 -p:Version=$VERSION -c Release -o out # Generate default config.conf files RUN /src/out/OF\ DL --non-interactive || true && \ diff --git a/OF DL.Core/Services/ConfigService.cs b/OF DL.Core/Services/ConfigService.cs index d49a987..6de7c00 100644 --- a/OF DL.Core/Services/ConfigService.cs +++ b/OF DL.Core/Services/ConfigService.cs @@ -20,6 +20,21 @@ public class ConfigService(ILoggingService loggingService) : IConfigService try { IsCliNonInteractive = false; + if (args.Length > 0) + { + const string nonInteractiveArg = "--non-interactive"; + if (args.Any(a => a.Equals(nonInteractiveArg, StringComparison.OrdinalIgnoreCase))) + { + IsCliNonInteractive = true; + Log.Debug("NonInteractiveMode set via command line"); + } + + Log.Debug("Additional arguments:"); + foreach (string argument in args) + { + Log.Debug(argument); + } + } // Migrate from config.json to config.conf if needed await MigrateFromJsonToConfAsync(); @@ -43,21 +58,10 @@ public class ConfigService(ILoggingService loggingService) : IConfigService } } - // Check for command-line arguments - if (args.Length > 0) + if (IsCliNonInteractive && !CurrentConfig.NonInteractiveMode) { - const string NON_INTERACTIVE_ARG = "--non-interactive"; - if (args.Any(a => a.Equals(NON_INTERACTIVE_ARG, StringComparison.OrdinalIgnoreCase))) - { - IsCliNonInteractive = true; - Log.Debug("NonInteractiveMode set via command line"); - } - - Log.Debug("Additional arguments:"); - foreach (string argument in args) - { - Log.Debug(argument); - } + CurrentConfig.NonInteractiveMode = true; + Log.Debug("NonInteractiveMode overridden to true via command line"); } return true; diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 31339ba..fd3150c 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -94,9 +94,9 @@ public class Program(IServiceProvider serviceProvider) if (!await configService.LoadConfigurationAsync(args)) { AnsiConsole.MarkupLine("\n[red]config.conf is not valid, check your syntax![/]\n"); - AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); if (!configService.IsCliNonInteractive) { + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); Console.ReadKey(); } @@ -146,10 +146,9 @@ public class Program(IServiceProvider serviceProvider) Console.Write( "OF-DL requires Windows 10 or higher when being run on Windows. Your reported version is: {0}\n\n", startupResult.OsVersionString); - Console.Write("Press any key to continue.\n"); - if (!configService.CurrentConfig.NonInteractiveMode) { + Console.Write("Press any key to continue.\n"); Console.ReadKey(); } @@ -158,12 +157,17 @@ public class Program(IServiceProvider serviceProvider) if (!startupResult.FfmpegFound) { - AnsiConsole.Markup( - "[red]Cannot locate FFmpeg; please modify config.conf with the correct path. Press any key to exit.[/]"); 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); } @@ -210,7 +214,11 @@ public class Program(IServiceProvider serviceProvider) 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"); - Console.ReadKey(); + if (!configService.CurrentConfig.NonInteractiveMode) + { + Console.WriteLine("\nPress any key to exit."); + Console.ReadKey(); + } Environment.Exit(2); } } @@ -234,9 +242,9 @@ public class Program(IServiceProvider serviceProvider) ex.InnerException.StackTrace); } - Console.WriteLine("\nPress any key to exit."); if (!configService.CurrentConfig.NonInteractiveMode) { + Console.WriteLine("\nPress any key to exit."); Console.ReadKey(); } @@ -737,9 +745,6 @@ public class Program(IServiceProvider serviceProvider) 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[/]"); - AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - - Console.ReadKey(); Environment.Exit(2); } @@ -749,14 +754,14 @@ public class Program(IServiceProvider serviceProvider) } else { - ShowAuthMissingError(); + ShowAuthMissingError(configService.CurrentConfig.NonInteractiveMode); } } else { if (configService.CurrentConfig.NonInteractiveMode) { - ShowAuthMissingError(); + ShowAuthMissingError(configService.CurrentConfig.NonInteractiveMode); } else if (!configService.CurrentConfig.DisableBrowserAuth) { @@ -764,21 +769,24 @@ public class Program(IServiceProvider serviceProvider) } else { - ShowAuthMissingError(); + ShowAuthMissingError(configService.CurrentConfig.NonInteractiveMode); } } } - private static void ShowAuthMissingError() + 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[/]"); - AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - Console.ReadKey(); + if (!nonInteractiveMode) + { + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); + Console.ReadKey(); + } Environment.Exit(2); } @@ -881,11 +889,11 @@ public class Program(IServiceProvider serviceProvider) { 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"); - AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); Log.Error("rules.json processing failed: {Error}", result.RulesJsonError); if (!configService.CurrentConfig.NonInteractiveMode) { + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); Console.ReadKey(); } From 4a218a3efeeab622442e3bbdf892b6079fcf60b7 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 09:10:59 -0600 Subject: [PATCH 38/55] Fix remaining compiler warnings --- OF DL.Core/Services/ApiService.cs | 103 +++++++-------- .../Services/DownloadOrchestrationService.cs | 2 +- OF DL.Core/Services/DownloadService.cs | 123 +++--------------- OF DL.Core/Services/StartupService.cs | 66 ++++------ OF DL.Core/Utils/ThrottledStream.cs | 4 +- OF DL.Core/Widevine/CDM.cs | 34 +++-- OF DL/CLI/SpectreDownloadEventHandler.cs | 44 +++++-- OF DL/Program.cs | 4 +- 8 files changed, 145 insertions(+), 235 deletions(-) diff --git a/OF DL.Core/Services/ApiService.cs b/OF DL.Core/Services/ApiService.cs index eb57808..9e1eb22 100644 --- a/OF DL.Core/Services/ApiService.cs +++ b/OF DL.Core/Services/ApiService.cs @@ -384,13 +384,13 @@ public class ApiService(IAuthService authService, IConfigService configService, string endpoint, string? username, string folder, - List paid_post_ids) + List paidPostIds) { Log.Debug($"Calling GetMedia - {username}"); try { - Dictionary return_urls = new(); + Dictionary returnUrls = new(); const int postLimit = 50; int limit = 5; int offset = 0; @@ -419,7 +419,7 @@ public class ApiService(IAuthService authService, IConfigService configService, if (string.IsNullOrWhiteSpace(body)) { Log.Warning("GetMedia returned empty response for {Endpoint}", endpoint); - return return_urls; + return returnUrls; } if (mediatype == MediaType.Stories) @@ -485,9 +485,9 @@ public class ApiService(IAuthService authService, IConfigService configService, continue; } - if (medium.CanView && !return_urls.ContainsKey(medium.Id)) + if (medium.CanView && !returnUrls.ContainsKey(medium.Id)) { - return_urls.Add(medium.Id, mediaUrl); + returnUrls.Add(medium.Id, mediaUrl); } } } @@ -538,22 +538,22 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - foreach (string highlight_id in highlightIds) + foreach (string highlightId in highlightIds) { - Dictionary highlight_headers = - GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, ""); + Dictionary highlightHeaders = + GetDynamicHeaders("/api2/v2/stories/highlights/" + highlightId, ""); - HttpClient highlight_client = GetHttpClient(); + HttpClient highlightClient = GetHttpClient(); - HttpRequestMessage highlight_request = new(HttpMethod.Get, - $"https://onlyfans.com/api2/v2/stories/highlights/{highlight_id}"); + HttpRequestMessage highlightRequest = new(HttpMethod.Get, + $"https://onlyfans.com/api2/v2/stories/highlights/{highlightId}"); - foreach (KeyValuePair keyValuePair in highlight_headers) + foreach (KeyValuePair keyValuePair in highlightHeaders) { - highlight_request.Headers.Add(keyValuePair.Key, keyValuePair.Value); + highlightRequest.Headers.Add(keyValuePair.Key, keyValuePair.Value); } - using HttpResponseMessage highlightResponse = await highlight_client.SendAsync(highlight_request); + using HttpResponseMessage highlightResponse = await highlightClient.SendAsync(highlightRequest); highlightResponse.EnsureSuccessStatusCode(); string highlightBody = await highlightResponse.Content.ReadAsStringAsync(); HighlightDtos.HighlightMediaDto? highlightMediaDto = @@ -608,9 +608,9 @@ public class ApiService(IAuthService authService, IConfigService configService, continue; } - if (!return_urls.ContainsKey(medium.Id) && !string.IsNullOrEmpty(storyUrl)) + if (!returnUrls.ContainsKey(medium.Id) && !string.IsNullOrEmpty(storyUrl)) { - return_urls.Add(medium.Id, storyUrl); + returnUrls.Add(medium.Id, storyUrl); } } } @@ -618,7 +618,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - return return_urls; + return returnUrls; } catch (Exception ex) { @@ -640,7 +640,7 @@ public class ApiService(IAuthService authService, IConfigService configService, public async Task GetPaidPosts(string endpoint, string folder, string username, - List paid_post_ids, IStatusReporter statusReporter) + List paidPostIds, IStatusReporter statusReporter) { Log.Debug($"Calling GetPaidPosts - {username}"); @@ -726,7 +726,7 @@ public class ApiService(IAuthService authService, IConfigService configService, { if (!previewids.Contains(medium.Id)) { - paid_post_ids.Add(medium.Id); + paidPostIds.Add(medium.Id); } if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) @@ -834,7 +834,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } - public async Task GetPosts(string endpoint, string folder, List paid_post_ids, + public async Task GetPosts(string endpoint, string folder, List paidPostIds, IStatusReporter statusReporter) { Log.Debug($"Calling GetPosts - {endpoint}"); @@ -978,7 +978,7 @@ public class ApiService(IAuthService authService, IConfigService configService, if (medium.CanView && medium.Files?.Drm == null) { - bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); + bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); if (!has && !string.IsNullOrEmpty(fullUrl)) { if (!postCollection.Posts.ContainsKey(medium.Id)) @@ -1004,7 +1004,7 @@ public class ApiService(IAuthService authService, IConfigService configService, TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, out string cloudFrontSignature, out string cloudFrontKeyPairId)) { - bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); + bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); if (!has && !postCollection.Posts.ContainsKey(medium.Id)) { await dbService.AddMedia(folder, medium.Id, post.Id, manifestDash, null, null, null, @@ -1197,7 +1197,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } public async Task GetStreams(string endpoint, string folder, - List paid_post_ids, + List paidPostIds, IStatusReporter statusReporter) { Log.Debug($"Calling GetStreams - {endpoint}"); @@ -1311,7 +1311,7 @@ public class ApiService(IAuthService authService, IConfigService configService, if (medium.CanView && medium.Files?.Drm == null) { - bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); + bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); if (!has && !string.IsNullOrEmpty(fullUrl)) { if (!streamsCollection.Streams.ContainsKey(medium.Id)) @@ -1327,7 +1327,7 @@ public class ApiService(IAuthService authService, IConfigService configService, TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, out string cloudFrontSignature, out string cloudFrontKeyPairId)) { - bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); + bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); if (!has && !streamsCollection.Streams.ContainsKey(medium.Id)) { await dbService.AddMedia(folder, medium.Id, stream.Id, manifestDash, null, null, null, @@ -1451,7 +1451,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - await dbService.AddPost(folder, archive.Id, archive.Text != null ? archive.Text : "", + await dbService.AddPost(folder, archive.Id, archive.Text ?? "", archive.Price ?? "0", archive.Price != null && archive.IsOpened, archive.IsArchived, archive.PostedAt); archivedCollection.ArchivedPostObjects.Add(archive); @@ -1567,7 +1567,7 @@ public class ApiService(IAuthService authService, IConfigService configService, break; } - getParams["id"] = newMessages.List[newMessages.List.Count - 1].Id.ToString(); + getParams["id"] = newMessages.List[^1].Id.ToString(); } } @@ -1601,8 +1601,8 @@ public class ApiService(IAuthService authService, IConfigService configService, { DateTime createdAt = list.CreatedAt ?? DateTime.Now; await dbService.AddMessage(folder, list.Id, list.Text ?? "", list.Price ?? "0", - list.CanPurchaseReason == "opened" ? true : - list.CanPurchaseReason != "opened" ? false : (bool?)null ?? false, false, + list.CanPurchaseReason == "opened" || + (list.CanPurchaseReason == "opened" && ((bool?)null ?? false)), false, createdAt, list.FromUser?.Id ?? int.MinValue); messageCollection.MessageObjects.Add(list); @@ -2907,24 +2907,19 @@ public class ApiService(IAuthService authService, IConfigService configService, throw new Exception("Auth service is missing required fields"); } - string? pssh; - HttpClient client = new(); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); request.Headers.Add("user-agent", currentAuth.UserAgent); request.Headers.Add("Accept", "*/*"); request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {currentAuth.Cookie};"); - using (HttpResponseMessage response = await client.SendAsync(request)) - { - response.EnsureSuccessStatusCode(); - string body = await response.Content.ReadAsStringAsync(); - XNamespace ns = "urn:mpeg:dash:schema:mpd:2011"; - XNamespace cenc = "urn:mpeg:cenc:2013"; - XDocument xmlDoc = XDocument.Parse(body); - IEnumerable psshElements = xmlDoc.Descendants(cenc + "pssh"); - pssh = psshElements.ElementAt(1).Value; - } + using HttpResponseMessage response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + string body = await response.Content.ReadAsStringAsync(); + XNamespace cenc = "urn:mpeg:cenc:2013"; + XDocument xmlDoc = XDocument.Parse(body); + IEnumerable psshElements = xmlDoc.Descendants(cenc + "pssh"); + string pssh = psshElements.ElementAt(1).Value; return pssh; } @@ -2956,8 +2951,6 @@ public class ApiService(IAuthService authService, IConfigService configService, try { - DateTime lastmodified; - Auth? currentAuth = authService.CurrentAuth; if (currentAuth == null || currentAuth.UserAgent == null || currentAuth.Cookie == null) { @@ -2970,14 +2963,12 @@ public class ApiService(IAuthService authService, IConfigService configService, request.Headers.Add("Accept", "*/*"); request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {currentAuth.Cookie};"); - using (HttpResponseMessage response = - await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) - { - response.EnsureSuccessStatusCode(); - lastmodified = response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now; + using HttpResponseMessage response = + await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + DateTime lastmodified = response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now; - Log.Debug($"Last modified: {lastmodified}"); - } + Log.Debug($"Last modified: {lastmodified}"); return lastmodified; } @@ -3061,14 +3052,14 @@ public class ApiService(IAuthService authService, IConfigService configService, return string.Empty; } - public async Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, + public async Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceUrl, string pssh) { Log.Debug("Calling GetDecryptionKeyCDM"); try { - byte[] resp1 = await PostData(licenceURL, drmHeaders, [0x08, 0x04]); + byte[] resp1 = await PostData(licenceUrl, drmHeaders, [0x08, 0x04]); string certDataB64 = Convert.ToBase64String(resp1); CDMApi cdm = new(); byte[]? challenge = cdm.GetChallenge(pssh, certDataB64); @@ -3077,7 +3068,7 @@ public class ApiService(IAuthService authService, IConfigService configService, throw new Exception("Failed to get challenge from CDM"); } - byte[] resp2 = await PostData(licenceURL, drmHeaders, challenge); + byte[] resp2 = await PostData(licenceUrl, drmHeaders, challenge); string licenseB64 = Convert.ToBase64String(resp2); Log.Debug("resp1: {Resp1}", resp1); Log.Debug("certDataB64: {CertDataB64}", certDataB64); @@ -3156,7 +3147,7 @@ public class ApiService(IAuthService authService, IConfigService configService, diff.TotalSeconds; // This gives the number of seconds. If you need milliseconds, use diff.TotalMilliseconds } - public static bool IsStringOnlyDigits(string input) => input.All(char.IsDigit); + private static bool IsStringOnlyDigits(string input) => input.All(char.IsDigit); private HttpClient GetHttpClient() @@ -3277,7 +3268,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } - public async Task?> GetAllSubscriptions(Dictionary getParams, + private async Task?> GetAllSubscriptions(Dictionary getParams, string endpoint, bool includeRestricted) { try @@ -3350,7 +3341,7 @@ public class ApiService(IAuthService authService, IConfigService configService, return null; } - public static string? GetDynamicRules() + private static string? GetDynamicRules() { Log.Debug("Calling GetDynamicRules"); try diff --git a/OF DL.Core/Services/DownloadOrchestrationService.cs b/OF DL.Core/Services/DownloadOrchestrationService.cs index ebf2d08..f71177c 100644 --- a/OF DL.Core/Services/DownloadOrchestrationService.cs +++ b/OF DL.Core/Services/DownloadOrchestrationService.cs @@ -94,7 +94,7 @@ public class DownloadOrchestrationService( { long listId = lists[listName]; List listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; - return allUsers.Where(x => listUsernames.Contains(x.Key)).Distinct() + return allUsers.Where(x => listUsernames.Contains(x.Key)) .ToDictionary(x => x.Key, x => x.Value); } diff --git a/OF DL.Core/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs index 0fb1f80..5eb046c 100644 --- a/OF DL.Core/Services/DownloadService.cs +++ b/OF DL.Core/Services/DownloadService.cs @@ -1,6 +1,5 @@ using System.Security.Cryptography; using System.Text.RegularExpressions; -using System.Xml.Linq; using FFmpeg.NET; using FFmpeg.NET.Events; using OF_DL.Models; @@ -31,7 +30,7 @@ public class DownloadService( { try { - string path = "/Profile"; + const string path = "/Profile"; if (!Directory.Exists(folder + path)) { @@ -61,7 +60,7 @@ public class DownloadService( } } - private async Task DownloadProfileImage(string url, string folder, string subFolder, string username) + private static async Task DownloadProfileImage(string url, string folder, string subFolder, string username) { if (!Directory.Exists(folder + subFolder)) { @@ -85,7 +84,7 @@ public class DownloadService( memoryStream.Seek(0, SeekOrigin.Begin); MD5 md5 = MD5.Create(); - byte[] hash = md5.ComputeHash(memoryStream); + byte[] hash = await md5.ComputeHashAsync(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); if (!md5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant())) { @@ -94,7 +93,7 @@ public class DownloadService( ? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy") : DateTime.Now.ToString("dd-MM-yyyy")); - using (FileStream fileStream = File.Create(destinationPath)) + await using (FileStream fileStream = File.Create(destinationPath)) { await memoryStream.CopyToAsync(fileStream); } @@ -104,9 +103,9 @@ public class DownloadService( } } - private async Task DownloadDrmMedia(string user_agent, string policy, string signature, string kvp, - string sess, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, - string api_type, IProgressReporter progressReporter, string customFileName, string filename, string path) + private async Task DownloadDrmMedia(string userAgent, string policy, string signature, string kvp, + string sess, string url, string decryptionKey, string folder, DateTime lastModified, long mediaId, + string apiType, IProgressReporter progressReporter, string customFileName, string filename, string path) { try { @@ -116,35 +115,12 @@ public class DownloadService( string decKey = ""; if (pos1 >= 0) { - decKey = decryptionKey.Substring(pos1 + 1); + decKey = decryptionKey[(pos1 + 1)..]; } int streamIndex = 0; string tempFilename = $"{folder}{path}/{filename}_source.mp4"; - //int? streamIndex = await GetVideoStreamIndexFromMpd(url, policy, signature, kvp, downloadConfig.DownloadVideoResolution); - - //if (streamIndex == null) - // throw new Exception($"Could not find video stream for resolution {downloadConfig.DownloadVideoResolution}"); - - //string tempFilename; - - //switch (downloadConfig.DownloadVideoResolution) - //{ - // case VideoResolution.source: - // tempFilename = $"{folder}{path}/{filename}_source.mp4"; - // break; - // case VideoResolution._240: - // tempFilename = $"{folder}{path}/{filename}_240.mp4"; - // break; - // case VideoResolution._720: - // tempFilename = $"{folder}{path}/{filename}_720.mp4"; - // break; - // default: - // tempFilename = $"{folder}{path}/{filename}_source.mp4"; - // break; - //} - // Configure ffmpeg log level and optional report file location bool ffmpegDebugLogging = Log.IsEnabled(LogEventLevel.Debug); @@ -187,7 +163,7 @@ public class DownloadService( $"{logLevelArgs} " + $"-cenc_decryption_key {decKey} " + $"-headers \"{cookieHeader}\" " + - $"-user_agent \"{user_agent}\" " + + $"-user_agent \"{userAgent}\" " + "-referer \"https://onlyfans.com\" " + "-rw_timeout 20000000 " + "-reconnect 1 -reconnect_streamed 1 -reconnect_on_network_error 1 -reconnect_delay_max 10 " + @@ -205,7 +181,7 @@ public class DownloadService( { _completionSource.TrySetResult(true); await OnFFMPEGDownloadComplete(tempFilename, lastModified, folder, path, customFileName, filename, - media_id, api_type, progressReporter); + mediaId, apiType, progressReporter); }; await ffmpeg.ExecuteAsync(parameters, CancellationToken.None); @@ -287,59 +263,7 @@ public class DownloadService( Log.Error("FFmpeg failed. Input={Input} Output={Output} ExitCode={ExitCode} Message={Message} Inner={Inner}", input, output, exitCode, message, inner); - _completionSource?.TrySetResult(false); - } - - private async Task GetVideoStreamIndexFromMpd(string mpdUrl, string policy, string signature, string kvp, - VideoResolution resolution) - { - if (authService.CurrentAuth == null) - { - return null; - } - - HttpClient client = new(); - HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); - request.Headers.Add("user-agent", authService.CurrentAuth.UserAgent); - request.Headers.Add("Accept", "*/*"); - request.Headers.Add("Cookie", - $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.Cookie};"); - using (HttpResponseMessage response = await client.SendAsync(request)) - { - response.EnsureSuccessStatusCode(); - string body = await response.Content.ReadAsStringAsync(); - XDocument doc = XDocument.Parse(body); - XNamespace ns = "urn:mpeg:dash:schema:mpd:2011"; - // XNamespace cenc = "urn:mpeg:cenc:2013"; - XElement? videoAdaptationSet = doc - .Descendants(ns + "AdaptationSet") - .FirstOrDefault(e => (string?)e.Attribute("mimeType") == "video/mp4"); - - if (videoAdaptationSet == null) - { - return null; - } - - string targetHeight = resolution switch - { - VideoResolution._240 => "240", - VideoResolution._720 => "720", - VideoResolution.source => "1280", - _ => throw new ArgumentOutOfRangeException(nameof(resolution)) - }; - - List representations = videoAdaptationSet.Elements(ns + "Representation").ToList(); - - for (int i = 0; i < representations.Count; i++) - { - if ((string?)representations[i].Attribute("height") == targetHeight) - { - return i; // this is the index FFmpeg will use for `-map 0:v:{i}` - } - } - } - - return null; + _completionSource.TrySetResult(false); } private static List CalculateFolderMD5(string folder) @@ -900,42 +824,31 @@ public class DownloadService( long totalFileSize = 0; if (urls.Count > 250) { - int batchSize = 250; + const int batchSize = 250; - List> tasks = new(); + List> tasks = []; for (int i = 0; i < urls.Count; i += batchSize) { List batchUrls = urls.Skip(i).Take(batchSize).ToList(); - IEnumerable> batchTasks = batchUrls.Select(GetFileSizeAsync); + Task[] batchTasks = batchUrls.Select(GetFileSizeAsync).ToArray(); tasks.AddRange(batchTasks); - await Task.WhenAll(batchTasks); await Task.Delay(5000); } long[] fileSizes = await Task.WhenAll(tasks); - foreach (long fileSize in fileSizes) - { - totalFileSize += fileSize; - } + totalFileSize += fileSizes.Sum(); } else { - List> tasks = new(); - - foreach (string url in urls) - { - tasks.Add(GetFileSizeAsync(url)); - } + List> tasks = []; + tasks.AddRange(urls.Select(GetFileSizeAsync)); long[] fileSizes = await Task.WhenAll(tasks); - foreach (long fileSize in fileSizes) - { - totalFileSize += fileSize; - } + totalFileSize += fileSizes.Sum(); } return totalFileSize; diff --git a/OF DL.Core/Services/StartupService.cs b/OF DL.Core/Services/StartupService.cs index b33747d..08d80d2 100644 --- a/OF DL.Core/Services/StartupService.cs +++ b/OF DL.Core/Services/StartupService.cs @@ -6,6 +6,7 @@ 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; @@ -30,11 +31,11 @@ public class StartupService(IConfigService configService, IAuthService authServi // FFmpeg detection DetectFfmpeg(result); - if (result.FfmpegFound) + if (result.FfmpegFound && result.FfmpegPath != null) { // Escape backslashes for Windows if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && - result.FfmpegPath!.Contains(@":\") && + result.FfmpegPath.Contains(@":\") && !result.FfmpegPath.Contains(@":\\")) { result.FfmpegPath = result.FfmpegPath.Replace(@"\", @"\\"); @@ -42,7 +43,7 @@ public class StartupService(IConfigService configService, IAuthService authServi } // Get FFmpeg version - result.FfmpegVersion = await GetFfmpegVersionAsync(result.FfmpegPath!); + result.FfmpegVersion = await GetFfmpegVersionAsync(result.FfmpegPath); } // Widevine device checks @@ -51,40 +52,27 @@ public class StartupService(IConfigService configService, IAuthService authServi result.DevicePrivateKeyMissing = !File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, WidevineConstants.DEVICE_NAME, "device_private_key")); - if (!result.ClientIdBlobMissing) - { - Log.Debug("device_client_id_blob found"); - } - else - { - Log.Debug("device_client_id_blob missing"); - } - - if (!result.DevicePrivateKeyMissing) - { - Log.Debug("device_private_key found"); - } - else - { - Log.Debug("device_private_key missing"); - } + 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")) + if (!File.Exists("rules.json")) { - result.RulesJsonExists = true; - try - { - JsonConvert.DeserializeObject(File.ReadAllText("rules.json")); - Log.Debug("Rules.json: "); - Log.Debug(JsonConvert.SerializeObject(File.ReadAllText("rules.json"), Formatting.Indented)); - result.RulesJsonValid = true; - } - catch (Exception e) - { - result.RulesJsonError = e.Message; - Log.Error("rules.json processing failed.", e.Message); - } + 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; @@ -100,7 +88,7 @@ public class StartupService(IConfigService configService, IAuthService authServi result.LocalVersion = Assembly.GetEntryAssembly()?.GetName().Version; using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); - string? latestReleaseTag = null; + string? latestReleaseTag; try { @@ -121,18 +109,18 @@ public class StartupService(IConfigService configService, IAuthService authServi } result.LatestVersion = new Version(latestReleaseTag.Replace("OFDLV", "")); - int versionComparison = result.LocalVersion!.CompareTo(result.LatestVersion); + 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}"); + $"{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.", e.Message); + Log.Error("Error checking latest release on GitHub. {Message}", e.Message); } #else Log.Debug("Running in Debug/Local mode. Version check skipped."); @@ -207,7 +195,7 @@ public class StartupService(IConfigService configService, IAuthService authServi if (firstLine.StartsWith("ffmpeg version")) { int versionStart = "ffmpeg version ".Length; - int copyrightIndex = firstLine.IndexOf(" Copyright"); + int copyrightIndex = firstLine.IndexOf(" Copyright", StringComparison.Ordinal); return copyrightIndex > versionStart ? firstLine.Substring(versionStart, copyrightIndex - versionStart) : firstLine.Substring(versionStart); diff --git a/OF DL.Core/Utils/ThrottledStream.cs b/OF DL.Core/Utils/ThrottledStream.cs index 0e1b128..6e0ef5e 100644 --- a/OF DL.Core/Utils/ThrottledStream.cs +++ b/OF DL.Core/Utils/ThrottledStream.cs @@ -61,9 +61,7 @@ public class ThrottledStream : Stream TimeSpan sleep = targetTime - actualTime; if (sleep > TimeSpan.Zero) { - using AutoResetEvent waitHandle = new(false); - scheduler.Sleep(sleep).GetAwaiter().OnCompleted(() => waitHandle.Set()); - waitHandle.WaitOne(); + scheduler.Sleep(sleep).GetAwaiter().GetResult(); } } diff --git a/OF DL.Core/Widevine/CDM.cs b/OF DL.Core/Widevine/CDM.cs index 4dc3cb0..9328847 100644 --- a/OF DL.Core/Widevine/CDM.cs +++ b/OF DL.Core/Widevine/CDM.cs @@ -264,23 +264,24 @@ public class CDM Serializer.Serialize(memoryStream, session.Device.ClientID); byte[] data = Padding.AddPKCS7Padding(memoryStream.ToArray(), 16); - using AesCryptoServiceProvider aesProvider = new() - { - BlockSize = 128, Padding = PaddingMode.PKCS7, Mode = CipherMode.CBC - }; - aesProvider.GenerateKey(); - aesProvider.GenerateIV(); + using Aes aes = Aes.Create(); + aes.BlockSize = 128; + aes.Padding = PaddingMode.PKCS7; + aes.Mode = CipherMode.CBC; + + aes.GenerateKey(); + aes.GenerateIV(); using MemoryStream mstream = new(); - using CryptoStream cryptoStream = new(mstream, aesProvider.CreateEncryptor(aesProvider.Key, aesProvider.IV), + using CryptoStream cryptoStream = new(mstream, aes.CreateEncryptor(aes.Key, aes.IV), CryptoStreamMode.Write); cryptoStream.Write(data, 0, data.Length); encryptedClientIdProto.EncryptedClientId = mstream.ToArray(); using RSACryptoServiceProvider RSA = new(); RSA.ImportRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey, out int _); - encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aesProvider.Key, RSAEncryptionPadding.OaepSHA1); - encryptedClientIdProto.EncryptedClientIdIv = aesProvider.IV; + encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aes.Key, RSAEncryptionPadding.OaepSHA1); + encryptedClientIdProto.EncryptedClientIdIv = aes.IV; encryptedClientIdProto.ServiceId = Encoding.UTF8.GetString(session.ServiceCertificate.DeviceCertificate.ServiceId); encryptedClientIdProto.ServiceCertificateSerialNumber = @@ -397,27 +398,24 @@ public class CDM continue; } - byte[] keyId; byte[] encryptedKey = key.Key; byte[] iv = key.Iv; - keyId = key.Id; + byte[] keyId = key.Id; if (keyId == null) { keyId = Encoding.ASCII.GetBytes(key.Type.ToString()); } - byte[] decryptedKey; - using MemoryStream mstream = new(); - using AesCryptoServiceProvider aesProvider = new(); - aesProvider.Mode = CipherMode.CBC; - aesProvider.Padding = PaddingMode.PKCS7; + using Aes aes = Aes.Create(); + aes.Padding = PaddingMode.PKCS7; + aes.Mode = CipherMode.CBC; - using CryptoStream cryptoStream = new(mstream, aesProvider.CreateDecryptor(session.DerivedKeys.Enc, iv), + using CryptoStream cryptoStream = new(mstream, aes.CreateDecryptor(session.DerivedKeys.Enc, iv), CryptoStreamMode.Write); cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); - decryptedKey = mstream.ToArray(); + byte[] decryptedKey = mstream.ToArray(); List permissions = []; if (type == "OperatorSession") diff --git a/OF DL/CLI/SpectreDownloadEventHandler.cs b/OF DL/CLI/SpectreDownloadEventHandler.cs index 8758324..245bf0a 100644 --- a/OF DL/CLI/SpectreDownloadEventHandler.cs +++ b/OF DL/CLI/SpectreDownloadEventHandler.cs @@ -12,35 +12,55 @@ public class SpectreDownloadEventHandler : IDownloadEventHandler { public async Task WithStatusAsync(string statusMessage, Func> work) { - T result = default!; + TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + await AnsiConsole.Status() .StartAsync($"[red]{Markup.Escape(statusMessage)}[/]", async ctx => { - SpectreStatusReporter reporter = new(ctx); - result = await work(reporter); + try + { + SpectreStatusReporter reporter = new(ctx); + T result = await work(reporter); + tcs.TrySetResult(result); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } }); - return result; + + return await tcs.Task; } public async Task WithProgressAsync(string description, long maxValue, bool showSize, Func> work) { - T result = default!; + TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + await AnsiConsole.Progress() .Columns(GetProgressColumns(showSize)) .StartAsync(async ctx => { - ProgressTask task = ctx.AddTask($"[red]{Markup.Escape(description)}[/]", false); - task.MaxValue = maxValue; - task.StartTask(); + try + { + ProgressTask task = ctx.AddTask($"[red]{Markup.Escape(description)}[/]", false); + task.MaxValue = maxValue; + task.StartTask(); - SpectreProgressReporter progressReporter = new(task); - result = await work(progressReporter); + SpectreProgressReporter progressReporter = new(task); + T result = await work(progressReporter); + tcs.TrySetResult(result); - task.StopTask(); + task.StopTask(); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } }); - return result; + + return await tcs.Task; } public void OnContentFound(string contentType, int mediaCount, int objectCount) => diff --git a/OF DL/Program.cs b/OF DL/Program.cs index fd3150c..23647e2 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -219,6 +219,7 @@ public class Program(IServiceProvider serviceProvider) Console.WriteLine("\nPress any key to exit."); Console.ReadKey(); } + Environment.Exit(2); } } @@ -542,7 +543,7 @@ public class Program(IServiceProvider serviceProvider) } } - selectedUsers = users.Where(x => listUsernames.Contains($"{x.Key}")).Distinct() + 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))); @@ -787,6 +788,7 @@ public class Program(IServiceProvider serviceProvider) AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); Console.ReadKey(); } + Environment.Exit(2); } From 4889be18900696446f510f99ac80b09b2b330055 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 09:52:59 -0600 Subject: [PATCH 39/55] Update services to use coding and naming standards --- OF DL.Core/Services/ApiService.cs | 22 +- OF DL.Core/Services/AuthService.cs | 4 +- .../Services/{DBService.cs => DbService.cs} | 269 +++++++------- .../Services/DownloadOrchestrationService.cs | 10 +- OF DL.Core/Services/DownloadService.cs | 329 ++++++++---------- OF DL.Core/Services/FileNameService.cs | 6 +- .../{IAPIService.cs => IApiService.cs} | 30 +- OF DL.Core/Services/IDBService.cs | 27 -- OF DL.Core/Services/IDbService.cs | 31 ++ OF DL.Core/Services/IDownloadService.cs | 10 +- OF DL.Core/Services/ILoggingService.cs | 2 + OF DL.Core/Services/StartupService.cs | 14 +- OF DL/Program.cs | 14 +- 13 files changed, 363 insertions(+), 405 deletions(-) rename OF DL.Core/Services/{DBService.cs => DbService.cs} (65%) rename OF DL.Core/Services/{IAPIService.cs => IApiService.cs} (79%) delete mode 100644 OF DL.Core/Services/IDBService.cs create mode 100644 OF DL.Core/Services/IDbService.cs diff --git a/OF DL.Core/Services/ApiService.cs b/OF DL.Core/Services/ApiService.cs index 9e1eb22..ee08a24 100644 --- a/OF DL.Core/Services/ApiService.cs +++ b/OF DL.Core/Services/ApiService.cs @@ -37,8 +37,8 @@ using SinglePostCollection = OF_DL.Models.Entities.Posts.SinglePostCollection; namespace OF_DL.Services; -public class ApiService(IAuthService authService, IConfigService configService, IDBService dbService) - : IAPIService +public class ApiService(IAuthService authService, IConfigService configService, IDbService dbService) + : IApiService { private const int MaxAttempts = 30; private const int DelayBetweenAttempts = 3000; @@ -200,9 +200,9 @@ public class ApiService(IAuthService authService, IConfigService configService, response.EnsureSuccessStatusCode(); string body = await response.Content.ReadAsStringAsync(); - //if the content creator doesnt exist, we get a 200 response, but the content isnt usable - //so let's not throw an exception, since "content creator no longer exists" is handled elsewhere - //which means we wont get loads of exceptions + // if the content creator doesn't exist, we get a 200 response, but the content isn't usable + // so let's not throw an exception, since "content creator no longer exists" is handled elsewhere + // which means we won't get loads of exceptions if (body.Equals("[]")) { return null; @@ -2897,7 +2897,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } - public async Task GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp) + public async Task GetDrmMpdPssh(string mpdUrl, string policy, string signature, string kvp) { try { @@ -2941,9 +2941,9 @@ public class ApiService(IAuthService authService, IConfigService configService, } - public async Task GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp) + public async Task GetDrmMpdLastModified(string mpdUrl, string policy, string signature, string kvp) { - Log.Debug("Calling GetDRMMPDLastModified"); + Log.Debug("Calling GetDrmMpdLastModified"); Log.Debug($"mpdUrl: {mpdUrl}"); Log.Debug($"policy: {policy}"); Log.Debug($"signature: {signature}"); @@ -2989,10 +2989,10 @@ public class ApiService(IAuthService authService, IConfigService configService, return DateTime.Now; } - public async Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceUrl, + public async Task GetDecryptionKeyOfdl(Dictionary drmHeaders, string licenceUrl, string pssh) { - Log.Debug("Calling GetDecryptionOFDL"); + Log.Debug("Calling GetDecryptionKeyOfdl"); try { @@ -3052,7 +3052,7 @@ public class ApiService(IAuthService authService, IConfigService configService, return string.Empty; } - public async Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceUrl, + public async Task GetDecryptionKeyCdm(Dictionary drmHeaders, string licenceUrl, string pssh) { Log.Debug("Calling GetDecryptionKeyCDM"); diff --git a/OF DL.Core/Services/AuthService.cs b/OF DL.Core/Services/AuthService.cs index 45c7afc..bf20e50 100644 --- a/OF DL.Core/Services/AuthService.cs +++ b/OF DL.Core/Services/AuthService.cs @@ -161,8 +161,8 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService public async Task ValidateAuthAsync() { - // Resolve IAPIService lazily to avoid circular dependency - IAPIService apiService = serviceProvider.GetRequiredService(); + // Resolve IApiService lazily to avoid circular dependency + IApiService apiService = serviceProvider.GetRequiredService(); return await apiService.GetUserInfo("/users/me"); } diff --git a/OF DL.Core/Services/DBService.cs b/OF DL.Core/Services/DbService.cs similarity index 65% rename from OF DL.Core/Services/DBService.cs rename to OF DL.Core/Services/DbService.cs index c9b6df0..eb3543e 100644 --- a/OF DL.Core/Services/DBService.cs +++ b/OF DL.Core/Services/DbService.cs @@ -4,9 +4,9 @@ using Serilog; namespace OF_DL.Services; -public class DBService(IConfigService configService) : IDBService +public class DbService(IConfigService configService) : IDbService { - public async Task CreateDB(string folder) + public async Task CreateDb(string folder) { try { @@ -18,15 +18,15 @@ public class DBService(IConfigService configService) : IDBService string dbFilePath = $"{folder}/Metadata/user_data.db"; // connect to the new database file - using SqliteConnection connection = new($"Data Source={dbFilePath}"); + await using SqliteConnection connection = new($"Data Source={dbFilePath}"); // open the connection connection.Open(); // create the 'medias' table - using (SqliteCommand cmd = - new( - "CREATE TABLE IF NOT EXISTS medias (id INTEGER NOT NULL, media_id INTEGER, post_id INTEGER NOT NULL, link VARCHAR, directory VARCHAR, filename VARCHAR, size INTEGER, api_type VARCHAR, media_type VARCHAR, preview INTEGER, linked VARCHAR, downloaded INTEGER, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(media_id));", - connection)) + await using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS medias (id INTEGER NOT NULL, media_id INTEGER, post_id INTEGER NOT NULL, link VARCHAR, directory VARCHAR, filename VARCHAR, size INTEGER, api_type VARCHAR, media_type VARCHAR, preview INTEGER, linked VARCHAR, downloaded INTEGER, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(media_id));", + connection)) { await cmd.ExecuteNonQueryAsync(); } @@ -36,7 +36,7 @@ public class DBService(IConfigService configService) : IDBService // // Alter existing databases to create unique constraint on `medias` // - using (SqliteCommand cmd = new(@" + await using (SqliteCommand cmd = new(@" PRAGMA foreign_keys=off; BEGIN TRANSACTION; @@ -74,55 +74,55 @@ public class DBService(IConfigService configService) : IDBService } // create the 'messages' table - using (SqliteCommand cmd = - new( - "CREATE TABLE IF NOT EXISTS messages (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, user_id INTEGER, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", - connection)) + await using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS messages (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, user_id INTEGER, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) { await cmd.ExecuteNonQueryAsync(); } // create the 'posts' table - using (SqliteCommand cmd = - new( - "CREATE TABLE IF NOT EXISTS posts (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", - connection)) + await using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS posts (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) { await cmd.ExecuteNonQueryAsync(); } // create the 'stories' table - using (SqliteCommand cmd = - new( - "CREATE TABLE IF NOT EXISTS stories (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", - connection)) + await using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS stories (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) { await cmd.ExecuteNonQueryAsync(); } // create the 'others' table - using (SqliteCommand cmd = - new( - "CREATE TABLE IF NOT EXISTS others (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", - connection)) + await using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS others (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) { await cmd.ExecuteNonQueryAsync(); } // create the 'products' table - using (SqliteCommand cmd = - new( - "CREATE TABLE IF NOT EXISTS products (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, title VARCHAR, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", - connection)) + await using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS products (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, title VARCHAR, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) { await cmd.ExecuteNonQueryAsync(); } // create the 'profiles' table - using (SqliteCommand cmd = - new( - "CREATE TABLE IF NOT EXISTS profiles (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(username));", - connection)) + await using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS profiles (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(username));", + connection)) { await cmd.ExecuteNonQueryAsync(); } @@ -144,19 +144,19 @@ public class DBService(IConfigService configService) : IDBService } } - public async Task CreateUsersDB(Dictionary users) + public async Task CreateUsersDb(Dictionary users) { try { - using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); + await using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); Log.Debug("Database data source: " + connection.DataSource); connection.Open(); - using (SqliteCommand cmd = - new( - "CREATE TABLE IF NOT EXISTS users (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, PRIMARY KEY(id), UNIQUE(username));", - connection)) + await using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS users (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, PRIMARY KEY(id), UNIQUE(username));", + connection)) { await cmd.ExecuteNonQueryAsync(); } @@ -164,29 +164,23 @@ public class DBService(IConfigService configService) : IDBService Log.Debug("Adding missing creators"); foreach (KeyValuePair user in users) { - using (SqliteCommand checkCmd = new("SELECT user_id, username FROM users WHERE user_id = @userId;", - connection)) + await using SqliteCommand checkCmd = new("SELECT user_id, username FROM users WHERE user_id = @userId;", + connection); + checkCmd.Parameters.AddWithValue("@userId", user.Value); + await using SqliteDataReader reader = await checkCmd.ExecuteReaderAsync(); + if (!reader.Read()) { - checkCmd.Parameters.AddWithValue("@userId", user.Value); - using (SqliteDataReader reader = await checkCmd.ExecuteReaderAsync()) - { - if (!reader.Read()) - { - using (SqliteCommand insertCmd = - new("INSERT INTO users (user_id, username) VALUES (@userId, @username);", - connection)) - { - insertCmd.Parameters.AddWithValue("@userId", user.Value); - insertCmd.Parameters.AddWithValue("@username", user.Key); - await insertCmd.ExecuteNonQueryAsync(); - Log.Debug("Inserted new creator: " + user.Key); - } - } - else - { - Log.Debug("Creator " + user.Key + " already exists"); - } - } + await using SqliteCommand insertCmd = + new("INSERT INTO users (user_id, username) VALUES (@userId, @username);", + connection); + insertCmd.Parameters.AddWithValue("@userId", user.Value); + insertCmd.Parameters.AddWithValue("@username", user.Key); + await insertCmd.ExecuteNonQueryAsync(); + Log.Debug("Inserted new creator: " + user.Key); + } + else + { + Log.Debug("Creator " + user.Key + " already exists"); } } @@ -211,12 +205,12 @@ public class DBService(IConfigService configService) : IDBService { try { - using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); + await using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); connection.Open(); - using (SqliteCommand checkCmd = new("SELECT user_id, username FROM users WHERE user_id = @userId;", - connection)) + await using (SqliteCommand checkCmd = new("SELECT user_id, username FROM users WHERE user_id = @userId;", + connection)) { checkCmd.Parameters.AddWithValue("@userId", user.Value); await using (SqliteDataReader reader = await checkCmd.ExecuteReaderAsync()) @@ -264,16 +258,16 @@ public class DBService(IConfigService configService) : IDBService } } - public async Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, - bool is_archived, DateTime created_at, long user_id) + public async Task AddMessage(string folder, long postId, string messageText, string price, bool isPaid, + bool isArchived, DateTime createdAt, long userId) { try { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + await using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); connection.Open(); await EnsureCreatedAtColumnExists(connection, "messages"); - using SqliteCommand cmd = new("SELECT COUNT(*) FROM messages WHERE post_id=@post_id", connection); - cmd.Parameters.AddWithValue("@post_id", post_id); + await using SqliteCommand cmd = new("SELECT COUNT(*) FROM messages WHERE post_id=@post_id", connection); + cmd.Parameters.AddWithValue("@post_id", postId); int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); if (count == 0) { @@ -282,13 +276,13 @@ public class DBService(IConfigService configService) : IDBService new( "INSERT INTO messages(post_id, text, price, paid, archived, created_at, user_id, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @user_id, @record_created_at)", connection); - insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text); + insertCmd.Parameters.AddWithValue("@post_id", postId); + insertCmd.Parameters.AddWithValue("@message_text", messageText); insertCmd.Parameters.AddWithValue("@price", price); - insertCmd.Parameters.AddWithValue("@is_paid", is_paid); - insertCmd.Parameters.AddWithValue("@is_archived", is_archived); - insertCmd.Parameters.AddWithValue("@created_at", created_at); - insertCmd.Parameters.AddWithValue("@user_id", user_id); + insertCmd.Parameters.AddWithValue("@is_paid", isPaid); + insertCmd.Parameters.AddWithValue("@is_archived", isArchived); + insertCmd.Parameters.AddWithValue("@created_at", createdAt); + insertCmd.Parameters.AddWithValue("@user_id", userId); insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); await insertCmd.ExecuteNonQueryAsync(); } @@ -309,30 +303,30 @@ public class DBService(IConfigService configService) : IDBService } - public async Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, - bool is_archived, DateTime created_at) + public async Task AddPost(string folder, long postId, string messageText, string price, bool isPaid, + bool isArchived, DateTime createdAt) { try { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + await using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); connection.Open(); await EnsureCreatedAtColumnExists(connection, "posts"); - using SqliteCommand cmd = new("SELECT COUNT(*) FROM posts WHERE post_id=@post_id", connection); - cmd.Parameters.AddWithValue("@post_id", post_id); + await using SqliteCommand cmd = new("SELECT COUNT(*) FROM posts WHERE post_id=@post_id", connection); + cmd.Parameters.AddWithValue("@post_id", postId); int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); if (count == 0) { // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = + await using SqliteCommand insertCmd = new( "INSERT INTO posts(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", connection); - insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text); + insertCmd.Parameters.AddWithValue("@post_id", postId); + insertCmd.Parameters.AddWithValue("@message_text", messageText); insertCmd.Parameters.AddWithValue("@price", price); - insertCmd.Parameters.AddWithValue("@is_paid", is_paid); - insertCmd.Parameters.AddWithValue("@is_archived", is_archived); - insertCmd.Parameters.AddWithValue("@created_at", created_at); + insertCmd.Parameters.AddWithValue("@is_paid", isPaid); + insertCmd.Parameters.AddWithValue("@is_archived", isArchived); + insertCmd.Parameters.AddWithValue("@created_at", createdAt); insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); await insertCmd.ExecuteNonQueryAsync(); } @@ -353,30 +347,30 @@ public class DBService(IConfigService configService) : IDBService } - public async Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, - bool is_archived, DateTime created_at) + public async Task AddStory(string folder, long postId, string messageText, string price, bool isPaid, + bool isArchived, DateTime createdAt) { try { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + await using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); connection.Open(); await EnsureCreatedAtColumnExists(connection, "stories"); - using SqliteCommand cmd = new("SELECT COUNT(*) FROM stories WHERE post_id=@post_id", connection); - cmd.Parameters.AddWithValue("@post_id", post_id); + await using SqliteCommand cmd = new("SELECT COUNT(*) FROM stories WHERE post_id=@post_id", connection); + cmd.Parameters.AddWithValue("@post_id", postId); int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); if (count == 0) { // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = + await using SqliteCommand insertCmd = new( "INSERT INTO stories(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", connection); - insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text); + insertCmd.Parameters.AddWithValue("@post_id", postId); + insertCmd.Parameters.AddWithValue("@message_text", messageText); insertCmd.Parameters.AddWithValue("@price", price); - insertCmd.Parameters.AddWithValue("@is_paid", is_paid); - insertCmd.Parameters.AddWithValue("@is_archived", is_archived); - insertCmd.Parameters.AddWithValue("@created_at", created_at); + insertCmd.Parameters.AddWithValue("@is_paid", isPaid); + insertCmd.Parameters.AddWithValue("@is_archived", isArchived); + insertCmd.Parameters.AddWithValue("@created_at", createdAt); insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); await insertCmd.ExecuteNonQueryAsync(); } @@ -397,13 +391,13 @@ public class DBService(IConfigService configService) : IDBService } - public async Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, - string? filename, long? size, string api_type, string media_type, bool preview, bool downloaded, - DateTime? created_at) + public async Task AddMedia(string folder, long mediaId, long postId, string link, string? directory, + string? filename, long? size, string apiType, string mediaType, bool preview, bool downloaded, + DateTime? createdAt) { try { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + await using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); connection.Open(); await EnsureCreatedAtColumnExists(connection, "medias"); StringBuilder sql = new("SELECT COUNT(*) FROM medias WHERE media_id=@media_id"); @@ -412,15 +406,15 @@ public class DBService(IConfigService configService) : IDBService sql.Append(" and api_type=@api_type"); } - using SqliteCommand cmd = new(sql.ToString(), connection); - cmd.Parameters.AddWithValue("@media_id", media_id); - cmd.Parameters.AddWithValue("@api_type", api_type); + await using SqliteCommand cmd = new(sql.ToString(), connection); + cmd.Parameters.AddWithValue("@media_id", mediaId); + cmd.Parameters.AddWithValue("@api_type", apiType); int count = Convert.ToInt32(cmd.ExecuteScalar()); if (count == 0) { // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = new( - $"INSERT INTO medias(media_id, post_id, link, directory, filename, size, api_type, media_type, preview, downloaded, created_at, record_created_at) VALUES({media_id}, {post_id}, '{link}', '{directory ?? "NULL"}', '{filename ?? "NULL"}', {size?.ToString() ?? "NULL"}, '{api_type}', '{media_type}', {Convert.ToInt32(preview)}, {Convert.ToInt32(downloaded)}, '{created_at?.ToString("yyyy-MM-dd HH:mm:ss")}', '{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}')", + await using SqliteCommand insertCmd = new( + $"INSERT INTO medias(media_id, post_id, link, directory, filename, size, api_type, media_type, preview, downloaded, created_at, record_created_at) VALUES({mediaId}, {postId}, '{link}', '{directory ?? "NULL"}', '{filename ?? "NULL"}', {size?.ToString() ?? "NULL"}, '{apiType}', '{mediaType}', {Convert.ToInt32(preview)}, {Convert.ToInt32(downloaded)}, '{createdAt?.ToString("yyyy-MM-dd HH:mm:ss")}', '{DateTime.Now:yyyy-MM-dd HH:mm:ss}')", connection); await insertCmd.ExecuteNonQueryAsync(); } @@ -441,12 +435,10 @@ public class DBService(IConfigService configService) : IDBService } - public async Task CheckDownloaded(string folder, long media_id, string api_type) + public async Task CheckDownloaded(string folder, long mediaId, string apiType) { try { - bool downloaded; - await using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); StringBuilder sql = new("SELECT downloaded FROM medias WHERE media_id=@media_id"); if (configService.CurrentConfig.DownloadDuplicatedMedia) @@ -456,9 +448,9 @@ public class DBService(IConfigService configService) : IDBService connection.Open(); await using SqliteCommand cmd = new(sql.ToString(), connection); - cmd.Parameters.AddWithValue("@media_id", media_id); - cmd.Parameters.AddWithValue("@api_type", api_type); - downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync()); + cmd.Parameters.AddWithValue("@media_id", mediaId); + cmd.Parameters.AddWithValue("@api_type", apiType); + bool downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync()); return downloaded; } @@ -480,10 +472,10 @@ public class DBService(IConfigService configService) : IDBService } - public async Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, - long size, bool downloaded, DateTime created_at) + public async Task UpdateMedia(string folder, long mediaId, string apiType, string directory, string filename, + long size, bool downloaded, DateTime createdAt) { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + await using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); connection.Open(); // Construct the update command @@ -496,33 +488,30 @@ public class DBService(IConfigService configService) : IDBService } // Create a new command object - using SqliteCommand command = new(sql.ToString(), connection); + await using SqliteCommand command = new(sql.ToString(), connection); // Add parameters to the command object command.Parameters.AddWithValue("@directory", directory); command.Parameters.AddWithValue("@filename", filename); command.Parameters.AddWithValue("@size", size); command.Parameters.AddWithValue("@downloaded", downloaded ? 1 : 0); - command.Parameters.AddWithValue("@created_at", created_at); - command.Parameters.AddWithValue("@media_id", media_id); - command.Parameters.AddWithValue("@api_type", api_type); + command.Parameters.AddWithValue("@created_at", createdAt); + command.Parameters.AddWithValue("@media_id", mediaId); + command.Parameters.AddWithValue("@api_type", apiType); // Execute the command await command.ExecuteNonQueryAsync(); } - public async Task GetStoredFileSize(string folder, long media_id, string api_type) + public async Task GetStoredFileSize(string folder, long mediaId, string apiType) { - long size; - using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) - { - connection.Open(); - using SqliteCommand cmd = new("SELECT size FROM medias WHERE media_id=@media_id and api_type=@api_type", - connection); - cmd.Parameters.AddWithValue("@media_id", media_id); - cmd.Parameters.AddWithValue("@api_type", api_type); - size = Convert.ToInt64(await cmd.ExecuteScalarAsync()); - } + await using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + connection.Open(); + await using SqliteCommand cmd = new("SELECT size FROM medias WHERE media_id=@media_id and api_type=@api_type", + connection); + cmd.Parameters.AddWithValue("@media_id", mediaId); + cmd.Parameters.AddWithValue("@api_type", apiType); + long size = Convert.ToInt64(await cmd.ExecuteScalarAsync()); return size; } @@ -530,10 +519,9 @@ public class DBService(IConfigService configService) : IDBService public async Task GetMostRecentPostDate(string folder) { DateTime? mostRecentDate = null; - using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) - { - connection.Open(); - using SqliteCommand cmd = new(@" + await using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + connection.Open(); + await using SqliteCommand cmd = new(@" SELECT MIN(created_at) AS created_at FROM ( @@ -549,11 +537,10 @@ public class DBService(IConfigService configService) : IDBService ON P.post_id = m.post_id WHERE m.downloaded = 0 )", connection); - object? scalarValue = await cmd.ExecuteScalarAsync(); - if (scalarValue != null && scalarValue != DBNull.Value) - { - mostRecentDate = Convert.ToDateTime(scalarValue); - } + object? scalarValue = await cmd.ExecuteScalarAsync(); + if (scalarValue != null && scalarValue != DBNull.Value) + { + mostRecentDate = Convert.ToDateTime(scalarValue); } return mostRecentDate; @@ -561,8 +548,8 @@ public class DBService(IConfigService configService) : IDBService private async Task EnsureCreatedAtColumnExists(SqliteConnection connection, string tableName) { - using SqliteCommand cmd = new($"PRAGMA table_info({tableName});", connection); - using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); + await using SqliteCommand cmd = new($"PRAGMA table_info({tableName});", connection); + await using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); bool columnExists = false; while (await reader.ReadAsync()) @@ -576,7 +563,7 @@ public class DBService(IConfigService configService) : IDBService if (!columnExists) { - using SqliteCommand alterCmd = new($"ALTER TABLE {tableName} ADD COLUMN record_created_at TIMESTAMP;", + await using SqliteCommand alterCmd = new($"ALTER TABLE {tableName} ADD COLUMN record_created_at TIMESTAMP;", connection); await alterCmd.ExecuteNonQueryAsync(); } diff --git a/OF DL.Core/Services/DownloadOrchestrationService.cs b/OF DL.Core/Services/DownloadOrchestrationService.cs index f71177c..f48fa3a 100644 --- a/OF DL.Core/Services/DownloadOrchestrationService.cs +++ b/OF DL.Core/Services/DownloadOrchestrationService.cs @@ -10,10 +10,10 @@ using UserEntities = OF_DL.Models.Entities.Users; namespace OF_DL.Services; public class DownloadOrchestrationService( - IAPIService apiService, + IApiService apiService, IConfigService configService, IDownloadService downloadService, - IDBService dbService) : IDownloadOrchestrationService + IDbService dbService) : IDownloadOrchestrationService { public List PaidPostIds { get; } = new(); @@ -85,7 +85,7 @@ public class DownloadOrchestrationService( } } - await dbService.CreateUsersDB(result.Users); + await dbService.CreateUsersDb(result.Users); return result; } @@ -113,7 +113,7 @@ public class DownloadOrchestrationService( Log.Debug($"Created folder for {username}"); } - await dbService.CreateDB(path); + await dbService.CreateDb(path); } public async Task DownloadCreatorContentAsync( @@ -346,7 +346,7 @@ public class DownloadOrchestrationService( } await apiService.GetUserInfo($"/users/{user.Key}"); - await dbService.CreateDB(path); + await dbService.CreateDb(path); } string basePath = !string.IsNullOrEmpty(config.DownloadPath) diff --git a/OF DL.Core/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs index 5eb046c..e071d24 100644 --- a/OF DL.Core/Services/DownloadService.cs +++ b/OF DL.Core/Services/DownloadService.cs @@ -19,9 +19,9 @@ namespace OF_DL.Services; public class DownloadService( IAuthService authService, IConfigService configService, - IDBService dbService, + IDbService dbService, IFileNameService fileNameService, - IAPIService apiService) + IApiService apiService) : IDownloadService { private TaskCompletionSource _completionSource = new(); @@ -67,7 +67,7 @@ public class DownloadService( Directory.CreateDirectory(folder + subFolder); } - List md5Hashes = CalculateFolderMD5(folder + subFolder); + List md5Hashes = CalculateFolderMd5(folder + subFolder); Uri uri = new(url); string destinationPath = $"{folder}{subFolder}/"; @@ -205,7 +205,7 @@ public class DownloadService( } private async Task OnFFMPEGDownloadComplete(string tempFilename, DateTime lastModified, string folder, string path, - string customFileName, string filename, long media_id, string api_type, IProgressReporter progressReporter) + string customFileName, string filename, long mediaId, string apiType, IProgressReporter progressReporter) { try { @@ -223,16 +223,9 @@ public class DownloadService( long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : tempFilename).Length; - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } + progressReporter.ReportProgress(configService.CurrentConfig.ShowScrapeSize ? fileSizeInBytes : 1); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + await dbService.UpdateMedia(folder, mediaId, apiType, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } @@ -266,32 +259,27 @@ public class DownloadService( _completionSource.TrySetResult(false); } - private static List CalculateFolderMD5(string folder) + private static List CalculateFolderMd5(string folder) { - List md5Hashes = new(); - if (Directory.Exists(folder)) + List md5Hashes = []; + if (!Directory.Exists(folder)) { - string[] files = Directory.GetFiles(folder); - - foreach (string file in files) - { - md5Hashes.Add(CalculateMD5(file)); - } + return md5Hashes; } + string[] files = Directory.GetFiles(folder); + + md5Hashes.AddRange(files.Select(CalculateMd5)); + return md5Hashes; } - private static string CalculateMD5(string filePath) + private static string CalculateMd5(string filePath) { - using (MD5 md5 = MD5.Create()) - { - using (FileStream stream = File.OpenRead(filePath)) - { - byte[] hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } + using MD5 md5 = MD5.Create(); + using FileStream stream = File.OpenRead(filePath); + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } /// @@ -299,17 +287,17 @@ public class DownloadService( /// /// /// - /// - /// + /// + /// /// /// /// /// - protected async Task CreateDirectoriesAndDownloadMedia(string path, + private async Task CreateDirectoriesAndDownloadMedia(string path, string url, string folder, - long media_id, - string api_type, + long mediaId, + string apiType, IProgressReporter progressReporter, string serverFileName, string resolvedFileName) @@ -325,7 +313,7 @@ public class DownloadService( path = UpdatePathBasedOnExtension(folder, path, extension); - return await ProcessMediaDownload(folder, media_id, api_type, url, path, serverFileName, resolvedFileName, + return await ProcessMediaDownload(folder, mediaId, apiType, url, path, serverFileName, resolvedFileName, extension, progressReporter); } catch (Exception ex) @@ -461,12 +449,12 @@ public class DownloadService( if (uri.Host == "cdn3.onlyfans.com" && uri.LocalPath.Contains("/dash/files")) { string[] messageUrlParsed = url.Split(','); - string mpdURL = messageUrlParsed[0]; + string mpdUrl = messageUrlParsed[0]; string policy = messageUrlParsed[1]; string signature = messageUrlParsed[2]; string kvp = messageUrlParsed[3]; - mpdURL = mpdURL.Replace(".mpd", "_source.mp4"); + mpdUrl = mpdUrl.Replace(".mpd", "_source.mp4"); using HttpClient client = new(); client.DefaultRequestHeaders.Add("Cookie", @@ -474,7 +462,7 @@ public class DownloadService( client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.UserAgent); using HttpResponseMessage response = - await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); + await client.GetAsync(mpdUrl, HttpCompletionOption.ResponseHeadersRead); if (response.IsSuccessStatusCode) { fileSize = response.Content.Headers.ContentLength ?? 0; @@ -500,7 +488,7 @@ public class DownloadService( return fileSize; } - public static async Task GetDRMVideoLastModified(string url, Auth auth) + public static async Task GetDrmVideoLastModified(string url, Auth auth) { string[] messageUrlParsed = url.Split(','); string mpdUrl = messageUrlParsed[0]; @@ -532,15 +520,12 @@ public class DownloadService( using HttpClient client = new(); using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) + if (!response.IsSuccessStatusCode) { - if (response.Content.Headers.LastModified != null) - { - return response.Content.Headers.LastModified.Value.DateTime; - } + return DateTime.Now; } - return DateTime.Now; + return response.Content.Headers.LastModified?.DateTime ?? DateTime.Now; } /// @@ -636,8 +621,8 @@ public class DownloadService( /// Handles new media by downloading and updating the database. /// /// - /// - /// + /// + /// /// /// /// @@ -646,8 +631,8 @@ public class DownloadService( /// /// A Task resulting in a boolean indicating whether the media is newly downloaded or not. private async Task HandleNewMedia(string folder, - long media_id, - string api_type, + long mediaId, + string apiType, string url, string path, string serverFilename, @@ -696,32 +681,18 @@ public class DownloadService( fileSizeInBytes = GetLocalFileSize(finalPath); lastModified = File.GetLastWriteTime(finalPath); - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } + progressReporter.ReportProgress(configService.CurrentConfig.ShowScrapeSize ? fileSizeInBytes : 1); status = false; } // Handle the case where the file has been downloaded in the past with a custom filename. - //but it has downloaded outsite of this application so it doesn't exist in the database + // but it has downloaded outside of this application so it doesn't exist in the database // this is a bit improbable but we should check for that. else if (File.Exists(fullPathWithTheNewFileName)) { fileSizeInBytes = GetLocalFileSize(fullPathWithTheNewFileName); lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName); - if (configService.CurrentConfig.ShowScrapeSize) - { - progressReporter.ReportProgress(fileSizeInBytes); - } - else - { - progressReporter.ReportProgress(1); - } + progressReporter.ReportProgress(configService.CurrentConfig.ShowScrapeSize ? fileSizeInBytes : 1); status = false; } @@ -733,9 +704,9 @@ public class DownloadService( } //finaly check which filename we should use. Custom or the server one. - //if a custom is used, then the servefilename will be different from the resolved filename. + //if a custom is used, then the serverFilename will be different from the resolved filename. string finalName = serverFilename == resolvedFilename ? serverFilename : resolvedFilename; - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, finalName + extension, fileSizeInBytes, + await dbService.UpdateMedia(folder, mediaId, apiType, folder + path, finalName + extension, fileSizeInBytes, true, lastModified); return status; } @@ -745,16 +716,16 @@ public class DownloadService( /// Handles media that has been previously downloaded and updates the task accordingly. /// /// - /// - /// + /// + /// /// /// A boolean indicating whether the media is newly downloaded or not. - private async Task HandlePreviouslyDownloadedMediaAsync(string folder, long media_id, string api_type, + private async Task HandlePreviouslyDownloadedMediaAsync(string folder, long mediaId, string apiType, IProgressReporter progressReporter) { if (configService.CurrentConfig.ShowScrapeSize) { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + long size = await dbService.GetStoredFileSize(folder, mediaId, apiType); progressReporter.ReportProgress(size); } else @@ -791,11 +762,12 @@ public class DownloadService( Stream body = await response.Content.ReadAsStreamAsync(); // Wrap the body stream with the ThrottledStream to limit read rate. - using (ThrottledStream throttledStream = new(body, - configService.CurrentConfig.DownloadLimitInMbPerSec * 1_000_000, - configService.CurrentConfig.LimitDownloadRate)) + await using (ThrottledStream throttledStream = new(body, + configService.CurrentConfig.DownloadLimitInMbPerSec * 1_000_000, + configService.CurrentConfig.LimitDownloadRate)) { - using FileStream fileStream = new(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 16384, + await using FileStream fileStream = new(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, + 16384, true); byte[] buffer = new byte[16384]; int read; @@ -854,7 +826,7 @@ public class DownloadService( return totalFileSize; } - public async Task DownloadMedia(string url, string folder, long media_id, string api_type, + public async Task DownloadMedia(string url, string folder, long mediaId, string apiType, IProgressReporter progressReporter, string path, string? filenameFormat, object? postInfo, object? postMedia, object? author, Dictionary users) @@ -864,12 +836,12 @@ public class DownloadService( string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal); - return await CreateDirectoriesAndDownloadMedia(path, url, folder, media_id, api_type, progressReporter, + return await CreateDirectoriesAndDownloadMedia(path, url, folder, mediaId, apiType, progressReporter, filename, resolvedFilename); } - public async Task DownloadDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + public async Task DownloadDrmVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long mediaId, string apiType, IProgressReporter progressReporter, string path, string? filenameFormat, object? postInfo, object? postMedia, object? author, Dictionary users) @@ -902,14 +874,14 @@ public class DownloadService( string customFileName = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author, folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnEmpty); - if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + if (!await dbService.CheckDownloaded(folder, mediaId, apiType)) { if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) { return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp, - authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, media_id, api_type, + authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, mediaId, apiType, progressReporter, customFileName, filename, path); } @@ -918,7 +890,7 @@ public class DownloadService( : folder + path + "/" + filename + "_source.mp4").Length; ReportProgress(progressReporter, fileSizeInBytes); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, + await dbService.UpdateMedia(folder, mediaId, apiType, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); } @@ -946,15 +918,15 @@ public class DownloadService( return false; } - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); - await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", + long size = await dbService.GetStoredFileSize(folder, mediaId, apiType); + await dbService.UpdateMedia(folder, mediaId, apiType, folder + path, customFileName + ".mp4", size, true, lastModified); } } if (configService.CurrentConfig.ShowScrapeSize) { - long size = await dbService.GetStoredFileSize(folder, media_id, api_type); + long size = await dbService.GetStoredFileSize(folder, mediaId, apiType); progressReporter.ReportProgress(size); } else @@ -982,26 +954,17 @@ public class DownloadService( return false; } - private void ReportProgress(IProgressReporter reporter, long sizeOrCount) - { - if (configService.CurrentConfig.ShowScrapeSize) - { - reporter.ReportProgress(sizeOrCount); - } - else - { - reporter.ReportProgress(1); - } - } + private void ReportProgress(IProgressReporter reporter, long sizeOrCount) => + reporter.ReportProgress(configService.CurrentConfig.ShowScrapeSize ? sizeOrCount : 1); public async Task<(string decryptionKey, DateTime lastModified)?> GetDecryptionInfo( string mpdUrl, string policy, string signature, string kvp, string mediaId, string contentId, string drmType, bool clientIdBlobMissing, bool devicePrivateKeyMissing) { - string pssh = await apiService.GetDRMMPDPSSH(mpdUrl, policy, signature, kvp); + string pssh = await apiService.GetDrmMpdPssh(mpdUrl, policy, signature, kvp); - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdUrl, policy, signature, kvp); + DateTime lastModified = await apiService.GetDrmMpdLastModified(mpdUrl, policy, signature, kvp); Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/{drmType}/{contentId}", "?type=widevine"); @@ -1009,8 +972,8 @@ public class DownloadService( $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/{drmType}/{contentId}?type=widevine"; string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing - ? await apiService.GetDecryptionKeyOFDL(drmHeaders, licenseUrl, pssh) - : await apiService.GetDecryptionKeyCDM(drmHeaders, licenseUrl, pssh); + ? await apiService.GetDecryptionKeyOfdl(drmHeaders, licenseUrl, pssh) + : await apiService.GetDecryptionKeyCdm(drmHeaders, licenseUrl, pssh); return (decryptionKey, lastModified); } @@ -1041,10 +1004,10 @@ public class DownloadService( int oldHighlightsCount = 0; int newHighlightsCount = 0; - foreach (KeyValuePair highlightKVP in highlights) + foreach (KeyValuePair highlightKvp in highlights) { bool isNew = - await DownloadMedia(highlightKVP.Value, path, highlightKVP.Key, "Stories", progressReporter, + await DownloadMedia(highlightKvp.Value, path, highlightKvp.Key, "Stories", progressReporter, "/Stories/Free", null, null, null, null, new Dictionary()); if (isNew) { @@ -1095,9 +1058,9 @@ public class DownloadService( int oldStoriesCount = 0; int newStoriesCount = 0; - foreach (KeyValuePair storyKVP in stories) + foreach (KeyValuePair storyKvp in stories) { - bool isNew = await DownloadMedia(storyKVP.Value, path, storyKVP.Key, "Stories", progressReporter, + bool isNew = await DownloadMedia(storyKvp.Value, path, storyKvp.Key, "Stories", progressReporter, "/Stories/Free", null, null, null, null, new Dictionary()); if (isNew) { @@ -1146,20 +1109,20 @@ public class DownloadService( int oldArchivedCount = 0; int newArchivedCount = 0; - foreach (KeyValuePair archivedKVP in archived.ArchivedPosts) + foreach (KeyValuePair archivedKvp in archived.ArchivedPosts) { bool isNew; ArchivedEntities.Medium? mediaInfo = - archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKVP.Key); + archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKvp.Key); ArchivedEntities.ListItem? postInfo = mediaInfo == null ? null : archived.ArchivedPostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; - if (archivedKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + if (archivedKvp.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = archivedKVP.Value.Split(','); + string[] parsed = archivedKvp.Value.Split(','); (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); @@ -1168,14 +1131,14 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, archivedKVP.Key, "Posts", + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, archivedKvp.Key, "Posts", progressReporter, "/Archived/Posts/Free/Videos", filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); } else { - isNew = await DownloadMedia(archivedKVP.Value, path, archivedKVP.Key, "Posts", progressReporter, + isNew = await DownloadMedia(archivedKvp.Value, path, archivedKvp.Key, "Posts", progressReporter, "/Archived/Posts/Free", filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); } @@ -1226,12 +1189,12 @@ public class DownloadService( int oldMessagesCount = 0; int newMessagesCount = 0; - foreach (KeyValuePair messageKVP in messages.Messages) + foreach (KeyValuePair messageKvp in messages.Messages) { bool isNew; - MessageEntities.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.Id == messageKVP.Key); + MessageEntities.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.Id == messageKvp.Key); MessageEntities.ListItem? messageInfo = messages.MessageObjects.FirstOrDefault(p => - p.Media?.Any(m => m.Id == messageKVP.Key) == true); + p.Media?.Any(m => m.Id == messageKvp.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? ""; string messagePath = configService.CurrentConfig.FolderPerMessage && messageInfo != null && @@ -1239,9 +1202,9 @@ public class DownloadService( ? $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Messages/Free"; - if (messageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + if (messageKvp.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = messageKVP.Value.Split(','); + string[] parsed = messageKvp.Value.Split(','); (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); @@ -1250,14 +1213,14 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, messageKVP.Key, "Messages", + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, messageKvp.Key, "Messages", progressReporter, messagePath + "/Videos", filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); } else { - isNew = await DownloadMedia(messageKVP.Value, path, messageKVP.Key, "Messages", progressReporter, + isNew = await DownloadMedia(messageKvp.Value, path, messageKvp.Key, "Messages", progressReporter, messagePath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); } @@ -1335,7 +1298,7 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, kvpEntry.Key, "Messages", progressReporter, paidMsgPath + "/Videos", filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); @@ -1415,7 +1378,7 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, kvpEntry.Key, "Streams", progressReporter, streamPath + "/Videos", filenameFormat, streamInfo, mediaInfo, streamInfo?.Author, users); @@ -1471,10 +1434,10 @@ public class DownloadService( int oldCount = 0, newCount = 0; - foreach (KeyValuePair postKVP in posts.Posts) + foreach (KeyValuePair postKvp in posts.Posts) { bool isNew; - PostEntities.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m.Id == postKVP.Key); + PostEntities.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m.Id == postKvp.Key); PostEntities.ListItem? postInfo = mediaInfo == null ? null : posts.PostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); @@ -1484,9 +1447,9 @@ public class DownloadService( ? $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}" : "/Posts/Free"; - if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + if (postKvp.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = postKVP.Value.Split(','); + string[] parsed = postKvp.Value.Split(','); (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); @@ -1495,14 +1458,14 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKVP.Key, "Posts", + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKvp.Key, "Posts", progressReporter, postPath + "/Videos", filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); } else { - isNew = await DownloadMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, + isNew = await DownloadMedia(postKvp.Value, path, postKvp.Key, "Posts", progressReporter, postPath, filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); } @@ -1551,13 +1514,13 @@ public class DownloadService( int oldCount = 0, newCount = 0; - foreach (KeyValuePair postKVP in purchasedPosts.PaidPosts) + foreach (KeyValuePair postKvp in purchasedPosts.PaidPosts) { bool isNew; MessageEntities.Medium? mediaInfo = - purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKVP.Key); + purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKvp.Key); PurchasedEntities.ListItem? postInfo = - purchasedPosts.PaidPostObjects.FirstOrDefault(p => p.Media?.Any(m => m.Id == postKVP.Key) == true); + purchasedPosts.PaidPostObjects.FirstOrDefault(p => p.Media?.Any(m => m.Id == postKvp.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? ""; string paidPostPath = configService.CurrentConfig.FolderPerPaidPost && postInfo != null && @@ -1565,9 +1528,9 @@ public class DownloadService( ? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Posts/Paid"; - if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + if (postKvp.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = postKVP.Value.Split(','); + string[] parsed = postKvp.Value.Split(','); (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); @@ -1576,14 +1539,14 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKVP.Key, "Posts", + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKvp.Key, "Posts", progressReporter, paidPostPath + "/Videos", filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users); } else { - isNew = await DownloadMedia(postKVP.Value, path, postKVP.Key, "Posts", progressReporter, + isNew = await DownloadMedia(postKvp.Value, path, postKvp.Key, "Posts", progressReporter, paidPostPath, filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users); } @@ -1622,14 +1585,14 @@ public class DownloadService( int oldCount = 0, newCount = 0; - foreach (KeyValuePair purchasedPostKVP in purchasedPosts.PaidPosts) + foreach (KeyValuePair purchasedPostKvp in purchasedPosts.PaidPosts) { bool isNew; MessageEntities.Medium? mediaInfo = - purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key); + purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKvp.Key); PurchasedEntities.ListItem? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => - p.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true) + p.Media?.Any(m => m.Id == purchasedPostKvp.Key) == true) : null; string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) .PaidPostFileNameFormat ?? ""; @@ -1638,9 +1601,9 @@ public class DownloadService( ? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Posts/Paid"; - if (purchasedPostKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + if (purchasedPostKvp.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = purchasedPostKVP.Value.Split(','); + string[] parsed = purchasedPostKvp.Value.Split(','); (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); @@ -1649,15 +1612,15 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, purchasedPostKVP.Key, + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, purchasedPostKvp.Key, "Posts", progressReporter, paidPostPath + "/Videos", filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users); } else { - isNew = await DownloadMedia(purchasedPostKVP.Value, path, - purchasedPostKVP.Key, "Posts", progressReporter, + isNew = await DownloadMedia(purchasedPostKvp.Value, path, + purchasedPostKvp.Key, "Posts", progressReporter, paidPostPath, filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users); } @@ -1696,14 +1659,14 @@ public class DownloadService( int oldCount = 0, newCount = 0; - foreach (KeyValuePair paidMessageKVP in paidMessageCollection.PaidMessages) + foreach (KeyValuePair paidMessageKvp in paidMessageCollection.PaidMessages) { bool isNew; MessageEntities.Medium? mediaInfo = - paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKVP.Key); + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKvp.Key); PurchasedEntities.ListItem? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => - p.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + p.Media?.Any(m => m.Id == paidMessageKvp.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) .PaidMessageFileNameFormat ?? ""; string paidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && @@ -1711,9 +1674,9 @@ public class DownloadService( ? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}" : "/Messages/Paid"; - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + if (paidMessageKvp.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = paidMessageKVP.Value.Split(','); + string[] parsed = paidMessageKvp.Value.Split(','); (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); @@ -1722,15 +1685,15 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKvp.Key, "Messages", progressReporter, paidMsgPath + "/Videos", filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); } else { - isNew = await DownloadMedia(paidMessageKVP.Value, path, - paidMessageKVP.Key, "Messages", progressReporter, + isNew = await DownloadMedia(paidMessageKvp.Value, path, + paidMessageKvp.Key, "Messages", progressReporter, paidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); } @@ -1769,9 +1732,9 @@ public class DownloadService( int oldCount = 0, newCount = 0; - foreach (KeyValuePair postKVP in post.SinglePosts) + foreach (KeyValuePair postKvp in post.SinglePosts) { - PostEntities.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.Id == postKVP.Key); + PostEntities.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.Id == postKvp.Key); PostEntities.SinglePost? postInfo = mediaInfo == null ? null : post.SinglePostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true); @@ -1782,9 +1745,9 @@ public class DownloadService( : "/Posts/Free"; bool isNew; - if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + if (postKvp.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = postKVP.Value.Split(','); + string[] parsed = postKvp.Value.Split(','); (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing); @@ -1793,8 +1756,8 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKVP.Key, "Posts", + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKvp.Key, "Posts", progressReporter, postPath + "/Videos", filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); } @@ -1802,8 +1765,8 @@ public class DownloadService( { try { - isNew = await DownloadMedia(postKVP.Value, path, - postKVP.Key, "Posts", progressReporter, + isNew = await DownloadMedia(postKvp.Value, path, + postKvp.Key, "Posts", progressReporter, postPath, filenameFormat, postInfo, mediaInfo, postInfo?.Author, users); } catch @@ -1845,14 +1808,14 @@ public class DownloadService( // Download preview messages if (singlePaidMessageCollection.PreviewSingleMessages.Count > 0) { - foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.PreviewSingleMessages) + foreach (KeyValuePair paidMessageKvp in singlePaidMessageCollection.PreviewSingleMessages) { MessageEntities.Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => - m.Id == paidMessageKVP.Key); + m.Id == paidMessageKvp.Key); MessageEntities.SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + p.Media?.Any(m => m.Id == paidMessageKvp.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) .PaidMessageFileNameFormat ?? ""; string previewMsgPath = configService.CurrentConfig.FolderPerMessage && messageInfo != null && @@ -1861,9 +1824,9 @@ public class DownloadService( : "/Messages/Free"; bool isNew; - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + if (paidMessageKvp.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = paidMessageKVP.Value.Split(','); + string[] parsed = paidMessageKvp.Value.Split(','); (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); @@ -1872,15 +1835,15 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKvp.Key, "Messages", progressReporter, previewMsgPath + "/Videos", filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); } else { - isNew = await DownloadMedia(paidMessageKVP.Value, path, - paidMessageKVP.Key, "Messages", progressReporter, + isNew = await DownloadMedia(paidMessageKvp.Value, path, + paidMessageKvp.Key, "Messages", progressReporter, previewMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); } @@ -1898,14 +1861,14 @@ public class DownloadService( // Download actual paid messages if (singlePaidMessageCollection.SingleMessages.Count > 0) { - foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.SingleMessages) + foreach (KeyValuePair paidMessageKvp in singlePaidMessageCollection.SingleMessages) { MessageEntities.Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => - m.Id == paidMessageKVP.Key); + m.Id == paidMessageKvp.Key); MessageEntities.SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => - p.Media?.Any(m => m.Id == paidMessageKVP.Key) == true); + p.Media?.Any(m => m.Id == paidMessageKvp.Key) == true); string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) .PaidMessageFileNameFormat ?? ""; string singlePaidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && @@ -1915,9 +1878,9 @@ public class DownloadService( : "/Messages/Paid"; bool isNew; - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + if (paidMessageKvp.Value.Contains("cdn3.onlyfans.com/dash/files")) { - string[] parsed = paidMessageKVP.Value.Split(','); + string[] parsed = paidMessageKvp.Value.Split(','); (string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3], parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing); @@ -1926,15 +1889,15 @@ public class DownloadService( continue; } - isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0], - drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key, + isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0], + drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKvp.Key, "Messages", progressReporter, singlePaidMsgPath + "/Videos", filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); } else { - isNew = await DownloadMedia(paidMessageKVP.Value, path, - paidMessageKVP.Key, "Messages", progressReporter, + isNew = await DownloadMedia(paidMessageKvp.Value, path, + paidMessageKvp.Key, "Messages", progressReporter, singlePaidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users); } diff --git a/OF DL.Core/Services/FileNameService.cs b/OF DL.Core/Services/FileNameService.cs index cd8732e..0f533bd 100644 --- a/OF DL.Core/Services/FileNameService.cs +++ b/OF DL.Core/Services/FileNameService.cs @@ -27,7 +27,7 @@ public class FileNameService(IAuthService authService) : IFileNameService if (fileProperty != null && drmProperty != null && propertyName == "mediaCreatedAt") { - string? mpdurl = GetNestedPropertyValue(media, "Files.Drm.Manifest.Dash") as string; + string? mpdUrl = GetNestedPropertyValue(media, "Files.Drm.Manifest.Dash") as string; string? policy = GetNestedPropertyValue(media, "Files.Drm.Signature.Dash.CloudFrontPolicy") as string; string? signature = @@ -35,7 +35,7 @@ public class FileNameService(IAuthService authService) : IFileNameService string? kvp = GetNestedPropertyValue(media, "Files.Drm.Signature.Dash.CloudFrontKeyPairId") as string; - if (string.IsNullOrEmpty(mpdurl) || string.IsNullOrEmpty(policy) || + if (string.IsNullOrEmpty(mpdUrl) || string.IsNullOrEmpty(policy) || string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(kvp) || authService.CurrentAuth == null) { @@ -43,7 +43,7 @@ public class FileNameService(IAuthService authService) : IFileNameService } DateTime lastModified = - await DownloadService.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), + await DownloadService.GetDrmVideoLastModified(string.Join(",", mpdUrl, policy, signature, kvp), authService.CurrentAuth); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); continue; diff --git a/OF DL.Core/Services/IAPIService.cs b/OF DL.Core/Services/IApiService.cs similarity index 79% rename from OF DL.Core/Services/IAPIService.cs rename to OF DL.Core/Services/IApiService.cs index 9fa9349..0ff8a39 100644 --- a/OF DL.Core/Services/IAPIService.cs +++ b/OF DL.Core/Services/IApiService.cs @@ -9,27 +9,31 @@ using UserEntities = OF_DL.Models.Entities.Users; namespace OF_DL.Services; -public interface IAPIService +public interface IApiService { - Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, string pssh); - Task GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp); - Task GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp); + Task GetDecryptionKeyCdm(Dictionary drmHeaders, string licenceUrl, string pssh); + + Task GetDrmMpdLastModified(string mpdUrl, string policy, string signature, string kvp); + + Task GetDrmMpdPssh(string mpdUrl, string policy, string signature, string kvp); + Task?> GetLists(string endpoint); + Task?> GetListUsers(string endpoint); - Task?> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, - List paid_post_ids); + Task?> GetMedia(MediaType mediaType, string endpoint, string? username, string folder, + List paidPostIds); Task GetPaidPosts(string endpoint, string folder, string username, - List paid_post_ids, + List paidPostIds, IStatusReporter statusReporter); - Task GetPosts(string endpoint, string folder, List paid_post_ids, + Task GetPosts(string endpoint, string folder, List paidPostIds, IStatusReporter statusReporter); Task GetPost(string endpoint, string folder); - Task GetStreams(string endpoint, string folder, List paid_post_ids, + Task GetStreams(string endpoint, string folder, List paidPostIds, IStatusReporter statusReporter); Task GetArchived(string endpoint, string folder, @@ -41,15 +45,21 @@ public interface IAPIService IStatusReporter statusReporter); Task GetPaidMessage(string endpoint, string folder); + Task> GetPurchasedTabUsers(string endpoint, Dictionary users); Task> GetPurchasedTab(string endpoint, string folder, Dictionary users); Task GetUserInfo(string endpoint); + Task GetUserInfoById(string endpoint); + Dictionary GetDynamicHeaders(string path, string queryParam); + Task?> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions); + Task?> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions); - Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, string pssh); + + Task GetDecryptionKeyOfdl(Dictionary drmHeaders, string licenceUrl, string pssh); } diff --git a/OF DL.Core/Services/IDBService.cs b/OF DL.Core/Services/IDBService.cs deleted file mode 100644 index 9a14484..0000000 --- a/OF DL.Core/Services/IDBService.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace OF_DL.Services; - -public interface IDBService -{ - Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, - DateTime created_at, long user_id); - - Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, - DateTime created_at); - - Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, - DateTime created_at); - - Task CreateDB(string folder); - Task CreateUsersDB(Dictionary users); - Task CheckUsername(KeyValuePair user, string path); - - Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, string? filename, - long? size, string api_type, string media_type, bool preview, bool downloaded, DateTime? created_at); - - Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, long size, - bool downloaded, DateTime created_at); - - Task GetStoredFileSize(string folder, long media_id, string api_type); - Task CheckDownloaded(string folder, long media_id, string api_type); - Task GetMostRecentPostDate(string folder); -} diff --git a/OF DL.Core/Services/IDbService.cs b/OF DL.Core/Services/IDbService.cs new file mode 100644 index 0000000..c7b05f4 --- /dev/null +++ b/OF DL.Core/Services/IDbService.cs @@ -0,0 +1,31 @@ +namespace OF_DL.Services; + +public interface IDbService +{ + Task AddMessage(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, + DateTime createdAt, long userId); + + Task AddPost(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, + DateTime createdAt); + + Task AddStory(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, + DateTime createdAt); + + Task CreateDb(string folder); + + Task CreateUsersDb(Dictionary users); + + Task CheckUsername(KeyValuePair user, string path); + + Task AddMedia(string folder, long mediaId, long postId, string link, string? directory, string? filename, + long? size, string apiType, string mediaType, bool preview, bool downloaded, DateTime? createdAt); + + Task UpdateMedia(string folder, long mediaId, string apiType, string directory, string filename, long size, + bool downloaded, DateTime createdAt); + + Task GetStoredFileSize(string folder, long mediaId, string apiType); + + Task CheckDownloaded(string folder, long mediaId, string apiType); + + Task GetMostRecentPostDate(string folder); +} diff --git a/OF DL.Core/Services/IDownloadService.cs b/OF DL.Core/Services/IDownloadService.cs index b474f60..b2a6e26 100644 --- a/OF DL.Core/Services/IDownloadService.cs +++ b/OF DL.Core/Services/IDownloadService.cs @@ -11,22 +11,22 @@ public interface IDownloadService { Task CalculateTotalFileSize(List urls); - Task ProcessMediaDownload(string folder, long media_id, string api_type, string url, string path, + Task ProcessMediaDownload(string folder, long mediaId, string apiType, string url, string path, string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); - Task DownloadMedia(string url, string folder, long media_id, string api_type, + Task DownloadMedia(string url, string folder, long mediaId, string apiType, IProgressReporter progressReporter, string path, string? filenameFormat, object? postInfo, object? postMedia, object? author, Dictionary users); - Task DownloadDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + Task DownloadDrmVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long mediaId, string apiType, IProgressReporter progressReporter, string path, string? filenameFormat, object? postInfo, object? postMedia, object? author, Dictionary users); Task<(string decryptionKey, DateTime lastModified)?> GetDecryptionInfo( - string mpdURL, string policy, string signature, string kvp, + string mpdUrl, string policy, string signature, string kvp, string mediaId, string contentId, string drmType, bool clientIdBlobMissing, bool devicePrivateKeyMissing); diff --git a/OF DL.Core/Services/ILoggingService.cs b/OF DL.Core/Services/ILoggingService.cs index 7c6a8a3..787ca92 100644 --- a/OF DL.Core/Services/ILoggingService.cs +++ b/OF DL.Core/Services/ILoggingService.cs @@ -6,6 +6,8 @@ namespace OF_DL.Services; public interface ILoggingService { LoggingLevelSwitch LevelSwitch { get; } + void UpdateLoggingLevel(LoggingLevel newLevel); + LoggingLevel GetCurrentLoggingLevel(); } diff --git a/OF DL.Core/Services/StartupService.cs b/OF DL.Core/Services/StartupService.cs index 08d80d2..ff96321 100644 --- a/OF DL.Core/Services/StartupService.cs +++ b/OF DL.Core/Services/StartupService.cs @@ -221,7 +221,7 @@ public class StartupService(IConfigService configService, IAuthService authServi return File.Exists(path); } - public static string? GetFullPath(string filename) + private static string? GetFullPath(string filename) { if (File.Exists(filename)) { @@ -229,15 +229,7 @@ public class StartupService(IConfigService configService, IAuthService authServi } string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? ""; - foreach (string path in pathEnv.Split(Path.PathSeparator)) - { - string fullPath = Path.Combine(path, filename); - if (File.Exists(fullPath)) - { - return fullPath; - } - } - - return null; + return pathEnv.Split(Path.PathSeparator).Select(path => Path.Combine(path, filename)) + .FirstOrDefault(File.Exists); } } diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 23647e2..f43d269 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -110,8 +110,8 @@ public class Program(IServiceProvider serviceProvider) services.AddSingleton(loggingService); services.AddSingleton(configService); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -403,8 +403,8 @@ public class Program(IServiceProvider serviceProvider) AnsiConsole.Markup($"[red]Folder for {Markup.Escape(username)} already created\n[/]"); } - IDBService dbService = serviceProvider.GetRequiredService(); - await dbService.CreateDB(path); + IDbService dbService = serviceProvider.GetRequiredService(); + await dbService.CreateDb(path); await orchestrationService.DownloadSinglePostAsync(username, postId, path, users, startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler); @@ -475,8 +475,8 @@ public class Program(IServiceProvider serviceProvider) Log.Debug($"Folder for {username} already created"); } - IDBService dbService = serviceProvider.GetRequiredService(); - await dbService.CreateDB(path); + IDbService dbService = serviceProvider.GetRequiredService(); + await dbService.CreateDb(path); await orchestrationService.DownloadSinglePaidMessageAsync(username, messageId, path, users, startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler); @@ -488,7 +488,7 @@ public class Program(IServiceProvider serviceProvider) { IConfigService configService = serviceProvider.GetRequiredService(); IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); + IApiService apiService = serviceProvider.GetRequiredService(); ILoggingService loggingService = serviceProvider.GetRequiredService(); bool hasSelectedUsers = false; From e184df906f64e4564258b467fbd7cfb0a167ed1f Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 10:30:05 -0600 Subject: [PATCH 40/55] Add header comments and extract duplicated exception logging to a helper function --- OF DL.Core/Helpers/ExceptionLoggerHelper.cs | 26 + OF DL.Core/Services/ApiService.cs | 828 +++++------------- OF DL.Core/Services/AuthService.cs | 26 + OF DL.Core/Services/ConfigService.cs | 28 + OF DL.Core/Services/DbService.cs | 178 ++-- .../Services/DownloadOrchestrationService.cs | 68 ++ OF DL.Core/Services/DownloadService.cs | 274 ++++-- OF DL.Core/Services/FileNameService.cs | 16 + OF DL.Core/Services/IApiService.cs | 66 ++ OF DL.Core/Services/IAuthService.cs | 12 + OF DL.Core/Services/IConfigService.cs | 15 + OF DL.Core/Services/IDbService.cs | 33 + OF DL.Core/Services/IDownloadService.cs | 54 ++ OF DL.Core/Services/IFileNameService.cs | 6 + OF DL.Core/Services/ILoggingService.cs | 9 + OF DL.Core/Services/IStartupService.cs | 6 + OF DL.Core/Services/LoggingService.cs | 10 + OF DL.Core/Services/StartupService.cs | 8 + 18 files changed, 917 insertions(+), 746 deletions(-) create mode 100644 OF DL.Core/Helpers/ExceptionLoggerHelper.cs diff --git a/OF DL.Core/Helpers/ExceptionLoggerHelper.cs b/OF DL.Core/Helpers/ExceptionLoggerHelper.cs new file mode 100644 index 0000000..0162e6c --- /dev/null +++ b/OF DL.Core/Helpers/ExceptionLoggerHelper.cs @@ -0,0 +1,26 @@ +using Serilog; + +namespace OF_DL.Helpers; + +internal static class ExceptionLoggerHelper +{ + /// + /// Logs an exception to the console and Serilog with inner exception details. + /// + /// The exception to log. + public static void LogException(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) + { + return; + } + + 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); + } +} diff --git a/OF DL.Core/Services/ApiService.cs b/OF DL.Core/Services/ApiService.cs index ee08a24..09017f0 100644 --- a/OF DL.Core/Services/ApiService.cs +++ b/OF DL.Core/Services/ApiService.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json.Linq; using OF_DL.Models; using OF_DL.Models.Entities.Common; using OF_DL.Enumerations; +using OF_DL.Helpers; using ArchivedDtos = OF_DL.Models.Dtos.Archived; using HighlightDtos = OF_DL.Models.Dtos.Highlights; using ListDtos = OF_DL.Models.Dtos.Lists; @@ -50,6 +51,12 @@ public class ApiService(IAuthService authService, IConfigService configService, s_mJsonSerializerSettings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }; + /// + /// Builds signed headers for API requests using dynamic rules. + /// + /// The API path. + /// The query string. + /// Signed headers required by the API. public Dictionary GetDynamicHeaders(string path, string queryParams) { Log.Debug("Calling GetDynamicHeaders"); @@ -141,6 +148,11 @@ public class ApiService(IAuthService authService, IConfigService configService, } + /// + /// Retrieves user information from the API. + /// + /// The user endpoint. + /// The user entity when available. public async Task GetUserInfo(string endpoint) { Log.Debug($"Calling GetUserInfo: {endpoint}"); @@ -173,21 +185,17 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return null; } + /// + /// Retrieves user information by ID. + /// + /// The user list endpoint. + /// A JSON object when available. public async Task GetUserInfoById(string endpoint) { try @@ -214,21 +222,18 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return null; } + /// + /// Retrieves active subscriptions. + /// + /// The subscriptions endpoint. + /// Whether to include restricted subscriptions. + /// A username-to-userId map. public async Task?> GetActiveSubscriptions(string endpoint, bool includeRestricted) { Dictionary getParams = new() @@ -240,6 +245,12 @@ public class ApiService(IAuthService authService, IConfigService configService, } + /// + /// Retrieves expired subscriptions. + /// + /// The subscriptions endpoint. + /// Whether to include restricted subscriptions. + /// A username-to-userId map. public async Task?> GetExpiredSubscriptions(string endpoint, bool includeRestricted) { Dictionary getParams = new() @@ -253,6 +264,11 @@ public class ApiService(IAuthService authService, IConfigService configService, } + /// + /// Retrieves the user's lists. + /// + /// The lists endpoint. + /// A list name to list ID map. public async Task?> GetLists(string endpoint) { Log.Debug("Calling GetLists"); @@ -303,22 +319,17 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return null; } - + /// + /// Retrieves usernames in a specific list. + /// + /// The list users endpoint. + /// The usernames in the list. public async Task?> GetListUsers(string endpoint) { Log.Debug($"Calling GetListUsers - {endpoint}"); @@ -327,7 +338,7 @@ public class ApiService(IAuthService authService, IConfigService configService, { int offset = 0; Dictionary getParams = new() { { "offset", offset.ToString() }, { "limit", "50" } }; - List users = new(); + List users = []; while (true) { @@ -346,10 +357,7 @@ public class ApiService(IAuthService authService, IConfigService configService, break; } - foreach (ListEntities.UsersList ul in usersList) - { - users.Add(ul.Username); - } + users.AddRange(usersList.Select(ul => ul.Username)); if (users.Count < 50) { @@ -364,22 +372,22 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return null; } + /// + /// Retrieves media URLs for stories or highlights. + /// + /// The media type to fetch. + /// The endpoint to query. + /// Optional username context. + /// The creator folder path. + /// Paid post media IDs. + /// A mediaId-to-URL map. public async Task?> GetMedia(MediaType mediatype, string endpoint, string? username, @@ -392,7 +400,7 @@ public class ApiService(IAuthService authService, IConfigService configService, { Dictionary returnUrls = new(); const int postLimit = 50; - int limit = 5; + const int limit = 5; int offset = 0; Dictionary getParams = new(); @@ -465,22 +473,7 @@ public class ApiService(IAuthService authService, IConfigService configService, await dbService.AddMedia(folder, medium.Id, story.Id, mediaUrl, null, null, null, "Stories", mediaType, false, false, null); - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -588,22 +581,7 @@ public class ApiService(IAuthService authService, IConfigService configService, string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; await dbService.AddMedia(folder, medium.Id, item.Id, storyUrlValue, null, null, null, "Stories", mediaType, false, false, null); - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -622,22 +600,22 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return null; } + /// + /// Retrieves paid posts and their media. + /// + /// The paid posts endpoint. + /// The creator folder path. + /// The creator username. + /// A list to collect paid media IDs. + /// Status reporter. + /// A paid post collection. public async Task GetPaidPosts(string endpoint, string folder, string username, List paidPostIds, IStatusReporter statusReporter) @@ -729,22 +707,7 @@ public class ApiService(IAuthService authService, IConfigService configService, paidPostIds.Add(medium.Id); } - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -818,22 +781,21 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return new PurchasedEntities.PaidPostCollection(); } + /// + /// Retrieves posts and their media. + /// + /// The posts endpoint. + /// The creator folder path. + /// Paid post media IDs to skip. + /// Status reporter. + /// A post collection. public async Task GetPosts(string endpoint, string folder, List paidPostIds, IStatusReporter statusReporter) { @@ -951,22 +913,7 @@ public class ApiService(IAuthService authService, IConfigService configService, { foreach (PostEntities.Medium medium in post.Media) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1022,21 +969,18 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return new PostEntities.PostCollection(); } + /// + /// Retrieves a single post and its media. + /// + /// The post endpoint. + /// The creator folder path. + /// A single post collection. public async Task GetPost(string endpoint, string folder) { Log.Debug($"Calling GetPost - {endpoint}"); @@ -1078,22 +1022,7 @@ public class ApiService(IAuthService authService, IConfigService configService, { foreach (PostEntities.Medium medium in singlePost.Media) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1181,21 +1110,20 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return new SinglePostCollection(); } + /// + /// Retrieves streams and their media. + /// + /// The streams endpoint. + /// The creator folder path. + /// Paid post media IDs to skip. + /// Status reporter. + /// A streams collection. public async Task GetStreams(string endpoint, string folder, List paidPostIds, IStatusReporter statusReporter) @@ -1285,22 +1213,7 @@ public class ApiService(IAuthService authService, IConfigService configService, { foreach (StreamEntities.Medium medium in stream.Media) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1345,22 +1258,20 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return new StreamEntities.StreamsCollection(); } + /// + /// Retrieves archived posts and their media. + /// + /// The archived posts endpoint. + /// The creator folder path. + /// Status reporter. + /// An archived collection. public async Task GetArchived(string endpoint, string folder, IStatusReporter statusReporter) { @@ -1459,22 +1370,7 @@ public class ApiService(IAuthService authService, IConfigService configService, { foreach (ArchivedEntities.Medium medium in archive.Media) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1514,22 +1410,20 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return new ArchivedEntities.ArchivedCollection(); } + /// + /// Retrieves messages and their media. + /// + /// The messages endpoint. + /// The creator folder path. + /// Status reporter. + /// A message collection. public async Task GetMessages(string endpoint, string folder, IStatusReporter statusReporter) { @@ -1616,22 +1510,7 @@ public class ApiService(IAuthService authService, IConfigService configService, if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1649,22 +1528,7 @@ public class ApiService(IAuthService authService, IConfigService configService, out string cloudFrontPolicy, out string cloudFrontSignature, out string cloudFrontKeyPairId)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1690,22 +1554,7 @@ public class ApiService(IAuthService authService, IConfigService configService, if (medium.CanView && !string.IsNullOrEmpty(fullUrl) && isPreview) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1723,22 +1572,7 @@ public class ApiService(IAuthService authService, IConfigService configService, out string cloudFrontPolicy, out string cloudFrontSignature, out string cloudFrontKeyPairId)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1761,21 +1595,18 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return new MessageEntities.MessageCollection(); } + /// + /// Retrieves a single paid message and its media. + /// + /// The paid message endpoint. + /// The creator folder path. + /// A single paid message collection. public async Task GetPaidMessage(string endpoint, string folder) { Log.Debug($"Calling GetPaidMessage - {endpoint}"); @@ -1825,22 +1656,7 @@ public class ApiService(IAuthService authService, IConfigService configService, if (!isPreview && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1855,22 +1671,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } else if (isPreview && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1887,22 +1688,7 @@ public class ApiService(IAuthService authService, IConfigService configService, TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, out string cloudFrontSignature, out string cloudFrontKeyPairId)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1921,22 +1707,7 @@ public class ApiService(IAuthService authService, IConfigService configService, out string previewCloudFrontPolicy, out string previewCloudFrontSignature, out string previewCloudFrontKeyPairId)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -1958,22 +1729,21 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return new PurchasedEntities.SinglePaidMessageCollection(); } + /// + /// Retrieves paid messages and their media. + /// + /// The paid messages endpoint. + /// The creator folder path. + /// The creator username. + /// Status reporter. + /// A paid message collection. public async Task GetPaidMessages(string endpoint, string folder, string username, IStatusReporter statusReporter) @@ -2096,22 +1866,7 @@ public class ApiService(IAuthService authService, IConfigService configService, bool has = previewids.Any(cus => cus.Equals(medium.Id)); if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -2130,22 +1885,7 @@ public class ApiService(IAuthService authService, IConfigService configService, out string cloudFrontPolicy, out string cloudFrontSignature, out string cloudFrontKeyPairId)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -2165,22 +1905,7 @@ public class ApiService(IAuthService authService, IConfigService configService, { if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -2199,22 +1924,7 @@ public class ApiService(IAuthService authService, IConfigService configService, out string cloudFrontPolicy, out string cloudFrontSignature, out string cloudFrontKeyPairId)) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -2240,21 +1950,18 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return new PurchasedEntities.PaidMessageCollection(); } + /// + /// Retrieves users that appear in the Purchased tab. + /// + /// The purchased tab endpoint. + /// Known users map. + /// A username-to-userId map. public async Task> GetPurchasedTabUsers(string endpoint, Dictionary users) { Log.Debug($"Calling GetPurchasedTabUsers - {endpoint}"); @@ -2410,21 +2117,19 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return new Dictionary(); } + /// + /// Retrieves Purchased tab content grouped by user. + /// + /// The purchased tab endpoint. + /// The base download folder. + /// Known users map. + /// A list of purchased tab collections. public async Task> GetPurchasedTab(string endpoint, string folder, Dictionary users) { @@ -2575,22 +2280,7 @@ public class ApiService(IAuthService authService, IConfigService configService, purchasedTabCollection.PaidPosts.PaidPostObjects.Add(purchase); foreach (MessageEntities.Medium medium in purchase.Media) { - if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -2706,25 +2396,7 @@ public class ApiService(IAuthService authService, IConfigService configService, bool has = paidMessagePreviewids.Any(cus => cus.Equals(medium.Id)); if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { - if (medium.Type == "photo" && - !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && - !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && - !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -2746,25 +2418,7 @@ public class ApiService(IAuthService authService, IConfigService configService, out string cloudFrontPolicy, out string cloudFrontSignature, out string cloudFrontKeyPairId)) { - if (medium.Type == "photo" && - !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && - !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && - !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -2790,25 +2444,7 @@ public class ApiService(IAuthService authService, IConfigService configService, if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { - if (medium.Type == "photo" && - !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && - !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && - !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -2830,25 +2466,7 @@ public class ApiService(IAuthService authService, IConfigService configService, out string cloudFrontPolicy, out string cloudFrontSignature, out string cloudFrontKeyPairId)) { - if (medium.Type == "photo" && - !configService.CurrentConfig.DownloadImages) - { - continue; - } - - if (medium.Type == "video" && - !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "gif" && !configService.CurrentConfig.DownloadVideos) - { - continue; - } - - if (medium.Type == "audio" && - !configService.CurrentConfig.DownloadAudios) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { continue; } @@ -2881,22 +2499,21 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return []; } + /// + /// Retrieves the Widevine PSSH from an MPD manifest. + /// + /// The MPD URL. + /// CloudFront policy token. + /// CloudFront signature token. + /// CloudFront key pair ID. + /// The PSSH value or an empty string. public async Task GetDrmMpdPssh(string mpdUrl, string policy, string signature, string kvp) { try @@ -2925,22 +2542,21 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return string.Empty; } + /// + /// Retrieves the Last-Modified timestamp for an MPD manifest. + /// + /// The MPD URL. + /// CloudFront policy token. + /// CloudFront signature token. + /// CloudFront key pair ID. + /// The last modified timestamp. public async Task GetDrmMpdLastModified(string mpdUrl, string policy, string signature, string kvp) { Log.Debug("Calling GetDrmMpdLastModified"); @@ -2974,21 +2590,19 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return DateTime.Now; } + /// + /// Retrieves a decryption key via the OFDL fallback service. + /// + /// The DRM request headers. + /// The license URL. + /// The PSSH payload. + /// The decryption key string. public async Task GetDecryptionKeyOfdl(Dictionary drmHeaders, string licenceUrl, string pssh) { @@ -3037,21 +2651,19 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return string.Empty; } + /// + /// Retrieves a decryption key using the local CDM integration. + /// + /// The DRM request headers. + /// The license URL. + /// The PSSH payload. + /// The decryption key string. public async Task GetDecryptionKeyCdm(Dictionary drmHeaders, string licenceUrl, string pssh) { @@ -3085,16 +2697,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return string.Empty; @@ -3173,6 +2776,23 @@ public class ApiService(IAuthService authService, IConfigService configService, : JsonConvert.DeserializeObject(body, settings); } + private bool IsMediaTypeDownloadEnabled(string? type) + { + if (string.IsNullOrWhiteSpace(type)) + { + return true; + } + + return type switch + { + "photo" => configService.CurrentConfig.DownloadImages, + "video" => configService.CurrentConfig.DownloadVideos, + "gif" => configService.CurrentConfig.DownloadVideos, + "audio" => configService.CurrentConfig.DownloadAudios, + _ => true + }; + } + private static string? ResolveMediaType(string? type) => type switch { @@ -3326,16 +2946,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return null; @@ -3366,16 +2977,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } 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); - } + ExceptionLoggerHelper.LogException(ex); } return null; diff --git a/OF DL.Core/Services/AuthService.cs b/OF DL.Core/Services/AuthService.cs index bf20e50..a8b725c 100644 --- a/OF DL.Core/Services/AuthService.cs +++ b/OF DL.Core/Services/AuthService.cs @@ -29,8 +29,16 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService UserDataDir = Path.GetFullPath("chrome-data") }; + /// + /// Gets or sets the current authentication state. + /// public Auth? CurrentAuth { get; set; } + /// + /// Loads authentication data from disk. + /// + /// The auth file path. + /// True when auth data is loaded successfully. public async Task LoadFromFileAsync(string filePath = "auth.json") { try @@ -53,6 +61,10 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService } } + /// + /// Launches a browser session and extracts auth data after login. + /// + /// True when auth data is captured successfully. public async Task LoadFromBrowserAsync() { try @@ -71,6 +83,10 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService } } + /// + /// Persists the current auth data to disk. + /// + /// The auth file path. public async Task SaveToFileAsync(string filePath = "auth.json") { if (CurrentAuth == null) @@ -129,6 +145,9 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService private async Task GetBcToken(IPage page) => await page.EvaluateExpressionAsync("window.localStorage.getItem('bcTokenSha') || ''"); + /// + /// Normalizes the stored cookie string to only include required cookie values. + /// public void ValidateCookieString() { if (CurrentAuth == null) @@ -159,6 +178,10 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService } } + /// + /// Validates auth by requesting the current user profile. + /// + /// The authenticated user or null when validation fails. public async Task ValidateAuthAsync() { // Resolve IApiService lazily to avoid circular dependency @@ -166,6 +189,9 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService return await apiService.GetUserInfo("/users/me"); } + /// + /// Clears persisted auth data and browser profile state. + /// public void Logout() { if (Directory.Exists("chrome-data")) diff --git a/OF DL.Core/Services/ConfigService.cs b/OF DL.Core/Services/ConfigService.cs index 6de7c00..f71586b 100644 --- a/OF DL.Core/Services/ConfigService.cs +++ b/OF DL.Core/Services/ConfigService.cs @@ -12,9 +12,21 @@ namespace OF_DL.Services; public class ConfigService(ILoggingService loggingService) : IConfigService { + /// + /// Gets the active configuration in memory. + /// public Config CurrentConfig { get; private set; } = new(); + + /// + /// Gets whether the CLI requested non-interactive mode. + /// public bool IsCliNonInteractive { get; private set; } + /// + /// Loads configuration from disk and applies runtime settings. + /// + /// CLI arguments used to influence configuration. + /// True when configuration is loaded successfully. public async Task LoadConfigurationAsync(string[] args) { try @@ -73,6 +85,10 @@ public class ConfigService(ILoggingService loggingService) : IConfigService } } + /// + /// Saves the current configuration to disk. + /// + /// The destination config file path. public async Task SaveConfigurationAsync(string filePath = "config.conf") { try @@ -87,6 +103,10 @@ public class ConfigService(ILoggingService loggingService) : IConfigService } } + /// + /// Replaces the current configuration and applies runtime settings. + /// + /// The new configuration instance. public void UpdateConfig(Config newConfig) { CurrentConfig = newConfig; @@ -399,6 +419,9 @@ public class ConfigService(ILoggingService loggingService) : IConfigService } } + /// + /// Returns toggleable config properties and their current values. + /// public List<(string Name, bool Value)> GetToggleableProperties() { List<(string Name, bool Value)> result = []; @@ -420,6 +443,11 @@ public class ConfigService(ILoggingService loggingService) : IConfigService return result; } + /// + /// Applies a set of toggle selections to the configuration. + /// + /// The names of toggles that should be enabled. + /// True when any values were changed. public bool ApplyToggleableSelections(List selectedNames) { bool configChanged = false; diff --git a/OF DL.Core/Services/DbService.cs b/OF DL.Core/Services/DbService.cs index eb3543e..99d51ef 100644 --- a/OF DL.Core/Services/DbService.cs +++ b/OF DL.Core/Services/DbService.cs @@ -1,11 +1,16 @@ using System.Text; using Microsoft.Data.Sqlite; +using OF_DL.Helpers; using Serilog; namespace OF_DL.Services; public class DbService(IConfigService configService) : IDbService { + /// + /// Creates or updates the per-user metadata database. + /// + /// The user folder path. public async Task CreateDb(string folder) { try @@ -131,19 +136,14 @@ public class DbService(IConfigService configService) : IDbService } 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); - } + ExceptionLoggerHelper.LogException(ex); } } + /// + /// Creates or updates the global users database. + /// + /// The users to seed or update. public async Task CreateUsersDb(Dictionary users) { try @@ -188,19 +188,15 @@ public class DbService(IConfigService configService) : IDbService } 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); - } + ExceptionLoggerHelper.LogException(ex); } } + /// + /// Ensures a username matches the stored user ID and migrates folders if needed. + /// + /// The user pair to validate. + /// The expected user folder path. public async Task CheckUsername(KeyValuePair user, string path) { try @@ -245,19 +241,21 @@ public class DbService(IConfigService configService) : IDbService } 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); - } + ExceptionLoggerHelper.LogException(ex); } } + /// + /// Inserts a message record when it does not already exist. + /// + /// The user folder path. + /// The message or post ID. + /// The message text. + /// The price string. + /// Whether the message is paid. + /// Whether the message is archived. + /// The creation timestamp. + /// The sender user ID. public async Task AddMessage(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, DateTime createdAt, long userId) { @@ -289,20 +287,21 @@ public class DbService(IConfigService configService) : IDbService } 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); - } + ExceptionLoggerHelper.LogException(ex); } } + /// + /// Inserts a post record when it does not already exist. + /// + /// The user folder path. + /// The post ID. + /// The post text. + /// The price string. + /// Whether the post is paid. + /// Whether the post is archived. + /// The creation timestamp. public async Task AddPost(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, DateTime createdAt) { @@ -333,20 +332,21 @@ public class DbService(IConfigService configService) : IDbService } 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); - } + ExceptionLoggerHelper.LogException(ex); } } + /// + /// Inserts a story record when it does not already exist. + /// + /// The user folder path. + /// The story ID. + /// The story text. + /// The price string. + /// Whether the story is paid. + /// Whether the story is archived. + /// The creation timestamp. public async Task AddStory(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, DateTime createdAt) { @@ -377,20 +377,26 @@ public class DbService(IConfigService configService) : IDbService } 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); - } + ExceptionLoggerHelper.LogException(ex); } } + /// + /// Inserts a media record when it does not already exist. + /// + /// The user folder path. + /// The media ID. + /// The parent post ID. + /// The media URL. + /// The local directory path. + /// The local filename. + /// The media size in bytes. + /// The API type label. + /// The media type label. + /// Whether the media is a preview. + /// Whether the media is downloaded. + /// The creation timestamp. public async Task AddMedia(string folder, long mediaId, long postId, string link, string? directory, string? filename, long? size, string apiType, string mediaType, bool preview, bool downloaded, DateTime? createdAt) @@ -421,20 +427,18 @@ public class DbService(IConfigService configService) : IDbService } 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); - } + ExceptionLoggerHelper.LogException(ex); } } + /// + /// Checks whether the media has been marked as downloaded. + /// + /// The user folder path. + /// The media ID. + /// The API type label. + /// True when the media is marked as downloaded. public async Task CheckDownloaded(string folder, long mediaId, string apiType) { try @@ -456,22 +460,24 @@ public class DbService(IConfigService configService) : IDbService } 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); - } + ExceptionLoggerHelper.LogException(ex); } return false; } + /// + /// Updates the media record with local file details. + /// + /// The user folder path. + /// The media ID. + /// The API type label. + /// The local directory path. + /// The local filename. + /// The file size in bytes. + /// Whether the media is downloaded. + /// The creation timestamp. public async Task UpdateMedia(string folder, long mediaId, string apiType, string directory, string filename, long size, bool downloaded, DateTime createdAt) { @@ -503,6 +509,13 @@ public class DbService(IConfigService configService) : IDbService } + /// + /// Returns the stored size for a media record. + /// + /// The user folder path. + /// The media ID. + /// The API type label. + /// The stored file size. public async Task GetStoredFileSize(string folder, long mediaId, string apiType) { await using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); @@ -516,6 +529,11 @@ public class DbService(IConfigService configService) : IDbService return size; } + /// + /// Returns the most recent post date based on downloaded and pending media. + /// + /// The user folder path. + /// The most recent post date if available. public async Task GetMostRecentPostDate(string folder) { DateTime? mostRecentDate = null; diff --git a/OF DL.Core/Services/DownloadOrchestrationService.cs b/OF DL.Core/Services/DownloadOrchestrationService.cs index f48fa3a..25497f5 100644 --- a/OF DL.Core/Services/DownloadOrchestrationService.cs +++ b/OF DL.Core/Services/DownloadOrchestrationService.cs @@ -15,8 +15,15 @@ public class DownloadOrchestrationService( IDownloadService downloadService, IDbService dbService) : IDownloadOrchestrationService { + /// + /// Gets the list of paid post media IDs to avoid duplicates. + /// public List PaidPostIds { get; } = new(); + /// + /// Retrieves the available users and lists based on current configuration. + /// + /// A result containing users, lists, and any errors. public async Task GetAvailableUsersAsync() { UserListResult result = new(); @@ -89,6 +96,13 @@ public class DownloadOrchestrationService( return result; } + /// + /// Resolves the users that belong to a specific list. + /// + /// The list name. + /// All available users. + /// Known lists keyed by name. + /// The users that belong to the list. public async Task> GetUsersForListAsync( string listName, Dictionary allUsers, Dictionary lists) { @@ -98,11 +112,22 @@ public class DownloadOrchestrationService( .ToDictionary(x => x.Key, x => x.Value); } + /// + /// Resolves the download path for a username based on configuration. + /// + /// The creator username. + /// The resolved download path. public string ResolveDownloadPath(string username) => !string.IsNullOrEmpty(configService.CurrentConfig.DownloadPath) ? Path.Combine(configService.CurrentConfig.DownloadPath, username) : $"__user_data__/sites/OnlyFans/{username}"; + /// + /// Ensures the user folder and metadata database exist. + /// + /// The creator username. + /// The creator user ID. + /// The creator folder path. public async Task PrepareUserFolderAsync(string username, long userId, string path) { await dbService.CheckUsername(new KeyValuePair(username, userId), path); @@ -116,6 +141,17 @@ public class DownloadOrchestrationService( await dbService.CreateDb(path); } + /// + /// Downloads all configured content types for a creator. + /// + /// The creator username. + /// The creator user ID. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// Download event handler. + /// Counts of downloaded items per content type. public async Task DownloadCreatorContentAsync( string username, long userId, string path, Dictionary users, @@ -281,6 +317,16 @@ public class DownloadOrchestrationService( return counts; } + /// + /// Downloads a single post by ID for a creator. + /// + /// The creator username. + /// The post ID. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// Download event handler. public async Task DownloadSinglePostAsync( string username, long postId, string path, Dictionary users, @@ -320,6 +366,13 @@ public class DownloadOrchestrationService( } } + /// + /// Downloads content from the Purchased tab across creators. + /// + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// Download event handler. public async Task DownloadPurchasedTabAsync( Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, @@ -427,6 +480,16 @@ public class DownloadOrchestrationService( } } + /// + /// Downloads a single paid message by ID. + /// + /// The creator username. + /// The message ID. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// Download event handler. public async Task DownloadSinglePaidMessageAsync( string username, long messageId, string path, Dictionary users, @@ -493,6 +556,11 @@ public class DownloadOrchestrationService( } } + /// + /// Resolves a username for a user ID, including deleted users. + /// + /// The user ID. + /// The resolved username or a deleted user placeholder. public async Task ResolveUsernameAsync(long userId) { JObject? user = await apiService.GetUserInfoById($"/users/list?x[]={userId}"); diff --git a/OF DL.Core/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs index e071d24..e1dd837 100644 --- a/OF DL.Core/Services/DownloadService.cs +++ b/OF DL.Core/Services/DownloadService.cs @@ -4,6 +4,7 @@ using FFmpeg.NET; using FFmpeg.NET.Events; using OF_DL.Models; using OF_DL.Enumerations; +using OF_DL.Helpers; using OF_DL.Models.Downloads; using ArchivedEntities = OF_DL.Models.Entities.Archived; using MessageEntities = OF_DL.Models.Entities.Messages; @@ -26,6 +27,13 @@ public class DownloadService( { private TaskCompletionSource _completionSource = new(); + /// + /// Downloads profile avatar and header images for a creator. + /// + /// The avatar URL. + /// The header URL. + /// The creator folder path. + /// The creator username. public async Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username) { try @@ -189,16 +197,7 @@ public class DownloadService( } 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); - } + ExceptionLoggerHelper.LogException(ex); } return false; @@ -223,7 +222,7 @@ public class DownloadService( long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : tempFilename).Length; - progressReporter.ReportProgress(configService.CurrentConfig.ShowScrapeSize ? fileSizeInBytes : 1); + ReportProgress(progressReporter, fileSizeInBytes); await dbService.UpdateMedia(folder, mediaId, apiType, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4", @@ -231,16 +230,7 @@ public class DownloadService( } 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); - } + ExceptionLoggerHelper.LogException(ex); } } @@ -488,6 +478,12 @@ public class DownloadService( return fileSize; } + /// + /// Retrieves the last modified timestamp for a DRM media URL. + /// + /// The DRM media URL (including CloudFront tokens). + /// The current auth context. + /// The last modified timestamp if available. public static async Task GetDrmVideoLastModified(string url, Auth auth) { string[] messageUrlParsed = url.Split(','); @@ -515,6 +511,11 @@ public class DownloadService( return DateTime.Now; } + /// + /// Retrieves the last modified timestamp for a media URL. + /// + /// The media URL. + /// The last modified timestamp if available. public static async Task GetMediaLastModified(string url) { using HttpClient client = new(); @@ -681,7 +682,7 @@ public class DownloadService( fileSizeInBytes = GetLocalFileSize(finalPath); lastModified = File.GetLastWriteTime(finalPath); - progressReporter.ReportProgress(configService.CurrentConfig.ShowScrapeSize ? fileSizeInBytes : 1); + ReportProgress(progressReporter, fileSizeInBytes); status = false; } @@ -692,7 +693,7 @@ public class DownloadService( { fileSizeInBytes = GetLocalFileSize(fullPathWithTheNewFileName); lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName); - progressReporter.ReportProgress(configService.CurrentConfig.ShowScrapeSize ? fileSizeInBytes : 1); + ReportProgress(progressReporter, fileSizeInBytes); status = false; } @@ -723,15 +724,10 @@ public class DownloadService( private async Task HandlePreviouslyDownloadedMediaAsync(string folder, long mediaId, string apiType, IProgressReporter progressReporter) { - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, mediaId, apiType); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } + long size = configService.CurrentConfig.ShowScrapeSize + ? await dbService.GetStoredFileSize(folder, mediaId, apiType) + : 1; + ReportProgress(progressReporter, size); return false; } @@ -791,6 +787,11 @@ public class DownloadService( return response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now; } + /// + /// Calculates the total size of a set of URLs by fetching their metadata. + /// + /// The media URLs. + /// The total size in bytes. public async Task CalculateTotalFileSize(List urls) { long totalFileSize = 0; @@ -826,6 +827,21 @@ public class DownloadService( return totalFileSize; } + /// + /// Downloads a single media item, applying filename formatting and folder rules. + /// + /// The media URL. + /// The creator folder path. + /// The media ID. + /// The API type label. + /// Progress reporter. + /// The relative folder path. + /// Optional filename format. + /// Post or message info. + /// Media info. + /// Author info. + /// Known users map. + /// True when the media is newly downloaded. public async Task DownloadMedia(string url, string folder, long mediaId, string apiType, IProgressReporter progressReporter, string path, string? filenameFormat, object? postInfo, object? postMedia, @@ -840,6 +856,26 @@ public class DownloadService( filename, resolvedFilename); } + /// + /// Downloads a DRM-protected video using the provided decryption key. + /// + /// CloudFront policy token. + /// CloudFront signature token. + /// CloudFront key pair ID. + /// The MPD URL. + /// The decryption key. + /// The creator folder path. + /// The source last modified timestamp. + /// The media ID. + /// The API type label. + /// Progress reporter. + /// The relative folder path. + /// Optional filename format. + /// Post or message info. + /// Media info. + /// Author info. + /// Known users map. + /// True when the media is newly downloaded. public async Task DownloadDrmVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long mediaId, string apiType, IProgressReporter progressReporter, string path, @@ -918,37 +954,23 @@ public class DownloadService( return false; } - long size = await dbService.GetStoredFileSize(folder, mediaId, apiType); + long storedFileSize = await dbService.GetStoredFileSize(folder, mediaId, apiType); await dbService.UpdateMedia(folder, mediaId, apiType, folder + path, customFileName + ".mp4", - size, true, lastModified); + storedFileSize, true, lastModified); } } - if (configService.CurrentConfig.ShowScrapeSize) - { - long size = await dbService.GetStoredFileSize(folder, mediaId, apiType); - progressReporter.ReportProgress(size); - } - else - { - progressReporter.ReportProgress(1); - } + long progressSize = configService.CurrentConfig.ShowScrapeSize + ? await dbService.GetStoredFileSize(folder, mediaId, apiType) + : 1; + ReportProgress(progressReporter, progressSize); } return false; } 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); - } + ExceptionLoggerHelper.LogException(ex); } return false; @@ -957,6 +979,19 @@ public class DownloadService( private void ReportProgress(IProgressReporter reporter, long sizeOrCount) => reporter.ReportProgress(configService.CurrentConfig.ShowScrapeSize ? sizeOrCount : 1); + /// + /// Retrieves decryption information for a DRM media item. + /// + /// The MPD URL. + /// CloudFront policy token. + /// CloudFront signature token. + /// CloudFront key pair ID. + /// The media ID. + /// The content ID. + /// The DRM type. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The decryption key and last modified timestamp. public async Task<(string decryptionKey, DateTime lastModified)?> GetDecryptionInfo( string mpdUrl, string policy, string signature, string kvp, string mediaId, string contentId, string drmType, @@ -978,6 +1013,15 @@ public class DownloadService( return (decryptionKey, lastModified); } + /// + /// Downloads highlight media for a creator. + /// + /// The creator username. + /// The creator user ID. + /// The creator folder path. + /// Paid post media IDs. + /// Progress reporter. + /// The download result. public async Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter) { @@ -1032,6 +1076,15 @@ public class DownloadService( }; } + /// + /// Downloads story media for a creator. + /// + /// The creator username. + /// The creator user ID. + /// The creator folder path. + /// Paid post media IDs. + /// Progress reporter. + /// The download result. public async Task DownloadStories(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter) { @@ -1084,6 +1137,18 @@ public class DownloadService( }; } + /// + /// Downloads archived posts for a creator. + /// + /// The creator username. + /// The creator user ID. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The archived posts collection. + /// Progress reporter. + /// The download result. public async Task DownloadArchived(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedEntities.ArchivedCollection archived, IProgressReporter progressReporter) @@ -1165,6 +1230,18 @@ public class DownloadService( }; } + /// + /// Downloads free messages for a creator. + /// + /// The creator username. + /// The creator user ID. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The messages collection. + /// Progress reporter. + /// The download result. public async Task DownloadMessages(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageEntities.MessageCollection messages, IProgressReporter progressReporter) @@ -1247,6 +1324,17 @@ public class DownloadService( } + /// + /// Downloads paid messages for a creator. + /// + /// The creator username. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The paid message collection. + /// Progress reporter. + /// The download result. public async Task DownloadPaidMessages(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidMessageCollection paidMessageCollection, @@ -1330,6 +1418,18 @@ public class DownloadService( }; } + /// + /// Downloads stream posts for a creator. + /// + /// The creator username. + /// The creator user ID. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The streams collection. + /// Progress reporter. + /// The download result. public async Task DownloadStreams(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamEntities.StreamsCollection streams, IProgressReporter progressReporter) @@ -1410,6 +1510,18 @@ public class DownloadService( }; } + /// + /// Downloads free posts for a creator. + /// + /// The creator username. + /// The creator user ID. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The posts collection. + /// Progress reporter. + /// The download result. public async Task DownloadFreePosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostEntities.PostCollection posts, @@ -1490,6 +1602,18 @@ public class DownloadService( }; } + /// + /// Downloads paid posts for a creator. + /// + /// The creator username. + /// The creator user ID. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The paid post collection. + /// Progress reporter. + /// The download result. public async Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter) @@ -1571,6 +1695,17 @@ public class DownloadService( }; } + /// + /// Downloads paid posts sourced from the Purchased tab. + /// + /// The creator username. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The paid post collection. + /// Progress reporter. + /// The download result. public async Task DownloadPaidPostsPurchasedTab(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter) @@ -1645,6 +1780,17 @@ public class DownloadService( }; } + /// + /// Downloads paid messages sourced from the Purchased tab. + /// + /// The creator username. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The paid message collection. + /// Progress reporter. + /// The download result. public async Task DownloadPaidMessagesPurchasedTab(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter) @@ -1718,6 +1864,17 @@ public class DownloadService( }; } + /// + /// Downloads a single post collection. + /// + /// The creator username. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The single post collection. + /// Progress reporter. + /// The download result. public async Task DownloadSinglePost(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostEntities.SinglePostCollection post, IProgressReporter progressReporter) @@ -1796,6 +1953,17 @@ public class DownloadService( }; } + /// + /// Downloads a single paid message collection (including previews). + /// + /// The creator username. + /// The creator folder path. + /// Known users map. + /// Whether the CDM client ID blob is missing. + /// Whether the CDM private key is missing. + /// The single paid message collection. + /// Progress reporter. + /// The download result. public async Task DownloadSinglePaidMessage(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection, diff --git a/OF DL.Core/Services/FileNameService.cs b/OF DL.Core/Services/FileNameService.cs index 0f533bd..92414e8 100644 --- a/OF DL.Core/Services/FileNameService.cs +++ b/OF DL.Core/Services/FileNameService.cs @@ -5,6 +5,16 @@ namespace OF_DL.Services; public class FileNameService(IAuthService authService) : IFileNameService { + /// + /// Builds a map of filename token values from post, media, and author data. + /// + /// The post or message object. + /// The media object. + /// The author object. + /// The tokens requested by the filename format. + /// The resolved username when available. + /// Optional lookup of user IDs to usernames. + /// A dictionary of token values keyed by token name. public async Task> GetFilename(object info, object media, object author, List selectedProperties, string username, Dictionary? users = null) { @@ -168,6 +178,12 @@ public class FileNameService(IAuthService authService) : IFileNameService return values; } + /// + /// Applies token values to a filename format and removes invalid file name characters. + /// + /// The filename format string. + /// Token values to substitute. + /// The resolved filename. public Task BuildFilename(string fileFormat, Dictionary values) { foreach (KeyValuePair kvp in values) diff --git a/OF DL.Core/Services/IApiService.cs b/OF DL.Core/Services/IApiService.cs index 0ff8a39..0359153 100644 --- a/OF DL.Core/Services/IApiService.cs +++ b/OF DL.Core/Services/IApiService.cs @@ -11,55 +11,121 @@ namespace OF_DL.Services; public interface IApiService { + /// + /// Retrieves a decryption key using the local CDM integration. + /// Task GetDecryptionKeyCdm(Dictionary drmHeaders, string licenceUrl, string pssh); + /// + /// Retrieves the last modified timestamp for a DRM MPD manifest. + /// Task GetDrmMpdLastModified(string mpdUrl, string policy, string signature, string kvp); + /// + /// Retrieves the Widevine PSSH from an MPD manifest. + /// Task GetDrmMpdPssh(string mpdUrl, string policy, string signature, string kvp); + /// + /// Retrieves the user's lists. + /// Task?> GetLists(string endpoint); + /// + /// Retrieves usernames for a specific list. + /// Task?> GetListUsers(string endpoint); + /// + /// Retrieves media URLs for stories or highlights. + /// Task?> GetMedia(MediaType mediaType, string endpoint, string? username, string folder, List paidPostIds); + /// + /// Retrieves paid posts and their media. + /// Task GetPaidPosts(string endpoint, string folder, string username, List paidPostIds, IStatusReporter statusReporter); + /// + /// Retrieves posts and their media. + /// Task GetPosts(string endpoint, string folder, List paidPostIds, IStatusReporter statusReporter); + /// + /// Retrieves a single post and its media. + /// Task GetPost(string endpoint, string folder); + /// + /// Retrieves streams and their media. + /// Task GetStreams(string endpoint, string folder, List paidPostIds, IStatusReporter statusReporter); + /// + /// Retrieves archived posts and their media. + /// Task GetArchived(string endpoint, string folder, IStatusReporter statusReporter); + /// + /// Retrieves messages and their media. + /// Task GetMessages(string endpoint, string folder, IStatusReporter statusReporter); + /// + /// Retrieves paid messages and their media. + /// Task GetPaidMessages(string endpoint, string folder, string username, IStatusReporter statusReporter); + /// + /// Retrieves a single paid message and its media. + /// Task GetPaidMessage(string endpoint, string folder); + /// + /// Retrieves users that appear in the Purchased tab. + /// Task> GetPurchasedTabUsers(string endpoint, Dictionary users); + /// + /// Retrieves Purchased tab content grouped by user. + /// Task> GetPurchasedTab(string endpoint, string folder, Dictionary users); + /// + /// Retrieves user information. + /// Task GetUserInfo(string endpoint); + /// + /// Retrieves user information by ID. + /// Task GetUserInfoById(string endpoint); + /// + /// Builds signed headers for API requests. + /// Dictionary GetDynamicHeaders(string path, string queryParam); + /// + /// Retrieves active subscriptions. + /// Task?> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions); + /// + /// Retrieves expired subscriptions. + /// Task?> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions); + /// + /// Retrieves a decryption key via the OFDL fallback service. + /// Task GetDecryptionKeyOfdl(Dictionary drmHeaders, string licenceUrl, string pssh); } diff --git a/OF DL.Core/Services/IAuthService.cs b/OF DL.Core/Services/IAuthService.cs index c990bfd..4ebe1da 100644 --- a/OF DL.Core/Services/IAuthService.cs +++ b/OF DL.Core/Services/IAuthService.cs @@ -5,12 +5,24 @@ namespace OF_DL.Services; public interface IAuthService { + /// + /// Gets or sets the current authentication state. + /// Auth? CurrentAuth { get; set; } + /// + /// Loads authentication data from disk. + /// Task LoadFromFileAsync(string filePath = "auth.json"); + /// + /// Launches a browser session and extracts auth data after login. + /// Task LoadFromBrowserAsync(); + /// + /// Persists the current auth data to disk. + /// Task SaveToFileAsync(string filePath = "auth.json"); /// diff --git a/OF DL.Core/Services/IConfigService.cs b/OF DL.Core/Services/IConfigService.cs index 088e3fd..27c767d 100644 --- a/OF DL.Core/Services/IConfigService.cs +++ b/OF DL.Core/Services/IConfigService.cs @@ -4,14 +4,29 @@ namespace OF_DL.Services; public interface IConfigService { + /// + /// Gets the active configuration in memory. + /// Config CurrentConfig { get; } + /// + /// Gets whether the CLI requested non-interactive mode. + /// bool IsCliNonInteractive { get; } + /// + /// Loads configuration from disk and applies runtime settings. + /// Task LoadConfigurationAsync(string[] args); + /// + /// Saves the current configuration to disk. + /// Task SaveConfigurationAsync(string filePath = "config.conf"); + /// + /// Replaces the current configuration and applies runtime settings. + /// void UpdateConfig(Config newConfig); /// diff --git a/OF DL.Core/Services/IDbService.cs b/OF DL.Core/Services/IDbService.cs index c7b05f4..90ba140 100644 --- a/OF DL.Core/Services/IDbService.cs +++ b/OF DL.Core/Services/IDbService.cs @@ -2,30 +2,63 @@ namespace OF_DL.Services; public interface IDbService { + /// + /// Inserts a message record when it does not already exist. + /// Task AddMessage(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, DateTime createdAt, long userId); + /// + /// Inserts a post record when it does not already exist. + /// Task AddPost(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, DateTime createdAt); + /// + /// Inserts a story record when it does not already exist. + /// Task AddStory(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, DateTime createdAt); + /// + /// Creates or updates the per-user metadata database. + /// Task CreateDb(string folder); + /// + /// Creates or updates the global users database. + /// Task CreateUsersDb(Dictionary users); + /// + /// Ensures a username matches the stored user ID and migrates folders if needed. + /// Task CheckUsername(KeyValuePair user, string path); + /// + /// Inserts a media record when it does not already exist. + /// Task AddMedia(string folder, long mediaId, long postId, string link, string? directory, string? filename, long? size, string apiType, string mediaType, bool preview, bool downloaded, DateTime? createdAt); + /// + /// Updates the media record with local file details. + /// Task UpdateMedia(string folder, long mediaId, string apiType, string directory, string filename, long size, bool downloaded, DateTime createdAt); + /// + /// Returns the stored size for a media record. + /// Task GetStoredFileSize(string folder, long mediaId, string apiType); + /// + /// Checks whether the media has been marked as downloaded. + /// Task CheckDownloaded(string folder, long mediaId, string apiType); + /// + /// Returns the most recent post date based on downloaded and pending media. + /// Task GetMostRecentPostDate(string folder); } diff --git a/OF DL.Core/Services/IDownloadService.cs b/OF DL.Core/Services/IDownloadService.cs index b2a6e26..f41b42f 100644 --- a/OF DL.Core/Services/IDownloadService.cs +++ b/OF DL.Core/Services/IDownloadService.cs @@ -9,72 +9,126 @@ namespace OF_DL.Services; public interface IDownloadService { + /// + /// Calculates the total size of a set of URLs by fetching their metadata. + /// Task CalculateTotalFileSize(List urls); + /// + /// Downloads media and updates metadata storage. + /// Task ProcessMediaDownload(string folder, long mediaId, string apiType, string url, string path, string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); + /// + /// Downloads a single media item. + /// Task DownloadMedia(string url, string folder, long mediaId, string apiType, IProgressReporter progressReporter, string path, string? filenameFormat, object? postInfo, object? postMedia, object? author, Dictionary users); + /// + /// Downloads a DRM-protected video. + /// Task DownloadDrmVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long mediaId, string apiType, IProgressReporter progressReporter, string path, string? filenameFormat, object? postInfo, object? postMedia, object? author, Dictionary users); + /// + /// Retrieves decryption information for a DRM media item. + /// Task<(string decryptionKey, DateTime lastModified)?> GetDecryptionInfo( string mpdUrl, string policy, string signature, string kvp, string mediaId, string contentId, string drmType, bool clientIdBlobMissing, bool devicePrivateKeyMissing); + /// + /// Downloads profile avatar and header images for a creator. + /// Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username); + /// + /// Downloads highlight media for a creator. + /// Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); + /// + /// Downloads story media for a creator. + /// Task DownloadStories(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); + /// + /// Downloads archived posts for a creator. + /// Task DownloadArchived(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedEntities.ArchivedCollection archived, IProgressReporter progressReporter); + /// + /// Downloads free messages for a creator. + /// Task DownloadMessages(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageEntities.MessageCollection messages, IProgressReporter progressReporter); + /// + /// Downloads paid messages for a creator. + /// Task DownloadPaidMessages(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter); + /// + /// Downloads stream posts for a creator. + /// Task DownloadStreams(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamEntities.StreamsCollection streams, IProgressReporter progressReporter); + /// + /// Downloads free posts for a creator. + /// Task DownloadFreePosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostEntities.PostCollection posts, IProgressReporter progressReporter); + /// + /// Downloads paid posts for a creator. + /// Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter); + /// + /// Downloads paid posts sourced from the Purchased tab. + /// Task DownloadPaidPostsPurchasedTab(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter); + /// + /// Downloads paid messages sourced from the Purchased tab. + /// Task DownloadPaidMessagesPurchasedTab(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter); + /// + /// Downloads a single post collection. + /// Task DownloadSinglePost(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostEntities.SinglePostCollection post, IProgressReporter progressReporter); + /// + /// Downloads a single paid message collection. + /// Task DownloadSinglePaidMessage(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection, diff --git a/OF DL.Core/Services/IFileNameService.cs b/OF DL.Core/Services/IFileNameService.cs index 35d5c2f..51c97b1 100644 --- a/OF DL.Core/Services/IFileNameService.cs +++ b/OF DL.Core/Services/IFileNameService.cs @@ -2,8 +2,14 @@ namespace OF_DL.Services; public interface IFileNameService { + /// + /// Applies token values to a filename format and removes invalid characters. + /// Task BuildFilename(string fileFormat, Dictionary values); + /// + /// Builds a map of filename token values from post, media, and author data. + /// Task> GetFilename(object info, object media, object author, List selectedProperties, string username, Dictionary? users = null); diff --git a/OF DL.Core/Services/ILoggingService.cs b/OF DL.Core/Services/ILoggingService.cs index 787ca92..69f5277 100644 --- a/OF DL.Core/Services/ILoggingService.cs +++ b/OF DL.Core/Services/ILoggingService.cs @@ -5,9 +5,18 @@ namespace OF_DL.Services; public interface ILoggingService { + /// + /// Gets the level switch that controls runtime logging verbosity. + /// LoggingLevelSwitch LevelSwitch { get; } + /// + /// Updates the minimum logging level at runtime. + /// void UpdateLoggingLevel(LoggingLevel newLevel); + /// + /// Returns the current minimum logging level. + /// LoggingLevel GetCurrentLoggingLevel(); } diff --git a/OF DL.Core/Services/IStartupService.cs b/OF DL.Core/Services/IStartupService.cs index 3b54466..12bd881 100644 --- a/OF DL.Core/Services/IStartupService.cs +++ b/OF DL.Core/Services/IStartupService.cs @@ -4,7 +4,13 @@ namespace OF_DL.Services; public interface IStartupService { + /// + /// Validates the runtime environment and returns a structured result. + /// Task ValidateEnvironmentAsync(); + /// + /// Checks the current application version against the latest release tag. + /// Task CheckVersionAsync(); } diff --git a/OF DL.Core/Services/LoggingService.cs b/OF DL.Core/Services/LoggingService.cs index b4c47fa..a0b11b4 100644 --- a/OF DL.Core/Services/LoggingService.cs +++ b/OF DL.Core/Services/LoggingService.cs @@ -13,14 +13,24 @@ public class LoggingService : ILoggingService InitializeLogger(); } + /// + /// Gets the level switch that controls runtime logging verbosity. + /// public LoggingLevelSwitch LevelSwitch { get; } + /// + /// Updates the minimum logging level at runtime. + /// + /// The new minimum log level. public void UpdateLoggingLevel(LoggingLevel newLevel) { LevelSwitch.MinimumLevel = (LogEventLevel)newLevel; Log.Debug("Logging level updated to: {LoggingLevel}", newLevel); } + /// + /// Returns the current minimum logging level. + /// public LoggingLevel GetCurrentLoggingLevel() => (LoggingLevel)LevelSwitch.MinimumLevel; private void InitializeLogger() diff --git a/OF DL.Core/Services/StartupService.cs b/OF DL.Core/Services/StartupService.cs index ff96321..ae0e856 100644 --- a/OF DL.Core/Services/StartupService.cs +++ b/OF DL.Core/Services/StartupService.cs @@ -13,6 +13,10 @@ 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(); @@ -78,6 +82,10 @@ public class StartupService(IConfigService configService, IAuthService authServi 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(); From 70738fd4ae32c51197889a36dd2aa14004aac56e Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 11:18:42 -0600 Subject: [PATCH 41/55] Add unit tests for the mappers --- .../Models/Mappers/ArchivedMapperTests.cs | 80 ++++++++ .../Models/Mappers/CommonMapperTests.cs | 173 ++++++++++++++++++ .../Models/Mappers/HighlightsMapperTests.cs | 75 ++++++++ .../Models/Mappers/MessagesMapperTests.cs | 113 ++++++++++++ OF DL.Tests/Models/Mappers/PostMapperTests.cs | 136 ++++++++++++++ .../Models/Mappers/PurchasedMapperTests.cs | 77 ++++++++ .../Models/Mappers/StoriesMapperTests.cs | 58 ++++++ .../Models/Mappers/StreamsMapperTests.cs | 91 +++++++++ .../Mappers/SubscriptionsMapperTests.cs | 34 ++++ .../Models/Mappers/UserListsMapperTests.cs | 49 +++++ OF DL.Tests/Models/Mappers/UserMapperTests.cs | 25 +++ OF DL.Tests/OF DL.Tests.csproj | 26 +++ OF DL.sln | 6 + 13 files changed, 943 insertions(+) create mode 100644 OF DL.Tests/Models/Mappers/ArchivedMapperTests.cs create mode 100644 OF DL.Tests/Models/Mappers/CommonMapperTests.cs create mode 100644 OF DL.Tests/Models/Mappers/HighlightsMapperTests.cs create mode 100644 OF DL.Tests/Models/Mappers/MessagesMapperTests.cs create mode 100644 OF DL.Tests/Models/Mappers/PostMapperTests.cs create mode 100644 OF DL.Tests/Models/Mappers/PurchasedMapperTests.cs create mode 100644 OF DL.Tests/Models/Mappers/StoriesMapperTests.cs create mode 100644 OF DL.Tests/Models/Mappers/StreamsMapperTests.cs create mode 100644 OF DL.Tests/Models/Mappers/SubscriptionsMapperTests.cs create mode 100644 OF DL.Tests/Models/Mappers/UserListsMapperTests.cs create mode 100644 OF DL.Tests/Models/Mappers/UserMapperTests.cs create mode 100644 OF DL.Tests/OF DL.Tests.csproj diff --git a/OF DL.Tests/Models/Mappers/ArchivedMapperTests.cs b/OF DL.Tests/Models/Mappers/ArchivedMapperTests.cs new file mode 100644 index 0000000..ea32519 --- /dev/null +++ b/OF DL.Tests/Models/Mappers/ArchivedMapperTests.cs @@ -0,0 +1,80 @@ +using OF_DL.Models.Dtos.Archived; +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Entities.Archived; +using OF_DL.Models.Mappers; + +namespace OF_DL.Tests.Models.Mappers; + +public class ArchivedMapperTests +{ + [Fact] + public void FromDto_ReturnsDefaults_WhenDtoNull() + { + Archived result = ArchivedMapper.FromDto(null); + + Assert.False(result.HasMore); + Assert.Null(result.TailMarker); + Assert.Empty(result.List); + } + + [Fact] + public void FromDto_MapsListItems() + { + DateTime postedAt = new(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc); + + ArchivedDto dto = new() + { + HasMore = true, + TailMarker = "tail", + List = + [ + new ListItemDto + { + Id = 123, + PostedAt = postedAt, + Author = new AuthorDto { Id = 7 }, + Text = "hello", + Price = "9.99", + IsOpened = true, + IsArchived = true, + Preview = ["preview"], + Media = + [ + new MediumDto + { + Id = 456, + Type = "photo", + CanView = true, + Files = new FilesDto { Full = new FullDto { Url = "https://example.com/full.jpg" } } + } + ] + } + ] + }; + + Archived result = ArchivedMapper.FromDto(dto); + + Assert.True(result.HasMore); + Assert.Equal("tail", result.TailMarker); + Assert.Single(result.List); + + ListItem item = result.List[0]; + Assert.Equal(123, item.Id); + Assert.Equal(postedAt, item.PostedAt); + Assert.NotNull(item.Author); + Assert.Equal(7, item.Author.Id); + Assert.Equal("hello", item.Text); + Assert.Equal("9.99", item.Price); + Assert.True(item.IsOpened); + Assert.True(item.IsArchived); + Assert.NotNull(item.Media); + Assert.Single(item.Media); + + Medium media = item.Media[0]; + Assert.Equal(456, media.Id); + Assert.Equal("photo", media.Type); + Assert.NotNull(media.Files); + Assert.NotNull(media.Files.Full); + Assert.Equal("https://example.com/full.jpg", media.Files.Full.Url); + } +} diff --git a/OF DL.Tests/Models/Mappers/CommonMapperTests.cs b/OF DL.Tests/Models/Mappers/CommonMapperTests.cs new file mode 100644 index 0000000..f3e9da0 --- /dev/null +++ b/OF DL.Tests/Models/Mappers/CommonMapperTests.cs @@ -0,0 +1,173 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Entities.Common; +using OF_DL.Models.Mappers; + +namespace OF_DL.Tests.Models.Mappers; + +public class CommonMapperTests +{ + [Fact] + public void MapAuthor_ReturnsNull_WhenDtoNull() => Assert.Null(CommonMapper.MapAuthor(null)); + + [Fact] + public void MapAuthor_ReturnsNull_WhenIdZero() + { + AuthorDto dto = new() { Id = 0 }; + + Assert.Null(CommonMapper.MapAuthor(dto)); + } + + [Fact] + public void MapAuthor_MapsId_WhenValid() + { + AuthorDto dto = new() { Id = 42 }; + + Author? result = CommonMapper.MapAuthor(dto); + + Assert.NotNull(result); + Assert.Equal(42, result.Id); + } + + [Fact] + public void MapFull_ReturnsNull_WhenUrlEmpty() + { + FullDto dto = new() { Url = "" }; + + Assert.Null(CommonMapper.MapFull(dto)); + } + + [Fact] + public void MapFull_MapsUrl_WhenPresent() + { + FullDto dto = new() { Url = "https://example.com/full.jpg" }; + + Full? result = CommonMapper.MapFull(dto); + + Assert.NotNull(result); + Assert.Equal(dto.Url, result.Url); + } + + [Fact] + public void MapPreview_ReturnsNull_WhenUrlEmpty() + { + PreviewDto dto = new() { Url = "" }; + + Assert.Null(CommonMapper.MapPreview(dto)); + } + + [Fact] + public void MapPreview_MapsUrl_WhenPresent() + { + PreviewDto dto = new() { Url = "https://example.com/preview.jpg" }; + + Preview? result = CommonMapper.MapPreview(dto); + + Assert.NotNull(result); + Assert.Equal(dto.Url, result.Url); + } + + [Fact] + public void MapManifest_ReturnsNull_WhenDashEmpty() + { + ManifestDto dto = new() { Dash = "" }; + + Assert.Null(CommonMapper.MapManifest(dto)); + } + + [Fact] + public void MapManifest_MapsDash_WhenPresent() + { + ManifestDto dto = new() { Dash = "dash.mpd" }; + + Manifest? result = CommonMapper.MapManifest(dto); + + Assert.NotNull(result); + Assert.Equal(dto.Dash, result.Dash); + } + + [Fact] + public void MapSignature_ReturnsNull_WhenDashEmpty() + { + SignatureDto dto = new() { Dash = new DashDto() }; + + Assert.Null(CommonMapper.MapSignature(dto)); + } + + [Fact] + public void MapSignature_MapsDash_WhenAnyDashFieldPresent() + { + SignatureDto dto = new() { Dash = new DashDto { CloudFrontPolicy = "policy" } }; + + Signature? result = CommonMapper.MapSignature(dto); + + Assert.NotNull(result); + Assert.NotNull(result.Dash); + Assert.Equal("policy", result.Dash.CloudFrontPolicy); + } + + [Fact] + public void MapDrm_ReturnsNull_WhenDtoNull() => Assert.Null(CommonMapper.MapDrm(null)); + + [Fact] + public void MapDrm_MapsManifestAndSignature_WhenPresent() + { + DrmDto dto = new() + { + Manifest = new ManifestDto { Dash = "dash.mpd" }, + Signature = new SignatureDto { Dash = new DashDto { CloudFrontSignature = "signature" } } + }; + + Drm? result = CommonMapper.MapDrm(dto); + + Assert.NotNull(result); + Assert.NotNull(result.Manifest); + Assert.NotNull(result.Signature); + Assert.Equal("dash.mpd", result.Manifest.Dash); + Assert.Equal("signature", result.Signature.Dash?.CloudFrontSignature); + } + + [Fact] + public void MapFiles_ReturnsNull_WhenAllPartsNull() + { + FilesDto dto = new() { Full = { Url = "" }, Preview = { Url = "" }, Drm = null }; + + Assert.Null(CommonMapper.MapFiles(dto)); + } + + [Fact] + public void MapFiles_MapsParts_WhenPresent() + { + FilesDto dto = new() + { + Full = { Url = "https://example.com/full.jpg" }, Preview = { Url = "https://example.com/preview.jpg" } + }; + + Files? result = CommonMapper.MapFiles(dto); + + Assert.NotNull(result); + Assert.NotNull(result.Full); + Assert.NotNull(result.Preview); + Assert.Equal(dto.Full.Url, result.Full.Url); + Assert.Equal(dto.Preview.Url, result.Preview.Url); + } + + [Fact] + public void MapVideoSources_ReturnsNull_WhenAllEmpty() + { + VideoSourcesDto dto = new() { _240 = "", _720 = "" }; + + Assert.Null(CommonMapper.MapVideoSources(dto)); + } + + [Fact] + public void MapVideoSources_MapsFields_WhenPresent() + { + VideoSourcesDto dto = new() { _240 = "240.mp4", _720 = "720.mp4" }; + + VideoSources? result = CommonMapper.MapVideoSources(dto); + + Assert.NotNull(result); + Assert.Equal("240.mp4", result._240); + Assert.Equal("720.mp4", result._720); + } +} diff --git a/OF DL.Tests/Models/Mappers/HighlightsMapperTests.cs b/OF DL.Tests/Models/Mappers/HighlightsMapperTests.cs new file mode 100644 index 0000000..2b577b9 --- /dev/null +++ b/OF DL.Tests/Models/Mappers/HighlightsMapperTests.cs @@ -0,0 +1,75 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Highlights; +using OF_DL.Models.Entities.Highlights; +using OF_DL.Models.Mappers; + +namespace OF_DL.Tests.Models.Mappers; + +public class HighlightsMapperTests +{ + [Fact] + public void FromDto_ReturnsDefaults_WhenDtoNull() + { + Highlights result = HighlightsMapper.FromDto((HighlightsDto?)null); + + Assert.False(result.HasMore); + Assert.Empty(result.List); + } + + [Fact] + public void FromDto_MapsListItems() + { + HighlightsDto dto = new() { HasMore = true, List = [new ListItemDto { Id = 99 }] }; + + Highlights result = HighlightsMapper.FromDto(dto); + + Assert.True(result.HasMore); + Assert.Single(result.List); + Assert.Equal(99, result.List[0].Id); + } + + [Fact] + public void FromDto_MapsHighlightMediaStories() + { + DateTime createdAt = new(2024, 2, 3, 4, 5, 6, DateTimeKind.Utc); + + HighlightMediaDto dto = new() + { + Stories = + [ + new StoryDto + { + Id = 5, + CreatedAt = createdAt, + Media = + [ + new MediumDto + { + Id = 8, + Type = "video", + CanView = true, + CreatedAt = createdAt, + Files = new FilesDto { Full = new FullDto { Url = "https://example.com/full.mp4" } } + } + ] + } + ] + }; + + HighlightMedia result = HighlightsMapper.FromDto(dto); + + Assert.Single(result.Stories); + + Story story = result.Stories[0]; + Assert.Equal(5, story.Id); + Assert.Equal(createdAt, story.CreatedAt); + Assert.NotNull(story.Media); + Assert.Single(story.Media); + + Medium media = story.Media[0]; + Assert.Equal(8, media.Id); + Assert.NotNull(media.Files); + Assert.NotNull(media.Files.Full); + Assert.Equal("https://example.com/full.mp4", media.Files.Full.Url); + } +} diff --git a/OF DL.Tests/Models/Mappers/MessagesMapperTests.cs b/OF DL.Tests/Models/Mappers/MessagesMapperTests.cs new file mode 100644 index 0000000..456cb4c --- /dev/null +++ b/OF DL.Tests/Models/Mappers/MessagesMapperTests.cs @@ -0,0 +1,113 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Messages; +using OF_DL.Models.Entities.Messages; +using OF_DL.Models.Mappers; + +namespace OF_DL.Tests.Models.Mappers; + +public class MessagesMapperTests +{ + [Fact] + public void FromDto_ReturnsDefaults_WhenDtoNull() + { + Messages result = MessagesMapper.FromDto(null as MessagesDto); + + Assert.False(result.HasMore); + Assert.Empty(result.List); + } + + [Fact] + public void FromDto_MapsListItems() + { + DateTime createdAt = new(2024, 3, 4, 5, 6, 7, DateTimeKind.Utc); + + MessagesDto dto = new() + { + HasMore = true, + List = + [ + new ListItemDto + { + Id = 11, + Text = "message", + Price = "4.99", + CreatedAt = createdAt, + FromUser = new FromUserDto { Id = 77 }, + Media = + [ + new MediumDto + { + Id = 22, + Type = "photo", + CanView = true, + Files = new FilesDto { Full = new FullDto { Url = "https://example.com/full.jpg" } } + } + ] + } + ] + }; + + Messages result = MessagesMapper.FromDto(dto); + + Assert.True(result.HasMore); + Assert.Single(result.List); + + ListItem item = result.List[0]; + Assert.Equal(11, item.Id); + Assert.Equal("message", item.Text); + Assert.Equal("4.99", item.Price); + Assert.Equal(createdAt, item.CreatedAt); + Assert.NotNull(item.FromUser); + Assert.Equal(77, item.FromUser.Id); + Assert.NotNull(item.Media); + Assert.Single(item.Media); + Assert.Equal(22, item.Media[0].Id); + } + + [Fact] + public void FromDto_MapsSingleMessage() + { + DateTime createdAt = new(2024, 4, 5, 6, 7, 8, DateTimeKind.Utc); + + SingleMessageDto dto = new() + { + Id = 99, + Text = "single", + Price = 1.23, + CreatedAt = createdAt, + FromUser = new FromUserDto { Id = 55 }, + Media = + [ + new MediumDto + { + Id = 33, + Type = "video", + CanView = true, + Files = new FilesDto { Full = new FullDto { Url = "https://example.com/full.mp4" } } + } + ] + }; + + SingleMessage result = MessagesMapper.FromDto(dto); + + Assert.Equal(99, result.Id); + Assert.Equal("single", result.Text); + Assert.Equal(1.23, result.Price); + Assert.Equal(createdAt, result.CreatedAt); + Assert.NotNull(result.FromUser); + Assert.Equal(55, result.FromUser.Id); + Assert.NotNull(result.Media); + Assert.Single(result.Media); + Assert.Equal(33, result.Media[0].Id); + } + + [Fact] + public void FromDto_SingleMessage_ReturnsNullFromUser_WhenIdMissing() + { + SingleMessageDto dto = new() { Id = 1, FromUser = new FromUserDto { Id = null } }; + + SingleMessage result = MessagesMapper.FromDto(dto); + + Assert.Null(result.FromUser); + } +} diff --git a/OF DL.Tests/Models/Mappers/PostMapperTests.cs b/OF DL.Tests/Models/Mappers/PostMapperTests.cs new file mode 100644 index 0000000..0c2a941 --- /dev/null +++ b/OF DL.Tests/Models/Mappers/PostMapperTests.cs @@ -0,0 +1,136 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Posts; +using OF_DL.Models.Entities.Posts; +using OF_DL.Models.Mappers; + +namespace OF_DL.Tests.Models.Mappers; + +public class PostMapperTests +{ + [Fact] + public void PostFromDto_ReturnsDefaults_WhenDtoNull() + { + Post result = PostMapper.FromDto((PostDto?)null); + + Assert.False(result.HasMore); + Assert.Null(result.TailMarker); + Assert.Empty(result.List); + } + + [Fact] + public void FromDto_MapsListItems() + { + DateTime postedAt = new(2024, 5, 6, 7, 8, 9, DateTimeKind.Utc); + + PostDto dto = new() + { + HasMore = true, + TailMarker = "tail", + List = + [ + new ListItemDto + { + Id = 10, + PostedAt = postedAt, + Author = new AuthorDto { Id = 3 }, + Text = "post", + RawText = "post", + IsOpened = true, + Price = "2.50", + IsArchived = true, + Media = + [ + new MediumDto + { + Id = 20, + Type = "photo", + CanView = true, + Preview = "preview", + Files = new FilesDto + { + Full = new FullDto { Url = "https://example.com/full.jpg" }, + Preview = new PreviewDto { Url = "https://example.com/preview.jpg" } + } + } + ] + } + ] + }; + + Post result = PostMapper.FromDto(dto); + + Assert.True(result.HasMore); + Assert.Equal("tail", result.TailMarker); + Assert.Single(result.List); + + ListItem item = result.List[0]; + Assert.Equal(10, item.Id); + Assert.Equal(postedAt, item.PostedAt); + Assert.NotNull(item.Author); + Assert.Equal(3, item.Author.Id); + Assert.Equal("post", item.Text); + Assert.True(item.IsOpened); + Assert.NotNull(item.Media); + Assert.Single(item.Media); + + Medium media = item.Media[0]; + Assert.Equal(20, media.Id); + Assert.Equal("photo", media.Type); + Assert.NotNull(media.Files); + Assert.NotNull(media.Files.Full); + Assert.Equal("https://example.com/full.jpg", media.Files.Full.Url); + Assert.NotNull(media.Files.Preview); + Assert.Equal("https://example.com/preview.jpg", media.Files.Preview.Url); + } + + [Fact] + public void FromDto_MapsSinglePost() + { + DateTime postedAt = new(2024, 6, 7, 8, 9, 10, DateTimeKind.Utc); + + SinglePostDto dto = new() + { + Id = 77, + PostedAt = postedAt, + Author = new AuthorDto { Id = 8 }, + Text = "single", + RawText = "single", + IsOpened = true, + Price = "4.00", + IsArchived = false, + Preview = ["preview"], + Media = + [ + new MediumDto + { + Id = 88, + Type = "video", + CanView = true, + Preview = "preview", + Files = new FilesDto { Full = new FullDto { Url = "https://example.com/full.mp4" } }, + VideoSources = new VideoSourcesDto { _240 = "240.mp4", _720 = "720.mp4" } + } + ] + }; + + SinglePost result = PostMapper.FromDto(dto); + + Assert.Equal(77, result.Id); + Assert.Equal(postedAt, result.PostedAt); + Assert.NotNull(result.Author); + Assert.Equal(8, result.Author.Id); + Assert.Equal("single", result.Text); + Assert.True(result.IsOpened); + Assert.NotNull(result.Media); + Assert.Single(result.Media); + + Medium media = result.Media[0]; + Assert.Equal(88, media.Id); + Assert.NotNull(media.Files); + Assert.NotNull(media.Files.Full); + Assert.Equal("https://example.com/full.mp4", media.Files.Full.Url); + Assert.NotNull(media.VideoSources); + Assert.Equal("240.mp4", media.VideoSources._240); + Assert.Equal("720.mp4", media.VideoSources._720); + } +} diff --git a/OF DL.Tests/Models/Mappers/PurchasedMapperTests.cs b/OF DL.Tests/Models/Mappers/PurchasedMapperTests.cs new file mode 100644 index 0000000..e58f4b9 --- /dev/null +++ b/OF DL.Tests/Models/Mappers/PurchasedMapperTests.cs @@ -0,0 +1,77 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Messages; +using OF_DL.Models.Dtos.Purchased; +using OF_DL.Models.Entities.Purchased; +using OF_DL.Models.Mappers; +using FromUserDto = OF_DL.Models.Dtos.Purchased.FromUserDto; +using ListItemDto = OF_DL.Models.Dtos.Purchased.ListItemDto; + +namespace OF_DL.Tests.Models.Mappers; + +public class PurchasedMapperTests +{ + [Fact] + public void FromDto_ReturnsDefaults_WhenDtoNull() + { + Purchased result = PurchasedMapper.FromDto(null); + + Assert.False(result.HasMore); + Assert.Empty(result.List); + } + + [Fact] + public void FromDto_MapsListItems() + { + DateTime createdAt = new(2024, 7, 8, 9, 10, 11, DateTimeKind.Utc); + + PurchasedDto dto = new() + { + HasMore = true, + List = + [ + new ListItemDto + { + Id = 111, + Text = "purchased", + Price = "12.34", + IsOpened = true, + IsArchived = true, + CreatedAt = createdAt, + PostedAt = createdAt, + FromUser = new FromUserDto { Id = 0 }, + Author = new AuthorDto { Id = 5 }, + Media = + [ + new MediumDto + { + Id = 222, + Type = "video", + CanView = true, + Files = new FilesDto { Full = new FullDto { Url = "https://example.com/full.mp4" } } + } + ] + } + ] + }; + + Purchased result = PurchasedMapper.FromDto(dto); + + Assert.True(result.HasMore); + Assert.Single(result.List); + + ListItem item = result.List[0]; + Assert.Equal(111, item.Id); + Assert.Equal("purchased", item.Text); + Assert.Equal("12.34", item.Price); + Assert.True(item.IsOpened); + Assert.True(item.IsArchived); + Assert.Equal(createdAt, item.CreatedAt); + Assert.Equal(createdAt, item.PostedAt); + Assert.NotNull(item.Author); + Assert.Equal(5, item.Author.Id); + Assert.Null(item.FromUser); + Assert.NotNull(item.Media); + Assert.Single(item.Media); + Assert.Equal(222, item.Media[0].Id); + } +} diff --git a/OF DL.Tests/Models/Mappers/StoriesMapperTests.cs b/OF DL.Tests/Models/Mappers/StoriesMapperTests.cs new file mode 100644 index 0000000..ecbfe76 --- /dev/null +++ b/OF DL.Tests/Models/Mappers/StoriesMapperTests.cs @@ -0,0 +1,58 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Stories; +using OF_DL.Models.Entities.Stories; +using OF_DL.Models.Mappers; + +namespace OF_DL.Tests.Models.Mappers; + +public class StoriesMapperTests +{ + [Fact] + public void FromDto_ReturnsEmptyList_WhenDtoNull() + { + List result = StoriesMapper.FromDto(null); + + Assert.Empty(result); + } + + [Fact] + public void FromDto_MapsStories() + { + DateTime createdAt = new(2024, 9, 10, 11, 12, 13, DateTimeKind.Utc); + + List dto = + [ + new() + { + Id = 12, + CreatedAt = createdAt, + Media = + [ + new MediumDto + { + Id = 34, + Type = "photo", + CanView = true, + CreatedAt = createdAt, + Files = new FilesDto { Full = new FullDto { Url = "https://example.com/full.jpg" } } + } + ] + } + ]; + + List result = StoriesMapper.FromDto(dto); + + Assert.Single(result); + + Stories story = result[0]; + Assert.Equal(12, story.Id); + Assert.Equal(createdAt, story.CreatedAt); + Assert.Single(story.Media); + + Medium media = story.Media[0]; + Assert.Equal(34, media.Id); + Assert.NotNull(media.Files); + Assert.NotNull(media.Files.Full); + Assert.Equal("https://example.com/full.jpg", media.Files.Full.Url); + } +} diff --git a/OF DL.Tests/Models/Mappers/StreamsMapperTests.cs b/OF DL.Tests/Models/Mappers/StreamsMapperTests.cs new file mode 100644 index 0000000..d2434c3 --- /dev/null +++ b/OF DL.Tests/Models/Mappers/StreamsMapperTests.cs @@ -0,0 +1,91 @@ +using OF_DL.Models.Dtos.Common; +using OF_DL.Models.Dtos.Streams; +using OF_DL.Models.Entities.Streams; +using OF_DL.Models.Mappers; + +namespace OF_DL.Tests.Models.Mappers; + +public class StreamsMapperTests +{ + [Fact] + public void FromDto_ReturnsDefaults_WhenDtoNull() + { + Streams result = StreamsMapper.FromDto(null); + + Assert.False(result.HasMore); + Assert.Null(result.TailMarker); + Assert.Empty(result.List); + } + + [Fact] + public void FromDto_MapsListItems() + { + DateTime postedAt = new(2024, 8, 9, 10, 11, 12, DateTimeKind.Utc); + + StreamsDto dto = new() + { + HasMore = true, + TailMarker = "tail", + List = + [ + new ListItemDto + { + Id = 333, + PostedAt = postedAt, + Author = new AuthorDto { Id = 9 }, + Text = "stream", + RawText = "stream", + Price = "1.00", + IsOpened = null, + IsArchived = null, + Media = + [ + new MediumDto + { + Id = 444, + Type = "video", + CanView = true, + Files = new FilesDto + { + Full = new FullDto { Url = "https://example.com/full.mp4" }, + Drm = new DrmDto + { + Manifest = new ManifestDto { Dash = "dash.mpd" }, + Signature = new SignatureDto + { + Dash = new DashDto { CloudFrontPolicy = "policy" } + } + } + } + } + ] + } + ] + }; + + Streams result = StreamsMapper.FromDto(dto); + + Assert.True(result.HasMore); + Assert.Equal("tail", result.TailMarker); + Assert.Single(result.List); + + ListItem item = result.List[0]; + Assert.Equal(333, item.Id); + Assert.Equal(postedAt, item.PostedAt); + Assert.NotNull(item.Author); + Assert.Equal(9, item.Author.Id); + Assert.Equal("stream", item.Text); + Assert.False(item.IsOpened); + Assert.False(item.IsArchived); + Assert.NotNull(item.Media); + Assert.Single(item.Media); + + Medium media = item.Media[0]; + Assert.Equal(444, media.Id); + Assert.NotNull(media.Files); + Assert.NotNull(media.Files.Full); + Assert.Equal("https://example.com/full.mp4", media.Files.Full.Url); + Assert.NotNull(media.Files.Drm?.Manifest); + Assert.Equal("dash.mpd", media.Files.Drm.Manifest.Dash); + } +} diff --git a/OF DL.Tests/Models/Mappers/SubscriptionsMapperTests.cs b/OF DL.Tests/Models/Mappers/SubscriptionsMapperTests.cs new file mode 100644 index 0000000..cf4527f --- /dev/null +++ b/OF DL.Tests/Models/Mappers/SubscriptionsMapperTests.cs @@ -0,0 +1,34 @@ +using OF_DL.Models.Dtos.Subscriptions; +using OF_DL.Models.Entities.Subscriptions; +using OF_DL.Models.Mappers; + +namespace OF_DL.Tests.Models.Mappers; + +public class SubscriptionsMapperTests +{ + [Fact] + public void FromDto_ReturnsDefaults_WhenDtoNull() + { + Subscriptions result = SubscriptionsMapper.FromDto(null); + + Assert.False(result.HasMore); + Assert.Empty(result.List); + } + + [Fact] + public void FromDto_MapsListItems() + { + SubscriptionsDto dto = new() + { + HasMore = true, List = [new ListItemDto { Id = 55, Username = null, IsRestricted = true }] + }; + + Subscriptions result = SubscriptionsMapper.FromDto(dto); + + Assert.True(result.HasMore); + Assert.Single(result.List); + Assert.Equal(55, result.List[0].Id); + Assert.Equal(string.Empty, result.List[0].Username); + Assert.True(result.List[0].IsRestricted); + } +} diff --git a/OF DL.Tests/Models/Mappers/UserListsMapperTests.cs b/OF DL.Tests/Models/Mappers/UserListsMapperTests.cs new file mode 100644 index 0000000..da08044 --- /dev/null +++ b/OF DL.Tests/Models/Mappers/UserListsMapperTests.cs @@ -0,0 +1,49 @@ +using OF_DL.Models.Dtos.Lists; +using OF_DL.Models.Entities.Lists; +using OF_DL.Models.Mappers; + +namespace OF_DL.Tests.Models.Mappers; + +public class UserListsMapperTests +{ + [Fact] + public void FromDto_ReturnsDefaults_WhenDtoNull() + { + UserList result = UserListsMapper.FromDto(null as UserListDto); + + Assert.False(result.HasMore); + Assert.Empty(result.List); + } + + [Fact] + public void FromDto_MapsUserListItems() + { + UserListDto dto = new() { HasMore = true, List = [new UserListItemDto { Id = "1", Name = "Favorites" }] }; + + UserList result = UserListsMapper.FromDto(dto); + + Assert.True(result.HasMore); + Assert.Single(result.List); + Assert.Equal("1", result.List[0].Id); + Assert.Equal("Favorites", result.List[0].Name); + } + + [Fact] + public void FromDto_UsersList_ReturnsEmptyList_WhenDtoNull() + { + List result = UserListsMapper.FromDto((List?)null); + + Assert.Empty(result); + } + + [Fact] + public void FromDto_UsersList_MapsItems() + { + List dto = [new() { Username = "creator" }]; + + List result = UserListsMapper.FromDto(dto); + + Assert.Single(result); + Assert.Equal("creator", result[0].Username); + } +} diff --git a/OF DL.Tests/Models/Mappers/UserMapperTests.cs b/OF DL.Tests/Models/Mappers/UserMapperTests.cs new file mode 100644 index 0000000..75cd1c0 --- /dev/null +++ b/OF DL.Tests/Models/Mappers/UserMapperTests.cs @@ -0,0 +1,25 @@ +using OF_DL.Models.Dtos.Users; +using OF_DL.Models.Entities.Users; +using OF_DL.Models.Mappers; + +namespace OF_DL.Tests.Models.Mappers; + +public class UserMapperTests +{ + [Fact] + public void FromDto_ReturnsNull_WhenDtoNull() => Assert.Null(UserMapper.FromDto(null)); + + [Fact] + public void FromDto_MapsFields() + { + UserDto dto = new() { Avatar = "avatar", Header = "header", Name = "Name", Username = "user" }; + + User? result = UserMapper.FromDto(dto); + + Assert.NotNull(result); + Assert.Equal("avatar", result.Avatar); + Assert.Equal("header", result.Header); + Assert.Equal("Name", result.Name); + Assert.Equal("user", result.Username); + } +} diff --git a/OF DL.Tests/OF DL.Tests.csproj b/OF DL.Tests/OF DL.Tests.csproj new file mode 100644 index 0000000..cb77d5a --- /dev/null +++ b/OF DL.Tests/OF DL.Tests.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + OF_DL.Tests + enable + enable + false + + + + + + + + + + + + + + + + + + diff --git a/OF DL.sln b/OF DL.sln index 62e5364..6beeff9 100644 --- a/OF DL.sln +++ b/OF DL.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OF DL", "OF DL\OF DL.csproj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OF DL.Core", "OF DL.Core\OF DL.Core.csproj", "{7B8B6A26-6732-4B3A-AE62-1CE589DFF8F2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OF DL.Tests", "OF DL.Tests\OF DL.Tests.csproj", "{FF5EC4D7-6369-4A78-8C02-E370343E797C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {7B8B6A26-6732-4B3A-AE62-1CE589DFF8F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {7B8B6A26-6732-4B3A-AE62-1CE589DFF8F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B8B6A26-6732-4B3A-AE62-1CE589DFF8F2}.Release|Any CPU.Build.0 = Release|Any CPU + {FF5EC4D7-6369-4A78-8C02-E370343E797C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF5EC4D7-6369-4A78-8C02-E370343E797C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF5EC4D7-6369-4A78-8C02-E370343E797C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF5EC4D7-6369-4A78-8C02-E370343E797C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 04004c7084d4640c9222ae674d9c0a7b9e6fe223 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 12:01:33 -0600 Subject: [PATCH 42/55] Refactor services --- OF DL.Core/Helpers/Constants.cs | 9 +- OF DL.Core/Helpers/VersionHelper.cs | 19 +- OF DL.Core/Services/ApiService.cs | 1305 +++++++++-------- OF DL.Core/Services/DbService.cs | 8 +- .../Services/DownloadOrchestrationService.cs | 12 +- OF DL.Core/Services/DownloadService.cs | 20 +- OF DL.Core/Services/IApiService.cs | 5 +- OF DL.Core/Services/IAuthService.cs | 2 +- OF DL.Core/Services/IDownloadService.cs | 17 - OF DL.Core/Services/IProgressReporter.cs | 6 - OF DL.Core/Utils/HttpUtil.cs | 4 +- OF DL/CLI/SpectreProgressReporter.cs | 6 - 12 files changed, 709 insertions(+), 704 deletions(-) diff --git a/OF DL.Core/Helpers/Constants.cs b/OF DL.Core/Helpers/Constants.cs index 01a410d..2843ee7 100644 --- a/OF DL.Core/Helpers/Constants.cs +++ b/OF DL.Core/Helpers/Constants.cs @@ -2,8 +2,11 @@ namespace OF_DL.Helpers; public static class Constants { - public const string API_URL = "https://onlyfans.com/api2/v2"; + public const string ApiUrl = "https://onlyfans.com/api2/v2"; - public const int WIDEVINE_RETRY_DELAY = 10; - public const int WIDEVINE_MAX_RETRIES = 3; + public const int ApiPageSize = 50; + + public const int WidevineRetryDelay = 10; + + public const int WidevineMaxRetries = 3; } diff --git a/OF DL.Core/Helpers/VersionHelper.cs b/OF DL.Core/Helpers/VersionHelper.cs index 17e01a1..593e74c 100644 --- a/OF DL.Core/Helpers/VersionHelper.cs +++ b/OF DL.Core/Helpers/VersionHelper.cs @@ -6,15 +6,15 @@ namespace OF_DL.Helpers; public static class VersionHelper { - private const string url = "https://git.ofdl.tools/api/v1/repos/sim0n00ps/OF-DL/releases/latest"; - private static readonly HttpClient httpClient = new(); + private const string Url = "https://git.ofdl.tools/api/v1/repos/sim0n00ps/OF-DL/releases/latest"; + private static readonly HttpClient s_httpClient = new(); public static async Task GetLatestReleaseTag(CancellationToken cancellationToken = default) { Log.Debug("Calling GetLatestReleaseTag"); try { - HttpResponseMessage response = await httpClient.GetAsync(url, cancellationToken); + HttpResponseMessage response = await s_httpClient.GetAsync(Url, cancellationToken); if (!response.IsSuccessStatusCode) { @@ -22,21 +22,20 @@ public static class VersionHelper return null; } - string body = await response.Content.ReadAsStringAsync(); + string body = await response.Content.ReadAsStringAsync(cancellationToken); - Log.Debug("GetLatestReleaseTag API Response: "); - Log.Debug(body); + Log.Debug("GetLatestReleaseTag API Response: {Body}", body); LatestReleaseApiResponse? versionCheckResponse = JsonConvert.DeserializeObject(body); - if (versionCheckResponse == null || versionCheckResponse.TagName == "") + if (versionCheckResponse != null && versionCheckResponse.TagName != "") { - Log.Debug("GetLatestReleaseTag did not return a valid tag name"); - return null; + return versionCheckResponse.TagName; } - return versionCheckResponse.TagName; + Log.Debug("GetLatestReleaseTag did not return a valid tag name"); + return null; } catch (OperationCanceledException) { diff --git a/OF DL.Core/Services/ApiService.cs b/OF DL.Core/Services/ApiService.cs index 09017f0..09efb41 100644 --- a/OF DL.Core/Services/ApiService.cs +++ b/OF DL.Core/Services/ApiService.cs @@ -74,7 +74,7 @@ public class ApiService(IAuthService authService, IConfigService configService, } else { - //Get rules from GitHub and fallback to local file + // Get rules from GitHub and fallback to a local file string? dynamicRulesJson = GetDynamicRules(); if (!string.IsNullOrEmpty(dynamicRulesJson)) { @@ -90,7 +90,7 @@ public class ApiService(IAuthService authService, IConfigService configService, Log.Debug("Using dynamic rules from local file"); root = JsonConvert.DeserializeObject(File.ReadAllText("rules.json")); - // Cache the dynamic rules from local file to prevent unnecessary disk + // Cache the dynamic rules from a local file to prevent unnecessary disk // operations and frequent call to GitHub. Since the GitHub dynamic rules // are preferred to the local file, the cache time is shorter than when dynamic rules // are successfully retrieved from GitHub. @@ -160,10 +160,9 @@ public class ApiService(IAuthService authService, IConfigService configService, try { UserEntities.User user = new(); - const int postLimit = 50; Dictionary getParams = new() { - { "limit", postLimit.ToString() }, { "order", "publish_date_asc" } + { "limit", Constants.ApiPageSize.ToString() }, { "order", "publish_date_asc" } }; HttpClient client = new(); @@ -386,20 +385,17 @@ public class ApiService(IAuthService authService, IConfigService configService, /// The endpoint to query. /// Optional username context. /// The creator folder path. - /// Paid post media IDs. /// A mediaId-to-URL map. public async Task?> GetMedia(MediaType mediatype, string endpoint, string? username, - string folder, - List paidPostIds) + string folder) { Log.Debug($"Calling GetMedia - {username}"); try { Dictionary returnUrls = new(); - const int postLimit = 50; const int limit = 5; int offset = 0; @@ -410,7 +406,9 @@ public class ApiService(IAuthService authService, IConfigService configService, case MediaType.Stories: getParams = new Dictionary { - { "limit", postLimit.ToString() }, { "order", "publish_date_desc" }, { "skip_users", "all" } + { "limit", Constants.ApiPageSize.ToString() }, + { "order", "publish_date_desc" }, + { "skip_users", "all" } }; break; @@ -478,9 +476,9 @@ public class ApiService(IAuthService authService, IConfigService configService, continue; } - if (medium.CanView && !returnUrls.ContainsKey(medium.Id)) + if (medium.CanView) { - returnUrls.Add(medium.Id, mediaUrl); + returnUrls.TryAdd(medium.Id, mediaUrl); } } } @@ -555,7 +553,7 @@ public class ApiService(IAuthService authService, IConfigService configService, foreach (HighlightEntities.Story item in highlightMedia.Stories) { - DateTime? createdAt = item.Media != null && item.Media.Count > 0 + DateTime? createdAt = item.Media is { Count: > 0 } ? item.Media[0].CreatedAt : null; @@ -572,24 +570,26 @@ public class ApiService(IAuthService authService, IConfigService configService, await dbService.AddStory(folder, item.Id, "", "0", false, false, DateTime.Now); } - if (item.Media != null && item.Media.Count > 0 && item.Media[0].CanView) + if (item.Media is not { Count: > 0 } || !item.Media[0].CanView) { - string? storyUrl = item.Media[0].Files?.Full?.Url; - string storyUrlValue = storyUrl ?? string.Empty; - foreach (HighlightEntities.Medium medium in item.Media) - { - string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; - await dbService.AddMedia(folder, medium.Id, item.Id, storyUrlValue, null, null, null, - "Stories", mediaType, false, false, null); - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + continue; + } - if (!returnUrls.ContainsKey(medium.Id) && !string.IsNullOrEmpty(storyUrl)) - { - returnUrls.Add(medium.Id, storyUrl); - } + string? storyUrl = item.Media[0].Files?.Full?.Url; + string storyUrlValue = storyUrl ?? string.Empty; + foreach (HighlightEntities.Medium medium in item.Media) + { + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + await dbService.AddMedia(folder, medium.Id, item.Id, storyUrlValue, null, null, null, + "Stories", mediaType, false, false, null); + if (!IsMediaTypeDownloadEnabled(medium.Type)) + { + continue; + } + + if (!returnUrls.ContainsKey(medium.Id) && !string.IsNullOrEmpty(storyUrl)) + { + returnUrls.Add(medium.Id, storyUrl); } } } @@ -625,10 +625,9 @@ public class ApiService(IAuthService authService, IConfigService configService, try { PurchasedEntities.PaidPostCollection paidPostCollection = new(); - const int postLimit = 50; Dictionary getParams = new() { - { "limit", postLimit.ToString() }, + { "limit", Constants.ApiPageSize.ToString() }, { "skip_users", "all" }, { "order", "publish_date_desc" }, { "format", "infinite" }, @@ -657,120 +656,123 @@ public class ApiService(IAuthService authService, IConfigService configService, break; } - getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit); + getParams["offset"] = + Convert.ToString(Convert.ToInt32(getParams["offset"]) + Constants.ApiPageSize); } } List paidPostList = paidPosts.List; foreach (PurchasedEntities.ListItem purchase in paidPostList) { - if (purchase.ResponseType == "post" && purchase.Media != null && purchase.Media.Count > 0) + if (purchase.ResponseType != "post" || purchase.Media is not { Count: > 0 }) { - List previewids = new(); - if (purchase.Previews != null) + continue; + } + + List previewIds = []; + if (purchase.Previews != null) + { + for (int i = 0; i < purchase.Previews.Count; i++) { - for (int i = 0; i < purchase.Previews.Count; i++) + if (purchase.Previews[i] is long previewId) { - if (purchase.Previews[i] is long previewId) + if (!previewIds.Contains(previewId)) { - if (!previewids.Contains(previewId)) - { - previewids.Add(previewId); - } + previewIds.Add(previewId); } } } - else if (purchase.Preview != null) + } + else if (purchase.Preview != null) + { + for (int i = 0; i < purchase.Preview.Count; i++) { - for (int i = 0; i < purchase.Preview.Count; i++) + if (purchase.Preview[i] is long previewId) { - if (purchase.Preview[i] is long previewId) + if (!previewIds.Contains(previewId)) { - if (!previewids.Contains(previewId)) - { - previewids.Add(previewId); - } + previewIds.Add(previewId); } } } + } - DateTime createdAt = purchase.CreatedAt ?? purchase.PostedAt ?? DateTime.Now; - bool isArchived = purchase.IsArchived ?? false; - await dbService.AddPost(folder, purchase.Id, purchase.Text ?? "", - purchase.Price ?? "0", - purchase.Price != null && purchase.IsOpened, isArchived, createdAt); - paidPostCollection.PaidPostObjects.Add(purchase); - foreach (MessageEntities.Medium medium in purchase.Media) + DateTime createdAt = purchase.CreatedAt ?? purchase.PostedAt ?? DateTime.Now; + bool isArchived = purchase.IsArchived ?? false; + await dbService.AddPost(folder, purchase.Id, purchase.Text ?? "", + purchase.Price ?? "0", + purchase is { Price: not null, IsOpened: true }, isArchived, createdAt); + paidPostCollection.PaidPostObjects.Add(purchase); + foreach (MessageEntities.Medium medium in purchase.Media) + { + if (!previewIds.Contains(medium.Id)) { - if (!previewids.Contains(medium.Id)) - { - paidPostIds.Add(medium.Id); - } + paidPostIds.Add(medium.Id); + } - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + if (!IsMediaTypeDownloadEnabled(medium.Type)) + { + continue; + } - string mediaType = ResolveMediaType(medium.Type) ?? ""; - string? fullUrl = medium.Files?.Full?.Url; - bool isPreview = previewids.Contains(medium.Id); + string mediaType = ResolveMediaType(medium.Type) ?? ""; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = previewIds.Contains(medium.Id); - if (previewids.Count > 0) + if (previewIds.Count > 0) + { + bool has = previewIds.Any(cus => cus.Equals(medium.Id)); + if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { - bool has = previewids.Any(cus => cus.Equals(medium.Id)); - if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) + if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, purchase.Id, fullUrl, - null, null, null, "Posts", - mediaType, isPreview, false, null); - paidPostCollection.PaidPosts.Add(medium.Id, fullUrl); - paidPostCollection.PaidPostMedia.Add(medium); - } - } - else if (!has && medium.CanView && - TryGetDrmInfo(medium.Files, out string manifestDash, - out string cloudFrontPolicy, out string cloudFrontSignature, - out string cloudFrontKeyPairId)) - { - if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, purchase.Id, - manifestDash, null, null, null, "Posts", mediaType, isPreview, false, null); - paidPostCollection.PaidPosts.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); - paidPostCollection.PaidPostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, purchase.Id, fullUrl, + null, null, null, "Posts", + mediaType, isPreview, false, null); + paidPostCollection.PaidPosts.Add(medium.Id, fullUrl); + paidPostCollection.PaidPostMedia.Add(medium); } } - else + else if (!has && medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) { - if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) + if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, purchase.Id, fullUrl, - null, null, null, "Posts", - mediaType, isPreview, false, null); - paidPostCollection.PaidPosts.Add(medium.Id, fullUrl); - paidPostCollection.PaidPostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, purchase.Id, + manifestDash, null, null, null, "Posts", mediaType, isPreview, false, null); + paidPostCollection.PaidPosts.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + paidPostCollection.PaidPostMedia.Add(medium); } - else if (medium.CanView && - TryGetDrmInfo(medium.Files, out string manifestDash, - out string cloudFrontPolicy, out string cloudFrontSignature, - out string cloudFrontKeyPairId)) + } + } + else + { + if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) + { + if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) { - if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, purchase.Id, - manifestDash, null, null, null, "Posts", mediaType, isPreview, false, null); - paidPostCollection.PaidPosts.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); - paidPostCollection.PaidPostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, purchase.Id, fullUrl, + null, null, null, "Posts", + mediaType, isPreview, false, null); + paidPostCollection.PaidPosts.Add(medium.Id, fullUrl); + paidPostCollection.PaidPostMedia.Add(medium); + } + } + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) + { + if (!paidPostCollection.PaidPosts.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, purchase.Id, + manifestDash, null, null, null, "Posts", mediaType, isPreview, false, null); + paidPostCollection.PaidPosts.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + paidPostCollection.PaidPostMedia.Add(medium); } } } @@ -804,10 +806,9 @@ public class ApiService(IAuthService authService, IConfigService configService, try { PostEntities.PostCollection postCollection = new(); - const int postLimit = 50; Dictionary getParams = new() { - { "limit", postLimit.ToString() }, + { "limit", Constants.ApiPageSize.ToString() }, { "order", "publish_date_desc" }, { "format", "infinite" }, { "skip_users", "all" } @@ -816,8 +817,7 @@ public class ApiService(IAuthService authService, IConfigService configService, DownloadDateSelection downloadDateSelection = DownloadDateSelection.before; DateTime? downloadAsOf = null; - if (configService.CurrentConfig.DownloadOnlySpecificDates && - configService.CurrentConfig.CustomDate.HasValue) + if (configService.CurrentConfig is { DownloadOnlySpecificDates: true, CustomDate: not null }) { downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; downloadAsOf = configService.CurrentConfig.CustomDate; @@ -890,77 +890,83 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - List postPreviewIds = new(); - if (post.Preview != null && post.Preview.Count > 0) + List postPreviewIds = []; + if (post.Preview is { Count: > 0 }) { for (int i = 0; i < post.Preview.Count; i++) { - if (post.Preview[i] is long previewId) + if (post.Preview[i] is not long previewId) { - if (!postPreviewIds.Contains(previewId)) - { - postPreviewIds.Add(previewId); - } + continue; + } + + if (!postPreviewIds.Contains(previewId)) + { + postPreviewIds.Add(previewId); } } } await dbService.AddPost(folder, post.Id, !string.IsNullOrEmpty(post.RawText) ? post.RawText : "", - post.Price ?? "0", post.Price != null && post.IsOpened, + post.Price ?? "0", post is { Price: not null, IsOpened: true }, post.IsArchived, post.PostedAt); + postCollection.PostObjects.Add(post); - if (post.Media != null && post.Media.Count > 0) + + if (post.Media is not { Count: > 0 }) { - foreach (PostEntities.Medium medium in post.Media) + continue; + } + + foreach (PostEntities.Medium medium in post.Media) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + continue; + } - string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; - string? fullUrl = medium.Files?.Full?.Url; - string? previewUrl = medium.Files?.Preview?.Url; - bool isPreview = postPreviewIds.Contains(medium.Id); + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + string? previewUrl = medium.Files?.Preview?.Url; + bool isPreview = postPreviewIds.Contains(medium.Id); - if (medium.CanView && medium.Files?.Drm == null) + if (medium.CanView && medium.Files?.Drm == null) + { + bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); + if (!has && !string.IsNullOrEmpty(fullUrl)) { - bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); - if (!has && !string.IsNullOrEmpty(fullUrl)) + if (!postCollection.Posts.ContainsKey(medium.Id)) { - if (!postCollection.Posts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, post.Id, fullUrl, null, null, null, - "Posts", mediaType, isPreview, false, null); - postCollection.Posts.Add(medium.Id, fullUrl); - postCollection.PostMedia.Add(medium); - } - } - else if (!has && string.IsNullOrEmpty(fullUrl) && !string.IsNullOrEmpty(previewUrl)) - { - if (!postCollection.Posts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, post.Id, previewUrl, null, null, null, - "Posts", mediaType, isPreview, false, null); - postCollection.Posts.Add(medium.Id, previewUrl); - postCollection.PostMedia.Add(medium); - } - } - } - else if (medium.CanView && - TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, - out string cloudFrontSignature, out string cloudFrontKeyPairId)) - { - bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); - if (!has && !postCollection.Posts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, post.Id, manifestDash, null, null, null, + await dbService.AddMedia(folder, medium.Id, post.Id, fullUrl, null, null, null, "Posts", mediaType, isPreview, false, null); - postCollection.Posts.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{post.Id}"); + postCollection.Posts.Add(medium.Id, fullUrl); postCollection.PostMedia.Add(medium); } } + else if (!has && string.IsNullOrEmpty(fullUrl) && !string.IsNullOrEmpty(previewUrl)) + { + if (!postCollection.Posts.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, post.Id, previewUrl, null, null, null, + "Posts", mediaType, isPreview, false, null); + postCollection.Posts.Add(medium.Id, previewUrl); + postCollection.PostMedia.Add(medium); + } + } + } + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId)) + { + bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); + if (!has && !postCollection.Posts.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, post.Id, manifestDash, null, null, null, + "Posts", mediaType, isPreview, false, null); + postCollection.Posts.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{post.Id}"); + postCollection.PostMedia.Add(medium); + } } } } @@ -997,17 +1003,19 @@ public class ApiService(IAuthService authService, IConfigService configService, if (singlePostDto != null) { - List postPreviewIds = new(); - if (singlePost.Preview != null && singlePost.Preview.Count > 0) + List postPreviewIds = []; + if (singlePost.Preview is { Count: > 0 }) { for (int i = 0; i < singlePost.Preview.Count; i++) { - if (singlePost.Preview[i] is long previewId) + if (singlePost.Preview[i] is not long previewId) { - if (!postPreviewIds.Contains(previewId)) - { - postPreviewIds.Add(previewId); - } + continue; + } + + if (!postPreviewIds.Contains(previewId)) + { + postPreviewIds.Add(previewId); } } } @@ -1015,92 +1023,95 @@ public class ApiService(IAuthService authService, IConfigService configService, await dbService.AddPost(folder, singlePost.Id, !string.IsNullOrEmpty(singlePost.Text) ? singlePost.Text : "", singlePost.Price ?? "0", - singlePost.Price != null && singlePost.IsOpened, singlePost.IsArchived, + singlePost is { Price: not null, IsOpened: true }, singlePost.IsArchived, singlePost.PostedAt); singlePostCollection.SinglePostObjects.Add(singlePost); - if (singlePost.Media != null && singlePost.Media.Count > 0) + + if (singlePost.Media == null || singlePost.Media.Count <= 0) { - foreach (PostEntities.Medium medium in singlePost.Media) + return singlePostCollection; + } + + foreach (PostEntities.Medium medium in singlePost.Media) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + continue; + } - string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; - bool isPreview = postPreviewIds.Contains(medium.Id); - string? fullUrl = medium.Files?.Full?.Url; - string? previewUrl = medium.Files?.Preview?.Url; + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + bool isPreview = postPreviewIds.Contains(medium.Id); + string? fullUrl = medium.Files?.Full?.Url; + string? previewUrl = medium.Files?.Preview?.Url; - if (medium.CanView && medium.Files?.Drm == null) + if (medium.CanView && medium.Files?.Drm == null) + { + switch (configService.CurrentConfig.DownloadVideoResolution) { - switch (configService.CurrentConfig.DownloadVideoResolution) - { - case VideoResolution.source: - if (!string.IsNullOrEmpty(fullUrl)) + case VideoResolution.source: + if (!string.IsNullOrEmpty(fullUrl)) + { + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, singlePost.Id, - fullUrl, null, null, null, "Posts", mediaType, isPreview, false, null); - singlePostCollection.SinglePosts.Add(medium.Id, fullUrl); - singlePostCollection.SinglePostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, singlePost.Id, + fullUrl, null, null, null, "Posts", mediaType, isPreview, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, fullUrl); + singlePostCollection.SinglePostMedia.Add(medium); } + } - break; - case VideoResolution._240: - string? video240 = medium.VideoSources?._240; - if (!string.IsNullOrEmpty(video240)) + break; + case VideoResolution._240: + string? video240 = medium.VideoSources?._240; + if (!string.IsNullOrEmpty(video240)) + { + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, singlePost.Id, video240, null, - null, null, "Posts", mediaType, isPreview, false, null); - singlePostCollection.SinglePosts.Add(medium.Id, video240); - singlePostCollection.SinglePostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, singlePost.Id, video240, null, + null, null, "Posts", mediaType, isPreview, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, video240); + singlePostCollection.SinglePostMedia.Add(medium); } + } - break; - case VideoResolution._720: - string? video720 = medium.VideoSources?._720; - if (!string.IsNullOrEmpty(video720)) + break; + case VideoResolution._720: + string? video720 = medium.VideoSources?._720; + if (!string.IsNullOrEmpty(video720)) + { + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, singlePost.Id, video720, null, - null, null, "Posts", mediaType, isPreview, false, null); - singlePostCollection.SinglePosts.Add(medium.Id, video720); - singlePostCollection.SinglePostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, singlePost.Id, video720, null, + null, null, "Posts", mediaType, isPreview, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, video720); + singlePostCollection.SinglePostMedia.Add(medium); } + } - break; - } + break; } - else if (medium.CanView && - TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, - out string cloudFrontSignature, out string cloudFrontKeyPairId)) + } + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId)) + { + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, singlePost.Id, manifestDash, null, null, - null, "Posts", mediaType, isPreview, false, null); - singlePostCollection.SinglePosts.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{singlePost.Id}"); - singlePostCollection.SinglePostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, singlePost.Id, manifestDash, null, null, + null, "Posts", mediaType, isPreview, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{singlePost.Id}"); + singlePostCollection.SinglePostMedia.Add(medium); } - else if (!string.IsNullOrEmpty(previewUrl) && medium.Files?.Full == null) + } + else if (!string.IsNullOrEmpty(previewUrl) && medium.Files?.Full == null) + { + if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) { - if (!singlePostCollection.SinglePosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, singlePost.Id, previewUrl, null, null, null, - "Posts", mediaType, isPreview, false, null); - singlePostCollection.SinglePosts.Add(medium.Id, previewUrl); - singlePostCollection.SinglePostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, singlePost.Id, previewUrl, null, null, null, + "Posts", mediaType, isPreview, false, null); + singlePostCollection.SinglePosts.Add(medium.Id, previewUrl); + singlePostCollection.SinglePostMedia.Add(medium); } } } @@ -1133,18 +1144,16 @@ public class ApiService(IAuthService authService, IConfigService configService, try { StreamEntities.StreamsCollection streamsCollection = new(); - const int postLimit = 50; Dictionary getParams = new() { - { "limit", postLimit.ToString() }, + { "limit", Constants.ApiPageSize.ToString() }, { "order", "publish_date_desc" }, { "format", "infinite" }, { "skip_users", "all" } }; DownloadDateSelection downloadDateSelection = DownloadDateSelection.before; - if (configService.CurrentConfig.DownloadOnlySpecificDates && - configService.CurrentConfig.CustomDate.HasValue) + if (configService.CurrentConfig is { DownloadOnlySpecificDates: true, CustomDate: not null }) { downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; } @@ -1190,67 +1199,73 @@ public class ApiService(IAuthService authService, IConfigService configService, List streamList = streams.List; foreach (StreamEntities.ListItem stream in streamList) { - List streamPreviewIds = new(); - if (stream.Preview != null && stream.Preview.Count > 0) + List streamPreviewIds = []; + if (stream.Preview is { Count: > 0 }) { for (int i = 0; i < stream.Preview.Count; i++) { - if (stream.Preview[i] is long previewId) + if (stream.Preview[i] is not long previewId) { - if (!streamPreviewIds.Contains(previewId)) - { - streamPreviewIds.Add(previewId); - } + continue; + } + + if (!streamPreviewIds.Contains(previewId)) + { + streamPreviewIds.Add(previewId); } } } await dbService.AddPost(folder, stream.Id, !string.IsNullOrEmpty(stream.Text) ? stream.Text : "", - stream.Price ?? "0", stream.Price != null && stream.IsOpened, + stream.Price ?? "0", stream is { Price: not null, IsOpened: true }, stream.IsArchived, stream.PostedAt); + streamsCollection.StreamObjects.Add(stream); - if (stream.Media != null && stream.Media.Count > 0) + + if (stream.Media is not { Count: > 0 }) { - foreach (StreamEntities.Medium medium in stream.Media) + continue; + } + + foreach (StreamEntities.Medium medium in stream.Media) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + continue; + } - string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; - string? fullUrl = medium.Files?.Full?.Url; - bool isPreview = streamPreviewIds.Contains(medium.Id); + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = streamPreviewIds.Contains(medium.Id); - if (medium.CanView && medium.Files?.Drm == null) + if (medium.CanView && medium.Files?.Drm == null) + { + bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); + if (!has && !string.IsNullOrEmpty(fullUrl)) { - bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); - if (!has && !string.IsNullOrEmpty(fullUrl)) + if (!streamsCollection.Streams.ContainsKey(medium.Id)) { - if (!streamsCollection.Streams.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, stream.Id, fullUrl, null, null, null, - "Posts", mediaType, isPreview, false, null); - streamsCollection.Streams.Add(medium.Id, fullUrl); - streamsCollection.StreamMedia.Add(medium); - } - } - } - else if (medium.CanView && - TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, - out string cloudFrontSignature, out string cloudFrontKeyPairId)) - { - bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); - if (!has && !streamsCollection.Streams.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, stream.Id, manifestDash, null, null, null, + await dbService.AddMedia(folder, medium.Id, stream.Id, fullUrl, null, null, null, "Posts", mediaType, isPreview, false, null); - streamsCollection.Streams.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{stream.Id}"); + streamsCollection.Streams.Add(medium.Id, fullUrl); streamsCollection.StreamMedia.Add(medium); } } } + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId)) + { + bool has = paidPostIds.Any(cus => cus.Equals(medium.Id)); + if (!has && !streamsCollection.Streams.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, stream.Id, manifestDash, null, null, null, + "Posts", mediaType, isPreview, false, null); + streamsCollection.Streams.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{stream.Id}"); + streamsCollection.StreamMedia.Add(medium); + } + } } } @@ -1280,10 +1295,9 @@ public class ApiService(IAuthService authService, IConfigService configService, try { ArchivedEntities.ArchivedCollection archivedCollection = new(); - const int postLimit = 50; Dictionary getParams = new() { - { "limit", postLimit.ToString() }, + { "limit", Constants.ApiPageSize.ToString() }, { "order", "publish_date_desc" }, { "skip_users", "all" }, { "format", "infinite" }, @@ -1292,8 +1306,7 @@ public class ApiService(IAuthService authService, IConfigService configService, }; DownloadDateSelection downloadDateSelection = DownloadDateSelection.before; - if (configService.CurrentConfig.DownloadOnlySpecificDates && - configService.CurrentConfig.CustomDate.HasValue) + if (configService.CurrentConfig is { DownloadOnlySpecificDates: true, CustomDate: not null }) { downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; } @@ -1364,43 +1377,47 @@ public class ApiService(IAuthService authService, IConfigService configService, await dbService.AddPost(folder, archive.Id, archive.Text ?? "", archive.Price ?? "0", - archive.Price != null && archive.IsOpened, archive.IsArchived, archive.PostedAt); + archive is { Price: not null, IsOpened: true }, archive.IsArchived, archive.PostedAt); + archivedCollection.ArchivedPostObjects.Add(archive); - if (archive.Media != null && archive.Media.Count > 0) + + if (archive.Media is not { Count: > 0 }) { - foreach (ArchivedEntities.Medium medium in archive.Media) + continue; + } + + foreach (ArchivedEntities.Medium medium in archive.Media) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + continue; + } - string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; - string? fullUrl = medium.Files?.Full?.Url; - bool isPreview = previewids.Contains(medium.Id); + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = previewids.Contains(medium.Id); - if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) + if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) + { + if (!archivedCollection.ArchivedPosts.ContainsKey(medium.Id)) { - if (!archivedCollection.ArchivedPosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, archive.Id, fullUrl, null, null, null, - "Posts", mediaType, isPreview, false, null); - archivedCollection.ArchivedPosts.Add(medium.Id, fullUrl); - archivedCollection.ArchivedPostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, archive.Id, fullUrl, null, null, null, + "Posts", mediaType, isPreview, false, null); + archivedCollection.ArchivedPosts.Add(medium.Id, fullUrl); + archivedCollection.ArchivedPostMedia.Add(medium); } - else if (medium.CanView && - TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, - out string cloudFrontSignature, out string cloudFrontKeyPairId)) + } + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId)) + { + if (!archivedCollection.ArchivedPosts.ContainsKey(medium.Id)) { - if (!archivedCollection.ArchivedPosts.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, archive.Id, manifestDash, null, null, null, - "Posts", mediaType, isPreview, false, null); - archivedCollection.ArchivedPosts.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{archive.Id}"); - archivedCollection.ArchivedPostMedia.Add(medium); - } + await dbService.AddMedia(folder, medium.Id, archive.Id, manifestDash, null, null, null, + "Posts", mediaType, isPreview, false, null); + archivedCollection.ArchivedPosts.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{archive.Id}"); + archivedCollection.ArchivedPostMedia.Add(medium); } } } @@ -1432,10 +1449,9 @@ public class ApiService(IAuthService authService, IConfigService configService, try { MessageEntities.MessageCollection messageCollection = new(); - const int postLimit = 50; Dictionary getParams = new() { - { "limit", postLimit.ToString() }, { "order", "desc" }, { "skip_users", "all" } + { "limit", Constants.ApiPageSize.ToString() }, { "order", "desc" }, { "skip_users", "all" } }; int currentUserId = GetCurrentUserIdOrDefault(); @@ -1476,115 +1492,122 @@ public class ApiService(IAuthService authService, IConfigService configService, } } - List messagePreviewIds = new(); - if (list.Previews != null && list.Previews.Count > 0) + List messagePreviewIds = []; + + if (list.Previews is { Count: > 0 }) { for (int i = 0; i < list.Previews.Count; i++) { - if (list.Previews[i] is long previewId) + if (list.Previews[i] is not long previewId) { - if (!messagePreviewIds.Contains(previewId)) - { - messagePreviewIds.Add(previewId); - } + continue; + } + + if (!messagePreviewIds.Contains(previewId)) + { + messagePreviewIds.Add(previewId); } } } - if (!configService.CurrentConfig.IgnoreOwnMessages || list.FromUser?.Id != currentUserId) + if (configService.CurrentConfig.IgnoreOwnMessages && list.FromUser?.Id == currentUserId) { - DateTime createdAt = list.CreatedAt ?? DateTime.Now; - await dbService.AddMessage(folder, list.Id, list.Text ?? "", list.Price ?? "0", - list.CanPurchaseReason == "opened" || - (list.CanPurchaseReason == "opened" && ((bool?)null ?? false)), false, - createdAt, - list.FromUser?.Id ?? int.MinValue); - messageCollection.MessageObjects.Add(list); - if (list.CanPurchaseReason != "opened" && list.Media != null && list.Media.Count > 0) + continue; + } + + DateTime createdAt = list.CreatedAt ?? DateTime.Now; + await dbService.AddMessage(folder, list.Id, list.Text ?? "", list.Price ?? "0", + list.CanPurchaseReason == "opened" || + (list.CanPurchaseReason == "opened" && ((bool?)null ?? false)), false, + createdAt, + list.FromUser?.Id ?? int.MinValue); + + messageCollection.MessageObjects.Add(list); + + if (list.CanPurchaseReason != "opened" && list.Media is { Count: > 0 }) + { + foreach (MessageEntities.Medium medium in list.Media ?? []) { - foreach (MessageEntities.Medium medium in list.Media ?? new List()) + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = messagePreviewIds.Contains(medium.Id); + + if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { - string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; - string? fullUrl = medium.Files?.Full?.Url; - bool isPreview = messagePreviewIds.Contains(medium.Id); - - if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } - - if (!messageCollection.Messages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, list.Id, fullUrl, null, null, null, - "Messages", mediaType, isPreview, false, null); - messageCollection.Messages.Add(medium.Id, fullUrl); - messageCollection.MessageMedia.Add(medium); - } + continue; } - else if (medium.CanView && - TryGetDrmInfo(medium.Files, out string manifestDash, - out string cloudFrontPolicy, out string cloudFrontSignature, - out string cloudFrontKeyPairId)) - { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } - if (!messageCollection.Messages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, list.Id, manifestDash, null, null, null, - "Messages", mediaType, isPreview, false, null); - messageCollection.Messages.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{list.Id}"); - messageCollection.MessageMedia.Add(medium); - } + if (!messageCollection.Messages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, list.Id, fullUrl, null, null, null, + "Messages", mediaType, isPreview, false, null); + messageCollection.Messages.Add(medium.Id, fullUrl); + messageCollection.MessageMedia.Add(medium); + } + } + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) + { + continue; + } + + if (!messageCollection.Messages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, list.Id, manifestDash, null, null, null, + "Messages", mediaType, isPreview, false, null); + messageCollection.Messages.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{list.Id}"); + messageCollection.MessageMedia.Add(medium); } } } - else if (messagePreviewIds.Count > 0) + } + else if (messagePreviewIds.Count > 0) + { + foreach (MessageEntities.Medium medium in list.Media ?? new List()) { - foreach (MessageEntities.Medium medium in list.Media ?? new List()) + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = messagePreviewIds.Contains(medium.Id); + + if (medium.CanView && !string.IsNullOrEmpty(fullUrl) && isPreview) { - string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; - string? fullUrl = medium.Files?.Full?.Url; - bool isPreview = messagePreviewIds.Contains(medium.Id); - - if (medium.CanView && !string.IsNullOrEmpty(fullUrl) && isPreview) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } - - if (!messageCollection.Messages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, list.Id, fullUrl, null, null, null, - "Messages", mediaType, isPreview, false, null); - messageCollection.Messages.Add(medium.Id, fullUrl); - messageCollection.MessageMedia.Add(medium); - } + continue; } - else if (medium.CanView && isPreview && - TryGetDrmInfo(medium.Files, out string manifestDash, - out string cloudFrontPolicy, out string cloudFrontSignature, - out string cloudFrontKeyPairId)) - { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } - if (!messageCollection.Messages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, list.Id, manifestDash, null, null, null, - "Messages", mediaType, isPreview, false, null); - messageCollection.Messages.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{list.Id}"); - messageCollection.MessageMedia.Add(medium); - } + if (!messageCollection.Messages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, list.Id, fullUrl, null, null, null, + "Messages", mediaType, isPreview, false, null); + messageCollection.Messages.Add(medium.Id, fullUrl); + messageCollection.MessageMedia.Add(medium); + } + } + else if (medium.CanView && isPreview && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) + { + continue; + } + + if (!messageCollection.Messages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, list.Id, manifestDash, null, null, null, + "Messages", mediaType, isPreview, false, null); + messageCollection.Messages.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{list.Id}"); + messageCollection.MessageMedia.Add(medium); } } } @@ -1614,8 +1637,8 @@ public class ApiService(IAuthService authService, IConfigService configService, try { PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection = new(); - const int postLimit = 50; - Dictionary getParams = new() { { "limit", postLimit.ToString() }, { "order", "desc" } }; + Dictionary getParams = + new() { { "limit", Constants.ApiPageSize.ToString() }, { "order", "desc" } }; int currentUserId = GetCurrentUserIdOrDefault(); string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); @@ -1623,104 +1646,113 @@ public class ApiService(IAuthService authService, IConfigService configService, DeserializeJson(body, s_mJsonSerializerSettings); MessageEntities.SingleMessage message = MessagesMapper.FromDto(messageDto); - if (!configService.CurrentConfig.IgnoreOwnMessages || message.FromUser?.Id != currentUserId) + if (configService.CurrentConfig.IgnoreOwnMessages && message.FromUser?.Id == currentUserId) { - DateTime createdAt = message.CreatedAt ?? DateTime.Now; - await dbService.AddMessage(folder, message.Id, message.Text ?? "", - message.Price?.ToString() ?? "0", true, false, - createdAt, - message.FromUser?.Id ?? int.MinValue); - singlePaidMessageCollection.SingleMessageObjects.Add(message); - List messagePreviewIds = new(); - if (message.Previews != null && message.Previews.Count > 0) + return singlePaidMessageCollection; + } + + DateTime createdAt = message.CreatedAt ?? DateTime.Now; + await dbService.AddMessage(folder, message.Id, message.Text ?? "", + message.Price?.ToString() ?? "0", true, false, + createdAt, + message.FromUser?.Id ?? int.MinValue); + + singlePaidMessageCollection.SingleMessageObjects.Add(message); + + List messagePreviewIds = []; + if (message.Previews is { Count: > 0 }) + { + for (int i = 0; i < message.Previews.Count; i++) { - for (int i = 0; i < message.Previews.Count; i++) + if (message.Previews[i] is not long previewId) { - if (message.Previews[i] is long previewId) - { - if (!messagePreviewIds.Contains(previewId)) - { - messagePreviewIds.Add(previewId); - } - } + continue; + } + + if (!messagePreviewIds.Contains(previewId)) + { + messagePreviewIds.Add(previewId); } } + } - if (message.Media != null && message.Media.Count > 0) + if (message.Media is not { Count: > 0 }) + { + return singlePaidMessageCollection; + } + + foreach (MessageEntities.Medium medium in message.Media) + { + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = messagePreviewIds.Contains(medium.Id); + + if (!isPreview && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { - foreach (MessageEntities.Medium medium in message.Media) + if (!IsMediaTypeDownloadEnabled(medium.Type)) { - string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; - string? fullUrl = medium.Files?.Full?.Url; - bool isPreview = messagePreviewIds.Contains(medium.Id); + continue; + } - if (!isPreview && medium.CanView && !string.IsNullOrEmpty(fullUrl)) - { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + if (!singlePaidMessageCollection.SingleMessages.TryAdd(medium.Id, fullUrl)) + { + continue; + } - if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, message.Id, fullUrl, null, null, null, - "Messages", mediaType, isPreview, false, null); - singlePaidMessageCollection.SingleMessages.Add(medium.Id, fullUrl); - singlePaidMessageCollection.SingleMessageMedia.Add(medium); - } - } - else if (isPreview && medium.CanView && !string.IsNullOrEmpty(fullUrl)) - { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + await dbService.AddMedia(folder, medium.Id, message.Id, fullUrl, null, null, null, + "Messages", mediaType, isPreview, false, null); + singlePaidMessageCollection.SingleMessageMedia.Add(medium); + } + else if (isPreview && medium.CanView && !string.IsNullOrEmpty(fullUrl)) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) + { + continue; + } - if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, message.Id, fullUrl, null, null, null, - "Messages", mediaType, isPreview, false, null); - singlePaidMessageCollection.PreviewSingleMessages.Add(medium.Id, fullUrl); - singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); - } - } - else if (!isPreview && medium.CanView && - TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, - out string cloudFrontSignature, out string cloudFrontKeyPairId)) - { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, message.Id, fullUrl, null, null, null, + "Messages", mediaType, isPreview, false, null); + singlePaidMessageCollection.PreviewSingleMessages.Add(medium.Id, fullUrl); + singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); + } + } + else if (!isPreview && medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId)) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) + { + continue; + } - if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, message.Id, manifestDash, null, null, null, - "Messages", mediaType, isPreview, false, null); - singlePaidMessageCollection.SingleMessages.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{message.Id}"); - singlePaidMessageCollection.SingleMessageMedia.Add(medium); - } - } - else if (isPreview && medium.CanView && - TryGetDrmInfo(medium.Files, out string previewManifestDash, - out string previewCloudFrontPolicy, out string previewCloudFrontSignature, - out string previewCloudFrontKeyPairId)) - { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, message.Id, manifestDash, null, null, null, + "Messages", mediaType, isPreview, false, null); + singlePaidMessageCollection.SingleMessages.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{message.Id}"); + singlePaidMessageCollection.SingleMessageMedia.Add(medium); + } + } + else if (isPreview && medium.CanView && + TryGetDrmInfo(medium.Files, out string previewManifestDash, + out string previewCloudFrontPolicy, out string previewCloudFrontSignature, + out string previewCloudFrontKeyPairId)) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) + { + continue; + } - if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, message.Id, previewManifestDash, null, null, - null, "Messages", mediaType, isPreview, false, null); - singlePaidMessageCollection.PreviewSingleMessages.Add(medium.Id, - $"{previewManifestDash},{previewCloudFrontPolicy},{previewCloudFrontSignature},{previewCloudFrontKeyPairId},{medium.Id},{message.Id}"); - singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); - } - } + if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, message.Id, previewManifestDash, null, null, + null, "Messages", mediaType, isPreview, false, null); + singlePaidMessageCollection.PreviewSingleMessages.Add(medium.Id, + $"{previewManifestDash},{previewCloudFrontPolicy},{previewCloudFrontSignature},{previewCloudFrontKeyPairId},{medium.Id},{message.Id}"); + singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } } } @@ -1753,10 +1785,9 @@ public class ApiService(IAuthService authService, IConfigService configService, try { PurchasedEntities.PaidMessageCollection paidMessageCollection = new(); - const int postLimit = 50; Dictionary getParams = new() { - { "limit", postLimit.ToString() }, + { "limit", Constants.ApiPageSize.ToString() }, { "order", "publish_date_desc" }, { "format", "infinite" }, { "author", username }, @@ -1780,7 +1811,7 @@ public class ApiService(IAuthService authService, IConfigService configService, HttpClient loopclient = GetHttpClient(); HttpRequestMessage looprequest = - new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); + new(HttpMethod.Get, $"{Constants.ApiUrl}{endpoint}{loopqueryParams}"); foreach (KeyValuePair keyValuePair in loopheaders) { @@ -1803,7 +1834,8 @@ public class ApiService(IAuthService authService, IConfigService configService, break; } - getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit); + getParams["offset"] = + Convert.ToString(Convert.ToInt32(getParams["offset"]) + Constants.ApiPageSize); } } @@ -1816,129 +1848,135 @@ public class ApiService(IAuthService authService, IConfigService configService, .OrderByDescending(p => p.PostedAt ?? p.CreatedAt)) { long fromUserId = purchase.FromUser?.Id ?? long.MinValue; - if (!configService.CurrentConfig.IgnoreOwnMessages || fromUserId != currentUserId) + if (configService.CurrentConfig.IgnoreOwnMessages && fromUserId == currentUserId) { - DateTime createdAt = purchase.PostedAt ?? purchase.CreatedAt ?? DateTime.Now; - await dbService.AddMessage(folder, purchase.Id, - purchase.Text ?? "", - purchase.Price ?? "0", true, false, createdAt, - fromUserId); + continue; + } - paidMessageCollection.PaidMessageObjects.Add(purchase); - if (purchase.Media != null && purchase.Media.Count > 0) + DateTime createdAt = purchase.PostedAt ?? purchase.CreatedAt ?? DateTime.Now; + await dbService.AddMessage(folder, purchase.Id, + purchase.Text ?? "", + purchase.Price ?? "0", true, false, createdAt, + fromUserId); + + paidMessageCollection.PaidMessageObjects.Add(purchase); + if (purchase.Media is not { Count: > 0 }) + { + continue; + } + + List previewIds = []; + if (purchase.Previews != null) + { + for (int i = 0; i < purchase.Previews.Count; i++) { - List previewids = new(); - if (purchase.Previews != null) + if (purchase.Previews[i] is not long previewId) { - for (int i = 0; i < purchase.Previews.Count; i++) - { - if (purchase.Previews[i] is long previewId) - { - if (!previewids.Contains(previewId)) - { - previewids.Add(previewId); - } - } - } - } - else if (purchase.Preview != null) - { - for (int i = 0; i < purchase.Preview.Count; i++) - { - if (purchase.Preview[i] is long previewId) - { - if (!previewids.Contains(previewId)) - { - previewids.Add(previewId); - } - } - } + continue; } - foreach (MessageEntities.Medium medium in purchase.Media) + if (!previewIds.Contains(previewId)) { - string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; - string? fullUrl = medium.Files?.Full?.Url; - bool isPreview = previewids.Contains(medium.Id); - - if (previewids.Count > 0) + previewIds.Add(previewId); + } + } + } + else if (purchase.Preview != null) + { + for (int i = 0; i < purchase.Preview.Count; i++) + { + if (purchase.Preview[i] is long previewId) + { + if (!previewIds.Contains(previewId)) { - bool has = previewids.Any(cus => cus.Equals(medium.Id)); - if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) - { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } - - if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, purchase.Id, - fullUrl, null, null, null, "Messages", mediaType, isPreview, false, - null); - paidMessageCollection.PaidMessages.Add(medium.Id, fullUrl); - paidMessageCollection.PaidMessageMedia.Add(medium); - } - } - else if (!has && medium.CanView && - TryGetDrmInfo(medium.Files, out string manifestDash, - out string cloudFrontPolicy, out string cloudFrontSignature, - out string cloudFrontKeyPairId)) - { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } - - if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, purchase.Id, - manifestDash, null, null, null, "Messages", mediaType, isPreview, false, - null); - paidMessageCollection.PaidMessages.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); - paidMessageCollection.PaidMessageMedia.Add(medium); - } - } + previewIds.Add(previewId); } - else + } + } + } + + foreach (MessageEntities.Medium medium in purchase.Media) + { + string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; + string? fullUrl = medium.Files?.Full?.Url; + bool isPreview = previewIds.Contains(medium.Id); + + if (previewIds.Count > 0) + { + bool has = previewIds.Any(cus => cus.Equals(medium.Id)); + if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) { - if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) - { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + continue; + } - if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, purchase.Id, - fullUrl, null, null, null, "Messages", mediaType, isPreview, false, - null); - paidMessageCollection.PaidMessages.Add(medium.Id, fullUrl); - paidMessageCollection.PaidMessageMedia.Add(medium); - } - } - else if (medium.CanView && - TryGetDrmInfo(medium.Files, out string manifestDash, - out string cloudFrontPolicy, out string cloudFrontSignature, - out string cloudFrontKeyPairId)) - { - if (!IsMediaTypeDownloadEnabled(medium.Type)) - { - continue; - } + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, purchase.Id, + fullUrl, null, null, null, "Messages", mediaType, isPreview, false, + null); + paidMessageCollection.PaidMessages.Add(medium.Id, fullUrl); + paidMessageCollection.PaidMessageMedia.Add(medium); + } + } + else if (!has && medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) + { + continue; + } - if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) - { - await dbService.AddMedia(folder, medium.Id, purchase.Id, - manifestDash, null, null, null, "Messages", mediaType, isPreview, false, - null); - paidMessageCollection.PaidMessages.Add(medium.Id, - $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); - paidMessageCollection.PaidMessageMedia.Add(medium); - } - } + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, purchase.Id, + manifestDash, null, null, null, "Messages", mediaType, isPreview, false, + null); + paidMessageCollection.PaidMessages.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + paidMessageCollection.PaidMessageMedia.Add(medium); + } + } + } + else + { + if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) + { + continue; + } + + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, purchase.Id, + fullUrl, null, null, null, "Messages", mediaType, isPreview, false, + null); + paidMessageCollection.PaidMessages.Add(medium.Id, fullUrl); + paidMessageCollection.PaidMessageMedia.Add(medium); + } + } + else if (medium.CanView && + TryGetDrmInfo(medium.Files, out string manifestDash, + out string cloudFrontPolicy, out string cloudFrontSignature, + out string cloudFrontKeyPairId)) + { + if (!IsMediaTypeDownloadEnabled(medium.Type)) + { + continue; + } + + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.Id)) + { + await dbService.AddMedia(folder, medium.Id, purchase.Id, + manifestDash, null, null, null, "Messages", mediaType, isPreview, false, + null); + paidMessageCollection.PaidMessages.Add(medium.Id, + $"{manifestDash},{cloudFrontPolicy},{cloudFrontSignature},{cloudFrontKeyPairId},{medium.Id},{purchase.Id}"); + paidMessageCollection.PaidMessageMedia.Add(medium); } } } @@ -1969,10 +2007,9 @@ public class ApiService(IAuthService authService, IConfigService configService, try { Dictionary purchasedTabUsers = new(); - const int postLimit = 50; Dictionary getParams = new() { - { "limit", postLimit.ToString() }, + { "limit", Constants.ApiPageSize.ToString() }, { "order", "publish_date_desc" }, { "format", "infinite" }, { "skip_users", "all" } @@ -1998,7 +2035,7 @@ public class ApiService(IAuthService authService, IConfigService configService, HttpClient loopclient = GetHttpClient(); HttpRequestMessage looprequest = - new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); + new(HttpMethod.Get, $"{Constants.ApiUrl}{endpoint}{loopqueryParams}"); foreach (KeyValuePair keyValuePair in loopheaders) { @@ -2020,7 +2057,8 @@ public class ApiService(IAuthService authService, IConfigService configService, break; } - getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit); + getParams["offset"] = + Convert.ToString(Convert.ToInt32(getParams["offset"]) + Constants.ApiPageSize); } } @@ -2039,10 +2077,7 @@ public class ApiService(IAuthService authService, IConfigService configService, string? matchedUsername = users.FirstOrDefault(x => x.Value == fromUserId).Key; if (!string.IsNullOrEmpty(matchedUsername)) { - if (!purchasedTabUsers.ContainsKey(matchedUsername)) - { - purchasedTabUsers.Add(matchedUsername, fromUserId); - } + purchasedTabUsers.TryAdd(matchedUsername, fromUserId); } else if (!purchasedTabUsers.ContainsKey($"Deleted User - {fromUserId}")) { @@ -2064,9 +2099,9 @@ public class ApiService(IAuthService authService, IConfigService configService, Log.Debug("Content creator not longer exists - {0}", fromUserId); } - else if (!purchasedTabUsers.ContainsKey(fetchedUsername)) + else { - purchasedTabUsers.Add(fetchedUsername, fromUserId); + purchasedTabUsers.TryAdd(fetchedUsername, fromUserId); } } } @@ -2139,10 +2174,9 @@ public class ApiService(IAuthService authService, IConfigService configService, { Dictionary> userPurchases = new(); List purchasedTabCollections = []; - const int postLimit = 50; Dictionary getParams = new() { - { "limit", postLimit.ToString() }, + { "limit", Constants.ApiPageSize.ToString() }, { "order", "publish_date_desc" }, { "format", "infinite" }, { "skip_users", "all" } @@ -2163,7 +2197,7 @@ public class ApiService(IAuthService authService, IConfigService configService, HttpClient loopclient = GetHttpClient(); HttpRequestMessage looprequest = - new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); + new(HttpMethod.Get, $"{Constants.ApiUrl}{endpoint}{loopqueryParams}"); foreach (KeyValuePair keyValuePair in loopheaders) { @@ -2185,7 +2219,8 @@ public class ApiService(IAuthService authService, IConfigService configService, break; } - getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit); + getParams["offset"] = + Convert.ToString(Convert.ToInt32(getParams["offset"]) + Constants.ApiPageSize); } } @@ -2274,10 +2309,12 @@ public class ApiService(IAuthService authService, IConfigService configService, await dbService.AddPost(path, purchase.Id, purchase.Text ?? "", purchase.Price ?? "0", - purchase.Price != null && purchase.IsOpened, + purchase is { Price: not null, IsOpened: true }, isArchived, createdAt); + purchasedTabCollection.PaidPosts.PaidPostObjects.Add(purchase); + foreach (MessageEntities.Medium medium in purchase.Media) { if (!IsMediaTypeDownloadEnabled(medium.Type)) @@ -2356,18 +2393,18 @@ public class ApiService(IAuthService authService, IConfigService configService, messageCreatedAt, fromUserId); purchasedTabCollection.PaidMessages.PaidMessageObjects.Add(purchase); - if (purchase.Media != null && purchase.Media.Count > 0) + if (purchase.Media is { Count: > 0 }) { - List paidMessagePreviewids = new(); + List paidMessagePreviewIds = []; if (purchase.Previews != null) { for (int i = 0; i < purchase.Previews.Count; i++) { if (purchase.Previews[i] is long previewId) { - if (!paidMessagePreviewids.Contains(previewId)) + if (!paidMessagePreviewIds.Contains(previewId)) { - paidMessagePreviewids.Add(previewId); + paidMessagePreviewIds.Add(previewId); } } } @@ -2378,9 +2415,9 @@ public class ApiService(IAuthService authService, IConfigService configService, { if (purchase.Preview[i] is long previewId) { - if (!paidMessagePreviewids.Contains(previewId)) + if (!paidMessagePreviewIds.Contains(previewId)) { - paidMessagePreviewids.Add(previewId); + paidMessagePreviewIds.Add(previewId); } } } @@ -2388,12 +2425,12 @@ public class ApiService(IAuthService authService, IConfigService configService, foreach (MessageEntities.Medium medium in purchase.Media) { - if (paidMessagePreviewids.Count > 0) + if (paidMessagePreviewIds.Count > 0) { string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; string? fullUrl = medium.Files?.Full?.Url; - bool isPreview = paidMessagePreviewids.Contains(medium.Id); - bool has = paidMessagePreviewids.Any(cus => cus.Equals(medium.Id)); + bool isPreview = paidMessagePreviewIds.Contains(medium.Id); + bool has = paidMessagePreviewIds.Any(cus => cus.Equals(medium.Id)); if (!has && medium.CanView && !string.IsNullOrEmpty(fullUrl)) { if (!IsMediaTypeDownloadEnabled(medium.Type)) @@ -2440,7 +2477,7 @@ public class ApiService(IAuthService authService, IConfigService configService, { string mediaType = ResolveMediaType(medium.Type) ?? string.Empty; string? fullUrl = medium.Files?.Full?.Url; - bool isPreview = paidMessagePreviewids.Contains(medium.Id); + bool isPreview = paidMessagePreviewIds.Contains(medium.Id); if (medium.CanView && !string.IsNullOrEmpty(fullUrl)) { @@ -2729,9 +2766,9 @@ public class ApiService(IAuthService authService, IConfigService configService, Dictionary headers = GetDynamicHeaders($"/api2/v2{endpoint}", queryParams); - HttpRequestMessage request = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{queryParams}"); + HttpRequestMessage request = new(HttpMethod.Get, $"{Constants.ApiUrl}{endpoint}{queryParams}"); - Log.Debug($"Full request URL: {Constants.API_URL}{endpoint}{queryParams}"); + Log.Debug($"Full request URL: {Constants.ApiUrl}{endpoint}{queryParams}"); foreach (KeyValuePair keyValuePair in headers) { @@ -2756,7 +2793,7 @@ public class ApiService(IAuthService authService, IConfigService configService, private HttpClient GetHttpClient() { HttpClient client = new(); - if (configService.CurrentConfig.Timeout != null && configService.CurrentConfig.Timeout > 0) + if (configService.CurrentConfig.Timeout is > 0) { client.Timeout = TimeSpan.FromSeconds(configService.CurrentConfig.Timeout.Value); } diff --git a/OF DL.Core/Services/DbService.cs b/OF DL.Core/Services/DbService.cs index 99d51ef..872118d 100644 --- a/OF DL.Core/Services/DbService.cs +++ b/OF DL.Core/Services/DbService.cs @@ -572,11 +572,13 @@ public class DbService(IConfigService configService) : IDbService while (await reader.ReadAsync()) { - if (reader["name"].ToString() == "record_created_at") + if (reader["name"].ToString() != "record_created_at") { - columnExists = true; - break; + continue; } + + columnExists = true; + break; } if (!columnExists) diff --git a/OF DL.Core/Services/DownloadOrchestrationService.cs b/OF DL.Core/Services/DownloadOrchestrationService.cs index 25497f5..8e7006c 100644 --- a/OF DL.Core/Services/DownloadOrchestrationService.cs +++ b/OF DL.Core/Services/DownloadOrchestrationService.cs @@ -18,10 +18,10 @@ public class DownloadOrchestrationService( /// /// Gets the list of paid post media IDs to avoid duplicates. /// - public List PaidPostIds { get; } = new(); + public List PaidPostIds { get; } = []; /// - /// Retrieves the available users and lists based on current configuration. + /// Retrieves the available users and lists based on the current configuration. /// /// A result containing users, lists, and any errors. public async Task GetAvailableUsersAsync() @@ -235,9 +235,9 @@ public class DownloadOrchestrationService( { eventHandler.OnMessage("Getting Stories"); Dictionary? tempStories = await apiService.GetMedia(MediaType.Stories, - $"/users/{userId}/stories", null, path, PaidPostIds); + $"/users/{userId}/stories", null, path); - if (tempStories != null && tempStories.Count > 0) + if (tempStories is { Count: > 0 }) { eventHandler.OnContentFound("Stories", tempStories.Count, tempStories.Count); @@ -263,9 +263,9 @@ public class DownloadOrchestrationService( { eventHandler.OnMessage("Getting Highlights"); Dictionary? tempHighlights = await apiService.GetMedia(MediaType.Highlights, - $"/users/{userId}/stories/highlights", null, path, PaidPostIds); + $"/users/{userId}/stories/highlights", null, path); - if (tempHighlights != null && tempHighlights.Count > 0) + if (tempHighlights is { Count: > 0 }) { eventHandler.OnContentFound("Highlights", tempHighlights.Count, tempHighlights.Count); diff --git a/OF DL.Core/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs index e1dd837..631fb20 100644 --- a/OF DL.Core/Services/DownloadService.cs +++ b/OF DL.Core/Services/DownloadService.cs @@ -500,15 +500,9 @@ public class DownloadService( client.DefaultRequestHeaders.Add("User-Agent", auth.UserAgent); using HttpResponseMessage response = await client.GetAsync(mpdUrl, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) - { - if (response.Content.Headers.LastModified != null) - { - return response.Content.Headers.LastModified.Value.DateTime; - } - } - - return DateTime.Now; + return response is { IsSuccessStatusCode: true, Content.Headers.LastModified: not null } + ? response.Content.Headers.LastModified.Value.DateTime + : DateTime.Now; } /// @@ -842,7 +836,7 @@ public class DownloadService( /// Author info. /// Known users map. /// True when the media is newly downloaded. - public async Task DownloadMedia(string url, string folder, long mediaId, string apiType, + private async Task DownloadMedia(string url, string folder, long mediaId, string apiType, IProgressReporter progressReporter, string path, string? filenameFormat, object? postInfo, object? postMedia, object? author, Dictionary users) @@ -876,7 +870,7 @@ public class DownloadService( /// Author info. /// Known users map. /// True when the media is newly downloaded. - public async Task DownloadDrmVideo(string policy, string signature, string kvp, string url, + private async Task DownloadDrmVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long mediaId, string apiType, IProgressReporter progressReporter, string path, string? filenameFormat, object? postInfo, object? postMedia, @@ -1028,7 +1022,7 @@ public class DownloadService( Log.Debug($"Calling DownloadHighlights - {username}"); Dictionary? highlights = await apiService.GetMedia(MediaType.Highlights, - $"/users/{userId}/stories/highlights", null, path, paidPostIds.ToList()); + $"/users/{userId}/stories/highlights", null, path); if (highlights == null || highlights.Count == 0) { @@ -1091,7 +1085,7 @@ public class DownloadService( Log.Debug($"Calling DownloadStories - {username}"); Dictionary? stories = await apiService.GetMedia(MediaType.Stories, $"/users/{userId}/stories", - null, path, paidPostIds.ToList()); + null, path); if (stories == null || stories.Count == 0) { diff --git a/OF DL.Core/Services/IApiService.cs b/OF DL.Core/Services/IApiService.cs index 0359153..1a293bc 100644 --- a/OF DL.Core/Services/IApiService.cs +++ b/OF DL.Core/Services/IApiService.cs @@ -39,8 +39,7 @@ public interface IApiService /// /// Retrieves media URLs for stories or highlights. /// - Task?> GetMedia(MediaType mediaType, string endpoint, string? username, string folder, - List paidPostIds); + Task?> GetMedia(MediaType mediaType, string endpoint, string? username, string folder); /// /// Retrieves paid posts and their media. @@ -125,7 +124,7 @@ public interface IApiService Task?> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions); /// - /// Retrieves a decryption key via the OFDL fallback service. + /// Retrieves a decryption key via the OF DL fallback service. /// Task GetDecryptionKeyOfdl(Dictionary drmHeaders, string licenceUrl, string pssh); } diff --git a/OF DL.Core/Services/IAuthService.cs b/OF DL.Core/Services/IAuthService.cs index 4ebe1da..432d13f 100644 --- a/OF DL.Core/Services/IAuthService.cs +++ b/OF DL.Core/Services/IAuthService.cs @@ -11,7 +11,7 @@ public interface IAuthService Auth? CurrentAuth { get; set; } /// - /// Loads authentication data from disk. + /// Loads authentication data from the disk. /// Task LoadFromFileAsync(string filePath = "auth.json"); diff --git a/OF DL.Core/Services/IDownloadService.cs b/OF DL.Core/Services/IDownloadService.cs index f41b42f..598b09b 100644 --- a/OF DL.Core/Services/IDownloadService.cs +++ b/OF DL.Core/Services/IDownloadService.cs @@ -20,23 +20,6 @@ public interface IDownloadService Task ProcessMediaDownload(string folder, long mediaId, string apiType, string url, string path, string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); - /// - /// Downloads a single media item. - /// - Task DownloadMedia(string url, string folder, long mediaId, string apiType, - IProgressReporter progressReporter, string path, - string? filenameFormat, object? postInfo, object? postMedia, - object? author, Dictionary users); - - /// - /// Downloads a DRM-protected video. - /// - Task DownloadDrmVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long mediaId, string apiType, - IProgressReporter progressReporter, string path, - string? filenameFormat, object? postInfo, object? postMedia, - object? author, Dictionary users); - /// /// Retrieves decryption information for a DRM media item. /// diff --git a/OF DL.Core/Services/IProgressReporter.cs b/OF DL.Core/Services/IProgressReporter.cs index d36d1f3..dc59aca 100644 --- a/OF DL.Core/Services/IProgressReporter.cs +++ b/OF DL.Core/Services/IProgressReporter.cs @@ -11,10 +11,4 @@ public interface IProgressReporter /// /// The amount to increment progress by void ReportProgress(long increment); - - /// - /// Reports a status message (optional for implementations). - /// - /// The status message to report - void ReportStatus(string message); } diff --git a/OF DL.Core/Utils/HttpUtil.cs b/OF DL.Core/Utils/HttpUtil.cs index bd0203a..5982964 100644 --- a/OF DL.Core/Utils/HttpUtil.cs +++ b/OF DL.Core/Utils/HttpUtil.cs @@ -111,13 +111,13 @@ internal class HttpUtil int retryCount = 0; - while (retryCount < Constants.WIDEVINE_MAX_RETRIES && response.StatusCode == HttpStatusCode.TooManyRequests) + while (retryCount < Constants.WidevineMaxRetries && response.StatusCode == HttpStatusCode.TooManyRequests) { // // We've hit a rate limit, so we should wait before retrying. // int retryAfterSeconds = - Constants.WIDEVINE_RETRY_DELAY * (retryCount + 1); // Default retry time. Increases with each retry. + Constants.WidevineRetryDelay * (retryCount + 1); // Default retry time. Increases with each retry. if (response.Headers.RetryAfter != null && response.Headers.RetryAfter.Delta.HasValue) { if (response.Headers.RetryAfter.Delta.Value.TotalSeconds > 0) diff --git a/OF DL/CLI/SpectreProgressReporter.cs b/OF DL/CLI/SpectreProgressReporter.cs index a9fbd4a..3a334d6 100644 --- a/OF DL/CLI/SpectreProgressReporter.cs +++ b/OF DL/CLI/SpectreProgressReporter.cs @@ -11,10 +11,4 @@ public class SpectreProgressReporter(ProgressTask task) : IProgressReporter private readonly ProgressTask _task = task ?? throw new ArgumentNullException(nameof(task)); public void ReportProgress(long increment) => _task.Increment(increment); - - public void ReportStatus(string message) - { - // Optionally update task description or handle status messages - // For now, we'll leave this empty as the task description is set when creating the progress bar - } } From 4afa10186c1e463779d443ca86a038a15da80c48 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 12:03:34 -0600 Subject: [PATCH 43/55] Add generated docs to the .gitignore file --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 822cbd2..4d0320d 100644 --- a/.gitignore +++ b/.gitignore @@ -370,4 +370,7 @@ FodyWeavers.xsd !.gitea-actions/**/node_modules/ # venv -venv/ \ No newline at end of file +venv/ + +# Generated docs +/site From 9794eacbc9df3adeb899f41f819a716fc09febeb Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 12:07:18 -0600 Subject: [PATCH 44/55] Update AGENTS.md to include testing and execution commands --- AGENTS.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 1803141..ee45881 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,34 @@ most important change points. directory. - `users.db` is stored at the working directory root. +## Execution and Testing + +- .NET SDK: 8.x (`net8.0` for all projects). +- Build from the repo root: + +```bash +dotnet build OF DL.sln +``` + +- Run from source (runtime files are read from the current working directory): + +```bash +dotnet run --project "OF DL/OF DL.csproj" +``` + +- If you want a local `rules.json` fallback, run from `OF DL/` or copy `OF DL/rules.json` into your working directory. +- Run tests: + +```bash +dotnet test "OF DL.Tests/OF DL.Tests.csproj" +``` + +- Optional coverage (coverlet collector): + +```bash +dotnet test "OF DL.Tests/OF DL.Tests.csproj" --collect:"XPlat Code Coverage" +``` + ## Authentication Flow - Auth data is stored in `auth.json` using the `Auth` model in `OF DL.Core/Models/Auth/Auth.cs`. From d9825ae62b9518a2c3097398091cd0ce3b78b429 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 12:44:52 -0600 Subject: [PATCH 45/55] Add AI generated tests for ApiService and DownloadService --- OF DL.Core/Models/Config/Config.cs | 38 ++-- OF DL.Core/Services/StartupService.cs | 8 +- OF DL.Core/Utils/XmlUtils.cs | 4 +- OF DL.Tests/Services/ApiServiceTests.cs | 151 ++++++++++++++ OF DL.Tests/Services/DownloadServiceTests.cs | 159 ++++++++++++++ OF DL.Tests/Services/TestDoubles.cs | 208 +++++++++++++++++++ 6 files changed, 541 insertions(+), 27 deletions(-) create mode 100644 OF DL.Tests/Services/ApiServiceTests.cs create mode 100644 OF DL.Tests/Services/DownloadServiceTests.cs create mode 100644 OF DL.Tests/Services/TestDoubles.cs diff --git a/OF DL.Core/Models/Config/Config.cs b/OF DL.Core/Models/Config/Config.cs index 50323d8..c6d53fc 100644 --- a/OF DL.Core/Models/Config/Config.cs +++ b/OF DL.Core/Models/Config/Config.cs @@ -31,32 +31,32 @@ public class Config : IFileNameFormatConfig [ToggleableConfig] public bool DownloadAudios { get; set; } = true; - [ToggleableConfig] public bool IncludeExpiredSubscriptions { get; set; } = false; + [ToggleableConfig] public bool IncludeExpiredSubscriptions { get; set; } - [ToggleableConfig] public bool IncludeRestrictedSubscriptions { get; set; } = false; + [ToggleableConfig] public bool IncludeRestrictedSubscriptions { get; set; } - [ToggleableConfig] public bool SkipAds { get; set; } = false; + [ToggleableConfig] public bool SkipAds { get; set; } public string? DownloadPath { get; set; } = ""; - [ToggleableConfig] public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } = false; + [ToggleableConfig] public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } public int? Timeout { get; set; } = -1; - [ToggleableConfig] public bool FolderPerPaidPost { get; set; } = false; + [ToggleableConfig] public bool FolderPerPaidPost { get; set; } - [ToggleableConfig] public bool FolderPerPost { get; set; } = false; + [ToggleableConfig] public bool FolderPerPost { get; set; } - [ToggleableConfig] public bool FolderPerPaidMessage { get; set; } = false; + [ToggleableConfig] public bool FolderPerPaidMessage { get; set; } - [ToggleableConfig] public bool FolderPerMessage { get; set; } = false; + [ToggleableConfig] public bool FolderPerMessage { get; set; } - [ToggleableConfig] public bool LimitDownloadRate { get; set; } = false; + [ToggleableConfig] public bool LimitDownloadRate { get; set; } public int DownloadLimitInMbPerSec { get; set; } = 4; // Indicates if you want to download only on specific dates. - [ToggleableConfig] public bool DownloadOnlySpecificDates { get; set; } = false; + [ToggleableConfig] public bool DownloadOnlySpecificDates { get; set; } // This enum will define if we want data from before or after the CustomDate. [JsonConverter(typeof(StringEnumConverter))] @@ -66,37 +66,37 @@ public class Config : IFileNameFormatConfig [JsonConverter(typeof(ShortDateConverter))] public DateTime? CustomDate { get; set; } = null; - [ToggleableConfig] public bool ShowScrapeSize { get; set; } = false; + [ToggleableConfig] public bool ShowScrapeSize { get; set; } - [ToggleableConfig] public bool DownloadPostsIncrementally { get; set; } = false; + [ToggleableConfig] public bool DownloadPostsIncrementally { get; set; } - public bool NonInteractiveMode { get; set; } = false; + public bool NonInteractiveMode { get; set; } public string NonInteractiveModeListName { get; set; } = ""; - [ToggleableConfig] public bool NonInteractiveModePurchasedTab { get; set; } = false; + [ToggleableConfig] public bool NonInteractiveModePurchasedTab { get; set; } public string? FFmpegPath { get; set; } = ""; - [ToggleableConfig] public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } = false; + [ToggleableConfig] public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } public Dictionary CreatorConfigs { get; set; } = new(); - [ToggleableConfig] public bool DownloadDuplicatedMedia { get; set; } = false; + [ToggleableConfig] public bool DownloadDuplicatedMedia { get; set; } public string IgnoredUsersListName { get; set; } = ""; [JsonConverter(typeof(StringEnumConverter))] public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error; - [ToggleableConfig] public bool IgnoreOwnMessages { get; set; } = false; + [ToggleableConfig] public bool IgnoreOwnMessages { get; set; } - [ToggleableConfig] public bool DisableBrowserAuth { get; set; } = false; + [ToggleableConfig] public bool DisableBrowserAuth { get; set; } [JsonConverter(typeof(StringEnumConverter))] public VideoResolution DownloadVideoResolution { get; set; } = VideoResolution.source; // When enabled, post/message text is stored as-is without XML stripping. - [ToggleableConfig] public bool DisableTextSanitization { get; set; } = false; + [ToggleableConfig] public bool DisableTextSanitization { get; set; } public string? PaidPostFileNameFormat { get; set; } = ""; public string? PostFileNameFormat { get; set; } = ""; diff --git a/OF DL.Core/Services/StartupService.cs b/OF DL.Core/Services/StartupService.cs index ae0e856..c5e4570 100644 --- a/OF DL.Core/Services/StartupService.cs +++ b/OF DL.Core/Services/StartupService.cs @@ -131,6 +131,7 @@ public class StartupService(IConfigService configService, IAuthService authServi 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 @@ -221,12 +222,7 @@ public class StartupService(IConfigService configService, IAuthService authServi private static bool ValidateFilePath(string path) { char[] invalidChars = Path.GetInvalidPathChars(); - if (path.Any(c => invalidChars.Contains(c))) - { - return false; - } - - return File.Exists(path); + return !path.Any(c => invalidChars.Contains(c)) && File.Exists(path); } private static string? GetFullPath(string filename) diff --git a/OF DL.Core/Utils/XmlUtils.cs b/OF DL.Core/Utils/XmlUtils.cs index 4f9bbf6..3b57c0f 100644 --- a/OF DL.Core/Utils/XmlUtils.cs +++ b/OF DL.Core/Utils/XmlUtils.cs @@ -4,8 +4,8 @@ namespace OF_DL.Utils; internal static class XmlUtils { - // When true, return original text without parsing/stripping. - public static bool Passthrough { get; set; } = false; + // When true, return the original text without parsing/stripping. + public static bool Passthrough { get; set; } public static string EvaluateInnerText(string xmlValue) { diff --git a/OF DL.Tests/Services/ApiServiceTests.cs b/OF DL.Tests/Services/ApiServiceTests.cs new file mode 100644 index 0000000..af711bf --- /dev/null +++ b/OF DL.Tests/Services/ApiServiceTests.cs @@ -0,0 +1,151 @@ +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using OF_DL.Models; +using OF_DL.Models.Config; +using OF_DL.Models.OfdlApi; +using OF_DL.Services; + +namespace OF_DL.Tests.Services; + +public class ApiServiceTests +{ + [Fact] + public void GetDynamicHeaders_ReturnsSignedHeaders() + { + FakeAuthService authService = new() + { + CurrentAuth = new Auth + { + UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_cookie=abc;" + } + }; + ApiService service = CreateService(authService); + DynamicRules rules = new() + { + AppToken = "app-token", + StaticParam = "static", + Prefix = "prefix", + Suffix = "suffix", + ChecksumConstant = 7, + ChecksumIndexes = [0, 5, 10, 15] + }; + + using DynamicRulesCacheScope _ = new(rules); + + Dictionary headers = service.GetDynamicHeaders("/api2/v2/users", "?limit=1"); + + Assert.Equal("application/json, text/plain", headers["accept"]); + Assert.Equal("app-token", headers["app-token"]); + Assert.Equal("auth_cookie=abc;", headers["cookie"]); + Assert.Equal("unit-test-agent", headers["user-agent"]); + Assert.Equal("xbc-token", headers["x-bc"]); + Assert.Equal("123", headers["user-id"]); + Assert.True(long.TryParse(headers["time"], out long timestamp)); + + string expectedSign = BuildSign(rules, timestamp, "/api2/v2/users?limit=1", "123"); + Assert.Equal(expectedSign, headers["sign"]); + } + + [Fact] + public void GetDynamicHeaders_ThrowsWhenRulesInvalid() + { + FakeAuthService authService = new() + { + CurrentAuth = new Auth + { + UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_cookie=abc;" + } + }; + ApiService service = CreateService(authService); + DynamicRules rules = new() + { + AppToken = null, + StaticParam = "static", + Prefix = null, + Suffix = "suffix", + ChecksumConstant = null, + ChecksumIndexes = [] + }; + + using DynamicRulesCacheScope _ = new(rules); + + Exception ex = Assert.Throws(() => service.GetDynamicHeaders("/api2/v2/users", "?limit=1")); + Assert.Contains("Invalid dynamic rules", ex.Message); + } + + [Fact] + public void GetDynamicHeaders_ThrowsWhenAuthMissingFields() + { + FakeAuthService authService = new() + { + CurrentAuth = new Auth + { + UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = null + } + }; + ApiService service = CreateService(authService); + DynamicRules rules = new() + { + AppToken = "app-token", + StaticParam = "static", + Prefix = "prefix", + Suffix = "suffix", + ChecksumConstant = 1, + ChecksumIndexes = [0] + }; + + using DynamicRulesCacheScope _ = new(rules); + + Exception ex = Assert.Throws(() => service.GetDynamicHeaders("/api2/v2/users", "?limit=1")); + Assert.Contains("Auth service is missing required fields", ex.Message); + } + + private static ApiService CreateService(FakeAuthService authService) => + new(authService, new FakeConfigService(new Config()), new FakeDbService()); + + private static string BuildSign(DynamicRules rules, long timestamp, string pathWithQuery, string userId) + { + string input = $"{rules.StaticParam}\n{timestamp}\n{pathWithQuery}\n{userId}"; + byte[] hashBytes = SHA1.HashData(Encoding.UTF8.GetBytes(input)); + string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + + Assert.NotNull(rules.ChecksumConstant); + + int checksum = rules.ChecksumIndexes.Aggregate(0, (current, index) => current + hashString[index]) + + rules.ChecksumConstant.Value; + string checksumHex = checksum.ToString("X").ToLowerInvariant(); + + return $"{rules.Prefix}:{hashString}:{checksumHex}:{rules.Suffix}"; + } + + private sealed class DynamicRulesCacheScope : IDisposable + { + private static readonly FieldInfo s_rulesField = + typeof(ApiService).GetField("s_cachedDynamicRules", BindingFlags.NonPublic | BindingFlags.Static) ?? + throw new InvalidOperationException("Unable to access cached rules field."); + + private static readonly FieldInfo s_expirationField = + typeof(ApiService).GetField("s_cachedDynamicRulesExpiration", + BindingFlags.NonPublic | BindingFlags.Static) ?? + throw new InvalidOperationException("Unable to access cached rules expiration field."); + + private readonly object? _priorRules; + private readonly DateTime? _priorExpiration; + + public DynamicRulesCacheScope(DynamicRules rules) + { + _priorRules = s_rulesField.GetValue(null); + _priorExpiration = (DateTime?)s_expirationField.GetValue(null); + + s_rulesField.SetValue(null, rules); + s_expirationField.SetValue(null, DateTime.UtcNow.AddHours(1)); + } + + public void Dispose() + { + s_rulesField.SetValue(null, _priorRules); + s_expirationField.SetValue(null, _priorExpiration); + } + } +} diff --git a/OF DL.Tests/Services/DownloadServiceTests.cs b/OF DL.Tests/Services/DownloadServiceTests.cs new file mode 100644 index 0000000..87d4a38 --- /dev/null +++ b/OF DL.Tests/Services/DownloadServiceTests.cs @@ -0,0 +1,159 @@ +using OF_DL.Models.Config; +using OF_DL.Models.Downloads; +using OF_DL.Services; + +namespace OF_DL.Tests.Services; + +public class DownloadServiceTests +{ + [Fact] + public async Task ProcessMediaDownload_RenamesServerFileAndUpdatesDb_WhenNotDownloadedButServerFileExists() + { + using TempFolder temp = new(); + string folder = NormalizeFolder(Path.Combine(temp.Path, "creator")); + string path = "/Posts/Free"; + string url = "https://example.com/image.jpg"; + string serverFilename = "server"; + string resolvedFilename = "custom"; + string serverFilePath = $"{folder}{path}/{serverFilename}.jpg"; + Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException()); + await File.WriteAllTextAsync(serverFilePath, "abc"); + + FakeDbService dbService = new() { CheckDownloadedResult = false }; + FakeConfigService configService = new(new Config { ShowScrapeSize = false }); + DownloadService service = CreateService(configService, dbService); + ProgressRecorder progress = new(); + + bool isNew = await service.ProcessMediaDownload(folder, 1, "Posts", url, path, serverFilename, + resolvedFilename, ".jpg", progress); + + string renamedPath = $"{folder}{path}/{resolvedFilename}.jpg"; + Assert.False(isNew); + Assert.False(File.Exists(serverFilePath)); + Assert.True(File.Exists(renamedPath)); + Assert.NotNull(dbService.LastUpdateMedia); + Assert.Equal($"{folder}{path}", dbService.LastUpdateMedia.Value.directory); + Assert.Equal("custom.jpg", dbService.LastUpdateMedia.Value.filename); + Assert.Equal(new FileInfo(renamedPath).Length, dbService.LastUpdateMedia.Value.size); + Assert.Equal(1, progress.Total); + } + + [Fact] + public async Task ProcessMediaDownload_RenamesExistingFile_WhenDownloadedAndCustomFormatEnabled() + { + using TempFolder temp = new(); + string folder = NormalizeFolder(Path.Combine(temp.Path, "creator")); + string path = "/Posts/Free"; + string url = "https://example.com/image.jpg"; + string serverFilename = "server"; + string resolvedFilename = "custom"; + string serverFilePath = $"{folder}{path}/{serverFilename}.jpg"; + Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException()); + await File.WriteAllTextAsync(serverFilePath, "abc"); + + FakeDbService dbService = new() { CheckDownloadedResult = true, StoredFileSize = 123 }; + FakeConfigService configService = + new(new Config { ShowScrapeSize = false, RenameExistingFilesWhenCustomFormatIsSelected = true }); + DownloadService service = CreateService(configService, dbService); + ProgressRecorder progress = new(); + + bool isNew = await service.ProcessMediaDownload(folder, 1, "Posts", url, path, serverFilename, + resolvedFilename, ".jpg", progress); + + string renamedPath = $"{folder}{path}/{resolvedFilename}.jpg"; + Assert.False(isNew); + Assert.False(File.Exists(serverFilePath)); + Assert.True(File.Exists(renamedPath)); + Assert.NotNull(dbService.LastUpdateMedia); + Assert.Equal("custom.jpg", dbService.LastUpdateMedia.Value.filename); + Assert.Equal(123, dbService.LastUpdateMedia.Value.size); + Assert.Equal(1, progress.Total); + } + + [Fact] + public async Task GetDecryptionInfo_UsesOfdlWhenCdmMissing() + { + FakeApiService apiService = new(); + DownloadService service = + CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService); + + (string decryptionKey, DateTime lastModified)? result = await service.GetDecryptionInfo( + "https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post", + true, false); + + Assert.NotNull(result); + Assert.Equal("ofdl-key", result.Value.decryptionKey); + Assert.Equal(apiService.LastModifiedToReturn, result.Value.lastModified); + Assert.True(apiService.OfdlCalled); + Assert.False(apiService.CdmCalled); + } + + [Fact] + public async Task GetDecryptionInfo_UsesCdmWhenAvailable() + { + FakeApiService apiService = new(); + DownloadService service = + CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService); + + (string decryptionKey, DateTime lastModified)? result = await service.GetDecryptionInfo( + "https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post", + false, false); + + Assert.NotNull(result); + Assert.Equal("cdm-key", result.Value.decryptionKey); + Assert.Equal(apiService.LastModifiedToReturn, result.Value.lastModified); + Assert.True(apiService.CdmCalled); + Assert.False(apiService.OfdlCalled); + } + + [Fact] + public async Task DownloadHighlights_ReturnsZeroWhenNoMedia() + { + FakeApiService apiService = new() { MediaToReturn = new Dictionary() }; + DownloadService service = + CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService); + + DownloadResult result = await service.DownloadHighlights("user", 1, "/tmp/creator", new HashSet(), + new ProgressRecorder()); + + Assert.Equal(0, result.TotalCount); + Assert.Equal(0, result.NewDownloads); + Assert.Equal(0, result.ExistingDownloads); + Assert.Equal("Highlights", result.MediaType); + Assert.True(result.Success); + } + + [Fact] + public async Task DownloadHighlights_CountsExistingWhenAlreadyDownloaded() + { + using TempFolder temp = new(); + string folder = NormalizeFolder(Path.Combine(temp.Path, "creator")); + FakeApiService apiService = new() + { + MediaToReturn = new Dictionary + { + { 1, "https://example.com/one.jpg" }, { 2, "https://example.com/two.jpg" } + } + }; + FakeDbService dbService = new() { CheckDownloadedResult = true }; + FakeConfigService configService = new(new Config { ShowScrapeSize = false }); + ProgressRecorder progress = new(); + DownloadService service = CreateService(configService, dbService, apiService); + + DownloadResult result = await service.DownloadHighlights("user", 1, folder, new HashSet(), progress); + + Assert.Equal(2, result.TotalCount); + Assert.Equal(0, result.NewDownloads); + Assert.Equal(2, result.ExistingDownloads); + Assert.Equal("Highlights", result.MediaType); + Assert.True(result.Success); + Assert.Equal(2, progress.Total); + } + + private static DownloadService CreateService(FakeConfigService configService, FakeDbService dbService, + FakeApiService? apiService = null) => + new(new FakeAuthService(), configService, dbService, new FakeFileNameService(), + apiService ?? new FakeApiService()); + + private static string NormalizeFolder(string folder) => folder.Replace("\\", "/"); +} diff --git a/OF DL.Tests/Services/TestDoubles.cs b/OF DL.Tests/Services/TestDoubles.cs new file mode 100644 index 0000000..0cfce8b --- /dev/null +++ b/OF DL.Tests/Services/TestDoubles.cs @@ -0,0 +1,208 @@ +using Newtonsoft.Json.Linq; +using OF_DL.Enumerations; +using OF_DL.Models; +using OF_DL.Models.Config; +using OF_DL.Models.Entities.Users; +using OF_DL.Services; + +namespace OF_DL.Tests.Services; + +internal sealed class TempFolder : IDisposable +{ + public TempFolder() + { + Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ofdl-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Path); + } + + public string Path { get; } + + public void Dispose() + { + try + { + Directory.Delete(Path, true); + } + catch + { + // ignored + } + } +} + +internal sealed class ProgressRecorder : IProgressReporter +{ + public long Total { get; private set; } + + public void ReportProgress(long increment) => Total += increment; +} + +internal sealed class FakeConfigService(Config config) : IConfigService +{ + public Config CurrentConfig { get; private set; } = config; + + public bool IsCliNonInteractive { get; } = config.NonInteractiveMode; + + public Task LoadConfigurationAsync(string[] args) => Task.FromResult(true); + + public Task SaveConfigurationAsync(string filePath = "config.conf") => Task.CompletedTask; + + public void UpdateConfig(Config newConfig) => CurrentConfig = newConfig; + + public List<(string Name, bool Value)> GetToggleableProperties() => []; + + public bool ApplyToggleableSelections(List selectedNames) => false; +} + +internal sealed class FakeDbService : IDbService +{ + public bool CheckDownloadedResult { get; init; } + + public long StoredFileSize { get; init; } + + public (string folder, long mediaId, string apiType, string directory, string filename, long size, + bool downloaded, DateTime createdAt)? LastUpdateMedia { get; private set; } + + public Task UpdateMedia(string folder, long mediaId, string apiType, string directory, string filename, + long size, bool downloaded, DateTime createdAt) + { + LastUpdateMedia = (folder, mediaId, apiType, directory, filename, size, downloaded, createdAt); + return Task.CompletedTask; + } + + public Task GetStoredFileSize(string folder, long mediaId, string apiType) => + Task.FromResult(StoredFileSize); + + public Task CheckDownloaded(string folder, long mediaId, string apiType) => + Task.FromResult(CheckDownloadedResult); + + public Task AddMessage(string folder, long postId, string messageText, string price, bool isPaid, + bool isArchived, DateTime createdAt, long userId) => throw new NotImplementedException(); + + public Task AddPost(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, + DateTime createdAt) => throw new NotImplementedException(); + + public Task AddStory(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived, + DateTime createdAt) => throw new NotImplementedException(); + + public Task CreateDb(string folder) => throw new NotImplementedException(); + + public Task CreateUsersDb(Dictionary users) => throw new NotImplementedException(); + + public Task CheckUsername(KeyValuePair user, string path) => throw new NotImplementedException(); + + public Task AddMedia(string folder, long mediaId, long postId, string link, string? directory, + string? filename, long? size, string apiType, string mediaType, bool preview, bool downloaded, + DateTime? createdAt) => throw new NotImplementedException(); + + public Task GetMostRecentPostDate(string folder) => throw new NotImplementedException(); +} + +internal sealed class FakeApiService : IApiService +{ + public Dictionary? MediaToReturn { get; init; } + + public DateTime LastModifiedToReturn { get; set; } = new(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public bool OfdlCalled { get; private set; } + + public bool CdmCalled { get; private set; } + + public Task GetDrmMpdPssh(string mpdUrl, string policy, string signature, string kvp) => + Task.FromResult("pssh"); + + public Task GetDrmMpdLastModified(string mpdUrl, string policy, string signature, string kvp) => + Task.FromResult(LastModifiedToReturn); + + public Task GetDecryptionKeyOfdl(Dictionary drmHeaders, string licenceUrl, string pssh) + { + OfdlCalled = true; + return Task.FromResult("ofdl-key"); + } + + public Task GetDecryptionKeyCdm(Dictionary drmHeaders, string licenceUrl, string pssh) + { + CdmCalled = true; + return Task.FromResult("cdm-key"); + } + + public Dictionary GetDynamicHeaders(string path, string queryParam) => + new() { { "X-Test", "value" } }; + + public Task?> GetMedia(MediaType mediaType, string endpoint, string? username, + string folder) => Task.FromResult(MediaToReturn); + + public Task?> GetLists(string endpoint) => throw new NotImplementedException(); + + public Task?> GetListUsers(string endpoint) => throw new NotImplementedException(); + + public Task GetPaidPosts(string endpoint, string folder, + string username, List paidPostIds, IStatusReporter statusReporter) => + throw new NotImplementedException(); + + public Task GetPosts(string endpoint, string folder, + List paidPostIds, IStatusReporter statusReporter) => throw new NotImplementedException(); + + public Task GetPost(string endpoint, string folder) => + throw new NotImplementedException(); + + public Task GetStreams(string endpoint, string folder, + List paidPostIds, IStatusReporter statusReporter) => throw new NotImplementedException(); + + public Task GetArchived(string endpoint, string folder, + IStatusReporter statusReporter) => throw new NotImplementedException(); + + public Task GetMessages(string endpoint, string folder, + IStatusReporter statusReporter) => throw new NotImplementedException(); + + public Task GetPaidMessages(string endpoint, + string folder, string username, IStatusReporter statusReporter) => throw new NotImplementedException(); + + public Task GetPaidMessage(string endpoint, + string folder) => throw new NotImplementedException(); + + public Task> GetPurchasedTabUsers(string endpoint, Dictionary users) => + throw new NotImplementedException(); + + public Task> GetPurchasedTab(string endpoint, + string folder, Dictionary users) => throw new NotImplementedException(); + + public Task GetUserInfo(string endpoint) => + throw new NotImplementedException(); + + public Task GetUserInfoById(string endpoint) => + throw new NotImplementedException(); + + public Task?> GetActiveSubscriptions(string endpoint, + bool includeRestrictedSubscriptions) => throw new NotImplementedException(); + + public Task?> GetExpiredSubscriptions(string endpoint, + bool includeRestrictedSubscriptions) => throw new NotImplementedException(); +} + +internal sealed class FakeFileNameService : IFileNameService +{ + public Task BuildFilename(string fileFormat, Dictionary values) => + throw new NotImplementedException(); + + public Task> GetFilename(object info, object media, object author, + List selectedProperties, string username, Dictionary? users = null) => + throw new NotImplementedException(); +} + +internal sealed class FakeAuthService : IAuthService +{ + public Auth? CurrentAuth { get; set; } + + public Task LoadFromFileAsync(string filePath = "auth.json") => throw new NotImplementedException(); + + public Task LoadFromBrowserAsync() => throw new NotImplementedException(); + + public Task SaveToFileAsync(string filePath = "auth.json") => throw new NotImplementedException(); + + public void ValidateCookieString() => throw new NotImplementedException(); + + public Task ValidateAuthAsync() => throw new NotImplementedException(); + + public void Logout() => throw new NotImplementedException(); +} From e7fd0ee138a41d9ca9091ac5daf82aab7c112fff Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 12:46:25 -0600 Subject: [PATCH 46/55] Update AGENTS.md --- AGENTS.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ee45881..368b337 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ most important change points. - DTOs live under `OF DL.Core/Models/Dtos/` and mirror API response JSON. - Entities live under `OF DL.Core/Models/Entities/` and represent the internal domain used by download logic. - Mappers in `OF DL.Core/Models/Mappers/` convert DTOs into entities to isolate API changes from downstream logic. -- Non-DTO/Entity models are grouped by concern under `OF DL.Core/Models/Api/`, `Auth/`, `Config/`, `Downloads/`, +- Non-DTO/Entity models are grouped by concern under `OF DL.Core/Models/OfdlApi/`, `Auth/`, `Config/`, `Downloads/`, and `Startup/`. - Classes in `OF DL.Core/Models/OfdlApi/` mirror request and response JOSN OF DL APIs (custom and gitea) - Classes in `OF DL.Core/Models/Config/` are used for reading and storing application configuration @@ -133,7 +133,8 @@ Environment variables used by auth: fallback to local `rules.json` in the current working directory. The repo ships `OF DL/rules.json` as the default rules file. - Cache durations: 15 minutes for remote rules, 5 minutes for local rules. -- `DynamicRules` shape is defined in `OF DL.Core/Models/Api/DynamicRules.cs` and includes `app-token`, `static_param`, +- `DynamicRules` shape is defined in `OF DL.Core/Models/OfdlApi/DynamicRules.cs` and includes `app-token`, + `static_param`, `prefix`, `suffix`, `checksum_constant`, and `checksum_indexes`. Signature algorithm in `GetDynamicHeaders`: From 94e135f16827f4354c2e484b256ccf11e66edacd Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 16:14:31 -0600 Subject: [PATCH 47/55] Add additional AI generated unit tests --- OF DL.Tests/Services/ApiServiceTests.cs | 434 ++++++++++++++++-- OF DL.Tests/Services/AuthServiceTests.cs | 89 ++++ OF DL.Tests/Services/ConfigServiceTests.cs | 84 ++++ .../DownloadOrchestrationServiceTests.cs | 302 ++++++++++++ OF DL.Tests/Services/DownloadServiceTests.cs | 27 +- .../Services/FileNameServiceTestModels.cs | 30 ++ OF DL.Tests/Services/FileNameServiceTests.cs | 79 ++++ OF DL.Tests/Services/SimpleHttpServer.cs | 83 ++++ OF DL.Tests/Services/TestDoubles.cs | 318 ++++++++++++- OF DL.Tests/Services/TestScopes.cs | 53 +++ 10 files changed, 1450 insertions(+), 49 deletions(-) create mode 100644 OF DL.Tests/Services/AuthServiceTests.cs create mode 100644 OF DL.Tests/Services/ConfigServiceTests.cs create mode 100644 OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs create mode 100644 OF DL.Tests/Services/FileNameServiceTestModels.cs create mode 100644 OF DL.Tests/Services/FileNameServiceTests.cs create mode 100644 OF DL.Tests/Services/SimpleHttpServer.cs create mode 100644 OF DL.Tests/Services/TestScopes.cs diff --git a/OF DL.Tests/Services/ApiServiceTests.cs b/OF DL.Tests/Services/ApiServiceTests.cs index af711bf..54b90dc 100644 --- a/OF DL.Tests/Services/ApiServiceTests.cs +++ b/OF DL.Tests/Services/ApiServiceTests.cs @@ -1,10 +1,15 @@ using System.Reflection; using System.Security.Cryptography; using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OF_DL.Enumerations; using OF_DL.Models; using OF_DL.Models.Config; +using OF_DL.Models.Entities.Common; using OF_DL.Models.OfdlApi; using OF_DL.Services; +using UserEntities = OF_DL.Models.Entities.Users; namespace OF_DL.Tests.Services; @@ -101,8 +106,405 @@ public class ApiServiceTests Assert.Contains("Auth service is missing required fields", ex.Message); } + [Fact] + public async Task BuildHttpRequestMessage_BuildsUrlAndAddsHeaders() + { + FakeAuthService authService = new() + { + CurrentAuth = new Auth + { + UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_cookie=abc;" + } + }; + ApiService service = CreateService(authService); + DynamicRules rules = new() + { + AppToken = "app-token", + StaticParam = "static", + Prefix = "prefix", + Suffix = "suffix", + ChecksumConstant = 7, + ChecksumIndexes = [0] + }; + + using DynamicRulesCacheScope _ = new(rules); + + Dictionary getParams = new() { { "limit", "10" }, { "offset", "5" } }; + HttpRequestMessage request = await InvokeBuildHttpRequestMessage(service, getParams, "/users"); + + Assert.Equal("https://onlyfans.com/api2/v2/users?limit=10&offset=5", request.RequestUri?.ToString()); + Assert.True(request.Headers.Contains("app-token")); + Assert.True(request.Headers.Contains("sign")); + Assert.True(request.Headers.Contains("user-id")); + } + + [Fact] + public void DeserializeJson_ReturnsDefaultForWhitespace() + { + object? result = InvokeDeserializeJson(typeof(Dictionary), " ", null); + Assert.Null(result); + } + + [Fact] + public void DeserializeJson_ParsesValidJson() + { + object? result = InvokeDeserializeJson(typeof(Dictionary), "{\"a\":1}", null); + Assert.NotNull(result); + Dictionary dict = Assert.IsType>(result); + Assert.Equal(1, dict["a"]); + } + + [Fact] + public void UpdateGetParamsForDateSelection_BeforeAddsBeforePublishTime() + { + Dictionary getParams = new(); + + InvokeUpdateGetParamsForDateSelection(DownloadDateSelection.before, getParams, "123.000000"); + + Assert.Equal("123.000000", getParams["beforePublishTime"]); + } + + [Fact] + public void UpdateGetParamsForDateSelection_AfterAddsAfterPublishTimeAndOrder() + { + Dictionary getParams = new(); + + InvokeUpdateGetParamsForDateSelection(DownloadDateSelection.after, getParams, "456.000000"); + + Assert.Equal("publish_date_asc", getParams["order"]); + Assert.Equal("456.000000", getParams["afterPublishTime"]); + } + + [Theory] + [InlineData("photo", "Images")] + [InlineData("video", "Videos")] + [InlineData("gif", "Videos")] + [InlineData("audio", "Audios")] + [InlineData("unknown", null)] + public void ResolveMediaType_ReturnsExpectedValue(string input, string? expected) + { + string? result = InvokeResolveMediaType(input); + + Assert.Equal(expected, result); + } + + [Fact] + public void IsMediaTypeDownloadEnabled_RespectsConfigFlags() + { + Config config = new() { DownloadImages = false, DownloadVideos = false, DownloadAudios = false }; + ApiService service = new(new FakeAuthService(), new FakeConfigService(config), new MediaTrackingDbService()); + + Assert.False(InvokeIsMediaTypeDownloadEnabled(service, "photo")); + Assert.False(InvokeIsMediaTypeDownloadEnabled(service, "video")); + Assert.False(InvokeIsMediaTypeDownloadEnabled(service, "audio")); + Assert.True(InvokeIsMediaTypeDownloadEnabled(service, "other")); + } + + [Fact] + public void TryGetDrmInfo_ReturnsTrueWhenComplete() + { + Files files = new() + { + Drm = new Drm + { + Manifest = new Manifest { Dash = "dash" }, + Signature = new Signature + { + Dash = new Dash + { + CloudFrontPolicy = "policy", + CloudFrontSignature = "signature", + CloudFrontKeyPairId = "kvp" + } + } + } + }; + + bool result = InvokeTryGetDrmInfo(files, out string manifestDash, out string policy, + out string signature, out string kvp); + + Assert.True(result); + Assert.Equal("dash", manifestDash); + Assert.Equal("policy", policy); + Assert.Equal("signature", signature); + Assert.Equal("kvp", kvp); + } + + [Fact] + public void TryGetDrmInfo_ReturnsFalseWhenMissingFields() + { + Files files = new() + { + Drm = new Drm + { + Manifest = new Manifest { Dash = null }, + Signature = new Signature { Dash = new Dash { CloudFrontPolicy = "policy" } } + } + }; + + bool result = InvokeTryGetDrmInfo(files, out _, out _, out _, out _); + + Assert.False(result); + } + + [Fact] + public void GetCurrentUserIdOrDefault_ReturnsMinValueWhenMissingOrInvalid() + { + ApiService serviceMissing = + new(new FakeAuthService(), new FakeConfigService(new Config()), new MediaTrackingDbService()); + Assert.Equal(int.MinValue, InvokeGetCurrentUserIdOrDefault(serviceMissing)); + + FakeAuthService authService = new() { CurrentAuth = new Auth { UserId = "not-a-number" } }; + ApiService serviceInvalid = new(authService, new FakeConfigService(new Config()), new MediaTrackingDbService()); + Assert.Equal(int.MinValue, InvokeGetCurrentUserIdOrDefault(serviceInvalid)); + } + + [Fact] + public void GetCurrentUserIdOrDefault_ReturnsParsedUserId() + { + FakeAuthService authService = new() { CurrentAuth = new Auth { UserId = "42" } }; + ApiService service = new(authService, new FakeConfigService(new Config()), new MediaTrackingDbService()); + + Assert.Equal(42, InvokeGetCurrentUserIdOrDefault(service)); + } + + [Fact] + public void ConvertToUnixTimestampWithMicrosecondPrecision_ReturnsExpectedSeconds() + { + double epoch = InvokeConvertToUnixTimestampWithMicrosecondPrecision( + new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + double oneSecond = InvokeConvertToUnixTimestampWithMicrosecondPrecision( + new DateTime(1970, 1, 1, 0, 0, 1, DateTimeKind.Utc)); + + Assert.Equal(0, epoch, 6); + Assert.Equal(1, oneSecond, 6); + } + + [Fact] + public async Task GetDrmMpdPssh_ReturnsSecondPssh() + { + string mpd = """ + + + + + FIRST + SECOND + + + + """; + using SimpleHttpServer server = new(mpd); + FakeAuthService authService = new() + { + CurrentAuth = new Auth + { + UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_id=1; sess=2;" + } + }; + ApiService service = CreateService(authService); + + string pssh = await service.GetDrmMpdPssh(server.Url.ToString(), "policy", "signature", "kvp"); + await server.Completion; + + Assert.Equal("SECOND", pssh); + } + + [Fact] + public async Task GetDrmMpdLastModified_ReturnsLastModifiedHeader() + { + DateTime lastModifiedUtc = new(2024, 2, 3, 4, 5, 6, DateTimeKind.Utc); + using SimpleHttpServer server = new("", lastModifiedUtc); + FakeAuthService authService = new() + { + CurrentAuth = new Auth + { + UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_id=1; sess=2;" + } + }; + ApiService service = CreateService(authService); + + DateTime result = + await service.GetDrmMpdLastModified(server.Url.ToString(), "policy", "signature", "kvp"); + await server.Completion; + + DateTime expectedLocal = lastModifiedUtc.ToLocalTime(); + Assert.True((result - expectedLocal).Duration() < TimeSpan.FromSeconds(1)); + } + + [Fact] + public async Task GetUserInfo_ReturnsNullWhenAuthMissing() + { + ApiService service = CreateService(new FakeAuthService()); + using DynamicRulesCacheScope _ = new(BuildTestRules()); + + UserEntities.User? user = await service.GetUserInfo("/users/me"); + + Assert.Null(user); + } + + [Fact] + public async Task GetUserInfoById_ReturnsNullWhenAuthMissing() + { + ApiService service = CreateService(new FakeAuthService()); + using DynamicRulesCacheScope _ = new(BuildTestRules()); + + JObject? user = await service.GetUserInfoById("/users/list?x[]=1"); + + Assert.Null(user); + } + + [Fact] + public async Task GetActiveSubscriptions_ReturnsNullWhenAuthMissing() + { + ApiService service = CreateService(new FakeAuthService()); + using DynamicRulesCacheScope _ = new(BuildTestRules()); + + Dictionary? result = await service.GetActiveSubscriptions("/subscriptions", false); + + Assert.Null(result); + } + + [Fact] + public async Task GetExpiredSubscriptions_ReturnsNullWhenAuthMissing() + { + ApiService service = CreateService(new FakeAuthService()); + using DynamicRulesCacheScope _ = new(BuildTestRules()); + + Dictionary? result = await service.GetExpiredSubscriptions("/subscriptions", false); + + Assert.Null(result); + } + + [Fact] + public async Task GetLists_ReturnsNullWhenAuthMissing() + { + ApiService service = CreateService(new FakeAuthService()); + using DynamicRulesCacheScope _ = new(BuildTestRules()); + + Dictionary? result = await service.GetLists("/lists"); + + Assert.Null(result); + } + + [Fact] + public async Task GetListUsers_ReturnsNullWhenAuthMissing() + { + ApiService service = CreateService(new FakeAuthService()); + using DynamicRulesCacheScope _ = new(BuildTestRules()); + + List? result = await service.GetListUsers("/lists/1/users"); + + Assert.Null(result); + } + + [Fact] + public async Task GetMedia_ReturnsNullWhenAuthMissing() + { + ApiService service = CreateService(new FakeAuthService()); + using DynamicRulesCacheScope _ = new(BuildTestRules()); + + Dictionary? result = await service.GetMedia(MediaType.Stories, "/users/1/stories", null, "/tmp"); + + Assert.Null(result); + } + private static ApiService CreateService(FakeAuthService authService) => - new(authService, new FakeConfigService(new Config()), new FakeDbService()); + new(authService, new FakeConfigService(new Config()), new MediaTrackingDbService()); + + private static DynamicRules BuildTestRules() => + new() + { + AppToken = "app-token", + StaticParam = "static", + Prefix = "prefix", + Suffix = "suffix", + ChecksumConstant = 7, + ChecksumIndexes = [0] + }; + + private static async Task InvokeBuildHttpRequestMessage(ApiService service, + Dictionary getParams, string endpoint) + { + MethodInfo method = typeof(ApiService).GetMethod("BuildHttpRequestMessage", + BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new InvalidOperationException("BuildHttpRequestMessage not found."); + Task task = (Task)method.Invoke(service, + [getParams, endpoint])!; + return await task; + } + + private static object? InvokeDeserializeJson(Type type, string? body, JsonSerializerSettings? settings) + { + MethodInfo method = typeof(ApiService).GetMethod("DeserializeJson", + BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException("DeserializeJson not found."); + MethodInfo generic = method.MakeGenericMethod(type); + return generic.Invoke(null, [body, settings]); + } + + private static void InvokeUpdateGetParamsForDateSelection(DownloadDateSelection selection, + Dictionary getParams, string? timestamp) + { + MethodInfo method = typeof(ApiService).GetMethod("UpdateGetParamsForDateSelection", + BindingFlags.NonPublic | BindingFlags.Static, null, + [ + typeof(DownloadDateSelection), typeof(Dictionary).MakeByRefType(), + typeof(string) + ], + null) + ?? throw new InvalidOperationException("UpdateGetParamsForDateSelection not found."); + object?[] args = { selection, getParams, timestamp }; + method.Invoke(null, args); + } + + private static string? InvokeResolveMediaType(string? type) + { + MethodInfo method = typeof(ApiService).GetMethod("ResolveMediaType", + BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException("ResolveMediaType not found."); + return (string?)method.Invoke(null, new object?[] { type }); + } + + private static bool InvokeIsMediaTypeDownloadEnabled(ApiService service, string? type) + { + MethodInfo method = typeof(ApiService).GetMethod("IsMediaTypeDownloadEnabled", + BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new InvalidOperationException("IsMediaTypeDownloadEnabled not found."); + return (bool)method.Invoke(service, [type])!; + } + + private static bool InvokeTryGetDrmInfo(Files files, out string manifestDash, out string cloudFrontPolicy, + out string cloudFrontSignature, out string cloudFrontKeyPairId) + { + MethodInfo method = typeof(ApiService).GetMethod("TryGetDrmInfo", + BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException("TryGetDrmInfo not found."); + object?[] args = { files, null, null, null, null }; + bool result = (bool)method.Invoke(null, args)!; + manifestDash = (string)args[1]!; + cloudFrontPolicy = (string)args[2]!; + cloudFrontSignature = (string)args[3]!; + cloudFrontKeyPairId = (string)args[4]!; + return result; + } + + private static int InvokeGetCurrentUserIdOrDefault(ApiService service) + { + MethodInfo method = typeof(ApiService).GetMethod("GetCurrentUserIdOrDefault", + BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new InvalidOperationException("GetCurrentUserIdOrDefault not found."); + return (int)method.Invoke(service, null)!; + } + + private static double InvokeConvertToUnixTimestampWithMicrosecondPrecision(DateTime dateTime) + { + MethodInfo method = typeof(ApiService).GetMethod("ConvertToUnixTimestampWithMicrosecondPrecision", + BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException( + "ConvertToUnixTimestampWithMicrosecondPrecision not found."); + return (double)method.Invoke(null, [dateTime])!; + } private static string BuildSign(DynamicRules rules, long timestamp, string pathWithQuery, string userId) { @@ -118,34 +520,4 @@ public class ApiServiceTests return $"{rules.Prefix}:{hashString}:{checksumHex}:{rules.Suffix}"; } - - private sealed class DynamicRulesCacheScope : IDisposable - { - private static readonly FieldInfo s_rulesField = - typeof(ApiService).GetField("s_cachedDynamicRules", BindingFlags.NonPublic | BindingFlags.Static) ?? - throw new InvalidOperationException("Unable to access cached rules field."); - - private static readonly FieldInfo s_expirationField = - typeof(ApiService).GetField("s_cachedDynamicRulesExpiration", - BindingFlags.NonPublic | BindingFlags.Static) ?? - throw new InvalidOperationException("Unable to access cached rules expiration field."); - - private readonly object? _priorRules; - private readonly DateTime? _priorExpiration; - - public DynamicRulesCacheScope(DynamicRules rules) - { - _priorRules = s_rulesField.GetValue(null); - _priorExpiration = (DateTime?)s_expirationField.GetValue(null); - - s_rulesField.SetValue(null, rules); - s_expirationField.SetValue(null, DateTime.UtcNow.AddHours(1)); - } - - public void Dispose() - { - s_rulesField.SetValue(null, _priorRules); - s_expirationField.SetValue(null, _priorExpiration); - } - } } diff --git a/OF DL.Tests/Services/AuthServiceTests.cs b/OF DL.Tests/Services/AuthServiceTests.cs new file mode 100644 index 0000000..b25a576 --- /dev/null +++ b/OF DL.Tests/Services/AuthServiceTests.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using OF_DL.Models; +using OF_DL.Services; + +namespace OF_DL.Tests.Services; + +[Collection("NonParallel")] +public class AuthServiceTests +{ + [Fact] + public async Task LoadFromFileAsync_ReturnsFalseWhenMissing() + { + using TempFolder temp = new(); + using CurrentDirectoryScope _ = new(temp.Path); + AuthService service = CreateService(); + + bool result = await service.LoadFromFileAsync(); + + Assert.False(result); + Assert.Null(service.CurrentAuth); + } + + [Fact] + public async Task SaveToFileAsync_WritesAuthFile() + { + using TempFolder temp = new(); + using CurrentDirectoryScope _ = new(temp.Path); + AuthService service = CreateService(); + service.CurrentAuth = new Auth + { + UserId = "123", + UserAgent = "agent", + XBc = "xbc", + Cookie = "auth_id=123; sess=abc;" + }; + + await service.SaveToFileAsync(); + + Assert.True(File.Exists("auth.json")); + string json = await File.ReadAllTextAsync("auth.json"); + Auth? saved = JsonConvert.DeserializeObject(json); + Assert.NotNull(saved); + Assert.Equal("123", saved.UserId); + Assert.Equal("agent", saved.UserAgent); + Assert.Equal("xbc", saved.XBc); + Assert.Equal("auth_id=123; sess=abc;", saved.Cookie); + } + + [Fact] + public void ValidateCookieString_NormalizesAndPersists() + { + using TempFolder temp = new(); + using CurrentDirectoryScope _ = new(temp.Path); + AuthService service = CreateService(); + service.CurrentAuth = new Auth + { + Cookie = "auth_id=123; other=1; sess=abc" + }; + + service.ValidateCookieString(); + + Assert.Equal("auth_id=123; sess=abc;", service.CurrentAuth.Cookie); + Assert.True(File.Exists("auth.json")); + string json = File.ReadAllText("auth.json"); + Auth? saved = JsonConvert.DeserializeObject(json); + Assert.NotNull(saved); + Assert.Equal("auth_id=123; sess=abc;", saved.Cookie); + } + + [Fact] + public void Logout_DeletesAuthAndChromeData() + { + using TempFolder temp = new(); + using CurrentDirectoryScope _ = new(temp.Path); + AuthService service = CreateService(); + Directory.CreateDirectory("chrome-data"); + File.WriteAllText("chrome-data/test.txt", "x"); + File.WriteAllText("auth.json", "{}"); + + service.Logout(); + + Assert.False(Directory.Exists("chrome-data")); + Assert.False(File.Exists("auth.json")); + } + + private static AuthService CreateService() => + new(new ServiceCollection().BuildServiceProvider()); +} diff --git a/OF DL.Tests/Services/ConfigServiceTests.cs b/OF DL.Tests/Services/ConfigServiceTests.cs new file mode 100644 index 0000000..7157ee9 --- /dev/null +++ b/OF DL.Tests/Services/ConfigServiceTests.cs @@ -0,0 +1,84 @@ +using OF_DL.Enumerations; +using OF_DL.Models.Config; +using OF_DL.Services; + +namespace OF_DL.Tests.Services; + +[Collection("NonParallel")] +public class ConfigServiceTests +{ + [Fact] + public async Task LoadConfigurationAsync_CreatesDefaultConfigWhenMissing() + { + using TempFolder temp = new(); + using CurrentDirectoryScope _ = new(temp.Path); + FakeLoggingService loggingService = new(); + ConfigService service = new(loggingService); + + bool result = await service.LoadConfigurationAsync([]); + + Assert.True(result); + Assert.True(File.Exists("config.conf")); + Assert.True(loggingService.UpdateCount > 0); + Assert.Equal(service.CurrentConfig.LoggingLevel, loggingService.LastLevel); + } + + [Fact] + public async Task LoadConfigurationAsync_OverridesNonInteractiveFromCli() + { + using TempFolder temp = new(); + using CurrentDirectoryScope _ = new(temp.Path); + FakeLoggingService loggingService = new(); + ConfigService service = new(loggingService); + await service.SaveConfigurationAsync(); + + bool result = await service.LoadConfigurationAsync(["--non-interactive"]); + + Assert.True(result); + Assert.True(service.IsCliNonInteractive); + Assert.True(service.CurrentConfig.NonInteractiveMode); + } + + [Fact] + public async Task LoadConfigurationAsync_ReturnsFalseWhenInvalidFilenameFormat() + { + using TempFolder temp = new(); + using CurrentDirectoryScope _ = new(temp.Path); + FakeLoggingService loggingService = new(); + ConfigService service = new(loggingService); + await service.SaveConfigurationAsync(); + + string hocon = await File.ReadAllTextAsync("config.conf"); + hocon = hocon.Replace("PaidPostFileNameFormat = \"\"", + "PaidPostFileNameFormat = \"invalid-format\""); + await File.WriteAllTextAsync("config.conf", hocon); + + bool result = await service.LoadConfigurationAsync([]); + + Assert.False(result); + } + + [Fact] + public void ApplyToggleableSelections_UpdatesConfigAndReturnsChange() + { + FakeLoggingService loggingService = new(); + ConfigService service = new(loggingService); + Config initialConfig = new() + { + DownloadPosts = true, + DownloadMessages = true, + DownloadPath = "/downloads", + LoggingLevel = LoggingLevel.Warning + }; + service.UpdateConfig(initialConfig); + + bool changed = service.ApplyToggleableSelections(["DownloadPosts"]); + + Assert.True(changed); + Assert.True(service.CurrentConfig.DownloadPosts); + Assert.False(service.CurrentConfig.DownloadMessages); + Assert.Equal("/downloads", service.CurrentConfig.DownloadPath); + Assert.Equal(LoggingLevel.Warning, loggingService.LastLevel); + } + +} diff --git a/OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs b/OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs new file mode 100644 index 0000000..9167b52 --- /dev/null +++ b/OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs @@ -0,0 +1,302 @@ +using Newtonsoft.Json.Linq; +using OF_DL.Models.Config; +using OF_DL.Models.Downloads; +using OF_DL.Services; +using MessageEntities = OF_DL.Models.Entities.Messages; +using PostEntities = OF_DL.Models.Entities.Posts; +using PurchasedEntities = OF_DL.Models.Entities.Purchased; + +namespace OF_DL.Tests.Services; + +public class DownloadOrchestrationServiceTests +{ + [Fact] + public async Task GetAvailableUsersAsync_FiltersIgnoredUsers() + { + Config config = CreateConfig(c => + { + c.IncludeExpiredSubscriptions = true; + c.IgnoredUsersListName = "ignored"; + }); + FakeConfigService configService = new(config); + ConfigurableApiService apiService = new() + { + ActiveSubscriptionsHandler = (_, _) => + Task.FromResult?>(new Dictionary { { "alice", 1 } }), + ExpiredSubscriptionsHandler = (_, _) => + Task.FromResult?>(new Dictionary { { "bob", 2 } }), + ListsHandler = + _ => Task.FromResult?>(new Dictionary { { "ignored", 10 } }), + ListUsersHandler = _ => Task.FromResult?>(["alice"]) + }; + UserTrackingDbService dbService = new(); + DownloadOrchestrationService service = + new(apiService, configService, new OrchestrationDownloadServiceStub(), dbService); + + UserListResult result = await service.GetAvailableUsersAsync(); + + Assert.Single(result.Users); + Assert.True(result.Users.ContainsKey("bob")); + Assert.Null(result.IgnoredListError); + Assert.NotNull(dbService.CreatedUsers); + Assert.True(dbService.CreatedUsers.ContainsKey("bob")); + Assert.False(dbService.CreatedUsers.ContainsKey("alice")); + } + + [Fact] + public async Task GetAvailableUsersAsync_SetsIgnoredListErrorWhenMissing() + { + Config config = CreateConfig(c => + { + c.IncludeExpiredSubscriptions = false; + c.IgnoredUsersListName = "missing"; + }); + FakeConfigService configService = new(config); + ConfigurableApiService apiService = new() + { + ActiveSubscriptionsHandler = (_, _) => + Task.FromResult?>(new Dictionary { { "alice", 1 } }), + ListsHandler = _ => Task.FromResult?>(new Dictionary()) + }; + UserTrackingDbService dbService = new(); + DownloadOrchestrationService service = + new(apiService, configService, new OrchestrationDownloadServiceStub(), dbService); + + UserListResult result = await service.GetAvailableUsersAsync(); + + Assert.NotNull(result.IgnoredListError); + Assert.Single(result.Users); + Assert.True(result.Users.ContainsKey("alice")); + } + + [Fact] + public async Task GetUsersForListAsync_ReturnsUsersInList() + { + FakeConfigService configService = new(CreateConfig()); + ConfigurableApiService apiService = new() { ListUsersHandler = _ => Task.FromResult?>(["bob"]) }; + DownloadOrchestrationService service = + new(apiService, configService, new OrchestrationDownloadServiceStub(), new UserTrackingDbService()); + Dictionary allUsers = new() { { "alice", 1 }, { "bob", 2 } }; + Dictionary lists = new() { { "mylist", 5 } }; + + Dictionary result = await service.GetUsersForListAsync("mylist", allUsers, lists); + + Assert.Single(result); + Assert.Equal(2, result["bob"]); + } + + [Fact] + public void ResolveDownloadPath_UsesConfiguredPathWhenSet() + { + Config config = CreateConfig(c => c.DownloadPath = "C:\\Downloads"); + DownloadOrchestrationService service = + new(new ConfigurableApiService(), new FakeConfigService(config), new OrchestrationDownloadServiceStub(), + new UserTrackingDbService()); + + string path = service.ResolveDownloadPath("creator"); + + Assert.Equal(Path.Combine("C:\\Downloads", "creator"), path); + } + + [Fact] + public void ResolveDownloadPath_UsesDefaultWhenBlank() + { + Config config = CreateConfig(c => c.DownloadPath = ""); + DownloadOrchestrationService service = + new(new ConfigurableApiService(), new FakeConfigService(config), new OrchestrationDownloadServiceStub(), + new UserTrackingDbService()); + + string path = service.ResolveDownloadPath("creator"); + + Assert.Equal("__user_data__/sites/OnlyFans/creator", path); + } + + [Fact] + public async Task PrepareUserFolderAsync_CreatesFolderAndDb() + { + using TempFolder temp = new(); + string userPath = Path.Combine(temp.Path, "creator"); + UserTrackingDbService dbService = new(); + DownloadOrchestrationService service = + new(new ConfigurableApiService(), new FakeConfigService(CreateConfig()), + new OrchestrationDownloadServiceStub(), dbService); + + await service.PrepareUserFolderAsync("creator", 99, userPath); + + Assert.True(Directory.Exists(userPath)); + Assert.True(dbService.CheckedUser.HasValue); + Assert.Equal("creator", dbService.CheckedUser.Value.user.Key); + Assert.Equal(99, dbService.CheckedUser.Value.user.Value); + Assert.Equal(userPath, dbService.CheckedUser.Value.path); + Assert.Contains(userPath, dbService.CreatedDbs); + } + + [Fact] + public async Task DownloadSinglePostAsync_WhenMissingPost_SendsMessage() + { + ConfigurableApiService apiService = new() + { + PostHandler = (_, _) => Task.FromResult(new PostEntities.SinglePostCollection()) + }; + OrchestrationDownloadServiceStub downloadService = new(); + RecordingDownloadEventHandler eventHandler = new(); + DownloadOrchestrationService service = + new(apiService, new FakeConfigService(CreateConfig()), downloadService, new UserTrackingDbService()); + + await service.DownloadSinglePostAsync("creator", 42, "/tmp", new Dictionary(), + true, true, eventHandler); + + Assert.Contains("Getting Post", eventHandler.Messages); + Assert.Contains("Couldn't find post", eventHandler.Messages); + Assert.False(downloadService.SinglePostCalled); + } + + [Fact] + public async Task DownloadSinglePostAsync_WhenDownloaded_SendsDownloadedMessage() + { + PostEntities.SinglePostCollection collection = new() + { + SinglePosts = new Dictionary { { 1, "https://example.com/post.jpg" } } + }; + ConfigurableApiService apiService = new() { PostHandler = (_, _) => Task.FromResult(collection) }; + OrchestrationDownloadServiceStub downloadService = new() + { + SinglePostResult = new DownloadResult { NewDownloads = 1, TotalCount = 1 } + }; + RecordingDownloadEventHandler eventHandler = new(); + DownloadOrchestrationService service = + new(apiService, new FakeConfigService(CreateConfig()), downloadService, new UserTrackingDbService()); + + await service.DownloadSinglePostAsync("creator", 99, "/tmp", new Dictionary(), + true, true, eventHandler); + + Assert.Contains("Post 99 downloaded", eventHandler.Messages); + Assert.True(downloadService.SinglePostCalled); + Assert.True(eventHandler.ProgressCalls.Count > 0); + } + + [Fact] + public async Task DownloadSinglePaidMessageAsync_WithPreviewDownloads() + { + PurchasedEntities.SinglePaidMessageCollection collection = new() + { + PreviewSingleMessages = new Dictionary { { 1, "https://example.com/preview.jpg" } }, + SingleMessages = new Dictionary { { 2, "https://example.com/full.jpg" } }, + SingleMessageObjects = [new MessageEntities.SingleMessage()] + }; + ConfigurableApiService apiService = new() { PaidMessageHandler = (_, _) => Task.FromResult(collection) }; + OrchestrationDownloadServiceStub downloadService = new() + { + SinglePaidMessageResult = new DownloadResult { TotalCount = 1, NewDownloads = 1 } + }; + RecordingDownloadEventHandler eventHandler = new(); + DownloadOrchestrationService service = + new(apiService, new FakeConfigService(CreateConfig()), downloadService, new UserTrackingDbService()); + + await service.DownloadSinglePaidMessageAsync("creator", 5, "/tmp", new Dictionary(), + true, true, eventHandler); + + Assert.Contains(eventHandler.ContentFound, entry => entry.contentType == "Preview Paid Messages"); + Assert.True(downloadService.SinglePaidMessageCalled); + } + + [Fact] + public async Task DownloadCreatorContentAsync_DownloadsStoriesWhenEnabled() + { + using TempFolder temp = new(); + string path = Path.Combine(temp.Path, "creator"); + Config config = CreateConfig(c => + { + c.DownloadStories = true; + c.ShowScrapeSize = false; + }); + FakeConfigService configService = new(config); + ConfigurableApiService apiService = new() + { + MediaHandler = (_, _, _, _) => Task.FromResult?>( + new Dictionary + { + { 1, "https://example.com/one.jpg" }, { 2, "https://example.com/two.jpg" } + }) + }; + OrchestrationDownloadServiceStub downloadService = new() + { + StoriesResult = new DownloadResult { TotalCount = 2, NewDownloads = 2 } + }; + RecordingDownloadEventHandler eventHandler = new(); + DownloadOrchestrationService service = + new(apiService, configService, downloadService, new UserTrackingDbService()); + + CreatorDownloadResult result = await service.DownloadCreatorContentAsync("creator", 1, path, + new Dictionary(), true, true, eventHandler); + + Assert.Equal(2, result.StoriesCount); + Assert.Contains(eventHandler.ContentFound, entry => entry.contentType == "Stories"); + Assert.Contains(eventHandler.DownloadCompletes, entry => entry.contentType == "Stories"); + } + + [Fact] + public async Task ResolveUsernameAsync_ReturnsDeletedPlaceholderWhenMissing() + { + ConfigurableApiService apiService = new() { UserInfoByIdHandler = _ => Task.FromResult(null) }; + DownloadOrchestrationService service = + new(apiService, new FakeConfigService(CreateConfig()), new OrchestrationDownloadServiceStub(), + new UserTrackingDbService()); + + string? result = await service.ResolveUsernameAsync(123); + + Assert.Equal("Deleted User - 123", result); + } + + [Fact] + public async Task ResolveUsernameAsync_ReturnsUsernameWhenPresent() + { + JObject payload = new() { ["5"] = new JObject { ["username"] = "creator" } }; + ConfigurableApiService apiService = new() { UserInfoByIdHandler = _ => Task.FromResult(payload) }; + DownloadOrchestrationService service = + new(apiService, new FakeConfigService(CreateConfig()), new OrchestrationDownloadServiceStub(), + new UserTrackingDbService()); + + string? result = await service.ResolveUsernameAsync(5); + + Assert.Equal("creator", result); + } + + private static Config CreateConfig(Action? configure = null) + { + Config config = new() + { + DownloadAvatarHeaderPhoto = false, + DownloadPaidPosts = false, + DownloadPosts = false, + DownloadArchived = false, + DownloadStreams = false, + DownloadStories = false, + DownloadHighlights = false, + DownloadMessages = false, + DownloadPaidMessages = false, + DownloadImages = false, + DownloadVideos = false, + DownloadAudios = false, + IncludeExpiredSubscriptions = false, + IncludeRestrictedSubscriptions = false, + SkipAds = false, + IgnoreOwnMessages = false, + DownloadPostsIncrementally = false, + BypassContentForCreatorsWhoNoLongerExist = false, + DownloadDuplicatedMedia = false, + DownloadOnlySpecificDates = false, + NonInteractiveModePurchasedTab = false, + LimitDownloadRate = false, + FolderPerPaidPost = false, + FolderPerPost = false, + FolderPerPaidMessage = false, + FolderPerMessage = false, + ShowScrapeSize = false, + DisableBrowserAuth = false + }; + + configure?.Invoke(config); + return config; + } +} diff --git a/OF DL.Tests/Services/DownloadServiceTests.cs b/OF DL.Tests/Services/DownloadServiceTests.cs index 87d4a38..e9ee291 100644 --- a/OF DL.Tests/Services/DownloadServiceTests.cs +++ b/OF DL.Tests/Services/DownloadServiceTests.cs @@ -19,7 +19,7 @@ public class DownloadServiceTests Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException()); await File.WriteAllTextAsync(serverFilePath, "abc"); - FakeDbService dbService = new() { CheckDownloadedResult = false }; + MediaTrackingDbService dbService = new() { CheckDownloadedResult = false }; FakeConfigService configService = new(new Config { ShowScrapeSize = false }); DownloadService service = CreateService(configService, dbService); ProgressRecorder progress = new(); @@ -51,7 +51,7 @@ public class DownloadServiceTests Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException()); await File.WriteAllTextAsync(serverFilePath, "abc"); - FakeDbService dbService = new() { CheckDownloadedResult = true, StoredFileSize = 123 }; + MediaTrackingDbService dbService = new() { CheckDownloadedResult = true, StoredFileSize = 123 }; FakeConfigService configService = new(new Config { ShowScrapeSize = false, RenameExistingFilesWhenCustomFormatIsSelected = true }); DownloadService service = CreateService(configService, dbService); @@ -73,9 +73,9 @@ public class DownloadServiceTests [Fact] public async Task GetDecryptionInfo_UsesOfdlWhenCdmMissing() { - FakeApiService apiService = new(); + StaticApiService apiService = new(); DownloadService service = - CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService); + CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService); (string decryptionKey, DateTime lastModified)? result = await service.GetDecryptionInfo( "https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post", @@ -91,9 +91,9 @@ public class DownloadServiceTests [Fact] public async Task GetDecryptionInfo_UsesCdmWhenAvailable() { - FakeApiService apiService = new(); + StaticApiService apiService = new(); DownloadService service = - CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService); + CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService); (string decryptionKey, DateTime lastModified)? result = await service.GetDecryptionInfo( "https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post", @@ -109,9 +109,9 @@ public class DownloadServiceTests [Fact] public async Task DownloadHighlights_ReturnsZeroWhenNoMedia() { - FakeApiService apiService = new() { MediaToReturn = new Dictionary() }; + StaticApiService apiService = new() { MediaToReturn = new Dictionary() }; DownloadService service = - CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService); + CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService); DownloadResult result = await service.DownloadHighlights("user", 1, "/tmp/creator", new HashSet(), new ProgressRecorder()); @@ -128,14 +128,14 @@ public class DownloadServiceTests { using TempFolder temp = new(); string folder = NormalizeFolder(Path.Combine(temp.Path, "creator")); - FakeApiService apiService = new() + StaticApiService apiService = new() { MediaToReturn = new Dictionary { { 1, "https://example.com/one.jpg" }, { 2, "https://example.com/two.jpg" } } }; - FakeDbService dbService = new() { CheckDownloadedResult = true }; + MediaTrackingDbService dbService = new() { CheckDownloadedResult = true }; FakeConfigService configService = new(new Config { ShowScrapeSize = false }); ProgressRecorder progress = new(); DownloadService service = CreateService(configService, dbService, apiService); @@ -150,10 +150,11 @@ public class DownloadServiceTests Assert.Equal(2, progress.Total); } - private static DownloadService CreateService(FakeConfigService configService, FakeDbService dbService, - FakeApiService? apiService = null) => + private static DownloadService CreateService(FakeConfigService configService, MediaTrackingDbService dbService, + StaticApiService? apiService = null) => new(new FakeAuthService(), configService, dbService, new FakeFileNameService(), - apiService ?? new FakeApiService()); + apiService ?? new StaticApiService()); private static string NormalizeFolder(string folder) => folder.Replace("\\", "/"); } + diff --git a/OF DL.Tests/Services/FileNameServiceTestModels.cs b/OF DL.Tests/Services/FileNameServiceTestModels.cs new file mode 100644 index 0000000..9d1098b --- /dev/null +++ b/OF DL.Tests/Services/FileNameServiceTestModels.cs @@ -0,0 +1,30 @@ +namespace OF_DL.Tests.Services; + +internal sealed class TestInfo +{ + public long Id { get; set; } + public string? Text { get; set; } + public DateTime CreatedAt { get; set; } +} + +internal sealed class TestAuthor +{ + public long Id { get; set; } +} + +internal sealed class TestMedia +{ + public long Id { get; set; } + public TestMediaFiles Files { get; set; } = new(); +} + +internal sealed class TestMediaFiles +{ + public TestMediaFull Full { get; set; } = new(); + public object? Drm { get; set; } +} + +internal sealed class TestMediaFull +{ + public string? Url { get; set; } +} diff --git a/OF DL.Tests/Services/FileNameServiceTests.cs b/OF DL.Tests/Services/FileNameServiceTests.cs new file mode 100644 index 0000000..3d8805d --- /dev/null +++ b/OF DL.Tests/Services/FileNameServiceTests.cs @@ -0,0 +1,79 @@ +using OF_DL.Services; + +namespace OF_DL.Tests.Services; + +public class FileNameServiceTests +{ + [Fact] + public async Task GetFilename_ReturnsExpectedValues() + { + TestInfo info = new() { Id = 7, Text = "
hello world
", CreatedAt = new DateTime(2024, 1, 2) }; + TestMedia media = new() + { + Id = 99, + Files = new TestMediaFiles + { + Full = new TestMediaFull { Url = "https://cdn.test/file-name.jpg" }, Drm = new object() + } + }; + TestAuthor author = new() { Id = 123 }; + FileNameService service = new(new FakeAuthService()); + + List selectedProperties = ["mediaId", "filename", "username", "text", "createdAt", "id"]; + Dictionary values = + await service.GetFilename(info, media, author, selectedProperties, "creator"); + + Assert.Equal("99", values["mediaId"]); + Assert.Equal("file-name", values["filename"]); + Assert.Equal("creator", values["username"]); + Assert.Equal("hello world", values["text"]); + Assert.Equal("2024-01-02", values["createdAt"]); + Assert.Equal("7", values["id"]); + } + + [Fact] + public async Task GetFilename_TruncatesTextTo100Chars() + { + string longText = new('a', 120); + TestInfo info = new() { Text = $"

{longText}

" }; + TestMedia media = new() + { + Id = 1, + Files = new TestMediaFiles + { + Full = new TestMediaFull { Url = "https://cdn.test/short.jpg" }, Drm = null + } + }; + FileNameService service = new(new FakeAuthService()); + + Dictionary values = + await service.GetFilename(info, media, new TestAuthor(), ["text"], "creator"); + + Assert.Equal(100, values["text"].Length); + Assert.Equal(new string('a', 100), values["text"]); + } + + [Fact] + public async Task GetFilename_UsesUserLookupWhenUsernameMissing() + { + TestAuthor author = new() { Id = 55 }; + Dictionary users = new() { { "mapped", 55 } }; + FileNameService service = new(new FakeAuthService()); + + Dictionary values = + await service.GetFilename(new TestInfo(), new TestMedia(), author, ["username"], "", users); + + Assert.Equal("mapped", values["username"]); + } + + [Fact] + public async Task BuildFilename_ReplacesTokensAndRemovesInvalidChars() + { + FileNameService service = new(new FakeAuthService()); + Dictionary values = new() { { "username", "creator" }, { "mediaId", "99" } }; + + string result = await service.BuildFilename("{username}_{mediaId}:*?", values); + + Assert.Equal("creator_99", result); + } +} diff --git a/OF DL.Tests/Services/SimpleHttpServer.cs b/OF DL.Tests/Services/SimpleHttpServer.cs new file mode 100644 index 0000000..f6a3396 --- /dev/null +++ b/OF DL.Tests/Services/SimpleHttpServer.cs @@ -0,0 +1,83 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace OF_DL.Tests.Services; + +internal sealed class SimpleHttpServer : IDisposable +{ + private readonly TcpListener _listener; + private readonly Task _handlerTask; + private readonly byte[] _responseBytes; + + public SimpleHttpServer(string body, DateTime? lastModifiedUtc = null) + { + _listener = new TcpListener(IPAddress.Loopback, 0); + _listener.Start(); + int port = ((IPEndPoint)_listener.LocalEndpoint).Port; + Url = new Uri($"http://127.0.0.1:{port}/"); + _responseBytes = BuildResponse(body, lastModifiedUtc); + _handlerTask = Task.Run(HandleOnceAsync); + } + + public Uri Url { get; } + + public Task Completion => _handlerTask; + + public void Dispose() + { + _listener.Stop(); + try + { + _handlerTask.Wait(TimeSpan.FromSeconds(1)); + } + catch + { + // ignored + } + } + + private async Task HandleOnceAsync() + { + using TcpClient client = await _listener.AcceptTcpClientAsync(); + await using NetworkStream stream = client.GetStream(); + await ReadHeadersAsync(stream); + await stream.WriteAsync(_responseBytes); + } + + private static async Task ReadHeadersAsync(NetworkStream stream) + { + byte[] buffer = new byte[1024]; + int read; + string data = ""; + while ((read = await stream.ReadAsync(buffer)) > 0) + { + data += Encoding.ASCII.GetString(buffer, 0, read); + if (data.Contains("\r\n\r\n", StringComparison.Ordinal)) + { + break; + } + } + } + + private static byte[] BuildResponse(string body, DateTime? lastModifiedUtc) + { + byte[] bodyBytes = Encoding.UTF8.GetBytes(body); + StringBuilder header = new(); + header.Append("HTTP/1.1 200 OK\r\n"); + header.Append("Content-Type: application/xml\r\n"); + header.Append($"Content-Length: {bodyBytes.Length}\r\n"); + if (lastModifiedUtc.HasValue) + { + header.Append($"Last-Modified: {lastModifiedUtc.Value.ToUniversalTime():R}\r\n"); + } + + header.Append("Connection: close\r\n\r\n"); + byte[] headerBytes = Encoding.ASCII.GetBytes(header.ToString()); + + byte[] response = new byte[headerBytes.Length + bodyBytes.Length]; + Buffer.BlockCopy(headerBytes, 0, response, 0, headerBytes.Length); + Buffer.BlockCopy(bodyBytes, 0, response, headerBytes.Length, bodyBytes.Length); + return response; + } +} diff --git a/OF DL.Tests/Services/TestDoubles.cs b/OF DL.Tests/Services/TestDoubles.cs index 0cfce8b..ba8797c 100644 --- a/OF DL.Tests/Services/TestDoubles.cs +++ b/OF DL.Tests/Services/TestDoubles.cs @@ -2,8 +2,16 @@ using Newtonsoft.Json.Linq; using OF_DL.Enumerations; using OF_DL.Models; using OF_DL.Models.Config; -using OF_DL.Models.Entities.Users; +using OF_DL.Models.Downloads; using OF_DL.Services; +using Serilog.Core; +using Serilog.Events; +using ArchivedEntities = OF_DL.Models.Entities.Archived; +using MessageEntities = OF_DL.Models.Entities.Messages; +using PostEntities = OF_DL.Models.Entities.Posts; +using PurchasedEntities = OF_DL.Models.Entities.Purchased; +using StreamEntities = OF_DL.Models.Entities.Streams; +using UserEntities = OF_DL.Models.Entities.Users; namespace OF_DL.Tests.Services; @@ -54,7 +62,7 @@ internal sealed class FakeConfigService(Config config) : IConfigService public bool ApplyToggleableSelections(List selectedNames) => false; } -internal sealed class FakeDbService : IDbService +internal sealed class MediaTrackingDbService : IDbService { public bool CheckDownloadedResult { get; init; } @@ -98,7 +106,7 @@ internal sealed class FakeDbService : IDbService public Task GetMostRecentPostDate(string folder) => throw new NotImplementedException(); } -internal sealed class FakeApiService : IApiService +internal sealed class StaticApiService : IApiService { public Dictionary? MediaToReturn { get; init; } @@ -167,7 +175,7 @@ internal sealed class FakeApiService : IApiService public Task> GetPurchasedTab(string endpoint, string folder, Dictionary users) => throw new NotImplementedException(); - public Task GetUserInfo(string endpoint) => + public Task GetUserInfo(string endpoint) => throw new NotImplementedException(); public Task GetUserInfoById(string endpoint) => @@ -180,6 +188,290 @@ internal sealed class FakeApiService : IApiService bool includeRestrictedSubscriptions) => throw new NotImplementedException(); } +internal sealed class ConfigurableApiService : IApiService +{ + public Func?>>? ActiveSubscriptionsHandler { get; init; } + public Func?>>? ExpiredSubscriptionsHandler { get; init; } + public Func?>>? ListsHandler { get; init; } + public Func?>>? ListUsersHandler { get; init; } + public Func?>>? MediaHandler { get; init; } + public Func>? PostHandler { get; init; } + public Func>? PaidMessageHandler { get; init; } + public Func>? UserInfoHandler { get; init; } + public Func>? UserInfoByIdHandler { get; init; } + + public Task?> GetActiveSubscriptions(string endpoint, + bool includeRestrictedSubscriptions) => + ActiveSubscriptionsHandler?.Invoke(endpoint, includeRestrictedSubscriptions) ?? + Task.FromResult?>(null); + + public Task?> GetExpiredSubscriptions(string endpoint, + bool includeRestrictedSubscriptions) => + ExpiredSubscriptionsHandler?.Invoke(endpoint, includeRestrictedSubscriptions) ?? + Task.FromResult?>(null); + + public Task?> GetLists(string endpoint) => + ListsHandler?.Invoke(endpoint) ?? Task.FromResult?>(null); + + public Task?> GetListUsers(string endpoint) => + ListUsersHandler?.Invoke(endpoint) ?? Task.FromResult?>(null); + + public Task?> GetMedia(MediaType mediaType, string endpoint, string? username, + string folder) => + MediaHandler?.Invoke(mediaType, endpoint, username, folder) ?? + Task.FromResult?>(null); + + public Task GetPost(string endpoint, string folder) => + PostHandler?.Invoke(endpoint, folder) ?? Task.FromResult(new PostEntities.SinglePostCollection()); + + public Task GetPaidMessage(string endpoint, string folder) => + PaidMessageHandler?.Invoke(endpoint, folder) ?? + Task.FromResult(new PurchasedEntities.SinglePaidMessageCollection()); + + public Task GetUserInfo(string endpoint) => + UserInfoHandler?.Invoke(endpoint) ?? Task.FromResult(null); + + public Task GetUserInfoById(string endpoint) => + UserInfoByIdHandler?.Invoke(endpoint) ?? Task.FromResult(null); + + public Task GetPaidPosts(string endpoint, string folder, string username, + List paidPostIds, IStatusReporter statusReporter) => + throw new NotImplementedException(); + + public Task GetPosts(string endpoint, string folder, List paidPostIds, + IStatusReporter statusReporter) => + throw new NotImplementedException(); + + public Task GetStreams(string endpoint, string folder, List paidPostIds, + IStatusReporter statusReporter) => + throw new NotImplementedException(); + + public Task GetArchived(string endpoint, string folder, + IStatusReporter statusReporter) => + throw new NotImplementedException(); + + public Task GetMessages(string endpoint, string folder, + IStatusReporter statusReporter) => + throw new NotImplementedException(); + + public Task GetPaidMessages(string endpoint, string folder, + string username, IStatusReporter statusReporter) => + throw new NotImplementedException(); + + public Task> GetPurchasedTabUsers(string endpoint, Dictionary users) => + throw new NotImplementedException(); + + public Task> GetPurchasedTab(string endpoint, string folder, + Dictionary users) => + throw new NotImplementedException(); + + public Dictionary GetDynamicHeaders(string path, string queryParam) => + throw new NotImplementedException(); + + public Task GetDecryptionKeyCdm(Dictionary drmHeaders, string licenceUrl, string pssh) => + throw new NotImplementedException(); + + public Task GetDrmMpdLastModified(string mpdUrl, string policy, string signature, string kvp) => + throw new NotImplementedException(); + + public Task GetDrmMpdPssh(string mpdUrl, string policy, string signature, string kvp) => + throw new NotImplementedException(); + + public Task GetDecryptionKeyOfdl(Dictionary drmHeaders, string licenceUrl, string pssh) => + throw new NotImplementedException(); +} + +internal sealed class OrchestrationDownloadServiceStub : IDownloadService +{ + public bool SinglePostCalled { get; private set; } + public bool SinglePaidMessageCalled { get; private set; } + + public DownloadResult? SinglePostResult { get; init; } + public DownloadResult? SinglePaidMessageResult { get; init; } + public DownloadResult? StoriesResult { get; init; } + + public Task CalculateTotalFileSize(List urls) => Task.FromResult((long)urls.Count); + + public Task ProcessMediaDownload(string folder, long mediaId, string apiType, string url, string path, + string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter) => + throw new NotImplementedException(); + + public Task<(string decryptionKey, DateTime lastModified)?> GetDecryptionInfo(string mpdUrl, string policy, + string signature, string kvp, string mediaId, string contentId, string drmType, bool clientIdBlobMissing, + bool devicePrivateKeyMissing) => + throw new NotImplementedException(); + + public Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username) => + Task.CompletedTask; + + public Task DownloadHighlights(string username, long userId, string path, + HashSet paidPostIds, IProgressReporter progressReporter) => + Task.FromResult(new DownloadResult()); + + public Task DownloadStories(string username, long userId, string path, + HashSet paidPostIds, IProgressReporter progressReporter) => + Task.FromResult(StoriesResult ?? new DownloadResult()); + + public Task DownloadArchived(string username, long userId, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + ArchivedEntities.ArchivedCollection archived, IProgressReporter progressReporter) => + throw new NotImplementedException(); + + public Task DownloadMessages(string username, long userId, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + MessageEntities.MessageCollection messages, IProgressReporter progressReporter) => + throw new NotImplementedException(); + + public Task DownloadPaidMessages(string username, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter) => + throw new NotImplementedException(); + + public Task DownloadStreams(string username, long userId, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + StreamEntities.StreamsCollection streams, IProgressReporter progressReporter) => + throw new NotImplementedException(); + + public Task DownloadFreePosts(string username, long userId, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PostEntities.PostCollection posts, IProgressReporter progressReporter) => + throw new NotImplementedException(); + + public Task DownloadPaidPosts(string username, long userId, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter) => + throw new NotImplementedException(); + + public Task DownloadPaidPostsPurchasedTab(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter) => + throw new NotImplementedException(); + + public Task DownloadPaidMessagesPurchasedTab(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter) => + throw new NotImplementedException(); + + public Task DownloadSinglePost(string username, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostEntities.SinglePostCollection post, + IProgressReporter progressReporter) + { + SinglePostCalled = true; + return Task.FromResult(SinglePostResult ?? new DownloadResult()); + } + + public Task DownloadSinglePaidMessage(string username, string path, + Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, + PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection, + IProgressReporter progressReporter) + { + SinglePaidMessageCalled = true; + return Task.FromResult(SinglePaidMessageResult ?? new DownloadResult()); + } +} + +internal sealed class UserTrackingDbService : IDbService +{ + public Dictionary? CreatedUsers { get; private set; } + public List CreatedDbs { get; } = []; + public (KeyValuePair user, string path)? CheckedUser { get; private set; } + + public Task CreateDb(string folder) + { + CreatedDbs.Add(folder); + return Task.CompletedTask; + } + + public Task CreateUsersDb(Dictionary users) + { + CreatedUsers = new Dictionary(users); + return Task.CompletedTask; + } + + public Task CheckUsername(KeyValuePair user, string path) + { + CheckedUser = (user, path); + return Task.CompletedTask; + } + + public Task AddMessage(string folder, long postId, string messageText, string price, bool isPaid, + bool isArchived, DateTime createdAt, long userId) => throw new NotImplementedException(); + + public Task AddPost(string folder, long postId, string messageText, string price, bool isPaid, + bool isArchived, DateTime createdAt) => throw new NotImplementedException(); + + public Task AddStory(string folder, long postId, string messageText, string price, bool isPaid, + bool isArchived, DateTime createdAt) => throw new NotImplementedException(); + + public Task AddMedia(string folder, long mediaId, long postId, string link, string? directory, + string? filename, long? size, string apiType, string mediaType, bool preview, bool downloaded, + DateTime? createdAt) => throw new NotImplementedException(); + + public Task UpdateMedia(string folder, long mediaId, string apiType, string directory, string filename, + long size, bool downloaded, DateTime createdAt) => throw new NotImplementedException(); + + public Task GetStoredFileSize(string folder, long mediaId, string apiType) => + throw new NotImplementedException(); + + public Task CheckDownloaded(string folder, long mediaId, string apiType) => + throw new NotImplementedException(); + + public Task GetMostRecentPostDate(string folder) => throw new NotImplementedException(); +} + +internal sealed class RecordingDownloadEventHandler : IDownloadEventHandler +{ + public List Messages { get; } = []; + public List<(string contentType, int mediaCount, int objectCount)> ContentFound { get; } = []; + public List NoContent { get; } = []; + public List<(string contentType, DownloadResult result)> DownloadCompletes { get; } = []; + public List<(string description, long maxValue, bool showSize)> ProgressCalls { get; } = []; + + public Task WithStatusAsync(string statusMessage, Func> work) => + work(new RecordingStatusReporter(statusMessage)); + + public Task WithProgressAsync(string description, long maxValue, bool showSize, + Func> work) + { + ProgressCalls.Add((description, maxValue, showSize)); + return work(new ProgressRecorder()); + } + + public void OnContentFound(string contentType, int mediaCount, int objectCount) => + ContentFound.Add((contentType, mediaCount, objectCount)); + + public void OnNoContentFound(string contentType) => NoContent.Add(contentType); + + public void OnDownloadComplete(string contentType, DownloadResult result) => + DownloadCompletes.Add((contentType, result)); + + public void OnUserStarting(string username) => Messages.Add($"Starting {username}"); + + public void OnUserComplete(string username, CreatorDownloadResult result) => + Messages.Add($"Completed {username}"); + + public void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount) => + Messages.Add($"Purchased {username}"); + + public void OnScrapeComplete(TimeSpan elapsed) => Messages.Add("Scrape complete"); + + public void OnMessage(string message) => Messages.Add(message); +} + +internal sealed class RecordingStatusReporter : IStatusReporter +{ + private readonly List _statuses; + + public RecordingStatusReporter(string initialStatus) + { + _statuses = [initialStatus]; + } + + public IReadOnlyList Statuses => _statuses; + + public void ReportStatus(string message) => _statuses.Add(message); +} + internal sealed class FakeFileNameService : IFileNameService { public Task BuildFilename(string fileFormat, Dictionary values) => @@ -202,7 +494,23 @@ internal sealed class FakeAuthService : IAuthService public void ValidateCookieString() => throw new NotImplementedException(); - public Task ValidateAuthAsync() => throw new NotImplementedException(); + public Task ValidateAuthAsync() => throw new NotImplementedException(); public void Logout() => throw new NotImplementedException(); } + +internal sealed class FakeLoggingService : ILoggingService +{ + public LoggingLevelSwitch LevelSwitch { get; } = new(); + public LoggingLevel LastLevel { get; private set; } = LoggingLevel.Error; + public int UpdateCount { get; private set; } + + public void UpdateLoggingLevel(LoggingLevel newLevel) + { + UpdateCount++; + LastLevel = newLevel; + LevelSwitch.MinimumLevel = (LogEventLevel)newLevel; + } + + public LoggingLevel GetCurrentLoggingLevel() => (LoggingLevel)LevelSwitch.MinimumLevel; +} diff --git a/OF DL.Tests/Services/TestScopes.cs b/OF DL.Tests/Services/TestScopes.cs new file mode 100644 index 0000000..ea96707 --- /dev/null +++ b/OF DL.Tests/Services/TestScopes.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using OF_DL.Models.OfdlApi; +using OF_DL.Services; + +namespace OF_DL.Tests.Services; + +[CollectionDefinition("NonParallel", DisableParallelization = true)] +public class NonParallelCollection +{ +} + +internal sealed class DynamicRulesCacheScope : IDisposable +{ + private static readonly FieldInfo s_rulesField = + typeof(ApiService).GetField("s_cachedDynamicRules", BindingFlags.NonPublic | BindingFlags.Static) ?? + throw new InvalidOperationException("Unable to access cached rules field."); + + private static readonly FieldInfo s_expirationField = + typeof(ApiService).GetField("s_cachedDynamicRulesExpiration", + BindingFlags.NonPublic | BindingFlags.Static) ?? + throw new InvalidOperationException("Unable to access cached rules expiration field."); + + private readonly object? _priorRules; + private readonly DateTime? _priorExpiration; + + public DynamicRulesCacheScope(DynamicRules rules) + { + _priorRules = s_rulesField.GetValue(null); + _priorExpiration = (DateTime?)s_expirationField.GetValue(null); + + s_rulesField.SetValue(null, rules); + s_expirationField.SetValue(null, DateTime.UtcNow.AddHours(1)); + } + + public void Dispose() + { + s_rulesField.SetValue(null, _priorRules); + s_expirationField.SetValue(null, _priorExpiration); + } +} + +internal sealed class CurrentDirectoryScope : IDisposable +{ + private readonly string _original; + + public CurrentDirectoryScope(string path) + { + _original = Environment.CurrentDirectory; + Environment.CurrentDirectory = path; + } + + public void Dispose() => Environment.CurrentDirectory = _original; +} From a9a4c2ee2068e76e869b7058413def5dcf45fd54 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 16:40:29 -0600 Subject: [PATCH 48/55] Add checks for valid auth data before making API calls for media --- OF DL.Core/Services/ApiService.cs | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/OF DL.Core/Services/ApiService.cs b/OF DL.Core/Services/ApiService.cs index 09efb41..c6126bd 100644 --- a/OF DL.Core/Services/ApiService.cs +++ b/OF DL.Core/Services/ApiService.cs @@ -147,6 +147,12 @@ public class ApiService(IAuthService authService, IConfigService configService, return headers; } + private bool HasSignedRequestAuth() + { + Auth? currentAuth = authService.CurrentAuth; + return currentAuth is { UserId: not null, Cookie: not null, UserAgent: not null, XBc: not null }; + } + /// /// Retrieves user information from the API. @@ -157,6 +163,11 @@ public class ApiService(IAuthService authService, IConfigService configService, { Log.Debug($"Calling GetUserInfo: {endpoint}"); + if (!HasSignedRequestAuth()) + { + return null; + } + try { UserEntities.User user = new(); @@ -197,6 +208,11 @@ public class ApiService(IAuthService authService, IConfigService configService, /// A JSON object when available. public async Task GetUserInfoById(string endpoint) { + if (!HasSignedRequestAuth()) + { + return null; + } + try { HttpClient client = new(); @@ -272,6 +288,11 @@ public class ApiService(IAuthService authService, IConfigService configService, { Log.Debug("Calling GetLists"); + if (!HasSignedRequestAuth()) + { + return null; + } + try { int offset = 0; @@ -333,6 +354,11 @@ public class ApiService(IAuthService authService, IConfigService configService, { Log.Debug($"Calling GetListUsers - {endpoint}"); + if (!HasSignedRequestAuth()) + { + return null; + } + try { int offset = 0; @@ -393,6 +419,11 @@ public class ApiService(IAuthService authService, IConfigService configService, { Log.Debug($"Calling GetMedia - {username}"); + if (!HasSignedRequestAuth()) + { + return null; + } + try { Dictionary returnUrls = new(); @@ -2928,6 +2959,11 @@ public class ApiService(IAuthService authService, IConfigService configService, private async Task?> GetAllSubscriptions(Dictionary getParams, string endpoint, bool includeRestricted) { + if (!HasSignedRequestAuth()) + { + return null; + } + try { Dictionary users = new(); From f5a8c27cd138ae3311125bdcaea6634c8bcceb45 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 17:57:20 -0600 Subject: [PATCH 49/55] Fix builds --- .gitea/workflows/publish-release.yml | 6 +++--- OF DL.Core/OF DL.Core.csproj | 22 +++++++++++----------- OF DL.Tests/OF DL.Tests.csproj | 14 ++++++++++---- OF DL/OF DL.csproj | 16 ++++++++++++++++ 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/.gitea/workflows/publish-release.yml b/.gitea/workflows/publish-release.yml index ba39afb..a11f22d 100644 --- a/.gitea/workflows/publish-release.yml +++ b/.gitea/workflows/publish-release.yml @@ -29,12 +29,12 @@ jobs: - name: Build for Windows and Linux run: | - dotnet publish -p:Version=${{ steps.version.outputs.version }} \ + dotnet publish "OF DL/OF DL.csproj" -p:Version=${{ steps.version.outputs.version }} \ -p:PackageVersion=${{ steps.version.outputs.version }} \ -p:WarningLevel=0 -c Release -r win-x86 \ --self-contained true -p:PublishSingleFile=true -o outwin - dotnet publish -p:Version=${{ steps.version.outputs.version }} \ + dotnet publish "OF DL/OF DL.csproj" -p:Version=${{ steps.version.outputs.version }} \ -p:PackageVersion=${{ steps.version.outputs.version }} \ -p:WarningLevel=0 -c Release -r linux-x64 \ --self-contained true -p:PublishSingleFile=true -o outlin @@ -70,4 +70,4 @@ jobs: env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_REPOSITORY: ${{ gitea.repository }} - GITEA_SERVER_URL: ${{ gitea.server_url }} \ No newline at end of file + GITEA_SERVER_URL: ${{ gitea.server_url }} diff --git a/OF DL.Core/OF DL.Core.csproj b/OF DL.Core/OF DL.Core.csproj index 88e3b0c..bd0d0bb 100644 --- a/OF DL.Core/OF DL.Core.csproj +++ b/OF DL.Core/OF DL.Core.csproj @@ -8,18 +8,18 @@ - + - - - - - - - - - - + + + + + + + + + + diff --git a/OF DL.Tests/OF DL.Tests.csproj b/OF DL.Tests/OF DL.Tests.csproj index cb77d5a..3909cfa 100644 --- a/OF DL.Tests/OF DL.Tests.csproj +++ b/OF DL.Tests/OF DL.Tests.csproj @@ -9,10 +9,16 @@ - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/OF DL/OF DL.csproj b/OF DL/OF DL.csproj index 63aeb86..63622ca 100644 --- a/OF DL/OF DL.csproj +++ b/OF DL/OF DL.csproj @@ -17,6 +17,22 @@ + + + + + + + + + + + + + + + + References\Spectre.Console.dll From 6e0b4eba85551ecf713599c1be1b5e156543105c Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 18:01:53 -0600 Subject: [PATCH 50/55] Upgrade dotnet from 8 to 10 --- Dockerfile | 4 ++-- OF DL.Core/OF DL.Core.csproj | 22 +++++++++++----------- OF DL.Tests/OF DL.Tests.csproj | 10 +++++----- OF DL/OF DL.csproj | 22 +++++++++++----------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Dockerfile b/Dockerfile index 013ab31..2b8e1ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM alpine:3.23 AS build ARG VERSION RUN apk --no-cache --repository community add \ - dotnet8-sdk + dotnet10-sdk # Copy source code COPY ["OF DL.sln", "/src/OF DL.sln"] @@ -28,7 +28,7 @@ FROM alpine:3.23 AS final RUN apk --no-cache --repository community add \ bash \ tini \ - dotnet8-runtime \ + dotnet10-runtime \ ffmpeg7 \ udev \ ttf-freefont \ diff --git a/OF DL.Core/OF DL.Core.csproj b/OF DL.Core/OF DL.Core.csproj index bd0d0bb..2ef833a 100644 --- a/OF DL.Core/OF DL.Core.csproj +++ b/OF DL.Core/OF DL.Core.csproj @@ -1,25 +1,25 @@ - net8.0 + net10.0 OF_DL enable enable - + - - + + - - - - - - - + + + + + + + diff --git a/OF DL.Tests/OF DL.Tests.csproj b/OF DL.Tests/OF DL.Tests.csproj index 3909cfa..d591629 100644 --- a/OF DL.Tests/OF DL.Tests.csproj +++ b/OF DL.Tests/OF DL.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 OF_DL.Tests enable enable @@ -13,8 +13,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -22,11 +22,11 @@ - + - + diff --git a/OF DL/OF DL.csproj b/OF DL/OF DL.csproj index 63622ca..f4c4eb8 100644 --- a/OF DL/OF DL.csproj +++ b/OF DL/OF DL.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 OF_DL enable enable @@ -18,18 +18,18 @@ - + - - + + - - - - - - - + + + + + + + From 3503fe1eb4295e76d39ee8e0cca4029e26d8e923 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 21:07:50 -0600 Subject: [PATCH 51/55] Fix capitalization in status message --- OF DL.Core/Services/DownloadOrchestrationService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OF DL.Core/Services/DownloadOrchestrationService.cs b/OF DL.Core/Services/DownloadOrchestrationService.cs index 8e7006c..c821c31 100644 --- a/OF DL.Core/Services/DownloadOrchestrationService.cs +++ b/OF DL.Core/Services/DownloadOrchestrationService.cs @@ -383,7 +383,7 @@ public class DownloadOrchestrationService( Dictionary purchasedTabUsers = await apiService.GetPurchasedTabUsers("/posts/paid/all", users); - eventHandler.OnMessage("Checking folders for Users in Purchased Tab"); + eventHandler.OnMessage("Checking folders for users in Purchased Tab"); foreach (KeyValuePair user in purchasedTabUsers) { From fe0c81b2acbf995b23eaddbbb547841007c14ee9 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Wed, 11 Feb 2026 12:28:53 -0600 Subject: [PATCH 52/55] Remove BOM --- OF DL.Tests/OF DL.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OF DL.Tests/OF DL.Tests.csproj b/OF DL.Tests/OF DL.Tests.csproj index d591629..1689e24 100644 --- a/OF DL.Tests/OF DL.Tests.csproj +++ b/OF DL.Tests/OF DL.Tests.csproj @@ -1,4 +1,4 @@ - + net10.0 From fdb6d554546169cef94f06a3f270194b3b8a2f44 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Thu, 12 Feb 2026 09:50:59 -0600 Subject: [PATCH 53/55] Fix ffmpeg logs path on windows --- OF DL.Core/Services/DownloadService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OF DL.Core/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs index 631fb20..5fedd5a 100644 --- a/OF DL.Core/Services/DownloadService.cs +++ b/OF DL.Core/Services/DownloadService.cs @@ -146,10 +146,10 @@ public class DownloadService( if (logLevelArgs.Contains("-report", StringComparison.OrdinalIgnoreCase)) { - // Direct ffmpeg report files into the same logs directory Serilog uses (relative to current working directory) - string logDir = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "logs")); + // Use a relative path so FFREPORT parsing works on Windows (drive-letter ':' breaks option parsing). + string logDir = Path.Combine(Environment.CurrentDirectory, "logs"); Directory.CreateDirectory(logDir); - string ffReportPath = Path.Combine(logDir, "ffmpeg-%p-%t.log"); // ffmpeg will replace %p/%t + string ffReportPath = Path.Combine("logs", "ffmpeg-%p-%t.log").Replace("\\", "/"); Environment.SetEnvironmentVariable("FFREPORT", $"file={ffReportPath}:level=32"); Log.Debug("FFREPORT enabled at: {FFREPORT} (cwd: {Cwd})", Environment.GetEnvironmentVariable("FFREPORT"), Environment.CurrentDirectory); From f16ad7f138a507c938dee85a3af0869e5be5586b Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Thu, 12 Feb 2026 10:48:44 -0600 Subject: [PATCH 54/55] Improve CLI output to show counts of media --- .../Services/DownloadOrchestrationService.cs | 55 ++++++++++++++----- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/OF DL.Core/Services/DownloadOrchestrationService.cs b/OF DL.Core/Services/DownloadOrchestrationService.cs index c821c31..2a27c79 100644 --- a/OF DL.Core/Services/DownloadOrchestrationService.cs +++ b/OF DL.Core/Services/DownloadOrchestrationService.cs @@ -433,7 +433,7 @@ public class DownloadOrchestrationService( : purchasedTabCollection.PaidPosts.PaidPosts.Count; DownloadResult postResult = await eventHandler.WithProgressAsync( - $"Downloading {purchasedTabCollection.PaidPosts.PaidPosts.Count} Paid Posts", + $"Downloading {purchasedTabCollection.PaidPosts.PaidPosts.Count} Media from {purchasedTabCollection.PaidPosts.PaidPostObjects.Count} Paid Posts", totalSize, config.ShowScrapeSize, async reporter => await downloadService.DownloadPaidPostsPurchasedTab( purchasedTabCollection.Username, path, users, @@ -461,7 +461,7 @@ public class DownloadOrchestrationService( : purchasedTabCollection.PaidMessages.PaidMessages.Count; DownloadResult msgResult = await eventHandler.WithProgressAsync( - $"Downloading {purchasedTabCollection.PaidMessages.PaidMessages.Count} Paid Messages", + $"Downloading {purchasedTabCollection.PaidMessages.PaidMessages.Count} Media from {purchasedTabCollection.PaidMessages.PaidMessageObjects.Count} Paid Messages", totalSize, config.ShowScrapeSize, async reporter => await downloadService.DownloadPaidMessagesPurchasedTab( purchasedTabCollection.Username, path, users, @@ -502,48 +502,77 @@ public class DownloadOrchestrationService( PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection = await apiService.GetPaidMessage($"/messages/{messageId}", path); - if (singlePaidMessageCollection.SingleMessages.Count == 0) + if (singlePaidMessageCollection.SingleMessages.Count == 0 && + singlePaidMessageCollection.PreviewSingleMessages.Count == 0) { eventHandler.OnNoContentFound("Paid Messages"); return; } Config config = configService.CurrentConfig; + int messageCount = singlePaidMessageCollection.SingleMessageObjects.Count; + string messageLabel = messageCount == 1 ? "Paid Message" : "Paid Messages"; + int previewCount = singlePaidMessageCollection.PreviewSingleMessages.Count; + int paidCount = singlePaidMessageCollection.SingleMessages.Count; + int totalCount = previewCount + paidCount; - // Handle preview messages - if (singlePaidMessageCollection.PreviewSingleMessages.Count > 0) + // Handle mixed paid + preview message media. + if (previewCount > 0 && paidCount > 0) + { + eventHandler.OnContentFound("Paid Messages", + totalCount, + singlePaidMessageCollection.SingleMessageObjects.Count); + + List allMessageUrls = []; + allMessageUrls.AddRange(singlePaidMessageCollection.PreviewSingleMessages.Values); + allMessageUrls.AddRange(singlePaidMessageCollection.SingleMessages.Values); + + long totalSize = config.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize(allMessageUrls) + : totalCount; + + DownloadResult result = await eventHandler.WithProgressAsync( + $"Downloading {totalCount} Media from {messageCount} {messageLabel} ({paidCount} Paid + {previewCount} Preview)", + totalSize, config.ShowScrapeSize, + async reporter => await downloadService.DownloadSinglePaidMessage(username, path, users, + clientIdBlobMissing, devicePrivateKeyMissing, singlePaidMessageCollection, reporter)); + + eventHandler.OnDownloadComplete("Paid Messages", result); + } + // Handle preview-only message media. + else if (previewCount > 0) { eventHandler.OnContentFound("Preview Paid Messages", - singlePaidMessageCollection.PreviewSingleMessages.Count, + previewCount, singlePaidMessageCollection.SingleMessageObjects.Count); long previewSize = config.ShowScrapeSize ? await downloadService.CalculateTotalFileSize( singlePaidMessageCollection.PreviewSingleMessages.Values.ToList()) - : singlePaidMessageCollection.PreviewSingleMessages.Count; + : previewCount; DownloadResult previewResult = await eventHandler.WithProgressAsync( - $"Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Paid Messages", + $"Downloading {previewCount} Preview Media from {messageCount} {messageLabel}", previewSize, config.ShowScrapeSize, async reporter => await downloadService.DownloadSinglePaidMessage(username, path, users, clientIdBlobMissing, devicePrivateKeyMissing, singlePaidMessageCollection, reporter)); eventHandler.OnDownloadComplete("Paid Messages", previewResult); } - else if (singlePaidMessageCollection.SingleMessages.Count > 0) + else if (paidCount > 0) { // Only actual paid messages, no preview eventHandler.OnContentFound("Paid Messages", - singlePaidMessageCollection.SingleMessages.Count, + paidCount, singlePaidMessageCollection.SingleMessageObjects.Count); long totalSize = config.ShowScrapeSize ? await downloadService.CalculateTotalFileSize( singlePaidMessageCollection.SingleMessages.Values.ToList()) - : singlePaidMessageCollection.SingleMessages.Count; + : paidCount; DownloadResult result = await eventHandler.WithProgressAsync( - $"Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages", + $"Downloading {paidCount} Paid Media from {messageCount} {messageLabel}", totalSize, config.ShowScrapeSize, async reporter => await downloadService.DownloadSinglePaidMessage(username, path, users, clientIdBlobMissing, devicePrivateKeyMissing, singlePaidMessageCollection, reporter)); @@ -607,7 +636,7 @@ public class DownloadOrchestrationService( : mediaCount; DownloadResult result = await eventHandler.WithProgressAsync( - $"Downloading {mediaCount} {contentType}", totalSize, config.ShowScrapeSize, + $"Downloading {mediaCount} Media from {objectCount} {contentType}", totalSize, config.ShowScrapeSize, async reporter => await downloadData(data, reporter)); eventHandler.OnDownloadComplete(contentType, result); From 5f945aadfa66ea37bd1cedda43aa6ddea2de5277 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Thu, 12 Feb 2026 10:57:01 -0600 Subject: [PATCH 55/55] Fix failing test --- OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs b/OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs index 9167b52..1e04fb9 100644 --- a/OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs +++ b/OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs @@ -196,7 +196,7 @@ public class DownloadOrchestrationServiceTests await service.DownloadSinglePaidMessageAsync("creator", 5, "/tmp", new Dictionary(), true, true, eventHandler); - Assert.Contains(eventHandler.ContentFound, entry => entry.contentType == "Preview Paid Messages"); + Assert.Contains(eventHandler.ContentFound, entry => entry.contentType == "Paid Messages"); Assert.True(downloadService.SinglePaidMessageCalled); }