Files

167 lines
5.1 KiB
JavaScript

const express = require('express');
const router = express.Router();
const multer = require('multer');
const fs = require('fs');
const os = require('os');
const path = require('path');
const axios = require('axios');
const FormData = require('form-data');
const crypto = require('crypto');
const db = require('../db');
// Upload audio to system temp folder
const upload = multer({ dest: os.tmpdir() });
// ACRCloud Identification helper
async function identifyACRCloud(buffer) {
const host = process.env.ACRCLOUD_HOST;
const accessKey = process.env.ACRCLOUD_ACCESS_KEY;
const accessSecret = process.env.ACRCLOUD_ACCESS_SECRET;
if (!host || !accessKey || !accessSecret) return null;
const timestamp = Math.floor(Date.now() / 1000);
const stringToSign = [
'POST',
'/v1/identify',
accessKey,
'audio',
'1',
timestamp
].join('\n');
const signature = crypto
.createHmac('sha1', accessSecret)
.update(Buffer.from(stringToSign, 'utf-8'))
.digest()
.toString('base64');
const form = new FormData();
form.append('sample', buffer, { filename: 'sample.webm' });
form.append('sample_bytes', buffer.length);
form.append('access_key', accessKey);
form.append('data_type', 'audio');
form.append('signature_version', '1');
form.append('signature', signature);
form.append('timestamp', timestamp);
const url = host.startsWith('http') ? `${host}/v1/identify` : `https://${host}/v1/identify`;
const response = await axios.post(url, form, {
headers: form.getHeaders(),
timeout: 8000
});
const data = response.data;
if (data.status && data.status.code === 0 && data.metadata && data.metadata.music && data.metadata.music.length > 0) {
const music = data.metadata.music[0];
const title = music.title;
const artist = music.artists ? music.artists.map(a => a.name).join(' & ') : 'Unknown Artist';
const score = music.score ? `${music.score}%` : '95%';
return { title, artist, matchScore: score };
}
return null;
}
// AudD Identification helper
async function identifyAudD(buffer) {
const token = process.env.AUDD_API_TOKEN || process.env.AUDD_API_KEY;
if (!token) return null;
const form = new FormData();
form.append('file', buffer, { filename: 'sample.webm' });
form.append('api_token', token);
form.append('return', 'apple_music,spotify');
const response = await axios.post('https://api.audd.io/', form, {
headers: form.getHeaders(),
timeout: 8000
});
if (response.data && response.data.status === 'success' && response.data.result) {
const r = response.data.result;
return {
title: r.title,
artist: r.artist,
matchScore: '98%',
cover_url: r.spotify && r.spotify.images && r.spotify.images[0] ? r.spotify.images[0].url : null
};
}
return null;
}
// POST /api/recognize
router.post('/', upload.single('audio'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No audio file uploaded.' });
}
const filePath = req.file.path;
try {
const audioBuffer = fs.readFileSync(filePath);
// 1. Check configured providers
const hasAcr = !!(process.env.ACRCLOUD_HOST && process.env.ACRCLOUD_ACCESS_KEY);
const hasAudd = !!(process.env.AUDD_API_TOKEN || process.env.AUDD_API_KEY);
if (!hasAcr && !hasAudd) {
return res.json({
status: 'not_configured',
message: 'Kein Audio-Erkennungs-Key hinterlegt. Bitte ACRCloud (kostenlos) oder AudD in .env eintragen. Nutze solange die manuelle Suche.'
});
}
let match = null;
if (hasAcr) {
match = await identifyACRCloud(audioBuffer);
}
if (!match && hasAudd) {
match = await identifyAudD(audioBuffer);
}
if (!match) {
return res.json({
status: 'not_found',
message: 'Kein passender Track erkannt. Spiele das Audio lauter ab oder nutze die Suche.'
});
}
// 2. Track recognized! Check local PostgreSQL database first
const existing = await db.findTrack(match.artist, match.title);
let key = existing ? existing.camelot_key : null;
let bpm = existing ? existing.bpm : null;
let musicKey = existing ? existing.music_key : null;
let isCorrected = existing ? existing.is_corrected : false;
let coverUrl = existing ? existing.cover_url : match.cover_url;
res.json({
status: 'success',
data: {
title: match.title,
artist: match.artist,
matchScore: match.matchScore,
key: key,
bpm: bpm,
musicKey: musicKey,
isCorrected: isCorrected,
cover_url: coverUrl,
preview_url: existing ? existing.preview_url : null,
duration_sec: existing ? existing.duration_sec : null,
energy: existing ? existing.energy : null,
danceability: existing ? existing.danceability : null,
happiness: existing ? existing.happiness : null
}
});
} catch (err) {
console.error('[Recognize] Recognition error:', err.message);
res.status(500).json({ error: 'Audio recognition failed: ' + err.message });
} finally {
fs.unlink(filePath, () => {});
}
});
module.exports = router;