49 lines
1.7 KiB
TypeScript
49 lines
1.7 KiB
TypeScript
/**
|
|
* Serves an audio file dump's waveform, a sub-resource of its dump, for the
|
|
* player to seek on. The frontend builds the URL with `dumpWaveformUrl`.
|
|
* lib/ffmpeg.ts makes it from the upload on first request and keeps it, so a
|
|
* listener downloads a few hundred bytes of peaks rather than the file a
|
|
* second time to decode it.
|
|
*
|
|
* - `GET /api/dumps/:dumpId/waveform?v=&token=` — public; a private dump's
|
|
* waveform needs its poster's media `token`. Answers a `DumpWaveform`; 404
|
|
* when the dump isn't an audio file or its audio can't be read.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { Router } from "@oak/oak";
|
|
import { APIException } from "../lib/api-exception.ts";
|
|
import { DUMPS_DIR } from "../config.ts";
|
|
import { resolveMediaDump } from "../lib/dump-media.ts";
|
|
import {
|
|
audioWaveform,
|
|
ensureGenerated,
|
|
generatedPaths,
|
|
} from "../lib/ffmpeg.ts";
|
|
import { serveFile } from "../lib/serve-file.ts";
|
|
|
|
/**
|
|
* Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware.
|
|
* An `APIException` thrown by a handler becomes the JSON error response there.
|
|
*/
|
|
const router: Router = new Router({ prefix: "/api/dumps" });
|
|
|
|
router.get("/:dumpId/waveform", async (ctx) => {
|
|
const { dump, cacheControl } = await resolveMediaDump(ctx, ctx.params.dumpId);
|
|
if (dump.kind !== "file" || !dump.fileMime?.startsWith("audio/")) {
|
|
throw new APIException(404, "No waveform for this dump");
|
|
}
|
|
|
|
const outPath = generatedPaths(dump.id).waveform;
|
|
const srcPath = `${DUMPS_DIR}/${dump.id}`;
|
|
if (
|
|
!await ensureGenerated(outPath, () => audioWaveform(srcPath, outPath)) ||
|
|
!await serveFile(ctx, outPath, { mime: "application/json", cacheControl })
|
|
) {
|
|
throw new APIException(404, "Could not generate waveform");
|
|
}
|
|
});
|
|
|
|
export default router;
|