// ── Blocklist modal ── // A proper artist/album/track blacklist. Search the active metadata source, // block by ID (cross-source matched in the background so a ban survives a // source switch), and manage existing bans. Opened from the Watchlist page. // Distinct from the download-source blacklist. let _blEntityType = 'artist'; // active tab: artist | album | track let _blSearchSeq = 0; // guards against out-of-order search results function openBlocklistModal(initialType) { _blEntityType = ['artist', 'album', 'track'].includes(initialType) ? initialType : 'artist'; let overlay = document.getElementById('blocklist-modal-overlay'); if (!overlay) { overlay = document.createElement('div'); overlay.id = 'blocklist-modal-overlay'; overlay.className = 'modal-overlay blocklist-modal-overlay'; overlay.innerHTML = `

Blocklist

Block an artist, album, or track from ever being downloaded. Matched across all your metadata sources.

Currently blocked
`; overlay.addEventListener('click', (e) => { if (e.target === overlay) closeBlocklistModal(); }); document.body.appendChild(overlay); } overlay.classList.remove('hidden'); _blRefreshTabs(); _blLoadCurrent(); const input = document.getElementById('blocklist-search-input'); if (input) { input.value = ''; input.focus(); } document.getElementById('blocklist-search-results').innerHTML = ''; } function closeBlocklistModal() { const o = document.getElementById('blocklist-modal-overlay'); if (o) o.classList.add('hidden'); } function switchBlocklistTab(type) { if (type === _blEntityType) return; _blEntityType = type; _blRefreshTabs(); const input = document.getElementById('blocklist-search-input'); if (input) input.value = ''; document.getElementById('blocklist-search-results').innerHTML = ''; _blLoadCurrent(); } function _blRefreshTabs() { document.querySelectorAll('.blocklist-tab').forEach(b => b.classList.toggle('active', b.dataset.bl === _blEntityType)); const input = document.getElementById('blocklist-search-input'); if (input) input.placeholder = `Search ${_blEntityType}s to block…`; } let _blSearchTimer = null; function onBlocklistSearchInput() { clearTimeout(_blSearchTimer); _blSearchTimer = setTimeout(_blRunSearch, 300); // debounce } async function _blRunSearch() { const input = document.getElementById('blocklist-search-input'); const box = document.getElementById('blocklist-search-results'); const spinner = document.getElementById('blocklist-search-spinner'); const q = (input.value || '').trim(); if (!q) { box.innerHTML = ''; return; } const seq = ++_blSearchSeq; spinner.classList.add('spinning'); try { const res = await fetch(`/api/blocklist/search?type=${_blEntityType}&q=${encodeURIComponent(q)}`); const data = await res.json(); if (seq !== _blSearchSeq) return; // a newer search superseded this if (!data.success) throw new Error(data.error || 'Search failed'); const results = data.results || []; if (!results.length) { box.innerHTML = '
No matches.
'; return; } box.innerHTML = results.map(r => { const img = r.image ? `` : `
🎵
`; const payload = encodeURIComponent(JSON.stringify({ name: r.name || 'Unknown', source: r.provider || data.source, source_id: r.id, parent_name: r.extra || '' })); return `
${img}
${escapeHtml(r.name || 'Unknown')}
${r.extra ? `
${escapeHtml(r.extra)}
` : ''}
`; }).join(''); } catch (e) { if (seq === _blSearchSeq) box.innerHTML = `
Couldn't search: ${escapeHtml(e.message)}
`; } finally { if (seq === _blSearchSeq) spinner.classList.remove('spinning'); } } async function blockFromSearch(payloadEnc) { let p; try { p = JSON.parse(decodeURIComponent(payloadEnc)); } catch (e) { return; } try { const res = await fetch('/api/blocklist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entity_type: _blEntityType, ...p }), }); const data = await res.json(); if (data.success) { showToast(`Blocked ${_blEntityType}: ${p.name}`, 'success'); const input = document.getElementById('blocklist-search-input'); if (input) input.value = ''; document.getElementById('blocklist-search-results').innerHTML = ''; _blLoadCurrent(); } else { showToast(data.error || 'Failed to block', 'error'); } } catch (e) { showToast('Error blocking item', 'error'); } } async function _blLoadCurrent() { const box = document.getElementById('blocklist-current'); box.innerHTML = '
Loading…
'; try { const res = await fetch(`/api/blocklist?entity_type=${_blEntityType}`); const data = await res.json(); if (!data.success) throw new Error(data.error || 'Failed to load'); const entries = data.entries || []; if (!entries.length) { box.innerHTML = `
No blocked ${_blEntityType}s yet.
`; return; } box.innerHTML = entries.map(e => { const sources = ['spotify_id', 'itunes_id', 'deezer_id', 'musicbrainz_id'] .filter(k => e[k]).length; const matchTag = e.match_status === 'matched' || sources >= 2 ? `${sources}★` : ``; return `
${escapeHtml(e.name)}
${e.parent_name ? `
${escapeHtml(e.parent_name)}
` : ''}
${matchTag}
`; }).join(''); } catch (e) { box.innerHTML = `
Couldn't load: ${escapeHtml(e.message)}
`; } } async function unblockEntry(id) { try { const res = await fetch(`/api/blocklist/${id}`, { method: 'DELETE' }); const data = await res.json(); if (data.success) { showToast('Removed from blocklist', 'success'); _blLoadCurrent(); } else showToast(data.error || 'Failed to remove', 'error'); } catch (e) { showToast('Error removing entry', 'error'); } }