Files
KeySelector/server/routes/suggestions.js
T

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;