OF-DL/OF DL.Core/Services/DownloadService.cs

2096 lines
87 KiB
C#

using System.Security.Cryptography;
using System.Text.RegularExpressions;
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;
using PostEntities = OF_DL.Models.Entities.Posts;
using PurchasedEntities = OF_DL.Models.Entities.Purchased;
using StreamEntities = OF_DL.Models.Entities.Streams;
using OF_DL.Utils;
using Serilog;
using Serilog.Events;
namespace OF_DL.Services;
public class DownloadService(
IAuthService authService,
IConfigService configService,
IDbService dbService,
IFileNameService fileNameService,
IApiService apiService)
: IDownloadService
{
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
{
const string path = "/Profile";
if (!Directory.Exists(folder + path))
{
Directory.CreateDirectory(folder + path);
}
if (!string.IsNullOrEmpty(avatarUrl))
{
await DownloadProfileImage(avatarUrl, folder, $"{path}/Avatars", username);
}
if (!string.IsNullOrEmpty(headerUrl))
{
await DownloadProfileImage(headerUrl, folder, $"{path}/Headers", username);
}
}
catch (Exception ex)
{
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
if (ex.InnerException != null)
{
Console.WriteLine("\nInner Exception:");
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message,
ex.InnerException.StackTrace);
}
}
}
private static async Task DownloadProfileImage(string url, string folder, string subFolder, string username)
{
if (!Directory.Exists(folder + subFolder))
{
Directory.CreateDirectory(folder + subFolder);
}
List<string> md5Hashes = CalculateFolderMd5(folder + subFolder);
Uri uri = new(url);
string destinationPath = $"{folder}{subFolder}/";
HttpClient client = new();
HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = uri };
using HttpResponseMessage response =
await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
using MemoryStream memoryStream = new();
await response.Content.CopyToAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
MD5 md5 = MD5.Create();
byte[] hash = await md5.ComputeHashAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
if (!md5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant()))
{
destinationPath = destinationPath + string.Format("{0} {1}.jpg", username,
response.Content.Headers.LastModified.HasValue
? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy")
: DateTime.Now.ToString("dd-MM-yyyy"));
await using (FileStream fileStream = File.Create(destinationPath))
{
await memoryStream.CopyToAsync(fileStream);
}
File.SetLastWriteTime(destinationPath,
response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now);
}
}
private async Task<bool> DownloadDrmMedia(string userAgent, string policy, string signature, string kvp,
string sess, string url, string decryptionKey, string folder, DateTime lastModified, long mediaId,
string apiType, IProgressReporter progressReporter, string customFileName, string filename, string path)
{
try
{
_completionSource = new TaskCompletionSource<bool>();
int pos1 = decryptionKey.IndexOf(':');
string decKey = "";
if (pos1 >= 0)
{
decKey = decryptionKey[(pos1 + 1)..];
}
int streamIndex = 0;
string tempFilename = $"{folder}{path}/{filename}_source.mp4";
// Configure ffmpeg log level and optional report file location
bool ffmpegDebugLogging = Log.IsEnabled(LogEventLevel.Debug);
string logLevelArgs = ffmpegDebugLogging ||
configService.CurrentConfig.LoggingLevel is LoggingLevel.Verbose or LoggingLevel.Debug
? "-loglevel debug -report"
: configService.CurrentConfig.LoggingLevel switch
{
LoggingLevel.Information => "-loglevel info",
LoggingLevel.Warning => "-loglevel warning",
LoggingLevel.Error => "-loglevel error",
LoggingLevel.Fatal => "-loglevel fatal",
_ => ""
};
if (logLevelArgs.Contains("-report", StringComparison.OrdinalIgnoreCase))
{
// Direct ffmpeg report files into the same logs directory Serilog uses (relative to current working directory)
string logDir = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "logs"));
Directory.CreateDirectory(logDir);
string ffReportPath = Path.Combine(logDir, "ffmpeg-%p-%t.log"); // ffmpeg will replace %p/%t
Environment.SetEnvironmentVariable("FFREPORT", $"file={ffReportPath}:level=32");
Log.Debug("FFREPORT enabled at: {FFREPORT} (cwd: {Cwd})",
Environment.GetEnvironmentVariable("FFREPORT"), Environment.CurrentDirectory);
}
else
{
Environment.SetEnvironmentVariable("FFREPORT", null);
Log.Debug("FFREPORT disabled (cwd: {Cwd})", Environment.CurrentDirectory);
}
string cookieHeader =
"Cookie: " +
$"CloudFront-Policy={policy}; " +
$"CloudFront-Signature={signature}; " +
$"CloudFront-Key-Pair-Id={kvp}; " +
$"{sess}";
string parameters =
$"{logLevelArgs} " +
$"-cenc_decryption_key {decKey} " +
$"-headers \"{cookieHeader}\" " +
$"-user_agent \"{userAgent}\" " +
"-referer \"https://onlyfans.com\" " +
"-rw_timeout 20000000 " +
"-reconnect 1 -reconnect_streamed 1 -reconnect_on_network_error 1 -reconnect_delay_max 10 " +
"-y " +
$"-i \"{url}\" " +
$"-map 0:v:{streamIndex} -map 0:a? " +
"-c copy " +
$"\"{tempFilename}\"";
Log.Debug($"Calling FFMPEG with Parameters: {parameters}");
Engine ffmpeg = new(configService.CurrentConfig.FFmpegPath);
ffmpeg.Error += OnError;
ffmpeg.Complete += async (_, _) =>
{
_completionSource.TrySetResult(true);
await OnFFMPEGDownloadComplete(tempFilename, lastModified, folder, path, customFileName, filename,
mediaId, apiType, progressReporter);
};
await ffmpeg.ExecuteAsync(parameters, CancellationToken.None);
return await _completionSource.Task;
}
catch (Exception ex)
{
ExceptionLoggerHelper.LogException(ex);
}
return false;
}
private async Task OnFFMPEGDownloadComplete(string tempFilename, DateTime lastModified, string folder, string path,
string customFileName, string filename, long mediaId, string apiType, IProgressReporter progressReporter)
{
try
{
if (File.Exists(tempFilename))
{
File.SetLastWriteTime(tempFilename, lastModified);
}
if (!string.IsNullOrEmpty(customFileName))
{
File.Move(tempFilename, $"{folder + path + "/" + customFileName + ".mp4"}");
}
// Cleanup Files
long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName)
? folder + path + "/" + customFileName + ".mp4"
: tempFilename).Length;
ReportProgress(progressReporter, fileSizeInBytes);
await dbService.UpdateMedia(folder, mediaId, apiType, folder + path,
!string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4",
fileSizeInBytes, true, lastModified);
}
catch (Exception ex)
{
ExceptionLoggerHelper.LogException(ex);
}
}
private void OnError(object? sender, ConversionErrorEventArgs e)
{
// Guard all fields to avoid NullReference exceptions from FFmpeg.NET
string input = e.Input?.Name ?? "<none>";
string output = e.Output?.Name ?? "<none>";
string exitCode = e.Exception?.ExitCode.ToString() ?? "<unknown>";
string message = e.Exception?.Message ?? "<no message>";
string inner = e.Exception?.InnerException?.Message ?? "<no inner>";
Log.Error("FFmpeg failed. Input={Input} Output={Output} ExitCode={ExitCode} Message={Message} Inner={Inner}",
input, output, exitCode, message, inner);
_completionSource.TrySetResult(false);
}
private static List<string> CalculateFolderMd5(string folder)
{
List<string> md5Hashes = [];
if (!Directory.Exists(folder))
{
return md5Hashes;
}
string[] files = Directory.GetFiles(folder);
md5Hashes.AddRange(files.Select(CalculateMd5));
return md5Hashes;
}
private static string CalculateMd5(string filePath)
{
using MD5 md5 = MD5.Create();
using FileStream stream = File.OpenRead(filePath);
byte[] hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
/// <summary>
/// </summary>
/// <param name="path"></param>
/// <param name="url"></param>
/// <param name="folder"></param>
/// <param name="mediaId"></param>
/// <param name="apiType"></param>
/// <param name="progressReporter"></param>
/// <param name="serverFileName"></param>
/// <param name="resolvedFileName"></param>
/// <returns></returns>
private async Task<bool> CreateDirectoriesAndDownloadMedia(string path,
string url,
string folder,
long mediaId,
string apiType,
IProgressReporter progressReporter,
string serverFileName,
string resolvedFileName)
{
try
{
if (!Directory.Exists(folder + path))
{
Directory.CreateDirectory(folder + path);
}
string extension = Path.GetExtension(url.Split("?")[0]);
path = UpdatePathBasedOnExtension(folder, path, extension);
return await ProcessMediaDownload(folder, mediaId, apiType, url, path, serverFileName, resolvedFileName,
extension, progressReporter);
}
catch (Exception ex)
{
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
if (ex.InnerException != null)
{
Console.WriteLine("\nInner Exception:");
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message,
ex.InnerException.StackTrace);
}
}
return false;
}
/// <summary>
/// Updates the given path based on the file extension.
/// </summary>
/// <param name="folder">The parent folder.</param>
/// <param name="path">The initial relative path.</param>
/// <param name="extension">The file extension.</param>
/// <returns>A string that represents the updated path based on the file extension.</returns>
private static string UpdatePathBasedOnExtension(string folder, string path, string extension)
{
string subdirectory = "";
switch (extension.ToLower())
{
case ".jpg":
case ".jpeg":
case ".png":
subdirectory = "/Images";
break;
case ".mp4":
case ".avi":
case ".wmv":
case ".gif":
case ".mov":
subdirectory = "/Videos";
break;
case ".mp3":
case ".wav":
case ".ogg":
subdirectory = "/Audios";
break;
}
if (!string.IsNullOrEmpty(subdirectory))
{
path += subdirectory;
string fullPath = folder + path;
if (!Directory.Exists(fullPath))
{
Directory.CreateDirectory(fullPath);
}
}
return path;
}
/// <summary>
/// Generates a custom filename based on the given format and properties.
/// </summary>
/// <param name="filename"></param>
/// <param name="filenameFormat">The format string for the filename.</param>
/// <param name="postInfo">General information about the post.</param>
/// <param name="postMedia">Media associated with the post.</param>
/// <param name="author">Author of the post.</param>
/// <param name="username"></param>
/// <param name="users">Dictionary containing user-related data.</param>
/// <param name="fileNameService"></param>
/// <param name="option"></param>
/// <returns>A Task resulting in a string that represents the custom filename.</returns>
private static async Task<string> GenerateCustomFileName(string filename,
string? filenameFormat,
object? postInfo,
object? postMedia,
object? author,
string username,
Dictionary<string, long> users,
IFileNameService fileNameService,
CustomFileNameOption option)
{
if (string.IsNullOrEmpty(filenameFormat) || postInfo == null || postMedia == null || author == null)
{
return option switch
{
CustomFileNameOption.ReturnOriginal => filename,
CustomFileNameOption.ReturnEmpty => "",
_ => filename
};
}
List<string> properties = new();
string pattern = @"\{(.*?)\}";
MatchCollection matches = Regex.Matches(filenameFormat, pattern);
properties.AddRange(matches.Select(match => match.Groups[1].Value));
Dictionary<string, string> values =
await fileNameService.GetFilename(postInfo, postMedia, author, properties, username, users);
return await fileNameService.BuildFilename(filenameFormat, values);
}
private async Task<long> GetFileSizeAsync(string url)
{
long fileSize = 0;
try
{
if (authService.CurrentAuth == null)
{
throw new Exception("No authentication information available.");
}
if (authService.CurrentAuth.Cookie == null)
{
throw new Exception("No authentication cookie available.");
}
if (authService.CurrentAuth.UserAgent == null)
{
throw new Exception("No user agent available.");
}
Uri uri = new(url);
if (uri.Host == "cdn3.onlyfans.com" && uri.LocalPath.Contains("/dash/files"))
{
string[] messageUrlParsed = url.Split(',');
string mpdUrl = messageUrlParsed[0];
string policy = messageUrlParsed[1];
string signature = messageUrlParsed[2];
string kvp = messageUrlParsed[3];
mpdUrl = mpdUrl.Replace(".mpd", "_source.mp4");
using HttpClient client = new();
client.DefaultRequestHeaders.Add("Cookie",
$"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.Cookie}");
client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.UserAgent);
using HttpResponseMessage response =
await client.GetAsync(mpdUrl, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
fileSize = response.Content.Headers.ContentLength ?? 0;
}
}
else
{
using HttpClient client = new();
client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.UserAgent);
using HttpResponseMessage response =
await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
fileSize = response.Content.Headers.ContentLength ?? 0;
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error getting file size for URL '{url}': {ex.Message}");
}
return fileSize;
}
/// <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(',');
string mpdUrl = messageUrlParsed[0];
string policy = messageUrlParsed[1];
string signature = messageUrlParsed[2];
string kvp = messageUrlParsed[3];
mpdUrl = mpdUrl.Replace(".mpd", "_source.mp4");
using HttpClient client = new();
client.DefaultRequestHeaders.Add("Cookie",
$"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {auth.Cookie}");
client.DefaultRequestHeaders.Add("User-Agent", auth.UserAgent);
using HttpResponseMessage response = await client.GetAsync(mpdUrl, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
if (response.Content.Headers.LastModified != null)
{
return response.Content.Headers.LastModified.Value.DateTime;
}
}
return DateTime.Now;
}
/// <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();
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
{
return DateTime.Now;
}
return response.Content.Headers.LastModified?.DateTime ?? DateTime.Now;
}
/// <summary>
/// Processes the download and database update of media.
/// </summary>
/// <param name="folder">The folder where the media is stored.</param>
/// <param name="mediaId">The ID of the media.</param>
/// <param name="apiType"></param>
/// <param name="url">The URL from where to download the media.</param>
/// <param name="path">The relative path to the media.</param>
/// <param name="serverFilename"></param>
/// <param name="resolvedFilename">The filename after any required manipulations.</param>
/// <param name="extension">The file extension.</param>
/// <param name="progressReporter"></param>
/// <returns>A Task resulting in a boolean indicating whether the media is newly downloaded or not.</returns>
public async Task<bool> ProcessMediaDownload(string folder,
long mediaId,
string apiType,
string url,
string path,
string serverFilename,
string resolvedFilename,
string extension,
IProgressReporter progressReporter)
{
try
{
if (!await dbService.CheckDownloaded(folder, mediaId, apiType))
{
return await HandleNewMedia(folder,
mediaId,
apiType,
url,
path,
serverFilename,
resolvedFilename,
extension,
progressReporter);
}
bool status = await HandlePreviouslyDownloadedMediaAsync(folder, mediaId, apiType, progressReporter);
if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected &&
serverFilename != resolvedFilename)
{
await HandleRenamingOfExistingFilesAsync(folder, mediaId, apiType, path, serverFilename,
resolvedFilename, extension);
}
return status;
}
catch (Exception ex)
{
// Handle exception (e.g., log it)
Console.WriteLine($"An error occurred: {ex.Message}");
return false;
}
}
private async Task HandleRenamingOfExistingFilesAsync(string folder,
long mediaId,
string apiType,
string path,
string serverFilename,
string resolvedFilename,
string extension)
{
string fullPathWithTheServerFileName = $"{folder}{path}/{serverFilename}{extension}";
string fullPathWithTheNewFileName = $"{folder}{path}/{resolvedFilename}{extension}";
if (!File.Exists(fullPathWithTheServerFileName))
{
return;
}
try
{
File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName);
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
return;
}
long size = await dbService.GetStoredFileSize(folder, mediaId, apiType);
DateTime lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName);
await dbService.UpdateMedia(folder, mediaId, apiType, folder + path, resolvedFilename + extension, size, true,
lastModified);
}
/// <summary>
/// Handles new media by downloading and updating the database.
/// </summary>
/// <param name="folder"></param>
/// <param name="mediaId"></param>
/// <param name="apiType"></param>
/// <param name="url"></param>
/// <param name="path"></param>
/// <param name="serverFilename"></param>
/// <param name="resolvedFilename"></param>
/// <param name="extension"></param>
/// <param name="progressReporter"></param>
/// <returns>A Task resulting in a boolean indicating whether the media is newly downloaded or not.</returns>
private async Task<bool> HandleNewMedia(string folder,
long mediaId,
string apiType,
string url,
string path,
string serverFilename,
string resolvedFilename,
string extension,
IProgressReporter progressReporter)
{
long fileSizeInBytes;
DateTime lastModified;
bool status;
string fullPathWithTheServerFileName = $"{folder}{path}/{serverFilename}{extension}";
string fullPathWithTheNewFileName = $"{folder}{path}/{resolvedFilename}{extension}";
//there are a few possibilities here.
//1.file has been downloaded in the past but it has the server filename
// in that case it should be set as existing and it should be renamed
//2.file has been downloaded in the past but it has custom filename.
// it should be set as existing and nothing else.
// of coures 1 and 2 depends in the fact that there may be a difference in the resolved file name
// (ie user has selected a custom format. If he doesn't then the resolved name will be the same as the server filename
//3.file doesn't exist and it should be downloaded.
// Handle the case where the file has been downloaded in the past with the server filename
//but it has downloaded outsite of this application so it doesn't exist in the database
if (File.Exists(fullPathWithTheServerFileName))
{
string finalPath;
if (fullPathWithTheServerFileName != fullPathWithTheNewFileName)
{
finalPath = fullPathWithTheNewFileName;
//rename.
try
{
File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName);
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
else
{
finalPath = fullPathWithTheServerFileName;
}
fileSizeInBytes = GetLocalFileSize(finalPath);
lastModified = File.GetLastWriteTime(finalPath);
ReportProgress(progressReporter, fileSizeInBytes);
status = false;
}
// Handle the case where the file has been downloaded in the past with a custom filename.
// but it has downloaded outside of this application so it doesn't exist in the database
// this is a bit improbable but we should check for that.
else if (File.Exists(fullPathWithTheNewFileName))
{
fileSizeInBytes = GetLocalFileSize(fullPathWithTheNewFileName);
lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName);
ReportProgress(progressReporter, fileSizeInBytes);
status = false;
}
else //file doesn't exist and we should download it.
{
lastModified = await DownloadFile(url, fullPathWithTheNewFileName, progressReporter);
fileSizeInBytes = GetLocalFileSize(fullPathWithTheNewFileName);
status = true;
}
//finaly check which filename we should use. Custom or the server one.
//if a custom is used, then the serverFilename will be different from the resolved filename.
string finalName = serverFilename == resolvedFilename ? serverFilename : resolvedFilename;
await dbService.UpdateMedia(folder, mediaId, apiType, folder + path, finalName + extension, fileSizeInBytes,
true, lastModified);
return status;
}
/// <summary>
/// Handles media that has been previously downloaded and updates the task accordingly.
/// </summary>
/// <param name="folder"></param>
/// <param name="mediaId"></param>
/// <param name="apiType"></param>
/// <param name="progressReporter"></param>
/// <returns>A boolean indicating whether the media is newly downloaded or not.</returns>
private async Task<bool> HandlePreviouslyDownloadedMediaAsync(string folder, long mediaId, string apiType,
IProgressReporter progressReporter)
{
long size = configService.CurrentConfig.ShowScrapeSize
? await dbService.GetStoredFileSize(folder, mediaId, apiType)
: 1;
ReportProgress(progressReporter, size);
return false;
}
/// <summary>
/// Gets the file size of the media.
/// </summary>
/// <param name="filePath">The path to the file.</param>
/// <returns>The file size in bytes.</returns>
private static long GetLocalFileSize(string filePath) => new FileInfo(filePath).Length;
/// <summary>
/// Downloads a file from the given URL and saves it to the specified destination path.
/// </summary>
/// <param name="url">The URL to download the file from.</param>
/// <param name="destinationPath">The path where the downloaded file will be saved.</param>
/// <param name="progressReporter"></param>
/// <returns>A Task resulting in a DateTime indicating the last modified date of the downloaded file.</returns>
private async Task<DateTime> DownloadFile(string url, string destinationPath, IProgressReporter progressReporter)
{
using HttpClient client = new();
HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = new Uri(url) };
using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
Stream body = await response.Content.ReadAsStreamAsync();
// Wrap the body stream with the ThrottledStream to limit read rate.
await using (ThrottledStream throttledStream = new(body,
configService.CurrentConfig.DownloadLimitInMbPerSec * 1_000_000,
configService.CurrentConfig.LimitDownloadRate))
{
await using FileStream fileStream = new(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None,
16384,
true);
byte[] buffer = new byte[16384];
int read;
while ((read = await throttledStream.ReadAsync(buffer, CancellationToken.None)) > 0)
{
if (configService.CurrentConfig.ShowScrapeSize)
{
progressReporter.ReportProgress(read);
}
await fileStream.WriteAsync(buffer.AsMemory(0, read), CancellationToken.None);
}
}
File.SetLastWriteTime(destinationPath, response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now);
if (!configService.CurrentConfig.ShowScrapeSize)
{
progressReporter.ReportProgress(1);
}
return response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now;
}
/// <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;
if (urls.Count > 250)
{
const int batchSize = 250;
List<Task<long>> tasks = [];
for (int i = 0; i < urls.Count; i += batchSize)
{
List<string> batchUrls = urls.Skip(i).Take(batchSize).ToList();
Task<long>[] batchTasks = batchUrls.Select(GetFileSizeAsync).ToArray();
tasks.AddRange(batchTasks);
await Task.WhenAll(batchTasks);
await Task.Delay(5000);
}
long[] fileSizes = await Task.WhenAll(tasks);
totalFileSize += fileSizes.Sum();
}
else
{
List<Task<long>> tasks = [];
tasks.AddRange(urls.Select(GetFileSizeAsync));
long[] fileSizes = await Task.WhenAll(tasks);
totalFileSize += fileSizes.Sum();
}
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,
object? author, Dictionary<string, long> users)
{
Uri uri = new(url);
string filename = Path.GetFileNameWithoutExtension(uri.LocalPath);
string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author,
folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnOriginal);
return await CreateDirectoriesAndDownloadMedia(path, url, folder, mediaId, apiType, progressReporter,
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,
string? filenameFormat, object? postInfo, object? postMedia,
object? author, Dictionary<string, long> users)
{
try
{
if (authService.CurrentAuth == null)
{
throw new Exception("No authentication information available.");
}
if (authService.CurrentAuth.Cookie == null)
{
throw new Exception("No authentication cookie available.");
}
if (authService.CurrentAuth.UserAgent == null)
{
throw new Exception("No user agent available.");
}
Uri uri = new(url);
string filename = Path.GetFileName(uri.LocalPath).Split(".")[0];
if (!Directory.Exists(folder + path))
{
Directory.CreateDirectory(folder + path);
}
string customFileName = await GenerateCustomFileName(filename, filenameFormat, postInfo, postMedia, author,
folder.Split("/")[^1], users, fileNameService, CustomFileNameOption.ReturnEmpty);
if (!await dbService.CheckDownloaded(folder, mediaId, apiType))
{
if (!string.IsNullOrEmpty(customFileName)
? !File.Exists(folder + path + "/" + customFileName + ".mp4")
: !File.Exists(folder + path + "/" + filename + "_source.mp4"))
{
return await DownloadDrmMedia(authService.CurrentAuth.UserAgent, policy, signature, kvp,
authService.CurrentAuth.Cookie, url, decryptionKey, folder, lastModified, mediaId, apiType,
progressReporter, customFileName, filename, path);
}
long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName)
? folder + path + "/" + customFileName + ".mp4"
: folder + path + "/" + filename + "_source.mp4").Length;
ReportProgress(progressReporter, fileSizeInBytes);
await dbService.UpdateMedia(folder, mediaId, apiType, folder + path,
!string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4",
fileSizeInBytes, true, lastModified);
}
else
{
if (!string.IsNullOrEmpty(customFileName))
{
if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected &&
filename + "_source" != customFileName)
{
string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4";
string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4";
if (!File.Exists(fullPathWithTheServerFileName))
{
return false;
}
try
{
File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName);
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
return false;
}
long storedFileSize = await dbService.GetStoredFileSize(folder, mediaId, apiType);
await dbService.UpdateMedia(folder, mediaId, apiType, folder + path, customFileName + ".mp4",
storedFileSize, true, lastModified);
}
}
long progressSize = configService.CurrentConfig.ShowScrapeSize
? await dbService.GetStoredFileSize(folder, mediaId, apiType)
: 1;
ReportProgress(progressReporter, progressSize);
}
return false;
}
catch (Exception ex)
{
ExceptionLoggerHelper.LogException(ex);
}
return false;
}
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,
bool clientIdBlobMissing, bool devicePrivateKeyMissing)
{
string pssh = await apiService.GetDrmMpdPssh(mpdUrl, policy, signature, kvp);
DateTime lastModified = await apiService.GetDrmMpdLastModified(mpdUrl, policy, signature, kvp);
Dictionary<string, string> drmHeaders =
apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/{drmType}/{contentId}",
"?type=widevine");
string licenseUrl =
$"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/{drmType}/{contentId}?type=widevine";
string decryptionKey = clientIdBlobMissing || devicePrivateKeyMissing
? await apiService.GetDecryptionKeyOfdl(drmHeaders, licenseUrl, pssh)
: await apiService.GetDecryptionKeyCdm(drmHeaders, licenseUrl, pssh);
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)
{
Log.Debug($"Calling DownloadHighlights - {username}");
Dictionary<long, string>? highlights = await apiService.GetMedia(MediaType.Highlights,
$"/users/{userId}/stories/highlights", null, path, paidPostIds.ToList());
if (highlights == null || highlights.Count == 0)
{
Log.Debug("Found 0 Highlights");
return new DownloadResult
{
TotalCount = 0,
NewDownloads = 0,
ExistingDownloads = 0,
MediaType = "Highlights",
Success = true
};
}
Log.Debug($"Found {highlights.Count} Highlights");
int oldHighlightsCount = 0;
int newHighlightsCount = 0;
foreach (KeyValuePair<long, string> highlightKvp in highlights)
{
bool isNew =
await DownloadMedia(highlightKvp.Value, path, highlightKvp.Key, "Stories", progressReporter,
"/Stories/Free", null, null, null, null, new Dictionary<string, long>());
if (isNew)
{
newHighlightsCount++;
}
else
{
oldHighlightsCount++;
}
}
Log.Debug(
$"Highlights Already Downloaded: {oldHighlightsCount} New Highlights Downloaded: {newHighlightsCount}");
return new DownloadResult
{
TotalCount = highlights.Count,
NewDownloads = newHighlightsCount,
ExistingDownloads = oldHighlightsCount,
MediaType = "Highlights",
Success = true
};
}
/// <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)
{
Log.Debug($"Calling DownloadStories - {username}");
Dictionary<long, string>? stories = await apiService.GetMedia(MediaType.Stories, $"/users/{userId}/stories",
null, path, paidPostIds.ToList());
if (stories == null || stories.Count == 0)
{
Log.Debug("Found 0 Stories");
return new DownloadResult
{
TotalCount = 0,
NewDownloads = 0,
ExistingDownloads = 0,
MediaType = "Stories",
Success = true
};
}
Log.Debug($"Found {stories.Count} Stories");
int oldStoriesCount = 0;
int newStoriesCount = 0;
foreach (KeyValuePair<long, string> storyKvp in stories)
{
bool isNew = await DownloadMedia(storyKvp.Value, path, storyKvp.Key, "Stories", progressReporter,
"/Stories/Free", null, null, null, null, new Dictionary<string, long>());
if (isNew)
{
newStoriesCount++;
}
else
{
oldStoriesCount++;
}
}
Log.Debug($"Stories Already Downloaded: {oldStoriesCount} New Stories Downloaded: {newStoriesCount}");
return new DownloadResult
{
TotalCount = stories.Count,
NewDownloads = newStoriesCount,
ExistingDownloads = oldStoriesCount,
MediaType = "Stories",
Success = true
};
}
/// <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)
{
Log.Debug($"Calling DownloadArchived - {username}");
if (archived.ArchivedPosts.Count == 0)
{
Log.Debug("Found 0 Archived Posts");
return new DownloadResult
{
TotalCount = 0,
NewDownloads = 0,
ExistingDownloads = 0,
MediaType = "Archived Posts",
Success = true
};
}
Log.Debug(
$"Found {archived.ArchivedPosts.Count} Media from {archived.ArchivedPostObjects.Count} Archived Posts");
int oldArchivedCount = 0;
int newArchivedCount = 0;
foreach (KeyValuePair<long, string> archivedKvp in archived.ArchivedPosts)
{
bool isNew;
ArchivedEntities.Medium? mediaInfo =
archived.ArchivedPostMedia.FirstOrDefault(m => m.Id == archivedKvp.Key);
ArchivedEntities.ListItem? postInfo = mediaInfo == null
? null
: archived.ArchivedPostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true);
string filenameFormat =
configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? "";
if (archivedKvp.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = archivedKvp.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1],
parsed[2], parsed[3],
parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, archivedKvp.Key, "Posts",
progressReporter, "/Archived/Posts/Free/Videos", filenameFormat,
postInfo, mediaInfo, postInfo?.Author, users);
}
else
{
isNew = await DownloadMedia(archivedKvp.Value, path, archivedKvp.Key, "Posts", progressReporter,
"/Archived/Posts/Free", filenameFormat, postInfo, mediaInfo, postInfo?.Author, users);
}
if (isNew)
{
newArchivedCount++;
}
else
{
oldArchivedCount++;
}
}
Log.Debug(
$"Archived Posts Already Downloaded: {oldArchivedCount} New Archived Posts Downloaded: {newArchivedCount}");
return new DownloadResult
{
TotalCount = archived.ArchivedPosts.Count,
NewDownloads = newArchivedCount,
ExistingDownloads = oldArchivedCount,
MediaType = "Archived Posts",
Success = true
};
}
/// <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)
{
Log.Debug($"Calling DownloadMessages - {username}");
if (messages.Messages.Count == 0)
{
Log.Debug("Found 0 Messages");
return new DownloadResult
{
TotalCount = 0,
NewDownloads = 0,
ExistingDownloads = 0,
MediaType = "Messages",
Success = true
};
}
Log.Debug($"Found {messages.Messages.Count} Media from {messages.MessageObjects.Count} Messages");
int oldMessagesCount = 0;
int newMessagesCount = 0;
foreach (KeyValuePair<long, string> messageKvp in messages.Messages)
{
bool isNew;
MessageEntities.Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.Id == messageKvp.Key);
MessageEntities.ListItem? messageInfo = messages.MessageObjects.FirstOrDefault(p =>
p.Media?.Any(m => m.Id == messageKvp.Key) == true);
string filenameFormat =
configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? "";
string messagePath = configService.CurrentConfig.FolderPerMessage && messageInfo != null &&
messageInfo.Id != 0 && messageInfo.CreatedAt is not null
? $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"
: "/Messages/Free";
if (messageKvp.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = messageKvp.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1],
parsed[2], parsed[3],
parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, messageKvp.Key, "Messages",
progressReporter, messagePath + "/Videos", filenameFormat,
messageInfo, mediaInfo, messageInfo?.FromUser, users);
}
else
{
isNew = await DownloadMedia(messageKvp.Value, path, messageKvp.Key, "Messages", progressReporter,
messagePath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users);
}
if (isNew)
{
newMessagesCount++;
}
else
{
oldMessagesCount++;
}
}
Log.Debug($"Messages Already Downloaded: {oldMessagesCount} New Messages Downloaded: {newMessagesCount}");
return new DownloadResult
{
TotalCount = messages.Messages.Count,
NewDownloads = newMessagesCount,
ExistingDownloads = oldMessagesCount,
MediaType = "Messages",
Success = true
};
}
/// <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,
IProgressReporter progressReporter)
{
Log.Debug($"Calling DownloadPaidMessages - {username}");
if (paidMessageCollection.PaidMessages.Count == 0)
{
Log.Debug("Found 0 Paid Messages");
return new DownloadResult
{
TotalCount = 0,
NewDownloads = 0,
ExistingDownloads = 0,
MediaType = "Paid Messages",
Success = true
};
}
Log.Debug(
$"Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages");
int oldCount = 0;
int newCount = 0;
foreach (KeyValuePair<long, string> kvpEntry in paidMessageCollection.PaidMessages)
{
bool isNew;
MessageEntities.Medium? mediaInfo =
paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == kvpEntry.Key);
PurchasedEntities.ListItem? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p =>
p.Media?.Any(m => m.Id == kvpEntry.Key) == true);
string filenameFormat =
configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).MessageFileNameFormat ?? "";
string paidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null &&
messageInfo.Id != 0 && messageInfo.CreatedAt is not null
? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"
: "/Messages/Paid";
if (kvpEntry.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = kvpEntry.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1],
parsed[2], parsed[3],
parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, kvpEntry.Key, "Messages",
progressReporter, paidMsgPath + "/Videos", filenameFormat,
messageInfo, mediaInfo, messageInfo?.FromUser, users);
}
else
{
isNew = await DownloadMedia(kvpEntry.Value, path, kvpEntry.Key, "Messages", progressReporter,
paidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users);
}
if (isNew)
{
newCount++;
}
else
{
oldCount++;
}
}
Log.Debug($"Paid Messages Already Downloaded: {oldCount} New Paid Messages Downloaded: {newCount}");
return new DownloadResult
{
TotalCount = paidMessageCollection.PaidMessages.Count,
NewDownloads = newCount,
ExistingDownloads = oldCount,
MediaType = "Paid Messages",
Success = true
};
}
/// <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)
{
Log.Debug($"Calling DownloadStreams - {username}");
if (streams.Streams.Count == 0)
{
Log.Debug("Found 0 Streams");
return new DownloadResult
{
TotalCount = 0,
NewDownloads = 0,
ExistingDownloads = 0,
MediaType = "Streams",
Success = true
};
}
Log.Debug($"Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams");
int oldCount = 0;
int newCount = 0;
foreach (KeyValuePair<long, string> kvpEntry in streams.Streams)
{
bool isNew;
StreamEntities.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.Id == kvpEntry.Key);
StreamEntities.ListItem? streamInfo = mediaInfo == null
? null
: streams.StreamObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true);
string filenameFormat =
configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? "";
string streamPath = configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo.Id != 0
? $"/Posts/Free/{streamInfo.Id} {streamInfo.PostedAt:yyyy-MM-dd HH-mm-ss}"
: "/Posts/Free";
if (kvpEntry.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = kvpEntry.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1],
parsed[2], parsed[3],
parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, kvpEntry.Key, "Streams",
progressReporter, streamPath + "/Videos", filenameFormat,
streamInfo, mediaInfo, streamInfo?.Author, users);
}
else
{
isNew = await DownloadMedia(kvpEntry.Value, path, kvpEntry.Key, "Streams", progressReporter,
streamPath, filenameFormat, streamInfo, mediaInfo, streamInfo?.Author, users);
}
if (isNew)
{
newCount++;
}
else
{
oldCount++;
}
}
Log.Debug($"Streams Already Downloaded: {oldCount} New Streams Downloaded: {newCount}");
return new DownloadResult
{
TotalCount = streams.Streams.Count,
NewDownloads = newCount,
ExistingDownloads = oldCount,
MediaType = "Streams",
Success = true
};
}
/// <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,
IProgressReporter progressReporter)
{
Log.Debug($"Calling DownloadFreePosts - {username}");
if (posts.Posts.Count == 0)
{
Log.Debug("Found 0 Posts");
return new DownloadResult
{
TotalCount = 0,
NewDownloads = 0,
ExistingDownloads = 0,
MediaType = "Posts",
Success = true
};
}
Log.Debug($"Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts");
int oldCount = 0, newCount = 0;
foreach (KeyValuePair<long, string> postKvp in posts.Posts)
{
bool isNew;
PostEntities.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m.Id == postKvp.Key);
PostEntities.ListItem? postInfo = mediaInfo == null
? null
: posts.PostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true);
string filenameFormat =
configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? "";
string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo.Id != 0
? $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}"
: "/Posts/Free";
if (postKvp.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = postKvp.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1],
parsed[2], parsed[3],
parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKvp.Key, "Posts",
progressReporter, postPath + "/Videos", filenameFormat,
postInfo, mediaInfo, postInfo?.Author, users);
}
else
{
isNew = await DownloadMedia(postKvp.Value, path, postKvp.Key, "Posts", progressReporter,
postPath, filenameFormat, postInfo, mediaInfo, postInfo?.Author, users);
}
if (isNew)
{
newCount++;
}
else
{
oldCount++;
}
}
Log.Debug($"Posts Already Downloaded: {oldCount} New Posts Downloaded: {newCount}");
return new DownloadResult
{
TotalCount = posts.Posts.Count,
NewDownloads = newCount,
ExistingDownloads = oldCount,
MediaType = "Posts",
Success = true
};
}
/// <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)
{
Log.Debug($"Calling DownloadPaidPosts - {username}");
if (purchasedPosts.PaidPosts.Count == 0)
{
Log.Debug("Found 0 Paid Posts");
return new DownloadResult
{
TotalCount = 0,
NewDownloads = 0,
ExistingDownloads = 0,
MediaType = "Paid Posts",
Success = true
};
}
Log.Debug(
$"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts");
int oldCount = 0, newCount = 0;
foreach (KeyValuePair<long, string> postKvp in purchasedPosts.PaidPosts)
{
bool isNew;
MessageEntities.Medium? mediaInfo =
purchasedPosts.PaidPostMedia.FirstOrDefault(m => m.Id == postKvp.Key);
PurchasedEntities.ListItem? postInfo =
purchasedPosts.PaidPostObjects.FirstOrDefault(p => p.Media?.Any(m => m.Id == postKvp.Key) == true);
string filenameFormat =
configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? "";
string paidPostPath = configService.CurrentConfig.FolderPerPaidPost && postInfo != null &&
postInfo.Id != 0 && postInfo.PostedAt is not null
? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}"
: "/Posts/Paid";
if (postKvp.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = postKvp.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo = await GetDecryptionInfo(parsed[0], parsed[1],
parsed[2], parsed[3],
parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKvp.Key, "Posts",
progressReporter, paidPostPath + "/Videos", filenameFormat,
postInfo, mediaInfo, postInfo?.FromUser, users);
}
else
{
isNew = await DownloadMedia(postKvp.Value, path, postKvp.Key, "Posts", progressReporter,
paidPostPath, filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users);
}
if (isNew)
{
newCount++;
}
else
{
oldCount++;
}
}
Log.Debug($"Paid Posts Already Downloaded: {oldCount} New Paid Posts Downloaded: {newCount}");
return new DownloadResult
{
TotalCount = purchasedPosts.PaidPosts.Count,
NewDownloads = newCount,
ExistingDownloads = oldCount,
MediaType = "Paid Posts",
Success = true
};
}
/// <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)
{
Log.Debug($"Calling DownloadPaidPostsPurchasedTab - {username}");
if (purchasedPosts.PaidPosts.Count == 0)
{
Log.Debug("Found 0 Paid Posts");
return new DownloadResult { TotalCount = 0, MediaType = "Paid Posts", Success = true };
}
int oldCount = 0, newCount = 0;
foreach (KeyValuePair<long, string> purchasedPostKvp in purchasedPosts.PaidPosts)
{
bool isNew;
MessageEntities.Medium? mediaInfo =
purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKvp.Key);
PurchasedEntities.ListItem? postInfo = mediaInfo != null
? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p =>
p.Media?.Any(m => m.Id == purchasedPostKvp.Key) == true)
: null;
string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username)
.PaidPostFileNameFormat ?? "";
string paidPostPath = configService.CurrentConfig.FolderPerPaidPost && postInfo != null &&
postInfo.Id != 0 && postInfo.PostedAt is not null
? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}"
: "/Posts/Paid";
if (purchasedPostKvp.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = purchasedPostKvp.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo =
await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3],
parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, purchasedPostKvp.Key,
"Posts", progressReporter, paidPostPath + "/Videos", filenameFormat,
postInfo, mediaInfo, postInfo?.FromUser, users);
}
else
{
isNew = await DownloadMedia(purchasedPostKvp.Value, path,
purchasedPostKvp.Key, "Posts", progressReporter,
paidPostPath, filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users);
}
if (isNew)
{
newCount++;
}
else
{
oldCount++;
}
}
Log.Debug($"Paid Posts Already Downloaded: {oldCount} New Paid Posts Downloaded: {newCount}");
return new DownloadResult
{
TotalCount = purchasedPosts?.PaidPosts.Count ?? 0,
NewDownloads = newCount,
ExistingDownloads = oldCount,
MediaType = "Paid Posts",
Success = true
};
}
/// <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)
{
Log.Debug($"Calling DownloadPaidMessagesPurchasedTab - {username}");
if (paidMessageCollection.PaidMessages.Count == 0)
{
Log.Debug("Found 0 Paid Messages");
return new DownloadResult { TotalCount = 0, MediaType = "Paid Messages", Success = true };
}
int oldCount = 0, newCount = 0;
foreach (KeyValuePair<long, string> paidMessageKvp in paidMessageCollection.PaidMessages)
{
bool isNew;
MessageEntities.Medium? mediaInfo =
paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKvp.Key);
PurchasedEntities.ListItem? messageInfo =
paidMessageCollection.PaidMessageObjects.FirstOrDefault(p =>
p.Media?.Any(m => m.Id == paidMessageKvp.Key) == true);
string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username)
.PaidMessageFileNameFormat ?? "";
string paidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null &&
messageInfo.Id != 0 && messageInfo.CreatedAt is not null
? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"
: "/Messages/Paid";
if (paidMessageKvp.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = paidMessageKvp.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo =
await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3],
parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKvp.Key,
"Messages", progressReporter, paidMsgPath + "/Videos", filenameFormat,
messageInfo, mediaInfo, messageInfo?.FromUser, users);
}
else
{
isNew = await DownloadMedia(paidMessageKvp.Value, path,
paidMessageKvp.Key, "Messages", progressReporter,
paidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users);
}
if (isNew)
{
newCount++;
}
else
{
oldCount++;
}
}
Log.Debug($"Paid Messages Already Downloaded: {oldCount} New Paid Messages Downloaded: {newCount}");
return new DownloadResult
{
TotalCount = paidMessageCollection.PaidMessages.Count,
NewDownloads = newCount,
ExistingDownloads = oldCount,
MediaType = "Paid Messages",
Success = true
};
}
/// <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)
{
Log.Debug($"Calling DownloadSinglePost - {username}");
if (post.SinglePosts.Count == 0)
{
Log.Debug("Couldn't find post");
return new DownloadResult { TotalCount = 0, MediaType = "Posts", Success = true };
}
int oldCount = 0, newCount = 0;
foreach (KeyValuePair<long, string> postKvp in post.SinglePosts)
{
PostEntities.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.Id == postKvp.Key);
PostEntities.SinglePost? postInfo = mediaInfo == null
? null
: post.SinglePostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true);
string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username)
.PostFileNameFormat ?? "";
string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo.Id != 0
? $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}"
: "/Posts/Free";
bool isNew;
if (postKvp.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = postKvp.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo =
await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3],
parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKvp.Key, "Posts",
progressReporter, postPath + "/Videos", filenameFormat,
postInfo, mediaInfo, postInfo?.Author, users);
}
else
{
try
{
isNew = await DownloadMedia(postKvp.Value, path,
postKvp.Key, "Posts", progressReporter,
postPath, filenameFormat, postInfo, mediaInfo, postInfo?.Author, users);
}
catch
{
Log.Warning("Media was null");
continue;
}
}
if (isNew)
{
newCount++;
}
else
{
oldCount++;
}
}
return new DownloadResult
{
TotalCount = post.SinglePosts.Count,
NewDownloads = newCount,
ExistingDownloads = oldCount,
MediaType = "Posts",
Success = true
};
}
/// <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,
IProgressReporter progressReporter)
{
Log.Debug("Calling DownloadSinglePaidMessage - {Username}", username);
int totalNew = 0, totalOld = 0;
// Download preview messages
if (singlePaidMessageCollection.PreviewSingleMessages.Count > 0)
{
foreach (KeyValuePair<long, string> paidMessageKvp in singlePaidMessageCollection.PreviewSingleMessages)
{
MessageEntities.Medium? mediaInfo =
singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m =>
m.Id == paidMessageKvp.Key);
MessageEntities.SingleMessage? messageInfo =
singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p =>
p.Media?.Any(m => m.Id == paidMessageKvp.Key) == true);
string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username)
.PaidMessageFileNameFormat ?? "";
string previewMsgPath = configService.CurrentConfig.FolderPerMessage && messageInfo != null &&
messageInfo.Id != 0 && messageInfo.CreatedAt is not null
? $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"
: "/Messages/Free";
bool isNew;
if (paidMessageKvp.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = paidMessageKvp.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo =
await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3],
parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKvp.Key,
"Messages", progressReporter, previewMsgPath + "/Videos", filenameFormat,
messageInfo, mediaInfo, messageInfo?.FromUser, users);
}
else
{
isNew = await DownloadMedia(paidMessageKvp.Value, path,
paidMessageKvp.Key, "Messages", progressReporter,
previewMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users);
}
if (isNew)
{
totalNew++;
}
else
{
totalOld++;
}
}
}
// Download actual paid messages
if (singlePaidMessageCollection.SingleMessages.Count > 0)
{
foreach (KeyValuePair<long, string> paidMessageKvp in singlePaidMessageCollection.SingleMessages)
{
MessageEntities.Medium? mediaInfo =
singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m =>
m.Id == paidMessageKvp.Key);
MessageEntities.SingleMessage? messageInfo =
singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p =>
p.Media?.Any(m => m.Id == paidMessageKvp.Key) == true);
string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username)
.PaidMessageFileNameFormat ?? "";
string singlePaidMsgPath = configService.CurrentConfig.FolderPerPaidMessage &&
messageInfo != null && messageInfo.Id != 0 &&
messageInfo.CreatedAt is not null
? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"
: "/Messages/Paid";
bool isNew;
if (paidMessageKvp.Value.Contains("cdn3.onlyfans.com/dash/files"))
{
string[] parsed = paidMessageKvp.Value.Split(',');
(string decryptionKey, DateTime lastModified)? drmInfo =
await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3],
parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing);
if (drmInfo == null)
{
continue;
}
isNew = await DownloadDrmVideo(parsed[1], parsed[2], parsed[3], parsed[0],
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKvp.Key,
"Messages", progressReporter, singlePaidMsgPath + "/Videos", filenameFormat,
messageInfo, mediaInfo, messageInfo?.FromUser, users);
}
else
{
isNew = await DownloadMedia(paidMessageKvp.Value, path,
paidMessageKvp.Key, "Messages", progressReporter,
singlePaidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users);
}
if (isNew)
{
totalNew++;
}
else
{
totalOld++;
}
}
}
int totalCount = singlePaidMessageCollection.PreviewSingleMessages.Count +
singlePaidMessageCollection.SingleMessages.Count;
Log.Debug($"Paid Messages Already Downloaded: {totalOld} New Paid Messages Downloaded: {totalNew}");
return new DownloadResult
{
TotalCount = totalCount,
NewDownloads = totalNew,
ExistingDownloads = totalOld,
MediaType = "Paid Messages",
Success = true
};
}
}