From e184df906f64e4564258b467fbd7cfb0a167ed1f Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 10:30:05 -0600 Subject: [PATCH] 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();