diff --git a/PiwigoDotnet/Piwigo.Client.Tests/Piwigo.Client.Tests.csproj b/PiwigoDotnet/Piwigo.Client.Tests/Piwigo.Client.Tests.csproj
index 36edec5..9bf5783 100644
--- a/PiwigoDotnet/Piwigo.Client.Tests/Piwigo.Client.Tests.csproj
+++ b/PiwigoDotnet/Piwigo.Client.Tests/Piwigo.Client.Tests.csproj
@@ -9,11 +9,16 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/PiwigoDotnet/Piwigo.Client.Tests/PiwigoClientTests.cs b/PiwigoDotnet/Piwigo.Client.Tests/PiwigoClientTests.cs
new file mode 100644
index 0000000..a507507
--- /dev/null
+++ b/PiwigoDotnet/Piwigo.Client.Tests/PiwigoClientTests.cs
@@ -0,0 +1,45 @@
+using Flurl.Http.Testing;
+
+namespace Piwigo.Client.Tests;
+
+public class PiwigoClientTests
+{
+ private PiwigoClient _piwigoClient = null!;
+ private HttpTest _httpTest = null!;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _piwigoClient = new PiwigoClient();
+ _httpTest = new HttpTest();
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ _httpTest.Dispose();
+ }
+
+ [Test]
+ public async Task Login_should_set_cookies_and_session()
+ {
+ await LoginAsync();
+ }
+
+ [Test]
+ public async Task Logout_should_set_IsLoggedIn_to_false()
+ {
+ await LoginAsync();
+
+ _httpTest.RespondWith("OK");
+ await _piwigoClient.Logout();
+ _piwigoClient.IsLoggedIn.Should().BeFalse();
+ }
+
+ private async Task LoginAsync()
+ {
+ _httpTest.RespondWith("OK", 200, cookies: new { pwg_id = "pwg_id" });
+ await _piwigoClient.LoginAsync(new Uri("http://localhost:8080/foo/bar/ws.php?format=json"), "admin", "admin");
+ _piwigoClient.IsLoggedIn.Should().BeTrue();
+ }
+}
\ No newline at end of file
diff --git a/PiwigoDotnet/Piwigo.Client.Tests/UnitTest1.cs b/PiwigoDotnet/Piwigo.Client.Tests/UnitTest1.cs
deleted file mode 100644
index 8565c93..0000000
--- a/PiwigoDotnet/Piwigo.Client.Tests/UnitTest1.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Piwigo.Client.Tests;
-
-public class Tests
-{
- [SetUp]
- public void Setup()
- {
- }
-
- [Test]
- public void Test1()
- {
- Assert.Pass();
- }
-}
\ No newline at end of file
diff --git a/PiwigoDotnet/Piwigo.Client.Tests/Usings.cs b/PiwigoDotnet/Piwigo.Client.Tests/Usings.cs
index cefced4..25c79ab 100644
--- a/PiwigoDotnet/Piwigo.Client.Tests/Usings.cs
+++ b/PiwigoDotnet/Piwigo.Client.Tests/Usings.cs
@@ -1 +1,2 @@
-global using NUnit.Framework;
\ No newline at end of file
+global using NUnit.Framework;
+global using FluentAssertions;
\ No newline at end of file
diff --git a/PiwigoDotnet/Piwigo.Client/CapturedMultipartContentExtensions.cs b/PiwigoDotnet/Piwigo.Client/CapturedMultipartContentExtensions.cs
new file mode 100644
index 0000000..fb56758
--- /dev/null
+++ b/PiwigoDotnet/Piwigo.Client/CapturedMultipartContentExtensions.cs
@@ -0,0 +1,16 @@
+using Flurl.Http.Content;
+
+namespace Piwigo.Client;
+
+internal static class CapturedMultipartContentExtensions
+{
+ public static CapturedMultipartContent AddMethod(this CapturedMultipartContent part, string piwigoMethod)
+ {
+ if (string.IsNullOrWhiteSpace(piwigoMethod))
+ {
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(piwigoMethod));
+ }
+
+ return part.AddString("method", piwigoMethod);
+ }
+}
\ No newline at end of file
diff --git a/PiwigoDotnet/Piwigo.Client/IPiwigoClient.cs b/PiwigoDotnet/Piwigo.Client/IPiwigoClient.cs
index 6186555..94aa902 100644
--- a/PiwigoDotnet/Piwigo.Client/IPiwigoClient.cs
+++ b/PiwigoDotnet/Piwigo.Client/IPiwigoClient.cs
@@ -2,5 +2,8 @@
public interface IPiwigoClient
{
+ bool IsLoggedIn { get; }
+ int ChunkSize { get; set; }
Task LoginAsync(Uri uri, string username, string password);
+ Task Logout();
}
\ No newline at end of file
diff --git a/PiwigoDotnet/Piwigo.Client/PiwigoClient.cs b/PiwigoDotnet/Piwigo.Client/PiwigoClient.cs
new file mode 100644
index 0000000..be8d2dc
--- /dev/null
+++ b/PiwigoDotnet/Piwigo.Client/PiwigoClient.cs
@@ -0,0 +1,69 @@
+using System.Net;
+using System.Runtime.CompilerServices;
+using Flurl;
+using Flurl.Http;
+
+namespace Piwigo.Client;
+
+public class PiwigoClient : IPiwigoClient
+{
+ private CookieJar _cookies = new();
+ private string _piwigoBaseUri = null!;
+ public bool IsLoggedIn { get; private set; }
+ public int ChunkSize { get; set; } = 512;
+ private IFlurlRequest Request => _piwigoBaseUri.WithCookies(_cookies);
+
+ public async Task LoginAsync(Uri uri, string username, string password)
+ {
+ if (uri == null)
+ {
+ throw new ArgumentNullException(nameof(uri));
+ }
+
+ if (string.IsNullOrWhiteSpace(username))
+ {
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
+ }
+
+ if (string.IsNullOrWhiteSpace(password))
+ {
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(password));
+ }
+
+ if (IsLoggedIn)
+ {
+ throw new PiwigoException("The client is already logged in. Create a new instance or log out first!");
+ }
+
+ _piwigoBaseUri = uri.AppendPathSegment("ws.php").SetQueryParam("format", "json");
+
+ var response = await _piwigoBaseUri.WithCookies(out var cookieJar).PostMultipartAsync(c =>
+ c.PiwigoLogin(username, password));
+
+ if (response.StatusCode != (int)HttpStatusCode.OK)
+ {
+ throw new PiwigoException($"Could not log in to {_piwigoBaseUri} using username {username}");
+ }
+
+ _cookies = cookieJar;
+ IsLoggedIn = true;
+ }
+
+ public async Task Logout()
+ {
+ EnsureLoggedIn();
+
+ await Request.PostMultipartAsync(c => c.PiwigoLogout());
+
+ IsLoggedIn = false;
+ _cookies.Clear();
+ }
+
+ private void EnsureLoggedIn([CallerMemberName] string? callerName = null)
+ {
+ if (!IsLoggedIn)
+ {
+ throw new InvalidOperationException($"Could not execute {callerName} as the client is not logged in.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/PiwigoDotnet/Piwigo.Client/PiwigoException.cs b/PiwigoDotnet/Piwigo.Client/PiwigoException.cs
new file mode 100644
index 0000000..9890caa
--- /dev/null
+++ b/PiwigoDotnet/Piwigo.Client/PiwigoException.cs
@@ -0,0 +1,23 @@
+using System.Runtime.Serialization;
+
+namespace Piwigo.Client;
+
+[Serializable]
+public class PiwigoException : Exception
+{
+ public PiwigoException()
+ {
+ }
+
+ protected PiwigoException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+
+ public PiwigoException(string? message) : base(message)
+ {
+ }
+
+ public PiwigoException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+}
\ No newline at end of file
diff --git a/PiwigoDotnet/Piwigo.Client/PiwigoMethods.cs b/PiwigoDotnet/Piwigo.Client/PiwigoMethods.cs
new file mode 100644
index 0000000..5146e29
--- /dev/null
+++ b/PiwigoDotnet/Piwigo.Client/PiwigoMethods.cs
@@ -0,0 +1,17 @@
+using Flurl.Http.Content;
+
+namespace Piwigo.Client;
+
+internal static class PiwigoMethods
+{
+ public static CapturedMultipartContent PiwigoLogin(this CapturedMultipartContent part, string username,
+ string password)
+ {
+ return part.AddMethod("pwg.session.login").AddString("username", username).AddString("password", password);
+ }
+
+ public static CapturedMultipartContent PiwigoLogout(this CapturedMultipartContent part)
+ {
+ return part.AddMethod("pwg.session.logout");
+ }
+}
\ No newline at end of file
diff --git a/PiwigoDotnet/PiwigoDotnet.sln.DotSettings b/PiwigoDotnet/PiwigoDotnet.sln.DotSettings
index 312bea7..7a6b9ad 100644
--- a/PiwigoDotnet/PiwigoDotnet.sln.DotSettings
+++ b/PiwigoDotnet/PiwigoDotnet.sln.DotSettings
@@ -1,2 +1,6 @@
+ Required
+ Required
+ Required
+ Required
True
\ No newline at end of file