forked from sim0n00ps/OF-DL
Remove application logic from Program and continue to fix compiler warnings
This commit is contained in:
parent
17af1e8dfe
commit
4c680a40b5
127
OF DL/CLI/SpectreDownloadEventHandler.cs
Normal file
127
OF DL/CLI/SpectreDownloadEventHandler.cs
Normal file
@ -0,0 +1,127 @@
|
||||
using OF_DL.Models;
|
||||
using OF_DL.Services;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace OF_DL.CLI;
|
||||
|
||||
/// <summary>
|
||||
/// Spectre.Console implementation of IDownloadEventHandler.
|
||||
/// Handles all CLI-specific display logic for downloads.
|
||||
/// </summary>
|
||||
public class SpectreDownloadEventHandler : IDownloadEventHandler
|
||||
{
|
||||
public async Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work)
|
||||
{
|
||||
T result = default!;
|
||||
await AnsiConsole.Status()
|
||||
.StartAsync($"[red]{Markup.Escape(statusMessage)}[/]",
|
||||
async ctx =>
|
||||
{
|
||||
SpectreStatusReporter reporter = new(ctx);
|
||||
result = await work(reporter);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<T> WithProgressAsync<T>(string description, long maxValue, bool showSize,
|
||||
Func<IProgressReporter, Task<T>> work)
|
||||
{
|
||||
T result = default!;
|
||||
await AnsiConsole.Progress()
|
||||
.Columns(GetProgressColumns(showSize))
|
||||
.StartAsync(async ctx =>
|
||||
{
|
||||
ProgressTask task = ctx.AddTask($"[red]{Markup.Escape(description)}[/]", false);
|
||||
task.MaxValue = maxValue;
|
||||
task.StartTask();
|
||||
|
||||
SpectreProgressReporter progressReporter = new(task);
|
||||
result = await work(progressReporter);
|
||||
|
||||
task.StopTask();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public void OnContentFound(string contentType, int mediaCount, int objectCount)
|
||||
{
|
||||
AnsiConsole.Markup($"[red]Found {mediaCount} Media from {objectCount} {Markup.Escape(contentType)}\n[/]");
|
||||
}
|
||||
|
||||
public void OnNoContentFound(string contentType)
|
||||
{
|
||||
AnsiConsole.Markup($"[red]Found 0 {Markup.Escape(contentType)}\n[/]");
|
||||
}
|
||||
|
||||
public void OnDownloadComplete(string contentType, DownloadResult result)
|
||||
{
|
||||
AnsiConsole.Markup(
|
||||
$"[red]{Markup.Escape(contentType)} Already Downloaded: {result.ExistingDownloads} New {Markup.Escape(contentType)} Downloaded: {result.NewDownloads}[/]\n");
|
||||
}
|
||||
|
||||
public void OnUserStarting(string username)
|
||||
{
|
||||
AnsiConsole.Markup($"[red]\nScraping Data for {Markup.Escape(username)}\n[/]");
|
||||
}
|
||||
|
||||
public void OnUserComplete(string username, CreatorDownloadResult result)
|
||||
{
|
||||
AnsiConsole.Markup("\n");
|
||||
AnsiConsole.Write(new BreakdownChart()
|
||||
.FullSize()
|
||||
.AddItem("Paid Posts", result.PaidPostCount, Color.Red)
|
||||
.AddItem("Posts", result.PostCount, Color.Blue)
|
||||
.AddItem("Archived", result.ArchivedCount, Color.Green)
|
||||
.AddItem("Streams", result.StreamsCount, Color.Purple)
|
||||
.AddItem("Stories", result.StoriesCount, Color.Yellow)
|
||||
.AddItem("Highlights", result.HighlightsCount, Color.Orange1)
|
||||
.AddItem("Messages", result.MessagesCount, Color.LightGreen)
|
||||
.AddItem("Paid Messages", result.PaidMessagesCount, Color.Aqua));
|
||||
AnsiConsole.Markup("\n");
|
||||
}
|
||||
|
||||
public void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount)
|
||||
{
|
||||
AnsiConsole.Markup("\n");
|
||||
AnsiConsole.Write(new BreakdownChart()
|
||||
.FullSize()
|
||||
.AddItem("Paid Posts", paidPostCount, Color.Red)
|
||||
.AddItem("Paid Messages", paidMessagesCount, Color.Aqua));
|
||||
AnsiConsole.Markup("\n");
|
||||
}
|
||||
|
||||
public void OnScrapeComplete(TimeSpan elapsed)
|
||||
{
|
||||
AnsiConsole.Markup($"[green]Scrape Completed in {elapsed.TotalMinutes:0.00} minutes\n[/]");
|
||||
}
|
||||
|
||||
public void OnMessage(string message)
|
||||
{
|
||||
AnsiConsole.Markup($"[red]{Markup.Escape(message)}\n[/]");
|
||||
}
|
||||
|
||||
private static ProgressColumn[] GetProgressColumns(bool showScrapeSize)
|
||||
{
|
||||
List<ProgressColumn> progressColumns;
|
||||
if (showScrapeSize)
|
||||
{
|
||||
progressColumns =
|
||||
[
|
||||
new TaskDescriptionColumn(),
|
||||
new ProgressBarColumn(),
|
||||
new PercentageColumn(),
|
||||
new DownloadedColumn(),
|
||||
new RemainingTimeColumn()
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
progressColumns =
|
||||
[
|
||||
new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn()
|
||||
];
|
||||
}
|
||||
|
||||
return progressColumns.ToArray();
|
||||
}
|
||||
}
|
||||
@ -6,11 +6,9 @@ namespace OF_DL.CLI;
|
||||
/// <summary>
|
||||
/// Implementation of IProgressReporter that uses Spectre.Console's ProgressTask for CLI output.
|
||||
/// </summary>
|
||||
public class SpectreProgressReporter : IProgressReporter
|
||||
public class SpectreProgressReporter(ProgressTask task) : IProgressReporter
|
||||
{
|
||||
private readonly ProgressTask _task;
|
||||
|
||||
public SpectreProgressReporter(ProgressTask task) => _task = task ?? throw new ArgumentNullException(nameof(task));
|
||||
private readonly ProgressTask _task = task ?? throw new ArgumentNullException(nameof(task));
|
||||
|
||||
public void ReportProgress(long increment) => _task.Increment(increment);
|
||||
|
||||
|
||||
17
OF DL/CLI/SpectreStatusReporter.cs
Normal file
17
OF DL/CLI/SpectreStatusReporter.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using OF_DL.Services;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace OF_DL.CLI;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of IStatusReporter that uses Spectre.Console's StatusContext for CLI output.
|
||||
/// </summary>
|
||||
public class SpectreStatusReporter(StatusContext ctx) : IStatusReporter
|
||||
{
|
||||
public void ReportStatus(string message)
|
||||
{
|
||||
ctx.Status($"[red]{message}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
}
|
||||
}
|
||||
29
OF DL/Models/CreatorDownloadResult.cs
Normal file
29
OF DL/Models/CreatorDownloadResult.cs
Normal file
@ -0,0 +1,29 @@
|
||||
namespace OF_DL.Models;
|
||||
|
||||
public class CreatorDownloadResult
|
||||
{
|
||||
public int PaidPostCount { get; set; }
|
||||
|
||||
public int PostCount { get; set; }
|
||||
|
||||
public int ArchivedCount { get; set; }
|
||||
|
||||
public int StreamsCount { get; set; }
|
||||
|
||||
public int StoriesCount { get; set; }
|
||||
|
||||
public int HighlightsCount { get; set; }
|
||||
|
||||
public int MessagesCount { get; set; }
|
||||
|
||||
public int PaidMessagesCount { get; set; }
|
||||
}
|
||||
|
||||
public class UserListResult
|
||||
{
|
||||
public Dictionary<string, long> Users { get; set; } = new();
|
||||
|
||||
public Dictionary<string, long> Lists { get; set; } = new();
|
||||
|
||||
public string? IgnoredListError { get; set; }
|
||||
}
|
||||
39
OF DL/Models/StartupResult.cs
Normal file
39
OF DL/Models/StartupResult.cs
Normal file
@ -0,0 +1,39 @@
|
||||
namespace OF_DL.Models;
|
||||
|
||||
public class StartupResult
|
||||
{
|
||||
public bool IsWindowsVersionValid { get; set; } = true;
|
||||
|
||||
public string? OsVersionString { get; set; }
|
||||
|
||||
public bool FfmpegFound { get; set; }
|
||||
|
||||
public bool FfmpegPathAutoDetected { get; set; }
|
||||
|
||||
public string? FfmpegPath { get; set; }
|
||||
|
||||
public string? FfmpegVersion { get; set; }
|
||||
|
||||
public bool ClientIdBlobMissing { get; set; }
|
||||
|
||||
public bool DevicePrivateKeyMissing { get; set; }
|
||||
|
||||
public bool RulesJsonValid { get; set; }
|
||||
|
||||
public bool RulesJsonExists { get; set; }
|
||||
|
||||
public string? RulesJsonError { get; set; }
|
||||
}
|
||||
|
||||
public class VersionCheckResult
|
||||
{
|
||||
public Version? LocalVersion { get; set; }
|
||||
|
||||
public Version? LatestVersion { get; set; }
|
||||
|
||||
public bool IsUpToDate { get; set; }
|
||||
|
||||
public bool CheckFailed { get; set; }
|
||||
|
||||
public bool TimedOut { get; set; }
|
||||
}
|
||||
2137
OF DL/Program.cs
2137
OF DL/Program.cs
File diff suppressed because it is too large
Load Diff
@ -30,53 +30,52 @@ using UserEntities = OF_DL.Models.Entities.Users;
|
||||
using OF_DL.Models.Mappers;
|
||||
using OF_DL.Widevine;
|
||||
using Serilog;
|
||||
using Spectre.Console;
|
||||
using static OF_DL.Utils.HttpUtil;
|
||||
using Constants = OF_DL.Helpers.Constants;
|
||||
using SinglePostCollection = OF_DL.Models.Entities.Posts.SinglePostCollection;
|
||||
|
||||
namespace OF_DL.Services;
|
||||
|
||||
public class APIService(IAuthService authService, IConfigService configService, IDBService dbService)
|
||||
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 static readonly JsonSerializerSettings s_mJsonSerializerSettings;
|
||||
private static DateTime? s_cachedDynamicRulesExpiration;
|
||||
private static DynamicRules? s_cachedDynamicRules;
|
||||
|
||||
static APIService() =>
|
||||
m_JsonSerializerSettings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore };
|
||||
static ApiService() =>
|
||||
s_mJsonSerializerSettings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore };
|
||||
|
||||
|
||||
public Dictionary<string, string> GetDynamicHeaders(string path, string queryParams)
|
||||
{
|
||||
Log.Debug("Calling GetDynamicHeaders");
|
||||
Log.Debug($"Path: {path}");
|
||||
Log.Debug($"Query Params: {queryParams}");
|
||||
Log.Debug("Path: {Path}", path);
|
||||
Log.Debug("Query Params: {QueryParams}", queryParams);
|
||||
|
||||
DynamicRules? root;
|
||||
|
||||
//Check if we have a cached version of the dynamic rules
|
||||
if (cachedDynamicRules != null && cachedDynamicRulesExpiration.HasValue &&
|
||||
DateTime.UtcNow < cachedDynamicRulesExpiration)
|
||||
if (s_cachedDynamicRules != null && s_cachedDynamicRulesExpiration.HasValue &&
|
||||
DateTime.UtcNow < s_cachedDynamicRulesExpiration)
|
||||
{
|
||||
Log.Debug("Using cached dynamic rules");
|
||||
root = cachedDynamicRules;
|
||||
root = s_cachedDynamicRules;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Get rules from GitHub and fallback to local file
|
||||
string? dynamicRulesJSON = GetDynamicRules();
|
||||
if (!string.IsNullOrEmpty(dynamicRulesJSON))
|
||||
string? dynamicRulesJson = GetDynamicRules();
|
||||
if (!string.IsNullOrEmpty(dynamicRulesJson))
|
||||
{
|
||||
Log.Debug("Using dynamic rules from GitHub");
|
||||
root = JsonConvert.DeserializeObject<DynamicRules>(dynamicRulesJSON);
|
||||
root = JsonConvert.DeserializeObject<DynamicRules>(dynamicRulesJson);
|
||||
|
||||
// Cache the GitHub response for 15 minutes
|
||||
cachedDynamicRules = root;
|
||||
cachedDynamicRulesExpiration = DateTime.UtcNow.AddMinutes(15);
|
||||
s_cachedDynamicRules = root;
|
||||
s_cachedDynamicRulesExpiration = DateTime.UtcNow.AddMinutes(15);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -87,33 +86,55 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
// operations and frequent call to GitHub. Since the GitHub dynamic rules
|
||||
// are preferred to the local file, the cache time is shorter than when dynamic rules
|
||||
// are successfully retrieved from GitHub.
|
||||
cachedDynamicRules = root;
|
||||
cachedDynamicRulesExpiration = DateTime.UtcNow.AddMinutes(5);
|
||||
s_cachedDynamicRules = root;
|
||||
s_cachedDynamicRulesExpiration = DateTime.UtcNow.AddMinutes(5);
|
||||
}
|
||||
}
|
||||
|
||||
if (root == null)
|
||||
{
|
||||
throw new Exception("Unable to parse dynamic rules. Root is null");
|
||||
}
|
||||
|
||||
if (root.ChecksumConstant == null || root.ChecksumIndexes.Count == 0 || root.Prefix == null ||
|
||||
root.Suffix == null || root.AppToken == null)
|
||||
{
|
||||
throw new Exception("Invalid dynamic rules. Missing required fields");
|
||||
}
|
||||
|
||||
if (authService.CurrentAuth == null)
|
||||
{
|
||||
throw new Exception("Auth service is null");
|
||||
}
|
||||
|
||||
if (authService.CurrentAuth.UserId == null || authService.CurrentAuth.Cookie == null ||
|
||||
authService.CurrentAuth.UserAgent == null || authService.CurrentAuth.XBc == null)
|
||||
{
|
||||
throw new Exception("Auth service is missing required fields");
|
||||
}
|
||||
|
||||
DateTimeOffset dto = DateTime.UtcNow;
|
||||
long timestamp = dto.ToUnixTimeMilliseconds();
|
||||
|
||||
string input = $"{root!.StaticParam}\n{timestamp}\n{path + queryParams}\n{authService.CurrentAuth.UserId}";
|
||||
string input = $"{root.StaticParam}\n{timestamp}\n{path + queryParams}\n{authService.CurrentAuth.UserId}";
|
||||
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
|
||||
byte[] hashBytes = SHA1.HashData(inputBytes);
|
||||
string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
|
||||
|
||||
int checksum = root.ChecksumIndexes.Aggregate(0, (current, number) => current + hashString[number]) +
|
||||
root.ChecksumConstant!.Value;
|
||||
root.ChecksumConstant.Value;
|
||||
string sign = $"{root.Prefix}:{hashString}:{checksum.ToString("X").ToLower()}:{root.Suffix}";
|
||||
|
||||
Dictionary<string, string> headers = new()
|
||||
{
|
||||
{ "accept", "application/json, text/plain" },
|
||||
{ "app-token", root.AppToken! },
|
||||
{ "cookie", authService.CurrentAuth!.Cookie! },
|
||||
{ "app-token", root.AppToken },
|
||||
{ "cookie", authService.CurrentAuth.Cookie },
|
||||
{ "sign", sign },
|
||||
{ "time", timestamp.ToString() },
|
||||
{ "user-id", authService.CurrentAuth!.UserId! },
|
||||
{ "user-agent", authService.CurrentAuth!.UserAgent! },
|
||||
{ "x-bc", authService.CurrentAuth!.XBc! }
|
||||
{ "user-id", authService.CurrentAuth.UserId },
|
||||
{ "user-agent", authService.CurrentAuth.UserAgent },
|
||||
{ "x-bc", authService.CurrentAuth.XBc }
|
||||
};
|
||||
return headers;
|
||||
}
|
||||
@ -126,10 +147,10 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
try
|
||||
{
|
||||
UserEntities.User user = new();
|
||||
int post_limit = 50;
|
||||
const int postLimit = 50;
|
||||
Dictionary<string, string> getParams = new()
|
||||
{
|
||||
{ "limit", post_limit.ToString() }, { "order", "publish_date_asc" }
|
||||
{ "limit", postLimit.ToString() }, { "order", "publish_date_asc" }
|
||||
};
|
||||
|
||||
HttpClient client = new();
|
||||
@ -145,7 +166,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
response.EnsureSuccessStatusCode();
|
||||
string body = await response.Content.ReadAsStringAsync();
|
||||
UserDtos.UserDto? userDto =
|
||||
JsonConvert.DeserializeObject<UserDtos.UserDto>(body, m_JsonSerializerSettings);
|
||||
JsonConvert.DeserializeObject<UserDtos.UserDto>(body, s_mJsonSerializerSettings);
|
||||
user = UserMapper.FromDto(userDto) ?? new UserEntities.User();
|
||||
return user;
|
||||
}
|
||||
@ -369,7 +390,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
try
|
||||
{
|
||||
Dictionary<long, string> return_urls = new();
|
||||
int post_limit = 50;
|
||||
const int postLimit = 50;
|
||||
int limit = 5;
|
||||
int offset = 0;
|
||||
|
||||
@ -380,9 +401,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
case MediaType.Stories:
|
||||
getParams = new Dictionary<string, string>
|
||||
{
|
||||
{ "limit", post_limit.ToString() },
|
||||
{ "order", "publish_date_desc" },
|
||||
{ "skip_users", "all" }
|
||||
{ "limit", postLimit.ToString() }, { "order", "publish_date_desc" }, { "skip_users", "all" }
|
||||
};
|
||||
break;
|
||||
|
||||
@ -402,7 +421,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
Log.Debug("Media Stories - " + endpoint);
|
||||
|
||||
List<StoriesDtos.StoryDto>? storiesDto =
|
||||
JsonConvert.DeserializeObject<List<StoriesDtos.StoryDto>>(body, m_JsonSerializerSettings);
|
||||
JsonConvert.DeserializeObject<List<StoriesDtos.StoryDto>>(body, s_mJsonSerializerSettings);
|
||||
List<StoryEntities.Stories> stories = StoriesMapper.FromDto(storiesDto);
|
||||
|
||||
foreach (StoryEntities.Stories story in stories)
|
||||
@ -422,15 +441,27 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
await dbService.AddStory(folder, story.Id, "", "0", false, false, DateTime.Now);
|
||||
}
|
||||
|
||||
if (story.Media != null && story.Media.Count > 0)
|
||||
if (story.Media.Count > 0)
|
||||
{
|
||||
foreach (StoryEntities.Medium medium in story.Media)
|
||||
{
|
||||
if (medium.Files.Full == null || medium.Files.Full.Url == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string? mediaType = medium.Type == "photo" ? "Images" :
|
||||
medium.Type == "video" || medium.Type == "gif" ? "Videos" :
|
||||
medium.Type == "audio" ? "Audios" : null;
|
||||
if (mediaType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
mediaType, false, false, null);
|
||||
|
||||
if (medium.Type == "photo" && !configService.CurrentConfig.DownloadImages)
|
||||
{
|
||||
continue;
|
||||
@ -464,9 +495,9 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
}
|
||||
else if (mediatype == MediaType.Highlights)
|
||||
{
|
||||
List<string> highlight_ids = new();
|
||||
List<string> highlightIds = [];
|
||||
HighlightDtos.HighlightsDto? highlightsDto =
|
||||
JsonConvert.DeserializeObject<HighlightDtos.HighlightsDto>(body, m_JsonSerializerSettings);
|
||||
JsonConvert.DeserializeObject<HighlightDtos.HighlightsDto>(body, s_mJsonSerializerSettings);
|
||||
HighlightEntities.Highlights highlights = HighlightsMapper.FromDto(highlightsDto);
|
||||
|
||||
if (highlights.HasMore)
|
||||
@ -480,7 +511,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
HighlightDtos.HighlightsDto? newHighlightsDto =
|
||||
JsonConvert.DeserializeObject<HighlightDtos.HighlightsDto>(loopbody,
|
||||
m_JsonSerializerSettings);
|
||||
s_mJsonSerializerSettings);
|
||||
HighlightEntities.Highlights newHighlights = HighlightsMapper.FromDto(newHighlightsDto);
|
||||
|
||||
highlights.List.AddRange(newHighlights.List);
|
||||
@ -496,13 +527,13 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
foreach (HighlightEntities.ListItem list in highlights.List)
|
||||
{
|
||||
if (!highlight_ids.Contains(list.Id.ToString()))
|
||||
if (!highlightIds.Contains(list.Id.ToString()))
|
||||
{
|
||||
highlight_ids.Add(list.Id.ToString());
|
||||
highlightIds.Add(list.Id.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string highlight_id in highlight_ids)
|
||||
foreach (string highlight_id in highlightIds)
|
||||
{
|
||||
Dictionary<string, string> highlight_headers =
|
||||
GetDynamicHeaders("/api2/v2/stories/highlights/" + highlight_id, "");
|
||||
@ -522,15 +553,20 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
string highlightBody = await highlightResponse.Content.ReadAsStringAsync();
|
||||
HighlightDtos.HighlightMediaDto? highlightMediaDto =
|
||||
JsonConvert.DeserializeObject<HighlightDtos.HighlightMediaDto>(highlightBody,
|
||||
m_JsonSerializerSettings);
|
||||
s_mJsonSerializerSettings);
|
||||
HighlightEntities.HighlightMedia highlightMedia = HighlightsMapper.FromDto(highlightMediaDto);
|
||||
|
||||
foreach (HighlightEntities.Story item in highlightMedia.Stories)
|
||||
{
|
||||
if (item.Media != null && item.Media.Count > 0 && item.Media[0].CreatedAt.HasValue)
|
||||
if (item.Media != null && item.Media.Count > 0 && item.Media[0].CreatedAt.HasValue &&
|
||||
item.Media[0].CreatedAt != null)
|
||||
{
|
||||
DateTime? createdAt = item.Media[0].CreatedAt;
|
||||
if (createdAt != null)
|
||||
{
|
||||
await dbService.AddStory(folder, item.Id, "", "0", false, false,
|
||||
item.Media[0].CreatedAt.Value);
|
||||
createdAt.Value);
|
||||
}
|
||||
}
|
||||
else if (item.CreatedAt.HasValue)
|
||||
{
|
||||
@ -605,18 +641,17 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
public async Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder,
|
||||
string username,
|
||||
List<long> paid_post_ids, StatusContext ctx)
|
||||
List<long> paid_post_ids, IStatusReporter statusReporter)
|
||||
{
|
||||
Log.Debug($"Calling GetPaidPosts - {username}");
|
||||
|
||||
try
|
||||
{
|
||||
PurchasedEntities.Purchased paidPosts = new();
|
||||
PurchasedEntities.PaidPostCollection paidPostCollection = new();
|
||||
int post_limit = 50;
|
||||
const int postLimit = 50;
|
||||
Dictionary<string, string> getParams = new()
|
||||
{
|
||||
{ "limit", post_limit.ToString() },
|
||||
{ "limit", postLimit.ToString() },
|
||||
{ "skip_users", "all" },
|
||||
{ "order", "publish_date_desc" },
|
||||
{ "format", "infinite" },
|
||||
@ -625,11 +660,9 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
PurchasedDtos.PurchasedDto? paidPostsDto =
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(body, m_JsonSerializerSettings);
|
||||
paidPosts = PurchasedMapper.FromDto(paidPostsDto);
|
||||
ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.List.Count}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(body, s_mJsonSerializerSettings);
|
||||
PurchasedEntities.Purchased paidPosts = PurchasedMapper.FromDto(paidPostsDto);
|
||||
statusReporter.ReportStatus($"Getting Paid Posts - Found {paidPosts.List.Count}");
|
||||
if (paidPosts != null && paidPosts.HasMore)
|
||||
{
|
||||
getParams["offset"] = paidPosts.List.Count.ToString();
|
||||
@ -639,19 +672,17 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
PurchasedDtos.PurchasedDto? newPaidPostsDto =
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(loopbody, m_JsonSerializerSettings);
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(loopbody, s_mJsonSerializerSettings);
|
||||
newPaidPosts = PurchasedMapper.FromDto(newPaidPostsDto);
|
||||
|
||||
paidPosts.List.AddRange(newPaidPosts.List);
|
||||
ctx.Status($"[red]Getting Paid Posts\n[/] [red]Found {paidPosts.List.Count}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
statusReporter.ReportStatus($"Getting Paid Posts - Found {paidPosts.List.Count}");
|
||||
if (!newPaidPosts.HasMore)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit);
|
||||
getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit);
|
||||
}
|
||||
}
|
||||
|
||||
@ -812,18 +843,17 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
|
||||
public async Task<PostEntities.PostCollection> GetPosts(string endpoint, string folder, List<long> paid_post_ids,
|
||||
StatusContext ctx)
|
||||
IStatusReporter statusReporter)
|
||||
{
|
||||
Log.Debug($"Calling GetPosts - {endpoint}");
|
||||
|
||||
try
|
||||
{
|
||||
PostEntities.Post posts = new();
|
||||
PostEntities.PostCollection postCollection = new();
|
||||
int post_limit = 50;
|
||||
const int postLimit = 50;
|
||||
Dictionary<string, string> getParams = new()
|
||||
{
|
||||
{ "limit", post_limit.ToString() },
|
||||
{ "limit", postLimit.ToString() },
|
||||
{ "order", "publish_date_desc" },
|
||||
{ "format", "infinite" },
|
||||
{ "skip_users", "all" }
|
||||
@ -855,12 +885,9 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
|
||||
PostDtos.PostDto? postsDto =
|
||||
JsonConvert.DeserializeObject<PostDtos.PostDto>(body, m_JsonSerializerSettings);
|
||||
posts = PostMapper.FromDto(postsDto);
|
||||
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"));
|
||||
JsonConvert.DeserializeObject<PostDtos.PostDto>(body, s_mJsonSerializerSettings);
|
||||
PostEntities.Post posts = PostMapper.FromDto(postsDto);
|
||||
statusReporter.ReportStatus($"Getting Posts - Found {posts.List.Count}");
|
||||
if (posts != null && posts.HasMore)
|
||||
{
|
||||
UpdateGetParamsForDateSelection(
|
||||
@ -874,14 +901,11 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
PostDtos.PostDto? newPostsDto =
|
||||
JsonConvert.DeserializeObject<PostDtos.PostDto>(loopbody, m_JsonSerializerSettings);
|
||||
JsonConvert.DeserializeObject<PostDtos.PostDto>(loopbody, s_mJsonSerializerSettings);
|
||||
newposts = PostMapper.FromDto(newPostsDto);
|
||||
|
||||
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.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
statusReporter.ReportStatus($"Getting Posts - Found {posts.List.Count}");
|
||||
if (!newposts.HasMore)
|
||||
{
|
||||
break;
|
||||
@ -1046,7 +1070,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
|
||||
PostDtos.SinglePostDto? singlePostDto =
|
||||
JsonConvert.DeserializeObject<PostDtos.SinglePostDto>(body, m_JsonSerializerSettings);
|
||||
JsonConvert.DeserializeObject<PostDtos.SinglePostDto>(body, s_mJsonSerializerSettings);
|
||||
singlePost = PostMapper.FromDto(singlePostDto);
|
||||
|
||||
if (singlePostDto != null)
|
||||
@ -1217,18 +1241,17 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
public async Task<StreamEntities.StreamsCollection> GetStreams(string endpoint, string folder,
|
||||
List<long> paid_post_ids,
|
||||
StatusContext ctx)
|
||||
IStatusReporter statusReporter)
|
||||
{
|
||||
Log.Debug($"Calling GetStreams - {endpoint}");
|
||||
|
||||
try
|
||||
{
|
||||
StreamEntities.Streams streams = new();
|
||||
StreamEntities.StreamsCollection streamsCollection = new();
|
||||
int post_limit = 50;
|
||||
const int postLimit = 50;
|
||||
Dictionary<string, string> getParams = new()
|
||||
{
|
||||
{ "limit", post_limit.ToString() },
|
||||
{ "limit", postLimit.ToString() },
|
||||
{ "order", "publish_date_desc" },
|
||||
{ "format", "infinite" },
|
||||
{ "skip_users", "all" }
|
||||
@ -1248,11 +1271,9 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
|
||||
StreamsDtos.StreamsDto? streamsDto =
|
||||
JsonConvert.DeserializeObject<StreamsDtos.StreamsDto>(body, m_JsonSerializerSettings);
|
||||
streams = StreamsMapper.FromDto(streamsDto);
|
||||
ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.List.Count}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
JsonConvert.DeserializeObject<StreamsDtos.StreamsDto>(body, s_mJsonSerializerSettings);
|
||||
StreamEntities.Streams streams = StreamsMapper.FromDto(streamsDto);
|
||||
statusReporter.ReportStatus($"Getting Streams - Found {streams.List.Count}");
|
||||
if (streams != null && streams.HasMore)
|
||||
{
|
||||
UpdateGetParamsForDateSelection(
|
||||
@ -1266,13 +1287,11 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
StreamsDtos.StreamsDto? newStreamsDto =
|
||||
JsonConvert.DeserializeObject<StreamsDtos.StreamsDto>(loopbody, m_JsonSerializerSettings);
|
||||
JsonConvert.DeserializeObject<StreamsDtos.StreamsDto>(loopbody, s_mJsonSerializerSettings);
|
||||
newstreams = StreamsMapper.FromDto(newStreamsDto);
|
||||
|
||||
streams.List.AddRange(newstreams.List);
|
||||
ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.List.Count}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
statusReporter.ReportStatus($"Getting Streams - Found {streams.List.Count}");
|
||||
if (!newstreams.HasMore)
|
||||
{
|
||||
break;
|
||||
@ -1393,18 +1412,17 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
|
||||
public async Task<ArchivedEntities.ArchivedCollection> GetArchived(string endpoint, string folder,
|
||||
StatusContext ctx)
|
||||
IStatusReporter statusReporter)
|
||||
{
|
||||
Log.Debug($"Calling GetArchived - {endpoint}");
|
||||
|
||||
try
|
||||
{
|
||||
ArchivedEntities.Archived archived = new();
|
||||
ArchivedEntities.ArchivedCollection archivedCollection = new();
|
||||
int post_limit = 50;
|
||||
const int postLimit = 50;
|
||||
Dictionary<string, string> getParams = new()
|
||||
{
|
||||
{ "limit", post_limit.ToString() },
|
||||
{ "limit", postLimit.ToString() },
|
||||
{ "order", "publish_date_desc" },
|
||||
{ "skip_users", "all" },
|
||||
{ "format", "infinite" },
|
||||
@ -1425,13 +1443,16 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
configService.CurrentConfig.CustomDate);
|
||||
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
ArchivedDtos.ArchivedDto archivedDto =
|
||||
JsonConvert.DeserializeObject<ArchivedDtos.ArchivedDto>(body, m_JsonSerializerSettings);
|
||||
archived = ArchivedMapper.FromDto(archivedDto);
|
||||
ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.List.Count}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
if (archived != null && archived.HasMore)
|
||||
if (body == null)
|
||||
{
|
||||
throw new Exception("Failed to retrieve archived posts. Received null response.");
|
||||
}
|
||||
|
||||
ArchivedDtos.ArchivedDto? archivedDto =
|
||||
JsonConvert.DeserializeObject<ArchivedDtos.ArchivedDto>(body, s_mJsonSerializerSettings);
|
||||
ArchivedEntities.Archived archived = ArchivedMapper.FromDto(archivedDto);
|
||||
statusReporter.ReportStatus($"Getting Archived Posts - Found {archived.List.Count}");
|
||||
if (archived.HasMore)
|
||||
{
|
||||
UpdateGetParamsForDateSelection(
|
||||
downloadDateSelection,
|
||||
@ -1439,17 +1460,18 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
archived.TailMarker);
|
||||
while (true)
|
||||
{
|
||||
ArchivedEntities.Archived newarchived = new();
|
||||
|
||||
string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
ArchivedDtos.ArchivedDto newarchivedDto =
|
||||
JsonConvert.DeserializeObject<ArchivedDtos.ArchivedDto>(loopbody, m_JsonSerializerSettings);
|
||||
newarchived = ArchivedMapper.FromDto(newarchivedDto);
|
||||
if (loopbody == null)
|
||||
{
|
||||
throw new Exception("Failed to retrieve archived posts. Received null response.");
|
||||
}
|
||||
|
||||
ArchivedDtos.ArchivedDto? newarchivedDto =
|
||||
JsonConvert.DeserializeObject<ArchivedDtos.ArchivedDto>(loopbody, s_mJsonSerializerSettings);
|
||||
ArchivedEntities.Archived newarchived = ArchivedMapper.FromDto(newarchivedDto);
|
||||
|
||||
archived.List.AddRange(newarchived.List);
|
||||
ctx.Status($"[red]Getting Archived Posts\n[/] [red]Found {archived.List.Count}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
statusReporter.ReportStatus($"Getting Archived Posts - Found {archived.List.Count}");
|
||||
if (!newarchived.HasMore)
|
||||
{
|
||||
break;
|
||||
@ -1561,27 +1583,25 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
}
|
||||
|
||||
|
||||
public async Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder, StatusContext ctx)
|
||||
public async Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder,
|
||||
IStatusReporter statusReporter)
|
||||
{
|
||||
Log.Debug($"Calling GetMessages - {endpoint}");
|
||||
|
||||
try
|
||||
{
|
||||
MessageEntities.Messages messages = new();
|
||||
MessageEntities.MessageCollection messageCollection = new();
|
||||
int post_limit = 50;
|
||||
const int postLimit = 50;
|
||||
Dictionary<string, string> getParams = new()
|
||||
{
|
||||
{ "limit", post_limit.ToString() }, { "order", "desc" }, { "skip_users", "all" }
|
||||
{ "limit", postLimit.ToString() }, { "order", "desc" }, { "skip_users", "all" }
|
||||
};
|
||||
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
MessageDtos.MessagesDto? messagesDto =
|
||||
JsonConvert.DeserializeObject<MessageDtos.MessagesDto>(body, m_JsonSerializerSettings);
|
||||
messages = MessagesMapper.FromDto(messagesDto);
|
||||
ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.List.Count}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
JsonConvert.DeserializeObject<MessageDtos.MessagesDto>(body, s_mJsonSerializerSettings);
|
||||
MessageEntities.Messages messages = MessagesMapper.FromDto(messagesDto);
|
||||
statusReporter.ReportStatus($"Getting Messages - Found {messages.List.Count}");
|
||||
if (messages.HasMore)
|
||||
{
|
||||
getParams["id"] = messages.List[^1].Id.ToString();
|
||||
@ -1591,13 +1611,11 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
MessageDtos.MessagesDto? newMessagesDto =
|
||||
JsonConvert.DeserializeObject<MessageDtos.MessagesDto>(loopbody, m_JsonSerializerSettings);
|
||||
JsonConvert.DeserializeObject<MessageDtos.MessagesDto>(loopbody, s_mJsonSerializerSettings);
|
||||
newMessages = MessagesMapper.FromDto(newMessagesDto);
|
||||
|
||||
messages.List.AddRange(newMessages.List);
|
||||
ctx.Status($"[red]Getting Messages\n[/] [red]Found {messages.List.Count}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
statusReporter.ReportStatus($"Getting Messages - Found {messages.List.Count}");
|
||||
if (!newMessages.HasMore)
|
||||
{
|
||||
break;
|
||||
@ -1823,15 +1841,14 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
try
|
||||
{
|
||||
MessageEntities.SingleMessage message = new();
|
||||
PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection = new();
|
||||
int post_limit = 50;
|
||||
Dictionary<string, string> getParams = new() { { "limit", post_limit.ToString() }, { "order", "desc" } };
|
||||
const int postLimit = 50;
|
||||
Dictionary<string, string> getParams = new() { { "limit", postLimit.ToString() }, { "order", "desc" } };
|
||||
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
MessageDtos.SingleMessageDto? messageDto =
|
||||
JsonConvert.DeserializeObject<MessageDtos.SingleMessageDto>(body, m_JsonSerializerSettings);
|
||||
message = MessagesMapper.FromDto(messageDto);
|
||||
JsonConvert.DeserializeObject<MessageDtos.SingleMessageDto>(body, s_mJsonSerializerSettings);
|
||||
MessageEntities.SingleMessage message = MessagesMapper.FromDto(messageDto);
|
||||
|
||||
if (!configService.CurrentConfig.IgnoreOwnMessages ||
|
||||
message.FromUser?.Id != Convert.ToInt32(authService.CurrentAuth.UserId))
|
||||
@ -2028,18 +2045,17 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
public async Task<PurchasedEntities.PaidMessageCollection> GetPaidMessages(string endpoint, string folder,
|
||||
string username,
|
||||
StatusContext ctx)
|
||||
IStatusReporter statusReporter)
|
||||
{
|
||||
Log.Debug($"Calling GetPaidMessages - {username}");
|
||||
|
||||
try
|
||||
{
|
||||
PurchasedEntities.Purchased paidMessages = new();
|
||||
PurchasedEntities.PaidMessageCollection paidMessageCollection = new();
|
||||
int post_limit = 50;
|
||||
const int postLimit = 50;
|
||||
Dictionary<string, string> getParams = new()
|
||||
{
|
||||
{ "limit", post_limit.ToString() },
|
||||
{ "limit", postLimit.ToString() },
|
||||
{ "order", "publish_date_desc" },
|
||||
{ "format", "infinite" },
|
||||
{ "author", username },
|
||||
@ -2048,11 +2064,9 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
PurchasedDtos.PurchasedDto? paidMessagesDto =
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(body, m_JsonSerializerSettings);
|
||||
paidMessages = PurchasedMapper.FromDto(paidMessagesDto);
|
||||
ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.List.Count}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(body, s_mJsonSerializerSettings);
|
||||
PurchasedEntities.Purchased paidMessages = PurchasedMapper.FromDto(paidMessagesDto);
|
||||
statusReporter.ReportStatus($"Getting Paid Messages - Found {paidMessages.List.Count}");
|
||||
if (paidMessages != null && paidMessages.HasMore)
|
||||
{
|
||||
getParams["offset"] = paidMessages.List.Count.ToString();
|
||||
@ -2077,20 +2091,18 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
string loopbody = await loopresponse.Content.ReadAsStringAsync();
|
||||
PurchasedDtos.PurchasedDto? newPaidMessagesDto =
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(loopbody,
|
||||
m_JsonSerializerSettings);
|
||||
s_mJsonSerializerSettings);
|
||||
newpaidMessages = PurchasedMapper.FromDto(newPaidMessagesDto);
|
||||
}
|
||||
|
||||
paidMessages.List.AddRange(newpaidMessages.List);
|
||||
ctx.Status($"[red]Getting Paid Messages\n[/] [red]Found {paidMessages.List.Count}[/]");
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
ctx.SpinnerStyle(Style.Parse("blue"));
|
||||
statusReporter.ReportStatus($"Getting Paid Messages - Found {paidMessages.List.Count}");
|
||||
if (!newpaidMessages.HasMore)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit);
|
||||
getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2330,27 +2342,31 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
try
|
||||
{
|
||||
Dictionary<string, long> purchasedTabUsers = new();
|
||||
PurchasedEntities.Purchased purchased = new();
|
||||
int post_limit = 50;
|
||||
const int postLimit = 50;
|
||||
Dictionary<string, string> getParams = new()
|
||||
{
|
||||
{ "limit", post_limit.ToString() },
|
||||
{ "limit", postLimit.ToString() },
|
||||
{ "order", "publish_date_desc" },
|
||||
{ "format", "infinite" },
|
||||
{ "skip_users", "all" }
|
||||
};
|
||||
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
if (body == null)
|
||||
{
|
||||
throw new Exception("Failed to get purchased tab users. null body returned.");
|
||||
}
|
||||
|
||||
PurchasedDtos.PurchasedDto? purchasedDto =
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(body, m_JsonSerializerSettings);
|
||||
purchased = PurchasedMapper.FromDto(purchasedDto);
|
||||
if (purchased != null && purchased.HasMore)
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(body, s_mJsonSerializerSettings);
|
||||
PurchasedEntities.Purchased purchased = PurchasedMapper.FromDto(purchasedDto);
|
||||
if (purchased.HasMore)
|
||||
{
|
||||
getParams["offset"] = purchased.List.Count.ToString();
|
||||
while (true)
|
||||
{
|
||||
string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
||||
PurchasedEntities.Purchased newPurchased = new();
|
||||
PurchasedEntities.Purchased newPurchased;
|
||||
Dictionary<string, string> loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams);
|
||||
HttpClient loopclient = GetHttpClient();
|
||||
|
||||
@ -2368,7 +2384,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
string loopbody = await loopresponse.Content.ReadAsStringAsync();
|
||||
PurchasedDtos.PurchasedDto? newPurchasedDto =
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(loopbody,
|
||||
m_JsonSerializerSettings);
|
||||
s_mJsonSerializerSettings);
|
||||
newPurchased = PurchasedMapper.FromDto(newPurchasedDto);
|
||||
}
|
||||
|
||||
@ -2378,16 +2394,17 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
break;
|
||||
}
|
||||
|
||||
getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit);
|
||||
getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit);
|
||||
}
|
||||
}
|
||||
|
||||
if (purchased.List != null && purchased.List.Count > 0)
|
||||
if (purchased.List.Count > 0)
|
||||
{
|
||||
foreach (PurchasedEntities.ListItem purchase in
|
||||
purchased.List.OrderByDescending(p => p.PostedAt ?? p.CreatedAt))
|
||||
{
|
||||
if (purchase.FromUser != null)
|
||||
// purchase.FromUser.Id is not nullable, so the default value is 0
|
||||
if (purchase.FromUser.Id != 0)
|
||||
{
|
||||
if (users.Values.Contains(purchase.FromUser.Id))
|
||||
{
|
||||
@ -2412,9 +2429,9 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
}
|
||||
else
|
||||
{
|
||||
JObject user = await GetUserInfoById($"/users/list?x[]={purchase.FromUser.Id}");
|
||||
JObject? user = await GetUserInfoById($"/users/list?x[]={purchase.FromUser.Id}");
|
||||
|
||||
if (user is null)
|
||||
if (user == null)
|
||||
{
|
||||
if (!configService.CurrentConfig.BypassContentForCreatorsWhoNoLongerExist)
|
||||
{
|
||||
@ -2447,7 +2464,8 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (purchase.Author != null)
|
||||
// purchase.Author is not nullable, so we check against the Author's Id (default value 0)
|
||||
else if (purchase.Author.Id != 0)
|
||||
{
|
||||
if (users.Values.Contains(purchase.Author.Id))
|
||||
{
|
||||
@ -2471,7 +2489,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
}
|
||||
else
|
||||
{
|
||||
JObject user = await GetUserInfoById($"/users/list?x[]={purchase.Author.Id}");
|
||||
JObject? user = await GetUserInfoById($"/users/list?x[]={purchase.Author.Id}");
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
@ -2536,11 +2554,10 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
{
|
||||
Dictionary<long, List<PurchasedEntities.ListItem>> userPurchases = new();
|
||||
List<PurchasedEntities.PurchasedTabCollection> purchasedTabCollections = [];
|
||||
PurchasedEntities.Purchased purchased = new();
|
||||
int post_limit = 50;
|
||||
const int postLimit = 50;
|
||||
Dictionary<string, string> getParams = new()
|
||||
{
|
||||
{ "limit", post_limit.ToString() },
|
||||
{ "limit", postLimit.ToString() },
|
||||
{ "order", "publish_date_desc" },
|
||||
{ "format", "infinite" },
|
||||
{ "skip_users", "all" }
|
||||
@ -2548,9 +2565,9 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||
PurchasedDtos.PurchasedDto? purchasedDto =
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(body, m_JsonSerializerSettings);
|
||||
purchased = PurchasedMapper.FromDto(purchasedDto);
|
||||
if (purchased != null && purchased.HasMore)
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(body, s_mJsonSerializerSettings);
|
||||
PurchasedEntities.Purchased purchased = PurchasedMapper.FromDto(purchasedDto);
|
||||
if (purchased.HasMore)
|
||||
{
|
||||
getParams["offset"] = purchased.List.Count.ToString();
|
||||
while (true)
|
||||
@ -2574,7 +2591,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
string loopbody = await loopresponse.Content.ReadAsStringAsync();
|
||||
PurchasedDtos.PurchasedDto? newPurchasedDto =
|
||||
JsonConvert.DeserializeObject<PurchasedDtos.PurchasedDto>(loopbody,
|
||||
m_JsonSerializerSettings);
|
||||
s_mJsonSerializerSettings);
|
||||
newPurchased = PurchasedMapper.FromDto(newPurchasedDto);
|
||||
}
|
||||
|
||||
@ -2584,7 +2601,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
break;
|
||||
}
|
||||
|
||||
getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + post_limit);
|
||||
getParams["offset"] = Convert.ToString(Convert.ToInt32(getParams["offset"]) + postLimit);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2617,7 +2634,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
foreach (KeyValuePair<long, List<PurchasedEntities.ListItem>> user in userPurchases)
|
||||
{
|
||||
PurchasedEntities.PurchasedTabCollection purchasedTabCollection = new();
|
||||
JObject userObject = await GetUserInfoById($"/users/list?x[]={user.Key}");
|
||||
JObject? userObject = await GetUserInfoById($"/users/list?x[]={user.Key}");
|
||||
purchasedTabCollection.UserId = user.Key;
|
||||
purchasedTabCollection.Username =
|
||||
userObject is not null &&
|
||||
@ -3030,7 +3047,12 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
{
|
||||
try
|
||||
{
|
||||
string pssh = null;
|
||||
if (authService.CurrentAuth == null)
|
||||
{
|
||||
throw new Exception("No current authentication available");
|
||||
}
|
||||
|
||||
string? pssh;
|
||||
|
||||
HttpClient client = new();
|
||||
HttpRequestMessage request = new(HttpMethod.Get, mpdUrl);
|
||||
@ -3115,92 +3137,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
return DateTime.Now;
|
||||
}
|
||||
|
||||
public async Task<string> GetDecryptionKeyCDRMProject(Dictionary<string, string> drmHeaders, string licenceURL,
|
||||
string pssh)
|
||||
{
|
||||
Log.Debug("Calling GetDecryptionKey");
|
||||
|
||||
int attempt = 0;
|
||||
|
||||
try
|
||||
{
|
||||
string dcValue = "";
|
||||
HttpClient client = new();
|
||||
|
||||
CDRMProjectRequest cdrmProjectRequest = new()
|
||||
{
|
||||
Pssh = pssh,
|
||||
LicenseUrl = licenceURL,
|
||||
Headers = JsonConvert.SerializeObject(drmHeaders),
|
||||
Cookies = "",
|
||||
Data = ""
|
||||
};
|
||||
|
||||
string json = JsonConvert.SerializeObject(cdrmProjectRequest);
|
||||
|
||||
Log.Debug($"Posting to CDRM Project: {json}");
|
||||
|
||||
while (attempt < MaxAttempts)
|
||||
{
|
||||
attempt++;
|
||||
|
||||
HttpRequestMessage request = new(HttpMethod.Post, "https://cdrm-project.com/api/decrypt")
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
using HttpResponseMessage response = await client.SendAsync(request);
|
||||
|
||||
Log.Debug($"CDRM Project Response (Attempt {attempt}): {response.Content.ReadAsStringAsync().Result}");
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
string body = await response.Content.ReadAsStringAsync();
|
||||
JsonDocument doc = JsonDocument.Parse(body);
|
||||
|
||||
if (doc.RootElement.TryGetProperty("status", out JsonElement status))
|
||||
{
|
||||
if (status.ToString().Trim().Equals("success", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
dcValue = doc.RootElement.GetProperty("message").GetString().Trim();
|
||||
return dcValue;
|
||||
}
|
||||
|
||||
Log.Debug($"CDRM response status not successful. Retrying... Attempt {attempt} of {MaxAttempts}");
|
||||
if (attempt < MaxAttempts)
|
||||
{
|
||||
await Task.Delay(DelayBetweenAttempts);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug($"Status not in CDRM response. Retrying... Attempt {attempt} of {MaxAttempts}");
|
||||
if (attempt < MaxAttempts)
|
||||
{
|
||||
await Task.Delay(DelayBetweenAttempts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("Maximum retry attempts reached. Unable to get a valid decryption key.");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<string> GetDecryptionKeyOFDL(Dictionary<string, string> drmHeaders, string licenceURL,
|
||||
public async Task<string> GetDecryptionKeyOFDL(Dictionary<string, string> drmHeaders, string licenceUrl,
|
||||
string pssh)
|
||||
{
|
||||
Log.Debug("Calling GetDecryptionOFDL");
|
||||
@ -3212,12 +3149,12 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
|
||||
OFDLRequest ofdlRequest = new()
|
||||
{
|
||||
Pssh = pssh, LicenseUrl = licenceURL, Headers = JsonConvert.SerializeObject(drmHeaders)
|
||||
Pssh = pssh, LicenseUrl = licenceUrl, Headers = JsonConvert.SerializeObject(drmHeaders)
|
||||
};
|
||||
|
||||
string json = JsonConvert.SerializeObject(ofdlRequest);
|
||||
|
||||
Log.Debug($"Posting to ofdl.tools: {json}");
|
||||
Log.Debug("Posting to ofdl.tools: {Json}", json);
|
||||
|
||||
while (attempt < MaxAttempts)
|
||||
{
|
||||
@ -3263,28 +3200,34 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<string> GetDecryptionKeyCDM(Dictionary<string, string> drmHeaders, string licenceURL, string pssh)
|
||||
public async Task<string>? GetDecryptionKeyCDM(Dictionary<string, string> drmHeaders, string licenceURL,
|
||||
string pssh)
|
||||
{
|
||||
Log.Debug("Calling GetDecryptionKeyCDM");
|
||||
|
||||
try
|
||||
{
|
||||
byte[] resp1 = await PostData(licenceURL, drmHeaders, new byte[] { 0x08, 0x04 });
|
||||
byte[] resp1 = await PostData(licenceURL, drmHeaders, [0x08, 0x04]);
|
||||
string certDataB64 = Convert.ToBase64String(resp1);
|
||||
CDMApi cdm = new();
|
||||
byte[] challenge = cdm.GetChallenge(pssh, certDataB64);
|
||||
byte[]? challenge = cdm.GetChallenge(pssh, certDataB64);
|
||||
if (challenge == null)
|
||||
{
|
||||
throw new Exception("Failed to get challenge from CDM");
|
||||
}
|
||||
|
||||
byte[] resp2 = await PostData(licenceURL, drmHeaders, challenge);
|
||||
string licenseB64 = Convert.ToBase64String(resp2);
|
||||
Log.Debug($"resp1: {resp1}");
|
||||
Log.Debug($"certDataB64: {certDataB64}");
|
||||
Log.Debug($"challenge: {challenge}");
|
||||
Log.Debug($"resp2: {resp2}");
|
||||
Log.Debug($"licenseB64: {licenseB64}");
|
||||
Log.Debug("resp1: {Resp1}", resp1);
|
||||
Log.Debug("certDataB64: {CertDataB64}", certDataB64);
|
||||
Log.Debug("challenge: {Challenge}", challenge);
|
||||
Log.Debug("resp2: {Resp2}", resp2);
|
||||
Log.Debug("licenseB64: {LicenseB64}", licenseB64);
|
||||
cdm.ProvideLicense(licenseB64);
|
||||
List<ContentKey> keys = cdm.GetKeys();
|
||||
if (keys.Count > 0)
|
||||
{
|
||||
Log.Debug($"GetDecryptionKeyCDM Key: {keys[0]}");
|
||||
Log.Debug("GetDecryptionKeyCDM Key: {ContentKey}", keys[0]);
|
||||
return keys[0].ToString();
|
||||
}
|
||||
}
|
||||
@ -3322,7 +3265,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
}
|
||||
|
||||
|
||||
private async Task<HttpRequestMessage> BuildHttpRequestMessage(Dictionary<string, string> getParams,
|
||||
private Task<HttpRequestMessage> BuildHttpRequestMessage(Dictionary<string, string> getParams,
|
||||
string endpoint)
|
||||
{
|
||||
Log.Debug("Calling BuildHttpRequestMessage");
|
||||
@ -3340,7 +3283,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
request.Headers.Add(keyValuePair.Key, keyValuePair.Value);
|
||||
}
|
||||
|
||||
return request;
|
||||
return Task.FromResult(request);
|
||||
}
|
||||
|
||||
private static double ConvertToUnixTimestampWithMicrosecondPrecision(DateTime date)
|
||||
@ -3420,7 +3363,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
|
||||
|
||||
SubscriptionsDtos.SubscriptionsDto? subscriptionsDto =
|
||||
JsonConvert.DeserializeObject<SubscriptionsDtos.SubscriptionsDto>(body, m_JsonSerializerSettings);
|
||||
JsonConvert.DeserializeObject<SubscriptionsDtos.SubscriptionsDto>(body, s_mJsonSerializerSettings);
|
||||
subscriptions = SubscriptionsMapper.FromDto(subscriptionsDto);
|
||||
if (subscriptions.HasMore)
|
||||
{
|
||||
@ -3435,7 +3378,7 @@ public class APIService(IAuthService authService, IConfigService configService,
|
||||
{
|
||||
SubscriptionsDtos.SubscriptionsDto? newSubscriptionsDto =
|
||||
JsonConvert.DeserializeObject<SubscriptionsDtos.SubscriptionsDto>(loopbody,
|
||||
m_JsonSerializerSettings);
|
||||
s_mJsonSerializerSettings);
|
||||
newSubscriptions = SubscriptionsMapper.FromDto(newSubscriptionsDto);
|
||||
}
|
||||
else
|
||||
@ -1,12 +1,15 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
using OF_DL.Models;
|
||||
using PuppeteerSharp;
|
||||
using PuppeteerSharp.BrowserData;
|
||||
using Serilog;
|
||||
using UserEntities = OF_DL.Models.Entities.Users;
|
||||
|
||||
namespace OF_DL.Services;
|
||||
|
||||
public class AuthService : IAuthService
|
||||
public class AuthService(IServiceProvider serviceProvider) : IAuthService
|
||||
{
|
||||
private const int LoginTimeout = 600000; // 10 minutes
|
||||
private const int FeedLoadTimeout = 60000; // 1 minute
|
||||
@ -126,6 +129,53 @@ public class AuthService : IAuthService
|
||||
private async Task<string> GetBcToken(IPage page) =>
|
||||
await page.EvaluateExpressionAsync<string>("window.localStorage.getItem('bcTokenSha') || ''");
|
||||
|
||||
public void ValidateCookieString()
|
||||
{
|
||||
if (CurrentAuth == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string pattern = @"(auth_id=\d+)|(sess=[^;]+)";
|
||||
MatchCollection matches = Regex.Matches(CurrentAuth.Cookie, pattern);
|
||||
|
||||
string output = string.Join("; ", matches);
|
||||
|
||||
if (!output.EndsWith(";"))
|
||||
{
|
||||
output += ";";
|
||||
}
|
||||
|
||||
if (CurrentAuth.Cookie.Trim() != output.Trim())
|
||||
{
|
||||
CurrentAuth.Cookie = output;
|
||||
string newAuthString = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented);
|
||||
File.WriteAllText("auth.json", newAuthString);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UserEntities.User?> ValidateAuthAsync()
|
||||
{
|
||||
// Resolve IAPIService lazily to avoid circular dependency
|
||||
IAPIService apiService = serviceProvider.GetRequiredService<IAPIService>();
|
||||
return await apiService.GetUserInfo("/users/me");
|
||||
}
|
||||
|
||||
public void Logout()
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Auth?> GetAuthFromBrowser(bool isDocker = false)
|
||||
{
|
||||
try
|
||||
@ -184,6 +234,7 @@ public class AuthService : IAuthService
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Error getting bcToken");
|
||||
throw new Exception("Error getting bcToken");
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Akka.Configuration;
|
||||
using Akka.Configuration.Hocon;
|
||||
@ -228,13 +229,12 @@ public class ConfigService(ILoggingService loggingService) : IConfigService
|
||||
Akka.Configuration.Config? creatorConfigsSection = hoconConfig.GetConfig("CreatorConfigs");
|
||||
if (creatorConfigsSection != null)
|
||||
{
|
||||
foreach (KeyValuePair<string, HoconValue> key in creatorConfigsSection.AsEnumerable())
|
||||
foreach ((string? creatorKey, _) in creatorConfigsSection.AsEnumerable())
|
||||
{
|
||||
string creatorKey = key.Key;
|
||||
Akka.Configuration.Config? creatorHocon = creatorConfigsSection.GetConfig(creatorKey);
|
||||
if (!CurrentConfig.CreatorConfigs.ContainsKey(creatorKey) && creatorHocon != null)
|
||||
{
|
||||
CurrentConfig.CreatorConfigs.Add(key.Key,
|
||||
CurrentConfig.CreatorConfigs.Add(creatorKey,
|
||||
new CreatorConfig
|
||||
{
|
||||
PaidPostFileNameFormat = creatorHocon.GetString("PaidPostFileNameFormat"),
|
||||
@ -243,14 +243,14 @@ public class ConfigService(ILoggingService loggingService) : IConfigService
|
||||
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[creatorKey].PaidPostFileNameFormat,
|
||||
$"{creatorKey}.PaidPostFileNameFormat");
|
||||
ValidateFileNameFormat(CurrentConfig.CreatorConfigs[creatorKey].PostFileNameFormat,
|
||||
$"{creatorKey}.PostFileNameFormat");
|
||||
ValidateFileNameFormat(CurrentConfig.CreatorConfigs[creatorKey].PaidMessageFileNameFormat,
|
||||
$"{creatorKey}.PaidMessageFileNameFormat");
|
||||
ValidateFileNameFormat(CurrentConfig.CreatorConfigs[creatorKey].MessageFileNameFormat,
|
||||
$"{creatorKey}.MessageFileNameFormat");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -402,6 +402,50 @@ public class ConfigService(ILoggingService loggingService) : IConfigService
|
||||
}
|
||||
}
|
||||
|
||||
public List<(string Name, bool Value)> GetToggleableProperties()
|
||||
{
|
||||
List<(string Name, bool Value)> result = [];
|
||||
foreach (PropertyInfo propInfo in typeof(Config).GetProperties())
|
||||
{
|
||||
ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute<ToggleableConfigAttribute>();
|
||||
if (attr != null)
|
||||
{
|
||||
result.Add((propInfo.Name, (bool)propInfo.GetValue(CurrentConfig)!));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool ApplyToggleableSelections(List<string> selectedNames)
|
||||
{
|
||||
bool configChanged = false;
|
||||
Config newConfig = new();
|
||||
|
||||
foreach (PropertyInfo propInfo in typeof(Config).GetProperties())
|
||||
{
|
||||
ToggleableConfigAttribute? attr = propInfo.GetCustomAttribute<ToggleableConfigAttribute>();
|
||||
if (attr != null)
|
||||
{
|
||||
bool newValue = selectedNames.Contains(propInfo.Name);
|
||||
bool oldValue = (bool)propInfo.GetValue(CurrentConfig)!;
|
||||
propInfo.SetValue(newConfig, newValue);
|
||||
|
||||
if (newValue != oldValue)
|
||||
{
|
||||
configChanged = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
propInfo.SetValue(newConfig, propInfo.GetValue(CurrentConfig));
|
||||
}
|
||||
}
|
||||
|
||||
UpdateConfig(newConfig);
|
||||
return configChanged;
|
||||
}
|
||||
|
||||
private VideoResolution ParseVideoResolution(string value)
|
||||
{
|
||||
if (value.Equals("source", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
550
OF DL/Services/DownloadOrchestrationService.cs
Normal file
550
OF DL/Services/DownloadOrchestrationService.cs
Normal file
@ -0,0 +1,550 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OF_DL.Enumerations;
|
||||
using OF_DL.Models;
|
||||
using Serilog;
|
||||
using PostEntities = OF_DL.Models.Entities.Posts;
|
||||
using PurchasedEntities = OF_DL.Models.Entities.Purchased;
|
||||
using UserEntities = OF_DL.Models.Entities.Users;
|
||||
|
||||
namespace OF_DL.Services;
|
||||
|
||||
public class DownloadOrchestrationService(
|
||||
IAPIService apiService,
|
||||
IConfigService configService,
|
||||
IDownloadService downloadService,
|
||||
IDBService dbService) : IDownloadOrchestrationService
|
||||
{
|
||||
public List<long> PaidPostIds { get; } = new();
|
||||
|
||||
public async Task<UserListResult> GetAvailableUsersAsync()
|
||||
{
|
||||
UserListResult result = new();
|
||||
Config config = configService.CurrentConfig;
|
||||
|
||||
Dictionary<string, long>? activeSubs =
|
||||
await apiService.GetActiveSubscriptions("/subscriptions/subscribes",
|
||||
config.IncludeRestrictedSubscriptions);
|
||||
|
||||
if (activeSubs != null)
|
||||
{
|
||||
Log.Debug("Subscriptions: ");
|
||||
foreach (KeyValuePair<string, long> activeSub in activeSubs)
|
||||
{
|
||||
if (!result.Users.ContainsKey(activeSub.Key))
|
||||
{
|
||||
result.Users.Add(activeSub.Key, activeSub.Value);
|
||||
Log.Debug($"Name: {activeSub.Key} ID: {activeSub.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("Couldn't get active subscriptions. Received null response.");
|
||||
}
|
||||
|
||||
|
||||
if (config.IncludeExpiredSubscriptions)
|
||||
{
|
||||
Log.Debug("Inactive Subscriptions: ");
|
||||
Dictionary<string, long>? expiredSubs =
|
||||
await apiService.GetExpiredSubscriptions("/subscriptions/subscribes",
|
||||
config.IncludeRestrictedSubscriptions);
|
||||
|
||||
if (expiredSubs != null)
|
||||
{
|
||||
foreach (KeyValuePair<string, long> expiredSub in expiredSubs.Where(expiredSub =>
|
||||
!result.Users.ContainsKey(expiredSub.Key)))
|
||||
{
|
||||
result.Users.Add(expiredSub.Key, expiredSub.Value);
|
||||
Log.Debug("Name: {ExpiredSubKey} ID: {ExpiredSubValue}", expiredSub.Key, expiredSub.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("Couldn't get expired subscriptions. Received null response.");
|
||||
}
|
||||
}
|
||||
|
||||
result.Lists = await apiService.GetLists("/lists") ?? new Dictionary<string, long>();
|
||||
|
||||
// Remove users from the list if they are in the ignored list
|
||||
if (!string.IsNullOrEmpty(config.IgnoredUsersListName))
|
||||
{
|
||||
if (!result.Lists.TryGetValue(config.IgnoredUsersListName, out long ignoredUsersListId))
|
||||
{
|
||||
result.IgnoredListError = $"Ignored users list '{config.IgnoredUsersListName}' not found";
|
||||
Log.Error(result.IgnoredListError);
|
||||
}
|
||||
else
|
||||
{
|
||||
List<string> ignoredUsernames =
|
||||
await apiService.GetListUsers($"/lists/{ignoredUsersListId}/users") ?? [];
|
||||
result.Users = result.Users.Where(x => !ignoredUsernames.Contains(x.Key))
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
}
|
||||
|
||||
await dbService.CreateUsersDB(result.Users);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, long>> GetUsersForListAsync(
|
||||
string listName, Dictionary<string, long> allUsers, Dictionary<string, long> lists)
|
||||
{
|
||||
long listId = lists[listName];
|
||||
List<string> listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? [];
|
||||
return allUsers.Where(x => listUsernames.Contains(x.Key)).Distinct()
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
|
||||
public string ResolveDownloadPath(string username) =>
|
||||
!string.IsNullOrEmpty(configService.CurrentConfig.DownloadPath)
|
||||
? Path.Combine(configService.CurrentConfig.DownloadPath, username)
|
||||
: $"__user_data__/sites/OnlyFans/{username}";
|
||||
|
||||
public async Task PrepareUserFolderAsync(string username, long userId, string path)
|
||||
{
|
||||
await dbService.CheckUsername(new KeyValuePair<string, long>(username, userId), path);
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
Log.Debug($"Created folder for {username}");
|
||||
}
|
||||
|
||||
await dbService.CreateDB(path);
|
||||
}
|
||||
|
||||
public async Task<CreatorDownloadResult> DownloadCreatorContentAsync(
|
||||
string username, long userId, string path,
|
||||
Dictionary<string, long> users,
|
||||
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
IDownloadEventHandler eventHandler)
|
||||
{
|
||||
Config config = configService.CurrentConfig;
|
||||
CreatorDownloadResult counts = new();
|
||||
|
||||
eventHandler.OnUserStarting(username);
|
||||
Log.Debug($"Scraping Data for {username}");
|
||||
|
||||
await PrepareUserFolderAsync(username, userId, path);
|
||||
|
||||
if (config.DownloadAvatarHeaderPhoto)
|
||||
{
|
||||
UserEntities.User? userInfo = await apiService.GetUserInfo($"/users/{username}");
|
||||
if (userInfo != null)
|
||||
{
|
||||
await downloadService.DownloadAvatarHeader(userInfo.Avatar, userInfo.Header, path, username);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.DownloadPaidPosts)
|
||||
{
|
||||
counts.PaidPostCount = await DownloadContentTypeAsync("Paid Posts",
|
||||
async statusReporter =>
|
||||
await apiService.GetPaidPosts("/posts/paid/post", path, username, PaidPostIds, statusReporter),
|
||||
posts => posts?.PaidPosts?.Count ?? 0,
|
||||
posts => posts?.PaidPostObjects?.Count ?? 0,
|
||||
posts => posts?.PaidPosts?.Values?.ToList(),
|
||||
async (posts, reporter) => await downloadService.DownloadPaidPosts(username, userId, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing, posts, reporter),
|
||||
eventHandler);
|
||||
}
|
||||
|
||||
if (config.DownloadPosts)
|
||||
{
|
||||
eventHandler.OnMessage(
|
||||
"Getting Posts (this may take a long time, depending on the number of Posts the creator has)");
|
||||
Log.Debug($"Calling DownloadFreePosts - {username}");
|
||||
|
||||
counts.PostCount = await DownloadContentTypeAsync("Posts",
|
||||
async statusReporter =>
|
||||
await apiService.GetPosts($"/users/{userId}/posts", path, PaidPostIds, statusReporter),
|
||||
posts => posts?.Posts?.Count ?? 0,
|
||||
posts => posts?.PostObjects?.Count ?? 0,
|
||||
posts => posts?.Posts?.Values?.ToList(),
|
||||
async (posts, reporter) => await downloadService.DownloadFreePosts(username, userId, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing, posts, reporter),
|
||||
eventHandler);
|
||||
}
|
||||
|
||||
if (config.DownloadArchived)
|
||||
{
|
||||
counts.ArchivedCount = await DownloadContentTypeAsync("Archived Posts",
|
||||
async statusReporter =>
|
||||
await apiService.GetArchived($"/users/{userId}/posts", path, statusReporter),
|
||||
archived => archived?.ArchivedPosts?.Count ?? 0,
|
||||
archived => archived?.ArchivedPostObjects?.Count ?? 0,
|
||||
archived => archived?.ArchivedPosts?.Values?.ToList(),
|
||||
async (archived, reporter) => await downloadService.DownloadArchived(username, userId, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing, archived, reporter),
|
||||
eventHandler);
|
||||
}
|
||||
|
||||
if (config.DownloadStreams)
|
||||
{
|
||||
counts.StreamsCount = await DownloadContentTypeAsync("Streams",
|
||||
async statusReporter =>
|
||||
await apiService.GetStreams($"/users/{userId}/posts/streams", path, PaidPostIds, statusReporter),
|
||||
streams => streams?.Streams?.Count ?? 0,
|
||||
streams => streams?.StreamObjects?.Count ?? 0,
|
||||
streams => streams?.Streams?.Values?.ToList(),
|
||||
async (streams, reporter) => await downloadService.DownloadStreams(username, userId, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing, streams, reporter),
|
||||
eventHandler);
|
||||
}
|
||||
|
||||
if (config.DownloadStories)
|
||||
{
|
||||
eventHandler.OnMessage("Getting Stories");
|
||||
Dictionary<long, string>? tempStories = await apiService.GetMedia(MediaType.Stories,
|
||||
$"/users/{userId}/stories", null, path, PaidPostIds);
|
||||
|
||||
if (tempStories != null && tempStories.Count > 0)
|
||||
{
|
||||
eventHandler.OnContentFound("Stories", tempStories.Count, tempStories.Count);
|
||||
|
||||
long totalSize = config.ShowScrapeSize
|
||||
? await downloadService.CalculateTotalFileSize(tempStories.Values.ToList())
|
||||
: tempStories.Count;
|
||||
|
||||
DownloadResult result = await eventHandler.WithProgressAsync(
|
||||
$"Downloading {tempStories.Count} Stories", totalSize, config.ShowScrapeSize,
|
||||
async reporter => await downloadService.DownloadStories(username, userId, path,
|
||||
PaidPostIds.ToHashSet(), reporter));
|
||||
|
||||
eventHandler.OnDownloadComplete("Stories", result);
|
||||
counts.StoriesCount = result.TotalCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
eventHandler.OnNoContentFound("Stories");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.DownloadHighlights)
|
||||
{
|
||||
eventHandler.OnMessage("Getting Highlights");
|
||||
Dictionary<long, string>? tempHighlights = await apiService.GetMedia(MediaType.Highlights,
|
||||
$"/users/{userId}/stories/highlights", null, path, PaidPostIds);
|
||||
|
||||
if (tempHighlights != null && tempHighlights.Count > 0)
|
||||
{
|
||||
eventHandler.OnContentFound("Highlights", tempHighlights.Count, tempHighlights.Count);
|
||||
|
||||
long totalSize = config.ShowScrapeSize
|
||||
? await downloadService.CalculateTotalFileSize(tempHighlights.Values.ToList())
|
||||
: tempHighlights.Count;
|
||||
|
||||
DownloadResult result = await eventHandler.WithProgressAsync(
|
||||
$"Downloading {tempHighlights.Count} Highlights", totalSize, config.ShowScrapeSize,
|
||||
async reporter => await downloadService.DownloadHighlights(username, userId, path,
|
||||
PaidPostIds.ToHashSet(), reporter));
|
||||
|
||||
eventHandler.OnDownloadComplete("Highlights", result);
|
||||
counts.HighlightsCount = result.TotalCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
eventHandler.OnNoContentFound("Highlights");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.DownloadMessages)
|
||||
{
|
||||
counts.MessagesCount = await DownloadContentTypeAsync("Messages",
|
||||
async statusReporter =>
|
||||
await apiService.GetMessages($"/chats/{userId}/messages", path, statusReporter),
|
||||
messages => messages.Messages?.Count ?? 0,
|
||||
messages => messages.MessageObjects?.Count ?? 0,
|
||||
messages => messages?.Messages.Values.ToList(),
|
||||
async (messages, reporter) => await downloadService.DownloadMessages(username, userId, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing, messages, reporter),
|
||||
eventHandler);
|
||||
}
|
||||
|
||||
if (config.DownloadPaidMessages)
|
||||
{
|
||||
counts.PaidMessagesCount = await DownloadContentTypeAsync("Paid Messages",
|
||||
async statusReporter =>
|
||||
await apiService.GetPaidMessages("/posts/paid/chat", path, username, statusReporter),
|
||||
paidMessages => paidMessages?.PaidMessages?.Count ?? 0,
|
||||
paidMessages => paidMessages?.PaidMessageObjects?.Count ?? 0,
|
||||
paidMessages => paidMessages?.PaidMessages?.Values?.ToList(),
|
||||
async (paidMessages, reporter) => await downloadService.DownloadPaidMessages(username, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing, paidMessages, reporter),
|
||||
eventHandler);
|
||||
}
|
||||
|
||||
eventHandler.OnUserComplete(username, counts);
|
||||
return counts;
|
||||
}
|
||||
|
||||
public async Task DownloadSinglePostAsync(
|
||||
string username, long postId, string path,
|
||||
Dictionary<string, long> users,
|
||||
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
IDownloadEventHandler eventHandler)
|
||||
{
|
||||
Log.Debug($"Calling DownloadSinglePost - {postId}");
|
||||
eventHandler.OnMessage("Getting Post");
|
||||
|
||||
PostEntities.SinglePostCollection post = await apiService.GetPost($"/posts/{postId}", path);
|
||||
if (post.SinglePosts.Count == 0)
|
||||
{
|
||||
eventHandler.OnMessage("Couldn't find post");
|
||||
Log.Debug("Couldn't find post");
|
||||
return;
|
||||
}
|
||||
|
||||
Config config = configService.CurrentConfig;
|
||||
long totalSize = config.ShowScrapeSize
|
||||
? await downloadService.CalculateTotalFileSize(post.SinglePosts.Values.ToList())
|
||||
: post.SinglePosts.Count;
|
||||
|
||||
DownloadResult result = await eventHandler.WithProgressAsync(
|
||||
"Downloading Post", totalSize, config.ShowScrapeSize,
|
||||
async reporter => await downloadService.DownloadSinglePost(username, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing, post, reporter));
|
||||
|
||||
if (result.NewDownloads > 0)
|
||||
{
|
||||
eventHandler.OnMessage($"Post {postId} downloaded");
|
||||
Log.Debug($"Post {postId} downloaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
eventHandler.OnMessage($"Post {postId} already downloaded");
|
||||
Log.Debug($"Post {postId} already downloaded");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DownloadPurchasedTabAsync(
|
||||
Dictionary<string, long> users,
|
||||
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
IDownloadEventHandler eventHandler)
|
||||
{
|
||||
Config config = configService.CurrentConfig;
|
||||
|
||||
Dictionary<string, long> purchasedTabUsers =
|
||||
await apiService.GetPurchasedTabUsers("/posts/paid/all", users);
|
||||
|
||||
eventHandler.OnMessage("Checking folders for Users in Purchased Tab");
|
||||
|
||||
foreach (KeyValuePair<string, long> user in purchasedTabUsers)
|
||||
{
|
||||
string path = ResolveDownloadPath(user.Key);
|
||||
Log.Debug($"Download path: {path}");
|
||||
|
||||
await dbService.CheckUsername(user, path);
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
Log.Debug($"Created folder for {user.Key}");
|
||||
}
|
||||
|
||||
await apiService.GetUserInfo($"/users/{user.Key}");
|
||||
await dbService.CreateDB(path);
|
||||
}
|
||||
|
||||
string basePath = !string.IsNullOrEmpty(config.DownloadPath)
|
||||
? config.DownloadPath
|
||||
: "__user_data__/sites/OnlyFans/";
|
||||
|
||||
Log.Debug($"Download path: {basePath}");
|
||||
|
||||
List<PurchasedEntities.PurchasedTabCollection> purchasedTabCollections =
|
||||
await apiService.GetPurchasedTab("/posts/paid/all", basePath, users);
|
||||
|
||||
foreach (PurchasedEntities.PurchasedTabCollection purchasedTabCollection in purchasedTabCollections)
|
||||
{
|
||||
eventHandler.OnUserStarting(purchasedTabCollection.Username);
|
||||
string path = ResolveDownloadPath(purchasedTabCollection.Username);
|
||||
Log.Debug($"Download path: {path}");
|
||||
|
||||
int paidPostCount = 0;
|
||||
int paidMessagesCount = 0;
|
||||
|
||||
// Download paid posts
|
||||
if (purchasedTabCollection.PaidPosts?.PaidPosts?.Count > 0)
|
||||
{
|
||||
eventHandler.OnContentFound("Paid Posts",
|
||||
purchasedTabCollection.PaidPosts.PaidPosts.Count,
|
||||
purchasedTabCollection.PaidPosts.PaidPostObjects.Count);
|
||||
|
||||
long totalSize = config.ShowScrapeSize
|
||||
? await downloadService.CalculateTotalFileSize(
|
||||
purchasedTabCollection.PaidPosts.PaidPosts.Values.ToList())
|
||||
: purchasedTabCollection.PaidPosts.PaidPosts.Count;
|
||||
|
||||
DownloadResult postResult = await eventHandler.WithProgressAsync(
|
||||
$"Downloading {purchasedTabCollection.PaidPosts.PaidPosts.Count} Paid Posts",
|
||||
totalSize, config.ShowScrapeSize,
|
||||
async reporter => await downloadService.DownloadPaidPostsPurchasedTab(
|
||||
purchasedTabCollection.Username, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing,
|
||||
purchasedTabCollection.PaidPosts, reporter));
|
||||
|
||||
eventHandler.OnDownloadComplete("Paid Posts", postResult);
|
||||
paidPostCount = postResult.TotalCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
eventHandler.OnNoContentFound("Paid Posts");
|
||||
}
|
||||
|
||||
// Download paid messages
|
||||
if (purchasedTabCollection.PaidMessages?.PaidMessages?.Count > 0)
|
||||
{
|
||||
eventHandler.OnContentFound("Paid Messages",
|
||||
purchasedTabCollection.PaidMessages.PaidMessages.Count,
|
||||
purchasedTabCollection.PaidMessages.PaidMessageObjects.Count);
|
||||
|
||||
long totalSize = config.ShowScrapeSize
|
||||
? await downloadService.CalculateTotalFileSize(
|
||||
purchasedTabCollection.PaidMessages.PaidMessages.Values.ToList())
|
||||
: purchasedTabCollection.PaidMessages.PaidMessages.Count;
|
||||
|
||||
DownloadResult msgResult = await eventHandler.WithProgressAsync(
|
||||
$"Downloading {purchasedTabCollection.PaidMessages.PaidMessages.Count} Paid Messages",
|
||||
totalSize, config.ShowScrapeSize,
|
||||
async reporter => await downloadService.DownloadPaidMessagesPurchasedTab(
|
||||
purchasedTabCollection.Username, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing,
|
||||
purchasedTabCollection.PaidMessages, reporter));
|
||||
|
||||
eventHandler.OnDownloadComplete("Paid Messages", msgResult);
|
||||
paidMessagesCount = msgResult.TotalCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
eventHandler.OnNoContentFound("Paid Messages");
|
||||
}
|
||||
|
||||
eventHandler.OnPurchasedTabUserComplete(purchasedTabCollection.Username, paidPostCount, paidMessagesCount);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DownloadSinglePaidMessageAsync(
|
||||
string username, long messageId, string path,
|
||||
Dictionary<string, long> users,
|
||||
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
IDownloadEventHandler eventHandler)
|
||||
{
|
||||
Log.Debug($"Calling DownloadSinglePaidMessage - {username}");
|
||||
eventHandler.OnMessage("Getting Paid Message");
|
||||
|
||||
PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection =
|
||||
await apiService.GetPaidMessage($"/messages/{messageId}", path);
|
||||
|
||||
if (singlePaidMessageCollection.SingleMessages.Count == 0)
|
||||
{
|
||||
eventHandler.OnNoContentFound("Paid Messages");
|
||||
return;
|
||||
}
|
||||
|
||||
Config config = configService.CurrentConfig;
|
||||
|
||||
// Handle preview messages
|
||||
if (singlePaidMessageCollection.PreviewSingleMessages.Count > 0)
|
||||
{
|
||||
eventHandler.OnContentFound("Preview Paid Messages",
|
||||
singlePaidMessageCollection.PreviewSingleMessages.Count,
|
||||
singlePaidMessageCollection.SingleMessageObjects.Count);
|
||||
|
||||
long previewSize = config.ShowScrapeSize
|
||||
? await downloadService.CalculateTotalFileSize(
|
||||
singlePaidMessageCollection.PreviewSingleMessages.Values.ToList())
|
||||
: singlePaidMessageCollection.PreviewSingleMessages.Count;
|
||||
|
||||
DownloadResult previewResult = await eventHandler.WithProgressAsync(
|
||||
$"Downloading {singlePaidMessageCollection.PreviewSingleMessages.Count} Preview Paid Messages",
|
||||
previewSize, config.ShowScrapeSize,
|
||||
async reporter => await downloadService.DownloadSinglePaidMessage(username, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing, singlePaidMessageCollection, reporter));
|
||||
|
||||
eventHandler.OnDownloadComplete("Paid Messages", previewResult);
|
||||
}
|
||||
else if (singlePaidMessageCollection.SingleMessages.Count > 0)
|
||||
{
|
||||
// Only actual paid messages, no preview
|
||||
eventHandler.OnContentFound("Paid Messages",
|
||||
singlePaidMessageCollection.SingleMessages.Count,
|
||||
singlePaidMessageCollection.SingleMessageObjects.Count);
|
||||
|
||||
long totalSize = config.ShowScrapeSize
|
||||
? await downloadService.CalculateTotalFileSize(
|
||||
singlePaidMessageCollection.SingleMessages.Values.ToList())
|
||||
: singlePaidMessageCollection.SingleMessages.Count;
|
||||
|
||||
DownloadResult result = await eventHandler.WithProgressAsync(
|
||||
$"Downloading {singlePaidMessageCollection.SingleMessages.Count} Paid Messages",
|
||||
totalSize, config.ShowScrapeSize,
|
||||
async reporter => await downloadService.DownloadSinglePaidMessage(username, path, users,
|
||||
clientIdBlobMissing, devicePrivateKeyMissing, singlePaidMessageCollection, reporter));
|
||||
|
||||
eventHandler.OnDownloadComplete("Paid Messages", result);
|
||||
}
|
||||
else
|
||||
{
|
||||
eventHandler.OnNoContentFound("Paid Messages");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> ResolveUsernameAsync(long userId)
|
||||
{
|
||||
JObject? user = await apiService.GetUserInfoById($"/users/list?x[]={userId}");
|
||||
if (user == null)
|
||||
{
|
||||
return $"Deleted User - {userId}";
|
||||
}
|
||||
|
||||
string? username = user[userId.ToString()]?["username"]?.ToString();
|
||||
return !string.IsNullOrEmpty(username) ? username : $"Deleted User - {userId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic helper for the common pattern: fetch with status -> check count -> download with progress.
|
||||
/// </summary>
|
||||
private async Task<int> DownloadContentTypeAsync<T>(
|
||||
string contentType,
|
||||
Func<IStatusReporter, Task<T>> fetchData,
|
||||
Func<T, int> getMediaCount,
|
||||
Func<T, int> getObjectCount,
|
||||
Func<T, List<string>?> getUrls,
|
||||
Func<T, IProgressReporter, Task<DownloadResult>> downloadData,
|
||||
IDownloadEventHandler eventHandler)
|
||||
{
|
||||
T data = await eventHandler.WithStatusAsync($"Getting {contentType}",
|
||||
async statusReporter => await fetchData(statusReporter));
|
||||
|
||||
int mediaCount = getMediaCount(data);
|
||||
if (mediaCount <= 0)
|
||||
{
|
||||
eventHandler.OnNoContentFound(contentType);
|
||||
Log.Debug($"Found 0 {contentType}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int objectCount = getObjectCount(data);
|
||||
eventHandler.OnContentFound(contentType, mediaCount, objectCount);
|
||||
Log.Debug($"Found {mediaCount} Media from {objectCount} {contentType}");
|
||||
|
||||
Config config = configService.CurrentConfig;
|
||||
List<string>? urls = getUrls(data);
|
||||
long totalSize = config.ShowScrapeSize && urls != null
|
||||
? await downloadService.CalculateTotalFileSize(urls)
|
||||
: mediaCount;
|
||||
|
||||
DownloadResult result = await eventHandler.WithProgressAsync(
|
||||
$"Downloading {mediaCount} {contentType}", totalSize, config.ShowScrapeSize,
|
||||
async reporter => await downloadData(data, reporter));
|
||||
|
||||
eventHandler.OnDownloadComplete(contentType, result);
|
||||
Log.Debug(
|
||||
$"{contentType} Already Downloaded: {result.ExistingDownloads} New {contentType} Downloaded: {result.NewDownloads}");
|
||||
|
||||
return result.TotalCount;
|
||||
}
|
||||
}
|
||||
@ -292,6 +292,11 @@ public class DownloadService(
|
||||
private async Task<int?> GetVideoStreamIndexFromMpd(string mpdUrl, string policy, string signature, string kvp,
|
||||
VideoResolution resolution)
|
||||
{
|
||||
if (authService.CurrentAuth == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpClient client = new();
|
||||
HttpRequestMessage request = new(HttpMethod.Get, mpdUrl);
|
||||
request.Headers.Add("user-agent", authService.CurrentAuth.UserAgent);
|
||||
@ -307,7 +312,7 @@ public class DownloadService(
|
||||
XNamespace cenc = "urn:mpeg:cenc:2013";
|
||||
XElement? videoAdaptationSet = doc
|
||||
.Descendants(ns + "AdaptationSet")
|
||||
.FirstOrDefault(e => (string)e.Attribute("mimeType") == "video/mp4");
|
||||
.FirstOrDefault(e => (string?)e.Attribute("mimeType") == "video/mp4");
|
||||
|
||||
if (videoAdaptationSet == null)
|
||||
{
|
||||
@ -326,7 +331,7 @@ public class DownloadService(
|
||||
|
||||
for (int i = 0; i < representations.Count; i++)
|
||||
{
|
||||
if ((string)representations[i].Attribute("height") == targetHeight)
|
||||
if ((string?)representations[i].Attribute("height") == targetHeight)
|
||||
{
|
||||
return i; // this is the index FFmpeg will use for `-map 0:v:{i}`
|
||||
}
|
||||
@ -512,6 +517,21 @@ public class DownloadService(
|
||||
|
||||
try
|
||||
{
|
||||
if (authService.CurrentAuth == null)
|
||||
{
|
||||
throw new Exception("No authentication information available.");
|
||||
}
|
||||
|
||||
if (authService.CurrentAuth.Cookie == null)
|
||||
{
|
||||
throw new Exception("No authentication cookie available.");
|
||||
}
|
||||
|
||||
if (authService.CurrentAuth.UserAgent == null)
|
||||
{
|
||||
throw new Exception("No user agent available.");
|
||||
}
|
||||
|
||||
Uri uri = new(url);
|
||||
|
||||
if (uri.Host == "cdn3.onlyfans.com" && uri.LocalPath.Contains("/dash/files"))
|
||||
@ -558,8 +578,6 @@ public class DownloadService(
|
||||
|
||||
public static async Task<DateTime> GetDRMVideoLastModified(string url, Auth auth)
|
||||
{
|
||||
Uri uri = new(url);
|
||||
|
||||
string[] messageUrlParsed = url.Split(',');
|
||||
string mpdURL = messageUrlParsed[0];
|
||||
string policy = messageUrlParsed[1];
|
||||
@ -946,6 +964,21 @@ public class DownloadService(
|
||||
{
|
||||
try
|
||||
{
|
||||
if (authService.CurrentAuth == null)
|
||||
{
|
||||
throw new Exception("No authentication information available.");
|
||||
}
|
||||
|
||||
if (authService.CurrentAuth.Cookie == null)
|
||||
{
|
||||
throw new Exception("No authentication cookie available.");
|
||||
}
|
||||
|
||||
if (authService.CurrentAuth.UserAgent == null)
|
||||
{
|
||||
throw new Exception("No user agent available.");
|
||||
}
|
||||
|
||||
Uri uri = new(url);
|
||||
string filename = Path.GetFileName(uri.LocalPath).Split(".")[0];
|
||||
|
||||
@ -1079,7 +1112,7 @@ public class DownloadService(
|
||||
{
|
||||
Log.Debug($"Calling DownloadHighlights - {username}");
|
||||
|
||||
Dictionary<long, string> highlights = await apiService.GetMedia(MediaType.Highlights,
|
||||
Dictionary<long, string>? highlights = await apiService.GetMedia(MediaType.Highlights,
|
||||
$"/users/{userId}/stories/highlights", null, path, paidPostIds.ToList());
|
||||
|
||||
if (highlights == null || highlights.Count == 0)
|
||||
@ -1133,7 +1166,7 @@ public class DownloadService(
|
||||
{
|
||||
Log.Debug($"Calling DownloadStories - {username}");
|
||||
|
||||
Dictionary<long, string> stories = await apiService.GetMedia(MediaType.Stories, $"/users/{userId}/stories",
|
||||
Dictionary<long, string>? stories = await apiService.GetMedia(MediaType.Stories, $"/users/{userId}/stories",
|
||||
null, path, paidPostIds.ToList());
|
||||
|
||||
if (stories == null || stories.Count == 0)
|
||||
@ -1454,7 +1487,7 @@ public class DownloadService(
|
||||
bool isNew;
|
||||
StreamEntities.Medium? mediaInfo = streams.StreamMedia.FirstOrDefault(m => m.Id == kvpEntry.Key);
|
||||
StreamEntities.ListItem? streamInfo =
|
||||
streams.StreamObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true);
|
||||
streams.StreamObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true);
|
||||
string filenameFormat =
|
||||
configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? "";
|
||||
string streamPath = configService.CurrentConfig.FolderPerPost && streamInfo != null &&
|
||||
@ -1534,7 +1567,7 @@ public class DownloadService(
|
||||
bool isNew;
|
||||
PostEntities.Medium? mediaInfo = posts.PostMedia.FirstOrDefault(m => m?.Id == postKVP.Key);
|
||||
PostEntities.ListItem? postInfo =
|
||||
posts.PostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true);
|
||||
posts.PostObjects.FirstOrDefault(p => p.Media?.Contains(mediaInfo) == true);
|
||||
string filenameFormat =
|
||||
configService.CurrentConfig.GetCreatorFileNameFormatConfig(username).PostFileNameFormat ?? "";
|
||||
string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null &&
|
||||
@ -1665,4 +1698,363 @@ public class DownloadService(
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DownloadResult> DownloadPaidPostsPurchasedTab(string username, string path,
|
||||
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter)
|
||||
{
|
||||
Log.Debug($"Calling DownloadPaidPostsPurchasedTab - {username}");
|
||||
|
||||
if (purchasedPosts == null || purchasedPosts.PaidPosts.Count == 0)
|
||||
{
|
||||
Log.Debug("Found 0 Paid Posts");
|
||||
return new DownloadResult { TotalCount = 0, MediaType = "Paid Posts", Success = true };
|
||||
}
|
||||
|
||||
int oldCount = 0, newCount = 0;
|
||||
|
||||
foreach (KeyValuePair<long, string> purchasedPostKVP in purchasedPosts.PaidPosts)
|
||||
{
|
||||
bool isNew;
|
||||
MessageEntities.Medium? mediaInfo =
|
||||
purchasedPosts?.PaidPostMedia?.FirstOrDefault(m => m.Id == purchasedPostKVP.Key);
|
||||
PurchasedEntities.ListItem? postInfo = mediaInfo != null
|
||||
? purchasedPosts?.PaidPostObjects?.FirstOrDefault(p =>
|
||||
p?.Media?.Any(m => m.Id == purchasedPostKVP.Key) == true)
|
||||
: null;
|
||||
string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username)
|
||||
.PaidPostFileNameFormat ?? "";
|
||||
string paidPostPath = configService.CurrentConfig.FolderPerPaidPost && postInfo != null &&
|
||||
postInfo?.Id is not null && postInfo?.PostedAt is not null
|
||||
? $"/Posts/Paid/{postInfo.Id} {postInfo.PostedAt.Value:yyyy-MM-dd HH-mm-ss}"
|
||||
: "/Posts/Paid";
|
||||
|
||||
if (purchasedPostKVP.Value.Contains("cdn3.onlyfans.com/dash/files"))
|
||||
{
|
||||
string[] parsed = purchasedPostKVP.Value.Split(',');
|
||||
(string decryptionKey, DateTime lastModified)? drmInfo =
|
||||
await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3],
|
||||
parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing);
|
||||
if (drmInfo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0],
|
||||
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, purchasedPostKVP.Key,
|
||||
"Posts", progressReporter, paidPostPath + "/Videos", filenameFormat,
|
||||
postInfo, mediaInfo, postInfo?.FromUser, users);
|
||||
}
|
||||
else
|
||||
{
|
||||
isNew = await DownloadMedia(purchasedPostKVP.Value, path,
|
||||
purchasedPostKVP.Key, "Posts", progressReporter,
|
||||
paidPostPath, filenameFormat, postInfo, mediaInfo, postInfo?.FromUser, users);
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
newCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Debug($"Paid Posts Already Downloaded: {oldCount} New Paid Posts Downloaded: {newCount}");
|
||||
return new DownloadResult
|
||||
{
|
||||
TotalCount = purchasedPosts.PaidPosts.Count,
|
||||
NewDownloads = newCount,
|
||||
ExistingDownloads = oldCount,
|
||||
MediaType = "Paid Posts",
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DownloadResult> DownloadPaidMessagesPurchasedTab(string username, string path,
|
||||
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter)
|
||||
{
|
||||
Log.Debug($"Calling DownloadPaidMessagesPurchasedTab - {username}");
|
||||
|
||||
if (paidMessageCollection == null || paidMessageCollection.PaidMessages.Count == 0)
|
||||
{
|
||||
Log.Debug("Found 0 Paid Messages");
|
||||
return new DownloadResult { TotalCount = 0, MediaType = "Paid Messages", Success = true };
|
||||
}
|
||||
|
||||
int oldCount = 0, newCount = 0;
|
||||
|
||||
foreach (KeyValuePair<long, string> paidMessageKVP in paidMessageCollection.PaidMessages)
|
||||
{
|
||||
bool isNew;
|
||||
MessageEntities.Medium? mediaInfo =
|
||||
paidMessageCollection.PaidMessageMedia.FirstOrDefault(m => m.Id == paidMessageKVP.Key);
|
||||
PurchasedEntities.ListItem? messageInfo =
|
||||
paidMessageCollection.PaidMessageObjects.FirstOrDefault(p =>
|
||||
p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true);
|
||||
string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username)
|
||||
.PaidMessageFileNameFormat ?? "";
|
||||
string paidMsgPath = configService.CurrentConfig.FolderPerPaidMessage && messageInfo != null &&
|
||||
messageInfo?.Id is not null && messageInfo?.CreatedAt is not null
|
||||
? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"
|
||||
: "/Messages/Paid";
|
||||
|
||||
if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files"))
|
||||
{
|
||||
string[] parsed = paidMessageKVP.Value.Split(',');
|
||||
(string decryptionKey, DateTime lastModified)? drmInfo =
|
||||
await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3],
|
||||
parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing);
|
||||
if (drmInfo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0],
|
||||
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key,
|
||||
"Messages", progressReporter, paidMsgPath + "/Videos", filenameFormat,
|
||||
messageInfo, mediaInfo, messageInfo?.FromUser, users);
|
||||
}
|
||||
else
|
||||
{
|
||||
isNew = await DownloadMedia(paidMessageKVP.Value, path,
|
||||
paidMessageKVP.Key, "Messages", progressReporter,
|
||||
paidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users);
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
newCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Debug($"Paid Messages Already Downloaded: {oldCount} New Paid Messages Downloaded: {newCount}");
|
||||
return new DownloadResult
|
||||
{
|
||||
TotalCount = paidMessageCollection.PaidMessages.Count,
|
||||
NewDownloads = newCount,
|
||||
ExistingDownloads = oldCount,
|
||||
MediaType = "Paid Messages",
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DownloadResult> DownloadSinglePost(string username, string path,
|
||||
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
PostEntities.SinglePostCollection post, IProgressReporter progressReporter)
|
||||
{
|
||||
Log.Debug($"Calling DownloadSinglePost - {username}");
|
||||
|
||||
if (post == null || post.SinglePosts.Count == 0)
|
||||
{
|
||||
Log.Debug("Couldn't find post");
|
||||
return new DownloadResult { TotalCount = 0, MediaType = "Posts", Success = true };
|
||||
}
|
||||
|
||||
int oldCount = 0, newCount = 0;
|
||||
|
||||
foreach (KeyValuePair<long, string> postKVP in post.SinglePosts)
|
||||
{
|
||||
PostEntities.Medium? mediaInfo = post.SinglePostMedia.FirstOrDefault(m => m.Id == postKVP.Key);
|
||||
PostEntities.SinglePost? postInfo =
|
||||
post.SinglePostObjects.FirstOrDefault(p => p?.Media?.Contains(mediaInfo) == true);
|
||||
string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username)
|
||||
.PostFileNameFormat ?? "";
|
||||
string postPath = configService.CurrentConfig.FolderPerPost && postInfo != null &&
|
||||
postInfo?.Id is not null && postInfo?.PostedAt is not null
|
||||
? $"/Posts/Free/{postInfo.Id} {postInfo.PostedAt:yyyy-MM-dd HH-mm-ss}"
|
||||
: "/Posts/Free";
|
||||
|
||||
bool isNew;
|
||||
if (postKVP.Value.Contains("cdn3.onlyfans.com/dash/files"))
|
||||
{
|
||||
string[] parsed = postKVP.Value.Split(',');
|
||||
(string decryptionKey, DateTime lastModified)? drmInfo =
|
||||
await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3],
|
||||
parsed[4], parsed[5], "post", clientIdBlobMissing, devicePrivateKeyMissing);
|
||||
if (drmInfo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0],
|
||||
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, postKVP.Key, "Posts",
|
||||
progressReporter, postPath + "/Videos", filenameFormat,
|
||||
postInfo, mediaInfo, postInfo?.Author, users);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
isNew = await DownloadMedia(postKVP.Value, path,
|
||||
postKVP.Key, "Posts", progressReporter,
|
||||
postPath, filenameFormat, postInfo, mediaInfo, postInfo?.Author, users);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Log.Warning("Media was null");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
newCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
oldCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return new DownloadResult
|
||||
{
|
||||
TotalCount = post.SinglePosts.Count,
|
||||
NewDownloads = newCount,
|
||||
ExistingDownloads = oldCount,
|
||||
MediaType = "Posts",
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DownloadResult> DownloadSinglePaidMessage(string username, string path,
|
||||
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection,
|
||||
IProgressReporter progressReporter)
|
||||
{
|
||||
Log.Debug($"Calling DownloadSinglePaidMessage - {username}");
|
||||
|
||||
if (singlePaidMessageCollection == null)
|
||||
{
|
||||
return new DownloadResult { TotalCount = 0, MediaType = "Paid Messages", Success = true };
|
||||
}
|
||||
|
||||
int totalNew = 0, totalOld = 0;
|
||||
|
||||
// Download preview messages
|
||||
if (singlePaidMessageCollection.PreviewSingleMessages.Count > 0)
|
||||
{
|
||||
foreach (KeyValuePair<long, string> paidMessageKVP in singlePaidMessageCollection.PreviewSingleMessages)
|
||||
{
|
||||
MessageEntities.Medium? mediaInfo =
|
||||
singlePaidMessageCollection.PreviewSingleMessageMedia.FirstOrDefault(m =>
|
||||
m.Id == paidMessageKVP.Key);
|
||||
MessageEntities.SingleMessage? messageInfo =
|
||||
singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p =>
|
||||
p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true);
|
||||
string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username)
|
||||
.PaidMessageFileNameFormat ?? "";
|
||||
string previewMsgPath = configService.CurrentConfig.FolderPerMessage && messageInfo != null &&
|
||||
messageInfo?.Id is not null && messageInfo?.CreatedAt is not null
|
||||
? $"/Messages/Free/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"
|
||||
: "/Messages/Free";
|
||||
|
||||
bool isNew;
|
||||
if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files"))
|
||||
{
|
||||
string[] parsed = paidMessageKVP.Value.Split(',');
|
||||
(string decryptionKey, DateTime lastModified)? drmInfo =
|
||||
await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3],
|
||||
parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing);
|
||||
if (drmInfo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0],
|
||||
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key,
|
||||
"Messages", progressReporter, previewMsgPath + "/Videos", filenameFormat,
|
||||
messageInfo, mediaInfo, messageInfo?.FromUser, users);
|
||||
}
|
||||
else
|
||||
{
|
||||
isNew = await DownloadMedia(paidMessageKVP.Value, path,
|
||||
paidMessageKVP.Key, "Messages", progressReporter,
|
||||
previewMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users);
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
totalNew++;
|
||||
}
|
||||
else
|
||||
{
|
||||
totalOld++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Download actual paid messages
|
||||
if (singlePaidMessageCollection.SingleMessages.Count > 0)
|
||||
{
|
||||
foreach (KeyValuePair<long, string> paidMessageKVP in singlePaidMessageCollection.SingleMessages)
|
||||
{
|
||||
MessageEntities.Medium? mediaInfo =
|
||||
singlePaidMessageCollection.SingleMessageMedia.FirstOrDefault(m =>
|
||||
m.Id == paidMessageKVP.Key);
|
||||
MessageEntities.SingleMessage? messageInfo =
|
||||
singlePaidMessageCollection.SingleMessageObjects.FirstOrDefault(p =>
|
||||
p?.Media?.Any(m => m.Id == paidMessageKVP.Key) == true);
|
||||
string filenameFormat = configService.CurrentConfig.GetCreatorFileNameFormatConfig(username)
|
||||
.PaidMessageFileNameFormat ?? "";
|
||||
string singlePaidMsgPath = configService.CurrentConfig.FolderPerPaidMessage &&
|
||||
messageInfo != null && messageInfo?.Id is not null &&
|
||||
messageInfo?.CreatedAt is not null
|
||||
? $"/Messages/Paid/{messageInfo.Id} {messageInfo.CreatedAt.Value:yyyy-MM-dd HH-mm-ss}"
|
||||
: "/Messages/Paid";
|
||||
|
||||
bool isNew;
|
||||
if (paidMessageKVP.Value.Contains("cdn3.onlyfans.com/dash/files"))
|
||||
{
|
||||
string[] parsed = paidMessageKVP.Value.Split(',');
|
||||
(string decryptionKey, DateTime lastModified)? drmInfo =
|
||||
await GetDecryptionInfo(parsed[0], parsed[1], parsed[2], parsed[3],
|
||||
parsed[4], parsed[5], "message", clientIdBlobMissing, devicePrivateKeyMissing);
|
||||
if (drmInfo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
isNew = await DownloadDRMVideo(parsed[1], parsed[2], parsed[3], parsed[0],
|
||||
drmInfo.Value.decryptionKey, path, drmInfo.Value.lastModified, paidMessageKVP.Key,
|
||||
"Messages", progressReporter, singlePaidMsgPath + "/Videos", filenameFormat,
|
||||
messageInfo, mediaInfo, messageInfo?.FromUser, users);
|
||||
}
|
||||
else
|
||||
{
|
||||
isNew = await DownloadMedia(paidMessageKVP.Value, path,
|
||||
paidMessageKVP.Key, "Messages", progressReporter,
|
||||
singlePaidMsgPath, filenameFormat, messageInfo, mediaInfo, messageInfo?.FromUser, users);
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
totalNew++;
|
||||
}
|
||||
else
|
||||
{
|
||||
totalOld++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int totalCount = singlePaidMessageCollection.PreviewSingleMessages.Count +
|
||||
singlePaidMessageCollection.SingleMessages.Count;
|
||||
Log.Debug($"Paid Messages Already Downloaded: {totalOld} New Paid Messages Downloaded: {totalNew}");
|
||||
return new DownloadResult
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
NewDownloads = totalNew,
|
||||
ExistingDownloads = totalOld,
|
||||
MediaType = "Paid Messages",
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ public class FileNameService(IAuthService authService) : IFileNameService
|
||||
else
|
||||
{
|
||||
object? nestedPropertyValue = GetNestedPropertyValue(author, "Id");
|
||||
if (nestedPropertyValue != null)
|
||||
if (nestedPropertyValue != null && users != null)
|
||||
{
|
||||
values.Add(propertyName,
|
||||
users.FirstOrDefault(u => u.Value == Convert.ToInt32(nestedPropertyValue.ToString())).Key);
|
||||
@ -132,11 +132,11 @@ public class FileNameService(IAuthService authService) : IFileNameService
|
||||
if (propertyValue != null)
|
||||
{
|
||||
HtmlDocument pageDoc = new();
|
||||
pageDoc.LoadHtml(propertyValue.ToString());
|
||||
pageDoc.LoadHtml(propertyValue.ToString() ?? "");
|
||||
string str = pageDoc.DocumentNode.InnerText;
|
||||
if (str.Length > 100) // todo: add length limit to config
|
||||
if (str.Length > 100) // TODO: add length limit to config
|
||||
{
|
||||
str = str.Substring(0, 100);
|
||||
str = str[..100];
|
||||
}
|
||||
|
||||
values.Add(propertyName, str);
|
||||
|
||||
@ -6,13 +6,11 @@ using PostEntities = OF_DL.Models.Entities.Posts;
|
||||
using PurchasedEntities = OF_DL.Models.Entities.Purchased;
|
||||
using StreamEntities = OF_DL.Models.Entities.Streams;
|
||||
using UserEntities = OF_DL.Models.Entities.Users;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace OF_DL.Services;
|
||||
|
||||
public interface IAPIService
|
||||
{
|
||||
Task<string> GetDecryptionKeyCDRMProject(Dictionary<string, string> drmHeaders, string licenceURL, string pssh);
|
||||
Task<string> GetDecryptionKeyCDM(Dictionary<string, string> drmHeaders, string licenceURL, string pssh);
|
||||
Task<DateTime> GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp);
|
||||
Task<string> GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp);
|
||||
@ -24,21 +22,23 @@ public interface IAPIService
|
||||
|
||||
Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder, string username,
|
||||
List<long> paid_post_ids,
|
||||
StatusContext ctx);
|
||||
IStatusReporter statusReporter);
|
||||
|
||||
Task<PostEntities.PostCollection> GetPosts(string endpoint, string folder, List<long> paid_post_ids,
|
||||
StatusContext ctx);
|
||||
IStatusReporter statusReporter);
|
||||
|
||||
Task<PostEntities.SinglePostCollection> GetPost(string endpoint, string folder);
|
||||
|
||||
Task<StreamEntities.StreamsCollection> GetStreams(string endpoint, string folder, List<long> paid_post_ids,
|
||||
StatusContext ctx);
|
||||
IStatusReporter statusReporter);
|
||||
|
||||
Task<ArchivedEntities.ArchivedCollection> GetArchived(string endpoint, string folder, StatusContext ctx);
|
||||
Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder, StatusContext ctx);
|
||||
Task<ArchivedEntities.ArchivedCollection> GetArchived(string endpoint, string folder,
|
||||
IStatusReporter statusReporter);
|
||||
|
||||
Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder, IStatusReporter statusReporter);
|
||||
|
||||
Task<PurchasedEntities.PaidMessageCollection> GetPaidMessages(string endpoint, string folder, string username,
|
||||
StatusContext ctx);
|
||||
IStatusReporter statusReporter);
|
||||
|
||||
Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder);
|
||||
Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users);
|
||||
|
||||
@ -1,11 +1,30 @@
|
||||
using OF_DL.Models;
|
||||
using UserEntities = OF_DL.Models.Entities.Users;
|
||||
|
||||
namespace OF_DL.Services;
|
||||
|
||||
public interface IAuthService
|
||||
{
|
||||
Auth? CurrentAuth { get; set; }
|
||||
|
||||
Task<bool> LoadFromFileAsync(string filePath = "auth.json");
|
||||
|
||||
Task<bool> LoadFromBrowserAsync();
|
||||
|
||||
Task SaveToFileAsync(string filePath = "auth.json");
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up the cookie string to only contain auth_id and sess cookies.
|
||||
/// </summary>
|
||||
void ValidateCookieString();
|
||||
|
||||
/// <summary>
|
||||
/// Validates auth by calling the API and returns the user info if valid.
|
||||
/// </summary>
|
||||
Task<UserEntities.User?> ValidateAuthAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Logs out by deleting chrome-data and auth.json.
|
||||
/// </summary>
|
||||
void Logout();
|
||||
}
|
||||
|
||||
@ -5,8 +5,22 @@ namespace OF_DL.Services;
|
||||
public interface IConfigService
|
||||
{
|
||||
Config CurrentConfig { get; }
|
||||
|
||||
bool IsCliNonInteractive { get; }
|
||||
|
||||
Task<bool> LoadConfigurationAsync(string[] args);
|
||||
|
||||
Task SaveConfigurationAsync(string filePath = "config.conf");
|
||||
|
||||
void UpdateConfig(Config newConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Returns property names and current values for toggleable config properties.
|
||||
/// </summary>
|
||||
List<(string Name, bool Value)> GetToggleableProperties();
|
||||
|
||||
/// <summary>
|
||||
/// Applies selected toggleable properties. Returns true if any changed.
|
||||
/// </summary>
|
||||
bool ApplyToggleableSelections(List<string> selectedNames);
|
||||
}
|
||||
|
||||
63
OF DL/Services/IDownloadEventHandler.cs
Normal file
63
OF DL/Services/IDownloadEventHandler.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using OF_DL.Models;
|
||||
|
||||
namespace OF_DL.Services;
|
||||
|
||||
/// <summary>
|
||||
/// UI callback contract for download orchestration. Implementations handle
|
||||
/// status display, progress bars, and notifications in a UI-framework-specific way.
|
||||
/// </summary>
|
||||
public interface IDownloadEventHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps work in a status indicator (spinner) during API fetching.
|
||||
/// The implementation controls how the status is displayed.
|
||||
/// </summary>
|
||||
Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work);
|
||||
|
||||
/// <summary>
|
||||
/// Wraps work in a progress bar during downloading.
|
||||
/// The implementation controls how progress is displayed.
|
||||
/// </summary>
|
||||
Task<T> WithProgressAsync<T>(string description, long maxValue, bool showSize,
|
||||
Func<IProgressReporter, Task<T>> work);
|
||||
|
||||
/// <summary>
|
||||
/// Called when content of a specific type is found for a creator.
|
||||
/// </summary>
|
||||
void OnContentFound(string contentType, int mediaCount, int objectCount);
|
||||
|
||||
/// <summary>
|
||||
/// Called when no content of a specific type is found for a creator.
|
||||
/// </summary>
|
||||
void OnNoContentFound(string contentType);
|
||||
|
||||
/// <summary>
|
||||
/// Called when downloading of a content type completes.
|
||||
/// </summary>
|
||||
void OnDownloadComplete(string contentType, DownloadResult result);
|
||||
|
||||
/// <summary>
|
||||
/// Called when starting to process a specific user/creator.
|
||||
/// </summary>
|
||||
void OnUserStarting(string username);
|
||||
|
||||
/// <summary>
|
||||
/// Called when all downloads for a user/creator are complete.
|
||||
/// </summary>
|
||||
void OnUserComplete(string username, CreatorDownloadResult result);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a purchased tab user's downloads are complete.
|
||||
/// </summary>
|
||||
void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the entire scrape operation completes.
|
||||
/// </summary>
|
||||
void OnScrapeComplete(TimeSpan elapsed);
|
||||
|
||||
/// <summary>
|
||||
/// General status message display.
|
||||
/// </summary>
|
||||
void OnMessage(string message);
|
||||
}
|
||||
73
OF DL/Services/IDownloadOrchestrationService.cs
Normal file
73
OF DL/Services/IDownloadOrchestrationService.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using OF_DL.Models;
|
||||
using UserEntities = OF_DL.Models.Entities.Users;
|
||||
|
||||
namespace OF_DL.Services;
|
||||
|
||||
public interface IDownloadOrchestrationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetch subscriptions, lists, filter ignored users.
|
||||
/// </summary>
|
||||
Task<UserListResult> GetAvailableUsersAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get users for a specific list by name.
|
||||
/// </summary>
|
||||
Task<Dictionary<string, long>> GetUsersForListAsync(
|
||||
string listName, Dictionary<string, long> allUsers, Dictionary<string, long> lists);
|
||||
|
||||
/// <summary>
|
||||
/// Resolve download path for a username based on config.
|
||||
/// </summary>
|
||||
string ResolveDownloadPath(string username);
|
||||
|
||||
/// <summary>
|
||||
/// Prepare user folder (create dir, check username, create DB).
|
||||
/// </summary>
|
||||
Task PrepareUserFolderAsync(string username, long userId, string path);
|
||||
|
||||
/// <summary>
|
||||
/// Download all configured content types for a single creator.
|
||||
/// </summary>
|
||||
Task<CreatorDownloadResult> DownloadCreatorContentAsync(
|
||||
string username, long userId, string path,
|
||||
Dictionary<string, long> users,
|
||||
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
IDownloadEventHandler eventHandler);
|
||||
|
||||
/// <summary>
|
||||
/// Download a single post by ID.
|
||||
/// </summary>
|
||||
Task DownloadSinglePostAsync(
|
||||
string username, long postId, string path,
|
||||
Dictionary<string, long> users,
|
||||
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
IDownloadEventHandler eventHandler);
|
||||
|
||||
/// <summary>
|
||||
/// Download purchased tab content for all users.
|
||||
/// </summary>
|
||||
Task DownloadPurchasedTabAsync(
|
||||
Dictionary<string, long> users,
|
||||
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
IDownloadEventHandler eventHandler);
|
||||
|
||||
/// <summary>
|
||||
/// Download a single paid message by message ID.
|
||||
/// </summary>
|
||||
Task DownloadSinglePaidMessageAsync(
|
||||
string username, long messageId, string path,
|
||||
Dictionary<string, long> users,
|
||||
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
IDownloadEventHandler eventHandler);
|
||||
|
||||
/// <summary>
|
||||
/// Resolve username from user ID via API.
|
||||
/// </summary>
|
||||
Task<string?> ResolveUsernameAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// Tracks paid post IDs across downloads.
|
||||
/// </summary>
|
||||
List<long> PaidPostIds { get; }
|
||||
}
|
||||
@ -62,4 +62,21 @@ public interface IDownloadService
|
||||
Task<DownloadResult> DownloadPaidPosts(string username, long userId, string path, Dictionary<string, long> users,
|
||||
bool clientIdBlobMissing, bool devicePrivateKeyMissing, PurchasedEntities.PaidPostCollection purchasedPosts,
|
||||
IProgressReporter progressReporter);
|
||||
|
||||
Task<DownloadResult> DownloadPaidPostsPurchasedTab(string username, string path,
|
||||
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter);
|
||||
|
||||
Task<DownloadResult> DownloadPaidMessagesPurchasedTab(string username, string path,
|
||||
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter);
|
||||
|
||||
Task<DownloadResult> DownloadSinglePost(string username, string path,
|
||||
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
PostEntities.SinglePostCollection post, IProgressReporter progressReporter);
|
||||
|
||||
Task<DownloadResult> DownloadSinglePaidMessage(string username, string path,
|
||||
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||
PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection,
|
||||
IProgressReporter progressReporter);
|
||||
}
|
||||
|
||||
10
OF DL/Services/IStartupService.cs
Normal file
10
OF DL/Services/IStartupService.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using OF_DL.Models;
|
||||
|
||||
namespace OF_DL.Services;
|
||||
|
||||
public interface IStartupService
|
||||
{
|
||||
Task<StartupResult> ValidateEnvironmentAsync();
|
||||
|
||||
Task<VersionCheckResult> CheckVersionAsync();
|
||||
}
|
||||
14
OF DL/Services/IStatusReporter.cs
Normal file
14
OF DL/Services/IStatusReporter.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace OF_DL.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for reporting status updates in a UI-agnostic way.
|
||||
/// This replaces Spectre.Console's StatusContext in the service layer.
|
||||
/// </summary>
|
||||
public interface IStatusReporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports a status message (e.g., "Getting Posts\n Found 42").
|
||||
/// The reporter implementation decides how to format and display the message.
|
||||
/// </summary>
|
||||
void ReportStatus(string message);
|
||||
}
|
||||
254
OF DL/Services/StartupService.cs
Normal file
254
OF DL/Services/StartupService.cs
Normal file
@ -0,0 +1,254 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using Newtonsoft.Json;
|
||||
using OF_DL.Helpers;
|
||||
using OF_DL.Models;
|
||||
using Serilog;
|
||||
using WidevineConstants = OF_DL.Widevine.Constants;
|
||||
|
||||
namespace OF_DL.Services;
|
||||
|
||||
public class StartupService(IConfigService configService, IAuthService authService) : IStartupService
|
||||
{
|
||||
public async Task<StartupResult> ValidateEnvironmentAsync()
|
||||
{
|
||||
StartupResult result = new();
|
||||
|
||||
// OS validation
|
||||
OperatingSystem os = Environment.OSVersion;
|
||||
result.OsVersionString = os.VersionString;
|
||||
Log.Debug($"Operating system information: {os.VersionString}");
|
||||
|
||||
if (os.Platform == PlatformID.Win32NT && os.Version.Major < 10)
|
||||
{
|
||||
result.IsWindowsVersionValid = false;
|
||||
Log.Error("Windows version prior to 10.x: {0}", os.VersionString);
|
||||
}
|
||||
|
||||
// FFmpeg detection
|
||||
DetectFfmpeg(result);
|
||||
|
||||
if (result.FfmpegFound)
|
||||
{
|
||||
// Escape backslashes for Windows
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
result.FfmpegPath!.Contains(@":\") &&
|
||||
!result.FfmpegPath.Contains(@":\\"))
|
||||
{
|
||||
result.FfmpegPath = result.FfmpegPath.Replace(@"\", @"\\");
|
||||
configService.CurrentConfig!.FFmpegPath = result.FfmpegPath;
|
||||
}
|
||||
|
||||
// Get FFmpeg version
|
||||
result.FfmpegVersion = await GetFfmpegVersionAsync(result.FfmpegPath!);
|
||||
}
|
||||
|
||||
// Widevine device checks
|
||||
result.ClientIdBlobMissing = !File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER,
|
||||
WidevineConstants.DEVICE_NAME, "device_client_id_blob"));
|
||||
result.DevicePrivateKeyMissing = !File.Exists(Path.Join(WidevineConstants.DEVICES_FOLDER,
|
||||
WidevineConstants.DEVICE_NAME, "device_private_key"));
|
||||
|
||||
if (!result.ClientIdBlobMissing)
|
||||
{
|
||||
Log.Debug("device_client_id_blob found");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("device_client_id_blob missing");
|
||||
}
|
||||
|
||||
if (!result.DevicePrivateKeyMissing)
|
||||
{
|
||||
Log.Debug("device_private_key found");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("device_private_key missing");
|
||||
}
|
||||
|
||||
// rules.json validation
|
||||
if (File.Exists("rules.json"))
|
||||
{
|
||||
result.RulesJsonExists = true;
|
||||
try
|
||||
{
|
||||
JsonConvert.DeserializeObject<DynamicRules>(File.ReadAllText("rules.json"));
|
||||
Log.Debug("Rules.json: ");
|
||||
Log.Debug(JsonConvert.SerializeObject(File.ReadAllText("rules.json"), Formatting.Indented));
|
||||
result.RulesJsonValid = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
result.RulesJsonError = e.Message;
|
||||
Log.Error("rules.json processing failed.", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<VersionCheckResult> CheckVersionAsync()
|
||||
{
|
||||
VersionCheckResult result = new();
|
||||
|
||||
#if !DEBUG
|
||||
try
|
||||
{
|
||||
result.LocalVersion = Assembly.GetEntryAssembly()?.GetName().Version;
|
||||
|
||||
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
|
||||
string? latestReleaseTag = null;
|
||||
|
||||
try
|
||||
{
|
||||
latestReleaseTag = await VersionHelper.GetLatestReleaseTag(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
result.TimedOut = true;
|
||||
Log.Warning("Version check timed out after 30 seconds");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (latestReleaseTag == null)
|
||||
{
|
||||
result.CheckFailed = true;
|
||||
Log.Error("Failed to get the latest release tag.");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.LatestVersion = new Version(latestReleaseTag.Replace("OFDLV", ""));
|
||||
int versionComparison = result.LocalVersion!.CompareTo(result.LatestVersion);
|
||||
result.IsUpToDate = versionComparison >= 0;
|
||||
|
||||
Log.Debug("Detected client running version " +
|
||||
$"{result.LocalVersion.Major}.{result.LocalVersion.Minor}.{result.LocalVersion.Build}");
|
||||
Log.Debug("Latest release version " +
|
||||
$"{result.LatestVersion.Major}.{result.LatestVersion.Minor}.{result.LatestVersion.Build}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
result.CheckFailed = true;
|
||||
Log.Error("Error checking latest release on GitHub.", e.Message);
|
||||
}
|
||||
#else
|
||||
Log.Debug("Running in Debug/Local mode. Version check skipped.");
|
||||
result.IsUpToDate = true;
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void DetectFfmpeg(StartupResult result)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configService.CurrentConfig!.FFmpegPath) &&
|
||||
ValidateFilePath(configService.CurrentConfig.FFmpegPath))
|
||||
{
|
||||
result.FfmpegFound = true;
|
||||
result.FfmpegPath = configService.CurrentConfig.FFmpegPath;
|
||||
Log.Debug($"FFMPEG found: {result.FfmpegPath}");
|
||||
Log.Debug("FFMPEG path set in config.conf");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(authService.CurrentAuth?.FfmpegPath) &&
|
||||
ValidateFilePath(authService.CurrentAuth.FfmpegPath))
|
||||
{
|
||||
result.FfmpegFound = true;
|
||||
result.FfmpegPath = authService.CurrentAuth.FfmpegPath;
|
||||
configService.CurrentConfig.FFmpegPath = result.FfmpegPath;
|
||||
Log.Debug($"FFMPEG found: {result.FfmpegPath}");
|
||||
Log.Debug("FFMPEG path set in auth.json");
|
||||
}
|
||||
else if (string.IsNullOrEmpty(configService.CurrentConfig.FFmpegPath))
|
||||
{
|
||||
string? ffmpegPath = GetFullPath("ffmpeg") ?? GetFullPath("ffmpeg.exe");
|
||||
if (ffmpegPath != null)
|
||||
{
|
||||
result.FfmpegFound = true;
|
||||
result.FfmpegPathAutoDetected = true;
|
||||
result.FfmpegPath = ffmpegPath;
|
||||
configService.CurrentConfig.FFmpegPath = ffmpegPath;
|
||||
Log.Debug($"FFMPEG found: {ffmpegPath}");
|
||||
Log.Debug("FFMPEG path found via PATH or current directory");
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.FfmpegFound)
|
||||
{
|
||||
Log.Error($"Cannot locate FFmpeg with path: {configService.CurrentConfig.FFmpegPath}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string?> GetFfmpegVersionAsync(string ffmpegPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessStartInfo processStartInfo = new()
|
||||
{
|
||||
FileName = ffmpegPath,
|
||||
Arguments = "-version",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using Process? process = Process.Start(processStartInfo);
|
||||
if (process != null)
|
||||
{
|
||||
string output = await process.StandardOutput.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
Log.Information("FFmpeg version output:\n{Output}", output);
|
||||
|
||||
string firstLine = output.Split('\n')[0].Trim();
|
||||
if (firstLine.StartsWith("ffmpeg version"))
|
||||
{
|
||||
int versionStart = "ffmpeg version ".Length;
|
||||
int copyrightIndex = firstLine.IndexOf(" Copyright");
|
||||
return copyrightIndex > versionStart
|
||||
? firstLine.Substring(versionStart, copyrightIndex - versionStart)
|
||||
: firstLine.Substring(versionStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to get FFmpeg version");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ValidateFilePath(string path)
|
||||
{
|
||||
char[] invalidChars = Path.GetInvalidPathChars();
|
||||
if (path.Any(c => invalidChars.Contains(c)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return File.Exists(path);
|
||||
}
|
||||
|
||||
public static string? GetFullPath(string filename)
|
||||
{
|
||||
if (File.Exists(filename))
|
||||
{
|
||||
return Path.GetFullPath(filename);
|
||||
}
|
||||
|
||||
string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
foreach (string path in pathEnv.Split(Path.PathSeparator))
|
||||
{
|
||||
string fullPath = Path.Combine(path, filename);
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -78,7 +78,7 @@ public class CDM
|
||||
}
|
||||
|
||||
Session session;
|
||||
dynamic parsedInitData = ParseInitData(initData);
|
||||
dynamic? parsedInitData = ParseInitData(initData);
|
||||
|
||||
if (parsedInitData != null)
|
||||
{
|
||||
@ -409,18 +409,22 @@ public class CDM
|
||||
byte[] decryptedKey;
|
||||
|
||||
using MemoryStream mstream = new();
|
||||
using AesCryptoServiceProvider aesProvider = new() { Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 };
|
||||
|
||||
using AesCryptoServiceProvider aesProvider = new();
|
||||
aesProvider.Mode = CipherMode.CBC;
|
||||
aesProvider.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<string> permissions = new();
|
||||
List<string> permissions = [];
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,20 +1,32 @@
|
||||
using Serilog;
|
||||
|
||||
namespace OF_DL.Widevine;
|
||||
|
||||
public class CDMApi
|
||||
{
|
||||
private string SessionId { get; set; }
|
||||
private string? SessionId { get; set; }
|
||||
|
||||
public byte[] GetChallenge(string initDataB64, string certDataB64, bool offline = false, bool raw = false)
|
||||
public byte[]? GetChallenge(string initDataB64, string certDataB64, bool offline = false, bool raw = false)
|
||||
{
|
||||
SessionId = CDM.OpenSession(initDataB64, Constants.DEVICE_NAME, offline, raw);
|
||||
if (SessionId == null)
|
||||
{
|
||||
Log.Debug("CDM.OpenSession returned null, unable to proceed with challenge generation");
|
||||
return null;
|
||||
}
|
||||
|
||||
CDM.SetServiceCertificate(SessionId, Convert.FromBase64String(certDataB64));
|
||||
return CDM.GetLicenseRequest(SessionId);
|
||||
}
|
||||
|
||||
public bool ProvideLicense(string licenseB64)
|
||||
public void ProvideLicense(string licenseB64)
|
||||
{
|
||||
if (SessionId == null)
|
||||
{
|
||||
throw new Exception("No session ID set. Could not provide license");
|
||||
}
|
||||
|
||||
CDM.ProvideLicense(SessionId, Convert.FromBase64String(licenseB64));
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<ContentKey> GetKeys() => CDM.GetKeys(SessionId);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user