implemented explicit filter & other fixes
This commit is contained in:
@@ -21,6 +21,7 @@ services:
|
|||||||
- REDIS_DB=0
|
- REDIS_DB=0
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
- REDIS_BACKEND=redis://redis:6379/0
|
- REDIS_BACKEND=redis://redis:6379/0
|
||||||
|
- EXPLICIT_FILTER=false # Set to true to filter out explicit content
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
config_bp = Blueprint('config_bp', __name__)
|
config_bp = Blueprint('config_bp', __name__)
|
||||||
CONFIG_PATH = Path('./config/main.json')
|
CONFIG_PATH = Path('./config/main.json')
|
||||||
@@ -91,6 +92,10 @@ def handle_config():
|
|||||||
for key, default_value in defaults.items():
|
for key, default_value in defaults.items():
|
||||||
if key not in config:
|
if key not in config:
|
||||||
config[key] = default_value
|
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)
|
return jsonify(config)
|
||||||
|
|
||||||
@@ -101,6 +106,13 @@ def update_config():
|
|||||||
if not isinstance(new_config, dict):
|
if not isinstance(new_config, dict):
|
||||||
return jsonify({"error": "Invalid config format"}), 400
|
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):
|
if not save_config(new_config):
|
||||||
return jsonify({"error": "Failed to save config"}), 500
|
return jsonify({"error": "Failed to save config"}), 500
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,39 @@ body {
|
|||||||
transition: all 0.3s ease;
|
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 as floating icon - keep this for our floating button */
|
||||||
.back-button.floating-icon {
|
.back-button.floating-icon {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -454,4 +454,49 @@ a:hover, a:focus {
|
|||||||
right: 16px;
|
right: 16px;
|
||||||
bottom: 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;
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,27 @@ function renderAlbum(album) {
|
|||||||
document.getElementById('loading').classList.add('hidden');
|
document.getElementById('loading').classList.add('hidden');
|
||||||
document.getElementById('error').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 = `
|
||||||
|
<div class="explicit-filter-placeholder">
|
||||||
|
<h2>Explicit Content Filtered</h2>
|
||||||
|
<p>This album contains explicit content and has been filtered based on your settings.</p>
|
||||||
|
<p>The explicit content filter is controlled by environment variables.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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;
|
const baseUrl = window.location.origin;
|
||||||
|
|
||||||
// Set album header info.
|
// Set album header info.
|
||||||
@@ -75,6 +96,12 @@ function renderAlbum(album) {
|
|||||||
window.location.href = window.location.origin;
|
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.
|
// Create (if needed) the Download Album Button.
|
||||||
let downloadAlbumBtn = document.getElementById('downloadAlbumBtn');
|
let downloadAlbumBtn = document.getElementById('downloadAlbumBtn');
|
||||||
if (!downloadAlbumBtn) {
|
if (!downloadAlbumBtn) {
|
||||||
@@ -85,24 +112,32 @@ function renderAlbum(album) {
|
|||||||
document.getElementById('album-header').appendChild(downloadAlbumBtn);
|
document.getElementById('album-header').appendChild(downloadAlbumBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadAlbumBtn.addEventListener('click', () => {
|
if (isExplicitFilterEnabled && hasExplicitTrack) {
|
||||||
// Remove any other download buttons (keeping the full-album button in place).
|
// Disable the album download button and display a message explaining why
|
||||||
document.querySelectorAll('.download-btn').forEach(btn => {
|
|
||||||
if (btn.id !== 'downloadAlbumBtn') btn.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
downloadAlbumBtn.disabled = true;
|
downloadAlbumBtn.disabled = true;
|
||||||
downloadAlbumBtn.textContent = 'Queueing...';
|
downloadAlbumBtn.classList.add('download-btn--disabled');
|
||||||
|
downloadAlbumBtn.innerHTML = `<span title="Cannot download entire album because it contains explicit tracks">Album Contains Explicit Tracks</span>`;
|
||||||
downloadWholeAlbum(album)
|
} else {
|
||||||
.then(() => {
|
// Normal behavior when no explicit tracks are present
|
||||||
downloadAlbumBtn.textContent = 'Queued!';
|
downloadAlbumBtn.addEventListener('click', () => {
|
||||||
})
|
// Remove any other download buttons (keeping the full-album button in place).
|
||||||
.catch(err => {
|
document.querySelectorAll('.download-btn').forEach(btn => {
|
||||||
showError('Failed to queue album download: ' + (err?.message || 'Unknown error'));
|
if (btn.id !== 'downloadAlbumBtn') btn.remove();
|
||||||
downloadAlbumBtn.disabled = false;
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
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.
|
// Render each track.
|
||||||
const tracksList = document.getElementById('tracks-list');
|
const tracksList = document.getElementById('tracks-list');
|
||||||
@@ -112,6 +147,23 @@ function renderAlbum(album) {
|
|||||||
album.tracks.items.forEach((track, index) => {
|
album.tracks.items.forEach((track, index) => {
|
||||||
if (!track) return; // Skip null or undefined tracks
|
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 = `
|
||||||
|
<div class="track-number">${index + 1}</div>
|
||||||
|
<div class="track-info">
|
||||||
|
<div class="track-name explicit-filtered">Explicit Content Filtered</div>
|
||||||
|
<div class="track-artist">This track is not shown due to explicit content filter settings</div>
|
||||||
|
</div>
|
||||||
|
<div class="track-duration">--:--</div>
|
||||||
|
`;
|
||||||
|
tracksList.appendChild(trackElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const trackElement = document.createElement('div');
|
const trackElement = document.createElement('div');
|
||||||
trackElement.className = 'track';
|
trackElement.className = 'track';
|
||||||
trackElement.innerHTML = `
|
trackElement.innerHTML = `
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ function renderArtist(artistData, artistId) {
|
|||||||
document.getElementById('loading').classList.add('hidden');
|
document.getElementById('loading').classList.add('hidden');
|
||||||
document.getElementById('error').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 firstAlbum = artistData.items?.[0] || {};
|
||||||
const artistName = firstAlbum?.artists?.[0]?.name || 'Unknown Artist';
|
const artistName = firstAlbum?.artists?.[0]?.name || 'Unknown Artist';
|
||||||
const artistImage = firstAlbum?.images?.[0]?.url || '/static/images/placeholder.jpg';
|
const artistImage = firstAlbum?.images?.[0]?.url || '/static/images/placeholder.jpg';
|
||||||
@@ -65,35 +68,50 @@ function renderArtist(artistData, artistId) {
|
|||||||
document.getElementById('artist-header').appendChild(downloadArtistBtn);
|
document.getElementById('artist-header').appendChild(downloadArtistBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadArtistBtn.addEventListener('click', () => {
|
// When explicit filter is enabled, disable all download buttons
|
||||||
// Optionally remove other download buttons from individual albums.
|
if (isExplicitFilterEnabled) {
|
||||||
document.querySelectorAll('.download-btn:not(#downloadArtistBtn)').forEach(btn => btn.remove());
|
// Disable the artist download button and display a message explaining why
|
||||||
downloadArtistBtn.disabled = true;
|
downloadArtistBtn.disabled = true;
|
||||||
downloadArtistBtn.textContent = 'Queueing...';
|
downloadArtistBtn.classList.add('download-btn--disabled');
|
||||||
|
downloadArtistBtn.innerHTML = `<span title="Direct artist downloads are restricted when explicit filter is enabled. Please visit individual album pages.">Downloads Restricted</span>`;
|
||||||
|
} 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)
|
// Queue the entire discography (albums, singles, compilations, and appears_on)
|
||||||
// Use our local startDownload function instead of downloadQueue.startArtistDownload
|
// Use our local startDownload function instead of downloadQueue.startArtistDownload
|
||||||
startDownload(
|
startDownload(
|
||||||
artistUrl,
|
artistUrl,
|
||||||
'artist',
|
'artist',
|
||||||
{ name: artistName, artist: artistName },
|
{ name: artistName, artist: artistName },
|
||||||
'album,single,compilation'
|
'album,single,compilation'
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
downloadArtistBtn.textContent = 'Artist queued';
|
downloadArtistBtn.textContent = 'Artist queued';
|
||||||
// Make the queue visible after queueing
|
// Make the queue visible after queueing
|
||||||
downloadQueue.toggleVisibility(true);
|
downloadQueue.toggleVisibility(true);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
downloadArtistBtn.textContent = 'Download All Discography';
|
downloadArtistBtn.textContent = 'Download All Discography';
|
||||||
downloadArtistBtn.disabled = false;
|
downloadArtistBtn.disabled = false;
|
||||||
showError('Failed to queue artist download: ' + (err?.message || 'Unknown error'));
|
showError('Failed to queue artist download: ' + (err?.message || 'Unknown error'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Group albums by type (album, single, compilation, etc.)
|
// Group albums by type (album, single, compilation, etc.)
|
||||||
const albumGroups = (artistData.items || []).reduce((groups, album) => {
|
const albumGroups = (artistData.items || []).reduce((groups, album) => {
|
||||||
if (!album) return groups;
|
if (!album) return groups;
|
||||||
|
|
||||||
|
// Skip explicit albums if filter is enabled
|
||||||
|
if (isExplicitFilterEnabled && album.explicit) {
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
const type = (album.album_type || 'unknown').toLowerCase();
|
const type = (album.album_type || 'unknown').toLowerCase();
|
||||||
if (!groups[type]) groups[type] = [];
|
if (!groups[type]) groups[type] = [];
|
||||||
groups[type].push(album);
|
groups[type].push(album);
|
||||||
@@ -108,14 +126,22 @@ function renderArtist(artistData, artistId) {
|
|||||||
const groupSection = document.createElement('section');
|
const groupSection = document.createElement('section');
|
||||||
groupSection.className = 'album-group';
|
groupSection.className = 'album-group';
|
||||||
|
|
||||||
groupSection.innerHTML = `
|
// If explicit filter is enabled, don't show the group download button
|
||||||
<div class="album-group-header">
|
const groupHeaderHTML = isExplicitFilterEnabled ?
|
||||||
|
`<div class="album-group-header">
|
||||||
|
<h3>${capitalize(groupType)}s</h3>
|
||||||
|
<div class="download-note">Visit album pages to download content</div>
|
||||||
|
</div>` :
|
||||||
|
`<div class="album-group-header">
|
||||||
<h3>${capitalize(groupType)}s</h3>
|
<h3>${capitalize(groupType)}s</h3>
|
||||||
<button class="download-btn download-btn--main group-download-btn"
|
<button class="download-btn download-btn--main group-download-btn"
|
||||||
data-group-type="${groupType}">
|
data-group-type="${groupType}">
|
||||||
Download All ${capitalize(groupType)}s
|
Download All ${capitalize(groupType)}s
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>`;
|
||||||
|
|
||||||
|
groupSection.innerHTML = `
|
||||||
|
${groupHeaderHTML}
|
||||||
<div class="albums-list"></div>
|
<div class="albums-list"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -125,24 +151,41 @@ function renderArtist(artistData, artistId) {
|
|||||||
|
|
||||||
const albumElement = document.createElement('div');
|
const albumElement = document.createElement('div');
|
||||||
albumElement.className = 'album-card';
|
albumElement.className = 'album-card';
|
||||||
albumElement.innerHTML = `
|
|
||||||
<a href="/album/${album.id || ''}" class="album-link">
|
// Create album card with or without download button based on explicit filter setting
|
||||||
<img src="${album.images?.[1]?.url || album.images?.[0]?.url || '/static/images/placeholder.jpg'}"
|
if (isExplicitFilterEnabled) {
|
||||||
alt="Album cover"
|
albumElement.innerHTML = `
|
||||||
class="album-cover">
|
<a href="/album/${album.id || ''}" class="album-link">
|
||||||
</a>
|
<img src="${album.images?.[1]?.url || album.images?.[0]?.url || '/static/images/placeholder.jpg'}"
|
||||||
<div class="album-info">
|
alt="Album cover"
|
||||||
<div class="album-title">${album.name || 'Unknown Album'}</div>
|
class="album-cover">
|
||||||
<div class="album-artist">${album.artists?.map(a => a?.name || 'Unknown Artist').join(', ') || 'Unknown Artist'}</div>
|
</a>
|
||||||
</div>
|
<div class="album-info">
|
||||||
<button class="download-btn download-btn--circle"
|
<div class="album-title">${album.name || 'Unknown Album'}</div>
|
||||||
data-url="${album.external_urls?.spotify || ''}"
|
<div class="album-artist">${album.artists?.map(a => a?.name || 'Unknown Artist').join(', ') || 'Unknown Artist'}</div>
|
||||||
data-type="${album.album_type || 'album'}"
|
</div>
|
||||||
data-name="${album.name || 'Unknown Album'}"
|
`;
|
||||||
title="Download">
|
} else {
|
||||||
<img src="/static/images/download.svg" alt="Download">
|
albumElement.innerHTML = `
|
||||||
</button>
|
<a href="/album/${album.id || ''}" class="album-link">
|
||||||
`;
|
<img src="${album.images?.[1]?.url || album.images?.[0]?.url || '/static/images/placeholder.jpg'}"
|
||||||
|
alt="Album cover"
|
||||||
|
class="album-cover">
|
||||||
|
</a>
|
||||||
|
<div class="album-info">
|
||||||
|
<div class="album-title">${album.name || 'Unknown Album'}</div>
|
||||||
|
<div class="album-artist">${album.artists?.map(a => a?.name || 'Unknown Artist').join(', ') || 'Unknown Artist'}</div>
|
||||||
|
</div>
|
||||||
|
<button class="download-btn download-btn--circle"
|
||||||
|
data-url="${album.external_urls?.spotify || ''}"
|
||||||
|
data-type="${album.album_type || 'album'}"
|
||||||
|
data-name="${album.name || 'Unknown Album'}"
|
||||||
|
title="Download">
|
||||||
|
<img src="/static/images/download.svg" alt="Download">
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
albumsContainer.appendChild(albumElement);
|
albumsContainer.appendChild(albumElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,9 +195,12 @@ function renderArtist(artistData, artistId) {
|
|||||||
document.getElementById('artist-header').classList.remove('hidden');
|
document.getElementById('artist-header').classList.remove('hidden');
|
||||||
document.getElementById('albums-container').classList.remove('hidden');
|
document.getElementById('albums-container').classList.remove('hidden');
|
||||||
|
|
||||||
attachDownloadListeners();
|
// Only attach download listeners if explicit filter is not enabled
|
||||||
// Pass the artist URL and name so the group buttons can use the artist download function
|
if (!isExplicitFilterEnabled) {
|
||||||
attachGroupDownloadListeners(artistUrl, artistName);
|
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
|
// Event listeners for group downloads using the artist download function
|
||||||
|
|||||||
@@ -663,11 +663,31 @@ async function loadConfig() {
|
|||||||
document.getElementById('retryDelaySeconds').value = savedConfig.retryDelaySeconds || '5';
|
document.getElementById('retryDelaySeconds').value = savedConfig.retryDelaySeconds || '5';
|
||||||
document.getElementById('retryDelayIncrease').value = savedConfig.retry_delay_increase || '5';
|
document.getElementById('retryDelayIncrease').value = savedConfig.retry_delay_increase || '5';
|
||||||
document.getElementById('tracknumPaddingToggle').checked = savedConfig.tracknum_padding === undefined ? true : !!savedConfig.tracknum_padding;
|
document.getElementById('tracknumPaddingToggle').checked = savedConfig.tracknum_padding === undefined ? true : !!savedConfig.tracknum_padding;
|
||||||
|
|
||||||
|
// Update explicit filter status
|
||||||
|
updateExplicitFilterStatus(savedConfig.explicitFilter);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showConfigError('Error loading config: ' + error.message);
|
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) {
|
function showConfigError(message) {
|
||||||
const errorDiv = document.getElementById('configError');
|
const errorDiv = document.getElementById('configError');
|
||||||
errorDiv.textContent = message;
|
errorDiv.textContent = message;
|
||||||
|
|||||||
@@ -101,8 +101,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (data && data.items && data.items.length > 0) {
|
if (data && data.items && data.items.length > 0) {
|
||||||
resultsContainer.innerHTML = '';
|
resultsContainer.innerHTML = '';
|
||||||
|
|
||||||
// Filter out null/undefined items first
|
// Filter out items with null/undefined essential display parameters
|
||||||
const validItems = data.items.filter(item => item);
|
const validItems = filterValidItems(data.items, searchType.value);
|
||||||
|
|
||||||
|
if (validItems.length === 0) {
|
||||||
|
// No valid items found after filtering
|
||||||
|
resultsContainer.innerHTML = `
|
||||||
|
<div class="empty-search-results">
|
||||||
|
<p>No valid results found for "${query}"</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
validItems.forEach((item, index) => {
|
validItems.forEach((item, index) => {
|
||||||
const cardElement = createResultCard(item, searchType.value, 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
|
* Attaches download handlers to result cards
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ function renderPlaylist(playlist) {
|
|||||||
document.getElementById('loading').classList.add('hidden');
|
document.getElementById('loading').classList.add('hidden');
|
||||||
document.getElementById('error').classList.add('hidden');
|
document.getElementById('error').classList.add('hidden');
|
||||||
|
|
||||||
|
// Check if explicit filter is enabled
|
||||||
|
const isExplicitFilterEnabled = downloadQueue.isExplicitFilterEnabled();
|
||||||
|
|
||||||
// Update header info
|
// Update header info
|
||||||
document.getElementById('playlist-name').textContent = playlist.name || 'Unknown Playlist';
|
document.getElementById('playlist-name').textContent = playlist.name || 'Unknown Playlist';
|
||||||
document.getElementById('playlist-owner').textContent = `By ${playlist.owner?.display_name || 'Unknown User'}`;
|
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;
|
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 ---
|
// --- Add "Download Whole Playlist" Button ---
|
||||||
let downloadPlaylistBtn = document.getElementById('downloadPlaylistBtn');
|
let downloadPlaylistBtn = document.getElementById('downloadPlaylistBtn');
|
||||||
if (!downloadPlaylistBtn) {
|
if (!downloadPlaylistBtn) {
|
||||||
@@ -80,26 +89,6 @@ function renderPlaylist(playlist) {
|
|||||||
headerContainer.appendChild(downloadPlaylistBtn);
|
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 ---
|
// --- Add "Download Playlist's Albums" Button ---
|
||||||
let downloadAlbumsBtn = document.getElementById('downloadAlbumsBtn');
|
let downloadAlbumsBtn = document.getElementById('downloadAlbumsBtn');
|
||||||
@@ -114,24 +103,58 @@ function renderPlaylist(playlist) {
|
|||||||
headerContainer.appendChild(downloadAlbumsBtn);
|
headerContainer.appendChild(downloadAlbumsBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
downloadAlbumsBtn.addEventListener('click', () => {
|
|
||||||
// Remove individual track download buttons (but leave this album button).
|
if (isExplicitFilterEnabled && hasExplicitTrack) {
|
||||||
document.querySelectorAll('.download-btn').forEach(btn => {
|
// Disable both playlist buttons and display messages explaining why
|
||||||
if (btn.id !== 'downloadAlbumsBtn') btn.remove();
|
downloadPlaylistBtn.disabled = true;
|
||||||
|
downloadPlaylistBtn.classList.add('download-btn--disabled');
|
||||||
|
downloadPlaylistBtn.innerHTML = `<span title="Cannot download entire playlist because it contains explicit tracks">Playlist Contains Explicit Tracks</span>`;
|
||||||
|
|
||||||
|
downloadAlbumsBtn.disabled = true;
|
||||||
|
downloadAlbumsBtn.classList.add('download-btn--disabled');
|
||||||
|
downloadAlbumsBtn.innerHTML = `<span title="Cannot download albums from this playlist because it contains explicit tracks">Albums Access Restricted</span>`;
|
||||||
|
} 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.addEventListener('click', () => {
|
||||||
downloadAlbumsBtn.textContent = 'Queueing...';
|
// Remove individual track download buttons (but leave this album button).
|
||||||
|
document.querySelectorAll('.download-btn').forEach(btn => {
|
||||||
downloadPlaylistAlbums(playlist)
|
if (btn.id !== 'downloadAlbumsBtn') btn.remove();
|
||||||
.then(() => {
|
|
||||||
downloadAlbumsBtn.textContent = 'Queued!';
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
showError('Failed to queue album downloads: ' + (err?.message || 'Unknown error'));
|
|
||||||
downloadAlbumsBtn.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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Render tracks list
|
// Render tracks list
|
||||||
const tracksList = document.getElementById('tracks-list');
|
const tracksList = document.getElementById('tracks-list');
|
||||||
@@ -144,6 +167,25 @@ function renderPlaylist(playlist) {
|
|||||||
if (!item || !item.track) return; // Skip null/undefined tracks
|
if (!item || !item.track) return; // Skip null/undefined tracks
|
||||||
|
|
||||||
const track = item.track;
|
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 = `
|
||||||
|
<div class="track-number">${index + 1}</div>
|
||||||
|
<div class="track-info">
|
||||||
|
<div class="track-name explicit-filtered">Explicit Content Filtered</div>
|
||||||
|
<div class="track-artist">This track is not shown due to explicit content filter settings</div>
|
||||||
|
</div>
|
||||||
|
<div class="track-album">Not available</div>
|
||||||
|
<div class="track-duration">--:--</div>
|
||||||
|
`;
|
||||||
|
tracksList.appendChild(trackElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create links for track, artist, and album using their IDs.
|
// Create links for track, artist, and album using their IDs.
|
||||||
const trackLink = `/track/${track.id || ''}`;
|
const trackLink = `/track/${track.id || ''}`;
|
||||||
const artistLink = `/artist/${track.artists?.[0]?.id || ''}`;
|
const artistLink = `/artist/${track.artists?.[0]?.id || ''}`;
|
||||||
|
|||||||
@@ -1244,6 +1244,11 @@ class DownloadQueue {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a method to check if explicit filter is enabled
|
||||||
|
isExplicitFilterEnabled() {
|
||||||
|
return !!this.currentConfig.explicitFilter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
|
|||||||
@@ -40,6 +40,28 @@ function renderTrack(track) {
|
|||||||
document.getElementById('loading').classList.add('hidden');
|
document.getElementById('loading').classList.add('hidden');
|
||||||
document.getElementById('error').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 = `
|
||||||
|
<div class="explicit-filter-placeholder">
|
||||||
|
<h2>Explicit Content Filtered</h2>
|
||||||
|
<p>This track contains explicit content and has been filtered based on your settings.</p>
|
||||||
|
<p>The explicit content filter is controlled by environment variables.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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.
|
// Update track information fields.
|
||||||
document.getElementById('track-name').innerHTML =
|
document.getElementById('track-name').innerHTML =
|
||||||
`<a href="/track/${track.id || ''}" title="View track details">${track.name || 'Unknown Track'}</a>`;
|
`<a href="/track/${track.id || ''}" title="View track details">${track.name || 'Unknown Track'}</a>`;
|
||||||
|
|||||||
@@ -53,6 +53,17 @@
|
|||||||
<option value="FLAC">FLAC (premium)</option>
|
<option value="FLAC">FLAC (premium)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Explicit Filter Status -->
|
||||||
|
<div class="config-item">
|
||||||
|
<label>Explicit Content Filter:</label>
|
||||||
|
<div class="env-controlled-setting">
|
||||||
|
<span id="explicitFilterStatus" class="env-controlled-value">Loading...</span>
|
||||||
|
<div class="env-controlled-badge">ENV</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-description">
|
||||||
|
Filter explicit content. Controlled by environment variable EXPLICIT_FILTER.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="config-item">
|
<div class="config-item">
|
||||||
<label>Download Fallback:</label>
|
<label>Download Fallback:</label>
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
|
|||||||
Reference in New Issue
Block a user