Explicit content filter with configurable toggle to skip explicit tracks during downloads
This commit is contained in:
parent
16e01f6039
commit
bbccd3524f
4 changed files with 79 additions and 5 deletions
|
|
@ -248,6 +248,9 @@ class ConfigManager:
|
||||||
"url": "",
|
"url": "",
|
||||||
"api_key": "",
|
"api_key": "",
|
||||||
"auto_connect": False
|
"auto_connect": False
|
||||||
|
},
|
||||||
|
"content_filter": {
|
||||||
|
"allow_explicit": True
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3894,7 +3894,7 @@ def handle_settings():
|
||||||
if 'active_media_server' in new_settings:
|
if 'active_media_server' in new_settings:
|
||||||
config_manager.set_active_media_server(new_settings['active_media_server'])
|
config_manager.set_active_media_server(new_settings['active_media_server'])
|
||||||
|
|
||||||
for service in ['spotify', 'plex', 'jellyfin', 'navidrome', 'soulseek', 'download_source', 'settings', 'database', 'metadata_enhancement', 'file_organization', 'playlist_sync', 'tidal', 'tidal_download', 'listenbrainz', 'acoustid', 'import', 'lossy_copy', 'ui_appearance', 'youtube']:
|
for service in ['spotify', 'plex', 'jellyfin', 'navidrome', 'soulseek', 'download_source', 'settings', 'database', 'metadata_enhancement', 'file_organization', 'playlist_sync', 'tidal', 'tidal_download', 'listenbrainz', 'acoustid', 'import', 'lossy_copy', 'ui_appearance', 'youtube', 'content_filter']:
|
||||||
if service in new_settings:
|
if service in new_settings:
|
||||||
for key, value in new_settings[service].items():
|
for key, value in new_settings[service].items():
|
||||||
config_manager.set(f'{service}.{key}', value)
|
config_manager.set(f'{service}.{key}', value)
|
||||||
|
|
@ -10477,6 +10477,24 @@ def search_match():
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
def _is_explicit_blocked(track_data):
|
||||||
|
"""Check if a track should be blocked by the explicit content filter.
|
||||||
|
Returns True if the track is explicit and explicit content is disabled."""
|
||||||
|
if config_manager.get('content_filter.allow_explicit', True):
|
||||||
|
return False
|
||||||
|
# Check direct explicit field
|
||||||
|
if track_data.get('explicit', False):
|
||||||
|
return True
|
||||||
|
# Check nested spotify_data (wishlist tracks)
|
||||||
|
sp_data = track_data.get('spotify_data', {})
|
||||||
|
if isinstance(sp_data, str):
|
||||||
|
try:
|
||||||
|
sp_data = json.loads(sp_data)
|
||||||
|
except Exception:
|
||||||
|
sp_data = {}
|
||||||
|
return sp_data.get('explicit', False)
|
||||||
|
|
||||||
|
|
||||||
def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_artist, spotify_album):
|
def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_artist, spotify_album):
|
||||||
"""
|
"""
|
||||||
Download album tracks that have been matched to Spotify with full track metadata.
|
Download album tracks that have been matched to Spotify with full track metadata.
|
||||||
|
|
@ -10496,6 +10514,10 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar
|
||||||
slskd_track = matched_item['slskd_track']
|
slskd_track = matched_item['slskd_track']
|
||||||
spotify_track = matched_item['spotify_track']
|
spotify_track = matched_item['spotify_track']
|
||||||
|
|
||||||
|
if _is_explicit_blocked(spotify_track):
|
||||||
|
logger.info(f"🚫 [Content Filter] Skipping explicit track: '{spotify_track.get('name')}'")
|
||||||
|
continue
|
||||||
|
|
||||||
username = slskd_track.get('username')
|
username = slskd_track.get('username')
|
||||||
filename = slskd_track.get('filename')
|
filename = slskd_track.get('filename')
|
||||||
size = slskd_track.get('size', 0)
|
size = slskd_track.get('size', 0)
|
||||||
|
|
@ -10622,6 +10644,10 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album):
|
||||||
corrected_meta = _match_track_to_spotify_title(parsed_meta, official_spotify_tracks)
|
corrected_meta = _match_track_to_spotify_title(parsed_meta, official_spotify_tracks)
|
||||||
# --- END OF CRITICAL STEP ---
|
# --- END OF CRITICAL STEP ---
|
||||||
|
|
||||||
|
if _is_explicit_blocked(corrected_meta):
|
||||||
|
print(f"🚫 [Content Filter] Skipping explicit track: '{corrected_meta.get('title')}'")
|
||||||
|
continue
|
||||||
|
|
||||||
# Create a clean context object using the CORRECTED metadata
|
# Create a clean context object using the CORRECTED metadata
|
||||||
individual_track_context = {
|
individual_track_context = {
|
||||||
'username': username,
|
'username': username,
|
||||||
|
|
@ -10686,6 +10712,8 @@ def start_matched_download():
|
||||||
|
|
||||||
# NEW: Enhanced single track with full Spotify metadata
|
# NEW: Enhanced single track with full Spotify metadata
|
||||||
if is_single_track and spotify_track:
|
if is_single_track and spotify_track:
|
||||||
|
if _is_explicit_blocked(spotify_track):
|
||||||
|
return jsonify({"success": False, "error": "Explicit content is disabled in settings", "explicit_blocked": True}), 403
|
||||||
logger.info(f"🎯 Starting enhanced single track download: '{spotify_track['name']}' by {spotify_artist['name']}")
|
logger.info(f"🎯 Starting enhanced single track download: '{spotify_track['name']}' by {spotify_artist['name']}")
|
||||||
|
|
||||||
username = download_payload.get('username')
|
username = download_payload.get('username')
|
||||||
|
|
@ -13065,7 +13093,8 @@ def _get_spotify_album_tracks(spotify_album: dict) -> list:
|
||||||
'name': item.get('name'),
|
'name': item.get('name'),
|
||||||
'track_number': item.get('track_number'),
|
'track_number': item.get('track_number'),
|
||||||
'disc_number': item.get('disc_number', 1),
|
'disc_number': item.get('disc_number', 1),
|
||||||
'id': item.get('id')
|
'id': item.get('id'),
|
||||||
|
'explicit': item.get('explicit', False)
|
||||||
} for item in tracks_data['items']]
|
} for item in tracks_data['items']]
|
||||||
return []
|
return []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -13092,7 +13121,8 @@ def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) -
|
||||||
'artist': slsk_track_meta.get('artist'),
|
'artist': slsk_track_meta.get('artist'),
|
||||||
'album': slsk_track_meta.get('album'),
|
'album': slsk_track_meta.get('album'),
|
||||||
'track_number': sp_track['track_number'],
|
'track_number': sp_track['track_number'],
|
||||||
'disc_number': sp_track.get('disc_number', 1)
|
'disc_number': sp_track.get('disc_number', 1),
|
||||||
|
'explicit': sp_track.get('explicit', False)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Priority 2: Match by title similarity (if track number fails)
|
# Priority 2: Match by title similarity (if track number fails)
|
||||||
|
|
@ -13106,7 +13136,7 @@ def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) -
|
||||||
if score > best_score:
|
if score > best_score:
|
||||||
best_score = score
|
best_score = score
|
||||||
best_match = sp_track
|
best_match = sp_track
|
||||||
|
|
||||||
if best_match:
|
if best_match:
|
||||||
print(f"✅ Matched track by title similarity ({best_score:.2f}): '{slsk_track_meta['title']}' -> '{best_match['name']}'")
|
print(f"✅ Matched track by title similarity ({best_score:.2f}): '{slsk_track_meta['title']}' -> '{best_match['name']}'")
|
||||||
return {
|
return {
|
||||||
|
|
@ -13114,7 +13144,8 @@ def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) -
|
||||||
'artist': slsk_track_meta.get('artist'),
|
'artist': slsk_track_meta.get('artist'),
|
||||||
'album': slsk_track_meta.get('album'),
|
'album': slsk_track_meta.get('album'),
|
||||||
'track_number': best_match['track_number'],
|
'track_number': best_match['track_number'],
|
||||||
'disc_number': best_match.get('disc_number', 1)
|
'disc_number': best_match.get('disc_number', 1),
|
||||||
|
'explicit': best_match.get('explicit', False)
|
||||||
}
|
}
|
||||||
|
|
||||||
print(f"⚠️ Could not confidently match track '{slsk_track_meta['title']}'. Using original metadata.")
|
print(f"⚠️ Could not confidently match track '{slsk_track_meta['title']}'. Using original metadata.")
|
||||||
|
|
@ -18555,6 +18586,14 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
|
||||||
|
|
||||||
missing_tracks = [res for res in analysis_results if not res['found']]
|
missing_tracks = [res for res in analysis_results if not res['found']]
|
||||||
|
|
||||||
|
# Filter explicit tracks if content filter is enabled
|
||||||
|
if not config_manager.get('content_filter.allow_explicit', True):
|
||||||
|
before_count = len(missing_tracks)
|
||||||
|
missing_tracks = [res for res in missing_tracks if not _is_explicit_blocked(res.get('track', {}))]
|
||||||
|
skipped = before_count - len(missing_tracks)
|
||||||
|
if skipped > 0:
|
||||||
|
print(f"🚫 [Content Filter] Filtered out {skipped} explicit track(s) from download queue")
|
||||||
|
|
||||||
with tasks_lock:
|
with tasks_lock:
|
||||||
if batch_id in download_batches:
|
if batch_id in download_batches:
|
||||||
download_batches[batch_id]['analysis_results'] = analysis_results
|
download_batches[batch_id]['analysis_results'] = analysis_results
|
||||||
|
|
@ -19889,6 +19928,16 @@ def start_playlist_missing_downloads(playlist_id):
|
||||||
if not missing_tracks:
|
if not missing_tracks:
|
||||||
return jsonify({"success": False, "error": "No missing tracks provided"}), 400
|
return jsonify({"success": False, "error": "No missing tracks provided"}), 400
|
||||||
|
|
||||||
|
# Filter explicit tracks if content filter is enabled
|
||||||
|
if not config_manager.get('content_filter.allow_explicit', True):
|
||||||
|
before_count = len(missing_tracks)
|
||||||
|
missing_tracks = [t for t in missing_tracks if not _is_explicit_blocked(t.get('track', t))]
|
||||||
|
skipped = before_count - len(missing_tracks)
|
||||||
|
if skipped > 0:
|
||||||
|
print(f"🚫 [Content Filter] Filtered out {skipped} explicit track(s) from playlist download")
|
||||||
|
if not missing_tracks:
|
||||||
|
return jsonify({"success": False, "error": "All tracks were filtered by explicit content setting"}), 400
|
||||||
|
|
||||||
# Add activity for playlist download missing start
|
# Add activity for playlist download missing start
|
||||||
playlist_name = data.get('playlist_name', f'Playlist {playlist_id}')
|
playlist_name = data.get('playlist_name', f'Playlist {playlist_id}')
|
||||||
add_activity_item("📥", "Missing Tracks Download Started", f"'{playlist_name}' - {len(missing_tracks)} tracks", "Now")
|
add_activity_item("📥", "Missing Tracks Download Started", f"'{playlist_name}' - {len(missing_tracks)} tracks", "Now")
|
||||||
|
|
|
||||||
|
|
@ -3782,6 +3782,22 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Filter Settings -->
|
||||||
|
<div class="settings-group">
|
||||||
|
<h3>🔞 Content Filter</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="allow-explicit" checked>
|
||||||
|
Allow explicit content in downloads
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="help-text">
|
||||||
|
When disabled, tracks marked as explicit on Spotify will be skipped
|
||||||
|
during matched downloads. Does not affect manual Soulseek searches.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- File Organization Settings -->
|
<!-- File Organization Settings -->
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<h3>📁 File Organization</h3>
|
<h3>📁 File Organization</h3>
|
||||||
|
|
|
||||||
|
|
@ -3946,6 +3946,9 @@ async function loadSettingsData() {
|
||||||
document.getElementById('lossy-copy-options').style.display =
|
document.getElementById('lossy-copy-options').style.display =
|
||||||
settings.lossy_copy?.enabled ? 'block' : 'none';
|
settings.lossy_copy?.enabled ? 'block' : 'none';
|
||||||
|
|
||||||
|
// Populate Content Filter settings
|
||||||
|
document.getElementById('allow-explicit').checked = settings.content_filter?.allow_explicit !== false;
|
||||||
|
|
||||||
// Populate M3U Export settings
|
// Populate M3U Export settings
|
||||||
document.getElementById('m3u-export-enabled').checked = settings.m3u_export?.enabled === true;
|
document.getElementById('m3u-export-enabled').checked = settings.m3u_export?.enabled === true;
|
||||||
|
|
||||||
|
|
@ -4637,6 +4640,9 @@ async function saveSettings(quiet = false) {
|
||||||
playlist_sync: {
|
playlist_sync: {
|
||||||
create_backup: document.getElementById('create-backup').checked
|
create_backup: document.getElementById('create-backup').checked
|
||||||
},
|
},
|
||||||
|
content_filter: {
|
||||||
|
allow_explicit: document.getElementById('allow-explicit').checked
|
||||||
|
},
|
||||||
lossy_copy: {
|
lossy_copy: {
|
||||||
enabled: document.getElementById('lossy-copy-enabled').checked,
|
enabled: document.getElementById('lossy-copy-enabled').checked,
|
||||||
bitrate: document.getElementById('lossy-copy-bitrate').value,
|
bitrate: document.getElementById('lossy-copy-bitrate').value,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue