Major refactor #141

Merged
sim0n00ps merged 55 commits from whimsical-c4lic0/OF-DL:refactor-architecture into master 2026-02-13 00:21:58 +00:00
8 changed files with 145 additions and 235 deletions
Showing only changes of commit 4a218a3efe - Show all commits

View File

@ -384,13 +384,13 @@ public class ApiService(IAuthService authService, IConfigService configService,
string endpoint, string endpoint,
string? username, string? username,
string folder, string folder,
List<long> paid_post_ids) List<long> paidPostIds)
{ {
Log.Debug($"Calling GetMedia - {username}"); Log.Debug($"Calling GetMedia - {username}");
try try
{ {
Dictionary<long, string> return_urls = new(); Dictionary<long, string> returnUrls = new();
const int postLimit = 50; const int postLimit = 50;
int limit = 5; int limit = 5;
int offset = 0; int offset = 0;
@ -419,7 +419,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
if (string.IsNullOrWhiteSpace(body)) if (string.IsNullOrWhiteSpace(body))
{ {
Log.Warning("GetMedia returned empty response for {Endpoint}", endpoint); Log.Warning("GetMedia returned empty response for {Endpoint}", endpoint);
return return_urls; return returnUrls;
} }
if (mediatype == MediaType.Stories) if (mediatype == MediaType.Stories)
@ -485,9 +485,9 @@ public class ApiService(IAuthService authService, IConfigService configService,
continue; continue;
} }
if (medium.CanView && !return_urls.ContainsKey(medium.Id)) if (medium.CanView && !returnUrls.ContainsKey(medium.Id))
{ {
return_urls.Add(medium.Id, mediaUrl); returnUrls.Add(medium.Id, mediaUrl);
} }
} }
} }
@ -538,22 +538,22 @@ public class ApiService(IAuthService authService, IConfigService configService,
} }
} }
foreach (string highlight_id in highlightIds) foreach (string highlightId in highlightIds)
{ {
Dictionary<string, string> highlight_headers = Dictionary<string, string> highlightHeaders =
GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, ""); GetDynamicHeaders("/api2/v2/stories/highlights/" + highlightId, "");
HttpClient highlight_client = GetHttpClient(); HttpClient highlightClient = GetHttpClient();
HttpRequestMessage highlight_request = new(HttpMethod.Get, HttpRequestMessage highlightRequest = new(HttpMethod.Get,
$"https://onlyfans.com/api2/v2/stories/highlights/{highlight_id}"); $"https://onlyfans.com/api2/v2/stories/highlights/{highlightId}");
foreach (KeyValuePair<string, string> keyValuePair in highlight_headers) foreach (KeyValuePair<string, string> keyValuePair in highlightHeaders)
{ {
highlight_request.Headers.Add(keyValuePair.Key, keyValuePair.Value); highlightRequest.Headers.Add(keyValuePair.Key, keyValuePair.Value);
} }
using HttpResponseMessage highlightResponse = await highlight_client.SendAsync(highlight_request); using HttpResponseMessage highlightResponse = await highlightClient.SendAsync(highlightRequest);
highlightResponse.EnsureSuccessStatusCode(); highlightResponse.EnsureSuccessStatusCode();
string highlightBody = await highlightResponse.Content.ReadAsStringAsync(); string highlightBody = await highlightResponse.Content.ReadAsStringAsync();
HighlightDtos.HighlightMediaDto? highlightMediaDto = HighlightDtos.HighlightMediaDto? highlightMediaDto =
@ -608,9 +608,9 @@ public class ApiService(IAuthService authService, IConfigService configService,
continue; continue;
} }
if (!return_urls.ContainsKey(medium.Id) && !string.IsNullOrEmpty(storyUrl)) if (!returnUrls.ContainsKey(medium.Id) && !string.IsNullOrEmpty(storyUrl))
{ {
return_urls.Add(medium.Id, storyUrl); returnUrls.Add(medium.Id, storyUrl);
} }
} }
} }
@ -618,7 +618,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
} }
} }
return return_urls; return returnUrls;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -640,7 +640,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
public async Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder, public async Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder,
string username, string username,
List<long> paid_post_ids, IStatusReporter statusReporter) List<long> paidPostIds, IStatusReporter statusReporter)
{ {
Log.Debug($"Calling GetPaidPosts - {username}"); Log.Debug($"Calling GetPaidPosts - {username}");
@ -726,7 +726,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
{ {
if (!previewids.Contains(medium.Id)) if (!previewids.Contains(medium.Id))
{ {
paid_post_ids.Add(medium.Id); paidPostIds.Add(medium.Id);
} }
if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages) if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages)
@ -834,7 +834,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
} }
public async Task<PostEntities.PostCollection> GetPosts(string endpoint, string folder, List<long> paid_post_ids, public async Task<PostEntities.PostCollection> GetPosts(string endpoint, string folder, List<long> paidPostIds,
IStatusReporter statusReporter) IStatusReporter statusReporter)
{ {
Log.Debug($"Calling GetPosts - {endpoint}"); Log.Debug($"Calling GetPosts - {endpoint}");
@ -978,7 +978,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
if (medium.CanView && medium.Files?.Drm == null) if (medium.CanView && medium.Files?.Drm == null)
{ {
bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); bool has = paidPostIds.Any(cus => cus.Equals(medium.Id));
if (!has && !string.IsNullOrEmpty(fullUrl)) if (!has && !string.IsNullOrEmpty(fullUrl))
{ {
if (!postCollection.Posts.ContainsKey(medium.Id)) if (!postCollection.Posts.ContainsKey(medium.Id))
@ -1004,7 +1004,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy,
out string cloudFrontSignature, out string cloudFrontKeyPairId)) out string cloudFrontSignature, out string cloudFrontKeyPairId))
{ {
bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); bool has = paidPostIds.Any(cus => cus.Equals(medium.Id));
if (!has && !postCollection.Posts.ContainsKey(medium.Id)) if (!has && !postCollection.Posts.ContainsKey(medium.Id))
{ {
await dbService.AddMedia(folder, medium.Id, post.Id, manifestDash, null, null, null, await dbService.AddMedia(folder, medium.Id, post.Id, manifestDash, null, null, null,
@ -1197,7 +1197,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
} }
public async Task<StreamEntities.StreamsCollection> GetStreams(string endpoint, string folder, public async Task<StreamEntities.StreamsCollection> GetStreams(string endpoint, string folder,
List<long> paid_post_ids, List<long> paidPostIds,
IStatusReporter statusReporter) IStatusReporter statusReporter)
{ {
Log.Debug($"Calling GetStreams - {endpoint}"); Log.Debug($"Calling GetStreams - {endpoint}");
@ -1311,7 +1311,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
if (medium.CanView && medium.Files?.Drm == null) if (medium.CanView && medium.Files?.Drm == null)
{ {
bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); bool has = paidPostIds.Any(cus => cus.Equals(medium.Id));
if (!has && !string.IsNullOrEmpty(fullUrl)) if (!has && !string.IsNullOrEmpty(fullUrl))
{ {
if (!streamsCollection.Streams.ContainsKey(medium.Id)) if (!streamsCollection.Streams.ContainsKey(medium.Id))
@ -1327,7 +1327,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy, TryGetDrmInfo(medium.Files, out string manifestDash, out string cloudFrontPolicy,
out string cloudFrontSignature, out string cloudFrontKeyPairId)) out string cloudFrontSignature, out string cloudFrontKeyPairId))
{ {
bool has = paid_post_ids.Any(cus => cus.Equals(medium.Id)); bool has = paidPostIds.Any(cus => cus.Equals(medium.Id));
if (!has && !streamsCollection.Streams.ContainsKey(medium.Id)) if (!has && !streamsCollection.Streams.ContainsKey(medium.Id))
{ {
await dbService.AddMedia(folder, medium.Id, stream.Id, manifestDash, null, null, null, await dbService.AddMedia(folder, medium.Id, stream.Id, manifestDash, null, null, null,
@ -1451,7 +1451,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
} }
} }
await dbService.AddPost(folder, archive.Id, archive.Text != null ? archive.Text : "", await dbService.AddPost(folder, archive.Id, archive.Text ?? "",
archive.Price ?? "0", archive.Price ?? "0",
archive.Price != null && archive.IsOpened, archive.IsArchived, archive.PostedAt); archive.Price != null && archive.IsOpened, archive.IsArchived, archive.PostedAt);
archivedCollection.ArchivedPostObjects.Add(archive); archivedCollection.ArchivedPostObjects.Add(archive);
@ -1567,7 +1567,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
break; break;
} }
getParams["id"] = newMessages.List[newMessages.List.Count - 1].Id.ToString(); getParams["id"] = newMessages.List[^1].Id.ToString();
} }
} }
@ -1601,8 +1601,8 @@ public class ApiService(IAuthService authService, IConfigService configService,
{ {
DateTime createdAt = list.CreatedAt ?? DateTime.Now; DateTime createdAt = list.CreatedAt ?? DateTime.Now;
await dbService.AddMessage(folder, list.Id, list.Text ?? "", list.Price ?? "0", await dbService.AddMessage(folder, list.Id, list.Text ?? "", list.Price ?? "0",
list.CanPurchaseReason == "opened" ? true : list.CanPurchaseReason == "opened" ||
list.CanPurchaseReason != "opened" ? false : (bool?)null ?? false, false, (list.CanPurchaseReason == "opened" && ((bool?)null ?? false)), false,
createdAt, createdAt,
list.FromUser?.Id ?? int.MinValue); list.FromUser?.Id ?? int.MinValue);
messageCollection.MessageObjects.Add(list); messageCollection.MessageObjects.Add(list);
@ -2907,24 +2907,19 @@ public class ApiService(IAuthService authService, IConfigService configService,
throw new Exception("Auth service is missing required fields"); throw new Exception("Auth service is missing required fields");
} }
string? pssh;
HttpClient client = new(); HttpClient client = new();
HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); HttpRequestMessage request = new(HttpMethod.Get, mpdUrl);
request.Headers.Add("user-agent", currentAuth.UserAgent); request.Headers.Add("user-agent", currentAuth.UserAgent);
request.Headers.Add("Accept", "*/*"); request.Headers.Add("Accept", "*/*");
request.Headers.Add("Cookie", request.Headers.Add("Cookie",
$"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {currentAuth.Cookie};"); $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {currentAuth.Cookie};");
using (HttpResponseMessage response = await client.SendAsync(request)) using HttpResponseMessage response = await client.SendAsync(request);
{
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
string body = await response.Content.ReadAsStringAsync(); string body = await response.Content.ReadAsStringAsync();
XNamespace ns = "urn:mpeg:dash:schema:mpd:2011";
XNamespace cenc = "urn:mpeg:cenc:2013"; XNamespace cenc = "urn:mpeg:cenc:2013";
XDocument xmlDoc = XDocument.Parse(body); XDocument xmlDoc = XDocument.Parse(body);
IEnumerable<XElement> psshElements = xmlDoc.Descendants(cenc + "pssh"); IEnumerable<XElement> psshElements = xmlDoc.Descendants(cenc + "pssh");
pssh = psshElements.ElementAt(1).Value; string pssh = psshElements.ElementAt(1).Value;
}
return pssh; return pssh;
} }
@ -2956,8 +2951,6 @@ public class ApiService(IAuthService authService, IConfigService configService,
try try
{ {
DateTime lastmodified;
Auth? currentAuth = authService.CurrentAuth; Auth? currentAuth = authService.CurrentAuth;
if (currentAuth == null || currentAuth.UserAgent == null || currentAuth.Cookie == null) if (currentAuth == null || currentAuth.UserAgent == null || currentAuth.Cookie == null)
{ {
@ -2970,14 +2963,12 @@ public class ApiService(IAuthService authService, IConfigService configService,
request.Headers.Add("Accept", "*/*"); request.Headers.Add("Accept", "*/*");
request.Headers.Add("Cookie", request.Headers.Add("Cookie",
$"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {currentAuth.Cookie};"); $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {currentAuth.Cookie};");
using (HttpResponseMessage response = using HttpResponseMessage response =
await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
{
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
lastmodified = response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now; DateTime lastmodified = response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now;
Log.Debug($"Last modified: {lastmodified}"); Log.Debug($"Last modified: {lastmodified}");
}
return lastmodified; return lastmodified;
} }
@ -3061,14 +3052,14 @@ public class ApiService(IAuthService authService, IConfigService configService,
return string.Empty; return string.Empty;
} }
public async Task<string> GetDecryptionKeyCDM(Dictionary<string, string> drmHeaders, string licenceURL, public async Task<string> GetDecryptionKeyCDM(Dictionary<string, string> drmHeaders, string licenceUrl,
string pssh) string pssh)
{ {
Log.Debug("Calling GetDecryptionKeyCDM"); Log.Debug("Calling GetDecryptionKeyCDM");
try try
{ {
byte[] resp1 = await PostData(licenceURL, drmHeaders, [0x08, 0x04]); byte[] resp1 = await PostData(licenceUrl, drmHeaders, [0x08, 0x04]);
string certDataB64 = Convert.ToBase64String(resp1); string certDataB64 = Convert.ToBase64String(resp1);
CDMApi cdm = new(); CDMApi cdm = new();
byte[]? challenge = cdm.GetChallenge(pssh, certDataB64); byte[]? challenge = cdm.GetChallenge(pssh, certDataB64);
@ -3077,7 +3068,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
throw new Exception("Failed to get challenge from CDM"); throw new Exception("Failed to get challenge from CDM");
} }
byte[] resp2 = await PostData(licenceURL, drmHeaders, challenge); byte[] resp2 = await PostData(licenceUrl, drmHeaders, challenge);
string licenseB64 = Convert.ToBase64String(resp2); string licenseB64 = Convert.ToBase64String(resp2);
Log.Debug("resp1: {Resp1}", resp1); Log.Debug("resp1: {Resp1}", resp1);
Log.Debug("certDataB64: {CertDataB64}", certDataB64); Log.Debug("certDataB64: {CertDataB64}", certDataB64);
@ -3156,7 +3147,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
diff.TotalSeconds; // This gives the number of seconds. If you need milliseconds, use diff.TotalMilliseconds diff.TotalSeconds; // This gives the number of seconds. If you need milliseconds, use diff.TotalMilliseconds
} }
public static bool IsStringOnlyDigits(string input) => input.All(char.IsDigit); private static bool IsStringOnlyDigits(string input) => input.All(char.IsDigit);
private HttpClient GetHttpClient() private HttpClient GetHttpClient()
@ -3277,7 +3268,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
} }
public async Task<Dictionary<string, long>?> GetAllSubscriptions(Dictionary<string, string> getParams, private async Task<Dictionary<string, long>?> GetAllSubscriptions(Dictionary<string, string> getParams,
string endpoint, bool includeRestricted) string endpoint, bool includeRestricted)
{ {
try try
@ -3350,7 +3341,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
return null; return null;
} }
public static string? GetDynamicRules() private static string? GetDynamicRules()
{ {
Log.Debug("Calling GetDynamicRules"); Log.Debug("Calling GetDynamicRules");
try try

View File

@ -94,7 +94,7 @@ public class DownloadOrchestrationService(
{ {
long listId = lists[listName]; long listId = lists[listName];
List<string> listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; List<string> listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? [];
return allUsers.Where(x => listUsernames.Contains(x.Key)).Distinct() return allUsers.Where(x => listUsernames.Contains(x.Key))
.ToDictionary(x => x.Key, x => x.Value); .ToDictionary(x => x.Key, x => x.Value);
} }

View File

@ -1,6 +1,5 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml.Linq;
using FFmpeg.NET; using FFmpeg.NET;
using FFmpeg.NET.Events; using FFmpeg.NET.Events;
using OF_DL.Models; using OF_DL.Models;
@ -31,7 +30,7 @@ public class DownloadService(
{ {
try try
{ {
string path = "/Profile"; const string path = "/Profile";
if (!Directory.Exists(folder + path)) if (!Directory.Exists(folder + path))
{ {
@ -61,7 +60,7 @@ public class DownloadService(
} }
} }
private async Task DownloadProfileImage(string url, string folder, string subFolder, string username) private static async Task DownloadProfileImage(string url, string folder, string subFolder, string username)
{ {
if (!Directory.Exists(folder + subFolder)) if (!Directory.Exists(folder + subFolder))
{ {
@ -85,7 +84,7 @@ public class DownloadService(
memoryStream.Seek(0, SeekOrigin.Begin); memoryStream.Seek(0, SeekOrigin.Begin);
MD5 md5 = MD5.Create(); MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(memoryStream); byte[] hash = await md5.ComputeHashAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin); memoryStream.Seek(0, SeekOrigin.Begin);
if (!md5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant())) if (!md5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant()))
{ {
@ -94,7 +93,7 @@ public class DownloadService(
? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy") ? response.Content.Headers.LastModified.Value.LocalDateTime.ToString("dd-MM-yyyy")
: DateTime.Now.ToString("dd-MM-yyyy")); : DateTime.Now.ToString("dd-MM-yyyy"));
using (FileStream fileStream = File.Create(destinationPath)) await using (FileStream fileStream = File.Create(destinationPath))
{ {
await memoryStream.CopyToAsync(fileStream); await memoryStream.CopyToAsync(fileStream);
} }
@ -104,9 +103,9 @@ public class DownloadService(
} }
} }
private async Task<bool> DownloadDrmMedia(string user_agent, string policy, string signature, string kvp, private async Task<bool> DownloadDrmMedia(string userAgent, string policy, string signature, string kvp,
string sess, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string sess, string url, string decryptionKey, string folder, DateTime lastModified, long mediaId,
string api_type, IProgressReporter progressReporter, string customFileName, string filename, string path) string apiType, IProgressReporter progressReporter, string customFileName, string filename, string path)
{ {
try try
{ {
@ -116,35 +115,12 @@ public class DownloadService(
string decKey = ""; string decKey = "";
if (pos1 >= 0) if (pos1 >= 0)
{ {
decKey = decryptionKey.Substring(pos1 + 1); decKey = decryptionKey[(pos1 + 1)..];
} }
int streamIndex = 0; int streamIndex = 0;
string tempFilename = $"{folder}{path}/{filename}_source.mp4"; string tempFilename = $"{folder}{path}/{filename}_source.mp4";
//int? streamIndex = await GetVideoStreamIndexFromMpd(url, policy, signature, kvp, downloadConfig.DownloadVideoResolution);
//if (streamIndex == null)
// throw new Exception($"Could not find video stream for resolution {downloadConfig.DownloadVideoResolution}");
//string tempFilename;
//switch (downloadConfig.DownloadVideoResolution)
//{
// case VideoResolution.source:
// tempFilename = $"{folder}{path}/{filename}_source.mp4";
// break;
// case VideoResolution._240:
// tempFilename = $"{folder}{path}/{filename}_240.mp4";
// break;
// case VideoResolution._720:
// tempFilename = $"{folder}{path}/{filename}_720.mp4";
// break;
// default:
// tempFilename = $"{folder}{path}/{filename}_source.mp4";
// break;
//}
// Configure ffmpeg log level and optional report file location // Configure ffmpeg log level and optional report file location
bool ffmpegDebugLogging = Log.IsEnabled(LogEventLevel.Debug); bool ffmpegDebugLogging = Log.IsEnabled(LogEventLevel.Debug);
@ -187,7 +163,7 @@ public class DownloadService(
$"{logLevelArgs} " + $"{logLevelArgs} " +
$"-cenc_decryption_key {decKey} " + $"-cenc_decryption_key {decKey} " +
$"-headers \"{cookieHeader}\" " + $"-headers \"{cookieHeader}\" " +
$"-user_agent \"{user_agent}\" " + $"-user_agent \"{userAgent}\" " +
"-referer \"https://onlyfans.com\" " + "-referer \"https://onlyfans.com\" " +
"-rw_timeout 20000000 " + "-rw_timeout 20000000 " +
"-reconnect 1 -reconnect_streamed 1 -reconnect_on_network_error 1 -reconnect_delay_max 10 " + "-reconnect 1 -reconnect_streamed 1 -reconnect_on_network_error 1 -reconnect_delay_max 10 " +
@ -205,7 +181,7 @@ public class DownloadService(
{ {
_completionSource.TrySetResult(true); _completionSource.TrySetResult(true);
await OnFFMPEGDownloadComplete(tempFilename, lastModified, folder, path, customFileName, filename, await OnFFMPEGDownloadComplete(tempFilename, lastModified, folder, path, customFileName, filename,
media_id, api_type, progressReporter); mediaId, apiType, progressReporter);
}; };
await ffmpeg.ExecuteAsync(parameters, CancellationToken.None); await ffmpeg.ExecuteAsync(parameters, CancellationToken.None);
@ -287,59 +263,7 @@ public class DownloadService(
Log.Error("FFmpeg failed. Input={Input} Output={Output} ExitCode={ExitCode} Message={Message} Inner={Inner}", Log.Error("FFmpeg failed. Input={Input} Output={Output} ExitCode={ExitCode} Message={Message} Inner={Inner}",
input, output, exitCode, message, inner); input, output, exitCode, message, inner);
_completionSource?.TrySetResult(false); _completionSource.TrySetResult(false);
}
private async Task<int?> GetVideoStreamIndexFromMpd(string mpdUrl, string policy, string signature, string kvp,
VideoResolution resolution)
{
if (authService.CurrentAuth == null)
{
return null;
}
HttpClient client = new();
HttpRequestMessage request = new(HttpMethod.Get, mpdUrl);
request.Headers.Add("user-agent", authService.CurrentAuth.UserAgent);
request.Headers.Add("Accept", "*/*");
request.Headers.Add("Cookie",
$"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.Cookie};");
using (HttpResponseMessage response = await client.SendAsync(request))
{
response.EnsureSuccessStatusCode();
string body = await response.Content.ReadAsStringAsync();
XDocument doc = XDocument.Parse(body);
XNamespace ns = "urn:mpeg:dash:schema:mpd:2011";
// XNamespace cenc = "urn:mpeg:cenc:2013";
XElement? videoAdaptationSet = doc
.Descendants(ns + "AdaptationSet")
.FirstOrDefault(e => (string?)e.Attribute("mimeType") == "video/mp4");
if (videoAdaptationSet == null)
{
return null;
}
string targetHeight = resolution switch
{
VideoResolution._240 => "240",
VideoResolution._720 => "720",
VideoResolution.source => "1280",
_ => throw new ArgumentOutOfRangeException(nameof(resolution))
};
List<XElement> representations = videoAdaptationSet.Elements(ns + "Representation").ToList();
for (int i = 0; i < representations.Count; i++)
{
if ((string?)representations[i].Attribute("height") == targetHeight)
{
return i; // this is the index FFmpeg will use for `-map 0:v:{i}`
}
}
}
return null;
} }
private static List<string> CalculateFolderMD5(string folder) private static List<string> CalculateFolderMD5(string folder)
@ -900,42 +824,31 @@ public class DownloadService(
long totalFileSize = 0; long totalFileSize = 0;
if (urls.Count > 250) if (urls.Count > 250)
{ {
int batchSize = 250; const int batchSize = 250;
List<Task<long>> tasks = new(); List<Task<long>> tasks = [];
for (int i = 0; i < urls.Count; i += batchSize) for (int i = 0; i < urls.Count; i += batchSize)
{ {
List<string> batchUrls = urls.Skip(i).Take(batchSize).ToList(); List<string> batchUrls = urls.Skip(i).Take(batchSize).ToList();
IEnumerable<Task<long>> batchTasks = batchUrls.Select(GetFileSizeAsync); Task<long>[] batchTasks = batchUrls.Select(GetFileSizeAsync).ToArray();
tasks.AddRange(batchTasks); tasks.AddRange(batchTasks);
await Task.WhenAll(batchTasks); await Task.WhenAll(batchTasks);
await Task.Delay(5000); await Task.Delay(5000);
} }
long[] fileSizes = await Task.WhenAll(tasks); long[] fileSizes = await Task.WhenAll(tasks);
foreach (long fileSize in fileSizes) totalFileSize += fileSizes.Sum();
{
totalFileSize += fileSize;
}
} }
else else
{ {
List<Task<long>> tasks = new(); List<Task<long>> tasks = [];
tasks.AddRange(urls.Select(GetFileSizeAsync));
foreach (string url in urls)
{
tasks.Add(GetFileSizeAsync(url));
}
long[] fileSizes = await Task.WhenAll(tasks); long[] fileSizes = await Task.WhenAll(tasks);
foreach (long fileSize in fileSizes) totalFileSize += fileSizes.Sum();
{
totalFileSize += fileSize;
}
} }
return totalFileSize; return totalFileSize;

View File

@ -6,6 +6,7 @@ using OF_DL.Helpers;
using OF_DL.Models; using OF_DL.Models;
using OF_DL.Models.OfdlApi; using OF_DL.Models.OfdlApi;
using Serilog; using Serilog;
using static Newtonsoft.Json.JsonConvert;
using WidevineConstants = OF_DL.Widevine.Constants; using WidevineConstants = OF_DL.Widevine.Constants;
namespace OF_DL.Services; namespace OF_DL.Services;
@ -30,11 +31,11 @@ public class StartupService(IConfigService configService, IAuthService authServi
// FFmpeg detection // FFmpeg detection
DetectFfmpeg(result); DetectFfmpeg(result);
if (result.FfmpegFound) if (result.FfmpegFound && result.FfmpegPath != null)
{ {
// Escape backslashes for Windows // Escape backslashes for Windows
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
result.FfmpegPath!.Contains(@":\") && result.FfmpegPath.Contains(@":\") &&
!result.FfmpegPath.Contains(@":\\")) !result.FfmpegPath.Contains(@":\\"))
{ {
result.FfmpegPath = result.FfmpegPath.Replace(@"\", @"\\"); result.FfmpegPath = result.FfmpegPath.Replace(@"\", @"\\");
@ -42,7 +43,7 @@ public class StartupService(IConfigService configService, IAuthService authServi
} }
// Get FFmpeg version // Get FFmpeg version
result.FfmpegVersion = await GetFfmpegVersionAsync(result.FfmpegPath!); result.FfmpegVersion = await GetFfmpegVersionAsync(result.FfmpegPath);
} }
// Widevine device checks // Widevine device checks
@ -51,40 +52,27 @@ public class StartupService(IConfigService configService, IAuthService authServi
result.DevicePrivateKeyMissing = !File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER, result.DevicePrivateKeyMissing = !File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER,
WidevineConstants.DEVICE_NAME, "device_private_key")); WidevineConstants.DEVICE_NAME, "device_private_key"));
if (!result.ClientIdBlobMissing) Log.Debug("device_client_id_blob {Status}", result.ClientIdBlobMissing ? "missing" : "found");
{ Log.Debug("device_private_key {Status}", result.DevicePrivateKeyMissing ? " missing" : "found");
Log.Debug("device_client_id_blob found");
}
else
{
Log.Debug("device_client_id_blob missing");
}
if (!result.DevicePrivateKeyMissing)
{
Log.Debug("device_private_key found");
}
else
{
Log.Debug("device_private_key missing");
}
// rules.json validation // rules.json validation
if (File.Exists("rules.json")) if (!File.Exists("rules.json"))
{ {
return result;
}
result.RulesJsonExists = true; result.RulesJsonExists = true;
try try
{ {
JsonConvert.DeserializeObject<DynamicRules>(File.ReadAllText("rules.json")); DeserializeObject<DynamicRules>(await File.ReadAllTextAsync("rules.json"));
Log.Debug("Rules.json: "); Log.Debug("Rules.json: ");
Log.Debug(JsonConvert.SerializeObject(File.ReadAllText("rules.json"), Formatting.Indented)); Log.Debug(SerializeObject(await File.ReadAllTextAsync("rules.json"), Formatting.Indented));
result.RulesJsonValid = true; result.RulesJsonValid = true;
} }
catch (Exception e) catch (Exception e)
{ {
result.RulesJsonError = e.Message; result.RulesJsonError = e.Message;
Log.Error("rules.json processing failed.", e.Message); Log.Error("rules.json processing failed. {ErrorMessage}", e.Message);
}
} }
return result; return result;
@ -100,7 +88,7 @@ public class StartupService(IConfigService configService, IAuthService authServi
result.LocalVersion = Assembly.GetEntryAssembly()?.GetName().Version; result.LocalVersion = Assembly.GetEntryAssembly()?.GetName().Version;
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
string? latestReleaseTag = null; string? latestReleaseTag;
try try
{ {
@ -121,18 +109,18 @@ public class StartupService(IConfigService configService, IAuthService authServi
} }
result.LatestVersion = new Version(latestReleaseTag.Replace("OFDLV", "")); result.LatestVersion = new Version(latestReleaseTag.Replace("OFDLV", ""));
int versionComparison = result.LocalVersion!.CompareTo(result.LatestVersion); int? versionComparison = result.LocalVersion?.CompareTo(result.LatestVersion);
result.IsUpToDate = versionComparison >= 0; result.IsUpToDate = versionComparison >= 0;
Log.Debug("Detected client running version " + Log.Debug("Detected client running version " +
$"{result.LocalVersion.Major}.{result.LocalVersion.Minor}.{result.LocalVersion.Build}"); $"{result.LocalVersion?.Major}.{result.LocalVersion?.Minor}.{result.LocalVersion?.Build}");
Log.Debug("Latest release version " + Log.Debug("Latest release version " +
$"{result.LatestVersion.Major}.{result.LatestVersion.Minor}.{result.LatestVersion.Build}"); $"{result.LatestVersion.Major}.{result.LatestVersion.Minor}.{result.LatestVersion.Build}");
} }
catch (Exception e) catch (Exception e)
{ {
result.CheckFailed = true; result.CheckFailed = true;
Log.Error("Error checking latest release on GitHub.", e.Message); Log.Error("Error checking latest release on GitHub. {Message}", e.Message);
} }
#else #else
Log.Debug("Running in Debug/Local mode. Version check skipped."); Log.Debug("Running in Debug/Local mode. Version check skipped.");
@ -207,7 +195,7 @@ public class StartupService(IConfigService configService, IAuthService authServi
if (firstLine.StartsWith("ffmpeg version")) if (firstLine.StartsWith("ffmpeg version"))
{ {
int versionStart = "ffmpeg version ".Length; int versionStart = "ffmpeg version ".Length;
int copyrightIndex = firstLine.IndexOf(" Copyright"); int copyrightIndex = firstLine.IndexOf(" Copyright", StringComparison.Ordinal);
return copyrightIndex > versionStart return copyrightIndex > versionStart
? firstLine.Substring(versionStart, copyrightIndex - versionStart) ? firstLine.Substring(versionStart, copyrightIndex - versionStart)
: firstLine.Substring(versionStart); : firstLine.Substring(versionStart);

View File

@ -61,9 +61,7 @@ public class ThrottledStream : Stream
TimeSpan sleep = targetTime - actualTime; TimeSpan sleep = targetTime - actualTime;
if (sleep > TimeSpan.Zero) if (sleep > TimeSpan.Zero)
{ {
using AutoResetEvent waitHandle = new(false); scheduler.Sleep(sleep).GetAwaiter().GetResult();
scheduler.Sleep(sleep).GetAwaiter().OnCompleted(() => waitHandle.Set());
waitHandle.WaitOne();
} }
} }

View File

@ -264,23 +264,24 @@ public class CDM
Serializer.Serialize(memoryStream, session.Device.ClientID); Serializer.Serialize(memoryStream, session.Device.ClientID);
byte[] data = Padding.AddPKCS7Padding(memoryStream.ToArray(), 16); byte[] data = Padding.AddPKCS7Padding(memoryStream.ToArray(), 16);
using AesCryptoServiceProvider aesProvider = new() using Aes aes = Aes.Create();
{ aes.BlockSize = 128;
BlockSize = 128, Padding = PaddingMode.PKCS7, Mode = CipherMode.CBC aes.Padding = PaddingMode.PKCS7;
}; aes.Mode = CipherMode.CBC;
aesProvider.GenerateKey();
aesProvider.GenerateIV(); aes.GenerateKey();
aes.GenerateIV();
using MemoryStream mstream = new(); using MemoryStream mstream = new();
using CryptoStream cryptoStream = new(mstream, aesProvider.CreateEncryptor(aesProvider.Key, aesProvider.IV), using CryptoStream cryptoStream = new(mstream, aes.CreateEncryptor(aes.Key, aes.IV),
CryptoStreamMode.Write); CryptoStreamMode.Write);
cryptoStream.Write(data, 0, data.Length); cryptoStream.Write(data, 0, data.Length);
encryptedClientIdProto.EncryptedClientId = mstream.ToArray(); encryptedClientIdProto.EncryptedClientId = mstream.ToArray();
using RSACryptoServiceProvider RSA = new(); using RSACryptoServiceProvider RSA = new();
RSA.ImportRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey, out int _); RSA.ImportRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey, out int _);
encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aesProvider.Key, RSAEncryptionPadding.OaepSHA1); encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aes.Key, RSAEncryptionPadding.OaepSHA1);
encryptedClientIdProto.EncryptedClientIdIv = aesProvider.IV; encryptedClientIdProto.EncryptedClientIdIv = aes.IV;
encryptedClientIdProto.ServiceId = encryptedClientIdProto.ServiceId =
Encoding.UTF8.GetString(session.ServiceCertificate.DeviceCertificate.ServiceId); Encoding.UTF8.GetString(session.ServiceCertificate.DeviceCertificate.ServiceId);
encryptedClientIdProto.ServiceCertificateSerialNumber = encryptedClientIdProto.ServiceCertificateSerialNumber =
@ -397,27 +398,24 @@ public class CDM
continue; continue;
} }
byte[] keyId;
byte[] encryptedKey = key.Key; byte[] encryptedKey = key.Key;
byte[] iv = key.Iv; byte[] iv = key.Iv;
keyId = key.Id; byte[] keyId = key.Id;
if (keyId == null) if (keyId == null)
{ {
keyId = Encoding.ASCII.GetBytes(key.Type.ToString()); keyId = Encoding.ASCII.GetBytes(key.Type.ToString());
} }
byte[] decryptedKey;
using MemoryStream mstream = new(); using MemoryStream mstream = new();
using AesCryptoServiceProvider aesProvider = new(); using Aes aes = Aes.Create();
aesProvider.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7;
aesProvider.Padding = PaddingMode.PKCS7; aes.Mode = CipherMode.CBC;
using CryptoStream cryptoStream = new(mstream, aesProvider.CreateDecryptor(session.DerivedKeys.Enc, iv), using CryptoStream cryptoStream = new(mstream, aes.CreateDecryptor(session.DerivedKeys.Enc, iv),
CryptoStreamMode.Write); CryptoStreamMode.Write);
cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); cryptoStream.Write(encryptedKey, 0, encryptedKey.Length);
decryptedKey = mstream.ToArray(); byte[] decryptedKey = mstream.ToArray();
List<string> permissions = []; List<string> permissions = [];
if (type == "OperatorSession") if (type == "OperatorSession")

View File

@ -12,35 +12,55 @@ public class SpectreDownloadEventHandler : IDownloadEventHandler
{ {
public async Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work) public async Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work)
{ {
T result = default!; TaskCompletionSource<T> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
await AnsiConsole.Status() await AnsiConsole.Status()
.StartAsync($"[red]{Markup.Escape(statusMessage)}[/]", .StartAsync($"[red]{Markup.Escape(statusMessage)}[/]",
async ctx => async ctx =>
{
try
{ {
SpectreStatusReporter reporter = new(ctx); SpectreStatusReporter reporter = new(ctx);
result = await work(reporter); T result = await work(reporter);
tcs.TrySetResult(result);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}); });
return result;
return await tcs.Task;
} }
public async Task<T> WithProgressAsync<T>(string description, long maxValue, bool showSize, public async Task<T> WithProgressAsync<T>(string description, long maxValue, bool showSize,
Func<IProgressReporter, Task<T>> work) Func<IProgressReporter, Task<T>> work)
{ {
T result = default!; TaskCompletionSource<T> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
await AnsiConsole.Progress() await AnsiConsole.Progress()
.Columns(GetProgressColumns(showSize)) .Columns(GetProgressColumns(showSize))
.StartAsync(async ctx => .StartAsync(async ctx =>
{
try
{ {
ProgressTask task = ctx.AddTask($"[red]{Markup.Escape(description)}[/]", false); ProgressTask task = ctx.AddTask($"[red]{Markup.Escape(description)}[/]", false);
task.MaxValue = maxValue; task.MaxValue = maxValue;
task.StartTask(); task.StartTask();
SpectreProgressReporter progressReporter = new(task); SpectreProgressReporter progressReporter = new(task);
result = await work(progressReporter); T result = await work(progressReporter);
tcs.TrySetResult(result);
task.StopTask(); task.StopTask();
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}); });
return result;
return await tcs.Task;
} }
public void OnContentFound(string contentType, int mediaCount, int objectCount) => public void OnContentFound(string contentType, int mediaCount, int objectCount) =>

View File

@ -219,6 +219,7 @@ public class Program(IServiceProvider serviceProvider)
Console.WriteLine("\nPress any key to exit."); Console.WriteLine("\nPress any key to exit.");
Console.ReadKey(); Console.ReadKey();
} }
Environment.Exit(2); Environment.Exit(2);
} }
} }
@ -542,7 +543,7 @@ public class Program(IServiceProvider serviceProvider)
} }
} }
selectedUsers = users.Where(x => listUsernames.Contains($"{x.Key}")).Distinct() selectedUsers = users.Where(x => listUsernames.Contains($"{x.Key}"))
.ToDictionary(x => x.Key, x => x.Value); .ToDictionary(x => x.Key, x => x.Value);
AnsiConsole.Markup(string.Format("[red]Downloading from List(s): {0}[/]", AnsiConsole.Markup(string.Format("[red]Downloading from List(s): {0}[/]",
string.Join(", ", listSelection))); string.Join(", ", listSelection)));
@ -787,6 +788,7 @@ public class Program(IServiceProvider serviceProvider)
AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); AnsiConsole.MarkupLine("[red]Press any key to exit.[/]");
Console.ReadKey(); Console.ReadKey();
} }
Environment.Exit(2); Environment.Exit(2);
} }