Add header comments and extract duplicated exception logging to a helper function

This commit is contained in:
whimsical-c4lic0 2026-02-10 10:30:05 -06:00
parent 4889be1890
commit e184df906f
18 changed files with 917 additions and 746 deletions

View File

@ -0,0 +1,26 @@
using Serilog;
namespace OF_DL.Helpers;
internal static class ExceptionLoggerHelper
{
/// <summary>
/// Logs an exception to the console and Serilog with inner exception details.
/// </summary>
/// <param name="ex">The exception to log.</param>
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);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -29,8 +29,16 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
UserDataDir = Path.GetFullPath("chrome-data")
};
/// <summary>
/// Gets or sets the current authentication state.
/// </summary>
public Auth? CurrentAuth { get; set; }
/// <summary>
/// Loads authentication data from disk.
/// </summary>
/// <param name="filePath">The auth file path.</param>
/// <returns>True when auth data is loaded successfully.</returns>
public async Task<bool> LoadFromFileAsync(string filePath = "auth.json")
{
try
@ -53,6 +61,10 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
}
}
/// <summary>
/// Launches a browser session and extracts auth data after login.
/// </summary>
/// <returns>True when auth data is captured successfully.</returns>
public async Task<bool> LoadFromBrowserAsync()
{
try
@ -71,6 +83,10 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
}
}
/// <summary>
/// Persists the current auth data to disk.
/// </summary>
/// <param name="filePath">The auth file path.</param>
public async Task SaveToFileAsync(string filePath = "auth.json")
{
if (CurrentAuth == null)
@ -129,6 +145,9 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
private async Task<string> GetBcToken(IPage page) =>
await page.EvaluateExpressionAsync<string>("window.localStorage.getItem('bcTokenSha') || ''");
/// <summary>
/// Normalizes the stored cookie string to only include required cookie values.
/// </summary>
public void ValidateCookieString()
{
if (CurrentAuth == null)
@ -159,6 +178,10 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
}
}
/// <summary>
/// Validates auth by requesting the current user profile.
/// </summary>
/// <returns>The authenticated user or null when validation fails.</returns>
public async Task<UserEntities.User?> ValidateAuthAsync()
{
// Resolve IApiService lazily to avoid circular dependency
@ -166,6 +189,9 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
return await apiService.GetUserInfo("/users/me");
}
/// <summary>
/// Clears persisted auth data and browser profile state.
/// </summary>
public void Logout()
{
if (Directory.Exists("chrome-data"))

View File

@ -12,9 +12,21 @@ namespace OF_DL.Services;
public class ConfigService(ILoggingService loggingService) : IConfigService
{
/// <summary>
/// Gets the active configuration in memory.
/// </summary>
public Config CurrentConfig { get; private set; } = new();
/// <summary>
/// Gets whether the CLI requested non-interactive mode.
/// </summary>
public bool IsCliNonInteractive { get; private set; }
/// <summary>
/// Loads configuration from disk and applies runtime settings.
/// </summary>
/// <param name="args">CLI arguments used to influence configuration.</param>
/// <returns>True when configuration is loaded successfully.</returns>
public async Task<bool> LoadConfigurationAsync(string[] args)
{
try
@ -73,6 +85,10 @@ public class ConfigService(ILoggingService loggingService) : IConfigService
}
}
/// <summary>
/// Saves the current configuration to disk.
/// </summary>
/// <param name="filePath">The destination config file path.</param>
public async Task SaveConfigurationAsync(string filePath = "config.conf")
{
try
@ -87,6 +103,10 @@ public class ConfigService(ILoggingService loggingService) : IConfigService
}
}
/// <summary>
/// Replaces the current configuration and applies runtime settings.
/// </summary>
/// <param name="newConfig">The new configuration instance.</param>
public void UpdateConfig(Config newConfig)
{
CurrentConfig = newConfig;
@ -399,6 +419,9 @@ public class ConfigService(ILoggingService loggingService) : IConfigService
}
}
/// <summary>
/// Returns toggleable config properties and their current values.
/// </summary>
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;
}
/// <summary>
/// Applies a set of toggle selections to the configuration.
/// </summary>
/// <param name="selectedNames">The names of toggles that should be enabled.</param>
/// <returns>True when any values were changed.</returns>
public bool ApplyToggleableSelections(List<string> selectedNames)
{
bool configChanged = false;

View File

@ -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
{
/// <summary>
/// Creates or updates the per-user metadata database.
/// </summary>
/// <param name="folder">The user folder path.</param>
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);
}
}
/// <summary>
/// Creates or updates the global users database.
/// </summary>
/// <param name="users">The users to seed or update.</param>
public async Task CreateUsersDb(Dictionary<string, long> 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);
}
}
/// <summary>
/// Ensures a username matches the stored user ID and migrates folders if needed.
/// </summary>
/// <param name="user">The user pair to validate.</param>
/// <param name="path">The expected user folder path.</param>
public async Task CheckUsername(KeyValuePair<string, long> 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);
}
}
/// <summary>
/// Inserts a message record when it does not already exist.
/// </summary>
/// <param name="folder">The user folder path.</param>
/// <param name="postId">The message or post ID.</param>
/// <param name="messageText">The message text.</param>
/// <param name="price">The price string.</param>
/// <param name="isPaid">Whether the message is paid.</param>
/// <param name="isArchived">Whether the message is archived.</param>
/// <param name="createdAt">The creation timestamp.</param>
/// <param name="userId">The sender user ID.</param>
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);
}
}
/// <summary>
/// Inserts a post record when it does not already exist.
/// </summary>
/// <param name="folder">The user folder path.</param>
/// <param name="postId">The post ID.</param>
/// <param name="messageText">The post text.</param>
/// <param name="price">The price string.</param>
/// <param name="isPaid">Whether the post is paid.</param>
/// <param name="isArchived">Whether the post is archived.</param>
/// <param name="createdAt">The creation timestamp.</param>
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);
}
}
/// <summary>
/// Inserts a story record when it does not already exist.
/// </summary>
/// <param name="folder">The user folder path.</param>
/// <param name="postId">The story ID.</param>
/// <param name="messageText">The story text.</param>
/// <param name="price">The price string.</param>
/// <param name="isPaid">Whether the story is paid.</param>
/// <param name="isArchived">Whether the story is archived.</param>
/// <param name="createdAt">The creation timestamp.</param>
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);
}
}
/// <summary>
/// Inserts a media record when it does not already exist.
/// </summary>
/// <param name="folder">The user folder path.</param>
/// <param name="mediaId">The media ID.</param>
/// <param name="postId">The parent post ID.</param>
/// <param name="link">The media URL.</param>
/// <param name="directory">The local directory path.</param>
/// <param name="filename">The local filename.</param>
/// <param name="size">The media size in bytes.</param>
/// <param name="apiType">The API type label.</param>
/// <param name="mediaType">The media type label.</param>
/// <param name="preview">Whether the media is a preview.</param>
/// <param name="downloaded">Whether the media is downloaded.</param>
/// <param name="createdAt">The creation timestamp.</param>
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);
}
}
/// <summary>
/// Checks whether the media has been marked as downloaded.
/// </summary>
/// <param name="folder">The user folder path.</param>
/// <param name="mediaId">The media ID.</param>
/// <param name="apiType">The API type label.</param>
/// <returns>True when the media is marked as downloaded.</returns>
public async Task<bool> 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;
}
/// <summary>
/// Updates the media record with local file details.
/// </summary>
/// <param name="folder">The user folder path.</param>
/// <param name="mediaId">The media ID.</param>
/// <param name="apiType">The API type label.</param>
/// <param name="directory">The local directory path.</param>
/// <param name="filename">The local filename.</param>
/// <param name="size">The file size in bytes.</param>
/// <param name="downloaded">Whether the media is downloaded.</param>
/// <param name="createdAt">The creation timestamp.</param>
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
}
/// <summary>
/// Returns the stored size for a media record.
/// </summary>
/// <param name="folder">The user folder path.</param>
/// <param name="mediaId">The media ID.</param>
/// <param name="apiType">The API type label.</param>
/// <returns>The stored file size.</returns>
public async Task<long> 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;
}
/// <summary>
/// Returns the most recent post date based on downloaded and pending media.
/// </summary>
/// <param name="folder">The user folder path.</param>
/// <returns>The most recent post date if available.</returns>
public async Task<DateTime?> GetMostRecentPostDate(string folder)
{
DateTime? mostRecentDate = null;

View File

@ -15,8 +15,15 @@ public class DownloadOrchestrationService(
IDownloadService downloadService,
IDbService dbService) : IDownloadOrchestrationService
{
/// <summary>
/// Gets the list of paid post media IDs to avoid duplicates.
/// </summary>
public List<long> PaidPostIds { get; } = new();
/// <summary>
/// Retrieves the available users and lists based on current configuration.
/// </summary>
/// <returns>A result containing users, lists, and any errors.</returns>
public async Task<UserListResult> GetAvailableUsersAsync()
{
UserListResult result = new();
@ -89,6 +96,13 @@ public class DownloadOrchestrationService(
return result;
}
/// <summary>
/// Resolves the users that belong to a specific list.
/// </summary>
/// <param name="listName">The list name.</param>
/// <param name="allUsers">All available users.</param>
/// <param name="lists">Known lists keyed by name.</param>
/// <returns>The users that belong to the list.</returns>
public async Task<Dictionary<string, long>> GetUsersForListAsync(
string listName, Dictionary<string, long> allUsers, Dictionary<string, long> lists)
{
@ -98,11 +112,22 @@ public class DownloadOrchestrationService(
.ToDictionary(x => x.Key, x => x.Value);
}
/// <summary>
/// Resolves the download path for a username based on configuration.
/// </summary>
/// <param name="username">The creator username.</param>
/// <returns>The resolved download path.</returns>
public string ResolveDownloadPath(string username) =>
!string.IsNullOrEmpty(configService.CurrentConfig.DownloadPath)
? Path.Combine(configService.CurrentConfig.DownloadPath, username)
: $"__user_data__/sites/OnlyFans/{username}";
/// <summary>
/// Ensures the user folder and metadata database exist.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="userId">The creator user ID.</param>
/// <param name="path">The creator folder path.</param>
public async Task PrepareUserFolderAsync(string username, long userId, string path)
{
await dbService.CheckUsername(new KeyValuePair<string, long>(username, userId), path);
@ -116,6 +141,17 @@ public class DownloadOrchestrationService(
await dbService.CreateDb(path);
}
/// <summary>
/// Downloads all configured content types for a creator.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="userId">The creator user ID.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="eventHandler">Download event handler.</param>
/// <returns>Counts of downloaded items per content type.</returns>
public async Task<CreatorDownloadResult> DownloadCreatorContentAsync(
string username, long userId, string path,
Dictionary<string, long> users,
@ -281,6 +317,16 @@ public class DownloadOrchestrationService(
return counts;
}
/// <summary>
/// Downloads a single post by ID for a creator.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="postId">The post ID.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="eventHandler">Download event handler.</param>
public async Task DownloadSinglePostAsync(
string username, long postId, string path,
Dictionary<string, long> users,
@ -320,6 +366,13 @@ public class DownloadOrchestrationService(
}
}
/// <summary>
/// Downloads content from the Purchased tab across creators.
/// </summary>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="eventHandler">Download event handler.</param>
public async Task DownloadPurchasedTabAsync(
Dictionary<string, long> users,
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
@ -427,6 +480,16 @@ public class DownloadOrchestrationService(
}
}
/// <summary>
/// Downloads a single paid message by ID.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="messageId">The message ID.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="eventHandler">Download event handler.</param>
public async Task DownloadSinglePaidMessageAsync(
string username, long messageId, string path,
Dictionary<string, long> users,
@ -493,6 +556,11 @@ public class DownloadOrchestrationService(
}
}
/// <summary>
/// Resolves a username for a user ID, including deleted users.
/// </summary>
/// <param name="userId">The user ID.</param>
/// <returns>The resolved username or a deleted user placeholder.</returns>
public async Task<string?> ResolveUsernameAsync(long userId)
{
JObject? user = await apiService.GetUserInfoById($"/users/list?x[]={userId}");

View File

@ -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<bool> _completionSource = new();
/// <summary>
/// Downloads profile avatar and header images for a creator.
/// </summary>
/// <param name="avatarUrl">The avatar URL.</param>
/// <param name="headerUrl">The header URL.</param>
/// <param name="folder">The creator folder path.</param>
/// <param name="username">The creator username.</param>
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;
}
/// <summary>
/// Retrieves the last modified timestamp for a DRM media URL.
/// </summary>
/// <param name="url">The DRM media URL (including CloudFront tokens).</param>
/// <param name="auth">The current auth context.</param>
/// <returns>The last modified timestamp if available.</returns>
public static async Task<DateTime> GetDrmVideoLastModified(string url, Auth auth)
{
string[] messageUrlParsed = url.Split(',');
@ -515,6 +511,11 @@ public class DownloadService(
return DateTime.Now;
}
/// <summary>
/// Retrieves the last modified timestamp for a media URL.
/// </summary>
/// <param name="url">The media URL.</param>
/// <returns>The last modified timestamp if available.</returns>
public static async Task<DateTime> 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<bool> 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;
}
/// <summary>
/// Calculates the total size of a set of URLs by fetching their metadata.
/// </summary>
/// <param name="urls">The media URLs.</param>
/// <returns>The total size in bytes.</returns>
public async Task<long> CalculateTotalFileSize(List<string> urls)
{
long totalFileSize = 0;
@ -826,6 +827,21 @@ public class DownloadService(
return totalFileSize;
}
/// <summary>
/// Downloads a single media item, applying filename formatting and folder rules.
/// </summary>
/// <param name="url">The media URL.</param>
/// <param name="folder">The creator folder path.</param>
/// <param name="mediaId">The media ID.</param>
/// <param name="apiType">The API type label.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <param name="path">The relative folder path.</param>
/// <param name="filenameFormat">Optional filename format.</param>
/// <param name="postInfo">Post or message info.</param>
/// <param name="postMedia">Media info.</param>
/// <param name="author">Author info.</param>
/// <param name="users">Known users map.</param>
/// <returns>True when the media is newly downloaded.</returns>
public async Task<bool> 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);
}
/// <summary>
/// Downloads a DRM-protected video using the provided decryption key.
/// </summary>
/// <param name="policy">CloudFront policy token.</param>
/// <param name="signature">CloudFront signature token.</param>
/// <param name="kvp">CloudFront key pair ID.</param>
/// <param name="url">The MPD URL.</param>
/// <param name="decryptionKey">The decryption key.</param>
/// <param name="folder">The creator folder path.</param>
/// <param name="lastModified">The source last modified timestamp.</param>
/// <param name="mediaId">The media ID.</param>
/// <param name="apiType">The API type label.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <param name="path">The relative folder path.</param>
/// <param name="filenameFormat">Optional filename format.</param>
/// <param name="postInfo">Post or message info.</param>
/// <param name="postMedia">Media info.</param>
/// <param name="author">Author info.</param>
/// <param name="users">Known users map.</param>
/// <returns>True when the media is newly downloaded.</returns>
public async Task<bool> 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);
/// <summary>
/// Retrieves decryption information for a DRM media item.
/// </summary>
/// <param name="mpdUrl">The MPD URL.</param>
/// <param name="policy">CloudFront policy token.</param>
/// <param name="signature">CloudFront signature token.</param>
/// <param name="kvp">CloudFront key pair ID.</param>
/// <param name="mediaId">The media ID.</param>
/// <param name="contentId">The content ID.</param>
/// <param name="drmType">The DRM type.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <returns>The decryption key and last modified timestamp.</returns>
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);
}
/// <summary>
/// Downloads highlight media for a creator.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="userId">The creator user ID.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="paidPostIds">Paid post media IDs.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadHighlights(string username, long userId, string path,
HashSet<long> paidPostIds, IProgressReporter progressReporter)
{
@ -1032,6 +1076,15 @@ public class DownloadService(
};
}
/// <summary>
/// Downloads story media for a creator.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="userId">The creator user ID.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="paidPostIds">Paid post media IDs.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadStories(string username, long userId, string path,
HashSet<long> paidPostIds, IProgressReporter progressReporter)
{
@ -1084,6 +1137,18 @@ public class DownloadService(
};
}
/// <summary>
/// Downloads archived posts for a creator.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="userId">The creator user ID.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="archived">The archived posts collection.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadArchived(string username, long userId, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
ArchivedEntities.ArchivedCollection archived, IProgressReporter progressReporter)
@ -1165,6 +1230,18 @@ public class DownloadService(
};
}
/// <summary>
/// Downloads free messages for a creator.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="userId">The creator user ID.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="messages">The messages collection.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadMessages(string username, long userId, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
MessageEntities.MessageCollection messages, IProgressReporter progressReporter)
@ -1247,6 +1324,17 @@ public class DownloadService(
}
/// <summary>
/// Downloads paid messages for a creator.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="paidMessageCollection">The paid message collection.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadPaidMessages(string username, string path, Dictionary<string, long> users,
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PurchasedEntities.PaidMessageCollection paidMessageCollection,
@ -1330,6 +1418,18 @@ public class DownloadService(
};
}
/// <summary>
/// Downloads stream posts for a creator.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="userId">The creator user ID.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="streams">The streams collection.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadStreams(string username, long userId, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
StreamEntities.StreamsCollection streams, IProgressReporter progressReporter)
@ -1410,6 +1510,18 @@ public class DownloadService(
};
}
/// <summary>
/// Downloads free posts for a creator.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="userId">The creator user ID.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="posts">The posts collection.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadFreePosts(string username, long userId, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PostEntities.PostCollection posts,
@ -1490,6 +1602,18 @@ public class DownloadService(
};
}
/// <summary>
/// Downloads paid posts for a creator.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="userId">The creator user ID.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="purchasedPosts">The paid post collection.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadPaidPosts(string username, long userId, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter)
@ -1571,6 +1695,17 @@ public class DownloadService(
};
}
/// <summary>
/// Downloads paid posts sourced from the Purchased tab.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="purchasedPosts">The paid post collection.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadPaidPostsPurchasedTab(string username, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter)
@ -1645,6 +1780,17 @@ public class DownloadService(
};
}
/// <summary>
/// Downloads paid messages sourced from the Purchased tab.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="paidMessageCollection">The paid message collection.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadPaidMessagesPurchasedTab(string username, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter)
@ -1718,6 +1864,17 @@ public class DownloadService(
};
}
/// <summary>
/// Downloads a single post collection.
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="post">The single post collection.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadSinglePost(string username, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PostEntities.SinglePostCollection post, IProgressReporter progressReporter)
@ -1796,6 +1953,17 @@ public class DownloadService(
};
}
/// <summary>
/// Downloads a single paid message collection (including previews).
/// </summary>
/// <param name="username">The creator username.</param>
/// <param name="path">The creator folder path.</param>
/// <param name="users">Known users map.</param>
/// <param name="clientIdBlobMissing">Whether the CDM client ID blob is missing.</param>
/// <param name="devicePrivateKeyMissing">Whether the CDM private key is missing.</param>
/// <param name="singlePaidMessageCollection">The single paid message collection.</param>
/// <param name="progressReporter">Progress reporter.</param>
/// <returns>The download result.</returns>
public async Task<DownloadResult> DownloadSinglePaidMessage(string username, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection,

View File

@ -5,6 +5,16 @@ namespace OF_DL.Services;
public class FileNameService(IAuthService authService) : IFileNameService
{
/// <summary>
/// Builds a map of filename token values from post, media, and author data.
/// </summary>
/// <param name="info">The post or message object.</param>
/// <param name="media">The media object.</param>
/// <param name="author">The author object.</param>
/// <param name="selectedProperties">The tokens requested by the filename format.</param>
/// <param name="username">The resolved username when available.</param>
/// <param name="users">Optional lookup of user IDs to usernames.</param>
/// <returns>A dictionary of token values keyed by token name.</returns>
public async Task<Dictionary<string, string>> GetFilename(object info, object media, object author,
List<string> selectedProperties, string username, Dictionary<string, long>? users = null)
{
@ -168,6 +178,12 @@ public class FileNameService(IAuthService authService) : IFileNameService
return values;
}
/// <summary>
/// Applies token values to a filename format and removes invalid file name characters.
/// </summary>
/// <param name="fileFormat">The filename format string.</param>
/// <param name="values">Token values to substitute.</param>
/// <returns>The resolved filename.</returns>
public Task<string> BuildFilename(string fileFormat, Dictionary<string, string> values)
{
foreach (KeyValuePair<string, string> kvp in values)

View File

@ -11,55 +11,121 @@ namespace OF_DL.Services;
public interface IApiService
{
/// <summary>
/// Retrieves a decryption key using the local CDM integration.
/// </summary>
Task<string> GetDecryptionKeyCdm(Dictionary<string, string> drmHeaders, string licenceUrl, string pssh);
/// <summary>
/// Retrieves the last modified timestamp for a DRM MPD manifest.
/// </summary>
Task<DateTime> GetDrmMpdLastModified(string mpdUrl, string policy, string signature, string kvp);
/// <summary>
/// Retrieves the Widevine PSSH from an MPD manifest.
/// </summary>
Task<string> GetDrmMpdPssh(string mpdUrl, string policy, string signature, string kvp);
/// <summary>
/// Retrieves the user's lists.
/// </summary>
Task<Dictionary<string, long>?> GetLists(string endpoint);
/// <summary>
/// Retrieves usernames for a specific list.
/// </summary>
Task<List<string>?> GetListUsers(string endpoint);
/// <summary>
/// Retrieves media URLs for stories or highlights.
/// </summary>
Task<Dictionary<long, string>?> GetMedia(MediaType mediaType, string endpoint, string? username, string folder,
List<long> paidPostIds);
/// <summary>
/// Retrieves paid posts and their media.
/// </summary>
Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder, string username,
List<long> paidPostIds,
IStatusReporter statusReporter);
/// <summary>
/// Retrieves posts and their media.
/// </summary>
Task<PostEntities.PostCollection> GetPosts(string endpoint, string folder, List<long> paidPostIds,
IStatusReporter statusReporter);
/// <summary>
/// Retrieves a single post and its media.
/// </summary>
Task<PostEntities.SinglePostCollection> GetPost(string endpoint, string folder);
/// <summary>
/// Retrieves streams and their media.
/// </summary>
Task<StreamEntities.StreamsCollection> GetStreams(string endpoint, string folder, List<long> paidPostIds,
IStatusReporter statusReporter);
/// <summary>
/// Retrieves archived posts and their media.
/// </summary>
Task<ArchivedEntities.ArchivedCollection> GetArchived(string endpoint, string folder,
IStatusReporter statusReporter);
/// <summary>
/// Retrieves messages and their media.
/// </summary>
Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder, IStatusReporter statusReporter);
/// <summary>
/// Retrieves paid messages and their media.
/// </summary>
Task<PurchasedEntities.PaidMessageCollection> GetPaidMessages(string endpoint, string folder, string username,
IStatusReporter statusReporter);
/// <summary>
/// Retrieves a single paid message and its media.
/// </summary>
Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder);
/// <summary>
/// Retrieves users that appear in the Purchased tab.
/// </summary>
Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users);
/// <summary>
/// Retrieves Purchased tab content grouped by user.
/// </summary>
Task<List<PurchasedEntities.PurchasedTabCollection>> GetPurchasedTab(string endpoint, string folder,
Dictionary<string, long> users);
/// <summary>
/// Retrieves user information.
/// </summary>
Task<UserEntities.User?> GetUserInfo(string endpoint);
/// <summary>
/// Retrieves user information by ID.
/// </summary>
Task<JObject?> GetUserInfoById(string endpoint);
/// <summary>
/// Builds signed headers for API requests.
/// </summary>
Dictionary<string, string> GetDynamicHeaders(string path, string queryParam);
/// <summary>
/// Retrieves active subscriptions.
/// </summary>
Task<Dictionary<string, long>?> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions);
/// <summary>
/// Retrieves expired subscriptions.
/// </summary>
Task<Dictionary<string, long>?> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions);
/// <summary>
/// Retrieves a decryption key via the OFDL fallback service.
/// </summary>
Task<string> GetDecryptionKeyOfdl(Dictionary<string, string> drmHeaders, string licenceUrl, string pssh);
}

