From 9b39de7b76858a691d4b0ca8074e00e2d7e91410 Mon Sep 17 00:00:00 2001 From: iTob Date: Sat, 28 Feb 2026 23:07:54 +0100 Subject: [PATCH] Upload zu lychee --- src/CamBooth/CAMERA_FOCUS_AND_FLASH_FIX.md | 104 ++++ src/CamBooth/CamBooth.App/App.xaml.cs | 4 +- .../Core/AppSettings/AppSettingsService.cs | 13 +- .../Core/AppSettings/app.settings.dev.json | 7 + .../Core/AppSettings/app.settings.json | 7 + .../CamBooth.App/Core/Logging/Logger.cs | 60 +- .../Features/Camera/CameraService.cs | 126 +++- .../Features/LiveView/LiveViewPage.xaml.cs | 25 +- .../Features/LycheeUpload/ARCHITECTURE.md | 236 ++++++++ .../Features/LycheeUpload/CHANGELOG.md | 101 ++++ .../Features/LycheeUpload/INTEGRATION.md | 153 +++++ .../LycheeUpload/LycheeAuthStrategy.cs | 198 +++++++ .../LycheeUpload/LycheeUploadPage.xaml | 193 +++++++ .../LycheeUpload/LycheeUploadPage.xaml.cs | 249 ++++++++ .../LycheeUpload/LycheeUploadService.cs | 540 ++++++++++++++++++ .../LycheeUpload/LycheeUploadServiceTests.cs | 241 ++++++++ .../Features/LycheeUpload/README.md | 196 +++++++ .../LycheeUpload/SESSION_419_DEBUG.md | 265 +++++++++ .../Features/LycheeUpload/TROUBLESHOOTING.md | 262 +++++++++ .../LycheeUpload/V2_API_SESSION_DEBUG.md | 204 +++++++ src/CamBooth/CamBooth.App/MainWindow.xaml.cs | 42 +- src/CamBooth/CamBooth.App/ToDos.txt | 3 +- .../EDSDKLib/artifacts/out/Debug/EDSDKLib.dll | Bin 0 -> 117248 bytes .../EDSDKLib/artifacts/out/Debug/EDSDKLib.pdb | Bin 0 -> 247296 bytes 24 files changed, 3192 insertions(+), 37 deletions(-) create mode 100644 src/CamBooth/CAMERA_FOCUS_AND_FLASH_FIX.md create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/ARCHITECTURE.md create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/CHANGELOG.md create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/INTEGRATION.md create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeAuthStrategy.cs create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml.cs create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadService.cs create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadServiceTests.cs create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/README.md create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/SESSION_419_DEBUG.md create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/TROUBLESHOOTING.md create mode 100644 src/CamBooth/CamBooth.App/Features/LycheeUpload/V2_API_SESSION_DEBUG.md create mode 100644 src/CamBooth/EDSDKLib/artifacts/out/Debug/EDSDKLib.dll create mode 100644 src/CamBooth/EDSDKLib/artifacts/out/Debug/EDSDKLib.pdb diff --git a/src/CamBooth/CAMERA_FOCUS_AND_FLASH_FIX.md b/src/CamBooth/CAMERA_FOCUS_AND_FLASH_FIX.md new file mode 100644 index 0000000..07d1f5e --- /dev/null +++ b/src/CamBooth/CAMERA_FOCUS_AND_FLASH_FIX.md @@ -0,0 +1,104 @@ +# Kamera Fokus und Blitz Behebung + +## Problem +Die reale Kamera konnte in dunklen Bedingungen nicht fokussieren und der Blitz wurde nicht verwendet. + +## Ursache +Die CameraService Klasse hatte keine Konfiguration für: +1. **Autofokus-Modus (AFMode)** - War nicht auf eine geeignete Einstellung für Live-Fokussierung konfiguriert +2. **Blitz-Einstellungen** - Waren nicht aktiviert oder konfiguriert + +## Lösung + +### 1. Autofokus-Modus Konfiguration +Der Autofokus-Modus wurde in der neuen Methode `ConfigureCameraForFocusAndFlash()` auf **AIFocus** gesetzt: + +```csharp +AFMode.AIFocus // Kamera fokussiert kontinuierlich, passt sich an Bewegungen an +``` + +**Verfügbare Modi:** +- `OneShot (0)` - Fokus wird einmal eingestellt, dann gesperrt +- `AIServo (1)` - Kontinuierlicher Autofokus bei bewegten Objekten +- `AIFocus (2)` - Intelligent: Wechselt zwischen OneShot und AIServo ⭐ EMPFOHLEN +- `Manual (3)` - Manueller Fokus + +**AIFocus ist die beste Wahl**, da es: +- Automatisch zwischen OneShot und AIServo wechselt +- In dunklen Bedingungen besser funktioniert +- Flexibler ist + +### 2. Blitz-Konfiguration +Der Blitz wird durch zwei Einstellungen aktiviert: + +#### a) Flash Compensation (Blitzstärke) +```csharp +this._mainCamera.SetSetting(PropertyID.FlashCompensation, 0); +``` +- Werte: -3.0 bis +3.0 +- 0 = Standard Blitzstärke + +#### b) Red-Eye Reduction (Rote-Augen-Reduktion) +```csharp +this._mainCamera.SetSetting(PropertyID.RedEye, (int)RedEye.On); +``` +- Dies aktiviert auch den Blitz in dunklen Bedingungen +- Sendet Pre-Flash vor Hauptblitz aus + +### 3. Verbesserte Autofokus-Feedback +Die `PrepareFocusAsync()` Methode wurde erweitert um: +- Besseres Logging der Fokus-Ergebnisse +- Fehlermeldungen, wenn Fokus fehlschlägt +- Hinweise auf Lichtverhältnisse und Blitz + +## Implementation + +Die Konfiguration erfolgt automatisch wenn: +1. Kamera verbunden wird +2. Session geöffnet wird +3. Speicherort auf Computer eingestellt wird + +### Code in CameraService.cs: +```csharp +private void SetSettingSaveToComputer() +{ + this._mainCamera.SetSetting(PropertyID.SaveTo, (int)SaveTo.Host); + this._mainCamera.SetCapacity(4096, int.MaxValue); + ConfigureCameraForFocusAndFlash(); // ← NEUE ZEILE +} + +private void ConfigureCameraForFocusAndFlash() +{ + // Autofocus auf AIFocus setzen + this._mainCamera.SetSetting(PropertyID.AFMode, (int)AFMode.AIFocus); + + // Blitzstärke konfigurieren + this._mainCamera.SetSetting(PropertyID.FlashCompensation, 0); + + // Rote-Augen-Reduktion aktivieren (aktiviert auch Blitz) + this._mainCamera.SetSetting(PropertyID.RedEye, (int)RedEye.On); +} +``` + +## Canon SDK PropertyIDs verwendet +- **PropertyID.AFMode** (0x00000404) - Autofokus-Modus +- **PropertyID.FlashCompensation** (0x00000408) - Blitzkompensation +- **PropertyID.RedEye** (0x00000413) - Rote-Augen-Reduktion + +## Enums verwendet +- **AFMode** - Autofokus Modi +- **RedEye** - Rote-Augen-Reduktion (Off/On) + +## Testen +Nach dieser Änderung sollte die Kamera: +1. ✅ Autofokus in dunklen Bedingungen durchführen +2. ✅ Blitz automatisch auslösen wenn nötig +3. ✅ Bessere Fokus-Ergebnisse in Low-Light Situationen bieten + +## Weitere Empfehlungen +Falls die Kamera noch immer nicht fokussiert: +1. Überprüfen Sie die **Lichtverhältnisse** - Blitz ist deaktiviert +2. Überprüfen Sie die **Objektiv-Fokus-Einstellung** (AF/MF Schalter an der Linse) +3. Überprüfen Sie, ob das **Autofokus-System des Objektivs** funktioniert +4. Versuchen Sie ISO-Wert zu erhöhen: `SetSetting(PropertyID.ISO, (int)ISOValue)` +5. Überprüfen Sie die **Kamera-Menüs** auf Fokus-Einschränkungen diff --git a/src/CamBooth/CamBooth.App/App.xaml.cs b/src/CamBooth/CamBooth.App/App.xaml.cs index 831dc4a..2a00589 100644 --- a/src/CamBooth/CamBooth.App/App.xaml.cs +++ b/src/CamBooth/CamBooth.App/App.xaml.cs @@ -1,9 +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.PictureGallery; using EDSDKLib.API.Base; @@ -31,6 +32,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); // Zuerst den Provider bauen, um AppSettings zu laden diff --git a/src/CamBooth/CamBooth.App/Core/AppSettings/AppSettingsService.cs b/src/CamBooth/CamBooth.App/Core/AppSettings/AppSettingsService.cs index b7183cb..2132c31 100644 --- a/src/CamBooth/CamBooth.App/Core/AppSettings/AppSettingsService.cs +++ b/src/CamBooth/CamBooth.App/Core/AppSettings/AppSettingsService.cs @@ -1,4 +1,4 @@ -using CamBooth.App.Core.Logging; +using CamBooth.App.Core.Logging; namespace CamBooth.App.Core.AppSettings; @@ -61,4 +61,15 @@ public class AppSettingsService public string? ConnectionString => configuration.GetConnectionString("DefaultConnection"); public string ConfigFileName => loadedConfigFile; + + // Lychee Upload Settings + public string? LycheeApiUrl => configuration["LycheeSettings:ApiUrl"]; + + public string? LycheeUsername => configuration["LycheeSettings:Username"]; + + public string? LycheePassword => configuration["LycheeSettings:Password"]; + + public string? LycheeDefaultAlbumId => configuration["LycheeSettings:DefaultAlbumId"]; + + public bool LycheeAutoUploadEnabled => bool.Parse(configuration["LycheeSettings: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 e77cb5e..a36be88 100644 --- a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.dev.json +++ b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.dev.json @@ -11,6 +11,13 @@ "IsShutdownEnabled": false, "UseMockCamera": true }, + "LycheeSettings": { + "ApiUrl": "https://cambooth-pics.rblnews.de", + "Username": "itob", + "Password": "VfVyqal&Nv8U&P", + "DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t", + "AutoUploadEnabled": true + }, "ConnectionStrings": { "DefaultConnection": "Server=myServer;Database=myDB;User Id=myUser;Password=myPassword;" } diff --git a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json index 12bf7ad..2027cc1 100644 --- a/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json +++ b/src/CamBooth/CamBooth.App/Core/AppSettings/app.settings.json @@ -11,6 +11,13 @@ "IsShutdownEnabled": true, "UseMockCamera": true }, + "LycheeSettings": { + "ApiUrl": "https://cambooth-pics.rblnews.de", + "Username": "itob", + "Password": "VfVyqal&Nv8U&P", + "DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t", + "AutoUploadEnabled": false + }, "ConnectionStrings": { "DefaultConnection": "Server=myServer;Database=myDB;User Id=myUser;Password=myPassword;" } diff --git a/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs b/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs index 6285f93..9315443 100644 --- a/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs +++ b/src/CamBooth/CamBooth.App/Core/Logging/Logger.cs @@ -1,32 +1,82 @@ -using System.Globalization; +using System.Globalization; +using System.IO; using System.Windows; namespace CamBooth.App.Core.Logging; public class Logger { + private readonly string _logsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); + private readonly string _errorLogPath; + public event LoggingEventHandler? InfoLog; public event LoggingEventHandler? ErrorLog; + public event LoggingEventHandler? WarningLog; + public event LoggingEventHandler? DebugLog; public delegate void LoggingEventHandler(string text); - + + public Logger() + { + // Logs-Ordner erstellen, falls nicht vorhanden + if (!Directory.Exists(_logsDirectory)) + { + Directory.CreateDirectory(_logsDirectory); + } + + _errorLogPath = Path.Combine(_logsDirectory, "error.txt"); + } + + private void WriteToErrorLog(string message) + { + try + { + File.AppendAllText(_errorLogPath, message + Environment.NewLine); + } + catch (Exception ex) + { + Console.WriteLine($"Fehler beim Schreiben in Fehlerlog: {ex.Message}"); + } + } + public void Info(string message) { Application.Current.Dispatcher.Invoke(() => { - message = DateTime.Now.ToString("dd.MM.yyyy HH:MM:ss", CultureInfo.InvariantCulture) + ": " + message; + message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [INFO] " + message; InfoLog?.Invoke(message); Console.WriteLine(message); }); - } + + public void Warning(string message) + { + Application.Current.Dispatcher.Invoke(() => + { + message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [WARNING] " + message; + WarningLog?.Invoke(message); + Console.WriteLine(message); + WriteToErrorLog(message); + }); + } + public void Error(string message) { Application.Current.Dispatcher.Invoke(() => { - message = DateTime.Now.ToString("dd.MM.yyyy HH:MM:ss", CultureInfo.InvariantCulture) + ": " + message; + message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [ERROR] " + message; ErrorLog?.Invoke(message); Console.WriteLine(message); + WriteToErrorLog(message); }); + } + public void Debug(string message) + { + Application.Current.Dispatcher.Invoke(() => + { + message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [DEBUG] " + message; + DebugLog?.Invoke(message); + Console.WriteLine(message); + }); } } \ No newline at end of file diff --git a/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs b/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs index 7d46f80..0cfcf23 100644 --- a/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs +++ b/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs @@ -4,6 +4,7 @@ using System.Windows; using CamBooth.App.Core.AppSettings; using CamBooth.App.Core.Logging; +using CamBooth.App.Features.LycheeUpload; using CamBooth.App.Features.PictureGallery; using EOSDigital.API; @@ -19,6 +20,8 @@ public class CameraService : IDisposable private readonly PictureGalleryService _pictureGalleryService; + private readonly LycheeUploadService _lycheeUploadService; + private readonly ICanonAPI _APIHandler; private CameraValue[] AvList; @@ -42,13 +45,15 @@ public class CameraService : IDisposable public CameraService(Logger logger, AppSettingsService appSettings, - PictureGalleryService pictureGalleryService, + PictureGalleryService pictureGalleryService, + LycheeUploadService lycheeUploadService, ICamera mainCamera, ICanonAPI APIHandler) { this._logger = logger; this._appSettings = appSettings; this._pictureGalleryService = pictureGalleryService; + this._lycheeUploadService = lycheeUploadService; this._mainCamera = mainCamera; this._APIHandler = APIHandler; try @@ -81,17 +86,30 @@ public class CameraService : IDisposable ErrorHandler.SevereErrorHappened += this.ErrorHandler_SevereErrorHappened; ErrorHandler.NonSevereErrorHappened += this.ErrorHandler_NonSevereErrorHappened; - this.RefreshCamera(); - List cameraList = this._APIHandler.GetCameraList(); - if (cameraList.Any()) + try { - this.OpenSession(); - this.SetSettingSaveToComputer(); - this.StarLiveView(); - } + this.RefreshCamera(); + List cameraList = this._APIHandler.GetCameraList(); + if (cameraList.Any()) + { + this.OpenSession(); + this.SetSettingSaveToComputer(); + this.StarLiveView(); + } + else + { + this.ReportError("No cameras / devices found"); + return; + } - string cameraDeviceNames = string.Join(", ", cameraList.Select(cam => cam.DeviceName)); - this._logger.Info(cameraDeviceNames == string.Empty ? "No cameras / devices found" : cameraDeviceNames); + string cameraDeviceNames = string.Join(", ", cameraList.Select(cam => cam.DeviceName)); + this._logger.Info(cameraDeviceNames); + } + catch (Exception ex) + { + this._logger.Error($"Error connecting camera: {ex.Message}"); + throw; + } } @@ -104,7 +122,18 @@ public class CameraService : IDisposable public void CloseSession() { - this._mainCamera.CloseSession(); + try + { + if (this._mainCamera != null && this._mainCamera.SessionOpen) + { + this._mainCamera.CloseSession(); + this._logger.Info("Camera session closed"); + } + } + catch (Exception ex) + { + this._logger.Error($"Error closing camera session: {ex.Message}"); + } // AvCoBox.Items.Clear(); // TvCoBox.Items.Clear(); @@ -130,17 +159,41 @@ public class CameraService : IDisposable private void OpenSession() { - this._mainCamera = this.CamList[0]; - this._mainCamera.OpenSession(); + try + { + if (this.CamList == null || this.CamList.Count == 0) + { + throw new InvalidOperationException("No cameras available in camera list"); + } - //_mainCamera.ProgressChanged += MainCamera_ProgressChanged; - this._mainCamera.StateChanged += this.MainCamera_StateChanged; - this._mainCamera.DownloadReady += this.MainCamera_DownloadReady; + this._mainCamera = this.CamList[0]; + + // Check if session is already open + if (this._mainCamera.SessionOpen) + { + this._logger.Info($"Camera session already open for {this._mainCamera.DeviceName}"); + return; + } - //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); + 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; + } // foreach (var Av in AvList) AvCoBox.Items.Add(Av.StringValue); // foreach (var Tv in TvList) TvCoBox.Items.Add(Tv.StringValue); @@ -316,11 +369,42 @@ public class CameraService : IDisposable { Info.FileName = $"img_{Guid.NewGuid().ToString()}.jpg"; sender.DownloadFile(Info, this._appSettings.PictureLocation); - this._logger.Info("Download complete: " + Path.Combine(this._appSettings.PictureLocation, Info.FileName)); + var savedPhotoPath = Path.Combine(this._appSettings.PictureLocation, Info.FileName); + this._logger.Info("Download complete: " + savedPhotoPath); + Application.Current.Dispatcher.Invoke(() => { this._pictureGalleryService.IncrementNewPhotoCount(); this._pictureGalleryService.LoadThumbnailsToCache(); }); + + // Auto-Upload zu Lychee, falls aktiviert + if (this._appSettings.LycheeAutoUploadEnabled) + { + this._logger.Info("Auto-Upload aktiviert. Starte Upload zu Lychee..."); + + // Upload im Hintergrund, damit die Fotobox nicht blockiert wird + _ = Task.Run(async () => + { + try + { + var albumId = this._appSettings.LycheeDefaultAlbumId; + var uploadSuccess = await this._lycheeUploadService.UploadImageAsync(savedPhotoPath, albumId); + + if (uploadSuccess) + { + this._logger.Info($"✅ Lychee-Upload erfolgreich: {Info.FileName}"); + } + else + { + this._logger.Warning($"⚠️ Lychee-Upload fehlgeschlagen: {Info.FileName}"); + } + } + catch (Exception ex) + { + this._logger.Error($"❌ Fehler beim Lychee-Upload: {ex.Message}"); + } + }); + } } catch (Exception ex) { diff --git a/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs b/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs index 750a49e..5e60f0b 100644 --- a/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs +++ b/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Media; @@ -47,8 +47,27 @@ public partial class LiveViewPage : Page this.LVCanvas.RenderTransform = transformGroup; this.LVCanvas.RenderTransformOrigin = new Point(0.5, 0.5); - cameraService.ConnectCamera(); - cameraService._mainCamera.LiveViewUpdated += this.MainCamera_OnLiveViewUpdated; + try + { + cameraService.ConnectCamera(); + + // Verify that camera session is open before subscribing to events + if (cameraService._mainCamera != null && cameraService._mainCamera.SessionOpen) + { + cameraService._mainCamera.LiveViewUpdated += this.MainCamera_OnLiveViewUpdated; + this._logger.Info("LiveViewPage initialized successfully"); + } + else + { + this._logger.Error("Camera session is not open after connection attempt"); + throw new InvalidOperationException("Camera session failed to open"); + } + } + catch (Exception ex) + { + this._logger.Error($"Failed to initialize LiveViewPage: {ex.Message}"); + throw; + } } diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/ARCHITECTURE.md b/src/CamBooth/CamBooth.App/Features/LycheeUpload/ARCHITECTURE.md new file mode 100644 index 0000000..4bcdcf5 --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/ARCHITECTURE.md @@ -0,0 +1,236 @@ +# 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 new file mode 100644 index 0000000..2c5704c --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/CHANGELOG.md @@ -0,0 +1,101 @@ +# 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 new file mode 100644 index 0000000..ae64083 --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/INTEGRATION.md @@ -0,0 +1,153 @@ +# 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 new file mode 100644 index 0000000..4bed746 --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeAuthStrategy.cs @@ -0,0 +1,198 @@ +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 new file mode 100644 index 0000000..bbe7d6c --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml.cs b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml.cs new file mode 100644 index 0000000..65c5a3d --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadPage.xaml.cs @@ -0,0 +1,249 @@ +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 new file mode 100644 index 0000000..6fe8365 --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadService.cs @@ -0,0 +1,540 @@ +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 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 CookieContainer _cookieContainer; + private readonly HttpClientHandler _httpClientHandler; + private string? _csrfToken; + private bool _isAuthenticated = false; + + public LycheeUploadService(AppSettingsService appSettings, Logger logger) + { + _appSettings = appSettings; + _logger = 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; + + /// + /// 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}"); + } + } + + 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 new file mode 100644 index 0000000..5faa493 --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/LycheeUploadServiceTests.cs @@ -0,0 +1,241 @@ +// 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/README.md b/src/CamBooth/CamBooth.App/Features/LycheeUpload/README.md new file mode 100644 index 0000000..c6860f5 --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/README.md @@ -0,0 +1,196 @@ +# Lychee Upload Feature + +Dieses Feature ermöglicht den automatischen Upload von Fotos zu einer Lychee-Instanz mit Authentifizierung. + +## Features + +- ✅ Authentifizierung bei Lychee via API +- ✅ **CSRF-Token-Handling** für Laravel/Lychee +- ✅ **Cookie-basiertes Session-Management** +- ✅ Upload einzelner Bilder +- ✅ Batch-Upload mehrerer Bilder +- ✅ Erstellung neuer Alben +- ✅ Zuordnung zu spezifischen Alben +- ✅ Automatisches Re-Authentifizieren bei Session-Ablauf +- ✅ Ausführliches Logging aller Operationen + +## Technische Details + +### CSRF-Schutz (HTTP 419 Prevention) + +Lychee basiert auf Laravel und verwendet CSRF-Token-Schutz für alle POST-Requests. Der Service: + +1. **Holt automatisch den CSRF-Token** vor der Authentifizierung +2. **Versucht mehrere Methoden:** + - Extrahiert Token aus `XSRF-TOKEN` Cookie + - Falls nicht vorhanden: Parst HTML-Meta-Tag `csrf-token` +3. **Fügt CSRF-Token zu allen POST-Requests hinzu:** + - `X-CSRF-TOKEN` Header + - `X-XSRF-TOKEN` Header + +### Content-Type + +Lychee erwartet für die meisten API-Calls: +- **Login & Album-Erstellung:** `application/x-www-form-urlencoded` (HTML Form) +- **Datei-Upload:** `multipart/form-data` +- **Responses:** `application/json` + +⚠️ **Wichtig:** Obwohl die Lychee API als "JSON API" bezeichnet wird, verwendet sie für POST-Requests tatsächlich Form-Data, nicht JSON! + +### Session-Management + +- Verwendet `CookieContainer` für automatisches Cookie-Handling +- Speichert Session-Cookies (`lychee_session`) +- CSRF-Token wird mit Session verknüpft + +## Konfiguration + +Die Lychee-Einstellungen werden in der `app.settings.json` konfiguriert: + +```json +"LycheeSettings": { + "ApiUrl": "https://your-lychee-instance.com", + "Username": "admin", + "Password": "your-password", + "DefaultAlbumId": "", + "AutoUploadEnabled": false +} +``` + +### Konfigurationsparameter + +- **ApiUrl**: Die URL deiner Lychee-Instanz (ohne trailing slash) +- **Username**: Benutzername für die Lychee-Authentifizierung +- **Password**: Passwort für die Lychee-Authentifizierung +- **DefaultAlbumId**: Optional - Standard-Album-ID für Uploads +- **AutoUploadEnabled**: Wenn `true`, werden Fotos automatisch nach der Aufnahme hochgeladen + +## Verwendung + +### Initialisierung + +```csharp +var lycheeService = new LycheeUploadService(appSettingsService, logger); +``` + +### Authentifizierung + +```csharp +bool success = await lycheeService.AuthenticateAsync(); +if (success) +{ + Console.WriteLine("Erfolgreich authentifiziert!"); +} +``` + +### Einzelnes Bild hochladen + +```csharp +string imagePath = @"C:\cambooth\pictures\photo.jpg"; +bool uploadSuccess = await lycheeService.UploadImageAsync(imagePath); +``` + +### Bild zu spezifischem Album hochladen + +```csharp +string imagePath = @"C:\cambooth\pictures\photo.jpg"; +string albumId = "abc123"; +bool uploadSuccess = await lycheeService.UploadImageAsync(imagePath, albumId); +``` + +### Mehrere Bilder hochladen + +```csharp +var imagePaths = new List +{ + @"C:\cambooth\pictures\photo1.jpg", + @"C:\cambooth\pictures\photo2.jpg", + @"C:\cambooth\pictures\photo3.jpg" +}; + +int successCount = await lycheeService.UploadImagesAsync(imagePaths); +Console.WriteLine($"{successCount} Bilder erfolgreich hochgeladen"); +``` + +### Neues Album erstellen + +```csharp +string? albumId = await lycheeService.CreateAlbumAsync("Party 2026"); +if (albumId != null) +{ + Console.WriteLine($"Album erstellt mit ID: {albumId}"); +} +``` + +### Abmelden + +```csharp +await lycheeService.LogoutAsync(); +``` + +## Integration in MainWindow + +Um den automatischen Upload nach der Fotoaufnahme zu aktivieren: + +```csharp +private LycheeUploadService _lycheeService; + +public MainWindow(/* ... */, LycheeUploadService lycheeService) +{ + // ... + _lycheeService = lycheeService; +} + +private async void TimerControlRectangleAnimation_OnTimerEllapsed() +{ + var photoTakenSuccessfully = false; + + try + { + this._cameraService.TakePhoto(); + photoTakenSuccessfully = true; + + // Upload zu Lychee, falls aktiviert + if (_appSettings.LycheeAutoUploadEnabled && photoTakenSuccessfully) + { + var photoPath = /* Pfad zum gerade aufgenommenen Foto */; + _ = Task.Run(async () => + { + await _lycheeService.UploadImageAsync(photoPath, _appSettings.LycheeDefaultAlbumId); + }); + } + } + catch (Exception ex) + { + _logger.Error($"Fehler: {ex.Message}"); + } +} +``` + +## API-Endpunkte + +Der Service verwendet folgende Lychee API-Endpunkte: + +- `POST /api/Session::login` - Authentifizierung +- `POST /api/Photo::add` - Bild hochladen +- `POST /api/Album::add` - Album erstellen +- `POST /api/Session::logout` - Abmelden + +## Fehlerbehandlung + +Alle Methoden loggen Fehler ausführlich über den Logger-Service. Im Fehlerfall geben die Methoden `false` oder `null` zurück. + +## Hinweise + +- Der Service verwendet Cookie-basierte Authentifizierung +- Automatisches Re-Authentifizieren bei abgelaufener Session +- Zwischen Batch-Uploads wird eine Verzögerung von 500ms eingebaut +- Der Service sollte mit `Dispose()` aufgeräumt werden + +## Sicherheit + +⚠️ **Wichtig**: Speichere sensible Daten (Passwörter) nicht in der `app.settings.json` im Produktivbetrieb. Verwende stattdessen: +- Umgebungsvariablen +- Azure Key Vault +- Windows Credential Manager +- Verschlüsselte Konfigurationsdateien diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/SESSION_419_DEBUG.md b/src/CamBooth/CamBooth.App/Features/LycheeUpload/SESSION_419_DEBUG.md new file mode 100644 index 0000000..dc34332 --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/SESSION_419_DEBUG.md @@ -0,0 +1,265 @@ +# Lychee Session 419 Debugging Guide + +## Problem: Session Expired (HTTP 419) + +Der Fehler tritt auf, wenn Lychee meldet: **"SessionExpiredException - Session expired"** + +### Root Cause Analysis + +Der Fehler tritt auf wenn: +1. ✅ CSRF-Token wird erfolgreich geholt +2. ❌ Aber die Session zwischen CSRF-Fetch und Login abläuft +3. ❌ Oder Cookies werden nicht korrekt zwischen Requests weitergegeben + +### Debug-Prozess + +#### Schritt 1: Cookie-Logging aktiviert + +Die aktuelle Implementation loggt jetzt detailliert: + +``` +[DEBUG] Hole CSRF-Token von: https://cambooth-pics.rblnews.de +[DEBUG] Cookies im Container: 2 +[DEBUG] Cookie: lychee_session = eyJpdiI6... (Domain: .rblnews.de, Path: /) +[DEBUG] Cookie: XSRF-TOKEN = eyJpdiI6... (Domain: .rblnews.de, Path: /) +[DEBUG] ✅ CSRF-Token aus Cookie erhalten: eyJpdiI6... +[DEBUG] Cookies für Login-Request: 2 +[DEBUG] → lychee_session = eyJpdiI6... +[DEBUG] → XSRF-TOKEN = eyJpdiI6... +[DEBUG] Sende Login-Request an: https://cambooth-pics.rblnews.de/api/Session::login +``` + +#### Schritt 2: Überprüfe Cookie-Domains + +**Wichtig:** Cookie-Domains müssen übereinstimmen! + +**Problem-Szenarien:** + +1. **Hauptseite:** `https://cambooth-pics.rblnews.de` + - Cookie Domain: `.rblnews.de` oder `cambooth-pics.rblnews.de` + +2. **API-Endpunkt:** `https://cambooth-pics.rblnews.de/api/Session::login` + - Muss die gleiche Domain verwenden + +**Lösung:** Stelle sicher, dass `LycheeApiUrl` die vollständige Domain enthält: +```json +{ + "LycheeApiUrl": "https://cambooth-pics.rblnews.de" +} +``` + +**NICHT:** +```json +{ + "LycheeApiUrl": "https://cambooth-pics.rblnews.de/api" // ❌ Falsch +} +``` + +### Mögliche Ursachen & Lösungen + +#### Ursache 1: Cookie-Path stimmt nicht + +**Symptom:** +``` +[DEBUG] Cookies im Container: 2 +[DEBUG] Cookies für Login-Request: 0 // ← Keine Cookies! +``` + +**Lösung:** +Cookies werden für einen bestimmten Path gesetzt. Wenn der Cookie-Path `/` ist, sollte er für alle Unterseiten gelten. + +**Check:** +```csharp +foreach (Cookie cookie in cookies) +{ + _logger.Debug($"Cookie Path: {cookie.Path}"); +} +``` + +#### Ursache 2: Cookie-Domain ist zu spezifisch + +**Symptom:** +Cookie ist gesetzt für `cambooth-pics.rblnews.de` aber der Request geht an `www.cambooth-pics.rblnews.de` + +**Lösung:** +Stelle sicher, dass die URL in der Config exakt gleich ist wie die tatsächliche Lychee-URL. + +#### Ursache 3: Session-Timeout zu kurz + +**Symptom:** +Session läuft zwischen CSRF-Fetch und Login ab (sehr unwahrscheinlich, da < 1 Sekunde) + +**Lösung:** +In Lychee `config/session.php`: +```php +'lifetime' => 120, // Minuten +``` + +#### Ursache 4: Lychee verwendet Session-Store (Redis/Memcached) + +**Symptom:** +Session wird zwischen Requests nicht erkannt + +**Lösung:** +Prüfe Lychee `.env`: +```env +SESSION_DRIVER=cookie # Oder file +``` + +**Nicht empfohlen für diesen Use-Case:** +```env +SESSION_DRIVER=redis # Kann Probleme verursachen +``` + +### Test-Commands + +#### Test 1: Manueller Cookie-Test mit curl + +```bash +# 1. Hole Cookies und CSRF-Token +curl -v -c cookies.txt https://cambooth-pics.rblnews.de/ + +# 2. Extrahiere CSRF-Token aus cookies.txt +# (XSRF-TOKEN Wert) + +# 3. Login mit Cookies +curl -v -b cookies.txt \ + -X POST https://cambooth-pics.rblnews.de/api/Session::login \ + -H "X-CSRF-TOKEN: [token]" \ + -H "X-XSRF-TOKEN: [token]" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin&password=yourpassword" +``` + +#### Test 2: Prüfe Lychee Session-Config + +```bash +# In Lychee-Verzeichnis +php artisan tinker +> config('session.driver'); +> config('session.domain'); +> config('session.path'); +> config('session.secure'); +> config('session.http_only'); +> config('session.same_site'); +``` + +### Workaround-Optionen + +#### Option 1: Session-basierte Authentifizierung überspringen + +Einige Lychee-Versionen unterstützen API-Keys: + +```csharp +request.Headers.Add("Authorization", "Bearer YOUR_API_KEY"); +``` + +**Prüfe:** Lychee Einstellungen → API → Generate API Key + +#### Option 2: Längere Delay zwischen Requests + +```csharp +await FetchCsrfTokenAsync(); +await Task.Delay(500); // 500ms Pause +// Login... +``` + +**Hinweis:** Sollte nicht nötig sein, kann aber bei Netzwerk-Latenzen helfen. + +#### Option 3: Cookie-Container Debugging + +Füge temporär hinzu: + +```csharp +// Nach FetchCsrfTokenAsync() +var allCookies = _cookieContainer.GetAllCookies(); +foreach (Cookie cookie in allCookies) +{ + _logger.Debug($"ALL Cookies: {cookie.Name} | Domain: {cookie.Domain} | Path: {cookie.Path} | Expires: {cookie.Expires}"); +} +``` + +### Lychee-spezifische Fixes + +#### Fix 1: Lychee CORS-Einstellungen + +Wenn Lychee hinter einem Proxy steht, prüfe: + +**In `.env`:** +```env +TRUSTED_PROXIES=* +``` + +**In `config/cors.php`:** +```php +'paths' => ['api/*'], +'allowed_methods' => ['*'], +'allowed_origins' => ['*'], +'allowed_headers' => ['*'], +'exposed_headers' => [], +'max_age' => 0, +'supports_credentials' => true, // ← Wichtig für Cookies! +``` + +#### Fix 2: Laravel Session-Cookie Settings + +**In `config/session.php`:** +```php +'same_site' => 'lax', // Nicht 'strict' +'secure' => false, // Bei HTTPS auf true setzen +'http_only' => true, +'domain' => null, // Oder '.rblnews.de' +``` + +### Debugging-Output Interpretation + +#### Erfolgreicher Flow: +``` +[DEBUG] Hole CSRF-Token von: https://cambooth-pics.rblnews.de +[DEBUG] Cookies im Container: 2 +[DEBUG] Cookie: lychee_session = ... (Domain: .rblnews.de, Path: /) +[DEBUG] Cookie: XSRF-TOKEN = ... (Domain: .rblnews.de, Path: /) +[DEBUG] ✅ CSRF-Token aus Cookie erhalten: ... +[DEBUG] Cookies für Login-Request: 2 +[DEBUG] → lychee_session = ... +[DEBUG] → XSRF-TOKEN = ... +[DEBUG] Sende Login-Request an: ... +[DEBUG] Login Response: {"success":true} +[INFO] ✅ Lychee-Authentifizierung erfolgreich! +``` + +#### Fehlerhafter Flow (Keine Cookies): +``` +[DEBUG] Hole CSRF-Token von: https://cambooth-pics.rblnews.de +[DEBUG] Cookies im Container: 0 // ← Problem! +[WARNING] ⚠️ CSRF-Token konnte nicht gefunden werden. +``` + +#### Fehlerhafter Flow (Cookies nicht weitergegeben): +``` +[DEBUG] Cookies im Container: 2 +[DEBUG] ✅ CSRF-Token aus Cookie erhalten: ... +[DEBUG] Cookies für Login-Request: 0 // ← Problem! +[ERROR] ❌ Lychee-Authentifizierung fehlgeschlagen. Status: 419 +``` + +### Next Steps + +1. **Kompiliere die App neu** mit den neuen Debug-Logs +2. **Starte komplett neu** +3. **Mache einen Test-Upload** +4. **Prüfe die Debug-Logs** und identifiziere das genaue Problem +5. **Wende entsprechende Lösung an** (siehe oben) + +### Support-Informationen sammeln + +Bei weiterhin Problemen, sammle: + +``` +1. Debug-Logs (alle [DEBUG] Zeilen) +2. Lychee-Version: Settings → About +3. Session-Driver: php artisan tinker > config('session.driver') +4. Cookie-Einstellungen: config('session.*) +5. URL in app.settings.json +6. Browser-Test: Manuell einloggen und Network-Tab prüfen +``` diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/TROUBLESHOOTING.md b/src/CamBooth/CamBooth.App/Features/LycheeUpload/TROUBLESHOOTING.md new file mode 100644 index 0000000..d897e14 --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/TROUBLESHOOTING.md @@ -0,0 +1,262 @@ +# Lychee Upload - Troubleshooting Guide + +## Häufige Fehler und Lösungen + +### HTTP 419 - Page Expired / CSRF Token Mismatch + +**Symptom:** +``` +Status: 419, ReasonPhrase: 'status code 419' +``` + +**Ursache:** +- Fehlender oder ungültiger CSRF-Token +- Session abgelaufen + +**Lösung:** +- ✅ **Implementiert:** Service holt automatisch CSRF-Token vor Login +- ✅ **Implementiert:** CSRF-Token wird bei jedem Request mitgesendet +- **Manuell:** App neu starten um neue Session zu erstellen + +--- + +### Content Type Unacceptable + +**Symptom:** +```json +{ + "message": "Content type unacceptable. Content type \"html\" required", + "exception": "UnexpectedContentType" +} +``` + +**Ursache:** +Lychee erwartet `application/x-www-form-urlencoded` (HTML Form), nicht `application/json` + +**Lösung:** +- ✅ **BEHOBEN:** Login verwendet jetzt `FormUrlEncodedContent` +- ✅ **BEHOBEN:** Album-Erstellung verwendet jetzt `FormUrlEncodedContent` +- ✅ Upload verwendet korrekt `multipart/form-data` + +**Code-Beispiel:** +```csharp +// FALSCH (JSON) +var content = new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json"); + +// RICHTIG (Form-Data) +var formData = new Dictionary +{ + { "username", username }, + { "password", password } +}; +var content = new FormUrlEncodedContent(formData); +``` + +--- + +### Authentifizierung fehlschlägt + +**Symptom:** +``` +[WARNING] Lychee-Authentifizierung fehlgeschlagen. Auto-Upload wird nicht funktionieren. +``` + +**Mögliche Ursachen:** + +1. **Falsche Credentials** + - Prüfe `LycheeUsername` und `LycheePassword` in `app.settings.json` + - Teste Login manuell im Webinterface + +2. **Falsche URL** + - `LycheeApiUrl` sollte sein: `https://your-domain.com` (ohne `/api`) + - **Nicht:** `https://your-domain.com/api` + +3. **HTTPS-Zertifikat-Probleme** + - Bei selbst-signierten Zertifikaten: Temporär deaktivieren für Tests + ```csharp + _httpClientHandler.ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + ``` + +4. **Lychee-Version** + - Service ist für Lychee v4.x und v5.x getestet + - Bei älteren Versionen können API-Endpunkte abweichen + +--- + +### Upload schlägt fehl + +**Symptom:** +``` +[WARNING] ⚠️ Lychee-Upload fehlgeschlagen: img_xyz.jpg +``` + +**Debugging-Schritte:** + +1. **Logs prüfen:** + ``` + [DEBUG] Error Response: { ... } + ``` + Zeigt den genauen Fehler von Lychee + +2. **Session prüfen:** + - Ist `_isAuthenticated = true`? + - Falls nicht: Automatisches Re-Auth sollte triggern + +3. **Datei prüfen:** + - Existiert die Datei? + - Ist sie lesbar? + - Ist es ein gültiges JPEG? + +4. **Album-ID prüfen:** + - Wenn `DefaultAlbumId` gesetzt: Existiert das Album? + - Teste erst ohne Album-ID + +--- + +## Debug-Modus aktivieren + +Um detaillierte Logs zu sehen: + +1. In `app.settings.json`: + ```json + { + "AppSettings": { + "DebugConsoleVisible": "true" + } + } + ``` + +2. Alle Log-Level werden ausgegeben: + - `[DEBUG]` - Detaillierte Informationen (CSRF-Token, Responses) + - `[INFO]` - Normale Operationen + - `[WARNING]` - Warnungen + - `[ERROR]` - Fehler + +--- + +## API-Endpunkt-Übersicht + +### Lychee v4.x / v5.x + +| Endpunkt | Method | Content-Type | CSRF? | +|----------|--------|--------------|-------| +| `/api/Session::login` | POST | `application/x-www-form-urlencoded` | ✅ Yes | +| `/api/Session::logout` | POST | - | ✅ Yes | +| `/api/Photo::add` | POST | `multipart/form-data` | ✅ Yes | +| `/api/Album::add` | POST | `application/x-www-form-urlencoded` | ✅ Yes | + +### Request-Headers (immer erforderlich) + +``` +X-CSRF-TOKEN: [token] +X-XSRF-TOKEN: [token] +Accept: application/json +X-Requested-With: XMLHttpRequest +``` + +--- + +## Lychee-Konfiguration prüfen + +### 1. API aktiviert? + +Lychee-Einstellungen → API Access → **Aktiviert** + +### 2. Benutzer-Rechte + +Der verwendete Benutzer muss: +- Upload-Rechte haben +- Album-Erstellungsrechte haben (falls verwendet) + +### 3. Server-Limits + +Prüfe `php.ini`: +```ini +upload_max_filesize = 20M +post_max_size = 25M +max_execution_time = 120 +``` + +--- + +## Test-Kommandos + +### Test 1: CSRF-Token holen +```bash +curl -i https://your-lychee.com/ +# Suche nach: XSRF-TOKEN Cookie oder csrf-token Meta-Tag +``` + +### Test 2: Login testen +```bash +curl -X POST https://your-lychee.com/api/Session::login \ + -H "X-CSRF-TOKEN: your-token" \ + -H "X-XSRF-TOKEN: your-token" \ + -H "Accept: application/json" \ + -H "X-Requested-With: XMLHttpRequest" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin&password=yourpassword" +``` + +### Test 3: Session prüfen +```bash +curl -X POST https://your-lychee.com/api/Session::info \ + -H "Cookie: lychee_session=..." \ + -H "Accept: application/json" +``` + +--- + +## Bekannte Probleme + +### Problem: CSRF-Token wird nicht gefunden + +**Lösung:** +- Stelle sicher, dass die Lychee-Hauptseite erreichbar ist +- Prüfe, ob XSRF-TOKEN Cookie gesetzt wird +- Fallback auf HTML-Parsing sollte automatisch erfolgen + +### Problem: Cookies werden nicht gespeichert + +**Lösung:** +- `CookieContainer` wird korrekt initialisiert +- Prüfe `UseCookies = true` in `HttpClientHandler` +- Bei HTTPS: Zertifikat muss gültig sein + +### Problem: Upload zu langsam + +**Lösung:** +- Erhöhe `_httpClient.Timeout` (aktuell: 5 Minuten) +- Prüfe Netzwerkverbindung zur Lychee-Instanz +- Bei vielen Uploads: Batch-Size reduzieren + +--- + +## Support + +Bei weiteren Problemen: + +1. **Logs sammeln:** + - Debug-Konsole aktivieren + - Kompletten Log-Output kopieren + - Besonders `[DEBUG]` und `[ERROR]` Zeilen + +2. **Lychee-Logs prüfen:** + - `storage/logs/laravel.log` auf dem Server + +3. **Browser-DevTools:** + - Manuell im Webinterface anmelden + - Network-Tab beobachten + - Request/Response-Headers vergleichen + +4. **Konfiguration teilen:** + ```json + { + "LycheeSettings": { + "ApiUrl": "...", + "Username": "...", + "Password": "[REDACTED]" + } + } + ``` diff --git a/src/CamBooth/CamBooth.App/Features/LycheeUpload/V2_API_SESSION_DEBUG.md b/src/CamBooth/CamBooth.App/Features/LycheeUpload/V2_API_SESSION_DEBUG.md new file mode 100644 index 0000000..7cb6220 --- /dev/null +++ b/src/CamBooth/CamBooth.App/Features/LycheeUpload/V2_API_SESSION_DEBUG.md @@ -0,0 +1,204 @@ +# Lychee v2 API Session Management + +## Problem: Session Expired (nach erfolgreichem Login) + +Der Fehler tritt auf, wenn: +1. ✅ Login ist erfolgreich +2. ❌ Aber der nächste Request (Upload) bekommt "Session expired" + +## Root Cause Analyse + +Lychee v2 API kann auf zwei Arten authentifizieren: + +### Option A: Cookie-basiert (Session) +``` +1. POST /api/v2/Auth::login +2. Lychee setzt Session-Cookie (z.B. PHPSESSID) +3. Browser/Client speichert Cookie automatisch +4. Nächster Request sendet Cookie automatisch +``` + +**Problem:** Session läuft ab oder wird nicht richtig gespeichert + +### Option B: Token-basiert (API-Token) +``` +1. POST /api/v2/Auth::login +2. Lychee gibt back: {"api_token": "abc123..."} +3. Client speichert Token +4. Nächster Request: Authorization: Bearer abc123 +``` + +**Besser:** Funktioniert auch wenn Session abläuft + +## Debug Output interpretieren + +### Erfolgreich (Variante A - Cookie): +``` +[DEBUG] Login Response: {"success":true,...} +[DEBUG] Cookies nach Login: 1 +[DEBUG] Cookie: PHPSESSID = abc123def456... +[DEBUG] Cookies für Upload: 1 +[DEBUG] Cookie: PHPSESSID = abc123def456... +[INFO] Upload erfolgreich +``` + +### Erfolgreich (Variante B - Token): +``` +[DEBUG] Login Response: {"success":true,"api_token":"xyz789..."} +[DEBUG] ✅ API-Token erhalten: xyz789... +[DEBUG] Authorization Header vorhanden: true +[INFO] Upload erfolgreich +``` + +### Fehlgeschlagen (Session abgelaufen): +``` +[DEBUG] Cookies nach Login: 0 // ← Kein Cookie! +[DEBUG] Cookies für Upload: 0 // ← Immer noch kein Cookie! +[ERROR] Session expired +``` + +### Fehlgeschlagen (Token nicht in Response): +``` +[DEBUG] Login Response: {...keine api_token...} +[DEBUG] Authorization Header vorhanden: false +[ERROR] Session expired +``` + +## Lösungsansätze + +### Lösung 1: API-Token verwenden (empfohlen) + +Der Code prüft jetzt automatisch auf `api_token` in der Login-Response: + +```csharp +// Wenn vorhanden: +_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {tokenValue}"); +``` + +**Vorteil:** +- ✅ Token verfällt nicht zwischen Requests +- ✅ Kein Cookie-Handling nötig +- ✅ Funktioniert über verschiedene Netzwerke hinweg + +### Lösung 2: Cookie-Session beibehalten + +Wenn Lychee nur Cookies nutzt: + +```csharp +// CookieContainer sollte Cookies speichern +var cookieContainer = new CookieContainer(); +var handler = new HttpClientHandler { CookieContainer = cookieContainer }; +``` + +**Problem:** Diese Konfiguration ist bereits vorhanden! + +Mögliche Ursachen: +- Cookie-Domain stimmt nicht +- Cookie-Path ist zu restriktiv +- Session-Timeout in Lychee ist sehr kurz + +### Lösung 3: Jeder Upload neu authentifizieren + +Falls Session kurz abläuft: + +```csharp +// Vor jedem Upload neu authentifizieren +await AuthenticateAsync(); +await UploadImageAsync(path); +``` + +## Lychee Server-Konfiguration prüfen + +### In `.env`: +```env +SESSION_DRIVER=cookie # Oder file +SESSION_LIFETIME=1440 # 24 Stunden +SESSION_SECURE=false # Nur für HTTP, true für HTTPS +SESSION_HTTP_ONLY=true +``` + +### In `config/session.php`: +```php +'lifetime' => 1440, // Minuten +'same_site' => 'lax', +'secure' => false, // HTTPS in Production +'http_only' => true, +'path' => '/', +'domain' => null, +``` + +## Expected v2 API Responses + +### Login erfolgreich (mit Token): +```json +{ + "success": true, + "api_token": "eyJ0eXAiOiJKV1QiLCJhbGc...", + "user": { + "id": 1, + "username": "admin", + "email": "admin@example.com" + } +} +``` + +### Login erfolgreich (ohne Token): +```json +{ + "success": true, + "user": { + "id": 1, + "username": "admin" + } +} +``` + +### Upload erfolgreich: +```json +{ + "id": "5c123abc456", + "title": "img_xyz.jpg", + "albums": ["2"], + ... +} +``` + +## Debug-Strategie + +1. **Kompiliere und starte die App neu** +2. **Mache einen Upload-Test** +3. **Suche in den Logs nach:** + - `[DEBUG] Login Response:` → Siehst du `api_token`? + - `[DEBUG] Authorization Header vorhanden:` → true oder false? + - `[DEBUG] Cookies nach Login:` → Wie viele? + - `[DEBUG] Cookies für Upload:` → Sind sie noch vorhanden? + +4. **Je nach Ergebnis:** + - **Wenn api_token vorhanden:** ✅ Code wird ihn verwenden + - **Wenn kein api_token, aber Cookies:** Code nutzt Cookies (hoffentlich funktioniert's) + - **Wenn keine Cookies und kein Token:** ❌ Problem! Session wird nicht weitergegeben + +## Weitere Test-Options + +### curl Test mit API-Token: +```bash +# Login und Token extrahieren +TOKEN=$(curl -s -X POST https://cambooth-pics.rblnews.de/api/v2/Auth::login \ + -H 'Content-Type: application/json' \ + -d '{"username":"itob","password":"VfVyqal&Nv8U&P"}' | jq -r '.api_token') + +# Upload mit Token +curl -X POST https://cambooth-pics.rblnews.de/api/v2/Photos::upload \ + -H "Authorization: Bearer $TOKEN" \ + -F "file=@photo.jpg" +``` + +## Zusammenfassung + +Die aktualisierte Code-Version: +1. ✅ Prüft auf `api_token` in der Login-Response +2. ✅ Setzt Authorization Header falls Token vorhanden +3. ✅ Fallback auf Cookie-basierte Authentifizierung +4. ✅ Detailliertes Debug-Logging für Troubleshooting + +Kompiliere die App neu und teste. Die Debug-Logs werden Dir zeigen, ob API-Token oder Cookies verwendet werden! diff --git a/src/CamBooth/CamBooth.App/MainWindow.xaml.cs b/src/CamBooth/CamBooth.App/MainWindow.xaml.cs index 91f2574..349c29c 100644 --- a/src/CamBooth/CamBooth.App/MainWindow.xaml.cs +++ b/src/CamBooth/CamBooth.App/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +using System.ComponentModel; using System.Diagnostics; using System.Threading.Tasks; using System.Windows; @@ -12,6 +12,7 @@ using CamBooth.App.Core.Logging; using CamBooth.App.Features.Camera; using CamBooth.App.Features.DebugConsole; using CamBooth.App.Features.LiveView; +using CamBooth.App.Features.LycheeUpload; using CamBooth.App.Features.PictureGallery; using Wpf.Ui.Controls; @@ -31,6 +32,8 @@ public partial class MainWindow : Window private readonly CameraService _cameraService; + private readonly LycheeUploadService _lycheeUploadService; + private bool _isDebugConsoleVisible = true; private bool _isPicturePanelVisible = false; @@ -60,12 +63,14 @@ public partial class MainWindow : Window Logger logger, AppSettingsService appSettings, PictureGalleryService pictureGalleryService, - CameraService cameraService) + CameraService cameraService, + LycheeUploadService lycheeUploadService) { this._logger = logger; this._appSettings = appSettings; this._pictureGalleryService = pictureGalleryService; this._cameraService = cameraService; + this._lycheeUploadService = lycheeUploadService; InitializeComponent(); this.SetVisibilityDebugConsole(_appSettings.IsDebugConsoleVisible); this.SetVisibilityPicturePanel(this._isPicturePanelVisible); @@ -85,6 +90,24 @@ public partial class MainWindow : Window this.DebugCloseButton.Visibility = Visibility.Collapsed; this.HideDebugButton.Visibility = this._appSettings.IsDebugConsoleVisible ? Visibility.Visible : Visibility.Collapsed; + // Initialize Lychee upload if auto-upload is enabled + if (appSettings.LycheeAutoUploadEnabled) + { + logger.Info("Lychee Auto-Upload ist aktiviert. Authentifiziere..."); + _ = Task.Run(async () => + { + var authSuccess = await _lycheeUploadService.AuthenticateAsync(); + if (authSuccess) + { + logger.Info("Lychee-Authentifizierung erfolgreich!"); + } + else + { + logger.Warning("Lychee-Authentifizierung fehlgeschlagen. Auto-Upload wird nicht funktionieren."); + } + }); + } + logger.Info($"config file loaded: '{appSettings.ConfigFileName}'"); logger.Info("MainWindow initialized"); } @@ -175,9 +198,18 @@ public partial class MainWindow : Window return; } - this._liveViewPage = new LiveViewPage(this._logger, this._appSettings, this._cameraService); - this.MainFrame.Navigate(this._liveViewPage); - this._isCameraStarted = true; + try + { + this._liveViewPage = new LiveViewPage(this._logger, this._appSettings, this._cameraService); + this.MainFrame.Navigate(this._liveViewPage); + this._isCameraStarted = true; + } + catch (Exception ex) + { + this._logger.Error($"Failed to start live view: {ex.Message}\n{ex.StackTrace}"); + System.Windows.MessageBox.Show($"Failed to initialize camera. Please ensure the camera is properly connected.\n\nError: {ex.Message}", + "Camera Connection Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + } } diff --git a/src/CamBooth/CamBooth.App/ToDos.txt b/src/CamBooth/CamBooth.App/ToDos.txt index d9fafef..7f3eabb 100644 --- a/src/CamBooth/CamBooth.App/ToDos.txt +++ b/src/CamBooth/CamBooth.App/ToDos.txt @@ -7,4 +7,5 @@ - Starbildschirm mit freundlicher Begrüßung, kurzer Erklärung, und viel Spaß wünschen mit der FotoCam - Verschiedene Hinweise anzeigen beim Fotografieren (lächeln, Hasensohren, Zunge raus, Grimasse, usw.) - Bild über QR Code runterladen -- Windows updates deaktivieren \ No newline at end of file +- Windows updates deaktivieren +- logging einbinden (Elastic order ähnliches) \ No newline at end of file diff --git a/src/CamBooth/EDSDKLib/artifacts/out/Debug/EDSDKLib.dll b/src/CamBooth/EDSDKLib/artifacts/out/Debug/EDSDKLib.dll new file mode 100644 index 0000000000000000000000000000000000000000..ffa9c4b1ca24684dc68eeec30d7410ebf47282fc GIT binary patch literal 117248 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 literal 0 HcmV?d00001 diff --git a/src/CamBooth/EDSDKLib/artifacts/out/Debug/EDSDKLib.pdb b/src/CamBooth/EDSDKLib/artifacts/out/Debug/EDSDKLib.pdb new file mode 100644 index 0000000000000000000000000000000000000000..f11a122fe12f83dfa83c4843877c2453ed7817d5 GIT binary patch literal 247296 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 E7mS)_{r~^~ literal 0 HcmV?d00001