# Feature Plan: EXIF-Daten Extraktion & Capture-Date Sortierung

**Status**: In Planung  
**Branch**: `feature/ExifExtraction`  
**Erstellt**: 09. November 2025

## Ziel

Automatische Extraktion von EXIF-Metadaten (insbesondere Aufnahmedatum) aus hochgeladenen Bildern und Nutzung dieser Informationen für eine präzisere chronologische Sortierung der Slideshow-Gruppen.

## Motivation

### Aktuelles Verhalten
- Slideshow-Gruppen werden nach **Jahr** (Benutzereingabe) und **Upload-Datum** sortiert
- Aufnahmedatum der Bilder wird nicht berücksichtigt
- Bei Bildern aus verschiedenen Jahren in einer Gruppe: Unklare Sortierung

### Gewünschtes Verhalten
- Automatische Extraktion des **Aufnahmedatums** (EXIF `DateTimeOriginal`) beim Upload
- Nutzung des **ältesten Aufnahmedatums** einer Gruppe als primäres Sortier-Kriterium
- Fallback auf Jahr/Upload-Datum wenn EXIF-Daten fehlen

## Use Cases

### Use Case 1: Alte Fotos digitalisieren
**Szenario**: Benutzer scannt alte Fotos von 1995 und lädt sie 2025 hoch
- **Problem**: Gruppe würde aktuell in 2025 einsortiert
- **Lösung**: EXIF-Datum (falls eingescannt mit Datum) oder Jahr-Eingabe nutzen

### Use Case 2: Gemischte Jahre in einer Gruppe
**Szenario**: Benutzer lädt Bilder von einem Urlaub hoch, der über Jahreswechsel ging
- 5 Bilder vom 28.12.2023
- 8 Bilder vom 02.01.2024
- **Lösung**: Gruppe wird nach ältestem Aufnahmedatum sortiert (28.12.2023)

### Use Case 3: Bilder ohne EXIF
**Szenario**: Screenshots, bearbeitete Bilder, oder alte digitalisierte Fotos ohne Metadaten
- **Lösung**: Fallback auf Jahr-Eingabe (wie bisher)

## Technische Lösung

### A. EXIF-Extraktion (Backend)

#### 1. Dependency
**Library**: `exif-parser` oder `exifr` (npm)
```bash
npm install exifr --save
```

**Warum `exifr`?**
- ✅ Modern, aktiv maintained
- ✅ Unterstützt viele Formate (JPEG, TIFF, HEIC, etc.)
- ✅ Promise-based API
- ✅ Klein und performant
- ✅ Keine Native Dependencies (Pure JavaScript)

#### 2. Datenbank-Schema Migration

**Neue Felder in `images` Tabelle**:
```sql
ALTER TABLE images ADD COLUMN exif_date_taken DATETIME DEFAULT NULL;
ALTER TABLE images ADD COLUMN exif_camera_model TEXT DEFAULT NULL;
ALTER TABLE images ADD COLUMN exif_location_lat REAL DEFAULT NULL;
ALTER TABLE images ADD COLUMN exif_location_lon REAL DEFAULT NULL;
```

**Neues berechnetes Feld in `groups` Tabelle**:
```sql
ALTER TABLE groups ADD COLUMN capture_date DATETIME DEFAULT NULL;
-- Wird beim Upload berechnet: MIN(exif_date_taken) aller Bilder der Gruppe
```

**Index für Performance**:
```sql
CREATE INDEX IF NOT EXISTS idx_groups_capture_date ON groups(capture_date);
CREATE INDEX IF NOT EXISTS idx_images_exif_date_taken ON images(exif_date_taken);
```

#### 3. EXIF Service (Backend)

**Datei**: `backend/src/services/ExifService.js`

