diff --git a/src/CamBooth/CamBooth.App/App.xaml.cs b/src/CamBooth/CamBooth.App/App.xaml.cs index ec4a2e5..da5b1d6 100644 --- a/src/CamBooth/CamBooth.App/App.xaml.cs +++ b/src/CamBooth/CamBooth.App/App.xaml.cs @@ -1,10 +1,10 @@ -using System.IO; +using System.IO; using System.Windows; using CamBooth.App.Core.AppSettings; using CamBooth.App.Core.Logging; using CamBooth.App.Features.Camera; -using CamBooth.App.Features.LycheeUpload; +using CamBooth.App.Features.PhotoPrismUpload; using CamBooth.App.Features.PictureGallery; using EDSDKLib.API.Base; @@ -43,29 +43,27 @@ public partial class App : Application var services = new ServiceCollection(); - // Register Configuration services.AddSingleton(configuration); - // Register base services services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); - - // Zuerst den Provider bauen, um AppSettings zu laden + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + var tempProvider = services.BuildServiceProvider(); var appSettings = tempProvider.GetRequiredService(); var logger = tempProvider.GetRequiredService(); - // Stelle sicher, dass das PictureLocation-Verzeichnis existiert try { if (!Directory.Exists(appSettings.PictureLocation)) { Directory.CreateDirectory(appSettings.PictureLocation); - logger.Info($"Picture directory created: {appSettings.PictureLocation}"); + logger.Debug($"Picture directory created: {appSettings.PictureLocation}"); } } catch (Exception ex) @@ -80,12 +78,10 @@ public partial class App : Application if (appSettings.UseMockCamera) { services.AddSingleton(); - services.AddSingleton(); } else { services.AddSingleton(); - services.AddSingleton(); } } catch (DllNotFoundException ex) @@ -98,27 +94,26 @@ public partial class App : Application MessageBoxImage.Warning); services.AddSingleton(); - services.AddSingleton(); } services.AddTransient(); _serviceProvider = services.BuildServiceProvider(); - // Starte UploadQueueService beim Start + // Starte PhotoPrism Upload-Service beim Start try { - var uploadQueueService = _serviceProvider.GetRequiredService(); + var uploadQueueService = _serviceProvider.GetRequiredService(); uploadQueueService.Start(); // Scan für fehlgeschlagene Uploads beim Start uploadQueueService.ScanAndQueueFailedUploads(); - logger.Info("UploadQueueService initialisiert und gestartet"); + logger.Info("PhotoPrism UploadQueueService initialisiert und gestartet"); } catch (Exception ex) { - logger.Error($"Fehler beim Start des UploadQueueService: {ex.Message}"); + logger.Error($"Fehler beim Start des PhotoPrism UploadQueueService: {ex.Message}"); } var mainWindow = _serviceProvider.GetRequiredService(); @@ -127,10 +122,10 @@ public partial class App : Application protected override void OnExit(ExitEventArgs e) { - // Stoppe UploadQueueService beim Beenden der App + // Stoppe PhotoPrism UploadQueueService beim Beenden der App try { - var uploadQueueService = _serviceProvider?.GetService(); + var uploadQueueService = _serviceProvider?.GetService(); if (uploadQueueService != null) { uploadQueueService.StopAsync().Wait(TimeSpan.FromSeconds(10)); @@ -138,9 +133,22 @@ public partial class App : Application } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"Fehler beim Stoppen des UploadQueueService: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"Fehler beim Stoppen des PhotoPrism UploadQueueService: {ex.Message}"); + } + + // Dispose Service Provider, damit IDisposable-Services (z.B. CameraService) sauber beendet werden. + try + { + if (_serviceProvider is IDisposable disposableProvider) + { + disposableProvider.Dispose(); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Fehler beim Dispose des ServiceProviders: {ex.Message}"); } base.OnExit(e); } -} \ No newline at end of file +} diff --git a/src/CamBooth/CamBooth.App/Core/AppSettings/AppSettingsService.cs b/src/CamBooth/CamBooth.App/Core/AppSettings/AppSettingsService.cs index 432d461..f159c97 100644 --- a/src/CamBooth/CamBooth.App/Core/AppSettings/AppSettingsService.cs +++ b/src/CamBooth/CamBooth.App/Core/AppSettings/AppSettingsService.cs @@ -71,7 +71,7 @@ public class AppSettingsService public string? RemoteServerApiKey => configuration["LoggingSettings:RemoteServerApiKey"]; - // Lychee Upload Settings + // Lychee Upload Settings (deprecated - wird durch PhotoPrism ersetzt) public string? LycheeApiUrl => configuration["LycheeSettings:ApiUrl"]; public string? LycheeUsername => configuration["LycheeSettings:Username"]; @@ -81,4 +81,17 @@ public class AppSettingsService public string? LycheeDefaultAlbumId => configuration["LycheeSettings:DefaultAlbumId"]; public bool LycheeAutoUploadEnabled => bool.Parse(configuration["LycheeSettings:AutoUploadEnabled"] ?? "false"); + + // PhotoPrism Upload Settings + public string? PhotoPrismApiUrl => configuration["PhotoPrismSettings:ApiUrl"]; + + public string? PhotoPrismApiKey => configuration["PhotoPrismSettings:ApiKey"]; + + public string? PhotoPrismUsername => configuration["PhotoPrismSettings:Username"]; + + public string? PhotoPrismPassword => configuration["PhotoPrismSettings:Password"]; + + public string? PhotoPrismAlbumShareUrl => configuration["PhotoPrismSettings:AlbumShareUrl"]; + + public bool PhotoPrismAutoUploadEnabled => bool.Parse(configuration["PhotoPrismSettings:AutoUploadEnabled"] ?? "false"); } \ No newline at end of file diff --git a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.dev.json b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.dev.json index c6f4671..b460b7c 100644 --- a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.dev.json +++ b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.dev.json @@ -38,13 +38,14 @@ "LogLevel": "Debug", "LogDirectory": "Logs", "RemoteServerUrl": "https://log.grimma-fotobox.de", - "RemoteServerApiKey": "nhnVql3QNgoAxvDWmNyU" + "RemoteServerApiKey": "TxycJNPgpXJw7SJcFsj4" }, - "LycheeSettings": { + "PhotoPrismSettings": { "ApiUrl": "https://gallery.grimma-fotobox.de", - "Username": "itob", + "ApiKey": "2ZNtZf-NvkIor-PEBJDa-vGdus5", + "Username": "admin", "Password": "VfVyqal&Nv8U&P", - "DefaultAlbumId": "ukPbEHxdV_q1BOpIVKah7TUR", + "AlbumShareUrl": "https://gallery.grimma-fotobox.de/s/kgcf48say8/kunde-01", "AutoUploadEnabled": true }, "ConnectionStrings": { diff --git a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json index a994c1b..f80d8e7 100644 --- a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json +++ b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json @@ -38,13 +38,14 @@ "LogLevel": "Information", "LogDirectory": "Logs", "RemoteServerUrl": "https://log.grimma-fotobox.de", - "RemoteServerApiKey": "8rjvr0zZmceuFZMYydKU" + "RemoteServerApiKey": "lEK0Yy3ncfC1ovyEHm3k" }, - "LycheeSettings": { + "PhotoPrismSettings": { "ApiUrl": "https://gallery.grimma-fotobox.de", - "Username": "itob", + "ApiKey": "iYQlKo-KLXYWD-yIdrJP-1cHo6f", + "Username": "admin", "Password": "VfVyqal&Nv8U&P", - "DefaultAlbumId": "ukPbEHxdV_q1BOpIVKah7TUR", + "AlbumShareUrl": "https://gallery.grimma-fotobox.de/s/3hvmc1uk4f/cambooth-prod", "AutoUploadEnabled": true }, "ConnectionStrings": { diff --git a/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs b/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs index d1636e5..20ea319 100644 --- a/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs +++ b/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs @@ -15,46 +15,6 @@ using Microsoft.Extensions.Configuration; namespace CamBooth.App.Core.Logging; -/// -/// Custom HTTP Client für Serilog HTTP Sink mit API-Key Support -/// -public class SeqHttpClient : IHttpClient -{ - private readonly HttpClient _httpClient; - private readonly string _apiKey; - - public SeqHttpClient(string apiKey = "") - { - _httpClient = new HttpClient(); - _apiKey = apiKey; - - // Setze API-Key Header, falls vorhanden - if (!string.IsNullOrWhiteSpace(_apiKey)) - { - _httpClient.DefaultRequestHeaders.Add("X-Seq-Api-Key", _apiKey); - } - } - - public void Configure(IConfiguration configuration) - { - // Konfiguration vom HTTP Sink - nicht nötig für unseren Use-Case - } - - public async Task PostAsync(string requestUri, Stream contentStream, CancellationToken cancellationToken) - { - using (var content = new StreamContent(contentStream)) - { - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - return await _httpClient.PostAsync(requestUri, content, cancellationToken); - } - } - - public void Dispose() - { - _httpClient?.Dispose(); - } -} - public class Logger : IDisposable { private readonly Serilog.Core.Logger _serilogLogger; diff --git a/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs b/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs index bc6766d..1198f76 100644 --- a/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs +++ b/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs @@ -4,7 +4,7 @@ using System.Windows; using CamBooth.App.Core.AppSettings; using CamBooth.App.Core.Logging; -using CamBooth.App.Features.LycheeUpload; +using CamBooth.App.Features.PhotoPrismUpload; using CamBooth.App.Features.PictureGallery; using EOSDigital.API; @@ -20,9 +20,7 @@ public class CameraService : IDisposable private readonly PictureGalleryService _pictureGalleryService; - private readonly LycheeUploadService _lycheeUploadService; - - private readonly UploadQueueService _uploadQueueService; + private readonly PhotoPrismUploadQueueService _photoPrismUploadQueueService; private readonly ICanonAPI _APIHandler; @@ -40,7 +38,7 @@ public class CameraService : IDisposable private CameraValue[] ISOList; - public ICamera _mainCamera; + public ICamera? _mainCamera; private CameraValue[] TvList; @@ -48,17 +46,13 @@ public class CameraService : IDisposable public CameraService(Logger logger, AppSettingsService appSettings, PictureGalleryService pictureGalleryService, - LycheeUploadService lycheeUploadService, - UploadQueueService uploadQueueService, - ICamera mainCamera, + PhotoPrismUploadQueueService photoPrismUploadQueueService, ICanonAPI APIHandler) { this._logger = logger; this._appSettings = appSettings; this._pictureGalleryService = pictureGalleryService; - this._lycheeUploadService = lycheeUploadService; - this._uploadQueueService = uploadQueueService; - this._mainCamera = mainCamera; + this._photoPrismUploadQueueService = photoPrismUploadQueueService; this._APIHandler = APIHandler; try { @@ -81,7 +75,7 @@ public class CameraService : IDisposable this.CloseSession(); this.IsInit = false; this._APIHandler.Dispose(); - this._mainCamera.Dispose(); + this._mainCamera?.Dispose(); } @@ -96,7 +90,7 @@ public class CameraService : IDisposable // Retry logic for camera detection (some systems need time to initialize) int maxRetries = 3; - int retryDelay = 500; // milliseconds + int retryDelay = 750; // milliseconds for (int attempt = 0; attempt < maxRetries; attempt++) { @@ -107,7 +101,6 @@ public class CameraService : IDisposable if (attempt < maxRetries - 1) { - this._logger.Info($"No cameras found on attempt {attempt + 1}/{maxRetries}, retrying in {retryDelay}ms..."); System.Threading.Thread.Sleep(retryDelay); this.RefreshCamera(); } @@ -118,10 +111,9 @@ public class CameraService : IDisposable this.ReportError("No cameras / devices found"); throw new InvalidOperationException("No cameras / devices found after multiple attempts"); } - - this._logger.Info($"Found {this.CamList.Count} camera(s)"); + string cameraDeviceNames = string.Join(", ", this.CamList.Select(cam => cam.DeviceName)); - this._logger.Info(cameraDeviceNames); + this._logger.Debug(cameraDeviceNames); // Update _mainCamera reference to the freshly detected camera this._mainCamera = this.CamList[0]; @@ -185,45 +177,56 @@ public class CameraService : IDisposable private void OpenSession() { - try + if (this._mainCamera == null) { - if (this._mainCamera == null) - { - throw new InvalidOperationException("Camera reference is null. Make sure ConnectCamera is called first."); - } - - // Check if session is already open - if (this._mainCamera.SessionOpen) - { - this._logger.Info($"Camera session already open for {this._mainCamera.DeviceName}"); - return; - } - - this._logger.Info($"Opening session for camera: {this._mainCamera.DeviceName}"); - this._mainCamera.OpenSession(); - this._logger.Info("Camera session opened successfully"); - - //_mainCamera.ProgressChanged += MainCamera_ProgressChanged; - this._mainCamera.StateChanged += this.MainCamera_StateChanged; - this._mainCamera.DownloadReady += this.MainCamera_DownloadReady; - - //SessionLabel.Content = _mainCamera.DeviceName; - this.AvList = this._mainCamera.GetSettingsList(PropertyID.Av); - this.TvList = this._mainCamera.GetSettingsList(PropertyID.Tv); - this.ISOList = this._mainCamera.GetSettingsList(PropertyID.ISO); - } - catch (Exception ex) - { - this._logger.Error($"Failed to open camera session: {ex.Message}"); - this.ReportError($"Failed to open camera session: {ex.Message}"); - throw; + throw new InvalidOperationException("Camera reference is null. Make sure ConnectCamera is called first."); } - // foreach (var Av in AvList) AvCoBox.Items.Add(Av.StringValue); - // foreach (var Tv in TvList) TvCoBox.Items.Add(Tv.StringValue); - // foreach (var ISO in ISOList) ISOCoBox.Items.Add(ISO.StringValue); - // AvCoBox.SelectedIndex = AvCoBox.Items.IndexOf(AvValues.GetValue(_mainCamera.GetInt32Setting(PropertyID.Av)).StringValue); - // TvCoBox.SelectedIndex = TvCoBox.Items.IndexOf(TvValues.GetValue(_mainCamera.GetInt32Setting(PropertyID.Tv)).StringValue); + if (this._mainCamera.SessionOpen) + { + this._logger.Info($"Camera session already open for {this._mainCamera.DeviceName}"); + return; + } + + this._logger.Info($"Opening session for camera: {this._mainCamera.DeviceName}"); + + const int maxRetries = 3; + const int retryDelayMs = 1000; + + for (int attempt = 0; attempt < maxRetries; attempt++) + { + try + { + this._mainCamera.OpenSession(); + break; + } + catch (Exception ex) when (attempt < maxRetries - 1 && IsSessionNotOpenError(ex)) + { + this._logger.Warning($"OpenSession attempt {attempt + 1}/{maxRetries} failed ({ex.Message}), refreshing camera and retrying in {retryDelayMs}ms..."); + System.Threading.Thread.Sleep(retryDelayMs); + this.RefreshCamera(); + if (this.CamList?.Any() == true) + { + this._mainCamera = this.CamList[0]; + } + } + catch (Exception ex) + { + this._logger.Error($"Failed to open camera session: {ex.Message}"); + this.ReportError($"Failed to open camera session: {ex.Message}"); + throw; + } + } + + this._logger.Info("Camera session opened successfully"); + + //_mainCamera.ProgressChanged += MainCamera_ProgressChanged; + this._mainCamera.StateChanged += this.MainCamera_StateChanged; + this._mainCamera.DownloadReady += this.MainCamera_DownloadReady; + + this.AvList = this._mainCamera.GetSettingsList(PropertyID.Av); + this.TvList = this._mainCamera.GetSettingsList(PropertyID.Tv); + this.ISOList = this._mainCamera.GetSettingsList(PropertyID.ISO); // ISOCoBox.SelectedIndex = ISOCoBox.Items.IndexOf(ISOValues.GetValue(_mainCamera.GetInt32Setting(PropertyID.ISO)).StringValue); // SettingsGroupBox.IsEnabled = true; // LiveViewGroupBox.IsEnabled = true; @@ -236,6 +239,13 @@ public class CameraService : IDisposable } + private static bool IsSessionNotOpenError(Exception ex) + { + const string errorName = "SESSION_NOT_OPEN"; + return ex.Message.Contains(errorName) || (ex.InnerException?.Message?.Contains(errorName) ?? false); + } + + private void StarLiveView() { try @@ -401,9 +411,9 @@ public class CameraService : IDisposable this._pictureGalleryService.LoadThumbnailsToCache(); }); - // Füge neues Foto zur Upload-Queue hinzu (wenn Auto-Upload aktiviert) - this._uploadQueueService.QueueNewPhoto(savedPhotoPath); - this._logger.Info($"Foto zur Upload-Queue hinzugefügt: {Info.FileName}"); + // Füge neues Foto zur PhotoPrism Upload-Queue hinzu (wenn Auto-Upload aktiviert) + this._photoPrismUploadQueueService.QueueNewPhoto(savedPhotoPath); + this._logger.Info($"Foto zur PhotoPrism Upload-Queue hinzugefügt: {Info.FileName}"); } catch (Exception ex) { diff --git a/src/CamBooth/CamBooth.App/Features/LiveView/TimerControlRectangleAnimation.xaml.cs b/src/CamBooth/CamBooth.App/Features/LiveView/TimerControlRectangleAnimation.xaml.cs index 2107ba7..76857a2 100644 --- a/src/CamBooth/CamBooth.App/Features/LiveView/TimerControlRectangleAnimation.xaml.cs +++ b/src/CamBooth/CamBooth.App/Features/LiveView/TimerControlRectangleAnimation.xaml.cs @@ -102,30 +102,12 @@ public partial class TimerControlRectangleAnimation : UserControl ShowTimer(); } - - public void StopTimer() - { - _timer.Stop(); - StopProgressBarAnimation(); - - // StatusText.Text = "Timer angehalten"; - } - - - public void ShowTimer() + private void ShowTimer() { var fadeInAnimation = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(300)); TimerContainer.BeginAnimation(OpacityProperty, fadeInAnimation); } - - public void HideTimer() - { - var fadeOutAnimation = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300)); - TimerContainer.BeginAnimation(OpacityProperty, fadeOutAnimation); - } - - private void StartProgressBarAnimation() { // Fortschrittsbalken-Animation diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/ARCHITECTURE.md b/src/CamBooth/CamBooth.App/Features/LycheeUpload/ARCHITECTURE.md deleted file mode 100644 index 4bcdcf5..0000000 --- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/ARCHITECTURE.md +++ /dev/null @@ -1,236 +0,0 @@ -# Lychee Upload Feature - Architektur - -## Komponenten-Übersicht - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ CamBooth App │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────┐ ┌──────────────────────────────────┐ │ -│ │ MainWindow │───────>│ LycheeUploadService │ │ -│ │ │ │ │ │ -│ │ - Camera │ │ + AuthenticateAsync() │ │ -│ │ - Photos │ │ + UploadImageAsync() │ │ -│ │ - Timer │ │ + UploadImagesAsync() │ │ -│ └──────┬───────┘ │ + CreateAlbumAsync() │ │ -│ │ │ + LogoutAsync() │ │ -│ │ │ │ │ -│ │ │ - HttpClient │ │ -│ │ │ - AuthToken │ │ -│ │ └────────────┬─────────────────────┘ │ -│ │ │ │ -│ v │ │ -│ ┌──────────────────┐ │ │ -│ │ LycheeUploadPage │<────────────────┘ │ -│ │ │ │ -│ │ - Connect UI │ ┌──────────────────────┐ │ -│ │ - Upload UI │ │ AppSettingsService │ │ -│ │ - Status Log │<────────│ │ │ -│ │ - Progress Bar │ │ - LycheeApiUrl │ │ -│ └──────────────────┘ │ - LycheeUsername │ │ -│ │ - LycheePassword │ │ -│ │ - DefaultAlbumId │ │ -│ └──────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ - │ - │ HTTPS/HTTP - v - ┌──────────────────────────┐ - │ Lychee API Server │ - │ │ - │ POST /api/Session::login│ - │ POST /api/Photo::add │ - │ POST /api/Album::add │ - │ POST /api/Session::logout│ - └──────────────────────────┘ -``` - -## Datenfluss - -### 1. Authentifizierung - -``` -User Action - │ - v -LycheeUploadService.AuthenticateAsync() - │ - ├─> Read Config (ApiUrl, Username, Password) - │ - ├─> POST /api/Session::login - │ └─> { username, password } - │ - ├─> Receive Response - │ └─> Extract Cookie - │ - └─> Store Cookie in HttpClient Headers - └─> Set IsAuthenticated = true -``` - -### 2. Foto-Upload (Manuell) - -``` -User clicks "Upload Last Photo" - │ - v -LycheeUploadPage.UploadLastPhotoButton_Click() - │ - ├─> Get latest photo from PictureLocation - │ - └─> LycheeUploadService.UploadImageAsync(path, albumId) - │ - ├─> Check IsAuthenticated - │ └─> If false: Call AuthenticateAsync() - │ - ├─> Read image file - │ - ├─> Create MultipartFormDataContent - │ ├─> Add image as ByteArrayContent - │ └─> Add albumID (optional) - │ - ├─> POST /api/Photo::add - │ - └─> Return success/failure - └─> Update UI Log -``` - -### 3. Foto-Upload (Automatisch) - -``` -Camera takes photo - │ - v -MainWindow.TimerControlRectangleAnimation_OnTimerEllapsed() - │ - ├─> CameraService.TakePhoto() - │ - ├─> Get path to new photo - │ - └─> If AutoUploadEnabled - │ - └─> Task.Run(async) - │ - └─> LycheeUploadService.UploadImageAsync(path, defaultAlbumId) - │ - ├─> Authenticate if needed - │ - ├─> Upload image - │ - └─> Log result (Background) -``` - -### 4. Album-Erstellung - -``` -User clicks "Create Album" - │ - v -LycheeUploadService.CreateAlbumAsync(title) - │ - ├─> Check IsAuthenticated - │ - ├─> POST /api/Album::add - │ └─> { title: "Album Name" } - │ - ├─> Parse Response - │ └─> Extract album ID - │ - └─> Return album ID - └─> Use for subsequent uploads -``` - -## Konfigurationsfluss - -``` -app.settings.json - │ - v -AppSettingsService - │ - ├─> LycheeApiUrl - ├─> LycheeUsername - ├─> LycheePassword - ├─> LycheeDefaultAlbumId - └─> LycheeAutoUploadEnabled - │ - v - Injected into LycheeUploadService - │ - └─> Used for API calls -``` - -## Fehlerbehandlung - -``` -API Call - │ - ├─> Success (200 OK) - │ └─> Parse Response - │ └─> Return Result - │ - └─> Error - │ - ├─> Network Error - │ └─> Log Error - │ └─> Return false/null - │ - ├─> Authentication Error (401) - │ └─> Log Error - │ └─> Trigger Re-Authentication - │ └─> Retry Operation - │ - └─> Other HTTP Error - └─> Log Error + Status Code - └─> Return false/null -``` - -## Thread-Sicherheit - -``` -UI Thread - │ - ├─> User Interaction - │ └─> Async Call - │ │ - │ v - │ Background Task - │ │ - │ ├─> HTTP Request - │ ├─> File I/O - │ └─> Processing - │ │ - │ v - │ Dispatcher.Invoke() - │ │ - │ v - └─────> UI Update -``` - -## Ressourcen-Management - -``` -LycheeUploadService : IDisposable - │ - ├─> Constructor - │ └─> new HttpClient() - │ - ├─> Usage - │ └─> Multiple API calls - │ - └─> Dispose() - └─> HttpClient.Dispose() - └─> Free network resources -``` - -## Best Practices - -1. **Asynchrone Operationen**: Alle API-Calls sind async/await -2. **Fehlerbehandlung**: Try-catch mit detailliertem Logging -3. **Ressourcen**: IDisposable Pattern für HttpClient -4. **Thread-Sicherheit**: Dispatcher für UI-Updates -5. **Konfiguration**: Zentrale Settings-Verwaltung -6. **Sicherheit**: Cookie-basierte Session-Verwaltung -7. **Performance**: Background-Tasks für Uploads -8. **User Experience**: Live-Feedback und Progress-Anzeigen diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/CHANGELOG.md b/src/CamBooth/CamBooth.App/Features/LycheeUpload/CHANGELOG.md deleted file mode 100644 index 2c5704c..0000000 --- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/CHANGELOG.md +++ /dev/null @@ -1,101 +0,0 @@ -# Lychee Upload Feature - Changelog - -## Version 1.0.0 - 2026-02-28 - -### ✨ Neue Features - -#### LycheeUploadService -- **Authentifizierung**: Vollständige Cookie-basierte Authentifizierung mit Lychee API -- **Single Upload**: Upload einzelner Bilder mit optionaler Album-Zuordnung -- **Batch Upload**: Upload mehrerer Bilder mit automatischer Fehlerbehandlung -- **Album-Verwaltung**: Erstellen neuer Alben direkt aus der Anwendung -- **Auto-Upload**: Konfigurierbare automatische Upload-Funktion nach Fotoaufnahme -- **Session-Management**: Automatisches Re-Authentifizieren bei abgelaufenen Sessions -- **Fehlerbehandlung**: Umfassendes Logging und Error-Handling - -#### UI-Komponenten -- **LycheeUploadPage**: Vollständige Verwaltungsoberfläche - - Verbindungsstatus-Anzeige - - Upload-Optionen und Album-Auswahl - - Live-Upload-Log mit Zeitstempel - - Fortschrittsanzeige für Batch-Uploads - - Einzelbild und Alle-Bilder Upload-Funktionen - -#### Konfiguration -- **AppSettingsService**: Erweitert um Lychee-Konfigurationsoptionen - - `LycheeApiUrl`: API-Endpunkt - - `LycheeUsername`: Benutzername - - `LycheePassword`: Passwort - - `LycheeDefaultAlbumId`: Standard-Album für Uploads - - `LycheeAutoUploadEnabled`: Auto-Upload ein/aus - -#### Dokumentation -- **README.md**: Vollständige Feature-Dokumentation -- **INTEGRATION.md**: Schritt-für-Schritt Integrationsanleitung -- **Unit Tests**: Beispiel-Tests für alle Hauptfunktionen - -### 🎨 Design -- Goldenes Farbschema (#D4AF37) passend zur Galerie -- Moderne, dunkle UI mit hohem Kontrast -- Responsive Layout mit Live-Feedback -- Statusanzeige mit farbcodierten Indikatoren - -### 🔧 Technische Details -- **HTTP Client**: Langlebiger HttpClient mit 5-Minuten-Timeout -- **Async/Await**: Vollständig asynchrone Implementierung -- **IDisposable**: Korrekte Ressourcenverwaltung -- **JSON Serialization**: System.Text.Json für API-Kommunikation -- **Multipart Forms**: Proper Image Upload mit Content-Type Headers - -### 📋 API-Endpunkte -- `POST /api/Session::login` - Authentifizierung -- `POST /api/Photo::add` - Bild-Upload -- `POST /api/Album::add` - Album-Erstellung -- `POST /api/Session::logout` - Abmeldung - -### 🔐 Sicherheit -- Cookie-basierte Session-Verwaltung -- Konfigurierbare Credentials -- HTTPS-Unterstützung -- Hinweise zur sicheren Credential-Speicherung - -### 🐛 Bekannte Einschränkungen -- Credentials werden aktuell in Plain-Text in JSON gespeichert - - **Empfehlung**: Für Production Environment Vault/Secrets verwenden -- Keine automatische Konfliktauflösung bei doppelten Uploads -- Batch-Upload ohne Pause-Funktion - -### 🚀 Geplante Features (Zukunft) -- [ ] Upload-Queue mit Retry-Mechanismus -- [ ] Verschlüsselte Credential-Speicherung -- [ ] Upload-Historie mit Datenbank -- [ ] Thumbnail-Generierung vor Upload -- [ ] Duplikat-Erkennung -- [ ] Multi-Album-Upload -- [ ] Upload-Scheduler -- [ ] Bandbreiten-Limitierung -- [ ] Resume-fähiger Upload bei Abbruch - -### 📦 Dateien -``` -Features/LycheeUpload/ -├── LycheeUploadService.cs # Haupt-Service -├── LycheeUploadPage.xaml # UI-Komponente -├── LycheeUploadPage.xaml.cs # UI Code-Behind -├── LycheeUploadServiceTests.cs # Unit Tests -├── README.md # Feature-Dokumentation -├── INTEGRATION.md # Integrations-Guide -└── CHANGELOG.md # Diese Datei -``` - -### 🔄 Migration -Keine Migration erforderlich - neues Feature. - -### ✅ Testing -- Getestet mit Lychee v4.x und v5.x -- Funktioniert mit selbst-gehosteten und Cloud-Instanzen -- Kompatibel mit HTTPS und HTTP (nur für Development!) - -### 👥 Credits -Entwickelt für CamBooth Photo Booth Application -Kompatibel mit Lychee Photo Management (https://lycheeorg.github.io/) diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/INTEGRATION.md b/src/CamBooth/CamBooth.App/Features/LycheeUpload/INTEGRATION.md deleted file mode 100644 index ae64083..0000000 --- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/INTEGRATION.md +++ /dev/null @@ -1,153 +0,0 @@ -# Integration von LycheeUploadService in MainWindow - -## Schritt 1: Service in MainWindow injizieren - -Füge den `LycheeUploadService` als Dependency im Constructor hinzu: - -```csharp -public partial class MainWindow : Window -{ - // ...existing fields... - private readonly LycheeUploadService _lycheeService; - - public MainWindow( - Logger logger, - AppSettingsService appSettings, - PictureGalleryService pictureGalleryService, - CameraService cameraService, - LycheeUploadService lycheeService) // <- Neu hinzufügen - { - // ...existing initialization... - this._lycheeService = lycheeService; - - // Optional: Bei Start authentifizieren - if (appSettings.LycheeAutoUploadEnabled) - { - _ = Task.Run(async () => await _lycheeService.AuthenticateAsync()); - } - } -} -``` - -## Schritt 2: Upload nach Fotoaufnahme hinzufügen - -Erweitere die `TimerControlRectangleAnimation_OnTimerEllapsed` Methode: - -```csharp -private async void TimerControlRectangleAnimation_OnTimerEllapsed() -{ - var photoTakenSuccessfully = false; - string? lastPhotoPath = null; - - try - { - // Foto aufnehmen - this._cameraService.TakePhoto(); - photoTakenSuccessfully = true; - - // Pfad zum letzten Foto ermitteln - var pictureLocation = _appSettings.PictureLocation; - if (Directory.Exists(pictureLocation)) - { - var latestPhoto = Directory.GetFiles(pictureLocation, "*.jpg") - .OrderByDescending(f => File.GetCreationTime(f)) - .FirstOrDefault(); - - lastPhotoPath = latestPhoto; - } - } - catch (Exception exception) - { - // ...existing error handling... - } - - // Upload zu Lychee (asynchron im Hintergrund) - if (photoTakenSuccessfully && - !string.IsNullOrEmpty(lastPhotoPath) && - _appSettings.LycheeAutoUploadEnabled) - { - _ = Task.Run(async () => - { - try - { - var albumId = _appSettings.LycheeDefaultAlbumId; - var success = await _lycheeService.UploadImageAsync(lastPhotoPath, albumId); - - if (success) - { - _logger.Info($"Foto erfolgreich zu Lychee hochgeladen: {Path.GetFileName(lastPhotoPath)}"); - } - else - { - _logger.Warning($"Lychee-Upload fehlgeschlagen: {Path.GetFileName(lastPhotoPath)}"); - } - } - catch (Exception ex) - { - _logger.Error($"Fehler beim Lychee-Upload: {ex.Message}"); - } - }); - } - - // ...rest of existing code... -} -``` - -## Schritt 3: Dependency Injection konfigurieren - -Wenn du einen DI-Container verwendest (z.B. in App.xaml.cs): - -```csharp -// Register LycheeUploadService -services.AddSingleton(); -``` - -Oder ohne DI-Container: - -```csharp -var lycheeService = new LycheeUploadService(appSettingsService, logger); -``` - -## Schritt 4: Optional - Upload-Page zur Navigation hinzufügen - -Falls du einen Zugriff auf die Upload-Verwaltung per UI möchtest: - -```csharp -// In MainWindow.xaml.cs -private void OpenLycheeUploadManager(object sender, RoutedEventArgs e) -{ - var uploadPage = new LycheeUploadPage( - _appSettings, - _logger, - _lycheeService, - _pictureGalleryService); - - // Navigiere zu Upload-Page oder zeige als Dialog - this.MainFrame.Navigate(uploadPage); -} -``` - -Und in MainWindow.xaml einen Button hinzufügen: - -```xml - -``` - -## Wichtige Hinweise - -1. **Asynchroner Upload**: Der Upload läuft im Hintergrund, sodass die Fotobox nicht blockiert wird -2. **Fehlerbehandlung**: Fehler beim Upload werden geloggt, aber die normale Funktionalität bleibt erhalten -3. **Performance**: Bei vielen Fotos kann der Upload Zeit in Anspruch nehmen -4. **Authentifizierung**: Der Service authentifiziert sich automatisch bei Bedarf - -## Testing - -1. Konfiguriere die Lychee-Einstellungen in `app.settings.json` -2. Setze `AutoUploadEnabled` auf `true` -3. Mache ein Testfoto -4. Prüfe die Logs für Upload-Status -5. Verifiziere in deiner Lychee-Instanz, dass das Foto angekommen ist diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeAuthStrategy.cs b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeAuthStrategy.cs deleted file mode 100644 index 4bed746..0000000 --- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeAuthStrategy.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.IO; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.RegularExpressions; -using CamBooth.App.Core.AppSettings; -using CamBooth.App.Core.Logging; - -namespace CamBooth.App.Features.LycheeUpload; - -/// -/// Alternative Authentifizierungs-Strategie für Lychee -/// Kombiniert CSRF-Token-Fetch und Login in einem Session-Flow -/// -public class LycheeAuthStrategy -{ - private readonly AppSettingsService _appSettings; - private readonly Logger _logger; - private readonly CookieContainer _cookieContainer; - private readonly HttpClientHandler _httpClientHandler; - private readonly HttpClient _httpClient; - - public LycheeAuthStrategy(AppSettingsService appSettings, Logger logger, CookieContainer cookieContainer, HttpClientHandler httpClientHandler, HttpClient httpClient) - { - _appSettings = appSettings; - _logger = logger; - _cookieContainer = cookieContainer; - _httpClientHandler = httpClientHandler; - _httpClient = httpClient; - } - - /// - /// Alternative Strategie: Verwende die Web-Login-Seite statt API - /// Dies umgeht Session-Probleme bei der API-Authentifizierung - /// - public async Task<(bool success, string? csrfToken)> AuthenticateViaWebAsync() - { - try - { - var lycheeUrl = _appSettings.LycheeApiUrl; - var username = _appSettings.LycheeUsername; - var password = _appSettings.LycheePassword; - - _logger.Info("Versuche Web-basierte Authentifizierung..."); - - // Schritt 1: Hole Login-Seite (enthält CSRF-Token) - _logger.Debug("Hole Login-Seite..."); - var loginPageResponse = await _httpClient.GetAsync($"{lycheeUrl}"); - - if (!loginPageResponse.IsSuccessStatusCode) - { - _logger.Error($"Login-Seite konnte nicht geladen werden: {loginPageResponse.StatusCode}"); - return (false, null); - } - - var html = await loginPageResponse.Content.ReadAsStringAsync(); - - // Extrahiere CSRF-Token aus HTML - var csrfMatch = Regex.Match(html, @"csrf-token""\s*content=""([^""]+)"""); - if (!csrfMatch.Success) - { - _logger.Error("CSRF-Token konnte nicht aus HTML extrahiert werden"); - return (false, null); - } - - var csrfToken = csrfMatch.Groups[1].Value; - _logger.Debug($"CSRF-Token gefunden: {csrfToken.Substring(0, Math.Min(20, csrfToken.Length))}..."); - - // Debug: Zeige Cookies nach Login-Seite - var uri = new Uri(lycheeUrl); - var cookies = _cookieContainer.GetCookies(uri); - _logger.Debug($"Cookies nach Login-Seite: {cookies.Count}"); - foreach (Cookie cookie in cookies) - { - _logger.Debug($" Cookie: {cookie.Name} (Domain: {cookie.Domain})"); - } - - // Schritt 2: Sende Login-Request SOFORT (gleiche Session) - _logger.Debug("Sende Login-Request..."); - - var loginData = new Dictionary - { - { "username", username }, - { "password", password }, - { "_token", csrfToken } // Laravel erwartet _token im Body - }; - - var loginContent = new FormUrlEncodedContent(loginData); - - var loginRequest = new HttpRequestMessage(HttpMethod.Post, $"{lycheeUrl}/api/Session::login") - { - Content = loginContent - }; - - // Wichtig: Setze alle erforderlichen Headers - loginRequest.Headers.Add("Accept", "application/json"); - loginRequest.Headers.Add("X-CSRF-TOKEN", csrfToken); - loginRequest.Headers.Add("X-XSRF-TOKEN", csrfToken); - loginRequest.Headers.Add("X-Requested-With", "XMLHttpRequest"); - loginRequest.Headers.Add("Origin", $"{uri.Scheme}://{uri.Host}"); - loginRequest.Headers.Add("Referer", lycheeUrl); - - var loginResponse = await _httpClient.SendAsync(loginRequest); - - if (loginResponse.IsSuccessStatusCode) - { - var responseContent = await loginResponse.Content.ReadAsStringAsync(); - _logger.Debug($"Login Response: {responseContent}"); - _logger.Info("✅ Web-basierte Authentifizierung erfolgreich!"); - return (true, csrfToken); - } - - _logger.Error($"Login fehlgeschlagen: {loginResponse.StatusCode}"); - var errorBody = await loginResponse.Content.ReadAsStringAsync(); - _logger.Debug($"Error Response: {errorBody}"); - return (false, null); - } - catch (Exception ex) - { - _logger.Error($"Fehler bei Web-Authentifizierung: {ex.Message}"); - return (false, null); - } - } - - /// - /// Strategie 2: Verwende Session-Cookie direkt ohne API-Login - /// Funktioniert wenn Lychee im Browser bereits eingeloggt ist - /// - public async Task TryExistingSessionAsync() - { - try - { - var lycheeUrl = _appSettings.LycheeApiUrl; - _logger.Info("Prüfe existierende Session..."); - - // Versuche Session::info abzurufen - var response = await _httpClient.GetAsync($"{lycheeUrl}/api/Session::info"); - - if (response.IsSuccessStatusCode) - { - var content = await response.Content.ReadAsStringAsync(); - _logger.Debug($"Session Info: {content}"); - - // Wenn wir Daten bekommen, ist die Session gültig - if (!string.IsNullOrWhiteSpace(content) && content.Contains("username")) - { - _logger.Info("✅ Existierende Session gefunden und gültig!"); - return true; - } - } - - _logger.Debug("Keine gültige existierende Session gefunden"); - return false; - } - catch (Exception ex) - { - _logger.Error($"Fehler beim Prüfen der Session: {ex.Message}"); - return false; - } - } - - /// - /// Strategie 3: Browser-Cookies importieren - /// Kopiert Session-Cookies aus dem Browser in den HttpClient - /// - public void ImportBrowserCookies(string sessionCookie, string xsrfToken) - { - try - { - var lycheeUrl = _appSettings.LycheeApiUrl; - var uri = new Uri(lycheeUrl); - - _logger.Info("Importiere Browser-Cookies..."); - - // Füge Session-Cookie hinzu - var sessionCookieObj = new Cookie("lychee_session", sessionCookie) - { - Domain = uri.Host.StartsWith("www.") ? uri.Host.Substring(4) : uri.Host, - Path = "/" - }; - _cookieContainer.Add(uri, sessionCookieObj); - - // Füge XSRF-Token hinzu - var xsrfCookieObj = new Cookie("XSRF-TOKEN", xsrfToken) - { - Domain = uri.Host.StartsWith("www.") ? uri.Host.Substring(4) : uri.Host, - Path = "/" - }; - _cookieContainer.Add(uri, xsrfCookieObj); - - _logger.Info("Browser-Cookies erfolgreich importiert"); - } - catch (Exception ex) - { - _logger.Error($"Fehler beim Importieren der Cookies: {ex.Message}"); - } - } -} diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml deleted file mode 100644 index d383a79..0000000 --- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml.cs b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml.cs deleted file mode 100644 index 65c5a3d..0000000 --- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System.IO; -using System.Windows; -using System.Windows.Controls; -using CamBooth.App.Core.AppSettings; -using CamBooth.App.Core.Logging; -using CamBooth.App.Features.PictureGallery; - -namespace CamBooth.App.Features.LycheeUpload; - -public partial class LycheeUploadPage : Page -{ - private readonly AppSettingsService _appSettings; - private readonly Logger _logger; - private readonly LycheeUploadService _lycheeService; - private readonly PictureGalleryService _pictureGalleryService; - - public LycheeUploadPage( - AppSettingsService appSettings, - Logger logger, - LycheeUploadService lycheeService, - PictureGalleryService pictureGalleryService) - { - _appSettings = appSettings; - _logger = logger; - _lycheeService = lycheeService; - _pictureGalleryService = pictureGalleryService; - - InitializeComponent(); - InitializePage(); - } - - private void InitializePage() - { - // Set initial values - ConnectionUrlText.Text = _appSettings.LycheeApiUrl ?? "Nicht konfiguriert"; - AlbumIdTextBox.Text = _appSettings.LycheeDefaultAlbumId ?? ""; - AutoUploadCheckBox.IsChecked = _appSettings.LycheeAutoUploadEnabled; - - UpdateConnectionStatus(); - } - - private void UpdateConnectionStatus() - { - ConnectionStatusText.Text = _lycheeService.IsAuthenticated ? "Verbunden" : "Nicht verbunden"; - ConnectButton.Content = _lycheeService.IsAuthenticated ? "Trennen" : "Verbinden"; - } - - private async void ConnectButton_Click(object sender, RoutedEventArgs e) - { - ConnectButton.IsEnabled = false; - - try - { - if (_lycheeService.IsAuthenticated) - { - // Disconnect - await _lycheeService.LogoutAsync(); - AddLogEntry("Von Lychee getrennt."); - } - else - { - // Connect - AddLogEntry("Verbinde mit Lychee..."); - var success = await _lycheeService.AuthenticateAsync(); - - if (success) - { - AddLogEntry("✅ Erfolgreich verbunden!"); - } - else - { - AddLogEntry("❌ Verbindung fehlgeschlagen. Prüfe deine Einstellungen."); - } - } - } - finally - { - ConnectButton.IsEnabled = true; - UpdateConnectionStatus(); - } - } - - private async void UploadLastPhotoButton_Click(object sender, RoutedEventArgs e) - { - UploadLastPhotoButton.IsEnabled = false; - UploadProgressBar.Visibility = Visibility.Visible; - UploadProgressBar.IsIndeterminate = true; - - try - { - var pictureLocation = _appSettings.PictureLocation; - - if (string.IsNullOrEmpty(pictureLocation) || !Directory.Exists(pictureLocation)) - { - AddLogEntry("❌ Bild-Verzeichnis nicht gefunden."); - return; - } - - // Get the latest photo - var imageFiles = Directory.GetFiles(pictureLocation, "*.jpg") - .OrderByDescending(f => File.GetCreationTime(f)) - .ToList(); - - if (imageFiles.Count == 0) - { - AddLogEntry("❌ Keine Fotos zum Hochladen gefunden."); - return; - } - - var lastPhoto = imageFiles.First(); - var fileName = Path.GetFileName(lastPhoto); - - AddLogEntry($"📤 Lade hoch: {fileName}"); - - var albumId = string.IsNullOrWhiteSpace(AlbumIdTextBox.Text) ? null : AlbumIdTextBox.Text; - var success = await _lycheeService.UploadImageAsync(lastPhoto, albumId); - - if (success) - { - AddLogEntry($"✅ Upload erfolgreich: {fileName}"); - } - else - { - AddLogEntry($"❌ Upload fehlgeschlagen: {fileName}"); - } - } - catch (Exception ex) - { - AddLogEntry($"❌ Fehler: {ex.Message}"); - _logger.Error($"Upload error: {ex.Message}"); - } - finally - { - UploadLastPhotoButton.IsEnabled = true; - UploadProgressBar.Visibility = Visibility.Collapsed; - } - } - - private async void UploadAllPhotosButton_Click(object sender, RoutedEventArgs e) - { - var result = MessageBox.Show( - "Möchtest du wirklich alle Fotos zu Lychee hochladen? Dies kann einige Zeit dauern.", - "Alle Fotos hochladen", - MessageBoxButton.YesNo, - MessageBoxImage.Question); - - if (result != MessageBoxResult.Yes) - { - return; - } - - UploadAllPhotosButton.IsEnabled = false; - UploadLastPhotoButton.IsEnabled = false; - UploadProgressBar.Visibility = Visibility.Visible; - UploadProgressBar.IsIndeterminate = false; - UploadProgressBar.Value = 0; - - try - { - var pictureLocation = _appSettings.PictureLocation; - - if (string.IsNullOrEmpty(pictureLocation) || !Directory.Exists(pictureLocation)) - { - AddLogEntry("❌ Bild-Verzeichnis nicht gefunden."); - return; - } - - // Get all photos - var imageFiles = Directory.GetFiles(pictureLocation, "*.jpg").ToList(); - - if (imageFiles.Count == 0) - { - AddLogEntry("❌ Keine Fotos zum Hochladen gefunden."); - return; - } - - AddLogEntry($"📤 Starte Upload von {imageFiles.Count} Fotos..."); - - var albumId = string.IsNullOrWhiteSpace(AlbumIdTextBox.Text) ? null : AlbumIdTextBox.Text; - int uploadedCount = 0; - - for (int i = 0; i < imageFiles.Count; i++) - { - var imagePath = imageFiles[i]; - var fileName = Path.GetFileName(imagePath); - - AddLogEntry($"📤 ({i + 1}/{imageFiles.Count}) {fileName}"); - - var success = await _lycheeService.UploadImageAsync(imagePath, albumId); - - if (success) - { - uploadedCount++; - AddLogEntry($"✅ Upload erfolgreich"); - } - else - { - AddLogEntry($"❌ Upload fehlgeschlagen"); - } - - // Update progress - UploadProgressBar.Value = ((i + 1) / (double)imageFiles.Count) * 100; - - // Small delay between uploads - await Task.Delay(500); - } - - AddLogEntry($"✅ Fertig! {uploadedCount} von {imageFiles.Count} Fotos erfolgreich hochgeladen."); - } - catch (Exception ex) - { - AddLogEntry($"❌ Fehler: {ex.Message}"); - _logger.Error($"Batch upload error: {ex.Message}"); - } - finally - { - UploadAllPhotosButton.IsEnabled = true; - UploadLastPhotoButton.IsEnabled = true; - UploadProgressBar.Visibility = Visibility.Collapsed; - } - } - - private void AutoUploadCheckBox_Changed(object sender, RoutedEventArgs e) - { - // Note: This would need to persist the setting back to configuration - // For now, it just shows the intended behavior - var isEnabled = AutoUploadCheckBox.IsChecked == true; - AddLogEntry(isEnabled - ? "ℹ️ Automatischer Upload aktiviert" - : "ℹ️ Automatischer Upload deaktiviert"); - } - - private void AddLogEntry(string message) - { - Dispatcher.Invoke(() => - { - var timestamp = DateTime.Now.ToString("HH:mm:ss"); - var logEntry = $"[{timestamp}] {message}\n"; - - UploadLogTextBlock.Text += logEntry; - - // Auto-scroll to bottom - if (UploadLogTextBlock.Parent is ScrollViewer scrollViewer) - { - scrollViewer.ScrollToEnd(); - } - }); - } -} diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadService.cs b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadService.cs deleted file mode 100644 index 44e3f3f..0000000 --- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadService.cs +++ /dev/null @@ -1,594 +0,0 @@ -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Windows.Media.Imaging; -using CamBooth.App.Core.AppSettings; -using CamBooth.App.Core.Logging; - -namespace CamBooth.App.Features.LycheeUpload; - -/// -/// Service für den Upload von Bildern zu Lychee mit Authentifizierung -/// -public class LycheeUploadService : IDisposable -{ - private readonly AppSettingsService _appSettings; - private readonly Logger _logger; - private readonly HttpClient _httpClient; - private readonly QRCodeGenerationService _qrCodeGenerationService; - private readonly CookieContainer _cookieContainer; - private readonly HttpClientHandler _httpClientHandler; - private string? _csrfToken; - private bool _isAuthenticated = false; - - public LycheeUploadService(AppSettingsService appSettings, Logger logger) - { - _appSettings = appSettings; - _logger = logger; - _qrCodeGenerationService = new QRCodeGenerationService(logger); - - // CookieContainer für Session-Management - _cookieContainer = new CookieContainer(); - _httpClientHandler = new HttpClientHandler - { - CookieContainer = _cookieContainer, - UseCookies = true - }; - - _httpClient = new HttpClient(_httpClientHandler); - _httpClient.Timeout = TimeSpan.FromMinutes(5); - // Note: Accept header wird per-Request gesetzt, nicht als Default - _httpClient.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest"); - } - - /// - /// Gibt an, ob der Service authentifiziert ist - /// - public bool IsAuthenticated => _isAuthenticated; - - /// - /// Der zuletzt generierte QR-Code - /// - public BitmapImage? LastGeneratedQRCode { get; private set; } - - /// - /// Authentifiziert sich bei Lychee - /// - /// True wenn erfolgreich, sonst False - public async Task AuthenticateAsync() - { - try - { - _logger.Info("Starte Lychee-Authentifizierung..."); - - var lycheeUrl = _appSettings.LycheeApiUrl; - var username = _appSettings.LycheeUsername; - var password = _appSettings.LycheePassword; - - if (string.IsNullOrEmpty(lycheeUrl) || string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) - { - _logger.Error("Lychee-Konfiguration unvollständig. Bitte LycheeApiUrl, LycheeUsername und LycheePassword in den Einstellungen setzen."); - return false; - } - - // WICHTIG: Zuerst die Hauptseite abrufen um Session zu initialisieren! - // Ohne vorherige Session weigert sich Lychee, den Login zu akzeptieren - _logger.Debug($"Initialisiere Session durch GET-Request zu: {lycheeUrl}"); - var sessionInitResponse = await _httpClient.GetAsync(lycheeUrl); - - if (!sessionInitResponse.IsSuccessStatusCode) - { - _logger.Warning($"Session-Initialisierung fehlgeschlagen (Status: {sessionInitResponse.StatusCode}), versuche trotzdem Login..."); - } - else - { - _logger.Debug("✅ Session initialisiert"); - - // Debug: Zeige Cookies nach Initialisierung - var uri = new Uri(lycheeUrl); - var cookies = _cookieContainer.GetCookies(uri); - _logger.Debug($"Cookies nach Session-Init: {cookies.Count}"); - foreach (Cookie cookie in cookies) - { - _logger.Debug($" Cookie: {cookie.Name} = {cookie.Value.Substring(0, Math.Min(30, cookie.Value.Length))}..."); - } - } - - // Jetzt den Login-Request senden - var loginData = new - { - username = username, - password = password - }; - - var jsonContent = JsonSerializer.Serialize(loginData); - _logger.Debug($"Login Data: {jsonContent}"); - - var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - - // Erstelle Request für v2 API - var request = new HttpRequestMessage(HttpMethod.Post, $"{lycheeUrl}/api/v2/Auth::login") - { - Content = content - }; - - // WICHTIG: Extrahiere XSRF-TOKEN aus Cookie und sende als Header - // Laravel braucht das für CSRF-Schutz! - var uri2 = new Uri(lycheeUrl); - var cookiesForLogin = _cookieContainer.GetCookies(uri2); - string? xsrfToken = null; - - foreach (Cookie cookie in cookiesForLogin) - { - if (cookie.Name == "XSRF-TOKEN") - { - // URL-decode den Cookie-Wert - xsrfToken = Uri.UnescapeDataString(cookie.Value); - _logger.Debug($"XSRF-TOKEN aus Cookie extrahiert: {xsrfToken.Substring(0, Math.Min(30, xsrfToken.Length))}..."); - break; - } - } - - // Setze erforderliche Headers für v2 API - request.Headers.Add("Accept", "application/json"); - request.Headers.Add("User-Agent", "CamBooth/1.0"); - request.Headers.Add("Origin", new Uri(lycheeUrl).GetLeftPart(UriPartial.Authority)); - request.Headers.Add("Referer", lycheeUrl); - - // CSRF-Token als Header hinzufügen (falls vorhanden) - if (!string.IsNullOrEmpty(xsrfToken)) - { - request.Headers.Add("X-XSRF-TOKEN", xsrfToken); - _logger.Debug("✅ X-XSRF-TOKEN Header hinzugefügt"); - } - else - { - _logger.Warning("⚠️ Kein XSRF-TOKEN Cookie gefunden!"); - } - - _logger.Debug($"Sende Login-Request an: {lycheeUrl}/api/v2/Auth::login"); - var response = await _httpClient.SendAsync(request); - - // Lychee v2 API kann 204 No Content zurückgeben (erfolgreich, aber kein Body) - if (response.StatusCode == System.Net.HttpStatusCode.NoContent) - { - _logger.Info("✅ Login-Response: 204 No Content (erfolgreich)"); - - // 204 bedeutet Login war erfolgreich - Session ist aktiv - // Optionale Validierung mit auth/me (kann übersprungen werden) - _logger.Debug("Validiere Session mit auth/me endpoint..."); - - try - { - var meRequest = new HttpRequestMessage(HttpMethod.Get, $"{lycheeUrl}/api/v2/auth/me"); - // Lychee erwartet manchmal text/html statt application/json - meRequest.Headers.Add("Accept", "application/json, text/html, */*"); - - var meResponse = await _httpClient.SendAsync(meRequest); - - if (meResponse.IsSuccessStatusCode) - { - var meContent = await meResponse.Content.ReadAsStringAsync(); - _logger.Debug($"auth/me Response: {meContent}"); - _logger.Info("✅ Session validiert!"); - } - else - { - _logger.Warning($"Session-Validierung nicht erfolgreich (Status: {meResponse.StatusCode}), aber Login war OK - fahre fort."); - } - } - catch (Exception ex) - { - _logger.Warning($"Session-Validierung fehlgeschlagen: {ex.Message}, aber Login war OK - fahre fort."); - } - - // Authentifizierung ist erfolgreich (Login gab 204 zurück) - _isAuthenticated = true; - _logger.Info("✅ Lychee-Authentifizierung erfolgreich!"); - - // Debug: Zeige Cookies - var uri3 = new Uri(lycheeUrl); - var cookiesAfterLogin = _cookieContainer.GetCookies(uri3); - _logger.Debug($"Cookies nach Login: {cookiesAfterLogin.Count}"); - foreach (Cookie cookie in cookiesAfterLogin) - { - _logger.Debug($" Cookie: {cookie.Name} = {cookie.Value.Substring(0, Math.Min(30, cookie.Value.Length))}..."); - } - - return true; - } - - if (response.IsSuccessStatusCode) - { - var responseContent = await response.Content.ReadAsStringAsync(); - _logger.Debug($"Login Response: {responseContent}"); - - // Debug: Zeige alle Response-Header - foreach (var header in response.Headers) - { - _logger.Debug($"Response Header: {header.Key} = {string.Join(", ", header.Value)}"); - } - - // Debug: Zeige Cookies - var uri4 = new Uri(lycheeUrl); - var cookiesAfterLogin = _cookieContainer.GetCookies(uri4); - _logger.Debug($"Cookies nach Login: {cookiesAfterLogin.Count}"); - foreach (Cookie cookie in cookiesAfterLogin) - { - _logger.Debug($" Cookie: {cookie.Name} = {cookie.Value.Substring(0, Math.Min(30, cookie.Value.Length))}..."); - } - - // Parse die Response - using var jsonDoc = JsonDocument.Parse(responseContent); - - // Prüfe auf API-Token in der Response - if (jsonDoc.RootElement.TryGetProperty("api_token", out var apiToken)) - { - var tokenValue = apiToken.GetString(); - _logger.Debug($"✅ API-Token erhalten: {tokenValue?.Substring(0, Math.Min(30, tokenValue?.Length ?? 0))}..."); - - // Setze API-Token als Authorization Header für zukünftige Requests - _httpClient.DefaultRequestHeaders.Remove("Authorization"); - _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {tokenValue}"); - - _isAuthenticated = true; - _logger.Info("✅ Lychee-Authentifizierung erfolgreich mit API-Token!"); - return true; - } - - // v2 API gibt success oder status zurück - if (jsonDoc.RootElement.TryGetProperty("success", out var success) && success.GetBoolean()) - { - _isAuthenticated = true; - _logger.Info("✅ Lychee-Authentifizierung erfolgreich!"); - return true; - } - - // Fallback für andere Response-Formate - if (jsonDoc.RootElement.ValueKind == JsonValueKind.Object) - { - _isAuthenticated = true; - _logger.Info("✅ Lychee-Authentifizierung erfolgreich!"); - return true; - } - } - - _logger.Error($"❌ Lychee-Authentifizierung fehlgeschlagen. Status: {response.StatusCode} - {response.ReasonPhrase}"); - var errorBody = await response.Content.ReadAsStringAsync(); - _logger.Debug($"Error Response: {errorBody}"); - return false; - } - catch (Exception ex) - { - _logger.Error($"❌ Fehler bei der Lychee-Authentifizierung: {ex.Message}"); - return false; - } - } - - /// - /// Lädt ein Bild zu Lychee hoch - /// - /// Pfad zum Bild - /// Optional: Album-ID in Lychee - /// True wenn erfolgreich, sonst False - public async Task UploadImageAsync(string imagePath, string? albumId = null) - { - try - { - if (!_isAuthenticated) - { - _logger.Warning("Nicht authentifiziert. Versuche Authentifizierung..."); - var authSuccess = await AuthenticateAsync(); - if (!authSuccess) - { - return false; - } - } - - if (!File.Exists(imagePath)) - { - _logger.Error($"Bild nicht gefunden: {imagePath}"); - return false; - } - - _logger.Info($"Starte Upload: {imagePath}"); - - var lycheeUrl = _appSettings.LycheeApiUrl; - - // Debug: Zeige Authentifizierungs-Status - _logger.Debug($"IsAuthenticated: {_isAuthenticated}"); - _logger.Debug($"Authorization Header vorhanden: {_httpClient.DefaultRequestHeaders.Contains("Authorization")}"); - - // Debug: Zeige Cookies für Upload - var uri = new Uri(lycheeUrl); - var cookies = _cookieContainer.GetCookies(uri); - _logger.Debug($"Cookies für Upload: {cookies.Count}"); - foreach (Cookie cookie in cookies) - { - _logger.Debug($" Cookie: {cookie.Name} = {cookie.Value.Substring(0, Math.Min(30, cookie.Value.Length))}..."); - } - - // Lese das Bild - var fileName = Path.GetFileName(imagePath); - var imageBytes = await File.ReadAllBytesAsync(imagePath); - - // Extrahiere Datei-Informationen - var fileInfo = new FileInfo(imagePath); - var fileExtension = Path.GetExtension(imagePath).TrimStart('.'); - - // File last modified time als Unix-Timestamp in Millisekunden (Lychee erwartet eine Zahl!) - var lastModifiedUnixMs = new DateTimeOffset(fileInfo.LastWriteTimeUtc).ToUnixTimeMilliseconds().ToString(); - - // Erstelle Multipart Form Data nach Lychee v2 API Spezifikation - using var form = new MultipartFormDataContent(); - - // Album ID (optional) - var albumIdContent = new StringContent(albumId ?? ""); - albumIdContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "album_id" }; - form.Add(albumIdContent); - - // File last modified time (Unix-Timestamp in Millisekunden) - var modifiedTimeContent = new StringContent(lastModifiedUnixMs); - modifiedTimeContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "file_last_modified_time" }; - form.Add(modifiedTimeContent); - - // File (das eigentliche Bild) - var imageContent = new ByteArrayContent(imageBytes); - imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); - imageContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "file", FileName = fileName }; - form.Add(imageContent); - - // File name - var fileNameContent = new StringContent(fileName); - fileNameContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "file_name" }; - form.Add(fileNameContent); - - // UUID name - Lychee erwartet NULL/leer für automatische Generierung! - var uuidContent = new StringContent(""); - uuidContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "uuid_name" }; - form.Add(uuidContent); - - // Extension - Lychee erwartet NULL/leer für automatische Erkennung! - var extensionContent = new StringContent(""); - extensionContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "extension" }; - form.Add(extensionContent); - - // Chunk number (für nicht-gechunkte Uploads: 1, nicht 0!) - var chunkNumberContent = new StringContent("1"); - chunkNumberContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "chunk_number" }; - form.Add(chunkNumberContent); - - // Total chunks (für nicht-gechunkte Uploads: 1) - var totalChunksContent = new StringContent("1"); - totalChunksContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "total_chunks" }; - form.Add(totalChunksContent); - - // WICHTIG: Extrahiere XSRF-TOKEN aus Cookie für Upload-Request - string? xsrfTokenForUpload = null; - foreach (Cookie cookie in cookies) - { - if (cookie.Name == "XSRF-TOKEN") - { - xsrfTokenForUpload = Uri.UnescapeDataString(cookie.Value); - _logger.Debug($"XSRF-TOKEN für Upload extrahiert: {xsrfTokenForUpload.Substring(0, Math.Min(30, xsrfTokenForUpload.Length))}..."); - break; - } - } - - // Sende Upload-Request (Lychee v2 API) - // Korrekter Endpoint: /api/v2/Photo (mit großem P, wie im C# Example!) - var request = new HttpRequestMessage(HttpMethod.Post, $"{lycheeUrl}/api/v2/Photo") - { - Content = form - }; - - // Lychee erwartet manchmal text/html statt application/json - request.Headers.Add("Accept", "application/json, text/html, */*"); - - // CSRF-Token als Header hinzufügen (WICHTIG!) - if (!string.IsNullOrEmpty(xsrfTokenForUpload)) - { - request.Headers.Add("X-XSRF-TOKEN", xsrfTokenForUpload); - _logger.Debug("✅ X-XSRF-TOKEN Header zum Upload hinzugefügt"); - } - else - { - _logger.Warning("⚠️ Kein XSRF-TOKEN für Upload gefunden!"); - } - - var response = await _httpClient.SendAsync(request); - - if (response.IsSuccessStatusCode) - { - var responseContent = await response.Content.ReadAsStringAsync(); - _logger.Info($"Upload erfolgreich: {fileName}"); - _logger.Debug($"Lychee Response: {responseContent}"); - return true; - } - - _logger.Error($"Upload fehlgeschlagen. Status: {response.StatusCode}"); - var errorContent = await response.Content.ReadAsStringAsync(); - _logger.Error($"Error Response: {errorContent}"); - return false; - } - catch (Exception ex) - { - _logger.Error($"Fehler beim Upload: {ex.Message}"); - return false; - } - } - - /// - /// Lädt mehrere Bilder zu Lychee hoch - /// - /// Liste von Bildpfaden - /// Optional: Album-ID in Lychee - /// Anzahl erfolgreich hochgeladener Bilder - public async Task UploadImagesAsync(IEnumerable imagePaths, string? albumId = null) - { - int successCount = 0; - - foreach (var imagePath in imagePaths) - { - var success = await UploadImageAsync(imagePath, albumId); - if (success) - { - successCount++; - } - - // Kleine Verzögerung zwischen Uploads - await Task.Delay(500); - } - - _logger.Info($"{successCount} von {imagePaths.Count()} Bildern erfolgreich hochgeladen."); - return successCount; - } - - /// - /// Erstellt ein neues Album in Lychee - /// - /// Titel des Albums - /// Album-ID wenn erfolgreich, sonst null - public async Task CreateAlbumAsync(string albumTitle) - { - try - { - if (!_isAuthenticated) - { - _logger.Warning("Nicht authentifiziert. Versuche Authentifizierung..."); - var authSuccess = await AuthenticateAsync(); - if (!authSuccess) - { - return null; - } - } - - _logger.Info($"Erstelle Album: {albumTitle}"); - - var lycheeUrl = _appSettings.LycheeApiUrl; - - // Lychee v2 API - JSON Format - var albumData = new - { - title = albumTitle - }; - - var jsonContent = JsonSerializer.Serialize(albumData); - var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - - // Sende Request (Lychee v2 API) - var request = new HttpRequestMessage(HttpMethod.Post, $"{lycheeUrl}/api/v2/Albums") - { - Content = content - }; - - request.Headers.Add("Accept", "application/json"); - - var response = await _httpClient.SendAsync(request); - - if (response.IsSuccessStatusCode) - { - var responseContent = await response.Content.ReadAsStringAsync(); - using var jsonDoc = JsonDocument.Parse(responseContent); - - if (jsonDoc.RootElement.TryGetProperty("id", out var idElement)) - { - var albumId = idElement.GetString(); - _logger.Info($"Album erfolgreich erstellt. ID: {albumId}"); - return albumId; - } - } - - _logger.Error($"Album-Erstellung fehlgeschlagen. Status: {response.StatusCode}"); - return null; - } - catch (Exception ex) - { - _logger.Error($"Fehler beim Erstellen des Albums: {ex.Message}"); - return null; - } - } - - /// - /// Meldet sich von Lychee ab - /// - public async Task LogoutAsync() - { - try - { - if (_isAuthenticated) - { - var lycheeUrl = _appSettings.LycheeApiUrl; - - var request = new HttpRequestMessage(HttpMethod.Post, $"{lycheeUrl}/api/v2/Auth::logout"); - request.Headers.Add("Accept", "application/json"); - - await _httpClient.SendAsync(request); - _logger.Info("Von Lychee abgemeldet."); - } - - _isAuthenticated = false; - _httpClient.DefaultRequestHeaders.Clear(); - } - catch (Exception ex) - { - _logger.Error($"Fehler beim Abmelden: {ex.Message}"); - } - } - - /// - /// Generiert einen QR-Code für die Lychee-Galerie-URL - /// - /// BitmapImage des QR-Codes oder null bei Fehler - public BitmapImage? GenerateGalleryQRCode() - { - try - { - var lycheeUrl = _appSettings.LycheeApiUrl; - var albumId = _appSettings.LycheeDefaultAlbumId; - - if (string.IsNullOrEmpty(lycheeUrl)) - { - _logger.Error("Lychee-URL ist nicht konfiguriert. Kann QR-Code nicht generieren."); - return null; - } - - if (string.IsNullOrEmpty(albumId)) - { - _logger.Warning("Lychee DefaultAlbumId ist nicht konfiguriert. QR-Code zeigt nur die Basis-URL."); - } - - _logger.Info("Generiere QR-Code für Lychee-Galerie..."); - - // Konstruiere die Gallery-URL: ApiUrl + /Gallery + DefaultAlbumId - var galleryUrl = $"{lycheeUrl}/gallery/{albumId}"; - _logger.Debug($"QR-Code URL: {galleryUrl}"); - - // Generiere QR-Code mit der Gallery-URL - var qrCode = _qrCodeGenerationService.GenerateQRCode(galleryUrl); - - if (qrCode != null) - { - LastGeneratedQRCode = qrCode; - _logger.Info("✅ QR-Code erfolgreich generiert"); - } - - return qrCode; - } - catch (Exception ex) - { - _logger.Error($"Fehler beim Generieren des QR-Codes: {ex.Message}"); - return null; - } - } - - public void Dispose() - { - _httpClient?.Dispose(); - _httpClientHandler?.Dispose(); - } -} \ No newline at end of file diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadServiceTests.cs b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadServiceTests.cs deleted file mode 100644 index 5faa493..0000000 --- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadServiceTests.cs +++ /dev/null @@ -1,241 +0,0 @@ -// using System; -// using System.IO; -// using System.Threading.Tasks; -// using CamBooth.App.Core.AppSettings; -// using CamBooth.App.Core.Logging; -// using CamBooth.App.Features.LycheeUpload; -// -// namespace CamBooth.Tests.Features.LycheeUpload; -// -// /// -// /// Example Unit Tests for LycheeUploadService -// /// Note: Requires a running Lychee instance for integration tests -// /// -// public class LycheeUploadServiceTests -// { -// private readonly Logger _logger; -// private readonly AppSettingsService _appSettings; -// private LycheeUploadService _service; -// -// public LycheeUploadServiceTests() -// { -// _logger = new Logger(null); -// _appSettings = new AppSettingsService(_logger); -// _service = new LycheeUploadService(_appSettings, _logger); -// } -// -// // Unit Test: Authentication -// public async Task TestAuthentication() -// { -// Console.WriteLine("Testing Lychee Authentication..."); -// -// var result = await _service.AuthenticateAsync(); -// -// if (result) -// { -// Console.WriteLine("✅ Authentication successful"); -// Console.WriteLine($" IsAuthenticated: {_service.IsAuthenticated}"); -// } -// else -// { -// Console.WriteLine("❌ Authentication failed"); -// } -// } -// -// // Unit Test: Single Image Upload -// public async Task TestSingleImageUpload() -// { -// Console.WriteLine("\nTesting Single Image Upload..."); -// -// // Authenticate first -// await _service.AuthenticateAsync(); -// -// // Create a test image -// var testImagePath = CreateTestImage(); -// -// var result = await _service.UploadImageAsync(testImagePath); -// -// if (result) -// { -// Console.WriteLine("✅ Image upload successful"); -// } -// else -// { -// Console.WriteLine("❌ Image upload failed"); -// } -// -// // Cleanup -// if (File.Exists(testImagePath)) -// { -// File.Delete(testImagePath); -// } -// } -// -// // Unit Test: Batch Upload -// public async Task TestBatchUpload() -// { -// Console.WriteLine("\nTesting Batch Image Upload..."); -// -// // Authenticate first -// await _service.AuthenticateAsync(); -// -// // Create test images -// var testImages = new[] -// { -// CreateTestImage("test1.jpg"), -// CreateTestImage("test2.jpg"), -// CreateTestImage("test3.jpg") -// }; -// -// var successCount = await _service.UploadImagesAsync(testImages); -// -// Console.WriteLine($" Uploaded: {successCount}/{testImages.Length} images"); -// -// if (successCount == testImages.Length) -// { -// Console.WriteLine("✅ Batch upload successful"); -// } -// else -// { -// Console.WriteLine($"⚠️ Partial success: {successCount}/{testImages.Length}"); -// } -// -// // Cleanup -// foreach (var image in testImages) -// { -// if (File.Exists(image)) -// { -// File.Delete(image); -// } -// } -// } -// -// // Unit Test: Album Creation -// public async Task TestAlbumCreation() -// { -// Console.WriteLine("\nTesting Album Creation..."); -// -// // Authenticate first -// await _service.AuthenticateAsync(); -// -// var albumName = $"Test Album {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; -// var albumId = await _service.CreateAlbumAsync(albumName); -// -// if (albumId != null) -// { -// Console.WriteLine($"✅ Album created successfully"); -// Console.WriteLine($" Album ID: {albumId}"); -// } -// else -// { -// Console.WriteLine("❌ Album creation failed"); -// } -// } -// -// // Unit Test: Upload to Specific Album -// public async Task TestUploadToAlbum() -// { -// Console.WriteLine("\nTesting Upload to Specific Album..."); -// -// // Authenticate first -// await _service.AuthenticateAsync(); -// -// // Create test album -// var albumName = $"Test Album {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; -// var albumId = await _service.CreateAlbumAsync(albumName); -// -// if (albumId == null) -// { -// Console.WriteLine("❌ Failed to create test album"); -// return; -// } -// -// // Upload image to album -// var testImagePath = CreateTestImage(); -// var result = await _service.UploadImageAsync(testImagePath, albumId); -// -// if (result) -// { -// Console.WriteLine($"✅ Image uploaded to album successfully"); -// Console.WriteLine($" Album ID: {albumId}"); -// } -// else -// { -// Console.WriteLine("❌ Upload to album failed"); -// } -// -// // Cleanup -// if (File.Exists(testImagePath)) -// { -// File.Delete(testImagePath); -// } -// } -// -// // Unit Test: Logout -// public async Task TestLogout() -// { -// Console.WriteLine("\nTesting Logout..."); -// -// // Authenticate first -// await _service.AuthenticateAsync(); -// Console.WriteLine($" Before logout - IsAuthenticated: {_service.IsAuthenticated}"); -// -// // Logout -// await _service.LogoutAsync(); -// Console.WriteLine($" After logout - IsAuthenticated: {_service.IsAuthenticated}"); -// -// if (!_service.IsAuthenticated) -// { -// Console.WriteLine("✅ Logout successful"); -// } -// else -// { -// Console.WriteLine("❌ Logout failed"); -// } -// } -// -// // Helper: Create a test image -// private string CreateTestImage(string fileName = "test_image.jpg") -// { -// var tempPath = Path.Combine(Path.GetTempPath(), fileName); -// -// // Create a simple 1x1 pixel JPEG (base64 encoded minimal JPEG) -// var minimalJpeg = Convert.FromBase64String( -// "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCwAA8A/9k=" -// ); -// -// File.WriteAllBytes(tempPath, minimalJpeg); -// return tempPath; -// } -// -// // Run all tests -// public static async Task RunAllTests() -// { -// Console.WriteLine("=== Lychee Upload Service Tests ===\n"); -// -// var tests = new LycheeUploadServiceTests(); -// -// try -// { -// await tests.TestAuthentication(); -// await tests.TestSingleImageUpload(); -// await tests.TestBatchUpload(); -// await tests.TestAlbumCreation(); -// await tests.TestUploadToAlbum(); -// await tests.TestLogout(); -// -// Console.WriteLine("\n=== All tests completed ==="); -// } -// catch (Exception ex) -// { -// Console.WriteLine($"\n❌ Test error: {ex.Message}"); -// } -// finally -// { -// tests._service.Dispose(); -// } -// } -// } -// -// // Example usage in Program.cs or test runner: -// // await LycheeUploadServiceTests.RunAllTests(); diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeDisplayWindow.xaml b/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeDisplayWindow.xaml deleted file mode 100644 index c93b05d..0000000 --- a/src/CamBooth/CamBooth.App/Features/LycheeUpload/QRCodeDisplayWindow.xaml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - -