111 lines
4.1 KiB
JavaScript
111 lines
4.1 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const db = require('../db');
|
|
const { mixingRules, compatLabels } = require('../../shared/mixing-rules');
|
|
const camelotToMusicKey = require('../../shared/camelot-to-musickey');
|
|
const colorMatrix = require('../../shared/color-matrix');
|
|
|
|
// GET /api/suggestions?key=4B&bpm=120&artist=...&title=...
|
|
router.get('/', async (req, res) => {
|
|
const currentKey = req.query.key;
|
|
const currentBpm = parseInt(req.query.bpm, 10) || 120;
|
|
const artist = req.query.artist || '';
|
|
const title = req.query.title || '';
|
|
|
|
if (!currentKey || !mixingRules[currentKey]) {
|
|
return res.status(400).json({ error: 'Valid current key required (e.g. 4B, 1A)' });
|
|
}
|
|
|
|
try {
|
|
const compatibleKeys = mixingRules[currentKey]; // Array of 8 keys: [Perfect, Quinte -1, ...]
|
|
|
|
// BPM tolerance: ±6% by default for seamless mixing
|
|
let minBpm = Math.round(currentBpm * 0.94);
|
|
let maxBpm = Math.round(currentBpm * 1.06);
|
|
|
|
// 1. Fetch tracks from PostgreSQL within ±6%
|
|
let tracks = await db.getSuggestions(compatibleKeys, minBpm, maxBpm, 10, artist, title);
|
|
|
|
// 2. If fewer than 8 tracks, widen BPM range to ±12%
|
|
if (tracks.length < 8) {
|
|
minBpm = Math.round(currentBpm * 0.88);
|
|
maxBpm = Math.round(currentBpm * 1.12);
|
|
tracks = await db.getSuggestions(compatibleKeys, minBpm, maxBpm, 10, artist, title);
|
|
}
|
|
|
|
// 3. If fewer than 5 tracks, widen BPM range to ±20% for smaller libraries
|
|
if (tracks.length < 5) {
|
|
minBpm = Math.round(currentBpm * 0.80);
|
|
maxBpm = Math.round(currentBpm * 1.20);
|
|
tracks = await db.getSuggestions(compatibleKeys, minBpm, maxBpm, 10, artist, title);
|
|
}
|
|
|
|
// 4. If still fewer than 8 tracks, widen to any compatible key track (50-220 BPM)
|
|
if (tracks.length < 8) {
|
|
const moreTracks = await db.getSuggestions(compatibleKeys, 50, 220, 15, artist, title);
|
|
const trackIds = new Set(tracks.map(t => t.id || `${t.artist}-${t.title}`));
|
|
for (const mt of moreTracks) {
|
|
const idKey = mt.id || `${mt.artist}-${mt.title}`;
|
|
if (!trackIds.has(idKey)) {
|
|
tracks.push(mt);
|
|
trackIds.add(idKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Map tracks with compatibility info, colors and Tunebat audio features
|
|
const enriched = tracks.map(t => {
|
|
const ruleIdx = compatibleKeys.indexOf(t.camelot_key);
|
|
const compatLabel = ruleIdx !== -1 ? compatLabels[ruleIdx] : 'Compatible';
|
|
const color = colorMatrix[t.camelot_key] || '#4F9D69';
|
|
const musicKey = t.music_key || camelotToMusicKey[t.camelot_key] || '';
|
|
|
|
const bpmVal = t.bpm || 120;
|
|
const isMajor = t.camelot_key && t.camelot_key.endsWith('B');
|
|
const defaultEnergy = Math.min(96, Math.max(40, Math.round(45 + ((bpmVal - 80) / 90) * 50)));
|
|
const defaultDance = Math.min(94, Math.max(50, Math.round(65 + ((bpmVal - 100) / 60) * 25)));
|
|
const defaultHappy = isMajor ? 70 : 46;
|
|
|
|
const pitchDiff = currentBpm ? Math.round(((bpmVal - currentBpm) / currentBpm) * 100) : 0;
|
|
|
|
return {
|
|
id: t.id,
|
|
title: t.title,
|
|
artist: t.artist,
|
|
camelot_key: t.camelot_key,
|
|
key: t.camelot_key,
|
|
c: t.camelot_key,
|
|
bpm: t.bpm,
|
|
pitch_diff_pct: pitchDiff,
|
|
musicKey: musicKey,
|
|
m: musicKey,
|
|
color: color,
|
|
compat: ruleIdx !== -1 ? ruleIdx : 0,
|
|
compatLabel: compatLabel,
|
|
cover_url: t.cover_url || null,
|
|
preview_url: t.preview_url || null,
|
|
deezer_link: t.deezer_link || (t.deezer_id ? `https://www.deezer.com/track/${t.deezer_id}` : null),
|
|
duration_sec: t.duration_sec || 215,
|
|
energy: t.energy || defaultEnergy,
|
|
danceability: t.danceability || defaultDance,
|
|
happiness: t.happiness || defaultHappy,
|
|
tags: t.tags || []
|
|
};
|
|
});
|
|
|
|
// Sort by compatibility level (Perfect -> Quinte -> Energy -> ...)
|
|
enriched.sort((a, b) => a.compat - b.compat);
|
|
|
|
res.json({
|
|
status: 'success',
|
|
data: enriched
|
|
});
|
|
|
|
} catch (err) {
|
|
console.error('[Suggestions] Error:', err.message);
|
|
res.status(500).json({ error: 'Failed to generate suggestions' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|