Files
KeySelector/server/routes/suggestions.js
T

87 lines
3.0 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 empty, fetch any compatible key track (50-220 BPM)
if (tracks.length === 0) {
tracks = await db.getSuggestions(compatibleKeys, 50, 220, 10, artist, title);
}
// 3. Map tracks with compatibility info and colors
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] || '';
return {
id: t.id,
title: t.title,
artist: t.artist,
key: t.camelot_key,
c: t.camelot_key,
bpm: t.bpm,
musicKey: musicKey,
m: musicKey,
color: color,
compat: ruleIdx !== -1 ? ruleIdx : 0,
compatLabel: compatLabel,
cover_url: t.cover_url || null,
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;