Replace helper classes with services

This commit is contained in:
whimsical-c4lic0 2026-02-06 00:59:07 -06:00
parent d7bae3e260
commit 4711c53746
23 changed files with 3657 additions and 3609 deletions

View File

@ -0,0 +1,28 @@
using OF_DL.Services;
using Spectre.Console;
namespace OF_DL.CLI;
/// <summary>
/// Implementation of IProgressReporter that uses Spectre.Console's ProgressTask for CLI output.
/// </summary>
public class SpectreProgressReporter : IProgressReporter
{
private readonly ProgressTask _task;
public SpectreProgressReporter(ProgressTask task)
{
_task = task ?? throw new ArgumentNullException(nameof(task));
}
public void ReportProgress(long increment)
{
_task.Increment(increment);
}
public void ReportStatus(string message)
{
// Optionally update task description or handle status messages
// For now, we'll leave this empty as the task description is set when creating the progress bar
}
}

View File

@ -1,11 +1,12 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using OF_DL.Enumerations; using OF_DL.Enumerations;
using Serilog;
namespace OF_DL.Entities namespace OF_DL.Entities
{ {
public class Config : IDownloadConfig, IFileNameFormatConfig public class Config : IFileNameFormatConfig
{ {
[ToggleableConfig] [ToggleableConfig]
public bool DownloadAvatarHeaderPhoto { get; set; } = true; public bool DownloadAvatarHeaderPhoto { get; set; } = true;
@ -107,6 +108,39 @@ namespace OF_DL.Entities
// When enabled, post/message text is stored as-is without XML stripping. // When enabled, post/message text is stored as-is without XML stripping.
[ToggleableConfig] [ToggleableConfig]
public bool DisableTextSanitization { get; set; } = false; public bool DisableTextSanitization { get; set; } = false;
public IFileNameFormatConfig GetCreatorFileNameFormatConfig(string username)
{
FileNameFormatConfig createFileNameFormatConfig = new FileNameFormatConfig();
Func<string?, string?, string?> func = (val1, val2) =>
{
if (string.IsNullOrEmpty(val1))
return val2;
else
return val1;
};
if(CreatorConfigs.TryGetValue(username, out CreatorConfig? creatorConfig))
{
createFileNameFormatConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat;
createFileNameFormatConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat;
createFileNameFormatConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat;
createFileNameFormatConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat;
}
createFileNameFormatConfig.PaidMessageFileNameFormat = func(createFileNameFormatConfig.PaidMessageFileNameFormat, PaidMessageFileNameFormat);
createFileNameFormatConfig.PostFileNameFormat = func(createFileNameFormatConfig.PostFileNameFormat, PostFileNameFormat);
createFileNameFormatConfig.MessageFileNameFormat = func(createFileNameFormatConfig.MessageFileNameFormat, MessageFileNameFormat);
createFileNameFormatConfig.PaidPostFileNameFormat = func(createFileNameFormatConfig.PaidPostFileNameFormat, PaidPostFileNameFormat);
Log.Debug("PaidMessageFilenameFormat: {CombinedConfigPaidMessageFileNameFormat}", createFileNameFormatConfig.PaidMessageFileNameFormat);
Log.Debug("PostFileNameFormat: {CombinedConfigPostFileNameFormat}", createFileNameFormatConfig.PostFileNameFormat);
Log.Debug("MessageFileNameFormat: {CombinedConfigMessageFileNameFormat}", createFileNameFormatConfig.MessageFileNameFormat);
Log.Debug("PaidPostFileNameFormat: {CombinedConfigPaidPostFileNameFormat}", createFileNameFormatConfig.PaidPostFileNameFormat);
return createFileNameFormatConfig;
}
} }
public class CreatorConfig : IFileNameFormatConfig public class CreatorConfig : IFileNameFormatConfig

View File

@ -0,0 +1,37 @@
namespace OF_DL.Entities;
/// <summary>
/// Represents the result of a download operation.
/// </summary>
public class DownloadResult
{
/// <summary>
/// Total number of media items processed.
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Number of newly downloaded media items.
/// </summary>
public int NewDownloads { get; set; }
/// <summary>
/// Number of media items that were already downloaded.
/// </summary>
public int ExistingDownloads { get; set; }
/// <summary>
/// The type of media downloaded (e.g., "Posts", "Messages", "Stories", etc.).
/// </summary>
public string MediaType { get; set; } = string.Empty;
/// <summary>
/// Indicates whether the download operation was successful.
/// </summary>
public bool Success { get; set; } = true;
/// <summary>
/// Optional error message if the download failed.
/// </summary>
public string? ErrorMessage { get; set; }
}

View File

@ -1,56 +0,0 @@
using OF_DL.Enumerations;
namespace OF_DL.Entities
{
public interface IDownloadConfig
{
bool DownloadAvatarHeaderPhoto { get; set; }
bool DownloadPaidPosts { get; set; }
bool DownloadPosts { get; set; }
bool DownloadArchived { get; set; }
bool DownloadStreams { get; set; }
bool DownloadStories { get; set; }
bool DownloadHighlights { get; set; }
bool DownloadMessages { get; set; }
bool DownloadPaidMessages { get; set; }
bool DownloadImages { get; set; }
bool DownloadVideos { get; set; }
bool DownloadAudios { get; set; }
VideoResolution DownloadVideoResolution { get; set; }
int? Timeout { get; set; }
bool FolderPerPaidPost { get; set; }
bool FolderPerPost { get; set; }
bool FolderPerPaidMessage { get; set; }
bool FolderPerMessage { get; set; }
bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; }
bool ShowScrapeSize { get; set; }
bool LimitDownloadRate { get; set; }
int DownloadLimitInMbPerSec { get; set; }
string? FFmpegPath { get; set; }
bool SkipAds { get; set; }
bool BypassContentForCreatorsWhoNoLongerExist { get; set; }
#region Download Date Configurations
bool DownloadOnlySpecificDates { get; set; }
// This enum will define if we want data from before or after the CustomDate.
DownloadDateSelection DownloadDateSelection { get; set; }
// This is the specific date used in combination with the above enum.
DateTime? CustomDate { get; set; }
#endregion
bool DownloadPostsIncrementally { get; set; }
bool DownloadDuplicatedMedia { get; set; }
public LoggingLevel LoggingLevel { get; set; }
bool IgnoreOwnMessages { get; set; }
}
}

View File

@ -1,167 +0,0 @@
using OF_DL.Entities;
using OF_DL.Helpers.Interfaces;
using PuppeteerSharp;
using PuppeteerSharp.BrowserData;
using Serilog;
namespace OF_DL.Helpers;
public class AuthHelper : IAuthHelper
{
private readonly LaunchOptions _options = new()
{
Headless = false,
Channel = ChromeReleaseChannel.Stable,
DefaultViewport = null,
Args = ["--no-sandbox", "--disable-setuid-sandbox"],
UserDataDir = Path.GetFullPath("chrome-data")
};
private readonly string[] _desiredCookies =
[
"auth_id",
"sess"
];
private const int LoginTimeout = 600000; // 10 minutes
private const int FeedLoadTimeout = 60000; // 1 minute
public async Task SetupBrowser(bool runningInDocker)
{
string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH");
if (executablePath != null)
{
Log.Information("OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}", executablePath);
_options.ExecutablePath = executablePath;
}
else
{
var browserFetcher = new BrowserFetcher();
var installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList();
if (installedBrowsers.Count == 0)
{
Log.Information("Downloading browser.");
var downloadedBrowser = await browserFetcher.DownloadAsync();
Log.Information("Browser downloaded. Path: {executablePath}",
downloadedBrowser.GetExecutablePath());
_options.ExecutablePath = downloadedBrowser.GetExecutablePath();
}
else
{
_options.ExecutablePath = installedBrowsers.First().GetExecutablePath();
}
}
if (runningInDocker)
{
Log.Information("Running in Docker. Disabling sandbox and GPU.");
_options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"];
}
}
private async Task<string> GetBcToken(IPage page)
{
return await page.EvaluateExpressionAsync<string>("window.localStorage.getItem('bcTokenSha') || ''");
}
public async Task<Auth?> GetAuthFromBrowser(bool isDocker = false)
{
try
{
IBrowser? browser;
try
{
browser = await Puppeteer.LaunchAsync(_options);
}
catch (ProcessException e)
{
if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir))
{
Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again.");
Directory.Delete(_options.UserDataDir, true);
browser = await Puppeteer.LaunchAsync(_options);
}
else
{
throw;
}
}
if (browser == null)
{
throw new Exception("Could not get browser");
}
IPage[]? pages = await browser.PagesAsync();
IPage? page = pages.First();
if (page == null)
{
throw new Exception("Could not get page");
}
Log.Debug("Navigating to OnlyFans.");
await page.GoToAsync("https://onlyfans.com");
Log.Debug("Waiting for user to login");
await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout });
Log.Debug("Feed element detected (user logged in)");
await page.ReloadAsync();
await page.WaitForNavigationAsync(new NavigationOptions {
WaitUntil = [WaitUntilNavigation.Networkidle2],
Timeout = FeedLoadTimeout
});
Log.Debug("DOM loaded. Getting BC token and cookies ...");
string xBc;
try
{
xBc = await GetBcToken(page);
}
catch (Exception e)
{
throw new Exception("Error getting bcToken");
}
Dictionary<string, string> mappedCookies = (await page.GetCookiesAsync())
.Where(cookie => cookie.Domain.Contains("onlyfans.com"))
.ToDictionary(cookie => cookie.Name, cookie => cookie.Value);
mappedCookies.TryGetValue("auth_id", out string? userId);
if (userId == null)
{
throw new Exception("Could not find 'auth_id' cookie");
}
mappedCookies.TryGetValue("sess", out string? sess);
if (sess == null)
{
throw new Exception("Could not find 'sess' cookie");
}
string? userAgent = await browser.GetUserAgentAsync();
if (userAgent == null)
{
throw new Exception("Could not get user agent");
}
string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key))
.Select(key => $"${key}={mappedCookies[key]};"));
return new Auth()
{
COOKIE = cookies,
USER_AGENT = userAgent,
USER_ID = userId,
X_BC = xBc
};
}
catch (Exception e)
{
Log.Error(e, "Error getting auth from browser");
return null;
}
}
}

