/pictures und /audio endpoints moved under /poi
This commit is contained in:
+40
-27
@@ -7,7 +7,7 @@ import {
|
||||
} from '../services/routes-service.js';
|
||||
import {
|
||||
listPictures, getPicture, getPictureFile, createPicture, updatePicture, deletePicture,
|
||||
listAudio, getAudio, getAudioFile, createAudio, updateAudio, deleteAudio
|
||||
getAudio, getAudioFile, createAudio, updateAudio, deleteAudio
|
||||
} from '../services/media-service.js';
|
||||
|
||||
const numberOrUndefined = value => {
|
||||
@@ -90,41 +90,52 @@ export function createApiRouter(db) {
|
||||
res.json(updatePoi(db, Number(req.params.id), req.body));
|
||||
});
|
||||
|
||||
api.get('/routes/:id/pictures', (req, res) => {
|
||||
res.json({ pictures: listPictures(db, Number(req.params.id), { poiId: req.query.poiId }) });
|
||||
api.get('/routes/:routeId/pois/:poiId/pictures', (req, res) => {
|
||||
res.json({ pictures: listPictures(db, Number(req.params.routeId), Number(req.params.poiId)) });
|
||||
});
|
||||
|
||||
api.get('/routes/:id/pictures/:pictureId', (req, res) => {
|
||||
const routeId = Number(req.params.id);
|
||||
api.get('/routes/:routeId/pois/:poiId/pictures/:pictureId', (req, res) => {
|
||||
const routeId = Number(req.params.routeId);
|
||||
const poiId = Number(req.params.poiId);
|
||||
const pictureId = Number(req.params.pictureId);
|
||||
if (req.query.metadata === 'true') {
|
||||
res.json(getPicture(db, routeId, pictureId));
|
||||
res.json(getPicture(db, routeId, poiId, pictureId));
|
||||
return;
|
||||
}
|
||||
sendFileResource(res, getPictureFile(db, routeId, pictureId));
|
||||
sendFileResource(res, getPictureFile(db, routeId, poiId, pictureId));
|
||||
});
|
||||
|
||||
api.post('/routes/:id/pictures', upload.single('picture'), (req, res) => {
|
||||
const picture = createPicture(db, Number(req.params.id), req.body, req.file);
|
||||
api.post('/routes/:routeId/pois/:poiId/pictures', upload.single('picture'), (req, res) => {
|
||||
const routeId = Number(req.params.routeId);
|
||||
const poiId = Number(req.params.poiId);
|
||||
const picture = createPicture(db, routeId, poiId, req.body, req.file);
|
||||
res.status(201)
|
||||
.location(`/api/routes/${req.params.id}/pictures/${picture.id}`)
|
||||
.location(`/api/routes/${routeId}/pois/${poiId}/pictures/${picture.id}`)
|
||||
.json(picture);
|
||||
});
|
||||
|
||||
api.put('/routes/:id/pictures/:pictureId', upload.single('picture'), (req, res) => {
|
||||
res.json(updatePicture(db, Number(req.params.id), Number(req.params.pictureId), req.body, req.file));
|
||||
api.put('/routes/:routeId/pois/:poiId/pictures/:pictureId', upload.single('picture'), (req, res) => {
|
||||
res.json(updatePicture(
|
||||
db,
|
||||
Number(req.params.routeId),
|
||||
Number(req.params.poiId),
|
||||
Number(req.params.pictureId),
|
||||
req.body,
|
||||
req.file
|
||||
));
|
||||
});
|
||||
|
||||
api.delete('/routes/:id/pictures/:pictureId', (req, res) => {
|
||||
res.json(deletePicture(db, Number(req.params.id), Number(req.params.pictureId)));
|
||||
api.delete('/routes/:routeId/pois/:poiId/pictures/:pictureId', (req, res) => {
|
||||
res.json(deletePicture(
|
||||
db,
|
||||
Number(req.params.routeId),
|
||||
Number(req.params.poiId),
|
||||
Number(req.params.pictureId)
|
||||
));
|
||||
});
|
||||
|
||||
api.get('/routes/:id/audio', (req, res) => {
|
||||
res.json({ audio: listAudio(db, Number(req.params.id), { poiId: req.query.poiId }) });
|
||||
});
|
||||
|
||||
api.get('/routes/:id/audio/:poiId', (req, res) => {
|
||||
const routeId = Number(req.params.id);
|
||||
api.get('/routes/:routeId/pois/:poiId/audio', (req, res) => {
|
||||
const routeId = Number(req.params.routeId);
|
||||
const poiId = Number(req.params.poiId);
|
||||
if (req.query.metadata === 'true') {
|
||||
res.json(getAudio(db, routeId, poiId));
|
||||
@@ -133,19 +144,21 @@ export function createApiRouter(db) {
|
||||
sendFileResource(res, getAudioFile(db, routeId, poiId));
|
||||
});
|
||||
|
||||
api.post('/routes/:id/audio', upload.single('audio'), (req, res) => {
|
||||
const audio = createAudio(db, Number(req.params.id), req.body, req.file);
|
||||
api.post('/routes/:routeId/pois/:poiId/audio', upload.single('audio'), (req, res) => {
|
||||
const routeId = Number(req.params.routeId);
|
||||
const poiId = Number(req.params.poiId);
|
||||
const audio = createAudio(db, routeId, poiId, req.file);
|
||||
res.status(201)
|
||||
.location(`/api/routes/${req.params.id}/audio/${audio.poiId}`)
|
||||
.location(`/api/routes/${routeId}/pois/${poiId}/audio`)
|
||||
.json(audio);
|
||||
});
|
||||
|
||||
api.put('/routes/:id/audio/:poiId', upload.single('audio'), (req, res) => {
|
||||
res.json(updateAudio(db, Number(req.params.id), Number(req.params.poiId), req.file));
|
||||
api.put('/routes/:routeId/pois/:poiId/audio', upload.single('audio'), (req, res) => {
|
||||
res.json(updateAudio(db, Number(req.params.routeId), Number(req.params.poiId), req.file));
|
||||
});
|
||||
|
||||
api.delete('/routes/:id/audio/:poiId', (req, res) => {
|
||||
res.json(deleteAudio(db, Number(req.params.id), Number(req.params.poiId)));
|
||||
api.delete('/routes/:routeId/pois/:poiId/audio', (req, res) => {
|
||||
res.json(deleteAudio(db, Number(req.params.routeId), Number(req.params.poiId)));
|
||||
});
|
||||
|
||||
api.delete('/routes/:id', (req, res) => {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user