diff --git a/webui/index.html b/webui/index.html index a9e0749d..b96cea07 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1039,6 +1039,23 @@ + + +
+
+

Download Blacklist

+
+

Blocked sources that won't be used for future downloads

+
+
+ Blocked: + 0 +
+
+
+ +
+
diff --git a/webui/static/script.js b/webui/static/script.js index 01fda8a3..a38cec53 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -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 = ` +
+
+

Download Blacklist

+ +
+
+
Loading...
+
+
+ `; + + 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 = '
No blocked sources. Sources can be blacklisted from the Source Info (ℹ) button on tracks in the enhanced library view.
'; + 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 ` +
+
${icon}
+
+
${_esc(e.track_artist || '')}${e.track_artist && e.track_title ? ' — ' : ''}${_esc(e.track_title || '')}
+
${_esc(displayFile)}
+ ${e.blocked_username && svc === 'soulseek' ? `
from ${_esc(e.blocked_username)}
` : ''} +
+
${ago}
+ +
`; + }).join(''); + + } catch (e) { + document.getElementById('blacklist-modal-body').innerHTML = `
Error: ${e.message}
`; + } +} + +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() { diff --git a/webui/static/style.css b/webui/static/style.css index c44dd291..bcb6e889 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -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; }