View File

@ -1,28 +1,26 @@
using OF_DL.Entities; using OF_DL.Entities;
using OF_DL.Services;
namespace OF_DL.Helpers namespace OF_DL.Helpers
{ {
internal interface IDownloadContext internal interface IDownloadContext
{ {
public IDownloadConfig DownloadConfig { get; }
public IFileNameFormatConfig FileNameFormatConfig { get; } public IFileNameFormatConfig FileNameFormatConfig { get; }
public IAPIHelper ApiHelper { get; } public IAPIService ApiService { get; }
public IDBHelper DBHelper { get; } public IDBService DBService { get; }
public IDownloadHelper DownloadHelper { get; } public IDownloadService DownloadService { get; }
} }
internal class DownloadContext( internal class DownloadContext(
IDownloadConfig downloadConfig,
IFileNameFormatConfig fileNameFormatConfig, IFileNameFormatConfig fileNameFormatConfig,
IAPIHelper apiHelper, IAPIService apiService,
IDBHelper dBHelper, IDBService dBService,
IDownloadHelper downloadHelper) IDownloadService downloadService)
: IDownloadContext : IDownloadContext
{ {
public IAPIHelper ApiHelper { get; } = apiHelper; public IAPIService ApiService { get; } = apiService;
public IDBHelper DBHelper { get; } = dBHelper; public IDBService DBService { get; } = dBService;
public IDownloadHelper DownloadHelper { get; } = downloadHelper; public IDownloadService DownloadService { get; } = downloadService;
public IDownloadConfig DownloadConfig { get; } = downloadConfig;
public IFileNameFormatConfig FileNameFormatConfig { get; } = fileNameFormatConfig; public IFileNameFormatConfig FileNameFormatConfig { get; } = fileNameFormatConfig;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
using OF_DL.Entities;
namespace OF_DL.Helpers.Interfaces
{
public interface IAuthHelper
{
Task SetupBrowser(bool runningInDocker);
Task<Auth?> GetAuthFromBrowser(bool isDocker = false);
}
}

View File

@ -1,45 +0,0 @@
using OF_DL.Entities;
using OF_DL.Entities.Archived;
using OF_DL.Entities.Messages;
using OF_DL.Entities.Post;
using OF_DL.Entities.Purchased;
using OF_DL.Entities.Streams;
using Spectre.Console;
using static OF_DL.Entities.Messages.Messages;
using FromUser = OF_DL.Entities.Messages.FromUser;
namespace OF_DL.Helpers
{
public interface IDownloadHelper
{
Task<long> CalculateTotalFileSize(List<string> urls);
Task<bool> DownloadArchivedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Archived.List messageInfo, Archived.Medium messageMedia, Archived.Author author, Dictionary<string, long> users);
Task<bool> DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Archived.List postInfo, Archived.Medium postMedia, Archived.Author author, Dictionary<string, long> users);
Task<bool> DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, Dictionary<string, long> users);
Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username);
Task<bool> DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Messages.List messageInfo, Messages.Medium messageMedia, Messages.FromUser fromUser, Dictionary<string, long> users);
Task<bool> DownloadMessageMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Messages.List messageInfo, Messages.Medium messageMedia, Messages.FromUser fromUser, Dictionary<string, long> users);
Task<bool> DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Post.List postInfo, Post.Medium postMedia, Post.Author author, Dictionary<string, long> users);
Task<bool> DownloadPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary<string, long> users);
Task<bool> DownloadPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, SinglePost.Author? author, Dictionary<string, long> users);
Task<bool> DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List messageInfo, Medium messageMedia, Purchased.FromUser fromUser, Dictionary<string, long> users);
Task<bool> DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List messageInfo, Medium messageMedia, Purchased.FromUser fromUser, Dictionary<string, long> users);
Task<bool> DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List postInfo, Medium postMedia, Purchased.FromUser fromUser, Dictionary<string, long> users);
Task<bool> DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List messageInfo, Medium messageMedia, Purchased.FromUser fromUser, Dictionary<string, long> users);
Task<bool> DownloadStoryMedia(string url, string folder, long media_id, string api_type, ProgressTask task);
Task<bool> DownloadStreamMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary<string, long> users);
Task<bool> DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Streams.List streamInfo, Streams.Medium streamMedia, Streams.Author author, Dictionary<string, long> users);
Task<bool> DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url,
string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type,
ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia,
FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type,
ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia,
FromUser? fromUser, Dictionary<string, long> users);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,31 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using OF_DL.Entities; using OF_DL.Entities;
using OF_DL.Helpers.Interfaces; using PuppeteerSharp;
using PuppeteerSharp.BrowserData;
using Serilog; using Serilog;
namespace OF_DL.Services namespace OF_DL.Services
{ {
public class AuthService(IAuthHelper authHelper) : IAuthService public class AuthService : IAuthService
{ {
private readonly LaunchOptions _options = new()
{
Headless = false,
Channel = ChromeReleaseChannel.Stable,
DefaultViewport = null,
Args = ["--no-sandbox", "--disable-setuid-sandbox"],
UserDataDir = Path.GetFullPath("chrome-data")
};
private readonly string[] _desiredCookies =
[
"auth_id",
"sess"
];
private const int LoginTimeout = 600000; // 10 minutes
private const int FeedLoadTimeout = 60000; // 1 minute
public Auth? CurrentAuth { get; set; } public Auth? CurrentAuth { get; set; }
public async Task<bool> LoadFromFileAsync(string filePath = "auth.json") public async Task<bool> LoadFromFileAsync(string filePath = "auth.json")
@ -37,8 +56,8 @@ namespace OF_DL.Services
{ {
bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null;
await authHelper.SetupBrowser(runningInDocker); await SetupBrowser(runningInDocker);
CurrentAuth = await authHelper.GetAuthFromBrowser(); CurrentAuth = await GetAuthFromBrowser();
return CurrentAuth != null; return CurrentAuth != null;
} }
@ -68,5 +87,144 @@ namespace OF_DL.Services
Log.Error(ex, "Failed to save auth to file"); Log.Error(ex, "Failed to save auth to file");
} }
} }
private async Task SetupBrowser(bool runningInDocker)
{
string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH");
if (executablePath != null)
{
Log.Information("OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}", executablePath);
_options.ExecutablePath = executablePath;
}
else
{
var browserFetcher = new BrowserFetcher();
var installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList();
if (installedBrowsers.Count == 0)
{
Log.Information("Downloading browser.");
var downloadedBrowser = await browserFetcher.DownloadAsync();
Log.Information("Browser downloaded. Path: {executablePath}",
downloadedBrowser.GetExecutablePath());
_options.ExecutablePath = downloadedBrowser.GetExecutablePath();
}
else
{
_options.ExecutablePath = installedBrowsers.First().GetExecutablePath();
}
}
if (runningInDocker)
{
Log.Information("Running in Docker. Disabling sandbox and GPU.");
_options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"];
}
}
private async Task<string> GetBcToken(IPage page)
{
return await page.EvaluateExpressionAsync<string>("window.localStorage.getItem('bcTokenSha') || ''");
}
private async Task<Auth?> GetAuthFromBrowser(bool isDocker = false)
{
try
{
IBrowser? browser;
try
{
browser = await Puppeteer.LaunchAsync(_options);
}
catch (ProcessException e)
{
if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir))
{
Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again.");
Directory.Delete(_options.UserDataDir, true);
browser = await Puppeteer.LaunchAsync(_options);
}
else
{
throw;
}
}
if (browser == null)
{
throw new Exception("Could not get browser");
}
IPage[]? pages = await browser.PagesAsync();
IPage? page = pages.First();
if (page == null)
{
throw new Exception("Could not get page");
}
Log.Debug("Navigating to OnlyFans.");
await page.GoToAsync("https://onlyfans.com");
Log.Debug("Waiting for user to login");
await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout });
Log.Debug("Feed element detected (user logged in)");
await page.ReloadAsync();
await page.WaitForNavigationAsync(new NavigationOptions {
WaitUntil = [WaitUntilNavigation.Networkidle2],
Timeout = FeedLoadTimeout
});
Log.Debug("DOM loaded. Getting BC token and cookies ...");
string xBc;
try
{
xBc = await GetBcToken(page);
}
catch (Exception e)
{
throw new Exception("Error getting bcToken");
}
Dictionary<string, string> mappedCookies = (await page.GetCookiesAsync())
.Where(cookie => cookie.Domain.Contains("onlyfans.com"))
.ToDictionary(cookie => cookie.Name, cookie => cookie.Value);
mappedCookies.TryGetValue("auth_id", out string? userId);
if (userId == null)
{
throw new Exception("Could not find 'auth_id' cookie");
}
mappedCookies.TryGetValue("sess", out string? sess);
if (sess == null)
{
throw new Exception("Could not find 'sess' cookie");
}
string? userAgent = await browser.GetUserAgentAsync();
if (userAgent == null)
{
throw new Exception("Could not get user agent");
}
string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key))
.Select(key => $"${key}={mappedCookies[key]};"));
return new Auth()
{
COOKIE = cookies,
USER_AGENT = userAgent,
USER_ID = userId,
X_BC = xBc
};
}
catch (Exception e)
{
Log.Error(e, "Error getting auth from browser");
return null;
}
}
} }
} }

