Add blacklist viewer on Settings tools page

New tool card shows blocked source count. "View Blacklist" opens a
modal listing all blacklisted sources with track name, filename,
username, service icon, and time ago. Each entry has a remove button
to unblock. Empty state explains how to blacklist from Source Info.
This commit is contained in:
Broque Thomas 2026-04-01 15:35:07 -07:00
parent b8006d93c3
commit 27aca53339
3 changed files with 170 additions and 0 deletions

View file

@ -1039,6 +1039,23 @@
<button onclick="openCacheHealthModal()" class="tool-card-btn-secondary">Cache Health</button>
</div>
</div>
<!-- Download Blacklist Card -->
<div class="tool-card" id="blacklist-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Download Blacklist</h4>
</div>
<p class="tool-card-info">Blocked sources that won't be used for future downloads</p>
<div class="tool-card-stats">
<div class="stat-item">
<span class="stat-item-label">Blocked:</span>
<span class="stat-item-value" id="blacklist-count">0</span>
</div>
</div>
<div class="tool-card-controls">
<button onclick="openBlacklistModal()">View Blacklist</button>
</div>
</div>
</div>
</div>

View file

@ -2839,6 +2839,7 @@ async function loadPageData(pageId) {
await loadSettingsData();
await loadQualityProfile();
loadApiKeys();
loadBlacklistCount();
switchSettingsTab('connections');
break;
case 'stats':
@ -20357,6 +20358,95 @@ function _cleanupModalSync(playlistId, finalStatus) {
let _mcacheCurrentTab = 'artist';
let _mcachePage = 0;
let _mcacheSearchTimeout = null;
// ==================================================================================
// DOWNLOAD BLACKLIST VIEWER
// ==================================================================================
async function loadBlacklistCount() {
try {
const res = await fetch('/api/library/blacklist');
const data = await res.json();
const el = document.getElementById('blacklist-count');
if (el) el.textContent = data.entries?.length || 0;
} catch (e) { /* ignore */ }
}
async function openBlacklistModal() {
const existing = document.getElementById('blacklist-modal-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.id = 'blacklist-modal-overlay';
overlay.className = 'redownload-overlay';
overlay.onclick = e => { if (e.target === overlay) overlay.remove(); };
overlay.innerHTML = `
<div class="blacklist-modal">
<div class="blacklist-modal-header">
<h3>Download Blacklist</h3>
<button class="redownload-close" onclick="document.getElementById('blacklist-modal-overlay')?.remove()">&times;</button>
</div>
<div class="blacklist-modal-body" id="blacklist-modal-body">
<div class="redownload-loading"><div class="server-search-spinner"></div>Loading...</div>
</div>
</div>
`;
document.body.appendChild(overlay);
const escH = e => { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', escH); } };
document.addEventListener('keydown', escH);
try {
const res = await fetch('/api/library/blacklist');
const data = await res.json();
const body = document.getElementById('blacklist-modal-body');
if (!data.success || !data.entries || data.entries.length === 0) {
body.innerHTML = '<div class="blacklist-empty">No blocked sources. Sources can be blacklisted from the Source Info () button on tracks in the enhanced library view.</div>';
return;
}
const serviceIcons = { soulseek: '🔍', youtube: '▶️', tidal: '🌊', qobuz: '🎵', hifi: '🎧', deezer_dl: '💜' };
body.innerHTML = data.entries.map(e => {
const displayFile = (e.blocked_filename || '').replace(/\\/g, '/').split('/').pop() || 'Unknown';
const svc = e.blocked_username && ['youtube','tidal','qobuz','hifi','deezer_dl'].includes(e.blocked_username) ? e.blocked_username : 'soulseek';
const icon = serviceIcons[svc] || '🔍';
const ago = e.created_at ? timeAgo(e.created_at) : '';
return `
<div class="blacklist-entry">
<div class="blacklist-entry-icon">${icon}</div>
<div class="blacklist-entry-info">
<div class="blacklist-entry-track">${_esc(e.track_artist || '')}${e.track_artist && e.track_title ? ' — ' : ''}${_esc(e.track_title || '')}</div>
<div class="blacklist-entry-file" title="${_esc(e.blocked_filename || '')}">${_esc(displayFile)}</div>
${e.blocked_username && svc === 'soulseek' ? `<div class="blacklist-entry-user">from ${_esc(e.blocked_username)}</div>` : ''}
</div>
<div class="blacklist-entry-meta">${ago}</div>
<button class="blacklist-entry-remove" onclick="_removeBlacklistEntry(${e.id}, this)" title="Remove from blacklist"></button>
</div>`;
}).join('');
} catch (e) {
document.getElementById('blacklist-modal-body').innerHTML = `<div class="blacklist-empty">Error: ${e.message}</div>`;
}
}
async function _removeBlacklistEntry(id, btn) {
if (!await showConfirmDialog({ title: 'Remove from Blacklist', message: 'Allow this source to be used for downloads again?', confirmText: 'Remove' })) return;
try {
const res = await fetch(`/api/library/blacklist/${id}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
btn.closest('.blacklist-entry').remove();
showToast('Removed from blacklist', 'success');
loadBlacklistCount();
}
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
}
const MCACHE_PAGE_SIZE = 48;
function openMetadataCacheModal() {

View file

@ -52486,3 +52486,66 @@ tr:hover .enhanced-track-actions-group { opacity: 1; }
.source-info-history {
padding: 8px 16px 14px; font-size: 10px; color: rgba(255,255,255,0.2); text-align: center;
}
/* ── Blacklist Modal ── */
.blacklist-modal {
background: rgba(18, 18, 26, 0.95);
backdrop-filter: blur(30px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
max-width: 600px;
width: 95vw;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 32px 80px rgba(0, 0, 0, 0.6);
}
.blacklist-modal-header {
display: flex; justify-content: space-between; align-items: center;
padding: 20px 24px 16px;
}
.blacklist-modal-header h3 { font-size: 16px; font-weight: 700; color: #fff; margin: 0; }
.blacklist-modal-body { padding: 0 24px 24px; overflow-y: auto; flex: 1; }
.blacklist-modal-body::-webkit-scrollbar { width: 4px; }
.blacklist-modal-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 2px; }
.blacklist-empty {
text-align: center; padding: 30px; font-size: 12px;
color: rgba(255, 255, 255, 0.25); line-height: 1.6;
}
.blacklist-entry {
display: flex; align-items: center; gap: 12px;
padding: 10px 12px; border-radius: 10px; margin-bottom: 4px;
border: 1px solid rgba(255, 255, 255, 0.04);
transition: background 0.15s;
}
.blacklist-entry:hover { background: rgba(255, 255, 255, 0.03); }
.blacklist-entry-icon {
font-size: 16px; flex-shrink: 0;
width: 32px; height: 32px; border-radius: 8px;
background: rgba(239, 83, 80, 0.08);
display: flex; align-items: center; justify-content: center;
}
.blacklist-entry-info { flex: 1; min-width: 0; }
.blacklist-entry-track {
font-size: 13px; font-weight: 600; color: rgba(255, 255, 255, 0.85);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.blacklist-entry-file {
font-size: 10px; color: rgba(255, 255, 255, 0.3); font-family: monospace;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px;
}
.blacklist-entry-user { font-size: 10px; color: rgba(255, 255, 255, 0.25); margin-top: 1px; }
.blacklist-entry-meta { font-size: 10px; color: rgba(255, 255, 255, 0.2); flex-shrink: 0; }
.blacklist-entry-remove {
width: 24px; height: 24px; border-radius: 6px; border: none;
background: transparent; color: rgba(255, 255, 255, 0.2);
cursor: pointer; display: flex; align-items: center; justify-content: center;
font-size: 12px; flex-shrink: 0; transition: all 0.15s;
}
.blacklist-entry-remove:hover { background: rgba(239, 83, 80, 0.12); color: #ef5350; }