/pictures und /audio endpoints moved under /poi

This commit is contained in:
Florian Zumpe
2026-06-17 01:14:19 +02:00
parent 833b5ad3a4
commit 5c85a0194b
9 changed files with 225 additions and 267 deletions
+55 -92
View File
@@ -24,16 +24,8 @@ const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
const AUDIO_EXTENSIONS = new Set(['.mp3', '.mp4', '.m4a', '.aac', '.ogg', '.wav', '.webm']);
const uniqueFilename = original => `${crypto.randomUUID()}${path.extname(original).toLowerCase()}`;
const pictureUrl = (routeId, pictureId) => `/api/routes/${routeId}/pictures/${pictureId}`;
const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/audio/${poiId}`;
function requirePositiveInteger(value, name) {
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new HttpError(400, `${name} muss eine positive Ganzzahl sein.`);
}
return parsed;
}
const pictureUrl = (routeId, poiId, pictureId) => `/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}`;
const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/pois/${poiId}/audio`;
function optionalSequence(value, fallback) {
if (value == null || value === '') return fallback;
@@ -44,15 +36,9 @@ function optionalSequence(value, fallback) {
return parsed;
}
function activeRoute(db, routeId) {
const route = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(routeId);
if (!route) throw new HttpError(404, 'Strecke nicht gefunden.');
return route;
}
function poiOnRoute(db, routeId, poiId) {
const poi = db.prepare(`
SELECT p.id, p.route_id, p.title, p.audio_path
SELECT p.id, p.route_id, p.title, p.audio_path, p.updated_at
FROM pois p
JOIN routes r ON r.id = p.route_id
WHERE p.id = ? AND p.route_id = ? AND r.status = 'active'
@@ -85,7 +71,7 @@ function pictureFromRow(row) {
poiTitle: row.poi_title,
caption: row.caption,
sequence: row.sequence,
url: pictureUrl(row.route_id, row.id),
url: pictureUrl(row.route_id, row.poi_id, row.id),
createdAt: row.created_at
};
}
@@ -98,40 +84,42 @@ const pictureSelect = `
JOIN routes r ON r.id = p.route_id
`;
export function listPictures(db, routeId, { poiId } = {}) {
activeRoute(db, routeId);
const filterPoiId = poiId == null || poiId === '' ? null : requirePositiveInteger(poiId, 'poiId');
const rows = filterPoiId == null
? db.prepare(`${pictureSelect} WHERE p.route_id = ? AND r.status = 'active' ORDER BY p.sequence, p.id, i.sequence, i.id`).all(routeId)
: db.prepare(`${pictureSelect} WHERE p.route_id = ? AND p.id = ? AND r.status = 'active' ORDER BY i.sequence, i.id`).all(routeId, filterPoiId);
return rows.map(pictureFromRow);
export function listPictures(db, routeId, poiId) {
poiOnRoute(db, routeId, poiId);
return db.prepare(`${pictureSelect}
WHERE p.route_id = ? AND p.id = ? AND r.status = 'active'
ORDER BY i.sequence, i.id`)
.all(routeId, poiId)
.map(pictureFromRow);
}
export function getPicture(db, routeId, pictureId) {
activeRoute(db, routeId);
const row = db.prepare(`${pictureSelect} WHERE p.route_id = ? AND i.id = ? AND r.status = 'active'`).get(routeId, pictureId);
if (!row) throw new HttpError(404, 'Bild auf dieser Strecke nicht gefunden.');
return pictureFromRow(row);
function pictureRow(db, routeId, poiId, pictureId) {
poiOnRoute(db, routeId, poiId);
const row = db.prepare(`${pictureSelect}
WHERE p.route_id = ? AND p.id = ? AND i.id = ? AND r.status = 'active'`)
.get(routeId, poiId, pictureId);
if (!row) throw new HttpError(404, 'Bild für diesen POI nicht gefunden.');
return row;
}
export function getPictureFile(db, routeId, pictureId) {
activeRoute(db, routeId);
const row = db.prepare(`${pictureSelect} WHERE p.route_id = ? AND i.id = ? AND r.status = 'active'`).get(routeId, pictureId);
if (!row) throw new HttpError(404, 'Bild auf dieser Strecke nicht gefunden.');
export function getPicture(db, routeId, poiId, pictureId) {
return pictureFromRow(pictureRow(db, routeId, poiId, pictureId));
}
export function getPictureFile(db, routeId, poiId, pictureId) {
const row = pictureRow(db, routeId, poiId, pictureId);
return {
absolutePath: resolveStoredFile(row.path),
filename: path.basename(row.path)
};
}
export function createPicture(db, routeId, fields, file) {
export function createPicture(db, routeId, poiId, fields, file) {
let relativePath;
try {
activeRoute(db, routeId);
poiOnRoute(db, routeId, poiId);
validateUpload(file, 'picture');
const poiId = requirePositiveInteger(fields.poiId, 'poiId');
poiOnRoute(db, routeId, poiId);
const sequence = optionalSequence(fields.sequence,
db.prepare('SELECT COALESCE(MAX(sequence), -1) + 1 AS value FROM poi_images WHERE poi_id = ?').get(poiId).value);
const caption = String(fields.caption ?? '');
@@ -140,7 +128,7 @@ export function createPicture(db, routeId, fields, file) {
relativePath = moveUploadedFile(file, path.join(root, 'images', uniqueFilename(file.originalname)));
const result = db.prepare('INSERT INTO poi_images (poi_id, path, caption, sequence) VALUES (?, ?, ?, ?)')
.run(poiId, relativePath, caption, sequence);
return getPicture(db, routeId, Number(result.lastInsertRowid));
return getPicture(db, routeId, poiId, Number(result.lastInsertRowid));
} catch (error) {
if (relativePath) removeStoredFile(relativePath);
else removeUpload(file);
@@ -148,17 +136,13 @@ export function createPicture(db, routeId, fields, file) {
}
}
export function updatePicture(db, routeId, pictureId, fields, file) {
export function updatePicture(db, routeId, poiId, pictureId, fields, file) {
let newPath;
let oldPath;
try {
const existing = getPicture(db, routeId, pictureId);
const row = db.prepare(`${pictureSelect} WHERE p.route_id = ? AND i.id = ? AND r.status = 'active'`).get(routeId, pictureId);
const existing = getPicture(db, routeId, poiId, pictureId);
const row = pictureRow(db, routeId, poiId, pictureId);
oldPath = row.path;
const poiId = fields.poiId == null || fields.poiId === ''
? existing.poiId
: requirePositiveInteger(fields.poiId, 'poiId');
poiOnRoute(db, routeId, poiId);
const caption = fields.caption == null ? existing.caption : String(fields.caption);
const sequence = optionalSequence(fields.sequence, existing.sequence);
@@ -169,11 +153,11 @@ export function updatePicture(db, routeId, pictureId, fields, file) {
newPath = moveUploadedFile(file, path.join(root, 'images', uniqueFilename(file.originalname)));
}
db.prepare('UPDATE poi_images SET poi_id = ?, path = ?, caption = ?, sequence = ? WHERE id = ?')
.run(poiId, newPath, caption, sequence, pictureId);
db.prepare('UPDATE poi_images SET path = ?, caption = ?, sequence = ? WHERE id = ? AND poi_id = ?')
.run(newPath, caption, sequence, pictureId, poiId);
if (file && newPath !== oldPath) removeStoredFile(oldPath);
return getPicture(db, routeId, pictureId);
return getPicture(db, routeId, poiId, pictureId);
} catch (error) {
if (file) {
if (newPath && newPath !== oldPath) removeStoredFile(newPath);
@@ -183,56 +167,36 @@ export function updatePicture(db, routeId, pictureId, fields, file) {
}
}
export function deletePicture(db, routeId, pictureId) {
const picture = getPicture(db, routeId, pictureId);
const row = db.prepare(`${pictureSelect} WHERE p.route_id = ? AND i.id = ? AND r.status = 'active'`).get(routeId, pictureId);
db.prepare('DELETE FROM poi_images WHERE id = ?').run(pictureId);
export function deletePicture(db, routeId, poiId, pictureId) {
const picture = getPicture(db, routeId, poiId, pictureId);
const row = pictureRow(db, routeId, poiId, pictureId);
db.prepare('DELETE FROM poi_images WHERE id = ? AND poi_id = ?').run(pictureId, poiId);
removeStoredFile(row.path);
return { id: picture.id, routeId: picture.routeId, poiId: picture.poiId, deleted: true };
}
function audioFromRow(row) {
function audioFromPoi(poi) {
return {
routeId: row.route_id,
poiId: row.poi_id,
poiTitle: row.poi_title,
url: audioUrl(row.route_id, row.poi_id),
updatedAt: row.updated_at
routeId: poi.route_id,
poiId: poi.id,
poiTitle: poi.title,
url: audioUrl(poi.route_id, poi.id),
updatedAt: poi.updated_at
};
}
const audioSelect = `
SELECT p.id AS poi_id, p.route_id, p.title AS poi_title,
p.audio_path, p.updated_at
FROM pois p
JOIN routes r ON r.id = p.route_id
`;
export function listAudio(db, routeId, { poiId } = {}) {
activeRoute(db, routeId);
const filterPoiId = poiId == null || poiId === '' ? null : requirePositiveInteger(poiId, 'poiId');
const rows = filterPoiId == null
? db.prepare(`${audioSelect} WHERE p.route_id = ? AND p.audio_path IS NOT NULL AND r.status = 'active' ORDER BY p.sequence, p.id`).all(routeId)
: db.prepare(`${audioSelect} WHERE p.route_id = ? AND p.id = ? AND p.audio_path IS NOT NULL AND r.status = 'active'`).all(routeId, filterPoiId);
return rows.map(audioFromRow);
}
export function getAudio(db, routeId, poiId) {
activeRoute(db, routeId);
const row = db.prepare(`${audioSelect} WHERE p.route_id = ? AND p.id = ? AND p.audio_path IS NOT NULL AND r.status = 'active'`)
.get(routeId, poiId);
if (!row) throw new HttpError(404, 'Audiodatei für diesen POI nicht gefunden.');
return audioFromRow(row);
const poi = poiOnRoute(db, routeId, poiId);
if (!poi.audio_path) throw new HttpError(404, 'Audiodatei für diesen POI nicht gefunden.');
return audioFromPoi(poi);
}
export function getAudioFile(db, routeId, poiId) {
activeRoute(db, routeId);
const row = db.prepare(`${audioSelect} WHERE p.route_id = ? AND p.id = ? AND p.audio_path IS NOT NULL AND r.status = 'active'`)
.get(routeId, poiId);
if (!row) throw new HttpError(404, 'Audiodatei für diesen POI nicht gefunden.');
const poi = poiOnRoute(db, routeId, poiId);
if (!poi.audio_path) throw new HttpError(404, 'Audiodatei für diesen POI nicht gefunden.');
return {
absolutePath: resolveStoredFile(row.audio_path),
filename: path.basename(row.audio_path)
absolutePath: resolveStoredFile(poi.audio_path),
filename: path.basename(poi.audio_path)
};
}
@@ -240,7 +204,6 @@ function storeAudio(db, routeId, poiId, file, { requireAbsent, requireExisting }
let relativePath;
let oldPath;
try {
activeRoute(db, routeId);
validateUpload(file, 'audio');
const poi = poiOnRoute(db, routeId, poiId);
oldPath = poi.audio_path;
@@ -254,8 +217,8 @@ function storeAudio(db, routeId, poiId, file, { requireAbsent, requireExisting }
const root = ensureRouteDirectories(routeId);
relativePath = moveUploadedFile(file, path.join(root, 'audio', uniqueFilename(file.originalname)));
db.prepare('UPDATE pois SET audio_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
.run(relativePath, poiId);
db.prepare('UPDATE pois SET audio_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND route_id = ?')
.run(relativePath, poiId, routeId);
if (oldPath && oldPath !== relativePath) removeStoredFile(oldPath);
return getAudio(db, routeId, poiId);
@@ -266,8 +229,7 @@ function storeAudio(db, routeId, poiId, file, { requireAbsent, requireExisting }
}
}
export function createAudio(db, routeId, fields, file) {
const poiId = requirePositiveInteger(fields.poiId, 'poiId');
export function createAudio(db, routeId, poiId, file) {
return storeAudio(db, routeId, poiId, file, { requireAbsent: true, requireExisting: false });
}
@@ -278,7 +240,8 @@ export function updateAudio(db, routeId, poiId, file) {
export function deleteAudio(db, routeId, poiId) {
const audio = getAudio(db, routeId, poiId);
const poi = poiOnRoute(db, routeId, poiId);
db.prepare('UPDATE pois SET audio_path = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(poiId);
db.prepare('UPDATE pois SET audio_path = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND route_id = ?')
.run(poiId, routeId);
removeStoredFile(poi.audio_path);
return { routeId: audio.routeId, poiId: audio.poiId, deleted: true };
}
+3 -3
View File
@@ -12,8 +12,8 @@ import {
const slugify = value => value.toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 80) || `route-${Date.now()}`;
const routeGpxUrl = routeId => `/api/routes/${routeId}/gpx`;
const pictureUrl = (routeId, pictureId) => `/api/routes/${routeId}/pictures/${pictureId}`;
const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/audio/${poiId}`;
const pictureUrl = (routeId, poiId, pictureId) => `/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}`;
const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/pois/${poiId}/audio`;
function rowToRoute(row, includeDeleted = false) {
if (!row || (!includeDeleted && row.status !== 'active')) return null;
return {
@@ -146,7 +146,7 @@ export function listPois(db, routeId) {
id: image.id,
caption: image.caption,
sequence: image.sequence,
url: pictureUrl(poi.route_id, image.id)
url: pictureUrl(poi.route_id, poi.id, image.id)
}))
}));
}