View File

@ -10,7 +10,7 @@ namespace OF_DL.Services
{ {
public class ConfigService(ILoggingService loggingService) : IConfigService public class ConfigService(ILoggingService loggingService) : IConfigService
{ {
public Config? CurrentConfig { get; private set; } public Config CurrentConfig { get; private set; } = new();
public bool IsCliNonInteractive { get; private set; } public bool IsCliNonInteractive { get; private set; }
public async Task<bool> LoadConfigurationAsync(string[] args) public async Task<bool> LoadConfigurationAsync(string[] args)

View File

@ -1,25 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
using OF_DL.Enumurations;
using System.IO;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Serilog; using Serilog;
using OF_DL.Entities;
namespace OF_DL.Helpers namespace OF_DL.Services
{ {
public class DBHelper : IDBHelper public class DBService(IConfigService configService) : IDBService
{ {
private readonly IDownloadConfig downloadConfig;
public DBHelper(IDownloadConfig downloadConfig)
{
this.downloadConfig = downloadConfig;
}
public async Task CreateDB(string folder) public async Task CreateDB(string folder)
{ {
try try
@ -366,7 +352,7 @@ namespace OF_DL.Helpers
connection.Open(); connection.Open();
await EnsureCreatedAtColumnExists(connection, "medias"); await EnsureCreatedAtColumnExists(connection, "medias");
StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM medias WHERE media_id=@media_id"); StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM medias WHERE media_id=@media_id");
if (downloadConfig.DownloadDuplicatedMedia) if (configService.CurrentConfig.DownloadDuplicatedMedia)
{ {
sql.Append(" and api_type=@api_type"); sql.Append(" and api_type=@api_type");
} }
@ -405,7 +391,7 @@ namespace OF_DL.Helpers
using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"))
{ {
StringBuilder sql = new StringBuilder("SELECT downloaded FROM medias WHERE media_id=@media_id"); StringBuilder sql = new StringBuilder("SELECT downloaded FROM medias WHERE media_id=@media_id");
if(downloadConfig.DownloadDuplicatedMedia) if(configService.CurrentConfig.DownloadDuplicatedMedia)
{ {
sql.Append(" and api_type=@api_type"); sql.Append(" and api_type=@api_type");
} }
@ -440,7 +426,7 @@ namespace OF_DL.Helpers
// Construct the update command // Construct the update command
StringBuilder sql = new StringBuilder("UPDATE medias SET directory=@directory, filename=@filename, size=@size, downloaded=@downloaded, created_at=@created_at WHERE media_id=@media_id"); StringBuilder sql = new StringBuilder("UPDATE medias SET directory=@directory, filename=@filename, size=@size, downloaded=@downloaded, created_at=@created_at WHERE media_id=@media_id");
if (downloadConfig.DownloadDuplicatedMedia) if (configService.CurrentConfig.DownloadDuplicatedMedia)
{ {
sql.Append(" and api_type=@api_type"); sql.Append(" and api_type=@api_type");
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,10 @@
using HtmlAgilityPack; using HtmlAgilityPack;
using OF_DL.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace OF_DL.Helpers namespace OF_DL.Services
{ {
public class FileNameHelper : IFileNameHelper public class FileNameService(IAuthService authService) : IFileNameService
{ {
private readonly Auth auth;
public FileNameHelper(Auth auth)
{
this.auth = auth;
}
public async Task<Dictionary<string, string>> GetFilename(object obj1, object obj2, object obj3, List<string> selectedProperties, string username, Dictionary<string, long> users = null) public async Task<Dictionary<string, string>> GetFilename(object obj1, object obj2, object obj3, List<string> selectedProperties, string username, Dictionary<string, long> users = null)
{ {
Dictionary<string, string> values = new(); Dictionary<string, string> values = new();
@ -43,7 +30,7 @@ namespace OF_DL.Helpers
object policy = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontPolicy"); object policy = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontPolicy");
object signature = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontSignature"); object signature = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontSignature");
object kvp = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontKeyPairId"); object kvp = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontKeyPairId");
DateTime lastModified = await DownloadHelper.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), auth); DateTime lastModified = await DownloadService.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), authService.CurrentAuth);
values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd"));
continue; continue;
} }
@ -52,7 +39,7 @@ namespace OF_DL.Helpers
object source = GetNestedPropertyValue(obj2, "files.full.url"); object source = GetNestedPropertyValue(obj2, "files.full.url");
if(source != null) if(source != null)
{ {
DateTime lastModified = await DownloadHelper.GetMediaLastModified(source.ToString()); DateTime lastModified = await DownloadService.GetMediaLastModified(source.ToString());
values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd"));
continue; continue;
} }
@ -61,7 +48,7 @@ namespace OF_DL.Helpers
object preview = GetNestedPropertyValue(obj2, "preview"); object preview = GetNestedPropertyValue(obj2, "preview");
if(preview != null) if(preview != null)
{ {
DateTime lastModified = await DownloadHelper.GetMediaLastModified(preview.ToString()); DateTime lastModified = await DownloadService.GetMediaLastModified(preview.ToString());
values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd"));
continue; continue;
} }

View File

@ -8,32 +8,32 @@ using OF_DL.Entities.Streams;
using OF_DL.Enumurations; using OF_DL.Enumurations;
using Spectre.Console; using Spectre.Console;
namespace OF_DL.Helpers namespace OF_DL.Services
{ {
public interface IAPIHelper public interface IAPIService
{ {
Task<string> GetDecryptionKeyCDRMProject(Dictionary<string, string> drmHeaders, string licenceURL, string pssh); Task<string> GetDecryptionKeyCDRMProject(Dictionary<string, string> drmHeaders, string licenceURL, string pssh);
Task<string> GetDecryptionKeyCDM(Dictionary<string, string> drmHeaders, string licenceURL, string pssh); Task<string> GetDecryptionKeyCDM(Dictionary<string, string> drmHeaders, string licenceURL, string pssh);
Task<DateTime> GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp); Task<DateTime> GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp);
Task<string> GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp); Task<string> GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp);
Task<Dictionary<string, long>> GetLists(string endpoint, IDownloadConfig config); Task<Dictionary<string, long>> GetLists(string endpoint);
Task<List<string>> GetListUsers(string endpoint, IDownloadConfig config); Task<List<string>> GetListUsers(string endpoint);
Task<Dictionary<long, string>> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, IDownloadConfig config, List<long> paid_post_ids); Task<Dictionary<long, string>> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, List<long> paid_post_ids);
Task<PaidPostCollection> GetPaidPosts(string endpoint, string folder, string username, IDownloadConfig config, List<long> paid_post_ids, StatusContext ctx); Task<PaidPostCollection> GetPaidPosts(string endpoint, string folder, string username, List<long> paid_post_ids, StatusContext ctx);
Task<PostCollection> GetPosts(string endpoint, string folder, IDownloadConfig config, List<long> paid_post_ids, StatusContext ctx); Task<PostCollection> GetPosts(string endpoint, string folder, List<long> paid_post_ids, StatusContext ctx);
Task<SinglePostCollection> GetPost(string endpoint, string folder, IDownloadConfig config); Task<SinglePostCollection> GetPost(string endpoint, string folder);
Task<StreamsCollection> GetStreams(string endpoint, string folder, IDownloadConfig config, List<long> paid_post_ids, StatusContext ctx); Task<StreamsCollection> GetStreams(string endpoint, string folder, List<long> paid_post_ids, StatusContext ctx);
Task<ArchivedCollection> GetArchived(string endpoint, string folder, IDownloadConfig config, StatusContext ctx); Task<ArchivedCollection> GetArchived(string endpoint, string folder, StatusContext ctx);
Task<MessageCollection> GetMessages(string endpoint, string folder, IDownloadConfig config, StatusContext ctx); Task<MessageCollection> GetMessages(string endpoint, string folder, StatusContext ctx);
Task<PaidMessageCollection> GetPaidMessages(string endpoint, string folder, string username, IDownloadConfig config, StatusContext ctx); Task<PaidMessageCollection> GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx);
Task<SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder, IDownloadConfig config); Task<SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder);
Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, IDownloadConfig config, Dictionary<string, long> users); Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users);
Task<List<PurchasedTabCollection>> GetPurchasedTab(string endpoint, string folder, IDownloadConfig config, Dictionary<string, long> users); Task<List<PurchasedTabCollection>> GetPurchasedTab(string endpoint, string folder, Dictionary<string, long> users);
Task<User> GetUserInfo(string endpoint); Task<User> GetUserInfo(string endpoint);
Task<JObject> GetUserInfoById(string endpoint); Task<JObject> GetUserInfoById(string endpoint);
Dictionary<string, string> GetDynamicHeaders(string path, string queryParam); Dictionary<string, string> GetDynamicHeaders(string path, string queryParam);
Task<Dictionary<string, long>> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions, IDownloadConfig config); Task<Dictionary<string, long>> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions);
Task<Dictionary<string, long>> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions, IDownloadConfig config); Task<Dictionary<string, long>> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions);
Task<string> GetDecryptionKeyOFDL(Dictionary<string, string> drmHeaders, string licenceURL, string pssh); Task<string> GetDecryptionKeyOFDL(Dictionary<string, string> drmHeaders, string licenceURL, string pssh);
} }
} }

