diff --git a/README.md b/README.md index 2d81db9..3afbc3f 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ For all examples, assume: using SkylabStudio; var apiClient = new StudioClient("YOUR_SKYLAB_API_TOKEN"); + +// option to configure max concurrent downloads (for when using DownloadAllPhotos method) +// defaults to 5 concurrent downloads at a time +var studioOptions = new StudioOptions { MaxConcurrentDownloads = 5 }; +var apiClient = new StudioClient(Environment.GetEnvironmentVariable("SKYLAB_API_TOKEN"), studioOptions); ``` ```dotnet @@ -138,12 +143,6 @@ api.UpdateProfile(profileId, new { name = $"Test Profile", enable_crop = false, For all payload options, consult the [API documentation](https://studio-docs.skylabtech.ai/#tag/profile/operation/updateProfileById). -#### List all photos - -```dotnet -api.ListPhotos(); -``` - #### Get photo ```dotnet @@ -173,7 +172,13 @@ This function handles downloading the output photos to a specified directory. ```dotnet JArray photosList = completedJob.photos; -api.DownloadAllPhotos(photosList, completedJob.profile, "/output/folder/"); +DownloadAllPhotosResult downloadResults = await apiClient.DownloadAllPhotos(photosList, completedJob.profile, "/output/folder/"); +Console.WriteLine($"Success photos: [{string.Join(", ", downloadResults.SuccessPhotos)}]"); +Console.WriteLine($"Erorred photos: [{string.Join(", ", downloadResults.ErroredPhotos)}]"); + +Output: +Success photos: [1.jpg, 2.jpg, 3.jpg] +Erorred photos: [4.jpg] ``` OR diff --git a/StudioClient.Example/Program.cs b/StudioClient.Example/Program.cs index 38e7254..3f63a9d 100644 --- a/StudioClient.Example/Program.cs +++ b/StudioClient.Example/Program.cs @@ -9,7 +9,8 @@ class Program { static async Task Main(string[] args) { - var apiClient = new StudioClient(Environment.GetEnvironmentVariable("SKYLAB_API_TOKEN")); + var studioOptions = new StudioOptions { MaxConcurrentDownloads = 5 }; + var apiClient = new StudioClient(Environment.GetEnvironmentVariable("SKYLAB_API_TOKEN"), studioOptions); try { @@ -29,17 +30,21 @@ static async Task Main(string[] args) // QUEUE JOB dynamic queuedJob = await apiClient.QueueJob(job.id.Value, new { callback_url = "YOUR_CALLBACK_ENDPOINT" }); - // FETCH COMPLETED JOB (wait until job status is completed) + // ... + // !(wait until job status is completed by waiting for callback or by polling)! + // FETCH COMPLETED JOB dynamic completedJob = await apiClient.GetJob(queuedJob.id.Value); // DOWNLOAD COMPLETED JOB PHOTOS JArray photosList = completedJob.photos; - await apiClient.DownloadAllPhotos(photosList, completedJob.profile, "/output/folder/"); + DownloadAllPhotosResult downloadResults = await apiClient.DownloadAllPhotos(photosList, completedJob.profile, "/output/folder/"); + Console.WriteLine($"Success photos: [{string.Join(", ", downloadResults.SuccessPhotos)}]"); + Console.WriteLine($"Erorred photos: [{string.Join(", ", downloadResults.ErroredPhotos)}]"); } catch (Exception ex) { - Console.WriteLine($"An error occurred: {ex.Message}"); + Console.Error.WriteLine($"An error occurred: {ex.Message}"); } } } diff --git a/StudioClient/DownloadHelper.cs b/StudioClient/DownloadHelper.cs index b762fd1..2a79594 100644 --- a/StudioClient/DownloadHelper.cs +++ b/StudioClient/DownloadHelper.cs @@ -4,6 +4,18 @@ namespace SkylabStudio { + public class DownloadAllPhotosResult + { + public List SuccessPhotos { get; set; } + public List ErroredPhotos { get; set; } + + // Default constructor (parameterless) + public DownloadAllPhotosResult() + { + SuccessPhotos = new List(); + ErroredPhotos = new List(); + } + } public partial class StudioClient { private async Task?> DownloadBgImages(dynamic profile) @@ -21,11 +33,14 @@ public partial class StudioClient } return tempBgs; - } private static async Task DownloadImageAsync(string imageUrl) { + if (!imageUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { + throw new Exception($"Invalid retouchedUrl: \"{imageUrl}\" - Please ensure the job is complete"); + } + try { using (HttpClient httpClient = new HttpClient()) @@ -38,7 +53,7 @@ public partial class StudioClient } catch (HttpRequestException ex) { - Console.WriteLine($"Error downloading image: {ex.Message}"); + Console.Error.WriteLine($"Error downloading image: {ex.Message}"); return null; } } @@ -70,13 +85,16 @@ private async Task DownloadReplacedBackgroundImage(string fileName, Image } catch (Exception ex) { - Console.WriteLine($"Error downloading background image: {ex.Message}"); + Console.Error.WriteLine($"Error downloading background image: {ex.Message}"); return false; } } - public async Task DownloadAllPhotos(JArray photosList, dynamic profile, string outputPath) + public async Task DownloadAllPhotos(JArray photosList, dynamic profile, string outputPath) { + List successPhotos = new List(); + List erroredPhotos = new List(); + try { profile = await GetProfile(profile.id.Value); List bgs = await DownloadBgImages(profile); @@ -87,36 +105,56 @@ public async Task DownloadAllPhotos(JArray photosList, dynamic profile, st // Use a semaphore to control access to the download operation var semaphore = new SemaphoreSlim(_maxConcurrentDownloads); - List downloadTasks = new List(); + List>> downloadTasks = new List>>(); foreach (string photoId in photoIds) { downloadTasks.Add(DownloadPhoto(long.Parse(photoId), outputPath, profile, null, semaphore)); } // Wait for all download tasks to complete - await Task.WhenAll(downloadTasks); + IEnumerable> results = await Task.WhenAll(downloadTasks); - return true; + foreach (var result in results) + { + if (result.Item2) { + successPhotos.Add(result.Item1); + } else { + erroredPhotos.Add(result.Item1); + } + } + + DownloadAllPhotosResult downloadResults = new DownloadAllPhotosResult { + SuccessPhotos = successPhotos, + ErroredPhotos = erroredPhotos + }; + + return downloadResults; } catch (Exception _e) { - Console.WriteLine(_e); - return false; + Console.Error.WriteLine(_e); + + DownloadAllPhotosResult downloadResults = new DownloadAllPhotosResult { + SuccessPhotos = successPhotos, + ErroredPhotos = erroredPhotos + }; + + return downloadResults; } } - public async Task DownloadPhoto(long photoId, string outputPath, dynamic? profile = null, dynamic? options = null, SemaphoreSlim? semaphore = null) + public async Task> DownloadPhoto(long photoId, string outputPath, dynamic? profile = null, dynamic? options = null, SemaphoreSlim? semaphore = null) { - try { - if (semaphore != null) await semaphore.WaitAsync(); // Wait until a slot is available + dynamic photo = await GetPhoto(photoId); + long profileId = photo.job.profileId; - dynamic photo = await GetPhoto(photoId); - long profileId = photo.job.profileId; + string fileName = photo.name.Value; - string fileName = photo.name.Value; + try { + if (semaphore != null) await semaphore.WaitAsync(); // Wait until a slot is available if (profile == null) { profile = await GetProfile(profileId); } bool isExtract = Convert.ToBoolean(profile.enableExtract.Value); - bool replaceBackground = Convert.ToBoolean(profile.enableExtract.Value); + bool replaceBackground = Convert.ToBoolean(profile.replaceBackground.Value); bool isDualFileOutput = Convert.ToBoolean(profile.dualFileOutput.Value); bool enableStripPngMetadata = Convert.ToBoolean(profile.enableStripPngMetadata.Value); List? bgs = options?.bgs; @@ -139,18 +177,22 @@ public async Task DownloadPhoto(long photoId, string outputPath, dynamic? } // Regular Extract output - if (!isDualFileOutput) image.WriteToFile(Path.Combine(outputPath, pngFileName)); + if (!isDualFileOutput && !replaceBackground) image.WriteToFile(Path.Combine(outputPath, pngFileName)); } else { // Non-extracted regular image output image.WriteToFile(Path.Combine(outputPath, fileName)); } Console.WriteLine($"Successfully downloaded: {fileName}"); - return true; + return new (fileName, true); } catch (Exception _e) { - Console.WriteLine($"Failed to download photo id: {photoId}"); - Console.WriteLine(_e); - return false; + Console.Error.WriteLine($"Failed to download photo id: {photoId}"); + Console.Error.WriteLine(_e); + + return new (fileName, false); + } finally + { + if (semaphore != null) semaphore.Release(); // Release the semaphore } } } diff --git a/StudioClient/Job.cs b/StudioClient/Job.cs index 0f6fed8..0ac1688 100644 --- a/StudioClient/Job.cs +++ b/StudioClient/Job.cs @@ -38,6 +38,11 @@ public async Task CancelJob(long jobId) return await Request($"jobs/{jobId}/cancel", HttpMethod.Post); } + public async Task DeleteJob(long jobId) + { + return await Request($"jobs/{jobId}", HttpMethod.Delete); + } + public async Task JobsInFront(long jobId) { return await Request($"jobs/{jobId}/jobs_in_front", HttpMethod.Get); diff --git a/StudioClient/StudioClient.cs b/StudioClient/StudioClient.cs index b06222c..ee46e47 100644 --- a/StudioClient/StudioClient.cs +++ b/StudioClient/StudioClient.cs @@ -4,13 +4,18 @@ namespace SkylabStudio { + public class StudioOptions + { + public int? MaxConcurrentDownloads { get; set; } + } + public partial class StudioClient { private readonly HttpClient _httpClient; private readonly string _apiKey; private readonly int _maxConcurrentDownloads = 5; - public StudioClient(string? apiKey = null, dynamic? options = null) + public StudioClient(string? apiKey = null, StudioOptions? options = null) { if (apiKey == null) throw new Exception("No API key provided"); @@ -18,7 +23,7 @@ public StudioClient(string? apiKey = null, dynamic? options = null) _httpClient = new HttpClient(); _httpClient.BaseAddress = new Uri(baseUrl); _apiKey = apiKey; - _maxConcurrentDownloads = options?.maxConcurrentDownloads ?? 5; + _maxConcurrentDownloads = options?.MaxConcurrentDownloads ?? 5; } private async Task Request(string endpoint, HttpMethod httpMethod, object? payload = null)