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? username,
string folder,
List<long> paid_post_ids)
List<long> paidPostIds)
{
Log.Debug($"Calling GetMedia - {username}");
try
{
Dictionary<long, string> return_urls = new();
Dictionary<long, string> returnUrls = new();
const int postLimit = 50;
int limit = 5;
int offset = 0;
@ -419,7 +419,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
if (string.IsNullOrWhiteSpace(body))
{
Log.Warning("GetMedia returned empty response for {Endpoint}", endpoint);
return return_urls;
return returnUrls;
}
if (mediatype == MediaType.Stories)
@ -485,9 +485,9 @@ public class ApiService(IAuthService authService, IConfigService configService,
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 =
GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, "");
Dictionary<string, string> highlightHeaders =
GetDynamicHeaders("/api2/v2/stories/highlights/" + highlightId, "");
HttpClient highlight_client = GetHttpClient();
HttpClient highlightClient = GetHttpClient();
HttpRequestMessage highlight_request = new(HttpMethod.Get,
$"https://onlyfans.com/api2/v2/stories/highlights/{highlight_id}");
HttpRequestMessage highlightRequest = new(HttpMethod.Get,
$"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();
string highlightBody = await highlightResponse.Content.ReadAsStringAsync();
HighlightDtos.HighlightMediaDto? highlightMediaDto =
@ -608,9 +608,9 @@ public class ApiService(IAuthService authService, IConfigService configService,
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)
{
@ -640,7 +640,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
public async Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder,
string username,
List<long> paid_post_ids, IStatusReporter statusReporter)
List<long> paidPostIds, IStatusReporter statusReporter)
{
Log.Debug($"Calling GetPaidPosts - {username}");
@ -726,7 +726,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
{
if (!previewids.Contains(medium.Id))
{
paid_post_ids.Add(medium.Id);
paidPostIds.Add(medium.Id);
}
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)
{
Log.Debug($"Calling GetPosts - {endpoint}");
@ -978,7 +978,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
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 (!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,
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))
{
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,
List<long> paid_post_ids,
List<long> paidPostIds,
IStatusReporter statusReporter)
{
Log.Debug($"Calling GetStreams - {endpoint}");
@ -1311,7 +1311,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
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 (!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,
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))
{
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 != null && archive.IsOpened, archive.IsArchived, archive.PostedAt);
archivedCollection.ArchivedPostObjects.Add(archive);
@ -1567,7 +1567,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
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;
await dbService.AddMessage(folder, list.Id, list.Text ?? "", list.Price ?? "0",
list.CanPurchaseReason == "opened" ? true :
list.CanPurchaseReason != "opened" ? false : (bool?)null ?? false, false,
list.CanPurchaseReason == "opened" ||
(list.CanPurchaseReason == "opened" && ((bool?)null ?? false)), false,
createdAt,
list.FromUser?.Id ?? int.MinValue);
messageCollection.MessageObjects.Add(list);
@ -2907,24 +2907,19 @@ public class ApiService(IAuthService authService, IConfigService configService,
throw new Exception("Auth service is missing required fields");
}
string? pssh;
HttpClient client = new();
HttpRequestMessage request = new(HttpMethod.Get, mpdUrl);
request.Headers.Add("user-agent", currentAuth.UserAgent);
request.Headers.Add("Accept", "*/*");
request.Headers.Add("Cookie",
$"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {currentAuth.Cookie};");
using (HttpResponseMessage response = await client.SendAsync(request))
{
response.EnsureSuccessStatusCode();
string body = await response.Content.ReadAsStringAsync();
XNamespace ns = "urn:mpeg:dash:schema:mpd:2011";
XNamespace cenc = "urn:mpeg:cenc:2013";
XDocument xmlDoc = XDocument.Parse(body);
IEnumerable<XElement> psshElements = xmlDoc.Descendants(cenc + "pssh");
pssh = psshElements.ElementAt(1).Value;
}
using HttpResponseMessage response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
string body = await response.Content.ReadAsStringAsync();
XNamespace cenc = "urn:mpeg:cenc:2013";
XDocument xmlDoc = XDocument.Parse(body);
IEnumerable<XElement> psshElements = xmlDoc.Descendants(cenc + "pssh");
string pssh = psshElements.ElementAt(1).Value;
return pssh;
}
@ -2956,8 +2951,6 @@ public class ApiService(IAuthService authService, IConfigService configService,
try
{
DateTime lastmodified;
Auth? currentAuth = authService.CurrentAuth;
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("Cookie",
$"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {currentAuth.Cookie};");
using (HttpResponseMessage response =
await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
lastmodified = response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now;
using HttpResponseMessage response =
await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
DateTime lastmodified = response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now;
Log.Debug($"Last modified: {lastmodified}");
}
Log.Debug($"Last modified: {lastmodified}");
return lastmodified;
}
@ -3061,14 +3052,14 @@ public class ApiService(IAuthService authService, IConfigService configService,
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)
{
Log.Debug("Calling GetDecryptionKeyCDM");
try
{
byte[] resp1 = await PostData(licenceURL, drmHeaders, [0x08, 0x04]);
byte[] resp1 = await PostData(licenceUrl, drmHeaders, [0x08, 0x04]);
string certDataB64 = Convert.ToBase64String(resp1);
CDMApi cdm = new();
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");
}
byte[] resp2 = await PostData(licenceURL, drmHeaders, challenge);
byte[] resp2 = await PostData(licenceUrl, drmHeaders, challenge);
string licenseB64 = Convert.ToBase64String(resp2);
Log.Debug("resp1: {Resp1}", resp1);
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
}
public static bool IsStringOnlyDigits(string input) => input.All(char.IsDigit);
private static bool IsStringOnlyDigits(string input) => input.All(char.IsDigit);
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)
{
try
@ -3350,7 +3341,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
return null;
}
public static string? GetDynamicRules()
private static string? GetDynamicRules()
{
Log.Debug("Calling GetDynamicRules");
try

View File

@ -94,7 +94,7 @@ public class DownloadOrchestrationService(
{
long listId = lists[listName];
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);
}

View File

@ -1,6 +1,5 @@
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using FFmpeg.NET;
using FFmpeg.NET.Events;
using OF_DL.Models;
@ -31,7 +30,7 @@ public class DownloadService(
{
try
{
string path = "/Profile";
const string path = "/Profile";
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))
{
@ -85,7 +84,7 @@ public class DownloadService(
memoryStream.Seek(0, SeekOrigin.Begin);
MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(memoryStream);
byte[] hash = await md5.ComputeHashAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
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")
: DateTime.Now.ToString("dd-MM-yyyy"));
using (FileStream fileStream = File.Create(destinationPath))
await using (FileStream fileStream = File.Create(destinationPath))
{
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,
string sess, string url, string decryptionKey, string folder, DateTime lastModified, long media_id,
string api_type, IProgressReporter progressReporter, string customFileName, string filename, string path)
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
{
@ -116,35 +115,12 @@ public class DownloadService(
string decKey = "";
if (pos1 >= 0)
{
decKey = decryptionKey.Substring(pos1 + 1);
decKey = decryptionKey[(pos1 + 1)..];
}
int streamIndex = 0;
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
bool ffmpegDebugLogging = Log.IsEnabled(LogEventLevel.Debug);
@ -187,7 +163,7 @@ public class DownloadService(
$"{logLevelArgs} " +
$"-cenc_decryption_key {decKey} " +
$"-headers \"{cookieHeader}\" " +
$"-user_agent \"{user_agent}\" " +
$"-user_agent \"{userAgent}\" " +
"-referer \"https://onlyfans.com\" " +
"-rw_timeout 20000000 " +
"-reconnect 1 -reconnect_streamed 1 -reconnect_on_network_error 1 -reconnect_delay_max 10 " +
@ -205,7 +181,7 @@ public class DownloadService(
{
_completionSource.TrySetResult(true);
await OnFFMPEGDownloadComplete(tempFilename, lastModified, folder, path, customFileName, filename,
media_id, api_type, progressReporter);
mediaId, apiType, progressReporter);
};
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}",
input, output, exitCode, message, inner);
_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;
_completionSource.TrySetResult(false);
}
private static List<string> CalculateFolderMD5(string folder)
@ -900,42 +824,31 @@ public class DownloadService(
long totalFileSize = 0;
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)
{
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);
await Task.WhenAll(batchTasks);
await Task.Delay(5000);
}
long[] fileSizes = await Task.WhenAll(tasks);
foreach (long fileSize in fileSizes)
{
totalFileSize += fileSize;
}
totalFileSize += fileSizes.Sum();
}
else
{
List<Task<long>> tasks = new();
foreach (string url in urls)
{
tasks.Add(GetFileSizeAsync(url));
}
List<Task<long>> tasks = [];
tasks.AddRange(urls.Select(GetFileSizeAsync));
long[] fileSizes = await Task.WhenAll(tasks);
foreach (long fileSize in fileSizes)
{
totalFileSize += fileSize;
}
totalFileSize += fileSizes.Sum();
}
return totalFileSize;

View File

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

View File

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

View File

@ -264,23 +264,24 @@ public class CDM
Serializer.Serialize(memoryStream, session.Device.ClientID);
byte[] data = Padding.AddPKCS7Padding(memoryStream.ToArray(), 16);
using AesCryptoServiceProvider aesProvider = new()
{
BlockSize = 128, Padding = PaddingMode.PKCS7, Mode = CipherMode.CBC
};
aesProvider.GenerateKey();
aesProvider.GenerateIV();
using Aes aes = Aes.Create();
aes.BlockSize = 128;
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.CBC;
aes.GenerateKey();
aes.GenerateIV();
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);
cryptoStream.Write(data, 0, data.Length);
encryptedClientIdProto.EncryptedClientId = mstream.ToArray();
using RSACryptoServiceProvider RSA = new();
RSA.ImportRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey, out int _);
encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aesProvider.Key, RSAEncryptionPadding.OaepSHA1);
encryptedClientIdProto.EncryptedClientIdIv = aesProvider.IV;
encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aes.Key, RSAEncryptionPadding.OaepSHA1);
encryptedClientIdProto.EncryptedClientIdIv = aes.IV;
encryptedClientIdProto.ServiceId =
Encoding.UTF8.GetString(session.ServiceCertificate.DeviceCertificate.ServiceId);
encryptedClientIdProto.ServiceCertificateSerialNumber =
@ -397,27 +398,24 @@ public class CDM
continue;
}
byte[] keyId;
byte[] encryptedKey = key.Key;
byte[] iv = key.Iv;
keyId = key.Id;
byte[] keyId = key.Id;
if (keyId == null)
{
keyId = Encoding.ASCII.GetBytes(key.Type.ToString());
}
byte[] decryptedKey;
using MemoryStream mstream = new();
using AesCryptoServiceProvider aesProvider = new();
aesProvider.Mode = CipherMode.CBC;
aesProvider.Padding = PaddingMode.PKCS7;
using Aes aes = Aes.Create();
aes.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);
cryptoStream.Write(encryptedKey, 0, encryptedKey.Length);
decryptedKey = mstream.ToArray();
byte[] decryptedKey = mstream.ToArray();
List<string> permissions = [];
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)
{
T result = default!;
TaskCompletionSource<T> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
await AnsiConsole.Status()
.StartAsync($"[red]{Markup.Escape(statusMessage)}[/]",
async ctx =>
{
SpectreStatusReporter reporter = new(ctx);
result = await work(reporter);
try
{
SpectreStatusReporter reporter = new(ctx);
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,
Func<IProgressReporter, Task<T>> work)
{
T result = default!;
TaskCompletionSource<T> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
await AnsiConsole.Progress()
.Columns(GetProgressColumns(showSize))
.StartAsync(async ctx =>
{
ProgressTask task = ctx.AddTask($"[red]{Markup.Escape(description)}[/]", false);
task.MaxValue = maxValue;
task.StartTask();
try
{
ProgressTask task = ctx.AddTask($"[red]{Markup.Escape(description)}[/]", false);
task.MaxValue = maxValue;
task.StartTask();
SpectreProgressReporter progressReporter = new(task);
result = await work(progressReporter);
SpectreProgressReporter progressReporter = new(task);
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) =>

View File

@ -219,6 +219,7 @@ public class Program(IServiceProvider serviceProvider)
Console.WriteLine("\nPress any key to exit.");
Console.ReadKey();
}
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);
AnsiConsole.Markup(string.Format("[red]Downloading from List(s): {0}[/]",
string.Join(", ", listSelection)));
@ -787,6 +788,7 @@ public class Program(IServiceProvider serviceProvider)
AnsiConsole.MarkupLine("[red]Press any key to exit.[/]");
Console.ReadKey();
}
Environment.Exit(2);
}