Files
KeySelector/public/search.js
T

374 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.renderSuggestions = renderSuggestions;
window.selectTrack = selectTrack;
window.openKeyBpmModal = openKeyBpmModal;
window.closeKeyBpmModal = closeKeyBpmModal;
window.saveKeyBpmCorrection = saveKeyBpmCorrection;
window.closeModalOnOverlay = closeModalOnOverlay;