View File

@ -4,7 +4,7 @@ namespace OF_DL.Services
{ {
public interface IConfigService public interface IConfigService
{ {
Config? CurrentConfig { get; } Config CurrentConfig { get; }
bool IsCliNonInteractive { get; } bool IsCliNonInteractive { get; }
Task<bool> LoadConfigurationAsync(string[] args); Task<bool> LoadConfigurationAsync(string[] args);
Task SaveConfigurationAsync(string filePath = "config.conf"); Task SaveConfigurationAsync(string filePath = "config.conf");

View File

@ -1,6 +1,6 @@
namespace OF_DL.Helpers namespace OF_DL.Services
{ {
public interface IDBHelper public interface IDBService
{ {
Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at, long user_id); Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at, long user_id);
Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at); Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at);

View File

@ -0,0 +1,50 @@
using OF_DL.Entities;
using OF_DL.Entities.Archived;
using OF_DL.Entities.Messages;
using OF_DL.Entities.Post;
using OF_DL.Entities.Purchased;
using OF_DL.Entities.Streams;
using static OF_DL.Entities.Messages.Messages;
using FromUser = OF_DL.Entities.Messages.FromUser;
namespace OF_DL.Services
{
public interface IDownloadService
{
Task<long> CalculateTotalFileSize(List<string> urls);
Task<bool> ProcessMediaDownload(string folder, long media_id, string api_type, string url, string path, string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter);
Task<bool> DownloadArchivedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Archived.List? messageInfo, Archived.Medium? messageMedia, Archived.Author? author, Dictionary<string, long> users);
Task<bool> DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Archived.List? postInfo, Archived.Medium? postMedia, Archived.Author? author, Dictionary<string, long> users);
Task<bool> DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, Dictionary<string, long> users);
Task<bool> DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary<string, long> users);
Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username);
Task<bool> DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Messages.List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadMessageMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Messages.List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary<string, long> users);
Task<bool> DownloadPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, SinglePost.Author? author, Dictionary<string, long> users);
Task<bool> DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, Medium? postMedia, Purchased.FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadStoryMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter);
Task<bool> DownloadStreamMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary<string, long> users);
Task<bool> DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary<string, long> users);
Task<bool> DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url,
string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type,
IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia,
FromUser? fromUser, Dictionary<string, long> users);
Task<bool> DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type,
IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia,
FromUser? fromUser, Dictionary<string, long> users);
Task<DownloadResult> DownloadHighlights(string username, long userId, string path, HashSet<long> paidPostIds, IProgressReporter progressReporter);
Task<DownloadResult> DownloadStories(string username, long userId, string path, HashSet<long> paidPostIds, IProgressReporter progressReporter);
Task<DownloadResult> DownloadArchived(string username, long userId, string path, Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedCollection archived, IProgressReporter progressReporter);
Task<DownloadResult> DownloadMessages(string username, long userId, string path, Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageCollection messages, IProgressReporter progressReporter);
Task<DownloadResult> DownloadPaidMessages(string username, string path, Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter);
Task<DownloadResult> DownloadStreams(string username, long userId, string path, Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamsCollection streams, IProgressReporter progressReporter);
Task<DownloadResult> DownloadFreePosts(string username, long userId, string path, Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostCollection posts, IProgressReporter progressReporter);
Task<DownloadResult> DownloadPaidPosts(string username, long userId, string path, Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidPostCollection purchasedPosts, IProgressReporter progressReporter);
}
}

View File

@ -0,0 +1,8 @@
namespace OF_DL.Services
{
public interface IFileNameService
{
Task<string> BuildFilename(string fileFormat, Dictionary<string, string> values);
Task<Dictionary<string, string>> GetFilename(object obj1, object obj2, object obj3, List<string> selectedProperties, string username, Dictionary<string, long> users = null);
}
}

View File

@ -0,0 +1,20 @@
namespace OF_DL.Services;
/// <summary>
/// Interface for reporting download progress in a UI-agnostic way.
/// This allows the download service to report progress without being coupled to any specific UI framework.
/// </summary>
public interface IProgressReporter
{
/// <summary>
/// Reports progress increment. The value represents either bytes downloaded or file count depending on configuration.
/// </summary>
/// <param name="increment">The amount to increment progress by</param>
void ReportProgress(long increment);
/// <summary>
/// Reports a status message (optional for implementations).
/// </summary>
/// <param name="message">The status message to report</param>
void ReportStatus(string message);
}

View File

@ -1,7 +1,6 @@
using System; using WidevineClient.Widevine;
using System.Collections.Generic;
namespace WidevineClient.Widevine namespace OF_DL.Widevine
{ {
public class CDMApi public class CDMApi
{ {