artist blocking

This commit is contained in:
Eduard Prigoana 2026-02-10 21:03:48 +00:00
parent ea005c68ad
commit f6dae2223f
8 changed files with 759 additions and 49 deletions

View file

@ -47,6 +47,31 @@
<li data-action="track-info" data-type-filter="track">Track info</li>
<li data-action="open-original-url" data-type-filter="track">Open original URL</li>
<li data-action="download">Download</li>
<li class="separator"></li>
<li
data-action="block-track"
data-type-filter="track"
data-label-block="Block track"
data-label-unblock="Unblock track"
>
Block track
</li>
<li
data-action="block-album"
data-type-filter="album,track"
data-label-block="Block album"
data-label-unblock="Unblock album"
>
Block album
</li>
<li
data-action="block-artist"
data-type-filter="track,album,artist"
data-label-block="Block artist"
data-label-unblock="Unblock artist"
>
Block artist
</li>
</ul>
</div>
@ -3676,6 +3701,78 @@
</div>
<ul id="api-instance-list"></ul>
</div>
<div
class="setting-item"
style="padding-bottom: 1rem; border-top: 1px solid var(--border)"
>
<div class="info">
<span class="label">Blocked Content</span>
<span class="description"
>Manage artists, albums, and tracks you've blocked from
recommendations</span
>
</div>
<div style="display: flex; gap: 0.5rem">
<button id="manage-blocked-btn" class="btn-secondary">Manage</button>
<button
id="clear-all-blocked-btn"
class="btn-secondary danger"
style="display: none"
>
Clear All
</button>
</div>
</div>
<div id="blocked-content-list" style="display: none">
<div id="blocked-artists-section" style="margin-bottom: 1rem">
<h4
style="
font-size: 0.9rem;
margin-bottom: 0.5rem;
color: var(--muted-foreground);
"
>
Blocked Artists
</h4>
<ul id="blocked-artists-list" class="blocked-items-list"></ul>
</div>
<div id="blocked-albums-section" style="margin-bottom: 1rem">
<h4
style="
font-size: 0.9rem;
margin-bottom: 0.5rem;
color: var(--muted-foreground);
"
>
Blocked Albums
</h4>
<ul id="blocked-albums-list" class="blocked-items-list"></ul>
</div>
<div id="blocked-tracks-section" style="margin-bottom: 1rem">
<h4
style="
font-size: 0.9rem;
margin-bottom: 0.5rem;
color: var(--muted-foreground);
"
>
Blocked Tracks
</h4>
<ul id="blocked-tracks-list" class="blocked-items-list"></ul>
</div>
<div
id="blocked-empty-message"
style="
text-align: center;
padding: 1rem;
color: var(--muted-foreground);
display: none;
"
>
No blocked content
</div>
</div>
</div>
</div>
</div>

View file