```javascript
const exifr = require('exifr');
const fs = require('fs').promises;

class ExifService {
    /**
     * Extrahiere EXIF-Daten aus einem Bild
     * @param {string} filePath - Absoluter Pfad zum Bild
     * @returns {Promise<Object>} EXIF-Daten oder null
     */
    async extractExifData(filePath) {
        try {
            const exifData = await exifr.parse(filePath, {
                pick: [
                    'DateTimeOriginal',    // Aufnahmedatum
                    'CreateDate',          // Fallback
                    'Make',                // Kamera-Hersteller
                    'Model',               // Kamera-Modell
                    'latitude',            // GPS-Koordinaten
                    'longitude'
                ]
            });

            if (!exifData) return null;

            return {
                dateTaken: exifData.DateTimeOriginal || exifData.CreateDate || null,
                cameraModel: exifData.Model ? `${exifData.Make || ''} ${exifData.Model}`.trim() : null,
                location: (exifData.latitude && exifData.longitude) 
                    ? { lat: exifData.latitude, lon: exifData.longitude }
                    : null
            };
        } catch (error) {
            console.warn(`[ExifService] Failed to extract EXIF from ${filePath}:`, error.message);
            return null;
        }
    }

    /**
     * Finde das älteste Aufnahmedatum in einer Gruppe
     * @param {Array} images - Array von Bild-Objekten mit exif_date_taken
     * @returns {Date|null} Ältestes Datum oder null
     */
    getEarliestCaptureDate(images) {
        const dates = images
            .map(img => img.exif_date_taken)
            .filter(date => date !== null)
            .map(date => new Date(date));

        if (dates.length === 0) return null;

        return new Date(Math.min(...dates));
    }
}

module.exports = new ExifService();
```

#### 4. Upload-Route Integration

**Datei**: `backend/src/routes/upload.js`

```javascript
const ExifService = require('../services/ExifService');

// Nach dem Speichern des Bildes:
router.post('/upload', async (req, res) => {
    // ... existing upload logic ...
    
    // EXIF-Extraktion für jedes Bild
    for (const file of uploadedFiles) {
        const filePath = path.join(UPLOAD_DIR, file.filename);
        const exifData = await ExifService.extractExifData(filePath);
        
        // In Datenbank speichern
        await db.run(`
            UPDATE images 
            SET exif_date_taken = ?,
                exif_camera_model = ?,
                exif_location_lat = ?,
                exif_location_lon = ?
            WHERE file_name = ?
        `, [
            exifData?.dateTaken || null,
            exifData?.cameraModel || null,
            exifData?.location?.lat || null,
            exifData?.location?.lon || null,
            file.filename
        ]);
    }
    
    // Berechne capture_date für Gruppe
    const images = await db.all('SELECT exif_date_taken FROM images WHERE group_id = ?', [groupId]);
    const captureDate = ExifService.getEarliestCaptureDate(images);
    
    await db.run(`
        UPDATE groups 
        SET capture_date = ?
        WHERE group_id = ?
    `, [captureDate?.toISOString() || null, groupId]);
    
    // ... rest of upload logic ...
});
```

### B. Frontend-Änderungen

#### 1. Slideshow-Sortierung

**Datei**: `frontend/src/Components/Pages/SlideshowPage.js`

**Neue Sortier-Logik**:
```javascript
const sortedGroups = [...groupsData.groups].sort((a, b) => {
    // 1. Priorität: capture_date (EXIF-basiert)
    if (a.captureDate && b.captureDate) {
        return new Date(a.captureDate) - new Date(b.captureDate);
    }
    
    // 2. Priorität: Wenn nur eine Gruppe EXIF hat, diese zuerst
    if (a.captureDate && !b.captureDate) return -1;
    if (!a.captureDate && b.captureDate) return 1;
    
    // 3. Fallback: Jahr (Benutzereingabe)
    if (a.year !== b.year) {
        return a.year - b.year;
    }
    
    // 4. Fallback: Upload-Datum
    return new Date(a.uploadDate) - new Date(b.uploadDate);
});
```

#### 2. Metadaten-Anzeige (Optional)

**Datei**: `frontend/src/Components/Pages/SlideshowPage.js`

**Erweiterte Info-Box**:
```jsx
<Typography sx={metaTextSx}>
  {currentGroup.captureDate && (
    <>Aufnahme: {new Date(currentGroup.captureDate).toLocaleDateString('de-DE')} • </>
  )}
  Bild {currentImageIndex + 1} von {currentGroup.images.length} • 
  Slideshow {currentGroupIndex + 1} von {allGroups.length}
</Typography>
```

### C. Batch-Migration (Bestehende Bilder)

**Datei**: `backend/src/scripts/migrate-exif.js`

```javascript
/**
 * Einmaliges Skript zum Extrahieren von EXIF-Daten aus bestehenden Bildern
 */
const ExifService = require('../services/ExifService');
const db = require('../database/DatabaseManager');
const path = require('path');

async function migrateExistingImages() {
    console.log('[EXIF Migration] Starting...');
    
    const images = await db.all('SELECT id, file_name, group_id FROM images WHERE exif_date_taken IS NULL');
    console.log(`[EXIF Migration] Found ${images.length} images without EXIF data`);
    
    let successCount = 0;
    let failCount = 0;
    
    for (const image of images) {
        const filePath = path.join(__dirname, '../data/images', image.file_name);
        const exifData = await ExifService.extractExifData(filePath);
        
        if (exifData && exifData.dateTaken) {
            await db.run(`
                UPDATE images 
                SET exif_date_taken = ?,
                    exif_camera_model = ?,
                    exif_location_lat = ?,
                    exif_location_lon = ?
                WHERE id = ?
            `, [
                exifData.dateTaken,
                exifData.cameraModel,
                exifData.location?.lat,
                exifData.location?.lon,
                image.id
            ]);
            successCount++;
        } else {
            failCount++;
        }
    }
    
    // Update capture_date für alle Gruppen
    const groups = await db.all('SELECT group_id FROM groups WHERE capture_date IS NULL');
    for (const group of groups) {
        const groupImages = await db.all('SELECT exif_date_taken FROM images WHERE group_id = ?', [group.group_id]);
        const captureDate = ExifService.getEarliestCaptureDate(groupImages);
        
        if (captureDate) {
            await db.run('UPDATE groups SET capture_date = ? WHERE group_id = ?', [
                captureDate.toISOString(),
                group.group_id
            ]);
        }
    }
    
    console.log(`[EXIF Migration] Complete! Success: ${successCount}, Failed: ${failCount}`);
}

// Run migration
migrateExistingImages().catch(console.error);
```

## Implementierungs-Plan

### Phase 1: Backend - EXIF Extraktion (3-4 Stunden)

1. **Dependencies installieren** (10 min)
   - [ ] `npm install exifr` im Backend
   - [ ] Package-Lock aktualisieren

2. **Datenbank-Migration** (30 min)
   - [ ] Migration-Script erstellen (`migration-005-exif.sql`)
   - [ ] Neue Felder in `images` Tabelle
   - [ ] Neues Feld `capture_date` in `groups` Tabelle
   - [ ] Indizes erstellen
   - [ ] Migration testen

3. **ExifService implementieren** (60 min)
   - [ ] `services/ExifService.js` erstellen
   - [ ] `extractExifData()` Methode
   - [ ] `getEarliestCaptureDate()` Methode
   - [ ] Error-Handling
   - [ ] Unit-Tests (optional)

4. **Upload-Route erweitern** (60 min)
   - [ ] EXIF-Extraktion in Upload-Flow integrieren
   - [ ] Datenbank-Updates
   - [ ] `capture_date` Berechnung
   - [ ] Testing mit verschiedenen Bildtypen

5. **Migration-Script** (30 min)
   - [ ] `scripts/migrate-exif.js` erstellen
   - [ ] Batch-Processing für bestehende Bilder
   - [ ] Logging und Progress-Anzeige
   - [ ] Test mit Development-Daten

### Phase 2: Frontend - Sortierung & Anzeige (1-2 Stunden)

1. **Slideshow-Sortierung** (30 min)
   - [ ] `SlideshowPage.js` anpassen
   - [ ] Neue Sortier-Logik mit `captureDate`
   - [ ] Fallback-Logik testen

2. **Metadaten-Anzeige** (30 min, Optional)
   - [ ] Aufnahmedatum in Info-Box anzeigen
   - [ ] Kamera-Modell anzeigen (optional)
   - [ ] Responsive Design

3. **Groups-Overview** (30 min, Optional)
   - [ ] EXIF-Daten in Gruppen-Übersicht anzeigen
   - [ ] Filter nach Kamera-Modell (optional)

### Phase 3: Testing & Dokumentation (1 Stunde)

1. **Testing** (30 min)
   - [ ] Upload mit EXIF-Daten
   - [ ] Upload ohne EXIF-Daten (Screenshots)
   - [ ] Sortierung mit gemischten Gruppen
   - [ ] Migration-Script auf bestehenden Daten
   - [ ] Performance-Test (viele Bilder)

2. **Dokumentation** (30 min)
   - [ ] README.md aktualisieren
   - [ ] CHANGELOG.md Entry
   - [ ] API-Dokumentation (falls vorhanden)
   - [ ] Migration-Anleitung

## Technische Details

### EXIF-Datenformate

**EXIF DateTimeOriginal**:
- Format: `"YYYY:MM:DD HH:MM:SS"` (z.B. `"2023:12:28 15:30:45"`)
- Konvertierung zu ISO-8601: `new Date("2023-12-28T15:30:45").toISOString()`

**GPS-Koordinaten**:
- `latitude`: -90 bis +90 (Süd zu Nord)
- `longitude`: -180 bis +180 (West zu Ost)

### Performance-Überlegungen

**EXIF-Extraktion ist I/O-intensiv**:
- Pro Bild: ~10-50ms (je nach Größe)
- 10 Bilder: ~100-500ms zusätzlich beim Upload
- **Mitigation**: Async/Parallel processing mit `Promise.all()`

**Speicherverbrauch**:
- EXIF-Daten: ~100-500 bytes pro Bild in DB
- Vernachlässigbar bei 1000+ Bildern

## Erwartete Verbesserungen

### Sortierung
- ✅ Präzise chronologische Sortierung nach tatsächlichem Aufnahmedatum
- ✅ Automatisch, keine manuelle Eingabe nötig
- ✅ Funktioniert auch bei Bildern aus verschiedenen Jahren in einer Gruppe

### User Experience
- ✅ Mehr Kontext durch Anzeige von Kamera-Modell
- ✅ Potenziell: Geo-Tagging für Location-basierte Features (Zukunft)
- ✅ Bessere Archivierung alter Fotos

### Daten-Qualität
- ✅ Strukturierte Metadaten in Datenbank
- ✅ Basis für zukünftige Features (z.B. Kamera-Filter, Geo-Map)

## Risiken & Mitigationen

### Risiko 1: Bilder ohne EXIF
**Problem**: Screenshots, bearbeitete Bilder, oder alte Scans haben keine EXIF-Daten  
**Mitigation**: Fallback auf Jahr-Eingabe und Upload-Datum (wie bisher)

### Risiko 2: Falsche EXIF-Daten
**Problem**: Kamera-Uhrzeit falsch eingestellt  
**Mitigation**: User-Eingabe (Jahr) hat Vorrang, EXIF nur als Ergänzung

### Risiko 3: Performance beim Upload
**Problem**: EXIF-Extraktion verlangsamt Upload  
**Mitigation**: 
- Parallel-Processing mit `Promise.all()`
- Timeout (max 100ms pro Bild)
- Falls Timeout: Upload trotzdem erfolgreich, EXIF später nachholen

### Risiko 4: HEIC/HEIF Support
**Problem**: Apple-Formate nicht von allen Libraries unterstützt  
**Mitigation**: `exifr` unterstützt HEIC nativ

## Alternativen

### Alternative 1: Nur Jahr-Feld nutzen (Status Quo)
**Nachteil**: Ungenau bei gemischten Jahren, manuelle Eingabe erforderlich

### Alternative 2: Server-Side Image Processing (Sharp + EXIF)
```javascript
const sharp = require('sharp');
const metadata = await sharp(filePath).metadata();
```
**Nachteil**: `sharp` hat native dependencies (schwierigeres Deployment)

### Alternative 3: Frontend EXIF-Extraktion
**Nachteil**: Mehr Client-Traffic, nicht bei allen Browsern zuverlässig

## Erfolgs-Kriterien

✅ **Must-Have**:
1. EXIF-Daten werden beim Upload automatisch extrahiert
2. `capture_date` wird korrekt berechnet (ältestes Bild der Gruppe)
3. Slideshow sortiert nach `capture_date` (mit Fallback)
4. Migration-Script funktioniert für bestehende Bilder
5. Upload-Performance bleibt akzeptabel (<500ms zusätzlich für 10 Bilder)

✅ **Nice-to-Have**:
1. Anzeige von Aufnahmedatum in Slideshow
2. Anzeige von Kamera-Modell
3. GPS-Koordinaten gespeichert (für zukünftige Features)

## Offene Fragen

- [ ] Soll Aufnahmedatum editierbar sein (falls EXIF fehlt oder falsch)?
- [ ] Soll Kamera-Modell in der Gruppen-Übersicht angezeigt werden?
- [ ] Sollen GPS-Koordinaten für Geo-Tagging genutzt werden (Map-View)?
- [ ] Soll EXIF-Extraktion synchron (beim Upload) oder asynchron (Background-Job) erfolgen?

## Rollout-Plan

1. **Development** (feature/ExifExtraction Branch)
   - Implementierung & Unit-Tests
   - Migration-Script testen
   - Code Review

2. **Staging/Testing**
   - Migration auf Dev-Environment
   - Test-Uploads mit verschiedenen Bildtypen
   - Performance-Messungen

3. **Production**
   1. Datenbank-Backup
   2. Migration-Script ausführen
   3. Deployment via Docker Compose
   4. Monitoring für 24h

---

**Erstellt von**: GitHub Copilot  
**Review durch**: @lotzm
