# Wegwichtel REST-API Diese Datei dokumentiert die vollständige HTTP-Schnittstelle des Wegwichtel-Servers. Sämtliche JSON-Ressourcen und aktiven GPX-, Bild- und Audiodateien werden unter `/api` bereitgestellt. Ein separates öffentliches `/media`-URL-Schema wird nicht verwendet. ## 1. Grundlagen ### Basisadressen Lokaler Standard: ```text http://127.0.0.1:47145 ``` API-Basis: ```text http://127.0.0.1:47145/api ``` Bei einer Nginx-Installation bleibt der Pfad gleich, beispielsweise: ```text https://wegwichtel.example.org/api ``` ### Formate - Listen- und Metadatenzugriffe liefern JSON. - Die Einzelendpunkte für GPX, Bilder und Audio liefern die jeweilige Datei direkt aus. Mit `?metadata=true` liefern die Bild- und Audioendpunkte stattdessen JSON-Metadaten. - Schreibzugriffe ohne Dateien akzeptieren `application/json`, `application/x-www-form-urlencoded` oder `multipart/form-data`. - GPX-Dateien werden ausschließlich als `multipart/form-data` übertragen. - Bild- und Audioendpunkte akzeptieren wahlweise `multipart/form-data` oder ein JSON-Objekt mit Base64-kodierten Dateidaten. - Der Server ermittelt den tatsächlichen MIME-Typ aus dem Dateiinhalt. Dateiname, Dateiendung, Multipart-`Content-Type`, JSON-Felder und Data-URL-Präfixe werden nicht als Typnachweis verwendet. - Für JSON wird `Content-Type: application/json` empfohlen. `Content-Type: text/json` wird aus Kompatibilitätsgründen ebenfalls akzeptiert. - Ein leeres JSON-Objekt (`{}`) enthält keine Datei und kann deshalb keinen Bild- oder Audio-Upload ausführen. - Pro Medien-Request wird genau eine Bild- beziehungsweise Audiodatei verarbeitet. - Base64 vergrößert die Requestgröße um ungefähr ein Drittel und benötigt beim Verarbeiten zusätzlichen Arbeitsspeicher. Für große Dateien ist `multipart/form-data` vorzuziehen. - Zeitstempel werden als SQLite- oder ISO-8601-Text ausgegeben. - Die Node.js-Anwendung besitzt keine eigene Authentifizierung. Ein vorgeschalteter Webserver kann Basic Auth erzwingen; die mitgelieferten Python-Werkzeuge erkennen `401 Unauthorized`, fragen Zugangsdaten ab und wiederholen den Request. ### Uploadgrenzen und Dateitypen Das Dateilimit pro Datei wird durch `MAX_UPLOAD_MB` festgelegt und beträgt standardmäßig `50 MB`. Unterstützte Dateitypen: | Ressource | Dateiendungen beziehungsweise MIME-Typen | |---|---| | GPX | GPX-XML mit ``-Wurzelelement | | Bilder | JPEG, PNG, WebP | | Audio | MP3, MP4/M4A, AAC, Ogg, WAV, WebM | Die Uploadfelder heißen: | Ressource | Multipart-Feld beziehungsweise JSON-Eigenschaft | |---|---| | GPX | `gpx` | | einzelnes Bild | `picture` | | einzelne Audiodatei | `audio` | Bei einem JSON-Medienupload ist `picture` beziehungsweise `audio` ein Objekt: | Eigenschaft | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `filename` | Text | nein | ursprünglicher Dateiname für Protokollierung; die Endung wird nicht zur Typbestimmung verwendet | | `base64` | Text | ja, sofern `dataUrl` fehlt | reiner Base64-Inhalt ohne Präfix | | `dataUrl` | Text | alternativ zu `base64` | vollständige Base64-Data-URL; der dort genannte Medientyp wird ignoriert | `base64` und `dataUrl` sind Alternativen. Der Server erkennt Format und Speicherendung ausschließlich anhand der dekodierten Bytes. Ein vom Client mitgesendetes `contentType`- oder `mimeType`-Feld wird ignoriert und ist nicht erforderlich. ### Inhaltsbasierte Dateityperkennung Binäre Bild- und Audiodateien werden mit `file-type` anhand ihrer Magic Bytes analysiert. Für Audio-/Video-Container wird zusätzlich `@file-type/av` verwendet, damit beispielsweise M4A und WebM möglichst zuverlässig als Audio oder Video unterschieden werden. GPX ist ein textbasiertes XML-Format und wird deshalb separat als UTF-8 gelesen und anhand des ``-Wurzelelements validiert; anschließend übernimmt der vorhandene GPX-Parser die fachliche Prüfung. Daraus folgen zwei wichtige Regeln: - Eine als `image/png` deklarierte Textdatei wird mit `415 Unsupported Media Type` abgewiesen. - Eine echte PNG-Datei wird auch dann akzeptiert, wenn sie `datei.bin` heißt oder im Multipart-Request kein Datei-`Content-Type` angegeben ist. ### Fehlerformat ```json { "error": "HttpError", "message": "Strecke nicht gefunden." } ``` Typische Statuscodes: | Status | Bedeutung | |---:|---| | `200` | Anfrage erfolgreich | | `201` | Ressource wurde angelegt | | `400` | Parameter oder Upload fehlt beziehungsweise ist ungültig | | `401` | vorgeschalteter Webserver verlangt Authentifizierung | | `404` | Route, POI oder Medienressource wurde nicht gefunden | | `409` | Ressource existiert bereits oder Dateisystemzustand verhindert die Operation | | `413` | Datei überschreitet `MAX_UPLOAD_MB` | | `415` | Dateityp wird nicht unterstützt | | `500` | Interner Serverfehler | ### IDs Alle IDs sind positive, von SQLite erzeugte Ganzzahlen. - `:id` bezeichnet bei `/routes/:id/...` die Route. - `:pictureId` bezeichnet einen Datensatz aus `poi_images`. - `:poiId` bezeichnet den POI und zugleich seine höchstens eine Audioressource. Beispiele verwenden überwiegend Route `1`, POI `7` und Bild `15`. ## 2. Datenmodelle ### Route ```json { "id": 1, "slug": "schulwald-runde", "name": "Schulwald-Runde", "description": "Naturkundlicher Rundweg", "schoolName": "Beispielschule", "status": "active", "start": { "lat": 52.5208, "lon": 13.407 }, "center": { "lat": 52.521, "lon": 13.408 }, "bounds": { "minLat": 52.5208, "minLon": 13.407, "maxLat": 52.5212, "maxLon": 13.409 }, "distanceM": 842.6, "elevationGainM": 14.2, "pointCount": 87, "gpxUrl": "/api/routes/1/gpx", "createdAt": "2026-06-16 12:00:00", "updatedAt": "2026-06-16 12:00:00", "deletedAt": null, "proximityM": 324.8 } ``` ### POI ```json { "id": 7, "routeId": 1, "title": "Die alte Eiche", "description": "Hier wird das Alter der Eiche erklärt.", "lat": 52.5208, "lon": 13.407, "triggerRadiusM": 60, "sequence": 2, "audioUrl": "/api/routes/1/pois/7/audio", "images": [ { "id": 15, "caption": "Blick auf die Baumkrone", "sequence": 0, "url": "/api/routes/1/pois/7/pictures/15" } ] } ``` ### Bildressource ```json { "id": 15, "routeId": 1, "poiId": 7, "poiTitle": "Die alte Eiche", "caption": "Blick auf die Baumkrone", "sequence": 0, "url": "/api/routes/1/pois/7/pictures/15", "createdAt": "2026-06-16 12:30:00" } ``` ### Audioressource Pro POI kann höchstens eine Audiodatei existieren. Deshalb wird die Audioressource über die `poiId` adressiert. ```json { "routeId": 1, "poiId": 7, "poiTitle": "Die alte Eiche", "url": "/api/routes/1/pois/7/audio", "updatedAt": "2026-06-16 12:35:00" } ``` ## 3. Endpunktübersicht | Methode | Pfad | Zweck | |---|---|---| | `GET` | `/api/health` | Server- und SQLite-Zustand | | `GET` | `/api/routes` | Routen auflisten | | `GET` | `/api/routes/:id` | Route mit Punkten und POIs lesen | | `GET` | `/api/routes/:id/gpx` | GPX-Datei der Route ausliefern | | `POST` | `/api/routes` | Route anlegen | | `PUT` | `/api/routes/:id` | Route aktualisieren | | `POST` | `/api/routes/:id/append` | GPX-Punkte anhängen | | `DELETE` | `/api/routes/:id` | Route weich löschen | | `POST` | `/api/routes/:id/restore` | Route wiederherstellen | | `GET` | `/api/routes/:id/pois` | POIs einer Route auflisten | | `GET` | `/api/routes/:routeId/pois/:poiId` | einzelnen POI der Route lesen | | `POST` | `/api/routes/:id/pois` | POI-Metadaten anlegen | | `PUT` | `/api/routes/:routeId/pois/:poiId` | POI-Metadaten aktualisieren | | `DELETE` | `/api/routes/:routeId/pois/:poiId` | POI einschließlich Bildern und Audio löschen | | `GET` | `/api/routes/:routeId/pois/:poiId/pictures` | Bilder eines POIs auflisten | | `GET` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | Bilddatei ausliefern; optional Metadaten mit `?metadata=true` | | `POST` | `/api/routes/:routeId/pois/:poiId/pictures` | einzelnes Bild für den POI hochladen | | `PUT` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | Bilddatei oder Metadaten aktualisieren | | `DELETE` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | einzelnes Bild löschen | | `GET` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei ausliefern; optional Metadaten mit `?metadata=true` | | `POST` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei für den POI anlegen | | `PUT` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei des POIs ersetzen | | `DELETE` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei des POIs löschen | ## 4. Systemzustand ### `GET /api/health` Parameter: keine. ```bash curl http://127.0.0.1:47145/api/health ``` ```json { "ok": true, "service": "wegwichtel", "socket": "127.0.0.1:47145", "sqliteVersion": "3.46.1", "timestamp": "2026-06-16T12:00:00.000Z" } ``` ## 5. Routen lesen ### `GET /api/routes` Ohne Positionsparameter werden alle aktiven Routen alphabetisch geliefert. Mit `lat` und `lon` wird die Entfernung zum Routenstart berechnet, anhand `radiusKm` gefiltert und nach Entfernung sortiert. #### Query-Parameter | Parameter | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `lat` | Dezimalzahl | nein | – | Breitengrad; nur zusammen mit `lon` wirksam | | `lon` | Dezimalzahl | nein | – | Längengrad; nur zusammen mit `lat` wirksam | | `radiusKm` | Dezimalzahl | nein | `DEFAULT_ROUTE_RADIUS_KM`, standardmäßig `25` | maximaler Abstand zum Routenstart | | `includeDeleted` | Boolean-Text | nein | `false` | der exakte Wert `true` schließt gelöschte Routen ein | Alle aktiven Routen: ```bash curl http://127.0.0.1:47145/api/routes ``` Mit allen Query-Parametern: ```bash curl --get http://127.0.0.1:47145/api/routes \ --data-urlencode 'lat=52.5208' \ --data-urlencode 'lon=13.4070' \ --data-urlencode 'radiusKm=12.5' \ --data-urlencode 'includeDeleted=true' ``` ### `GET /api/routes/:id` Liefert Route, GPX-Punkte und POIs einschließlich Medien-URLs. #### Pfadparameter | Parameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `id` | Ganzzahl | ja | Route | #### Query-Parameter | Parameter | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `includeDeleted` | Boolean-Text | nein | `false` | mit `true` kann eine gelöschte Route gelesen werden | ```bash curl http://127.0.0.1:47145/api/routes/1 ``` ```bash curl 'http://127.0.0.1:47145/api/routes/1?includeDeleted=true' ``` ### `GET /api/routes/:id/gpx` Liefert die aktive GPX-Datei der Route direkt mit `Content-Type: application/gpx+xml` aus. Der in einer Routenressource enthaltene Wert `gpxUrl` verweist auf diesen Endpunkt. ```bash curl http://127.0.0.1:47145/api/routes/1/gpx \ --output schulwald-runde.gpx ``` Parameter außer der Routen-ID sind nicht vorgesehen. Gelöschte oder unbekannte Routen antworten mit `404`. ## 6. Route anlegen und bearbeiten ### `POST /api/routes` Legt eine Route aus einer GPX-Datei an. Content-Type: `multipart/form-data` | Feld | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---|---| | `gpx` | Datei | ja | – | GPX-Datei mit mindestens einem Trackpunkt; der Dateiname ist unerheblich | | `name` | Text | bedingt | Name aus GPX | erforderlich, wenn GPX keinen Namen enthält | | `slug` | Text | nein | aus `name` | interne URL-freundliche Kennung | | `description` | Text | nein | leer | Routenbeschreibung | | `schoolName` | Text | nein | leer | Schule oder Einrichtung | ```bash curl -X POST http://127.0.0.1:47145/api/routes \ -F 'name=Schulwald-Runde' \ -F 'slug=schulwald-runde-klasse-7a' \ -F 'description=Naturkundlicher Rundweg der Klasse 7a' \ -F 'schoolName=Beispielschule' \ -F 'gpx=@examples/sample-route.gpx' ``` Erfolg: `201 Created` und `Location: /api/routes/`. ### `PUT /api/routes/:id` Aktualisiert Metadaten. Eine optionale GPX-Datei ersetzt alle bisherigen Trackpunkte. | Feld | Typ | Pflicht | Verhalten ohne Feld | |---|---|---:|---| | `gpx` | Datei | nein | bisherige GPX-Punkte bleiben erhalten | | `name` | Text | nein | bisheriger Wert bleibt | | `description` | Text | nein | bisheriger Wert bleibt | | `schoolName` | Text | nein | bisheriger Wert bleibt | ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1 \ -F 'name=Schulwald-Runde 2026' \ -F 'description=Überarbeitete Strecke' \ -F 'schoolName=Beispielschule' \ -F 'gpx=@route-neu.gpx' ``` ### `POST /api/routes/:id/append` Hängt alle Trackpunkte einer GPX-Datei an die Route an und berechnet Streckenwerte neu. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `gpx` | Datei | ja | anzuhängende GPX-Datei | ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/append \ -F 'gpx=@verlaengerung.gpx' ``` ## 7. POIs lesen und bearbeiten ### `GET /api/routes/:id/pois` Liefert alle POIs einer aktiven Route nach `sequence` und `id`. ```bash curl http://127.0.0.1:47145/api/routes/1/pois ``` ### `GET /api/routes/:routeId/pois/:poiId` Liefert einen einzelnen POI einschließlich seiner aktuellen Bild- und Audio-URLs. Route und POI werden gemeinsam geprüft; der POI muss zur angegebenen aktiven Route gehören. | Pfadparameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `routeId` | Ganzzahl | ja | ID der aktiven Route | | `poiId` | Ganzzahl | ja | ID des POIs innerhalb dieser Route | ```bash curl http://127.0.0.1:47145/api/routes/1/pois/7 ``` Eine unbekannte Route, ein unbekannter POI oder eine falsche Route-POI-Kombination liefert `404 Not Found`. ### `POST /api/routes/:id/pois` Legt ausschließlich die POI-Metadaten an. Bilder und Audio werden anschließend über die gesonderten Medienendpunkte hochgeladen. | Feld | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `title` | Text | nein | `Unbenannter POI` | Stationsname | | `description` | Text | nein | leer | Beschreibung | | `lat` | Dezimalzahl | ja | – | Breitengrad | | `lon` | Dezimalzahl | ja | – | Längengrad | | `triggerRadiusM` | Dezimalzahl | nein | `DEFAULT_POI_TRIGGER_METERS`, standardmäßig `80` | Aktivierungsradius in Metern | | `sequence` | Ganzzahl | nein | `0` | Reihenfolge in der Stationsliste | JSON-Beispiel mit allen Parametern: ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/pois \ -H 'Content-Type: application/json' \ -d '{ "title": "Die alte Eiche", "description": "Hier wird das Alter der Eiche erklärt.", "lat": 52.5208, "lon": 13.4070, "triggerRadiusM": 60, "sequence": 2 }' ``` Erfolg: `201 Created` und `Location: /api/routes//pois/`. ### `PUT /api/routes/:routeId/pois/:poiId` Aktualisiert ausschließlich POI-Metadaten. Nicht übergebene Werte bleiben erhalten. Route und POI werden gemeinsam validiert; ein POI kann über diesen Endpunkt keiner anderen Route zugeordnet werden. | Pfadparameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `routeId` | Ganzzahl | ja | ID der aktiven Route | | `poiId` | Ganzzahl | ja | ID des POIs innerhalb dieser Route | | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `title` | Text | nein | neuer Stationsname | | `description` | Text | nein | neue Beschreibung | | `lat` | Dezimalzahl | nein | neuer Breitengrad | | `lon` | Dezimalzahl | nein | neuer Längengrad | | `triggerRadiusM` | Dezimalzahl | nein | neuer Aktivierungsradius | | `sequence` | nichtnegative Ganzzahl | nein | neue Reihenfolge | ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7 \ -H 'Content-Type: application/json' \ -d '{ "title": "Die sehr alte Eiche", "description": "Aktualisierte Beschreibung", "lat": 52.5209, "lon": 13.4071, "triggerRadiusM": 45, "sequence": 3 }' ``` ### `DELETE /api/routes/:routeId/pois/:poiId` Löscht den POI-Datensatz sowie alle zugehörigen Bilddatensätze, Bilddateien und die optionale Audiodatei. Dieser Vorgang ist im Gegensatz zum Soft Delete einer vollständigen Route nicht wiederherstellbar. ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7 ``` ```json { "id": 7, "routeId": 1, "deleted": true } ``` ## 8. Bilder eines POIs einzeln verwalten Die Route und der POI sind Bestandteil jedes Bildpfades. Dadurch ist die Zuordnung eindeutig und beim Upload muss keine zusätzliche `poiId` übergeben werden. ### Gemeinsame Pfadparameter | Parameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `routeId` | positive Ganzzahl | ja | ID der aktiven Route | | `poiId` | positive Ganzzahl | ja | ID eines POIs, der zu dieser Route gehört | | `pictureId` | positive Ganzzahl | nur bei Einzelressourcen | ID des Bildes, das zu diesem POI gehört | ### `GET /api/routes/:routeId/pois/:poiId/pictures` Liefert alle Bilder des angegebenen POIs in Diashow-Reihenfolge. ```bash curl http://127.0.0.1:47145/api/routes/1/pois/7/pictures ``` ```json { "pictures": [ { "id": 15, "routeId": 1, "poiId": 7, "poiTitle": "Die alte Eiche", "caption": "Blick auf die Baumkrone", "sequence": 0, "url": "/api/routes/1/pois/7/pictures/15", "createdAt": "2026-06-16 12:30:00" } ] } ``` ### `GET /api/routes/:routeId/pois/:poiId/pictures/:pictureId` Liefert standardmäßig die Bilddatei direkt aus. Genau dieser Pfad wird im Feld `url` der Bildressource und unter `pois[].images[].url` ausgegeben. Bild speichern: ```bash curl http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \ --output bild-15.jpg ``` Metadaten statt Dateidaten abrufen: ```bash curl --get http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \ --data-urlencode 'metadata=true' ``` | Query-Parameter | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `metadata` | Boolean | nein | `false` | bei `true` JSON-Metadaten statt der Bilddatei liefern | Die API antwortet mit `404`, wenn Route, POI oder Bild nicht zusammengehören. ### `POST /api/routes/:routeId/pois/:poiId/pictures` Lädt genau ein Bild für den im Pfad angegebenen POI hoch. Zulässig sind zwei Übertragungsformen. #### Variante A: `multipart/form-data` | Feld | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `picture` | Datei | ja | – | JPEG-, PNG- oder WebP-Datei; der Typ wird aus dem Inhalt erkannt | | `caption` | Text | nein | leer | sichtbare Bildbeschreibung und Grundlage für den Alternativtext | | `sequence` | nichtnegative Ganzzahl | nein | nächster freier Wert des POIs | Reihenfolge in der Diashow | ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/pictures \ -F 'caption=Blick auf die Baumkrone' \ -F 'sequence=0' \ -F 'picture=@eiche.jpg' ``` #### Variante B: JSON mit Base64 Empfohlener Content-Type: `application/json`. Der Server akzeptiert zusätzlich `text/json`. ```json { "caption": "Blick auf die Baumkrone", "sequence": 0, "picture": { "filename": "eiche.jpg", "base64": "/9j/4AAQSkZJRgABAQ..." } } ``` Beispiel mit `text/json` und einer separat erzeugten Payload-Datei: ```bash base64 < eiche.jpg | tr -d '\n' > eiche.jpg.b64 jq -n \ --arg caption 'Blick auf die Baumkrone' \ --argjson sequence 0 \ --rawfile data eiche.jpg.b64 \ '{ caption: $caption, sequence: $sequence, picture: { filename: "eiche.jpg", base64: $data } }' > picture.json curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/pictures \ -H 'Content-Type: text/json' \ --data-binary @picture.json ``` Alternativ kann eine Data-URL übertragen werden: ```json { "caption": "Blick auf die Baumkrone", "picture": { "filename": "eiche.png", "dataUrl": "data:image/png;base64,iVBORw0KGgoAAA..." } } ``` Ein Request mit `-d '{}'` schlägt mit `400 Bad Request` fehl, weil weder eine Multipart-Datei noch ein JSON-Dateiobjekt enthalten ist. Erfolg: `201 Created` und `Location: /api/routes/1/pois/7/pictures/`. ### `PUT /api/routes/:routeId/pois/:poiId/pictures/:pictureId` Aktualisiert Metadaten und kann optional die Datei ersetzen. Nicht übergebene Metadaten bleiben erhalten. Ein Bild kann über diesen Endpunkt nicht einem anderen POI zugeordnet werden; dafür muss es beim bisherigen POI gelöscht und beim Ziel-POI neu angelegt werden. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `picture` | Multipart-Datei oder JSON-Dateiobjekt | nein | ersetzt die bisherige Bilddatei | | `caption` | Text | nein | neue Bildbeschreibung; leerer Text entfernt die Beschreibung | | `sequence` | nichtnegative Ganzzahl | nein | neue Position in der Diashow | Nur Metadaten per JSON ändern: ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \ -H 'Content-Type: application/json' \ -d '{ "caption": "Nahaufnahme der Eichenblätter", "sequence": 1 }' ``` Datei und Metadaten per Multipart ersetzen: ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \ -F 'caption=Neue Aufnahme der Eiche' \ -F 'sequence=2' \ -F 'picture=@eiche-neu.webp' ``` Datei und Metadaten per JSON ersetzen: ```json { "caption": "Neue Aufnahme der Eiche", "sequence": 2, "picture": { "filename": "eiche-neu.webp", "base64": "UklGRiQAAABXRUJQVlA4..." } } ``` ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \ -H 'Content-Type: application/json' \ --data-binary @picture-update.json ``` Wird eine Datei ersetzt, entfernt der Server die bisherige Datei nach erfolgreicher Datenbankaktualisierung. ### `DELETE /api/routes/:routeId/pois/:poiId/pictures/:pictureId` Entfernt Bilddatensatz und Datei. ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 ``` ```json { "id": 15, "routeId": 1, "poiId": 7, "deleted": true } ``` ## 9. Audiodatei eines POIs verwalten Pro POI ist höchstens eine Audiodatei vorgesehen. Daher ist `/audio` selbst die Einzelressource; eine zusätzliche Audio-ID ist nicht erforderlich. ### Gemeinsame Pfadparameter | Parameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `routeId` | positive Ganzzahl | ja | ID der aktiven Route | | `poiId` | positive Ganzzahl | ja | ID eines POIs, der zu dieser Route gehört | ### `GET /api/routes/:routeId/pois/:poiId/audio` Liefert standardmäßig die Audiodatei des POIs direkt aus. Genau dieser Pfad wird in `audioUrl` und im Feld `url` der Audioressource ausgegeben. Der Endpunkt unterstützt HTTP-Range-Anfragen, damit Browser innerhalb der Audiodatei springen können. Audiodatei speichern: ```bash curl http://127.0.0.1:47145/api/routes/1/pois/7/audio \ --output ansage-7.mp3 ``` Metadaten statt Dateidaten abrufen: ```bash curl --get http://127.0.0.1:47145/api/routes/1/pois/7/audio \ --data-urlencode 'metadata=true' ``` | Query-Parameter | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `metadata` | Boolean | nein | `false` | bei `true` JSON-Metadaten statt der Audiodatei liefern | Antwortet mit `404`, wenn Route und POI nicht zusammengehören oder der POI keine Audiodatei besitzt. ### `POST /api/routes/:routeId/pois/:poiId/audio` Legt die Audiodatei des im Pfad angegebenen POIs an. Zulässig sind `multipart/form-data` und JSON mit Base64. #### Variante A: `multipart/form-data` | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `audio` | Datei | ja | MP3-, MP4/M4A-, AAC-, Ogg-, WAV- oder WebM-Datei; der Typ wird aus dem Inhalt erkannt | ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/audio \ -F 'audio=@ansage.mp3' ``` #### Variante B: JSON mit Base64 ```json { "audio": { "filename": "ansage.mp3", "base64": "SUQzBAAAAAAAI1RTU0UAAA..." } } ``` ```bash base64 < ansage.mp3 | tr -d '\n' > ansage.mp3.b64 jq -n --rawfile data ansage.mp3.b64 \ '{ audio: { filename: "ansage.mp3", base64: $data } }' > audio.json curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/audio \ -H 'Content-Type: application/json' \ --data-binary @audio.json ``` Auch hier wird `Content-Type: text/json` akzeptiert. Ein leeres `{}` enthält keine Audiodatei und liefert `400 Bad Request`. Erfolg: `201 Created` und `Location: /api/routes/1/pois/7/audio`. Existiert bereits eine Audiodatei, antwortet der Server mit `409`. Zum Ersetzen ist `PUT` zu verwenden. ### `PUT /api/routes/:routeId/pois/:poiId/audio` Ersetzt die vorhandene Audiodatei des POIs. Die Datei kann als Multipart-Upload oder als JSON-Dateiobjekt übertragen werden. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `audio` | Multipart-Datei oder JSON-Dateiobjekt | ja | neue Audiodatei | Multipart-Beispiel: ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/audio \ -F 'audio=@ansage-neu.ogg' ``` JSON-Beispiel: ```json { "audio": { "filename": "ansage-neu.ogg", "base64": "T2dnUwACAAAAAAAAAAB..." } } ``` ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/audio \ -H 'Content-Type: text/json' \ --data-binary @audio-update.json ``` Der Server entfernt die bisherige Datei nach erfolgreicher Aktualisierung. Besitzt der POI noch keine Audiodatei, antwortet der Server mit `404`; zum erstmaligen Anlegen ist `POST` zu verwenden. ### `DELETE /api/routes/:routeId/pois/:poiId/audio` Entfernt die Audiodatei und setzt `audioUrl` des POIs auf `null`. ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7/audio ``` ```json { "routeId": 1, "poiId": 7, "deleted": true } ``` ## 10. Route weich löschen und wiederherstellen ### `DELETE /api/routes/:id` Markiert die Route als gelöscht und verschiebt das vollständige Streckenverzeichnis mit GPX, Bildern und Audio nach `storage/trash/routes/`. Die Datenbankeinträge bleiben erhalten und ihre Pfade werden auf den Papierkorb umgeschrieben. ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1 ``` ```json { "route": { "id": 1, "status": "deleted", "gpxUrl": null }, "softDeleted": true } ``` ### `POST /api/routes/:id/restore` Verschiebt eine gelöschte Route zurück in den aktiven Speicher und schreibt alle GPX-, Bild- und Audiopfade zurück. Parameter: keine. ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/restore ``` ## 11. Dateien über REST ausliefern Die API veröffentlicht keine internen Speicherpfade und keine `/media/...`-Adressen. Alle in JSON ausgegebenen Datei-URLs verweisen auf stabile REST-Endpunkte: ```text GET /api/routes/1/gpx GET /api/routes/1/pois/7/pictures/15 GET /api/routes/1/pois/7/audio ``` Die Zuordnung lautet: | JSON-Feld | Datei-Endpunkt | |---|---| | `route.gpxUrl` | `/api/routes/:id/gpx` | | `poi.audioUrl` | `/api/routes/:routeId/pois/:poiId/audio` | | `poi.images[].url` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | | `picture.url` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | | `audio.url` | `/api/routes/:routeId/pois/:poiId/audio` | Die Dateinamen und relativen Pfade unter `storage/` bleiben ausschließlich interne Implementierungsdetails. Dateien gelöschter Routen liegen im Papierkorb und sind über keinen Datei-Endpunkt erreichbar. ## 12. Python-Werkzeuge Unter `tools/python/` liegen interaktive Skripte für Anlegen, Ändern, Erweitern, Wiederherstellen und Löschen von Routen, POIs, Bildern und Audio. Sie verwenden ausschließlich die Python-Standardbibliothek. Fehlende Parameter werden abgefragt. Antwortet ein vorgeschalteter Webserver mit HTTP 401, fragt die gemeinsame Request-Schicht Benutzername und Passwort ab und wiederholt den ursprünglichen Request. Details und Aufrufbeispiele stehen in [`tools/python/README.md`](../tools/python/README.md).