643 lines
24 KiB
JavaScript
643 lines
24 KiB
JavaScript
// Live Search & PostgreSQL Correction Management
|
||
|
||
let searchTimeout = null;
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const searchInput = document.getElementById('search-input');
|
||
const searchDropdown = document.getElementById('search-results-dropdown');
|
||
const clearBtn = document.getElementById('search-clear-btn');
|
||
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', (e) => {
|
||
const val = e.target.value.trim();
|
||
clearBtn.style.display = val ? 'block' : 'none';
|
||
|
||
clearTimeout(searchTimeout);
|
||
if (!val) {
|
||
searchDropdown.style.display = 'none';
|
||
searchDropdown.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
searchTimeout = setTimeout(() => {
|
||
executeSearch(val);
|
||
}, 250);
|
||
});
|
||
|
||
// Close dropdown when clicking outside
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.search-box-container')) {
|
||
searchDropdown.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Populate Key options in edit modal
|
||
initEditKeyOptions();
|
||
});
|
||
|
||
function clearSearch() {
|
||
const searchInput = document.getElementById('search-input');
|
||
const searchDropdown = document.getElementById('search-results-dropdown');
|
||
const clearBtn = document.getElementById('search-clear-btn');
|
||
searchInput.value = '';
|
||
clearBtn.style.display = 'none';
|
||
searchDropdown.style.display = 'none';
|
||
searchDropdown.innerHTML = '';
|
||
}
|
||
|
||
async function executeSearch(query) {
|
||
const searchDropdown = document.getElementById('search-results-dropdown');
|
||
searchDropdown.innerHTML = '<div style="padding:14px;color:var(--text-dim);font-size:12px;font-family:monospace;">Suche in PostgreSQL & Online...</div>';
|
||
searchDropdown.style.display = 'block';
|
||
|
||
try {
|
||
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
||
const json = await res.json();
|
||
|
||
if (json.status !== 'success' || !json.data || json.data.length === 0) {
|
||
searchDropdown.innerHTML = '<div style="padding:14px;color:var(--text-dim);font-size:12px;font-family:monospace;">Keine Tracks gefunden.</div>';
|
||
return;
|
||
}
|
||
|
||
searchDropdown.innerHTML = '';
|
||
json.data.forEach(track => {
|
||
const item = document.createElement('div');
|
||
item.className = 'search-result-item';
|
||
|
||
const coverHtml = track.cover_url
|
||
? `<img src="${track.cover_url}" alt="cover" loading="lazy">`
|
||
: `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-dim);font-size:16px;">♪</div>`;
|
||
|
||
let badgesHtml = '';
|
||
if (track.is_local && track.camelot_key) {
|
||
const color = window.colorMatrix ? window.colorMatrix[track.camelot_key] : '#4F9D69';
|
||
badgesHtml += `<span class="search-badge" style="background:${color};">${track.camelot_key}</span>`;
|
||
if (track.bpm) {
|
||
badgesHtml += `<span class="search-badge-bpm">${track.bpm} BPM</span>`;
|
||
}
|
||
badgesHtml += `<span class="search-badge search-badge-local">${track.is_corrected ? 'KORRIGIERT' : 'LOKAL'}</span>`;
|
||
} else {
|
||
badgesHtml += `<span class="search-badge" style="background:var(--surface-2);color:var(--text-dim);border:1px solid var(--border);">NEU</span>`;
|
||
}
|
||
|
||
item.innerHTML = `
|
||
<div class="search-result-cover">${coverHtml}</div>
|
||
<div class="search-result-info">
|
||
<div class="search-result-title">${escapeHtml(track.title)}</div>
|
||
<div class="search-result-artist">${escapeHtml(track.artist)}</div>
|
||
</div>
|
||
<div class="search-result-badges">${badgesHtml}</div>
|
||
`;
|
||
|
||
item.addEventListener('click', () => {
|
||
selectTrack(track);
|
||
searchDropdown.style.display = 'none';
|
||
clearSearch();
|
||
});
|
||
|
||
searchDropdown.appendChild(item);
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('[Search] Error:', err);
|
||
searchDropdown.innerHTML = '<div style="padding:14px;color:#D62828;font-size:12px;">Suchfehler aufgetreten.</div>';
|
||
}
|
||
}
|
||
|
||
function formatDuration(sec) {
|
||
if (!sec || isNaN(sec)) return '-:--';
|
||
const m = Math.floor(sec / 60);
|
||
const s = Math.floor(sec % 60);
|
||
return `${m}:${s < 10 ? '0' : ''}${s}`;
|
||
}
|
||
|
||
function getCleanSearchTerm(artist, title) {
|
||
const raw = `${artist || ''} ${title || ''}`;
|
||
return raw.replace(/[/\\?#%&"'\(\)\[\]]/g, ' ').replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
function getDeezerUrl(track) {
|
||
if (track && track.deezer_link) return track.deezer_link;
|
||
if (track && track.deezer_id) return `https://www.deezer.com/track/${track.deezer_id}`;
|
||
const term = getCleanSearchTerm(track ? track.artist : '', track ? track.title : '');
|
||
return `https://www.deezer.com/search/${encodeURIComponent(term)}/track`;
|
||
}
|
||
|
||
function getSpotifyUrl(track) {
|
||
const term = getCleanSearchTerm(track ? track.artist : '', track ? track.title : '');
|
||
return `https://open.spotify.com/search/${encodeURIComponent(term)}`;
|
||
}
|
||
|
||
function getYoutubeUrl(track) {
|
||
const term = getCleanSearchTerm(track ? track.artist : '', track ? track.title : '');
|
||
return `https://www.youtube.com/results?search_query=${encodeURIComponent(term)}`;
|
||
}
|
||
|
||
function formatSourceInfo(source, isCorrected) {
|
||
if (isCorrected) return { match: '100%', label: 'PostgreSQL (Fix)' };
|
||
switch (source) {
|
||
case 'spotify':
|
||
case 'seed':
|
||
return { match: '100%', label: 'Spotify (Verifiziert)' };
|
||
case 'songbpm':
|
||
return { match: '98%', label: 'SongBPM (Spotify API)' };
|
||
case 'audio_analysis':
|
||
return { match: '85%', label: 'Audio-Analyse (ffmpeg)' };
|
||
case 'getsongbpm':
|
||
return { match: '90%', label: 'GetSongBPM' };
|
||
case 'postgresql':
|
||
return { match: '100%', label: 'PostgreSQL (Lokal)' };
|
||
default:
|
||
return { match: 'Online', label: 'Deezer / Online' };
|
||
}
|
||
}
|
||
|
||
async function selectTrack(track) {
|
||
// Update Hero UI
|
||
const initialSource = formatSourceInfo(track.source || (track.is_local ? 'postgresql' : null), track.is_corrected);
|
||
document.getElementById('track-title').textContent = track.title;
|
||
document.getElementById('track-artist').textContent = track.artist;
|
||
document.getElementById('stat-match').textContent = initialSource.match;
|
||
document.getElementById('stat-source-sub').textContent = initialSource.label;
|
||
|
||
// Stop any ongoing preview playback
|
||
if (window.stopAllPreviews) window.stopAllPreviews();
|
||
|
||
// Update Cover
|
||
if (track.cover_url) {
|
||
const coverEl = document.getElementById('cover');
|
||
coverEl.innerHTML = `<img src="${track.cover_url}" alt="cover">
|
||
<button id="hero-preview-btn" class="cover-play-btn" onclick="event.stopPropagation(); window.toggleHeroPreview()" title="30s Audio-Vorschau abspielen">
|
||
<i class="fa-solid fa-play"></i>
|
||
</button>`;
|
||
} else {
|
||
window.fetchCover(`${track.artist} ${track.title}`, document.getElementById('cover'));
|
||
}
|
||
|
||
// Update External Streaming Links
|
||
const trackActions = document.getElementById('track-actions');
|
||
if (trackActions) {
|
||
trackActions.style.display = 'flex';
|
||
const spotEl = document.getElementById('action-spotify');
|
||
const deezEl = document.getElementById('action-deezer');
|
||
const ytEl = document.getElementById('action-youtube');
|
||
if (spotEl) spotEl.href = getSpotifyUrl(track);
|
||
if (deezEl) deezEl.href = getDeezerUrl(track);
|
||
if (ytEl) ytEl.href = getYoutubeUrl(track);
|
||
}
|
||
|
||
// Fetch or apply metadata
|
||
let key = track.camelot_key || track.key || track.c;
|
||
let bpm = track.bpm;
|
||
let tags = track.tags || ['dance'];
|
||
let previewUrl = track.preview_url || null;
|
||
let durationSec = track.duration_sec || 215;
|
||
let energy = track.energy !== undefined ? track.energy : null;
|
||
let danceability = track.danceability !== undefined ? track.danceability : null;
|
||
let happiness = track.happiness !== undefined ? track.happiness : null;
|
||
let deezerLink = track.deezer_link || (track.deezer_id ? `https://www.deezer.com/track/${track.deezer_id}` : null);
|
||
let deezerId = track.deezer_id || null;
|
||
|
||
if (!key || !bpm || !energy) {
|
||
// Query metadata route to get key, bpm, and persist into PostgreSQL
|
||
try {
|
||
const metaRes = await fetch(`/api/metadata?track=${encodeURIComponent(track.title)}&artist=${encodeURIComponent(track.artist)}`);
|
||
const metaJson = await metaRes.json();
|
||
if (metaJson.status === 'success') {
|
||
const d = metaJson.data;
|
||
key = d.key;
|
||
bpm = d.bpm;
|
||
tags = d.tags || tags;
|
||
previewUrl = d.preview_url || previewUrl;
|
||
durationSec = d.duration_sec || durationSec;
|
||
energy = d.energy;
|
||
danceability = d.danceability;
|
||
happiness = d.happiness;
|
||
if (d.deezer_link || d.deezer_id) {
|
||
deezerLink = d.deezer_link || deezerLink;
|
||
deezerId = d.deezer_id || deezerId;
|
||
track.deezer_link = deezerLink;
|
||
track.deezer_id = deezerId;
|
||
const deezEl = document.getElementById('action-deezer');
|
||
if (deezEl) deezEl.href = getDeezerUrl(track);
|
||
}
|
||
const metaSource = formatSourceInfo(d.source, d.is_corrected);
|
||
document.getElementById('stat-match').textContent = metaSource.match;
|
||
document.getElementById('stat-source-sub').textContent = metaSource.label;
|
||
}
|
||
} catch (e) {
|
||
console.warn('[Search] Metadata fetch failed, using fallback', e);
|
||
key = '8B';
|
||
bpm = 120;
|
||
energy = 75;
|
||
danceability = 70;
|
||
happiness = 60;
|
||
}
|
||
}
|
||
|
||
window.currentTrack = {
|
||
id: track.id,
|
||
title: track.title,
|
||
artist: track.artist,
|
||
key: key,
|
||
bpm: bpm,
|
||
cover_url: track.cover_url,
|
||
preview_url: previewUrl,
|
||
duration_sec: durationSec,
|
||
energy: energy,
|
||
danceability: danceability,
|
||
happiness: happiness,
|
||
tags: tags
|
||
};
|
||
|
||
// Show hero preview button
|
||
const heroBtn = document.getElementById('hero-preview-btn');
|
||
if (heroBtn) heroBtn.style.display = 'flex';
|
||
|
||
updateHeroStats(key, bpm, tags, { durationSec, energy, danceability, happiness });
|
||
renderSuggestions(key, bpm, track.artist, track.title);
|
||
}
|
||
|
||
function updateHeroStats(key, bpm, tags = [], attrs = {}) {
|
||
document.getElementById('stat-key').textContent = key;
|
||
const musicKey = window.camelotToMusicKey ? window.camelotToMusicKey[key] : '';
|
||
document.getElementById('stat-key-sub').textContent = musicKey;
|
||
|
||
const keyColor = window.colorMatrix ? window.colorMatrix[key] : '#4F9D69';
|
||
document.getElementById('stat-key').style.color = keyColor;
|
||
document.documentElement.style.setProperty('--key-current', keyColor);
|
||
|
||
document.getElementById('stat-bpm').textContent = bpm;
|
||
document.getElementById('stat-bpm-sub').textContent = `± ${Math.round(bpm * 0.04)} BPM`;
|
||
|
||
// Render Tunebat Audio Attributes Grid
|
||
const attrGrid = document.getElementById('attributes-grid');
|
||
if (attrGrid) {
|
||
attrGrid.style.display = 'grid';
|
||
const durSec = attrs.durationSec || 215;
|
||
const nrg = attrs.energy || Math.min(95, Math.round(50 + ((bpm - 80) / 80) * 40));
|
||
const dnc = attrs.danceability || 75;
|
||
const mood = attrs.happiness || (key && key.endsWith('B') ? 68 : 45);
|
||
|
||
document.getElementById('attr-duration').textContent = formatDuration(durSec);
|
||
document.getElementById('attr-energy').textContent = `${nrg}%`;
|
||
document.getElementById('bar-energy').style.width = `${nrg}%`;
|
||
|
||
document.getElementById('attr-dance').textContent = `${dnc}%`;
|
||
document.getElementById('bar-dance').style.width = `${dnc}%`;
|
||
|
||
document.getElementById('attr-mood').textContent = `${mood}%`;
|
||
document.getElementById('bar-mood').style.width = `${mood}%`;
|
||
}
|
||
|
||
// Render Tags
|
||
const tagsRow = document.getElementById('tags-row');
|
||
tagsRow.innerHTML = '';
|
||
|
||
const keyTag = document.createElement('span');
|
||
keyTag.className = 'tag tag-key';
|
||
keyTag.textContent = `${key} · ${musicKey}`;
|
||
keyTag.style.backgroundColor = keyColor;
|
||
tagsRow.appendChild(keyTag);
|
||
|
||
tags.forEach(t => {
|
||
const el = document.createElement('span');
|
||
el.className = 'tag tag-genre';
|
||
el.textContent = t;
|
||
tagsRow.appendChild(el);
|
||
});
|
||
|
||
// Update Camelot Wheel
|
||
window.updateWheel(key);
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// Suggestions Management & Interactive Filters (Tunebat Style)
|
||
// ----------------------------------------------------
|
||
let allSuggestionsData = [];
|
||
let currentHarmonyFilter = 'all'; // 'all', 'exact', 'quinte', 'relative', 'energy'
|
||
let currentBpmTolerance = 4; // 4, 8, 'all'
|
||
let currentRefBpm = 120;
|
||
|
||
function setHarmonyFilter(filter) {
|
||
currentHarmonyFilter = filter;
|
||
document.querySelectorAll('#suggestions-filters [data-filter]').forEach(btn => {
|
||
btn.classList.toggle('active', btn.getAttribute('data-filter') === filter);
|
||
});
|
||
applySuggestionsFilter();
|
||
}
|
||
|
||
function setBpmTolerance(tolerance) {
|
||
currentBpmTolerance = tolerance;
|
||
document.querySelectorAll('#suggestions-filters [data-bpm-tol]').forEach(btn => {
|
||
btn.classList.toggle('active', btn.getAttribute('data-bpm-tol') === String(tolerance));
|
||
});
|
||
applySuggestionsFilter();
|
||
}
|
||
|
||
function applySuggestionsFilter() {
|
||
const sugContainer = document.getElementById('suggestions');
|
||
const countBadge = document.getElementById('suggestions-count');
|
||
if (!sugContainer) return;
|
||
|
||
if (!allSuggestionsData || allSuggestionsData.length === 0) {
|
||
if (countBadge) countBadge.textContent = '0 Tracks';
|
||
sugContainer.innerHTML = '<div style="color:var(--text-dim);font-size:12px;font-family:monospace;">Keine passenden Tracks gefunden.</div>';
|
||
return;
|
||
}
|
||
|
||
// 1. Filter by Harmony Mix Type
|
||
let filtered = allSuggestionsData.filter(s => {
|
||
if (currentHarmonyFilter === 'all') return true;
|
||
if (currentHarmonyFilter === 'exact') return s.compat === 0; // Perfect (same key)
|
||
if (currentHarmonyFilter === 'quinte') return s.compat === 1 || s.compat === 2; // Quinte -1 / +1
|
||
if (currentHarmonyFilter === 'relative') return s.compat === 4; // Scale (A <-> B)
|
||
if (currentHarmonyFilter === 'energy') return s.compat === 3; // Energy Boost (+2)
|
||
return true;
|
||
});
|
||
|
||
let isFallback = false;
|
||
if (currentBpmTolerance !== 'all') {
|
||
const tolPercent = Number(currentBpmTolerance) / 100;
|
||
const minBpm = currentRefBpm * (1 - tolPercent);
|
||
const maxBpm = currentRefBpm * (1 + tolPercent);
|
||
const bpmFiltered = filtered.filter(s => s.bpm >= minBpm && s.bpm <= maxBpm);
|
||
if (bpmFiltered.length > 0) {
|
||
filtered = bpmFiltered;
|
||
} else if (filtered.length > 0) {
|
||
isFallback = true;
|
||
// Keep best matches sorted by compatibility
|
||
filtered = filtered.slice(0, 10);
|
||
}
|
||
}
|
||
|
||
if (countBadge) {
|
||
countBadge.textContent = isFallback ? `${filtered.length} Tracks (Erweitert)` : `${filtered.length} Tracks`;
|
||
}
|
||
|
||
if (filtered.length === 0) {
|
||
sugContainer.innerHTML = '<div style="color:var(--text-dim);font-size:12px;font-family:monospace;">Keine Tracks für die gewählten Filter vorhanden. Wähle eine größere BPM-Toleranz oder "Alle".</div>';
|
||
return;
|
||
}
|
||
|
||
sugContainer.innerHTML = '';
|
||
|
||
if (isFallback) {
|
||
const notice = document.createElement('div');
|
||
notice.className = 'sug-notice-banner';
|
||
notice.innerHTML = `<i class="fa-solid fa-circle-info"></i> Keine Tracks bei ±${currentBpmTolerance}% — zeige die harmonisch passendsten verfügbaren Tracks mit Pitch-Abweichung:`;
|
||
sugContainer.appendChild(notice);
|
||
}
|
||
|
||
filtered.forEach(s => {
|
||
const div = document.createElement('div');
|
||
div.className = 'sug';
|
||
const idSafe = `sug-${s.c}-${(s.title || '').replace(/[^A-Za-z0-9]/g, '')}`;
|
||
|
||
const pitchDiff = s.pitch_diff_pct !== undefined
|
||
? s.pitch_diff_pct
|
||
: (currentRefBpm ? Math.round(((s.bpm - currentRefBpm) / currentRefBpm) * 100) : 0);
|
||
const pitchSign = pitchDiff > 0 ? `+${pitchDiff}%` : `${pitchDiff}%`;
|
||
const pitchBadgeClass = Math.abs(pitchDiff) <= 4 ? 'pitch-dj' : (Math.abs(pitchDiff) <= 8 ? 'pitch-mid' : 'pitch-far');
|
||
const pitchBadgeHtml = (currentRefBpm && Math.abs(pitchDiff) > 0)
|
||
? `<span class="sug-pitch-badge ${pitchBadgeClass}" title="Pitch-Anpassung: ${pitchSign}">${pitchSign}</span>`
|
||
: '';
|
||
|
||
div.innerHTML = `
|
||
<div class="sug-cover" id="${idSafe}">
|
||
${s.cover_url ? `<img src="${s.cover_url}" alt="cover" loading="lazy">` : '<div class="sug-cover-placeholder">♪</div>'}
|
||
<button class="sug-play-btn" title="30s Vorschau abspielen">
|
||
<i class="fa-solid fa-play"></i>
|
||
</button>
|
||
</div>
|
||
<div class="sug-meta">
|
||
<div class="sug-key" style="background:${s.color}">${s.c}</div>
|
||
<div class="sug-bpm">${s.bpm} BPM ${pitchBadgeHtml}</div>
|
||
</div>
|
||
<div class="sug-sub-stats">
|
||
<span>⏱️ ${formatDuration(s.duration_sec)}</span>
|
||
<span class="sug-attr-pill sug-attr-energy">⚡ ${s.energy || 75}%</span>
|
||
</div>
|
||
<div>
|
||
<div class="sug-title" title="${escapeHtml(s.title)}">${escapeHtml(s.title)}</div>
|
||
<div class="sug-artist">${escapeHtml(s.artist)} · ${s.m}</div>
|
||
</div>
|
||
<div class="sug-bottom-row">
|
||
<div class="sug-compat">${s.compatLabel || 'KOMPATIBEL'}</div>
|
||
<div class="sug-links">
|
||
<a href="${getSpotifyUrl(s)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-spotify" onclick="event.stopPropagation()" title="Auf Spotify öffnen">
|
||
<i class="fa-brands fa-spotify"></i>
|
||
</a>
|
||
<a href="${getDeezerUrl(s)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-deezer" onclick="event.stopPropagation()" title="Auf Deezer öffnen">
|
||
<i class="fa-brands fa-deezer"></i>
|
||
</a>
|
||
<a href="${getYoutubeUrl(s)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-youtube" onclick="event.stopPropagation()" title="Auf YouTube suchen">
|
||
<i class="fa-brands fa-youtube"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Direct event listener for play preview button
|
||
const playBtn = div.querySelector('.sug-play-btn');
|
||
if (playBtn) {
|
||
playBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
window.togglePreviewTrack(s, playBtn);
|
||
});
|
||
}
|
||
|
||
// Click to transition to this song as next current track!
|
||
div.addEventListener('click', () => {
|
||
selectTrack(s);
|
||
});
|
||
|
||
sugContainer.appendChild(div);
|
||
|
||
// Lazy load cover if not already available
|
||
if (!s.cover_url) {
|
||
window.fetchCover(`${s.artist} ${s.title}`, div.querySelector('.sug-cover'));
|
||
}
|
||
});
|
||
}
|
||
|
||
// Render real harmonic suggestions from PostgreSQL & Online
|
||
async function renderSuggestions(currentKey, currentBpm = 120, artist = '', title = '') {
|
||
const sugContainer = document.getElementById('suggestions');
|
||
if (!sugContainer) return;
|
||
|
||
currentRefBpm = currentBpm;
|
||
|
||
if (!currentKey) {
|
||
sugContainer.innerHTML = '<div style="color:var(--text-dim);font-size:12px;font-family:monospace;">Wähle einen Track für Vorschläge.</div>';
|
||
return;
|
||
}
|
||
|
||
sugContainer.innerHTML = '<div style="color:var(--text-dim);font-size:12px;font-family:monospace;">Berechne harmonische Übergänge & Tunebat-Metriken...</div>';
|
||
|
||
try {
|
||
const url = `/api/suggestions?key=${encodeURIComponent(currentKey)}&bpm=${currentBpm}&artist=${encodeURIComponent(artist)}&title=${encodeURIComponent(title)}`;
|
||
const res = await fetch(url);
|
||
const json = await res.json();
|
||
|
||
if (json.status !== 'success') {
|
||
sugContainer.innerHTML = `<div style="color:#D62828;font-size:12px;font-family:monospace;">Fehler beim Laden der Vorschläge: ${escapeHtml(json.error || 'Serverfehler')}</div>`;
|
||
return;
|
||
}
|
||
|
||
allSuggestionsData = json.data || [];
|
||
applySuggestionsFilter();
|
||
|
||
} catch (e) {
|
||
console.error('[Suggestions] Error loading suggestions:', e);
|
||
sugContainer.innerHTML = '<div style="color:#D62828;font-size:12px;">Fehler beim Laden der Vorschläge.</div>';
|
||
}
|
||
}
|
||
|
||
// Key & BPM Correction Modal
|
||
function initEditKeyOptions() {
|
||
const select = document.getElementById('edit-key-select');
|
||
if (!select) return;
|
||
|
||
const camelotKeys = [
|
||
['12B', 'E'], ['12A', 'Dbm (C#m)'],
|
||
['1B', 'B'], ['1A', 'Abm (G#m)'],
|
||
['2B', 'F#'], ['2A', 'Ebm (D#m)'],
|
||
['3B', 'Db'], ['3A', 'Bbm (A#m)'],
|
||
['4B', 'Ab'], ['4A', 'Fm'],
|
||
['5B', 'Eb'], ['5A', 'Cm'],
|
||
['6B', 'Bb'], ['6A', 'Gm'],
|
||
['7B', 'F'], ['7A', 'Dm'],
|
||
['8B', 'C'], ['8A', 'Am'],
|
||
['9B', 'G'], ['9A', 'Em'],
|
||
['10B', 'D'], ['10A', 'Bm'],
|
||
['11B', 'A'], ['11A', 'F#m'],
|
||
];
|
||
|
||
select.innerHTML = '';
|
||
camelotKeys.forEach(([c, m]) => {
|
||
const opt = document.createElement('option');
|
||
opt.value = c;
|
||
opt.textContent = `${c} — ${m}`;
|
||
select.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
function openKeyBpmModal() {
|
||
if (!window.currentTrack || !window.currentTrack.title) {
|
||
alert('Bitte wähle zuerst einen Track aus oder spiele einen Song ab.');
|
||
return;
|
||
}
|
||
|
||
const modal = document.getElementById('edit-modal');
|
||
const titleEl = document.getElementById('modal-track-title');
|
||
const keySelect = document.getElementById('edit-key-select');
|
||
const bpmInput = document.getElementById('edit-bpm-input');
|
||
|
||
titleEl.textContent = `Daten korrigieren: ${window.currentTrack.artist} – ${window.currentTrack.title}`;
|
||
if (window.currentTrack.key) keySelect.value = window.currentTrack.key;
|
||
if (window.currentTrack.bpm) bpmInput.value = window.currentTrack.bpm;
|
||
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
function closeKeyBpmModal() {
|
||
const modal = document.getElementById('edit-modal');
|
||
if (modal) modal.style.display = 'none';
|
||
}
|
||
|
||
function closeModalOnOverlay(e) {
|
||
if (e.target.classList.contains('modal-overlay')) {
|
||
closeKeyBpmModal();
|
||
}
|
||
}
|
||
|
||
async function saveKeyBpmCorrection() {
|
||
if (!window.currentTrack) return;
|
||
|
||
const keySelect = document.getElementById('edit-key-select');
|
||
const bpmInput = document.getElementById('edit-bpm-input');
|
||
|
||
const newKey = keySelect.value;
|
||
const newBpm = parseInt(bpmInput.value, 10);
|
||
|
||
if (!newKey || isNaN(newBpm)) {
|
||
alert('Bitte gültigen Key und BPM eingeben.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/tracks/correct', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
artist: window.currentTrack.artist,
|
||
title: window.currentTrack.title,
|
||
camelotKey: newKey,
|
||
bpm: newBpm
|
||
})
|
||
});
|
||
|
||
const json = await res.json();
|
||
if (json.status === 'success') {
|
||
window.currentTrack.key = newKey;
|
||
window.currentTrack.bpm = newBpm;
|
||
window.currentTrack.is_corrected = true;
|
||
|
||
document.getElementById('stat-source-sub').textContent = 'PostgreSQL (Korrigiert)';
|
||
updateHeroStats(newKey, newBpm, window.currentTrack.tags || ['dance']);
|
||
renderSuggestions(newKey, newBpm, window.currentTrack.artist, window.currentTrack.title);
|
||
closeKeyBpmModal();
|
||
} else {
|
||
alert('Speichern fehlgeschlagen: ' + (json.error || 'Serverfehler'));
|
||
}
|
||
} catch (err) {
|
||
console.error('Correction save error:', err);
|
||
alert('Verbindungsfehler beim Speichern in PostgreSQL.');
|
||
}
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
function selectKeyFromWheel(key, musicKey) {
|
||
if (!key) return;
|
||
const bpm = currentRefBpm || 120;
|
||
|
||
// Set hero title/artist if no active track
|
||
if (!window.currentTrack || !window.currentTrack.title) {
|
||
const titleEl = document.getElementById('track-title');
|
||
const artistEl = document.getElementById('track-artist');
|
||
if (titleEl) titleEl.textContent = `Harmonie für ${key}`;
|
||
if (artistEl) artistEl.textContent = musicKey ? `${musicKey} (Auswahl über Wheel)` : 'Auswahl über Wheel';
|
||
}
|
||
|
||
const keyColor = window.colorMatrix ? window.colorMatrix[key] : '#4F9D69';
|
||
const keyEl = document.getElementById('stat-key');
|
||
const keySubEl = document.getElementById('stat-key-sub');
|
||
if (keyEl) {
|
||
keyEl.textContent = key;
|
||
keyEl.style.color = keyColor;
|
||
}
|
||
if (keySubEl) {
|
||
keySubEl.textContent = musicKey || (window.camelotToMusicKey ? window.camelotToMusicKey[key] : '');
|
||
}
|
||
document.documentElement.style.setProperty('--key-current', keyColor);
|
||
|
||
window.updateWheel(key);
|
||
renderSuggestions(key, bpm, window.currentTrack ? window.currentTrack.artist : '', window.currentTrack ? window.currentTrack.title : '');
|
||
}
|
||
|
||
window.renderSuggestions = renderSuggestions;
|
||
window.selectTrack = selectTrack;
|
||
window.selectKeyFromWheel = selectKeyFromWheel;
|
||
window.openKeyBpmModal = openKeyBpmModal;
|
||
window.closeKeyBpmModal = closeKeyBpmModal;
|
||
window.saveKeyBpmCorrection = saveKeyBpmCorrection;
|
||
window.closeModalOnOverlay = closeModalOnOverlay;
|
||
window.setHarmonyFilter = setHarmonyFilter;
|
||
window.setBpmTolerance = setBpmTolerance;
|