diff --git a/db/schema.sql b/db/schema.sql index 1654570..3143fdc 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -9,6 +9,10 @@ CREATE TABLE IF NOT EXISTS tracks ( music_key VARCHAR(10), cover_url TEXT, preview_url TEXT, + duration_sec INTEGER, + energy INTEGER, + danceability INTEGER, + happiness INTEGER, tags TEXT[] DEFAULT '{}', is_corrected BOOLEAN DEFAULT FALSE, source VARCHAR(50) DEFAULT 'manual', @@ -18,80 +22,105 @@ CREATE TABLE IF NOT EXISTS tracks ( CONSTRAINT unique_track UNIQUE (artist, title) ); +-- Idempotent column migrations for existing databases +ALTER TABLE tracks ADD COLUMN IF NOT EXISTS duration_sec INTEGER; +ALTER TABLE tracks ADD COLUMN IF NOT EXISTS energy INTEGER; +ALTER TABLE tracks ADD COLUMN IF NOT EXISTS danceability INTEGER; +ALTER TABLE tracks ADD COLUMN IF NOT EXISTS happiness INTEGER; +ALTER TABLE tracks ADD COLUMN IF NOT EXISTS preview_url TEXT; + CREATE INDEX IF NOT EXISTS idx_tracks_camelot ON tracks(camelot_key); CREATE INDEX IF NOT EXISTS idx_tracks_bpm ON tracks(bpm); CREATE INDEX IF NOT EXISTS idx_tracks_artist ON tracks(artist); CREATE INDEX IF NOT EXISTS idx_tracks_title ON tracks(title); --- Initial curated seed data across all 24 Camelot keys (Mixed In Key Standard) -INSERT INTO tracks (artist, title, camelot_key, bpm, music_key, tags, source) VALUES +-- Curated seed data across all 24 Camelot keys with Tunebat audio features +INSERT INTO tracks (artist, title, camelot_key, bpm, music_key, duration_sec, energy, danceability, happiness, tags, source) VALUES -- 12B & 12A (E-Dur / C#-Moll) -('Avicii', 'Wake Me Up', '12B', 124, 'E', ARRAY['folk-dance', 'anthem'], 'seed'), -('Otto Knows', 'Million Voices', '12B', 126, 'E', ARRAY['progressive house'], 'seed'), -('Fred again..', 'Delilah (pull me out of this)', '12A', 134, 'C#m', ARRAY['electronic', 'club'], 'seed'), -('Robin Schulz', 'Sugar', '12A', 123, 'C#m', ARRAY['deep house', 'pop'], 'seed'), +('Avicii', 'Wake Me Up', '12B', 124, 'E', 247, 78, 53, 64, ARRAY['folk-dance', 'anthem', 'festival'], 'seed'), +('Otto Knows', 'Million Voices', '12B', 126, 'E', 192, 85, 62, 58, ARRAY['progressive house', 'festival'], 'seed'), +('Kungs', 'Never Going Home', '12B', 124, 'E', 170, 88, 77, 82, ARRAY['dance-pop', 'house'], 'seed'), +('Fred again..', 'Delilah (pull me out of this)', '12A', 134, 'C#m', 251, 84, 68, 49, ARRAY['electronic', 'club', 'emotional'], 'seed'), +('Robin Schulz', 'Sugar', '12A', 123, 'C#m', 219, 82, 64, 63, ARRAY['deep house', 'pop', 'radio'], 'seed'), +('The Chainsmokers', 'Closer', '12A', 95, 'C#m', 244, 52, 75, 66, ARRAY['pop', 'future bass'], 'seed'), -- 1B & 1A (B-Dur / G#-Moll) -('Galantis', 'Runaway (U & I)', '1B', 126, 'B', ARRAY['electro pop', 'edm'], 'seed'), -('Kygo', 'Firestone', '1B', 114, 'B', ARRAY['tropical house'], 'seed'), -('CamelPhat & Elderbrook', 'Cola', '1A', 122, 'G#m', ARRAY['tech house', 'deep house'], 'seed'), -('Peggy Gou', '(It Goes Like) Nanana', '1A', 130, 'G#m', ARRAY['house', 'summer'], 'seed'), +('Galantis', 'Runaway (U & I)', '1B', 126, 'B', 227, 82, 57, 48, ARRAY['electro pop', 'edm', 'anthem'], 'seed'), +('Kygo', 'Firestone', '1B', 114, 'B', 273, 56, 68, 51, ARRAY['tropical house', 'melodic'], 'seed'), +('Martin Solveig', 'Intoxicated', '1B', 125, 'B', 160, 89, 82, 79, ARRAY['future house', 'groove'], 'seed'), +('CamelPhat & Elderbrook', 'Cola', '1A', 122, 'G#m', 223, 73, 70, 42, ARRAY['tech house', 'deep house', 'club'], 'seed'), +('Peggy Gou', '(It Goes Like) Nanana', '1A', 130, 'G#m', 231, 86, 74, 80, ARRAY['house', 'summer', 'vocal'], 'seed'), +('RÜFÜS DU SOL', 'Innerbloom', '1A', 121, 'G#m', 578, 67, 65, 33, ARRAY['melodic techno', 'deep house'], 'seed'), -- 2B & 2A (F#-Dur / D#-Moll) -('Disclosure ft. Sam Smith', 'Latch', '2B', 122, 'F#', ARRAY['garage', 'house'], 'seed'), -('Major Lazer', 'Lean On', '2B', 98, 'F#', ARRAY['dancehall', 'moombahton'], 'seed'), -('Eric Prydz', 'Opus', '2A', 126, 'D#m', ARRAY['progressive house', 'masterpiece'], 'seed'), -('Darude', 'Sandstorm', '2A', 136, 'D#m', ARRAY['trance', 'classic'], 'seed'), +('Disclosure ft. Sam Smith', 'Latch', '2B', 122, 'F#', 256, 73, 50, 52, ARRAY['uk garage', 'house', 'vocal'], 'seed'), +('Major Lazer', 'Lean On', '2B', 98, 'F#', 176, 81, 72, 52, ARRAY['dancehall', 'moombahton', 'pop'], 'seed'), +('Calvin Harris & Dua Lipa', 'One Kiss', '2B', 124, 'F#', 214, 86, 79, 59, ARRAY['house', 'pop', 'dance'], 'seed'), +('Eric Prydz', 'Opus', '2A', 126, 'D#m', 560, 89, 54, 18, ARRAY['progressive house', 'masterpiece'], 'seed'), +('Darude', 'Sandstorm', '2A', 136, 'D#m', 225, 99, 54, 76, ARRAY['trance', 'classic', 'peak time'], 'seed'), +('Gorgon City', 'Ready For Your Love', '2A', 122, 'D#m', 199, 81, 74, 55, ARRAY['deep house', 'uk bass'], 'seed'), -- 3B & 3A (Db-Dur / Bb-Moll) -('Calvin Harris', 'Feel So Close', '3B', 128, 'Db', ARRAY['dance', 'pop'], 'seed'), -('Gigi D''Agostino', 'L''Amour Toujours', '3A', 138, 'Bbm', ARRAY['italo dance', 'classic'], 'seed'), -('Paul Kalkbrenner', 'Sky and Sand', '3A', 122, 'Bbm', ARRAY['minimal', 'berlin techno'], 'seed'), +('Calvin Harris', 'Feel So Close', '3B', 128, 'Db', 206, 92, 71, 91, ARRAY['dance', 'pop', 'festival'], 'seed'), +('Tiësto & Karol G', 'Don''t Be Shy', '3B', 120, 'Db', 140, 79, 77, 85, ARRAY['slap house', 'latin dance'], 'seed'), +('Gigi D''Agostino', 'L''Amour Toujours', '3A', 138, 'Bbm', 238, 77, 62, 72, ARRAY['italo dance', 'classic', 'retro'], 'seed'), +('Paul Kalkbrenner', 'Sky and Sand', '3A', 122, 'Bbm', 239, 57, 72, 38, ARRAY['minimal', 'berlin techno', 'iconic'], 'seed'), +('Swedish House Mafia', 'Moth To A Flame', '3A', 120, 'Bbm', 234, 75, 59, 36, ARRAY['synth-pop', 'house', 'dark'], 'seed'), -- 4B & 4A (Ab-Dur / F-Moll) -('Clean Bandit', 'Rather Be', '4B', 121, 'Ab', ARRAY['deep house', 'classical crossover'], 'seed'), -('Swedish House Mafia', 'One', '4A', 125, 'Fm', ARRAY['electro house', 'classic'], 'seed'), -('Alan Walker', 'Faded', '4A', 90, 'Fm', ARRAY['electro', 'melodic'], 'seed'), +('Clean Bandit', 'Rather Be', '4B', 121, 'Ab', 228, 79, 73, 75, ARRAY['deep house', 'classical crossover'], 'seed'), +('Duke Dumont', 'Ocean Drive', '4B', 115, 'Ab', 206, 72, 75, 48, ARRAY['nu-disco', 'synth-pop', 'vibe'], 'seed'), +('Swedish House Mafia', 'One', '4A', 125, 'Fm', 349, 93, 71, 26, ARRAY['electro house', 'club', 'classic'], 'seed'), +('Alan Walker', 'Faded', '4A', 90, 'Fm', 212, 65, 47, 16, ARRAY['electro', 'melodic', 'anthem'], 'seed'), +('Oliver Heldens', 'Gecko (Overdrive)', '4A', 125, 'Fm', 165, 87, 76, 68, ARRAY['future house', 'bassline'], 'seed'), -- 5B & 5A (Eb-Dur / C-Moll) -('deadmau5', 'Strobe', '5B', 128, 'Eb', ARRAY['progressive house'], 'seed'), -('Bicep', 'Glue', '5B', 130, 'Eb', ARRAY['breakbeat', 'ambient'], 'seed'), -('Benny Benassi', 'Satisfaction', '5A', 130, 'Cm', ARRAY['electro house', 'legend'], 'seed'), -('Kungs vs Cookin'' on 3 Burners', 'This Girl', '5A', 122, 'Cm', ARRAY['funk house', 'brass'], 'seed'), +('deadmau5', 'Strobe', '5B', 128, 'Eb', 637, 71, 56, 12, ARRAY['progressive house', 'legendary'], 'seed'), +('Bicep', 'Glue', '5B', 130, 'Eb', 269, 77, 66, 31, ARRAY['breakbeat', 'ambient', 'rave'], 'seed'), +('Benny Benassi', 'Satisfaction', '5A', 130, 'Cm', 285, 96, 81, 74, ARRAY['electro house', 'legend', 'bass'], 'seed'), +('Kungs vs Cookin'' on 3 Burners', 'This Girl', '5A', 122, 'Cm', 195, 82, 75, 84, ARRAY['funk house', 'brass', 'summer'], 'seed'), +('Fisher', 'You Little Beauty', '5A', 126, 'Cm', 206, 91, 78, 64, ARRAY['tech house', 'peak time'], 'seed'), -- 6B & 6A (Bb-Dur / G-Moll) -('Modjo', 'Lady (Hear Me Tonight)', '6B', 126, 'Bb', ARRAY['french touch', 'filter house'], 'seed'), -('Fatboy Slim', 'Praise You', '6B', 110, 'Bb', ARRAY['big beat', 'breakbeat'], 'seed'), -('Robin S', 'Show Me Love', '6A', 120, 'Gm', ARRAY['90s house', 'anthem'], 'seed'), -('Robert Miles', 'Children', '6A', 138, 'Gm', ARRAY['dream trance', '90s'], 'seed'), +('Modjo', 'Lady (Hear Me Tonight)', '6B', 126, 'Bb', 306, 81, 72, 87, ARRAY['french touch', 'filter house', 'classic'], 'seed'), +('Fatboy Slim', 'Praise You', '6B', 110, 'Bb', 323, 83, 70, 71, ARRAY['big beat', 'breakbeat', '90s'], 'seed'), +('Robin S', 'Show Me Love', '6A', 120, 'Gm', 252, 75, 78, 83, ARRAY['90s house', 'anthem', 'organ'], 'seed'), +('Robert Miles', 'Children', '6A', 138, 'Gm', 243, 79, 58, 23, ARRAY['dream trance', '90s', 'piano'], 'seed'), +('ATB', '9 PM (Till I Come)', '6A', 130, 'Gm', 194, 88, 71, 62, ARRAY['trance', 'classic', 'guitar'], 'seed'), -- 7B & 7A (F-Dur / D-Moll) -('Shouse', 'Love Tonight', '7B', 123, 'F', ARRAY['indie dance', 'choir'], 'seed'), -('Stardust', 'Music Sounds Better With You', '7B', 124, 'F', ARRAY['french house', 'disco'], 'seed'), -('Faithless', 'God Is a DJ', '7A', 131, 'Dm', ARRAY['dance anthem', 'classic'], 'seed'), -('Paul van Dyk', 'For an Angel', '7A', 138, 'Dm', ARRAY['trance', 'berlin'], 'seed'), +('Shouse', 'Love Tonight', '7B', 123, 'F', 241, 87, 68, 69, ARRAY['indie dance', 'choir', 'anthem'], 'seed'), +('Stardust', 'Music Sounds Better With You', '7B', 124, 'F', 261, 84, 75, 91, ARRAY['french house', 'disco', 'classic'], 'seed'), +('Faithless', 'God Is a DJ', '7A', 131, 'Dm', 207, 91, 66, 42, ARRAY['dance anthem', 'classic', 'trance'], 'seed'), +('Paul van Dyk', 'For an Angel', '7A', 138, 'Dm', 231, 94, 53, 39, ARRAY['trance', 'berlin', 'timeless'], 'seed'), +('Topic & A7S', 'Breaking Me', '7A', 122, 'Dm', 167, 72, 79, 66, ARRAY['deep house', 'slap house'], 'seed'), -- 8B & 8A (C-Dur / A-Moll) -('Axwell /\\ Ingrosso', 'More Than You Know', '8B', 123, 'C', ARRAY['house', 'edm'], 'seed'), -('Avicii', 'The Nights', '8B', 126, 'C', ARRAY['dance', 'melodic'], 'seed'), -('Tiësto', 'The Business', '8A', 120, 'Am', ARRAY['deep house', 'bass'], 'seed'), -('Meduza', 'Piece Of Your Heart', '8A', 124, 'Am', ARRAY['house', 'electronic'], 'seed'), +('Axwell /\\ Ingrosso', 'More Than You Know', '8B', 123, 'C', 203, 87, 65, 54, ARRAY['house', 'edm', 'festival'], 'seed'), +('Avicii', 'The Nights', '8B', 126, 'C', 176, 84, 53, 65, ARRAY['dance', 'melodic', 'acoustic'], 'seed'), +('Tiësto', 'The Business', '8A', 120, 'Am', 164, 80, 80, 56, ARRAY['deep house', 'bass', 'club'], 'seed'), +('Meduza', 'Piece Of Your Heart', '8A', 124, 'Am', 153, 74, 68, 63, ARRAY['house', 'electronic', 'dark'], 'seed'), +('John Summit', 'Where You Are', '8A', 126, 'Am', 234, 85, 62, 45, ARRAY['vocal house', 'euphoric'], 'seed'), -- 9B & 9A (G-Dur / E-Moll) -('Calvin Harris', 'Summer', '9B', 128, 'G', ARRAY['dance', 'summer'], 'seed'), -('David Guetta ft. Sia', 'Titanium', '9B', 126, 'G', ARRAY['edm', 'pop'], 'seed'), -('Martin Garrix', 'Animals', '9A', 128, 'Em', ARRAY['big room', 'electro'], 'seed'), -('Fisher', 'Losing It', '9A', 125, 'Em', ARRAY['tech house', 'club'], 'seed'), +('Calvin Harris', 'Summer', '9B', 128, 'G', 222, 86, 60, 74, ARRAY['dance', 'summer', 'festival'], 'seed'), +('David Guetta ft. Sia', 'Titanium', '9B', 126, 'G', 245, 79, 60, 30, ARRAY['edm', 'pop', 'anthem'], 'seed'), +('Martin Garrix', 'Animals', '9A', 128, 'Em', 176, 90, 58, 28, ARRAY['big room', 'electro', 'festival'], 'seed'), +('Fisher', 'Losing It', '9A', 125, 'Em', 248, 96, 76, 64, ARRAY['tech house', 'club', 'banger'], 'seed'), +('Dom Dolla', 'Rhyme Dust', '9A', 128, 'Em', 190, 92, 79, 70, ARRAY['tech house', 'bass'], 'seed'), -- 10B & 10A (D-Dur / H-Moll) -('Daft Punk', 'One More Time', '10B', 123, 'D', ARRAY['french house', 'disco'], 'seed'), -('Swedish House Mafia', 'Don''t You Worry Child', '10B', 129, 'D', ARRAY['progressive', 'anthem'], 'seed'), -('Alesso', 'Heroes (we could be)', '10B', 126, 'D', ARRAY['edm', 'euphoric'], 'seed'), -('ACRAZE', 'Do It To It', '10A', 125, 'Bm', ARRAY['house', 'viral'], 'seed'), -('Faithless', 'Insomnia', '10A', 127, 'Bm', ARRAY['classic', 'trance'], 'seed'), +('Daft Punk', 'One More Time', '10B', 123, 'D', 320, 71, 61, 64, ARRAY['french house', 'disco', 'legend'], 'seed'), +('Swedish House Mafia', 'Don''t You Worry Child', '10B', 129, 'D', 212, 84, 61, 44, ARRAY['progressive', 'anthem', 'festival'], 'seed'), +('Alesso', 'Heroes (we could be)', '10B', 126, 'D', 210, 86, 52, 42, ARRAY['edm', 'euphoric', 'vocal'], 'seed'), +('ACRAZE', 'Do It To It', '10A', 125, 'Bm', 158, 81, 85, 66, ARRAY['house', 'viral', 'bass'], 'seed'), +('Faithless', 'Insomnia', '10A', 127, 'Bm', 214, 94, 76, 53, ARRAY['classic', 'trance', 'pizzicato'], 'seed'), +('Mau P', 'Drugs From Amsterdam', '10A', 125, 'Bm', 236, 92, 83, 62, ARRAY['tech house', 'club', 'groove'], 'seed'), -- 11B & 11A (A-Dur / F#-Moll) -('Alphaville', 'Big in Japan', '11B', 118, 'A', ARRAY['80s', 'synth-pop'], 'seed'), -('Dua Lipa', 'Don''t Start Now', '11A', 124, 'F#m', ARRAY['nu-disco', 'dance'], 'seed'), -('The Weeknd', 'Blinding Lights', '11A', 171, 'F#m', ARRAY['synthwave', '80s'], 'seed') +('Alphaville', 'Big in Japan', '11B', 118, 'A', 232, 69, 73, 71, ARRAY['80s', 'synth-pop', 'classic'], 'seed'), +('Daft Punk', 'Around The World', '11B', 121, 'A', 429, 80, 86, 84, ARRAY['french house', 'groove', 'funk'], 'seed'), +('Dua Lipa', 'Don''t Start Now', '11A', 124, 'F#m', 183, 79, 79, 68, ARRAY['nu-disco', 'dance', 'bassline'], 'seed'), +('The Weeknd', 'Blinding Lights', '11A', 171, 'F#m', 200, 73, 51, 33, ARRAY['synthwave', '80s', 'retro'], 'seed'), +('Oliver Tree & Robin Schulz', 'Miss You', '11A', 145, 'F#m', 206, 88, 72, 57, ARRAY['dance', 'speed-house'], 'seed') ON CONFLICT (artist, title) DO NOTHING; diff --git a/public/app.js b/public/app.js index cc88e89..c252eac 100644 --- a/public/app.js +++ b/public/app.js @@ -282,7 +282,7 @@ async function processAudio(blob) { const json = await res.json(); if (json.status === 'success' && json.data) { - const { title, artist, matchScore, key, bpm, cover_url, isCorrected } = json.data; + const { title, artist, matchScore, key, bpm, cover_url, isCorrected, preview_url, duration_sec, energy, danceability, happiness } = json.data; // Avoid refetching if same track is still playing if (currentTrack && currentTrack.title.toLowerCase() === title.toLowerCase() && currentTrack.artist.toLowerCase() === artist.toLowerCase()) { @@ -298,6 +298,11 @@ async function processAudio(blob) { title, artist, cover_url, + preview_url, + duration_sec, + energy, + danceability, + happiness, camelot_key: key, bpm: bpm, is_corrected: isCorrected, @@ -353,6 +358,108 @@ function resetHero() { document.getElementById('hero').classList.remove('wheel-mode', 'cover-mode'); } +// ---------------------------------------------------- +// Global 30-Second Audio Preview Player (Tunebat Style) +// ---------------------------------------------------- +let activeAudio = null; +let activePreviewUrl = null; +let activePlayBtn = null; + +function stopAllPreviews() { + if (activeAudio) { + activeAudio.pause(); + activeAudio.currentTime = 0; + activeAudio = null; + } + activePreviewUrl = null; + if (activePlayBtn) { + activePlayBtn.classList.remove('playing'); + const icon = activePlayBtn.querySelector('i'); + if (icon) icon.className = 'fa-solid fa-play'; + activePlayBtn = null; + } + // Also reset hero preview button if needed + const heroBtn = document.getElementById('hero-preview-btn'); + if (heroBtn) { + heroBtn.classList.remove('playing'); + const heroIcon = heroBtn.querySelector('i'); + if (heroIcon) heroIcon.className = 'fa-solid fa-play'; + } +} + +async function togglePreviewTrack(track, buttonEl) { + if (!track) return; + + let previewUrl = track.preview_url; + + // If clicking on already playing preview -> Pause & Stop + if (activeAudio && activePreviewUrl === previewUrl) { + stopAllPreviews(); + return; + } + + // If another track was playing, stop it first + stopAllPreviews(); + + // If track doesn't have preview_url yet, try to search Deezer dynamically + if (!previewUrl) { + if (buttonEl) { + const icon = buttonEl.querySelector('i'); + if (icon) icon.className = 'fa-solid fa-spinner fa-spin'; + } + try { + const sRes = await fetch(`/api/search?q=${encodeURIComponent(track.artist + ' ' + track.title)}`); + const sJson = await sRes.json(); + if (sJson.status === 'success' && sJson.data && sJson.data.length > 0 && sJson.data[0].preview_url) { + previewUrl = sJson.data[0].preview_url; + track.preview_url = previewUrl; + } + } catch (e) { + console.warn('[Preview] Could not find preview URL:', e); + } + } + + if (!previewUrl) { + alert(`Keine 30s-Vorschau für "${track.title}" verfügbar.`); + if (buttonEl) { + const icon = buttonEl.querySelector('i'); + if (icon) icon.className = 'fa-solid fa-play'; + } + return; + } + + try { + activeAudio = new Audio(previewUrl); + activePreviewUrl = previewUrl; + activePlayBtn = buttonEl; + + if (buttonEl) { + buttonEl.classList.add('playing'); + const icon = buttonEl.querySelector('i'); + if (icon) icon.className = 'fa-solid fa-pause'; + } + + activeAudio.onended = () => { + stopAllPreviews(); + }; + + activeAudio.onerror = (e) => { + console.warn('[Preview] Audio playback error:', e); + stopAllPreviews(); + }; + + await activeAudio.play(); + } catch (err) { + console.warn('[Preview] Play failed (user interaction needed):', err); + stopAllPreviews(); + } +} + +function toggleHeroPreview() { + const heroBtn = document.getElementById('hero-preview-btn'); + togglePreviewTrack(window.currentTrack, heroBtn); +} + // Expose globals window.toggleTheme = toggleTheme; window.changeAudioSource = changeAudioSource; @@ -361,6 +468,9 @@ window.toggleListening = toggleListening; window.toggleWheel = toggleWheel; window.toggleCover = toggleCover; window.resetHero = resetHero; +window.stopAllPreviews = stopAllPreviews; +window.togglePreviewTrack = togglePreviewTrack; +window.toggleHeroPreview = toggleHeroPreview; // Initial state document.addEventListener('DOMContentLoaded', () => { diff --git a/public/index.html b/public/index.html index dac24a8..f19202d 100644 --- a/public/index.html +++ b/public/index.html @@ -9,7 +9,7 @@ - +