diff --git a/OF DL/CLI/SpectreProgressReporter.cs b/OF DL/CLI/SpectreProgressReporter.cs index f6c65ab..49cf12b 100644 --- a/OF DL/CLI/SpectreProgressReporter.cs +++ b/OF DL/CLI/SpectreProgressReporter.cs @@ -4,21 +4,15 @@ using Spectre.Console; namespace OF_DL.CLI; /// -/// Implementation of IProgressReporter that uses Spectre.Console's ProgressTask for CLI output. +/// Implementation of IProgressReporter that uses Spectre.Console's ProgressTask for CLI output. /// public class SpectreProgressReporter : IProgressReporter { private readonly ProgressTask _task; - public SpectreProgressReporter(ProgressTask task) - { - _task = task ?? throw new ArgumentNullException(nameof(task)); - } + public SpectreProgressReporter(ProgressTask task) => _task = task ?? throw new ArgumentNullException(nameof(task)); - public void ReportProgress(long increment) - { - _task.Increment(increment); - } + public void ReportProgress(long increment) => _task.Increment(increment); public void ReportStatus(string message) { diff --git a/OF DL/Crypto/CryptoUtils.cs b/OF DL/Crypto/CryptoUtils.cs index 05f4c67..624dce0 100644 --- a/OF DL/Crypto/CryptoUtils.cs +++ b/OF DL/Crypto/CryptoUtils.cs @@ -4,30 +4,26 @@ using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Macs; using Org.BouncyCastle.Crypto.Parameters; -namespace OF_DL.Crypto +namespace OF_DL.Crypto; + +public class CryptoUtils { - public class CryptoUtils + public static byte[] GetHMACSHA256Digest(byte[] data, byte[] key) => new HMACSHA256(key).ComputeHash(data); + + public static byte[] GetCMACDigest(byte[] data, byte[] key) { - public static byte[] GetHMACSHA256Digest(byte[] data, byte[] key) - { - return new HMACSHA256(key).ComputeHash(data); - } + IBlockCipher cipher = new AesEngine(); + IMac mac = new CMac(cipher, 128); - public static byte[] GetCMACDigest(byte[] data, byte[] key) - { - IBlockCipher cipher = new AesEngine(); - IMac mac = new CMac(cipher, 128); + KeyParameter keyParam = new(key); - KeyParameter keyParam = new KeyParameter(key); + mac.Init(keyParam); - mac.Init(keyParam); + mac.BlockUpdate(data, 0, data.Length); - mac.BlockUpdate(data, 0, data.Length); + byte[] outBytes = new byte[16]; - byte[] outBytes = new byte[16]; - - mac.DoFinal(outBytes, 0); - return outBytes; - } + mac.DoFinal(outBytes, 0); + return outBytes; } } diff --git a/OF DL/Crypto/Padding.cs b/OF DL/Crypto/Padding.cs index f819ea8..e3075e4 100644 --- a/OF DL/Crypto/Padding.cs +++ b/OF DL/Crypto/Padding.cs @@ -1,118 +1,126 @@ using System.Security.Cryptography; -namespace OF_DL.Crypto +namespace OF_DL.Crypto; + +public class Padding { - public class Padding + public static byte[] AddPKCS7Padding(byte[] data, int k) { - public static byte[] AddPKCS7Padding(byte[] data, int k) + int m = k - data.Length % k; + + byte[] padding = new byte[m]; + Array.Fill(padding, (byte)m); + + byte[] paddedBytes = new byte[data.Length + padding.Length]; + Buffer.BlockCopy(data, 0, paddedBytes, 0, data.Length); + Buffer.BlockCopy(padding, 0, paddedBytes, data.Length, padding.Length); + + return paddedBytes; + } + + public static byte[] RemovePKCS7Padding(byte[] paddedByteArray) + { + byte last = paddedByteArray[^1]; + if (paddedByteArray.Length <= last) { - int m = k - (data.Length % k); - - byte[] padding = new byte[m]; - Array.Fill(padding, (byte)m); - - byte[] paddedBytes = new byte[data.Length + padding.Length]; - Buffer.BlockCopy(data, 0, paddedBytes, 0, data.Length); - Buffer.BlockCopy(padding, 0, paddedBytes, data.Length, padding.Length); - - return paddedBytes; + return paddedByteArray; } - public static byte[] RemovePKCS7Padding(byte[] paddedByteArray) - { - var last = paddedByteArray[^1]; - if (paddedByteArray.Length <= last) - { - return paddedByteArray; - } + return SubArray(paddedByteArray, 0, paddedByteArray.Length - last); + } - return SubArray(paddedByteArray, 0, (paddedByteArray.Length - last)); + public static T[] SubArray(T[] arr, int start, int length) + { + T[] result = new T[length]; + Buffer.BlockCopy(arr, start, result, 0, length); + + return result; + } + + public static byte[] AddPSSPadding(byte[] hash) + { + int modBits = 2048; + int hLen = 20; + int emLen = 256; + + int lmask = 0; + for (int i = 0; i < 8 * emLen - (modBits - 1); i++) + { + lmask = (lmask >> 1) | 0x80; } - public static T[] SubArray(T[] arr, int start, int length) + if (emLen < hLen + hLen + 2) { - var result = new T[length]; - Buffer.BlockCopy(arr, start, result, 0, length); - - return result; + return null; } - public static byte[] AddPSSPadding(byte[] hash) + byte[] salt = new byte[hLen]; + new Random().NextBytes(salt); + + byte[] m_prime = Enumerable.Repeat((byte)0, 8).ToArray().Concat(hash).Concat(salt).ToArray(); + byte[] h = SHA1.Create().ComputeHash(m_prime); + + byte[] ps = Enumerable.Repeat((byte)0, emLen - hLen - hLen - 2).ToArray(); + byte[] db = ps.Concat(new byte[] { 0x01 }).Concat(salt).ToArray(); + + byte[] dbMask = MGF1(h, emLen - hLen - 1); + + byte[] maskedDb = new byte[dbMask.Length]; + for (int i = 0; i < dbMask.Length; i++) { - int modBits = 2048; - int hLen = 20; - int emLen = 256; - - int lmask = 0; - for (int i = 0; i < 8 * emLen - (modBits - 1); i++) - lmask = lmask >> 1 | 0x80; - - if (emLen < hLen + hLen + 2) - { - return null; - } - - byte[] salt = new byte[hLen]; - new Random().NextBytes(salt); - - byte[] m_prime = Enumerable.Repeat((byte)0, 8).ToArray().Concat(hash).Concat(salt).ToArray(); - byte[] h = SHA1.Create().ComputeHash(m_prime); - - byte[] ps = Enumerable.Repeat((byte)0, emLen - hLen - hLen - 2).ToArray(); - byte[] db = ps.Concat(new byte[] { 0x01 }).Concat(salt).ToArray(); - - byte[] dbMask = MGF1(h, emLen - hLen - 1); - - byte[] maskedDb = new byte[dbMask.Length]; - for (int i = 0; i < dbMask.Length; i++) - maskedDb[i] = (byte)(db[i] ^ dbMask[i]); - - maskedDb[0] = (byte)(maskedDb[0] & ~lmask); - - byte[] padded = maskedDb.Concat(h).Concat(new byte[] { 0xBC }).ToArray(); - - return padded; + maskedDb[i] = (byte)(db[i] ^ dbMask[i]); } - public static byte[] RemoveOAEPPadding(byte[] data) + maskedDb[0] = (byte)(maskedDb[0] & ~lmask); + + byte[] padded = maskedDb.Concat(h).Concat(new byte[] { 0xBC }).ToArray(); + + return padded; + } + + public static byte[] RemoveOAEPPadding(byte[] data) + { + int k = 256; + int hLen = 20; + + byte[] maskedSeed = data[1..(hLen + 1)]; + byte[] maskedDB = data[(hLen + 1)..]; + + byte[] seedMask = MGF1(maskedDB, hLen); + + byte[] seed = new byte[maskedSeed.Length]; + for (int i = 0; i < maskedSeed.Length; i++) { - int k = 256; - int hLen = 20; - - byte[] maskedSeed = data[1..(hLen + 1)]; - byte[] maskedDB = data[(hLen + 1)..]; - - byte[] seedMask = MGF1(maskedDB, hLen); - - byte[] seed = new byte[maskedSeed.Length]; - for (int i = 0; i < maskedSeed.Length; i++) - seed[i] = (byte)(maskedSeed[i] ^ seedMask[i]); - - byte[] dbMask = MGF1(seed, k - hLen - 1); - - byte[] db = new byte[maskedDB.Length]; - for (int i = 0; i < maskedDB.Length; i++) - db[i] = (byte)(maskedDB[i] ^ dbMask[i]); - - int onePos = BitConverter.ToString(db[hLen..]).Replace("-", "").IndexOf("01") / 2; - byte[] unpadded = db[(hLen + onePos + 1)..]; - - return unpadded; + seed[i] = (byte)(maskedSeed[i] ^ seedMask[i]); } - static byte[] MGF1(byte[] seed, int maskLen) + byte[] dbMask = MGF1(seed, k - hLen - 1); + + byte[] db = new byte[maskedDB.Length]; + for (int i = 0; i < maskedDB.Length; i++) { - SHA1 hobj = SHA1.Create(); - int hLen = hobj.HashSize / 8; - List T = new List(); - for (int i = 0; i < (int)Math.Ceiling(((double)maskLen / (double)hLen)); i++) - { - byte[] c = BitConverter.GetBytes(i); - Array.Reverse(c); - byte[] digest = hobj.ComputeHash(seed.Concat(c).ToArray()); - T.AddRange(digest); - } - return T.GetRange(0, maskLen).ToArray(); + db[i] = (byte)(maskedDB[i] ^ dbMask[i]); } + + int onePos = BitConverter.ToString(db[hLen..]).Replace("-", "").IndexOf("01") / 2; + byte[] unpadded = db[(hLen + onePos + 1)..]; + + return unpadded; + } + + private static byte[] MGF1(byte[] seed, int maskLen) + { + SHA1 hobj = SHA1.Create(); + int hLen = hobj.HashSize / 8; + List T = new(); + for (int i = 0; i < (int)Math.Ceiling(maskLen / (double)hLen); i++) + { + byte[] c = BitConverter.GetBytes(i); + Array.Reverse(c); + byte[] digest = hobj.ComputeHash(seed.Concat(c).ToArray()); + T.AddRange(digest); + } + + return T.GetRange(0, maskLen).ToArray(); } } diff --git a/OF DL/Entities/Archived/Archived.cs b/OF DL/Entities/Archived/Archived.cs index 1231e1c..a8ff9dd 100644 --- a/OF DL/Entities/Archived/Archived.cs +++ b/OF DL/Entities/Archived/Archived.cs @@ -1,270 +1,262 @@ using Newtonsoft.Json; using OF_DL.Utils; -namespace OF_DL.Entities.Archived +namespace OF_DL.Entities.Archived; + +public class Archived { - public class Archived + public List list { get; set; } + public bool hasMore { get; set; } + public string headMarker { get; set; } + public string tailMarker { get; set; } + public Counters counters { get; set; } + + public class Author { - public List list { get; set; } - public bool hasMore { get; set; } - public string headMarker { get; set; } - public string tailMarker { get; set; } - public Counters counters { get; set; } - public class Author + public long id { get; set; } + public string _view { get; set; } + } + + public class Counters + { + public int? audiosCount { get; set; } + public int? photosCount { get; set; } + public int? videosCount { get; set; } + public int? mediasCount { get; set; } + public int? postsCount { get; set; } + public int? streamsCount { get; set; } + public int? archivedPostsCount { get; set; } + } + + public class Dash + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } + + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } + + public class Drm + { + public Manifest manifest { get; set; } + public Signature signature { get; set; } + } + + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + public Drm drm { get; set; } + } + + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } + + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } + + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } + + public class Hls + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } + + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } + + public class Info + { + public Source source { get; set; } + public Preview preview { get; set; } + } + + public class LinkedPost + { + private string _rawText; + public string responseType { get; set; } + public long? id { get; set; } + public DateTime? postedAt { get; set; } + public string postedAtPrecise { get; set; } + public object expiredAt { get; set; } + public Author author { get; set; } + public string text { get; set; } + + public string rawText { - public long id { get; set; } - public string _view { get; set; } - } - - public class Counters - { - public int? audiosCount { get; set; } - public int? photosCount { get; set; } - public int? videosCount { get; set; } - public int? mediasCount { get; set; } - public int? postsCount { get; set; } - public int? streamsCount { get; set; } - public int? archivedPostsCount { get; set; } - } - - public class Dash - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } - - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } - - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class Hls - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } - - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } - - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } - - public class LinkedPost - { - public string responseType { get; set; } - public long? id { get; set; } - public DateTime? postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - private string _rawText; - public string rawText + get { - get + if (string.IsNullOrEmpty(_rawText)) { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } + _rawText = XmlUtils.EvaluateInnerText(text); + } - return _rawText; - } - set - { - _rawText = value; - } + return _rawText; } - public bool? lockedText { get; set; } - public bool? isFavorite { get; set; } - public bool? canReport { get; set; } - public bool? canDelete { get; set; } - public bool? canComment { get; set; } - public bool? canEdit { get; set; } - public bool? isPinned { get; set; } - public int? favoritesCount { get; set; } - public int? mediaCount { get; set; } - public bool? isMediaReady { get; set; } - public object voting { get; set; } - public bool? isOpened { get; set; } - public bool? canToggleFavorite { get; set; } - public object streamId { get; set; } - public string? price { get; set; } - public bool? hasVoting { get; set; } - public bool? isAddedToBookmarks { get; set; } - public bool? isArchived { get; set; } - public bool? isPrivateArchived { get; set; } - public bool? isDeleted { get; set; } - public bool? hasUrl { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public string cantCommentReason { get; set; } - public int? commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public List media { get; set; } - public bool? canViewMedia { get; set; } - public List preview { get; set; } + set => _rawText = value; } - public class List + public bool? lockedText { get; set; } + public bool? isFavorite { get; set; } + public bool? canReport { get; set; } + public bool? canDelete { get; set; } + public bool? canComment { get; set; } + public bool? canEdit { get; set; } + public bool? isPinned { get; set; } + public int? favoritesCount { get; set; } + public int? mediaCount { get; set; } + public bool? isMediaReady { get; set; } + public object voting { get; set; } + public bool? isOpened { get; set; } + public bool? canToggleFavorite { get; set; } + public object streamId { get; set; } + public string? price { get; set; } + public bool? hasVoting { get; set; } + public bool? isAddedToBookmarks { get; set; } + public bool? isArchived { get; set; } + public bool? isPrivateArchived { get; set; } + public bool? isDeleted { get; set; } + public bool? hasUrl { get; set; } + public bool? isCouplePeopleMedia { get; set; } + public string cantCommentReason { get; set; } + public int? commentsCount { get; set; } + public List mentionedUsers { get; set; } + public List linkedUsers { get; set; } + public List media { get; set; } + public bool? canViewMedia { get; set; } + public List preview { get; set; } + } + + public class List + { + private string _rawText; + public string responseType { get; set; } + public long id { get; set; } + public DateTime postedAt { get; set; } + public string postedAtPrecise { get; set; } + public object expiredAt { get; set; } + public Author author { get; set; } + public string text { get; set; } + + public string rawText { - public string responseType { get; set; } - public long id { get; set; } - public DateTime postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - private string _rawText; - public string rawText + get { - get + if (string.IsNullOrEmpty(_rawText)) { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } + _rawText = XmlUtils.EvaluateInnerText(text); + } - return _rawText; - } - set - { - _rawText = value; - } + return _rawText; } - public bool? lockedText { get; set; } - public bool? isFavorite { get; set; } - public bool? canReport { get; set; } - public bool? canDelete { get; set; } - public bool? canComment { get; set; } - public bool? canEdit { get; set; } - public bool? isPinned { get; set; } - public int? favoritesCount { get; set; } - public int? mediaCount { get; set; } - public bool? isMediaReady { get; set; } - public object voting { get; set; } - public bool isOpened { get; set; } - public bool? canToggleFavorite { get; set; } - public object streamId { get; set; } - public string price { get; set; } - public bool? hasVoting { get; set; } - public bool? isAddedToBookmarks { get; set; } - public bool isArchived { get; set; } - public bool? isPrivateArchived { get; set; } - public bool? isDeleted { get; set; } - public bool? hasUrl { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public int? commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public List media { get; set; } - public bool? canViewMedia { get; set; } - public List preview { get; set; } - public string cantCommentReason { get; set; } + set => _rawText = value; } - public class Manifest - { - public string hls { get; set; } - public string dash { get; set; } - } + public bool? lockedText { get; set; } + public bool? isFavorite { get; set; } + public bool? canReport { get; set; } + public bool? canDelete { get; set; } + public bool? canComment { get; set; } + public bool? canEdit { get; set; } + public bool? isPinned { get; set; } + public int? favoritesCount { get; set; } + public int? mediaCount { get; set; } + public bool? isMediaReady { get; set; } + public object voting { get; set; } + public bool isOpened { get; set; } + public bool? canToggleFavorite { get; set; } + public object streamId { get; set; } + public string price { get; set; } + public bool? hasVoting { get; set; } + public bool? isAddedToBookmarks { get; set; } + public bool isArchived { get; set; } + public bool? isPrivateArchived { get; set; } + public bool? isDeleted { get; set; } + public bool? hasUrl { get; set; } + public bool? isCouplePeopleMedia { get; set; } + public int? commentsCount { get; set; } + public List mentionedUsers { get; set; } + public List linkedUsers { get; set; } + public List media { get; set; } + public bool? canViewMedia { get; set; } + public List preview { get; set; } + public string cantCommentReason { get; set; } + } - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool? convertedToVideo { get; set; } - public bool canView { get; set; } - public bool? hasError { get; set; } - public DateTime? createdAt { get; set; } - public Info info { get; set; } - public Source source { get; set; } - public string squarePreview { get; set; } - public string full { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public Files files { get; set; } - public VideoSources videoSources { get; set; } - } + public class Manifest + { + public string hls { get; set; } + public string dash { get; set; } + } - public class Preview - { - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - public string url { get; set; } - } + public class Medium + { + public long id { get; set; } + public string type { get; set; } + public bool? convertedToVideo { get; set; } + public bool canView { get; set; } + public bool? hasError { get; set; } + public DateTime? createdAt { get; set; } + public Info info { get; set; } + public Source source { get; set; } + public string squarePreview { get; set; } + public string full { get; set; } + public string preview { get; set; } + public string thumb { get; set; } + public Files files { get; set; } + public VideoSources videoSources { get; set; } + } - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } + public class Preview + { + public int? width { get; set; } + public int? height { get; set; } + public int? size { get; set; } + public string url { get; set; } + } - public class Source - { - public string source { get; set; } - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - public int? duration { get; set; } - } + public class Signature + { + public Hls hls { get; set; } + public Dash dash { get; set; } + } - public class VideoSources - { - [JsonProperty("720")] - public string _720 { get; set; } + public class Source + { + public string source { get; set; } + public int? width { get; set; } + public int? height { get; set; } + public int? size { get; set; } + public int? duration { get; set; } + } - [JsonProperty("240")] - public string _240 { get; set; } - } + public class VideoSources + { + [JsonProperty("720")] public string _720 { get; set; } + + [JsonProperty("240")] public string _240 { get; set; } } } diff --git a/OF DL/Entities/Archived/ArchivedCollection.cs b/OF DL/Entities/Archived/ArchivedCollection.cs index 82a1ab3..c61931a 100644 --- a/OF DL/Entities/Archived/ArchivedCollection.cs +++ b/OF DL/Entities/Archived/ArchivedCollection.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Entities.Archived +namespace OF_DL.Entities.Archived; + +public class ArchivedCollection { - public class ArchivedCollection - { - public Dictionary ArchivedPosts = new Dictionary(); - public List ArchivedPostObjects = new List(); - public List ArchivedPostMedia = new List(); - } + public List ArchivedPostMedia = new(); + public List ArchivedPostObjects = new(); + public Dictionary ArchivedPosts = new(); } diff --git a/OF DL/Entities/Auth.cs b/OF DL/Entities/Auth.cs index e2ea9ff..58b9da5 100644 --- a/OF DL/Entities/Auth.cs +++ b/OF DL/Entities/Auth.cs @@ -1,14 +1,13 @@ using Newtonsoft.Json; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class Auth { - public class Auth - { - public string? USER_ID { get; set; } = string.Empty; - public string? USER_AGENT { get; set; } = string.Empty; - public string? X_BC { get; set; } = string.Empty; - public string? COOKIE { get; set; } = string.Empty; - [JsonIgnore] - public string? FFMPEG_PATH { get; set; } = string.Empty; - } + public string? USER_ID { get; set; } = string.Empty; + public string? USER_AGENT { get; set; } = string.Empty; + public string? X_BC { get; set; } = string.Empty; + public string? COOKIE { get; set; } = string.Empty; + + [JsonIgnore] public string? FFMPEG_PATH { get; set; } = string.Empty; } diff --git a/OF DL/Entities/CDRMProjectRequest.cs b/OF DL/Entities/CDRMProjectRequest.cs index 69be2be..6f92897 100644 --- a/OF DL/Entities/CDRMProjectRequest.cs +++ b/OF DL/Entities/CDRMProjectRequest.cs @@ -1,22 +1,16 @@ using Newtonsoft.Json; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class CDRMProjectRequest { - public class CDRMProjectRequest - { - [JsonProperty("pssh")] - public string PSSH { get; set; } = ""; + [JsonProperty("pssh")] public string PSSH { get; set; } = ""; - [JsonProperty("licurl")] - public string LicenseURL { get; set; } = ""; + [JsonProperty("licurl")] public string LicenseURL { get; set; } = ""; - [JsonProperty("headers")] - public string Headers { get; set; } = ""; + [JsonProperty("headers")] public string Headers { get; set; } = ""; - [JsonProperty("cookies")] - public string Cookies { get; set; } = ""; + [JsonProperty("cookies")] public string Cookies { get; set; } = ""; - [JsonProperty("data")] - public string Data { get; set; } = ""; - } + [JsonProperty("data")] public string Data { get; set; } = ""; } diff --git a/OF DL/Entities/Config.cs b/OF DL/Entities/Config.cs index 7fbc8c9..814cc5c 100644 --- a/OF DL/Entities/Config.cs +++ b/OF DL/Entities/Config.cs @@ -3,152 +3,154 @@ using Newtonsoft.Json.Converters; using OF_DL.Enumerations; using Serilog; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class Config : IFileNameFormatConfig { + [ToggleableConfig] public bool DownloadAvatarHeaderPhoto { get; set; } = true; - public class Config : IFileNameFormatConfig + [ToggleableConfig] public bool DownloadPaidPosts { get; set; } = true; + + [ToggleableConfig] public bool DownloadPosts { get; set; } = true; + + [ToggleableConfig] public bool DownloadArchived { get; set; } = true; + + [ToggleableConfig] public bool DownloadStreams { get; set; } = true; + + [ToggleableConfig] public bool DownloadStories { get; set; } = true; + + [ToggleableConfig] public bool DownloadHighlights { get; set; } = true; + + [ToggleableConfig] public bool DownloadMessages { get; set; } = true; + + [ToggleableConfig] public bool DownloadPaidMessages { get; set; } = true; + + [ToggleableConfig] public bool DownloadImages { get; set; } = true; + + [ToggleableConfig] public bool DownloadVideos { get; set; } = true; + + [ToggleableConfig] public bool DownloadAudios { get; set; } = true; + + [ToggleableConfig] public bool IncludeExpiredSubscriptions { get; set; } = false; + + [ToggleableConfig] public bool IncludeRestrictedSubscriptions { get; set; } = false; + + [ToggleableConfig] public bool SkipAds { get; set; } = false; + + public string? DownloadPath { get; set; } = string.Empty; + + [ToggleableConfig] public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } = false; + + public int? Timeout { get; set; } = -1; + + [ToggleableConfig] public bool FolderPerPaidPost { get; set; } = false; + + [ToggleableConfig] public bool FolderPerPost { get; set; } = false; + + [ToggleableConfig] public bool FolderPerPaidMessage { get; set; } = false; + + [ToggleableConfig] public bool FolderPerMessage { get; set; } = false; + + [ToggleableConfig] public bool LimitDownloadRate { get; set; } = false; + + public int DownloadLimitInMbPerSec { get; set; } = 4; + + // Indicates if you want to download only on specific dates. + [ToggleableConfig] public bool DownloadOnlySpecificDates { get; set; } = false; + + // This enum will define if we want data from before or after the CustomDate. + [JsonConverter(typeof(StringEnumConverter))] + public DownloadDateSelection DownloadDateSelection { get; set; } = DownloadDateSelection.before; + // This is the specific date used in combination with the above enum. + + [JsonConverter(typeof(ShortDateConverter))] + public DateTime? CustomDate { get; set; } = null; + + [ToggleableConfig] public bool ShowScrapeSize { get; set; } = false; + + [ToggleableConfig] public bool DownloadPostsIncrementally { get; set; } = false; + + public bool NonInteractiveMode { get; set; } = false; + public string NonInteractiveModeListName { get; set; } = string.Empty; + + [ToggleableConfig] public bool NonInteractiveModePurchasedTab { get; set; } = false; + + public string? FFmpegPath { get; set; } = string.Empty; + + [ToggleableConfig] public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } = false; + + public Dictionary CreatorConfigs { get; set; } = new(); + + [ToggleableConfig] public bool DownloadDuplicatedMedia { get; set; } = false; + + public string IgnoredUsersListName { get; set; } = string.Empty; + + [JsonConverter(typeof(StringEnumConverter))] + public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error; + + [ToggleableConfig] public bool IgnoreOwnMessages { get; set; } = false; + + [ToggleableConfig] public bool DisableBrowserAuth { get; set; } = false; + + [JsonConverter(typeof(StringEnumConverter))] + public VideoResolution DownloadVideoResolution { get; set; } = VideoResolution.source; + + // When enabled, post/message text is stored as-is without XML stripping. + [ToggleableConfig] public bool DisableTextSanitization { get; set; } = false; + + public string? PaidPostFileNameFormat { get; set; } = string.Empty; + public string? PostFileNameFormat { get; set; } = string.Empty; + public string? PaidMessageFileNameFormat { get; set; } = string.Empty; + public string? MessageFileNameFormat { get; set; } = string.Empty; + + public IFileNameFormatConfig GetCreatorFileNameFormatConfig(string username) { - [ToggleableConfig] - public bool DownloadAvatarHeaderPhoto { get; set; } = true; - [ToggleableConfig] - public bool DownloadPaidPosts { get; set; } = true; - [ToggleableConfig] - public bool DownloadPosts { get; set; } = true; - [ToggleableConfig] - public bool DownloadArchived { get; set; } = true; - [ToggleableConfig] - public bool DownloadStreams { get; set; } = true; - [ToggleableConfig] - public bool DownloadStories { get; set; } = true; - [ToggleableConfig] - public bool DownloadHighlights { get; set; } = true; - [ToggleableConfig] - public bool DownloadMessages { get; set; } = true; - [ToggleableConfig] - public bool DownloadPaidMessages { get; set; } = true; - [ToggleableConfig] - public bool DownloadImages { get; set; } = true; - [ToggleableConfig] - public bool DownloadVideos { get; set; } = true; - [ToggleableConfig] - public bool DownloadAudios { get; set; } = true; - [ToggleableConfig] - public bool IncludeExpiredSubscriptions { get; set; } = false; - [ToggleableConfig] - public bool IncludeRestrictedSubscriptions { get; set; } = false; - [ToggleableConfig] - public bool SkipAds { get; set; } = false; + FileNameFormatConfig createFileNameFormatConfig = new(); - public string? DownloadPath { get; set; } = string.Empty; - public string? PaidPostFileNameFormat { get; set; } = string.Empty; - public string? PostFileNameFormat { get; set; } = string.Empty; - public string? PaidMessageFileNameFormat { get; set; } = string.Empty; - public string? MessageFileNameFormat { get; set; } = string.Empty; - [ToggleableConfig] - public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } = false; - public int? Timeout { get; set; } = -1; - [ToggleableConfig] - public bool FolderPerPaidPost { get; set; } = false; - [ToggleableConfig] - public bool FolderPerPost { get; set; } = false; - [ToggleableConfig] - public bool FolderPerPaidMessage { get; set; } = false; - [ToggleableConfig] - public bool FolderPerMessage { get; set; } = false; - [ToggleableConfig] - public bool LimitDownloadRate { get; set; } = false; - public int DownloadLimitInMbPerSec { get; set; } = 4; - - // Indicates if you want to download only on specific dates. - [ToggleableConfig] - public bool DownloadOnlySpecificDates { get; set; } = false; - - // This enum will define if we want data from before or after the CustomDate. - [JsonConverter(typeof(StringEnumConverter))] - public DownloadDateSelection DownloadDateSelection { get; set; } = DownloadDateSelection.before; - // This is the specific date used in combination with the above enum. - - [JsonConverter(typeof(ShortDateConverter))] - public DateTime? CustomDate { get; set; } = null; - - [ToggleableConfig] - public bool ShowScrapeSize { get; set; } = false; - - [ToggleableConfig] - public bool DownloadPostsIncrementally { get; set; } = false; - - public bool NonInteractiveMode { get; set; } = false; - public string NonInteractiveModeListName { get; set; } = string.Empty; - [ToggleableConfig] - public bool NonInteractiveModePurchasedTab { get; set; } = false; - public string? FFmpegPath { get; set; } = string.Empty; - - [ToggleableConfig] - public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } = false; - - public Dictionary CreatorConfigs { get; set; } = new Dictionary(); - - [ToggleableConfig] - public bool DownloadDuplicatedMedia { get; set; } = false; - - public string IgnoredUsersListName { get; set; } = string.Empty; - - [JsonConverter(typeof(StringEnumConverter))] - public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error; - - [ToggleableConfig] - public bool IgnoreOwnMessages { get; set; } = false; - - [ToggleableConfig] - public bool DisableBrowserAuth { get; set; } = false; - - [JsonConverter(typeof(StringEnumConverter))] - public VideoResolution DownloadVideoResolution { get; set; } = VideoResolution.source; - - // When enabled, post/message text is stored as-is without XML stripping. - [ToggleableConfig] - public bool DisableTextSanitization { get; set; } = false; - - public IFileNameFormatConfig GetCreatorFileNameFormatConfig(string username) + Func func = (val1, val2) => { - FileNameFormatConfig createFileNameFormatConfig = new FileNameFormatConfig(); - - Func func = (val1, val2) => + if (string.IsNullOrEmpty(val1)) { - if (string.IsNullOrEmpty(val1)) - return val2; - else - return val1; - }; - - if(CreatorConfigs.TryGetValue(username, out CreatorConfig? creatorConfig)) - { - createFileNameFormatConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat; - createFileNameFormatConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat; - createFileNameFormatConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat; - createFileNameFormatConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat; + return val2; } - createFileNameFormatConfig.PaidMessageFileNameFormat = func(createFileNameFormatConfig.PaidMessageFileNameFormat, PaidMessageFileNameFormat); - createFileNameFormatConfig.PostFileNameFormat = func(createFileNameFormatConfig.PostFileNameFormat, PostFileNameFormat); - createFileNameFormatConfig.MessageFileNameFormat = func(createFileNameFormatConfig.MessageFileNameFormat, MessageFileNameFormat); - createFileNameFormatConfig.PaidPostFileNameFormat = func(createFileNameFormatConfig.PaidPostFileNameFormat, PaidPostFileNameFormat); + return val1; + }; - Log.Debug("PaidMessageFilenameFormat: {CombinedConfigPaidMessageFileNameFormat}", createFileNameFormatConfig.PaidMessageFileNameFormat); - Log.Debug("PostFileNameFormat: {CombinedConfigPostFileNameFormat}", createFileNameFormatConfig.PostFileNameFormat); - Log.Debug("MessageFileNameFormat: {CombinedConfigMessageFileNameFormat}", createFileNameFormatConfig.MessageFileNameFormat); - Log.Debug("PaidPostFileNameFormat: {CombinedConfigPaidPostFileNameFormat}", createFileNameFormatConfig.PaidPostFileNameFormat); - - return createFileNameFormatConfig; + if (CreatorConfigs.TryGetValue(username, out CreatorConfig? creatorConfig)) + { + createFileNameFormatConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat; + createFileNameFormatConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat; + createFileNameFormatConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat; + createFileNameFormatConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat; } - } - public class CreatorConfig : IFileNameFormatConfig - { - public string? PaidPostFileNameFormat { get; set; } - public string? PostFileNameFormat { get; set; } - public string? PaidMessageFileNameFormat { get; set; } - public string? MessageFileNameFormat { get; set; } - } + createFileNameFormatConfig.PaidMessageFileNameFormat = + func(createFileNameFormatConfig.PaidMessageFileNameFormat, PaidMessageFileNameFormat); + createFileNameFormatConfig.PostFileNameFormat = + func(createFileNameFormatConfig.PostFileNameFormat, PostFileNameFormat); + createFileNameFormatConfig.MessageFileNameFormat = + func(createFileNameFormatConfig.MessageFileNameFormat, MessageFileNameFormat); + createFileNameFormatConfig.PaidPostFileNameFormat = + func(createFileNameFormatConfig.PaidPostFileNameFormat, PaidPostFileNameFormat); + Log.Debug("PaidMessageFilenameFormat: {CombinedConfigPaidMessageFileNameFormat}", + createFileNameFormatConfig.PaidMessageFileNameFormat); + Log.Debug("PostFileNameFormat: {CombinedConfigPostFileNameFormat}", + createFileNameFormatConfig.PostFileNameFormat); + Log.Debug("MessageFileNameFormat: {CombinedConfigMessageFileNameFormat}", + createFileNameFormatConfig.MessageFileNameFormat); + Log.Debug("PaidPostFileNameFormat: {CombinedConfigPaidPostFileNameFormat}", + createFileNameFormatConfig.PaidPostFileNameFormat); + + return createFileNameFormatConfig; + } +} + +public class CreatorConfig : IFileNameFormatConfig +{ + public string? PaidPostFileNameFormat { get; set; } + public string? PostFileNameFormat { get; set; } + public string? PaidMessageFileNameFormat { get; set; } + public string? MessageFileNameFormat { get; set; } } diff --git a/OF DL/Entities/DownloadResult.cs b/OF DL/Entities/DownloadResult.cs index f44d26d..0bfc589 100644 --- a/OF DL/Entities/DownloadResult.cs +++ b/OF DL/Entities/DownloadResult.cs @@ -1,37 +1,37 @@ namespace OF_DL.Entities; /// -/// Represents the result of a download operation. +/// Represents the result of a download operation. /// public class DownloadResult { /// - /// Total number of media items processed. + /// Total number of media items processed. /// public int TotalCount { get; set; } /// - /// Number of newly downloaded media items. + /// Number of newly downloaded media items. /// public int NewDownloads { get; set; } /// - /// Number of media items that were already downloaded. + /// Number of media items that were already downloaded. /// public int ExistingDownloads { get; set; } /// - /// The type of media downloaded (e.g., "Posts", "Messages", "Stories", etc.). + /// The type of media downloaded (e.g., "Posts", "Messages", "Stories", etc.). /// public string MediaType { get; set; } = string.Empty; /// - /// Indicates whether the download operation was successful. + /// Indicates whether the download operation was successful. /// public bool Success { get; set; } = true; /// - /// Optional error message if the download failed. + /// Optional error message if the download failed. /// public string? ErrorMessage { get; set; } } diff --git a/OF DL/Entities/DynamicRules.cs b/OF DL/Entities/DynamicRules.cs index eb41a7c..fc7684a 100644 --- a/OF DL/Entities/DynamicRules.cs +++ b/OF DL/Entities/DynamicRules.cs @@ -1,28 +1,30 @@ using Newtonsoft.Json; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class DynamicRules { - public class DynamicRules + [JsonProperty(PropertyName = "app-token")] + public string? AppToken { get; set; } + + [JsonProperty(PropertyName = "app_token")] + private string AppToken2 { - [JsonProperty(PropertyName="app-token")] - public string? AppToken { get; set; } - - [JsonProperty(PropertyName="app_token")] - private string AppToken2 { set { AppToken = value; } } - - [JsonProperty(PropertyName="static_param")] - public string? StaticParam { get; set; } - - [JsonProperty(PropertyName="prefix")] - public string? Prefix { get; set; } - - [JsonProperty(PropertyName="suffix")] - public string? Suffix { get; set; } - - [JsonProperty(PropertyName="checksum_constant")] - public int? ChecksumConstant { get; set; } - - [JsonProperty(PropertyName = "checksum_indexes")] - public List ChecksumIndexes { get; set; } + set => AppToken = value; } + + [JsonProperty(PropertyName = "static_param")] + public string? StaticParam { get; set; } + + [JsonProperty(PropertyName = "prefix")] + public string? Prefix { get; set; } + + [JsonProperty(PropertyName = "suffix")] + public string? Suffix { get; set; } + + [JsonProperty(PropertyName = "checksum_constant")] + public int? ChecksumConstant { get; set; } + + [JsonProperty(PropertyName = "checksum_indexes")] + public List ChecksumIndexes { get; set; } } diff --git a/OF DL/Entities/FileNameFormatConfig.cs b/OF DL/Entities/FileNameFormatConfig.cs index 73b7b73..46e36d5 100644 --- a/OF DL/Entities/FileNameFormatConfig.cs +++ b/OF DL/Entities/FileNameFormatConfig.cs @@ -1,11 +1,9 @@ -namespace OF_DL.Entities -{ - public class FileNameFormatConfig : IFileNameFormatConfig - { - public string? PaidPostFileNameFormat { get; set; } - public string? PostFileNameFormat { get; set; } - public string? PaidMessageFileNameFormat { get; set; } - public string? MessageFileNameFormat { get; set; } - } +namespace OF_DL.Entities; +public class FileNameFormatConfig : IFileNameFormatConfig +{ + public string? PaidPostFileNameFormat { get; set; } + public string? PostFileNameFormat { get; set; } + public string? PaidMessageFileNameFormat { get; set; } + public string? MessageFileNameFormat { get; set; } } diff --git a/OF DL/Entities/Highlights/HighlightMedia.cs b/OF DL/Entities/Highlights/HighlightMedia.cs index 12b4ccf..c75e01a 100644 --- a/OF DL/Entities/Highlights/HighlightMedia.cs +++ b/OF DL/Entities/Highlights/HighlightMedia.cs @@ -1,103 +1,102 @@ using Newtonsoft.Json; -namespace OF_DL.Entities.Highlights +namespace OF_DL.Entities.Highlights; + +public class HighlightMedia { - public class HighlightMedia - { - public long id { get; set; } - public long userId { get; set; } - public string title { get; set; } - public long coverStoryId { get; set; } - public string cover { get; set; } - public int storiesCount { get; set; } - public DateTime? createdAt { get; set; } - public List stories { get; set; } - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - } + public long id { get; set; } + public long userId { get; set; } + public string title { get; set; } + public long coverStoryId { get; set; } + public string cover { get; set; } + public int storiesCount { get; set; } + public DateTime? createdAt { get; set; } + public List stories { get; set; } - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + } - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Files files { get; set; } - } + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } - public class Preview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } + public class Medium + { + public long id { get; set; } + public string type { get; set; } + public bool convertedToVideo { get; set; } + public bool canView { get; set; } + public bool hasError { get; set; } + public DateTime? createdAt { get; set; } + public Files files { get; set; } + } - public class Source - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public int duration { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } + public class Preview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } - public class Sources - { - [JsonProperty("720")] - public string _720 { get; set; } + public class Source + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public int duration { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } - [JsonProperty("240")] - public string _240 { get; set; } - public string w150 { get; set; } - public string w480 { get; set; } - } + public class Sources + { + [JsonProperty("720")] public string _720 { get; set; } - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } + [JsonProperty("240")] public string _240 { get; set; } - public class Story - { - public long id { get; set; } - public long userId { get; set; } - public bool isWatched { get; set; } - public bool isReady { get; set; } - public List media { get; set; } - public DateTime? createdAt { get; set; } - public object question { get; set; } - public bool canLike { get; set; } - public bool isLiked { get; set; } - } + public string w150 { get; set; } + public string w480 { get; set; } + } - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - } + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } + + public class Story + { + public long id { get; set; } + public long userId { get; set; } + public bool isWatched { get; set; } + public bool isReady { get; set; } + public List media { get; set; } + public DateTime? createdAt { get; set; } + public object question { get; set; } + public bool canLike { get; set; } + public bool isLiked { get; set; } + } + + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } } diff --git a/OF DL/Entities/Highlights/Highlights.cs b/OF DL/Entities/Highlights/Highlights.cs index 8f3df8a..a5fa1b0 100644 --- a/OF DL/Entities/Highlights/Highlights.cs +++ b/OF DL/Entities/Highlights/Highlights.cs @@ -1,18 +1,18 @@ -namespace OF_DL.Entities.Highlights +namespace OF_DL.Entities.Highlights; + +public class Highlights { - public class Highlights - { - public List list { get; set; } - public bool hasMore { get; set; } - public class List - { - public long id { get; set; } - public long userId { get; set; } - public string title { get; set; } - public long coverStoryId { get; set; } - public string cover { get; set; } - public int storiesCount { get; set; } - public DateTime? createdAt { get; set; } - } - } + public List list { get; set; } + public bool hasMore { get; set; } + + public class List + { + public long id { get; set; } + public long userId { get; set; } + public string title { get; set; } + public long coverStoryId { get; set; } + public string cover { get; set; } + public int storiesCount { get; set; } + public DateTime? createdAt { get; set; } + } } diff --git a/OF DL/Entities/IFileNameFormatConfig.cs b/OF DL/Entities/IFileNameFormatConfig.cs index 1c3bc3f..663cc47 100644 --- a/OF DL/Entities/IFileNameFormatConfig.cs +++ b/OF DL/Entities/IFileNameFormatConfig.cs @@ -1,11 +1,9 @@ -namespace OF_DL.Entities -{ - public interface IFileNameFormatConfig - { - string? PaidPostFileNameFormat { get; set; } - string? PostFileNameFormat { get; set; } - string? PaidMessageFileNameFormat { get; set; } - string? MessageFileNameFormat { get; set; } - } +namespace OF_DL.Entities; +public interface IFileNameFormatConfig +{ + string? PaidPostFileNameFormat { get; set; } + string? PostFileNameFormat { get; set; } + string? PaidMessageFileNameFormat { get; set; } + string? MessageFileNameFormat { get; set; } } diff --git a/OF DL/Entities/Lists/UserList.cs b/OF DL/Entities/Lists/UserList.cs index 28a60d6..0c833a8 100644 --- a/OF DL/Entities/Lists/UserList.cs +++ b/OF DL/Entities/Lists/UserList.cs @@ -1,35 +1,35 @@ -namespace OF_DL.Entities.Lists -{ - public class UserList - { - public List list { get; set; } - public bool? hasMore { get; set; } - public class List - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public int? usersCount { get; set; } - public int? postsCount { get; set; } - public bool? canUpdate { get; set; } - public bool? canDelete { get; set; } - public bool? canManageUsers { get; set; } - public bool? canAddUsers { get; set; } - public bool? canPinnedToFeed { get; set; } - public bool? isPinnedToFeed { get; set; } - public bool? canPinnedToChat { get; set; } - public bool? isPinnedToChat { get; set; } - public string order { get; set; } - public string direction { get; set; } - public List users { get; set; } - public List customOrderUsersIds { get; set; } - public List posts { get; set; } - } +namespace OF_DL.Entities.Lists; - public class User - { - public long? id { get; set; } - public string _view { get; set; } - } - } +public class UserList +{ + public List list { get; set; } + public bool? hasMore { get; set; } + + public class List + { + public string id { get; set; } + public string type { get; set; } + public string name { get; set; } + public int? usersCount { get; set; } + public int? postsCount { get; set; } + public bool? canUpdate { get; set; } + public bool? canDelete { get; set; } + public bool? canManageUsers { get; set; } + public bool? canAddUsers { get; set; } + public bool? canPinnedToFeed { get; set; } + public bool? isPinnedToFeed { get; set; } + public bool? canPinnedToChat { get; set; } + public bool? isPinnedToChat { get; set; } + public string order { get; set; } + public string direction { get; set; } + public List users { get; set; } + public List customOrderUsersIds { get; set; } + public List posts { get; set; } + } + + public class User + { + public long? id { get; set; } + public string _view { get; set; } + } } diff --git a/OF DL/Entities/Lists/UsersList.cs b/OF DL/Entities/Lists/UsersList.cs index acd4c2a..77c0095 100644 --- a/OF DL/Entities/Lists/UsersList.cs +++ b/OF DL/Entities/Lists/UsersList.cs @@ -1,165 +1,164 @@ -namespace OF_DL.Entities.Lists +namespace OF_DL.Entities.Lists; + +public class UsersList { - public class UsersList - { - public string view { get; set; } - public string avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long? id { get; set; } - public string name { get; set; } - public string username { get; set; } - public bool? canLookStory { get; set; } - public bool? canCommentStory { get; set; } - public bool? hasNotViewedStory { get; set; } - public bool? isVerified { get; set; } - public bool? canPayInternal { get; set; } - public bool? hasScheduledStream { get; set; } - public bool? hasStream { get; set; } - public bool? hasStories { get; set; } - public bool? tipsEnabled { get; set; } - public bool? tipsTextEnabled { get; set; } - public int? tipsMin { get; set; } - public int? tipsMinInternal { get; set; } - public int? tipsMax { get; set; } - public bool? canEarn { get; set; } - public bool? canAddSubscriber { get; set; } - public string? subscribePrice { get; set; } - public List subscriptionBundles { get; set; } - public string displayName { get; set; } - public string notice { get; set; } - public bool? isPaywallRequired { get; set; } - public bool? unprofitable { get; set; } - public List listsStates { get; set; } - public bool? isMuted { get; set; } - public bool? isRestricted { get; set; } - public bool? canRestrict { get; set; } - public bool? subscribedBy { get; set; } - public bool? subscribedByExpire { get; set; } - public DateTime? subscribedByExpireDate { get; set; } - public bool? subscribedByAutoprolong { get; set; } - public bool? subscribedIsExpiredNow { get; set; } - public string? currentSubscribePrice { get; set; } - public bool? subscribedOn { get; set; } - public bool? subscribedOnExpiredNow { get; set; } - public string subscribedOnDuration { get; set; } - public bool? canReport { get; set; } - public bool? canReceiveChatMessage { get; set; } - public bool? hideChat { get; set; } - public DateTime? lastSeen { get; set; } - public bool? isPerformer { get; set; } - public bool? isRealPerformer { get; set; } - public SubscribedByData subscribedByData { get; set; } - public SubscribedOnData subscribedOnData { get; set; } - public bool? canTrialSend { get; set; } - public bool? isBlocked { get; set; } - public List promoOffers { get; set; } - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } + public string view { get; set; } + public string avatar { get; set; } + public AvatarThumbs avatarThumbs { get; set; } + public string header { get; set; } + public HeaderSize headerSize { get; set; } + public HeaderThumbs headerThumbs { get; set; } + public long? id { get; set; } + public string name { get; set; } + public string username { get; set; } + public bool? canLookStory { get; set; } + public bool? canCommentStory { get; set; } + public bool? hasNotViewedStory { get; set; } + public bool? isVerified { get; set; } + public bool? canPayInternal { get; set; } + public bool? hasScheduledStream { get; set; } + public bool? hasStream { get; set; } + public bool? hasStories { get; set; } + public bool? tipsEnabled { get; set; } + public bool? tipsTextEnabled { get; set; } + public int? tipsMin { get; set; } + public int? tipsMinInternal { get; set; } + public int? tipsMax { get; set; } + public bool? canEarn { get; set; } + public bool? canAddSubscriber { get; set; } + public string? subscribePrice { get; set; } + public List subscriptionBundles { get; set; } + public string displayName { get; set; } + public string notice { get; set; } + public bool? isPaywallRequired { get; set; } + public bool? unprofitable { get; set; } + public List listsStates { get; set; } + public bool? isMuted { get; set; } + public bool? isRestricted { get; set; } + public bool? canRestrict { get; set; } + public bool? subscribedBy { get; set; } + public bool? subscribedByExpire { get; set; } + public DateTime? subscribedByExpireDate { get; set; } + public bool? subscribedByAutoprolong { get; set; } + public bool? subscribedIsExpiredNow { get; set; } + public string? currentSubscribePrice { get; set; } + public bool? subscribedOn { get; set; } + public bool? subscribedOnExpiredNow { get; set; } + public string subscribedOnDuration { get; set; } + public bool? canReport { get; set; } + public bool? canReceiveChatMessage { get; set; } + public bool? hideChat { get; set; } + public DateTime? lastSeen { get; set; } + public bool? isPerformer { get; set; } + public bool? isRealPerformer { get; set; } + public SubscribedByData subscribedByData { get; set; } + public SubscribedOnData subscribedOnData { get; set; } + public bool? canTrialSend { get; set; } + public bool? isBlocked { get; set; } + public List promoOffers { get; set; } - public class HeaderSize - { - public int? width { get; set; } - public int? height { get; set; } - } + public class AvatarThumbs + { + public string c50 { get; set; } + public string c144 { get; set; } + } - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } + public class HeaderSize + { + public int? width { get; set; } + public int? height { get; set; } + } - public class ListsState - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool hasUser { get; set; } - public bool canAddUser { get; set; } - } + public class HeaderThumbs + { + public string w480 { get; set; } + public string w760 { get; set; } + } - public class Subscribe - { - public object id { get; set; } - public long? userId { get; set; } - public int? subscriberId { get; set; } - public DateTime? date { get; set; } - public int? duration { get; set; } - public DateTime? startDate { get; set; } - public DateTime? expireDate { get; set; } - public object cancelDate { get; set; } - public string? price { get; set; } - public string? regularPrice { get; set; } - public string? discount { get; set; } - public string action { get; set; } - public string type { get; set; } - public object offerStart { get; set; } - public object offerEnd { get; set; } - public bool? isCurrent { get; set; } - } + public class ListsState + { + public string id { get; set; } + public string type { get; set; } + public string name { get; set; } + public bool hasUser { get; set; } + public bool canAddUser { get; set; } + } - public class SubscribedByData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public string? discountPercent { get; set; } - public string? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public object renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public string status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public bool? showPostsInFeed { get; set; } - public List subscribes { get; set; } - } + public class Subscribe + { + public object id { get; set; } + public long? userId { get; set; } + public int? subscriberId { get; set; } + public DateTime? date { get; set; } + public int? duration { get; set; } + public DateTime? startDate { get; set; } + public DateTime? expireDate { get; set; } + public object cancelDate { get; set; } + public string? price { get; set; } + public string? regularPrice { get; set; } + public string? discount { get; set; } + public string action { get; set; } + public string type { get; set; } + public object offerStart { get; set; } + public object offerEnd { get; set; } + public bool? isCurrent { get; set; } + } - public class SubscribedOnData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public string? discountPercent { get; set; } - public string? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public object renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public object status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public string? tipsSumm { get; set; } - public string? subscribesSumm { get; set; } - public string? messagesSumm { get; set; } - public string? postsSumm { get; set; } - public string? streamsSumm { get; set; } - public string? totalSumm { get; set; } - public DateTime? lastActivity { get; set; } - public int? recommendations { get; set; } - public List subscribes { get; set; } - } + public class SubscribedByData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } + public string? subscribePrice { get; set; } + public string? discountPercent { get; set; } + public string? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public object renewedAt { get; set; } + public object discountFinishedAt { get; set; } + public object discountStartedAt { get; set; } + public string status { get; set; } + public bool? isMuted { get; set; } + public string unsubscribeReason { get; set; } + public string duration { get; set; } + public bool? showPostsInFeed { get; set; } + public List subscribes { get; set; } + } - public class SubscriptionBundle - { - public long? id { get; set; } - public string? discount { get; set; } - public string? duration { get; set; } - public string? price { get; set; } - public bool? canBuy { get; set; } - } + public class SubscribedOnData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } + public string? subscribePrice { get; set; } + public string? discountPercent { get; set; } + public string? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public object renewedAt { get; set; } + public object discountFinishedAt { get; set; } + public object discountStartedAt { get; set; } + public object status { get; set; } + public bool? isMuted { get; set; } + public string unsubscribeReason { get; set; } + public string duration { get; set; } + public string? tipsSumm { get; set; } + public string? subscribesSumm { get; set; } + public string? messagesSumm { get; set; } + public string? postsSumm { get; set; } + public string? streamsSumm { get; set; } + public string? totalSumm { get; set; } + public DateTime? lastActivity { get; set; } + public int? recommendations { get; set; } + public List subscribes { get; set; } + } - } + public class SubscriptionBundle + { + public long? id { get; set; } + public string? discount { get; set; } + public string? duration { get; set; } + public string? price { get; set; } + public bool? canBuy { get; set; } + } } diff --git a/OF DL/Entities/Messages/MessageCollection.cs b/OF DL/Entities/Messages/MessageCollection.cs index 4766fc1..c12cfcb 100644 --- a/OF DL/Entities/Messages/MessageCollection.cs +++ b/OF DL/Entities/Messages/MessageCollection.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Entities.Messages +namespace OF_DL.Entities.Messages; + +public class MessageCollection { - public class MessageCollection - { - public Dictionary Messages = new Dictionary(); - public List MessageObjects = new List(); - public List MessageMedia = new List(); - } + public List MessageMedia = new(); + public List MessageObjects = new(); + public Dictionary Messages = new(); } diff --git a/OF DL/Entities/Messages/Messages.cs b/OF DL/Entities/Messages/Messages.cs index 0809d57..ce05661 100644 --- a/OF DL/Entities/Messages/Messages.cs +++ b/OF DL/Entities/Messages/Messages.cs @@ -1,179 +1,173 @@ using Newtonsoft.Json; -namespace OF_DL.Entities.Messages +namespace OF_DL.Entities.Messages; + +public class Messages { - public class Messages - { - public List list { get; set; } - public bool hasMore { get; set; } - public class Dash - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public List list { get; set; } + public bool hasMore { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + public class Dash + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } + public class Drm + { + public Manifest manifest { get; set; } + public Signature signature { get; set; } + } - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + public Drm drm { get; set; } + } - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class FromUser - { - public long? id { get; set; } - public string _view { get; set; } - } + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class Hls - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class FromUser + { + public long? id { get; set; } + public string _view { get; set; } + } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + public class Hls + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } - public class List - { - public string responseType { get; set; } - public string text { get; set; } - public object giphyId { get; set; } - public bool? lockedText { get; set; } - public bool? isFree { get; set; } - public string? price { get; set; } - public bool? isMediaReady { get; set; } - public int? mediaCount { get; set; } - public List media { get; set; } - public List previews { get; set; } - public bool? isTip { get; set; } - public bool? isReportedByMe { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public object queueId { get; set; } - public FromUser fromUser { get; set; } - public bool? isFromQueue { get; set; } - public bool? canUnsendQueue { get; set; } - public int? unsendSecondsQueue { get; set; } - public long id { get; set; } - public bool? isOpened { get; set; } - public bool? isNew { get; set; } - public DateTime? createdAt { get; set; } - public DateTime? changedAt { get; set; } - public int? cancelSeconds { get; set; } - public bool? isLiked { get; set; } - public bool? canPurchase { get; set; } - public string canPurchaseReason { get; set; } - public bool? canReport { get; set; } - public bool? canBePinned { get; set; } - public bool? isPinned { get; set; } - } + public class Info + { + public Source source { get; set; } + public Preview preview { get; set; } + } - public class Manifest - { - public string hls { get; set; } - public string dash { get; set; } - } + public class List + { + public string responseType { get; set; } + public string text { get; set; } + public object giphyId { get; set; } + public bool? lockedText { get; set; } + public bool? isFree { get; set; } + public string? price { get; set; } + public bool? isMediaReady { get; set; } + public int? mediaCount { get; set; } + public List media { get; set; } + public List previews { get; set; } + public bool? isTip { get; set; } + public bool? isReportedByMe { get; set; } + public bool? isCouplePeopleMedia { get; set; } + public object queueId { get; set; } + public FromUser fromUser { get; set; } + public bool? isFromQueue { get; set; } + public bool? canUnsendQueue { get; set; } + public int? unsendSecondsQueue { get; set; } + public long id { get; set; } + public bool? isOpened { get; set; } + public bool? isNew { get; set; } + public DateTime? createdAt { get; set; } + public DateTime? changedAt { get; set; } + public int? cancelSeconds { get; set; } + public bool? isLiked { get; set; } + public bool? canPurchase { get; set; } + public string canPurchaseReason { get; set; } + public bool? canReport { get; set; } + public bool? canBePinned { get; set; } + public bool? isPinned { get; set; } + } - public class Medium - { - public long id { get; set; } - public bool canView { get; set; } - public string type { get; set; } - public string src { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public object locked { get; set; } - public int? duration { get; set; } - public bool? hasError { get; set; } - public string squarePreview { get; set; } - public Video video { get; set; } - public VideoSources videoSources { get; set; } - public Source source { get; set; } - public Info info { get; set; } - public Files files { get; set; } - } + public class Manifest + { + public string hls { get; set; } + public string dash { get; set; } + } - public class Preview - { - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - } + public class Medium + { + public long id { get; set; } + public bool canView { get; set; } + public string type { get; set; } + public string src { get; set; } + public string preview { get; set; } + public string thumb { get; set; } + public object locked { get; set; } + public int? duration { get; set; } + public bool? hasError { get; set; } + public string squarePreview { get; set; } + public Video video { get; set; } + public VideoSources videoSources { get; set; } + public Source source { get; set; } + public Info info { get; set; } + public Files files { get; set; } + } - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } + public class Preview + { + public int? width { get; set; } + public int? height { get; set; } + public int? size { get; set; } + } - public class Source - { - public string source { get; set; } - public int? width { get; set; } - public int? height { get; set; } - public int? size { get; set; } - } + public class Signature + { + public Hls hls { get; set; } + public Dash dash { get; set; } + } - public class Video - { - public string mp4 { get; set; } - } + public class Source + { + public string source { get; set; } + public int? width { get; set; } + public int? height { get; set; } + public int? size { get; set; } + } - public class VideoSources - { - [JsonProperty("720")] - public string _720 { get; set; } + public class Video + { + public string mp4 { get; set; } + } - [JsonProperty("240")] - public string _240 { get; set; } - } - } + public class VideoSources + { + [JsonProperty("720")] public string _720 { get; set; } + + [JsonProperty("240")] public string _240 { get; set; } + } } diff --git a/OF DL/Entities/Messages/SingleMessage.cs b/OF DL/Entities/Messages/SingleMessage.cs index 8b7be53..69e43f9 100644 --- a/OF DL/Entities/Messages/SingleMessage.cs +++ b/OF DL/Entities/Messages/SingleMessage.cs @@ -1,118 +1,115 @@ using static OF_DL.Entities.Messages.Messages; -namespace OF_DL.Entities.Messages +namespace OF_DL.Entities.Messages; + +public class AvatarThumbs { - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } - - public class FromUser - { - public string view { get; set; } - public string avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long? id { get; set; } - public string name { get; set; } - public string username { get; set; } - public bool canLookStory { get; set; } - public bool canCommentStory { get; set; } - public bool hasNotViewedStory { get; set; } - public bool isVerified { get; set; } - public bool canPayInternal { get; set; } - public bool hasScheduledStream { get; set; } - public bool hasStream { get; set; } - public bool hasStories { get; set; } - public bool tipsEnabled { get; set; } - public bool tipsTextEnabled { get; set; } - public int tipsMin { get; set; } - public int tipsMinInternal { get; set; } - public int tipsMax { get; set; } - public bool canEarn { get; set; } - public bool canAddSubscriber { get; set; } - public string? subscribePrice { get; set; } - public List subscriptionBundles { get; set; } - public bool isPaywallRequired { get; set; } - public List listsStates { get; set; } - public bool isRestricted { get; set; } - public bool canRestrict { get; set; } - public object subscribedBy { get; set; } - public object subscribedByExpire { get; set; } - public DateTime subscribedByExpireDate { get; set; } - public object subscribedByAutoprolong { get; set; } - public bool subscribedIsExpiredNow { get; set; } - public object currentSubscribePrice { get; set; } - public object subscribedOn { get; set; } - public object subscribedOnExpiredNow { get; set; } - public object subscribedOnDuration { get; set; } - public int callPrice { get; set; } - public DateTime? lastSeen { get; set; } - public bool canReport { get; set; } - } - - public class HeaderSize - { - public int width { get; set; } - public int height { get; set; } - } - - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } - - public class ListsState - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool hasUser { get; set; } - public bool canAddUser { get; set; } - public string cannotAddUserReason { get; set; } - } - - public class Preview - { - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } - - public class SingleMessage - { - public string responseType { get; set; } - public string text { get; set; } - public object giphyId { get; set; } - public bool lockedText { get; set; } - public bool isFree { get; set; } - public double price { get; set; } - public bool isMediaReady { get; set; } - public int mediaCount { get; set; } - public List media { get; set; } - public List previews { get; set; } - public bool isTip { get; set; } - public bool isReportedByMe { get; set; } - public bool isCouplePeopleMedia { get; set; } - public long queueId { get; set; } - public FromUser fromUser { get; set; } - public bool isFromQueue { get; set; } - public bool canUnsendQueue { get; set; } - public int unsendSecondsQueue { get; set; } - public long id { get; set; } - public bool isOpened { get; set; } - public bool isNew { get; set; } - public DateTime? createdAt { get; set; } - public DateTime? changedAt { get; set; } - public int cancelSeconds { get; set; } - public bool isLiked { get; set; } - public bool canPurchase { get; set; } - public bool canReport { get; set; } - } - + public string c50 { get; set; } + public string c144 { get; set; } } +public class FromUser +{ + public string view { get; set; } + public string avatar { get; set; } + public AvatarThumbs avatarThumbs { get; set; } + public string header { get; set; } + public HeaderSize headerSize { get; set; } + public HeaderThumbs headerThumbs { get; set; } + public long? id { get; set; } + public string name { get; set; } + public string username { get; set; } + public bool canLookStory { get; set; } + public bool canCommentStory { get; set; } + public bool hasNotViewedStory { get; set; } + public bool isVerified { get; set; } + public bool canPayInternal { get; set; } + public bool hasScheduledStream { get; set; } + public bool hasStream { get; set; } + public bool hasStories { get; set; } + public bool tipsEnabled { get; set; } + public bool tipsTextEnabled { get; set; } + public int tipsMin { get; set; } + public int tipsMinInternal { get; set; } + public int tipsMax { get; set; } + public bool canEarn { get; set; } + public bool canAddSubscriber { get; set; } + public string? subscribePrice { get; set; } + public List subscriptionBundles { get; set; } + public bool isPaywallRequired { get; set; } + public List listsStates { get; set; } + public bool isRestricted { get; set; } + public bool canRestrict { get; set; } + public object subscribedBy { get; set; } + public object subscribedByExpire { get; set; } + public DateTime subscribedByExpireDate { get; set; } + public object subscribedByAutoprolong { get; set; } + public bool subscribedIsExpiredNow { get; set; } + public object currentSubscribePrice { get; set; } + public object subscribedOn { get; set; } + public object subscribedOnExpiredNow { get; set; } + public object subscribedOnDuration { get; set; } + public int callPrice { get; set; } + public DateTime? lastSeen { get; set; } + public bool canReport { get; set; } +} + +public class HeaderSize +{ + public int width { get; set; } + public int height { get; set; } +} + +public class HeaderThumbs +{ + public string w480 { get; set; } + public string w760 { get; set; } +} + +public class ListsState +{ + public string id { get; set; } + public string type { get; set; } + public string name { get; set; } + public bool hasUser { get; set; } + public bool canAddUser { get; set; } + public string cannotAddUserReason { get; set; } +} + +public class Preview +{ + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } +} + +public class SingleMessage +{ + public string responseType { get; set; } + public string text { get; set; } + public object giphyId { get; set; } + public bool lockedText { get; set; } + public bool isFree { get; set; } + public double price { get; set; } + public bool isMediaReady { get; set; } + public int mediaCount { get; set; } + public List media { get; set; } + public List previews { get; set; } + public bool isTip { get; set; } + public bool isReportedByMe { get; set; } + public bool isCouplePeopleMedia { get; set; } + public long queueId { get; set; } + public FromUser fromUser { get; set; } + public bool isFromQueue { get; set; } + public bool canUnsendQueue { get; set; } + public int unsendSecondsQueue { get; set; } + public long id { get; set; } + public bool isOpened { get; set; } + public bool isNew { get; set; } + public DateTime? createdAt { get; set; } + public DateTime? changedAt { get; set; } + public int cancelSeconds { get; set; } + public bool isLiked { get; set; } + public bool canPurchase { get; set; } + public bool canReport { get; set; } +} diff --git a/OF DL/Entities/OFDLRequest.cs b/OF DL/Entities/OFDLRequest.cs index 718fac4..b5d4184 100644 --- a/OF DL/Entities/OFDLRequest.cs +++ b/OF DL/Entities/OFDLRequest.cs @@ -1,16 +1,12 @@ using Newtonsoft.Json; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class OFDLRequest { - public class OFDLRequest - { - [JsonProperty("pssh")] - public string PSSH { get; set; } = ""; + [JsonProperty("pssh")] public string PSSH { get; set; } = ""; - [JsonProperty("licenceURL")] - public string LicenseURL { get; set; } = ""; + [JsonProperty("licenceURL")] public string LicenseURL { get; set; } = ""; - [JsonProperty("headers")] - public string Headers { get; set; } = ""; - } + [JsonProperty("headers")] public string Headers { get; set; } = ""; } diff --git a/OF DL/Entities/Post/Post.cs b/OF DL/Entities/Post/Post.cs index 13613fd..de711d4 100644 --- a/OF DL/Entities/Post/Post.cs +++ b/OF DL/Entities/Post/Post.cs @@ -12,6 +12,7 @@ public class Post public string headMarker { get; set; } public string tailMarker { get; set; } + public class Author { public long id { get; set; } @@ -20,11 +21,9 @@ public class Post public class Dash { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } [JsonProperty("CloudFront-Key-Pair-Id")] public string CloudFrontKeyPairId { get; set; } @@ -72,11 +71,9 @@ public class Post public class Hls { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } [JsonProperty("CloudFront-Key-Pair-Id")] public string CloudFrontKeyPairId { get; set; } @@ -90,6 +87,7 @@ public class Post public class List { + private string _rawText; public string responseType { get; set; } public long id { get; set; } public DateTime postedAt { get; set; } @@ -98,23 +96,20 @@ public class Post public Author author { get; set; } public string text { get; set; } - private string _rawText; public string rawText { get { - if(string.IsNullOrEmpty(_rawText)) + if (string.IsNullOrEmpty(_rawText)) { _rawText = XmlUtils.EvaluateInnerText(text); } return _rawText; } - set - { - _rawText = value; - } + set => _rawText = value; } + public bool? lockedText { get; set; } public bool? isFavorite { get; set; } public bool? canReport { get; set; } @@ -197,11 +192,9 @@ public class Post public class VideoSources { - [JsonProperty("720")] - public object _720 { get; set; } + [JsonProperty("720")] public object _720 { get; set; } - [JsonProperty("240")] - public object _240 { get; set; } + [JsonProperty("240")] public object _240 { get; set; } } #pragma warning restore IDE1006 // Naming Styles } diff --git a/OF DL/Entities/Post/PostCollection.cs b/OF DL/Entities/Post/PostCollection.cs index bc2acbf..541c728 100644 --- a/OF DL/Entities/Post/PostCollection.cs +++ b/OF DL/Entities/Post/PostCollection.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Entities.Post +namespace OF_DL.Entities.Post; + +public class PostCollection { - public class PostCollection - { - public Dictionary Posts = new Dictionary(); - public List PostObjects = new List(); - public List PostMedia = new List(); - } + public List PostMedia = new(); + public List PostObjects = new(); + public Dictionary Posts = new(); } diff --git a/OF DL/Entities/Post/SinglePost.cs b/OF DL/Entities/Post/SinglePost.cs index 909e3e8..7f137fc 100644 --- a/OF DL/Entities/Post/SinglePost.cs +++ b/OF DL/Entities/Post/SinglePost.cs @@ -1,191 +1,188 @@ using Newtonsoft.Json; using OF_DL.Utils; -namespace OF_DL.Entities.Post +namespace OF_DL.Entities.Post; + +public class SinglePost { - public class SinglePost + private string _rawText; + public string responseType { get; set; } + public long id { get; set; } + public DateTime postedAt { get; set; } + public string postedAtPrecise { get; set; } + public object expiredAt { get; set; } + public Author author { get; set; } + public string text { get; set; } + + public string rawText + { + get + { + if (string.IsNullOrEmpty(_rawText)) + { + _rawText = XmlUtils.EvaluateInnerText(text); + } + + return _rawText; + } + set => _rawText = value; + } + + public bool lockedText { get; set; } + public bool isFavorite { get; set; } + public bool canReport { get; set; } + public bool canDelete { get; set; } + public bool canComment { get; set; } + public bool canEdit { get; set; } + public bool isPinned { get; set; } + public int favoritesCount { get; set; } + public int mediaCount { get; set; } + public bool isMediaReady { get; set; } + public object voting { get; set; } + public bool isOpened { get; set; } + public bool canToggleFavorite { get; set; } + public string streamId { get; set; } + public string price { get; set; } + public bool hasVoting { get; set; } + public bool isAddedToBookmarks { get; set; } + public bool isArchived { get; set; } + public bool isPrivateArchived { get; set; } + public bool isDeleted { get; set; } + public bool hasUrl { get; set; } + public bool isCouplePeopleMedia { get; set; } + public int commentsCount { get; set; } + public List mentionedUsers { get; set; } + public List linkedUsers { get; set; } + public string tipsAmount { get; set; } + public string tipsAmountRaw { get; set; } + public List media { get; set; } + public bool canViewMedia { get; set; } + public List preview { get; set; } + + public class Author { - public string responseType { get; set; } public long id { get; set; } - public DateTime postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - private string _rawText; - public string rawText - { - get - { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } + public string _view { get; set; } + } - return _rawText; - } - set - { - _rawText = value; - } - } - public bool lockedText { get; set; } - public bool isFavorite { get; set; } - public bool canReport { get; set; } - public bool canDelete { get; set; } - public bool canComment { get; set; } - public bool canEdit { get; set; } - public bool isPinned { get; set; } - public int favoritesCount { get; set; } - public int mediaCount { get; set; } - public bool isMediaReady { get; set; } - public object voting { get; set; } - public bool isOpened { get; set; } - public bool canToggleFavorite { get; set; } - public string streamId { get; set; } - public string price { get; set; } - public bool hasVoting { get; set; } - public bool isAddedToBookmarks { get; set; } - public bool isArchived { get; set; } - public bool isPrivateArchived { get; set; } - public bool isDeleted { get; set; } - public bool hasUrl { get; set; } - public bool isCouplePeopleMedia { get; set; } - public int commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public string tipsAmount { get; set; } - public string tipsAmountRaw { get; set; } - public List media { get; set; } - public bool canViewMedia { get; set; } - public List preview { get; set; } - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + public Drm drm { get; set; } + } - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class Info + { + public Source source { get; set; } + public Preview preview { get; set; } + } - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } + public class Medium + { + public long id { get; set; } + public string type { get; set; } + public bool convertedToVideo { get; set; } + public bool canView { get; set; } + public bool hasError { get; set; } + public DateTime? createdAt { get; set; } + public Info info { get; set; } + public Source source { get; set; } + public string squarePreview { get; set; } + public string full { get; set; } + public string preview { get; set; } + public string thumb { get; set; } + public bool hasCustomPreview { get; set; } + public Files files { get; set; } + public VideoSources videoSources { get; set; } + } - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Info info { get; set; } - public Source source { get; set; } - public string squarePreview { get; set; } - public string full { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public bool hasCustomPreview { get; set; } - public Files files { get; set; } - public VideoSources videoSources { get; set; } - } + public class Preview + { + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public string url { get; set; } + } - public class Preview - { - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public string url { get; set; } - } + public class Source + { + public string source { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public int duration { get; set; } + } - public class Source - { - public string source { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public int duration { get; set; } - } + public class VideoSources + { + [JsonProperty("720")] public string _720 { get; set; } - public class VideoSources - { - [JsonProperty("720")] - public string _720 { get; set; } + [JsonProperty("240")] public string _240 { get; set; } + } - [JsonProperty("240")] - public string _240 { get; set; } - } - public class Dash - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class Dash + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - public class Hls - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class Drm + { + public Manifest manifest { get; set; } + public Signature signature { get; set; } + } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + public class Hls + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - public class Manifest - { - public string? hls { get; set; } - public string? dash { get; set; } - } - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } + + public class Manifest + { + public string? hls { get; set; } + public string? dash { get; set; } + } + + public class Signature + { + public Hls hls { get; set; } + public Dash dash { get; set; } } } diff --git a/OF DL/Entities/Post/SinglePostCollection.cs b/OF DL/Entities/Post/SinglePostCollection.cs index 6d374e2..272dda8 100644 --- a/OF DL/Entities/Post/SinglePostCollection.cs +++ b/OF DL/Entities/Post/SinglePostCollection.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Entities.Post +namespace OF_DL.Entities.Post; + +public class SinglePostCollection { - public class SinglePostCollection - { - public Dictionary SinglePosts = new Dictionary(); - public List SinglePostObjects = new List(); - public List SinglePostMedia = new List(); - } + public List SinglePostMedia = new(); + public List SinglePostObjects = new(); + public Dictionary SinglePosts = new(); } diff --git a/OF DL/Entities/Purchased/PaidMessageCollection.cs b/OF DL/Entities/Purchased/PaidMessageCollection.cs index 09111eb..c75fc2c 100644 --- a/OF DL/Entities/Purchased/PaidMessageCollection.cs +++ b/OF DL/Entities/Purchased/PaidMessageCollection.cs @@ -1,11 +1,10 @@ using static OF_DL.Entities.Messages.Messages; -namespace OF_DL.Entities.Purchased +namespace OF_DL.Entities.Purchased; + +public class PaidMessageCollection { - public class PaidMessageCollection - { - public Dictionary PaidMessages = new Dictionary(); - public List PaidMessageObjects = new List(); - public List PaidMessageMedia = new List(); - } + public List PaidMessageMedia = new(); + public List PaidMessageObjects = new(); + public Dictionary PaidMessages = new(); } diff --git a/OF DL/Entities/Purchased/PaidPostCollection.cs b/OF DL/Entities/Purchased/PaidPostCollection.cs index 2e86d97..d0cfccf 100644 --- a/OF DL/Entities/Purchased/PaidPostCollection.cs +++ b/OF DL/Entities/Purchased/PaidPostCollection.cs @@ -1,11 +1,10 @@ using static OF_DL.Entities.Messages.Messages; -namespace OF_DL.Entities.Purchased +namespace OF_DL.Entities.Purchased; + +public class PaidPostCollection { - public class PaidPostCollection - { - public Dictionary PaidPosts = new Dictionary(); - public List PaidPostObjects = new List(); - public List PaidPostMedia = new List(); - } + public List PaidPostMedia = new(); + public List PaidPostObjects = new(); + public Dictionary PaidPosts = new(); } diff --git a/OF DL/Entities/Purchased/Purchased.cs b/OF DL/Entities/Purchased/Purchased.cs index 2e0c087..8b470b7 100644 --- a/OF DL/Entities/Purchased/Purchased.cs +++ b/OF DL/Entities/Purchased/Purchased.cs @@ -1,77 +1,75 @@ using Newtonsoft.Json; using static OF_DL.Entities.Messages.Messages; -namespace OF_DL.Entities.Purchased +namespace OF_DL.Entities.Purchased; + +public class Purchased { - public class Purchased + public List list { get; set; } + public bool hasMore { get; set; } + + public class FromUser { - public List list { get; set; } - public bool hasMore { get; set; } + public long id { get; set; } + public string _view { get; set; } + } - public class FromUser - { - public long id { get; set; } - public string _view { get; set; } - } - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } + public class Author + { + public long id { get; set; } + public string _view { get; set; } + } - public class Hls - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class Hls + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } - public class List - { - public string responseType { get; set; } - public string text { get; set; } - public object giphyId { get; set; } - public bool? lockedText { get; set; } - public bool? isFree { get; set; } - public string? price { get; set; } - public bool? isMediaReady { get; set; } - public int? mediaCount { get; set; } - public List media { get; set; } - public List previews { get; set; } - public List preview { get; set; } - public bool? isTip { get; set; } - public bool? isReportedByMe { get; set; } - public bool? isCouplePeopleMedia { get; set; } - public object queueId { get; set; } - public FromUser fromUser { get; set; } - public Author author { get; set; } - public bool? isFromQueue { get; set; } - public bool? canUnsendQueue { get; set; } - public int? unsendSecondsQueue { get; set; } - public long id { get; set; } - public bool isOpened { get; set; } - public bool? isNew { get; set; } - public DateTime? createdAt { get; set; } - public DateTime? postedAt { get; set; } - public DateTime? changedAt { get; set; } - public int? cancelSeconds { get; set; } - public bool? isLiked { get; set; } - public bool? canPurchase { get; set; } - public bool? canReport { get; set; } - public bool? isCanceled { get; set; } - public bool? isArchived { get; set; } - } + public class List + { + public string responseType { get; set; } + public string text { get; set; } + public object giphyId { get; set; } + public bool? lockedText { get; set; } + public bool? isFree { get; set; } + public string? price { get; set; } + public bool? isMediaReady { get; set; } + public int? mediaCount { get; set; } + public List media { get; set; } + public List previews { get; set; } + public List preview { get; set; } + public bool? isTip { get; set; } + public bool? isReportedByMe { get; set; } + public bool? isCouplePeopleMedia { get; set; } + public object queueId { get; set; } + public FromUser fromUser { get; set; } + public Author author { get; set; } + public bool? isFromQueue { get; set; } + public bool? canUnsendQueue { get; set; } + public int? unsendSecondsQueue { get; set; } + public long id { get; set; } + public bool isOpened { get; set; } + public bool? isNew { get; set; } + public DateTime? createdAt { get; set; } + public DateTime? postedAt { get; set; } + public DateTime? changedAt { get; set; } + public int? cancelSeconds { get; set; } + public bool? isLiked { get; set; } + public bool? canPurchase { get; set; } + public bool? canReport { get; set; } + public bool? isCanceled { get; set; } + public bool? isArchived { get; set; } + } - public class Manifest - { - public string hls { get; set; } - public string dash { get; set; } - } + public class Manifest + { + public string hls { get; set; } + public string dash { get; set; } } } diff --git a/OF DL/Entities/Purchased/PurchasedTabCollection.cs b/OF DL/Entities/Purchased/PurchasedTabCollection.cs index 822c0dd..4d39a33 100644 --- a/OF DL/Entities/Purchased/PurchasedTabCollection.cs +++ b/OF DL/Entities/Purchased/PurchasedTabCollection.cs @@ -1,10 +1,9 @@ -namespace OF_DL.Entities.Purchased +namespace OF_DL.Entities.Purchased; + +public class PurchasedTabCollection { - public class PurchasedTabCollection - { - public long UserId { get; set; } - public string Username { get; set; } = string.Empty; - public PaidPostCollection PaidPosts { get; set; } = new PaidPostCollection(); - public PaidMessageCollection PaidMessages { get; set; } = new PaidMessageCollection(); - } + public long UserId { get; set; } + public string Username { get; set; } = string.Empty; + public PaidPostCollection PaidPosts { get; set; } = new(); + public PaidMessageCollection PaidMessages { get; set; } = new(); } diff --git a/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs b/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs index ba9515e..1e5c4dc 100644 --- a/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs +++ b/OF DL/Entities/Purchased/SinglePaidMessageCollection.cs @@ -1,15 +1,14 @@ using OF_DL.Entities.Messages; using static OF_DL.Entities.Messages.Messages; -namespace OF_DL.Entities.Purchased -{ - public class SinglePaidMessageCollection - { - public Dictionary SingleMessages = new Dictionary(); - public List SingleMessageObjects = new List(); - public List SingleMessageMedia = new List(); +namespace OF_DL.Entities.Purchased; - public Dictionary PreviewSingleMessages = new Dictionary(); - public List PreviewSingleMessageMedia = new List(); - } +public class SinglePaidMessageCollection +{ + public List PreviewSingleMessageMedia = new(); + + public Dictionary PreviewSingleMessages = new(); + public List SingleMessageMedia = new(); + public List SingleMessageObjects = new(); + public Dictionary SingleMessages = new(); } diff --git a/OF DL/Entities/ShortDateConverter.cs b/OF DL/Entities/ShortDateConverter.cs index 93278f3..11ac1d6 100644 --- a/OF DL/Entities/ShortDateConverter.cs +++ b/OF DL/Entities/ShortDateConverter.cs @@ -1,12 +1,8 @@ using Newtonsoft.Json.Converters; -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class ShortDateConverter : IsoDateTimeConverter { - public class ShortDateConverter : IsoDateTimeConverter - { - public ShortDateConverter() - { - DateTimeFormat = "yyyy-MM-dd"; - } - } + public ShortDateConverter() => DateTimeFormat = "yyyy-MM-dd"; } diff --git a/OF DL/Entities/Stories/Stories.cs b/OF DL/Entities/Stories/Stories.cs index 90f0291..dc03628 100644 --- a/OF DL/Entities/Stories/Stories.cs +++ b/OF DL/Entities/Stories/Stories.cs @@ -1,91 +1,90 @@ using Newtonsoft.Json; -namespace OF_DL.Entities.Stories +namespace OF_DL.Entities.Stories; + +public class Stories { - public class Stories + public long id { get; set; } + public long userId { get; set; } + public bool isWatched { get; set; } + public bool isReady { get; set; } + public List media { get; set; } + public DateTime? createdAt { get; set; } + public object question { get; set; } + public bool canLike { get; set; } + public bool isLiked { get; set; } + + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + } + + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } + + public class Medium { public long id { get; set; } - public long userId { get; set; } - public bool isWatched { get; set; } - public bool isReady { get; set; } - public List media { get; set; } + public string type { get; set; } + public bool convertedToVideo { get; set; } + public bool canView { get; set; } + public bool hasError { get; set; } public DateTime? createdAt { get; set; } - public object question { get; set; } - public bool canLike { get; set; } - public bool isLiked { get; set; } - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - } + public Files files { get; set; } + } - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } + public class Preview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Files files { get; set; } - } + public class Source + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public int duration { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } - public class Preview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } + public class Sources + { + [JsonProperty("720")] public object _720 { get; set; } - public class Source - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public int duration { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } + [JsonProperty("240")] public object _240 { get; set; } - public class Sources - { - [JsonProperty("720")] - public object _720 { get; set; } + public string w150 { get; set; } + public string w480 { get; set; } + } - [JsonProperty("240")] - public object _240 { get; set; } - public string w150 { get; set; } - public string w480 { get; set; } - } + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public Sources sources { get; set; } + } - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public Sources sources { get; set; } - } - - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } } } diff --git a/OF DL/Entities/Streams/Streams.cs b/OF DL/Entities/Streams/Streams.cs index 13bc4b1..872d191 100644 --- a/OF DL/Entities/Streams/Streams.cs +++ b/OF DL/Entities/Streams/Streams.cs @@ -1,211 +1,209 @@ using Newtonsoft.Json; using OF_DL.Utils; -namespace OF_DL.Entities.Streams +namespace OF_DL.Entities.Streams; + +public class Streams { - public class Streams + public List list { get; set; } + public bool hasMore { get; set; } + public string headMarker { get; set; } + public string tailMarker { get; set; } + public Counters counters { get; set; } + + public class Author { - public List list { get; set; } - public bool hasMore { get; set; } - public string headMarker { get; set; } - public string tailMarker { get; set; } - public Counters counters { get; set; } - public class Author - { - public long id { get; set; } - public string _view { get; set; } - } + public long id { get; set; } + public string _view { get; set; } + } - public class Counters - { - public int audiosCount { get; set; } - public int photosCount { get; set; } - public int videosCount { get; set; } - public int mediasCount { get; set; } - public int postsCount { get; set; } - public int streamsCount { get; set; } - public int archivedPostsCount { get; set; } - } + public class Counters + { + public int audiosCount { get; set; } + public int photosCount { get; set; } + public int videosCount { get; set; } + public int mediasCount { get; set; } + public int postsCount { get; set; } + public int streamsCount { get; set; } + public int archivedPostsCount { get; set; } + } - public class Files - { - public Full full { get; set; } - public Thumb thumb { get; set; } - public Preview preview { get; set; } - public SquarePreview squarePreview { get; set; } - public Drm drm { get; set; } - } + public class Files + { + public Full full { get; set; } + public Thumb thumb { get; set; } + public Preview preview { get; set; } + public SquarePreview squarePreview { get; set; } + public Drm drm { get; set; } + } - public class Full - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public List sources { get; set; } - } + public class Full + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public List sources { get; set; } + } - public class SquarePreview - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class SquarePreview + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class Thumb - { - public string url { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - } + public class Thumb + { + public string url { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + } - public class Info - { - public Source source { get; set; } - public Preview preview { get; set; } - } + public class Info + { + public Source source { get; set; } + public Preview preview { get; set; } + } - public class List + public class List + { + private string _rawText; + public string responseType { get; set; } + public long id { get; set; } + public DateTime postedAt { get; set; } + public string postedAtPrecise { get; set; } + public object expiredAt { get; set; } + public Author author { get; set; } + public string text { get; set; } + + public string rawText { - public string responseType { get; set; } - public long id { get; set; } - public DateTime postedAt { get; set; } - public string postedAtPrecise { get; set; } - public object expiredAt { get; set; } - public Author author { get; set; } - public string text { get; set; } - private string _rawText; - public string rawText + get { - get + if (string.IsNullOrEmpty(_rawText)) { - if (string.IsNullOrEmpty(_rawText)) - { - _rawText = XmlUtils.EvaluateInnerText(text); - } + _rawText = XmlUtils.EvaluateInnerText(text); + } - return _rawText; - } - set - { - _rawText = value; - } + return _rawText; } - public bool lockedText { get; set; } - public bool isFavorite { get; set; } - public bool canReport { get; set; } - public bool canDelete { get; set; } - public bool canComment { get; set; } - public bool canEdit { get; set; } - public bool isPinned { get; set; } - public int favoritesCount { get; set; } - public int mediaCount { get; set; } - public bool isMediaReady { get; set; } - public object voting { get; set; } - public bool isOpened { get; set; } - public bool canToggleFavorite { get; set; } - public int streamId { get; set; } - public string price { get; set; } - public bool hasVoting { get; set; } - public bool isAddedToBookmarks { get; set; } - public bool isArchived { get; set; } - public bool isPrivateArchived { get; set; } - public bool isDeleted { get; set; } - public bool hasUrl { get; set; } - public bool isCouplePeopleMedia { get; set; } - public string cantCommentReason { get; set; } - public int commentsCount { get; set; } - public List mentionedUsers { get; set; } - public List linkedUsers { get; set; } - public string tipsAmount { get; set; } - public string tipsAmountRaw { get; set; } - public List media { get; set; } - public bool canViewMedia { get; set; } - public List preview { get; set; } + set => _rawText = value; } - public class Medium - { - public long id { get; set; } - public string type { get; set; } - public bool convertedToVideo { get; set; } - public bool canView { get; set; } - public bool hasError { get; set; } - public DateTime? createdAt { get; set; } - public Info info { get; set; } - public Source source { get; set; } - public string squarePreview { get; set; } - public string full { get; set; } - public string preview { get; set; } - public string thumb { get; set; } - public bool hasCustomPreview { get; set; } - public Files files { get; set; } - public VideoSources videoSources { get; set; } - } + public bool lockedText { get; set; } + public bool isFavorite { get; set; } + public bool canReport { get; set; } + public bool canDelete { get; set; } + public bool canComment { get; set; } + public bool canEdit { get; set; } + public bool isPinned { get; set; } + public int favoritesCount { get; set; } + public int mediaCount { get; set; } + public bool isMediaReady { get; set; } + public object voting { get; set; } + public bool isOpened { get; set; } + public bool canToggleFavorite { get; set; } + public int streamId { get; set; } + public string price { get; set; } + public bool hasVoting { get; set; } + public bool isAddedToBookmarks { get; set; } + public bool isArchived { get; set; } + public bool isPrivateArchived { get; set; } + public bool isDeleted { get; set; } + public bool hasUrl { get; set; } + public bool isCouplePeopleMedia { get; set; } + public string cantCommentReason { get; set; } + public int commentsCount { get; set; } + public List mentionedUsers { get; set; } + public List linkedUsers { get; set; } + public string tipsAmount { get; set; } + public string tipsAmountRaw { get; set; } + public List media { get; set; } + public bool canViewMedia { get; set; } + public List preview { get; set; } + } - public class Preview - { - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public string url { get; set; } - } + public class Medium + { + public long id { get; set; } + public string type { get; set; } + public bool convertedToVideo { get; set; } + public bool canView { get; set; } + public bool hasError { get; set; } + public DateTime? createdAt { get; set; } + public Info info { get; set; } + public Source source { get; set; } + public string squarePreview { get; set; } + public string full { get; set; } + public string preview { get; set; } + public string thumb { get; set; } + public bool hasCustomPreview { get; set; } + public Files files { get; set; } + public VideoSources videoSources { get; set; } + } - public class Source - { - public string source { get; set; } - public int width { get; set; } - public int height { get; set; } - public long size { get; set; } - public int duration { get; set; } - } + public class Preview + { + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public string url { get; set; } + } - public class VideoSources - { - [JsonProperty("720")] - public object _720 { get; set; } + public class Source + { + public string source { get; set; } + public int width { get; set; } + public int height { get; set; } + public long size { get; set; } + public int duration { get; set; } + } - [JsonProperty("240")] - public object _240 { get; set; } - } - public class Drm - { - public Manifest manifest { get; set; } - public Signature signature { get; set; } - } - public class Manifest - { - public string? hls { get; set; } - public string? dash { get; set; } - } - public class Signature - { - public Hls hls { get; set; } - public Dash dash { get; set; } - } - public class Hls - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class VideoSources + { + [JsonProperty("720")] public object _720 { get; set; } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + [JsonProperty("240")] public object _240 { get; set; } + } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } - public class Dash - { - [JsonProperty("CloudFront-Policy")] - public string CloudFrontPolicy { get; set; } + public class Drm + { + public Manifest manifest { get; set; } + public Signature signature { get; set; } + } - [JsonProperty("CloudFront-Signature")] - public string CloudFrontSignature { get; set; } + public class Manifest + { + public string? hls { get; set; } + public string? dash { get; set; } + } - [JsonProperty("CloudFront-Key-Pair-Id")] - public string CloudFrontKeyPairId { get; set; } - } + public class Signature + { + public Hls hls { get; set; } + public Dash dash { get; set; } + } + + public class Hls + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } + + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } + } + + public class Dash + { + [JsonProperty("CloudFront-Policy")] public string CloudFrontPolicy { get; set; } + + [JsonProperty("CloudFront-Signature")] public string CloudFrontSignature { get; set; } + + [JsonProperty("CloudFront-Key-Pair-Id")] + public string CloudFrontKeyPairId { get; set; } } } diff --git a/OF DL/Entities/Streams/StreamsCollection.cs b/OF DL/Entities/Streams/StreamsCollection.cs index c9a76f2..bc895a3 100644 --- a/OF DL/Entities/Streams/StreamsCollection.cs +++ b/OF DL/Entities/Streams/StreamsCollection.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Entities.Streams +namespace OF_DL.Entities.Streams; + +public class StreamsCollection { - public class StreamsCollection - { - public Dictionary Streams = new Dictionary(); - public List StreamObjects = new List(); - public List StreamMedia = new List(); - } + public List StreamMedia = new(); + public List StreamObjects = new(); + public Dictionary Streams = new(); } diff --git a/OF DL/Entities/Subscriptions.cs b/OF DL/Entities/Subscriptions.cs index b5da541..22973d6 100644 --- a/OF DL/Entities/Subscriptions.cs +++ b/OF DL/Entities/Subscriptions.cs @@ -1,159 +1,159 @@ -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class Subscriptions { - public class Subscriptions + public List list { get; set; } + public bool hasMore { get; set; } + + public class AvatarThumbs { - public List list { get; set; } - public bool hasMore { get; set; } - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } + public string c50 { get; set; } + public string c144 { get; set; } + } - public class HeaderSize - { - public int? width { get; set; } - public int? height { get; set; } - } + public class HeaderSize + { + public int? width { get; set; } + public int? height { get; set; } + } - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } + public class HeaderThumbs + { + public string w480 { get; set; } + public string w760 { get; set; } + } - public class List - { - public string view { get; set; } - public string avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long id { get; set; } - public string name { get; set; } - public string username { get; set; } - public bool? canLookStory { get; set; } - public bool? canCommentStory { get; set; } - public bool? hasNotViewedStory { get; set; } - public bool? isVerified { get; set; } - public bool? canPayInternal { get; set; } - public bool? hasScheduledStream { get; set; } - public bool? hasStream { get; set; } - public bool? hasStories { get; set; } - public bool? tipsEnabled { get; set; } - public bool? tipsTextEnabled { get; set; } - public int? tipsMin { get; set; } - public int? tipsMinInternal { get; set; } - public int? tipsMax { get; set; } - public bool? canEarn { get; set; } - public bool? canAddSubscriber { get; set; } - public string? subscribePrice { get; set; } - public bool? isPaywallRequired { get; set; } - public bool? unprofitable { get; set; } - public List listsStates { get; set; } - public bool? isMuted { get; set; } - public bool? isRestricted { get; set; } - public bool? canRestrict { get; set; } - public bool? subscribedBy { get; set; } - public bool? subscribedByExpire { get; set; } - public DateTime? subscribedByExpireDate { get; set; } - public bool? subscribedByAutoprolong { get; set; } - public bool? subscribedIsExpiredNow { get; set; } - public string? currentSubscribePrice { get; set; } - public bool? subscribedOn { get; set; } - public bool? subscribedOnExpiredNow { get; set; } - public string subscribedOnDuration { get; set; } - public bool? canReport { get; set; } - public bool? canReceiveChatMessage { get; set; } - public bool? hideChat { get; set; } - public DateTime? lastSeen { get; set; } - public bool? isPerformer { get; set; } - public bool? isRealPerformer { get; set; } - public SubscribedByData subscribedByData { get; set; } - public SubscribedOnData subscribedOnData { get; set; } - public bool? canTrialSend { get; set; } - public bool? isBlocked { get; set; } - public string displayName { get; set; } - public string notice { get; set; } - } + public class List + { + public string view { get; set; } + public string avatar { get; set; } + public AvatarThumbs avatarThumbs { get; set; } + public string header { get; set; } + public HeaderSize headerSize { get; set; } + public HeaderThumbs headerThumbs { get; set; } + public long id { get; set; } + public string name { get; set; } + public string username { get; set; } + public bool? canLookStory { get; set; } + public bool? canCommentStory { get; set; } + public bool? hasNotViewedStory { get; set; } + public bool? isVerified { get; set; } + public bool? canPayInternal { get; set; } + public bool? hasScheduledStream { get; set; } + public bool? hasStream { get; set; } + public bool? hasStories { get; set; } + public bool? tipsEnabled { get; set; } + public bool? tipsTextEnabled { get; set; } + public int? tipsMin { get; set; } + public int? tipsMinInternal { get; set; } + public int? tipsMax { get; set; } + public bool? canEarn { get; set; } + public bool? canAddSubscriber { get; set; } + public string? subscribePrice { get; set; } + public bool? isPaywallRequired { get; set; } + public bool? unprofitable { get; set; } + public List listsStates { get; set; } + public bool? isMuted { get; set; } + public bool? isRestricted { get; set; } + public bool? canRestrict { get; set; } + public bool? subscribedBy { get; set; } + public bool? subscribedByExpire { get; set; } + public DateTime? subscribedByExpireDate { get; set; } + public bool? subscribedByAutoprolong { get; set; } + public bool? subscribedIsExpiredNow { get; set; } + public string? currentSubscribePrice { get; set; } + public bool? subscribedOn { get; set; } + public bool? subscribedOnExpiredNow { get; set; } + public string subscribedOnDuration { get; set; } + public bool? canReport { get; set; } + public bool? canReceiveChatMessage { get; set; } + public bool? hideChat { get; set; } + public DateTime? lastSeen { get; set; } + public bool? isPerformer { get; set; } + public bool? isRealPerformer { get; set; } + public SubscribedByData subscribedByData { get; set; } + public SubscribedOnData subscribedOnData { get; set; } + public bool? canTrialSend { get; set; } + public bool? isBlocked { get; set; } + public string displayName { get; set; } + public string notice { get; set; } + } - public class ListsState - { - public object id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool? hasUser { get; set; } - public bool? canAddUser { get; set; } - } + public class ListsState + { + public object id { get; set; } + public string type { get; set; } + public string name { get; set; } + public bool? hasUser { get; set; } + public bool? canAddUser { get; set; } + } - public class Subscribe - { - public object id { get; set; } - public long? userId { get; set; } - public int? subscriberId { get; set; } - public DateTime? date { get; set; } - public int? duration { get; set; } - public DateTime? startDate { get; set; } - public DateTime? expireDate { get; set; } - public object cancelDate { get; set; } - public string? price { get; set; } - public string? regularPrice { get; set; } - public string? discount { get; set; } - public string action { get; set; } - public string type { get; set; } - public object offerStart { get; set; } - public object offerEnd { get; set; } - public bool? isCurrent { get; set; } - } + public class Subscribe + { + public object id { get; set; } + public long? userId { get; set; } + public int? subscriberId { get; set; } + public DateTime? date { get; set; } + public int? duration { get; set; } + public DateTime? startDate { get; set; } + public DateTime? expireDate { get; set; } + public object cancelDate { get; set; } + public string? price { get; set; } + public string? regularPrice { get; set; } + public string? discount { get; set; } + public string action { get; set; } + public string type { get; set; } + public object offerStart { get; set; } + public object offerEnd { get; set; } + public bool? isCurrent { get; set; } + } - public class SubscribedByData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public DateTime? renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public string status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public bool? showPostsInFeed { get; set; } - public List subscribes { get; set; } - public bool? hasActivePaidSubscriptions { get; set; } - } + public class SubscribedByData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } + public string? subscribePrice { get; set; } + public int? discountPercent { get; set; } + public int? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public DateTime? renewedAt { get; set; } + public object discountFinishedAt { get; set; } + public object discountStartedAt { get; set; } + public string status { get; set; } + public bool? isMuted { get; set; } + public string unsubscribeReason { get; set; } + public string duration { get; set; } + public bool? showPostsInFeed { get; set; } + public List subscribes { get; set; } + public bool? hasActivePaidSubscriptions { get; set; } + } - public class SubscribedOnData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public DateTime? renewedAt { get; set; } - public object discountFinishedAt { get; set; } - public object discountStartedAt { get; set; } - public object status { get; set; } - public bool? isMuted { get; set; } - public string unsubscribeReason { get; set; } - public string duration { get; set; } - public string? tipsSumm { get; set; } - public string? subscribesSumm { get; set; } - public string? messagesSumm { get; set; } - public string? postsSumm { get; set; } - public string? streamsSumm { get; set; } - public string? totalSumm { get; set; } - public List subscribes { get; set; } - public bool? hasActivePaidSubscriptions { get; set; } - } + public class SubscribedOnData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } + public string? subscribePrice { get; set; } + public int? discountPercent { get; set; } + public int? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public DateTime? renewedAt { get; set; } + public object discountFinishedAt { get; set; } + public object discountStartedAt { get; set; } + public object status { get; set; } + public bool? isMuted { get; set; } + public string unsubscribeReason { get; set; } + public string duration { get; set; } + public string? tipsSumm { get; set; } + public string? subscribesSumm { get; set; } + public string? messagesSumm { get; set; } + public string? postsSumm { get; set; } + public string? streamsSumm { get; set; } + public string? totalSumm { get; set; } + public List subscribes { get; set; } + public bool? hasActivePaidSubscriptions { get; set; } } } diff --git a/OF DL/Entities/ToggleableConfigAttribute.cs b/OF DL/Entities/ToggleableConfigAttribute.cs index ff0a4e8..62d413f 100644 --- a/OF DL/Entities/ToggleableConfigAttribute.cs +++ b/OF DL/Entities/ToggleableConfigAttribute.cs @@ -1,8 +1,6 @@ -namespace OF_DL.Entities -{ - [AttributeUsage(AttributeTargets.Property)] - internal class ToggleableConfigAttribute : Attribute - { - } +namespace OF_DL.Entities; +[AttributeUsage(AttributeTargets.Property)] +internal class ToggleableConfigAttribute : Attribute +{ } diff --git a/OF DL/Entities/User.cs b/OF DL/Entities/User.cs index 1273c2b..606f2ac 100644 --- a/OF DL/Entities/User.cs +++ b/OF DL/Entities/User.cs @@ -1,185 +1,185 @@ -namespace OF_DL.Entities +namespace OF_DL.Entities; + +public class User { - public class User + public string view { get; set; } + public string? avatar { get; set; } + public AvatarThumbs avatarThumbs { get; set; } + public string? header { get; set; } + public HeaderSize headerSize { get; set; } + public HeaderThumbs headerThumbs { get; set; } + public long? id { get; set; } + public string name { get; set; } + public string username { get; set; } + public bool? canLookStory { get; set; } + public bool? canCommentStory { get; set; } + public bool? hasNotViewedStory { get; set; } + public bool? isVerified { get; set; } + public bool? canPayInternal { get; set; } + public bool? hasScheduledStream { get; set; } + public bool? hasStream { get; set; } + public bool? hasStories { get; set; } + public bool? tipsEnabled { get; set; } + public bool? tipsTextEnabled { get; set; } + public int? tipsMin { get; set; } + public int? tipsMinInternal { get; set; } + public int? tipsMax { get; set; } + public bool? canEarn { get; set; } + public bool? canAddSubscriber { get; set; } + public string? subscribePrice { get; set; } + public string displayName { get; set; } + public string notice { get; set; } + public bool? isPaywallRequired { get; set; } + public bool? unprofitable { get; set; } + public List listsStates { get; set; } + public bool? isMuted { get; set; } + public bool? isRestricted { get; set; } + public bool? canRestrict { get; set; } + public bool? subscribedBy { get; set; } + public bool? subscribedByExpire { get; set; } + public DateTime? subscribedByExpireDate { get; set; } + public bool? subscribedByAutoprolong { get; set; } + public bool? subscribedIsExpiredNow { get; set; } + public string? currentSubscribePrice { get; set; } + public bool? subscribedOn { get; set; } + public bool? subscribedOnExpiredNow { get; set; } + public string subscribedOnDuration { get; set; } + public DateTime? joinDate { get; set; } + public bool? isReferrerAllowed { get; set; } + public string about { get; set; } + public string rawAbout { get; set; } + public object website { get; set; } + public object wishlist { get; set; } + public object location { get; set; } + public int? postsCount { get; set; } + public int? archivedPostsCount { get; set; } + public int? privateArchivedPostsCount { get; set; } + public int? photosCount { get; set; } + public int? videosCount { get; set; } + public int? audiosCount { get; set; } + public int? mediasCount { get; set; } + public DateTime? lastSeen { get; set; } + public int? favoritesCount { get; set; } + public int? favoritedCount { get; set; } + public bool? showPostsInFeed { get; set; } + public bool? canReceiveChatMessage { get; set; } + public bool? isPerformer { get; set; } + public bool? isRealPerformer { get; set; } + public bool? isSpotifyConnected { get; set; } + public int? subscribersCount { get; set; } + public bool? hasPinnedPosts { get; set; } + public bool? hasLabels { get; set; } + public bool? canChat { get; set; } + public string? callPrice { get; set; } + public bool? isPrivateRestriction { get; set; } + public bool? showSubscribersCount { get; set; } + public bool? showMediaCount { get; set; } + public SubscribedByData subscribedByData { get; set; } + public SubscribedOnData subscribedOnData { get; set; } + public bool? canPromotion { get; set; } + public bool? canCreatePromotion { get; set; } + public bool? canCreateTrial { get; set; } + public bool? isAdultContent { get; set; } + public bool? canTrialSend { get; set; } + public bool? hadEnoughLastPhotos { get; set; } + public bool? hasLinks { get; set; } + public DateTime? firstPublishedPostDate { get; set; } + public bool? isSpringConnected { get; set; } + public bool? isFriend { get; set; } + public bool? isBlocked { get; set; } + public bool? canReport { get; set; } + + public class AvatarThumbs { - public string view { get; set; } - public string? avatar { get; set; } - public AvatarThumbs avatarThumbs { get; set; } - public string? header { get; set; } - public HeaderSize headerSize { get; set; } - public HeaderThumbs headerThumbs { get; set; } - public long? id { get; set; } + public string c50 { get; set; } + public string c144 { get; set; } + } + + public class HeaderSize + { + public int? width { get; set; } + public int? height { get; set; } + } + + public class HeaderThumbs + { + public string w480 { get; set; } + public string w760 { get; set; } + } + + public class ListsState + { + public string id { get; set; } + public string type { get; set; } public string name { get; set; } - public string username { get; set; } - public bool? canLookStory { get; set; } - public bool? canCommentStory { get; set; } - public bool? hasNotViewedStory { get; set; } - public bool? isVerified { get; set; } - public bool? canPayInternal { get; set; } - public bool? hasScheduledStream { get; set; } - public bool? hasStream { get; set; } - public bool? hasStories { get; set; } - public bool? tipsEnabled { get; set; } - public bool? tipsTextEnabled { get; set; } - public int? tipsMin { get; set; } - public int? tipsMinInternal { get; set; } - public int? tipsMax { get; set; } - public bool? canEarn { get; set; } - public bool? canAddSubscriber { get; set; } + public bool hasUser { get; set; } + public bool canAddUser { get; set; } + } + + public class Subscribe + { + public long? id { get; set; } + public long? userId { get; set; } + public int? subscriberId { get; set; } + public DateTime? date { get; set; } + public int? duration { get; set; } + public DateTime? startDate { get; set; } + public DateTime? expireDate { get; set; } + public object cancelDate { get; set; } + public string? price { get; set; } + public string? regularPrice { get; set; } + public int? discount { get; set; } + public string action { get; set; } + public string type { get; set; } + public object offerStart { get; set; } + public object offerEnd { get; set; } + public bool? isCurrent { get; set; } + } + + public class SubscribedByData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } public string? subscribePrice { get; set; } - public string displayName { get; set; } - public string notice { get; set; } - public bool? isPaywallRequired { get; set; } - public bool? unprofitable { get; set; } - public List listsStates { get; set; } + public int? discountPercent { get; set; } + public int? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public object? renewedAt { get; set; } + public object? discountFinishedAt { get; set; } + public object? discountStartedAt { get; set; } + public string? status { get; set; } public bool? isMuted { get; set; } - public bool? isRestricted { get; set; } - public bool? canRestrict { get; set; } - public bool? subscribedBy { get; set; } - public bool? subscribedByExpire { get; set; } - public DateTime? subscribedByExpireDate { get; set; } - public bool? subscribedByAutoprolong { get; set; } - public bool? subscribedIsExpiredNow { get; set; } - public string? currentSubscribePrice { get; set; } - public bool? subscribedOn { get; set; } - public bool? subscribedOnExpiredNow { get; set; } - public string subscribedOnDuration { get; set; } - public DateTime? joinDate { get; set; } - public bool? isReferrerAllowed { get; set; } - public string about { get; set; } - public string rawAbout { get; set; } - public object website { get; set; } - public object wishlist { get; set; } - public object location { get; set; } - public int? postsCount { get; set; } - public int? archivedPostsCount { get; set; } - public int? privateArchivedPostsCount { get; set; } - public int? photosCount { get; set; } - public int? videosCount { get; set; } - public int? audiosCount { get; set; } - public int? mediasCount { get; set; } - public DateTime? lastSeen { get; set; } - public int? favoritesCount { get; set; } - public int? favoritedCount { get; set; } + public string? unsubscribeReason { get; set; } + public string? duration { get; set; } public bool? showPostsInFeed { get; set; } - public bool? canReceiveChatMessage { get; set; } - public bool? isPerformer { get; set; } - public bool? isRealPerformer { get; set; } - public bool? isSpotifyConnected { get; set; } - public int? subscribersCount { get; set; } - public bool? hasPinnedPosts { get; set; } - public bool? hasLabels { get; set; } - public bool? canChat { get; set; } - public string? callPrice { get; set; } - public bool? isPrivateRestriction { get; set; } - public bool? showSubscribersCount { get; set; } - public bool? showMediaCount { get; set; } - public SubscribedByData subscribedByData { get; set; } - public SubscribedOnData subscribedOnData { get; set; } - public bool? canPromotion { get; set; } - public bool? canCreatePromotion { get; set; } - public bool? canCreateTrial { get; set; } - public bool? isAdultContent { get; set; } - public bool? canTrialSend { get; set; } - public bool? hadEnoughLastPhotos { get; set; } - public bool? hasLinks { get; set; } - public DateTime? firstPublishedPostDate { get; set; } - public bool? isSpringConnected { get; set; } - public bool? isFriend { get; set; } - public bool? isBlocked { get; set; } - public bool? canReport { get; set; } - public class AvatarThumbs - { - public string c50 { get; set; } - public string c144 { get; set; } - } + public List? subscribes { get; set; } + } - public class HeaderSize - { - public int? width { get; set; } - public int? height { get; set; } - } - - public class HeaderThumbs - { - public string w480 { get; set; } - public string w760 { get; set; } - } - - public class ListsState - { - public string id { get; set; } - public string type { get; set; } - public string name { get; set; } - public bool hasUser { get; set; } - public bool canAddUser { get; set; } - } - - public class Subscribe - { - public long? id { get; set; } - public long? userId { get; set; } - public int? subscriberId { get; set; } - public DateTime? date { get; set; } - public int? duration { get; set; } - public DateTime? startDate { get; set; } - public DateTime? expireDate { get; set; } - public object cancelDate { get; set; } - public string? price { get; set; } - public string? regularPrice { get; set; } - public int? discount { get; set; } - public string action { get; set; } - public string type { get; set; } - public object offerStart { get; set; } - public object offerEnd { get; set; } - public bool? isCurrent { get; set; } - } - - public class SubscribedByData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public object? renewedAt { get; set; } - public object? discountFinishedAt { get; set; } - public object? discountStartedAt { get; set; } - public string? status { get; set; } - public bool? isMuted { get; set; } - public string? unsubscribeReason { get; set; } - public string? duration { get; set; } - public bool? showPostsInFeed { get; set; } - public List? subscribes { get; set; } - } - - public class SubscribedOnData - { - public string? price { get; set; } - public string? newPrice { get; set; } - public string? regularPrice { get; set; } - public string? subscribePrice { get; set; } - public int? discountPercent { get; set; } - public int? discountPeriod { get; set; } - public DateTime? subscribeAt { get; set; } - public DateTime? expiredAt { get; set; } - public DateTime? renewedAt { get; set; } - public object? discountFinishedAt { get; set; } - public object? discountStartedAt { get; set; } - public object? status { get; set; } - public bool? isMuted { get; set; } - public string? unsubscribeReason { get; set; } - public string? duration { get; set; } - public string? tipsSumm { get; set; } - public string? subscribesSumm { get; set; } - public string? messagesSumm { get; set; } - public string? postsSumm { get; set; } - public string? streamsSumm { get; set; } - public string? totalSumm { get; set; } - public List? subscribes { get; set; } - } + public class SubscribedOnData + { + public string? price { get; set; } + public string? newPrice { get; set; } + public string? regularPrice { get; set; } + public string? subscribePrice { get; set; } + public int? discountPercent { get; set; } + public int? discountPeriod { get; set; } + public DateTime? subscribeAt { get; set; } + public DateTime? expiredAt { get; set; } + public DateTime? renewedAt { get; set; } + public object? discountFinishedAt { get; set; } + public object? discountStartedAt { get; set; } + public object? status { get; set; } + public bool? isMuted { get; set; } + public string? unsubscribeReason { get; set; } + public string? duration { get; set; } + public string? tipsSumm { get; set; } + public string? subscribesSumm { get; set; } + public string? messagesSumm { get; set; } + public string? postsSumm { get; set; } + public string? streamsSumm { get; set; } + public string? totalSumm { get; set; } + public List? subscribes { get; set; } } } diff --git a/OF DL/Enumerations/CustomFileNameOption.cs b/OF DL/Enumerations/CustomFileNameOption.cs index 4e9659f..6656873 100644 --- a/OF DL/Enumerations/CustomFileNameOption.cs +++ b/OF DL/Enumerations/CustomFileNameOption.cs @@ -3,5 +3,5 @@ namespace OF_DL.Enumerations; public enum CustomFileNameOption { ReturnOriginal, - ReturnEmpty, + ReturnEmpty } diff --git a/OF DL/Enumerations/DownloadDateSelection.cs b/OF DL/Enumerations/DownloadDateSelection.cs index e40c38f..2f1d949 100644 --- a/OF DL/Enumerations/DownloadDateSelection.cs +++ b/OF DL/Enumerations/DownloadDateSelection.cs @@ -1,8 +1,7 @@ -namespace OF_DL.Enumerations +namespace OF_DL.Enumerations; + +public enum DownloadDateSelection { - public enum DownloadDateSelection - { - before, - after, - } + before, + after } diff --git a/OF DL/Enumerations/LoggingLevel.cs b/OF DL/Enumerations/LoggingLevel.cs index 876c527..f9c1d35 100644 --- a/OF DL/Enumerations/LoggingLevel.cs +++ b/OF DL/Enumerations/LoggingLevel.cs @@ -1,30 +1,34 @@ -namespace OF_DL.Enumerations +namespace OF_DL.Enumerations; + +public enum LoggingLevel { - public enum LoggingLevel - { - // - // Summary: - // Anything and everything you might want to know about a running block of code. - Verbose, - // - // Summary: - // Internal system events that aren't necessarily observable from the outside. - Debug, - // - // Summary: - // The lifeblood of operational intelligence - things happen. - Information, - // - // Summary: - // Service is degraded or endangered. - Warning, - // - // Summary: - // Functionality is unavailable, invariants are broken or data is lost. - Error, - // - // Summary: - // If you have a pager, it goes off when one of these occurs. - Fatal - } + // + // Summary: + // Anything and everything you might want to know about a running block of code. + Verbose, + + // + // Summary: + // Internal system events that aren't necessarily observable from the outside. + Debug, + + // + // Summary: + // The lifeblood of operational intelligence - things happen. + Information, + + // + // Summary: + // Service is degraded or endangered. + Warning, + + // + // Summary: + // Functionality is unavailable, invariants are broken or data is lost. + Error, + + // + // Summary: + // If you have a pager, it goes off when one of these occurs. + Fatal } diff --git a/OF DL/Enumerations/MediaType.cs b/OF DL/Enumerations/MediaType.cs index 38e3e81..da579c6 100644 --- a/OF DL/Enumerations/MediaType.cs +++ b/OF DL/Enumerations/MediaType.cs @@ -1,13 +1,12 @@ -namespace OF_DL.Enumerations +namespace OF_DL.Enumerations; + +public enum MediaType { - public enum MediaType - { - PaidPosts = 10, - Posts = 20, - Archived = 30, - Stories = 40, - Highlights = 50, - Messages = 60, - PaidMessages = 70 - } + PaidPosts = 10, + Posts = 20, + Archived = 30, + Stories = 40, + Highlights = 50, + Messages = 60, + PaidMessages = 70 } diff --git a/OF DL/Enumerations/VideoResolution.cs b/OF DL/Enumerations/VideoResolution.cs index 6c2f340..d07a1fd 100644 --- a/OF DL/Enumerations/VideoResolution.cs +++ b/OF DL/Enumerations/VideoResolution.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Enumerations +namespace OF_DL.Enumerations; + +public enum VideoResolution { - public enum VideoResolution - { - _240, - _720, - source - } + _240, + _720, + source } diff --git a/OF DL/Helpers/DownloadContext.cs b/OF DL/Helpers/DownloadContext.cs index cae7598..00f41f1 100644 --- a/OF DL/Helpers/DownloadContext.cs +++ b/OF DL/Helpers/DownloadContext.cs @@ -1,26 +1,25 @@ using OF_DL.Entities; using OF_DL.Services; -namespace OF_DL.Helpers -{ - internal interface IDownloadContext - { - public IFileNameFormatConfig FileNameFormatConfig { get; } - public IAPIService ApiService { get; } - public IDBService DBService { get; } - public IDownloadService DownloadService { get; } - } +namespace OF_DL.Helpers; - internal class DownloadContext( - IFileNameFormatConfig fileNameFormatConfig, - IAPIService apiService, - IDBService dBService, - IDownloadService downloadService) - : IDownloadContext - { - public IAPIService ApiService { get; } = apiService; - public IDBService DBService { get; } = dBService; - public IDownloadService DownloadService { get; } = downloadService; - public IFileNameFormatConfig FileNameFormatConfig { get; } = fileNameFormatConfig; - } +internal interface IDownloadContext +{ + public IFileNameFormatConfig FileNameFormatConfig { get; } + public IAPIService ApiService { get; } + public IDBService DBService { get; } + public IDownloadService DownloadService { get; } +} + +internal class DownloadContext( + IFileNameFormatConfig fileNameFormatConfig, + IAPIService apiService, + IDBService dBService, + IDownloadService downloadService) + : IDownloadContext +{ + public IAPIService ApiService { get; } = apiService; + public IDBService DBService { get; } = dBService; + public IDownloadService DownloadService { get; } = downloadService; + public IFileNameFormatConfig FileNameFormatConfig { get; } = fileNameFormatConfig; } diff --git a/OF DL/Helpers/IFileNameHelper.cs b/OF DL/Helpers/IFileNameHelper.cs index a3cc701..6454742 100644 --- a/OF DL/Helpers/IFileNameHelper.cs +++ b/OF DL/Helpers/IFileNameHelper.cs @@ -1,8 +1,9 @@ -namespace OF_DL.Helpers +namespace OF_DL.Helpers; + +public interface IFileNameHelper { - public interface IFileNameHelper - { - Task BuildFilename(string fileFormat, Dictionary values); - Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, string username, Dictionary users = null); - } + Task BuildFilename(string fileFormat, Dictionary values); + + Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, + string username, Dictionary users = null); } diff --git a/OF DL/Helpers/VersionHelper.cs b/OF DL/Helpers/VersionHelper.cs index dc2b163..8d6913a 100644 --- a/OF DL/Helpers/VersionHelper.cs +++ b/OF DL/Helpers/VersionHelper.cs @@ -6,15 +6,15 @@ namespace OF_DL.Helpers; public static class VersionHelper { - private static readonly HttpClient httpClient = new HttpClient(); private const string url = "https://git.ofdl.tools/api/v1/repos/sim0n00ps/OF-DL/releases/latest"; + private static readonly HttpClient httpClient = new(); public static async Task GetLatestReleaseTag(CancellationToken cancellationToken = default) { Log.Debug("Calling GetLatestReleaseTag"); try { - var response = await httpClient.GetAsync(url, cancellationToken); + HttpResponseMessage response = await httpClient.GetAsync(url, cancellationToken); if (!response.IsSuccessStatusCode) { @@ -22,12 +22,13 @@ public static class VersionHelper return null; } - var body = await response.Content.ReadAsStringAsync(); + string body = await response.Content.ReadAsStringAsync(); Log.Debug("GetLatestReleaseTag API Response: "); Log.Debug(body); - var versionCheckResponse = JsonConvert.DeserializeObject(body); + LatestReleaseAPIResponse? versionCheckResponse = + JsonConvert.DeserializeObject(body); if (versionCheckResponse == null || versionCheckResponse.TagName == "") { @@ -48,10 +49,13 @@ public static class VersionHelper if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } } diff --git a/OF DL/OF DL.csproj b/OF DL/OF DL.csproj index be35c42..a57dfe5 100644 --- a/OF DL/OF DL.csproj +++ b/OF DL/OF DL.csproj @@ -10,23 +10,23 @@ - + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/OF DL/Program.cs b/OF DL/Program.cs index 54d6934..754e8c4 100644 --- a/OF DL/Program.cs +++ b/OF DL/Program.cs @@ -1,5 +1,11 @@ +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OF_DL.CLI; using OF_DL.Entities; using OF_DL.Entities.Archived; using OF_DL.Entities.Messages; @@ -7,88 +13,90 @@ using OF_DL.Entities.Post; using OF_DL.Entities.Purchased; using OF_DL.Entities.Streams; using OF_DL.Enumerations; -using OF_DL.CLI; using OF_DL.Helpers; using OF_DL.Services; using Serilog; using Spectre.Console; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using static OF_DL.Entities.Messages.Messages; -using Microsoft.Extensions.DependencyInjection; using Constants = OF_DL.Widevine.Constants; namespace OF_DL; public class Program(IServiceProvider serviceProvider) { - public static List paid_post_ids = new(); + public static List paid_post_ids = new(); - private static bool clientIdBlobMissing = false; - private static bool devicePrivateKeyMissing = false; + private static bool clientIdBlobMissing; + private static bool devicePrivateKeyMissing; private async Task LoadAuthFromBrowser() - { - var authService = serviceProvider.GetRequiredService(); - bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; + { + IAuthService authService = serviceProvider.GetRequiredService(); + bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; - // Show initial message - AnsiConsole.MarkupLine($"[yellow]Downloading dependencies. Please wait ...[/]"); + // Show initial message + AnsiConsole.MarkupLine("[yellow]Downloading dependencies. Please wait ...[/]"); - // Show instructions based on the environment - await Task.Delay(5000); - if (runningInDocker) - { - AnsiConsole.MarkupLine( - "[yellow]In your web browser, navigate to the port forwarded from your docker container.[/]"); - AnsiConsole.MarkupLine( - "[yellow]For instance, if your docker run command included \"-p 8080:8080\", open your web browser to \"http://localhost:8080\".[/]"); - AnsiConsole.MarkupLine("[yellow]Once on that webpage, please use it to log in to your OF account. Do not navigate away from the page.[/]"); - } - else - { - AnsiConsole.MarkupLine($"[yellow]In the new window that has opened, please log in to your OF account. Do not close the window or tab. Do not navigate away from the page.[/]\n"); - AnsiConsole.MarkupLine($"[yellow]Note: Some users have reported that \"Sign in with Google\" has not been working with the new authentication method.[/]"); - AnsiConsole.MarkupLine($"[yellow]If you use this method or encounter other issues while logging in, use one of the legacy authentication methods documented here:[/]"); - AnsiConsole.MarkupLine($"[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); - } + // Show instructions based on the environment + await Task.Delay(5000); + if (runningInDocker) + { + AnsiConsole.MarkupLine( + "[yellow]In your web browser, navigate to the port forwarded from your docker container.[/]"); + AnsiConsole.MarkupLine( + "[yellow]For instance, if your docker run command included \"-p 8080:8080\", open your web browser to \"http://localhost:8080\".[/]"); + AnsiConsole.MarkupLine( + "[yellow]Once on that webpage, please use it to log in to your OF account. Do not navigate away from the page.[/]"); + } + else + { + AnsiConsole.MarkupLine( + "[yellow]In the new window that has opened, please log in to your OF account. Do not close the window or tab. Do not navigate away from the page.[/]\n"); + AnsiConsole.MarkupLine( + "[yellow]Note: Some users have reported that \"Sign in with Google\" has not been working with the new authentication method.[/]"); + AnsiConsole.MarkupLine( + "[yellow]If you use this method or encounter other issues while logging in, use one of the legacy authentication methods documented here:[/]"); + AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); + } - // Load auth from browser using the service - bool success = await authService.LoadFromBrowserAsync(); + // Load auth from browser using the service + bool success = await authService.LoadFromBrowserAsync(); - if (!success || authService.CurrentAuth == null) - { - AnsiConsole.MarkupLine($"\n[red]Authentication failed. Be sure to log into to OF using the new window that opened automatically.[/]"); - AnsiConsole.MarkupLine($"[red]The window will close automatically when the authentication process is finished.[/]"); - AnsiConsole.MarkupLine($"[red]If the problem persists, you may want to try using a legacy authentication method documented here:[/]\n"); - AnsiConsole.MarkupLine($"[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - Log.Error("auth invalid after attempt to get auth from browser"); + if (!success || authService.CurrentAuth == null) + { + AnsiConsole.MarkupLine( + "\n[red]Authentication failed. Be sure to log into to OF using the new window that opened automatically.[/]"); + AnsiConsole.MarkupLine( + "[red]The window will close automatically when the authentication process is finished.[/]"); + AnsiConsole.MarkupLine( + "[red]If the problem persists, you may want to try using a legacy authentication method documented here:[/]\n"); + AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); + Log.Error("auth invalid after attempt to get auth from browser"); - Environment.Exit(2); - } + Environment.Exit(2); + } - await authService.SaveToFileAsync(); - } + await authService.SaveToFileAsync(); + } - public static async Task Main(string[] args) - { - AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red)); - AnsiConsole.Markup("Documentation: [link]https://docs.ofdl.tools/[/]\n"); - AnsiConsole.Markup("Discord server: [link]https://discord.com/invite/6bUW8EJ53j[/]\n\n"); + public static async Task Main(string[] args) + { + AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red)); + AnsiConsole.Markup("Documentation: [link]https://docs.ofdl.tools/[/]\n"); + AnsiConsole.Markup("Discord server: [link]https://discord.com/invite/6bUW8EJ53j[/]\n\n"); ServiceCollection services = await ConfigureServices(args); - var serviceProvider = services.BuildServiceProvider(); + ServiceProvider serviceProvider = services.BuildServiceProvider(); - // Get the Program instance and run - var program = serviceProvider.GetRequiredService(); - await program.RunAsync(); - } + // Get the Program instance and run + Program program = serviceProvider.GetRequiredService(); + await program.RunAsync(); + } - private static async Task ConfigureServices(string[] args) - { + private static async Task ConfigureServices(string[] args) + { // Set up dependency injection with LoggingService and ConfigService ServiceCollection services = new(); services.AddSingleton(); @@ -101,12 +109,13 @@ public class Program(IServiceProvider serviceProvider) if (!await configService.LoadConfigurationAsync(args)) { - AnsiConsole.MarkupLine($"\n[red]config.conf is not valid, check your syntax![/]\n"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); + AnsiConsole.MarkupLine("\n[red]config.conf is not valid, check your syntax![/]\n"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); if (!configService.IsCliNonInteractive) { Console.ReadKey(); } + Environment.Exit(3); } @@ -114,388 +123,422 @@ public class Program(IServiceProvider serviceProvider) // Set up full dependency injection with loaded config services = []; - services.AddSingleton(loggingService); - services.AddSingleton(configService); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(loggingService); + services.AddSingleton(configService); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); return services; } - private async Task RunAsync() - { + private async Task RunAsync() + { IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); IAPIService apiService = serviceProvider.GetRequiredService(); - try - { - var os = Environment.OSVersion; + try + { + OperatingSystem os = Environment.OSVersion; - Log.Debug($"Operating system information: {os.VersionString}"); + Log.Debug($"Operating system information: {os.VersionString}"); - if (os.Platform == PlatformID.Win32NT) - { - // check if this is windows 10+ - if (os.Version.Major < 10) - { - Console.Write("This appears to be running on an older version of Windows which is not supported.\n\n"); - Console.Write("OF-DL requires Windows 10 or higher when being run on Windows. Your reported version is: {0}\n\n", os.VersionString); - Console.Write("Press any key to continue.\n"); - Log.Error("Windows version prior to 10.x: {0}", os.VersionString); + if (os.Platform == PlatformID.Win32NT) + { + // check if this is windows 10+ + if (os.Version.Major < 10) + { + Console.Write( + "This appears to be running on an older version of Windows which is not supported.\n\n"); + Console.Write( + "OF-DL requires Windows 10 or higher when being run on Windows. Your reported version is: {0}\n\n", + os.VersionString); + Console.Write("Press any key to continue.\n"); + Log.Error("Windows version prior to 10.x: {0}", os.VersionString); - if (!configService.CurrentConfig.NonInteractiveMode) - { - Console.ReadKey(); - } - Environment.Exit(1); - } - else - { - AnsiConsole.Markup("[green]Valid version of Windows found.\n[/]"); - } - } + if (!configService.CurrentConfig.NonInteractiveMode) + { + Console.ReadKey(); + } + + Environment.Exit(1); + } + else + { + AnsiConsole.Markup("[green]Valid version of Windows found.\n[/]"); + } + } try - { - // Only run the version check if not in DEBUG mode - #if !DEBUG - Version localVersion = Assembly.GetEntryAssembly()?.GetName().Version; //Only tested with numeric values. + { + // Only run the version check if not in DEBUG mode +#if !DEBUG + Version localVersion = + Assembly.GetEntryAssembly()?.GetName().Version; //Only tested with numeric values. - // Create a cancellation token with 30 second timeout - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - String? latestReleaseTag = null; + // Create a cancellation token with 30 second timeout + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + string? latestReleaseTag = null; - try - { - latestReleaseTag = await VersionHelper.GetLatestReleaseTag(cts.Token); - } - catch (OperationCanceledException) - { - AnsiConsole.Markup("[yellow]Version check timed out after 30 seconds.\n[/]"); - Log.Warning("Version check timed out after 30 seconds"); - latestReleaseTag = null; - } + try + { + latestReleaseTag = await VersionHelper.GetLatestReleaseTag(cts.Token); + } + catch (OperationCanceledException) + { + AnsiConsole.Markup("[yellow]Version check timed out after 30 seconds.\n[/]"); + Log.Warning("Version check timed out after 30 seconds"); + latestReleaseTag = null; + } - if (latestReleaseTag == null) - { - AnsiConsole.Markup("[yellow]Failed to verify that OF-DL is up-to-date.\n[/]"); - Log.Error("Failed to get the latest release tag."); - } - else - { - Version latestGiteaRelease = new Version(latestReleaseTag.Replace("OFDLV", "")); + if (latestReleaseTag == null) + { + AnsiConsole.Markup("[yellow]Failed to verify that OF-DL is up-to-date.\n[/]"); + Log.Error("Failed to get the latest release tag."); + } + else + { + Version latestGiteaRelease = new(latestReleaseTag.Replace("OFDLV", "")); - // Compare the Versions - int versionComparison = localVersion.CompareTo(latestGiteaRelease); - if (versionComparison < 0) - { - // The version on GitHub is more up to date than this local release. - AnsiConsole.Markup("[red]You are running OF-DL version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]"); - AnsiConsole.Markup("[red]Please update to the current release, " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}: [link=https://git.ofdl.tools/sim0n00ps/OF-DL/releases]https://git.ofdl.tools/sim0n00ps/OF-DL/releases[/]\n[/]"); - Log.Debug("Detected outdated client running version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}"); - Log.Debug("Latest release version " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}"); - } - else - { - // This local version is greater than the release version on GitHub. - AnsiConsole.Markup("[green]You are running OF-DL version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]"); - AnsiConsole.Markup("[green]Latest Release version: " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}\n[/]"); - Log.Debug("Detected client running version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}"); - Log.Debug("Latest release version " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}"); - } - } + // Compare the Versions + int versionComparison = localVersion.CompareTo(latestGiteaRelease); + if (versionComparison < 0) + { + // The version on GitHub is more up to date than this local release. + AnsiConsole.Markup("[red]You are running OF-DL version " + + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]"); + AnsiConsole.Markup("[red]Please update to the current release, " + + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}: [link=https://git.ofdl.tools/sim0n00ps/OF-DL/releases]https://git.ofdl.tools/sim0n00ps/OF-DL/releases[/]\n[/]"); + Log.Debug("Detected outdated client running version " + + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}"); + Log.Debug("Latest release version " + + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}"); + } + else + { + // This local version is greater than the release version on GitHub. + AnsiConsole.Markup("[green]You are running OF-DL version " + + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]"); + AnsiConsole.Markup("[green]Latest Release version: " + + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}\n[/]"); + Log.Debug("Detected client running version " + + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}"); + Log.Debug("Latest release version " + + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}"); + } + } - #else +#else AnsiConsole.Markup("[yellow]Running in Debug/Local mode. Version check skipped.\n[/]"); Log.Debug("Running in Debug/Local mode. Version check skipped."); - #endif - } - catch (Exception e) - { - AnsiConsole.Markup("[red]Error checking latest release on GitHub:\n[/]"); - Console.WriteLine(e); - Log.Error("Error checking latest release on GitHub.", e.Message); - } +#endif + } + catch (Exception e) + { + AnsiConsole.Markup("[red]Error checking latest release on GitHub:\n[/]"); + Console.WriteLine(e); + Log.Error("Error checking latest release on GitHub.", e.Message); + } if (await authService.LoadFromFileAsync()) - { - AnsiConsole.Markup("[green]auth.json located successfully!\n[/]"); - } - else if (File.Exists("auth.json")) - { - // File exists but failed to load - Log.Information("Auth file found but could not be deserialized"); - if (!configService.CurrentConfig!.DisableBrowserAuth) - { - Log.Debug("Deleting auth.json"); - File.Delete("auth.json"); - } + { + AnsiConsole.Markup("[green]auth.json located successfully!\n[/]"); + } + else if (File.Exists("auth.json")) + { + // File exists but failed to load + Log.Information("Auth file found but could not be deserialized"); + if (!configService.CurrentConfig!.DisableBrowserAuth) + { + Log.Debug("Deleting auth.json"); + File.Delete("auth.json"); + } - if (configService.CurrentConfig.NonInteractiveMode) - { - AnsiConsole.MarkupLine($"\n[red]auth.json has invalid JSON syntax. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n"); - AnsiConsole.MarkupLine($"[red]You may also want to try using the browser extension which is documented here:[/]\n"); - AnsiConsole.MarkupLine($"[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); + if (configService.CurrentConfig.NonInteractiveMode) + { + AnsiConsole.MarkupLine( + "\n[red]auth.json has invalid JSON syntax. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n"); + AnsiConsole.MarkupLine( + "[red]You may also want to try using the browser extension which is documented here:[/]\n"); + AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - Console.ReadKey(); - Environment.Exit(2); - } + Console.ReadKey(); + Environment.Exit(2); + } - if (!configService.CurrentConfig!.DisableBrowserAuth) - { - await LoadAuthFromBrowser(); - } - else - { - AnsiConsole.MarkupLine($"\n[red]auth.json is missing. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n"); - AnsiConsole.MarkupLine($"[red]You may also want to try using the browser extension which is documented here:[/]\n"); - AnsiConsole.MarkupLine($"[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - - Console.ReadKey(); - Environment.Exit(2); - } - } - else - { - if (configService.CurrentConfig.NonInteractiveMode) - { - AnsiConsole.MarkupLine($"\n[red]auth.json is missing. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n"); - AnsiConsole.MarkupLine($"[red]You may also want to try using the browser extension which is documented here:[/]\n"); - AnsiConsole.MarkupLine($"[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - - Console.ReadKey(); - Environment.Exit(2); - } - if (!configService.CurrentConfig!.DisableBrowserAuth) { await LoadAuthFromBrowser(); } else { - AnsiConsole.MarkupLine($"\n[red]auth.json is missing. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n"); - AnsiConsole.MarkupLine($"[red]You may also want to try using the browser extension which is documented here:[/]\n"); - AnsiConsole.MarkupLine($"[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); + AnsiConsole.MarkupLine( + "\n[red]auth.json is missing. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n"); + AnsiConsole.MarkupLine( + "[red]You may also want to try using the browser extension which is documented here:[/]\n"); + AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); Console.ReadKey(); Environment.Exit(2); } - } + } + else + { + if (configService.CurrentConfig.NonInteractiveMode) + { + AnsiConsole.MarkupLine( + "\n[red]auth.json is missing. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n"); + AnsiConsole.MarkupLine( + "[red]You may also want to try using the browser extension which is documented here:[/]\n"); + AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - //Added to stop cookie being filled with un-needed headers - ValidateCookieString(authService.CurrentAuth!); + Console.ReadKey(); + Environment.Exit(2); + } - if (File.Exists("rules.json")) - { - AnsiConsole.Markup("[green]rules.json located successfully!\n[/]"); - try - { - JsonConvert.DeserializeObject(File.ReadAllText("rules.json")); - Log.Debug($"Rules.json: "); - Log.Debug(JsonConvert.SerializeObject(File.ReadAllText("rules.json"), Formatting.Indented)); - } - catch (Exception e) - { - Console.WriteLine(e); - AnsiConsole.MarkupLine($"\n[red]rules.json is not valid, check your JSON syntax![/]\n"); - AnsiConsole.MarkupLine($"[red]Please ensure you are using the latest version of the software.[/]\n"); - AnsiConsole.MarkupLine($"[red]Press any key to exit.[/]"); - Log.Error("rules.json processing failed.", e.Message); + if (!configService.CurrentConfig!.DisableBrowserAuth) + { + await LoadAuthFromBrowser(); + } + else + { + AnsiConsole.MarkupLine( + "\n[red]auth.json is missing. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n"); + AnsiConsole.MarkupLine( + "[red]You may also want to try using the browser extension which is documented here:[/]\n"); + AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); - if (!configService.CurrentConfig.NonInteractiveMode) - { - Console.ReadKey(); - } - Environment.Exit(2); - } - } + Console.ReadKey(); + Environment.Exit(2); + } + } - if(configService.CurrentConfig.NonInteractiveMode) - { - // CLI argument overrides configuration - configService.CurrentConfig!.NonInteractiveMode = true; - Log.Debug("NonInteractiveMode = true"); - } + //Added to stop cookie being filled with un-needed headers + ValidateCookieString(authService.CurrentAuth!); - if(configService.CurrentConfig!.NonInteractiveMode) - { - configService.CurrentConfig.NonInteractiveMode = true; // If it was set in the config, reset the cli value so exception handling works - Log.Debug("NonInteractiveMode = true (set via config)"); - } + if (File.Exists("rules.json")) + { + AnsiConsole.Markup("[green]rules.json located successfully!\n[/]"); + try + { + JsonConvert.DeserializeObject(File.ReadAllText("rules.json")); + Log.Debug("Rules.json: "); + Log.Debug(JsonConvert.SerializeObject(File.ReadAllText("rules.json"), Formatting.Indented)); + } + catch (Exception e) + { + Console.WriteLine(e); + AnsiConsole.MarkupLine("\n[red]rules.json is not valid, check your JSON syntax![/]\n"); + AnsiConsole.MarkupLine("[red]Please ensure you are using the latest version of the software.[/]\n"); + AnsiConsole.MarkupLine("[red]Press any key to exit.[/]"); + Log.Error("rules.json processing failed.", e.Message); - var ffmpegFound = false; - var pathAutoDetected = false; - if (!string.IsNullOrEmpty(configService.CurrentConfig!.FFmpegPath) && ValidateFilePath(configService.CurrentConfig.FFmpegPath)) - { - // FFmpeg path is set in config.json and is valid - ffmpegFound = true; - Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); - Log.Debug("FFMPEG path set in config.conf"); - } - else if (!string.IsNullOrEmpty(authService.CurrentAuth!.FFMPEG_PATH) && ValidateFilePath(authService.CurrentAuth.FFMPEG_PATH)) - { - // FFmpeg path is set in auth.json and is valid (config.conf takes precedence and auth.json is only available for backward compatibility) - ffmpegFound = true; - configService.CurrentConfig.FFmpegPath = authService.CurrentAuth.FFMPEG_PATH; - Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); - Log.Debug("FFMPEG path set in auth.json"); - } - else if (string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath)) - { - // FFmpeg path is not set in config.conf, so we will try to locate it in the PATH or current directory - var ffmpegPath = GetFullPath("ffmpeg"); - if (ffmpegPath != null) - { - // FFmpeg is found in the PATH or current directory - ffmpegFound = true; - pathAutoDetected = true; - configService.CurrentConfig.FFmpegPath = ffmpegPath; - Log.Debug($"FFMPEG found: {ffmpegPath}"); - Log.Debug("FFMPEG path found via PATH or current directory"); - } - else - { - // FFmpeg is not found in the PATH or current directory, so we will try to locate the windows executable - ffmpegPath = GetFullPath("ffmpeg.exe"); - if (ffmpegPath != null) - { - // FFmpeg windows executable is found in the PATH or current directory - ffmpegFound = true; - pathAutoDetected = true; - configService.CurrentConfig.FFmpegPath = ffmpegPath; - Log.Debug($"FFMPEG found: {ffmpegPath}"); - Log.Debug("FFMPEG path found in windows excutable directory"); - } - } - } + if (!configService.CurrentConfig.NonInteractiveMode) + { + Console.ReadKey(); + } - if (ffmpegFound) - { - if (pathAutoDetected) - { - AnsiConsole.Markup($"[green]FFmpeg located successfully. Path auto-detected: {configService.CurrentConfig.FFmpegPath}\n[/]"); - } - else - { - AnsiConsole.Markup($"[green]FFmpeg located successfully\n[/]"); - } + Environment.Exit(2); + } + } - // Escape backslashes in the path for Windows - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && configService.CurrentConfig.FFmpegPath!.Contains(@":\") && !configService.CurrentConfig.FFmpegPath.Contains(@":\\")) - { - configService.CurrentConfig.FFmpegPath = configService.CurrentConfig.FFmpegPath.Replace(@"\", @"\\"); - } + if (configService.CurrentConfig.NonInteractiveMode) + { + // CLI argument overrides configuration + configService.CurrentConfig!.NonInteractiveMode = true; + Log.Debug("NonInteractiveMode = true"); + } - // Get FFmpeg version - try - { - var processStartInfo = new System.Diagnostics.ProcessStartInfo - { - FileName = configService.CurrentConfig.FFmpegPath, - Arguments = "-version", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; + if (configService.CurrentConfig!.NonInteractiveMode) + { + configService.CurrentConfig.NonInteractiveMode = + true; // If it was set in the config, reset the cli value so exception handling works + Log.Debug("NonInteractiveMode = true (set via config)"); + } - using (var process = System.Diagnostics.Process.Start(processStartInfo)) - { - if (process != null) - { - string output = await process.StandardOutput.ReadToEndAsync(); - await process.WaitForExitAsync(); + bool ffmpegFound = false; + bool pathAutoDetected = false; + if (!string.IsNullOrEmpty(configService.CurrentConfig!.FFmpegPath) && + ValidateFilePath(configService.CurrentConfig.FFmpegPath)) + { + // FFmpeg path is set in config.json and is valid + ffmpegFound = true; + Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); + Log.Debug("FFMPEG path set in config.conf"); + } + else if (!string.IsNullOrEmpty(authService.CurrentAuth!.FFMPEG_PATH) && + ValidateFilePath(authService.CurrentAuth.FFMPEG_PATH)) + { + // FFmpeg path is set in auth.json and is valid (config.conf takes precedence and auth.json is only available for backward compatibility) + ffmpegFound = true; + configService.CurrentConfig.FFmpegPath = authService.CurrentAuth.FFMPEG_PATH; + Log.Debug($"FFMPEG found: {configService.CurrentConfig.FFmpegPath}"); + Log.Debug("FFMPEG path set in auth.json"); + } + else if (string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath)) + { + // FFmpeg path is not set in config.conf, so we will try to locate it in the PATH or current directory + string? ffmpegPath = GetFullPath("ffmpeg"); + if (ffmpegPath != null) + { + // FFmpeg is found in the PATH or current directory + ffmpegFound = true; + pathAutoDetected = true; + configService.CurrentConfig.FFmpegPath = ffmpegPath; + Log.Debug($"FFMPEG found: {ffmpegPath}"); + Log.Debug("FFMPEG path found via PATH or current directory"); + } + else + { + // FFmpeg is not found in the PATH or current directory, so we will try to locate the windows executable + ffmpegPath = GetFullPath("ffmpeg.exe"); + if (ffmpegPath != null) + { + // FFmpeg windows executable is found in the PATH or current directory + ffmpegFound = true; + pathAutoDetected = true; + configService.CurrentConfig.FFmpegPath = ffmpegPath; + Log.Debug($"FFMPEG found: {ffmpegPath}"); + Log.Debug("FFMPEG path found in windows excutable directory"); + } + } + } - // Log full output - Log.Information("FFmpeg version output:\n{Output}", output); + if (ffmpegFound) + { + if (pathAutoDetected) + { + AnsiConsole.Markup( + $"[green]FFmpeg located successfully. Path auto-detected: {configService.CurrentConfig.FFmpegPath}\n[/]"); + } + else + { + AnsiConsole.Markup("[green]FFmpeg located successfully\n[/]"); + } - // Parse first line for console output - string firstLine = output.Split('\n')[0].Trim(); - if (firstLine.StartsWith("ffmpeg version")) - { - // Extract version string (text between "ffmpeg version " and " Copyright") - int versionStart = "ffmpeg version ".Length; - int copyrightIndex = firstLine.IndexOf(" Copyright"); - if (copyrightIndex > versionStart) - { - string version = firstLine.Substring(versionStart, copyrightIndex - versionStart); - AnsiConsole.Markup($"[green]ffmpeg version detected as {version}[/]\n"); - } - else - { - // Fallback if Copyright not found - string version = firstLine.Substring(versionStart); - AnsiConsole.Markup($"[green]ffmpeg version detected as {version}[/]\n"); - } - } - else - { - AnsiConsole.Markup($"[yellow]ffmpeg version could not be parsed[/]\n"); - } - } - } - } - catch (Exception ex) - { - Log.Warning(ex, "Failed to get FFmpeg version"); - AnsiConsole.Markup($"[yellow]Could not retrieve ffmpeg version[/]\n"); - } - } - else - { - AnsiConsole.Markup("[red]Cannot locate FFmpeg; please modify config.conf with the correct path. Press any key to exit.[/]"); - Log.Error($"Cannot locate FFmpeg with path: {configService.CurrentConfig.FFmpegPath}"); - if (!configService.CurrentConfig.NonInteractiveMode) - { - Console.ReadKey(); - } - Environment.Exit(4); - } + // Escape backslashes in the path for Windows + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + configService.CurrentConfig.FFmpegPath!.Contains(@":\") && + !configService.CurrentConfig.FFmpegPath.Contains(@":\\")) + { + configService.CurrentConfig.FFmpegPath = + configService.CurrentConfig.FFmpegPath.Replace(@"\", @"\\"); + } - if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_client_id_blob"))) - { - clientIdBlobMissing = true; - Log.Debug("clientIdBlobMissing missing"); - } - else - { - AnsiConsole.Markup($"[green]device_client_id_blob located successfully![/]\n"); - Log.Debug("clientIdBlobMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_client_id_blob"))); - } + // Get FFmpeg version + try + { + ProcessStartInfo processStartInfo = new() + { + FileName = configService.CurrentConfig.FFmpegPath, + Arguments = "-version", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; - if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_private_key"))) - { - devicePrivateKeyMissing = true; - Log.Debug("devicePrivateKeyMissing missing"); - } - else - { - AnsiConsole.Markup($"[green]device_private_key located successfully![/]\n"); - Log.Debug("devicePrivateKeyMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_private_key"))); - } + using (Process? process = Process.Start(processStartInfo)) + { + if (process != null) + { + string output = await process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync(); - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - AnsiConsole.Markup("[yellow]device_client_id_blob and/or device_private_key missing, https://ofdl.tools/ or https://cdrm-project.com/ will be used instead for DRM protected videos\n[/]"); - } + // Log full output + Log.Information("FFmpeg version output:\n{Output}", output); - Entities.User? validate = await apiService.GetUserInfo($"/users/me"); - if (validate == null || (validate?.name == null && validate?.username == null)) - { - Log.Error("Auth failed"); + // Parse first line for console output + string firstLine = output.Split('\n')[0].Trim(); + if (firstLine.StartsWith("ffmpeg version")) + { + // Extract version string (text between "ffmpeg version " and " Copyright") + int versionStart = "ffmpeg version ".Length; + int copyrightIndex = firstLine.IndexOf(" Copyright"); + if (copyrightIndex > versionStart) + { + string version = firstLine.Substring(versionStart, copyrightIndex - versionStart); + AnsiConsole.Markup($"[green]ffmpeg version detected as {version}[/]\n"); + } + else + { + // Fallback if Copyright not found + string version = firstLine.Substring(versionStart); + AnsiConsole.Markup($"[green]ffmpeg version detected as {version}[/]\n"); + } + } + else + { + AnsiConsole.Markup("[yellow]ffmpeg version could not be parsed[/]\n"); + } + } + } + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to get FFmpeg version"); + AnsiConsole.Markup("[yellow]Could not retrieve ffmpeg version[/]\n"); + } + } + else + { + AnsiConsole.Markup( + "[red]Cannot locate FFmpeg; please modify config.conf with the correct path. Press any key to exit.[/]"); + Log.Error($"Cannot locate FFmpeg with path: {configService.CurrentConfig.FFmpegPath}"); + if (!configService.CurrentConfig.NonInteractiveMode) + { + Console.ReadKey(); + } - authService.CurrentAuth = null; + Environment.Exit(4); + } + + if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_client_id_blob"))) + { + clientIdBlobMissing = true; + Log.Debug("clientIdBlobMissing missing"); + } + else + { + AnsiConsole.Markup("[green]device_client_id_blob located successfully![/]\n"); + Log.Debug("clientIdBlobMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, + Constants.DEVICE_NAME, "device_client_id_blob"))); + } + + if (!File.Exists(Path.Join(Constants.DEVICES_FOLDER, Constants.DEVICE_NAME, "device_private_key"))) + { + devicePrivateKeyMissing = true; + Log.Debug("devicePrivateKeyMissing missing"); + } + else + { + AnsiConsole.Markup("[green]device_private_key located successfully![/]\n"); + Log.Debug("devicePrivateKeyMissing found: " + File.Exists(Path.Join(Constants.DEVICES_FOLDER, + Constants.DEVICE_NAME, "device_private_key"))); + } + + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + AnsiConsole.Markup( + "[yellow]device_client_id_blob and/or device_private_key missing, https://ofdl.tools/ or https://cdrm-project.com/ will be used instead for DRM protected videos\n[/]"); + } + + User? validate = await apiService.GetUserInfo("/users/me"); + if (validate == null || (validate?.name == null && validate?.username == null)) + { + Log.Error("Auth failed"); + + authService.CurrentAuth = null; if (!configService.CurrentConfig!.DisableBrowserAuth) { if (File.Exists("auth.json")) @@ -504,1871 +547,2107 @@ public class Program(IServiceProvider serviceProvider) } } - if (!configService.CurrentConfig.NonInteractiveMode && !configService.CurrentConfig!.DisableBrowserAuth) - { + if (!configService.CurrentConfig.NonInteractiveMode && !configService.CurrentConfig!.DisableBrowserAuth) + { await LoadAuthFromBrowser(); } - if (authService.CurrentAuth == null) - { - AnsiConsole.MarkupLine($"\n[red]Auth failed. Please try again or use other authentication methods detailed here:[/]\n"); - AnsiConsole.MarkupLine($"[link]https://docs.ofdl.tools/config/auth[/]\n"); - Console.ReadKey(); - Environment.Exit(2); - } - } + if (authService.CurrentAuth == null) + { + AnsiConsole.MarkupLine( + "\n[red]Auth failed. Please try again or use other authentication methods detailed here:[/]\n"); + AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth[/]\n"); + Console.ReadKey(); + Environment.Exit(2); + } + } - AnsiConsole.Markup($"[green]Logged In successfully as {validate.name} {validate.username}\n[/]"); - await DownloadAllData(); - } - catch (Exception ex) - { - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - if (ex.InnerException != null) - { - Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - } - Console.WriteLine("\nPress any key to exit."); - if (!configService.CurrentConfig.NonInteractiveMode) - { - Console.ReadKey(); - } - Environment.Exit(5); - } - } - - private async Task DownloadAllData() - { - IDBService dbService = serviceProvider.GetRequiredService(); - IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - Entities.Config Config = configService.CurrentConfig!; - - Log.Debug("Calling DownloadAllData"); - - do - { - DateTime startTime = DateTime.Now; - Dictionary users = new(); - Dictionary activeSubs = await apiService.GetActiveSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions); - - Log.Debug("Subscriptions: "); - - foreach (KeyValuePair activeSub in activeSubs) - { - if (!users.ContainsKey(activeSub.Key)) - { - users.Add(activeSub.Key, activeSub.Value); - Log.Debug($"Name: {activeSub.Key} ID: {activeSub.Value}"); - } - } - if (Config!.IncludeExpiredSubscriptions) - { - Log.Debug("Inactive Subscriptions: "); - - Dictionary expiredSubs = await apiService.GetExpiredSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions); - foreach (KeyValuePair expiredSub in expiredSubs) - { - if (!users.ContainsKey(expiredSub.Key)) - { - users.Add(expiredSub.Key, expiredSub.Value); - Log.Debug($"Name: {expiredSub.Key} ID: {expiredSub.Value}"); - } - } - } - - Dictionary lists = await apiService.GetLists("/lists"); - - // Remove users from the list if they are in the ignored list - if (!string.IsNullOrEmpty(Config.IgnoredUsersListName)) - { - if (!lists.TryGetValue(Config.IgnoredUsersListName, out var ignoredUsersListId)) - { - AnsiConsole.Markup($"[red]Ignored users list '{Config.IgnoredUsersListName}' not found\n[/]"); - Log.Error($"Ignored users list '{Config.IgnoredUsersListName}' not found"); - } - else - { - var ignoredUsernames = await apiService.GetListUsers($"/lists/{ignoredUsersListId}/users") ?? []; - users = users.Where(x => !ignoredUsernames.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value); - } - } - - await dbService.CreateUsersDB(users); - KeyValuePair> hasSelectedUsersKVP; - if(Config.NonInteractiveMode && Config.NonInteractiveModePurchasedTab) - { - hasSelectedUsersKVP = new KeyValuePair>(true, new Dictionary { { "PurchasedTab", 0 } }); - } - else if (Config.NonInteractiveMode && string.IsNullOrEmpty(Config.NonInteractiveModeListName)) - { - hasSelectedUsersKVP = new KeyValuePair>(true, users); - } - else if (Config.NonInteractiveMode && !string.IsNullOrEmpty(Config.NonInteractiveModeListName)) - { - var listId = lists[Config.NonInteractiveModeListName]; - var listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; - var selectedUsers = users.Where(x => listUsernames.Contains(x.Key)).Distinct().ToDictionary(x => x.Key, x => x.Value); - hasSelectedUsersKVP = new KeyValuePair>(true, selectedUsers); - } - else - { - var loggingService = serviceProvider.GetRequiredService(); - var userSelectionResult = await HandleUserSelection(users, lists); - - Config = configService.CurrentConfig!; - hasSelectedUsersKVP = new KeyValuePair>(userSelectionResult.IsExit, userSelectionResult.selectedUsers); - } - - if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && hasSelectedUsersKVP.Value.ContainsKey("SinglePost")) - { - AnsiConsole.Markup("[red]To find an individual post URL, click on the ... at the top right corner of the post and select 'Copy link to post'.\n\nTo return to the main menu, enter 'back' or 'exit' when prompted for the URL.\n\n[/]"); - string postUrl = AnsiConsole.Prompt( - new TextPrompt("[red]Please enter a post URL: [/]") - .ValidationErrorMessage("[red]Please enter a valid post URL[/]") - .Validate(url => - { - Log.Debug($"Single Post URL: {url}"); - Regex regex = new Regex("https://onlyfans\\.com/[0-9]+/[A-Za-z0-9]+", RegexOptions.IgnoreCase); - if (regex.IsMatch(url)) - { - return ValidationResult.Success(); - } - if (url == "" || url == "exit" || url == "back") { - return ValidationResult.Success(); - } - Log.Error("Post URL invalid"); - return ValidationResult.Error("[red]Please enter a valid post URL[/]"); - })); - - if (postUrl != "" && postUrl != "exit" && postUrl != "back") { - long post_id = Convert.ToInt64(postUrl.Split("/")[3]); - string username = postUrl.Split("/")[4]; - - Log.Debug($"Single Post ID: {post_id.ToString()}"); - Log.Debug($"Single Post Creator: {username}"); - - if (users.ContainsKey(username)) - { - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = System.IO.Path.Combine(Config.DownloadPath, username); - } - else - { - path = $"__user_data__/sites/OnlyFans/{username}"; - } - - Log.Debug($"Download path: {path}"); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {username}\n[/]"); - Log.Debug($"Created folder for {username}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); - } - - await dbService.CreateDB(path); - - await DownloadSinglePost(username, post_id, path, users); - } - } - } - else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && hasSelectedUsersKVP.Value.ContainsKey("PurchasedTab")) - { - Dictionary purchasedTabUsers = await apiService.GetPurchasedTabUsers("/posts/paid/all", users); - AnsiConsole.Markup($"[red]Checking folders for Users in Purchased Tab\n[/]"); - foreach (KeyValuePair user in purchasedTabUsers) - { - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = System.IO.Path.Combine(Config.DownloadPath, user.Key); - } - else - { - path = $"__user_data__/sites/OnlyFans/{user.Key}"; - } - - Log.Debug($"Download path: {path}"); - - await dbService.CheckUsername(user, path); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {user.Key}\n[/]"); - Log.Debug($"Created folder for {user.Key}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {user.Key} already created\n[/]"); - Log.Debug($"Folder for {user.Key} already created"); - } - - Entities.User user_info = await apiService.GetUserInfo($"/users/{user.Key}"); - - await dbService.CreateDB(path); - } - - string p = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - p = Config.DownloadPath; - } - else - { - p = $"__user_data__/sites/OnlyFans/"; - } - - Log.Debug($"Download path: {p}"); - - List purchasedTabCollections = await apiService.GetPurchasedTab("/posts/paid/all", p, users); - foreach(PurchasedTabCollection purchasedTabCollection in purchasedTabCollections) - { - AnsiConsole.Markup($"[red]\nScraping Data for {purchasedTabCollection.Username}\n[/]"); - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = System.IO.Path.Combine(Config.DownloadPath, purchasedTabCollection.Username); - } - else - { - path = $"__user_data__/sites/OnlyFans/{purchasedTabCollection.Username}"; - } - - - Log.Debug($"Download path: {path}"); - - int paidPostCount = 0; - int paidMessagesCount = 0; - paidPostCount = await DownloadPaidPostsPurchasedTab(purchasedTabCollection.Username, purchasedTabCollection.PaidPosts, users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidPostCount, path, users); - paidMessagesCount = await DownloadPaidMessagesPurchasedTab(purchasedTabCollection.Username, purchasedTabCollection.PaidMessages, users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidMessagesCount, path, users); - - AnsiConsole.Markup("\n"); - AnsiConsole.Write(new BreakdownChart() - .FullSize() - .AddItem("Paid Posts", paidPostCount, Color.Red) - .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); - AnsiConsole.Markup("\n"); - } - DateTime endTime = DateTime.Now; - TimeSpan totalTime = endTime - startTime; - AnsiConsole.Markup($"[green]Scrape Completed in {totalTime.TotalMinutes:0.00} minutes\n[/]"); - Log.Debug($"Scrape Completed in {totalTime.TotalMinutes:0.00} minutes"); - } - else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && hasSelectedUsersKVP.Value.ContainsKey("SingleMessage")) - { - AnsiConsole.Markup("[red]To find an individual message URL, note that you can only do so for PPV messages that you have unlocked. Go the main OnlyFans timeline, click on the Purchased tab, find the relevant message, click on the ... at the top right corner of the message, and select 'Copy link to message'. For all other messages, you cannot scrape them individually, you must scrape all messages from that creator.\n\nTo return to the main menu, enter 'back' or 'exit' when prompted for the URL.\n\n[/]"); - string messageUrl = AnsiConsole.Prompt( - new TextPrompt("[red]Please enter a message URL: [/]") - .ValidationErrorMessage("[red]Please enter a valid message URL[/]") - .Validate(url => - { - Log.Debug($"Single Paid Message URL: {url}"); - Regex regex = new Regex("https://onlyfans\\.com/my/chats/chat/[0-9]+/\\?firstId=[0-9]+$", RegexOptions.IgnoreCase); - if (regex.IsMatch(url)) - { - return ValidationResult.Success(); - } - if (url == "" || url == "back" || url == "exit") - { - return ValidationResult.Success(); - } - Log.Error("Message URL invalid"); - return ValidationResult.Error("[red]Please enter a valid message URL[/]"); - })); - - if (messageUrl != "" && messageUrl != "exit" && messageUrl != "back") - { - long message_id = Convert.ToInt64(messageUrl.Split("?firstId=")[1]); - long user_id = Convert.ToInt64(messageUrl.Split("/")[6]); - JObject user = await apiService.GetUserInfoById($"/users/list?x[]={user_id.ToString()}"); - string username = string.Empty; - - Log.Debug($"Message ID: {message_id}"); - Log.Debug($"User ID: {user_id}"); - - if (user is null) - { - username = $"Deleted User - {user_id.ToString()}"; - Log.Debug("Content creator not longer exists - ", user_id.ToString()); - } - else if (!string.IsNullOrEmpty(user[user_id.ToString()]["username"].ToString())) - { - username = user[user_id.ToString()]["username"].ToString(); - Log.Debug("Content creator: ", username); - } - - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = System.IO.Path.Combine(Config.DownloadPath, username); - } - else - { - path = $"__user_data__/sites/OnlyFans/{username}"; - } - - Log.Debug("Download path: ", path); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {username}\n[/]"); - Log.Debug($"Created folder for {username}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); - Log.Debug($"Folder for {username} already created"); - } - - await dbService.CreateDB(path); - - await DownloadPaidMessage(username, hasSelectedUsersKVP, 1, path, message_id); - } - } - else if (hasSelectedUsersKVP.Key && !hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) - { - //Iterate over each user in the list of users - foreach (KeyValuePair user in hasSelectedUsersKVP.Value) - { - int paidPostCount = 0; - int postCount = 0; - int archivedCount = 0; - int streamsCount = 0; - int storiesCount = 0; - int highlightsCount = 0; - int messagesCount = 0; - int paidMessagesCount = 0; - AnsiConsole.Markup($"[red]\nScraping Data for {user.Key}\n[/]"); - - Log.Debug($"Scraping Data for {user.Key}"); - - string path = ""; - if (!string.IsNullOrEmpty(Config.DownloadPath)) - { - path = System.IO.Path.Combine(Config.DownloadPath, user.Key); - } - else - { - path = $"__user_data__/sites/OnlyFans/{user.Key}"; - } - - Log.Debug("Download path: ", path); - - await dbService.CheckUsername(user, path); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - AnsiConsole.Markup($"[red]Created folder for {user.Key}\n[/]"); - Log.Debug($"Created folder for {user.Key}"); - } - else - { - AnsiConsole.Markup($"[red]Folder for {user.Key} already created\n[/]"); - Log.Debug($"Folder for {user.Key} already created"); - } - - await dbService.CreateDB(path); - - if (Config.DownloadAvatarHeaderPhoto) - { - Entities.User? user_info = await apiService.GetUserInfo($"/users/{user.Key}"); - if (user_info != null) - { - await downloadService.DownloadAvatarHeader(user_info.avatar, user_info.header, path, user.Key); - } - } - - if (Config.DownloadPaidPosts) - { - paidPostCount = await DownloadPaidPosts(user.Key, hasSelectedUsersKVP, user, paidPostCount, path); - } - - if (Config.DownloadPosts) - { - postCount = await DownloadFreePosts(user.Key, hasSelectedUsersKVP, user, postCount, path); - } - - if (Config.DownloadArchived) - { - archivedCount = await DownloadArchived(user.Key, hasSelectedUsersKVP, user, archivedCount, path); - } - - if (Config.DownloadStreams) - { - streamsCount = await DownloadStreams(user.Key, hasSelectedUsersKVP, user, streamsCount, path); - } - - if (Config.DownloadStories) - { - storiesCount = await DownloadStories(user.Key, user, storiesCount, path); - } - - if (Config.DownloadHighlights) - { - highlightsCount = await DownloadHighlights(user.Key, user, highlightsCount, path); - } - - if (Config.DownloadMessages) - { - messagesCount = await DownloadMessages(user.Key, hasSelectedUsersKVP, user, messagesCount, path); - } - - if (Config.DownloadPaidMessages) - { - paidMessagesCount = await DownloadPaidMessages(user.Key, hasSelectedUsersKVP, user, paidMessagesCount, path); - } - - AnsiConsole.Markup("\n"); - AnsiConsole.Write(new BreakdownChart() - .FullSize() - .AddItem("Paid Posts", paidPostCount, Color.Red) - .AddItem("Posts", postCount, Color.Blue) - .AddItem("Archived", archivedCount, Color.Green) - .AddItem("Streams", streamsCount, Color.Purple) - .AddItem("Stories", storiesCount, Color.Yellow) - .AddItem("Highlights", highlightsCount, Color.Orange1) - .AddItem("Messages", messagesCount, Color.LightGreen) - .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); - AnsiConsole.Markup("\n"); - } - DateTime endTime = DateTime.Now; - TimeSpan totalTime = endTime - startTime; - AnsiConsole.Markup($"[green]Scrape Completed in {totalTime.TotalMinutes:0.00} minutes\n[/]"); - } - else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) - { - continue; - } - else - { - break; - } - } while (!Config.NonInteractiveMode); - } - - private async Task DownloadPaidMessages(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int paidMessagesCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - PaidMessageCollection paidMessageCollection = new PaidMessageCollection(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Paid Messages[/]", async ctx => + AnsiConsole.Markup($"[green]Logged In successfully as {validate.name} {validate.username}\n[/]"); + await DownloadAllData(); + } + catch (Exception ex) { - paidMessageCollection = await apiService.GetPaidMessages("/posts/paid/chat", path, user.Key, ctx); - }); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + if (ex.InnerException != null) + { + Console.WriteLine("\nInner Exception:"); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + } - if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) - { - AnsiConsole.Markup($"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); + Console.WriteLine("\nPress any key to exit."); + if (!configService.CurrentConfig.NonInteractiveMode) + { + Console.ReadKey(); + } - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values.ToList()); - } - else - { - totalSize = paidMessageCollection.PaidMessages.Count; - } + Environment.Exit(5); + } + } - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadPaidMessages(username, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, paidMessageCollection, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Paid Messages Already Downloaded: {result.ExistingDownloads} New Paid Messages Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - else - { - AnsiConsole.Markup($"[red]Found 0 Paid Messages\n[/]"); - return 0; - } - } - private async Task DownloadMessages(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int messagesCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - MessageCollection messages = new MessageCollection(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Messages[/]", async ctx => - { - messages = await apiService.GetMessages($"/chats/{user.Value}/messages", path, ctx); - }); - - if (messages != null && messages.Messages.Count > 0) - { - AnsiConsole.Markup($"[red]Found {messages.Messages.Count} Media from {messages.MessageObjects.Count} Messages\n[/]"); - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(messages.Messages.Values.ToList()); - } - else - { - totalSize = messages.Messages.Count; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {messages.Messages.Count} Messages[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadMessages(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, messages, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Messages Already Downloaded: {result.ExistingDownloads} New Messages Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - else - { - AnsiConsole.Markup($"[red]Found 0 Messages\n[/]"); - return 0; - } - } - - private async Task DownloadHighlights(string username, KeyValuePair user, int highlightsCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup($"[red]Getting Highlights\n[/]"); - - // Calculate total size for progress bar - long totalSize = 0; - var tempHighlights = await apiService.GetMedia(MediaType.Highlights, $"/users/{user.Value}/stories/highlights", null, path, paid_post_ids); - if (tempHighlights != null && tempHighlights.Count > 0) - { - AnsiConsole.Markup($"[red]Found {tempHighlights.Count} Highlights\n[/]"); - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(tempHighlights.Values.ToList()); - } - else - { - totalSize = tempHighlights.Count; - } - } - else - { - AnsiConsole.Markup($"[red]Found 0 Highlights\n[/]"); - return 0; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {tempHighlights.Count} Highlights[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadHighlights(username, user.Value, path, paid_post_ids.ToHashSet(), progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Highlights Already Downloaded: {result.ExistingDownloads} New Highlights Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - - private async Task DownloadStories(string username, KeyValuePair user, int storiesCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup($"[red]Getting Stories\n[/]"); - - // Calculate total size for progress bar - long totalSize = 0; - var tempStories = await serviceProvider.GetRequiredService().GetMedia(MediaType.Stories, $"/users/{user.Value}/stories", null, path, paid_post_ids); - if (tempStories != null && tempStories.Count > 0) - { - AnsiConsole.Markup($"[red]Found {tempStories.Count} Stories\n[/]"); - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(tempStories.Values.ToList()); - } - else - { - totalSize = tempStories.Count; - } - } - else - { - AnsiConsole.Markup($"[red]Found 0 Stories\n[/]"); - return 0; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {tempStories.Count} Stories[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadStories(username, user.Value, path, paid_post_ids.ToHashSet(), progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Stories Already Downloaded: {result.ExistingDownloads} New Stories Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - - private async Task DownloadArchived(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int archivedCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - ArchivedCollection archived = new ArchivedCollection(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Archived Posts[/]", async ctx => - { - archived = await apiService.GetArchived($"/users/{user.Value}/posts", path, ctx); - }); - - if (archived != null && archived.ArchivedPosts.Count > 0) - { - AnsiConsole.Markup($"[red]Found {archived.ArchivedPosts.Count} Media from {archived.ArchivedPostObjects.Count} Archived Posts\n[/]"); - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(archived.ArchivedPosts.Values.ToList()); - } - else - { - totalSize = archived.ArchivedPosts.Count; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {archived.ArchivedPosts.Count} Archived Posts[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadArchived(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, archived, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Archived Posts Already Downloaded: {result.ExistingDownloads} New Archived Posts Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - else - { - AnsiConsole.Markup($"[red]Found 0 Archived Posts\n[/]"); - return 0; - } - } - - private async Task DownloadFreePosts(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int postCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup($"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/]"); - Log.Debug($"Calling DownloadFreePosts - {user.Key}"); - - PostCollection posts = new PostCollection(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Posts[/]", async ctx => - { - posts = await apiService.GetPosts($"/users/{user.Value}/posts", path, paid_post_ids, ctx); - }); - - if (posts == null || posts.Posts.Count <= 0) - { - AnsiConsole.Markup($"[red]Found 0 Posts\n[/]"); - Log.Debug($"Found 0 Posts"); - return 0; - } - - AnsiConsole.Markup($"[red]Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts\n[/]"); - Log.Debug($"Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts"); - - long totalSize = configService.CurrentConfig.ShowScrapeSize - ? await downloadService.CalculateTotalFileSize(posts.Posts.Values.ToList()) - : posts.Posts.Count; - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {posts.Posts.Count} Posts[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadFreePosts(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, posts, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}[/]\n"); - Log.Debug($"Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}"); - - return result.TotalCount; - } - - private async Task DownloadPaidPosts(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int paidPostCount, string path) - { - IConfigService configService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - AnsiConsole.Markup($"[red]Getting Paid Posts\n[/]"); - Log.Debug($"Calling DownloadPaidPosts - {user.Key}"); - - PaidPostCollection purchasedPosts = new PaidPostCollection(); - - await AnsiConsole.Status() - .StartAsync("[red]Getting Paid Posts[/]", async ctx => - { - purchasedPosts = await apiService.GetPaidPosts("/posts/paid/post", path, user.Key, paid_post_ids, ctx); - }); - - if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) - { - AnsiConsole.Markup($"[red]Found 0 Paid Posts\n[/]"); - Log.Debug("Found 0 Paid Posts"); - return 0; - } - - AnsiConsole.Markup($"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); - Log.Debug($"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); - - long totalSize = configService.CurrentConfig.ShowScrapeSize - ? await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()) - : purchasedPosts.PaidPosts.Count; - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadPaidPosts(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, purchasedPosts, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}[/]\n"); - Log.Debug($"Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}"); - - return result.TotalCount; - } - - private async Task DownloadPaidPostsPurchasedTab(string username, PaidPostCollection purchasedPosts, KeyValuePair user, int paidPostCount, string path, Dictionary users) - { + private async Task DownloadAllData() + { IDBService dbService = serviceProvider.GetRequiredService(); IConfigService configService = serviceProvider.GetRequiredService(); IAuthService authService = serviceProvider.GetRequiredService(); IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - int oldPaidPostCount = 0; - int newPaidPostCount = 0; - if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) - { - AnsiConsole.Markup($"[red]Found 0 Paid Posts\n[/]"); - Log.Debug("Found 0 Paid Posts"); - return 0; - } + Config Config = configService.CurrentConfig!; - AnsiConsole.Markup($"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); - Log.Debug($"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); + Log.Debug("Calling DownloadAllData"); - paidPostCount = purchasedPosts.PaidPosts.Count; - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()); - } - else - { - totalSize = paidPostCount; - } - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - var task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", autoStart: false); - Log.Debug($"Downloading {purchasedPosts.PaidPosts.Count} Paid Posts"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair purchasedPostKVP in purchasedPosts.PaidPosts) - { - bool isNew; - if (purchasedPostKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = purchasedPostKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh == null) - { - continue; - } + do + { + DateTime startTime = DateTime.Now; + Dictionary users = new(); + Dictionary activeSubs = + await apiService.GetActiveSubscriptions("/subscriptions/subscribes", + Config.IncludeRestrictedSubscriptions); - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - Medium? mediaInfo = purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); - Purchased.List? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true) : null; + Log.Debug("Subscriptions: "); - isNew = await downloadService.DownloadPurchasedPostDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: purchasedPostKVP.Key, - api_type: "Posts", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidPostFileNameFormat ?? string.Empty, - postInfo: postInfo, - postMedia: mediaInfo, - fromUser: postInfo?.fromUser, - users: users); - if (isNew) - { - newPaidPostCount++; - } - else - { - oldPaidPostCount++; - } - } - else - { - Medium? mediaInfo = purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); - Purchased.List? postInfo = mediaInfo != null ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true) : null; + foreach (KeyValuePair activeSub in activeSubs) + { + if (!users.ContainsKey(activeSub.Key)) + { + users.Add(activeSub.Key, activeSub.Value); + Log.Debug($"Name: {activeSub.Key} ID: {activeSub.Value}"); + } + } - isNew = await downloadService.DownloadPurchasedPostMedia( - url: purchasedPostKVP.Value, - folder: path, - media_id: purchasedPostKVP.Key, - api_type: "Posts", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidPostFileNameFormat ?? string.Empty, - messageInfo: postInfo, - messageMedia: mediaInfo, - fromUser: postInfo?.fromUser, - users: users); - if (isNew) - { - newPaidPostCount++; - } - else - { - oldPaidPostCount++; - } - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}[/]\n"); - Log.Debug($"Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}"); - return paidPostCount; - } + if (Config!.IncludeExpiredSubscriptions) + { + Log.Debug("Inactive Subscriptions: "); - private async Task DownloadPaidMessagesPurchasedTab(string username, PaidMessageCollection paidMessageCollection, KeyValuePair user, int paidMessagesCount, string path, Dictionary users) - { - IDBService dbService = serviceProvider.GetRequiredService(); - IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); + Dictionary expiredSubs = + await apiService.GetExpiredSubscriptions("/subscriptions/subscribes", + Config.IncludeRestrictedSubscriptions); + foreach (KeyValuePair expiredSub in expiredSubs) + { + if (!users.ContainsKey(expiredSub.Key)) + { + users.Add(expiredSub.Key, expiredSub.Value); + Log.Debug($"Name: {expiredSub.Key} ID: {expiredSub.Value}"); + } + } + } - int oldPaidMessagesCount = 0; - int newPaidMessagesCount = 0; - if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) - { - AnsiConsole.Markup($"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); - Log.Debug($"Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages"); - paidMessagesCount = paidMessageCollection.PaidMessages.Count; - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values.ToList()); - } - else - { - totalSize = paidMessagesCount; - } - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - var task = ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", autoStart: false); - Log.Debug($"Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair paidMessageKVP in paidMessageCollection.PaidMessages) - { - bool isNew; - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = paidMessageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) - { - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } + Dictionary lists = await apiService.GetLists("/lists"); - Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - Purchased.List? messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + // Remove users from the list if they are in the ignored list + if (!string.IsNullOrEmpty(Config.IgnoredUsersListName)) + { + if (!lists.TryGetValue(Config.IgnoredUsersListName, out long ignoredUsersListId)) + { + AnsiConsole.Markup($"[red]Ignored users list '{Config.IgnoredUsersListName}' not found\n[/]"); + Log.Error($"Ignored users list '{Config.IgnoredUsersListName}' not found"); + } + else + { + List ignoredUsernames = + await apiService.GetListUsers($"/lists/{ignoredUsersListId}/users") ?? []; + users = users.Where(x => !ignoredUsernames.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value); + } + } - isNew = await downloadService.DownloadPurchasedMessageDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: users); + await dbService.CreateUsersDB(users); + KeyValuePair> hasSelectedUsersKVP; + if (Config.NonInteractiveMode && Config.NonInteractiveModePurchasedTab) + { + hasSelectedUsersKVP = new KeyValuePair>(true, + new Dictionary { { "PurchasedTab", 0 } }); + } + else if (Config.NonInteractiveMode && string.IsNullOrEmpty(Config.NonInteractiveModeListName)) + { + hasSelectedUsersKVP = new KeyValuePair>(true, users); + } + else if (Config.NonInteractiveMode && !string.IsNullOrEmpty(Config.NonInteractiveModeListName)) + { + long listId = lists[Config.NonInteractiveModeListName]; + List listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? []; + Dictionary selectedUsers = users.Where(x => listUsernames.Contains(x.Key)).Distinct() + .ToDictionary(x => x.Key, x => x.Value); + hasSelectedUsersKVP = new KeyValuePair>(true, selectedUsers); + } + else + { + ILoggingService loggingService = serviceProvider.GetRequiredService(); + (bool IsExit, Dictionary? selectedUsers) userSelectionResult = + await HandleUserSelection(users, lists); - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } - else - { - Medium? mediaInfo = paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - Purchased.List messageInfo = paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + Config = configService.CurrentConfig!; + hasSelectedUsersKVP = new KeyValuePair>(userSelectionResult.IsExit, + userSelectionResult.selectedUsers); + } - isNew = await downloadService.DownloadPurchasedMedia( - url: paidMessageKVP.Value, - folder: path, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: users); - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); - Log.Debug($"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}"); - } - else - { - AnsiConsole.Markup($"[red]Found 0 Paid Messages\n[/]"); - Log.Debug($"Found 0 Paid Messages"); - } + if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + hasSelectedUsersKVP.Value.ContainsKey("SinglePost")) + { + AnsiConsole.Markup( + "[red]To find an individual post URL, click on the ... at the top right corner of the post and select 'Copy link to post'.\n\nTo return to the main menu, enter 'back' or 'exit' when prompted for the URL.\n\n[/]"); + string postUrl = AnsiConsole.Prompt( + new TextPrompt("[red]Please enter a post URL: [/]") + .ValidationErrorMessage("[red]Please enter a valid post URL[/]") + .Validate(url => + { + Log.Debug($"Single Post URL: {url}"); + Regex regex = new("https://onlyfans\\.com/[0-9]+/[A-Za-z0-9]+", RegexOptions.IgnoreCase); + if (regex.IsMatch(url)) + { + return ValidationResult.Success(); + } - return paidMessagesCount; - } + if (url == "" || url == "exit" || url == "back") + { + return ValidationResult.Success(); + } - private async Task DownloadStreams(string username, KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, int streamsCount, string path) - { + Log.Error("Post URL invalid"); + return ValidationResult.Error("[red]Please enter a valid post URL[/]"); + })); + + if (postUrl != "" && postUrl != "exit" && postUrl != "back") + { + long post_id = Convert.ToInt64(postUrl.Split("/")[3]); + string username = postUrl.Split("/")[4]; + + Log.Debug($"Single Post ID: {post_id.ToString()}"); + Log.Debug($"Single Post Creator: {username}"); + + if (users.ContainsKey(username)) + { + string path = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + path = Path.Combine(Config.DownloadPath, username); + } + else + { + path = $"__user_data__/sites/OnlyFans/{username}"; + } + + Log.Debug($"Download path: {path}"); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + AnsiConsole.Markup($"[red]Created folder for {username}\n[/]"); + Log.Debug($"Created folder for {username}"); + } + else + { + AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); + } + + await dbService.CreateDB(path); + + await DownloadSinglePost(username, post_id, path, users); + } + } + } + else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + hasSelectedUsersKVP.Value.ContainsKey("PurchasedTab")) + { + Dictionary purchasedTabUsers = + await apiService.GetPurchasedTabUsers("/posts/paid/all", users); + AnsiConsole.Markup("[red]Checking folders for Users in Purchased Tab\n[/]"); + foreach (KeyValuePair user in purchasedTabUsers) + { + string path = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + path = Path.Combine(Config.DownloadPath, user.Key); + } + else + { + path = $"__user_data__/sites/OnlyFans/{user.Key}"; + } + + Log.Debug($"Download path: {path}"); + + await dbService.CheckUsername(user, path); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + AnsiConsole.Markup($"[red]Created folder for {user.Key}\n[/]"); + Log.Debug($"Created folder for {user.Key}"); + } + else + { + AnsiConsole.Markup($"[red]Folder for {user.Key} already created\n[/]"); + Log.Debug($"Folder for {user.Key} already created"); + } + + User user_info = await apiService.GetUserInfo($"/users/{user.Key}"); + + await dbService.CreateDB(path); + } + + string p = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + p = Config.DownloadPath; + } + else + { + p = "__user_data__/sites/OnlyFans/"; + } + + Log.Debug($"Download path: {p}"); + + List purchasedTabCollections = + await apiService.GetPurchasedTab("/posts/paid/all", p, users); + foreach (PurchasedTabCollection purchasedTabCollection in purchasedTabCollections) + { + AnsiConsole.Markup($"[red]\nScraping Data for {purchasedTabCollection.Username}\n[/]"); + string path = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + path = Path.Combine(Config.DownloadPath, purchasedTabCollection.Username); + } + else + { + path = $"__user_data__/sites/OnlyFans/{purchasedTabCollection.Username}"; + } + + + Log.Debug($"Download path: {path}"); + + int paidPostCount = 0; + int paidMessagesCount = 0; + paidPostCount = await DownloadPaidPostsPurchasedTab(purchasedTabCollection.Username, + purchasedTabCollection.PaidPosts, + users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidPostCount, path, + users); + paidMessagesCount = await DownloadPaidMessagesPurchasedTab(purchasedTabCollection.Username, + purchasedTabCollection.PaidMessages, + users.FirstOrDefault(u => u.Value == purchasedTabCollection.UserId), paidMessagesCount, path, + users); + + AnsiConsole.Markup("\n"); + AnsiConsole.Write(new BreakdownChart() + .FullSize() + .AddItem("Paid Posts", paidPostCount, Color.Red) + .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); + AnsiConsole.Markup("\n"); + } + + DateTime endTime = DateTime.Now; + TimeSpan totalTime = endTime - startTime; + AnsiConsole.Markup($"[green]Scrape Completed in {totalTime.TotalMinutes:0.00} minutes\n[/]"); + Log.Debug($"Scrape Completed in {totalTime.TotalMinutes:0.00} minutes"); + } + else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + hasSelectedUsersKVP.Value.ContainsKey("SingleMessage")) + { + AnsiConsole.Markup( + "[red]To find an individual message URL, note that you can only do so for PPV messages that you have unlocked. Go the main OnlyFans timeline, click on the Purchased tab, find the relevant message, click on the ... at the top right corner of the message, and select 'Copy link to message'. For all other messages, you cannot scrape them individually, you must scrape all messages from that creator.\n\nTo return to the main menu, enter 'back' or 'exit' when prompted for the URL.\n\n[/]"); + string messageUrl = AnsiConsole.Prompt( + new TextPrompt("[red]Please enter a message URL: [/]") + .ValidationErrorMessage("[red]Please enter a valid message URL[/]") + .Validate(url => + { + Log.Debug($"Single Paid Message URL: {url}"); + Regex regex = new("https://onlyfans\\.com/my/chats/chat/[0-9]+/\\?firstId=[0-9]+$", + RegexOptions.IgnoreCase); + if (regex.IsMatch(url)) + { + return ValidationResult.Success(); + } + + if (url == "" || url == "back" || url == "exit") + { + return ValidationResult.Success(); + } + + Log.Error("Message URL invalid"); + return ValidationResult.Error("[red]Please enter a valid message URL[/]"); + })); + + if (messageUrl != "" && messageUrl != "exit" && messageUrl != "back") + { + long message_id = Convert.ToInt64(messageUrl.Split("?firstId=")[1]); + long user_id = Convert.ToInt64(messageUrl.Split("/")[6]); + JObject user = await apiService.GetUserInfoById($"/users/list?x[]={user_id.ToString()}"); + string username = string.Empty; + + Log.Debug($"Message ID: {message_id}"); + Log.Debug($"User ID: {user_id}"); + + if (user is null) + { + username = $"Deleted User - {user_id.ToString()}"; + Log.Debug("Content creator not longer exists - ", user_id.ToString()); + } + else if (!string.IsNullOrEmpty(user[user_id.ToString()]["username"].ToString())) + { + username = user[user_id.ToString()]["username"].ToString(); + Log.Debug("Content creator: ", username); + } + + string path = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + path = Path.Combine(Config.DownloadPath, username); + } + else + { + path = $"__user_data__/sites/OnlyFans/{username}"; + } + + Log.Debug("Download path: ", path); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + AnsiConsole.Markup($"[red]Created folder for {username}\n[/]"); + Log.Debug($"Created folder for {username}"); + } + else + { + AnsiConsole.Markup($"[red]Folder for {username} already created\n[/]"); + Log.Debug($"Folder for {username} already created"); + } + + await dbService.CreateDB(path); + + await DownloadPaidMessage(username, hasSelectedUsersKVP, 1, path, message_id); + } + } + else if (hasSelectedUsersKVP.Key && !hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) + { + //Iterate over each user in the list of users + foreach (KeyValuePair user in hasSelectedUsersKVP.Value) + { + int paidPostCount = 0; + int postCount = 0; + int archivedCount = 0; + int streamsCount = 0; + int storiesCount = 0; + int highlightsCount = 0; + int messagesCount = 0; + int paidMessagesCount = 0; + AnsiConsole.Markup($"[red]\nScraping Data for {user.Key}\n[/]"); + + Log.Debug($"Scraping Data for {user.Key}"); + + string path = ""; + if (!string.IsNullOrEmpty(Config.DownloadPath)) + { + path = Path.Combine(Config.DownloadPath, user.Key); + } + else + { + path = $"__user_data__/sites/OnlyFans/{user.Key}"; + } + + Log.Debug("Download path: ", path); + + await dbService.CheckUsername(user, path); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + AnsiConsole.Markup($"[red]Created folder for {user.Key}\n[/]"); + Log.Debug($"Created folder for {user.Key}"); + } + else + { + AnsiConsole.Markup($"[red]Folder for {user.Key} already created\n[/]"); + Log.Debug($"Folder for {user.Key} already created"); + } + + await dbService.CreateDB(path); + + if (Config.DownloadAvatarHeaderPhoto) + { + User? user_info = await apiService.GetUserInfo($"/users/{user.Key}"); + if (user_info != null) + { + await downloadService.DownloadAvatarHeader(user_info.avatar, user_info.header, path, + user.Key); + } + } + + if (Config.DownloadPaidPosts) + { + paidPostCount = + await DownloadPaidPosts(user.Key, hasSelectedUsersKVP, user, paidPostCount, path); + } + + if (Config.DownloadPosts) + { + postCount = await DownloadFreePosts(user.Key, hasSelectedUsersKVP, user, postCount, path); + } + + if (Config.DownloadArchived) + { + archivedCount = + await DownloadArchived(user.Key, hasSelectedUsersKVP, user, archivedCount, path); + } + + if (Config.DownloadStreams) + { + streamsCount = await DownloadStreams(user.Key, hasSelectedUsersKVP, user, streamsCount, path); + } + + if (Config.DownloadStories) + { + storiesCount = await DownloadStories(user.Key, user, storiesCount, path); + } + + if (Config.DownloadHighlights) + { + highlightsCount = await DownloadHighlights(user.Key, user, highlightsCount, path); + } + + if (Config.DownloadMessages) + { + messagesCount = + await DownloadMessages(user.Key, hasSelectedUsersKVP, user, messagesCount, path); + } + + if (Config.DownloadPaidMessages) + { + paidMessagesCount = await DownloadPaidMessages(user.Key, hasSelectedUsersKVP, user, + paidMessagesCount, path); + } + + AnsiConsole.Markup("\n"); + AnsiConsole.Write(new BreakdownChart() + .FullSize() + .AddItem("Paid Posts", paidPostCount, Color.Red) + .AddItem("Posts", postCount, Color.Blue) + .AddItem("Archived", archivedCount, Color.Green) + .AddItem("Streams", streamsCount, Color.Purple) + .AddItem("Stories", storiesCount, Color.Yellow) + .AddItem("Highlights", highlightsCount, Color.Orange1) + .AddItem("Messages", messagesCount, Color.LightGreen) + .AddItem("Paid Messages", paidMessagesCount, Color.Aqua)); + AnsiConsole.Markup("\n"); + } + + DateTime endTime = DateTime.Now; + TimeSpan totalTime = endTime - startTime; + AnsiConsole.Markup($"[green]Scrape Completed in {totalTime.TotalMinutes:0.00} minutes\n[/]"); + } + else if (hasSelectedUsersKVP.Key && hasSelectedUsersKVP.Value != null && + hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged")) + { + } + else + { + break; + } + } while (!Config.NonInteractiveMode); + } + + private async Task DownloadPaidMessages(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int paidMessagesCount, string path) + { IConfigService configService = serviceProvider.GetRequiredService(); IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - StreamsCollection streams = new StreamsCollection(); + PaidMessageCollection paidMessageCollection = new(); await AnsiConsole.Status() - .StartAsync("[red]Getting Streams[/]", async ctx => + .StartAsync("[red]Getting Paid Messages[/]", + async ctx => + { + paidMessageCollection = await apiService.GetPaidMessages("/posts/paid/chat", path, user.Key, ctx); + }); + + if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) { - streams = await apiService.GetStreams($"/users/{user.Value}/posts/streams", path, paid_post_ids, ctx); - }); + AnsiConsole.Markup( + $"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); - if (streams != null && streams.Streams.Count > 0) - { - AnsiConsole.Markup($"[red]Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams\n[/]"); - - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(streams.Streams.Values.ToList()); - } - else - { - totalSize = streams.Streams.Count; - } - - DownloadResult result = null; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading {streams.Streams.Count} Streams[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - - var progressReporter = new SpectreProgressReporter(task); - result = await downloadService.DownloadStreams(username, user.Value, path, hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, streams, progressReporter); - - task.StopTask(); - }); - - AnsiConsole.Markup($"[red]Streams Already Downloaded: {result.ExistingDownloads} New Streams Downloaded: {result.NewDownloads}[/]\n"); - return result.TotalCount; - } - else - { - AnsiConsole.Markup($"[red]Found 0 Streams\n[/]"); - return 0; - } - } - private async Task DownloadPaidMessage(string username, KeyValuePair> hasSelectedUsersKVP, int paidMessagesCount, string path, long message_id) - { - IDBService dbService = serviceProvider.GetRequiredService(); - IConfigService configService = serviceProvider.GetRequiredService(); - IAuthService authService = serviceProvider.GetRequiredService(); - IAPIService apiService = serviceProvider.GetRequiredService(); - IDownloadService downloadService = serviceProvider.GetRequiredService(); - - Log.Debug($"Calling DownloadPaidMessage - {username}"); - - AnsiConsole.Markup($"[red]Getting Paid Message\n[/]"); - - SinglePaidMessageCollection singlePaidMessageCollection = await apiService.GetPaidMessage($"/messages/{message_id.ToString()}", path); - int oldPreviewPaidMessagesCount = 0; - int newPreviewPaidMessagesCount = 0; - int oldPaidMessagesCount = 0; - int newPaidMessagesCount = 0; - if (singlePaidMessageCollection != null && singlePaidMessageCollection.PreviewSingleMessages.Count > 0) - { - AnsiConsole.Markup($"[red]Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages\n[/]"); - Log.Debug($"Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); - paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; long totalSize = 0; if (configService.CurrentConfig.ShowScrapeSize) { - totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection.PreviewSingleMessages.Values.ToList()); + totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values + .ToList()); + } + else + { + totalSize = paidMessageCollection.PaidMessages.Count; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = + ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", + false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadPaidMessages(username, path, hasSelectedUsersKVP.Value, + clientIdBlobMissing, devicePrivateKeyMissing, paidMessageCollection, progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Paid Messages Already Downloaded: {result.ExistingDownloads} New Paid Messages Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + AnsiConsole.Markup("[red]Found 0 Paid Messages\n[/]"); + return 0; + } + + private async Task DownloadMessages(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int messagesCount, string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + MessageCollection messages = new(); + + await AnsiConsole.Status() + .StartAsync("[red]Getting Messages[/]", + async ctx => { messages = await apiService.GetMessages($"/chats/{user.Value}/messages", path, ctx); }); + + if (messages != null && messages.Messages.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {messages.Messages.Count} Media from {messages.MessageObjects.Count} Messages\n[/]"); + + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(messages.Messages.Values.ToList()); + } + else + { + totalSize = messages.Messages.Count; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {messages.Messages.Count} Messages[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadMessages(username, user.Value, path, + hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, messages, + progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Messages Already Downloaded: {result.ExistingDownloads} New Messages Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + AnsiConsole.Markup("[red]Found 0 Messages\n[/]"); + return 0; + } + + private async Task DownloadHighlights(string username, KeyValuePair user, int highlightsCount, + string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + + AnsiConsole.Markup("[red]Getting Highlights\n[/]"); + + // Calculate total size for progress bar + long totalSize = 0; + Dictionary? tempHighlights = await apiService.GetMedia(MediaType.Highlights, + $"/users/{user.Value}/stories/highlights", null, path, paid_post_ids); + if (tempHighlights != null && tempHighlights.Count > 0) + { + AnsiConsole.Markup($"[red]Found {tempHighlights.Count} Highlights\n[/]"); + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(tempHighlights.Values.ToList()); + } + else + { + totalSize = tempHighlights.Count; + } + } + else + { + AnsiConsole.Markup("[red]Found 0 Highlights\n[/]"); + return 0; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {tempHighlights.Count} Highlights[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadHighlights(username, user.Value, path, paid_post_ids.ToHashSet(), + progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Highlights Already Downloaded: {result.ExistingDownloads} New Highlights Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + private async Task DownloadStories(string username, KeyValuePair user, int storiesCount, + string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + AnsiConsole.Markup("[red]Getting Stories\n[/]"); + + // Calculate total size for progress bar + long totalSize = 0; + Dictionary? tempStories = await serviceProvider.GetRequiredService() + .GetMedia(MediaType.Stories, $"/users/{user.Value}/stories", null, path, paid_post_ids); + if (tempStories != null && tempStories.Count > 0) + { + AnsiConsole.Markup($"[red]Found {tempStories.Count} Stories\n[/]"); + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(tempStories.Values.ToList()); + } + else + { + totalSize = tempStories.Count; + } + } + else + { + AnsiConsole.Markup("[red]Found 0 Stories\n[/]"); + return 0; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {tempStories.Count} Stories[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadStories(username, user.Value, path, paid_post_ids.ToHashSet(), + progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Stories Already Downloaded: {result.ExistingDownloads} New Stories Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + private async Task DownloadArchived(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int archivedCount, string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + ArchivedCollection archived = new(); + + await AnsiConsole.Status() + .StartAsync("[red]Getting Archived Posts[/]", + async ctx => { archived = await apiService.GetArchived($"/users/{user.Value}/posts", path, ctx); }); + + if (archived != null && archived.ArchivedPosts.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {archived.ArchivedPosts.Count} Media from {archived.ArchivedPostObjects.Count} Archived Posts\n[/]"); + + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(archived.ArchivedPosts.Values.ToList()); + } + else + { + totalSize = archived.ArchivedPosts.Count; + } + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = + ctx.AddTask($"[red]Downloading {archived.ArchivedPosts.Count} Archived Posts[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadArchived(username, user.Value, path, + hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, archived, + progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Archived Posts Already Downloaded: {result.ExistingDownloads} New Archived Posts Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + AnsiConsole.Markup("[red]Found 0 Archived Posts\n[/]"); + return 0; + } + + private async Task DownloadFreePosts(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int postCount, string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + AnsiConsole.Markup( + "[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/]"); + Log.Debug($"Calling DownloadFreePosts - {user.Key}"); + + PostCollection posts = new(); + + await AnsiConsole.Status() + .StartAsync("[red]Getting Posts[/]", + async ctx => + { + posts = await apiService.GetPosts($"/users/{user.Value}/posts", path, paid_post_ids, ctx); + }); + + if (posts == null || posts.Posts.Count <= 0) + { + AnsiConsole.Markup("[red]Found 0 Posts\n[/]"); + Log.Debug("Found 0 Posts"); + return 0; + } + + AnsiConsole.Markup($"[red]Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts\n[/]"); + Log.Debug($"Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts"); + + long totalSize = configService.CurrentConfig.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize(posts.Posts.Values.ToList()) + : posts.Posts.Count; + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {posts.Posts.Count} Posts[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadFreePosts(username, user.Value, path, hasSelectedUsersKVP.Value, + clientIdBlobMissing, devicePrivateKeyMissing, posts, progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}[/]\n"); + Log.Debug($"Posts Already Downloaded: {result.ExistingDownloads} New Posts Downloaded: {result.NewDownloads}"); + + return result.TotalCount; + } + + private async Task DownloadPaidPosts(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int paidPostCount, string path) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + AnsiConsole.Markup("[red]Getting Paid Posts\n[/]"); + Log.Debug($"Calling DownloadPaidPosts - {user.Key}"); + + PaidPostCollection purchasedPosts = new(); + + await AnsiConsole.Status() + .StartAsync("[red]Getting Paid Posts[/]", + async ctx => + { + purchasedPosts = + await apiService.GetPaidPosts("/posts/paid/post", path, user.Key, paid_post_ids, ctx); + }); + + if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) + { + AnsiConsole.Markup("[red]Found 0 Paid Posts\n[/]"); + Log.Debug("Found 0 Paid Posts"); + return 0; + } + + AnsiConsole.Markup( + $"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); + Log.Debug( + $"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); + + long totalSize = configService.CurrentConfig.ShowScrapeSize + ? await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()) + : purchasedPosts.PaidPosts.Count; + + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", + false); + task.MaxValue = totalSize; + task.StartTask(); + + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadPaidPosts(username, user.Value, path, hasSelectedUsersKVP.Value, + clientIdBlobMissing, devicePrivateKeyMissing, purchasedPosts, progressReporter); + + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}[/]\n"); + Log.Debug( + $"Paid Posts Already Downloaded: {result.ExistingDownloads} New Paid Posts Downloaded: {result.NewDownloads}"); + + return result.TotalCount; + } + + private async Task DownloadPaidPostsPurchasedTab(string username, PaidPostCollection purchasedPosts, + KeyValuePair user, int paidPostCount, string path, Dictionary users) + { + IDBService dbService = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + int oldPaidPostCount = 0; + int newPaidPostCount = 0; + if (purchasedPosts == null || purchasedPosts.PaidPosts.Count <= 0) + { + AnsiConsole.Markup("[red]Found 0 Paid Posts\n[/]"); + Log.Debug("Found 0 Paid Posts"); + return 0; + } + + AnsiConsole.Markup( + $"[red]Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts\n[/]"); + Log.Debug( + $"Found {purchasedPosts.PaidPosts.Count} Media from {purchasedPosts.PaidPostObjects.Count} Paid Posts"); + + paidPostCount = purchasedPosts.PaidPosts.Count; + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(purchasedPosts.PaidPosts.Values.ToList()); + } + else + { + totalSize = paidPostCount; + } + + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + // Define tasks + ProgressTask task = ctx.AddTask($"[red]Downloading {purchasedPosts.PaidPosts.Count} Paid Posts[/]", + false); + Log.Debug($"Downloading {purchasedPosts.PaidPosts.Count} Paid Posts"); + task.MaxValue = totalSize; + task.StartTask(); + foreach (KeyValuePair purchasedPostKVP in purchasedPosts.PaidPosts) + { + bool isNew; + if (purchasedPostKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = purchasedPostKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string postId = messageUrlParsed[5]; + string? licenseURL = null; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh == null) + { + continue; + } + + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", + "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", + pssh); + } + else + { + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", + pssh); + } + + Medium? mediaInfo = + purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); + Purchased.List? postInfo = mediaInfo != null + ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true) + : null; + + isNew = await downloadService.DownloadPurchasedPostDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + purchasedPostKVP.Key, + "Posts", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidPostFileNameFormat ?? string.Empty, + postInfo, + mediaInfo, + postInfo?.fromUser, + users); + if (isNew) + { + newPaidPostCount++; + } + else + { + oldPaidPostCount++; + } + } + else + { + Medium? mediaInfo = + purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.id == purchasedPostKVP.Key); + Purchased.List? postInfo = mediaInfo != null + ? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true) + : null; + + isNew = await downloadService.DownloadPurchasedPostMedia( + purchasedPostKVP.Value, + path, + purchasedPostKVP.Key, + "Posts", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidPostFileNameFormat ?? string.Empty, + postInfo, + mediaInfo, + postInfo?.fromUser, + users); + if (isNew) + { + newPaidPostCount++; + } + else + { + oldPaidPostCount++; + } + } + } + + task.StopTask(); + }); + AnsiConsole.Markup( + $"[red]Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}[/]\n"); + Log.Debug($"Paid Posts Already Downloaded: {oldPaidPostCount} New Paid Posts Downloaded: {newPaidPostCount}"); + return paidPostCount; + } + + private async Task DownloadPaidMessagesPurchasedTab(string username, + PaidMessageCollection paidMessageCollection, KeyValuePair user, int paidMessagesCount, + string path, Dictionary users) + { + IDBService dbService = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + int oldPaidMessagesCount = 0; + int newPaidMessagesCount = 0; + if (paidMessageCollection != null && paidMessageCollection.PaidMessages.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages\n[/]"); + Log.Debug( + $"Found {paidMessageCollection.PaidMessages.Count} Media from {paidMessageCollection.PaidMessageObjects.Count} Paid Messages"); + paidMessagesCount = paidMessageCollection.PaidMessages.Count; + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(paidMessageCollection.PaidMessages.Values + .ToList()); } else { totalSize = paidMessagesCount; } + await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - var task = ctx.AddTask($"[red]Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Paid Messages[/]", autoStart: false); - Log.Debug($"Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Paid Messages"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.PreviewSingleMessages) - { - bool isNew; - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = paidMessageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) - { - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + // Define tasks + ProgressTask task = + ctx.AddTask($"[red]Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages[/]", + false); + Log.Debug($"Downloading {paidMessageCollection.PaidMessages.Count} Paid Messages"); + task.MaxValue = totalSize; + task.StartTask(); + foreach (KeyValuePair paidMessageKVP in paidMessageCollection.PaidMessages) + { + bool isNew; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = paidMessageKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string messageId = messageUrlParsed[5]; + string? licenseURL = null; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh != null) + { + DateTime lastModified = + await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders( + $"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } + else + { + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } - Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => + m.id == paidMessageKVP.Key); + Purchased.List? messageInfo = + paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); - isNew = await downloadService.DownloadSingleMessagePreviewDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); + isNew = await downloadService.DownloadPurchasedMessageDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + users); - if (isNew) - { - newPreviewPaidMessagesCount++; - } - else - { - oldPreviewPaidMessagesCount++; - } - } - } - else - { - Medium? mediaInfo = singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + if (isNew) + { + newPaidMessagesCount++; + } + else + { + oldPaidMessagesCount++; + } + } + } + else + { + Medium? mediaInfo = + paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); + Purchased.List messageInfo = + paidMessageCollection.PaidMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); - isNew = await downloadService.DownloadMessagePreviewMedia( - url: paidMessageKVP.Value, - folder: path, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newPreviewPaidMessagesCount++; - } - else - { - oldPreviewPaidMessagesCount++; - } - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Preview Paid Messages Already Downloaded: {oldPreviewPaidMessagesCount} New Preview Paid Messages Downloaded: {newPreviewPaidMessagesCount}[/]\n"); - Log.Debug($"Preview Paid Messages Already Downloaded: {oldPreviewPaidMessagesCount} New Preview Paid Messages Downloaded: {newPreviewPaidMessagesCount}"); + isNew = await downloadService.DownloadPurchasedMedia( + paidMessageKVP.Value, + path, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + users); + if (isNew) + { + newPaidMessagesCount++; + } + else + { + oldPaidMessagesCount++; + } + } + } + + task.StopTask(); + }); + AnsiConsole.Markup( + $"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); + Log.Debug( + $"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}"); + } + else + { + AnsiConsole.Markup("[red]Found 0 Paid Messages\n[/]"); + Log.Debug("Found 0 Paid Messages"); } - if (singlePaidMessageCollection != null && singlePaidMessageCollection.SingleMessages.Count > 0) - { - AnsiConsole.Markup($"[red]Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages\n[/]"); - Log.Debug($"Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); - paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection.SingleMessages.Values.ToList()); - } - else - { - totalSize = paidMessagesCount; - } - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - // Define tasks - var task = ctx.AddTask($"[red]Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages[/]", autoStart: false); - Log.Debug($"Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages"); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.SingleMessages) - { - bool isNew; - if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = paidMessageKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string messageId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh != null) - { - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); - } - Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + return paidMessagesCount; + } - isNew = await downloadService.DownloadSinglePurchasedMessageDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } - else - { - Medium? mediaInfo = singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => m.id == paidMessageKVP.Key); - SingleMessage? messageInfo = singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); - - isNew = await downloadService.DownloadSinglePurchasedMedia( - url: paidMessageKVP.Value, - folder: path, - media_id: paidMessageKVP.Key, - api_type: "Messages", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PaidMessageFileNameFormat ?? string.Empty, - messageInfo: messageInfo, - messageMedia: mediaInfo, - fromUser: messageInfo?.fromUser, - users: hasSelectedUsersKVP.Value); - if (isNew) - { - newPaidMessagesCount++; - } - else - { - oldPaidMessagesCount++; - } - } - } - task.StopTask(); - }); - AnsiConsole.Markup($"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); - Log.Debug($"Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}"); - } - else - { - AnsiConsole.Markup($"[red]Found 0 Paid Messages\n[/]"); - Log.Debug($"Found 0 Paid Messages"); - } - - return paidMessagesCount; - } - - private async Task DownloadSinglePost(string username, long post_id, string path, Dictionary users) - { + private async Task DownloadStreams(string username, + KeyValuePair> hasSelectedUsersKVP, KeyValuePair user, + int streamsCount, string path) + { IConfigService configService = serviceProvider.GetRequiredService(); IAPIService apiService = serviceProvider.GetRequiredService(); IDownloadService downloadService = serviceProvider.GetRequiredService(); - Log.Debug($"Calling DownloadSinglePost - {post_id.ToString()}"); + StreamsCollection streams = new(); - AnsiConsole.Markup($"[red]Getting Post\n[/]"); - SinglePostCollection post = await apiService.GetPost($"/posts/{post_id.ToString()}", path); - if (post == null) - { - AnsiConsole.Markup($"[red]Couldn't find post\n[/]"); - Log.Debug($"Couldn't find post"); - return; - } + await AnsiConsole.Status() + .StartAsync("[red]Getting Streams[/]", + async ctx => + { + streams = await apiService.GetStreams($"/users/{user.Value}/posts/streams", path, paid_post_ids, + ctx); + }); - long totalSize = 0; - if (configService.CurrentConfig.ShowScrapeSize) - { - totalSize = await downloadService.CalculateTotalFileSize(post.SinglePosts.Values.ToList()); - } - else - { - totalSize = post.SinglePosts.Count; - } - bool isNew = false; - await AnsiConsole.Progress() - .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) - .StartAsync(async ctx => - { - var task = ctx.AddTask($"[red]Downloading Post[/]", autoStart: false); - task.MaxValue = totalSize; - task.StartTask(); - foreach (KeyValuePair postKVP in post.SinglePosts) - { - if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) - { - string[] messageUrlParsed = postKVP.Value.Split(','); - string mpdURL = messageUrlParsed[0]; - string policy = messageUrlParsed[1]; - string signature = messageUrlParsed[2]; - string kvp = messageUrlParsed[3]; - string mediaId = messageUrlParsed[4]; - string postId = messageUrlParsed[5]; - string? licenseURL = null; - string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); - if (pssh == null) - { - continue; - } + if (streams != null && streams.Streams.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams\n[/]"); - DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); - Dictionary drmHeaders = apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", "?type=widevine"); - string decryptionKey; - if (clientIdBlobMissing || devicePrivateKeyMissing) - { - decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - else - { - decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); - } - SinglePost.Medium mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.id == postKVP.Key); - SinglePost postInfo = post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(streams.Streams.Values.ToList()); + } + else + { + totalSize = streams.Streams.Count; + } - isNew = await downloadService.DownloadPostDRMVideo( - policy: policy, - signature: signature, - kvp: kvp, - url: mpdURL, - decryptionKey: decryptionKey, - folder: path, - lastModified: lastModified, - media_id: postKVP.Key, - api_type: "Posts", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? string.Empty, - postInfo: postInfo, - postMedia: mediaInfo, - author: postInfo?.author, - users: users); - } - else - { - try - { - SinglePost.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => (m?.id == postKVP.Key) == true); - SinglePost? postInfo = post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + DownloadResult result = null; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask($"[red]Downloading {streams.Streams.Count} Streams[/]", false); + task.MaxValue = totalSize; + task.StartTask(); - isNew = await downloadService.DownloadPostMedia( - url: postKVP.Value, - folder: path, - media_id: postKVP.Key, - api_type: "Posts", - progressReporter: new SpectreProgressReporter(task), - filenameFormat: configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? string.Empty, - postInfo: postInfo, - postMedia: mediaInfo, - author: postInfo?.author, - users: users); - } - catch - { - Console.WriteLine("Media was null"); - } - } - } - task.StopTask(); - }); - if (isNew) - { - AnsiConsole.Markup($"[red]Post {post_id} downloaded\n[/]"); - Log.Debug($"Post {post_id} downloaded"); - } - else - { - AnsiConsole.Markup($"[red]Post {post_id} already downloaded\n[/]"); - Log.Debug($"Post {post_id} already downloaded"); - } - } + SpectreProgressReporter progressReporter = new(task); + result = await downloadService.DownloadStreams(username, user.Value, path, + hasSelectedUsersKVP.Value, clientIdBlobMissing, devicePrivateKeyMissing, streams, + progressReporter); - public async Task<(bool IsExit, Dictionary? selectedUsers)> HandleUserSelection(Dictionary users, Dictionary lists) - { + task.StopTask(); + }); + + AnsiConsole.Markup( + $"[red]Streams Already Downloaded: {result.ExistingDownloads} New Streams Downloaded: {result.NewDownloads}[/]\n"); + return result.TotalCount; + } + + AnsiConsole.Markup("[red]Found 0 Streams\n[/]"); + return 0; + } + + private async Task DownloadPaidMessage(string username, + KeyValuePair> hasSelectedUsersKVP, int paidMessagesCount, string path, + long message_id) + { + IDBService dbService = serviceProvider.GetRequiredService(); + IConfigService configService = serviceProvider.GetRequiredService(); + IAuthService authService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + Log.Debug($"Calling DownloadPaidMessage - {username}"); + + AnsiConsole.Markup("[red]Getting Paid Message\n[/]"); + + SinglePaidMessageCollection singlePaidMessageCollection = + await apiService.GetPaidMessage($"/messages/{message_id.ToString()}", path); + int oldPreviewPaidMessagesCount = 0; + int newPreviewPaidMessagesCount = 0; + int oldPaidMessagesCount = 0; + int newPaidMessagesCount = 0; + if (singlePaidMessageCollection != null && singlePaidMessageCollection.PreviewSingleMessages.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages\n[/]"); + Log.Debug( + $"Found {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); + paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection + .PreviewSingleMessages.Values.ToList()); + } + else + { + totalSize = paidMessagesCount; + } + + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + // Define tasks + ProgressTask task = + ctx.AddTask( + $"[red]Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Paid Messages[/]", + false); + Log.Debug($"Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Paid Messages"); + task.MaxValue = totalSize; + task.StartTask(); + foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection + .PreviewSingleMessages) + { + bool isNew; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = paidMessageKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string messageId = messageUrlParsed[5]; + string? licenseURL = null; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh != null) + { + DateTime lastModified = + await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders( + $"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } + else + { + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } + + Medium? mediaInfo = + singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => + m.id == paidMessageKVP.Key); + SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadSingleMessagePreviewDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + hasSelectedUsersKVP.Value); + + if (isNew) + { + newPreviewPaidMessagesCount++; + } + else + { + oldPreviewPaidMessagesCount++; + } + } + } + else + { + Medium? mediaInfo = + singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m => + m.id == paidMessageKVP.Key); + SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadMessagePreviewMedia( + paidMessageKVP.Value, + path, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + hasSelectedUsersKVP.Value); + if (isNew) + { + newPreviewPaidMessagesCount++; + } + else + { + oldPreviewPaidMessagesCount++; + } + } + } + + task.StopTask(); + }); + AnsiConsole.Markup( + $"[red]Preview Paid Messages Already Downloaded: {oldPreviewPaidMessagesCount} New Preview Paid Messages Downloaded: {newPreviewPaidMessagesCount}[/]\n"); + Log.Debug( + $"Preview Paid Messages Already Downloaded: {oldPreviewPaidMessagesCount} New Preview Paid Messages Downloaded: {newPreviewPaidMessagesCount}"); + } + + if (singlePaidMessageCollection != null && singlePaidMessageCollection.SingleMessages.Count > 0) + { + AnsiConsole.Markup( + $"[red]Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages\n[/]"); + Log.Debug( + $"Found {singlePaidMessageCollection.SingleMessages.Count} Media from {singlePaidMessageCollection.SingleMessageObjects.Count} Paid Messages"); + paidMessagesCount = singlePaidMessageCollection.SingleMessages.Count; + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(singlePaidMessageCollection.SingleMessages + .Values.ToList()); + } + else + { + totalSize = paidMessagesCount; + } + + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + // Define tasks + ProgressTask task = + ctx.AddTask( + $"[red]Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages[/]", + false); + Log.Debug($"Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages"); + task.MaxValue = totalSize; + task.StartTask(); + foreach (KeyValuePair paidMessageKVP in singlePaidMessageCollection.SingleMessages) + { + bool isNew; + if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = paidMessageKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string messageId = messageUrlParsed[5]; + string? licenseURL = null; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh != null) + { + DateTime lastModified = + await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders( + $"/api2/v2/users/media/{mediaId}/drm/message/{messageId}", "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } + else + { + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", + pssh); + } + + Medium? mediaInfo = + singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => + m.id == paidMessageKVP.Key); + SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadSinglePurchasedMessageDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + hasSelectedUsersKVP.Value); + + if (isNew) + { + newPaidMessagesCount++; + } + else + { + oldPaidMessagesCount++; + } + } + } + else + { + Medium? mediaInfo = + singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m => + m.id == paidMessageKVP.Key); + SingleMessage? messageInfo = + singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p => + p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadSinglePurchasedMedia( + paidMessageKVP.Value, + path, + paidMessageKVP.Key, + "Messages", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PaidMessageFileNameFormat ?? string.Empty, + messageInfo, + mediaInfo, + messageInfo?.fromUser, + hasSelectedUsersKVP.Value); + if (isNew) + { + newPaidMessagesCount++; + } + else + { + oldPaidMessagesCount++; + } + } + } + + task.StopTask(); + }); + AnsiConsole.Markup( + $"[red]Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}[/]\n"); + Log.Debug( + $"Paid Messages Already Downloaded: {oldPaidMessagesCount} New Paid Messages Downloaded: {newPaidMessagesCount}"); + } + else + { + AnsiConsole.Markup("[red]Found 0 Paid Messages\n[/]"); + Log.Debug("Found 0 Paid Messages"); + } + + return paidMessagesCount; + } + + private async Task DownloadSinglePost(string username, long post_id, string path, Dictionary users) + { + IConfigService configService = serviceProvider.GetRequiredService(); + IAPIService apiService = serviceProvider.GetRequiredService(); + IDownloadService downloadService = serviceProvider.GetRequiredService(); + + Log.Debug($"Calling DownloadSinglePost - {post_id.ToString()}"); + + AnsiConsole.Markup("[red]Getting Post\n[/]"); + SinglePostCollection post = await apiService.GetPost($"/posts/{post_id.ToString()}", path); + if (post == null) + { + AnsiConsole.Markup("[red]Couldn't find post\n[/]"); + Log.Debug("Couldn't find post"); + return; + } + + long totalSize = 0; + if (configService.CurrentConfig.ShowScrapeSize) + { + totalSize = await downloadService.CalculateTotalFileSize(post.SinglePosts.Values.ToList()); + } + else + { + totalSize = post.SinglePosts.Count; + } + + bool isNew = false; + await AnsiConsole.Progress() + .Columns(GetProgressColumns(configService.CurrentConfig.ShowScrapeSize)) + .StartAsync(async ctx => + { + ProgressTask task = ctx.AddTask("[red]Downloading Post[/]", false); + task.MaxValue = totalSize; + task.StartTask(); + foreach (KeyValuePair postKVP in post.SinglePosts) + { + if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files")) + { + string[] messageUrlParsed = postKVP.Value.Split(','); + string mpdURL = messageUrlParsed[0]; + string policy = messageUrlParsed[1]; + string signature = messageUrlParsed[2]; + string kvp = messageUrlParsed[3]; + string mediaId = messageUrlParsed[4]; + string postId = messageUrlParsed[5]; + string? licenseURL = null; + string? pssh = await apiService.GetDRMMPDPSSH(mpdURL, policy, signature, kvp); + if (pssh == null) + { + continue; + } + + DateTime lastModified = await apiService.GetDRMMPDLastModified(mpdURL, policy, signature, kvp); + Dictionary drmHeaders = + apiService.GetDynamicHeaders($"/api2/v2/users/media/{mediaId}/drm/post/{postId}", + "?type=widevine"); + string decryptionKey; + if (clientIdBlobMissing || devicePrivateKeyMissing) + { + decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", + pssh); + } + else + { + decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, + $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", + pssh); + } + + SinglePost.Medium mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.id == postKVP.Key); + SinglePost postInfo = + post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadPostDRMVideo( + policy, + signature, + kvp, + mpdURL, + decryptionKey, + path, + lastModified, + postKVP.Key, + "Posts", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? + string.Empty, + postInfo, + mediaInfo, + postInfo?.author, + users); + } + else + { + try + { + SinglePost.Medium? mediaInfo = + post.SinglePostMedia.FirstOrDefault(m => m?.id == postKVP.Key); + SinglePost? postInfo = + post.SinglePostObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); + + isNew = await downloadService.DownloadPostMedia( + postKVP.Value, + path, + postKVP.Key, + "Posts", + new SpectreProgressReporter(task), + configService.CurrentConfig.GetCreatorFileNameFormatConfig(username) + .PostFileNameFormat ?? string.Empty, + postInfo, + mediaInfo, + postInfo?.author, + users); + } + catch + { + Console.WriteLine("Media was null"); + } + } + } + + task.StopTask(); + }); + if (isNew) + { + AnsiConsole.Markup($"[red]Post {post_id} downloaded\n[/]"); + Log.Debug($"Post {post_id} downloaded"); + } + else + { + AnsiConsole.Markup($"[red]Post {post_id} already downloaded\n[/]"); + Log.Debug($"Post {post_id} already downloaded"); + } + } + + public async Task<(bool IsExit, Dictionary? selectedUsers)> HandleUserSelection( + Dictionary users, Dictionary lists) + { IConfigService configService = serviceProvider.GetRequiredService(); IAuthService authService = serviceProvider.GetRequiredService(); IAPIService apiService = serviceProvider.GetRequiredService(); ILoggingService loggingService = serviceProvider.GetRequiredService(); - bool hasSelectedUsers = false; - Dictionary selectedUsers = new Dictionary(); - Entities.Config currentConfig = configService.CurrentConfig!; + bool hasSelectedUsers = false; + Dictionary selectedUsers = new(); + Config currentConfig = configService.CurrentConfig!; - while (!hasSelectedUsers) - { - var mainMenuOptions = GetMainMenuOptions(users, lists); + while (!hasSelectedUsers) + { + List mainMenuOptions = GetMainMenuOptions(users, lists); - var mainMenuSelection = AnsiConsole.Prompt( - new SelectionPrompt() - .Title("[red]Select Accounts to Scrape | Select All = All Accounts | List = Download content from users on List | Custom = Specific Account(s)[/]") - .AddChoices(mainMenuOptions) - ); + string mainMenuSelection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title( + "[red]Select Accounts to Scrape | Select All = All Accounts | List = Download content from users on List | Custom = Specific Account(s)[/]") + .AddChoices(mainMenuOptions) + ); - switch (mainMenuSelection) - { - case "[red]Select All[/]": - selectedUsers = users; - hasSelectedUsers = true; - break; - case "[red]List[/]": - while (true) - { - var listSelectionPrompt = new MultiSelectionPrompt(); - listSelectionPrompt.Title = "[red]Select List[/]"; - listSelectionPrompt.PageSize = 10; - listSelectionPrompt.AddChoice("[red]Go Back[/]"); - foreach (string key in lists.Keys.Select(k => $"[red]{k}[/]").ToList()) - { - listSelectionPrompt.AddChoice(key); - } - var listSelection = AnsiConsole.Prompt(listSelectionPrompt); + switch (mainMenuSelection) + { + case "[red]Select All[/]": + selectedUsers = users; + hasSelectedUsers = true; + break; + case "[red]List[/]": + while (true) + { + MultiSelectionPrompt listSelectionPrompt = new(); + listSelectionPrompt.Title = "[red]Select List[/]"; + listSelectionPrompt.PageSize = 10; + listSelectionPrompt.AddChoice("[red]Go Back[/]"); + foreach (string key in lists.Keys.Select(k => $"[red]{k}[/]").ToList()) + { + listSelectionPrompt.AddChoice(key); + } - if (listSelection.Contains("[red]Go Back[/]")) - { - break; // Go back to the main menu - } - else - { - hasSelectedUsers = true; - List listUsernames = new(); - foreach (var item in listSelection) - { - long listId = lists[item.Replace("[red]", "").Replace("[/]", "")]; - List usernames = await apiService.GetListUsers($"/lists/{listId}/users"); - foreach (string user in usernames) - { - listUsernames.Add(user); - } - } - selectedUsers = users.Where(x => listUsernames.Contains($"{x.Key}")).Distinct().ToDictionary(x => x.Key, x => x.Value); - AnsiConsole.Markup(string.Format("[red]Downloading from List(s): {0}[/]", string.Join(", ", listSelection))); - break; - } - } - break; - case "[red]Custom[/]": - while (true) - { - var selectedNamesPrompt = new MultiSelectionPrompt(); - selectedNamesPrompt.MoreChoicesText("[grey](Move up and down to reveal more choices)[/]"); - selectedNamesPrompt.InstructionsText("[grey](Press to select, to accept)[/]\n[grey](Press A-Z to easily navigate the list)[/]"); - selectedNamesPrompt.Title("[red]Select users[/]"); - selectedNamesPrompt.PageSize(10); - selectedNamesPrompt.AddChoice("[red]Go Back[/]"); - foreach (string key in users.Keys.OrderBy(k => k).Select(k => $"[red]{k}[/]").ToList()) - { - selectedNamesPrompt.AddChoice(key); - } - var userSelection = AnsiConsole.Prompt(selectedNamesPrompt); - if (userSelection.Contains("[red]Go Back[/]")) - { - break; // Go back to the main menu - } - else - { - hasSelectedUsers = true; - selectedUsers = users.Where(x => userSelection.Contains($"[red]{x.Key}[/]")).ToDictionary(x => x.Key, x => x.Value); - break; - } - } - break; - case "[red]Download Single Post[/]": - return (true, new Dictionary { { "SinglePost", 0 } }); - case "[red]Download Single Paid Message[/]": - return (true, new Dictionary { { "SingleMessage", 0 } }); - case "[red]Download Purchased Tab[/]": - return (true, new Dictionary { { "PurchasedTab", 0 } }); - case "[red]Edit config.conf[/]": - while (true) - { - if (currentConfig == null) - currentConfig = new Entities.Config(); + List listSelection = AnsiConsole.Prompt(listSelectionPrompt); - var choices = new List<(string choice, bool isSelected)> - { - ("[red]Go Back[/]", false) - }; + if (listSelection.Contains("[red]Go Back[/]")) + { + break; // Go back to the main menu + } - foreach(var propInfo in typeof(Entities.Config).GetProperties()) - { - var attr = propInfo.GetCustomAttribute(); - if(attr != null) - { - string itemLabel = $"[red]{propInfo.Name}[/]"; - choices.Add(new(itemLabel, (bool)propInfo.GetValue(currentConfig)!)); - } - } + hasSelectedUsers = true; + List listUsernames = new(); + foreach (string item in listSelection) + { + long listId = lists[item.Replace("[red]", "").Replace("[/]", "")]; + List usernames = await apiService.GetListUsers($"/lists/{listId}/users"); + foreach (string user in usernames) + { + listUsernames.Add(user); + } + } - MultiSelectionPrompt multiSelectionPrompt = new MultiSelectionPrompt() - .Title("[red]Edit config.conf[/]") - .PageSize(25); + selectedUsers = users.Where(x => listUsernames.Contains($"{x.Key}")).Distinct() + .ToDictionary(x => x.Key, x => x.Value); + AnsiConsole.Markup(string.Format("[red]Downloading from List(s): {0}[/]", + string.Join(", ", listSelection))); + break; + } - foreach (var choice in choices) - { - multiSelectionPrompt.AddChoices(choice.choice, (selectionItem) => { if (choice.isSelected) selectionItem.Select(); }); - } + break; + case "[red]Custom[/]": + while (true) + { + MultiSelectionPrompt selectedNamesPrompt = new(); + selectedNamesPrompt.MoreChoicesText("[grey](Move up and down to reveal more choices)[/]"); + selectedNamesPrompt.InstructionsText( + "[grey](Press to select, to accept)[/]\n[grey](Press A-Z to easily navigate the list)[/]"); + selectedNamesPrompt.Title("[red]Select users[/]"); + selectedNamesPrompt.PageSize(10); + selectedNamesPrompt.AddChoice("[red]Go Back[/]"); + foreach (string key in users.Keys.OrderBy(k => k).Select(k => $"[red]{k}[/]").ToList()) + { + selectedNamesPrompt.AddChoice(key); + } - var configOptions = AnsiConsole.Prompt(multiSelectionPrompt); + List userSelection = AnsiConsole.Prompt(selectedNamesPrompt); + if (userSelection.Contains("[red]Go Back[/]")) + { + break; // Go back to the main menu + } - if (configOptions.Contains("[red]Go Back[/]")) - { - break; - } + hasSelectedUsers = true; + selectedUsers = users.Where(x => userSelection.Contains($"[red]{x.Key}[/]")) + .ToDictionary(x => x.Key, x => x.Value); + break; + } - bool configChanged = false; + break; + case "[red]Download Single Post[/]": + return (true, new Dictionary { { "SinglePost", 0 } }); + case "[red]Download Single Paid Message[/]": + return (true, new Dictionary { { "SingleMessage", 0 } }); + case "[red]Download Purchased Tab[/]": + return (true, new Dictionary { { "PurchasedTab", 0 } }); + case "[red]Edit config.conf[/]": + while (true) + { + if (currentConfig == null) + { + currentConfig = new Config(); + } - Entities.Config newConfig = new Entities.Config(); - foreach (var propInfo in typeof(Entities.Config).GetProperties()) - { - var attr = propInfo.GetCustomAttribute(); - if (attr != null) - { - // - // Get the new choice from the selection - // - string itemLabel = $"[red]{propInfo.Name}[/]"; - var newValue = configOptions.Contains(itemLabel); - var oldValue = choices.Where(c => c.choice == itemLabel).Select(c => c.isSelected).First(); - propInfo.SetValue(newConfig, newValue); + List<(string choice, bool isSelected)> choices = new() { ("[red]Go Back[/]", false) }; - if (newValue != oldValue) - configChanged = true; - } - else - { - // - // Reassign any non toggleable values - // - propInfo.SetValue(newConfig, propInfo.GetValue(currentConfig)); - } - } + foreach (PropertyInfo propInfo in typeof(Config).GetProperties()) + { + ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute(); + if (attr != null) + { + string itemLabel = $"[red]{propInfo.Name}[/]"; + choices.Add(new ValueTuple(itemLabel, + (bool)propInfo.GetValue(currentConfig)!)); + } + } - configService.UpdateConfig(newConfig); - await configService.SaveConfigurationAsync(); + MultiSelectionPrompt multiSelectionPrompt = new MultiSelectionPrompt() + .Title("[red]Edit config.conf[/]") + .PageSize(25); - currentConfig = newConfig; - if (configChanged) - { - return (true, new Dictionary { { "ConfigChanged", 0 } }); - } - break; - } - break; - case "[red]Change logging level[/]": - while (true) - { - var choices = new List<(string choice, bool isSelected)> - { - ("[red]Go Back[/]", false) - }; + foreach ((string choice, bool isSelected) choice in choices) + { + multiSelectionPrompt.AddChoices(choice.choice, selectionItem => + { + if (choice.isSelected) + { + selectionItem.Select(); + } + }); + } - foreach (string name in typeof(LoggingLevel).GetEnumNames()) - { - string itemLabel = $"[red]{name}[/]"; - choices.Add(new(itemLabel, name == loggingService.GetCurrentLoggingLevel().ToString())); - } + List configOptions = AnsiConsole.Prompt(multiSelectionPrompt); - SelectionPrompt selectionPrompt = new SelectionPrompt() - .Title("[red]Select logging level[/]") - .PageSize(25); + if (configOptions.Contains("[red]Go Back[/]")) + { + break; + } - foreach (var choice in choices) - { - selectionPrompt.AddChoice(choice.choice); - } + bool configChanged = false; - string levelOption = AnsiConsole.Prompt(selectionPrompt); + Config newConfig = new(); + foreach (PropertyInfo propInfo in typeof(Config).GetProperties()) + { + ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute(); + if (attr != null) + { + // + // Get the new choice from the selection + // + string itemLabel = $"[red]{propInfo.Name}[/]"; + bool newValue = configOptions.Contains(itemLabel); + bool oldValue = choices.Where(c => c.choice == itemLabel).Select(c => c.isSelected) + .First(); + propInfo.SetValue(newConfig, newValue); - if (levelOption.Contains("[red]Go Back[/]")) - { - break; - } + if (newValue != oldValue) + { + configChanged = true; + } + } + else + { + // + // Reassign any non toggleable values + // + propInfo.SetValue(newConfig, propInfo.GetValue(currentConfig)); + } + } - levelOption = levelOption.Replace("[red]", "").Replace("[/]", ""); - LoggingLevel newLogLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), levelOption, true); + configService.UpdateConfig(newConfig); + await configService.SaveConfigurationAsync(); - Log.Debug($"Logging level changed to: {levelOption}"); + currentConfig = newConfig; + if (configChanged) + { + return (true, new Dictionary { { "ConfigChanged", 0 } }); + } - bool configChanged = false; + break; + } - Entities.Config newConfig = new Entities.Config(); + break; + case "[red]Change logging level[/]": + while (true) + { + List<(string choice, bool isSelected)> choices = new() { ("[red]Go Back[/]", false) }; - newConfig = currentConfig; + foreach (string name in typeof(LoggingLevel).GetEnumNames()) + { + string itemLabel = $"[red]{name}[/]"; + choices.Add(new ValueTuple(itemLabel, + name == loggingService.GetCurrentLoggingLevel().ToString())); + } - newConfig.LoggingLevel = newLogLevel; + SelectionPrompt selectionPrompt = new SelectionPrompt() + .Title("[red]Select logging level[/]") + .PageSize(25); - currentConfig = newConfig; + foreach ((string choice, bool isSelected) choice in choices) + { + selectionPrompt.AddChoice(choice.choice); + } - configService.UpdateConfig(newConfig); - await configService.SaveConfigurationAsync(); + string levelOption = AnsiConsole.Prompt(selectionPrompt); - if (configChanged) - { - return (true, new Dictionary { { "ConfigChanged", 0 } }); - } + if (levelOption.Contains("[red]Go Back[/]")) + { + break; + } - break; - } - break; - case "[red]Logout and Exit[/]": - if (Directory.Exists("chrome-data")) - { - Log.Information("Deleting chrome-data folder"); - Directory.Delete("chrome-data", true); - } - if (File.Exists("auth.json")) - { - Log.Information("Deleting auth.json"); - File.Delete("auth.json"); - } - return (false, null); // Return false to indicate exit - case "[red]Exit[/]": - return (false, null); // Return false to indicate exit - } - } + levelOption = levelOption.Replace("[red]", "").Replace("[/]", ""); + LoggingLevel newLogLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), levelOption, true); - return (true, selectedUsers); // Return true to indicate selected users - } + Log.Debug($"Logging level changed to: {levelOption}"); - public static List GetMainMenuOptions(Dictionary users, Dictionary lists) - { - if (lists.Count > 0) - { - return new List - { - "[red]Select All[/]", - "[red]List[/]", - "[red]Custom[/]", - "[red]Download Single Post[/]", - "[red]Download Single Paid Message[/]", - "[red]Download Purchased Tab[/]", - "[red]Edit config.conf[/]", - "[red]Change logging level[/]", - "[red]Logout and Exit[/]", - "[red]Exit[/]" - }; - } - else - { - return new List - { - "[red]Select All[/]", - "[red]Custom[/]", - "[red]Download Single Post[/]", - "[red]Download Single Paid Message[/]", - "[red]Download Purchased Tab[/]", - "[red]Edit config.conf[/]", - "[red]Change logging level[/]", - "[red]Logout and Exit[/]", - "[red]Exit[/]" - }; - } - } + bool configChanged = false; - static bool ValidateFilePath(string path) - { - char[] invalidChars = System.IO.Path.GetInvalidPathChars(); - char[] foundInvalidChars = path.Where(c => invalidChars.Contains(c)).ToArray(); + Config newConfig = new(); - if (foundInvalidChars.Any()) - { - AnsiConsole.Markup($"[red]Invalid characters found in path {path}:[/] {string.Join(", ", foundInvalidChars)}\n"); - return false; - } + newConfig = currentConfig; - if (!System.IO.File.Exists(path)) - { - if (System.IO.Directory.Exists(path)) - { - AnsiConsole.Markup($"[red]The provided path {path} improperly points to a directory and not a file.[/]\n"); - } - else - { - AnsiConsole.Markup($"[red]The provided path {path} does not exist or is not accessible.[/]\n"); - } - return false; - } + newConfig.LoggingLevel = newLogLevel; - return true; - } + currentConfig = newConfig; - static ProgressColumn[] GetProgressColumns(bool showScrapeSize) - { - List progressColumns; - if (showScrapeSize) - { - progressColumns = new List() - { - new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn(), new DownloadedColumn(), new RemainingTimeColumn() - }; - } - else - { - progressColumns = new List() - { - new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn() - }; - } - return progressColumns.ToArray(); - } + configService.UpdateConfig(newConfig); + await configService.SaveConfigurationAsync(); - public static string? GetFullPath(string filename) - { - if (File.Exists(filename)) - { - return Path.GetFullPath(filename); - } + if (configChanged) + { + return (true, new Dictionary { { "ConfigChanged", 0 } }); + } - var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - foreach (var path in pathEnv.Split(Path.PathSeparator)) - { - var fullPath = Path.Combine(path, filename); - if (File.Exists(fullPath)) - { - return fullPath; - } - } - return null; - } + break; + } - public static void ValidateCookieString(Auth auth) - { - string pattern = @"(auth_id=\d+)|(sess=[^;]+)"; - var matches = Regex.Matches(auth.COOKIE, pattern); + break; + case "[red]Logout and Exit[/]": + if (Directory.Exists("chrome-data")) + { + Log.Information("Deleting chrome-data folder"); + Directory.Delete("chrome-data", true); + } - string output = string.Join("; ", matches); + if (File.Exists("auth.json")) + { + Log.Information("Deleting auth.json"); + File.Delete("auth.json"); + } - if (!output.EndsWith(";")) - { - output += ";"; - } + return (false, null); // Return false to indicate exit + case "[red]Exit[/]": + return (false, null); // Return false to indicate exit + } + } - if(auth.COOKIE.Trim() != output.Trim()) - { - auth.COOKIE = output; - string newAuthString = JsonConvert.SerializeObject(auth, Formatting.Indented); - File.WriteAllText("auth.json", newAuthString); - } - } + return (true, selectedUsers); // Return true to indicate selected users + } + + public static List GetMainMenuOptions(Dictionary users, Dictionary lists) + { + if (lists.Count > 0) + { + return new List + { + "[red]Select All[/]", + "[red]List[/]", + "[red]Custom[/]", + "[red]Download Single Post[/]", + "[red]Download Single Paid Message[/]", + "[red]Download Purchased Tab[/]", + "[red]Edit config.conf[/]", + "[red]Change logging level[/]", + "[red]Logout and Exit[/]", + "[red]Exit[/]" + }; + } + + return new List + { + "[red]Select All[/]", + "[red]Custom[/]", + "[red]Download Single Post[/]", + "[red]Download Single Paid Message[/]", + "[red]Download Purchased Tab[/]", + "[red]Edit config.conf[/]", + "[red]Change logging level[/]", + "[red]Logout and Exit[/]", + "[red]Exit[/]" + }; + } + + private static bool ValidateFilePath(string path) + { + char[] invalidChars = Path.GetInvalidPathChars(); + char[] foundInvalidChars = path.Where(c => invalidChars.Contains(c)).ToArray(); + + if (foundInvalidChars.Any()) + { + AnsiConsole.Markup( + $"[red]Invalid characters found in path {path}:[/] {string.Join(", ", foundInvalidChars)}\n"); + return false; + } + + if (!File.Exists(path)) + { + if (Directory.Exists(path)) + { + AnsiConsole.Markup( + $"[red]The provided path {path} improperly points to a directory and not a file.[/]\n"); + } + else + { + AnsiConsole.Markup($"[red]The provided path {path} does not exist or is not accessible.[/]\n"); + } + + return false; + } + + return true; + } + + private static ProgressColumn[] GetProgressColumns(bool showScrapeSize) + { + List progressColumns; + if (showScrapeSize) + { + progressColumns = new List + { + new TaskDescriptionColumn(), + new ProgressBarColumn(), + new PercentageColumn(), + new DownloadedColumn(), + new RemainingTimeColumn() + }; + } + else + { + progressColumns = new List + { + new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn() + }; + } + + return progressColumns.ToArray(); + } + + public static string? GetFullPath(string filename) + { + if (File.Exists(filename)) + { + return Path.GetFullPath(filename); + } + + string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + foreach (string path in pathEnv.Split(Path.PathSeparator)) + { + string fullPath = Path.Combine(path, filename); + if (File.Exists(fullPath)) + { + return fullPath; + } + } + + return null; + } + + public static void ValidateCookieString(Auth auth) + { + string pattern = @"(auth_id=\d+)|(sess=[^;]+)"; + MatchCollection matches = Regex.Matches(auth.COOKIE, pattern); + + string output = string.Join("; ", matches); + + if (!output.EndsWith(";")) + { + output += ";"; + } + + if (auth.COOKIE.Trim() != output.Trim()) + { + auth.COOKIE = output; + string newAuthString = JsonConvert.SerializeObject(auth, Formatting.Indented); + File.WriteAllText("auth.json", newAuthString); + } + } } diff --git a/OF DL/References/Spectre.Console.deps.json b/OF DL/References/Spectre.Console.deps.json index 5470288..3b1ab90 100644 --- a/OF DL/References/Spectre.Console.deps.json +++ b/OF DL/References/Spectre.Console.deps.json @@ -1,112 +1,112 @@ { - "runtimeTarget": { - "name": ".NETCoreApp,Version=v7.0", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETCoreApp,Version=v7.0": { - "Spectre.Console/0.0.0-preview.0": { - "dependencies": { - "Microsoft.SourceLink.GitHub": "1.1.1", - "MinVer": "4.2.0", - "Roslynator.Analyzers": "4.1.2", - "StyleCop.Analyzers": "1.2.0-beta.435", - "System.Memory": "4.5.5", - "Wcwidth.Sources": "1.0.0" + "runtimeTarget": { + "name": ".NETCoreApp,Version=v7.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v7.0": { + "Spectre.Console/0.0.0-preview.0": { + "dependencies": { + "Microsoft.SourceLink.GitHub": "1.1.1", + "MinVer": "4.2.0", + "Roslynator.Analyzers": "4.1.2", + "StyleCop.Analyzers": "1.2.0-beta.435", + "System.Memory": "4.5.5", + "Wcwidth.Sources": "1.0.0" + }, + "runtime": { + "Spectre.Console.dll": {} + } + }, + "Microsoft.Build.Tasks.Git/1.1.1": {}, + "Microsoft.SourceLink.Common/1.1.1": {}, + "Microsoft.SourceLink.GitHub/1.1.1": { + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.1", + "Microsoft.SourceLink.Common": "1.1.1" + } + }, + "MinVer/4.2.0": {}, + "Roslynator.Analyzers/4.1.2": {}, + "StyleCop.Analyzers/1.2.0-beta.435": { + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.435" + } + }, + "StyleCop.Analyzers.Unstable/1.2.0.435": {}, + "System.Memory/4.5.5": {}, + "Wcwidth.Sources/1.0.0": {} + } + }, + "libraries": { + "Spectre.Console/0.0.0-preview.0": { + "type": "project", + "serviceable": false, + "sha512": "" }, - "runtime": { - "Spectre.Console.dll": {} + "Microsoft.Build.Tasks.Git/1.1.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==", + "path": "microsoft.build.tasks.git/1.1.1", + "hashPath": "microsoft.build.tasks.git.1.1.1.nupkg.sha512" + }, + "Microsoft.SourceLink.Common/1.1.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==", + "path": "microsoft.sourcelink.common/1.1.1", + "hashPath": "microsoft.sourcelink.common.1.1.1.nupkg.sha512" + }, + "Microsoft.SourceLink.GitHub/1.1.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==", + "path": "microsoft.sourcelink.github/1.1.1", + "hashPath": "microsoft.sourcelink.github.1.1.1.nupkg.sha512" + }, + "MinVer/4.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Po4tv+sri1jsaebQ8F6+yD5ru9Gas5mR111F6HR2ULqwflvjjZXMstpeOc1GHMJeQa3g4E3b8MX8K2cShkuUAg==", + "path": "minver/4.2.0", + "hashPath": "minver.4.2.0.nupkg.sha512" + }, + "Roslynator.Analyzers/4.1.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-bNl3GRSBFjJymYnwq/IRDD9MOSZz9VKdGk9RsN0MWIXoSRnVKQv84f6s9nLE13y20lZgMZKlDqGw2uInBH4JgA==", + "path": "roslynator.analyzers/4.1.2", + "hashPath": "roslynator.analyzers.4.1.2.nupkg.sha512" + }, + "StyleCop.Analyzers/1.2.0-beta.435": { + "type": "package", + "serviceable": true, + "sha512": "sha512-TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==", + "path": "stylecop.analyzers/1.2.0-beta.435", + "hashPath": "stylecop.analyzers.1.2.0-beta.435.nupkg.sha512" + }, + "StyleCop.Analyzers.Unstable/1.2.0.435": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg==", + "path": "stylecop.analyzers.unstable/1.2.0.435", + "hashPath": "stylecop.analyzers.unstable.1.2.0.435.nupkg.sha512" + }, + "System.Memory/4.5.5": { + "type": "package", + "serviceable": true, + "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "path": "system.memory/4.5.5", + "hashPath": "system.memory.4.5.5.nupkg.sha512" + }, + "Wcwidth.Sources/1.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-86tmwfGXRz7GJQXBnoTFoMvqSqd6irfkEkRzQFR54W/nweaR8cUvzY8x++z+B/+eUPSuqD2Ah1iPJHgthy4pzg==", + "path": "wcwidth.sources/1.0.0", + "hashPath": "wcwidth.sources.1.0.0.nupkg.sha512" } - }, - "Microsoft.Build.Tasks.Git/1.1.1": {}, - "Microsoft.SourceLink.Common/1.1.1": {}, - "Microsoft.SourceLink.GitHub/1.1.1": { - "dependencies": { - "Microsoft.Build.Tasks.Git": "1.1.1", - "Microsoft.SourceLink.Common": "1.1.1" - } - }, - "MinVer/4.2.0": {}, - "Roslynator.Analyzers/4.1.2": {}, - "StyleCop.Analyzers/1.2.0-beta.435": { - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.435" - } - }, - "StyleCop.Analyzers.Unstable/1.2.0.435": {}, - "System.Memory/4.5.5": {}, - "Wcwidth.Sources/1.0.0": {} } - }, - "libraries": { - "Spectre.Console/0.0.0-preview.0": { - "type": "project", - "serviceable": false, - "sha512": "" - }, - "Microsoft.Build.Tasks.Git/1.1.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==", - "path": "microsoft.build.tasks.git/1.1.1", - "hashPath": "microsoft.build.tasks.git.1.1.1.nupkg.sha512" - }, - "Microsoft.SourceLink.Common/1.1.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==", - "path": "microsoft.sourcelink.common/1.1.1", - "hashPath": "microsoft.sourcelink.common.1.1.1.nupkg.sha512" - }, - "Microsoft.SourceLink.GitHub/1.1.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==", - "path": "microsoft.sourcelink.github/1.1.1", - "hashPath": "microsoft.sourcelink.github.1.1.1.nupkg.sha512" - }, - "MinVer/4.2.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-Po4tv+sri1jsaebQ8F6+yD5ru9Gas5mR111F6HR2ULqwflvjjZXMstpeOc1GHMJeQa3g4E3b8MX8K2cShkuUAg==", - "path": "minver/4.2.0", - "hashPath": "minver.4.2.0.nupkg.sha512" - }, - "Roslynator.Analyzers/4.1.2": { - "type": "package", - "serviceable": true, - "sha512": "sha512-bNl3GRSBFjJymYnwq/IRDD9MOSZz9VKdGk9RsN0MWIXoSRnVKQv84f6s9nLE13y20lZgMZKlDqGw2uInBH4JgA==", - "path": "roslynator.analyzers/4.1.2", - "hashPath": "roslynator.analyzers.4.1.2.nupkg.sha512" - }, - "StyleCop.Analyzers/1.2.0-beta.435": { - "type": "package", - "serviceable": true, - "sha512": "sha512-TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==", - "path": "stylecop.analyzers/1.2.0-beta.435", - "hashPath": "stylecop.analyzers.1.2.0-beta.435.nupkg.sha512" - }, - "StyleCop.Analyzers.Unstable/1.2.0.435": { - "type": "package", - "serviceable": true, - "sha512": "sha512-ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg==", - "path": "stylecop.analyzers.unstable/1.2.0.435", - "hashPath": "stylecop.analyzers.unstable.1.2.0.435.nupkg.sha512" - }, - "System.Memory/4.5.5": { - "type": "package", - "serviceable": true, - "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", - "path": "system.memory/4.5.5", - "hashPath": "system.memory.4.5.5.nupkg.sha512" - }, - "Wcwidth.Sources/1.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-86tmwfGXRz7GJQXBnoTFoMvqSqd6irfkEkRzQFR54W/nweaR8cUvzY8x++z+B/+eUPSuqD2Ah1iPJHgthy4pzg==", - "path": "wcwidth.sources/1.0.0", - "hashPath": "wcwidth.sources.1.0.0.nupkg.sha512" - } - } -} \ No newline at end of file +} diff --git a/OF DL/Services/APIService.cs b/OF DL/Services/APIService.cs index 00a4870..8132cd7 100644 --- a/OF DL/Services/APIService.cs +++ b/OF DL/Services/APIService.cs @@ -1,3 +1,8 @@ +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OF_DL.Entities; @@ -10,14 +15,9 @@ using OF_DL.Entities.Purchased; using OF_DL.Entities.Stories; using OF_DL.Entities.Streams; using OF_DL.Enumerations; +using OF_DL.Widevine; using Serilog; using Spectre.Console; -using System.Globalization; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Xml.Linq; -using OF_DL.Widevine; using static OF_DL.Utils.HttpUtil; using Constants = OF_DL.Helpers.Constants; @@ -26,19 +26,14 @@ namespace OF_DL.Services; public class APIService(IAuthService authService, IConfigService configService, IDBService dbService) : IAPIService { + private const int MaxAttempts = 30; + private const int DelayBetweenAttempts = 3000; private static readonly JsonSerializerSettings m_JsonSerializerSettings; private static DateTime? cachedDynamicRulesExpiration; private static DynamicRules? cachedDynamicRules; - private const int MaxAttempts = 30; - private const int DelayBetweenAttempts = 3000; - static APIService() - { - m_JsonSerializerSettings = new() - { - MissingMemberHandling = MissingMemberHandling.Ignore - }; - } + static APIService() => + m_JsonSerializerSettings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }; public Dictionary GetDynamicHeaders(string path, string queryParams) @@ -83,7 +78,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } - DateTimeOffset dto = (DateTimeOffset)DateTime.UtcNow; + DateTimeOffset dto = DateTime.UtcNow; long timestamp = dto.ToUnixTimeMilliseconds(); string input = $"{root!.StaticParam}\n{timestamp}\n{path + queryParams}\n{authService.CurrentAuth.USER_ID}"; @@ -91,8 +86,9 @@ public class APIService(IAuthService authService, IConfigService configService, byte[] hashBytes = SHA1.HashData(inputBytes); string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); - var checksum = root.ChecksumIndexes.Aggregate(0, (current, number) => current + hashString[number]) + root.ChecksumConstant!.Value; - var sign = $"{root.Prefix}:{hashString}:{checksum.ToString("X").ToLower()}:{root.Suffix}"; + int checksum = root.ChecksumIndexes.Aggregate(0, (current, number) => current + hashString[number]) + + root.ChecksumConstant!.Value; + string sign = $"{root.Prefix}:{hashString}:{checksum.ToString("X").ToLower()}:{root.Suffix}"; Dictionary headers = new() { @@ -109,121 +105,23 @@ public class APIService(IAuthService authService, IConfigService configService, } - private async Task BuildHeaderAndExecuteRequests(Dictionary getParams, string endpoint, HttpClient client) - { - Log.Debug("Calling BuildHeaderAndExecuteRequests"); - - HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint); - using var response = await client.SendAsync(request); - response.EnsureSuccessStatusCode(); - string body = await response.Content.ReadAsStringAsync(); - - Log.Debug(body); - - return body; - } - - - private async Task BuildHttpRequestMessage(Dictionary getParams, string endpoint) - { - Log.Debug("Calling BuildHttpRequestMessage"); - - string queryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); - - Dictionary headers = GetDynamicHeaders($"/api2/v2{endpoint}", queryParams); - - HttpRequestMessage request = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{queryParams}"); - - Log.Debug($"Full request URL: {Constants.API_URL}{endpoint}{queryParams}"); - - foreach (KeyValuePair keyValuePair in headers) - { - request.Headers.Add(keyValuePair.Key, keyValuePair.Value); - } - - return request; - } - - private static double ConvertToUnixTimestampWithMicrosecondPrecision(DateTime date) - { - DateTime origin = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - TimeSpan diff = date.ToUniversalTime() - origin; - - return diff.TotalSeconds; // This gives the number of seconds. If you need milliseconds, use diff.TotalMilliseconds - } - - public static bool IsStringOnlyDigits(string input) - { - return input.All(char.IsDigit); - } - - - private HttpClient GetHttpClient() - { - var client = new HttpClient(); - if (configService.CurrentConfig?.Timeout != null && configService.CurrentConfig.Timeout > 0) - { - client.Timeout = TimeSpan.FromSeconds(configService.CurrentConfig.Timeout.Value); - } - return client; - } - - - /// - /// this one is used during initialization only - /// if the config option is not available then no modificatiotns will be done on the getParams - /// - /// - /// - /// - private static void UpdateGetParamsForDateSelection(Enumerations.DownloadDateSelection downloadDateSelection, ref Dictionary getParams, DateTime? dt) - { - //if (config.DownloadOnlySpecificDates && dt.HasValue) - //{ - if (dt.HasValue) - { - UpdateGetParamsForDateSelection( - downloadDateSelection, - ref getParams, - ConvertToUnixTimestampWithMicrosecondPrecision(dt.Value).ToString("0.000000", CultureInfo.InvariantCulture) - ); - } - //} - } - - private static void UpdateGetParamsForDateSelection(Enumerations.DownloadDateSelection downloadDateSelection, ref Dictionary getParams, string unixTimeStampInMicrosec) - { - switch (downloadDateSelection) - { - case Enumerations.DownloadDateSelection.before: - getParams["beforePublishTime"] = unixTimeStampInMicrosec; - break; - case Enumerations.DownloadDateSelection.after: - getParams["order"] = "publish_date_asc"; - getParams["afterPublishTime"] = unixTimeStampInMicrosec; - break; - } - } - - public async Task GetUserInfo(string endpoint) { Log.Debug($"Calling GetUserInfo: {endpoint}"); try { - Entities.User? user = new(); + User? user = new(); int post_limit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, - { "order", "publish_date_asc" } + { "limit", post_limit.ToString() }, { "order", "publish_date_asc" } }; HttpClient client = new(); HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint); - using var response = await client.SendAsync(request); + using HttpResponseMessage response = await client.SendAsync(request); if (!response.IsSuccessStatusCode) { @@ -231,8 +129,8 @@ public class APIService(IAuthService authService, IConfigService configService, } response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); - user = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); + string body = await response.Content.ReadAsStringAsync(); + user = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); return user; } catch (Exception ex) @@ -242,10 +140,13 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } @@ -256,16 +157,18 @@ public class APIService(IAuthService authService, IConfigService configService, HttpClient client = new(); HttpRequestMessage request = await BuildHttpRequestMessage(new Dictionary(), endpoint); - using var response = await client.SendAsync(request); + using HttpResponseMessage response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); + string body = await response.Content.ReadAsStringAsync(); //if the content creator doesnt exist, we get a 200 response, but the content isnt usable //so let's not throw an exception, since "content creator no longer exists" is handled elsewhere //which means we wont get loads of exceptions if (body.Equals("[]")) + { return null; + } JObject jObject = JObject.Parse(body); @@ -278,75 +181,13 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } - return null; - } - - public async Task?> GetAllSubscriptions(Dictionary getParams, string endpoint, bool includeRestricted) - { - try - { - Dictionary users = new(); - Subscriptions subscriptions = new(); - - Log.Debug("Calling GetAllSubscrptions"); - - string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); - - subscriptions = JsonConvert.DeserializeObject(body); - if (subscriptions != null && subscriptions.hasMore) - { - getParams["offset"] = subscriptions.list.Count.ToString(); - - while (true) - { - Subscriptions newSubscriptions = new(); - string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); - - if (!string.IsNullOrEmpty(loopbody) && (!loopbody.Contains("[]") || loopbody.Trim() != "[]")) - { - newSubscriptions = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); - } - else - { - break; - } - - subscriptions.list.AddRange(newSubscriptions.list); - if (!newSubscriptions.hasMore) - { - break; - } - getParams["offset"] = subscriptions.list.Count.ToString(); - } - } - - foreach (Subscriptions.List subscription in subscriptions.list) - { - if ((!(subscription.isRestricted ?? false) || ((subscription.isRestricted ?? false) && includeRestricted)) - && !users.ContainsKey(subscription.username)) - { - users.Add(subscription.username, subscription.id); - } - } - - return users; - } - catch (Exception ex) - { - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - if (ex.InnerException != null) - { - Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - } - } return null; } @@ -354,10 +195,7 @@ public class APIService(IAuthService authService, IConfigService configService, { Dictionary getParams = new() { - { "offset", "0" }, - { "limit", "50" }, - { "type", "active" }, - { "format", "infinite"} + { "offset", "0" }, { "limit", "50" }, { "type", "active" }, { "format", "infinite" } }; return await GetAllSubscriptions(getParams, endpoint, includeRestricted); @@ -366,13 +204,9 @@ public class APIService(IAuthService authService, IConfigService configService, public async Task?> GetExpiredSubscriptions(string endpoint, bool includeRestricted) { - Dictionary getParams = new() { - { "offset", "0" }, - { "limit", "50" }, - { "type", "expired" }, - { "format", "infinite"} + { "offset", "0" }, { "limit", "50" }, { "type", "expired" }, { "format", "infinite" } }; Log.Debug("Calling GetExpiredSubscriptions"); @@ -428,8 +262,8 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } - } + return lists; } catch (Exception ex) @@ -439,10 +273,13 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } @@ -454,16 +291,12 @@ public class APIService(IAuthService authService, IConfigService configService, try { int offset = 0; - Dictionary getParams = new() - { - { "offset", offset.ToString() }, - { "limit", "50" }, - }; + Dictionary getParams = new() { { "offset", offset.ToString() }, { "limit", "50" } }; List users = new(); while (true) { - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); if (body == null) { break; @@ -488,8 +321,8 @@ public class APIService(IAuthService authService, IConfigService configService, offset += 50; getParams["offset"] = Convert.ToString(offset); - } + return users; } catch (Exception ex) @@ -499,21 +332,23 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } public async Task> GetMedia(MediaType mediatype, - string endpoint, - string? username, - string folder, - List paid_post_ids) + string endpoint, + string? username, + string folder, + List paid_post_ids) { - Log.Debug($"Calling GetMedia - {username}"); try @@ -527,7 +362,6 @@ public class APIService(IAuthService authService, IConfigService configService, switch (mediatype) { - case MediaType.Stories: getParams = new Dictionary { @@ -540,57 +374,67 @@ public class APIService(IAuthService authService, IConfigService configService, case MediaType.Highlights: getParams = new Dictionary { - { "limit", limit.ToString() }, - { "offset", offset.ToString() }, - { "skip_users", "all" } + { "limit", limit.ToString() }, { "offset", offset.ToString() }, { "skip_users", "all" } }; break; } - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); if (mediatype == MediaType.Stories) { Log.Debug("Media Stories - " + endpoint); - var stories = JsonConvert.DeserializeObject>(body, m_JsonSerializerSettings) ?? new List(); + List stories = JsonConvert.DeserializeObject>(body, m_JsonSerializerSettings) ?? + new List(); foreach (Stories story in stories) { if (story.media[0].createdAt.HasValue) { - await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, story.media[0].createdAt.Value); + await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, + story.media[0].createdAt.Value); } else if (story.createdAt.HasValue) { - await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, story.createdAt.Value); + await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, + story.createdAt.Value); } else { await dbService.AddStory(folder, story.id, string.Empty, "0", false, false, DateTime.Now); } + if (story.media != null && story.media.Count > 0) { foreach (Stories.Medium medium in story.media) { - await dbService.AddMedia(folder, medium.id, story.id, medium.files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); + await dbService.AddMedia(folder, medium.id, story.id, medium.files.full.url, null, null, + null, "Stories", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, false, false, null); if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (medium.canView) { if (!return_urls.ContainsKey(medium.id)) @@ -605,7 +449,8 @@ public class APIService(IAuthService authService, IConfigService configService, else if (mediatype == MediaType.Highlights) { List highlight_ids = new(); - var highlights = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings) ?? new Highlights(); + Highlights highlights = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings) ?? + new Highlights(); if (highlights.hasMore) { @@ -617,7 +462,7 @@ public class APIService(IAuthService authService, IConfigService configService, Log.Debug("Media Highlights - " + endpoint); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newhighlights = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); highlights.list.AddRange(newhighlights.list); @@ -625,10 +470,12 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } + offset += 5; getParams["offset"] = offset.ToString(); } } + foreach (Highlights.List list in highlights.list) { if (!highlight_ids.Contains(list.id.ToString())) @@ -640,58 +487,73 @@ public class APIService(IAuthService authService, IConfigService configService, foreach (string highlight_id in highlight_ids) { HighlightMedia highlightMedia = new(); - Dictionary highlight_headers = GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, string.Empty); + Dictionary highlight_headers = + GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, string.Empty); HttpClient highlight_client = GetHttpClient(); - HttpRequestMessage highlight_request = new(HttpMethod.Get, $"https://onlyfans.com/api2/v2/stories/highlights/{highlight_id}"); + HttpRequestMessage highlight_request = new(HttpMethod.Get, + $"https://onlyfans.com/api2/v2/stories/highlights/{highlight_id}"); foreach (KeyValuePair keyValuePair in highlight_headers) { highlight_request.Headers.Add(keyValuePair.Key, keyValuePair.Value); } - using var highlightResponse = await highlight_client.SendAsync(highlight_request); + using HttpResponseMessage highlightResponse = await highlight_client.SendAsync(highlight_request); highlightResponse.EnsureSuccessStatusCode(); - var highlightBody = await highlightResponse.Content.ReadAsStringAsync(); - highlightMedia = JsonConvert.DeserializeObject(highlightBody, m_JsonSerializerSettings); + string highlightBody = await highlightResponse.Content.ReadAsStringAsync(); + highlightMedia = + JsonConvert.DeserializeObject(highlightBody, m_JsonSerializerSettings); if (highlightMedia != null) { foreach (HighlightMedia.Story item in highlightMedia.stories) { if (item.media[0].createdAt.HasValue) { - await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, item.media[0].createdAt.Value); + await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, + item.media[0].createdAt.Value); } else if (item.createdAt.HasValue) { - await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, item.createdAt.Value); + await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, + item.createdAt.Value); } else { - await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, DateTime.Now); + await dbService.AddStory(folder, item.id, string.Empty, "0", false, false, + DateTime.Now); } + if (item.media.Count > 0 && item.media[0].canView) { foreach (HighlightMedia.Medium medium in item.media) { - await dbService.AddMedia(folder, medium.id, item.id, item.media[0].files.full.url, null, null, null, "Stories", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), false, false, null); + await dbService.AddMedia(folder, medium.id, item.id, item.media[0].files.full.url, + null, null, null, "Stories", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, false, false, null); if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!return_urls.ContainsKey(medium.id)) { return_urls.Add(medium.id, item.media[0].files.full.url); @@ -712,15 +574,19 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } - public async Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, StatusContext ctx) + public async Task GetPaidPosts(string endpoint, string folder, string username, + List paid_post_ids, StatusContext ctx) { Log.Debug($"Calling GetPaidPosts - {username}"); @@ -738,7 +604,7 @@ public class APIService(IAuthService authService, IConfigService configService, { "author", username } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); paidPosts = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -748,10 +614,9 @@ public class APIService(IAuthService authService, IConfigService configService, getParams["offset"] = paidPosts.list.Count.ToString(); while (true) { - Purchased newPaidPosts = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newPaidPosts = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); paidPosts.list.AddRange(newPaidPosts.list); @@ -762,9 +627,9 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); } - } foreach (Purchased.List purchase in paidPosts.list) @@ -798,7 +663,12 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); + + await dbService.AddPost(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", + purchase.price != null && purchase.isOpened ? true : false, + purchase.isArchived.HasValue ? purchase.isArchived.Value : false, + purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); paidPostCollection.PaidPostObjects.Add(purchase); foreach (Messages.Medium medium in purchase.media) { @@ -811,49 +681,69 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (previewids.Count > 0) { bool has = previewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!has && medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, medium.files.full.url); paidPostCollection.PaidPostMedia.Add(medium); } } else if (!has && medium.canView && medium.files != null && medium.files.drm != null) { - if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - paidPostCollection.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + paidPostCollection.PaidPosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidPostCollection.PaidPostMedia.Add(medium); } - } } else { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); paidPostCollection.PaidPosts.Add(medium.id, medium.files.full.url); paidPostCollection.PaidPostMedia.Add(medium); } @@ -862,8 +752,14 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!paidPostCollection.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - paidPostCollection.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + paidPostCollection.PaidPosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidPostCollection.PaidPostMedia.Add(medium); } } @@ -871,6 +767,7 @@ public class APIService(IAuthService authService, IConfigService configService, } } } + return paidPostCollection; } catch (Exception ex) @@ -880,15 +777,19 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } - public async Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx) + public async Task GetPosts(string endpoint, string folder, List paid_post_ids, + StatusContext ctx) { Log.Debug($"Calling GetPosts - {endpoint}"); @@ -905,20 +806,21 @@ public class APIService(IAuthService authService, IConfigService configService, { "skip_users", "all" } }; - Enumerations.DownloadDateSelection downloadDateSelection = Enumerations.DownloadDateSelection.before; + DownloadDateSelection downloadDateSelection = DownloadDateSelection.before; DateTime? downloadAsOf = null; - if (configService.CurrentConfig.DownloadOnlySpecificDates && configService.CurrentConfig.CustomDate.HasValue) + if (configService.CurrentConfig.DownloadOnlySpecificDates && + configService.CurrentConfig.CustomDate.HasValue) { downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; downloadAsOf = configService.CurrentConfig.CustomDate; } else if (configService.CurrentConfig.DownloadPostsIncrementally) { - var mostRecentPostDate = await dbService.GetMostRecentPostDate(folder); + DateTime? mostRecentPostDate = await dbService.GetMostRecentPostDate(folder); if (mostRecentPostDate.HasValue) { - downloadDateSelection = Enumerations.DownloadDateSelection.after; + downloadDateSelection = DownloadDateSelection.after; downloadAsOf = mostRecentPostDate.Value.AddMinutes(-5); // Back track a little for a margin of error } } @@ -928,9 +830,10 @@ public class APIService(IAuthService authService, IConfigService configService, ref getParams, downloadAsOf); - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); posts = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - ctx.Status($"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]"); + ctx.Status( + $"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); if (posts != null && posts.hasMore) @@ -944,11 +847,12 @@ public class APIService(IAuthService authService, IConfigService configService, { Post newposts = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newposts = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); posts.list.AddRange(newposts.list); - ctx.Status($"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]"); + ctx.Status( + $"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); if (!newposts.hasMore) @@ -967,16 +871,19 @@ public class APIService(IAuthService authService, IConfigService configService, { if (configService.CurrentConfig.SkipAds) { - if (post.rawText != null && (post.rawText.Contains("#ad") || post.rawText.Contains("/trial/") || post.rawText.Contains("#announcement"))) + if (post.rawText != null && (post.rawText.Contains("#ad") || post.rawText.Contains("/trial/") || + post.rawText.Contains("#announcement"))) { continue; } - if (post.text != null && (post.text.Contains("#ad") || post.text.Contains("/trial/") || post.text.Contains("#announcement"))) + if (post.text != null && (post.text.Contains("#ad") || post.text.Contains("/trial/") || + post.text.Contains("#announcement"))) { continue; } } + List postPreviewIds = new(); if (post.preview != null && post.preview.Count > 0) { @@ -991,7 +898,10 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(folder, post.id, post.rawText != null ? post.rawText : string.Empty, post.price != null ? post.price.ToString() : "0", post.price != null && post.isOpened ? true : false, post.isArchived, post.postedAt); + + await dbService.AddPost(folder, post.id, post.rawText != null ? post.rawText : string.Empty, + post.price != null ? post.price : "0", post.price != null && post.isOpened ? true : false, + post.isArchived, post.postedAt); postCollection.PostObjects.Add(post); if (post.media != null && post.media.Count > 0) { @@ -1001,18 +911,22 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (medium.canView && medium.files?.drm == null) { bool has = paid_post_ids.Any(cus => cus.Equals(medium.id)); @@ -1022,7 +936,12 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!postCollection.Posts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, post.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, post.id, medium.files!.full.url, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); postCollection.Posts.Add(medium.id, medium.files!.full.url); postCollection.PostMedia.Add(medium); } @@ -1034,7 +953,12 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!postCollection.Posts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, post.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, post.id, medium.files.preview.url, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); postCollection.Posts.Add(medium.id, medium.files.preview.url); postCollection.PostMedia.Add(medium); } @@ -1048,8 +972,14 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!postCollection.Posts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, post.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); - postCollection.Posts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{post.id}"); + await dbService.AddMedia(folder, medium.id, post.id, medium.files.drm.manifest.dash, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); + postCollection.Posts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{post.id}"); postCollection.PostMedia.Add(medium); } } @@ -1067,12 +997,16 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } + public async Task GetPost(string endpoint, string folder) { Log.Debug($"Calling GetPost - {endpoint}"); @@ -1081,12 +1015,9 @@ public class APIService(IAuthService authService, IConfigService configService, { SinglePost singlePost = new(); SinglePostCollection singlePostCollection = new(); - Dictionary getParams = new() - { - { "skip_users", "all" } - }; + Dictionary getParams = new() { { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); singlePost = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); if (singlePost != null) @@ -1105,7 +1036,11 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(folder, singlePost.id, singlePost.text != null ? singlePost.text : string.Empty, singlePost.price != null ? singlePost.price.ToString() : "0", singlePost.price != null && singlePost.isOpened ? true : false, singlePost.isArchived, singlePost.postedAt); + + await dbService.AddPost(folder, singlePost.id, singlePost.text != null ? singlePost.text : string.Empty, + singlePost.price != null ? singlePost.price : "0", + singlePost.price != null && singlePost.isOpened ? true : false, singlePost.isArchived, + singlePost.postedAt); singlePostCollection.SinglePostObjects.Add(singlePost); if (singlePost.media != null && singlePost.media.Count > 0) { @@ -1115,18 +1050,22 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (medium.canView && medium.files?.drm == null) { switch (configService.CurrentConfig.DownloadVideoResolution) @@ -1136,25 +1075,38 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files!.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, singlePost.id, + medium.files!.full.url, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.files!.full.url); singlePostCollection.SinglePostMedia.Add(medium); } } + break; case VideoResolution._240: - if(medium.videoSources != null) + if (medium.videoSources != null) { if (!string.IsNullOrEmpty(medium.videoSources._240)) { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._240, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, medium.videoSources._240); + await dbService.AddMedia(folder, medium.id, singlePost.id, + medium.videoSources._240, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.id, + medium.videoSources._240); singlePostCollection.SinglePostMedia.Add(medium); } } } + break; case VideoResolution._720: if (medium.videoSources != null) @@ -1163,14 +1115,20 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.videoSources._720, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, medium.videoSources._720); + await dbService.AddMedia(folder, medium.id, singlePost.id, + medium.videoSources._720, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.id, + medium.videoSources._720); singlePostCollection.SinglePostMedia.Add(medium); } } } - break; + break; } } else if (medium.canView && medium.files != null && medium.files.drm != null) @@ -1179,8 +1137,14 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); - singlePostCollection.SinglePosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{singlePost.id}"); + await dbService.AddMedia(folder, medium.id, singlePost.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); + singlePostCollection.SinglePosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{singlePost.id}"); singlePostCollection.SinglePostMedia.Add(medium); } } @@ -1189,7 +1153,12 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!singlePostCollection.SinglePosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files.preview.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), postPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, singlePost.id, medium.files.preview.url, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + postPreviewIds.Contains(medium.id) ? true : false, false, null); singlePostCollection.SinglePosts.Add(medium.id, medium.files.preview.url); singlePostCollection.SinglePostMedia.Add(medium); } @@ -1207,14 +1176,18 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } - public async Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx) + public async Task GetStreams(string endpoint, string folder, List paid_post_ids, + StatusContext ctx) { Log.Debug($"Calling GetStreams - {endpoint}"); @@ -1231,8 +1204,9 @@ public class APIService(IAuthService authService, IConfigService configService, { "skip_users", "all" } }; - Enumerations.DownloadDateSelection downloadDateSelection = Enumerations.DownloadDateSelection.before; - if (configService.CurrentConfig.DownloadOnlySpecificDates && configService.CurrentConfig.CustomDate.HasValue) + DownloadDateSelection downloadDateSelection = DownloadDateSelection.before; + if (configService.CurrentConfig.DownloadOnlySpecificDates && + configService.CurrentConfig.CustomDate.HasValue) { downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; } @@ -1242,14 +1216,13 @@ public class APIService(IAuthService authService, IConfigService configService, ref getParams, configService.CurrentConfig.CustomDate); - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); streams = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("blue")); if (streams != null && streams.hasMore) { - UpdateGetParamsForDateSelection( downloadDateSelection, ref getParams, @@ -1259,7 +1232,7 @@ public class APIService(IAuthService authService, IConfigService configService, { Streams newstreams = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newstreams = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); streams.list.AddRange(newstreams.list); @@ -1294,7 +1267,10 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(folder, stream.id, stream.text != null ? stream.text : string.Empty, stream.price != null ? stream.price.ToString() : "0", stream.price != null && stream.isOpened ? true : false, stream.isArchived, stream.postedAt); + + await dbService.AddPost(folder, stream.id, stream.text != null ? stream.text : string.Empty, + stream.price != null ? stream.price : "0", stream.price != null && stream.isOpened ? true : false, + stream.isArchived, stream.postedAt); streamsCollection.StreamObjects.Add(stream); if (stream.media != null && stream.media.Count > 0) { @@ -1304,26 +1280,36 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (medium.canView && medium.files?.drm == null) { bool has = paid_post_ids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!has && medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (!streamsCollection.Streams.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, stream.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, stream.id, medium.files.full.url, null, + null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + streamPreviewIds.Contains(medium.id) ? true : false, false, null); streamsCollection.Streams.Add(medium.id, medium.files.full.url); streamsCollection.StreamMedia.Add(medium); } @@ -1336,8 +1322,14 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!streamsCollection.Streams.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, stream.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), streamPreviewIds.Contains((long)medium.id) ? true : false, false, null); - streamsCollection.Streams.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{stream.id}"); + await dbService.AddMedia(folder, medium.id, stream.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + streamPreviewIds.Contains(medium.id) ? true : false, false, null); + streamsCollection.Streams.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{stream.id}"); streamsCollection.StreamMedia.Add(medium); } } @@ -1355,10 +1347,13 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } @@ -1382,8 +1377,9 @@ public class APIService(IAuthService authService, IConfigService configService, { "counters", "1" } }; - Enumerations.DownloadDateSelection downloadDateSelection = Enumerations.DownloadDateSelection.before; - if (configService.CurrentConfig.DownloadOnlySpecificDates && configService.CurrentConfig.CustomDate.HasValue) + DownloadDateSelection downloadDateSelection = DownloadDateSelection.before; + if (configService.CurrentConfig.DownloadOnlySpecificDates && + configService.CurrentConfig.CustomDate.HasValue) { downloadDateSelection = configService.CurrentConfig.DownloadDateSelection; } @@ -1393,7 +1389,7 @@ public class APIService(IAuthService authService, IConfigService configService, ref getParams, configService.CurrentConfig.CustomDate); - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); archived = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1401,14 +1397,14 @@ public class APIService(IAuthService authService, IConfigService configService, if (archived != null && archived.hasMore) { UpdateGetParamsForDateSelection( - downloadDateSelection, - ref getParams, - archived.tailMarker); + downloadDateSelection, + ref getParams, + archived.tailMarker); while (true) { Archived newarchived = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newarchived = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); archived.list.AddRange(newarchived.list); @@ -1419,10 +1415,11 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } + UpdateGetParamsForDateSelection( - downloadDateSelection, - ref getParams, - newarchived.tailMarker); + downloadDateSelection, + ref getParams, + newarchived.tailMarker); } } @@ -1442,7 +1439,10 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(folder, archive.id, archive.text != null ? archive.text : string.Empty, archive.price != null ? archive.price.ToString() : "0", archive.price != null && archive.isOpened ? true : false, archive.isArchived, archive.postedAt); + + await dbService.AddPost(folder, archive.id, archive.text != null ? archive.text : string.Empty, + archive.price != null ? archive.price : "0", + archive.price != null && archive.isOpened ? true : false, archive.isArchived, archive.postedAt); archivedCollection.ArchivedPostObjects.Add(archive); if (archive.media != null && archive.media.Count > 0) { @@ -1452,23 +1452,33 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (!archivedCollection.ArchivedPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, archive.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, archive.id, medium.files.full.url, null, + null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); archivedCollection.ArchivedPosts.Add(medium.id, medium.files.full.url); archivedCollection.ArchivedPostMedia.Add(medium); } @@ -1477,8 +1487,14 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!archivedCollection.ArchivedPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, archive.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - archivedCollection.ArchivedPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{archive.id}"); + await dbService.AddMedia(folder, medium.id, archive.id, medium.files.drm.manifest.dash, + null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + archivedCollection.ArchivedPosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{archive.id}"); archivedCollection.ArchivedPostMedia.Add(medium); } } @@ -1495,10 +1511,13 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } @@ -1514,12 +1533,10 @@ public class APIService(IAuthService authService, IConfigService configService, int post_limit = 50; Dictionary getParams = new() { - { "limit", post_limit.ToString() }, - { "order", "desc" }, - { "skip_users", "all" } + { "limit", post_limit.ToString() }, { "order", "desc" }, { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); messages = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1531,7 +1548,7 @@ public class APIService(IAuthService authService, IConfigService configService, { Messages newmessages = new(); - var loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); newmessages = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); messages.list.AddRange(newmessages.list); @@ -1542,6 +1559,7 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } + getParams["id"] = newmessages.list[newmessages.list.Count - 1].id.ToString(); } } @@ -1555,6 +1573,7 @@ public class APIService(IAuthService authService, IConfigService configService, continue; } } + List messagePreviewIds = new(); if (list.previews != null && list.previews.Count > 0) { @@ -1569,35 +1588,52 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - if (!configService.CurrentConfig.IgnoreOwnMessages || list.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + + if (!configService.CurrentConfig.IgnoreOwnMessages || + list.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - await dbService.AddMessage(folder, list.id, list.text != null ? list.text : string.Empty, list.price != null ? list.price.ToString() : "0", list.canPurchaseReason == "opened" ? true : list.canPurchaseReason != "opened" ? false : (bool?)null ?? false, false, list.createdAt.HasValue ? list.createdAt.Value : DateTime.Now, list.fromUser != null && list.fromUser.id != null ? list.fromUser.id.Value : int.MinValue); + await dbService.AddMessage(folder, list.id, list.text != null ? list.text : string.Empty, + list.price != null ? list.price : "0", + list.canPurchaseReason == "opened" ? true : + list.canPurchaseReason != "opened" ? false : (bool?)null ?? false, false, + list.createdAt.HasValue ? list.createdAt.Value : DateTime.Now, + list.fromUser != null && list.fromUser.id != null ? list.fromUser.id.Value : int.MinValue); messageCollection.MessageObjects.Add(list); if (list.canPurchaseReason != "opened" && list.media != null && list.media.Count > 0) { foreach (Messages.Medium medium in list.media) { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, + null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, medium.files.full.url); messageCollection.MessageMedia.Add(medium); } @@ -1608,22 +1644,32 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - messageCollection.Messages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, + null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + messageCollection.Messages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); messageCollection.MessageMedia.Add(medium); } } @@ -1633,53 +1679,74 @@ public class APIService(IAuthService authService, IConfigService configService, { foreach (Messages.Medium medium in list.media) { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url) && messagePreviewIds.Contains(medium.id)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url) && messagePreviewIds.Contains(medium.id)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.full.url, null, + null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); messageCollection.Messages.Add(medium.id, medium.files.full.url); messageCollection.MessageMedia.Add(medium); } } - else if (medium.canView && medium.files != null && medium.files.drm != null && messagePreviewIds.Contains(medium.id)) + else if (medium.canView && medium.files != null && medium.files.drm != null && + messagePreviewIds.Contains(medium.id)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!messageCollection.Messages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - messageCollection.Messages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); + await dbService.AddMedia(folder, medium.id, list.id, medium.files.drm.manifest.dash, + null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + messageCollection.Messages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{list.id}"); messageCollection.MessageMedia.Add(medium); } } @@ -1697,10 +1764,13 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } @@ -1713,18 +1783,18 @@ public class APIService(IAuthService authService, IConfigService configService, SingleMessage message = new(); SinglePaidMessageCollection singlePaidMessageCollection = new(); int post_limit = 50; - Dictionary getParams = new() - { - { "limit", post_limit.ToString() }, - { "order", "desc" } - }; + Dictionary getParams = new() { { "limit", post_limit.ToString() }, { "order", "desc" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); message = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); - if (!configService.CurrentConfig.IgnoreOwnMessages || message.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + if (!configService.CurrentConfig.IgnoreOwnMessages || + message.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { - await dbService.AddMessage(folder, message.id, message.text != null ? message.text : string.Empty, message.price != null ? message.price.ToString() : "0", true, false, message.createdAt.HasValue ? message.createdAt.Value : DateTime.Now, message.fromUser != null && message.fromUser.id != null ? message.fromUser.id.Value : int.MinValue); + await dbService.AddMessage(folder, message.id, message.text != null ? message.text : string.Empty, + message.price != null ? message.price.ToString() : "0", true, false, + message.createdAt.HasValue ? message.createdAt.Value : DateTime.Now, + message.fromUser != null && message.fromUser.id != null ? message.fromUser.id.Value : int.MinValue); singlePaidMessageCollection.SingleMessageObjects.Add(message); List messagePreviewIds = new(); if (message.previews != null && message.previews.Count > 0) @@ -1745,20 +1815,24 @@ public class APIService(IAuthService authService, IConfigService configService, { foreach (Messages.Medium medium in message.media) { - if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && + medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; @@ -1766,25 +1840,34 @@ public class APIService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.SingleMessages.Add(medium.id, medium.files.full.url.ToString()); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, + null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + singlePaidMessageCollection.SingleMessages.Add(medium.id, medium.files.full.url); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } } - else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && + medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; @@ -1792,25 +1875,34 @@ public class APIService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, medium.files.full.url.ToString()); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.full.url, null, + null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, medium.files.full.url); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } } - else if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.drm != null) + else if (!messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && + medium.files.drm != null) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; @@ -1818,25 +1910,35 @@ public class APIService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.SingleMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.SingleMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, + null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + singlePaidMessageCollection.SingleMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); singlePaidMessageCollection.SingleMessageMedia.Add(medium); } } - else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && medium.files.drm != null) + else if (messagePreviewIds.Contains(medium.id) && medium.canView && medium.files != null && + medium.files.drm != null) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; @@ -1844,8 +1946,14 @@ public class APIService(IAuthService authService, IConfigService configService, if (!singlePaidMessageCollection.PreviewSingleMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), messagePreviewIds.Contains(medium.id) ? true : false, false, null); - singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); + await dbService.AddMedia(folder, medium.id, message.id, medium.files.drm.manifest.dash, + null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + messagePreviewIds.Contains(medium.id) ? true : false, false, null); + singlePaidMessageCollection.PreviewSingleMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{message.id}"); singlePaidMessageCollection.PreviewSingleMessageMedia.Add(medium); } } @@ -1862,15 +1970,19 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } - public async Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx) + public async Task GetPaidMessages(string endpoint, string folder, string username, + StatusContext ctx) { Log.Debug($"Calling GetPaidMessages - {username}"); @@ -1888,7 +2000,7 @@ public class APIService(IAuthService authService, IConfigService configService, { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); paidMessages = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1903,18 +2015,21 @@ public class APIService(IAuthService authService, IConfigService configService, Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); - HttpRequestMessage looprequest = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); + HttpRequestMessage looprequest = + new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); foreach (KeyValuePair keyValuePair in loopheaders) { looprequest.Headers.Add(keyValuePair.Key, keyValuePair.Value); } - using (var loopresponse = await loopclient.SendAsync(looprequest)) + + using (HttpResponseMessage loopresponse = await loopclient.SendAsync(looprequest)) { loopresponse.EnsureSuccessStatusCode(); - var loopbody = await loopresponse.Content.ReadAsStringAsync(); + string loopbody = await loopresponse.Content.ReadAsStringAsync(); newpaidMessages = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); } + paidMessages.list.AddRange(newpaidMessages.list); ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.list.Count}[/]"); ctx.Spinner(Spinner.Known.Dots); @@ -1923,24 +2038,34 @@ public class APIService(IAuthService authService, IConfigService configService, { break; } + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); } } if (paidMessages.list != null && paidMessages.list.Count > 0) { - foreach (Purchased.List purchase in paidMessages.list.Where(p => p.responseType == "message").OrderByDescending(p => p.postedAt ?? p.createdAt)) + foreach (Purchased.List purchase in paidMessages.list.Where(p => p.responseType == "message") + .OrderByDescending(p => p.postedAt ?? p.createdAt)) { - if (!configService.CurrentConfig.IgnoreOwnMessages || purchase.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) + if (!configService.CurrentConfig.IgnoreOwnMessages || + purchase.fromUser.id != Convert.ToInt32(authService.CurrentAuth.USER_ID)) { if (purchase.postedAt != null) { - await dbService.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); + await dbService.AddMessage(folder, purchase.id, + purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, + purchase.fromUser.id); } else { - await dbService.AddMessage(folder, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); + await dbService.AddMessage(folder, purchase.id, + purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, + purchase.fromUser.id); } + paidMessageCollection.PaidMessageObjects.Add(purchase); if (purchase.media != null && purchase.media.Count > 0) { @@ -1977,27 +2102,37 @@ public class APIService(IAuthService authService, IConfigService configService, if (previewids.Count > 0) { bool has = previewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!has && medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, medium.files.full.url); paidMessageCollection.PaidMessageMedia.Add(medium); } @@ -2008,49 +2143,69 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - paidMessageCollection.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + paidMessageCollection.PaidMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidMessageCollection.PaidMessageMedia.Add(medium); } } } else { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); paidMessageCollection.PaidMessages.Add(medium.id, medium.files.full.url); paidMessageCollection.PaidMessageMedia.Add(medium); } @@ -2061,22 +2216,32 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (!paidMessageCollection.PaidMessages.ContainsKey(medium.id)) { - await dbService.AddMedia(folder, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - paidMessageCollection.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(folder, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + paidMessageCollection.PaidMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); paidMessageCollection.PaidMessageMedia.Add(medium); } } @@ -2096,10 +2261,13 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } @@ -2120,7 +2288,7 @@ public class APIService(IAuthService authService, IConfigService configService, { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); purchased = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); if (purchased != null && purchased.hasMore) { @@ -2132,23 +2300,27 @@ public class APIService(IAuthService authService, IConfigService configService, Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); - HttpRequestMessage looprequest = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); + HttpRequestMessage looprequest = + new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); foreach (KeyValuePair keyValuePair in loopheaders) { looprequest.Headers.Add(keyValuePair.Key, keyValuePair.Value); } - using (var loopresponse = await loopclient.SendAsync(looprequest)) + + using (HttpResponseMessage loopresponse = await loopclient.SendAsync(looprequest)) { loopresponse.EnsureSuccessStatusCode(); - var loopbody = await loopresponse.Content.ReadAsStringAsync(); + string loopbody = await loopresponse.Content.ReadAsStringAsync(); newPurchased = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); } + purchased.list.AddRange(newPurchased.list); if (!newPurchased.hasMore) { break; } + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); } } @@ -2163,16 +2335,20 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!string.IsNullOrEmpty(users.FirstOrDefault(x => x.Value == purchase.fromUser.id).Key)) { - if (!purchasedTabUsers.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.fromUser.id).Key)) + if (!purchasedTabUsers.ContainsKey(users + .FirstOrDefault(x => x.Value == purchase.fromUser.id).Key)) { - purchasedTabUsers.Add(users.FirstOrDefault(x => x.Value == purchase.fromUser.id).Key, purchase.fromUser.id); + purchasedTabUsers.Add( + users.FirstOrDefault(x => x.Value == purchase.fromUser.id).Key, + purchase.fromUser.id); } } else { if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", purchase.fromUser.id); + purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", + purchase.fromUser.id); } } } @@ -2180,29 +2356,35 @@ public class APIService(IAuthService authService, IConfigService configService, { JObject user = await GetUserInfoById($"/users/list?x[]={purchase.fromUser.id}"); - if(user is null) + if (user is null) { if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) { - if(!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) + if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", purchase.fromUser.id); + purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", + purchase.fromUser.id); } } + Log.Debug("Content creator not longer exists - {0}", purchase.fromUser.id); } - else if (!string.IsNullOrEmpty(user[purchase.fromUser.id.ToString()]["username"].ToString())) + else if (!string.IsNullOrEmpty(user[purchase.fromUser.id.ToString()]["username"] + .ToString())) { - if (!purchasedTabUsers.ContainsKey(user[purchase.fromUser.id.ToString()]["username"].ToString())) + if (!purchasedTabUsers.ContainsKey(user[purchase.fromUser.id.ToString()]["username"] + .ToString())) { - purchasedTabUsers.Add(user[purchase.fromUser.id.ToString()]["username"].ToString(), purchase.fromUser.id); + purchasedTabUsers.Add(user[purchase.fromUser.id.ToString()]["username"].ToString(), + purchase.fromUser.id); } } else { if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", purchase.fromUser.id); + purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", + purchase.fromUser.id); } } } @@ -2213,9 +2395,12 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!string.IsNullOrEmpty(users.FirstOrDefault(x => x.Value == purchase.author.id).Key)) { - if (!purchasedTabUsers.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.author.id).Key) && users.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.author.id).Key)) + if (!purchasedTabUsers.ContainsKey(users + .FirstOrDefault(x => x.Value == purchase.author.id).Key) && + users.ContainsKey(users.FirstOrDefault(x => x.Value == purchase.author.id).Key)) { - purchasedTabUsers.Add(users.FirstOrDefault(x => x.Value == purchase.author.id).Key, purchase.author.id); + purchasedTabUsers.Add(users.FirstOrDefault(x => x.Value == purchase.author.id).Key, + purchase.author.id); } } else @@ -2234,18 +2419,23 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist) { - if(!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.author.id}")) + if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.author.id}")) { - purchasedTabUsers.Add($"Deleted User - {purchase.author.id}", purchase.author.id); + purchasedTabUsers.Add($"Deleted User - {purchase.author.id}", + purchase.author.id); } } + Log.Debug("Content creator not longer exists - {0}", purchase.author.id); } else if (!string.IsNullOrEmpty(user[purchase.author.id.ToString()]["username"].ToString())) { - if (!purchasedTabUsers.ContainsKey(user[purchase.author.id.ToString()]["username"].ToString()) && users.ContainsKey(user[purchase.author.id.ToString()]["username"].ToString())) + if (!purchasedTabUsers.ContainsKey(user[purchase.author.id.ToString()]["username"] + .ToString()) && + users.ContainsKey(user[purchase.author.id.ToString()]["username"].ToString())) { - purchasedTabUsers.Add(user[purchase.author.id.ToString()]["username"].ToString(), purchase.author.id); + purchasedTabUsers.Add(user[purchase.author.id.ToString()]["username"].ToString(), + purchase.author.id); } } else @@ -2269,20 +2459,24 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } - public async Task> GetPurchasedTab(string endpoint, string folder, Dictionary users) + public async Task> GetPurchasedTab(string endpoint, string folder, + Dictionary users) { Log.Debug($"Calling GetPurchasedTab - {endpoint}"); try { - Dictionary> userPurchases = new Dictionary>(); + Dictionary> userPurchases = new(); List purchasedTabCollections = new(); Purchased purchased = new(); int post_limit = 50; @@ -2294,7 +2488,7 @@ public class APIService(IAuthService authService, IConfigService configService, { "skip_users", "all" } }; - var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient()); purchased = JsonConvert.DeserializeObject(body, m_JsonSerializerSettings); if (purchased != null && purchased.hasMore) { @@ -2306,23 +2500,27 @@ public class APIService(IAuthService authService, IConfigService configService, Dictionary loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams); HttpClient loopclient = GetHttpClient(); - HttpRequestMessage looprequest = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); + HttpRequestMessage looprequest = + new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{loopqueryParams}"); foreach (KeyValuePair keyValuePair in loopheaders) { looprequest.Headers.Add(keyValuePair.Key, keyValuePair.Value); } - using (var loopresponse = await loopclient.SendAsync(looprequest)) + + using (HttpResponseMessage loopresponse = await loopclient.SendAsync(looprequest)) { loopresponse.EnsureSuccessStatusCode(); - var loopbody = await loopresponse.Content.ReadAsStringAsync(); + string loopbody = await loopresponse.Content.ReadAsStringAsync(); newPurchased = JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); } + purchased.list.AddRange(newPurchased.list); if (!newPurchased.hasMore) { break; } + getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit); } } @@ -2337,6 +2535,7 @@ public class APIService(IAuthService authService, IConfigService configService, { userPurchases.Add(purchase.fromUser.id, new List()); } + userPurchases[purchase.fromUser.id].Add(purchase); } else if (purchase.author != null) @@ -2345,6 +2544,7 @@ public class APIService(IAuthService authService, IConfigService configService, { userPurchases.Add(purchase.author.id, new List()); } + userPurchases[purchase.author.id].Add(purchase); } } @@ -2352,20 +2552,28 @@ public class APIService(IAuthService authService, IConfigService configService, foreach (KeyValuePair> user in userPurchases) { - PurchasedTabCollection purchasedTabCollection = new PurchasedTabCollection(); + PurchasedTabCollection purchasedTabCollection = new(); JObject userObject = await GetUserInfoById($"/users/list?x[]={user.Key}"); purchasedTabCollection.UserId = user.Key; - purchasedTabCollection.Username = userObject is not null && !string.IsNullOrEmpty(userObject[user.Key.ToString()]["username"].ToString()) ? userObject[user.Key.ToString()]["username"].ToString() : $"Deleted User - {user.Key}"; - string path = System.IO.Path.Combine(folder, purchasedTabCollection.Username); + purchasedTabCollection.Username = + userObject is not null && + !string.IsNullOrEmpty(userObject[user.Key.ToString()]["username"].ToString()) + ? userObject[user.Key.ToString()]["username"].ToString() + : $"Deleted User - {user.Key}"; + string path = Path.Combine(folder, purchasedTabCollection.Username); if (Path.Exists(path)) { foreach (Purchased.List purchase in user.Value) { if (purchase.media == null) { - Log.Warning("PurchasedTab purchase media null, setting empty list | userId={UserId} username={Username} purchaseId={PurchaseId} responseType={ResponseType} createdAt={CreatedAt} postedAt={PostedAt}", user.Key, purchasedTabCollection.Username, purchase.id, purchase.responseType, purchase.createdAt, purchase.postedAt); + Log.Warning( + "PurchasedTab purchase media null, setting empty list | userId={UserId} username={Username} purchaseId={PurchaseId} responseType={ResponseType} createdAt={CreatedAt} postedAt={PostedAt}", + user.Key, purchasedTabCollection.Username, purchase.id, purchase.responseType, + purchase.createdAt, purchase.postedAt); purchase.media = new List(); } + switch (purchase.responseType) { case "post": @@ -2396,7 +2604,13 @@ public class APIService(IAuthService authService, IConfigService configService, } } } - await dbService.AddPost(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price.ToString() : "0", purchase.price != null && purchase.isOpened ? true : false, purchase.isArchived.HasValue ? purchase.isArchived.Value : false, purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); + + await dbService.AddPost(path, purchase.id, + purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", + purchase.price != null && purchase.isOpened ? true : false, + purchase.isArchived.HasValue ? purchase.isArchived.Value : false, + purchase.createdAt != null ? purchase.createdAt.Value : purchase.postedAt.Value); purchasedTabCollection.PaidPosts.PaidPostObjects.Add(purchase); foreach (Messages.Medium medium in purchase.media) { @@ -2404,51 +2618,73 @@ public class APIService(IAuthService authService, IConfigService configService, { continue; } + if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) { continue; } + if (previewids.Count > 0) { bool has = previewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!has && medium.canView && medium.files != null && + medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) { - if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, medium.files.full.url); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, + medium.files.full.url); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } - else if (!has && medium.canView && medium.files != null && medium.files.drm != null) + else if (!has && medium.canView && medium.files != null && + medium.files.drm != null) { - if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } - } } else { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, medium.files.full.url); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, + medium.files.full.url); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } @@ -2456,23 +2692,37 @@ public class APIService(IAuthService authService, IConfigService configService, { if (!purchasedTabCollection.PaidPosts.PaidPosts.ContainsKey(medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Posts", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), previewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Posts", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + previewids.Contains(medium.id) ? true : false, false, null); + purchasedTabCollection.PaidPosts.PaidPosts.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidPosts.PaidPostMedia.Add(medium); } } } } + break; case "message": if (purchase.postedAt != null) { - await dbService.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.postedAt.Value, purchase.fromUser.id); + await dbService.AddMessage(path, purchase.id, + purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", true, false, + purchase.postedAt.Value, purchase.fromUser.id); } else { - await dbService.AddMessage(path, purchase.id, purchase.text != null ? purchase.text : string.Empty, purchase.price != null ? purchase.price : "0", true, false, purchase.createdAt.Value, purchase.fromUser.id); + await dbService.AddMessage(path, purchase.id, + purchase.text != null ? purchase.text : string.Empty, + purchase.price != null ? purchase.price : "0", true, false, + purchase.createdAt.Value, purchase.fromUser.id); } + purchasedTabCollection.PaidMessages.PaidMessageObjects.Add(purchase); if (purchase.media != null && purchase.media.Count > 0) { @@ -2509,118 +2759,185 @@ public class APIService(IAuthService authService, IConfigService configService, if (paidMessagePreviewids.Count > 0) { bool has = paidMessagePreviewids.Any(cus => cus.Equals(medium.id)); - if (!has && medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (!has && medium.canView && medium.files != null && + medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.type == "photo" && + !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + + if (medium.type == "video" && + !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + + if (medium.type == "audio" && + !configService.CurrentConfig.DownloadAudios) { continue; } - if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) + + if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( + medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, medium.files.full.url); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.id) ? true : false, false, + null); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, + medium.files.full.url); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } } - else if (!has && medium.canView && medium.files != null && medium.files.drm != null) + else if (!has && medium.canView && medium.files != null && + medium.files.drm != null) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.type == "photo" && + !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + + if (medium.type == "video" && + !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + + if (medium.type == "audio" && + !configService.CurrentConfig.DownloadAudios) { continue; } - if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) + + if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( + medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.id) ? true : false, false, + null); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } } } else { - if (medium.canView && medium.files != null && medium.files.full != null && !string.IsNullOrEmpty(medium.files.full.url)) + if (medium.canView && medium.files != null && medium.files.full != null && + !string.IsNullOrEmpty(medium.files.full.url)) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.type == "photo" && + !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + + if (medium.type == "video" && + !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + + if (medium.type == "audio" && + !configService.CurrentConfig.DownloadAudios) { continue; } - if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) + + if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( + medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.full.url, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, medium.files.full.url); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.full.url, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.id) ? true : false, false, + null); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, + medium.files.full.url); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } } else if (medium.canView && medium.files != null && medium.files.drm != null) { - if (medium.type == "photo" && !configService.CurrentConfig.DownloadImages) + if (medium.type == "photo" && + !configService.CurrentConfig.DownloadImages) { continue; } - if (medium.type == "video" && !configService.CurrentConfig.DownloadVideos) + + if (medium.type == "video" && + !configService.CurrentConfig.DownloadVideos) { continue; } + if (medium.type == "gif" && !configService.CurrentConfig.DownloadVideos) { continue; } - if (medium.type == "audio" && !configService.CurrentConfig.DownloadAudios) + + if (medium.type == "audio" && + !configService.CurrentConfig.DownloadAudios) { continue; } - if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey(medium.id)) + + if (!purchasedTabCollection.PaidMessages.PaidMessages.ContainsKey( + medium.id)) { - await dbService.AddMedia(path, medium.id, purchase.id, medium.files.drm.manifest.dash, null, null, null, "Messages", medium.type == "photo" ? "Images" : (medium.type == "video" || medium.type == "gif" ? "Videos" : (medium.type == "audio" ? "Audios" : null)), paidMessagePreviewids.Contains(medium.id) ? true : false, false, null); - purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); + await dbService.AddMedia(path, medium.id, purchase.id, + medium.files.drm.manifest.dash, null, null, null, "Messages", + medium.type == "photo" ? "Images" : + medium.type == "video" || medium.type == "gif" ? "Videos" : + medium.type == "audio" ? "Audios" : null, + paidMessagePreviewids.Contains(medium.id) ? true : false, false, + null); + purchasedTabCollection.PaidMessages.PaidMessages.Add(medium.id, + $"{medium.files.drm.manifest.dash},{medium.files.drm.signature.dash.CloudFrontPolicy},{medium.files.drm.signature.dash.CloudFrontSignature},{medium.files.drm.signature.dash.CloudFrontKeyPairId},{medium.id},{purchase.id}"); purchasedTabCollection.PaidMessages.PaidMessageMedia.Add(medium); } } } } } + break; } } + purchasedTabCollections.Add(purchasedTabCollection); } } + return purchasedTabCollections; } catch (Exception ex) @@ -2630,10 +2947,13 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } @@ -2648,15 +2968,16 @@ public class APIService(IAuthService authService, IConfigService configService, HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); request.Headers.Add("user-agent", authService.CurrentAuth.USER_AGENT); request.Headers.Add("Accept", "*/*"); - request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); - using (var response = await client.SendAsync(request)) + 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(); - var 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"; XDocument xmlDoc = XDocument.Parse(body); - var psshElements = xmlDoc.Descendants(cenc + "pssh"); + IEnumerable psshElements = xmlDoc.Descendants(cenc + "pssh"); pssh = psshElements.ElementAt(1).Value; } @@ -2669,10 +2990,13 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } @@ -2693,14 +3017,17 @@ public class APIService(IAuthService authService, IConfigService configService, HttpRequestMessage request = new(HttpMethod.Get, mpdUrl); request.Headers.Add("user-agent", authService.CurrentAuth.USER_AGENT); request.Headers.Add("Accept", "*/*"); - request.Headers.Add("Cookie", $"CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {authService.CurrentAuth.COOKIE};"); - using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) + 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, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); lastmodified = response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now; Log.Debug($"Last modified: {lastmodified}"); } + return lastmodified; } catch (Exception ex) @@ -2710,14 +3037,18 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return DateTime.Now; } - public async Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, string pssh) + public async Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, + string pssh) { Log.Debug("Calling GetDecryptionKey"); @@ -2728,7 +3059,7 @@ public class APIService(IAuthService authService, IConfigService configService, string dcValue = string.Empty; HttpClient client = new(); - CDRMProjectRequest cdrmProjectRequest = new CDRMProjectRequest + CDRMProjectRequest cdrmProjectRequest = new() { PSSH = pssh, LicenseURL = licenceURL, @@ -2750,13 +3081,13 @@ public class APIService(IAuthService authService, IConfigService configService, Content = new StringContent(json, Encoding.UTF8, "application/json") }; - using var response = await client.SendAsync(request); + using HttpResponseMessage response = await client.SendAsync(request); Log.Debug($"CDRM Project Response (Attempt {attempt}): {response.Content.ReadAsStringAsync().Result}"); response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); - var doc = JsonDocument.Parse(body); + string body = await response.Content.ReadAsStringAsync(); + JsonDocument doc = JsonDocument.Parse(body); if (doc.RootElement.TryGetProperty("status", out JsonElement status)) { @@ -2765,13 +3096,11 @@ public class APIService(IAuthService authService, IConfigService configService, dcValue = doc.RootElement.GetProperty("message").GetString().Trim(); return dcValue; } - else + + Log.Debug($"CDRM response status not successful. Retrying... Attempt {attempt} of {MaxAttempts}"); + if (attempt < MaxAttempts) { - Log.Debug($"CDRM response status not successful. Retrying... Attempt {attempt} of {MaxAttempts}"); - if (attempt < MaxAttempts) - { - await Task.Delay(DelayBetweenAttempts); - } + await Task.Delay(DelayBetweenAttempts); } } else @@ -2793,14 +3122,18 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } - public async Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, string pssh) + public async Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, + string pssh) { Log.Debug("Calling GetDecryptionOFDL"); @@ -2809,11 +3142,9 @@ public class APIService(IAuthService authService, IConfigService configService, HttpClient client = new(); int attempt = 0; - OFDLRequest ofdlRequest = new OFDLRequest + OFDLRequest ofdlRequest = new() { - PSSH = pssh, - LicenseURL = licenceURL, - Headers = JsonConvert.SerializeObject(drmHeaders) + PSSH = pssh, LicenseURL = licenceURL, Headers = JsonConvert.SerializeObject(drmHeaders) }; string json = JsonConvert.SerializeObject(ofdlRequest); @@ -2829,15 +3160,19 @@ public class APIService(IAuthService authService, IConfigService configService, Content = new StringContent(json, Encoding.UTF8, "application/json") }; - using var response = await client.SendAsync(request); + using HttpResponseMessage response = await client.SendAsync(request); if (!response.IsSuccessStatusCode) + { continue; + } string body = await response.Content.ReadAsStringAsync(); if (!body.TrimStart().StartsWith('{')) + { return body; + } Log.Debug($"Received JSON object instead of string. Retrying... Attempt {attempt} of {MaxAttempts}"); await Task.Delay(DelayBetweenAttempts); @@ -2850,8 +3185,10 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } @@ -2864,12 +3201,12 @@ public class APIService(IAuthService authService, IConfigService configService, try { - var resp1 = await PostData(licenceURL, drmHeaders, new byte[] { 0x08, 0x04 }); - var certDataB64 = Convert.ToBase64String(resp1); - var cdm = new CDMApi(); - var challenge = cdm.GetChallenge(pssh, certDataB64, false, false); - var resp2 = await PostData(licenceURL, drmHeaders, challenge); - var licenseB64 = Convert.ToBase64String(resp2); + byte[] resp1 = await PostData(licenceURL, drmHeaders, new byte[] { 0x08, 0x04 }); + string certDataB64 = Convert.ToBase64String(resp1); + CDMApi cdm = new(); + byte[] challenge = cdm.GetChallenge(pssh, certDataB64); + byte[] resp2 = await PostData(licenceURL, drmHeaders, challenge); + string licenseB64 = Convert.ToBase64String(resp2); Log.Debug($"resp1: {resp1}"); Log.Debug($"certDataB64: {certDataB64}"); Log.Debug($"challenge: {challenge}"); @@ -2879,7 +3216,7 @@ public class APIService(IAuthService authService, IConfigService configService, List keys = cdm.GetKeys(); if (keys.Count > 0) { - Log.Debug($"GetDecryptionKeyCDM Key: {keys[0].ToString()}"); + Log.Debug($"GetDecryptionKeyCDM Key: {keys[0]}"); return keys[0].ToString(); } } @@ -2890,10 +3227,186 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + + return null; + } + + + private async Task BuildHeaderAndExecuteRequests(Dictionary getParams, string endpoint, + HttpClient client) + { + Log.Debug("Calling BuildHeaderAndExecuteRequests"); + + HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint); + using HttpResponseMessage response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + string body = await response.Content.ReadAsStringAsync(); + + Log.Debug(body); + + return body; + } + + + private async Task BuildHttpRequestMessage(Dictionary getParams, + string endpoint) + { + Log.Debug("Calling BuildHttpRequestMessage"); + + string queryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}")); + + Dictionary headers = GetDynamicHeaders($"/api2/v2{endpoint}", queryParams); + + HttpRequestMessage request = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{queryParams}"); + + Log.Debug($"Full request URL: {Constants.API_URL}{endpoint}{queryParams}"); + + foreach (KeyValuePair keyValuePair in headers) + { + request.Headers.Add(keyValuePair.Key, keyValuePair.Value); + } + + return request; + } + + private static double ConvertToUnixTimestampWithMicrosecondPrecision(DateTime date) + { + DateTime origin = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + TimeSpan diff = date.ToUniversalTime() - origin; + + return + 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 HttpClient GetHttpClient() + { + HttpClient client = new(); + if (configService.CurrentConfig?.Timeout != null && configService.CurrentConfig.Timeout > 0) + { + client.Timeout = TimeSpan.FromSeconds(configService.CurrentConfig.Timeout.Value); + } + + return client; + } + + + /// + /// this one is used during initialization only + /// if the config option is not available then no modificatiotns will be done on the getParams + /// + /// + /// + /// + private static void UpdateGetParamsForDateSelection(DownloadDateSelection downloadDateSelection, + ref Dictionary getParams, DateTime? dt) + { + //if (config.DownloadOnlySpecificDates && dt.HasValue) + //{ + if (dt.HasValue) + { + UpdateGetParamsForDateSelection( + downloadDateSelection, + ref getParams, + ConvertToUnixTimestampWithMicrosecondPrecision(dt.Value) + .ToString("0.000000", CultureInfo.InvariantCulture) + ); + } + //} + } + + private static void UpdateGetParamsForDateSelection(DownloadDateSelection downloadDateSelection, + ref Dictionary getParams, string unixTimeStampInMicrosec) + { + switch (downloadDateSelection) + { + case DownloadDateSelection.before: + getParams["beforePublishTime"] = unixTimeStampInMicrosec; + break; + case DownloadDateSelection.after: + getParams["order"] = "publish_date_asc"; + getParams["afterPublishTime"] = unixTimeStampInMicrosec; + break; + } + } + + + public async Task?> GetAllSubscriptions(Dictionary getParams, + string endpoint, bool includeRestricted) + { + try + { + Dictionary users = new(); + Subscriptions subscriptions = new(); + + Log.Debug("Calling GetAllSubscrptions"); + + string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + + subscriptions = JsonConvert.DeserializeObject(body); + if (subscriptions != null && subscriptions.hasMore) + { + getParams["offset"] = subscriptions.list.Count.ToString(); + + while (true) + { + Subscriptions newSubscriptions = new(); + string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient()); + + if (!string.IsNullOrEmpty(loopbody) && (!loopbody.Contains("[]") || loopbody.Trim() != "[]")) + { + newSubscriptions = + JsonConvert.DeserializeObject(loopbody, m_JsonSerializerSettings); + } + else + { + break; + } + + subscriptions.list.AddRange(newSubscriptions.list); + if (!newSubscriptions.hasMore) + { + break; + } + + getParams["offset"] = subscriptions.list.Count.ToString(); + } + } + + foreach (Subscriptions.List subscription in subscriptions.list) + { + if ((!(subscription.isRestricted ?? false) || + ((subscription.isRestricted ?? false) && includeRestricted)) + && !users.ContainsKey(subscription.username)) + { + users.Add(subscription.username, subscription.id); + } + } + + return users; + } + catch (Exception ex) + { + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + if (ex.InnerException != null) + { + Console.WriteLine("\nInner Exception:"); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + } + } + return null; } @@ -2902,9 +3415,10 @@ public class APIService(IAuthService authService, IConfigService configService, Log.Debug("Calling GetDynamicRules"); try { - HttpClient client = new HttpClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://git.ofdl.tools/sim0n00ps/dynamic-rules/raw/branch/main/rules.json"); - using var response = client.Send(request); + HttpClient client = new(); + HttpRequestMessage request = new(HttpMethod.Get, + "https://git.ofdl.tools/sim0n00ps/dynamic-rules/raw/branch/main/rules.json"); + using HttpResponseMessage response = client.Send(request); if (!response.IsSuccessStatusCode) { @@ -2912,7 +3426,7 @@ public class APIService(IAuthService authService, IConfigService configService, return null; } - var body = response.Content.ReadAsStringAsync().Result; + string body = response.Content.ReadAsStringAsync().Result; Log.Debug("GetDynamicRules Response: "); Log.Debug(body); @@ -2926,10 +3440,13 @@ public class APIService(IAuthService authService, IConfigService configService, if (ex.InnerException != null) { Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + return null; } } diff --git a/OF DL/Services/AuthService.cs b/OF DL/Services/AuthService.cs index c705b2f..17dfd81 100644 --- a/OF DL/Services/AuthService.cs +++ b/OF DL/Services/AuthService.cs @@ -4,227 +4,220 @@ using PuppeteerSharp; using PuppeteerSharp.BrowserData; using Serilog; -namespace OF_DL.Services +namespace OF_DL.Services; + +public class AuthService : IAuthService { - public class AuthService : IAuthService + private const int LoginTimeout = 600000; // 10 minutes + private const int FeedLoadTimeout = 60000; // 1 minute + + private readonly string[] _desiredCookies = + [ + "auth_id", + "sess" + ]; + + private readonly LaunchOptions _options = new() { - private readonly LaunchOptions _options = new() + Headless = false, + Channel = ChromeReleaseChannel.Stable, + DefaultViewport = null, + Args = ["--no-sandbox", "--disable-setuid-sandbox"], + UserDataDir = Path.GetFullPath("chrome-data") + }; + + public Auth? CurrentAuth { get; set; } + + public async Task LoadFromFileAsync(string filePath = "auth.json") + { + try { - Headless = false, - Channel = ChromeReleaseChannel.Stable, - DefaultViewport = null, - Args = ["--no-sandbox", "--disable-setuid-sandbox"], - UserDataDir = Path.GetFullPath("chrome-data") - }; - - private readonly string[] _desiredCookies = - [ - "auth_id", - "sess" - ]; - - private const int LoginTimeout = 600000; // 10 minutes - private const int FeedLoadTimeout = 60000; // 1 minute - - public Auth? CurrentAuth { get; set; } - - public async Task LoadFromFileAsync(string filePath = "auth.json") - { - try + if (!File.Exists(filePath)) { - if (!File.Exists(filePath)) - { - Log.Debug("Auth file not found: {FilePath}", filePath); - return false; - } - - var json = await File.ReadAllTextAsync(filePath); - CurrentAuth = JsonConvert.DeserializeObject(json); - Log.Debug("Auth file loaded and deserialized successfully"); - return CurrentAuth != null; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to load auth from file"); + Log.Debug("Auth file not found: {FilePath}", filePath); return false; } + + string json = await File.ReadAllTextAsync(filePath); + CurrentAuth = JsonConvert.DeserializeObject(json); + Log.Debug("Auth file loaded and deserialized successfully"); + return CurrentAuth != null; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load auth from file"); + return false; + } + } + + public async Task LoadFromBrowserAsync() + { + try + { + bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; + + await SetupBrowser(runningInDocker); + CurrentAuth = await GetAuthFromBrowser(); + + return CurrentAuth != null; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load auth from browser"); + return false; + } + } + + public async Task SaveToFileAsync(string filePath = "auth.json") + { + if (CurrentAuth == null) + { + Log.Warning("Attempted to save null auth to file"); + return; } - public async Task LoadFromBrowserAsync() + try { - try - { - bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; - - await SetupBrowser(runningInDocker); - CurrentAuth = await GetAuthFromBrowser(); - - return CurrentAuth != null; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to load auth from browser"); - return false; - } + string json = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented); + await File.WriteAllTextAsync(filePath, json); + Log.Debug($"Auth saved to file: {filePath}"); } - - public async Task SaveToFileAsync(string filePath = "auth.json") + catch (Exception ex) { - if (CurrentAuth == null) - { - Log.Warning("Attempted to save null auth to file"); - return; - } - - try - { - var json = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented); - await File.WriteAllTextAsync(filePath, json); - Log.Debug($"Auth saved to file: {filePath}"); - } - catch (Exception ex) - { - Log.Error(ex, "Failed to save auth to file"); - } + Log.Error(ex, "Failed to save auth to file"); } + } - private async Task SetupBrowser(bool runningInDocker) + private async Task SetupBrowser(bool runningInDocker) + { + string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH"); + if (executablePath != null) { - string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH"); - if (executablePath != null) + Log.Information( + "OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}", + executablePath); + _options.ExecutablePath = executablePath; + } + else + { + BrowserFetcher browserFetcher = new(); + List installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList(); + if (installedBrowsers.Count == 0) { - Log.Information("OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}", executablePath); - _options.ExecutablePath = executablePath; + Log.Information("Downloading browser."); + InstalledBrowser? downloadedBrowser = await browserFetcher.DownloadAsync(); + Log.Information("Browser downloaded. Path: {executablePath}", + downloadedBrowser.GetExecutablePath()); + _options.ExecutablePath = downloadedBrowser.GetExecutablePath(); } else { - var browserFetcher = new BrowserFetcher(); - var installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList(); - if (installedBrowsers.Count == 0) + _options.ExecutablePath = installedBrowsers.First().GetExecutablePath(); + } + } + + if (runningInDocker) + { + Log.Information("Running in Docker. Disabling sandbox and GPU."); + _options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]; + } + } + + private async Task GetBcToken(IPage page) => + await page.EvaluateExpressionAsync("window.localStorage.getItem('bcTokenSha') || ''"); + + private async Task GetAuthFromBrowser(bool isDocker = false) + { + try + { + IBrowser? browser; + try + { + browser = await Puppeteer.LaunchAsync(_options); + } + catch (ProcessException e) + { + if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir)) { - Log.Information("Downloading browser."); - var downloadedBrowser = await browserFetcher.DownloadAsync(); - Log.Information("Browser downloaded. Path: {executablePath}", - downloadedBrowser.GetExecutablePath()); - _options.ExecutablePath = downloadedBrowser.GetExecutablePath(); + Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again."); + Directory.Delete(_options.UserDataDir, true); + browser = await Puppeteer.LaunchAsync(_options); } else { - _options.ExecutablePath = installedBrowsers.First().GetExecutablePath(); + throw; } } - if (runningInDocker) + if (browser == null) { - Log.Information("Running in Docker. Disabling sandbox and GPU."); - _options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]; + throw new Exception("Could not get browser"); } - } - private async Task GetBcToken(IPage page) - { - return await page.EvaluateExpressionAsync("window.localStorage.getItem('bcTokenSha') || ''"); - } + IPage[]? pages = await browser.PagesAsync(); + IPage? page = pages.First(); - private async Task GetAuthFromBrowser(bool isDocker = false) - { + if (page == null) + { + throw new Exception("Could not get page"); + } + + Log.Debug("Navigating to OnlyFans."); + await page.GoToAsync("https://onlyfans.com"); + + Log.Debug("Waiting for user to login"); + await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout }); + Log.Debug("Feed element detected (user logged in)"); + + await page.ReloadAsync(); + + await page.WaitForNavigationAsync(new NavigationOptions + { + WaitUntil = [WaitUntilNavigation.Networkidle2], Timeout = FeedLoadTimeout + }); + Log.Debug("DOM loaded. Getting BC token and cookies ..."); + + string xBc; try { - IBrowser? browser; - try - { - browser = await Puppeteer.LaunchAsync(_options); - } - catch (ProcessException e) - { - if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir)) - { - Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again."); - Directory.Delete(_options.UserDataDir, true); - browser = await Puppeteer.LaunchAsync(_options); - } - else - { - throw; - } - } - - if (browser == null) - { - throw new Exception("Could not get browser"); - } - - IPage[]? pages = await browser.PagesAsync(); - IPage? page = pages.First(); - - if (page == null) - { - throw new Exception("Could not get page"); - } - - Log.Debug("Navigating to OnlyFans."); - await page.GoToAsync("https://onlyfans.com"); - - Log.Debug("Waiting for user to login"); - await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout }); - Log.Debug("Feed element detected (user logged in)"); - - await page.ReloadAsync(); - - await page.WaitForNavigationAsync(new NavigationOptions { - WaitUntil = [WaitUntilNavigation.Networkidle2], - Timeout = FeedLoadTimeout - }); - Log.Debug("DOM loaded. Getting BC token and cookies ..."); - - string xBc; - try - { - xBc = await GetBcToken(page); - } - catch (Exception e) - { - throw new Exception("Error getting bcToken"); - } - - Dictionary mappedCookies = (await page.GetCookiesAsync()) - .Where(cookie => cookie.Domain.Contains("onlyfans.com")) - .ToDictionary(cookie => cookie.Name, cookie => cookie.Value); - - mappedCookies.TryGetValue("auth_id", out string? userId); - if (userId == null) - { - throw new Exception("Could not find 'auth_id' cookie"); - } - - mappedCookies.TryGetValue("sess", out string? sess); - if (sess == null) - { - throw new Exception("Could not find 'sess' cookie"); - } - - string? userAgent = await browser.GetUserAgentAsync(); - if (userAgent == null) - { - throw new Exception("Could not get user agent"); - } - - string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key)) - .Select(key => $"${key}={mappedCookies[key]};")); - - return new Auth() - { - COOKIE = cookies, - USER_AGENT = userAgent, - USER_ID = userId, - X_BC = xBc - }; + xBc = await GetBcToken(page); } catch (Exception e) { - Log.Error(e, "Error getting auth from browser"); - return null; + throw new Exception("Error getting bcToken"); } + + Dictionary mappedCookies = (await page.GetCookiesAsync()) + .Where(cookie => cookie.Domain.Contains("onlyfans.com")) + .ToDictionary(cookie => cookie.Name, cookie => cookie.Value); + + mappedCookies.TryGetValue("auth_id", out string? userId); + if (userId == null) + { + throw new Exception("Could not find 'auth_id' cookie"); + } + + mappedCookies.TryGetValue("sess", out string? sess); + if (sess == null) + { + throw new Exception("Could not find 'sess' cookie"); + } + + string? userAgent = await browser.GetUserAgentAsync(); + if (userAgent == null) + { + throw new Exception("Could not get user agent"); + } + + string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key)) + .Select(key => $"${key}={mappedCookies[key]};")); + + return new Auth { COOKIE = cookies, USER_AGENT = userAgent, USER_ID = userId, X_BC = xBc }; + } + catch (Exception e) + { + Log.Error(e, "Error getting auth from browser"); + return null; } } } diff --git a/OF DL/Services/ConfigService.cs b/OF DL/Services/ConfigService.cs index b2c6a28..0b175da 100644 --- a/OF DL/Services/ConfigService.cs +++ b/OF DL/Services/ConfigService.cs @@ -1,223 +1,241 @@ +using System.Text; using Akka.Configuration; +using Akka.Configuration.Hocon; using Newtonsoft.Json; using OF_DL.Entities; -using Serilog; -using System.Text; using OF_DL.Enumerations; +using OF_DL.Utils; +using Serilog; using Config = OF_DL.Entities.Config; -namespace OF_DL.Services +namespace OF_DL.Services; + +public class ConfigService(ILoggingService loggingService) : IConfigService { - public class ConfigService(ILoggingService loggingService) : IConfigService + public Config CurrentConfig { get; private set; } = new(); + public bool IsCliNonInteractive { get; private set; } + + public async Task LoadConfigurationAsync(string[] args) { - public Config CurrentConfig { get; private set; } = new(); - public bool IsCliNonInteractive { get; private set; } - - public async Task LoadConfigurationAsync(string[] args) + try { - try + IsCliNonInteractive = false; + + // Migrate from config.json to config.conf if needed + await MigrateFromJsonToConfAsync(); + + // Load config.conf or create default + if (File.Exists("config.conf")) { - IsCliNonInteractive = false; - - // Migrate from config.json to config.conf if needed - await MigrateFromJsonToConfAsync(); - - // Load config.conf or create default - if (File.Exists("config.conf")) + Log.Debug("config.conf located successfully"); + if (!await LoadConfigFromFileAsync("config.conf")) { - Log.Debug("config.conf located successfully"); - if (!await LoadConfigFromFileAsync("config.conf")) - { - return false; - } - } - else - { - Log.Debug("config.conf not found, creating default"); - await CreateDefaultConfigFileAsync(); - if (!await LoadConfigFromFileAsync("config.conf")) - { - return false; - } - } - - // Check for command-line arguments - if (args != null && args.Length > 0) - { - const string NON_INTERACTIVE_ARG = "--non-interactive"; - if (args.Any(a => a.Equals(NON_INTERACTIVE_ARG, StringComparison.OrdinalIgnoreCase))) - { - IsCliNonInteractive = true; - Log.Debug("NonInteractiveMode set via command line"); - } - - Log.Debug("Additional arguments:"); - foreach (string argument in args) - { - Log.Debug(argument); - } - } - - return true; - } - catch (Exception ex) - { - Log.Error(ex, "Configuration loading failed"); - return false; - } - } - - public async Task SaveConfigurationAsync(string filePath = "config.conf") - { - if (CurrentConfig == null) - { - Log.Warning("Attempted to save null config to file"); - return; - } - - try - { - var hoconConfig = BuildHoconFromConfig(CurrentConfig); - await File.WriteAllTextAsync(filePath, hoconConfig); - Log.Debug($"Config saved to file: {filePath}"); - } - catch (Exception ex) - { - Log.Error(ex, "Failed to save config to file"); - } - } - - public void UpdateConfig(Config newConfig) - { - CurrentConfig = newConfig; - - // Update logging level - loggingService.UpdateLoggingLevel(newConfig.LoggingLevel); - - // Apply text sanitization preference globally - OF_DL.Utils.XmlUtils.Passthrough = newConfig.DisableTextSanitization; - - Log.Debug("Configuration updated"); - string configString = JsonConvert.SerializeObject(newConfig, Formatting.Indented); - Log.Debug(configString); - } - - private async Task MigrateFromJsonToConfAsync() - { - if (!File.Exists("config.json")) - return; - - try - { - Log.Debug("config.json found, migrating to config.conf"); - string jsonText = await File.ReadAllTextAsync("config.json"); - var jsonConfig = JsonConvert.DeserializeObject(jsonText); - - if (jsonConfig != null) - { - var hoconConfig = BuildHoconFromConfig(jsonConfig); - await File.WriteAllTextAsync("config.conf", hoconConfig); - File.Delete("config.json"); - Log.Information("config.conf created successfully from config.json"); + return false; } } - catch (Exception ex) + else { - Log.Error(ex, "Failed to migrate config.json to config.conf"); - throw; + Log.Debug("config.conf not found, creating default"); + await CreateDefaultConfigFileAsync(); + if (!await LoadConfigFromFileAsync("config.conf")) + { + return false; + } } + + // Check for command-line arguments + if (args != null && args.Length > 0) + { + const string NON_INTERACTIVE_ARG = "--non-interactive"; + if (args.Any(a => a.Equals(NON_INTERACTIVE_ARG, StringComparison.OrdinalIgnoreCase))) + { + IsCliNonInteractive = true; + Log.Debug("NonInteractiveMode set via command line"); + } + + Log.Debug("Additional arguments:"); + foreach (string argument in args) + { + Log.Debug(argument); + } + } + + return true; + } + catch (Exception ex) + { + Log.Error(ex, "Configuration loading failed"); + return false; + } + } + + public async Task SaveConfigurationAsync(string filePath = "config.conf") + { + if (CurrentConfig == null) + { + Log.Warning("Attempted to save null config to file"); + return; } - private async Task LoadConfigFromFileAsync(string filePath) + try { - try + string hoconConfig = BuildHoconFromConfig(CurrentConfig); + await File.WriteAllTextAsync(filePath, hoconConfig); + Log.Debug($"Config saved to file: {filePath}"); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to save config to file"); + } + } + + public void UpdateConfig(Config newConfig) + { + CurrentConfig = newConfig; + + // Update logging level + loggingService.UpdateLoggingLevel(newConfig.LoggingLevel); + + // Apply text sanitization preference globally + XmlUtils.Passthrough = newConfig.DisableTextSanitization; + + Log.Debug("Configuration updated"); + string configString = JsonConvert.SerializeObject(newConfig, Formatting.Indented); + Log.Debug(configString); + } + + private async Task MigrateFromJsonToConfAsync() + { + if (!File.Exists("config.json")) + { + return; + } + + try + { + Log.Debug("config.json found, migrating to config.conf"); + string jsonText = await File.ReadAllTextAsync("config.json"); + Config? jsonConfig = JsonConvert.DeserializeObject(jsonText); + + if (jsonConfig != null) { - string hoconText = await File.ReadAllTextAsync(filePath); - var hoconConfig = ConfigurationFactory.ParseString(hoconText); + string hoconConfig = BuildHoconFromConfig(jsonConfig); + await File.WriteAllTextAsync("config.conf", hoconConfig); + File.Delete("config.json"); + Log.Information("config.conf created successfully from config.json"); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to migrate config.json to config.conf"); + throw; + } + } - CurrentConfig = new Config + private async Task LoadConfigFromFileAsync(string filePath) + { + try + { + string hoconText = await File.ReadAllTextAsync(filePath); + Akka.Configuration.Config? hoconConfig = ConfigurationFactory.ParseString(hoconText); + + CurrentConfig = new Config + { + // Auth + DisableBrowserAuth = hoconConfig.GetBoolean("Auth.DisableBrowserAuth"), + + // FFmpeg Settings + FFmpegPath = hoconConfig.GetString("External.FFmpegPath"), + + // Download Settings + DownloadAvatarHeaderPhoto = hoconConfig.GetBoolean("Download.Media.DownloadAvatarHeaderPhoto"), + DownloadPaidPosts = hoconConfig.GetBoolean("Download.Media.DownloadPaidPosts"), + DownloadPosts = hoconConfig.GetBoolean("Download.Media.DownloadPosts"), + DownloadArchived = hoconConfig.GetBoolean("Download.Media.DownloadArchived"), + DownloadStreams = hoconConfig.GetBoolean("Download.Media.DownloadStreams"), + DownloadStories = hoconConfig.GetBoolean("Download.Media.DownloadStories"), + DownloadHighlights = hoconConfig.GetBoolean("Download.Media.DownloadHighlights"), + DownloadMessages = hoconConfig.GetBoolean("Download.Media.DownloadMessages"), + DownloadPaidMessages = hoconConfig.GetBoolean("Download.Media.DownloadPaidMessages"), + DownloadImages = hoconConfig.GetBoolean("Download.Media.DownloadImages"), + DownloadVideos = hoconConfig.GetBoolean("Download.Media.DownloadVideos"), + DownloadAudios = hoconConfig.GetBoolean("Download.Media.DownloadAudios"), + IgnoreOwnMessages = hoconConfig.GetBoolean("Download.IgnoreOwnMessages"), + DownloadPostsIncrementally = hoconConfig.GetBoolean("Download.DownloadPostsIncrementally"), + BypassContentForCreatorsWhoNoLongerExist = + hoconConfig.GetBoolean("Download.BypassContentForCreatorsWhoNoLongerExist"), + DownloadDuplicatedMedia = hoconConfig.GetBoolean("Download.DownloadDuplicatedMedia"), + SkipAds = hoconConfig.GetBoolean("Download.SkipAds"), + DownloadPath = hoconConfig.GetString("Download.DownloadPath"), + DownloadOnlySpecificDates = hoconConfig.GetBoolean("Download.DownloadOnlySpecificDates"), + DownloadDateSelection = + Enum.Parse(hoconConfig.GetString("Download.DownloadDateSelection"), true), + CustomDate = + !string.IsNullOrWhiteSpace(hoconConfig.GetString("Download.CustomDate")) + ? DateTime.Parse(hoconConfig.GetString("Download.CustomDate")) + : null, + ShowScrapeSize = hoconConfig.GetBoolean("Download.ShowScrapeSize"), + DisableTextSanitization = + bool.TryParse(hoconConfig.GetString("Download.DisableTextSanitization", "false"), out bool dts) + ? dts + : false, + DownloadVideoResolution = + ParseVideoResolution(hoconConfig.GetString("Download.DownloadVideoResolution", "source")), + + // File Settings + PaidPostFileNameFormat = hoconConfig.GetString("File.PaidPostFileNameFormat"), + PostFileNameFormat = hoconConfig.GetString("File.PostFileNameFormat"), + PaidMessageFileNameFormat = hoconConfig.GetString("File.PaidMessageFileNameFormat"), + MessageFileNameFormat = hoconConfig.GetString("File.MessageFileNameFormat"), + RenameExistingFilesWhenCustomFormatIsSelected = + hoconConfig.GetBoolean("File.RenameExistingFilesWhenCustomFormatIsSelected"), + + // Folder Settings + FolderPerPaidPost = hoconConfig.GetBoolean("Folder.FolderPerPaidPost"), + FolderPerPost = hoconConfig.GetBoolean("Folder.FolderPerPost"), + FolderPerPaidMessage = hoconConfig.GetBoolean("Folder.FolderPerPaidMessage"), + FolderPerMessage = hoconConfig.GetBoolean("Folder.FolderPerMessage"), + + // Subscription Settings + IncludeExpiredSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeExpiredSubscriptions"), + IncludeRestrictedSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeRestrictedSubscriptions"), + IgnoredUsersListName = hoconConfig.GetString("Subscriptions.IgnoredUsersListName"), + + // Interaction Settings + NonInteractiveMode = hoconConfig.GetBoolean("Interaction.NonInteractiveMode"), + NonInteractiveModeListName = hoconConfig.GetString("Interaction.NonInteractiveModeListName"), + NonInteractiveModePurchasedTab = hoconConfig.GetBoolean("Interaction.NonInteractiveModePurchasedTab"), + + // Performance Settings + Timeout = + string.IsNullOrWhiteSpace(hoconConfig.GetString("Performance.Timeout")) + ? -1 + : hoconConfig.GetInt("Performance.Timeout"), + LimitDownloadRate = hoconConfig.GetBoolean("Performance.LimitDownloadRate"), + DownloadLimitInMbPerSec = hoconConfig.GetInt("Performance.DownloadLimitInMbPerSec"), + + // Logging/Debug Settings + LoggingLevel = Enum.Parse(hoconConfig.GetString("Logging.LoggingLevel"), true) + }; + + // Validate file name formats + ValidateFileNameFormat(CurrentConfig.PaidPostFileNameFormat, "PaidPostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.PostFileNameFormat, "PostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.PaidMessageFileNameFormat, "PaidMessageFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.MessageFileNameFormat, "MessageFileNameFormat"); + + // Load creator-specific configs + Akka.Configuration.Config? creatorConfigsSection = hoconConfig.GetConfig("CreatorConfigs"); + if (creatorConfigsSection != null) + { + foreach (KeyValuePair key in creatorConfigsSection.AsEnumerable()) { - // Auth - DisableBrowserAuth = hoconConfig.GetBoolean("Auth.DisableBrowserAuth"), - - // FFmpeg Settings - FFmpegPath = hoconConfig.GetString("External.FFmpegPath"), - - // Download Settings - DownloadAvatarHeaderPhoto = hoconConfig.GetBoolean("Download.Media.DownloadAvatarHeaderPhoto"), - DownloadPaidPosts = hoconConfig.GetBoolean("Download.Media.DownloadPaidPosts"), - DownloadPosts = hoconConfig.GetBoolean("Download.Media.DownloadPosts"), - DownloadArchived = hoconConfig.GetBoolean("Download.Media.DownloadArchived"), - DownloadStreams = hoconConfig.GetBoolean("Download.Media.DownloadStreams"), - DownloadStories = hoconConfig.GetBoolean("Download.Media.DownloadStories"), - DownloadHighlights = hoconConfig.GetBoolean("Download.Media.DownloadHighlights"), - DownloadMessages = hoconConfig.GetBoolean("Download.Media.DownloadMessages"), - DownloadPaidMessages = hoconConfig.GetBoolean("Download.Media.DownloadPaidMessages"), - DownloadImages = hoconConfig.GetBoolean("Download.Media.DownloadImages"), - DownloadVideos = hoconConfig.GetBoolean("Download.Media.DownloadVideos"), - DownloadAudios = hoconConfig.GetBoolean("Download.Media.DownloadAudios"), - IgnoreOwnMessages = hoconConfig.GetBoolean("Download.IgnoreOwnMessages"), - DownloadPostsIncrementally = hoconConfig.GetBoolean("Download.DownloadPostsIncrementally"), - BypassContentForCreatorsWhoNoLongerExist = hoconConfig.GetBoolean("Download.BypassContentForCreatorsWhoNoLongerExist"), - DownloadDuplicatedMedia = hoconConfig.GetBoolean("Download.DownloadDuplicatedMedia"), - SkipAds = hoconConfig.GetBoolean("Download.SkipAds"), - DownloadPath = hoconConfig.GetString("Download.DownloadPath"), - DownloadOnlySpecificDates = hoconConfig.GetBoolean("Download.DownloadOnlySpecificDates"), - DownloadDateSelection = Enum.Parse(hoconConfig.GetString("Download.DownloadDateSelection"), true), - CustomDate = !string.IsNullOrWhiteSpace(hoconConfig.GetString("Download.CustomDate")) ? DateTime.Parse(hoconConfig.GetString("Download.CustomDate")) : null, - ShowScrapeSize = hoconConfig.GetBoolean("Download.ShowScrapeSize"), - DisableTextSanitization = bool.TryParse(hoconConfig.GetString("Download.DisableTextSanitization", "false"), out var dts) ? dts : false, - DownloadVideoResolution = ParseVideoResolution(hoconConfig.GetString("Download.DownloadVideoResolution", "source")), - - // File Settings - PaidPostFileNameFormat = hoconConfig.GetString("File.PaidPostFileNameFormat"), - PostFileNameFormat = hoconConfig.GetString("File.PostFileNameFormat"), - PaidMessageFileNameFormat = hoconConfig.GetString("File.PaidMessageFileNameFormat"), - MessageFileNameFormat = hoconConfig.GetString("File.MessageFileNameFormat"), - RenameExistingFilesWhenCustomFormatIsSelected = hoconConfig.GetBoolean("File.RenameExistingFilesWhenCustomFormatIsSelected"), - - // Folder Settings - FolderPerPaidPost = hoconConfig.GetBoolean("Folder.FolderPerPaidPost"), - FolderPerPost = hoconConfig.GetBoolean("Folder.FolderPerPost"), - FolderPerPaidMessage = hoconConfig.GetBoolean("Folder.FolderPerPaidMessage"), - FolderPerMessage = hoconConfig.GetBoolean("Folder.FolderPerMessage"), - - // Subscription Settings - IncludeExpiredSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeExpiredSubscriptions"), - IncludeRestrictedSubscriptions = hoconConfig.GetBoolean("Subscriptions.IncludeRestrictedSubscriptions"), - IgnoredUsersListName = hoconConfig.GetString("Subscriptions.IgnoredUsersListName"), - - // Interaction Settings - NonInteractiveMode = hoconConfig.GetBoolean("Interaction.NonInteractiveMode"), - NonInteractiveModeListName = hoconConfig.GetString("Interaction.NonInteractiveModeListName"), - NonInteractiveModePurchasedTab = hoconConfig.GetBoolean("Interaction.NonInteractiveModePurchasedTab"), - - // Performance Settings - Timeout = string.IsNullOrWhiteSpace(hoconConfig.GetString("Performance.Timeout")) ? -1 : hoconConfig.GetInt("Performance.Timeout"), - LimitDownloadRate = hoconConfig.GetBoolean("Performance.LimitDownloadRate"), - DownloadLimitInMbPerSec = hoconConfig.GetInt("Performance.DownloadLimitInMbPerSec"), - - // Logging/Debug Settings - LoggingLevel = Enum.Parse(hoconConfig.GetString("Logging.LoggingLevel"), true) - }; - - // Validate file name formats - ValidateFileNameFormat(CurrentConfig.PaidPostFileNameFormat, "PaidPostFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.PostFileNameFormat, "PostFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.PaidMessageFileNameFormat, "PaidMessageFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.MessageFileNameFormat, "MessageFileNameFormat"); - - // Load creator-specific configs - var creatorConfigsSection = hoconConfig.GetConfig("CreatorConfigs"); - if (creatorConfigsSection != null) - { - foreach (var key in creatorConfigsSection.AsEnumerable()) + string creatorKey = key.Key; + Akka.Configuration.Config? creatorHocon = creatorConfigsSection.GetConfig(creatorKey); + if (!CurrentConfig.CreatorConfigs.ContainsKey(creatorKey) && creatorHocon != null) { - var creatorKey = key.Key; - var creatorHocon = creatorConfigsSection.GetConfig(creatorKey); - if (!CurrentConfig.CreatorConfigs.ContainsKey(creatorKey) && creatorHocon != null) - { - CurrentConfig.CreatorConfigs.Add(key.Key, new CreatorConfig + CurrentConfig.CreatorConfigs.Add(key.Key, + new CreatorConfig { PaidPostFileNameFormat = creatorHocon.GetString("PaidPostFileNameFormat"), PostFileNameFormat = creatorHocon.GetString("PostFileNameFormat"), @@ -225,161 +243,172 @@ namespace OF_DL.Services MessageFileNameFormat = creatorHocon.GetString("MessageFileNameFormat") }); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidPostFileNameFormat, $"{key.Key}.PaidPostFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PostFileNameFormat, $"{key.Key}.PostFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidMessageFileNameFormat, $"{key.Key}.PaidMessageFileNameFormat"); - ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].MessageFileNameFormat, $"{key.Key}.MessageFileNameFormat"); - } + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidPostFileNameFormat, + $"{key.Key}.PaidPostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PostFileNameFormat, + $"{key.Key}.PostFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].PaidMessageFileNameFormat, + $"{key.Key}.PaidMessageFileNameFormat"); + ValidateFileNameFormat(CurrentConfig.CreatorConfigs[key.Key].MessageFileNameFormat, + $"{key.Key}.MessageFileNameFormat"); } } - - // Update logging level - loggingService.UpdateLoggingLevel(CurrentConfig.LoggingLevel); - - // Apply text sanitization preference globally - OF_DL.Utils.XmlUtils.Passthrough = CurrentConfig.DisableTextSanitization; - - Log.Debug("Configuration loaded successfully"); - string configString = JsonConvert.SerializeObject(CurrentConfig, Formatting.Indented); - Log.Debug(configString); - - return true; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to parse config file"); - return false; } + + // Update logging level + loggingService.UpdateLoggingLevel(CurrentConfig.LoggingLevel); + + // Apply text sanitization preference globally + XmlUtils.Passthrough = CurrentConfig.DisableTextSanitization; + + Log.Debug("Configuration loaded successfully"); + string configString = JsonConvert.SerializeObject(CurrentConfig, Formatting.Indented); + Log.Debug(configString); + + return true; } - - private async Task CreateDefaultConfigFileAsync() + catch (Exception ex) { - Config defaultConfig = new Config(); - var hoconConfig = BuildHoconFromConfig(defaultConfig); - await File.WriteAllTextAsync("config.conf", hoconConfig); - Log.Information("Created default config.conf file"); - } - - private string BuildHoconFromConfig(Config config) - { - var hocon = new StringBuilder(); - - hocon.AppendLine("# Auth"); - hocon.AppendLine("Auth {"); - hocon.AppendLine($" DisableBrowserAuth = {config.DisableBrowserAuth.ToString().ToLower()}"); - hocon.AppendLine("}"); - - hocon.AppendLine("# External Tools"); - hocon.AppendLine("External {"); - hocon.AppendLine($" FFmpegPath = \"{config.FFmpegPath}\""); - hocon.AppendLine("}"); - - hocon.AppendLine("# Download Settings"); - hocon.AppendLine("Download {"); - hocon.AppendLine(" Media {"); - hocon.AppendLine($" DownloadAvatarHeaderPhoto = {config.DownloadAvatarHeaderPhoto.ToString().ToLower()}"); - hocon.AppendLine($" DownloadPaidPosts = {config.DownloadPaidPosts.ToString().ToLower()}"); - hocon.AppendLine($" DownloadPosts = {config.DownloadPosts.ToString().ToLower()}"); - hocon.AppendLine($" DownloadArchived = {config.DownloadArchived.ToString().ToLower()}"); - hocon.AppendLine($" DownloadStreams = {config.DownloadStreams.ToString().ToLower()}"); - hocon.AppendLine($" DownloadStories = {config.DownloadStories.ToString().ToLower()}"); - hocon.AppendLine($" DownloadHighlights = {config.DownloadHighlights.ToString().ToLower()}"); - hocon.AppendLine($" DownloadMessages = {config.DownloadMessages.ToString().ToLower()}"); - hocon.AppendLine($" DownloadPaidMessages = {config.DownloadPaidMessages.ToString().ToLower()}"); - hocon.AppendLine($" DownloadImages = {config.DownloadImages.ToString().ToLower()}"); - hocon.AppendLine($" DownloadVideos = {config.DownloadVideos.ToString().ToLower()}"); - hocon.AppendLine($" DownloadAudios = {config.DownloadAudios.ToString().ToLower()}"); - hocon.AppendLine(" }"); - hocon.AppendLine($" IgnoreOwnMessages = {config.IgnoreOwnMessages.ToString().ToLower()}"); - hocon.AppendLine($" DownloadPostsIncrementally = {config.DownloadPostsIncrementally.ToString().ToLower()}"); - hocon.AppendLine($" BypassContentForCreatorsWhoNoLongerExist = {config.BypassContentForCreatorsWhoNoLongerExist.ToString().ToLower()}"); - hocon.AppendLine($" DownloadDuplicatedMedia = {config.DownloadDuplicatedMedia.ToString().ToLower()}"); - hocon.AppendLine($" SkipAds = {config.SkipAds.ToString().ToLower()}"); - hocon.AppendLine($" DownloadPath = \"{config.DownloadPath}\""); - hocon.AppendLine($" DownloadOnlySpecificDates = {config.DownloadOnlySpecificDates.ToString().ToLower()}"); - hocon.AppendLine($" DownloadDateSelection = \"{config.DownloadDateSelection.ToString().ToLower()}\""); - hocon.AppendLine($" CustomDate = \"{config.CustomDate?.ToString("yyyy-MM-dd")}\""); - hocon.AppendLine($" ShowScrapeSize = {config.ShowScrapeSize.ToString().ToLower()}"); - hocon.AppendLine($" DisableTextSanitization = {config.DisableTextSanitization.ToString().ToLower()}"); - hocon.AppendLine($" DownloadVideoResolution = \"{(config.DownloadVideoResolution == VideoResolution.source ? "source" : config.DownloadVideoResolution.ToString().TrimStart('_'))}\""); - hocon.AppendLine("}"); - - hocon.AppendLine("# File Settings"); - hocon.AppendLine("File {"); - hocon.AppendLine($" PaidPostFileNameFormat = \"{config.PaidPostFileNameFormat}\""); - hocon.AppendLine($" PostFileNameFormat = \"{config.PostFileNameFormat}\""); - hocon.AppendLine($" PaidMessageFileNameFormat = \"{config.PaidMessageFileNameFormat}\""); - hocon.AppendLine($" MessageFileNameFormat = \"{config.MessageFileNameFormat}\""); - hocon.AppendLine($" RenameExistingFilesWhenCustomFormatIsSelected = {config.RenameExistingFilesWhenCustomFormatIsSelected.ToString().ToLower()}"); - hocon.AppendLine("}"); - - hocon.AppendLine("# Creator-Specific Configurations"); - hocon.AppendLine("CreatorConfigs {"); - foreach (var creatorConfig in config.CreatorConfigs) - { - hocon.AppendLine($" \"{creatorConfig.Key}\" {{"); - hocon.AppendLine($" PaidPostFileNameFormat = \"{creatorConfig.Value.PaidPostFileNameFormat}\""); - hocon.AppendLine($" PostFileNameFormat = \"{creatorConfig.Value.PostFileNameFormat}\""); - hocon.AppendLine($" PaidMessageFileNameFormat = \"{creatorConfig.Value.PaidMessageFileNameFormat}\""); - hocon.AppendLine($" MessageFileNameFormat = \"{creatorConfig.Value.MessageFileNameFormat}\""); - hocon.AppendLine(" }"); - } - hocon.AppendLine("}"); - - hocon.AppendLine("# Folder Settings"); - hocon.AppendLine("Folder {"); - hocon.AppendLine($" FolderPerPaidPost = {config.FolderPerPaidPost.ToString().ToLower()}"); - hocon.AppendLine($" FolderPerPost = {config.FolderPerPost.ToString().ToLower()}"); - hocon.AppendLine($" FolderPerPaidMessage = {config.FolderPerPaidMessage.ToString().ToLower()}"); - hocon.AppendLine($" FolderPerMessage = {config.FolderPerMessage.ToString().ToLower()}"); - hocon.AppendLine("}"); - - hocon.AppendLine("# Subscription Settings"); - hocon.AppendLine("Subscriptions {"); - hocon.AppendLine($" IncludeExpiredSubscriptions = {config.IncludeExpiredSubscriptions.ToString().ToLower()}"); - hocon.AppendLine($" IncludeRestrictedSubscriptions = {config.IncludeRestrictedSubscriptions.ToString().ToLower()}"); - hocon.AppendLine($" IgnoredUsersListName = \"{config.IgnoredUsersListName}\""); - hocon.AppendLine("}"); - - hocon.AppendLine("# Interaction Settings"); - hocon.AppendLine("Interaction {"); - hocon.AppendLine($" NonInteractiveMode = {config.NonInteractiveMode.ToString().ToLower()}"); - hocon.AppendLine($" NonInteractiveModeListName = \"{config.NonInteractiveModeListName}\""); - hocon.AppendLine($" NonInteractiveModePurchasedTab = {config.NonInteractiveModePurchasedTab.ToString().ToLower()}"); - hocon.AppendLine("}"); - - hocon.AppendLine("# Performance Settings"); - hocon.AppendLine("Performance {"); - hocon.AppendLine($" Timeout = {(config.Timeout.HasValue ? config.Timeout.Value : -1)}"); - hocon.AppendLine($" LimitDownloadRate = {config.LimitDownloadRate.ToString().ToLower()}"); - hocon.AppendLine($" DownloadLimitInMbPerSec = {config.DownloadLimitInMbPerSec}"); - hocon.AppendLine("}"); - - hocon.AppendLine("# Logging/Debug Settings"); - hocon.AppendLine("Logging {"); - hocon.AppendLine($" LoggingLevel = \"{config.LoggingLevel.ToString().ToLower()}\""); - hocon.AppendLine("}"); - - return hocon.ToString(); - } - - private void ValidateFileNameFormat(string? format, string settingName) - { - if (!string.IsNullOrEmpty(format) && - !format.Contains("{mediaId}", StringComparison.OrdinalIgnoreCase) && - !format.Contains("{filename}", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException( - $"{settingName} is not unique enough. Please include either '{{mediaId}}' or '{{filename}}' to ensure files are not overwritten."); - } - } - - private VideoResolution ParseVideoResolution(string value) - { - if (value.Equals("source", StringComparison.OrdinalIgnoreCase)) - return VideoResolution.source; - - return Enum.Parse("_" + value, ignoreCase: true); + Log.Error(ex, "Failed to parse config file"); + return false; } } + + private async Task CreateDefaultConfigFileAsync() + { + Config defaultConfig = new(); + string hoconConfig = BuildHoconFromConfig(defaultConfig); + await File.WriteAllTextAsync("config.conf", hoconConfig); + Log.Information("Created default config.conf file"); + } + + private string BuildHoconFromConfig(Config config) + { + StringBuilder hocon = new(); + + hocon.AppendLine("# Auth"); + hocon.AppendLine("Auth {"); + hocon.AppendLine($" DisableBrowserAuth = {config.DisableBrowserAuth.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# External Tools"); + hocon.AppendLine("External {"); + hocon.AppendLine($" FFmpegPath = \"{config.FFmpegPath}\""); + hocon.AppendLine("}"); + + hocon.AppendLine("# Download Settings"); + hocon.AppendLine("Download {"); + hocon.AppendLine(" Media {"); + hocon.AppendLine($" DownloadAvatarHeaderPhoto = {config.DownloadAvatarHeaderPhoto.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPaidPosts = {config.DownloadPaidPosts.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPosts = {config.DownloadPosts.ToString().ToLower()}"); + hocon.AppendLine($" DownloadArchived = {config.DownloadArchived.ToString().ToLower()}"); + hocon.AppendLine($" DownloadStreams = {config.DownloadStreams.ToString().ToLower()}"); + hocon.AppendLine($" DownloadStories = {config.DownloadStories.ToString().ToLower()}"); + hocon.AppendLine($" DownloadHighlights = {config.DownloadHighlights.ToString().ToLower()}"); + hocon.AppendLine($" DownloadMessages = {config.DownloadMessages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPaidMessages = {config.DownloadPaidMessages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadImages = {config.DownloadImages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadVideos = {config.DownloadVideos.ToString().ToLower()}"); + hocon.AppendLine($" DownloadAudios = {config.DownloadAudios.ToString().ToLower()}"); + hocon.AppendLine(" }"); + hocon.AppendLine($" IgnoreOwnMessages = {config.IgnoreOwnMessages.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPostsIncrementally = {config.DownloadPostsIncrementally.ToString().ToLower()}"); + hocon.AppendLine( + $" BypassContentForCreatorsWhoNoLongerExist = {config.BypassContentForCreatorsWhoNoLongerExist.ToString().ToLower()}"); + hocon.AppendLine($" DownloadDuplicatedMedia = {config.DownloadDuplicatedMedia.ToString().ToLower()}"); + hocon.AppendLine($" SkipAds = {config.SkipAds.ToString().ToLower()}"); + hocon.AppendLine($" DownloadPath = \"{config.DownloadPath}\""); + hocon.AppendLine($" DownloadOnlySpecificDates = {config.DownloadOnlySpecificDates.ToString().ToLower()}"); + hocon.AppendLine($" DownloadDateSelection = \"{config.DownloadDateSelection.ToString().ToLower()}\""); + hocon.AppendLine($" CustomDate = \"{config.CustomDate?.ToString("yyyy-MM-dd")}\""); + hocon.AppendLine($" ShowScrapeSize = {config.ShowScrapeSize.ToString().ToLower()}"); + hocon.AppendLine($" DisableTextSanitization = {config.DisableTextSanitization.ToString().ToLower()}"); + hocon.AppendLine( + $" DownloadVideoResolution = \"{(config.DownloadVideoResolution == VideoResolution.source ? "source" : config.DownloadVideoResolution.ToString().TrimStart('_'))}\""); + hocon.AppendLine("}"); + + hocon.AppendLine("# File Settings"); + hocon.AppendLine("File {"); + hocon.AppendLine($" PaidPostFileNameFormat = \"{config.PaidPostFileNameFormat}\""); + hocon.AppendLine($" PostFileNameFormat = \"{config.PostFileNameFormat}\""); + hocon.AppendLine($" PaidMessageFileNameFormat = \"{config.PaidMessageFileNameFormat}\""); + hocon.AppendLine($" MessageFileNameFormat = \"{config.MessageFileNameFormat}\""); + hocon.AppendLine( + $" RenameExistingFilesWhenCustomFormatIsSelected = {config.RenameExistingFilesWhenCustomFormatIsSelected.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Creator-Specific Configurations"); + hocon.AppendLine("CreatorConfigs {"); + foreach (KeyValuePair creatorConfig in config.CreatorConfigs) + { + hocon.AppendLine($" \"{creatorConfig.Key}\" {{"); + hocon.AppendLine($" PaidPostFileNameFormat = \"{creatorConfig.Value.PaidPostFileNameFormat}\""); + hocon.AppendLine($" PostFileNameFormat = \"{creatorConfig.Value.PostFileNameFormat}\""); + hocon.AppendLine($" PaidMessageFileNameFormat = \"{creatorConfig.Value.PaidMessageFileNameFormat}\""); + hocon.AppendLine($" MessageFileNameFormat = \"{creatorConfig.Value.MessageFileNameFormat}\""); + hocon.AppendLine(" }"); + } + + hocon.AppendLine("}"); + + hocon.AppendLine("# Folder Settings"); + hocon.AppendLine("Folder {"); + hocon.AppendLine($" FolderPerPaidPost = {config.FolderPerPaidPost.ToString().ToLower()}"); + hocon.AppendLine($" FolderPerPost = {config.FolderPerPost.ToString().ToLower()}"); + hocon.AppendLine($" FolderPerPaidMessage = {config.FolderPerPaidMessage.ToString().ToLower()}"); + hocon.AppendLine($" FolderPerMessage = {config.FolderPerMessage.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Subscription Settings"); + hocon.AppendLine("Subscriptions {"); + hocon.AppendLine($" IncludeExpiredSubscriptions = {config.IncludeExpiredSubscriptions.ToString().ToLower()}"); + hocon.AppendLine( + $" IncludeRestrictedSubscriptions = {config.IncludeRestrictedSubscriptions.ToString().ToLower()}"); + hocon.AppendLine($" IgnoredUsersListName = \"{config.IgnoredUsersListName}\""); + hocon.AppendLine("}"); + + hocon.AppendLine("# Interaction Settings"); + hocon.AppendLine("Interaction {"); + hocon.AppendLine($" NonInteractiveMode = {config.NonInteractiveMode.ToString().ToLower()}"); + hocon.AppendLine($" NonInteractiveModeListName = \"{config.NonInteractiveModeListName}\""); + hocon.AppendLine( + $" NonInteractiveModePurchasedTab = {config.NonInteractiveModePurchasedTab.ToString().ToLower()}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Performance Settings"); + hocon.AppendLine("Performance {"); + hocon.AppendLine($" Timeout = {(config.Timeout.HasValue ? config.Timeout.Value : -1)}"); + hocon.AppendLine($" LimitDownloadRate = {config.LimitDownloadRate.ToString().ToLower()}"); + hocon.AppendLine($" DownloadLimitInMbPerSec = {config.DownloadLimitInMbPerSec}"); + hocon.AppendLine("}"); + + hocon.AppendLine("# Logging/Debug Settings"); + hocon.AppendLine("Logging {"); + hocon.AppendLine($" LoggingLevel = \"{config.LoggingLevel.ToString().ToLower()}\""); + hocon.AppendLine("}"); + + return hocon.ToString(); + } + + private void ValidateFileNameFormat(string? format, string settingName) + { + if (!string.IsNullOrEmpty(format) && + !format.Contains("{mediaId}", StringComparison.OrdinalIgnoreCase) && + !format.Contains("{filename}", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + $"{settingName} is not unique enough. Please include either '{{mediaId}}' or '{{filename}}' to ensure files are not overwritten."); + } + } + + private VideoResolution ParseVideoResolution(string value) + { + if (value.Equals("source", StringComparison.OrdinalIgnoreCase)) + { + return VideoResolution.source; + } + + return Enum.Parse("_" + value, true); + } } diff --git a/OF DL/Services/DBService.cs b/OF DL/Services/DBService.cs index 70efd80..2ff6c29 100644 --- a/OF DL/Services/DBService.cs +++ b/OF DL/Services/DBService.cs @@ -2,38 +2,41 @@ using System.Text; using Microsoft.Data.Sqlite; using Serilog; -namespace OF_DL.Services +namespace OF_DL.Services; + +public class DBService(IConfigService configService) : IDBService { - public class DBService(IConfigService configService) : IDBService + public async Task CreateDB(string folder) { - public async Task CreateDB(string folder) + try { - try + if (!Directory.Exists(folder + "/Metadata")) { - if (!Directory.Exists(folder + "/Metadata")) - { - Directory.CreateDirectory(folder + "/Metadata"); - } + Directory.CreateDirectory(folder + "/Metadata"); + } - string dbFilePath = $"{folder}/Metadata/user_data.db"; + string dbFilePath = $"{folder}/Metadata/user_data.db"; - // connect to the new database file - using SqliteConnection connection = new($"Data Source={dbFilePath}"); - // open the connection - connection.Open(); + // connect to the new database file + using SqliteConnection connection = new($"Data Source={dbFilePath}"); + // open the connection + connection.Open(); - // create the 'medias' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS medias (id INTEGER NOT NULL, media_id INTEGER, post_id INTEGER NOT NULL, link VARCHAR, directory VARCHAR, filename VARCHAR, size INTEGER, api_type VARCHAR, media_type VARCHAR, preview INTEGER, linked VARCHAR, downloaded INTEGER, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(media_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } + // create the 'medias' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS medias (id INTEGER NOT NULL, media_id INTEGER, post_id INTEGER NOT NULL, link VARCHAR, directory VARCHAR, filename VARCHAR, size INTEGER, api_type VARCHAR, media_type VARCHAR, preview INTEGER, linked VARCHAR, downloaded INTEGER, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(media_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } - await EnsureCreatedAtColumnExists(connection, "medias"); + await EnsureCreatedAtColumnExists(connection, "medias"); - // - // Alter existing databases to create unique constraint on `medias` - // - using (SqliteCommand cmd = new(@" + // + // Alter existing databases to create unique constraint on `medias` + // + using (SqliteCommand cmd = new(@" PRAGMA foreign_keys=off; BEGIN TRANSACTION; @@ -66,408 +69,473 @@ namespace OF_DL.Services COMMIT; PRAGMA foreign_keys=on;", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'messages' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS messages (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, user_id INTEGER, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'posts' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS posts (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'stories' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS stories (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'others' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS others (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'products' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS products (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, title VARCHAR, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - // create the 'profiles' table - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS profiles (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(username));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - connection.Close(); - } - catch (Exception ex) { - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - if (ex.InnerException != null) - { - Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - } + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'messages' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS messages (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, user_id INTEGER, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'posts' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS posts (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'stories' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS stories (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'others' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS others (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'products' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS products (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, title VARCHAR, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + // create the 'profiles' table + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS profiles (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(username));", + connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + connection.Close(); + } + catch (Exception ex) + { + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + if (ex.InnerException != null) + { + Console.WriteLine("\nInner Exception:"); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + } - public async Task CreateUsersDB(Dictionary users) + public async Task CreateUsersDB(Dictionary users) + { + try { - try + using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); + Log.Debug("Database data source: " + connection.DataSource); + + connection.Open(); + + using (SqliteCommand cmd = + new( + "CREATE TABLE IF NOT EXISTS users (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, PRIMARY KEY(id), UNIQUE(username));", + connection)) { - using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); - Log.Debug("Database data source: " + connection.DataSource); - - connection.Open(); - - using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS users (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, PRIMARY KEY(id), UNIQUE(username));", connection)) - { - await cmd.ExecuteNonQueryAsync(); - } - - Log.Debug("Adding missing creators"); - foreach (KeyValuePair user in users) - { - using (SqliteCommand checkCmd = new($"SELECT user_id, username FROM users WHERE user_id = @userId;", connection)) - { - checkCmd.Parameters.AddWithValue("@userId", user.Value); - using (var reader = await checkCmd.ExecuteReaderAsync()) - { - if (!reader.Read()) - { - using (SqliteCommand insertCmd = new($"INSERT INTO users (user_id, username) VALUES (@userId, @username);", connection)) - { - insertCmd.Parameters.AddWithValue("@userId", user.Value); - insertCmd.Parameters.AddWithValue("@username", user.Key); - await insertCmd.ExecuteNonQueryAsync(); - Log.Debug("Inserted new creator: " + user.Key); - } - } - else - { - Log.Debug("Creator " + user.Key + " already exists"); - } - } - } - } - - connection.Close(); + await cmd.ExecuteNonQueryAsync(); } - catch (Exception ex) + + Log.Debug("Adding missing creators"); + foreach (KeyValuePair user in users) { - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - if (ex.InnerException != null) - { - Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - } - } - } - - public async Task CheckUsername(KeyValuePair user, string path) - { - try - { - using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); - - connection.Open(); - - using (SqliteCommand checkCmd = new($"SELECT user_id, username FROM users WHERE user_id = @userId;", connection)) + using (SqliteCommand checkCmd = new("SELECT user_id, username FROM users WHERE user_id = @userId;", + connection)) { checkCmd.Parameters.AddWithValue("@userId", user.Value); - using (var reader = await checkCmd.ExecuteReaderAsync()) + using (SqliteDataReader reader = await checkCmd.ExecuteReaderAsync()) { - if (reader.Read()) + if (!reader.Read()) { - long storedUserId = reader.GetInt64(0); - string storedUsername = reader.GetString(1); - - if (storedUsername != user.Key) + using (SqliteCommand insertCmd = + new("INSERT INTO users (user_id, username) VALUES (@userId, @username);", + connection)) { - using (SqliteCommand updateCmd = new($"UPDATE users SET username = @newUsername WHERE user_id = @userId;", connection)) - { - updateCmd.Parameters.AddWithValue("@newUsername", user.Key); - updateCmd.Parameters.AddWithValue("@userId", user.Value); - await updateCmd.ExecuteNonQueryAsync(); - } + insertCmd.Parameters.AddWithValue("@userId", user.Value); + insertCmd.Parameters.AddWithValue("@username", user.Key); + await insertCmd.ExecuteNonQueryAsync(); + Log.Debug("Inserted new creator: " + user.Key); + } + } + else + { + Log.Debug("Creator " + user.Key + " already exists"); + } + } + } + } - string oldPath = path.Replace(path.Split("/")[^1], storedUsername); + connection.Close(); + } + catch (Exception ex) + { + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + if (ex.InnerException != null) + { + Console.WriteLine("\nInner Exception:"); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + } + } + } - if (Directory.Exists(oldPath)) - { - Directory.Move(path.Replace(path.Split("/")[^1], storedUsername), path); - } + public async Task CheckUsername(KeyValuePair user, string path) + { + try + { + using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); + + connection.Open(); + + using (SqliteCommand checkCmd = new("SELECT user_id, username FROM users WHERE user_id = @userId;", + connection)) + { + checkCmd.Parameters.AddWithValue("@userId", user.Value); + using (SqliteDataReader reader = await checkCmd.ExecuteReaderAsync()) + { + if (reader.Read()) + { + long storedUserId = reader.GetInt64(0); + string storedUsername = reader.GetString(1); + + if (storedUsername != user.Key) + { + using (SqliteCommand updateCmd = + new("UPDATE users SET username = @newUsername WHERE user_id = @userId;", connection)) + { + updateCmd.Parameters.AddWithValue("@newUsername", user.Key); + updateCmd.Parameters.AddWithValue("@userId", user.Value); + await updateCmd.ExecuteNonQueryAsync(); + } + + string oldPath = path.Replace(path.Split("/")[^1], storedUsername); + + if (Directory.Exists(oldPath)) + { + Directory.Move(path.Replace(path.Split("/")[^1], storedUsername), path); } } } } + } - connection.Close(); - } - catch (Exception ex) - { - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - if (ex.InnerException != null) - { - Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - } - } + connection.Close(); } - - public async Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at, long user_id) + catch (Exception ex) { - try + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + if (ex.InnerException != null) { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); - connection.Open(); - await EnsureCreatedAtColumnExists(connection, "messages"); - using SqliteCommand cmd = new($"SELECT COUNT(*) FROM messages WHERE post_id=@post_id", connection); - cmd.Parameters.AddWithValue("@post_id", post_id); - int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); - if (count == 0) - { - // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = new("INSERT INTO messages(post_id, text, price, paid, archived, created_at, user_id, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @user_id, @record_created_at)", connection); - insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@is_paid", is_paid); - insertCmd.Parameters.AddWithValue("@is_archived", is_archived); - insertCmd.Parameters.AddWithValue("@created_at", created_at); - insertCmd.Parameters.AddWithValue("@user_id", user_id); - insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); - await insertCmd.ExecuteNonQueryAsync(); - } - } - catch (Exception ex) - { - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - if (ex.InnerException != null) - { - Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - } + Console.WriteLine("\nInner Exception:"); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } } + } - - public async Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at) - { - try - { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); - connection.Open(); - await EnsureCreatedAtColumnExists(connection, "posts"); - using SqliteCommand cmd = new($"SELECT COUNT(*) FROM posts WHERE post_id=@post_id", connection); - cmd.Parameters.AddWithValue("@post_id", post_id); - int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); - if (count == 0) - { - // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = new("INSERT INTO posts(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", connection); - insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@is_paid", is_paid); - insertCmd.Parameters.AddWithValue("@is_archived", is_archived); - insertCmd.Parameters.AddWithValue("@created_at", created_at); - insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); - await insertCmd.ExecuteNonQueryAsync(); - } - } - catch (Exception ex) - { - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - if (ex.InnerException != null) - { - Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - } - } - } - - - public async Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at) - { - try - { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); - connection.Open(); - await EnsureCreatedAtColumnExists(connection, "stories"); - using SqliteCommand cmd = new($"SELECT COUNT(*) FROM stories WHERE post_id=@post_id", connection); - cmd.Parameters.AddWithValue("@post_id", post_id); - int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); - if (count == 0) - { - // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = new("INSERT INTO stories(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", connection); - insertCmd.Parameters.AddWithValue("@post_id", post_id); - insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); - insertCmd.Parameters.AddWithValue("@is_paid", is_paid); - insertCmd.Parameters.AddWithValue("@is_archived", is_archived); - insertCmd.Parameters.AddWithValue("@created_at", created_at); - insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); - await insertCmd.ExecuteNonQueryAsync(); - } - } - catch (Exception ex) - { - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - if (ex.InnerException != null) - { - Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - } - } - } - - - public async Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, string? filename, long? size, string api_type, string media_type, bool preview, bool downloaded, DateTime? created_at) - { - try - { - using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); - connection.Open(); - await EnsureCreatedAtColumnExists(connection, "medias"); - StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM medias WHERE media_id=@media_id"); - if (configService.CurrentConfig.DownloadDuplicatedMedia) - { - sql.Append(" and api_type=@api_type"); - } - - using SqliteCommand cmd = new(sql.ToString(), connection); - cmd.Parameters.AddWithValue("@media_id", media_id); - cmd.Parameters.AddWithValue("@api_type", api_type); - int count = Convert.ToInt32(cmd.ExecuteScalar()); - if (count == 0) - { - // If the record doesn't exist, insert a new one - using SqliteCommand insertCmd = new($"INSERT INTO medias(media_id, post_id, link, directory, filename, size, api_type, media_type, preview, downloaded, created_at, record_created_at) VALUES({media_id}, {post_id}, '{link}', '{directory?.ToString() ?? "NULL"}', '{filename?.ToString() ?? "NULL"}', {size?.ToString() ?? "NULL"}, '{api_type}', '{media_type}', {Convert.ToInt32(preview)}, {Convert.ToInt32(downloaded)}, '{created_at?.ToString("yyyy-MM-dd HH:mm:ss")}', '{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}')", connection); - await insertCmd.ExecuteNonQueryAsync(); - } - } - catch (Exception ex) - { - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - if (ex.InnerException != null) - { - Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - } - } - } - - - public async Task CheckDownloaded(string folder, long media_id, string api_type) - { - try - { - bool downloaded = false; - - using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) - { - StringBuilder sql = new StringBuilder("SELECT downloaded FROM medias WHERE media_id=@media_id"); - if(configService.CurrentConfig.DownloadDuplicatedMedia) - { - sql.Append(" and api_type=@api_type"); - } - - connection.Open(); - using SqliteCommand cmd = new (sql.ToString(), connection); - cmd.Parameters.AddWithValue("@media_id", media_id); - cmd.Parameters.AddWithValue("@api_type", api_type); - downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync()); - } - return downloaded; - } - catch (Exception ex) - { - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); - if (ex.InnerException != null) - { - Console.WriteLine("\nInner Exception:"); - Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace); - } - } - return false; - } - - - public async Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, long size, bool downloaded, DateTime created_at) + public async Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, + bool is_archived, DateTime created_at, long user_id) + { + try { using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); connection.Open(); + await EnsureCreatedAtColumnExists(connection, "messages"); + using SqliteCommand cmd = new("SELECT COUNT(*) FROM messages WHERE post_id=@post_id", connection); + cmd.Parameters.AddWithValue("@post_id", post_id); + int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + if (count == 0) + { + // If the record doesn't exist, insert a new one + using SqliteCommand insertCmd = + new( + "INSERT INTO messages(post_id, text, price, paid, archived, created_at, user_id, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @user_id, @record_created_at)", + connection); + insertCmd.Parameters.AddWithValue("@post_id", post_id); + insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@is_paid", is_paid); + insertCmd.Parameters.AddWithValue("@is_archived", is_archived); + insertCmd.Parameters.AddWithValue("@created_at", created_at); + insertCmd.Parameters.AddWithValue("@user_id", user_id); + insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + await insertCmd.ExecuteNonQueryAsync(); + } + } + catch (Exception ex) + { + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + if (ex.InnerException != null) + { + Console.WriteLine("\nInner Exception:"); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + } + } + } - // Construct the update command - StringBuilder sql = new StringBuilder("UPDATE medias SET directory=@directory, filename=@filename, size=@size, downloaded=@downloaded, created_at=@created_at WHERE media_id=@media_id"); + + public async Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, + bool is_archived, DateTime created_at) + { + try + { + using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + connection.Open(); + await EnsureCreatedAtColumnExists(connection, "posts"); + using SqliteCommand cmd = new("SELECT COUNT(*) FROM posts WHERE post_id=@post_id", connection); + cmd.Parameters.AddWithValue("@post_id", post_id); + int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + if (count == 0) + { + // If the record doesn't exist, insert a new one + using SqliteCommand insertCmd = + new( + "INSERT INTO posts(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", + connection); + insertCmd.Parameters.AddWithValue("@post_id", post_id); + insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@is_paid", is_paid); + insertCmd.Parameters.AddWithValue("@is_archived", is_archived); + insertCmd.Parameters.AddWithValue("@created_at", created_at); + insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + await insertCmd.ExecuteNonQueryAsync(); + } + } + catch (Exception ex) + { + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + if (ex.InnerException != null) + { + Console.WriteLine("\nInner Exception:"); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + } + } + } + + + public async Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, + bool is_archived, DateTime created_at) + { + try + { + using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + connection.Open(); + await EnsureCreatedAtColumnExists(connection, "stories"); + using SqliteCommand cmd = new("SELECT COUNT(*) FROM stories WHERE post_id=@post_id", connection); + cmd.Parameters.AddWithValue("@post_id", post_id); + int count = Convert.ToInt32(await cmd.ExecuteScalarAsync()); + if (count == 0) + { + // If the record doesn't exist, insert a new one + using SqliteCommand insertCmd = + new( + "INSERT INTO stories(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", + connection); + insertCmd.Parameters.AddWithValue("@post_id", post_id); + insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value); + insertCmd.Parameters.AddWithValue("@is_paid", is_paid); + insertCmd.Parameters.AddWithValue("@is_archived", is_archived); + insertCmd.Parameters.AddWithValue("@created_at", created_at); + insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + await insertCmd.ExecuteNonQueryAsync(); + } + } + catch (Exception ex) + { + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + if (ex.InnerException != null) + { + Console.WriteLine("\nInner Exception:"); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + } + } + } + + + public async Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, + string? filename, long? size, string api_type, string media_type, bool preview, bool downloaded, + DateTime? created_at) + { + try + { + using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + connection.Open(); + await EnsureCreatedAtColumnExists(connection, "medias"); + StringBuilder sql = new("SELECT COUNT(*) FROM medias WHERE media_id=@media_id"); if (configService.CurrentConfig.DownloadDuplicatedMedia) { sql.Append(" and api_type=@api_type"); } - // Create a new command object - using SqliteCommand command = new(sql.ToString(), connection); - // Add parameters to the command object - command.Parameters.AddWithValue("@directory", directory); - command.Parameters.AddWithValue("@filename", filename); - command.Parameters.AddWithValue("@size", size); - command.Parameters.AddWithValue("@downloaded", downloaded ? 1 : 0); - command.Parameters.AddWithValue("@created_at", created_at); - command.Parameters.AddWithValue("@media_id", media_id); - command.Parameters.AddWithValue("@api_type", api_type); - - // Execute the command - await command.ExecuteNonQueryAsync(); + using SqliteCommand cmd = new(sql.ToString(), connection); + cmd.Parameters.AddWithValue("@media_id", media_id); + cmd.Parameters.AddWithValue("@api_type", api_type); + int count = Convert.ToInt32(cmd.ExecuteScalar()); + if (count == 0) + { + // If the record doesn't exist, insert a new one + using SqliteCommand insertCmd = new( + $"INSERT INTO medias(media_id, post_id, link, directory, filename, size, api_type, media_type, preview, downloaded, created_at, record_created_at) VALUES({media_id}, {post_id}, '{link}', '{directory ?? "NULL"}', '{filename ?? "NULL"}', {size?.ToString() ?? "NULL"}, '{api_type}', '{media_type}', {Convert.ToInt32(preview)}, {Convert.ToInt32(downloaded)}, '{created_at?.ToString("yyyy-MM-dd HH:mm:ss")}', '{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}')", + connection); + await insertCmd.ExecuteNonQueryAsync(); + } } - - - public async Task GetStoredFileSize(string folder, long media_id, string api_type) + catch (Exception ex) { - long size; + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + if (ex.InnerException != null) + { + Console.WriteLine("\nInner Exception:"); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + } + } + } + + + public async Task CheckDownloaded(string folder, long media_id, string api_type) + { + try + { + bool downloaded = false; + using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) { + StringBuilder sql = new("SELECT downloaded FROM medias WHERE media_id=@media_id"); + if (configService.CurrentConfig.DownloadDuplicatedMedia) + { + sql.Append(" and api_type=@api_type"); + } + connection.Open(); - using SqliteCommand cmd = new($"SELECT size FROM medias WHERE media_id=@media_id and api_type=@api_type", connection); + using SqliteCommand cmd = new(sql.ToString(), connection); cmd.Parameters.AddWithValue("@media_id", media_id); cmd.Parameters.AddWithValue("@api_type", api_type); - size = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync()); + } + + return downloaded; + } + catch (Exception ex) + { + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace); + if (ex.InnerException != null) + { + Console.WriteLine("\nInner Exception:"); + Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); + Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, + ex.InnerException.StackTrace); } - return size; } - public async Task GetMostRecentPostDate(string folder) + return false; + } + + + public async Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, + long size, bool downloaded, DateTime created_at) + { + using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"); + connection.Open(); + + // Construct the update command + StringBuilder sql = + new( + "UPDATE medias SET directory=@directory, filename=@filename, size=@size, downloaded=@downloaded, created_at=@created_at WHERE media_id=@media_id"); + if (configService.CurrentConfig.DownloadDuplicatedMedia) { - DateTime? mostRecentDate = null; - using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) - { - connection.Open(); - using SqliteCommand cmd = new(@" + sql.Append(" and api_type=@api_type"); + } + + // Create a new command object + using SqliteCommand command = new(sql.ToString(), connection); + // Add parameters to the command object + command.Parameters.AddWithValue("@directory", directory); + command.Parameters.AddWithValue("@filename", filename); + command.Parameters.AddWithValue("@size", size); + command.Parameters.AddWithValue("@downloaded", downloaded ? 1 : 0); + command.Parameters.AddWithValue("@created_at", created_at); + command.Parameters.AddWithValue("@media_id", media_id); + command.Parameters.AddWithValue("@api_type", api_type); + + // Execute the command + await command.ExecuteNonQueryAsync(); + } + + + public async Task GetStoredFileSize(string folder, long media_id, string api_type) + { + long size; + using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) + { + connection.Open(); + using SqliteCommand cmd = new("SELECT size FROM medias WHERE media_id=@media_id and api_type=@api_type", + connection); + cmd.Parameters.AddWithValue("@media_id", media_id); + cmd.Parameters.AddWithValue("@api_type", api_type); + size = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + } + + return size; + } + + public async Task GetMostRecentPostDate(string folder) + { + DateTime? mostRecentDate = null; + using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db")) + { + connection.Open(); + using SqliteCommand cmd = new(@" SELECT MIN(created_at) AS created_at FROM ( @@ -483,35 +551,36 @@ namespace OF_DL.Services ON P.post_id = m.post_id WHERE m.downloaded = 0 )", connection); - var scalarValue = await cmd.ExecuteScalarAsync(); - if(scalarValue != null && scalarValue != DBNull.Value) - { - mostRecentDate = Convert.ToDateTime(scalarValue); - } + object? scalarValue = await cmd.ExecuteScalarAsync(); + if (scalarValue != null && scalarValue != DBNull.Value) + { + mostRecentDate = Convert.ToDateTime(scalarValue); } - return mostRecentDate; } - private async Task EnsureCreatedAtColumnExists(SqliteConnection connection, string tableName) + return mostRecentDate; + } + + private async Task EnsureCreatedAtColumnExists(SqliteConnection connection, string tableName) + { + using SqliteCommand cmd = new($"PRAGMA table_info({tableName});", connection); + using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); + bool columnExists = false; + + while (await reader.ReadAsync()) { - using SqliteCommand cmd = new($"PRAGMA table_info({tableName});", connection); - using var reader = await cmd.ExecuteReaderAsync(); - bool columnExists = false; - - while (await reader.ReadAsync()) + if (reader["name"].ToString() == "record_created_at") { - if (reader["name"].ToString() == "record_created_at") - { - columnExists = true; - break; - } + columnExists = true; + break; } + } - if (!columnExists) - { - using SqliteCommand alterCmd = new($"ALTER TABLE {tableName} ADD COLUMN record_created_at TIMESTAMP;", connection); - await alterCmd.ExecuteNonQueryAsync(); - } + if (!columnExists) + { + using SqliteCommand alterCmd = new($"ALTER TABLE {tableName} ADD COLUMN record_created_at TIMESTAMP;", + connection); + await alterCmd.ExecuteNonQueryAsync(); } } } diff --git a/OF DL/Services/DownloadService.cs b/OF DL/Services/DownloadService.cs index ab6cd7e..9fe84c4 100644 --- a/OF DL/Services/DownloadService.cs +++ b/OF DL/Services/DownloadService.cs @@ -34,12 +34,18 @@ public class DownloadService( { string path = "/Profile"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(avatarUrl)) { string avatarpath = $"{path}/Avatars"; - if (!Directory.Exists(folder + avatarpath)) Directory.CreateDirectory(folder + avatarpath); + if (!Directory.Exists(folder + avatarpath)) + { + Directory.CreateDirectory(folder + avatarpath); + } List avatarMD5Hashes = CalculateFolderMD5(folder + avatarpath); @@ -48,11 +54,7 @@ public class DownloadService( HttpClient client = new(); - HttpRequestMessage request = new() - { - Method = HttpMethod.Get, - RequestUri = uri - }; + HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = uri }; using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); @@ -84,7 +86,10 @@ public class DownloadService( if (!string.IsNullOrEmpty(headerUrl)) { string headerpath = $"{path}/Headers"; - if (!Directory.Exists(folder + headerpath)) Directory.CreateDirectory(folder + headerpath); + if (!Directory.Exists(folder + headerpath)) + { + Directory.CreateDirectory(folder + headerpath); + } List headerMD5Hashes = CalculateFolderMD5(folder + headerpath); @@ -93,11 +98,7 @@ public class DownloadService( HttpClient client = new(); - HttpRequestMessage request = new() - { - Method = HttpMethod.Get, - RequestUri = uri - }; + HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = uri }; using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); @@ -151,7 +152,10 @@ public class DownloadService( int pos1 = decryptionKey.IndexOf(':'); string decKey = ""; - if (pos1 >= 0) decKey = decryptionKey.Substring(pos1 + 1); + if (pos1 >= 0) + { + decKey = decryptionKey.Substring(pos1 + 1); + } int streamIndex = 0; string tempFilename = $"{folder}{path}/{filename}_source.mp4"; @@ -269,18 +273,28 @@ public class DownloadService( { try { - if (File.Exists(tempFilename)) File.SetLastWriteTime(tempFilename, lastModified); + if (File.Exists(tempFilename)) + { + File.SetLastWriteTime(tempFilename, lastModified); + } + if (!string.IsNullOrEmpty(customFileName)) + { File.Move(tempFilename, $"{folder + path + "/" + customFileName + ".mp4"}"); + } // Cleanup Files long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : tempFilename).Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + ".mp4" : filename + "_source.mp4", @@ -337,7 +351,9 @@ public class DownloadService( .FirstOrDefault(e => (string)e.Attribute("mimeType") == "video/mp4"); if (videoAdaptationSet == null) + { return null; + } string targetHeight = resolution switch { @@ -350,13 +366,45 @@ public class DownloadService( 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; } + private static List CalculateFolderMD5(string folder) + { + List md5Hashes = new(); + if (Directory.Exists(folder)) + { + string[] files = Directory.GetFiles(folder); + + foreach (string file in files) + { + md5Hashes.Add(CalculateMD5(file)); + } + } + + return md5Hashes; + } + + private static string CalculateMD5(string filePath) + { + using (MD5 md5 = MD5.Create()) + { + using (FileStream stream = File.OpenRead(filePath)) + { + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } + #region common /// @@ -384,7 +432,11 @@ public class DownloadService( try { string customFileName = string.Empty; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } + string extension = Path.GetExtension(url.Split("?")[0]); path = UpdatePathBasedOnExtension(folder, path, extension); @@ -445,7 +497,10 @@ public class DownloadService( path += subdirectory; string fullPath = folder + path; - if (!Directory.Exists(fullPath)) Directory.CreateDirectory(fullPath); + if (!Directory.Exists(fullPath)) + { + Directory.CreateDirectory(fullPath); + } } return path; @@ -473,12 +528,14 @@ public class DownloadService( CustomFileNameOption option) { if (string.IsNullOrEmpty(filenameFormat) || postInfo == null || postMedia == null || author == null) + { return option switch { CustomFileNameOption.ReturnOriginal => filename, CustomFileNameOption.ReturnEmpty => string.Empty, _ => filename }; + } List properties = new(); string pattern = @"\{(.*?)\}"; @@ -516,7 +573,10 @@ public class DownloadService( using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) fileSize = response.Content.Headers.ContentLength ?? 0; + if (response.IsSuccessStatusCode) + { + fileSize = response.Content.Headers.ContentLength ?? 0; + } } else { @@ -524,7 +584,10 @@ public class DownloadService( client.DefaultRequestHeaders.Add("User-Agent", authService.CurrentAuth.USER_AGENT); using HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) fileSize = response.Content.Headers.ContentLength ?? 0; + if (response.IsSuccessStatusCode) + { + fileSize = response.Content.Headers.ContentLength ?? 0; + } } } catch (Exception ex) @@ -553,7 +616,11 @@ public class DownloadService( client.DefaultRequestHeaders.Add("User-Agent", auth.USER_AGENT); using HttpResponseMessage response = await client.GetAsync(mpdURL, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) return response.Content.Headers.LastModified.Value.DateTime; + if (response.IsSuccessStatusCode) + { + return response.Content.Headers.LastModified.Value.DateTime; + } + return DateTime.Now; } @@ -562,7 +629,11 @@ public class DownloadService( using HttpClient client = new(); using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); - if (response.IsSuccessStatusCode) return response.Content.Headers.LastModified.Value.DateTime; + if (response.IsSuccessStatusCode) + { + return response.Content.Headers.LastModified.Value.DateTime; + } + return DateTime.Now; } @@ -591,6 +662,7 @@ public class DownloadService( try { if (!await dbService.CheckDownloaded(folder, media_id, api_type)) + { return await HandleNewMedia(folder, media_id, api_type, @@ -600,12 +672,16 @@ public class DownloadService( resolvedFilename, extension, progressReporter); + } bool status = await HandlePreviouslyDownloadedMediaAsync(folder, media_id, api_type, progressReporter); if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && serverFilename != resolvedFilename) + { await HandleRenamingOfExistingFilesAsync(folder, media_id, api_type, path, serverFilename, resolvedFilename, extension); + } + return status; } catch (Exception ex) @@ -627,7 +703,10 @@ public class DownloadService( { string fullPathWithTheServerFileName = $"{folder}{path}/{serverFilename}{extension}"; string fullPathWithTheNewFileName = $"{folder}{path}/{resolvedFilename}{extension}"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } try { @@ -711,9 +790,14 @@ public class DownloadService( fileSizeInBytes = GetLocalFileSize(finalPath); lastModified = File.GetLastWriteTime(finalPath); if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + status = false; } // Handle the case where the file has been downloaded in the past with a custom filename. @@ -724,9 +808,14 @@ public class DownloadService( fileSizeInBytes = GetLocalFileSize(fullPathWithTheNewFileName); lastModified = File.GetLastWriteTime(fullPathWithTheNewFileName); if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + status = false; } else //file doesn't exist and we should download it. @@ -775,10 +864,7 @@ public class DownloadService( /// /// The path to the file. /// The file size in bytes. - private long GetLocalFileSize(string filePath) - { - return new FileInfo(filePath).Length; - } + private long GetLocalFileSize(string filePath) => new FileInfo(filePath).Length; /// @@ -791,11 +877,7 @@ public class DownloadService( private async Task DownloadFile(string url, string destinationPath, IProgressReporter progressReporter) { using HttpClient client = new(); - HttpRequestMessage request = new() - { - Method = HttpMethod.Get, - RequestUri = new Uri(url) - }; + HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = new Uri(url) }; using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); @@ -812,13 +894,21 @@ public class DownloadService( int read; while ((read = await throttledStream.ReadAsync(buffer, CancellationToken.None)) > 0) { - if (configService.CurrentConfig.ShowScrapeSize) progressReporter.ReportProgress(read); + if (configService.CurrentConfig.ShowScrapeSize) + { + progressReporter.ReportProgress(read); + } + await fileStream.WriteAsync(buffer.AsMemory(0, read), CancellationToken.None); } } File.SetLastWriteTime(destinationPath, response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now); - if (!configService.CurrentConfig.ShowScrapeSize) progressReporter.ReportProgress(1); + if (!configService.CurrentConfig.ShowScrapeSize) + { + progressReporter.ReportProgress(1); + } + return response.Content.Headers.LastModified?.LocalDateTime ?? DateTime.Now; } @@ -844,16 +934,25 @@ public class DownloadService( } long[] fileSizes = await Task.WhenAll(tasks); - foreach (long fileSize in fileSizes) totalFileSize += fileSize; + foreach (long fileSize in fileSizes) + { + totalFileSize += fileSize; + } } else { List> tasks = new(); - foreach (string url in urls) tasks.Add(GetFileSizeAsync(url)); + foreach (string url in urls) + { + tasks.Add(GetFileSizeAsync(url)); + } long[] fileSizes = await Task.WhenAll(tasks); - foreach (long fileSize in fileSizes) totalFileSize += fileSize; + foreach (long fileSize in fileSizes) + { + totalFileSize += fileSize; + } } return totalFileSize; @@ -870,9 +969,13 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) + { path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Posts/Free"; + } Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); @@ -890,9 +993,13 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) + { path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Posts/Free"; + } Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); @@ -910,9 +1017,13 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.id is not null && streamInfo?.postedAt is not null) + { path = $"/Posts/Free/{streamInfo.id} {streamInfo.postedAt:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Posts/Free"; + } Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); @@ -931,9 +1042,14 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Messages/Free"; + } + Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, @@ -949,9 +1065,14 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Messages/Free"; + } + Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, @@ -992,9 +1113,14 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Messages/Paid"; + } + Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, @@ -1010,9 +1136,14 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Messages/Paid"; + } + Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, @@ -1035,9 +1166,14 @@ public class DownloadService( string path; if (configService.CurrentConfig.FolderPerPaidPost && messageInfo != null && messageInfo?.id is not null && messageInfo?.postedAt is not null) + { path = $"/Posts/Paid/{messageInfo.id} {messageInfo.postedAt.Value:yyyy-MM-dd HH-mm-ss}"; + } else + { path = "/Posts/Paid"; + } + Uri uri = new(url); string filename = Path.GetFileNameWithoutExtension(uri.LocalPath); string resolvedFilename = await GenerateCustomFileName(filename, filenameFormat, messageInfo, messageMedia, @@ -1064,10 +1200,18 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Messages/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) @@ -1075,7 +1219,11 @@ public class DownloadService( List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1086,17 +1234,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1104,12 +1259,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1124,6 +1284,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1168,10 +1329,18 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Free/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Messages/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) @@ -1179,7 +1348,11 @@ public class DownloadService( List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1190,17 +1363,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1208,12 +1388,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1228,6 +1413,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1273,17 +1459,29 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Messages/Paid/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1294,17 +1492,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1312,12 +1517,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1332,6 +1542,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1376,17 +1587,29 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null && messageInfo?.id is not null && messageInfo?.createdAt is not null) + { path = $"/Messages/Paid/{messageInfo.id} {messageInfo.createdAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Messages/Paid/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && messageInfo != null && messageMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(messageInfo, messageMedia, fromUser, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1397,17 +1620,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1415,12 +1645,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1435,6 +1670,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1480,17 +1716,29 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) + { path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Posts/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1501,17 +1749,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1519,12 +1774,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1539,6 +1799,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1583,17 +1844,29 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) + { path = $"/Posts/Free/{postInfo.id} {postInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Posts/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1604,17 +1877,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1622,12 +1902,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1642,6 +1927,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1686,17 +1972,29 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPost && streamInfo != null && streamInfo?.id is not null && streamInfo?.postedAt is not null) + { path = $"/Posts/Free/{streamInfo.id} {streamInfo.postedAt:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Posts/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && streamInfo != null && streamMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(streamInfo, streamMedia, author, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1707,17 +2005,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1725,12 +2030,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1745,6 +2055,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1789,10 +2100,18 @@ public class DownloadService( string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; if (configService.CurrentConfig.FolderPerPaidPost && postInfo != null && postInfo?.id is not null && postInfo?.postedAt is not null) + { path = $"/Posts/Paid/{postInfo.id} {postInfo.postedAt.Value:yyyy-MM-dd HH-mm-ss}/Videos"; + } else + { path = "/Posts/Paid/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + } + + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) @@ -1800,7 +2119,11 @@ public class DownloadService( List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, fromUser, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1811,17 +2134,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1829,12 +2159,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1849,6 +2184,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -1892,14 +2228,21 @@ public class DownloadService( Uri uri = new(url); string filename = Path.GetFileName(uri.LocalPath).Split(".")[0]; string path = "/Archived/Posts/Free/Videos"; - if (!Directory.Exists(folder + path)) Directory.CreateDirectory(folder + path); + if (!Directory.Exists(folder + path)) + { + Directory.CreateDirectory(folder + path); + } if (!string.IsNullOrEmpty(filenameFormat) && postInfo != null && postMedia != null) { List properties = new(); string pattern = @"\{(.*?)\}"; MatchCollection matches = Regex.Matches(filenameFormat, pattern); - foreach (Match match in matches) properties.Add(match.Groups[1].Value); + foreach (Match match in matches) + { + properties.Add(match.Groups[1].Value); + } + Dictionary values = await fileNameService.GetFilename(postInfo, postMedia, author, properties, folder.Split("/")[^1], users); customFileName = await fileNameService.BuildFilename(filenameFormat, values); @@ -1910,17 +2253,24 @@ public class DownloadService( if (!string.IsNullOrEmpty(customFileName) ? !File.Exists(folder + path + "/" + customFileName + ".mp4") : !File.Exists(folder + path + "/" + filename + "_source.mp4")) + { return await DownloadDrmMedia(authService.CurrentAuth.USER_AGENT, policy, signature, kvp, authService.CurrentAuth.COOKIE, url, decryptionKey, folder, lastModified, media_id, api_type, progressReporter, customFileName, filename, path); + } long fileSizeInBytes = new FileInfo(!string.IsNullOrEmpty(customFileName) ? folder + path + "/" + customFileName + ".mp4" : folder + path + "/" + filename + "_source.mp4").Length; if (configService.CurrentConfig.ShowScrapeSize) + { progressReporter.ReportProgress(fileSizeInBytes); + } else + { progressReporter.ReportProgress(1); + } + await dbService.UpdateMedia(folder, media_id, api_type, folder + path, !string.IsNullOrEmpty(customFileName) ? customFileName + "mp4" : filename + "_source.mp4", fileSizeInBytes, true, lastModified); @@ -1928,12 +2278,17 @@ public class DownloadService( else { if (!string.IsNullOrEmpty(customFileName)) + { if (configService.CurrentConfig.RenameExistingFilesWhenCustomFormatIsSelected && filename + "_source" != customFileName) { string fullPathWithTheServerFileName = $"{folder}{path}/{filename}_source.mp4"; string fullPathWithTheNewFileName = $"{folder}{path}/{customFileName}.mp4"; - if (!File.Exists(fullPathWithTheServerFileName)) return false; + if (!File.Exists(fullPathWithTheServerFileName)) + { + return false; + } + try { File.Move(fullPathWithTheServerFileName, fullPathWithTheNewFileName); @@ -1948,6 +2303,7 @@ public class DownloadService( await dbService.UpdateMedia(folder, media_id, api_type, folder + path, customFileName + ".mp4", size, true, lastModified); } + } if (configService.CurrentConfig.ShowScrapeSize) { @@ -2014,9 +2370,13 @@ public class DownloadService( bool isNew = await DownloadStoryMedia(highlightKVP.Value, path, highlightKVP.Key, "Stories", progressReporter); if (isNew) + { newHighlightsCount++; + } else + { oldHighlightsCount++; + } } Log.Debug( @@ -2062,9 +2422,13 @@ public class DownloadService( { bool isNew = await DownloadStoryMedia(storyKVP.Value, path, storyKVP.Key, "Stories", progressReporter); if (isNew) + { newStoriesCount++; + } else + { oldStoriesCount++; + } } Log.Debug($"Stories Already Downloaded: {oldStoriesCount} New Stories Downloaded: {newStoriesCount}"); @@ -2126,13 +2490,18 @@ public class DownloadService( "?type=widevine"); string decryptionKey; if (clientIdBlobMissing || devicePrivateKeyMissing) + { decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); + } else + { decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/post/{postId}?type=widevine", pssh); + } + Archived.Medium? mediaInfo = archived.ArchivedPostMedia.FirstOrDefault(m => m.id == archivedKVP.Key); Archived.List? postInfo = @@ -2182,9 +2551,13 @@ public class DownloadService( } if (isNew) + { newArchivedCount++; + } else + { oldArchivedCount++; + } } Log.Debug( @@ -2246,13 +2619,18 @@ public class DownloadService( "?type=widevine"); string decryptionKey; if (clientIdBlobMissing || devicePrivateKeyMissing) + { decryptionKey = await apiService.GetDecryptionKeyOFDL(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); + } else + { decryptionKey = await apiService.GetDecryptionKeyCDM(drmHeaders, $"https://onlyfans.com/api2/v2/users/media/{mediaId}/drm/message/{messageId}?type=widevine", pssh); + } + Medium? mediaInfo = messages.MessageMedia.FirstOrDefault(m => m.id == messageKVP.Key); List? messageInfo = messages.MessageObjects.FirstOrDefault(p => p?.media?.Contains(mediaInfo) == true); @@ -2300,9 +2678,13 @@ public class DownloadService( } if (isNew) + { newMessagesCount++; + } else + { oldMessagesCount++; + } } Log.Debug($"Messages Already Downloaded: {oldMessagesCount} New Messages Downloaded: {newMessagesCount}"); @@ -2329,7 +2711,11 @@ public class DownloadService( Log.Debug("Found 0 Paid Messages"); return new DownloadResult { - TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Paid Messages", Success = true + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Paid Messages", + Success = true }; } @@ -2386,15 +2772,24 @@ public class DownloadService( string.Empty, messageInfo, mediaInfo, messageInfo?.fromUser, users); } - if (isNew) newCount++; - else oldCount++; + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } } Log.Debug($"Paid Messages Already Downloaded: {oldCount} New Paid Messages Downloaded: {newCount}"); return new DownloadResult { - TotalCount = paidMessageCollection.PaidMessages.Count, NewDownloads = newCount, - ExistingDownloads = oldCount, MediaType = "Paid Messages", Success = true + TotalCount = paidMessageCollection.PaidMessages.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Paid Messages", + Success = true }; } @@ -2408,7 +2803,13 @@ public class DownloadService( { Log.Debug("Found 0 Streams"); return new DownloadResult - { TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Streams", Success = true }; + { + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Streams", + Success = true + }; } Log.Debug($"Found {streams.Streams.Count} Media from {streams.StreamObjects.Count} Streams"); @@ -2462,15 +2863,24 @@ public class DownloadService( string.Empty, streamInfo, mediaInfo, streamInfo?.author, users); } - if (isNew) newCount++; - else oldCount++; + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } } Log.Debug($"Streams Already Downloaded: {oldCount} New Streams Downloaded: {newCount}"); return new DownloadResult { - TotalCount = streams.Streams.Count, NewDownloads = newCount, ExistingDownloads = oldCount, - MediaType = "Streams", Success = true + TotalCount = streams.Streams.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Streams", + Success = true }; } @@ -2486,7 +2896,13 @@ public class DownloadService( { Log.Debug("Found 0 Posts"); return new DownloadResult - { TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Posts", Success = true }; + { + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Posts", + Success = true + }; } Log.Debug($"Found {posts.Posts.Count} Media from {posts.PostObjects.Count} Posts"); @@ -2537,14 +2953,23 @@ public class DownloadService( string.Empty, postInfo, mediaInfo, postInfo?.author, users); } - if (isNew) newCount++; - else oldCount++; + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } } Log.Debug($"Posts Already Downloaded: {oldCount} New Posts Downloaded: {newCount}"); return new DownloadResult { - TotalCount = posts.Posts.Count, NewDownloads = newCount, ExistingDownloads = oldCount, MediaType = "Posts", + TotalCount = posts.Posts.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Posts", Success = true }; } @@ -2559,7 +2984,13 @@ public class DownloadService( { Log.Debug("Found 0 Paid Posts"); return new DownloadResult - { TotalCount = 0, NewDownloads = 0, ExistingDownloads = 0, MediaType = "Paid Posts", Success = true }; + { + TotalCount = 0, + NewDownloads = 0, + ExistingDownloads = 0, + MediaType = "Paid Posts", + Success = true + }; } Log.Debug( @@ -2613,46 +3044,26 @@ public class DownloadService( string.Empty, postInfo, mediaInfo, postInfo?.fromUser, users); } - if (isNew) newCount++; - else oldCount++; + if (isNew) + { + newCount++; + } + else + { + oldCount++; + } } Log.Debug($"Paid Posts Already Downloaded: {oldCount} New Paid Posts Downloaded: {newCount}"); return new DownloadResult { - TotalCount = purchasedPosts.PaidPosts.Count, NewDownloads = newCount, ExistingDownloads = oldCount, - MediaType = "Paid Posts", Success = true + TotalCount = purchasedPosts.PaidPosts.Count, + NewDownloads = newCount, + ExistingDownloads = oldCount, + MediaType = "Paid Posts", + Success = true }; } #endregion - - private static List CalculateFolderMD5(string folder) - { - List md5Hashes = new List(); - if (Directory.Exists(folder)) - { - string[] files = Directory.GetFiles(folder); - - foreach (string file in files) - { - md5Hashes.Add(CalculateMD5(file)); - } - } - - return md5Hashes; - } - - private static string CalculateMD5(string filePath) - { - using (var md5 = MD5.Create()) - { - using (var stream = File.OpenRead(filePath)) - { - byte[] hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - } - } diff --git a/OF DL/Services/FileNameService.cs b/OF DL/Services/FileNameService.cs index f97a1d5..a692874 100644 --- a/OF DL/Services/FileNameService.cs +++ b/OF DL/Services/FileNameService.cs @@ -1,181 +1,190 @@ -using HtmlAgilityPack; using System.Reflection; +using HtmlAgilityPack; -namespace OF_DL.Services +namespace OF_DL.Services; + +public class FileNameService(IAuthService authService) : IFileNameService { - public class FileNameService(IAuthService authService) : IFileNameService + public async Task> GetFilename(object obj1, object obj2, object obj3, + List selectedProperties, string username, Dictionary users = null) { - public async Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, string username, Dictionary users = null) + Dictionary values = new(); + Type type1 = obj1.GetType(); + Type type2 = obj2.GetType(); + PropertyInfo[] properties1 = type1.GetProperties(); + PropertyInfo[] properties2 = type2.GetProperties(); + + foreach (string propertyName in selectedProperties) { - Dictionary values = new(); - Type type1 = obj1.GetType(); - Type type2 = obj2.GetType(); - PropertyInfo[] properties1 = type1.GetProperties(); - PropertyInfo[] properties2 = type2.GetProperties(); - - foreach (string propertyName in selectedProperties) + if (propertyName.Contains("media")) { - if (propertyName.Contains("media")) + object drmProperty = null; + object fileProperty = GetNestedPropertyValue(obj2, "files"); + if (fileProperty != null) { - object drmProperty = null; - object fileProperty = GetNestedPropertyValue(obj2, "files"); - if(fileProperty != null) - { - drmProperty = GetNestedPropertyValue(obj2, "files.drm"); - } + drmProperty = GetNestedPropertyValue(obj2, "files.drm"); + } - if(fileProperty != null && drmProperty != null && propertyName == "mediaCreatedAt") + if (fileProperty != null && drmProperty != null && propertyName == "mediaCreatedAt") + { + object mpdurl = GetNestedPropertyValue(obj2, "files.drm.manifest.dash"); + object policy = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontPolicy"); + object signature = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontSignature"); + object kvp = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontKeyPairId"); + DateTime lastModified = + await DownloadService.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), + authService.CurrentAuth); + values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); + continue; + } + + if ((fileProperty == null || drmProperty == null) && propertyName == "mediaCreatedAt") + { + object source = GetNestedPropertyValue(obj2, "files.full.url"); + if (source != null) { - object mpdurl = GetNestedPropertyValue(obj2, "files.drm.manifest.dash"); - object policy = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontPolicy"); - object signature = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontSignature"); - object kvp = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontKeyPairId"); - DateTime lastModified = await DownloadService.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), authService.CurrentAuth); + DateTime lastModified = await DownloadService.GetMediaLastModified(source.ToString()); values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); continue; } - else if((fileProperty == null || drmProperty == null) && propertyName == "mediaCreatedAt") + + object preview = GetNestedPropertyValue(obj2, "preview"); + if (preview != null) { - object source = GetNestedPropertyValue(obj2, "files.full.url"); - if(source != null) + DateTime lastModified = await DownloadService.GetMediaLastModified(preview.ToString()); + values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); + continue; + } + } + + PropertyInfo? property = Array.Find(properties2, + p => p.Name.Equals(propertyName.Replace("media", ""), StringComparison.OrdinalIgnoreCase)); + if (property != null) + { + object? propertyValue = property.GetValue(obj2); + if (propertyValue != null) + { + if (propertyValue is DateTime dateTimeValue) { - DateTime lastModified = await DownloadService.GetMediaLastModified(source.ToString()); - values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); - continue; + values.Add(propertyName, dateTimeValue.ToString("yyyy-MM-dd")); } else { - object preview = GetNestedPropertyValue(obj2, "preview"); - if(preview != null) - { - DateTime lastModified = await DownloadService.GetMediaLastModified(preview.ToString()); - values.Add(propertyName, lastModified.ToString("yyyy-MM-dd")); - continue; - } - } - - } - PropertyInfo? property = Array.Find(properties2, p => p.Name.Equals(propertyName.Replace("media", ""), StringComparison.OrdinalIgnoreCase)); - if (property != null) - { - object? propertyValue = property.GetValue(obj2); - if (propertyValue != null) - { - if (propertyValue is DateTime dateTimeValue) - { - values.Add(propertyName, dateTimeValue.ToString("yyyy-MM-dd")); - } - else - { - values.Add(propertyName, propertyValue.ToString()); - } + values.Add(propertyName, propertyValue.ToString()); } } } - else if (propertyName.Contains("filename")) + } + else if (propertyName.Contains("filename")) + { + string sourcePropertyPath = "files.full.url"; + object sourcePropertyValue = GetNestedPropertyValue(obj2, sourcePropertyPath); + if (sourcePropertyValue != null) { - string sourcePropertyPath = "files.full.url"; - object sourcePropertyValue = GetNestedPropertyValue(obj2, sourcePropertyPath); - if (sourcePropertyValue != null) - { - Uri uri = new(sourcePropertyValue.ToString()); - string filename = System.IO.Path.GetFileName(uri.LocalPath); - values.Add(propertyName, filename.Split(".")[0]); - } - else - { - string propertyPath = "files.drm.manifest.dash"; - object nestedPropertyValue = GetNestedPropertyValue(obj2, propertyPath); - if (nestedPropertyValue != null) - { - Uri uri = new(nestedPropertyValue.ToString()); - string filename = System.IO.Path.GetFileName(uri.LocalPath); - values.Add(propertyName, filename.Split(".")[0] + "_source"); - } - } - } - else if (propertyName.Contains("username")) - { - if(!string.IsNullOrEmpty(username)) - { - values.Add(propertyName, username); - } - else - { - string propertyPath = "id"; - object nestedPropertyValue = GetNestedPropertyValue(obj3, propertyPath); - if (nestedPropertyValue != null) - { - values.Add(propertyName, users.FirstOrDefault(u => u.Value == Convert.ToInt32(nestedPropertyValue.ToString())).Key); - } - } - } - else if (propertyName.Contains("text", StringComparison.OrdinalIgnoreCase)) - { - PropertyInfo property = Array.Find(properties1, p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); - if (property != null) - { - object propertyValue = property.GetValue(obj1); - if (propertyValue != null) - { - var pageDoc = new HtmlDocument(); - pageDoc.LoadHtml(propertyValue.ToString()); - var str = pageDoc.DocumentNode.InnerText; - if (str.Length > 100) // todo: add length limit to config - str = str.Substring(0, 100); - values.Add(propertyName, str); - } - } + Uri uri = new(sourcePropertyValue.ToString()); + string filename = Path.GetFileName(uri.LocalPath); + values.Add(propertyName, filename.Split(".")[0]); } else { - PropertyInfo property = Array.Find(properties1, p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); - if (property != null) + string propertyPath = "files.drm.manifest.dash"; + object nestedPropertyValue = GetNestedPropertyValue(obj2, propertyPath); + if (nestedPropertyValue != null) { - object propertyValue = property.GetValue(obj1); - if (propertyValue != null) + Uri uri = new(nestedPropertyValue.ToString()); + string filename = Path.GetFileName(uri.LocalPath); + values.Add(propertyName, filename.Split(".")[0] + "_source"); + } + } + } + else if (propertyName.Contains("username")) + { + if (!string.IsNullOrEmpty(username)) + { + values.Add(propertyName, username); + } + else + { + string propertyPath = "id"; + object nestedPropertyValue = GetNestedPropertyValue(obj3, propertyPath); + if (nestedPropertyValue != null) + { + values.Add(propertyName, + users.FirstOrDefault(u => u.Value == Convert.ToInt32(nestedPropertyValue.ToString())).Key); + } + } + } + else if (propertyName.Contains("text", StringComparison.OrdinalIgnoreCase)) + { + PropertyInfo property = Array.Find(properties1, + p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); + if (property != null) + { + object propertyValue = property.GetValue(obj1); + if (propertyValue != null) + { + HtmlDocument pageDoc = new(); + pageDoc.LoadHtml(propertyValue.ToString()); + string str = pageDoc.DocumentNode.InnerText; + if (str.Length > 100) // todo: add length limit to config { - if (propertyValue is DateTime dateTimeValue) - { - values.Add(propertyName, dateTimeValue.ToString("yyyy-MM-dd")); - } - else - { - values.Add(propertyName, propertyValue.ToString()); - } + str = str.Substring(0, 100); + } + + values.Add(propertyName, str); + } + } + } + else + { + PropertyInfo property = Array.Find(properties1, + p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); + if (property != null) + { + object propertyValue = property.GetValue(obj1); + if (propertyValue != null) + { + if (propertyValue is DateTime dateTimeValue) + { + values.Add(propertyName, dateTimeValue.ToString("yyyy-MM-dd")); + } + else + { + values.Add(propertyName, propertyValue.ToString()); } } } } - return values; - } - - static object GetNestedPropertyValue(object source, string propertyPath) - { - object value = source; - foreach (var propertyName in propertyPath.Split('.')) - { - PropertyInfo property = value.GetType().GetProperty(propertyName) ?? throw new ArgumentException($"Property '{propertyName}' not found."); - value = property.GetValue(value); - } - return value; - } - - public async Task BuildFilename(string fileFormat, Dictionary values) - { - foreach (var kvp in values) - { - string placeholder = "{" + kvp.Key + "}"; - fileFormat = fileFormat.Replace(placeholder, kvp.Value); - } - - return RemoveInvalidFileNameChars($"{fileFormat}"); - } - - private static string RemoveInvalidFileNameChars(string fileName) - { - return string.IsNullOrEmpty(fileName) ? fileName : string.Concat(fileName.Split(Path.GetInvalidFileNameChars())); } + return values; } + + public async Task BuildFilename(string fileFormat, Dictionary values) + { + foreach (KeyValuePair kvp in values) + { + string placeholder = "{" + kvp.Key + "}"; + fileFormat = fileFormat.Replace(placeholder, kvp.Value); + } + + return RemoveInvalidFileNameChars($"{fileFormat}"); + } + + private static object GetNestedPropertyValue(object source, string propertyPath) + { + object value = source; + foreach (string propertyName in propertyPath.Split('.')) + { + PropertyInfo property = value.GetType().GetProperty(propertyName) ?? + throw new ArgumentException($"Property '{propertyName}' not found."); + value = property.GetValue(value); + } + + return value; + } + + private static string RemoveInvalidFileNameChars(string fileName) => string.IsNullOrEmpty(fileName) + ? fileName + : string.Concat(fileName.Split(Path.GetInvalidFileNameChars())); } diff --git a/OF DL/Services/IAPIService.cs b/OF DL/Services/IAPIService.cs index a24c088..3163062 100644 --- a/OF DL/Services/IAPIService.cs +++ b/OF DL/Services/IAPIService.cs @@ -8,32 +8,36 @@ using OF_DL.Entities.Streams; using OF_DL.Enumerations; using Spectre.Console; -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IAPIService { - public interface IAPIService - { - Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, string pssh); - Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, string pssh); - Task GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp); - Task GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp); - Task> GetLists(string endpoint); - Task> GetListUsers(string endpoint); - Task> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, List paid_post_ids); - Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, StatusContext ctx); - Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx); - Task GetPost(string endpoint, string folder); - Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx); - Task GetArchived(string endpoint, string folder, StatusContext ctx); - Task GetMessages(string endpoint, string folder, StatusContext ctx); - Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx); - Task GetPaidMessage(string endpoint, string folder); - Task> GetPurchasedTabUsers(string endpoint, Dictionary users); - Task> GetPurchasedTab(string endpoint, string folder, Dictionary users); - Task GetUserInfo(string endpoint); - Task GetUserInfoById(string endpoint); - Dictionary GetDynamicHeaders(string path, string queryParam); - Task> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions); - Task> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions); - Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, string pssh); - } + Task GetDecryptionKeyCDRMProject(Dictionary drmHeaders, string licenceURL, string pssh); + Task GetDecryptionKeyCDM(Dictionary drmHeaders, string licenceURL, string pssh); + Task GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp); + Task GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp); + Task> GetLists(string endpoint); + Task> GetListUsers(string endpoint); + + Task> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, + List paid_post_ids); + + Task GetPaidPosts(string endpoint, string folder, string username, List paid_post_ids, + StatusContext ctx); + + Task GetPosts(string endpoint, string folder, List paid_post_ids, StatusContext ctx); + Task GetPost(string endpoint, string folder); + Task GetStreams(string endpoint, string folder, List paid_post_ids, StatusContext ctx); + Task GetArchived(string endpoint, string folder, StatusContext ctx); + Task GetMessages(string endpoint, string folder, StatusContext ctx); + Task GetPaidMessages(string endpoint, string folder, string username, StatusContext ctx); + Task GetPaidMessage(string endpoint, string folder); + Task> GetPurchasedTabUsers(string endpoint, Dictionary users); + Task> GetPurchasedTab(string endpoint, string folder, Dictionary users); + Task GetUserInfo(string endpoint); + Task GetUserInfoById(string endpoint); + Dictionary GetDynamicHeaders(string path, string queryParam); + Task> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions); + Task> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions); + Task GetDecryptionKeyOFDL(Dictionary drmHeaders, string licenceURL, string pssh); } diff --git a/OF DL/Services/IAuthService.cs b/OF DL/Services/IAuthService.cs index 8d14486..e79ad43 100644 --- a/OF DL/Services/IAuthService.cs +++ b/OF DL/Services/IAuthService.cs @@ -1,12 +1,11 @@ using OF_DL.Entities; -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IAuthService { - public interface IAuthService - { - Auth? CurrentAuth { get; set; } - Task LoadFromFileAsync(string filePath = "auth.json"); - Task LoadFromBrowserAsync(); - Task SaveToFileAsync(string filePath = "auth.json"); - } + Auth? CurrentAuth { get; set; } + Task LoadFromFileAsync(string filePath = "auth.json"); + Task LoadFromBrowserAsync(); + Task SaveToFileAsync(string filePath = "auth.json"); } diff --git a/OF DL/Services/IConfigService.cs b/OF DL/Services/IConfigService.cs index 57c639a..fd7c35d 100644 --- a/OF DL/Services/IConfigService.cs +++ b/OF DL/Services/IConfigService.cs @@ -1,13 +1,12 @@ using OF_DL.Entities; -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IConfigService { - public interface IConfigService - { - Config CurrentConfig { get; } - bool IsCliNonInteractive { get; } - Task LoadConfigurationAsync(string[] args); - Task SaveConfigurationAsync(string filePath = "config.conf"); - void UpdateConfig(Config newConfig); - } + Config CurrentConfig { get; } + bool IsCliNonInteractive { get; } + Task LoadConfigurationAsync(string[] args); + Task SaveConfigurationAsync(string filePath = "config.conf"); + void UpdateConfig(Config newConfig); } diff --git a/OF DL/Services/IDBService.cs b/OF DL/Services/IDBService.cs index 5a32c03..9a14484 100644 --- a/OF DL/Services/IDBService.cs +++ b/OF DL/Services/IDBService.cs @@ -1,17 +1,27 @@ -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IDBService { - public interface IDBService - { - Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at, long user_id); - Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at); - Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at); - Task CreateDB(string folder); - Task CreateUsersDB(Dictionary users); - Task CheckUsername(KeyValuePair user, string path); - Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, string? filename, long? size, string api_type, string media_type, bool preview, bool downloaded, DateTime? created_at); - Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, long size, bool downloaded, DateTime created_at); - Task GetStoredFileSize(string folder, long media_id, string api_type); - Task CheckDownloaded(string folder, long media_id, string api_type); - Task GetMostRecentPostDate(string folder); - } + Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, + DateTime created_at, long user_id); + + Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, + DateTime created_at); + + Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, + DateTime created_at); + + Task CreateDB(string folder); + Task CreateUsersDB(Dictionary users); + Task CheckUsername(KeyValuePair user, string path); + + Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, string? filename, + long? size, string api_type, string media_type, bool preview, bool downloaded, DateTime? created_at); + + Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, long size, + bool downloaded, DateTime created_at); + + Task GetStoredFileSize(string folder, long media_id, string api_type); + Task CheckDownloaded(string folder, long media_id, string api_type); + Task GetMostRecentPostDate(string folder); } diff --git a/OF DL/Services/IDownloadService.cs b/OF DL/Services/IDownloadService.cs index 696eaee..11d7c96 100644 --- a/OF DL/Services/IDownloadService.cs +++ b/OF DL/Services/IDownloadService.cs @@ -7,44 +7,128 @@ using OF_DL.Entities.Streams; using static OF_DL.Entities.Messages.Messages; using FromUser = OF_DL.Entities.Messages.FromUser; -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IDownloadService { - public interface IDownloadService - { - Task CalculateTotalFileSize(List urls); - Task ProcessMediaDownload(string folder, long media_id, string api_type, string url, string path, string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); - Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Archived.List? messageInfo, Archived.Medium? messageMedia, Archived.Author? author, Dictionary users); - Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Archived.List? postInfo, Archived.Medium? postMedia, Archived.Author? author, Dictionary users); - Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, Dictionary users); - Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary users); - Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username); - Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Messages.List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users); - Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Messages.List? messageInfo, Messages.Medium? messageMedia, Messages.FromUser? fromUser, Dictionary users); - Task DownloadPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary users); - Task DownloadPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, SinglePost.Author? author, Dictionary users); - Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); - Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary users); - Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); - Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary users); - Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, Medium? postMedia, Purchased.FromUser? fromUser, Dictionary users); - Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, Purchased.FromUser? fromUser, Dictionary users); - Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter); - Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary users); - Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary users); - Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, - string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, - FromUser? fromUser, Dictionary users); - Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, - IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, - FromUser? fromUser, Dictionary users); - Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); - Task DownloadStories(string username, long userId, string path, HashSet paidPostIds, IProgressReporter progressReporter); - Task DownloadArchived(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedCollection archived, IProgressReporter progressReporter); - Task DownloadMessages(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageCollection messages, IProgressReporter progressReporter); - Task DownloadPaidMessages(string username, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter); - Task DownloadStreams(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamsCollection streams, IProgressReporter progressReporter); - Task DownloadFreePosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostCollection posts, IProgressReporter progressReporter); - Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidPostCollection purchasedPosts, IProgressReporter progressReporter); - } + Task CalculateTotalFileSize(List urls); + + Task ProcessMediaDownload(string folder, long media_id, string api_type, string url, string path, + string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter); + + Task DownloadArchivedMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Archived.List? messageInfo, + Archived.Medium? messageMedia, Archived.Author? author, Dictionary users); + + Task DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Archived.List? postInfo, Archived.Medium? postMedia, + Archived.Author? author, Dictionary users); + + Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, + string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, + string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, + Dictionary users); + + Task DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, + string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, + string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, + Dictionary users); + + Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username); + + Task DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, + string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, + string? filenameFormat, List? messageInfo, Medium? messageMedia, Messages.FromUser? fromUser, + Dictionary users); + + Task DownloadMessageMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, List? messageInfo, Medium? messageMedia, + Messages.FromUser? fromUser, Dictionary users); + + Task DownloadPostMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, + Post.Author? author, Dictionary users); + + Task DownloadPostMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, + SinglePost.Author? author, Dictionary users); + + Task DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + Purchased.FromUser? fromUser, Dictionary users); + + Task DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users); + + Task DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + Purchased.FromUser? fromUser, Dictionary users); + + Task DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users); + + Task DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? postInfo, Medium? postMedia, + Purchased.FromUser? fromUser, Dictionary users); + + Task DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Purchased.List? messageInfo, Medium? messageMedia, + Purchased.FromUser? fromUser, Dictionary users); + + Task DownloadStoryMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter); + + Task DownloadStreamMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, Streams.List? streamInfo, + Streams.Medium? streamMedia, Streams.Author? author, Dictionary users); + + Task DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, + string folder, DateTime lastModified, long media_id, string api_type, IProgressReporter progressReporter, + string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, + Dictionary users); + + Task DownloadSingleMessagePreviewDRMVideo(string policy, string signature, string kvp, string url, + string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users); + + Task DownloadMessagePreviewMedia(string url, string folder, long media_id, string api_type, + IProgressReporter progressReporter, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, + FromUser? fromUser, Dictionary users); + + Task DownloadHighlights(string username, long userId, string path, HashSet paidPostIds, + IProgressReporter progressReporter); + + Task DownloadStories(string username, long userId, string path, HashSet paidPostIds, + IProgressReporter progressReporter); + + Task DownloadArchived(string username, long userId, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, ArchivedCollection archived, + IProgressReporter progressReporter); + + Task DownloadMessages(string username, long userId, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, MessageCollection messages, + IProgressReporter progressReporter); + + Task DownloadPaidMessages(string username, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidMessageCollection paidMessageCollection, + IProgressReporter progressReporter); + + Task DownloadStreams(string username, long userId, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, StreamsCollection streams, + IProgressReporter progressReporter); + + Task DownloadFreePosts(string username, long userId, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostCollection posts, + IProgressReporter progressReporter); + + Task DownloadPaidPosts(string username, long userId, string path, Dictionary users, + bool clientIdBlobMissing, bool devicePrivateKeyMissing, PaidPostCollection purchasedPosts, + IProgressReporter progressReporter); } diff --git a/OF DL/Services/IFileNameService.cs b/OF DL/Services/IFileNameService.cs index d692f30..4e4a026 100644 --- a/OF DL/Services/IFileNameService.cs +++ b/OF DL/Services/IFileNameService.cs @@ -1,8 +1,9 @@ -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface IFileNameService { - public interface IFileNameService - { - Task BuildFilename(string fileFormat, Dictionary values); - Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, string username, Dictionary users = null); - } + Task BuildFilename(string fileFormat, Dictionary values); + + Task> GetFilename(object obj1, object obj2, object obj3, List selectedProperties, + string username, Dictionary users = null); } diff --git a/OF DL/Services/ILoggingService.cs b/OF DL/Services/ILoggingService.cs index b0516ef..7c6a8a3 100644 --- a/OF DL/Services/ILoggingService.cs +++ b/OF DL/Services/ILoggingService.cs @@ -1,12 +1,11 @@ using OF_DL.Enumerations; using Serilog.Core; -namespace OF_DL.Services +namespace OF_DL.Services; + +public interface ILoggingService { - public interface ILoggingService - { - LoggingLevelSwitch LevelSwitch { get; } - void UpdateLoggingLevel(LoggingLevel newLevel); - LoggingLevel GetCurrentLoggingLevel(); - } + LoggingLevelSwitch LevelSwitch { get; } + void UpdateLoggingLevel(LoggingLevel newLevel); + LoggingLevel GetCurrentLoggingLevel(); } diff --git a/OF DL/Services/IProgressReporter.cs b/OF DL/Services/IProgressReporter.cs index d312059..d36d1f3 100644 --- a/OF DL/Services/IProgressReporter.cs +++ b/OF DL/Services/IProgressReporter.cs @@ -1,19 +1,19 @@ namespace OF_DL.Services; /// -/// Interface for reporting download progress in a UI-agnostic way. -/// This allows the download service to report progress without being coupled to any specific UI framework. +/// Interface for reporting download progress in a UI-agnostic way. +/// This allows the download service to report progress without being coupled to any specific UI framework. /// public interface IProgressReporter { /// - /// Reports progress increment. The value represents either bytes downloaded or file count depending on configuration. + /// Reports progress increment. The value represents either bytes downloaded or file count depending on configuration. /// /// The amount to increment progress by void ReportProgress(long increment); /// - /// Reports a status message (optional for implementations). + /// Reports a status message (optional for implementations). /// /// The status message to report void ReportStatus(string message); diff --git a/OF DL/Services/LoggingService.cs b/OF DL/Services/LoggingService.cs index c65511a..b4c47fa 100644 --- a/OF DL/Services/LoggingService.cs +++ b/OF DL/Services/LoggingService.cs @@ -3,40 +3,36 @@ using Serilog; using Serilog.Core; using Serilog.Events; -namespace OF_DL.Services +namespace OF_DL.Services; + +public class LoggingService : ILoggingService { - public class LoggingService : ILoggingService + public LoggingService() { - public LoggingLevelSwitch LevelSwitch { get; } + LevelSwitch = new LoggingLevelSwitch(); + InitializeLogger(); + } - public LoggingService() - { - LevelSwitch = new LoggingLevelSwitch(); - InitializeLogger(); - } + public LoggingLevelSwitch LevelSwitch { get; } - private void InitializeLogger() - { - // Set the initial level to Error (until we've read from config) - LevelSwitch.MinimumLevel = LogEventLevel.Error; + public void UpdateLoggingLevel(LoggingLevel newLevel) + { + LevelSwitch.MinimumLevel = (LogEventLevel)newLevel; + Log.Debug("Logging level updated to: {LoggingLevel}", newLevel); + } - Log.Logger = new LoggerConfiguration() - .MinimumLevel.ControlledBy(LevelSwitch) - .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day) - .CreateLogger(); + public LoggingLevel GetCurrentLoggingLevel() => (LoggingLevel)LevelSwitch.MinimumLevel; - Log.Debug("Logging service initialized"); - } + private void InitializeLogger() + { + // Set the initial level to Error (until we've read from config) + LevelSwitch.MinimumLevel = LogEventLevel.Error; - public void UpdateLoggingLevel(LoggingLevel newLevel) - { - LevelSwitch.MinimumLevel = (LogEventLevel)newLevel; - Log.Debug("Logging level updated to: {LoggingLevel}", newLevel); - } + Log.Logger = new LoggerConfiguration() + .MinimumLevel.ControlledBy(LevelSwitch) + .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); - public LoggingLevel GetCurrentLoggingLevel() - { - return (LoggingLevel)LevelSwitch.MinimumLevel; - } + Log.Debug("Logging service initialized"); } } diff --git a/OF DL/Utils/HttpUtil.cs b/OF DL/Utils/HttpUtil.cs index a239547..eaa7ebb 100644 --- a/OF DL/Utils/HttpUtil.cs +++ b/OF DL/Utils/HttpUtil.cs @@ -1,146 +1,141 @@ -using OF_DL.Helpers; +using System.Net; using System.Text; +using OF_DL.Helpers; -namespace OF_DL.Utils +namespace OF_DL.Utils; + +internal class HttpUtil { - class HttpUtil + public static HttpClient Client { get; set; } = new(new HttpClientHandler { - public static HttpClient Client { get; set; } = new HttpClient(new HttpClientHandler + AllowAutoRedirect = true + //Proxy = null + }); + + public static async Task PostData(string URL, Dictionary headers, string postData) + { + string mediaType = postData.StartsWith("{") ? "application/json" : "application/x-www-form-urlencoded"; + HttpResponseMessage response = await PerformOperation(async () => { - AllowAutoRedirect = true, - //Proxy = null + StringContent content = new(postData, Encoding.UTF8, mediaType); + //ByteArrayContent content = new ByteArrayContent(postData); + + return await Post(URL, headers, content); }); - public static async Task PostData(string URL, Dictionary headers, string postData) + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + return bytes; + } + + public static async Task PostData(string URL, Dictionary headers, byte[] postData) + { + HttpResponseMessage response = await PerformOperation(async () => { - var mediaType = postData.StartsWith("{") ? "application/json" : "application/x-www-form-urlencoded"; - var response = await PerformOperation(async () => + ByteArrayContent content = new(postData); + + return await Post(URL, headers, content); + }); + + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + return bytes; + } + + public static async Task PostData(string URL, Dictionary headers, + Dictionary postData) + { + HttpResponseMessage response = await PerformOperation(async () => + { + FormUrlEncodedContent content = new(postData); + + return await Post(URL, headers, content); + }); + + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + return bytes; + } + + public static async Task GetWebSource(string URL, Dictionary headers = null) + { + HttpResponseMessage response = await PerformOperation(async () => { return await Get(URL, headers); }); + + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + return Encoding.UTF8.GetString(bytes); + } + + public static async Task GetBinary(string URL, Dictionary headers = null) + { + HttpResponseMessage response = await PerformOperation(async () => { return await Get(URL, headers); }); + + byte[] bytes = await response.Content.ReadAsByteArrayAsync(); + return bytes; + } + + public static string GetString(byte[] bytes) => Encoding.UTF8.GetString(bytes); + + private static async Task Get(string URL, Dictionary headers = null) + { + HttpRequestMessage request = new() { RequestUri = new Uri(URL), Method = HttpMethod.Get }; + + if (headers != null) + { + foreach (KeyValuePair header in headers) { - StringContent content = new StringContent(postData, Encoding.UTF8, mediaType); - //ByteArrayContent content = new ByteArrayContent(postData); - - return await Post(URL, headers, content); - }); - - byte[] bytes = await response.Content.ReadAsByteArrayAsync(); - return bytes; + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } } - public static async Task PostData(string URL, Dictionary headers, byte[] postData) + return await Send(request); + } + + private static async Task Post(string URL, Dictionary headers, + HttpContent content) + { + HttpRequestMessage request = new() { RequestUri = new Uri(URL), Method = HttpMethod.Post, Content = content }; + + if (headers != null) { - var response = await PerformOperation(async () => + foreach (KeyValuePair header in headers) { - ByteArrayContent content = new ByteArrayContent(postData); - - return await Post(URL, headers, content); - }); - - byte[] bytes = await response.Content.ReadAsByteArrayAsync(); - return bytes; + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } } - public static async Task PostData(string URL, Dictionary headers, Dictionary postData) + return await Send(request); + } + + private static async Task Send(HttpRequestMessage request) => await Client.SendAsync(request); + + private static async Task PerformOperation(Func> operation) + { + HttpResponseMessage response = await operation(); + + int retryCount = 0; + + while (retryCount < Constants.WIDEVINE_MAX_RETRIES && response.StatusCode == HttpStatusCode.TooManyRequests) { - var response = await PerformOperation(async () => + // + // We've hit a rate limit, so we should wait before retrying. + // + int retryAfterSeconds = + Constants.WIDEVINE_RETRY_DELAY * (retryCount + 1); // Default retry time. Increases with each retry. + if (response.Headers.RetryAfter != null && response.Headers.RetryAfter.Delta.HasValue) { - FormUrlEncodedContent content = new FormUrlEncodedContent(postData); - - return await Post(URL, headers, content); - }); - - byte[] bytes = await response.Content.ReadAsByteArrayAsync(); - return bytes; - } - - public static async Task GetWebSource(string URL, Dictionary headers = null) - { - var response = await PerformOperation(async () => - { - return await Get(URL, headers); - }); - - byte[] bytes = await response.Content.ReadAsByteArrayAsync(); - return Encoding.UTF8.GetString(bytes); - } - - public static async Task GetBinary(string URL, Dictionary headers = null) - { - var response = await PerformOperation(async () => - { - return await Get(URL, headers); - }); - - byte[] bytes = await response.Content.ReadAsByteArrayAsync(); - return bytes; - } - public static string GetString(byte[] bytes) - { - return Encoding.UTF8.GetString(bytes); - } - - private static async Task Get(string URL, Dictionary headers = null) - { - HttpRequestMessage request = new HttpRequestMessage() - { - RequestUri = new Uri(URL), - Method = HttpMethod.Get - }; - - if (headers != null) - foreach (KeyValuePair header in headers) - request.Headers.TryAddWithoutValidation(header.Key, header.Value); - - return await Send(request); - } - - private static async Task Post(string URL, Dictionary headers, HttpContent content) - { - HttpRequestMessage request = new HttpRequestMessage() - { - RequestUri = new Uri(URL), - Method = HttpMethod.Post, - Content = content - }; - - if (headers != null) - foreach (KeyValuePair header in headers) - request.Headers.TryAddWithoutValidation(header.Key, header.Value); - - return await Send(request); - } - - private static async Task Send(HttpRequestMessage request) - { - return await Client.SendAsync(request); - } - - private static async Task PerformOperation(Func> operation) - { - var response = await operation(); - - var retryCount = 0; - - while (retryCount < Constants.WIDEVINE_MAX_RETRIES && response.StatusCode == System.Net.HttpStatusCode.TooManyRequests) - { - // - // We've hit a rate limit, so we should wait before retrying. - // - var retryAfterSeconds = Constants.WIDEVINE_RETRY_DELAY * (retryCount + 1); // Default retry time. Increases with each retry. - if (response.Headers.RetryAfter != null && response.Headers.RetryAfter.Delta.HasValue) + if (response.Headers.RetryAfter.Delta.Value.TotalSeconds > 0) { - if (response.Headers.RetryAfter.Delta.Value.TotalSeconds > 0) - retryAfterSeconds = (int)response.Headers.RetryAfter.Delta.Value.TotalSeconds + 1; // Add 1 second to ensure we wait a bit longer than the suggested time + retryAfterSeconds = + (int)response.Headers.RetryAfter.Delta.Value.TotalSeconds + + 1; // Add 1 second to ensure we wait a bit longer than the suggested time } - - await Task.Delay(retryAfterSeconds * 1000); // Peform the delay - - response = await operation(); - retryCount++; } - response.EnsureSuccessStatusCode(); // Throw an exception if the response is not successful + await Task.Delay(retryAfterSeconds * 1000); // Peform the delay - return response; + response = await operation(); + retryCount++; } + + response.EnsureSuccessStatusCode(); // Throw an exception if the response is not successful + + return response; } } diff --git a/OF DL/Utils/ThrottledStream.cs b/OF DL/Utils/ThrottledStream.cs index b52b413..b53b712 100644 --- a/OF DL/Utils/ThrottledStream.cs +++ b/OF DL/Utils/ThrottledStream.cs @@ -2,15 +2,14 @@ using System.Reactive.Concurrency; namespace OF_DL.Utils; - public class ThrottledStream : Stream { - private readonly Stream parent; private readonly int maxBytesPerSecond; + private readonly Stream parent; private readonly IScheduler scheduler; - private readonly IStopwatch stopwatch; private readonly bool shouldThrottle; + private readonly IStopwatch stopwatch; private long processed; @@ -30,16 +29,39 @@ public class ThrottledStream : Stream } + public override bool CanRead => parent.CanRead; + + + public override bool CanSeek => parent.CanSeek; + + + public override bool CanWrite => parent.CanWrite; + + + public override long Length => parent.Length; + + + public override long Position + { + get => parent.Position; + set => parent.Position = value; + } + + protected void Throttle(int bytes) { - if (!shouldThrottle) return; + if (!shouldThrottle) + { + return; + } + processed += bytes; - var targetTime = TimeSpan.FromSeconds((double)processed / maxBytesPerSecond); - var actualTime = stopwatch.Elapsed; - var sleep = targetTime - actualTime; + TimeSpan targetTime = TimeSpan.FromSeconds((double)processed / maxBytesPerSecond); + TimeSpan actualTime = stopwatch.Elapsed; + TimeSpan sleep = targetTime - actualTime; if (sleep > TimeSpan.Zero) { - using var waitHandle = new AutoResetEvent(initialState: false); + using AutoResetEvent waitHandle = new(false); scheduler.Sleep(sleep).GetAwaiter().OnCompleted(() => waitHandle.Set()); waitHandle.WaitOne(); } @@ -47,11 +69,15 @@ public class ThrottledStream : Stream protected async Task ThrottleAsync(int bytes) { - if (!shouldThrottle) return; + if (!shouldThrottle) + { + return; + } + processed += bytes; - var targetTime = TimeSpan.FromSeconds((double)processed / maxBytesPerSecond); - var actualTime = stopwatch.Elapsed; - var sleep = targetTime - actualTime; + TimeSpan targetTime = TimeSpan.FromSeconds((double)processed / maxBytesPerSecond); + TimeSpan actualTime = stopwatch.Elapsed; + TimeSpan sleep = targetTime - actualTime; if (sleep > TimeSpan.Zero) { @@ -61,7 +87,7 @@ public class ThrottledStream : Stream public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - var read = await parent.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); + int read = await parent.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); await ThrottleAsync(read).ConfigureAwait(false); return read; } @@ -79,71 +105,26 @@ public class ThrottledStream : Stream await parent.WriteAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); } - public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, + CancellationToken cancellationToken = default) { await ThrottleAsync(buffer.Length).ConfigureAwait(false); await parent.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); } - public override bool CanRead - { - get { return parent.CanRead; } - } - - - public override bool CanSeek - { - get { return parent.CanSeek; } - } - - - public override bool CanWrite - { - get { return parent.CanWrite; } - } - - - public override void Flush() - { - parent.Flush(); - } - - - public override long Length - { - get { return parent.Length; } - } - - - public override long Position - { - get - { - return parent.Position; - } - set - { - parent.Position = value; - } - } + public override void Flush() => parent.Flush(); public override int Read(byte[] buffer, int offset, int count) { - var read = parent.Read(buffer, offset, count); + int read = parent.Read(buffer, offset, count); Throttle(read); return read; } - public override long Seek(long offset, SeekOrigin origin) - { - return parent.Seek(offset, origin); - } + public override long Seek(long offset, SeekOrigin origin) => parent.Seek(offset, origin); - public override void SetLength(long value) - { - parent.SetLength(value); - } + public override void SetLength(long value) => parent.SetLength(value); public override void Write(byte[] buffer, int offset, int count) { diff --git a/OF DL/Utils/XmlUtils.cs b/OF DL/Utils/XmlUtils.cs index 940ebef..eef2073 100644 --- a/OF DL/Utils/XmlUtils.cs +++ b/OF DL/Utils/XmlUtils.cs @@ -1,28 +1,28 @@ using System.Xml.Linq; -namespace OF_DL.Utils +namespace OF_DL.Utils; + +internal static class XmlUtils { - internal static class XmlUtils + // When true, return original text without parsing/stripping. + public static bool Passthrough { get; set; } = false; + + public static string EvaluateInnerText(string xmlValue) { - // When true, return original text without parsing/stripping. - public static bool Passthrough { get; set; } = false; - - public static string EvaluateInnerText(string xmlValue) + if (Passthrough) { - if (Passthrough) - { - return xmlValue ?? string.Empty; - } - - try - { - var parsedText = XElement.Parse($"{xmlValue}"); - return parsedText.Value; - } - catch - { } - - return string.Empty; + return xmlValue ?? string.Empty; } + + try + { + XElement parsedText = XElement.Parse($"{xmlValue}"); + return parsedText.Value; + } + catch + { + } + + return string.Empty; } } diff --git a/OF DL/Widevine/CDM.cs b/OF DL/Widevine/CDM.cs index ffe25cc..d005cd5 100644 --- a/OF DL/Widevine/CDM.cs +++ b/OF DL/Widevine/CDM.cs @@ -4,579 +4,571 @@ using System.Text; using OF_DL.Crypto; using ProtoBuf; -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +public class CDM { - public class CDM + private static Dictionary Devices { get; } = new() { - static Dictionary Devices { get; } = new Dictionary() + [Constants.DEVICE_NAME] = new CDMDevice(Constants.DEVICE_NAME) + }; + + private static Dictionary Sessions { get; } = new(); + + private static byte[] CheckPSSH(string psshB64) + { + byte[] systemID = new byte[] { 237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237 }; + + if (psshB64.Length % 4 != 0) { - [Constants.DEVICE_NAME] = new CDMDevice(Constants.DEVICE_NAME, null, null, null) - }; - static Dictionary Sessions { get; set; } = new Dictionary(); - - static byte[] CheckPSSH(string psshB64) - { - byte[] systemID = new byte[] { 237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237 }; - - if (psshB64.Length % 4 != 0) - { - psshB64 = psshB64.PadRight(psshB64.Length + (4 - (psshB64.Length % 4)), '='); - } - - byte[] pssh = Convert.FromBase64String(psshB64); - - if (pssh.Length < 30) - return pssh; - - if (!pssh[12..28].SequenceEqual(systemID)) - { - List newPssh = new List() { 0, 0, 0 }; - newPssh.Add((byte)(32 + pssh.Length)); - newPssh.AddRange(Encoding.UTF8.GetBytes("pssh")); - newPssh.AddRange(new byte[] { 0, 0, 0, 0 }); - newPssh.AddRange(systemID); - newPssh.AddRange(new byte[] { 0, 0, 0, 0 }); - newPssh[31] = (byte)(pssh.Length); - newPssh.AddRange(pssh); - - return newPssh.ToArray(); - } - else - { - return pssh; - } + psshB64 = psshB64.PadRight(psshB64.Length + (4 - psshB64.Length % 4), '='); } - public static string OpenSession(string initDataB64, string deviceName, bool offline = false, bool raw = false) + byte[] pssh = Convert.FromBase64String(psshB64); + + if (pssh.Length < 30) { - byte[] initData = CheckPSSH(initDataB64); + return pssh; + } - var device = Devices[deviceName]; + if (!pssh[12..28].SequenceEqual(systemID)) + { + List newPssh = new() { 0, 0, 0 }; + newPssh.Add((byte)(32 + pssh.Length)); + newPssh.AddRange(Encoding.UTF8.GetBytes("pssh")); + newPssh.AddRange(new byte[] { 0, 0, 0, 0 }); + newPssh.AddRange(systemID); + newPssh.AddRange(new byte[] { 0, 0, 0, 0 }); + newPssh[31] = (byte)pssh.Length; + newPssh.AddRange(pssh); - byte[] sessionId = new byte[16]; + return newPssh.ToArray(); + } - if (device.IsAndroid) + return pssh; + } + + public static string OpenSession(string initDataB64, string deviceName, bool offline = false, bool raw = false) + { + byte[] initData = CheckPSSH(initDataB64); + + CDMDevice device = Devices[deviceName]; + + byte[] sessionId = new byte[16]; + + if (device.IsAndroid) + { + string randHex = ""; + + Random rand = new(); + string choice = "ABCDEF0123456789"; + for (int i = 0; i < 16; i++) { - string randHex = ""; - - Random rand = new Random(); - string choice = "ABCDEF0123456789"; - for (int i = 0; i < 16; i++) - randHex += choice[rand.Next(16)]; - - string counter = "01"; - string rest = "00000000000000"; - sessionId = Encoding.ASCII.GetBytes(randHex + counter + rest); - } - else - { - Random rand = new Random(); - rand.NextBytes(sessionId); + randHex += choice[rand.Next(16)]; } - Session session; - dynamic parsedInitData = ParseInitData(initData); + string counter = "01"; + string rest = "00000000000000"; + sessionId = Encoding.ASCII.GetBytes(randHex + counter + rest); + } + else + { + Random rand = new(); + rand.NextBytes(sessionId); + } - if (parsedInitData != null) + Session session; + dynamic parsedInitData = ParseInitData(initData); + + if (parsedInitData != null) + { + session = new Session(sessionId, parsedInitData, device, offline); + } + else if (raw) + { + session = new Session(sessionId, initData, device, offline); + } + else + { + return null; + } + + Sessions.Add(BytesToHex(sessionId), session); + + return BytesToHex(sessionId); + } + + private static WidevineCencHeader ParseInitData(byte[] initData) + { + WidevineCencHeader cencHeader; + + try + { + cencHeader = Serializer.Deserialize(new MemoryStream(initData[32..])); + } + catch + { + try { - session = new Session(sessionId, parsedInitData, device, offline); + //needed for HBO Max + + PSSHBox psshBox = PSSHBox.FromByteArray(initData); + cencHeader = Serializer.Deserialize(new MemoryStream(psshBox.Data)); } - else if (raw) - { - session = new Session(sessionId, initData, device, offline); - } - else + catch { + //Logger.Verbose("Unable to parse, unsupported init data format"); return null; } - - Sessions.Add(BytesToHex(sessionId), session); - - return BytesToHex(sessionId); } - static WidevineCencHeader ParseInitData(byte[] initData) + return cencHeader; + } + + public static bool CloseSession(string sessionId) + { + //Logger.Debug($"CloseSession(session_id={BytesToHex(sessionId)})"); + //Logger.Verbose("Closing CDM session"); + + if (Sessions.ContainsKey(sessionId)) { - WidevineCencHeader cencHeader; - - try - { - cencHeader = Serializer.Deserialize(new MemoryStream(initData[32..])); - } - catch - { - try - { - //needed for HBO Max - - PSSHBox psshBox = PSSHBox.FromByteArray(initData); - cencHeader = Serializer.Deserialize(new MemoryStream(psshBox.Data)); - } - catch - { - //Logger.Verbose("Unable to parse, unsupported init data format"); - return null; - } - } - - return cencHeader; - } - - public static bool CloseSession(string sessionId) - { - //Logger.Debug($"CloseSession(session_id={BytesToHex(sessionId)})"); - //Logger.Verbose("Closing CDM session"); - - if (Sessions.ContainsKey(sessionId)) - { - Sessions.Remove(sessionId); - //Logger.Verbose("CDM session closed"); - return true; - } - else - { - //Logger.Info($"Session {sessionId} not found"); - return false; - } - } - - public static bool SetServiceCertificate(string sessionId, byte[] certData) - { - //Logger.Debug($"SetServiceCertificate(sessionId={BytesToHex(sessionId)}, cert={certB64})"); - //Logger.Verbose($"Setting service certificate"); - - if (!Sessions.ContainsKey(sessionId)) - { - //Logger.Error("Session ID doesn't exist"); - return false; - } - - SignedMessage signedMessage = new SignedMessage(); - - try - { - signedMessage = Serializer.Deserialize(new MemoryStream(certData)); - } - catch - { - //Logger.Warn("Failed to parse cert as SignedMessage"); - } - - SignedDeviceCertificate serviceCertificate; - try - { - try - { - //Logger.Debug("Service cert provided as signedmessage"); - serviceCertificate = Serializer.Deserialize(new MemoryStream(signedMessage.Msg)); - } - catch - { - //Logger.Debug("Service cert provided as signeddevicecertificate"); - serviceCertificate = Serializer.Deserialize(new MemoryStream(certData)); - } - } - catch - { - //Logger.Error("Failed to parse service certificate"); - return false; - } - - Sessions[sessionId].ServiceCertificate = serviceCertificate; - Sessions[sessionId].PrivacyMode = true; - + Sessions.Remove(sessionId); + //Logger.Verbose("CDM session closed"); return true; } - public static byte[] GetLicenseRequest(string sessionId) + //Logger.Info($"Session {sessionId} not found"); + return false; + } + + public static bool SetServiceCertificate(string sessionId, byte[] certData) + { + //Logger.Debug($"SetServiceCertificate(sessionId={BytesToHex(sessionId)}, cert={certB64})"); + //Logger.Verbose($"Setting service certificate"); + + if (!Sessions.ContainsKey(sessionId)) { - //Logger.Debug($"GetLicenseRequest(sessionId={BytesToHex(sessionId)})"); - //Logger.Verbose($"Getting license request"); - - if (!Sessions.ContainsKey(sessionId)) - { - //Logger.Error("Session ID doesn't exist"); - return null; - } - - var session = Sessions[sessionId]; - - //Logger.Debug("Building license request"); - - dynamic licenseRequest; - - if (session.InitData is WidevineCencHeader) - { - licenseRequest = new SignedLicenseRequest - { - Type = SignedLicenseRequest.MessageType.LicenseRequest, - Msg = new LicenseRequest - { - Type = LicenseRequest.RequestType.New, - KeyControlNonce = 1093602366, - ProtocolVersion = ProtocolVersion.Current, - ContentId = new LicenseRequest.ContentIdentification - { - CencId = new LicenseRequest.ContentIdentification.Cenc - { - LicenseType = session.Offline ? LicenseType.Offline : LicenseType.Default, - RequestId = session.SessionId, - Pssh = session.InitData - } - } - } - }; - } - else - { - licenseRequest = new SignedLicenseRequestRaw - { - Type = SignedLicenseRequestRaw.MessageType.LicenseRequest, - Msg = new LicenseRequestRaw - { - Type = LicenseRequestRaw.RequestType.New, - KeyControlNonce = 1093602366, - ProtocolVersion = ProtocolVersion.Current, - ContentId = new LicenseRequestRaw.ContentIdentification - { - CencId = new LicenseRequestRaw.ContentIdentification.Cenc - { - LicenseType = session.Offline ? LicenseType.Offline : LicenseType.Default, - RequestId = session.SessionId, - Pssh = session.InitData - } - } - } - }; - } - - if (session.PrivacyMode) - { - //Logger.Debug("Privacy mode & serivce certificate loaded, encrypting client id"); - - EncryptedClientIdentification encryptedClientIdProto = new EncryptedClientIdentification(); - - //Logger.Debug("Unencrypted client id " + Utils.SerializeToString(clientId)); - - using var memoryStream = new MemoryStream(); - Serializer.Serialize(memoryStream, session.Device.ClientID); - byte[] data = Padding.AddPKCS7Padding(memoryStream.ToArray(), 16); - - using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider - { - BlockSize = 128, - Padding = PaddingMode.PKCS7, - Mode = CipherMode.CBC - }; - aesProvider.GenerateKey(); - aesProvider.GenerateIV(); - - using MemoryStream mstream = new MemoryStream(); - using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateEncryptor(aesProvider.Key, aesProvider.IV), CryptoStreamMode.Write); - cryptoStream.Write(data, 0, data.Length); - encryptedClientIdProto.EncryptedClientId = mstream.ToArray(); - - using RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); - RSA.ImportRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey, out int bytesRead); - encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aesProvider.Key, RSAEncryptionPadding.OaepSHA1); - encryptedClientIdProto.EncryptedClientIdIv = aesProvider.IV; - encryptedClientIdProto.ServiceId = Encoding.UTF8.GetString(session.ServiceCertificate.DeviceCertificate.ServiceId); - encryptedClientIdProto.ServiceCertificateSerialNumber = session.ServiceCertificate.DeviceCertificate.SerialNumber; - - licenseRequest.Msg.EncryptedClientId = encryptedClientIdProto; - } - else - { - licenseRequest.Msg.ClientId = session.Device.ClientID; - } - - //Logger.Debug("Signing license request"); - - using (var memoryStream = new MemoryStream()) - { - Serializer.Serialize(memoryStream, licenseRequest.Msg); - byte[] data = memoryStream.ToArray(); - session.LicenseRequest = data; - - licenseRequest.Signature = session.Device.Sign(data); - } - - //Logger.Verbose("License request created"); - - byte[] requestBytes; - using (var memoryStream = new MemoryStream()) - { - Serializer.Serialize(memoryStream, licenseRequest); - requestBytes = memoryStream.ToArray(); - } - - Sessions[sessionId] = session; - - //Logger.Debug($"license request b64: {Convert.ToBase64String(requestBytes)}"); - return requestBytes; + //Logger.Error("Session ID doesn't exist"); + return false; } - public static void ProvideLicense(string sessionId, byte[] license) + SignedMessage signedMessage = new(); + + try { - //Logger.Debug($"ProvideLicense(sessionId={BytesToHex(sessionId)}, licenseB64={licenseB64})"); - //Logger.Verbose("Decrypting provided license"); + signedMessage = Serializer.Deserialize(new MemoryStream(certData)); + } + catch + { + //Logger.Warn("Failed to parse cert as SignedMessage"); + } - if (!Sessions.ContainsKey(sessionId)) - { - throw new Exception("Session ID doesn't exist"); - } - - var session = Sessions[sessionId]; - - if (session.LicenseRequest == null) - { - throw new Exception("Generate a license request first"); - } - - SignedLicense signedLicense; + SignedDeviceCertificate serviceCertificate; + try + { try { - signedLicense = Serializer.Deserialize(new MemoryStream(license)); + //Logger.Debug("Service cert provided as signedmessage"); + serviceCertificate = + Serializer.Deserialize(new MemoryStream(signedMessage.Msg)); } catch { - throw new Exception("Unable to parse license"); + //Logger.Debug("Service cert provided as signeddevicecertificate"); + serviceCertificate = Serializer.Deserialize(new MemoryStream(certData)); } + } + catch + { + //Logger.Error("Failed to parse service certificate"); + return false; + } - //Logger.Debug("License: " + Utils.SerializeToString(signedLicense)); + Sessions[sessionId].ServiceCertificate = serviceCertificate; + Sessions[sessionId].PrivacyMode = true; - session.License = signedLicense; + return true; + } - //Logger.Debug($"Deriving keys from session key"); + public static byte[] GetLicenseRequest(string sessionId) + { + //Logger.Debug($"GetLicenseRequest(sessionId={BytesToHex(sessionId)})"); + //Logger.Verbose($"Getting license request"); - try + if (!Sessions.ContainsKey(sessionId)) + { + //Logger.Error("Session ID doesn't exist"); + return null; + } + + Session session = Sessions[sessionId]; + + //Logger.Debug("Building license request"); + + dynamic licenseRequest; + + if (session.InitData is WidevineCencHeader) + { + licenseRequest = new SignedLicenseRequest { - var sessionKey = session.Device.Decrypt(session.License.SessionKey); - - if (sessionKey.Length != 16) + Type = SignedLicenseRequest.MessageType.LicenseRequest, + Msg = new LicenseRequest { - throw new Exception("Unable to decrypt session key"); + Type = LicenseRequest.RequestType.New, + KeyControlNonce = 1093602366, + ProtocolVersion = ProtocolVersion.Current, + ContentId = new LicenseRequest.ContentIdentification + { + CencId = new LicenseRequest.ContentIdentification.Cenc + { + LicenseType = session.Offline ? LicenseType.Offline : LicenseType.Default, + RequestId = session.SessionId, + Pssh = session.InitData + } + } } + }; + } + else + { + licenseRequest = new SignedLicenseRequestRaw + { + Type = SignedLicenseRequestRaw.MessageType.LicenseRequest, + Msg = new LicenseRequestRaw + { + Type = LicenseRequestRaw.RequestType.New, + KeyControlNonce = 1093602366, + ProtocolVersion = ProtocolVersion.Current, + ContentId = new LicenseRequestRaw.ContentIdentification + { + CencId = new LicenseRequestRaw.ContentIdentification.Cenc + { + LicenseType = session.Offline ? LicenseType.Offline : LicenseType.Default, + RequestId = session.SessionId, + Pssh = session.InitData + } + } + } + }; + } - session.SessionKey = sessionKey; - } - catch + if (session.PrivacyMode) + { + //Logger.Debug("Privacy mode & serivce certificate loaded, encrypting client id"); + + EncryptedClientIdentification encryptedClientIdProto = new(); + + //Logger.Debug("Unencrypted client id " + Utils.SerializeToString(clientId)); + + using MemoryStream memoryStream = new(); + 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 MemoryStream mstream = new(); + using CryptoStream cryptoStream = new(mstream, aesProvider.CreateEncryptor(aesProvider.Key, aesProvider.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 bytesRead); + encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aesProvider.Key, RSAEncryptionPadding.OaepSHA1); + encryptedClientIdProto.EncryptedClientIdIv = aesProvider.IV; + encryptedClientIdProto.ServiceId = + Encoding.UTF8.GetString(session.ServiceCertificate.DeviceCertificate.ServiceId); + encryptedClientIdProto.ServiceCertificateSerialNumber = + session.ServiceCertificate.DeviceCertificate.SerialNumber; + + licenseRequest.Msg.EncryptedClientId = encryptedClientIdProto; + } + else + { + licenseRequest.Msg.ClientId = session.Device.ClientID; + } + + //Logger.Debug("Signing license request"); + + using (MemoryStream memoryStream = new()) + { + Serializer.Serialize(memoryStream, licenseRequest.Msg); + byte[] data = memoryStream.ToArray(); + session.LicenseRequest = data; + + licenseRequest.Signature = session.Device.Sign(data); + } + + //Logger.Verbose("License request created"); + + byte[] requestBytes; + using (MemoryStream memoryStream = new()) + { + Serializer.Serialize(memoryStream, licenseRequest); + requestBytes = memoryStream.ToArray(); + } + + Sessions[sessionId] = session; + + //Logger.Debug($"license request b64: {Convert.ToBase64String(requestBytes)}"); + return requestBytes; + } + + public static void ProvideLicense(string sessionId, byte[] license) + { + //Logger.Debug($"ProvideLicense(sessionId={BytesToHex(sessionId)}, licenseB64={licenseB64})"); + //Logger.Verbose("Decrypting provided license"); + + if (!Sessions.ContainsKey(sessionId)) + { + throw new Exception("Session ID doesn't exist"); + } + + Session session = Sessions[sessionId]; + + if (session.LicenseRequest == null) + { + throw new Exception("Generate a license request first"); + } + + SignedLicense signedLicense; + try + { + signedLicense = Serializer.Deserialize(new MemoryStream(license)); + } + catch + { + throw new Exception("Unable to parse license"); + } + + //Logger.Debug("License: " + Utils.SerializeToString(signedLicense)); + + session.License = signedLicense; + + //Logger.Debug($"Deriving keys from session key"); + + try + { + byte[] sessionKey = session.Device.Decrypt(session.License.SessionKey); + + if (sessionKey.Length != 16) { throw new Exception("Unable to decrypt session key"); } - //Logger.Debug("Session key: " + BytesToHex(session.SessionKey)); + session.SessionKey = sessionKey; + } + catch + { + throw new Exception("Unable to decrypt session key"); + } - session.DerivedKeys = DeriveKeys(session.LicenseRequest, session.SessionKey); + //Logger.Debug("Session key: " + BytesToHex(session.SessionKey)); - //Logger.Debug("Verifying license signature"); + session.DerivedKeys = DeriveKeys(session.LicenseRequest, session.SessionKey); - byte[] licenseBytes; - using (var memoryStream = new MemoryStream()) + //Logger.Debug("Verifying license signature"); + + byte[] licenseBytes; + using (MemoryStream memoryStream = new()) + { + Serializer.Serialize(memoryStream, signedLicense.Msg); + licenseBytes = memoryStream.ToArray(); + } + + byte[] hmacHash = CryptoUtils.GetHMACSHA256Digest(licenseBytes, session.DerivedKeys.Auth1); + + if (!hmacHash.SequenceEqual(signedLicense.Signature)) + { + throw new Exception("License signature mismatch"); + } + + foreach (License.KeyContainer key in signedLicense.Msg.Keys) + { + string type = key.Type.ToString(); + + if (type == "Signing") { - Serializer.Serialize(memoryStream, signedLicense.Msg); - licenseBytes = memoryStream.ToArray(); - } - byte[] hmacHash = CryptoUtils.GetHMACSHA256Digest(licenseBytes, session.DerivedKeys.Auth1); - - if (!hmacHash.SequenceEqual(signedLicense.Signature)) - { - throw new Exception("License signature mismatch"); + continue; } - foreach (License.KeyContainer key in signedLicense.Msg.Keys) + byte[] keyId; + byte[] encryptedKey = key.Key; + byte[] iv = key.Iv; + keyId = key.Id; + if (keyId == null) { - string type = key.Type.ToString(); + keyId = Encoding.ASCII.GetBytes(key.Type.ToString()); + } - if (type == "Signing") - continue; + byte[] decryptedKey; - byte[] keyId; - byte[] encryptedKey = key.Key; - byte[] iv = key.Iv; - keyId = key.Id; - if (keyId == null) + using MemoryStream mstream = new(); + using AesCryptoServiceProvider aesProvider = new() { Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 }; + using CryptoStream cryptoStream = new(mstream, aesProvider.CreateDecryptor(session.DerivedKeys.Enc, iv), + CryptoStreamMode.Write); + cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); + decryptedKey = mstream.ToArray(); + + List permissions = new(); + if (type == "OperatorSession") + { + foreach (PropertyInfo perm in key._OperatorSessionKeyPermissions.GetType().GetProperties()) { - keyId = Encoding.ASCII.GetBytes(key.Type.ToString()); - } - - byte[] decryptedKey; - - using MemoryStream mstream = new MemoryStream(); - using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider - { - Mode = CipherMode.CBC, - Padding = PaddingMode.PKCS7 - }; - using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateDecryptor(session.DerivedKeys.Enc, iv), CryptoStreamMode.Write); - cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); - decryptedKey = mstream.ToArray(); - - List permissions = new List(); - if (type == "OperatorSession") - { - foreach (PropertyInfo perm in key._OperatorSessionKeyPermissions.GetType().GetProperties()) + if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1) { - if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1) - { - permissions.Add(perm.Name); - } + permissions.Add(perm.Name); } } - session.ContentKeys.Add(new ContentKey - { - KeyID = keyId, - Type = type, - Bytes = decryptedKey, - Permissions = permissions - }); } - //Logger.Debug($"Key count: {session.Keys.Count}"); - - Sessions[sessionId] = session; - - //Logger.Verbose("Decrypted all keys"); - } - - public static DerivedKeys DeriveKeys(byte[] message, byte[] key) - { - byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[] { 0x0, }).Concat(message).Concat(new byte[] { 0x0, 0x0, 0x0, 0x80 }).ToArray(); - byte[] authKeyBase = Encoding.UTF8.GetBytes("AUTHENTICATION").Concat(new byte[] { 0x0, }).Concat(message).Concat(new byte[] { 0x0, 0x0, 0x2, 0x0 }).ToArray(); - - byte[] encKey = new byte[] { 0x01 }.Concat(encKeyBase).ToArray(); - byte[] authKey1 = new byte[] { 0x01 }.Concat(authKeyBase).ToArray(); - byte[] authKey2 = new byte[] { 0x02 }.Concat(authKeyBase).ToArray(); - byte[] authKey3 = new byte[] { 0x03 }.Concat(authKeyBase).ToArray(); - byte[] authKey4 = new byte[] { 0x04 }.Concat(authKeyBase).ToArray(); - - byte[] encCmacKey = CryptoUtils.GetCMACDigest(encKey, key); - byte[] authCmacKey1 = CryptoUtils.GetCMACDigest(authKey1, key); - byte[] authCmacKey2 = CryptoUtils.GetCMACDigest(authKey2, key); - byte[] authCmacKey3 = CryptoUtils.GetCMACDigest(authKey3, key); - byte[] authCmacKey4 = CryptoUtils.GetCMACDigest(authKey4, key); - - byte[] authCmacCombined1 = authCmacKey1.Concat(authCmacKey2).ToArray(); - byte[] authCmacCombined2 = authCmacKey3.Concat(authCmacKey4).ToArray(); - - return new DerivedKeys + session.ContentKeys.Add(new ContentKey { - Auth1 = authCmacCombined1, - Auth2 = authCmacCombined2, - Enc = encCmacKey - }; + KeyID = keyId, Type = type, Bytes = decryptedKey, Permissions = permissions + }); } - public static List GetKeys(string sessionId) - { - if (Sessions.ContainsKey(sessionId)) - return Sessions[sessionId].ContentKeys; - else - { - throw new Exception("Session not found"); - } - } + //Logger.Debug($"Key count: {session.Keys.Count}"); - private static string BytesToHex(byte[] data) - { - return BitConverter.ToString(data).Replace("-", ""); - } + Sessions[sessionId] = session; + + //Logger.Verbose("Decrypted all keys"); } + + public static DerivedKeys DeriveKeys(byte[] message, byte[] key) + { + byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[] { 0x0 }).Concat(message) + .Concat(new byte[] { 0x0, 0x0, 0x0, 0x80 }).ToArray(); + byte[] authKeyBase = Encoding.UTF8.GetBytes("AUTHENTICATION").Concat(new byte[] { 0x0 }).Concat(message) + .Concat(new byte[] { 0x0, 0x0, 0x2, 0x0 }).ToArray(); + + byte[] encKey = new byte[] { 0x01 }.Concat(encKeyBase).ToArray(); + byte[] authKey1 = new byte[] { 0x01 }.Concat(authKeyBase).ToArray(); + byte[] authKey2 = new byte[] { 0x02 }.Concat(authKeyBase).ToArray(); + byte[] authKey3 = new byte[] { 0x03 }.Concat(authKeyBase).ToArray(); + byte[] authKey4 = new byte[] { 0x04 }.Concat(authKeyBase).ToArray(); + + byte[] encCmacKey = CryptoUtils.GetCMACDigest(encKey, key); + byte[] authCmacKey1 = CryptoUtils.GetCMACDigest(authKey1, key); + byte[] authCmacKey2 = CryptoUtils.GetCMACDigest(authKey2, key); + byte[] authCmacKey3 = CryptoUtils.GetCMACDigest(authKey3, key); + byte[] authCmacKey4 = CryptoUtils.GetCMACDigest(authKey4, key); + + byte[] authCmacCombined1 = authCmacKey1.Concat(authCmacKey2).ToArray(); + byte[] authCmacCombined2 = authCmacKey3.Concat(authCmacKey4).ToArray(); + + return new DerivedKeys { Auth1 = authCmacCombined1, Auth2 = authCmacCombined2, Enc = encCmacKey }; + } + + public static List GetKeys(string sessionId) + { + if (Sessions.ContainsKey(sessionId)) + { + return Sessions[sessionId].ContentKeys; + } + + throw new Exception("Session not found"); + } + + private static string BytesToHex(byte[] data) => BitConverter.ToString(data).Replace("-", ""); } - - /* - public static List ProvideLicense(string requestB64, string licenseB64) + public static List ProvideLicense(string requestB64, string licenseB64) + { + byte[] licenseRequest; + + var request = Serializer.Deserialize(new MemoryStream(Convert.FromBase64String(requestB64))); + + using (var ms = new MemoryStream()) { - byte[] licenseRequest; + Serializer.Serialize(ms, request.Msg); + licenseRequest = ms.ToArray(); + } - var request = Serializer.Deserialize(new MemoryStream(Convert.FromBase64String(requestB64))); + SignedLicense signedLicense; + try + { + signedLicense = Serializer.Deserialize(new MemoryStream(Convert.FromBase64String(licenseB64))); + } + catch + { + return null; + } - using (var ms = new MemoryStream()) - { - Serializer.Serialize(ms, request.Msg); - licenseRequest = ms.ToArray(); - } + byte[] sessionKey; + try + { - SignedLicense signedLicense; - try - { - signedLicense = Serializer.Deserialize(new MemoryStream(Convert.FromBase64String(licenseB64))); - } - catch + sessionKey = Controllers.Adapter.OaepDecrypt(Convert.ToBase64String(signedLicense.SessionKey)); + + if (sessionKey.Length != 16) { return null; } + } + catch + { + return null; + } - byte[] sessionKey; - try + byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[] { 0x0, }).Concat(licenseRequest).Concat(new byte[] { 0x0, 0x0, 0x0, 0x80 }).ToArray(); + + byte[] encKey = new byte[] { 0x01 }.Concat(encKeyBase).ToArray(); + + byte[] encCmacKey = GetCmacDigest(encKey, sessionKey); + + byte[] encryptionKey = encCmacKey; + + List keys = new List(); + + foreach (License.KeyContainer key in signedLicense.Msg.Keys) + { + string type = key.Type.ToString(); + if (type == "Signing") { - - sessionKey = Controllers.Adapter.OaepDecrypt(Convert.ToBase64String(signedLicense.SessionKey)); - - if (sessionKey.Length != 16) - { - return null; - } - } - catch - { - return null; + continue; } - byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[] { 0x0, }).Concat(licenseRequest).Concat(new byte[] { 0x0, 0x0, 0x0, 0x80 }).ToArray(); - - byte[] encKey = new byte[] { 0x01 }.Concat(encKeyBase).ToArray(); - - byte[] encCmacKey = GetCmacDigest(encKey, sessionKey); - - byte[] encryptionKey = encCmacKey; - - List keys = new List(); - - foreach (License.KeyContainer key in signedLicense.Msg.Keys) + byte[] keyId; + byte[] encryptedKey = key.Key; + byte[] iv = key.Iv; + keyId = key.Id; + if (keyId == null) { - string type = key.Type.ToString(); - if (type == "Signing") - { - continue; - } + keyId = Encoding.ASCII.GetBytes(key.Type.ToString()); + } - byte[] keyId; - byte[] encryptedKey = key.Key; - byte[] iv = key.Iv; - keyId = key.Id; - if (keyId == null) - { - keyId = Encoding.ASCII.GetBytes(key.Type.ToString()); - } + byte[] decryptedKey; - byte[] decryptedKey; + using MemoryStream mstream = new MemoryStream(); + using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider + { + Mode = CipherMode.CBC, + Padding = PaddingMode.PKCS7 + }; + using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateDecryptor(encryptionKey, iv), CryptoStreamMode.Write); + cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); + decryptedKey = mstream.ToArray(); - using MemoryStream mstream = new MemoryStream(); - using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider + List permissions = new List(); + if (type == "OPERATOR_SESSION") + { + foreach (FieldInfo perm in key._OperatorSessionKeyPermissions.GetType().GetFields()) { - Mode = CipherMode.CBC, - Padding = PaddingMode.PKCS7 - }; - using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateDecryptor(encryptionKey, iv), CryptoStreamMode.Write); - cryptoStream.Write(encryptedKey, 0, encryptedKey.Length); - decryptedKey = mstream.ToArray(); - - List permissions = new List(); - if (type == "OPERATOR_SESSION") - { - foreach (FieldInfo perm in key._OperatorSessionKeyPermissions.GetType().GetFields()) + if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1) { - if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1) - { - permissions.Add(perm.Name); - } + permissions.Add(perm.Name); } } - keys.Add(BitConverter.ToString(keyId).Replace("-","").ToLower() + ":" + BitConverter.ToString(decryptedKey).Replace("-", "").ToLower()); } + keys.Add(BitConverter.ToString(keyId).Replace("-","").ToLower() + ":" + BitConverter.ToString(decryptedKey).Replace("-", "").ToLower()); + } - return keys; - }*/ + return keys; + }*/ diff --git a/OF DL/Widevine/CDMApi.cs b/OF DL/Widevine/CDMApi.cs index 4e0f1d9..e6d7315 100644 --- a/OF DL/Widevine/CDMApi.cs +++ b/OF DL/Widevine/CDMApi.cs @@ -1,25 +1,21 @@ -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +public class CDMApi { - public class CDMApi + private string SessionId { get; set; } + + public byte[] GetChallenge(string initDataB64, string certDataB64, bool offline = false, bool raw = false) { - string SessionId { get; set; } - - public byte[] GetChallenge(string initDataB64, string certDataB64, bool offline = false, bool raw = false) - { - SessionId = CDM.OpenSession(initDataB64, Constants.DEVICE_NAME, offline, raw); - CDM.SetServiceCertificate(SessionId, Convert.FromBase64String(certDataB64)); - return CDM.GetLicenseRequest(SessionId); - } - - public bool ProvideLicense(string licenseB64) - { - CDM.ProvideLicense(SessionId, Convert.FromBase64String(licenseB64)); - return true; - } - - public List GetKeys() - { - return CDM.GetKeys(SessionId); - } + SessionId = CDM.OpenSession(initDataB64, Constants.DEVICE_NAME, offline, raw); + CDM.SetServiceCertificate(SessionId, Convert.FromBase64String(certDataB64)); + return CDM.GetLicenseRequest(SessionId); } + + public bool ProvideLicense(string licenseB64) + { + CDM.ProvideLicense(SessionId, Convert.FromBase64String(licenseB64)); + return true; + } + + public List GetKeys() => CDM.GetKeys(SessionId); } diff --git a/OF DL/Widevine/CDMDevice.cs b/OF DL/Widevine/CDMDevice.cs index 7dc4023..09fc4ef 100644 --- a/OF DL/Widevine/CDMDevice.cs +++ b/OF DL/Widevine/CDMDevice.cs @@ -7,84 +7,86 @@ using Org.BouncyCastle.Crypto.Signers; using Org.BouncyCastle.OpenSsl; using ProtoBuf; -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +public class CDMDevice { - public class CDMDevice + public CDMDevice(string deviceName, byte[] clientIdBlobBytes = null, byte[] privateKeyBytes = null, + byte[] vmpBytes = null) { - public string DeviceName { get; set; } - public ClientIdentification ClientID { get; set; } - AsymmetricCipherKeyPair DeviceKeys { get; set; } + DeviceName = deviceName; - public virtual bool IsAndroid { get; set; } = true; + string privateKeyPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_private_key"); + string vmpPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_vmp_blob"); - public CDMDevice(string deviceName, byte[] clientIdBlobBytes = null, byte[] privateKeyBytes = null, byte[] vmpBytes = null) + if (clientIdBlobBytes == null) { - DeviceName = deviceName; + string clientIDPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_client_id_blob"); - string privateKeyPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_private_key"); - string vmpPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_vmp_blob"); - - if (clientIdBlobBytes == null) + if (!File.Exists(clientIDPath)) { - string clientIDPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_client_id_blob"); - - if (!File.Exists(clientIDPath)) - throw new Exception("No client id blob found"); - - clientIdBlobBytes = File.ReadAllBytes(clientIDPath); + throw new Exception("No client id blob found"); } - ClientID = Serializer.Deserialize(new MemoryStream(clientIdBlobBytes)); - - if (privateKeyBytes != null) - { - using var reader = new StringReader(Encoding.UTF8.GetString(privateKeyBytes)); - DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject(); - } - else if (File.Exists(privateKeyPath)) - { - using var reader = File.OpenText(privateKeyPath); - DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject(); - } - - if (vmpBytes != null) - { - var vmp = Serializer.Deserialize(new MemoryStream(vmpBytes)); - ClientID.FileHashes = vmp; - } - else if (File.Exists(vmpPath)) - { - var vmp = Serializer.Deserialize(new MemoryStream(File.ReadAllBytes(vmpPath))); - ClientID.FileHashes = vmp; - } + clientIdBlobBytes = File.ReadAllBytes(clientIDPath); } - public virtual byte[] Decrypt(byte[] data) + ClientID = Serializer.Deserialize(new MemoryStream(clientIdBlobBytes)); + + if (privateKeyBytes != null) { - OaepEncoding eng = new OaepEncoding(new RsaEngine()); - eng.Init(false, DeviceKeys.Private); - - int length = data.Length; - int blockSize = eng.GetInputBlockSize(); - - List plainText = new List(); - - for (int chunkPosition = 0; chunkPosition < length; chunkPosition += blockSize) - { - int chunkSize = Math.Min(blockSize, length - chunkPosition); - plainText.AddRange(eng.ProcessBlock(data, chunkPosition, chunkSize)); - } - - return plainText.ToArray(); + using StringReader reader = new(Encoding.UTF8.GetString(privateKeyBytes)); + DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject(); + } + else if (File.Exists(privateKeyPath)) + { + using StreamReader reader = File.OpenText(privateKeyPath); + DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject(); } - public virtual byte[] Sign(byte[] data) + if (vmpBytes != null) { - PssSigner eng = new PssSigner(new RsaEngine(), new Sha1Digest()); - - eng.Init(true, DeviceKeys.Private); - eng.BlockUpdate(data, 0, data.Length); - return eng.GenerateSignature(); + FileHashes? vmp = Serializer.Deserialize(new MemoryStream(vmpBytes)); + ClientID.FileHashes = vmp; + } + else if (File.Exists(vmpPath)) + { + FileHashes? vmp = Serializer.Deserialize(new MemoryStream(File.ReadAllBytes(vmpPath))); + ClientID.FileHashes = vmp; } } + + public string DeviceName { get; set; } + public ClientIdentification ClientID { get; set; } + private AsymmetricCipherKeyPair DeviceKeys { get; } + + public virtual bool IsAndroid { get; set; } = true; + + public virtual byte[] Decrypt(byte[] data) + { + OaepEncoding eng = new(new RsaEngine()); + eng.Init(false, DeviceKeys.Private); + + int length = data.Length; + int blockSize = eng.GetInputBlockSize(); + + List plainText = new(); + + for (int chunkPosition = 0; chunkPosition < length; chunkPosition += blockSize) + { + int chunkSize = Math.Min(blockSize, length - chunkPosition); + plainText.AddRange(eng.ProcessBlock(data, chunkPosition, chunkSize)); + } + + return plainText.ToArray(); + } + + public virtual byte[] Sign(byte[] data) + { + PssSigner eng = new(new RsaEngine(), new Sha1Digest()); + + eng.Init(true, DeviceKeys.Private); + eng.BlockUpdate(data, 0, data.Length); + return eng.GenerateSignature(); + } } diff --git a/OF DL/Widevine/Constants.cs b/OF DL/Widevine/Constants.cs index a692eb2..280dcaa 100644 --- a/OF DL/Widevine/Constants.cs +++ b/OF DL/Widevine/Constants.cs @@ -1,9 +1,10 @@ -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +public class Constants { - public class Constants - { - public static string WORKING_FOLDER { get; set; } = System.IO.Path.GetFullPath(System.IO.Path.Join(System.IO.Directory.GetCurrentDirectory(), "cdm")); - public static string DEVICES_FOLDER { get; set; } = System.IO.Path.GetFullPath(System.IO.Path.Join(WORKING_FOLDER, "devices")); - public static string DEVICE_NAME { get; set; } = "chrome_1610"; - } + public static string WORKING_FOLDER { get; set; } = + Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "cdm")); + + public static string DEVICES_FOLDER { get; set; } = Path.GetFullPath(Path.Join(WORKING_FOLDER, "devices")); + public static string DEVICE_NAME { get; set; } = "chrome_1610"; } diff --git a/OF DL/Widevine/ContentKey.cs b/OF DL/Widevine/ContentKey.cs index 8b611dd..1ad7507 100644 --- a/OF DL/Widevine/ContentKey.cs +++ b/OF DL/Widevine/ContentKey.cs @@ -1,39 +1,27 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +[Serializable] +public class ContentKey { - [Serializable] - public class ContentKey + [JsonPropertyName("key_id")] public byte[] KeyID { get; set; } + + [JsonPropertyName("type")] public string Type { get; set; } + + [JsonPropertyName("bytes")] public byte[] Bytes { get; set; } + + [NotMapped] + [JsonPropertyName("permissions")] + public List Permissions { - [JsonPropertyName("key_id")] - public byte[] KeyID { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("bytes")] - public byte[] Bytes { get; set; } - - [NotMapped] - [JsonPropertyName("permissions")] - public List Permissions { - get - { - return PermissionsString.Split(",").ToList(); - } - set - { - PermissionsString = string.Join(",", value); - } - } - - [JsonIgnore] - public string PermissionsString { get; set; } - - public override string ToString() - { - return $"{BitConverter.ToString(KeyID).Replace("-", "").ToLower()}:{BitConverter.ToString(Bytes).Replace("-", "").ToLower()}"; - } + get => PermissionsString.Split(",").ToList(); + set => PermissionsString = string.Join(",", value); } + + [JsonIgnore] public string PermissionsString { get; set; } + + public override string ToString() => + $"{BitConverter.ToString(KeyID).Replace("-", "").ToLower()}:{BitConverter.ToString(Bytes).Replace("-", "").ToLower()}"; } diff --git a/OF DL/Widevine/DerivedKeys.cs b/OF DL/Widevine/DerivedKeys.cs index fdcebec..5a61705 100644 --- a/OF DL/Widevine/DerivedKeys.cs +++ b/OF DL/Widevine/DerivedKeys.cs @@ -1,9 +1,8 @@ -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +public class DerivedKeys { - public class DerivedKeys - { - public byte[] Auth1 { get; set; } - public byte[] Auth2 { get; set; } - public byte[] Enc { get; set; } - } + public byte[] Auth1 { get; set; } + public byte[] Auth2 { get; set; } + public byte[] Enc { get; set; } } diff --git a/OF DL/Widevine/PSSHBox.cs b/OF DL/Widevine/PSSHBox.cs index 55b16de..a61f694 100644 --- a/OF DL/Widevine/PSSHBox.cs +++ b/OF DL/Widevine/PSSHBox.cs @@ -1,59 +1,68 @@ -namespace OF_DL.Widevine +namespace OF_DL.Widevine; + +internal class PSSHBox { - class PSSHBox + private static readonly byte[] PSSH_HEADER = new byte[] { 0x70, 0x73, 0x73, 0x68 }; + + private PSSHBox(List kids, byte[] data) { - static readonly byte[] PSSH_HEADER = new byte[] { 0x70, 0x73, 0x73, 0x68 }; + KIDs = kids; + Data = data; + } - public List KIDs { get; set; } = new List(); - public byte[] Data { get; set; } + public List KIDs { get; set; } = new(); + public byte[] Data { get; set; } - PSSHBox(List kids, byte[] data) + public static PSSHBox FromByteArray(byte[] psshbox) + { + using MemoryStream stream = new(psshbox); + + stream.Seek(4, SeekOrigin.Current); + byte[] header = new byte[4]; + stream.Read(header, 0, 4); + + if (!header.SequenceEqual(PSSH_HEADER)) { - KIDs = kids; - Data = data; + throw new Exception("Not a pssh box"); } - public static PSSHBox FromByteArray(byte[] psshbox) + stream.Seek(20, SeekOrigin.Current); + byte[] kidCountBytes = new byte[4]; + stream.Read(kidCountBytes, 0, 4); + + if (BitConverter.IsLittleEndian) { - using var stream = new System.IO.MemoryStream(psshbox); - - stream.Seek(4, System.IO.SeekOrigin.Current); - byte[] header = new byte[4]; - stream.Read(header, 0, 4); - - if (!header.SequenceEqual(PSSH_HEADER)) - throw new Exception("Not a pssh box"); - - stream.Seek(20, System.IO.SeekOrigin.Current); - byte[] kidCountBytes = new byte[4]; - stream.Read(kidCountBytes, 0, 4); - - if (BitConverter.IsLittleEndian) - Array.Reverse(kidCountBytes); - uint kidCount = BitConverter.ToUInt32(kidCountBytes); - - List kids = new List(); - for (int i = 0; i < kidCount; i++) - { - byte[] kid = new byte[16]; - stream.Read(kid); - kids.Add(kid); - } - - byte[] dataLengthBytes = new byte[4]; - stream.Read(dataLengthBytes); - - if (BitConverter.IsLittleEndian) - Array.Reverse(dataLengthBytes); - uint dataLength = BitConverter.ToUInt32(dataLengthBytes); - - if (dataLength == 0) - return new PSSHBox(kids, null); - - byte[] data = new byte[dataLength]; - stream.Read(data); - - return new PSSHBox(kids, data); + Array.Reverse(kidCountBytes); } + + uint kidCount = BitConverter.ToUInt32(kidCountBytes); + + List kids = new(); + for (int i = 0; i < kidCount; i++) + { + byte[] kid = new byte[16]; + stream.Read(kid); + kids.Add(kid); + } + + byte[] dataLengthBytes = new byte[4]; + stream.Read(dataLengthBytes); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(dataLengthBytes); + } + + uint dataLength = BitConverter.ToUInt32(dataLengthBytes); + + if (dataLength == 0) + { + return new PSSHBox(kids, null); + } + + byte[] data = new byte[dataLength]; + stream.Read(data); + + return new PSSHBox(kids, data); } } diff --git a/OF DL/Widevine/Session.cs b/OF DL/Widevine/Session.cs index 0c6a515..5e16afc 100644 --- a/OF DL/Widevine/Session.cs +++ b/OF DL/Widevine/Session.cs @@ -1,25 +1,24 @@ -namespace OF_DL.Widevine -{ - class Session - { - public byte[] SessionId { get; set; } - public dynamic InitData { get; set; } - public bool Offline { get; set; } - public CDMDevice Device { get; set; } - public byte[] SessionKey { get; set; } - public DerivedKeys DerivedKeys { get; set; } - public byte[] LicenseRequest { get; set; } - public SignedLicense License { get; set; } - public SignedDeviceCertificate ServiceCertificate { get; set; } - public bool PrivacyMode { get; set; } - public List ContentKeys { get; set; } = new List(); +namespace OF_DL.Widevine; - public Session(byte[] sessionId, dynamic initData, CDMDevice device, bool offline) - { - SessionId = sessionId; - InitData = initData; - Offline = offline; - Device = device; - } +internal class Session +{ + public Session(byte[] sessionId, dynamic initData, CDMDevice device, bool offline) + { + SessionId = sessionId; + InitData = initData; + Offline = offline; + Device = device; } + + public byte[] SessionId { get; set; } + public dynamic InitData { get; set; } + public bool Offline { get; set; } + public CDMDevice Device { get; set; } + public byte[] SessionKey { get; set; } + public DerivedKeys DerivedKeys { get; set; } + public byte[] LicenseRequest { get; set; } + public SignedLicense License { get; set; } + public SignedDeviceCertificate ServiceCertificate { get; set; } + public bool PrivacyMode { get; set; } + public List ContentKeys { get; set; } = new(); } diff --git a/OF DL/rules.json b/OF DL/rules.json index 9cb9785..d524235 100644 --- a/OF DL/rules.json +++ b/OF DL/rules.json @@ -4,5 +4,38 @@ "prefix": "30586", "suffix": "67000213", "checksum_constant": 521, - "checksum_indexes": [ 0, 2, 3, 7, 7, 8, 8, 10, 11, 13, 14, 16, 17, 17, 17, 19, 19, 20, 21, 21, 23, 23, 24, 24, 27, 27, 29, 30, 31, 34, 35, 39 ] + "checksum_indexes": [ + 0, + 2, + 3, + 7, + 7, + 8, + 8, + 10, + 11, + 13, + 14, + 16, + 17, + 17, + 17, + 19, + 19, + 20, + 21, + 21, + 23, + 23, + 24, + 24, + 27, + 27, + 29, + 30, + 31, + 34, + 35, + 39 + ] }