Add lossy MP3 copy of downloaded FLACs
Introduce a configurable "lossy_copy" feature that creates an MP3 copy alongside downloaded FLAC files. Adds default config (example and runtime) and UI controls for enabling the feature and selecting an MP3 bitrate. Implements _create_lossy_copy in web_server.py which checks the FLAC extension, respects the configured bitrate (default 320 kbps), locates ffmpeg (including a local tools/ffmpeg fallback), performs conversion, and attempts to update the QUALITY tag via mutagen. The feature is invoked after post-processing/moving downloads. Logs and graceful failures (missing ffmpeg, timeouts, tag errors) are included.
This commit is contained in:
parent
d858a7c85f
commit
d2adf17ca5
5 changed files with 116 additions and 1 deletions
|
|
@ -56,6 +56,10 @@
|
|||
"playlist_path": "$playlist/$artist - $title"
|
||||
}
|
||||
},
|
||||
"lossy_copy": {
|
||||
"enabled": false,
|
||||
"bitrate": "320"
|
||||
},
|
||||
"playlist_sync": {
|
||||
"create_backup": true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@ class ConfigManager:
|
|||
"settings": {
|
||||
"audio_quality": "flac"
|
||||
},
|
||||
"lossy_copy": {
|
||||
"enabled": False,
|
||||
"bitrate": "320"
|
||||
},
|
||||
"import": {
|
||||
"staging_path": "./Staging"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2451,7 +2451,7 @@ def handle_settings():
|
|||
if 'active_media_server' in new_settings:
|
||||
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', 'listenbrainz', 'acoustid', 'import']:
|
||||
for service in ['spotify', 'plex', 'jellyfin', 'navidrome', 'soulseek', 'download_source', 'settings', 'database', 'metadata_enhancement', 'file_organization', 'playlist_sync', 'tidal', 'listenbrainz', 'acoustid', 'import', 'lossy_copy']:
|
||||
if service in new_settings:
|
||||
for key, value in new_settings[service].items():
|
||||
config_manager.set(f'{service}.{key}', value)
|
||||
|
|
@ -8245,6 +8245,68 @@ def _get_audio_quality_string(file_path):
|
|||
logger.debug(f"Could not determine audio quality for {file_path}: {e}")
|
||||
return ''
|
||||
|
||||
def _create_lossy_copy(final_path):
|
||||
"""Convert a FLAC file to MP3 at the user's configured bitrate.
|
||||
|
||||
Only runs when lossy_copy is enabled and the file is a FLAC.
|
||||
Places the MP3 alongside the FLAC with the same basename.
|
||||
"""
|
||||
if not config_manager.get('lossy_copy.enabled', False):
|
||||
return
|
||||
|
||||
ext = os.path.splitext(final_path)[1].lower()
|
||||
if ext != '.flac':
|
||||
return
|
||||
|
||||
bitrate = config_manager.get('lossy_copy.bitrate', '320')
|
||||
mp3_quality = f'MP3-{bitrate}'
|
||||
mp3_path = os.path.splitext(final_path)[0] + '.mp3'
|
||||
|
||||
# If $quality was used in filename, swap FLAC quality for MP3 quality
|
||||
original_quality = _get_audio_quality_string(final_path)
|
||||
if original_quality:
|
||||
mp3_basename = os.path.basename(mp3_path)
|
||||
if original_quality in mp3_basename:
|
||||
mp3_basename = mp3_basename.replace(original_quality, mp3_quality)
|
||||
mp3_path = os.path.join(os.path.dirname(mp3_path), mp3_basename)
|
||||
|
||||
ffmpeg_bin = shutil.which('ffmpeg')
|
||||
if not ffmpeg_bin:
|
||||
local = os.path.join(os.path.dirname(__file__), 'tools', 'ffmpeg')
|
||||
if os.path.isfile(local):
|
||||
ffmpeg_bin = local
|
||||
else:
|
||||
print("⚠️ [Lossy Copy] ffmpeg not found — skipping MP3 conversion")
|
||||
return
|
||||
|
||||
try:
|
||||
print(f"🎵 [Lossy Copy] Converting to MP3-{bitrate}: {os.path.basename(final_path)}")
|
||||
result = subprocess.run([
|
||||
ffmpeg_bin, '-i', final_path,
|
||||
'-codec:a', 'libmp3lame',
|
||||
'-b:a', f'{bitrate}k',
|
||||
'-map_metadata', '0',
|
||||
'-id3v2_version', '3',
|
||||
'-y', mp3_path
|
||||
], capture_output=True, text=True, timeout=120)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"✅ [Lossy Copy] Created MP3-{bitrate} copy: {os.path.basename(mp3_path)}")
|
||||
# Fix QUALITY tag — the FLAC's tag (e.g. "FLAC 24bit") was copied verbatim
|
||||
try:
|
||||
from mutagen.id3 import ID3, TXXX
|
||||
tags = ID3(mp3_path)
|
||||
tags.add(TXXX(encoding=3, desc='QUALITY', text=[f'MP3-{bitrate}']))
|
||||
tags.save()
|
||||
except Exception as tag_err:
|
||||
print(f"⚠️ [Lossy Copy] Could not update QUALITY tag: {tag_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}")
|
||||
|
||||
def _apply_path_template(template: str, context: dict) -> str:
|
||||
"""
|
||||
Apply template to build file path.
|
||||
|
|
@ -9747,6 +9809,9 @@ 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)
|
||||
|
||||
# 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)
|
||||
|
|
@ -10006,6 +10071,9 @@ def _post_process_matched_download(context_key, context, file_path):
|
|||
# 4. Generate LRC lyrics file at final location (elegant addition)
|
||||
_generate_lrc_file(final_path, context, spotify_artist, album_info)
|
||||
|
||||
# Lossy copy: create MP3 version if enabled
|
||||
_create_lossy_copy(final_path)
|
||||
|
||||
downloads_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads'))
|
||||
_cleanup_empty_directories(downloads_path, file_path)
|
||||
|
||||
|
|
|
|||
|
|
@ -3190,6 +3190,35 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lossy Copy Settings -->
|
||||
<div class="settings-group">
|
||||
<h3>📀 Lossy Copy</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="lossy-copy-enabled"
|
||||
onchange="document.getElementById('lossy-copy-options').style.display = this.checked ? 'block' : 'none'">
|
||||
Create MP3 copy of downloaded FLAC files
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="lossy-copy-options" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label>MP3 Bitrate:</label>
|
||||
<select id="lossy-copy-bitrate">
|
||||
<option value="320">320 kbps</option>
|
||||
<option value="256">256 kbps</option>
|
||||
<option value="192">192 kbps</option>
|
||||
<option value="128">128 kbps</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="help-text">
|
||||
After downloading a FLAC, an MP3 copy at the selected bitrate
|
||||
will be created in the same folder. Requires ffmpeg.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Organization Settings -->
|
||||
<div class="settings-group">
|
||||
<h3>📁 File Organization</h3>
|
||||
|
|
|
|||
|
|
@ -1818,6 +1818,12 @@ async function loadSettingsData() {
|
|||
// Populate Playlist Sync settings
|
||||
document.getElementById('create-backup').checked = settings.playlist_sync?.create_backup !== false;
|
||||
|
||||
// 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-options').style.display =
|
||||
settings.lossy_copy?.enabled ? 'block' : 'none';
|
||||
|
||||
// Populate Logging information (read-only)
|
||||
document.getElementById('log-level-display').textContent = settings.logging?.level || 'INFO';
|
||||
document.getElementById('log-path-display').textContent = settings.logging?.path || 'logs/app.log';
|
||||
|
|
@ -2271,6 +2277,10 @@ async function saveSettings(quiet = false) {
|
|||
playlist_sync: {
|
||||
create_backup: document.getElementById('create-backup').checked
|
||||
},
|
||||
lossy_copy: {
|
||||
enabled: document.getElementById('lossy-copy-enabled').checked,
|
||||
bitrate: document.getElementById('lossy-copy-bitrate').value
|
||||
},
|
||||
import: {
|
||||
staging_path: document.getElementById('staging-path').value || './Staging'
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue