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:
parent
b8006d93c3
commit
27aca53339
3 changed files with 170 additions and 0 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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()">×</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() {
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue