docs: Add FEATURE_PLAN for EXIF metadata extraction
Plan for implementing automatic EXIF data extraction from uploaded images: - Extract capture date, camera model, and GPS coordinates - Use earliest capture date for chronological group sorting - Add new database fields: capture_date, exif_date_taken, exif_camera_model - Implement ExifService with exifr library - Create migration script for existing images - Update slideshow sorting logic with EXIF-based chronology - Fallback to year/upload date when EXIF unavailable Estimated effort: 5-7 hours (3 phases) Dependencies: exifr npm package
This commit is contained in:
parent
7be6d9e3e1
commit
3fafb621b0
480
docs/FEATURE_PLAN-exif-extraction.md
Normal file
480
docs/FEATURE_PLAN-exif-extraction.md
Normal file
|
|
@ -0,0 +1,480 @@
|
||||||
|
# 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
|
||||||
Loading…
Reference in New Issue
Block a user