diff --git a/.idea/.idea.CatBox.NET/.idea/vcs.xml b/.idea/.idea.CatBox.NET/.idea/vcs.xml
index 35eb1dd..e75ad0f 100644
--- a/.idea/.idea.CatBox.NET/.idea/vcs.xml
+++ b/.idea/.idea.CatBox.NET/.idea/vcs.xml
@@ -1,5 +1,13 @@
+
+
+
+
diff --git a/tests/CatBox.Tests/Images/test-file.png b/assets/test-file.png
similarity index 100%
rename from tests/CatBox.Tests/Images/test-file.png
rename to assets/test-file.png
diff --git a/samples/SampleApp/Program.cs b/samples/SampleApp/Program.cs
index d0a6169..50ca235 100644
--- a/samples/SampleApp/Program.cs
+++ b/samples/SampleApp/Program.cs
@@ -1,37 +1,31 @@
using CatBox.NET;
using CatBox.NET.Client;
using CatBox.NET.Enums;
+using CatBox.NET.Requests.Album;
using CatBox.NET.Requests.Album.Create;
using CatBox.NET.Requests.Album.Modify;
using CatBox.NET.Requests.File;
+using CatBox.NET.Requests.Litterbox;
+using CatBox.NET.Requests.URL;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-// Load configuration from user secrets
+// Load configuration from user secrets (optional — anonymous uploads work without it)
// To set your UserHash, run: dotnet user-secrets set "CatBox:UserHash" "your-hash-here" --project samples/SampleApp
// Get your UserHash from: https://catbox.moe/user/manage.php
-var configuration = new ConfigurationBuilder()
- .AddUserSecrets()
- .Build();
-
-var userHash = configuration["CatBox:UserHash"]
- ?? throw new InvalidOperationException(
- "CatBox UserHash not configured. Run: dotnet user-secrets set \"CatBox:UserHash\" \"your-hash-here\" --project samples/SampleApp");
-
-// Compute path to test image file within the project directory
-string testImagePath = Path.Combine(
- AppDomain.CurrentDomain.BaseDirectory,
- "..", "..", "..", "..", "..", // Navigate up from bin/Debug/net7.0
- "tests", "CatBox.Tests", "Images", "test-file.png"
-);
-testImagePath = Path.GetFullPath(testImagePath); // Normalize the path
-string testImageFileName = Path.GetFileName(testImagePath);
+string? userHash;
+try
+{
+ var configuration = new ConfigurationBuilder()
+ .AddUserSecrets()
+ .Build();
-// Verify the test image file exists
-if (!File.Exists(testImagePath))
+ userHash = configuration["CatBox:UserHash"];
+}
+catch
{
- throw new FileNotFoundException($"Test image not found at: {testImagePath}");
+ userHash = null;
}
var collection = new ServiceCollection()
@@ -40,152 +34,591 @@
f.CatBoxUrl = new Uri("https://catbox.moe/user/api.php");
f.LitterboxUrl = new Uri("https://litterbox.catbox.moe/resources/internals/api.php");
})
- .AddLogging(f => f.AddConsole())
+ .AddLogging(f => f.AddConsole().SetMinimumLevel(LogLevel.Warning))
.BuildServiceProvider();
-// Store uploaded file URLs and album URL for cleanup
-var uploadedFiles = new List();
-string? albumUrl = null;
+// Track uploaded file URLs and album URLs across operations
+var trackedFiles = new List();
+var trackedAlbums = new List();
+
+Console.WriteLine("=== CatBox.NET Interactive Sample ===");
+if (string.IsNullOrWhiteSpace(userHash))
+ Console.WriteLine("[Warning] UserHash not configured. Some operations will be unavailable.");
+else
+ Console.WriteLine($"[OK] UserHash configured.");
+Console.WriteLine();
+
+while (true)
+{
+ PrintMenu();
+ Console.Write("> ");
+ var input = Console.ReadLine()?.Trim();
+
+ if (!int.TryParse(input, out var choice))
+ {
+ Console.WriteLine("Invalid input. Enter a number.\n");
+ continue;
+ }
+
+ try
+ {
+ switch (choice)
+ {
+ case 0:
+ Console.WriteLine("Goodbye!");
+ return;
+ case 1:
+ await UploadFilesFromDisk();
+ break;
+ case 2:
+ await UploadFilesAsStream();
+ break;
+ case 3:
+ await UploadFilesByUrl();
+ break;
+ case 4:
+ await DeleteFiles();
+ break;
+ case 5:
+ await CreateAlbumFromTracked();
+ break;
+ case 6:
+ await CreateAlbumAndUpload();
+ break;
+ case 7:
+ await EditAlbum();
+ break;
+ case 8:
+ await ModifyAlbum();
+ break;
+ case 9:
+ await GetAlbumInfo();
+ break;
+ case 10:
+ await DownloadFile();
+ break;
+ case 11:
+ await DownloadAlbum();
+ break;
+ case 12:
+ await LitterboxUploadFromDisk();
+ break;
+ case 13:
+ await LitterboxUploadAsStream();
+ break;
+ case 14:
+ ShowTrackedItems();
+ break;
+ default:
+ Console.WriteLine("Unknown option.\n");
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}\n");
+ }
+}
+
+// --- Helpers ---
+
+static void PrintMenu()
+{
+ Console.WriteLine("""
+ --- File Operations ---
+ [1] Upload file(s) from disk
+ [2] Upload file(s) as stream
+ [3] Upload file(s) by URL
+ [4] Delete file(s)
+
+ --- Album Operations ---
+ [5] Create album (from tracked files)
+ [6] Create album + upload files (one step)
+ [7] Edit album
+ [8] Add/Remove files or Delete album
+ [9] Get album info
-// Upload a single image via stream
-using (var scope = collection.CreateScope())
+ --- Download ---
+ [10] Download a file
+ [11] Download entire album
+
+ --- Litterbox (Temporary) ---
+ [12] Upload temporary file(s) from disk
+ [13] Upload temporary file as stream
+
+ --- Tracked Items ---
+ [14] Show tracked files & albums
+ [0] Exit
+
+ """);
+}
+
+static string ExtractFileName(string url)
+{
+ if (url.Contains("files.catbox.moe"))
+ return url.Split('/')[^1];
+ return url;
+}
+
+static string ExtractAlbumShortCode(string url)
+{
+ if (url.Contains("catbox.moe/c/"))
+ return url.Split('/')[^1];
+ return url;
+}
+
+static ExpireAfter PromptExpiry()
+{
+ Console.WriteLine("Select expiry:");
+ Console.WriteLine(" [1] 1 hour");
+ Console.WriteLine(" [2] 12 hours");
+ Console.WriteLine(" [3] 24 hours (1 day)");
+ Console.WriteLine(" [4] 72 hours (3 days)");
+ Console.Write("Choice: ");
+ var expiryInput = Console.ReadLine()?.Trim();
+ return expiryInput switch
+ {
+ "2" => ExpireAfter.TwelveHours,
+ "3" => ExpireAfter.OneDay,
+ "4" => ExpireAfter.ThreeDays,
+ _ => ExpireAfter.OneHour,
+ };
+}
+
+bool RequireUserHash()
{
+ if (!string.IsNullOrWhiteSpace(userHash))
+ return true;
+
+ Console.WriteLine("This operation requires a UserHash. Configure one first.");
+ Console.WriteLine("Run: dotnet user-secrets set \"CatBox:UserHash\" \"your-hash-here\" --project samples/SampleApp\n");
+ return false;
+}
+
+// --- Operations ---
+
+async Task UploadFilesFromDisk()
+{
+ Console.Write("Enter file path(s) (comma-separated): ");
+ var paths = Console.ReadLine()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (paths is null || paths.Length == 0) return;
+
+ var files = paths.Select(p => new FileInfo(Path.GetFullPath(p))).ToList();
+ foreach (var f in files.Where(f => !f.Exists))
+ Console.WriteLine($" Warning: file not found: {f.FullName}");
+
+ using var scope = collection.CreateScope();
var client = scope.ServiceProvider.GetRequiredService();
- var responses = client.UploadFilesAsStreamAsync([new StreamUploadRequest
+ var request = new FileUploadRequest { Files = files, UserHash = userHash };
+
+ await foreach (var result in client.UploadFilesAsync(request))
{
- Stream = File.OpenRead(testImagePath),
- FileName = testImageFileName,
- UserHash = userHash
- }]);
+ Console.WriteLine($" Uploaded: {result}");
+ if (!string.IsNullOrWhiteSpace(result))
+ trackedFiles.Add(result);
+ }
+ Console.WriteLine();
+}
+
+async Task UploadFilesAsStream()
+{
+ Console.Write("Enter file path: ");
+ var path = Console.ReadLine()?.Trim();
+ if (string.IsNullOrWhiteSpace(path)) return;
+ path = Path.GetFullPath(path);
+
+ if (!File.Exists(path))
+ {
+ Console.WriteLine($" File not found: {path}\n");
+ return;
+ }
+
+ using var scope = collection.CreateScope();
+ var client = scope.ServiceProvider.GetRequiredService();
+ var stream = File.OpenRead(path);
- await foreach (var response in responses)
+ var requests = new[]
{
- Console.WriteLine(response);
- if (!string.IsNullOrWhiteSpace(response))
- uploadedFiles.Add(response);
+ new StreamUploadRequest
+ {
+ Stream = stream,
+ FileName = Path.GetFileName(path),
+ UserHash = userHash
+ }
+ };
+
+ await foreach (var result in client.UploadFilesAsStreamAsync(requests))
+ {
+ Console.WriteLine($" Uploaded: {result}");
+ if (!string.IsNullOrWhiteSpace(result))
+ trackedFiles.Add(result);
}
+ Console.WriteLine();
}
-// Create an album of images already on Catbox
-using (var scope = collection.CreateScope())
+async Task UploadFilesByUrl()
{
+ Console.Write("Enter URL(s) (comma-separated): ");
+ var urls = Console.ReadLine()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (urls is null || urls.Length == 0) return;
+
+ using var scope = collection.CreateScope();
var client = scope.ServiceProvider.GetRequiredService();
- var response = await client.CreateAlbumAsync(new RemoteCreateAlbumRequest
+ var request = new UrlUploadRequest
{
- Title = "Album Title",
- Description = "Album Description",
- Files = uploadedFiles, // Use the actual uploaded file(s) from previous step
+ Files = urls.Select(u => new Uri(u)),
UserHash = userHash
- });
+ };
- Console.WriteLine(response);
- albumUrl = response;
+ await foreach (var result in client.UploadFilesAsUrlAsync(request))
+ {
+ Console.WriteLine($" Uploaded: {result}");
+ if (!string.IsNullOrWhiteSpace(result))
+ trackedFiles.Add(result);
+ }
+ Console.WriteLine();
}
-// Cleanup: Delete the album and uploaded files
-using (var scope = collection.CreateScope())
+async Task DeleteFiles()
{
+ if (!RequireUserHash()) return;
+
+ if (trackedFiles.Count > 0)
+ {
+ Console.WriteLine("Tracked files:");
+ for (var i = 0; i < trackedFiles.Count; i++)
+ Console.WriteLine($" [{i}] {trackedFiles[i]}");
+ }
+
+ Console.Write("Enter CatBox file name(s) to delete (comma-separated, e.g., abc123.png): ");
+ var names = Console.ReadLine()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (names is null || names.Length == 0) return;
+
+ var fileNames = names.Select(ExtractFileName).ToList();
+
+ using var scope = collection.CreateScope();
var client = scope.ServiceProvider.GetRequiredService();
- await CleanupAsync(client, albumUrl, uploadedFiles, userHash);
+ var result = await client.DeleteMultipleFilesAsync(new DeleteFileRequest
+ {
+ UserHash = userHash!,
+ FileNames = fileNames
+ });
+
+ Console.WriteLine($" Delete result: {result}");
+
+ // Remove deleted files from tracking
+ trackedFiles.RemoveAll(f => fileNames.Contains(ExtractFileName(f)));
+ Console.WriteLine();
}
-return;
+async Task CreateAlbumFromTracked()
+{
+ if (trackedFiles.Count == 0)
+ {
+ Console.WriteLine("No tracked files. Upload some files first.\n");
+ return;
+ }
+
+ Console.WriteLine("Tracked files:");
+ for (var i = 0; i < trackedFiles.Count; i++)
+ Console.WriteLine($" [{i}] {trackedFiles[i]}");
+
+ Console.Write("Enter indices to include (comma-separated, or 'all'): ");
+ var selection = Console.ReadLine()?.Trim();
+ if (string.IsNullOrWhiteSpace(selection)) return;
+
+ List selectedFiles;
+ if (selection.Equals("all", StringComparison.OrdinalIgnoreCase))
+ {
+ selectedFiles = trackedFiles.ToList();
+ }
+ else
+ {
+ var indices = selection.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(int.Parse)
+ .Where(i => i >= 0 && i < trackedFiles.Count);
+ selectedFiles = indices.Select(i => trackedFiles[i]).ToList();
+ }
+
+ if (selectedFiles.Count == 0)
+ {
+ Console.WriteLine("No valid files selected.\n");
+ return;
+ }
+
+ Console.Write("Album title: ");
+ var title = Console.ReadLine()?.Trim() ?? "Untitled";
+ Console.Write("Album description (optional): ");
+ var description = Console.ReadLine()?.Trim();
+
+ // Extract just the file names from URLs
+ var fileNames = selectedFiles.Select(ExtractFileName).ToList();
+
+ using var scope = collection.CreateScope();
+ var client = scope.ServiceProvider.GetRequiredService();
+ var result = await client.CreateAlbumAsync(new RemoteCreateAlbumRequest
+ {
+ Title = title,
+ Description = description,
+ UserHash = userHash,
+ Files = fileNames
+ });
+
+ Console.WriteLine($" Album created: {result}");
+ if (!string.IsNullOrWhiteSpace(result))
+ trackedAlbums.Add(result);
+ Console.WriteLine();
+}
-/*// Upload images to CatBox, then create an album on CatBox, then place the uploaded images into the newly created album
-using (var scope = collection.CreateScope())
+async Task CreateAlbumAndUpload()
{
+ Console.Write("Enter file path(s) to upload (comma-separated): ");
+ var paths = Console.ReadLine()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (paths is null || paths.Length == 0) return;
+
+ Console.Write("Album title: ");
+ var title = Console.ReadLine()?.Trim() ?? "Untitled";
+ Console.Write("Album description (optional): ");
+ var description = Console.ReadLine()?.Trim();
+
+ var files = paths.Select(p => new FileInfo(Path.GetFullPath(p))).ToList();
+
+ using var scope = collection.CreateScope();
var client = scope.ServiceProvider.GetRequiredService();
- var response = await client.CreateAlbumFromFilesAsync(new CreateAlbumRequest
+ var result = await client.CreateAlbumFromFilesAsync(new CreateAlbumRequest
{
- Title = "Album Title",
- Description = "Album Description",
- UserHash = null,
- UploadRequest = new FileUploadRequest
- {
- Files = [new FileInfo(testImagePath)]
- }
+ Title = title,
+ Description = description,
+ UserHash = userHash,
+ UploadRequest = new FileUploadRequest { Files = files, UserHash = userHash }
});
- Console.WriteLine(response);
+ Console.WriteLine($" Album created: {result}");
+ if (!string.IsNullOrWhiteSpace(result))
+ trackedAlbums.Add(result);
+ Console.WriteLine();
}
-// Temporarily upload an image to litterbox
-using (var scope = collection.CreateScope())
+async Task EditAlbum()
{
- var client = scope.ServiceProvider.GetRequiredService();
- var response = await client.UploadImageAsync(new TemporaryStreamUploadRequest
+ if (!RequireUserHash()) return;
+
+ Console.Write("Enter album ID or URL: ");
+ var albumInput = Console.ReadLine()?.Trim();
+ if (string.IsNullOrWhiteSpace(albumInput)) return;
+ var albumId = ExtractAlbumShortCode(albumInput);
+
+ Console.Write("New title: ");
+ var title = Console.ReadLine()?.Trim() ?? "Untitled";
+ Console.Write("New description: ");
+ var description = Console.ReadLine()?.Trim() ?? "";
+
+ Console.Write("Enter file name(s) for the album (comma-separated): ");
+ var fileInput = Console.ReadLine()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (fileInput is null || fileInput.Length == 0)
+ {
+ Console.WriteLine("No files specified.\n");
+ return;
+ }
+
+ var fileNames = fileInput.Select(ExtractFileName).ToList();
+
+ using var scope = collection.CreateScope();
+ var client = scope.ServiceProvider.GetRequiredService();
+
+#pragma warning disable CS0618 // EditAlbumAsync is marked Obsolete as a safety warning
+ var result = await client.EditAlbumAsync(new EditAlbumRequest
+ {
+ UserHash = userHash!,
+ AlbumId = albumId,
+ Title = title,
+ Description = description,
+ Files = fileNames
+ });
+#pragma warning restore CS0618
+
+ Console.WriteLine($" Edit result: {result}\n");
+}
+
+async Task ModifyAlbum()
+{
+ if (!RequireUserHash()) return;
+
+ Console.WriteLine(" [1] Add files to album");
+ Console.WriteLine(" [2] Remove files from album");
+ Console.WriteLine(" [3] Delete album");
+ Console.Write("Choice: ");
+ var subChoice = Console.ReadLine()?.Trim();
+
+ Console.Write("Enter album ID or URL: ");
+ var albumInput = Console.ReadLine()?.Trim();
+ if (string.IsNullOrWhiteSpace(albumInput)) return;
+ var albumId = ExtractAlbumShortCode(albumInput);
+
+ RequestType requestType;
+ List fileNames = [];
+
+ switch (subChoice)
{
- Expiry = ExpireAfter.OneHour,
- FileName = testImageFileName,
- Stream = File.OpenRead(testImagePath)
+ case "1":
+ requestType = RequestType.AddToAlbum;
+ Console.Write("Enter file name(s) to add (comma-separated): ");
+ var addInput = Console.ReadLine()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (addInput is null || addInput.Length == 0) return;
+ fileNames = addInput.Select(ExtractFileName).ToList();
+ break;
+ case "2":
+ requestType = RequestType.RemoveFromAlbum;
+ Console.Write("Enter file name(s) to remove (comma-separated): ");
+ var removeInput = Console.ReadLine()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (removeInput is null || removeInput.Length == 0) return;
+ fileNames = removeInput.Select(ExtractFileName).ToList();
+ break;
+ case "3":
+ requestType = RequestType.DeleteAlbum;
+ break;
+ default:
+ Console.WriteLine("Invalid choice.\n");
+ return;
+ }
+
+ using var scope = collection.CreateScope();
+ var client = scope.ServiceProvider.GetRequiredService();
+ var result = await client.ModifyAlbumAsync(new ModifyAlbumImagesRequest
+ {
+ Request = requestType,
+ UserHash = userHash!,
+ AlbumId = albumId,
+ Files = fileNames
});
- Console.WriteLine(response);
+ Console.WriteLine($" Result: {result}");
+ if (requestType == RequestType.DeleteAlbum)
+ trackedAlbums.RemoveAll(a => ExtractAlbumShortCode(a) == albumId);
Console.WriteLine();
}
-Console.ReadLine();*/
+async Task GetAlbumInfo()
+{
+ Console.Write("Enter album ID or URL: ");
+ var albumInput = Console.ReadLine()?.Trim();
+ if (string.IsNullOrWhiteSpace(albumInput)) return;
+ var albumId = ExtractAlbumShortCode(albumInput);
+
+ using var scope = collection.CreateScope();
+ var client = scope.ServiceProvider.GetRequiredService();
+ var info = await client.GetAlbumAsync(albumId);
-// Cleanup method to delete album and uploaded files
-static async Task CleanupAsync(ICatBoxClient client, string? albumUrl, List uploadedFiles, string userHash)
+ Console.WriteLine($" Title: {info.Title}");
+ Console.WriteLine($" Description: {info.Description}");
+ Console.WriteLine($" Album ID: {info.AlbumId}");
+ Console.WriteLine($" Created: {info.DateCreated}");
+ Console.WriteLine($" Files ({info.Files.Length}):");
+ foreach (var file in info.Files)
+ Console.WriteLine($" - {file}");
+ Console.WriteLine();
+}
+
+async Task DownloadFile()
{
- Console.WriteLine("\n--- Starting Cleanup ---");
+ Console.Write("Enter CatBox file name (e.g., abc123.png): ");
+ var fileName = Console.ReadLine()?.Trim();
+ if (string.IsNullOrWhiteSpace(fileName)) return;
+ fileName = ExtractFileName(fileName);
+
+ Console.Write("Enter destination directory: ");
+ var destDir = Console.ReadLine()?.Trim();
+ if (string.IsNullOrWhiteSpace(destDir)) return;
+
+ using var scope = collection.CreateScope();
+ var client = scope.ServiceProvider.GetRequiredService();
+ await client.DownloadFileAsync(fileName, destDir);
+
+ var filePath = Path.Combine(destDir, fileName);
+ Console.WriteLine($" Downloaded: {filePath}\n");
+}
+
+async Task DownloadAlbum()
+{
+ Console.Write("Enter album ID or URL: ");
+ var albumInput = Console.ReadLine()?.Trim();
+ if (string.IsNullOrWhiteSpace(albumInput)) return;
+ var albumId = ExtractAlbumShortCode(albumInput);
+
+ Console.Write("Enter destination directory: ");
+ var destDir = Console.ReadLine()?.Trim();
+ if (string.IsNullOrWhiteSpace(destDir)) return;
- // Delete the album first
- if (!string.IsNullOrWhiteSpace(albumUrl))
+ using var scope = collection.CreateScope();
+ var client = scope.ServiceProvider.GetRequiredService();
+
+ Console.WriteLine(" Downloading album files...");
+ await foreach (var fileInfo in client.DownloadAlbumAsync(albumId, destDir))
{
- try
- {
- // Extract album short ID from URL (e.g., "pd412w" from "https://catbox.moe/c/pd412w")
- var albumUri = new Uri(albumUrl);
- var albumId = albumUri.AbsolutePath.TrimStart('/').Replace("c/", "");
+ Console.WriteLine($" Downloaded: {fileInfo.FullName}");
+ }
+ Console.WriteLine();
+}
- Console.WriteLine($"Deleting album: {albumId}");
+async Task LitterboxUploadFromDisk()
+{
+ Console.Write("Enter file path(s) (comma-separated): ");
+ var paths = Console.ReadLine()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (paths is null || paths.Length == 0) return;
- var albumDeleteResponse = await client.ModifyAlbumAsync(new ModifyAlbumImagesRequest
- {
- Request = RequestType.DeleteAlbum,
- UserHash = userHash,
- AlbumId = albumId,
- Files = [] // Empty for DeleteAlbum operation
- });
+ var expiry = PromptExpiry();
+ var files = paths.Select(p => new FileInfo(Path.GetFullPath(p))).ToList();
- Console.WriteLine($"Album deletion response: {albumDeleteResponse}");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Error deleting album: {ex.Message}");
- }
+ using var scope = collection.CreateScope();
+ var client = scope.ServiceProvider.GetRequiredService();
+
+ await foreach (var result in client.UploadMultipleImagesAsync(new TemporaryFileUploadRequest
+ {
+ Files = files,
+ Expiry = expiry
+ }))
+ {
+ Console.WriteLine($" Uploaded (temporary): {result}");
}
+ Console.WriteLine();
+}
- // Delete the uploaded files
- if (uploadedFiles.Count > 0)
+async Task LitterboxUploadAsStream()
+{
+ Console.Write("Enter file path: ");
+ var path = Console.ReadLine()?.Trim();
+ if (string.IsNullOrWhiteSpace(path)) return;
+ path = Path.GetFullPath(path);
+ if (!File.Exists(path))
{
- try
- {
- // Extract filenames from URLs (e.g., "8ce67f.jpg" from "https://files.catbox.moe/8ce67f.jpg")
- var fileNames = uploadedFiles.Select(url =>
- {
- var uri = new Uri(url);
- return Path.GetFileName(uri.AbsolutePath);
- }).ToList();
-
- Console.WriteLine($"Deleting {fileNames.Count} file(s): {string.Join(", ", fileNames)}");
-
- var fileDeleteResponse = await client.DeleteMultipleFilesAsync(new DeleteFileRequest
- {
- UserHash = userHash,
- FileNames = fileNames
- });
-
- Console.WriteLine($"File deletion response: {fileDeleteResponse}");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Error deleting files: {ex.Message}");
- }
+ Console.WriteLine(" File not found.\n");
+ return;
}
- Console.WriteLine("--- Cleanup Complete ---\n");
+ var expiry = PromptExpiry();
+
+ using var scope = collection.CreateScope();
+ var client = scope.ServiceProvider.GetRequiredService();
+ var result = await client.UploadImageAsync(new TemporaryStreamUploadRequest
+ {
+ FileName = Path.GetFileName(path),
+ Stream = File.OpenRead(path),
+ Expiry = expiry
+ });
+
+ Console.WriteLine($" Uploaded (temporary): {result}\n");
+}
+
+void ShowTrackedItems()
+{
+ Console.WriteLine($"--- Tracked Files ({trackedFiles.Count}) ---");
+ for (var i = 0; i < trackedFiles.Count; i++)
+ Console.WriteLine($" [{i}] {trackedFiles[i]}");
+
+ Console.WriteLine($"--- Tracked Albums ({trackedAlbums.Count}) ---");
+ for (var i = 0; i < trackedAlbums.Count; i++)
+ Console.WriteLine($" [{i}] {trackedAlbums[i]}");
+
+ Console.WriteLine();
}
diff --git a/samples/SampleApp/SampleApp.csproj b/samples/SampleApp/SampleApp.csproj
index ae05101..8ec181a 100644
--- a/samples/SampleApp/SampleApp.csproj
+++ b/samples/SampleApp/SampleApp.csproj
@@ -20,4 +20,10 @@
+
+
+ PreserveNewest
+
+
+
diff --git a/src/CatBox.NET/CatBox.NET.csproj b/src/CatBox.NET/CatBox.NET.csproj
index e510aed..b9dd03c 100644
--- a/src/CatBox.NET/CatBox.NET.csproj
+++ b/src/CatBox.NET/CatBox.NET.csproj
@@ -4,7 +4,7 @@
enable
enable
latest
- 1.0
+ 1.1
Chase Redmon, Kuinox, Adam Sears
CatBox.NET is a .NET Library for uploading files, URLs, and modifying albums on CatBox.moe
https://github.com/ChaseDRedmon/CatBox.NET
@@ -15,6 +15,7 @@
https://github.com/ChaseDRedmon/CatBox.NET/blob/main/license.txt
Fix required description field on create album endpoint. Description is optional when creating an endpoint.
net10.0
+ true
diff --git a/src/CatBox.NET/CatBox.NET.csproj.DotSettings b/src/CatBox.NET/CatBox.NET.csproj.DotSettings
new file mode 100644
index 0000000..89316e4
--- /dev/null
+++ b/src/CatBox.NET/CatBox.NET.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ Library
\ No newline at end of file
diff --git a/src/CatBox.NET/CatBoxServices.cs b/src/CatBox.NET/CatBoxServices.cs
index b532b1f..6e0840a 100644
--- a/src/CatBox.NET/CatBoxServices.cs
+++ b/src/CatBox.NET/CatBoxServices.cs
@@ -1,4 +1,6 @@
-using CatBox.NET.Client;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using CatBox.NET.Client;
using CatBox.NET.Exceptions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Resilience;
@@ -30,7 +32,7 @@ public IServiceCollection AddCatBoxServices(Action setupAction)
return services;
}
- private IServiceCollection AddHttpClientWithMessageHandler()
+ private IServiceCollection AddHttpClientWithMessageHandler()
where TInterface : class
where TImplementation : class, TInterface
{
@@ -48,8 +50,27 @@ private IServiceCollection AddHttpClientWithMessageHandler
+ {
+ // Don't retry custom CatBox exceptions - these are client errors (4xx)
+ // Our exceptions inherit from Exception, not HttpRequestException
+ if (args.Outcome.Exception is not null and not HttpRequestException)
+ return ValueTask.FromResult(false);
+
+ // Retry HttpRequestException (network failures, timeouts)
+ if (args.Outcome.Exception is HttpRequestException)
+ return ValueTask.FromResult(true);
+
+ // Retry 5xx server errors
+ if (args.Outcome.Result is { } response)
+ return ValueTask.FromResult(response.StatusCode >= HttpStatusCode.InternalServerError);
+
+ return ValueTask.FromResult(false);
+ }
};
+
+
});
return services;
diff --git a/src/CatBox.NET/CatboxOptions.cs b/src/CatBox.NET/CatboxOptions.cs
index 45a6aea..de37222 100644
--- a/src/CatBox.NET/CatboxOptions.cs
+++ b/src/CatBox.NET/CatboxOptions.cs
@@ -9,7 +9,12 @@ public sealed record CatboxOptions
/// URL for the catbox.moe domain
///
public Uri? CatBoxUrl { get; set; }
-
+
+ ///
+ /// Base URL for downloading CatBox files (default: https://files.catbox.moe/)
+ ///
+ public Uri CatBoxFilesUrl { get; set; } = new("https://files.catbox.moe/");
+
///
/// URL for the litterbox.moe domain
///
diff --git a/src/CatBox.NET/Client/CatBox/CatBox.cs b/src/CatBox.NET/Client/CatBox/CatBox.cs
index c6746d0..6fb3f00 100644
--- a/src/CatBox.NET/Client/CatBox/CatBox.cs
+++ b/src/CatBox.NET/Client/CatBox/CatBox.cs
@@ -1,6 +1,11 @@
-using CatBox.NET.Requests.Album;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using CatBox.NET.Requests.Album;
using CatBox.NET.Requests.Album.Create;
using CatBox.NET.Requests.Album.Modify;
+using CatBox.NET.Requests.File;
+using CatBox.NET.Requests.URL;
+using CatBox.NET.Responses.Album;
namespace CatBox.NET.Client;
@@ -24,56 +29,187 @@ public interface ICatBox
/// Cancellation Token.
///
Task UploadImagesToAlbumAsync(UploadToAlbumRequest request, CancellationToken ct = default);
-}
-///
-public sealed class Catbox : ICatBox
-{
- private readonly ICatBoxClient _client;
+ ///
+ /// Uploads files to an existing album, respecting the 500-file limit.
+ /// Fetches album first to determine available capacity and uploads only what fits.
+ ///
+ /// Album upload request
+ /// Cancellation Token
+ /// Result containing upload counts and remaining capacity
+ Task UploadImagesToAlbumSafeAsync(UploadToAlbumRequest request, CancellationToken ct = default);
///
- /// Instantiate a new catbox class
+ /// Gets album information including the list of files
///
- /// The CatBox Api Client ()
- public Catbox(ICatBoxClient client)
- {
- _client = client;
- }
-
+ /// The album ID to retrieve
+ /// Cancellation Token
+ /// Album information with parsed file list
+ Task GetAlbumAsync(string albumId, CancellationToken ct = default);
+
+ ///
+ /// Downloads a file from CatBox to the specified directory
+ ///
+ /// The file name (e.g., "abc123.png")
+ /// Directory to save the file
+ /// Cancellation Token
+ /// Skips download if file already exists at destination
+ Task DownloadFileAsync(string fileName, DirectoryInfo destination, CancellationToken ct = default);
+
+ ///
+ /// Downloads a file from CatBox to the specified path
+ ///
+ /// The file name (e.g., "abc123.png")
+ /// Directory path to save the file
+ /// Cancellation Token
+ /// Skips download if file already exists at destination
+ Task DownloadFileAsync(string fileName, [StringSyntax(StringSyntaxAttribute.Uri)] string destinationPath, CancellationToken ct = default);
+
+ ///
+ /// Downloads all files from an album to the specified directory
+ ///
+ /// The album ID to download
+ /// Directory to save the files
+ /// Cancellation Token
+ /// Yields FileInfo for each downloaded file
+ IAsyncEnumerable DownloadAlbumAsync(string albumId, DirectoryInfo destination, CancellationToken ct = default);
+
+ ///
+ /// Downloads all files from an album to the specified path
+ ///
+ /// The album ID to download
+ /// Directory path to save the files
+ /// Cancellation Token
+ /// Yields FileInfo for each downloaded file
+ IAsyncEnumerable DownloadAlbumAsync(string albumId, [StringSyntax(StringSyntaxAttribute.Uri)] string destinationPath, CancellationToken ct = default);
+}
+
+///
+///
+/// Instantiate a new catbox class
+///
+/// The CatBox Api Client ()
+public sealed class Catbox(ICatBoxClient client) : ICatBox
+{
///
- public Task CreateAlbumFromFilesAsync(CreateAlbumRequest requestFromFiles, CancellationToken ct = default)
+ public async Task CreateAlbumFromFilesAsync(CreateAlbumRequest requestFromFiles, CancellationToken ct = default)
{
- var enumerable = Upload(requestFromFiles, ct);
+ var uploadedFiles = await Upload(requestFromFiles, ct).ToListAsync(ct).ConfigureAwait(false);
var createAlbumRequest = new RemoteCreateAlbumRequest
{
Title = requestFromFiles.Title,
Description = requestFromFiles.Description,
UserHash = requestFromFiles.UserHash,
- Files = enumerable.ToBlockingEnumerable(cancellationToken: ct)
+ Files = uploadedFiles
};
- return _client.CreateAlbumAsync(createAlbumRequest, ct);
+ return await client.CreateAlbumAsync(createAlbumRequest, ct).ConfigureAwait(false);
}
///
- public Task UploadImagesToAlbumAsync(UploadToAlbumRequest request, CancellationToken ct = default)
+ public async Task UploadImagesToAlbumAsync(UploadToAlbumRequest request, CancellationToken ct = default)
{
- var requestType = request.Request;
- var userHash = request.UserHash;
- var albumId = request.AlbumId;
+ var uploadedFiles = await Upload(request, ct).ToListAsync(ct).ConfigureAwait(false);
- var enumerable = Upload(request, ct);
+ return await client.ModifyAlbumAsync(new ModifyAlbumImagesRequest
+ {
+ Request = request.Request,
+ UserHash = request.UserHash,
+ AlbumId = request.AlbumId,
+ Files = uploadedFiles
+ }, ct).ConfigureAwait(false);
+ }
- return _client.ModifyAlbumAsync(new ModifyAlbumImagesRequest
+ ///
+ public async Task UploadImagesToAlbumSafeAsync(UploadToAlbumRequest request, CancellationToken ct = default)
+ {
+ // Get album to determine current capacity
+ var album = await GetAlbumAsync(request.AlbumId, ct).ConfigureAwait(false);
+ var currentCount = album.Files.Length;
+ var remainingCapacity = Common.MaxAlbumFiles - currentCount;
+
+ // Count total files in request (materializes lazy enumerables)
+ var (totalFiles, materializedRequest) = MaterializeAndCountRequest(request);
+
+ // If album is full, return early
+ if (remainingCapacity <= 0)
{
- Request = requestType,
- UserHash = userHash,
- AlbumId = albumId,
- Files = enumerable.ToBlockingEnumerable()
- }, ct);
+ return new AlbumUploadResult
+ {
+ AlbumUrl = null,
+ FilesUploaded = 0,
+ FilesSkipped = totalFiles,
+ RemainingCapacity = 0
+ };
+ }
+
+ // Calculate how many to upload
+ var filesToUpload = Math.Min(remainingCapacity, totalFiles);
+ var filesToSkip = totalFiles - filesToUpload;
+
+ // If we need to upload all files, use the original request
+ // Otherwise, create a sliced request
+ var requestToUse = filesToUpload == totalFiles
+ ? materializedRequest
+ : TakeFilesFromRequest(materializedRequest, filesToUpload);
+
+ // Upload and add to album
+ var result = await UploadImagesToAlbumAsync(requestToUse, ct).ConfigureAwait(false);
+
+ return new AlbumUploadResult
+ {
+ AlbumUrl = result,
+ FilesUploaded = filesToUpload,
+ FilesSkipped = filesToSkip,
+ RemainingCapacity = remainingCapacity - filesToUpload
+ };
}
-
+
+ ///
+ public async Task GetAlbumAsync(string albumId, CancellationToken ct = default)
+ {
+ return await client.GetAlbumAsync(new GetAlbumRequest { AlbumId = albumId }, ct).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task DownloadFileAsync(string fileName, DirectoryInfo destination, CancellationToken ct = default)
+ {
+ await client.DownloadFileAsync(fileName, destination, ct).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task DownloadFileAsync(string fileName, [StringSyntax(StringSyntaxAttribute.Uri)] string destinationPath, CancellationToken ct = default)
+ {
+ await client.DownloadFileAsync(fileName, destinationPath, ct).ConfigureAwait(false);
+ }
+
+ ///
+ public async IAsyncEnumerable DownloadAlbumAsync(string albumId, DirectoryInfo destination, [EnumeratorCancellation] CancellationToken ct = default)
+ {
+ var album = await GetAlbumAsync(albumId, ct).ConfigureAwait(false);
+
+ if (!destination.Exists)
+ destination.Create();
+
+ foreach (var fileName in album.Files)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var filePath = Path.Combine(destination.FullName, fileName);
+
+ await client.DownloadFileAsync(fileName, destination, ct).ConfigureAwait(false);
+
+ yield return new FileInfo(filePath);
+ }
+ }
+
+ ///
+ public IAsyncEnumerable DownloadAlbumAsync(string albumId, [StringSyntax(StringSyntaxAttribute.Uri)] string destinationPath, CancellationToken ct = default)
+ {
+ return DownloadAlbumAsync(albumId, new DirectoryInfo(destinationPath), ct);
+ }
+
///
/// Upload files based on the requestBase type
///
@@ -85,10 +221,74 @@ public Catbox(ICatBoxClient client)
{
return request.UploadRequest switch
{
- { IsFirst: true } => _client.UploadFilesAsync(request.UploadRequest, ct),
- { IsSecond: true } => _client.UploadFilesAsStreamAsync(request.UploadRequest.Second, ct),
- { IsThird: true } => _client.UploadFilesAsUrlAsync(request.UploadRequest, ct),
+ { IsFirst: true } => client.UploadFilesAsync(request.UploadRequest, ct),
+ { IsSecond: true } => client.UploadFilesAsStreamAsync(request.UploadRequest.Second, ct),
+ { IsThird: true } => client.UploadFilesAsUrlAsync(request.UploadRequest, ct),
_ => throw new InvalidOperationException("Invalid requestBase type")
};
}
+
+ ///
+ /// Materializes lazy enumerables and counts total files in the request
+ ///
+ private static (int Count, UploadToAlbumRequest MaterializedRequest) MaterializeAndCountRequest(UploadToAlbumRequest request)
+ {
+ return request.UploadRequest switch
+ {
+ { IsFirst: true } => MaterializeFileUploadRequest(request),
+ { IsSecond: true } => MaterializeStreamUploadRequest(request),
+ { IsThird: true } => MaterializeUrlUploadRequest(request),
+ _ => throw new InvalidOperationException("Invalid request type")
+ };
+
+ static (int, UploadToAlbumRequest) MaterializeFileUploadRequest(UploadToAlbumRequest req)
+ {
+ var files = req.UploadRequest.First.Files.ToList();
+ var materialized = req with
+ {
+ UploadRequest = req.UploadRequest.First with { Files = files }
+ };
+ return (files.Count, materialized);
+ }
+
+ static (int, UploadToAlbumRequest) MaterializeStreamUploadRequest(UploadToAlbumRequest req)
+ {
+ var streams = req.UploadRequest.Second.ToList();
+ var materialized = req with { UploadRequest = streams };
+ return (streams.Count, materialized);
+ }
+
+ static (int, UploadToAlbumRequest) MaterializeUrlUploadRequest(UploadToAlbumRequest req)
+ {
+ var urls = req.UploadRequest.Third.Files.ToList();
+ var materialized = req with
+ {
+ UploadRequest = req.UploadRequest.Third with { Files = urls }
+ };
+ return (urls.Count, materialized);
+ }
+ }
+
+ ///
+ /// Creates a new request with only the first N files
+ ///
+ private static UploadToAlbumRequest TakeFilesFromRequest(UploadToAlbumRequest request, int count)
+ {
+ return request.UploadRequest switch
+ {
+ { IsFirst: true } => request with
+ {
+ UploadRequest = request.UploadRequest.First with { Files = request.UploadRequest.First.Files.Take(count) }
+ },
+ { IsSecond: true } => request with
+ {
+ UploadRequest = request.UploadRequest.Second.Take(count).ToList()
+ },
+ { IsThird: true } => request with
+ {
+ UploadRequest = request.UploadRequest.Third with { Files = request.UploadRequest.Third.Files.Take(count) }
+ },
+ _ => throw new InvalidOperationException("Invalid request type")
+ };
+ }
}
\ No newline at end of file
diff --git a/src/CatBox.NET/Client/CatBox/CatBoxClient.cs b/src/CatBox.NET/Client/CatBox/CatBoxClient.cs
index 888adf3..042b3bf 100644
--- a/src/CatBox.NET/Client/CatBox/CatBoxClient.cs
+++ b/src/CatBox.NET/Client/CatBox/CatBoxClient.cs
@@ -1,10 +1,13 @@
using System.Runtime.CompilerServices;
+using System.Text.Json;
using CatBox.NET.Enums;
using CatBox.NET.Requests.Album;
using CatBox.NET.Requests.Album.Create;
using CatBox.NET.Requests.Album.Modify;
using CatBox.NET.Requests.File;
using CatBox.NET.Requests.URL;
+using CatBox.NET.Responses;
+using CatBox.NET.Responses.Album;
using Microsoft.Extensions.Options;
using static CatBox.NET.Client.Common;
@@ -105,6 +108,41 @@ public interface ICatBoxClient
/// .
/// Use to edit an album
Task ModifyAlbumAsync(ModifyAlbumImagesRequest modifyAlbumImagesRequest, CancellationToken ct = default);
+
+ ///
+ /// Gets album information including the list of files
+ ///
+ /// Request containing the album ID
+ /// Cancellation Token
+ /// When is null
+ /// When is null or whitespace
+ /// When something bad happens when talking to the API
+ /// Album information with parsed file list
+ Task GetAlbumAsync(GetAlbumRequest getAlbumRequest, CancellationToken ct = default);
+
+ ///
+ /// Downloads a file from CatBox to the specified directory
+ ///
+ /// The file name (e.g., "abc123.png")
+ /// Directory to save the file
+ /// Cancellation Token
+ /// When fileName is null or whitespace
+ /// When destination is null
+ /// When something bad happens when talking to the API
+ /// Skips download if file already exists at destination
+ Task DownloadFileAsync(string fileName, DirectoryInfo destination, CancellationToken ct = default);
+
+ ///
+ /// Downloads a file from CatBox to the specified path
+ ///
+ /// The file name (e.g., "abc123.png")
+ /// Directory path to save the file
+ /// Cancellation Token
+ /// When fileName is null or whitespace
+ /// When destinationPath is null or whitespace
+ /// When something bad happens when talking to the API
+ /// Skips download if file already exists at destination
+ Task DownloadFileAsync(string fileName, string destinationPath, CancellationToken ct = default);
}
public sealed class CatBoxClient : ICatBoxClient
@@ -152,8 +190,8 @@ public CatBoxClient(HttpClient client, IOptions catboxOptions)
if (!string.IsNullOrWhiteSpace(fileUploadRequest.UserHash))
content.Add(new StringContent(fileUploadRequest.UserHash), RequestParameters.UserHash);
- using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct);
- yield return await response.Content.ReadAsStringAsync(ct);
+ using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct).ConfigureAwait(false);
+ yield return await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
}
}
@@ -178,8 +216,8 @@ public CatBoxClient(HttpClient client, IOptions catboxOptions)
if (!string.IsNullOrWhiteSpace(uploadRequest.UserHash))
content.Add(new StringContent(uploadRequest.UserHash), RequestParameters.UserHash);
- using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct);
- yield return await response.Content.ReadAsStringAsync(ct);
+ using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct).ConfigureAwait(false);
+ yield return await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
}
}
@@ -200,8 +238,8 @@ public CatBoxClient(HttpClient client, IOptions catboxOptions)
if (!string.IsNullOrWhiteSpace(urlUploadRequest.UserHash))
content.Add(new StringContent(urlUploadRequest.UserHash), RequestParameters.UserHash);
- using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct);
- yield return await response.Content.ReadAsStringAsync(ct);
+ using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct).ConfigureAwait(false);
+ yield return await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
}
}
@@ -221,24 +259,18 @@ public CatBoxClient(HttpClient client, IOptions catboxOptions)
{ new StringContent(fileNames), RequestParameters.Files }
};
- using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct);
- return await response.Content.ReadAsStringAsync(ct);
+ using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct).ConfigureAwait(false);
+ return await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
}
///
public async Task CreateAlbumAsync(RemoteCreateAlbumRequest remoteCreateAlbumRequest, CancellationToken ct = default)
{
- ThrowIfAlbumCreationRequestIsInvalid(remoteCreateAlbumRequest);
+ Throw.IfAlbumCreationRequestIsInvalid(remoteCreateAlbumRequest);
- var links = remoteCreateAlbumRequest.Files.Select(link =>
- {
- if (link?.Contains(_catboxOptions.CatBoxUrl!.Host) is true)
- {
- return new Uri(link).PathAndQuery[1..];
- }
+ var links = remoteCreateAlbumRequest.Files.Select(link => link.ToCatboxImageName()).ToList();
- return link;
- });
+ Throw.IfAlbumFileLimitExceeds(links.Count);
var fileNames = string.Join(" ", links);
ArgumentException.ThrowIfNullOrWhiteSpace(fileNames);
@@ -256,8 +288,8 @@ public CatBoxClient(HttpClient client, IOptions catboxOptions)
if (!string.IsNullOrWhiteSpace(remoteCreateAlbumRequest.Description))
content.Add(new StringContent(remoteCreateAlbumRequest.Description), RequestParameters.Description);
- using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct);
- return await response.Content.ReadAsStringAsync(ct);
+ using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct).ConfigureAwait(false);
+ return await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
}
///
@@ -284,8 +316,8 @@ public CatBoxClient(HttpClient client, IOptions catboxOptions)
{ new StringContent(fileNames), RequestParameters.Files }
};
- using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct);
- return await response.Content.ReadAsStringAsync(ct);
+ using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct).ConfigureAwait(false);
+ return await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
}
///
@@ -297,7 +329,12 @@ public CatBoxClient(HttpClient client, IOptions catboxOptions)
Throw.IfAlbumRequestTypeInvalid(IsAlbumRequestTypeValid(modifyAlbumImagesRequest), nameof(modifyAlbumImagesRequest.Request));
Throw.IfAlbumOperationInvalid(modifyAlbumImagesRequest.Request, RequestType.AddToAlbum, RequestType.RemoveFromAlbum, RequestType.DeleteAlbum);
- var fileNames = string.Join(" ", modifyAlbumImagesRequest.Files);
+ var files = modifyAlbumImagesRequest.Files.ToList();
+
+ if (modifyAlbumImagesRequest.Request == RequestType.AddToAlbum)
+ Throw.IfAlbumFileLimitExceeds(files.Count);
+
+ var fileNames = string.Join(" ", files);
using var content = new MultipartFormDataContent
{
@@ -313,7 +350,73 @@ public CatBoxClient(HttpClient client, IOptions catboxOptions)
content.Add(new StringContent(fileNames), RequestParameters.Files);
}
- using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct);
- return await response.Content.ReadAsStringAsync(ct);
+ using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct).ConfigureAwait(false);
+ return await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task GetAlbumAsync(GetAlbumRequest getAlbumRequest, CancellationToken ct = default)
+ {
+ ArgumentNullException.ThrowIfNull(getAlbumRequest);
+ ArgumentException.ThrowIfNullOrWhiteSpace(getAlbumRequest.AlbumId);
+
+ using var content = new MultipartFormDataContent
+ {
+ { new StringContent(RequestType.GetAlbum), RequestParameters.Request },
+ { new StringContent(getAlbumRequest.AlbumId), RequestParameters.AlbumIdShort }
+ };
+
+ using var response = await _client.PostAsync(_catboxOptions.CatBoxUrl, content, ct).ConfigureAwait(false);
+ var json = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
+
+ var apiResponse = JsonSerializer.Deserialize(json, CatBoxJsonContext.Default.GetAlbumApiResponse);
+
+ // ExceptionHandler will throw CatBoxAlbumNotFoundException for HTTP 400 errors
+ // This check is only for unexpected response format
+ if (apiResponse?.Data is null)
+ throw new HttpRequestException($"Unexpected response format: {json}");
+
+ var files = string.IsNullOrWhiteSpace(apiResponse.Data.Files)
+ ? []
+ : apiResponse.Data.Files.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+
+ return new AlbumInfo
+ {
+ Title = apiResponse.Data.Title,
+ Description = apiResponse.Data.Description,
+ AlbumId = apiResponse.Data.Short,
+ DateCreated = apiResponse.Data.DateCreated,
+ Files = files
+ };
+ }
+
+ ///
+ public async Task DownloadFileAsync(string fileName, DirectoryInfo destination, CancellationToken ct = default)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
+ ArgumentNullException.ThrowIfNull(destination);
+
+ if (!destination.Exists)
+ destination.Create();
+
+ var filePath = Path.Combine(destination.FullName, fileName);
+
+ // Skip if file exists
+ if (File.Exists(filePath))
+ return;
+
+ var fileUrl = new Uri(_catboxOptions.CatBoxFilesUrl, fileName);
+
+ using var response = await _client.GetAsync(fileUrl, ct).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ await using var fileStream = File.Create(filePath);
+ await response.Content.CopyToAsync(fileStream, ct).ConfigureAwait(false);
+ }
+
+ ///
+ public Task DownloadFileAsync(string fileName, string destinationPath, CancellationToken ct = default)
+ {
+ return DownloadFileAsync(fileName, new DirectoryInfo(destinationPath), ct);
}
}
\ No newline at end of file
diff --git a/src/CatBox.NET/Client/Common.cs b/src/CatBox.NET/Client/Common.cs
index 889be27..bc53b5d 100644
--- a/src/CatBox.NET/Client/Common.cs
+++ b/src/CatBox.NET/Client/Common.cs
@@ -1,12 +1,16 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
using CatBox.NET.Enums;
-using CatBox.NET.Requests.Album.Create;
using CatBox.NET.Requests.Album.Modify;
namespace CatBox.NET.Client;
internal static class Common
{
+ ///
+ /// Maximum number of files allowed in a CatBox album
+ ///
+ public const int MaxAlbumFiles = 500;
+
///
/// These file extensions are not allowed by the API, so filter them out
///
@@ -23,20 +27,6 @@ _ when extension.Contains(".doc") => false,
};
}
- ///
- /// Validates an Album Creation Request
- ///
- /// The album creation requestBase to validate
- /// when the requestBase is null
- /// when the description is null
- /// when the title is null
- public static void ThrowIfAlbumCreationRequestIsInvalid(AlbumCreationRequestBase requestBase)
- {
- ArgumentNullException.ThrowIfNull(requestBase);
- ArgumentException.ThrowIfNullOrWhiteSpace(requestBase.Description);
- ArgumentException.ThrowIfNullOrWhiteSpace(requestBase.Title);
- }
-
///
/// 1. Filter Invalid Request Types on the Album Endpoint
/// 2. Check that the user hash is not null, empty, or whitespace when attempting to modify or delete an album. User hash is required for those operations
@@ -56,4 +46,59 @@ public static bool IsAlbumRequestTypeValid(ModifyAlbumImagesRequest imagesReques
request == RequestType.RemoveFromAlbum ||
request == RequestType.DeleteAlbum) && hasUserHash;
}
+
+ /// The URL or file name
+ extension(string? url)
+ {
+ ///
+ /// Extracts the file name from a CatBox file URL (files.catbox.moe) or returns the original string.
+ ///
+ /// "https://files.catbox.moe/abc123.jpg" → "abc123.jpg"
+ /// The extracted file name or original string
+ public string? ToCatboxImageName()
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ return url;
+
+ if (url.Contains("files.catbox.moe"))
+ return url.Split('/')[^1];
+
+ return url;
+ }
+
+ ///
+ /// Extracts the album short code from a CatBox album URL (catbox.moe/c/) or returns the original string.
+ ///
+ /// "https://catbox.moe/c/abc123" → "abc123"
+ /// The extracted album short code or original string
+ public string? ToAlbumShortCode()
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ return url;
+
+ if (url.Contains("catbox.moe/c/"))
+ return url.Split('/')[^1];
+
+ return url;
+ }
+ }
+
+ /// The async enumerable source
+ extension(IAsyncEnumerable source)
+ {
+ ///
+ /// Asynchronously collects all elements from an IAsyncEnumerable into a List
+ ///
+ /// Cancellation token
+ /// A list containing all elements
+ public async Task> ToListAsync(CancellationToken ct = default)
+ {
+ var list = new List();
+ await foreach (var item in source.WithCancellation(ct).ConfigureAwait(false))
+ {
+ list.Add(item);
+ }
+ return list;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/CatBox.NET/Client/Litterbox/LitterboxClient.cs b/src/CatBox.NET/Client/Litterbox/LitterboxClient.cs
index 6a8dc4c..2aafaf3 100644
--- a/src/CatBox.NET/Client/Litterbox/LitterboxClient.cs
+++ b/src/CatBox.NET/Client/Litterbox/LitterboxClient.cs
@@ -71,9 +71,9 @@ public LitterboxClient(HttpClient client, IOptions catboxOptions)
{ new StringContent(RequestType.UploadFile), RequestParameters.Request },
{ new StringContent(temporaryFileUploadRequest.Expiry), RequestParameters.Expiry },
{ new StreamContent(fileStream), RequestParameters.FileToUpload, imageFile.Name }
- }, ct);
+ }, ct).ConfigureAwait(false);
- yield return await response.Content.ReadAsStringAsync(ct);
+ yield return await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
}
}
@@ -94,8 +94,8 @@ public LitterboxClient(HttpClient client, IOptions catboxOptions)
new StreamContent(temporaryStreamUploadRequest.Stream), RequestParameters.FileToUpload,
temporaryStreamUploadRequest.FileName
}
- }, ct);
+ }, ct).ConfigureAwait(false);
- return await response.Content.ReadAsStringAsync(ct);
+ return await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
}
}
\ No newline at end of file
diff --git a/src/CatBox.NET/Client/Throw.cs b/src/CatBox.NET/Client/Throw.cs
index c103d16..619abfb 100644
--- a/src/CatBox.NET/Client/Throw.cs
+++ b/src/CatBox.NET/Client/Throw.cs
@@ -1,5 +1,6 @@
+using System.Diagnostics.CodeAnalysis;
using CatBox.NET.Enums;
-using CatBox.NET.Requests.Album.Modify;
+using CatBox.NET.Requests.Album.Create;
namespace CatBox.NET.Client;
@@ -38,7 +39,7 @@ public static void IfLitterboxFileSizeExceeds(long fileSize, long maxSize)
/// Whether the request type is valid
/// The name of the parameter that is invalid
/// When the request type is invalid for the album endpoint
- public static void IfAlbumRequestTypeInvalid(bool isValid, string paramName)
+ public static void IfAlbumRequestTypeInvalid([DoesNotReturnIf(false)] bool isValid, string paramName)
{
if (!isValid)
throw new ArgumentException("Invalid Request Type for album endpoint", paramName);
@@ -59,4 +60,30 @@ public static void IfAlbumOperationInvalid(RequestType request, params RequestTy
throw new InvalidOperationException("Invalid Request Type for album endpoint");
}
+
+ ///
+ /// Validates an Album Creation Request
+ ///
+ /// The album creation requestBase to validate
+ /// when the requestBase is null
+ /// when the description is null
+ /// when the title is null
+ public static void IfAlbumCreationRequestIsInvalid(AlbumCreationRequestBase requestBase)
+ {
+ ArgumentNullException.ThrowIfNull(requestBase);
+ ArgumentException.ThrowIfNullOrWhiteSpace(requestBase.Description);
+ ArgumentException.ThrowIfNullOrWhiteSpace(requestBase.Title);
+ }
+
+ ///
+ /// Throws if the file count exceeds the album limit
+ ///
+ /// The number of files being added to the album
+ /// The maximum allowed files (default: )
+ /// When file count exceeds the maximum
+ public static void IfAlbumFileLimitExceeds(int fileCount, int maxFiles = Common.MaxAlbumFiles)
+ {
+ if (fileCount > maxFiles)
+ throw new Exceptions.CatBoxAlbumFileLimitExceededException(fileCount);
+ }
}
diff --git a/src/CatBox.NET/Enums/RequestType.cs b/src/CatBox.NET/Enums/RequestType.cs
index 84f554a..82a0f37 100644
--- a/src/CatBox.NET/Enums/RequestType.cs
+++ b/src/CatBox.NET/Enums/RequestType.cs
@@ -14,4 +14,5 @@ namespace CatBox.NET.Enums;
[Member("AddToAlbum", "addtoalbum")]
[Member("RemoveFromAlbum", "removefromalbum")]
[Member("DeleteAlbum", "deletealbum")]
+[Member("GetAlbum", "getalbum")]
public sealed partial class RequestType;
diff --git a/src/CatBox.NET/Exceptions/CatBoxAPIExceptions.cs b/src/CatBox.NET/Exceptions/CatBoxAPIExceptions.cs
index 71f2a30..0716298 100644
--- a/src/CatBox.NET/Exceptions/CatBoxAPIExceptions.cs
+++ b/src/CatBox.NET/Exceptions/CatBoxAPIExceptions.cs
@@ -1,7 +1,9 @@
using System.Diagnostics;
using System.Net;
+using System.Text.Json;
using CatBox.NET.Client;
using CatBox.NET.Logging;
+using CatBox.NET.Responses;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -29,6 +31,24 @@ internal sealed class CatBoxMissingFileException : Exception
public override string Message { get; } = "The FileToUpload parameter was not specified or is missing content. Did you miss an API parameter?";
}
+// API Response Message: No userhash provided!
+internal sealed class CatBoxMissingUserHashException : Exception
+{
+ public override string Message { get; } = "The UserHash parameter was not provided. UserHash is required for album modification and deletion operations.";
+}
+
+// API Response Message: No valid link given.
+internal sealed class CatBoxMissingUrlException : Exception
+{
+ public override string Message { get; } = "The URL parameter was not provided or is invalid. A valid URL is required for URL upload operations.";
+}
+
+// API Response Message: Tried to delete a file that didn't belong to that userhash.
+internal sealed class CatBoxFileOwnershipException : Exception
+{
+ public override string Message { get; } = "Attempted to delete a file that does not belong to the provided userhash. You can only delete files you own.";
+}
+
//API Response Message: No expire time specified.
internal sealed class LitterboxInvalidExpiry : Exception
{
@@ -47,36 +67,97 @@ internal sealed class CatBoxFileSizeLimitExceededException(long fileSize) : Exce
public override string Message { get; } = $"File size exceeds CatBox's 200 MB upload limit. File size: {fileSize:N0} bytes ({fileSize / 1024.0 / 1024.0:F2} MB)";
}
+// Album exceeds CatBox's file limit
+internal sealed class CatBoxAlbumFileLimitExceededException(int fileCount) : Exception
+{
+ public override string Message { get; } = $"Album exceeds CatBox's {Common.MaxAlbumFiles} file limit. Attempted to add {fileCount} files.";
+}
+
internal sealed class ExceptionHandler(ILogger? logger = null) : DelegatingHandler
{
+ // Plain-text error messages (HTTP 412)
private const string FileNotFound = "File doesn't exist?";
private const string AlbumNotFound = "No album found for user specified.";
- private const string MissingRequestType = "No requestBase type given.";
+ private const string MissingRequestType = "No request type given?";
private const string MissingFileParameter = "No files given.";
+ private const string MissingUserHash = "No userhash provided!";
+ private const string MissingUrl = "No valid link given.";
+ private const string FileOwnershipMismatch = "Tried to delete a file that didn't belong to that userhash.";
private const string InvalidExpiry = "No expire time specified.";
-
+
+ // JSON error messages (HTTP 400)
+ private const string JsonAlbumNotFound = "An album was not found. Either the album never existed, or was deleted.";
+
private readonly ILogger _logger = logger ?? NullLogger.Instance;
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
- var response = await base.SendAsync(request, cancellationToken);
- if (response.StatusCode != HttpStatusCode.PreconditionFailed)
+ var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
+
+ // Only process error status codes
+ if (response.IsSuccessStatusCode)
return response;
-
- var content = response.Content;
- var apiErrorMessage = await content.ReadAsStringAsync(cancellationToken);
- _logger.LogCatBoxAPIException(response.StatusCode, apiErrorMessage);
-
- throw apiErrorMessage switch
+
+ var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+
+ // Try JSON parsing first (HTTP 400 pattern)
+ if (response.StatusCode == HttpStatusCode.BadRequest)
+ {
+ var exception = TryParseJsonError(content);
+ if (exception is not null)
+ {
+ _logger.LogCatBoxAPIException(response.StatusCode, content);
+ throw exception;
+ }
+ }
+
+ // Fall back to plain-text matching (HTTP 412 pattern)
+ if (response.StatusCode == HttpStatusCode.PreconditionFailed)
+ {
+ _logger.LogCatBoxAPIException(response.StatusCode, content);
+ throw MatchPlainTextError(content, response.StatusCode);
+ }
+
+ // Return response for unhandled status codes (let caller handle)
+ return response;
+ }
+
+ private static Exception? TryParseJsonError(string content)
+ {
+ try
+ {
+ var errorResponse = JsonSerializer.Deserialize(content, CatBoxJsonContext.Default.CatBoxApiErrorResponse);
+ if (errorResponse is { Success: false, Data.Error: not null })
+ {
+ return errorResponse.Data.Error switch
+ {
+ var e when e.Equals(JsonAlbumNotFound, StringComparison.OrdinalIgnoreCase) => new CatBoxAlbumNotFoundException(),
+ _ => new HttpRequestException($"CatBox API Error: {errorResponse.Data.Error}")
+ };
+ }
+ }
+ catch (JsonException)
+ {
+ // Not valid JSON, return null to try other parsing
+ }
+ return null;
+ }
+
+ private static Exception MatchPlainTextError(string content, HttpStatusCode statusCode)
+ {
+ return content switch
{
AlbumNotFound => new CatBoxAlbumNotFoundException(),
FileNotFound => new CatBoxFileNotFoundException(),
InvalidExpiry => new LitterboxInvalidExpiry(),
MissingFileParameter => new CatBoxMissingFileException(),
+ MissingUserHash => new CatBoxMissingUserHashException(),
+ MissingUrl => new CatBoxMissingUrlException(),
+ FileOwnershipMismatch => new CatBoxFileOwnershipException(),
MissingRequestType => new CatBoxMissingRequestTypeException(),
- _ when response.StatusCode is >= HttpStatusCode.BadRequest and < HttpStatusCode.InternalServerError => new HttpRequestException($"Generic Request Failure: {apiErrorMessage}"),
- _ when response.StatusCode >= HttpStatusCode.InternalServerError => new HttpRequestException($"Generic Internal Server Error: {apiErrorMessage}"),
- _ => new UnreachableException($"I don't know how you got here, but please create an issue on our GitHub (https://github.com/ChaseDRedmon/CatBox.NET): {apiErrorMessage}")
+ _ when statusCode is >= HttpStatusCode.BadRequest and < HttpStatusCode.InternalServerError => new HttpRequestException($"Generic Request Failure: {content}"),
+ _ when statusCode >= HttpStatusCode.InternalServerError => new HttpRequestException($"Generic Internal Server Error: {content}"),
+ _ => new UnreachableException($"Unexpected error: {content}")
};
}
}
\ No newline at end of file
diff --git a/src/CatBox.NET/Requests/Album/GetAlbumRequest.cs b/src/CatBox.NET/Requests/Album/GetAlbumRequest.cs
new file mode 100644
index 0000000..20ed8b0
--- /dev/null
+++ b/src/CatBox.NET/Requests/Album/GetAlbumRequest.cs
@@ -0,0 +1,12 @@
+namespace CatBox.NET.Requests.Album;
+
+///
+/// Request to retrieve the list of files in an album
+///
+public sealed record GetAlbumRequest
+{
+ ///
+ /// The unique identifier for the album (API value: "short")
+ ///
+ public required string AlbumId { get; init; }
+}
diff --git a/src/CatBox.NET/Requests/Album/Modify/ModifyAlbumImagesRequest.cs b/src/CatBox.NET/Requests/Album/Modify/ModifyAlbumImagesRequest.cs
index f1f3a33..560dac1 100644
--- a/src/CatBox.NET/Requests/Album/Modify/ModifyAlbumImagesRequest.cs
+++ b/src/CatBox.NET/Requests/Album/Modify/ModifyAlbumImagesRequest.cs
@@ -9,5 +9,5 @@ public sealed record ModifyAlbumImagesRequest : AlbumBase
/// The list of files associated with the album
///
/// may alter the significance of this collection
- public required IEnumerable Files { get; init; }
+ public required IEnumerable Files { get; init; }
}
\ No newline at end of file
diff --git a/src/CatBox.NET/Responses/Album/AlbumInfo.cs b/src/CatBox.NET/Responses/Album/AlbumInfo.cs
new file mode 100644
index 0000000..8623326
--- /dev/null
+++ b/src/CatBox.NET/Responses/Album/AlbumInfo.cs
@@ -0,0 +1,22 @@
+namespace CatBox.NET.Responses.Album;
+
+///
+/// Processed album information with parsed file list
+///
+public sealed record AlbumInfo
+{
+ /// Album title
+ public required string Title { get; init; }
+
+ /// Album description
+ public required string Description { get; init; }
+
+ /// Album ID (short code)
+ public required string AlbumId { get; init; }
+
+ /// Date the album was created
+ public required DateOnly DateCreated { get; init; }
+
+ /// List of file names in the album
+ public required string[] Files { get; init; }
+}
diff --git a/src/CatBox.NET/Responses/Album/AlbumUploadResult.cs b/src/CatBox.NET/Responses/Album/AlbumUploadResult.cs
new file mode 100644
index 0000000..bc038d4
--- /dev/null
+++ b/src/CatBox.NET/Responses/Album/AlbumUploadResult.cs
@@ -0,0 +1,22 @@
+namespace CatBox.NET.Responses.Album;
+
+///
+/// Result of uploading files to an album with capacity awareness
+///
+public sealed record AlbumUploadResult
+{
+ /// The album URL after successful modification, or null if no files were uploaded
+ public required string? AlbumUrl { get; init; }
+
+ /// Number of files successfully uploaded and added to the album
+ public required int FilesUploaded { get; init; }
+
+ /// Number of files that were skipped due to album capacity limit
+ public required int FilesSkipped { get; init; }
+
+ /// Remaining capacity in the album after this operation
+ public required int RemainingCapacity { get; init; }
+
+ /// Whether the album has reached its maximum capacity of 500 files
+ public bool ReachedLimit => RemainingCapacity == 0;
+}
diff --git a/src/CatBox.NET/Responses/Album/GetAlbumApiResponse.cs b/src/CatBox.NET/Responses/Album/GetAlbumApiResponse.cs
new file mode 100644
index 0000000..2a2b7eb
--- /dev/null
+++ b/src/CatBox.NET/Responses/Album/GetAlbumApiResponse.cs
@@ -0,0 +1,36 @@
+using System.Text.Json.Serialization;
+
+namespace CatBox.NET.Responses.Album;
+
+///
+/// Raw API response from getalbum endpoint
+///
+internal sealed record GetAlbumApiResponse
+{
+ [JsonPropertyName("data")]
+ public required AlbumApiData? Data { get; init; }
+
+ [JsonPropertyName("success")]
+ public bool Success { get; init; }
+
+ [JsonPropertyName("status")]
+ public int Status { get; init; }
+}
+
+internal sealed record AlbumApiData
+{
+ [JsonPropertyName("files")]
+ public required string Files { get; init; }
+
+ [JsonPropertyName("title")]
+ public required string Title { get; init; }
+
+ [JsonPropertyName("description")]
+ public required string Description { get; init; }
+
+ [JsonPropertyName("short")]
+ public required string Short { get; init; }
+
+ [JsonPropertyName("datecreated")]
+ public required DateOnly DateCreated { get; init; }
+}
diff --git a/src/CatBox.NET/Responses/CatBoxApiErrorResponse.cs b/src/CatBox.NET/Responses/CatBoxApiErrorResponse.cs
new file mode 100644
index 0000000..f64440d
--- /dev/null
+++ b/src/CatBox.NET/Responses/CatBoxApiErrorResponse.cs
@@ -0,0 +1,30 @@
+using System.Text.Json.Serialization;
+
+namespace CatBox.NET.Responses;
+
+///
+/// Generic CatBox API error response format
+///
+internal sealed record CatBoxApiErrorResponse
+{
+ [JsonPropertyName("data")]
+ public CatBoxApiErrorData? Data { get; init; }
+
+ [JsonPropertyName("success")]
+ public bool Success { get; init; }
+
+ [JsonPropertyName("status")]
+ public int Status { get; init; }
+}
+
+internal sealed record CatBoxApiErrorData
+{
+ [JsonPropertyName("error")]
+ public string? Error { get; init; }
+
+ [JsonPropertyName("endpoint")]
+ public string? Endpoint { get; init; }
+
+ [JsonPropertyName("method")]
+ public string? Method { get; init; }
+}
diff --git a/src/CatBox.NET/Responses/CatBoxJsonContext.cs b/src/CatBox.NET/Responses/CatBoxJsonContext.cs
new file mode 100644
index 0000000..e7ea572
--- /dev/null
+++ b/src/CatBox.NET/Responses/CatBoxJsonContext.cs
@@ -0,0 +1,8 @@
+using System.Text.Json.Serialization;
+using CatBox.NET.Responses.Album;
+
+namespace CatBox.NET.Responses;
+
+[JsonSerializable(typeof(GetAlbumApiResponse))]
+[JsonSerializable(typeof(CatBoxApiErrorResponse))]
+internal sealed partial class CatBoxJsonContext : JsonSerializerContext;
diff --git a/tests/CatBox.Tests/CatBox.Tests.csproj b/tests/CatBox.Tests/CatBox.Tests.csproj
index 0921c23..c8256d3 100644
--- a/tests/CatBox.Tests/CatBox.Tests.csproj
+++ b/tests/CatBox.Tests/CatBox.Tests.csproj
@@ -34,7 +34,7 @@
-
+
PreserveNewest
diff --git a/tests/CatBox.Tests/CatBoxClientIntegrationTests.cs b/tests/CatBox.Tests/CatBoxClientIntegrationTests.cs
index a323e4b..85d2e82 100644
--- a/tests/CatBox.Tests/CatBoxClientIntegrationTests.cs
+++ b/tests/CatBox.Tests/CatBoxClientIntegrationTests.cs
@@ -1,10 +1,12 @@
using CatBox.NET;
using CatBox.NET.Client;
using CatBox.NET.Enums;
+using CatBox.NET.Requests.Album;
using CatBox.NET.Requests.Album.Create;
using CatBox.NET.Requests.Album.Modify;
using CatBox.NET.Requests.File;
using CatBox.NET.Requests.URL;
+using CatBox.NET.Responses.Album;
using CatBox.Tests.Helpers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -59,7 +61,7 @@ public async Task OneTimeTearDown()
// Delete albums first (they reference files)
if (_createdAlbums.Count > 0)
{
- TestContext.WriteLine($"Cleaning up {_createdAlbums.Count} album(s)...");
+ await TestContext.Out.WriteLineAsync($"Cleaning up {_createdAlbums.Count} album(s)...");
foreach (var albumId in _createdAlbums)
{
try
@@ -72,11 +74,11 @@ public async Task OneTimeTearDown()
Files = []
};
await _client.ModifyAlbumAsync(deleteAlbumRequest);
- TestContext.WriteLine($"Deleted album: {albumId}");
+ await TestContext.Out.WriteLineAsync($"Deleted album: {albumId}");
}
catch (Exception ex)
{
- TestContext.WriteLine($"Failed to delete album {albumId}: {ex.Message}");
+ await TestContext.Out.WriteLineAsync($"Failed to delete album {albumId}: {ex.Message}");
}
}
}
@@ -84,7 +86,7 @@ public async Task OneTimeTearDown()
// Then delete individual files
if (_uploadedFiles.Count > 0)
{
- TestContext.WriteLine($"Cleaning up {_uploadedFiles.Count} file(s)...");
+ await TestContext.Out.WriteLineAsync($"Cleaning up {_uploadedFiles.Count} file(s)...");
var deleteRequest = new DeleteFileRequest
{
UserHash = IntegrationTestConfig.UserHash!,
@@ -92,12 +94,12 @@ public async Task OneTimeTearDown()
};
var result = await _client.DeleteMultipleFilesAsync(deleteRequest);
- TestContext.WriteLine($"Delete result: {result}");
+ await TestContext.Out.WriteLineAsync($"Delete result: {result}");
}
}
catch (Exception ex)
{
- TestContext.WriteLine($"Cleanup error: {ex.Message}");
+ await TestContext.Out.WriteLineAsync($"Cleanup error: {ex.Message}");
}
}
@@ -106,14 +108,13 @@ private void TrackUploadedFile(string? url)
if (string.IsNullOrWhiteSpace(url))
return;
- // Extract filename from URL (e.g., "abc123.png" from "https://files.catbox.moe/abc123.png")
- var fileName = new Uri(url).Segments.Last();
+ var fileName = url.ToCatboxImageName()!;
using (_lock.EnterScope())
{
if (!_uploadedFiles.Contains(fileName))
{
_uploadedFiles.Add(fileName);
- TestContext.WriteLine($"Tracked file for cleanup: {fileName}");
+ TestContext.Out.WriteLine($"Tracked file for cleanup: {fileName}");
}
}
}
@@ -123,14 +124,13 @@ private void TrackCreatedAlbum(string? albumUrl)
if (string.IsNullOrWhiteSpace(albumUrl))
return;
- // Extract album ID from URL (e.g., "abc123" from "https://catbox.moe/c/abc123")
- var albumId = new Uri(albumUrl).Segments.Last();
+ var albumId = albumUrl.ToAlbumShortCode()!;
using (_lock.EnterScope())
{
if (!_createdAlbums.Contains(albumId))
{
_createdAlbums.Add(albumId);
- TestContext.WriteLine($"Tracked album for cleanup: {albumId}");
+ TestContext.Out.WriteLine($"Tracked album for cleanup: {albumId}");
}
}
}
@@ -161,7 +161,7 @@ public async Task UploadFilesAsync_WithFileFromDisk_Succeeds()
results.Count.ShouldBe(1);
results[0].ShouldNotBeNullOrWhiteSpace();
results[0].ShouldStartWith("https://files.catbox.moe/");
- TestContext.WriteLine($"Uploaded file URL: {results[0]}");
+ await TestContext.Out.WriteLineAsync($"Uploaded file URL: {results[0]}");
}
[Test]
@@ -195,7 +195,7 @@ public async Task UploadFilesAsStreamAsync_WithMemoryStream_Succeeds()
results.Count.ShouldBe(1);
results[0].ShouldNotBeNullOrWhiteSpace();
results[0].ShouldStartWith("https://files.catbox.moe/");
- TestContext.WriteLine($"Uploaded stream URL: {results[0]}");
+ await TestContext.Out.WriteLineAsync($"Uploaded stream URL: {results[0]}");
}
[Test]
@@ -221,7 +221,7 @@ public async Task UploadFilesAsUrlAsync_WithPublicUrl_Succeeds()
results.Count.ShouldBe(1);
results[0].ShouldNotBeNullOrWhiteSpace();
results[0].ShouldStartWith("https://files.catbox.moe/");
- TestContext.WriteLine($"Uploaded from URL: {results[0]}");
+ await TestContext.Out.WriteLineAsync($"Uploaded from URL: {results[0]}");
}
[Test]
@@ -246,7 +246,7 @@ public async Task CreateAlbumAsync_WithUploadedFiles_Succeeds()
// Extract filenames from URLs
var fileNames = uploadedFileUrls
.Where(url => !string.IsNullOrWhiteSpace(url))
- .Select(url => new Uri(url!).Segments.Last())
+ .Select(url => url.ToCatboxImageName()!)
.ToList();
var albumRequest = new RemoteCreateAlbumRequest
@@ -264,7 +264,7 @@ public async Task CreateAlbumAsync_WithUploadedFiles_Succeeds()
// Assert
albumUrl.ShouldNotBeNullOrWhiteSpace();
albumUrl.ShouldStartWith("https://catbox.moe/c/");
- TestContext.WriteLine($"Created album: {albumUrl}");
+ await TestContext.Out.WriteLineAsync($"Created album: {albumUrl}");
}
[Test]
@@ -286,7 +286,7 @@ public async Task ModifyAlbumAsync_AddAndRemoveFiles_Succeeds()
break;
}
TrackUploadedFile(uploadedUrl);
- var fileName = new Uri(uploadedUrl!).Segments.Last();
+ var fileName = uploadedUrl!.ToCatboxImageName()!;
var createAlbumRequest = new RemoteCreateAlbumRequest
{
@@ -298,7 +298,7 @@ public async Task ModifyAlbumAsync_AddAndRemoveFiles_Succeeds()
var albumUrl = await _client.CreateAlbumAsync(createAlbumRequest);
TrackCreatedAlbum(albumUrl);
- var albumId = new Uri(albumUrl!).Segments.Last();
+ var albumId = albumUrl!.ToAlbumShortCode()!;
// Upload another file to add to the album
string? secondUploadUrl = null;
@@ -308,7 +308,7 @@ public async Task ModifyAlbumAsync_AddAndRemoveFiles_Succeeds()
break;
}
TrackUploadedFile(secondUploadUrl);
- var secondFileName = new Uri(secondUploadUrl!).Segments.Last();
+ var secondFileName = secondUploadUrl!.ToCatboxImageName()!;
// Act - Add file to album
var addRequest = new ModifyAlbumImagesRequest
@@ -320,7 +320,7 @@ public async Task ModifyAlbumAsync_AddAndRemoveFiles_Succeeds()
};
var addResult = await _client.ModifyAlbumAsync(addRequest);
- TestContext.WriteLine($"Add to album result: {addResult}");
+ await TestContext.Out.WriteLineAsync($"Add to album result: {addResult}");
// Act - Remove file from album
var removeRequest = new ModifyAlbumImagesRequest
@@ -332,7 +332,7 @@ public async Task ModifyAlbumAsync_AddAndRemoveFiles_Succeeds()
};
var removeResult = await _client.ModifyAlbumAsync(removeRequest);
- TestContext.WriteLine($"Remove from album result: {removeResult}");
+ await TestContext.Out.WriteLineAsync($"Remove from album result: {removeResult}");
// Assert
addResult.ShouldNotBeNullOrWhiteSpace();
@@ -358,7 +358,7 @@ public async Task DeleteMultipleFilesAsync_WithUploadedFiles_Succeeds()
break;
}
uploadedUrl.ShouldNotBeNullOrWhiteSpace();
- var fileName = new Uri(uploadedUrl!).Segments.Last();
+ var fileName = uploadedUrl!.ToCatboxImageName()!;
var deleteRequest = new DeleteFileRequest
{
@@ -371,6 +371,148 @@ public async Task DeleteMultipleFilesAsync_WithUploadedFiles_Succeeds()
// Assert
result.ShouldNotBeNullOrWhiteSpace();
- TestContext.WriteLine($"Delete result: {result}");
+ await TestContext.Out.WriteLineAsync($"Delete result: {result}");
+ }
+
+ [Test]
+ [Order(7)]
+ public async Task GetAlbumAsync_WithCreatedAlbum_ReturnsAlbumInfo()
+ {
+ // Arrange - Upload a file and create an album
+ var testFilePath = IntegrationTestConfig.GetTestFilePath();
+ var uploadRequest = new FileUploadRequest
+ {
+ Files = [new FileInfo(testFilePath)],
+ UserHash = IntegrationTestConfig.UserHash
+ };
+
+ string? uploadedUrl = null;
+ await foreach (var url in _client!.UploadFilesAsync(uploadRequest))
+ {
+ uploadedUrl = url;
+ break;
+ }
+ TrackUploadedFile(uploadedUrl);
+ var fileName = uploadedUrl!.ToCatboxImageName()!;
+
+ var createAlbumRequest = new RemoteCreateAlbumRequest
+ {
+ Title = "GetAlbum Integration Test",
+ Description = "Testing GetAlbumAsync",
+ UserHash = IntegrationTestConfig.UserHash,
+ Files = [fileName]
+ };
+
+ var albumUrl = await _client.CreateAlbumAsync(createAlbumRequest);
+ TrackCreatedAlbum(albumUrl);
+ var albumId = albumUrl!.ToAlbumShortCode()!;
+
+ // Act
+ var albumInfo = await _client.GetAlbumAsync(new GetAlbumRequest { AlbumId = albumId });
+
+ // Assert
+ albumInfo.ShouldNotBeNull();
+ albumInfo.Title.ShouldBe("GetAlbum Integration Test");
+ albumInfo.Description.ShouldBe("Testing GetAlbumAsync");
+ albumInfo.AlbumId.ShouldBe(albumId);
+ albumInfo.Files.ShouldContain(fileName);
+ await TestContext.Out.WriteLineAsync($"Album info: {albumInfo.Title} ({albumInfo.AlbumId}), {albumInfo.Files.Length} file(s)");
+ }
+
+ [Test]
+ [Order(8)]
+ public async Task DownloadFileAsync_WithUploadedFile_DownloadsSuccessfully()
+ {
+ // Arrange - Upload a file
+ var testFilePath = IntegrationTestConfig.GetTestFilePath();
+ var uploadRequest = new FileUploadRequest
+ {
+ Files = [new FileInfo(testFilePath)],
+ UserHash = IntegrationTestConfig.UserHash
+ };
+
+ string? uploadedUrl = null;
+ await foreach (var url in _client!.UploadFilesAsync(uploadRequest))
+ {
+ uploadedUrl = url;
+ break;
+ }
+ TrackUploadedFile(uploadedUrl);
+ var fileName = uploadedUrl!.ToCatboxImageName()!;
+
+ var tempDir = Path.Combine(Path.GetTempPath(), $"catbox-test-{Guid.NewGuid():N}");
+ try
+ {
+ // Act
+ await _client.DownloadFileAsync(fileName, tempDir);
+
+ // Assert
+ var downloadedFile = Path.Combine(tempDir, fileName);
+ File.Exists(downloadedFile).ShouldBeTrue($"Downloaded file not found: {downloadedFile}");
+ new FileInfo(downloadedFile).Length.ShouldBeGreaterThan(0);
+ await TestContext.Out.WriteLineAsync($"Downloaded file: {downloadedFile} ({new FileInfo(downloadedFile).Length} bytes)");
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, recursive: true);
+ }
+ }
+
+ [Test]
+ [Order(9)]
+ public async Task EditAlbumAsync_WithCreatedAlbum_EditsSuccessfully()
+ {
+ // Arrange - Upload 2 files and create an album
+ var testFilePath = IntegrationTestConfig.GetTestFilePath();
+ var uploadRequest = new FileUploadRequest
+ {
+ Files = [new FileInfo(testFilePath), new FileInfo(testFilePath)],
+ UserHash = IntegrationTestConfig.UserHash
+ };
+
+ var uploadedFileNames = new List();
+ await foreach (var url in _client!.UploadFilesAsync(uploadRequest))
+ {
+ TrackUploadedFile(url);
+ uploadedFileNames.Add(url!.ToCatboxImageName()!);
+ }
+ uploadedFileNames.Count.ShouldBe(2);
+
+ var createAlbumRequest = new RemoteCreateAlbumRequest
+ {
+ Title = "EditAlbum Integration Test",
+ Description = "Before edit",
+ UserHash = IntegrationTestConfig.UserHash,
+ Files = uploadedFileNames
+ };
+
+ var albumUrl = await _client.CreateAlbumAsync(createAlbumRequest);
+ TrackCreatedAlbum(albumUrl);
+ var albumId = albumUrl!.ToAlbumShortCode()!;
+
+ // Act - Edit the album with new title, description, and only 1 file
+#pragma warning disable CS0618 // EditAlbumAsync is marked Obsolete as a safety warning
+ var editResult = await _client.EditAlbumAsync(new EditAlbumRequest
+ {
+ UserHash = IntegrationTestConfig.UserHash!,
+ AlbumId = albumId,
+ Title = "Edited Title",
+ Description = "Edited description",
+ Files = [uploadedFileNames[0]]
+ });
+#pragma warning restore CS0618
+
+ // Assert
+ editResult.ShouldNotBeNullOrWhiteSpace();
+ await TestContext.Out.WriteLineAsync($"Edit result: {editResult}");
+
+ // Verify changes via GetAlbum
+ var albumInfo = await _client.GetAlbumAsync(new GetAlbumRequest { AlbumId = albumId });
+ albumInfo.Title.ShouldBe("Edited Title");
+ albumInfo.Description.ShouldBe("Edited description");
+ albumInfo.Files.Length.ShouldBe(1);
+ albumInfo.Files.ShouldContain(uploadedFileNames[0]);
+ await TestContext.Out.WriteLineAsync($"Verified album after edit: {albumInfo.Title}, {albumInfo.Files.Length} file(s)");
}
}
diff --git a/tests/CatBox.Tests/CatBoxClientTests.cs b/tests/CatBox.Tests/CatBoxClientTests.cs
index 6e1bd09..e7ee725 100644
--- a/tests/CatBox.Tests/CatBoxClientTests.cs
+++ b/tests/CatBox.Tests/CatBoxClientTests.cs
@@ -250,7 +250,7 @@ public async Task UploadFilesAsync_CancellationToken_CancelsOperation()
};
var cts = new CancellationTokenSource();
- cts.Cancel();
+ await cts.CancelAsync();
// Act & Assert
await Should.ThrowAsync(async () =>
@@ -509,7 +509,7 @@ public async Task UploadFilesAsUrlAsync_CancellationToken_CancelsOperation()
};
var cts = new CancellationTokenSource();
- cts.Cancel();
+ await cts.CancelAsync();
// Act & Assert
await Should.ThrowAsync(async () =>
diff --git a/tests/CatBox.Tests/CommonTests.cs b/tests/CatBox.Tests/CommonTests.cs
index 1951ab5..bba3ae9 100644
--- a/tests/CatBox.Tests/CommonTests.cs
+++ b/tests/CatBox.Tests/CommonTests.cs
@@ -45,78 +45,39 @@ public void IsFileExtensionValid_WithValidExtensions_ReturnsTrue(string extensio
}
[Test]
- public void ThrowIfAlbumCreationRequestIsInvalid_WithNullRequest_ThrowsArgumentNullException()
+ public void IfAlbumCreationRequestIsInvalid_WithNullRequest_ThrowsArgumentNullException()
{
// Act & Assert
- Should.Throw(() => Common.ThrowIfAlbumCreationRequestIsInvalid(null!));
+ Should.Throw(() => Throw.IfAlbumCreationRequestIsInvalid(null!));
}
- [Test]
- public void ThrowIfAlbumCreationRequestIsInvalid_WithNullTitle_ThrowsArgumentException()
+ private static IEnumerable InvalidAlbumCreationRequestCases()
{
- // Arrange
- var request = new RemoteCreateAlbumRequest
- {
- Title = null!,
- Description = "Test Description",
- UserHash = "test-hash",
- Files = ["file1.jpg"]
- };
-
- // Act & Assert
- Should.Throw(() => Common.ThrowIfAlbumCreationRequestIsInvalid(request));
+ yield return new TestCaseData(null, "Test Description").SetName("Null Title");
+ yield return new TestCaseData(" ", "Test Description").SetName("Whitespace Title");
+ yield return new TestCaseData("Test Title", null).SetName("Null Description");
+ yield return new TestCaseData("Test Title", " ").SetName("Whitespace Description");
}
- [Test]
- public void ThrowIfAlbumCreationRequestIsInvalid_WithWhitespaceTitle_ThrowsArgumentException()
+ [TestCaseSource(nameof(InvalidAlbumCreationRequestCases))]
+ public void IfAlbumCreationRequestIsInvalid_WithInvalidRequest_ThrowsArgumentException(
+ string? title, string? description)
{
// Arrange
var request = new RemoteCreateAlbumRequest
{
- Title = " ",
- Description = "Test Description",
+ Title = title!,
+ Description = description!,
UserHash = "test-hash",
Files = ["file1.jpg"]
};
// Act & Assert
- Should.Throw(() => Common.ThrowIfAlbumCreationRequestIsInvalid(request));
+ Should.Throw(() => Throw.IfAlbumCreationRequestIsInvalid(request));
}
[Test]
- public void ThrowIfAlbumCreationRequestIsInvalid_WithNullDescription_ThrowsArgumentException()
- {
- // Arrange
- var request = new RemoteCreateAlbumRequest
- {
- Title = "Test Title",
- Description = null!,
- UserHash = "test-hash",
- Files = ["file1.jpg"]
- };
-
- // Act & Assert
- Should.Throw(() => Common.ThrowIfAlbumCreationRequestIsInvalid(request));
- }
-
- [Test]
- public void ThrowIfAlbumCreationRequestIsInvalid_WithWhitespaceDescription_ThrowsArgumentException()
- {
- // Arrange
- var request = new RemoteCreateAlbumRequest
- {
- Title = "Test Title",
- Description = " ",
- UserHash = "test-hash",
- Files = ["file1.jpg"]
- };
-
- // Act & Assert
- Should.Throw(() => Common.ThrowIfAlbumCreationRequestIsInvalid(request));
- }
-
- [Test]
- public void ThrowIfAlbumCreationRequestIsInvalid_WithValidRequest_DoesNotThrow()
+ public void IfAlbumCreationRequestIsInvalid_WithValidRequest_DoesNotThrow()
{
// Arrange
var request = new RemoteCreateAlbumRequest
@@ -128,7 +89,7 @@ public void ThrowIfAlbumCreationRequestIsInvalid_WithValidRequest_DoesNotThrow()
};
// Act & Assert
- Should.NotThrow(() => Common.ThrowIfAlbumCreationRequestIsInvalid(request));
+ Should.NotThrow(() => Throw.IfAlbumCreationRequestIsInvalid(request));
}
private static IEnumerable ValidAlbumRequestCases()
@@ -215,4 +176,95 @@ public void IsAlbumRequestTypeValid_WithInvalidRequestType_ReturnsFalse(RequestT
// Assert
result.ShouldBeFalse();
}
+
+ [TestCase("https://files.catbox.moe/abc123.jpg", "abc123.jpg")]
+ [TestCase("http://files.catbox.moe/xyz789.png", "xyz789.png")]
+ [TestCase("https://files.catbox.moe/test.gif", "test.gif")]
+ public void ToCatboxImageName_WithFullCatboxUrl_ExtractsFilename(string url, string expected)
+ {
+ url.ToCatboxImageName().ShouldBe(expected);
+ }
+
+ [TestCase("abc123.jpg", "abc123.jpg")]
+ [TestCase("myfile.png", "myfile.png")]
+ public void ToCatboxImageName_WithPlainFilename_ReturnsOriginal(string input, string expected)
+ {
+ input.ToCatboxImageName().ShouldBe(expected);
+ }
+
+ [TestCase("https://example.com/image.jpg")]
+ [TestCase("https://otherdomain.moe/file.png")]
+ public void ToCatboxImageName_WithNonCatboxUrl_ReturnsOriginal(string url)
+ {
+ url.ToCatboxImageName().ShouldBe(url);
+ }
+
+ [TestCase(null, null)]
+ [TestCase("", "")]
+ [TestCase(" ", " ")]
+ public void ToCatboxImageName_WithNullOrWhitespace_ReturnsInput(string? input, string? expected)
+ {
+ input.ToCatboxImageName().ShouldBe(expected);
+ }
+
+ [TestCase("https://catbox.moe/c/abc123", "abc123")]
+ [TestCase("http://catbox.moe/c/xyz789", "xyz789")]
+ [TestCase("https://catbox.moe/c/test", "test")]
+ public void ToAlbumShortCode_WithFullAlbumUrl_ExtractsShortCode(string url, string expected)
+ {
+ url.ToAlbumShortCode().ShouldBe(expected);
+ }
+
+ [TestCase("abc123", "abc123")]
+ [TestCase("xyz789", "xyz789")]
+ public void ToAlbumShortCode_WithPlainShortCode_ReturnsOriginal(string input, string expected)
+ {
+ input.ToAlbumShortCode().ShouldBe(expected);
+ }
+
+ [TestCase("https://catbox.moe/other/abc123")]
+ [TestCase("https://files.catbox.moe/abc123.jpg")]
+ [TestCase("https://example.com/c/abc123")]
+ public void ToAlbumShortCode_WithNonAlbumUrl_ReturnsOriginal(string url)
+ {
+ url.ToAlbumShortCode().ShouldBe(url);
+ }
+
+ [TestCase(null, null)]
+ [TestCase("", "")]
+ [TestCase(" ", " ")]
+ public void ToAlbumShortCode_WithNullOrWhitespace_ReturnsInput(string? input, string? expected)
+ {
+ input.ToAlbumShortCode().ShouldBe(expected);
+ }
+
+ [Test]
+ public void IfAlbumFileLimitExceeds_At501Files_Throws()
+ {
+ Should.Throw(() => Throw.IfAlbumFileLimitExceeds(501));
+ }
+
+ [Test]
+ public void IfAlbumFileLimitExceeds_At500Files_DoesNotThrow()
+ {
+ Should.NotThrow(() => Throw.IfAlbumFileLimitExceeds(500));
+ }
+
+ [Test]
+ public void IfAlbumFileLimitExceeds_At1File_DoesNotThrow()
+ {
+ Should.NotThrow(() => Throw.IfAlbumFileLimitExceeds(1));
+ }
+
+ [Test]
+ public void IfAlbumFileLimitExceeds_WithCustomLimit_ThrowsAtExceedingLimit()
+ {
+ Should.Throw(() => Throw.IfAlbumFileLimitExceeds(11, maxFiles: 10));
+ }
+
+ [Test]
+ public void IfAlbumFileLimitExceeds_WithCustomLimit_DoesNotThrowAtLimit()
+ {
+ Should.NotThrow(() => Throw.IfAlbumFileLimitExceeds(10, maxFiles: 10));
+ }
}
diff --git a/tests/CatBox.Tests/Helpers/IntegrationTestConfig.cs b/tests/CatBox.Tests/Helpers/IntegrationTestConfig.cs
index cdf3a33..57e30f6 100644
--- a/tests/CatBox.Tests/Helpers/IntegrationTestConfig.cs
+++ b/tests/CatBox.Tests/Helpers/IntegrationTestConfig.cs
@@ -53,6 +53,6 @@ public static class IntegrationTestConfig
public static string GetTestFilePath()
{
var testDirectory = NUnit.Framework.TestContext.CurrentContext.TestDirectory;
- return Path.Combine(testDirectory, "Images", "test-file.png");
+ return Path.Combine(testDirectory, "assets", "test-file.png");
}
}
diff --git a/tests/CatBox.Tests/LitterboxClientIntegrationTests.cs b/tests/CatBox.Tests/LitterboxClientIntegrationTests.cs
index 4228559..63738d3 100644
--- a/tests/CatBox.Tests/LitterboxClientIntegrationTests.cs
+++ b/tests/CatBox.Tests/LitterboxClientIntegrationTests.cs
@@ -66,7 +66,7 @@ public async Task UploadMultipleImagesAsync_WithOneHourExpiry_Succeeds()
results.Count.ShouldBe(1);
results[0].ShouldNotBeNullOrWhiteSpace();
results[0].ShouldStartWith("https://litter.catbox.moe/");
- TestContext.WriteLine($"Uploaded temporary file (1h expiry): {results[0]}");
+ await TestContext.Out.WriteLineAsync($"Uploaded temporary file (1h expiry): {results[0]}");
}
[Test]
@@ -91,7 +91,7 @@ public async Task UploadMultipleImagesAsync_WithTwelveHourExpiry_Succeeds()
results.Count.ShouldBe(1);
results[0].ShouldNotBeNullOrWhiteSpace();
results[0].ShouldStartWith("https://litter.catbox.moe/");
- TestContext.WriteLine($"Uploaded temporary file (12h expiry): {results[0]}");
+ await TestContext.Out.WriteLineAsync($"Uploaded temporary file (12h expiry): {results[0]}");
}
[Test]
@@ -116,7 +116,7 @@ public async Task UploadMultipleImagesAsync_WithOneDayExpiry_Succeeds()
results.Count.ShouldBe(1);
results[0].ShouldNotBeNullOrWhiteSpace();
results[0].ShouldStartWith("https://litter.catbox.moe/");
- TestContext.WriteLine($"Uploaded temporary file (1d expiry): {results[0]}");
+ await TestContext.Out.WriteLineAsync($"Uploaded temporary file (1d expiry): {results[0]}");
}
[Test]
@@ -141,7 +141,7 @@ public async Task UploadMultipleImagesAsync_WithThreeDaysExpiry_Succeeds()
results.Count.ShouldBe(1);
results[0].ShouldNotBeNullOrWhiteSpace();
results[0].ShouldStartWith("https://litter.catbox.moe/");
- TestContext.WriteLine($"Uploaded temporary file (3d expiry): {results[0]}");
+ await TestContext.Out.WriteLineAsync($"Uploaded temporary file (3d expiry): {results[0]}");
}
[Test]
@@ -165,7 +165,7 @@ public async Task UploadImageAsync_WithMemoryStream_Succeeds()
// Assert
result.ShouldNotBeNullOrWhiteSpace();
result.ShouldStartWith("https://litter.catbox.moe/");
- TestContext.WriteLine($"Uploaded temporary stream: {result}");
+ await TestContext.Out.WriteLineAsync($"Uploaded temporary stream: {result}");
}
[Test]
@@ -189,10 +189,10 @@ public async Task UploadMultipleImagesAsync_WithMultipleFiles_YieldsMultipleUrls
// Assert
results.Count.ShouldBe(3);
results.ShouldAllBe(r => !string.IsNullOrWhiteSpace(r) && r!.StartsWith("https://litter.catbox.moe/"));
- TestContext.WriteLine($"Uploaded {results.Count} temporary files");
+ await TestContext.Out.WriteLineAsync($"Uploaded {results.Count} temporary files");
foreach (var url in results)
{
- TestContext.WriteLine($" - {url}");
+ await TestContext.Out.WriteLineAsync($" - {url}");
}
}
}
diff --git a/tests/CatBox.Tests/LitterboxClientTests.cs b/tests/CatBox.Tests/LitterboxClientTests.cs
index 4d3c06e..78b0e53 100644
--- a/tests/CatBox.Tests/LitterboxClientTests.cs
+++ b/tests/CatBox.Tests/LitterboxClientTests.cs
@@ -272,7 +272,7 @@ public async Task UploadMultipleImagesAsync_CancellationToken_CancelsOperation()
};
var cts = new CancellationTokenSource();
- cts.Cancel();
+ await cts.CancelAsync();
// Act & Assert
await Should.ThrowAsync(async () =>
@@ -373,7 +373,7 @@ public async Task UploadImageAsync_CancellationToken_CancelsOperation()
};
var cts = new CancellationTokenSource();
- cts.Cancel();
+ await cts.CancelAsync();
// Act & Assert
await Should.ThrowAsync(async () => await client.UploadImageAsync(request, cts.Token));