From abf11a8f0fe6e11d82985c8dbf8d2d5d0ad0ca1a Mon Sep 17 00:00:00 2001 From: kev-le Date: Tue, 20 Feb 2024 13:14:54 -0800 Subject: [PATCH 1/4] Remove listPhotos from readme --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 2d81db9..2f27c28 100644 --- a/README.md +++ b/README.md @@ -138,12 +138,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 From e4d2b152bb33df7158f2a03757d71fa12ed0673a Mon Sep 17 00:00:00 2001 From: kev-le Date: Tue, 20 Feb 2024 13:15:56 -0800 Subject: [PATCH 2/4] Log errors to Console.Error stream --- StudioClient/DownloadHelper.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/StudioClient/DownloadHelper.cs b/StudioClient/DownloadHelper.cs index b762fd1..25d25cd 100644 --- a/StudioClient/DownloadHelper.cs +++ b/StudioClient/DownloadHelper.cs @@ -38,7 +38,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,7 +70,7 @@ 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; } } @@ -98,7 +98,7 @@ public async Task DownloadAllPhotos(JArray photosList, dynamic profile, st return true; } catch (Exception _e) { - Console.WriteLine(_e); + Console.Error.WriteLine(_e); return false; } } @@ -148,8 +148,8 @@ public async Task DownloadPhoto(long photoId, string outputPath, dynamic? return true; } catch (Exception _e) { - Console.WriteLine($"Failed to download photo id: {photoId}"); - Console.WriteLine(_e); + Console.Error.WriteLine($"Failed to download photo id: {photoId}"); + Console.Error.WriteLine(_e); return false; } } From 6cbb8196fd1440862946c2eb64f0518412014e6e Mon Sep 17 00:00:00 2001 From: kev-le Date: Tue, 20 Feb 2024 13:16:38 -0800 Subject: [PATCH 3/4] Add deleteJob method --- StudioClient/Job.cs | 5 +++++ 1 file changed, 5 insertions(+) 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); From 1b6eb0fecf18f3f5cee5c13b448fbc7fa53c08ce Mon Sep 17 00:00:00 2001 From: kev-le Date: Tue, 20 Feb 2024 17:20:45 -0800 Subject: [PATCH 4/4] Fix issue with method not release semaphore, add download results type for DownloadAllPhotos, add documentation for StudioOptions --- README.md | 13 +++++- StudioClient.Example/Program.cs | 13 ++++-- StudioClient/DownloadHelper.cs | 74 ++++++++++++++++++++++++++------- StudioClient/StudioClient.cs | 9 +++- 4 files changed, 86 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 2f27c28..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 @@ -167,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 25d25cd..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()) @@ -75,8 +90,11 @@ private async Task DownloadReplacedBackgroundImage(string fileName, Image } } - 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.Error.WriteLine(_e); - return false; + + 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.Error.WriteLine($"Failed to download photo id: {photoId}"); Console.Error.WriteLine(_e); - return false; + + return new (fileName, false); + } finally + { + if (semaphore != null) semaphore.Release(); // Release the semaphore } } } 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)