Ein spezialisiertes Python-Tool zur Reorganisation alter iPhone-Foto-Backups. Dieses Skript löst das Problem von rekursiven Ordnerstrukturen und kollidierenden Dateinamen (IMG_0001.JPG), indem es alle Medien chronologisch sortiert, global nummeriert und in einer flachen Struktur archiviert.
Alte iPhone-Backups (z. B. aus der Ära vor iCloud) speichern Bilder oft in unzähligen Unterordnern (100APPLE, 101APPLE etc.).
-
In jedem Ordner beginnt der Dateicounter erneut bei
0001. -
Ein einfaches Zusammenführen führt zu massiven Namenskollisionen.
-
Dateien ohne EXIF-Metadaten (z. B. WhatsApp-Bilder) unterbrechen die chronologische Anzeige in Programmen wie XnView MP.
Dieses Skript analysiert den gesamten Bestand über eine lokale Datenbank, sortiert alle Medien (Bilder & Videos) sekundengenau und exportiert sie mit einem standardisierten, web-konformen Dateinamen. Sorgenkinder ohne Metadaten werden durch ein spezifisches Anker-Datum (09.09.1971) wieder sortierbar gemacht.
Das Skript benötigt Python 3 und das Tool ExifTool, um Apple-spezifische Metadaten (HEIC, MOV, QuickTime) zu verarbeiten.
ExifTool unter Linux (Zorin OS/Ubuntu) installieren:
Bash
sudo apt update && sudo apt install libimage-exiftool-perl -y
Für die Umwandlung von HEIC- (Apple) und anderen Bildformaten müssen folgende Python-Bibliotheken installiert sein:
bash
# Installation unter Linux (Zorin OS)
pip3 install Pillow pillow-heif --break-system-packages
Das Skript arbeitet mit folgenden Verzeichnissen:
| Verzeichnis | Zweck |
|---|---|
/fotopool-alt |
Quellverzeichnis mit der ungeordneten iPhone-Struktur. |
/fotopool-bin |
Speicherort des Skripts und der Datenbank (photo_db.json). |
/fotopool-neu |
Zielordner für alle erfolgreich datierten Medien. |
/fotopool-ohne-datum |
Zielordner für Medien mit dem Anker-Datum 1971. |
Das Skript wird in zwei Phasen über das Terminal gesteuert:
Erstellt eine Datenbank aller vorhandenen Medien und extrahiert die Zeitstempel.
Bash
python3 iphone-photo-manager.py --scan
Es gibt drei Möglichkeiten, den Export durchzuführen:
-
Standard (PIC/VID): Unterscheidet automatisch zwischen Bildern und Videos.
Bash
python3 iphone-photo-manager.py --export -
Neutral (OBJ): Verwendet ein einheitliches Label für alle Objekte.
Bash
python3 iphone-photo-manager.py --export --nameit OBJ -
Individuell: Verwendet ein vom Nutzer definiertes Label.
Bash
python3 iphone-photo-manager.py --export --nameit MEINNAME
Das Skript ermöglicht es, alle Bildformate (HEIC, PNG, BMP, TIFF) während des Exports in das platzsparende JPEG-Format umzuwandeln. Dabei werden alle Metadaten (EXIF/IPTC/XMP) vom Original übernommen.
Nutze dazu einen der folgenden Parameter:
--extjpg10: Maximale Qualität (Qualitätsfaktor 95), nahezu verlustfrei--extjpg7: Ausgewogene Kompression (Qualitätsfaktor 75) – idealer Kompromiss aus Grösse und Qualität--extjpg5: Starke Kompression (Qualitätsfaktor 50) für minimale Dateigrössen
Beispiel:
Bash
python3 iphone-photo-manager.py --export --extjpg5 --nameit OBJ
Hinweis: Videos bleiben bei diesem Vorgang in ihrem Original-Container (z. B. .mov oder .mp4) erhalten.
Damit konvertierte Bilder sofort im Kategorien-Filter von XnView MP sichtbar sind, schreibt das Skript das Schlagwort "Bilder" redundant in drei verschiedene Metadaten-Felder (IPTC Keywords, XMP-dc:Subject und XMP-lr:HierarchicalSubject).
Wichtig: Katalog-Aktualisierung Da XnView MP eine interne Datenbank nutzt, müssen die vom Skript geschriebenen Daten nach dem Export einmalig "angestossen" werden:
- Navigiere in XnView MP zum Ordner
fotopool-neu. - Markiere alle Dateien (
Strg + A). - Wähle im Menü: Metadaten > Katalog aus Dateien aktualisieren.
Dadurch werden die Häkchen in der Kategorie-Spalte bei "Bilder" sofort auf "Zugeordnet" gesetzt.
Im Gegensatz zu Original-iPhone-Ordnern verwendet dieses Skript einen lückenlosen, globalen Zähler über den gesamten Bestand. Das Dateiformat ist YYYYMMDD-TYP-00001.ext. Dadurch bleibt die Sortierung auch bei gleicher Aufnahmesekunde stabil.
Dateien ohne EXIF-Daten erhalten das Anker-Datum 09.09.1971.
-
Metadaten-Fix: Das Datum wird via ExifTool fest in die Datei geschrieben (EXIF bei Bildern, QuickTime-Tags bei Videos).
-
Unix-Sicherheit: 1971 liegt sicher nach dem Unix-Nullpunkt (1970), was Anzeigefehler in älterer Software verhindert.
-
System-Sync: Das Skript setzt das Änderungsdatum des Dateisystems (
mtime) auf 1971, sodass Dateimanager und XnView MP die Dateien sofort korrekt oben einsortieren.
Das Skript ist darauf optimiert, Metadaten so zu schreiben, dass XnView MP sie in den Reitern "EXIF" (Bilder) und "ExifTool" (Videos) direkt auslesen kann.
Bei grossen Datenbeständen (z. B. 20'000+ Dateien) können vereinzelt Fehler auftreten (beschädigte Dateien, extrem lange Pfade im Quellordner). Damit der Fortschrittsbalken in der Konsole stabil bleibt, werden Fehler nicht direkt ausgegeben, sondern diskret protokolliert.
- Log-Datei: Alle Probleme werden mit Zeitstempel in der Datei
error_log.txtim Ordnerfotopool-bingespeichert. - Statistik: Am Ende jedes Durchlaufs zeigt das Skript eine Zusammenfassung der erfolgreichen und fehlgeschlagenen Operationen an.
Häufige Fehlermeldungen:
broken data stream: Die Quelldatei ist physisch beschädigt. Das Skript überspringt die Konvertierung und versucht eine einfache Kopie des Originals.Path too long: Der Pfad im Quellverzeichnis (fotopool-alt) überschreitet die Systemgrenzen. Das Skript loggt dies und schlägt fehl – oft hilft es, den Quellordner umzubenennen oder flacher zu strukturieren.
Python
import os
import json
import subprocess
import shutil
import argparse
import sys
from datetime import datetime, timedelta
# --- HEIC/PILLOW KOMPATIBILITÄTS-CHECK ---
PILLOW_INSTALLED = False
HEIF_SUPPORT = False
try:
from PIL import Image
PILLOW_INSTALLED = True
try:
import pillow_heif
pillow_heif.register_heif_opener()
HEIF_SUPPORT = True
except ImportError:
HEIF_SUPPORT = False
except ImportError:
PILLOW_INSTALLED = False
# --- RELATIVE PFADERMITTLUNG ---
BIN_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(BIN_DIR)
SOURCE_DIR = os.path.join(BASE_DIR, "fotopool-alt")
TARGET_DIR_OK = os.path.join(BASE_DIR, "fotopool-neu")
TARGET_DIR_NO_DATE = os.path.join(BASE_DIR, "fotopool-ohne-datum")
DB_FILE = os.path.join(BIN_DIR, "photo_db.json")
LOG_FILE = os.path.join(BIN_DIR, "error_log.txt")
IMAGE_EXT = {'.jpg', '.jpeg', '.png', '.heic', '.heif', '.tiff', '.tif', '.dng', '.gif', '.bmp'}
VIDEO_EXT = {'.mov', '.mp4', '.m4v', '.qt', '.3gp'}
def log_error(message):
"""Schreibt Fehler diskret in die Log-Datei statt in die Konsole."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(LOG_FILE, "a") as f:
f.write(f"[{timestamp}] {message}\n")
def print_progress(iteration, total, prefix=''):
"""Erzeugt einen stabilen Fortschrittsbalken."""
percent = ("{0:.1f}").format(100 * (iteration / float(total)))
filled_length = int(50 * iteration // total)
bar = '█' * filled_length + '░' * (50 - filled_length)
sys.stdout.write(f'\r{prefix} |{bar}| {percent}% ({iteration}/{total})')
sys.stdout.flush()
if iteration == total:
sys.stdout.write('\n')
def get_exif_date(file_path):
try:
cmd = ["exiftool", "-s3", "-d", "%Y:%m:%d %H:%M:%S", "-CreateDate", "-DateTimeOriginal", "-CreationDate", file_path]
result = subprocess.run(cmd, capture_output=True, text=True)
dates = [d for d in result.stdout.strip().split('\n') if d]
if dates:
return datetime.strptime(dates[0], "%Y:%m:%d %H:%M:%S")
except Exception as e:
log_error(f"Metadaten-Fehler bei {file_path}: {e}")
return None
def copy_metadata(source, target):
cmd = ["exiftool", "-overwrite_original", "-TagsFromFile", source, "-all:all", "-unsafe", target]
subprocess.run(cmd, capture_output=True)
def add_xnview_category(file_path, category="Bilder"):
cmd = ["exiftool", "-overwrite_original", f"-IPTC:Keywords={category}", f"-XMP-dc:Subject={category}", f"-XMP-lr:HierarchicalSubject={category}", file_path]
subprocess.run(cmd, capture_output=True)
def write_exif_date(file_path, date_obj):
date_str = date_obj.strftime("%Y:%m:%d %H:%M:%S")
ext = os.path.splitext(file_path)[1].lower()
if ext in VIDEO_EXT:
cmd = ["exiftool", "-overwrite_original", f"-QuickTime:CreateDate={date_str}", f"-QuickTime:ModifyDate={date_str}", f"-Keys:CreationDate={date_str}", file_path]
else:
cmd = ["exiftool", "-overwrite_original", f"-AllDates={date_str}", f"-CreationDate={date_str}", file_path]
subprocess.run(cmd, capture_output=True)
ts = date_obj.timestamp()
os.utime(file_path, (ts, ts))
def scan():
if os.path.exists(LOG_FILE): os.remove(LOG_FILE) # Log bei neuem Scan leeren
print(f"Suche Dateien in {SOURCE_DIR}...")
all_files = []
if not os.path.exists(SOURCE_DIR): return
for root, _, files in os.walk(SOURCE_DIR):
for file in files:
if os.path.splitext(file)[1].lower() in (IMAGE_EXT | VIDEO_EXT):
all_files.append(os.path.join(root, file))
total = len(all_files)
if total == 0: return
print(f"Analyse startet...")
db = []
for i, path in enumerate(all_files, start=1):
date = get_exif_date(path)
ext = os.path.splitext(path)[1].lower()
file_type = "PIC" if ext in IMAGE_EXT else "VID"
db.append({"orig_path": path, "full_timestamp": date.isoformat() if date else None, "day_string": date.strftime("%Y%m%d") if date else None, "type": file_type, "extension": ext})
print_progress(i, total, prefix='Analyse')
with open(DB_FILE, "w") as f: json.dump(db, f, indent=4)
def export(custom_name=None, jpg_quality=None):
if not os.path.exists(DB_FILE): return
with open(DB_FILE, "r") as f: db = json.load(f)
final_list = [e for e in db if not e["full_timestamp"]] + sorted([e for e in db if e["full_timestamp"]], key=lambda x: x["full_timestamp"])
total = len(final_list)
error_count = 0
os.makedirs(TARGET_DIR_OK, exist_ok=True)
os.makedirs(TARGET_DIR_NO_DATE, exist_ok=True)
anchor_start = datetime(1971, 9, 9, 0, 0, 0)
print(f"Export startet...")
for i, entry in enumerate(final_list, start=1):
display_type = custom_name if custom_name else entry["type"]
is_image = entry["extension"] in IMAGE_EXT
target_ext = ".jpg" if (jpg_quality and is_image) else entry["extension"]
if not entry["full_timestamp"]:
date_obj = anchor_start + timedelta(seconds=i)
new_name = f"19710909-{display_type}-{i:05d}{target_ext}"
dest_path = os.path.join(TARGET_DIR_NO_DATE, new_name)
else:
date_obj = datetime.fromisoformat(entry["full_timestamp"])
new_name = f"{entry['day_string']}-{display_type}-{i:05d}{target_ext}"
dest_path = os.path.join(TARGET_DIR_OK, new_name)
try:
if jpg_quality and is_image:
img = Image.open(entry['orig_path'])
img.convert("RGB").save(dest_path, "JPEG", quality=jpg_quality)
copy_metadata(entry['orig_path'], dest_path)
add_xnview_category(dest_path, "Bilder")
else:
shutil.copy2(entry['orig_path'], dest_path)
write_exif_date(dest_path, date_obj)
except Exception as e:
error_count += 1
log_error(f"Verarbeitungs-Fehler bei {entry['orig_path']}: {e}")
print_progress(i, total, prefix='Export ')
print(f"\nVorgang abgeschlossen. Erfolgreich: {total - error_count} | Fehler (siehe Log): {error_count}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--scan", action="store_true")
parser.add_argument("--export", action="store_true")
parser.add_argument("--nameit", type=str)
parser.add_argument("--extjpg5", action="store_true")
parser.add_argument("--extjpg7", action="store_true")
parser.add_argument("--extjpg10", action="store_true")
args = parser.parse_args()
quality = {args.extjpg5: 50, args.extjpg7: 75, args.extjpg10: 95}.get(True)
if args.scan: scan()
elif args.export:
if quality and (not PILLOW_INSTALLED or not HEIF_SUPPORT):
print("FEHLER: Pillow oder HEIF-Support fehlt.")
sys.exit(1)
export(custom_name=args.nameit, jpg_quality=quality)
