167 lines
5.1 KiB
JavaScript
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;
|