374 lines
13 KiB
JavaScript
374 lines
13 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>';
|
||
}
|
||
}
|
||
|
||
async function selectTrack(track) {
|
||
// Update Hero UI
|
||
document.getElementById('track-title').textContent = track.title;
|
||
document.getElementById('track-artist').textContent = track.artist;
|
||
document.getElementById('stat-match').textContent = track.is_corrected ? '100%' : 'Online';
|
||
document.getElementById('stat-source-sub').textContent = track.is_local ? (track.is_corrected ? 'PostgreSQL (Fix)' : 'PostgreSQL') : 'Deezer / Online';
|
||
|
||
// Update Cover
|
||
if (track.cover_url) {
|
||
const coverEl = document.getElementById('cover');
|
||
coverEl.innerHTML = `<img src="${track.cover_url}" alt="cover">`;
|
||
} else {
|
||
window.fetchCover(`${track.artist} ${track.title}`, document.getElementById('cover'));
|
||
}
|
||
|
||
// Fetch or apply metadata
|
||
let key = track.camelot_key;
|
||
let bpm = track.bpm;
|
||
let tags = track.tags || ['dance'];
|
||
|
||
if (!key || !bpm) {
|
||
// 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') {
|
||
key = metaJson.data.key;
|
||
bpm = metaJson.data.bpm;
|
||
tags = metaJson.data.tags || tags;
|
||
document.getElementById('stat-match').textContent = metaJson.data.is_corrected ? '100%' : (metaJson.data.source === 'postgresql' ? '100%' : 'Online');
|
||
document.getElementById('stat-source-sub').textContent = metaJson.data.source === 'postgresql' ? (metaJson.data.is_corrected ? 'PostgreSQL (Fix)' : 'PostgreSQL') : 'PostgreSQL (Neu)';
|
||
}
|
||
} catch (e) {
|
||
console.warn('[Search] Metadata fetch failed, using fallback', e);
|
||
key = '8B';
|
||
bpm = 120;
|
||
}
|
||
}
|
||
|
||
window.currentTrack = {
|
||
id: track.id,
|
||
title: track.title,
|
||
artist: track.artist,
|
||
key: key,
|
||
bpm: bpm,
|
||
cover_url: track.cover_url
|
||
};
|
||
|
||
updateHeroStats(key, bpm, tags);
|
||
renderSuggestions(key, bpm, track.artist, track.title);
|
||
}
|
||
|
||
function updateHeroStats(key, bpm, tags = []) {
|
||
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 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);
|
||
}
|
||
|
||
// Render real harmonic suggestions from PostgreSQL
|
||
async function renderSuggestions(currentKey, currentBpm = 120, artist = '', title = '') {
|
||
const sugContainer = document.getElementById('suggestions');
|
||
if (!sugContainer) return;
|
||
|
||
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 aus PostgreSQL...</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;
|
||
}
|
||
|
||
if (!json.data || json.data.length === 0) {
|
||
sugContainer.innerHTML = '<div style="color:var(--text-dim);font-size:12px;font-family:monospace;">Keine passenden Tracks in der Datenbank gefunden. Suche weitere Tracks, um deine Library zu erweitern.</div>';
|
||
return;
|
||
}
|
||
|
||
sugContainer.innerHTML = '';
|
||
json.data.forEach(s => {
|
||
const div = document.createElement('div');
|
||
div.className = 'sug';
|
||
const idSafe = `sug-${s.c}-${(s.title || '').replace(/[^A-Za-z0-9]/g, '')}`;
|
||
|
||
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>'}
|
||
</div>
|
||
<div class="sug-meta">
|
||
<div class="sug-key" style="background:${s.color}">${s.c}</div>
|
||
<div class="sug-bpm">${s.bpm} BPM</div>
|
||
</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-compat">${s.compatLabel || 'KOMPATIBEL'}</div>
|
||
`;
|
||
|
||
// 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'));
|
||
}
|
||
});
|
||
|
||
} 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, '"');
|
||
}
|
||
|
||
window.renderSuggestions = renderSuggestions;
|
||
window.selectTrack = selectTrack;
|
||
window.openKeyBpmModal = openKeyBpmModal;
|
||
window.closeKeyBpmModal = closeKeyBpmModal;
|
||
window.saveKeyBpmCorrection = saveKeyBpmCorrection;
|
||
window.closeModalOnOverlay = closeModalOnOverlay;
|