<# Version 1.0 - 11.07.2025 - @nurjns 1. Snapchat Daten-Export: JSON-Datei "json/snap_map_places_history.json" und der Ordner "memories" mit den Snaps werden benötigt. 2. ExifTool wird benötigt: https://exiftool.org/ 3. Ausführung in PowerShell: cd X:\Pfad\zum\Projektordner Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass .\geotag.ps1 Komplette Ordnerstruktur: 📁 Projektordner/ ├── 📁 exiftool_files/ ├── 📁 memories/ │ ├── 2024-04-06_abc123-main.jpg │ └── ... ├── 📁 json/ │ └── snap_map_places_history.json ├── 📄 exiftool.exe ├── 📄 geotag.ps1 #> # Pfade $sourceFolder = 'memories' $targetFolder = 'memories_geotagged' $jsonFile = 'json\snap_map_places_history.json' $exiftool = '.\exiftool.exe' # Ordner erstellen, falls nicht vorhanden if (-not (Test-Path $targetFolder)) { New-Item -ItemType Directory -Path $targetFolder | Out-Null Write-Host "📁 Zielordner wurde erstellt: $targetFolder" } # Funktion für Umlaut-Konvertierung function ConvertUmlautsAndUrlEncode { param([string]$str) # Komma entfernen $str = $str -replace ',', '' # Umlaute ersetzen $str = $str -replace 'ü', 'ue' $str = $str -replace 'Ü', 'Ue' $str = $str -replace 'ä', 'ae' $str = $str -replace 'Ä', 'Ae' $str = $str -replace 'ö', 'oe' $str = $str -replace 'Ö', 'Oe' $str = $str -replace 'ß', 'ss' # Leerzeichen zu %20 (keine weitere URL-Kodierung) return $str -replace ' ', '%20' } # JSON laden mit UTF8-Encoding $jsonRaw = Get-Content $jsonFile -Raw -Encoding utf8 $json = $jsonRaw | ConvertFrom-Json $history = $json.'Snap Map Places History' # Alle JPG- und PNG-Dateien laden (ohne -overlay) $files = Get-ChildItem -Path $sourceFolder | Where-Object { $_.Extension -match '^\.(jpg|png)$' -and $_.BaseName -notmatch '-overlay' } # Startpunkt-Abfrage (Dateiname oder Datum) $startInput = Read-Host '🔁 Falls du ab einer bestimmten Datei oder einem Datum (YYYY-MM-DD) weitermachen willst, gib es ein (oder Enter für Start ab Anfang)' $startFound = [string]::IsNullOrWhiteSpace($startInput) $startAtFile = '' if (-not $startFound) { if ($startInput -match '^\d{4}-\d{2}-\d{2}$') { # Start per Datum $match = $files | Where-Object { $_.BaseName -like "$startInput*" } | Select-Object -First 1 if ($match) { $startAtFile = $match.Name Write-Host '📆 Beginne ab erster Datei mit Datum' $startInput ':' $startAtFile } else { Write-Host '❗️Keine Datei mit Datum' $startInput 'gefunden. Starte trotzdem ganz normal.' $startFound = $true } } else { # Start per Dateiname if ($files.Name -contains $startInput) { $startAtFile = $startInput } else { Write-Host "❗️Datei '$startInput' wurde im Quellordner nicht gefunden. Starte trotzdem ganz normal." $startFound = $true } } } # Verarbeitung starten foreach ($file in $files) { if (-not $startFound) { if ($file.Name -eq $startAtFile) { $startFound = $true } else { continue } } Write-Host "`n➡️ Verarbeite Datei: $($file.Name)" # Ziel-Datei prüfen $targetFile = Join-Path $targetFolder ($file.BaseName + '_geotagged.jpg') if (Test-Path $targetFile) { Write-Host "⚠️ $($file.Name) wurde bereits verarbeitet. Überspringe." continue } # Datum aus Dateiname extrahieren $datePart = $file.BaseName -split '_' | Select-Object -First 1 if (-not ($datePart -match '^\d{4}-\d{2}-\d{2}$')) { Write-Host "❌ Ungültiges Datumsformat im Dateinamen: $($file.Name)" continue } # Einträge für Datum filtern $matchingEntries = $history | Where-Object { ($_.Date -split ' ')[0] -eq $datePart } if ($matchingEntries.Count -eq 0) { Write-Host "❌ Keine Einträge für $($file.Name) gefunden." continue } # Foto öffnen (Standardprogramm) $photoProcess = Start-Process -FilePath $file.FullName -PassThru if ($matchingEntries.Count -gt 1) { Write-Host "📅 Mehrere Einträge für $datePart gefunden:" for ($i = 0; $i -lt $matchingEntries.Count; $i++) { $entry = $matchingEntries[$i] Write-Host ($i+1) ':' $entry.Place '–' $entry.'Place Location' '–' $entry.Date } $choice = Read-Host '❓ Nummer auswählen oder (s)kip' # Foto schließen try { Start-Sleep -Seconds 1 $photoProcess.CloseMainWindow() | Out-Null Start-Sleep -Seconds 1 if (!$photoProcess.HasExited) { $photoProcess.Kill() } } catch {} if ($choice -eq 's') { Write-Host '⏩ Übersprungen.' if (Test-Path $targetFile) { Remove-Item -Path $targetFile -Force Write-Host "🗑️ Vorhandene Datei $targetFile gelöscht." } continue } if ($choice -match '^\d+$' -and [int]$choice -ge 1 -and [int]$choice -le $matchingEntries.Count) { $selected = $matchingEntries[[int]$choice - 1] } else { Write-Host '❌ Ungültige Eingabe. Überspringe.' continue } } else { $selected = $matchingEntries[0] Write-Host '📍 Eintrag gefunden:' $selected.Place '–' $selected.'Place Location' $confirm = Read-Host '✅ Verwenden? (y/n)' # Foto schließen try { Start-Sleep -Seconds 1 $photoProcess.CloseMainWindow() | Out-Null Start-Sleep -Seconds 1 if (!$photoProcess.HasExited) { $photoProcess.Kill() } } catch {} if ($confirm.ToLower() -ne 'y') { Write-Host '⏩ Foto übersprungen.' continue } } # API-Abfrage vorbereiten $placeQuery = "$($selected.Place) $($selected.'Place Location')" $encodedQuery = ConvertUmlautsAndUrlEncode $placeQuery $apiUrl = 'https://photon.komoot.io/api/?q=' + $encodedQuery Write-Host '🔗 API-URL:' $apiUrl try { $response = Invoke-RestMethod -Uri $apiUrl -UseBasicParsing if ($response.features.Count -eq 0) { Write-Host '❌ Keine Koordinaten gefunden.' continue } $coords = $response.features[0].geometry.coordinates $lon = [math]::Round($coords[0], 3) $lat = [math]::Round($coords[1], 3) Write-Host "🌍 Gefundene Koordinaten: $lat, $lon" } catch { Write-Host '❌ Fehler bei API-Anfrage:' $_ continue } # Datum mit Uhrzeit aus JSON-Eintrag übernehmen und UTC+2 korrigieren $utcString = $selected.Date.Replace(' UTC','') $utcTime = [datetime]::ParseExact($utcString, 'yyyy-MM-dd HH:mm:ss', $null, [System.Globalization.DateTimeStyles]::AssumeUniversal) $datetime = $utcTime.ToString('yyyy:MM:dd HH:mm:ss') # Datei in Zielordner kopieren Copy-Item -Path $file.FullName -Destination $targetFile -Force # ExifTool-Befehl auf kopierte Datei & $exiftool ` -quiet ` -overwrite_original ` "-GPSLatitude=$lat" ` "-GPSLatitudeRef=$(if ($lat -ge 0) {'N'} else {'S'})" ` "-GPSLongitude=$lon" ` "-GPSLongitudeRef=$(if ($lon -ge 0) {'E'} else {'W'})" ` "-DateTimeOriginal=$datetime" ` "-CreateDate=$datetime" ` "-ModifyDate=$datetime" ` "-FileModifyDate=$datetime" ` "-FileCreateDate=$datetime" ` "$targetFile" if ($LASTEXITCODE -eq 0) { Write-Host '✅ Geotagging abgeschlossen:' $targetFile } else { Write-Host '❌ Fehler beim Schreiben der Metadaten.' } } [console]::beep(500,200)