From 129f69fce9b10bede8713b86cdec6ecbb63173fb Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Sun, 22 Feb 2026 23:23:52 -0800 Subject: [PATCH] Add Blasphemy Mode to delete FLAC after MP3 Introduce an optional "Blasphemy Mode" that deletes the original FLAC after a verified MP3 copy is created. - config: add lossy_copy.delete_original (default: false). - webui/index.html & static script: add checkbox and warning in settings UI and persist the setting. - web_server.py: make _create_lossy_copy return the MP3 path when it deletes the FLAC (otherwise None); validate the MP3 using mutagen before removing the FLAC; rename associated .lrc files if present; update post-processing to use the final processed path in logs and wishlist checks and to consider .mp3 variants when FLAC may have been removed. Behavior is off by default and includes safety checks and logging to avoid accidental deletion of originals. --- config/settings.py | 3 ++- web_server.py | 59 +++++++++++++++++++++++++++++++++++------- webui/index.html | 10 +++++++ webui/static/script.js | 4 ++- 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/config/settings.py b/config/settings.py index a019e68e..cf5ea722 100644 --- a/config/settings.py +++ b/config/settings.py @@ -222,7 +222,8 @@ class ConfigManager: }, "lossy_copy": { "enabled": False, - "bitrate": "320" + "bitrate": "320", + "delete_original": False }, "import": { "staging_path": "./Staging" diff --git a/web_server.py b/web_server.py index 617b0baf..1b502331 100644 --- a/web_server.py +++ b/web_server.py @@ -8380,13 +8380,15 @@ def _create_lossy_copy(final_path): Only runs when lossy_copy is enabled and the file is a FLAC. Places the MP3 alongside the FLAC with the same basename. + + Returns the MP3 path if Blasphemy Mode deleted the original, else None. """ if not config_manager.get('lossy_copy.enabled', False): - return + return None ext = os.path.splitext(final_path)[1].lower() if ext != '.flac': - return + return None bitrate = config_manager.get('lossy_copy.bitrate', '320') mp3_quality = f'MP3-{bitrate}' @@ -8407,7 +8409,7 @@ def _create_lossy_copy(final_path): ffmpeg_bin = local else: print("⚠️ [Lossy Copy] ffmpeg not found — skipping MP3 conversion") - return + return None try: print(f"🎵 [Lossy Copy] Converting to MP3-{bitrate}: {os.path.basename(final_path)}") @@ -8430,12 +8432,39 @@ def _create_lossy_copy(final_path): tags.save() except Exception as tag_err: print(f"⚠️ [Lossy Copy] Could not update QUALITY tag: {tag_err}") + + # Blasphemy Mode: delete original FLAC if enabled and MP3 is verified + if config_manager.get('lossy_copy.delete_original', False): + try: + if os.path.isfile(mp3_path) and os.path.getsize(mp3_path) > 0: + from mutagen import File as MutagenFile + test_audio = MutagenFile(mp3_path) + if test_audio is not None: + os.remove(final_path) + print(f"🔥 [Blasphemy Mode] Deleted original: {os.path.basename(final_path)}") + # Rename LRC file to match the MP3 filename + flac_lrc = os.path.splitext(final_path)[0] + '.lrc' + if os.path.isfile(flac_lrc): + mp3_lrc = os.path.splitext(mp3_path)[0] + '.lrc' + try: + os.rename(flac_lrc, mp3_lrc) + print(f"🔥 [Blasphemy Mode] Renamed LRC: {os.path.basename(flac_lrc)} -> {os.path.basename(mp3_lrc)}") + except Exception as lrc_err: + print(f"⚠️ [Blasphemy Mode] Could not rename LRC: {lrc_err}") + return mp3_path + else: + print(f"⚠️ [Blasphemy Mode] MP3 failed audio validation, keeping original: {os.path.basename(final_path)}") + else: + print(f"⚠️ [Blasphemy Mode] MP3 missing or empty, keeping original: {os.path.basename(final_path)}") + except Exception as del_err: + print(f"⚠️ [Blasphemy Mode] Error during original deletion, keeping original: {del_err}") else: print(f"⚠️ [Lossy Copy] ffmpeg failed: {result.stderr[:200]}") except subprocess.TimeoutExpired: print(f"⚠️ [Lossy Copy] Conversion timed out for: {os.path.basename(final_path)}") except Exception as e: print(f"⚠️ [Lossy Copy] Conversion error: {e}") + return None def _apply_path_template(template: str, context: dict) -> str: """ @@ -9934,13 +9963,15 @@ def _post_process_matched_download(context_key, context, file_path): _safe_move_file(file_path, final_path) # Lossy copy: create MP3 version if enabled - _create_lossy_copy(final_path) + blasphemy_path = _create_lossy_copy(final_path) + if blasphemy_path: + context['_final_processed_path'] = blasphemy_path # Clean up empty directories in downloads folder downloads_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads')) _cleanup_empty_directories(downloads_path, file_path) - print(f"✅ [Playlist Folder Mode] Post-processing complete: {final_path}") + print(f"✅ [Playlist Folder Mode] Post-processing complete: {context.get('_final_processed_path', final_path)}") # WISHLIST REMOVAL: Check if this track should be removed from wishlist try: @@ -10194,11 +10225,17 @@ def _post_process_matched_download(context_key, context, file_path): expected_stem = os.path.splitext(os.path.basename(final_path))[0] expected_ext = os.path.splitext(final_path)[1] found_variant = None + # Also check for .mp3 if Blasphemy Mode may have deleted the .flac + check_exts = {expected_ext} + if expected_ext == '.flac' and config_manager.get('lossy_copy.enabled', False) and config_manager.get('lossy_copy.delete_original', False): + check_exts.add('.mp3') if os.path.exists(expected_dir): for f in os.listdir(expected_dir): - # Match files that start with the expected stem and have the same extension + # Match files that start with the expected stem and have a matching extension # This catches "01 - track [FLAC 24bit].flac" when expecting "01 - track.flac" - if f.endswith(expected_ext) and os.path.splitext(f)[0].startswith(expected_stem): + # and "01 - track [MP3-320].mp3" when Blasphemy Mode deleted the FLAC + f_ext = os.path.splitext(f)[1].lower() + if f_ext in check_exts and os.path.splitext(f)[0].startswith(expected_stem): found_variant = os.path.join(expected_dir, f) break if found_variant: @@ -10219,13 +10256,15 @@ def _post_process_matched_download(context_key, context, file_path): _generate_lrc_file(final_path, context, spotify_artist, album_info) # Lossy copy: create MP3 version if enabled - _create_lossy_copy(final_path) + blasphemy_path = _create_lossy_copy(final_path) + if blasphemy_path: + context['_final_processed_path'] = blasphemy_path downloads_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads')) _cleanup_empty_directories(downloads_path, file_path) - print(f"✅ Post-processing complete for: {final_path}") - + print(f"✅ Post-processing complete for: {context.get('_final_processed_path', final_path)}") + # WISHLIST REMOVAL: Check if this track should be removed from wishlist after successful download try: _check_and_remove_from_wishlist(context) diff --git a/webui/index.html b/webui/index.html index 3c590b19..f37694e5 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3309,6 +3309,16 @@ After downloading a FLAC, an MP3 copy at the selected bitrate will be created in the same folder. Requires ffmpeg. +
+ +
+
+ Warning: The original high-quality file will be permanently deleted. + Only the MP3 copy will remain. +
diff --git a/webui/static/script.js b/webui/static/script.js index dbc83c1c..a011dde4 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1833,6 +1833,7 @@ async function loadSettingsData() { // Populate Lossy Copy settings document.getElementById('lossy-copy-enabled').checked = settings.lossy_copy?.enabled === true; document.getElementById('lossy-copy-bitrate').value = settings.lossy_copy?.bitrate || '320'; + document.getElementById('lossy-copy-delete-original').checked = settings.lossy_copy?.delete_original === true; document.getElementById('lossy-copy-options').style.display = settings.lossy_copy?.enabled ? 'block' : 'none'; @@ -2432,7 +2433,8 @@ async function saveSettings(quiet = false) { }, lossy_copy: { enabled: document.getElementById('lossy-copy-enabled').checked, - bitrate: document.getElementById('lossy-copy-bitrate').value + bitrate: document.getElementById('lossy-copy-bitrate').value, + delete_original: document.getElementById('lossy-copy-delete-original').checked }, import: { staging_path: document.getElementById('staging-path').value || './Staging'