From 3644422ab8af43b8d09a7647e94d4c6cb36094aa Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Fri, 20 Feb 2026 15:05:45 -0800 Subject: [PATCH] Add FLAC bit depth filter to post-download quality gate --- database/music_database.py | 12 +++++--- web_server.py | 59 ++++++++++++++++++++++++++++++++++++++ webui/index.html | 11 +++++++ webui/static/script.js | 50 ++++++++++++++++++++++++++++++++ webui/static/style.css | 58 +++++++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+), 4 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index 2aaeec71..f091441d 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -2894,7 +2894,8 @@ class MusicDatabase: "enabled": True, "min_kbps": 500, "max_kbps": 10000, - "priority": 1 + "priority": 1, + "bit_depth": "any" }, "mp3_320": { "enabled": True, @@ -2942,7 +2943,8 @@ class MusicDatabase: "enabled": True, "min_kbps": 500, "max_kbps": 10000, - "priority": 1 + "priority": 1, + "bit_depth": "any" }, "mp3_320": { "enabled": False, @@ -2973,7 +2975,8 @@ class MusicDatabase: "enabled": True, "min_kbps": 500, "max_kbps": 10000, - "priority": 1 + "priority": 1, + "bit_depth": "any" }, "mp3_320": { "enabled": True, @@ -3004,7 +3007,8 @@ class MusicDatabase: "enabled": False, "min_kbps": 500, "max_kbps": 10000, - "priority": 4 + "priority": 4, + "bit_depth": "any" }, "mp3_320": { "enabled": True, diff --git a/web_server.py b/web_server.py index 31a2d0cb..66f51925 100644 --- a/web_server.py +++ b/web_server.py @@ -9221,6 +9221,56 @@ def _post_process_matched_download_with_verification(context_key, context, file_ _on_download_completed(batch_id, task_id, success=False) +def _check_flac_bit_depth(file_path, context, context_key): + """ + Check if a FLAC file matches the user's preferred bit depth. + Returns True if the file was rejected (caller should return), False if OK. + """ + if not context.get('_audio_quality', '').startswith('FLAC'): + return False + + from database.music_database import MusicDatabase + _qp_db = MusicDatabase() + _quality_profile = _qp_db.get_quality_profile() + _flac_pref = _quality_profile.get('qualities', {}).get('flac', {}).get('bit_depth', 'any') + + if _flac_pref == 'any': + return False + + # Parse actual bit depth from quality string like "FLAC 16bit" + _actual_bits = context['_audio_quality'].replace('FLAC ', '').replace('bit', '') + if _actual_bits == _flac_pref: + return False + + # Reject — same pattern as AcoustID verification failure + rejection_msg = f"FLAC bit depth mismatch: file is {_actual_bits}-bit, preference is {_flac_pref}-bit" + try: + quarantine_path = _move_to_quarantine(file_path, context, rejection_msg) + print(f"🚫 File quarantined due to bit depth filter: {quarantine_path}") + except Exception as quarantine_error: + logger.error(f"Quarantine failed ({quarantine_error}), deleting file: {file_path}") + try: + os.remove(file_path) + except Exception: + pass + + context['_bitdepth_rejected'] = True + with matched_context_lock: + if context_key in matched_downloads_context: + del matched_downloads_context[context_key] + + task_id = context.get('task_id') + batch_id = context.get('batch_id') + if task_id: + with tasks_lock: + if task_id in download_tasks: + download_tasks[task_id]['status'] = 'failed' + download_tasks[task_id]['error_message'] = f"Bit depth filter: {rejection_msg}" + if task_id and batch_id: + _on_download_completed(batch_id, task_id, success=False) + return True + + def _move_to_quarantine(file_path: str, context: dict, reason: str) -> str: """ Move a file to quarantine folder when AcoustID verification fails. @@ -9584,6 +9634,11 @@ def _post_process_matched_download(context_key, context, file_path): context['_audio_quality'] = _get_audio_quality_string(file_path) if context['_audio_quality']: print(f"🎧 Audio quality detected: {context['_audio_quality']}") + + # FLAC bit depth filter + if _check_flac_bit_depth(file_path, context, context_key): + return + final_path, _ = _build_final_path_for_track(context, spotify_artist, None, file_ext) print(f"📁 Playlist mode final path: '{final_path}'") @@ -9712,6 +9767,10 @@ def _post_process_matched_download(context_key, context, file_path): if context['_audio_quality']: print(f"🎧 Audio quality detected: {context['_audio_quality']}") + # FLAC bit depth filter + if _check_flac_bit_depth(file_path, context, context_key): + return + # 2. Build the final path using GUI-style track naming with multiple fallback sources if album_info and album_info['is_album']: album_name_sanitized = _sanitize_filename(album_info['album_name']) diff --git a/webui/index.html b/webui/index.html index 50c85645..bee4bb01 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3018,6 +3018,17 @@ +
+ +
+ + + +
+
+ Rejects FLAC files that don't match the selected bit depth +
+
diff --git a/webui/static/script.js b/webui/static/script.js index 6c4060d2..28389e9b 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1993,6 +1993,22 @@ function populateQualityProfileUI(profile) { sliders.classList.add('disabled'); } } + + // FLAC-specific: restore bit depth selector + if (quality === 'flac') { + const bitDepthValue = config.bit_depth || 'any'; + document.querySelectorAll('.bit-depth-btn').forEach(btn => { + btn.classList.toggle('active', btn.getAttribute('data-value') === bitDepthValue); + }); + const bitDepthSelector = document.getElementById('flac-bit-depth-selector'); + if (bitDepthSelector) { + if (config.enabled) { + bitDepthSelector.classList.remove('disabled'); + } else { + bitDepthSelector.classList.add('disabled'); + } + } + } } }); @@ -2042,6 +2058,18 @@ function toggleQuality(quality) { } } + // Also toggle FLAC bit depth selector + if (quality === 'flac') { + const bitDepthSelector = document.getElementById('flac-bit-depth-selector'); + if (bitDepthSelector && checkbox) { + if (checkbox.checked) { + bitDepthSelector.classList.remove('disabled'); + } else { + bitDepthSelector.classList.add('disabled'); + } + } + } + // Mark preset as custom when manually changing if (currentQualityProfile) { currentQualityProfile.preset = 'custom'; @@ -2051,6 +2079,22 @@ function toggleQuality(quality) { } } +function setFlacBitDepth(value) { + document.querySelectorAll('.bit-depth-btn').forEach(btn => { + btn.classList.toggle('active', btn.getAttribute('data-value') === value); + }); + + // Mark preset as custom when manually changing + if (currentQualityProfile) { + currentQualityProfile.preset = 'custom'; + document.querySelectorAll('.preset-button').forEach(btn => { + btn.classList.remove('active'); + }); + } + + debouncedAutoSaveSettings(); +} + async function applyQualityPreset(presetName) { try { showLoadingOverlay(`Applying ${presetName} preset...`); @@ -2100,6 +2144,12 @@ function collectQualityProfileFromUI() { max_kbps: parseInt(maxSlider?.value || 99999), priority: existingPriority }; + + // Add FLAC-specific bit_depth setting + if (quality === 'flac') { + const activeBtn = document.querySelector('.bit-depth-btn.active'); + profile.qualities[quality].bit_depth = activeBtn ? activeBtn.getAttribute('data-value') : 'any'; + } }); // Check if current profile matches a preset diff --git a/webui/static/style.css b/webui/static/style.css index 8b7f8c31..fc42b131 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -1499,6 +1499,64 @@ body { pointer-events: none; } +.flac-bit-depth-selector { + padding: 8px 12px; + transition: opacity 0.3s ease; +} + +.flac-bit-depth-selector.disabled { + opacity: 0.35; + pointer-events: none; +} + +.flac-bit-depth-selector label { + display: block; + margin-bottom: 6px; + color: rgba(255, 255, 255, 0.7); + font-size: 11px; +} + +.bit-depth-buttons { + display: flex; + gap: 8px; +} + +.bit-depth-btn { + flex: 1; + padding: 7px 12px; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02)); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + color: rgba(255, 255, 255, 0.7); + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.bit-depth-btn.active { + background: linear-gradient(135deg, + rgba(29, 185, 84, 0.2) 0%, + rgba(30, 215, 96, 0.12) 100%); + border-color: rgba(29, 185, 84, 0.4); + color: #1ed760; + font-weight: 600; + box-shadow: 0 4px 16px rgba(29, 185, 84, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.bit-depth-btn:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.15); + color: #ffffff; + transform: translateY(-1px); +} + +.bit-depth-btn.active:hover { + background: linear-gradient(135deg, + rgba(29, 185, 84, 0.28) 0%, + rgba(30, 215, 96, 0.18) 100%); +} + .slider-group { margin-top: 8px; }