diff --git a/docker-compose.yaml b/docker-compose.yaml index 3d21707..51cf668 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,6 +21,7 @@ services: - REDIS_DB=0 - REDIS_URL=redis://redis:6379/0 - REDIS_BACKEND=redis://redis:6379/0 + - EXPLICIT_FILTER=false # Set to true to filter out explicit content depends_on: - redis diff --git a/routes/config.py b/routes/config.py index fb46c07..de38a76 100644 --- a/routes/config.py +++ b/routes/config.py @@ -4,6 +4,7 @@ from pathlib import Path import logging import threading import time +import os config_bp = Blueprint('config_bp', __name__) CONFIG_PATH = Path('./config/main.json') @@ -91,6 +92,10 @@ def handle_config(): for key, default_value in defaults.items(): if key not in config: config[key] = default_value + + # Get explicit filter setting from environment variable + explicit_filter_env = os.environ.get('EXPLICIT_FILTER', 'false').lower() + config['explicitFilter'] = explicit_filter_env in ('true', '1', 'yes', 'on') return jsonify(config) @@ -101,6 +106,13 @@ def update_config(): if not isinstance(new_config, dict): return jsonify({"error": "Invalid config format"}), 400 + # Get existing config to preserve environment-controlled values + existing_config = get_config() or {} + + # Preserve the explicitFilter setting from environment + explicit_filter_env = os.environ.get('EXPLICIT_FILTER', 'false').lower() + new_config['explicitFilter'] = explicit_filter_env in ('true', '1', 'yes', 'on') + if not save_config(new_config): return jsonify({"error": "Failed to save config"}), 500 diff --git a/static/css/config/config.css b/static/css/config/config.css index 7c4959c..8770f46 100644 --- a/static/css/config/config.css +++ b/static/css/config/config.css @@ -32,6 +32,39 @@ body { transition: all 0.3s ease; } +/* Environment controlled setting styles */ +.env-controlled-setting { + display: flex; + align-items: center; + background-color: #2a2a2a; + border-radius: 8px; + padding: 0.8rem 1rem; + margin-top: 0.5rem; +} + +.env-controlled-value { + flex: 1; + font-weight: 500; +} + +.env-controlled-value.enabled { + color: #1db954; +} + +.env-controlled-value.disabled { + color: #ff5555; +} + +.env-controlled-badge { + background-color: #555; + color: white; + font-size: 0.7rem; + padding: 0.2rem 0.5rem; + border-radius: 4px; + margin-left: 0.5rem; + font-weight: bold; +} + /* Back button as floating icon - keep this for our floating button */ .back-button.floating-icon { position: fixed; diff --git a/static/css/main/base.css b/static/css/main/base.css index 1f71df9..a933b44 100644 --- a/static/css/main/base.css +++ b/static/css/main/base.css @@ -454,4 +454,49 @@ a:hover, a:focus { right: 16px; bottom: 16px; } +} + +/* Add styles for explicit content filter */ +.explicit-filter-placeholder { + background-color: #2a2a2a; + border-radius: 8px; + padding: 2rem; + margin: 1rem 0; + text-align: center; + color: #f5f5f5; + border: 1px solid #444; +} + +.explicit-filter-placeholder h2 { + color: #ff5555; + margin-bottom: 1rem; +} + +.track-filtered { + opacity: 0.7; +} + +.track-name.explicit-filtered { + color: #ff5555; + font-style: italic; +} + +/* Add styles for disabled download buttons */ +.download-btn--disabled { + background-color: #666; + cursor: not-allowed; + opacity: 0.7; +} + +.download-btn--disabled:hover { + background-color: #666; + transform: none; +} + +/* Add styles for download note in artist view */ +.download-note { + color: var(--color-text-secondary); + font-style: italic; + font-size: 0.9rem; + margin-top: 0.5rem; } \ No newline at end of file diff --git a/static/js/album.js b/static/js/album.js index b923132..615ddd3 100644 --- a/static/js/album.js +++ b/static/js/album.js @@ -34,6 +34,27 @@ function renderAlbum(album) { document.getElementById('loading').classList.add('hidden'); document.getElementById('error').classList.add('hidden'); + // Check if album itself is marked explicit and filter is enabled + const isExplicitFilterEnabled = downloadQueue.isExplicitFilterEnabled(); + if (isExplicitFilterEnabled && album.explicit) { + // Show placeholder for explicit album + const placeholderContent = ` +
+

