Update GUI so that the stop button works quicker and more reliably

This commit is contained in:
whimsical-c4lic0 2026-02-14 12:30:31 -06:00
parent 35f7d98112
commit 85e299db41
13 changed files with 133 additions and 70 deletions

View File

@ -159,7 +159,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// </summary>
/// <param name="endpoint">The user endpoint.</param>
/// <returns>The user entity when available.</returns>
public async Task<UserEntities.User?> GetUserInfo(string endpoint)
public async Task<UserEntities.User?> GetUserInfo(string endpoint, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetUserInfo: {endpoint}");
@ -170,6 +170,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
try
{
cancellationToken.ThrowIfCancellationRequested();
UserEntities.User user = new();
Dictionary<string, string> getParams = new()
{
@ -179,7 +180,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
HttpClient client = new();
HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint);
using HttpResponseMessage response = await client.SendAsync(request);
using HttpResponseMessage response = await client.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
@ -415,7 +416,8 @@ public class ApiService(IAuthService authService, IConfigService configService,
public async Task<Dictionary<long, string>?> GetMedia(MediaType mediatype,
string endpoint,
string? username,
string folder)
string folder,
CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetMedia - {username}");
@ -426,6 +428,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
try
{
cancellationToken.ThrowIfCancellationRequested();
Dictionary<long, string> returnUrls = new();
const int limit = 5;
int offset = 0;
@ -649,7 +652,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// <returns>A paid post collection.</returns>
public async Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder,
string username,
List<long> paidPostIds, IStatusReporter statusReporter)
List<long> paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetPaidPosts - {username}");
@ -830,7 +833,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// <param name="statusReporter">Status reporter.</param>
/// <returns>A post collection.</returns>
public async Task<PostEntities.PostCollection> GetPosts(string endpoint, string folder, List<long> paidPostIds,
IStatusReporter statusReporter)
IStatusReporter statusReporter, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetPosts - {endpoint}");
@ -1018,7 +1021,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// <param name="endpoint">The post endpoint.</param>
/// <param name="folder">The creator folder path.</param>
/// <returns>A single post collection.</returns>
public async Task<SinglePostCollection> GetPost(string endpoint, string folder)
public async Task<SinglePostCollection> GetPost(string endpoint, string folder, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetPost - {endpoint}");
@ -1168,7 +1171,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// <returns>A streams collection.</returns>
public async Task<StreamEntities.StreamsCollection> GetStreams(string endpoint, string folder,
List<long> paidPostIds,
IStatusReporter statusReporter)
IStatusReporter statusReporter, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetStreams - {endpoint}");
@ -1319,7 +1322,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// <param name="statusReporter">Status reporter.</param>
/// <returns>An archived collection.</returns>
public async Task<ArchivedEntities.ArchivedCollection> GetArchived(string endpoint, string folder,
IStatusReporter statusReporter)
IStatusReporter statusReporter, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetArchived - {endpoint}");
@ -1473,7 +1476,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// <param name="statusReporter">Status reporter.</param>
/// <returns>A message collection.</returns>
public async Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder,
IStatusReporter statusReporter)
IStatusReporter statusReporter, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetMessages - {endpoint}");
@ -1661,7 +1664,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// <param name="endpoint">The paid message endpoint.</param>
/// <param name="folder">The creator folder path.</param>
/// <returns>A single paid message collection.</returns>
public async Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder)
public async Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetPaidMessage - {endpoint}");
@ -1809,7 +1812,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// <returns>A paid message collection.</returns>
public async Task<PurchasedEntities.PaidMessageCollection> GetPaidMessages(string endpoint, string folder,
string username,
IStatusReporter statusReporter)
IStatusReporter statusReporter, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetPaidMessages - {username}");
@ -2031,7 +2034,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// <param name="endpoint">The purchased tab endpoint.</param>
/// <param name="users">Known users map.</param>
/// <returns>A username-to-userId map.</returns>
public async Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users)
public async Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetPurchasedTabUsers - {endpoint}");
@ -2046,6 +2049,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
{ "skip_users", "all" }
};
cancellationToken.ThrowIfCancellationRequested();
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
if (body == null)
{
@ -2060,6 +2064,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
getParams["offset"] = purchased.List.Count.ToString();
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
PurchasedEntities.Purchased newPurchased;
Dictionary<string, string> loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams);
@ -2073,7 +2078,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
looprequest.Headers.Add(keyValuePair.Key, keyValuePair.Value);
}
using (HttpResponseMessage loopresponse = await loopclient.SendAsync(looprequest))
using (HttpResponseMessage loopresponse = await loopclient.SendAsync(looprequest, cancellationToken))
{
loopresponse.EnsureSuccessStatusCode();
string loopbody = await loopresponse.Content.ReadAsStringAsync();
@ -2098,6 +2103,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
foreach (PurchasedEntities.ListItem purchase in
purchased.List.OrderByDescending(p => p.PostedAt ?? p.CreatedAt))
{
cancellationToken.ThrowIfCancellationRequested();
long fromUserId = purchase.FromUser?.Id ?? 0;
long authorId = purchase.Author?.Id ?? 0;
@ -2197,7 +2203,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
/// <param name="users">Known users map.</param>
/// <returns>A list of purchased tab collections.</returns>
public async Task<List<PurchasedEntities.PurchasedTabCollection>> GetPurchasedTab(string endpoint, string folder,
Dictionary<string, long> users)
Dictionary<string, long> users, CancellationToken cancellationToken = default)
{
Log.Debug($"Calling GetPurchasedTab - {endpoint}");
@ -2213,6 +2219,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
{ "skip_users", "all" }
};
cancellationToken.ThrowIfCancellationRequested();
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
PurchasedDtos.PurchasedDto? purchasedDto =
DeserializeJson<PurchasedDtos.PurchasedDto>(body, s_mJsonSerializerSettings);
@ -2222,6 +2229,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
getParams["offset"] = purchased.List.Count.ToString();
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
string loopqueryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
PurchasedEntities.Purchased newPurchased;
Dictionary<string, string> loopheaders = GetDynamicHeaders("/api2/v2" + endpoint, loopqueryParams);
@ -2235,7 +2243,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
looprequest.Headers.Add(keyValuePair.Key, keyValuePair.Value);
}
using (HttpResponseMessage loopresponse = await loopclient.SendAsync(looprequest))
using (HttpResponseMessage loopresponse = await loopclient.SendAsync(looprequest, cancellationToken))
{
loopresponse.EnsureSuccessStatusCode();
string loopbody = await loopresponse.Content.ReadAsStringAsync();
@ -2260,6 +2268,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
foreach (PurchasedEntities.ListItem purchase in
purchased.List.OrderByDescending(p => p.PostedAt ?? p.CreatedAt))
{
cancellationToken.ThrowIfCancellationRequested();
if (purchase.FromUser != null)
{
if (!userPurchases.ContainsKey(purchase.FromUser.Id))
@ -2283,6 +2292,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
foreach (KeyValuePair<long, List<PurchasedEntities.ListItem>> user in userPurchases)
{
cancellationToken.ThrowIfCancellationRequested();
PurchasedEntities.PurchasedTabCollection purchasedTabCollection = new();
JObject? userObject = await GetUserInfoById($"/users/list?x[]={user.Key}");
purchasedTabCollection.UserId = user.Key;

View File

@ -171,11 +171,13 @@ public class DownloadOrchestrationService(
eventHandler.OnUserStarting(username);
Log.Debug($"Scraping Data for {username}");
eventHandler.CancellationToken.ThrowIfCancellationRequested();
await PrepareUserFolderAsync(username, userId, path);
if (config.DownloadAvatarHeaderPhoto)
{
UserEntities.User? userInfo = await apiService.GetUserInfo($"/users/{username}");
eventHandler.CancellationToken.ThrowIfCancellationRequested();
UserEntities.User? userInfo = await apiService.GetUserInfo($"/users/{username}", eventHandler.CancellationToken);
if (userInfo != null)
{
await downloadService.DownloadAvatarHeader(userInfo.Avatar, userInfo.Header, path, username);
@ -240,9 +242,10 @@ public class DownloadOrchestrationService(
if (config.DownloadStories)
{
eventHandler.CancellationToken.ThrowIfCancellationRequested();
eventHandler.OnMessage("Getting Stories");
Dictionary<long, string>? tempStories = await apiService.GetMedia(MediaType.Stories,
$"/users/{userId}/stories", null, path);
$"/users/{userId}/stories", null, path, eventHandler.CancellationToken);
if (tempStories is { Count: > 0 })
{
@ -268,9 +271,10 @@ public class DownloadOrchestrationService(
if (config.DownloadHighlights)
{
eventHandler.CancellationToken.ThrowIfCancellationRequested();
eventHandler.OnMessage("Getting Highlights");
Dictionary<long, string>? tempHighlights = await apiService.GetMedia(MediaType.Highlights,
$"/users/{userId}/stories/highlights", null, path);
$"/users/{userId}/stories/highlights", null, path, eventHandler.CancellationToken);
if (tempHighlights is { Count: > 0 })
{
@ -387,13 +391,19 @@ public class DownloadOrchestrationService(
{
Config config = configService.CurrentConfig;
eventHandler.OnMessage("Fetching purchased tab users...");
eventHandler.CancellationToken.ThrowIfCancellationRequested();
Dictionary<string, long> purchasedTabUsers =
await apiService.GetPurchasedTabUsers("/posts/paid/all", users);
await apiService.GetPurchasedTabUsers("/posts/paid/all", users, eventHandler.CancellationToken);
eventHandler.OnMessage("Checking folders for users in Purchased Tab");
eventHandler.CancellationToken.ThrowIfCancellationRequested();
foreach (KeyValuePair<string, long> user in purchasedTabUsers)
{
eventHandler.CancellationToken.ThrowIfCancellationRequested();
string path = ResolveDownloadPath(user.Key);
Log.Debug($"Download path: {path}");
@ -405,7 +415,7 @@ public class DownloadOrchestrationService(
Log.Debug($"Created folder for {user.Key}");
}
await apiService.GetUserInfo($"/users/{user.Key}");
await apiService.GetUserInfo($"/users/{user.Key}", eventHandler.CancellationToken);
await dbService.CreateDb(path);
}
@ -415,11 +425,16 @@ public class DownloadOrchestrationService(
Log.Debug($"Download path: {basePath}");
eventHandler.OnMessage("Fetching purchased tab content...");
eventHandler.CancellationToken.ThrowIfCancellationRequested();
List<PurchasedEntities.PurchasedTabCollection> purchasedTabCollections =
await apiService.GetPurchasedTab("/posts/paid/all", basePath, users);
await apiService.GetPurchasedTab("/posts/paid/all", basePath, users, eventHandler.CancellationToken);
foreach (PurchasedEntities.PurchasedTabCollection purchasedTabCollection in purchasedTabCollections)
{
eventHandler.CancellationToken.ThrowIfCancellationRequested();
eventHandler.OnUserStarting(purchasedTabCollection.Username);
string path = ResolveDownloadPath(purchasedTabCollection.Username);
Log.Debug($"Download path: {path}");

View File

@ -191,7 +191,7 @@ public class DownloadService(
await OnFFMPEGDownloadComplete(tempFilename, lastModified, folder, path, customFileName, filename,
mediaId, apiType, progressReporter);
};
await ffmpeg.ExecuteAsync(parameters, CancellationToken.None);
await ffmpeg.ExecuteAsync(parameters, progressReporter.CancellationToken);
return await _completionSource.Task;
}
@ -747,9 +747,9 @@ public class DownloadService(
using HttpClient client = new();
HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = new Uri(url) };
using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, progressReporter.CancellationToken);
response.EnsureSuccessStatusCode();
Stream body = await response.Content.ReadAsStreamAsync();
Stream body = await response.Content.ReadAsStreamAsync(progressReporter.CancellationToken);
// Wrap the body stream with the ThrottledStream to limit read rate.
await using (ThrottledStream throttledStream = new(body,
@ -761,14 +761,14 @@ public class DownloadService(
true);
byte[] buffer = new byte[16384];
int read;
while ((read = await throttledStream.ReadAsync(buffer, CancellationToken.None)) > 0)
while ((read = await throttledStream.ReadAsync(buffer, progressReporter.CancellationToken)) > 0)
{
if (configService.CurrentConfig.ShowScrapeSize)
{
progressReporter.ReportProgress(read);
}
await fileStream.WriteAsync(buffer.AsMemory(0, read), CancellationToken.None);
await fileStream.WriteAsync(buffer.AsMemory(0, read), progressReporter.CancellationToken);
}
}

View File

@ -39,69 +39,69 @@ public interface IApiService
/// <summary>
/// Retrieves media URLs for stories or highlights.
/// </summary>
Task<Dictionary<long, string>?> GetMedia(MediaType mediaType, string endpoint, string? username, string folder);
Task<Dictionary<long, string>?> GetMedia(MediaType mediaType, string endpoint, string? username, string folder, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves paid posts and their media.
/// </summary>
Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder, string username,
List<long> paidPostIds,
IStatusReporter statusReporter);
IStatusReporter statusReporter, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves posts and their media.
/// </summary>
Task<PostEntities.PostCollection> GetPosts(string endpoint, string folder, List<long> paidPostIds,
IStatusReporter statusReporter);
IStatusReporter statusReporter, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a single post and its media.
/// </summary>
Task<PostEntities.SinglePostCollection> GetPost(string endpoint, string folder);
Task<PostEntities.SinglePostCollection> GetPost(string endpoint, string folder, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves streams and their media.
/// </summary>
Task<StreamEntities.StreamsCollection> GetStreams(string endpoint, string folder, List<long> paidPostIds,
IStatusReporter statusReporter);
IStatusReporter statusReporter, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves archived posts and their media.
/// </summary>
Task<ArchivedEntities.ArchivedCollection> GetArchived(string endpoint, string folder,
IStatusReporter statusReporter);
IStatusReporter statusReporter, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves messages and their media.
/// </summary>
Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder, IStatusReporter statusReporter);
Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder, IStatusReporter statusReporter, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves paid messages and their media.
/// </summary>
Task<PurchasedEntities.PaidMessageCollection> GetPaidMessages(string endpoint, string folder, string username,
IStatusReporter statusReporter);
IStatusReporter statusReporter, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a single paid message and its media.
/// </summary>
Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder);
Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves users that appear in the Purchased tab.
/// </summary>
Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users);
Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves Purchased tab content grouped by user.
/// </summary>
Task<List<PurchasedEntities.PurchasedTabCollection>> GetPurchasedTab(string endpoint, string folder,
Dictionary<string, long> users);
Dictionary<string, long> users, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves user information.
/// </summary>
Task<UserEntities.User?> GetUserInfo(string endpoint);
Task<UserEntities.User?> GetUserInfo(string endpoint, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves user information by ID.

View File

@ -8,6 +8,11 @@ namespace OF_DL.Services;
/// </summary>
public interface IDownloadEventHandler
{
/// <summary>
/// Gets the cancellation token for the operation.
/// </summary>
CancellationToken CancellationToken { get; }
/// <summary>
/// Wraps work in a status indicator (spinner) during API fetching.
/// The implementation controls how the status is displayed.

View File

@ -11,4 +11,9 @@ public interface IProgressReporter
/// </summary>
/// <param name="increment">The amount to increment progress by</param>
void ReportProgress(long increment);
/// <summary>
/// Gets the cancellation token for canceling the operation.
/// </summary>
CancellationToken CancellationToken { get; }
}

View File

@ -9,8 +9,11 @@ internal sealed class AvaloniaDownloadEventHandler(
Action<string, long, bool> progressStart,
Action<long> progressIncrement,
Action progressStop,
Func<bool> isCancellationRequested) : IDownloadEventHandler
Func<bool> isCancellationRequested,
CancellationToken cancellationToken) : IDownloadEventHandler
{
public CancellationToken CancellationToken { get; } = cancellationToken;
public async Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work)
{
ThrowIfCancellationRequested();
@ -33,7 +36,7 @@ internal sealed class AvaloniaDownloadEventHandler(
progressStart(description, maxValue, showSize);
try
{
AvaloniaProgressReporter reporter = new(progressIncrement, isCancellationRequested);
AvaloniaProgressReporter reporter = new(progressIncrement, isCancellationRequested, cancellationToken);
return await work(reporter);
}
finally

View File

@ -4,8 +4,11 @@ namespace OF_DL.Gui.Services;
internal sealed class AvaloniaProgressReporter(
Action<long> reportAction,
Func<bool> isCancellationRequested) : IProgressReporter
Func<bool> isCancellationRequested,
CancellationToken cancellationToken) : IProgressReporter
{
public CancellationToken CancellationToken { get; } = cancellationToken;
public void ReportProgress(long increment)
{
if (isCancellationRequested())

View File

@ -598,6 +598,17 @@ public partial class MainWindowViewModel(
AppendLog(downloadPurchasedTabOnly
? "Starting Purchased Tab download."
: $"Starting download for {selectedUsers.Count} users.");
StatusMessage = downloadPurchasedTabOnly
? "Starting Purchased Tab download..."
: $"Starting download for {selectedUsers.Count} users...";
// Show progress bar immediately with indeterminate state
StartDownloadProgress(
downloadPurchasedTabOnly
? "Initializing Purchased Tab download..."
: $"Initializing download for {selectedUsers.Count} users...",
0,
false);
AvaloniaDownloadEventHandler eventHandler = new(
AppendLog,
@ -605,7 +616,8 @@ public partial class MainWindowViewModel(
StartDownloadProgress,
IncrementDownloadProgress,
StopDownloadProgress,
() => _workCancellationSource?.IsCancellationRequested == true);
() => _workCancellationSource?.IsCancellationRequested == true,
_workCancellationSource.Token);
try
{

View File

@ -773,16 +773,18 @@
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Users" />
<CheckBox IsThreeState="True"
IsChecked="{Binding AllUsersSelected}"
Content="{Binding SelectedUsersSummary}" />
Content="{Binding SelectedUsersSummary}"
IsEnabled="{Binding !IsDownloading}" />
</StackPanel>
<ComboBox Grid.Column="1"
Width="280"
VerticalAlignment="Center"
PlaceholderText="Select a list of users"
ItemsSource="{Binding UserLists}"
SelectedItem="{Binding SelectedListName}" />
SelectedItem="{Binding SelectedListName}"
IsEnabled="{Binding !IsDownloading}" />
</Grid>
<ListBox Grid.Row="1" Margin="0,8,0,0" ItemsSource="{Binding AvailableUsers}">
<ListBox Grid.Row="1" Margin="0,8,0,0" ItemsSource="{Binding AvailableUsers}" IsEnabled="{Binding !IsDownloading}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0" />

View File

@ -42,6 +42,8 @@ internal sealed class ProgressRecorder : IProgressReporter
{
public long Total { get; private set; }
public CancellationToken CancellationToken { get; } = CancellationToken.None;
public void ReportProgress(long increment) => Total += increment;
}
@ -138,44 +140,44 @@ internal sealed class StaticApiService : IApiService
new() { { "X-Test", "value" } };
public Task<Dictionary<long, string>?> GetMedia(MediaType mediaType, string endpoint, string? username,
string folder) => Task.FromResult(MediaToReturn);
string folder, CancellationToken cancellationToken = default) => Task.FromResult(MediaToReturn);
public Task<Dictionary<string, long>?> GetLists(string endpoint) => throw new NotImplementedException();
public Task<List<string>?> GetListUsers(string endpoint) => throw new NotImplementedException();
public Task<OF_DL.Models.Entities.Purchased.PaidPostCollection> GetPaidPosts(string endpoint, string folder,
string username, List<long> paidPostIds, IStatusReporter statusReporter) =>
string username, List<long> paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<OF_DL.Models.Entities.Posts.PostCollection> GetPosts(string endpoint, string folder,
List<long> paidPostIds, IStatusReporter statusReporter) => throw new NotImplementedException();
List<long> paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task<OF_DL.Models.Entities.Posts.SinglePostCollection> GetPost(string endpoint, string folder) =>
public Task<OF_DL.Models.Entities.Posts.SinglePostCollection> GetPost(string endpoint, string folder, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<OF_DL.Models.Entities.Streams.StreamsCollection> GetStreams(string endpoint, string folder,
List<long> paidPostIds, IStatusReporter statusReporter) => throw new NotImplementedException();
List<long> paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task<OF_DL.Models.Entities.Archived.ArchivedCollection> GetArchived(string endpoint, string folder,
IStatusReporter statusReporter) => throw new NotImplementedException();
IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task<OF_DL.Models.Entities.Messages.MessageCollection> GetMessages(string endpoint, string folder,
IStatusReporter statusReporter) => throw new NotImplementedException();
IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task<OF_DL.Models.Entities.Purchased.PaidMessageCollection> GetPaidMessages(string endpoint,
string folder, string username, IStatusReporter statusReporter) => throw new NotImplementedException();
string folder, string username, IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task<OF_DL.Models.Entities.Purchased.SinglePaidMessageCollection> GetPaidMessage(string endpoint,
string folder) => throw new NotImplementedException();
string folder, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users) =>
public Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<List<OF_DL.Models.Entities.Purchased.PurchasedTabCollection>> GetPurchasedTab(string endpoint,
string folder, Dictionary<string, long> users) => throw new NotImplementedException();
string folder, Dictionary<string, long> users, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task<UserEntities.User?> GetUserInfo(string endpoint) =>
public Task<UserEntities.User?> GetUserInfo(string endpoint, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<JObject?> GetUserInfoById(string endpoint) =>
@ -217,52 +219,52 @@ internal sealed class ConfigurableApiService : IApiService
ListUsersHandler?.Invoke(endpoint) ?? Task.FromResult<List<string>?>(null);
public Task<Dictionary<long, string>?> GetMedia(MediaType mediaType, string endpoint, string? username,
string folder) =>
string folder, CancellationToken cancellationToken = default) =>
MediaHandler?.Invoke(mediaType, endpoint, username, folder) ??
Task.FromResult<Dictionary<long, string>?>(null);
public Task<PostEntities.SinglePostCollection> GetPost(string endpoint, string folder) =>
public Task<PostEntities.SinglePostCollection> GetPost(string endpoint, string folder, CancellationToken cancellationToken = default) =>
PostHandler?.Invoke(endpoint, folder) ?? Task.FromResult(new PostEntities.SinglePostCollection());
public Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder) =>
public Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder, CancellationToken cancellationToken = default) =>
PaidMessageHandler?.Invoke(endpoint, folder) ??
Task.FromResult(new PurchasedEntities.SinglePaidMessageCollection());
public Task<UserEntities.User?> GetUserInfo(string endpoint) =>
public Task<UserEntities.User?> GetUserInfo(string endpoint, CancellationToken cancellationToken = default) =>
UserInfoHandler?.Invoke(endpoint) ?? Task.FromResult<UserEntities.User?>(null);
public Task<JObject?> GetUserInfoById(string endpoint) =>
UserInfoByIdHandler?.Invoke(endpoint) ?? Task.FromResult<JObject?>(null);
public Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder, string username,
List<long> paidPostIds, IStatusReporter statusReporter) =>
List<long> paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<PostEntities.PostCollection> GetPosts(string endpoint, string folder, List<long> paidPostIds,
IStatusReporter statusReporter) =>
IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<StreamEntities.StreamsCollection> GetStreams(string endpoint, string folder, List<long> paidPostIds,
IStatusReporter statusReporter) =>
IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<ArchivedEntities.ArchivedCollection> GetArchived(string endpoint, string folder,
IStatusReporter statusReporter) =>
IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder,
IStatusReporter statusReporter) =>
IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<PurchasedEntities.PaidMessageCollection> GetPaidMessages(string endpoint, string folder,
string username, IStatusReporter statusReporter) =>
string username, IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users) =>
public Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<List<PurchasedEntities.PurchasedTabCollection>> GetPurchasedTab(string endpoint, string folder,
Dictionary<string, long> users) =>
Dictionary<string, long> users, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Dictionary<string, string> GetDynamicHeaders(string path, string queryParam) =>
@ -427,6 +429,8 @@ internal sealed class RecordingDownloadEventHandler : IDownloadEventHandler
public List<(string contentType, DownloadResult result)> DownloadCompletes { get; } = [];
public List<(string description, long maxValue, bool showSize)> ProgressCalls { get; } = [];
public CancellationToken CancellationToken { get; } = CancellationToken.None;
public Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work) =>
work(new RecordingStatusReporter(statusMessage));

View File

@ -10,6 +10,8 @@ namespace OF_DL.CLI;
/// </summary>
public class SpectreDownloadEventHandler : IDownloadEventHandler
{
public CancellationToken CancellationToken { get; } = CancellationToken.None;
public async Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work)
{
TaskCompletionSource<T> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);

View File

@ -6,9 +6,11 @@ namespace OF_DL.CLI;
/// <summary>
/// Implementation of IProgressReporter that uses Spectre.Console's ProgressTask for CLI output.
/// </summary>
public class SpectreProgressReporter(ProgressTask task) : IProgressReporter
public class SpectreProgressReporter(ProgressTask task, CancellationToken cancellationToken = default) : IProgressReporter
{
private readonly ProgressTask _task = task ?? throw new ArgumentNullException(nameof(task));
public CancellationToken CancellationToken { get; } = cancellationToken;
public void ReportProgress(long increment) => _task.Increment(increment);
}