2094 lines
87 KiB
C#
2094 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))
|
|
{
|
|
// Use a relative path so FFREPORT parsing works on Windows (drive-letter ':' breaks option parsing).
|
|
string logDir = Path.Combine(Environment.CurrentDirectory, "logs");
|
|
Directory.CreateDirectory(logDir);
|
|
string ffReportPath = Path.Combine("logs", "ffmpeg-%p-%t.log").Replace("\\", "/");
|
|
Environment.SetEnvironmentVariable("FFREPORT", $"file={ffReportPath}:level=32");
|
|
Log.Debug("FFREPORT enabled at: {FFREPORT} (cwd: {Cwd})",
|
|
Environment.GetEnvironmentVariable("FFREPORT"), Environment.CurrentDirectory);
|
|
}
|
|
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);
|
|
return response is { IsSuccessStatusCode: true, Content.Headers.LastModified: not null }
|
|
? response.Content.Headers.LastModified.Value.DateTime
|
|
: 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>
|
|
private 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>
|
|
private 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);
|
|
|
|
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);
|
|
|
|
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).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";
|
|
object? messageAuthor = messageInfo?.FromUser ?? (object?)messageInfo?.Author;
|
|
|
|
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, messageAuthor, users);
|
|
}
|
|
else
|
|
{
|
|
isNew = await DownloadMedia(kvpEntry.Value, path, kvpEntry.Key, "Messages", progressReporter,
|
|
paidMsgPath, filenameFormat, messageInfo, mediaInfo, messageAuthor, 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).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";
|
|
object? postAuthor = postInfo?.FromUser ?? (object?)postInfo?.Author;
|
|
|
|
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, postAuthor, users);
|
|
}
|
|
else
|
|
{
|
|
isNew = await DownloadMedia(postKvp.Value, path, postKvp.Key, "Posts", progressReporter,
|
|
paidPostPath, filenameFormat, postInfo, mediaInfo, postAuthor, 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";
|
|
object? postAuthor = postInfo?.FromUser ?? (object?)postInfo?.Author;
|
|
|
|
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, postAuthor, users);
|
|
}
|
|
else
|
|
{
|
|
isNew = await DownloadMedia(purchasedPostKvp.Value, path,
|
|
purchasedPostKvp.Key, "Posts", progressReporter,
|
|
paidPostPath, filenameFormat, postInfo, mediaInfo, postAuthor, 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";
|
|
object? messageAuthor = messageInfo?.FromUser ?? (object?)messageInfo?.Author;
|
|
|
|
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, messageAuthor, users);
|
|
}
|
|
else
|
|
{
|
|
isNew = await DownloadMedia(paidMessageKvp.Value, path,
|
|
paidMessageKvp.Key, "Messages", progressReporter,
|
|
paidMsgPath, filenameFormat, messageInfo, mediaInfo, messageAuthor, 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
|
|
};
|
|
}
|
|
}
|