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;