fix(repair-jobs): boolean settings saved as string 'true'/'false' by UI dropdown

HTML <select> options can only store string values, so setting_options booleans
([True, False]) were serialised as 'true'/'false' strings and sent to the API.
Python's `x is True` check returned False for the string, making require_top_target
and deep_audio_verify permanently read as False regardless of what the user saved.

Fix JS: convert 'true'/'false' strings to real booleans before POSTing.
Fix Python: _to_bool() in quality_upgrade + inline coercion in scanner to handle
both existing string values in config and correct future booleans.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pull/896/head
dev 2 days ago
parent 94637cbe6f
commit ff12d8bbf2

@ -50,6 +50,15 @@ from utils.logging_config import get_logger
logger = get_logger("repair_jobs.quality_upgrade")
def _to_bool(val) -> bool:
"""Coerce a setting value to bool. Handles Python bool, string 'true'/'false', and int."""
if isinstance(val, bool):
return val
if isinstance(val, str):
return val.lower() == 'true'
return bool(val) if val is not None else False
# Quality ranks — higher is better. Lossless tops everything; lossy tiers fall out
# of bitrate. 0 means "below the lowest tracked tier / unknown".
RANK_LOSSLESS = 4
@ -494,8 +503,8 @@ class QualityUpgradeJob(RepairJob):
merged['min_confidence'] = float(merged.get('min_confidence', 0.7))
except (TypeError, ValueError):
merged['min_confidence'] = 0.7
merged['deep_audio_verify'] = merged.get('deep_audio_verify') is True
merged['require_top_target'] = merged.get('require_top_target') is True
merged['deep_audio_verify'] = _to_bool(merged.get('deep_audio_verify'))
merged['require_top_target'] = _to_bool(merged.get('require_top_target'))
return merged
def _load_tracks(self, db: Any, scope: str) -> List[dict]:

@ -297,6 +297,10 @@ class QualityUpgradeScannerJob(RepairJob):
merged.update(cfg)
except Exception as e:
logger.debug("settings read failed: %s", e)
for key in ('library_tracks_only', 'deep_audio_verify', 'require_top_target'):
val = merged.get(key)
if not isinstance(val, bool):
merged[key] = str(val).lower() == 'true' if val is not None else False
return merged
def _collect_music_dirs(self, context: JobContext) -> list:

@ -1988,7 +1988,10 @@ async function saveRepairJobSettings(jobId) {
} else {
if (input.type === 'checkbox') settings[key] = input.checked;
else if (input.type === 'number') settings[key] = parseFloat(input.value);
else settings[key] = input.value;
else {
const v = input.value;
settings[key] = v === 'true' ? true : v === 'false' ? false : v;
}
}
});

Loading…
Cancel
Save