diff --git a/OF DL.Core/Services/ApiService.cs b/OF DL.Core/Services/ApiService.cs index eb57808..9e1eb22 100644 --- a/OF DL.Core/Services/ApiService.cs +++ b/OF DL.Core/Services/ApiService.cs @@ -384,13 +384,13 @@ public class ApiService(IAuthService authService, IConfigService configService, string endpoint, string? username, string folder, - List paid_post_ids) + List paidPostIds) { Log.Debug($"Calling GetMedia - {username}"); try { - Dictionary return_urls = new(); + Dictionary 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 highlight_headers = - GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, ""); + Dictionary 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 keyValuePair in highlight_headers) + foreach (KeyValuePair 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 GetPaidPosts(string endpoint, string folder, string username, - List paid_post_ids, IStatusReporter statusReporter) + List 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 GetPosts(string endpoint, string folder, List paid_post_ids, + public async Task GetPosts(string endpoint, string folder, List 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 GetStreams(string endpoint, string folder, - List paid_post_ids, + List 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 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 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 GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, + public async Task GetDecryptionKeyCDM(Dictionary 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?> GetAllSubscriptions(Dictionary getParams, + private async Task?> GetAllSubscriptions(Dictionary 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 diff --git a/OF DL.Core/Services/DownloadOrchestrationService.cs b/OF DL.Core/Services/DownloadOrchestrationService.cs index ebf2d08..f71177c 100644 --- a/OF DL.Core/Services/DownloadOrchestrationService.cs +++ b/OF DL.Core/Services/DownloadOrchestrationService.cs @@ -94,7 +94,7 @@ public class DownloadOrchestrationService( { long listId = lists[listName]; List 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); } diff --git a/OF DL.Core/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs index 0fb1f80..5eb046c 100644 --- a/OF DL.Core/Services/DownloadService.cs +++ b/OF DL.Core/Services/DownloadService.cs @@ -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 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 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 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 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 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> tasks = new(); + List> tasks = []; for (int i = 0; i < urls.Count; i += batchSize) { List batchUrls = urls.Skip(i).Take(batchSize).ToList(); - IEnumerable> batchTasks = batchUrls.Select(GetFileSizeAsync); + Task[] 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> tasks = new(); - - foreach (string url in urls) - { - tasks.Add(GetFileSizeAsync(url)); - } + List> tasks = []; + tasks.AddRange(urls.Select(GetFileSizeAsync)); long[] fileSizes = await Task.WhenAll(tasks); - foreach (long fileSize in fileSizes) - { - totalFileSize += fileSize; - } + totalFileSize += fileSizes.Sum(); } return totalFileSize; diff --git a/OF DL.Core/Services/StartupService.cs b/OF DL.Core/Services/StartupService.cs index b33747d..08d80d2 100644 --- a/OF DL.Core/Services/StartupService.cs +++ b/OF DL.Core/Services/StartupService.cs @@ -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(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(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); diff --git a/OF DL.Core/Utils/ThrottledStream.cs b/OF DL.Core/Utils/ThrottledStream.cs index 0e1b128..6e0ef5e 100644 --- a/OF DL.Core/Utils/ThrottledStream.cs +++ b/OF DL.Core/Utils/ThrottledStream.cs @@ -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(); } } diff --git a/OF DL.Core/Widevine/CDM.cs b/OF DL.Core/Widevine/CDM.cs index 4dc3cb0..9328847 100644 --- a/OF DL.Core/Widevine/CDM.cs +++ b/OF DL.Core/Widevine/CDM.cs @@ -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 permissions = []; if (type == "OperatorSession") diff --git a/OF DL/CLI/SpectreDownloadEventHandler.cs b/OF DL/CLI/SpectreDownloadEventHandler.cs index 8758324..245bf0a 100644 --- a/OF DL/CLI/SpectreDownloadEventHandler.cs +++ b/OF DL/CLI/SpectreDownloadEventHandler.cs @@ -12,35 +12,55 @@ public class SpectreDownloadEventHandler : IDownloadEventHandler { public async Task WithStatusAsync(string statusMessage, Func> work) { - T result = default!; + TaskCompletionSource 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 WithProgressAsync(string description, long maxValue, bool showSize, Func> work) { - T result = default!; + TaskCompletionSource 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) => diff --git a/OF DL/Program.cs b/OF DL/Program.cs index fd3150c..23647e2 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -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); }