View File

@ -5,12 +5,24 @@ namespace OF_DL.Services;
public interface IAuthService
{
/// <summary>
/// Gets or sets the current authentication state.
/// </summary>
Auth? CurrentAuth { get; set; }
/// <summary>
/// Loads authentication data from disk.
/// </summary>
Task<bool> LoadFromFileAsync(string filePath = "auth.json");
/// <summary>
/// Launches a browser session and extracts auth data after login.
/// </summary>
Task<bool> LoadFromBrowserAsync();
/// <summary>
/// Persists the current auth data to disk.
/// </summary>
Task SaveToFileAsync(string filePath = "auth.json");
/// <summary>

View File

@ -4,14 +4,29 @@ namespace OF_DL.Services;
public interface IConfigService
{
/// <summary>
/// Gets the active configuration in memory.
/// </summary>
Config CurrentConfig { get; }
/// <summary>
/// Gets whether the CLI requested non-interactive mode.
/// </summary>
bool IsCliNonInteractive { get; }
/// <summary>
/// Loads configuration from disk and applies runtime settings.
/// </summary>
Task<bool> LoadConfigurationAsync(string[] args);
/// <summary>
/// Saves the current configuration to disk.
/// </summary>
Task SaveConfigurationAsync(string filePath = "config.conf");
/// <summary>
/// Replaces the current configuration and applies runtime settings.
/// </summary>
void UpdateConfig(Config newConfig);
/// <summary>

View File

@ -2,30 +2,63 @@ namespace OF_DL.Services;
public interface IDbService
{
/// <summary>
/// Inserts a message record when it does not already exist.
/// </summary>
Task AddMessage(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived,
DateTime createdAt, long userId);
/// <summary>
/// Inserts a post record when it does not already exist.
/// </summary>
Task AddPost(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived,
DateTime createdAt);
/// <summary>
/// Inserts a story record when it does not already exist.
/// </summary>
Task AddStory(string folder, long postId, string messageText, string price, bool isPaid, bool isArchived,
DateTime createdAt);
/// <summary>
/// Creates or updates the per-user metadata database.
/// </summary>
Task CreateDb(string folder);
/// <summary>
/// Creates or updates the global users database.
/// </summary>
Task CreateUsersDb(Dictionary<string, long> users);
/// <summary>
/// Ensures a username matches the stored user ID and migrates folders if needed.
/// </summary>
Task CheckUsername(KeyValuePair<string, long> user, string path);
/// <summary>
/// Inserts a media record when it does not already exist.
/// </summary>
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);
/// <summary>
/// Updates the media record with local file details.
/// </summary>
Task UpdateMedia(string folder, long mediaId, string apiType, string directory, string filename, long size,
bool downloaded, DateTime createdAt);
/// <summary>
/// Returns the stored size for a media record.
/// </summary>
Task<long> GetStoredFileSize(string folder, long mediaId, string apiType);
/// <summary>
/// Checks whether the media has been marked as downloaded.
/// </summary>
Task<bool> CheckDownloaded(string folder, long mediaId, string apiType);
/// <summary>
/// Returns the most recent post date based on downloaded and pending media.
/// </summary>
Task<DateTime?> GetMostRecentPostDate(string folder);
}

View File

@ -9,72 +9,126 @@ namespace OF_DL.Services;
public interface IDownloadService
{
/// <summary>
/// Calculates the total size of a set of URLs by fetching their metadata.
/// </summary>
Task<long> CalculateTotalFileSize(List<string> urls);
/// <summary>
/// Downloads media and updates metadata storage.
/// </summary>
Task<bool> ProcessMediaDownload(string folder, long mediaId, string apiType, string url, string path,
string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter);
/// <summary>
/// Downloads a single media item.
/// </summary>
Task<bool> DownloadMedia(string url, string folder, long mediaId, string apiType,
IProgressReporter progressReporter, string path,
string? filenameFormat, object? postInfo, object? postMedia,
object? author, Dictionary<string, long> users);
/// <summary>
/// Downloads a DRM-protected video.
/// </summary>
Task<bool> 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<string, long> users);
/// <summary>
/// Retrieves decryption information for a DRM media item.
/// </summary>
Task<(string decryptionKey, DateTime lastModified)?> GetDecryptionInfo(
string mpdUrl, string policy, string signature, string kvp,
string mediaId, string contentId, string drmType,
bool clientIdBlobMissing, bool devicePrivateKeyMissing);
/// <summary>
/// Downloads profile avatar and header images for a creator.
/// </summary>
Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username);
/// <summary>
/// Downloads highlight media for a creator.
/// </summary>
Task<DownloadResult> DownloadHighlights(string username, long userId, string path, HashSet<long> paidPostIds,
IProgressReporter progressReporter);
/// <summary>
/// Downloads story media for a creator.
/// </summary>
Task<DownloadResult> DownloadStories(string username, long userId, string path, HashSet<long> paidPostIds,
IProgressReporter progressReporter);
/// <summary>
/// Downloads archived posts for a creator.
/// </summary>
Task<DownloadResult> DownloadArchived(string username, long userId, string path, Dictionary<string, long> users,
bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedEntities.ArchivedCollection archived,
IProgressReporter progressReporter);
/// <summary>
/// Downloads free messages for a creator.
/// </summary>
Task<DownloadResult> DownloadMessages(string username, long userId, string path, Dictionary<string, long> users,
bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageEntities.MessageCollection messages,
IProgressReporter progressReporter);
/// <summary>
/// Downloads paid messages for a creator.
/// </summary>
Task<DownloadResult> DownloadPaidMessages(string username, string path, Dictionary<string, long> users,
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PurchasedEntities.PaidMessageCollection paidMessageCollection,
IProgressReporter progressReporter);
/// <summary>
/// Downloads stream posts for a creator.
/// </summary>
Task<DownloadResult> DownloadStreams(string username, long userId, string path, Dictionary<string, long> users,
bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamEntities.StreamsCollection streams,
IProgressReporter progressReporter);
/// <summary>
/// Downloads free posts for a creator.
/// </summary>
Task<DownloadResult> DownloadFreePosts(string username, long userId, string path, Dictionary<string, long> users,
bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostEntities.PostCollection posts,
IProgressReporter progressReporter);
/// <summary>
/// Downloads paid posts for a creator.
/// </summary>
Task<DownloadResult> DownloadPaidPosts(string username, long userId, string path, Dictionary<string, long> users,
bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidPostCollection purchasedPosts,
IProgressReporter progressReporter);
/// <summary>
/// Downloads paid posts sourced from the Purchased tab.
/// </summary>
Task<DownloadResult> DownloadPaidPostsPurchasedTab(string username, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter);
/// <summary>
/// Downloads paid messages sourced from the Purchased tab.
/// </summary>
Task<DownloadResult> DownloadPaidMessagesPurchasedTab(string username, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter);
/// <summary>
/// Downloads a single post collection.
/// </summary>
Task<DownloadResult> DownloadSinglePost(string username, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PostEntities.SinglePostCollection post, IProgressReporter progressReporter);
/// <summary>
/// Downloads a single paid message collection.
/// </summary>
Task<DownloadResult> DownloadSinglePaidMessage(string username, string path,
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection,

View File

@ -2,8 +2,14 @@ namespace OF_DL.Services;
public interface IFileNameService
{
/// <summary>
/// Applies token values to a filename format and removes invalid characters.
/// </summary>
Task<string> BuildFilename(string fileFormat, Dictionary<string, string> values);
/// <summary>
/// Builds a map of filename token values from post, media, and author data.
/// </summary>
Task<Dictionary<string, string>> GetFilename(object info, object media, object author,
List<string> selectedProperties,
string username, Dictionary<string, long>? users = null);

View File

@ -5,9 +5,18 @@ namespace OF_DL.Services;
public interface ILoggingService
{
/// <summary>
/// Gets the level switch that controls runtime logging verbosity.
/// </summary>
LoggingLevelSwitch LevelSwitch { get; }
/// <summary>
/// Updates the minimum logging level at runtime.
/// </summary>
void UpdateLoggingLevel(LoggingLevel newLevel);
/// <summary>
/// Returns the current minimum logging level.
/// </summary>
LoggingLevel GetCurrentLoggingLevel();
}

View File

@ -4,7 +4,13 @@ namespace OF_DL.Services;
public interface IStartupService
{
/// <summary>
/// Validates the runtime environment and returns a structured result.
/// </summary>
Task<StartupResult> ValidateEnvironmentAsync();
/// <summary>
/// Checks the current application version against the latest release tag.
/// </summary>
Task<VersionCheckResult> CheckVersionAsync();
}

View File

@ -13,14 +13,24 @@ public class LoggingService : ILoggingService
InitializeLogger();
}
/// <summary>
/// Gets the level switch that controls runtime logging verbosity.
/// </summary>
public LoggingLevelSwitch LevelSwitch { get; }
/// <summary>
/// Updates the minimum logging level at runtime.
/// </summary>
/// <param name="newLevel">The new minimum log level.</param>
public void UpdateLoggingLevel(LoggingLevel newLevel)
{
LevelSwitch.MinimumLevel = (LogEventLevel)newLevel;
Log.Debug("Logging level updated to: {LoggingLevel}", newLevel);
}
/// <summary>
/// Returns the current minimum logging level.
/// </summary>
public LoggingLevel GetCurrentLoggingLevel() => (LoggingLevel)LevelSwitch.MinimumLevel;
private void InitializeLogger()

View File

@ -13,6 +13,10 @@ namespace OF_DL.Services;
public class StartupService(IConfigService configService, IAuthService authService) : IStartupService
{
/// <summary>
/// Validates the runtime environment and returns a structured result.
/// </summary>
/// <returns>A result describing environment checks and detected tools.</returns>
public async Task<StartupResult> ValidateEnvironmentAsync()
{
StartupResult result = new();
@ -78,6 +82,10 @@ public class StartupService(IConfigService configService, IAuthService authServi
return result;
}
/// <summary>
/// Checks the current application version against the latest release tag.
/// </summary>
/// <returns>A result describing the version check status.</returns>
public async Task<VersionCheckResult> CheckVersionAsync()
{
VersionCheckResult result = new();