Explicit Content Filtered

+

This album contains explicit content and has been filtered based on your settings.

+

The explicit content filter is controlled by environment variables.

+
+ `; + + const contentContainer = document.getElementById('album-header'); + if (contentContainer) { + contentContainer.innerHTML = placeholderContent; + contentContainer.classList.remove('hidden'); + } + + return; // Stop rendering the actual album content + } + const baseUrl = window.location.origin; // Set album header info. @@ -75,6 +96,12 @@ function renderAlbum(album) { window.location.href = window.location.origin; }); + // Check if any track in the album is explicit when filter is enabled + let hasExplicitTrack = false; + if (isExplicitFilterEnabled && album.tracks?.items) { + hasExplicitTrack = album.tracks.items.some(track => track && track.explicit); + } + // Create (if needed) the Download Album Button. let downloadAlbumBtn = document.getElementById('downloadAlbumBtn'); if (!downloadAlbumBtn) { @@ -85,24 +112,32 @@ function renderAlbum(album) { document.getElementById('album-header').appendChild(downloadAlbumBtn); } - downloadAlbumBtn.addEventListener('click', () => { - // Remove any other download buttons (keeping the full-album button in place). - document.querySelectorAll('.download-btn').forEach(btn => { - if (btn.id !== 'downloadAlbumBtn') btn.remove(); - }); - + if (isExplicitFilterEnabled && hasExplicitTrack) { + // Disable the album download button and display a message explaining why downloadAlbumBtn.disabled = true; - downloadAlbumBtn.textContent = 'Queueing...'; - - downloadWholeAlbum(album) - .then(() => { - downloadAlbumBtn.textContent = 'Queued!'; - }) - .catch(err => { - showError('Failed to queue album download: ' + (err?.message || 'Unknown error')); - downloadAlbumBtn.disabled = false; + downloadAlbumBtn.classList.add('download-btn--disabled'); + downloadAlbumBtn.innerHTML = `Album Contains Explicit Tracks`; + } else { + // Normal behavior when no explicit tracks are present + downloadAlbumBtn.addEventListener('click', () => { + // Remove any other download buttons (keeping the full-album button in place). + document.querySelectorAll('.download-btn').forEach(btn => { + if (btn.id !== 'downloadAlbumBtn') btn.remove(); }); - }); + + downloadAlbumBtn.disabled = true; + downloadAlbumBtn.textContent = 'Queueing...'; + + downloadWholeAlbum(album) + .then(() => { + downloadAlbumBtn.textContent = 'Queued!'; + }) + .catch(err => { + showError('Failed to queue album download: ' + (err?.message || 'Unknown error')); + downloadAlbumBtn.disabled = false; + }); + }); + } // Render each track. const tracksList = document.getElementById('tracks-list'); @@ -112,6 +147,23 @@ function renderAlbum(album) { album.tracks.items.forEach((track, index) => { if (!track) return; // Skip null or undefined tracks + // Skip explicit tracks if filter is enabled + if (isExplicitFilterEnabled && track.explicit) { + // Add a placeholder for filtered explicit tracks + const trackElement = document.createElement('div'); + trackElement.className = 'track track-filtered'; + trackElement.innerHTML = ` +
${index + 1}
+
+
Explicit Content Filtered
+
This track is not shown due to explicit content filter settings
+
+
--:--
+ `; + tracksList.appendChild(trackElement); + return; + } + const trackElement = document.createElement('div'); trackElement.className = 'track'; trackElement.innerHTML = ` diff --git a/static/js/artist.js b/static/js/artist.js index 627473f..34c2d4e 100644 --- a/static/js/artist.js +++ b/static/js/artist.js @@ -32,6 +32,9 @@ function renderArtist(artistData, artistId) { document.getElementById('loading').classList.add('hidden'); document.getElementById('error').classList.add('hidden'); + // Check if explicit filter is enabled + const isExplicitFilterEnabled = downloadQueue.isExplicitFilterEnabled(); + const firstAlbum = artistData.items?.[0] || {}; const artistName = firstAlbum?.artists?.[0]?.name || 'Unknown Artist'; const artistImage = firstAlbum?.images?.[0]?.url || '/static/images/placeholder.jpg'; @@ -65,35 +68,50 @@ function renderArtist(artistData, artistId) { document.getElementById('artist-header').appendChild(downloadArtistBtn); } - downloadArtistBtn.addEventListener('click', () => { - // Optionally remove other download buttons from individual albums. - document.querySelectorAll('.download-btn:not(#downloadArtistBtn)').forEach(btn => btn.remove()); + // When explicit filter is enabled, disable all download buttons + if (isExplicitFilterEnabled) { + // Disable the artist download button and display a message explaining why downloadArtistBtn.disabled = true; - downloadArtistBtn.textContent = 'Queueing...'; + downloadArtistBtn.classList.add('download-btn--disabled'); + downloadArtistBtn.innerHTML = `Downloads Restricted`; + } else { + // Normal behavior when explicit filter is not enabled + downloadArtistBtn.addEventListener('click', () => { + // Optionally remove other download buttons from individual albums. + document.querySelectorAll('.download-btn:not(#downloadArtistBtn)').forEach(btn => btn.remove()); + downloadArtistBtn.disabled = true; + downloadArtistBtn.textContent = 'Queueing...'; - // Queue the entire discography (albums, singles, compilations, and appears_on) - // Use our local startDownload function instead of downloadQueue.startArtistDownload - startDownload( - artistUrl, - 'artist', - { name: artistName, artist: artistName }, - 'album,single,compilation' - ) - .then(() => { - downloadArtistBtn.textContent = 'Artist queued'; - // Make the queue visible after queueing - downloadQueue.toggleVisibility(true); - }) - .catch(err => { - downloadArtistBtn.textContent = 'Download All Discography'; - downloadArtistBtn.disabled = false; - showError('Failed to queue artist download: ' + (err?.message || 'Unknown error')); - }); - }); + // Queue the entire discography (albums, singles, compilations, and appears_on) + // Use our local startDownload function instead of downloadQueue.startArtistDownload + startDownload( + artistUrl, + 'artist', + { name: artistName, artist: artistName }, + 'album,single,compilation' + ) + .then(() => { + downloadArtistBtn.textContent = 'Artist queued'; + // Make the queue visible after queueing + downloadQueue.toggleVisibility(true); + }) + .catch(err => { + downloadArtistBtn.textContent = 'Download All Discography'; + downloadArtistBtn.disabled = false; + showError('Failed to queue artist download: ' + (err?.message || 'Unknown error')); + }); + }); + } // Group albums by type (album, single, compilation, etc.) const albumGroups = (artistData.items || []).reduce((groups, album) => { if (!album) return groups; + + // Skip explicit albums if filter is enabled + if (isExplicitFilterEnabled && album.explicit) { + return groups; + } + const type = (album.album_type || 'unknown').toLowerCase(); if (!groups[type]) groups[type] = []; groups[type].push(album); @@ -108,14 +126,22 @@ function renderArtist(artistData, artistId) { const groupSection = document.createElement('section'); groupSection.className = 'album-group'; - groupSection.innerHTML = ` -
+ // If explicit filter is enabled, don't show the group download button + const groupHeaderHTML = isExplicitFilterEnabled ? + `
+

${capitalize(groupType)}s

+
Visit album pages to download content
+
` : + `

${capitalize(groupType)}s

-
+
`; + + groupSection.innerHTML = ` + ${groupHeaderHTML}
`; @@ -125,24 +151,41 @@ function renderArtist(artistData, artistId) { const albumElement = document.createElement('div'); albumElement.className = 'album-card'; - albumElement.innerHTML = ` - - Album cover - -
-
${album.name || 'Unknown Album'}
-
${album.artists?.map(a => a?.name || 'Unknown Artist').join(', ') || 'Unknown Artist'}
-
- - `; + + // Create album card with or without download button based on explicit filter setting + if (isExplicitFilterEnabled) { + albumElement.innerHTML = ` + + Album cover + +
+
${album.name || 'Unknown Album'}
+
${album.artists?.map(a => a?.name || 'Unknown Artist').join(', ') || 'Unknown Artist'}
+
+ `; + } else { + albumElement.innerHTML = ` + + Album cover + +
+
${album.name || 'Unknown Album'}
+
${album.artists?.map(a => a?.name || 'Unknown Artist').join(', ') || 'Unknown Artist'}
+
+ + `; + } + albumsContainer.appendChild(albumElement); }); @@ -152,9 +195,12 @@ function renderArtist(artistData, artistId) { document.getElementById('artist-header').classList.remove('hidden'); document.getElementById('albums-container').classList.remove('hidden'); - attachDownloadListeners(); - // Pass the artist URL and name so the group buttons can use the artist download function - attachGroupDownloadListeners(artistUrl, artistName); + // Only attach download listeners if explicit filter is not enabled + if (!isExplicitFilterEnabled) { + attachDownloadListeners(); + // Pass the artist URL and name so the group buttons can use the artist download function + attachGroupDownloadListeners(artistUrl, artistName); + } } // Event listeners for group downloads using the artist download function diff --git a/static/js/config.js b/static/js/config.js index 4cc0c51..706b9b6 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -663,11 +663,31 @@ async function loadConfig() { document.getElementById('retryDelaySeconds').value = savedConfig.retryDelaySeconds || '5'; document.getElementById('retryDelayIncrease').value = savedConfig.retry_delay_increase || '5'; document.getElementById('tracknumPaddingToggle').checked = savedConfig.tracknum_padding === undefined ? true : !!savedConfig.tracknum_padding; + + // Update explicit filter status + updateExplicitFilterStatus(savedConfig.explicitFilter); } catch (error) { showConfigError('Error loading config: ' + error.message); } } +function updateExplicitFilterStatus(isEnabled) { + const statusElement = document.getElementById('explicitFilterStatus'); + if (statusElement) { + // Remove existing classes + statusElement.classList.remove('enabled', 'disabled'); + + // Add appropriate class and text based on whether filter is enabled + if (isEnabled) { + statusElement.textContent = 'Enabled'; + statusElement.classList.add('enabled'); + } else { + statusElement.textContent = 'Disabled'; + statusElement.classList.add('disabled'); + } + } +} + function showConfigError(message) { const errorDiv = document.getElementById('configError'); errorDiv.textContent = message; diff --git a/static/js/main.js b/static/js/main.js index f5e2705..6b5af92 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -101,8 +101,18 @@ document.addEventListener('DOMContentLoaded', function() { if (data && data.items && data.items.length > 0) { resultsContainer.innerHTML = ''; - // Filter out null/undefined items first - const validItems = data.items.filter(item => item); + // Filter out items with null/undefined essential display parameters + const validItems = filterValidItems(data.items, searchType.value); + + if (validItems.length === 0) { + // No valid items found after filtering + resultsContainer.innerHTML = ` +
+

No valid results found for "${query}"

+
+ `; + return; + } validItems.forEach((item, index) => { const cardElement = createResultCard(item, searchType.value, index); @@ -137,6 +147,75 @@ document.addEventListener('DOMContentLoaded', function() { } } + /** + * Filters out items with null/undefined essential display parameters based on search type + */ + function filterValidItems(items, type) { + if (!items) return []; + + return items.filter(item => { + // Skip null/undefined items + if (!item) return false; + + // Skip explicit content if filter is enabled + if (downloadQueue.isExplicitFilterEnabled() && item.explicit === true) { + return false; + } + + // Check essential parameters based on search type + switch (type) { + case 'track': + // For tracks, we need name, artists, and album + return ( + item.name && + item.artists && + item.artists.length > 0 && + item.artists[0] && + item.artists[0].name && + item.album && + item.album.name && + item.external_urls && + item.external_urls.spotify + ); + + case 'album': + // For albums, we need name, artists, and cover image + return ( + item.name && + item.artists && + item.artists.length > 0 && + item.artists[0] && + item.artists[0].name && + item.external_urls && + item.external_urls.spotify + ); + + case 'playlist': + // For playlists, we need name, owner, and tracks + return ( + item.name && + item.owner && + item.owner.display_name && + item.tracks && + item.external_urls && + item.external_urls.spotify + ); + + case 'artist': + // For artists, we need name + return ( + item.name && + item.external_urls && + item.external_urls.spotify + ); + + default: + // Default case - just check if the item exists + return true; + } + }); + } + /** * Attaches download handlers to result cards */ diff --git a/static/js/playlist.js b/static/js/playlist.js index 010c860..bf57266 100644 --- a/static/js/playlist.js +++ b/static/js/playlist.js @@ -39,6 +39,9 @@ function renderPlaylist(playlist) { document.getElementById('loading').classList.add('hidden'); document.getElementById('error').classList.add('hidden'); + // Check if explicit filter is enabled + const isExplicitFilterEnabled = downloadQueue.isExplicitFilterEnabled(); + // Update header info document.getElementById('playlist-name').textContent = playlist.name || 'Unknown Playlist'; document.getElementById('playlist-owner').textContent = `By ${playlist.owner?.display_name || 'Unknown User'}`; @@ -67,6 +70,12 @@ function renderPlaylist(playlist) { window.location.href = window.location.origin; }); + // Check if any track in the playlist is explicit when filter is enabled + let hasExplicitTrack = false; + if (isExplicitFilterEnabled && playlist.tracks?.items) { + hasExplicitTrack = playlist.tracks.items.some(item => item?.track && item.track.explicit); + } + // --- Add "Download Whole Playlist" Button --- let downloadPlaylistBtn = document.getElementById('downloadPlaylistBtn'); if (!downloadPlaylistBtn) { @@ -80,26 +89,6 @@ function renderPlaylist(playlist) { headerContainer.appendChild(downloadPlaylistBtn); } } - downloadPlaylistBtn.addEventListener('click', () => { - // Remove individual track download buttons (but leave the whole playlist button). - document.querySelectorAll('.download-btn').forEach(btn => { - if (btn.id !== 'downloadPlaylistBtn') { - btn.remove(); - } - }); - - // Disable the whole playlist button to prevent repeated clicks. - downloadPlaylistBtn.disabled = true; - downloadPlaylistBtn.textContent = 'Queueing...'; - - // Initiate the playlist download. - downloadWholePlaylist(playlist).then(() => { - downloadPlaylistBtn.textContent = 'Queued!'; - }).catch(err => { - showError('Failed to queue playlist download: ' + (err?.message || 'Unknown error')); - downloadPlaylistBtn.disabled = false; - }); - }); // --- Add "Download Playlist's Albums" Button --- let downloadAlbumsBtn = document.getElementById('downloadAlbumsBtn'); @@ -114,24 +103,58 @@ function renderPlaylist(playlist) { headerContainer.appendChild(downloadAlbumsBtn); } } - downloadAlbumsBtn.addEventListener('click', () => { - // Remove individual track download buttons (but leave this album button). - document.querySelectorAll('.download-btn').forEach(btn => { - if (btn.id !== 'downloadAlbumsBtn') btn.remove(); + + if (isExplicitFilterEnabled && hasExplicitTrack) { + // Disable both playlist buttons and display messages explaining why + downloadPlaylistBtn.disabled = true; + downloadPlaylistBtn.classList.add('download-btn--disabled'); + downloadPlaylistBtn.innerHTML = `Playlist Contains Explicit Tracks`; + + downloadAlbumsBtn.disabled = true; + downloadAlbumsBtn.classList.add('download-btn--disabled'); + downloadAlbumsBtn.innerHTML = `Albums Access Restricted`; + } else { + // Normal behavior when no explicit tracks are present + downloadPlaylistBtn.addEventListener('click', () => { + // Remove individual track download buttons (but leave the whole playlist button). + document.querySelectorAll('.download-btn').forEach(btn => { + if (btn.id !== 'downloadPlaylistBtn') { + btn.remove(); + } + }); + + // Disable the whole playlist button to prevent repeated clicks. + downloadPlaylistBtn.disabled = true; + downloadPlaylistBtn.textContent = 'Queueing...'; + + // Initiate the playlist download. + downloadWholePlaylist(playlist).then(() => { + downloadPlaylistBtn.textContent = 'Queued!'; + }).catch(err => { + showError('Failed to queue playlist download: ' + (err?.message || 'Unknown error')); + downloadPlaylistBtn.disabled = false; + }); }); - downloadAlbumsBtn.disabled = true; - downloadAlbumsBtn.textContent = 'Queueing...'; - - downloadPlaylistAlbums(playlist) - .then(() => { - downloadAlbumsBtn.textContent = 'Queued!'; - }) - .catch(err => { - showError('Failed to queue album downloads: ' + (err?.message || 'Unknown error')); - downloadAlbumsBtn.disabled = false; + downloadAlbumsBtn.addEventListener('click', () => { + // Remove individual track download buttons (but leave this album button). + document.querySelectorAll('.download-btn').forEach(btn => { + if (btn.id !== 'downloadAlbumsBtn') btn.remove(); }); - }); + + downloadAlbumsBtn.disabled = true; + downloadAlbumsBtn.textContent = 'Queueing...'; + + downloadPlaylistAlbums(playlist) + .then(() => { + downloadAlbumsBtn.textContent = 'Queued!'; + }) + .catch(err => { + showError('Failed to queue album downloads: ' + (err?.message || 'Unknown error')); + downloadAlbumsBtn.disabled = false; + }); + }); + } // Render tracks list const tracksList = document.getElementById('tracks-list'); @@ -144,6 +167,25 @@ function renderPlaylist(playlist) { if (!item || !item.track) return; // Skip null/undefined tracks const track = item.track; + + // Skip explicit tracks if filter is enabled + if (isExplicitFilterEnabled && track.explicit) { + // Add a placeholder for filtered explicit tracks + const trackElement = document.createElement('div'); + trackElement.className = 'track track-filtered'; + trackElement.innerHTML = ` +
${index + 1}
+
+
Explicit Content Filtered
+
This track is not shown due to explicit content filter settings
+
+
Not available
+
--:--
+ `; + tracksList.appendChild(trackElement); + return; + } + // Create links for track, artist, and album using their IDs. const trackLink = `/track/${track.id || ''}`; const artistLink = `/artist/${track.artists?.[0]?.id || ''}`; diff --git a/static/js/queue.js b/static/js/queue.js index b2e61c6..8a58441 100644 --- a/static/js/queue.js +++ b/static/js/queue.js @@ -1244,6 +1244,11 @@ class DownloadQueue { throw error; } } + + // Add a method to check if explicit filter is enabled + isExplicitFilterEnabled() { + return !!this.currentConfig.explicitFilter; + } } // Singleton instance diff --git a/static/js/track.js b/static/js/track.js index 3500d55..f62696e 100644 --- a/static/js/track.js +++ b/static/js/track.js @@ -40,6 +40,28 @@ function renderTrack(track) { document.getElementById('loading').classList.add('hidden'); document.getElementById('error').classList.add('hidden'); + // Check if track is explicit and if explicit filter is enabled + if (track.explicit && downloadQueue.isExplicitFilterEnabled()) { + // Show placeholder for explicit content + document.getElementById('loading').classList.add('hidden'); + + const placeholderContent = ` +
+

Explicit Content Filtered

+

This track contains explicit content and has been filtered based on your settings.

+

The explicit content filter is controlled by environment variables.

+
+ `; + + const contentContainer = document.getElementById('track-header'); + if (contentContainer) { + contentContainer.innerHTML = placeholderContent; + contentContainer.classList.remove('hidden'); + } + + return; // Stop rendering the actual track content + } + // Update track information fields. document.getElementById('track-name').innerHTML = `${track.name || 'Unknown Track'}`; diff --git a/templates/config.html b/templates/config.html index 23e93bd..4a0cf7c 100644 --- a/templates/config.html +++ b/templates/config.html @@ -53,6 +53,17 @@ + +
+ +
+ Loading... +
ENV
+
+
+ Filter explicit content. Controlled by environment variable EXPLICIT_FILTER. +
+