Something went wrong. Try again.
A decentralized music tracking and discovery platform built on AT Protocol ๐ต rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495/** * Key/BPM/fingerprint analysis for uploaded audio. * * Runs the @rocksky/analysis native (Rust/Neon) binding on the libuv worker * pool and fills tracks.key / tracks.bpm / tracks.acoustid_fingerprint. All * three come out of one decode pass, so the fingerprint costs nothing beyond * what key and tempo already pay for. * * Fire-and-forget by design: the upload response never waits on this, and * nothing here can fail an upload โ a missing binding, a corrupt file or a DB * hiccup all end as a warning log. */
import { consola } from "consola";import { ctx } from "context";import { eq, sql } from "drizzle-orm";import tables from "schema";
type AnalysisModule = typeof import("@rocksky/analysis");
// Lazy-loaded so the API still boots when the native module hasn't been// built on this machine; analysis is then skipped, never crashed on.let analysisModule: AnalysisModule | null | undefined;
export async function loadAnalysisModule(): Promise<AnalysisModule | null> { if (analysisModule !== undefined) return analysisModule; try { analysisModule = await import("@rocksky/analysis"); } catch (e) { analysisModule = null; consola.warn( "[analysis] @rocksky/analysis native binding unavailable โ key/bpm analysis disabled. Build it with `bun run build` in crates/analysis-node.", e, ); } return analysisModule;}
/** * Persist key/bpm/fingerprint on a track, filling only what is still missing โ * a track is shared across users and sources, and an earlier answer is not * overwritten. */export async function storeTrackAnalysis( trackId: string, key: string | null, bpm: number | null, fingerprint: string | null = null,): Promise<void> { if (key == null && bpm == null && fingerprint == null) return; await ctx.db .update(tables.tracks) .set({ key: sql`coalesce(${tables.tracks.key}, ${key})`, bpm: sql`coalesce(${tables.tracks.bpm}, ${bpm})`, acoustidFingerprint: sql`coalesce(${tables.tracks.acoustidFingerprint}, ${fingerprint})`, }) .where(eq(tables.tracks.id, trackId));}
/** * Analyze one upload and store the result. Called without await from the * upload route; the returned promise only ever rejects on programmer error, * expected failures are logged and swallowed here. */export async function analyzeUploadAudio( buf: Buffer, extensionHint: string, trackId: string,): Promise<void> { try { const analysis = await loadAnalysisModule(); if (!analysis) return;
const started = Date.now(); const result = await analysis.analyze(buf, extensionHint); await storeTrackAnalysis( trackId, result.key, result.bpm, result.fingerprint, );
consola.info( `[analysis] track ${trackId}: key=${result.key ?? "?"} bpm=${ result.bpm?.toFixed(1) ?? "?" } fingerprint=${result.fingerprint ? "yes" : "no"} (${( (Date.now() - started) / 1000 ).toFixed(1)}s)`, ); } catch (e) { consola.warn(`[analysis] track ${trackId} failed:`, e); }}