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.
pull/154/head
Broque Thomas 3 months ago
parent 24bfc2462d
commit 129f69fce9

@ -222,7 +222,8 @@ class ConfigManager:
},
"lossy_copy": {
"enabled": False,
"bitrate": "320"
"bitrate": "320",
"delete_original": False
},
"import": {
"staging_path": "./Staging"

@ -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)

@ -3309,6 +3309,16 @@
After downloading a FLAC, an MP3 copy at the selected bitrate
will be created in the same folder. Requires ffmpeg.
</div>
<div class="form-group" style="margin-top: 12px;">
<label class="checkbox-label">
<input type="checkbox" id="lossy-copy-delete-original">
Blasphemy Mode — Delete original FLAC after MP3 is created
</label>
</div>
<div class="help-text" style="color: rgba(255, 100, 100, 0.8);">
Warning: The original high-quality file will be permanently deleted.
Only the MP3 copy will remain.
</div>
</div>
</div>

@ -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'

Loading…
Cancel
Save