@ -726,6 +726,13 @@ export async function handleTrackAction(
if (isCollection && collectionActions.includes(action)) {
try {
// Check if album/artist is blocked
const { contentBlockingSettings } = await import('./storage.js');
if (type === 'album' && contentBlockingSettings.shouldHideAlbum(item)) {
showNotification('This album is blocked');
return;
}
let tracks = [];
let collectionItem = item;
@ -780,6 +787,9 @@ export async function handleTrackAction(
return;
}
// Filter blocked tracks from collections
tracks = contentBlockingSettings.filterTracks(tracks);
if (action === 'add-to-queue') {
player.addToQueue(tracks);
if (window.renderQueueFunction) window.renderQueueFunction();
@ -835,6 +845,13 @@ export async function handleTrackAction(
}
// Individual Track Actions
// Check if track/artist is blocked
const { contentBlockingSettings } = await import('./storage.js');
if (type === 'track' && contentBlockingSettings.shouldHideTrack(item)) {
showNotification('This track is blocked');
return;
}
if (action === 'add-to-queue') {
player.addToQueue(item);
if (window.renderQueueFunction) window.renderQueueFunction();
@ -1242,6 +1259,57 @@ export async function handleTrackAction(
} else {
showNotification('No original URL available for this track.');
}
} else if (action === 'block-track') {
const { contentBlockingSettings } = await import('./storage.js');
if (contentBlockingSettings.isTrackBlocked(item.id)) {
contentBlockingSettings.unblockTrack(item.id);
showNotification(`Unblocked track: ${item.title}`);
} else {
contentBlockingSettings.blockTrack(item);
showNotification(`Blocked track: ${item.title}`);
}
} else if (action === 'block-album') {
const { contentBlockingSettings } = await import('./storage.js');
const albumId = type === 'album' ? item.id : item.album?.id;
const albumTitle = type === 'album' ? item.title : item.album?.title;
const albumArtist = type === 'album' ? item.artist : item.album?.artist;
if (!albumId) {
showNotification('No album information available');
return;
}
if (contentBlockingSettings.isAlbumBlocked(albumId)) {
contentBlockingSettings.unblockAlbum(albumId);
showNotification(`Unblocked album: ${albumTitle || 'Unknown Album'}`);
} else {
contentBlockingSettings.blockAlbum({
id: albumId,
title: albumTitle,
artist: albumArtist,
});
showNotification(`Blocked album: ${albumTitle || 'Unknown Album'}`);
}
} else if (action === 'block-artist') {
const { contentBlockingSettings } = await import('./storage.js');
const artistId = item.artist?.id || item.artists?.[0]?.id;
const artistName = item.artist?.name || item.artists?.[0]?.name || item.name;
if (!artistId) {
showNotification('No artist information available');
return;
}
if (contentBlockingSettings.isArtistBlocked(artistId)) {
contentBlockingSettings.unblockArtist(artistId);
showNotification(`Unblocked artist: ${artistName || 'Unknown Artist'}`);
} else {
contentBlockingSettings.blockArtist({
id: artistId,
name: artistName,
});
showNotification(`Blocked artist: ${artistName || 'Unknown Artist'}`);
}
}
}
@ -1267,8 +1335,37 @@ async function updateContextMenuLikeState(contextMenu, contextTrack) {
openOriginalUrlItem.style.display = isUnreleased ? 'block' : 'none';
}
// Filter items based on type
// Update block/unblock labels
const { contentBlockingSettings } = await import('./storage.js');
const type = contextMenu._contextType || 'track';
const blockTrackItem = contextMenu.querySelector('li[data-action="block-track"]');
if (blockTrackItem) {
const isBlocked = contentBlockingSettings.isTrackBlocked(contextTrack.id);
blockTrackItem.textContent = isBlocked
? blockTrackItem.dataset.labelUnblock || 'Unblock track'
: blockTrackItem.dataset.labelBlock || 'Block track';
}
const blockAlbumItem = contextMenu.querySelector('li[data-action="block-album"]');
if (blockAlbumItem) {
const albumId = type === 'album' ? contextTrack.id : contextTrack.album?.id;
const isBlocked = albumId ? contentBlockingSettings.isAlbumBlocked(albumId) : false;
blockAlbumItem.textContent = isBlocked
? blockAlbumItem.dataset.labelUnblock || 'Unblock album'
: blockAlbumItem.dataset.labelBlock || 'Block album';
}
const blockArtistItem = contextMenu.querySelector('li[data-action="block-artist"]');
if (blockArtistItem) {
const artistId = contextTrack.artist?.id || contextTrack.artists?.[0]?.id;
const isBlocked = artistId ? contentBlockingSettings.isArtistBlocked(artistId) : false;
blockArtistItem.textContent = isBlocked
? blockArtistItem.dataset.labelUnblock || 'Unblock artist'
: blockArtistItem.dataset.labelBlock || 'Block artist';
}
// Filter items based on type
contextMenu.querySelectorAll('li[data-action]').forEach((item) => {
const filter = item.dataset.typeFilter;
if (filter) {
@ -1389,7 +1486,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
}
const trackItem = e.target.closest('.track-item');
if (trackItem && trackItem.classList.contains('unavailable')) {
if (trackItem && (trackItem.classList.contains('unavailable') || trackItem.classList.contains('blocked'))) {
return;
}
if (trackItem && !trackItem.dataset.queueIndex && !e.target.closest('.remove-from-playlist-btn')) {
@ -1409,6 +1506,11 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
const card = e.target.closest('.card');
if (card) {
// Don't navigate if card is blocked (unless clicking menu button)
if (card.classList.contains('blocked') && !e.target.closest('.card-menu-btn')) {
return;
}
if (e.target.closest('.edit-playlist-btn') || e.target.closest('.delete-playlist-btn')) {
return;
}

View file

@ -327,6 +327,14 @@ export class Player {
return;
}
// Check if track is blocked
const { contentBlockingSettings } = await import('./storage.js');
if (contentBlockingSettings.shouldHideTrack(track)) {
console.warn(`Attempted to play blocked track: ${track.title}. Skipping...`);
this.playNext();
return;
}
this.saveQueueState();
this.currentTrack = track;
@ -574,33 +582,42 @@ export class Player {
const isLastTrack = this.currentQueueIndex >= currentQueue.length - 1;
if (recursiveCount > currentQueue.length) {
console.error('All tracks in queue are unavailable.');
console.error('All tracks in queue are unavailable or blocked.');
this.audio.pause();
return;
}
if (this.repeatMode === REPEAT_MODE.ONE && !currentQueue[this.currentQueueIndex]?.isUnavailable) {
// Import blocking settings dynamically
import('./storage.js').then(({ contentBlockingSettings }) => {
if (
this.repeatMode === REPEAT_MODE.ONE &&
!currentQueue[this.currentQueueIndex]?.isUnavailable &&
!contentBlockingSettings.shouldHideTrack(currentQueue[this.currentQueueIndex])
) {
this.playTrackFromQueue(0, recursiveCount);
return;
}
if (!isLastTrack) {
this.currentQueueIndex++;
const track = currentQueue[this.currentQueueIndex];
// Skip unavailable and blocked tracks
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
return this.playNext(recursiveCount + 1);
}
} else if (this.repeatMode === REPEAT_MODE.ALL) {
this.currentQueueIndex = 0;
const track = currentQueue[this.currentQueueIndex];
// Skip unavailable and blocked tracks
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
return this.playNext(recursiveCount + 1);
}
} else {
return;
}
this.playTrackFromQueue(0, recursiveCount);
return;
}
if (!isLastTrack) {
this.currentQueueIndex++;
// Skip unavailable tracks
if (currentQueue[this.currentQueueIndex].isUnavailable) {
return this.playNext(recursiveCount + 1);
}
} else if (this.repeatMode === REPEAT_MODE.ALL) {
this.currentQueueIndex = 0;
// Skip unavailable tracks
if (currentQueue[this.currentQueueIndex].isUnavailable) {
return this.playNext(recursiveCount + 1);
}
} else {
return;
}
this.playTrackFromQueue(0, recursiveCount);
});
}
playPrev(recursiveCount = 0) {
@ -609,19 +626,22 @@ export class Player {
this.updateMediaSessionPositionState();
} else if (this.currentQueueIndex > 0) {
this.currentQueueIndex--;
// Skip unavailable tracks
// Skip unavailable and blocked tracks
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
if (recursiveCount > currentQueue.length) {
console.error('All tracks in queue are unavailable.');
console.error('All tracks in queue are unavailable or blocked.');
this.audio.pause();
return;
}
if (currentQueue[this.currentQueueIndex].isUnavailable) {
return this.playPrev(recursiveCount + 1);
}
this.playTrackFromQueue(0, recursiveCount);
import('./storage.js').then(({ contentBlockingSettings }) => {
const track = currentQueue[this.currentQueueIndex];
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
return this.playPrev(recursiveCount + 1);
}
this.playTrackFromQueue(0, recursiveCount);
});
}
}

View file

@ -29,6 +29,7 @@ import {
audioEffectsSettings,
settingsUiState,
pwaUpdateSettings,
contentBlockingSettings,
} from './storage.js';
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
import { getButterchurnPresets } from './visualizers/butterchurn.js';
@ -1858,6 +1859,9 @@ export function initializeSettings(scrobbler, player, api, ui) {
// Settings Search functionality
setupSettingsSearch();
// Blocked Content Management
initializeBlockedContentManager();
}
function initializeFontSettings() {
@ -2107,3 +2111,138 @@ function filterSettings(query) {
group.style.display = hasMatch ? '' : 'none';
});
}
function initializeBlockedContentManager() {
const manageBtn = document.getElementById('manage-blocked-btn');
const clearAllBtn = document.getElementById('clear-all-blocked-btn');
const blockedListContainer = document.getElementById('blocked-content-list');
const blockedArtistsList = document.getElementById('blocked-artists-list');
const blockedAlbumsList = document.getElementById('blocked-albums-list');
const blockedTracksList = document.getElementById('blocked-tracks-list');
const blockedArtistsSection = document.getElementById('blocked-artists-section');
const blockedAlbumsSection = document.getElementById('blocked-albums-section');
const blockedTracksSection = document.getElementById('blocked-tracks-section');
const blockedEmptyMessage = document.getElementById('blocked-empty-message');
if (!manageBtn || !blockedListContainer) return;
function renderBlockedLists() {
const artists = contentBlockingSettings.getBlockedArtists();
const albums = contentBlockingSettings.getBlockedAlbums();
const tracks = contentBlockingSettings.getBlockedTracks();
const totalCount = artists.length + albums.length + tracks.length;
// Update manage button text
manageBtn.textContent = totalCount > 0 ? `Manage (${totalCount})` : 'Manage';
// Show/hide clear all button
if (clearAllBtn) {
clearAllBtn.style.display = totalCount > 0 ? 'inline-block' : 'none';
}
// Show/hide sections
blockedArtistsSection.style.display = artists.length > 0 ? 'block' : 'none';
blockedAlbumsSection.style.display = albums.length > 0 ? 'block' : 'none';
blockedTracksSection.style.display = tracks.length > 0 ? 'block' : 'none';
blockedEmptyMessage.style.display = totalCount === 0 ? 'block' : 'none';
// Render artists
if (blockedArtistsList) {
blockedArtistsList.innerHTML = artists
.map(
(artist) => `
<li data-id="${artist.id}" data-type="artist">
<div class="item-info">
<div class="item-name">${escapeHtml(artist.name)}</div>
<div class="item-meta">${new Date(artist.blockedAt).toLocaleDateString()}</div>
</div>
<button class="unblock-btn" data-id="${artist.id}" data-type="artist">Unblock</button>
</li>
`
)
.join('');
}
// Render albums
if (blockedAlbumsList) {
blockedAlbumsList.innerHTML = albums
.map(
(album) => `
<li data-id="${album.id}" data-type="album">
<div class="item-info">
<div class="item-name">${escapeHtml(album.title)}</div>
<div class="item-meta">${escapeHtml(album.artist || 'Unknown Artist')} ${new Date(album.blockedAt).toLocaleDateString()}</div>
</div>
<button class="unblock-btn" data-id="${album.id}" data-type="album">Unblock</button>
</li>
`
)
.join('');
}
// Render tracks
if (blockedTracksList) {
blockedTracksList.innerHTML = tracks
.map(
(track) => `
<li data-id="${track.id}" data-type="track">
<div class="item-info">
<div class="item-name">${escapeHtml(track.title)}</div>
<div class="item-meta">${escapeHtml(track.artist || 'Unknown Artist')} ${new Date(track.blockedAt).toLocaleDateString()}</div>
</div>
<button class="unblock-btn" data-id="${track.id}" data-type="track">Unblock</button>
</li>
`
)
.join('');
}
// Add unblock button handlers
blockedListContainer.querySelectorAll('.unblock-btn').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = btn.dataset.id;
const type = btn.dataset.type;
if (type === 'artist') {
contentBlockingSettings.unblockArtist(id);
} else if (type === 'album') {
contentBlockingSettings.unblockAlbum(id);
} else if (type === 'track') {
contentBlockingSettings.unblockTrack(id);
}
renderBlockedLists();
});
});
}
// Toggle blocked list visibility
manageBtn.addEventListener('click', () => {
const isVisible = blockedListContainer.style.display !== 'none';
blockedListContainer.style.display = isVisible ? 'none' : 'block';
if (!isVisible) {
renderBlockedLists();
}
});
// Clear all blocked content
if (clearAllBtn) {
clearAllBtn.addEventListener('click', () => {
if (confirm('Are you sure you want to unblock all artists, albums, and tracks?')) {
contentBlockingSettings.clearAllBlocked();
renderBlockedLists();
}
});
}
// Initial render
renderBlockedLists();
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

View file

@ -1706,3 +1706,172 @@ export const pwaUpdateSettings = {
localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false');
},
};
export const contentBlockingSettings = {
BLOCKED_ARTISTS_KEY: 'blocked-artists',
BLOCKED_TRACKS_KEY: 'blocked-tracks',
BLOCKED_ALBUMS_KEY: 'blocked-albums',
// Blocked Artists
getBlockedArtists() {
try {
const data = localStorage.getItem(this.BLOCKED_ARTISTS_KEY);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
},
setBlockedArtists(artists) {
localStorage.setItem(this.BLOCKED_ARTISTS_KEY, JSON.stringify(artists));
},
isArtistBlocked(artistId) {
if (!artistId) return false;
return this.getBlockedArtists().some((a) => a.id === artistId);
},
blockArtist(artist) {
if (!artist || !artist.id) return;
const blocked = this.getBlockedArtists();
if (!blocked.some((a) => a.id === artist.id)) {
blocked.push({
id: artist.id,
name: artist.name || 'Unknown Artist',
blockedAt: Date.now(),
});
this.setBlockedArtists(blocked);
}
},
unblockArtist(artistId) {
const blocked = this.getBlockedArtists().filter((a) => a.id !== artistId);
this.setBlockedArtists(blocked);
},
// Blocked Tracks
getBlockedTracks() {
try {
const data = localStorage.getItem(this.BLOCKED_TRACKS_KEY);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
},
setBlockedTracks(tracks) {
localStorage.setItem(this.BLOCKED_TRACKS_KEY, JSON.stringify(tracks));
},
isTrackBlocked(trackId) {
if (!trackId) return false;
return this.getBlockedTracks().some((t) => t.id === trackId);
},
blockTrack(track) {
if (!track || !track.id) return;
const blocked = this.getBlockedTracks();
if (!blocked.some((t) => t.id === track.id)) {
blocked.push({
id: track.id,
title: track.title || 'Unknown Track',
artist: track.artist?.name || track.artist || 'Unknown Artist',
blockedAt: Date.now(),
});
this.setBlockedTracks(blocked);
}
},
unblockTrack(trackId) {
const blocked = this.getBlockedTracks().filter((t) => t.id !== trackId);
this.setBlockedTracks(blocked);
},
// Blocked Albums
getBlockedAlbums() {
try {
const data = localStorage.getItem(this.BLOCKED_ALBUMS_KEY);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
},
setBlockedAlbums(albums) {
localStorage.setItem(this.BLOCKED_ALBUMS_KEY, JSON.stringify(albums));
},
isAlbumBlocked(albumId) {
if (!albumId) return false;
return this.getBlockedAlbums().some((a) => a.id === albumId);
},
blockAlbum(album) {
if (!album || !album.id) return;
const blocked = this.getBlockedAlbums();
if (!blocked.some((a) => a.id === album.id)) {
blocked.push({
id: album.id,
title: album.title || 'Unknown Album',
artist: album.artist?.name || album.artist || 'Unknown Artist',
blockedAt: Date.now(),
});
this.setBlockedAlbums(blocked);
}
},
unblockAlbum(albumId) {
const blocked = this.getBlockedAlbums().filter((a) => a.id !== albumId);
this.setBlockedAlbums(blocked);
},
// Check if track should be hidden (blocked track or by blocked artist)
shouldHideTrack(track) {
if (!track) return true;
if (this.isTrackBlocked(track.id)) return true;
if (track.artist?.id && this.isArtistBlocked(track.artist.id)) return true;
if (track.artists?.some((a) => this.isArtistBlocked(a.id))) return true;
if (track.album?.id && this.isAlbumBlocked(track.album.id)) return true;
return false;
},
// Check if album should be hidden
shouldHideAlbum(album) {
if (!album) return true;
if (this.isAlbumBlocked(album.id)) return true;
if (album.artist?.id && this.isArtistBlocked(album.artist.id)) return true;
if (album.artists?.some((a) => this.isArtistBlocked(a.id))) return true;
return false;
},
// Check if artist should be hidden
shouldHideArtist(artist) {
if (!artist) return true;
return this.isArtistBlocked(artist.id);
},
// Filter arrays
filterTracks(tracks) {
return tracks.filter((t) => !this.shouldHideTrack(t));
},
filterAlbums(albums) {
return albums.filter((a) => !this.shouldHideAlbum(a));
},
filterArtists(artists) {
return artists.filter((a) => !this.shouldHideArtist(a));
},
// Get all blocked items count
getTotalBlockedCount() {
return this.getBlockedArtists().length + this.getBlockedTracks().length + this.getBlockedAlbums().length;
},
// Clear all blocked items
clearAllBlocked() {
localStorage.removeItem(this.BLOCKED_ARTISTS_KEY);
localStorage.removeItem(this.BLOCKED_TRACKS_KEY);
localStorage.removeItem(this.BLOCKED_ALBUMS_KEY);
},
};

View file

@ -11,7 +11,7 @@ import {
createQualityBadgeHTML,
} from './utils.js';
import { sidePanelManager } from './side-panel.js';
import { downloadQualitySettings } from './storage.js';
import { downloadQualitySettings, contentBlockingSettings } from './storage.js';
import { db } from './db.js';
import { syncManager } from './accounts/pocketbase.js';
import { showNotification, downloadTracks } from './downloads.js';
@ -241,12 +241,16 @@ export function initializeUIInteractions(player, api, ui) {
const html = currentQueue
.map((track, index) => {
const isPlaying = index === player.currentQueueIndex;
const isBlocked = contentBlockingSettings?.shouldHideTrack(track);
const trackTitle = getTrackTitle(track);
const trackArtists = getTrackArtists(track, { fallback: 'Unknown' });
const qualityBadge = createQualityBadgeHTML(track);
const blockedTitle = isBlocked
? `title="Blocked: ${contentBlockingSettings.isTrackBlocked(track.id) ? 'Track blocked' : contentBlockingSettings.isArtistBlocked(track.artist?.id) ? 'Artist blocked' : 'Album blocked'}"`
: '';
return `
<div class="queue-track-item ${isPlaying ? 'playing' : ''}" data-queue-index="${index}" data-track-id="${track.id}" draggable="true">
<div class="queue-track-item ${isPlaying ? 'playing' : ''} ${isBlocked ? 'blocked' : ''}" data-queue-index="${index}" data-track-id="${track.id}" draggable="${isBlocked ? 'false' : 'true'}" ${blockedTitle}>
<div class="drag-handle">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="5" y1="8" x2="19" y2="8"></line>
@ -261,7 +265,7 @@ export function initializeUIInteractions(player, api, ui) {
<div class="artist">${escapeHtml(trackArtists)}</div>
</div>
</div>
<div class="track-item-duration">${formatTime(track.duration)}</div>
<div class="track-item-duration">${isBlocked ? '--:--' : formatTime(track.duration)}</div>
<button class="queue-like-btn" data-action="toggle-like" title="Add to Liked">
${SVG_HEART}
</button>
@ -319,6 +323,11 @@ export function initializeUIInteractions(player, api, ui) {
return;
}
// Don't play blocked tracks
if (item.classList.contains('blocked')) {
return;
}
player.playAtIndex(index);
refreshQueuePanel();
});

View file

@ -28,6 +28,7 @@ import {
visualizerSettings,
homePageSettings,
fontSettings,
contentBlockingSettings,
} from './storage.js';
import { db } from './db.js';
import { getVibrantColorFromImage } from './vibrant-color.js';
@ -261,6 +262,7 @@ export class UIRenderer {
createTrackItemHTML(track, index, showCover = false, hasMultipleDiscs = false, useTrackNumber = false) {
const isUnavailable = track.isUnavailable;
const isBlocked = contentBlockingSettings?.shouldHideTrack(track);
const trackImageHTML = showCover
? `<img src="${this.api.getCoverUrl(track.album?.cover)}" alt="Track Cover" class="track-item-cover" loading="lazy">`
: '';
@ -296,11 +298,16 @@ export class UIRenderer {
</button>
`;
const blockedTitle = isBlocked
? `title="Blocked: ${contentBlockingSettings.isTrackBlocked(track.id) ? 'Track blocked' : contentBlockingSettings.isArtistBlocked(track.artist?.id) ? 'Artist blocked' : 'Album blocked'}"`
: '';
return `
<div class="track-item ${isCurrentTrack ? 'playing' : ''} ${isUnavailable ? 'unavailable' : ''}"
<div class="track-item ${isCurrentTrack ? 'playing' : ''} ${isUnavailable ? 'unavailable' : ''} ${isBlocked ? 'blocked' : ''}"
data-track-id="${track.id}"
${track.isLocal ? 'data-is-local="true"' : ''}
${isUnavailable ? 'title="This track is currently unavailable"' : ''}>
${isUnavailable ? 'title="This track is currently unavailable"' : ''}
${blockedTitle}>
${trackNumberHTML}
<div class="track-item-info">
<div class="track-item-details">
@ -312,7 +319,7 @@ export class UIRenderer {
<div class="artist">${escapeHtml(trackArtists)}${yearDisplay}</div>
</div>
</div>
<div class="track-item-duration">${isUnavailable ? '--:--' : track.duration ? formatTime(track.duration) : '--:--'}</div>
<div class="track-item-duration">${isUnavailable || isBlocked ? '--:--' : track.duration ? formatTime(track.duration) : '--:--'}</div>
<div class="track-item-actions">
${actionsHTML}
</div>
@ -496,6 +503,7 @@ export class UIRenderer {
createAlbumCardHTML(album) {
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
const qualityBadge = createQualityBadgeHTML(album);
const isBlocked = contentBlockingSettings?.shouldHideAlbum(album);
let yearDisplay = '';
if (album.releaseDate) {
const date = new Date(album.releaseDate);
@ -521,11 +529,16 @@ export class UIRenderer {
</button>
`,
isCompact,
extraClasses: isBlocked ? 'blocked' : '',
extraAttributes: isBlocked
? `title="Blocked: ${contentBlockingSettings.isAlbumBlocked(album.id) ? 'Album blocked' : 'Artist blocked'}"`
: '',
});
}
createArtistCardHTML(artist) {
const isCompact = cardSettings.isCompactArtist();
const isBlocked = contentBlockingSettings?.shouldHideArtist(artist);
return this.createBaseCardHTML({
type: 'artist',
@ -540,7 +553,8 @@ export class UIRenderer {
</button>
`,
isCompact,
extraClasses: 'artist',
extraClasses: `artist${isBlocked ? ' blocked' : ''}`,
extraAttributes: isBlocked ? 'title="Blocked: Artist blocked"' : '',
});
}
@ -1590,6 +1604,19 @@ export class UIRenderer {
return;
}
// Filter out blocked content
const { contentBlockingSettings } = await import('./storage.js');
items = items.filter((item) => {
if (item.type === 'track') {
return !contentBlockingSettings.shouldHideTrack(item);
} else if (item.type === 'album') {
return !contentBlockingSettings.shouldHideAlbum(item);
} else if (item.type === 'artist') {
return !contentBlockingSettings.shouldHideArtist(item);
}
return true;
});
// Shuffle items if enabled
if (homePageSettings.shouldShuffleEditorsPicks()) {
items = [...items].sort(() => Math.random() - 0.5);
@ -1808,6 +1835,18 @@ export class UIRenderer {
async filterUserContent(items, type) {
if (!items || items.length === 0) return [];
// Import blocking settings
const { contentBlockingSettings } = await import('./storage.js');
// First filter out blocked content
if (type === 'track') {
items = contentBlockingSettings.filterTracks(items);
} else if (type === 'album') {
items = contentBlockingSettings.filterAlbums(items);
} else if (type === 'artist') {
items = contentBlockingSettings.filterArtists(items);
}
const favorites = await db.getFavorites(type);
const favoriteIds = new Set(favorites.map((i) => i.id));
@ -2138,12 +2177,24 @@ export class UIRenderer {
// Similar Artists
this.api
.getSimilarArtists(album.artist.id)
.then((similar) => {
if (similar && similar.length > 0 && similarArtistsContainer && similarArtistsSection) {
similarArtistsContainer.innerHTML = similar
.then(async (similar) => {
// Filter out blocked artists
const { contentBlockingSettings } = await import('./storage.js');
const filteredSimilar = contentBlockingSettings.filterArtists(similar || []);
if (filteredSimilar.length > 0 && similarArtistsContainer && similarArtistsSection) {
similarArtistsContainer.innerHTML = filteredSimilar
.map((a) => this.createArtistCardHTML(a))
.join('');
similarArtistsSection.style.display = 'block';
filteredSimilar.forEach((a) => {
const el = similarArtistsContainer.querySelector(`[data-artist-id="${a.id}"]`);
if (el) {
trackDataStore.set(el, a);
this.updateLikeState(el, 'artist', a.id);
}
});
}
})
.catch((e) => console.warn('Failed to load similar artists:', e));
@ -2151,12 +2202,18 @@ export class UIRenderer {
// Similar Albums
this.api
.getSimilarAlbums(albumId)
.then((similar) => {
if (similar && similar.length > 0 && similarAlbumsContainer && similarAlbumsSection) {
similarAlbumsContainer.innerHTML = similar.map((a) => this.createAlbumCardHTML(a)).join('');
.then(async (similar) => {
// Filter out blocked albums
const { contentBlockingSettings } = await import('./storage.js');
const filteredSimilar = contentBlockingSettings.filterAlbums(similar || []);
if (filteredSimilar.length > 0 && similarAlbumsContainer && similarAlbumsSection) {
similarAlbumsContainer.innerHTML = filteredSimilar
.map((a) => this.createAlbumCardHTML(a))
.join('');
similarAlbumsSection.style.display = 'block';
similar.forEach((a) => {
filteredSimilar.forEach((a) => {
const el = similarAlbumsContainer.querySelector(`[data-album-id="${a.id}"]`);
if (el) {
trackDataStore.set(el, a);
@ -2185,7 +2242,11 @@ export class UIRenderer {
}
try {
const recommendedTracks = await this.api.getRecommendedTracksForPlaylist(tracks, 20);
let recommendedTracks = await this.api.getRecommendedTracksForPlaylist(tracks, 20);
// Filter out blocked tracks
const { contentBlockingSettings } = await import('./storage.js');
recommendedTracks = contentBlockingSettings.filterTracks(recommendedTracks);
if (recommendedTracks.length > 0) {
this.renderListWithTracks(recommendedContainer, recommendedTracks, true);
@ -2739,12 +2800,18 @@ export class UIRenderer {
if (similarContainer && similarSection) {
this.api
.getSimilarArtists(artistId)
.then((similar) => {
if (similar && similar.length > 0) {
similarContainer.innerHTML = similar.map((a) => this.createArtistCardHTML(a)).join('');
.then(async (similar) => {
// Filter out blocked artists
const { contentBlockingSettings } = await import('./storage.js');
const filteredSimilar = contentBlockingSettings.filterArtists(similar || []);
if (filteredSimilar.length > 0) {
similarContainer.innerHTML = filteredSimilar
.map((a) => this.createArtistCardHTML(a))
.join('');
similarSection.style.display = 'block';
similar.forEach((a) => {
filteredSimilar.forEach((a) => {
const el = similarContainer.querySelector(`[data-artist-id="${a.id}"]`);
if (el) {
trackDataStore.set(el, a);

View file

@ -2431,6 +2431,113 @@ input:checked + .slider::before {
color: var(--foreground);
}
#context-menu li.separator {
height: 1px;
background-color: var(--border);
margin: 0.5rem 0;
padding: 0;
cursor: default;
pointer-events: none;
}
#context-menu li.separator:hover {
background-color: var(--border);
transform: none;
}
.blocked-items-list {
list-style: none;
max-height: 300px;
overflow-y: auto;
}
.blocked-items-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--accent);
border-radius: 4px;
margin-bottom: 0.25rem;
}
.blocked-items-list li:hover {
background: var(--secondary);
}
.blocked-items-list .item-info {
flex: 1;
min-width: 0;
}
.blocked-items-list .item-name {
font-weight: 500;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.blocked-items-list .item-meta {
font-size: 0.75rem;
color: var(--muted-foreground);
}
.blocked-items-list .unblock-btn {
background: transparent;
border: none;
color: var(--primary);
cursor: pointer;
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
border-radius: 4px;
}
.blocked-items-list .unblock-btn:hover {
background: var(--secondary);
}
/* Blocked content styling */
.track-item.blocked {
opacity: 0.4;
pointer-events: none;
filter: grayscale(100%);
}
.track-item.blocked .track-item-info {
text-decoration: line-through;
}
.track-item.blocked .track-menu-btn {
pointer-events: auto;
opacity: 1;
}
.card.blocked {
opacity: 0.4;
pointer-events: none;
filter: grayscale(100%);
}
.card.blocked .card-menu-btn {
pointer-events: auto;
opacity: 1;
}
.card.blocked .card-title,
.card.blocked .card-subtitle {
text-decoration: line-through;
}
.queue-track-item.blocked {
opacity: 0.4;
filter: grayscale(100%);
}
.queue-track-item.blocked .track-item-details {
text-decoration: line-through;
}
#queue-modal-overlay {
display: none;
position: fixed;