# Medien aus dem eigenen System abgleichen

Am Ende spiegelt ein Skript einen Ordner Ihres Systems in einen Ordner der Bibliothek: Neue Dateien kommen hinzu, entfernte verschwinden, unveränderte bleiben unberührt. Eine automatische Playlist auf diesem Ordner spielt das Ergebnis, ohne dass jemand sie pflegt.

> **Typ:** anleitung
> **Für wen:** Entwicklerinnen und Entwickler, die eine Produktdatenbank, ein DAM oder ein Marketing-Werkzeug anbinden, das Plakate und Videos ausgibt
> **Voraussetzung:** ein Schlüssel `ak_…` der Klasse **Medien-Automation** und ein Ordner in der Bibliothek
> **Ort:** Kommandozeile oder ein geplanter Auftrag auf Ihrem Server

## So funktioniert es

Jede Datei erkennt das Skript an ihrer SHA-256-Prüfsumme. Es vergleicht die Prüfsummen Ihres Exports mit denen im Ordner der Bibliothek und handelt nur bei Unterschieden:

| Lage | Was das Skript tut |
|---|---|
| Eine Datei liegt nur in Ihrem Export | Hochladen: vorprüfen, Bytes schieben, abschließen. |
| Eine Datei liegt nur im Ordner der Bibliothek | Löschen. Gelöschte Medien lassen sich wiederherstellen. |
| Eine Datei liegt in beiden | Nichts. Eine geänderte Datei hat eine neue Prüfsumme, gilt also als neu, und die alte Fassung wird entfernt. |

Das Hochladen hat drei Schritte. `POST /v1/assets/upload-precheck` prüft Dateityp und Speicherkontingent und legt den Mediendatensatz an. Gibt es dieselben Bytes schon in Ihrer Organisation, trägt die Antwort keine Upload-Adresse, und die Bytes werden wiederverwendet. Sonst schieben Sie die Datei an die Adresse in `presignedUrl`. `POST /v1/assets/upload-complete` startet danach die Verarbeitung, und ein paar Sekunden später ist die Datei für Displays bereit, ein langes Video nach einigen Minuten.

## Schritte

1. **Legen Sie den Ordner an** in der Bibliothek, etwa „Plakate aus dem PIM“, und suchen Sie seine Kennung:

   ```bash
   curl -sS "$SZENERA_API/v1/folders" \
     -H "Authorization: Bearer $SZENERA_API_KEY" \
     | jq -r '.[] | "\(.id)  \(.path)"'
   ```

   ```text
   fld_01KJ7MWKW0MAPPE00000000001  /Plakate aus dem PIM
   ```

2. **Speichern Sie das Skript:**

   ```ts
   // sync-media.ts: node sync-media.ts ./export fld_…
   import { createHash } from 'node:crypto';
   import { readdir, readFile } from 'node:fs/promises';
   import { extname, join } from 'node:path';

   const API = process.env.SZENERA_API;
   const KEY = process.env.SZENERA_API_KEY; // Klasse Medien-Automation
   const [dir, folderId] = process.argv.slice(2);

   const TYPES: Record<string, string> = {
     '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp',
     '.svg': 'image/svg+xml', '.mp4': 'video/mp4', '.webm': 'video/webm', '.pdf': 'application/pdf',
   };
   const SINGLE_UPLOAD_LIMIT = 50 * 1024 * 1024; // Darüber geht der Upload in Teilen

   async function call(method: string, path: string, body?: unknown) {
     const res = await fetch(`${API}${path}`, {
       method,
       headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
       body: body === undefined ? undefined : JSON.stringify(body),
     });
     if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${await res.text()}`);
     return res.json();
   }

   // Was der Ordner der Bibliothek hält, nach Prüfsumme
   const remote = new Map<string, { id: string; name: string }>();
   let cursor = '';
   do {
     const query = new URLSearchParams({ folderId, limit: '200' });
     if (cursor) query.set('cursor', cursor);
     const page = await call('GET', `/v1/assets?${query}`);
     for (const asset of page.items) remote.set(asset.sha256, asset);
     cursor = page.nextCursor ?? '';
   } while (cursor);

   // Hochladen, was neu ist
   const local = new Set<string>();
   for (const file of await readdir(dir)) {
     const mimeType = TYPES[extname(file).toLowerCase()];
     if (!mimeType) continue;
     const bytes = await readFile(join(dir, file));
     const sha256 = createHash('sha256').update(bytes).digest('hex');
     local.add(sha256);
     if (remote.has(sha256)) continue;
     if (bytes.length > SINGLE_UPLOAD_LIMIT) {
       console.log(`skipped, upload with szenera asset upload: ${file}`);
       continue;
     }

     const precheck = await call('POST', '/v1/assets/upload-precheck', {
       originalFileName: file, mimeType, sizeBytes: bytes.length, sha256, folderId,
     });
     if (!precheck.exists) {
       const { url, headers } = precheck.presignedUrl;
       const put = await fetch(url, { method: 'PUT', headers, body: bytes });
       if (!put.ok) throw new Error(`upload ${file}: ${put.status}`);
     }
     await call('POST', '/v1/assets/upload-complete', { assetId: precheck.assetId, sha256 });
     console.log(`uploaded: ${file}`);
   }

   // Löschen, was in Ihrem System nicht mehr da ist
   for (const [sha256, asset] of remote) {
     if (local.has(sha256)) continue;
     const res = await fetch(`${API}/v1/assets/${asset.id}`, {
       method: 'DELETE',
       headers: { Authorization: `Bearer ${KEY}` },
     });
     console.log(res.ok ? `deleted: ${asset.name}` : `kept, used in a scene: ${asset.name}`);
   }
   ```

3. **Starten Sie es** einmal von Hand und sehen Sie sich die Ausgabe an:

   ```bash
   node sync-media.ts ./export fld_01KJ7MWKW0MAPPE00000000001
   ```

   ```text
   uploaded: herbst-angebot.png
   uploaded: neue-oeffnungszeiten.mp4
   deleted: sommer-angebot.png
   ```

4. **Legen Sie eine automatische Playlist** auf den Ordner im Portal an und weisen Sie sie Ihren Displays zu, siehe [Eine Playlist automatisch aus einem Ordner oder Tags füllen](https://docs.szenera.com/de/programs/automatic-playlists). Neue Dateien kommen von selbst hinzu, sobald ihre Verarbeitung fertig ist.

5. **Lassen Sie das Skript regelmäßig laufen,** etwa jede Nacht nach Ihrem Export:

   ```text
   15 2 * * * cd /opt/szenera-sync && node sync-media.ts /exports/posters fld_01KJ7MWKW0MAPPE00000000001
   ```

Dateien über 50 MiB gehen in Teilen hoch. Das übernimmt die Kommandozeile: `szenera asset upload --file=<pfad> --folder=<fld_…>` lädt hoch, wartet, bis die Datei bereit ist, und räumt einen abgebrochenen Upload auf.

## Ergebnis prüfen

- Der Ordner der Bibliothek hält genau die Dateien Ihres Exports.
- Ein zweiter Lauf direkt nach dem ersten schreibt nichts aus, weil sich nichts geändert hat.
- Die automatische Playlist listet die neuen Dateien unter **Sequenz**, markiert mit **Aus der Quelle**.

## Wenn etwas nicht klappt

| Symptom | Bedeutung | Lösung |
|---|---|---|
| `403 Permission.Denied` | Dem Schlüssel fehlt `asset.upload`, `asset.list` oder `asset.delete`. | Einen Schlüssel der Klasse **Medien-Automation** verwenden. |
| `422 Upload.MimeTypeNotAllowed` | Der Dateityp wird nicht unterstützt. | JPEG, PNG, WebP, SVG, MP4, WebM oder PDF verwenden. |
| `402 Upload.QuotaExceeded` | Der Speicher Ihres Pakets ist aufgebraucht. | Medien löschen, die Sie nicht mehr brauchen, oder das Paket wechseln. |
| `kept, used in a scene` | Das Löschen antwortete mit `409 Asset.InUse`: Eine Szene nutzt die Datei direkt. | Die Datei aus der Szene nehmen oder sie liegen lassen. |
| Eine Datei bleibt in der Verarbeitung | Lange Videos brauchen Minuten. | Auf `media-library.asset.ready` warten oder mit `GET /v1/assets/{id}` nachsehen. |
| Jeder Lauf lädt alles neu hoch | Das Skript vergleicht den falschen Ordner, oder der Export schreibt Dateien mit neuen Bytes. | Die Ordner-Kennung prüfen und gleiche Dateien Byte für Byte gleich exportieren. |

## Weiterführendes

- [Befehle für Ihre Organisation](/de/cli/commands-organization)
- [Ereignistypen: Inhalte, Programme, Geräte, Apps](/de/webhooks/event-types-content-devices)
- [Blättern: limit, cursor, items, nextCursor](/de/api/pagination)
