diff --git a/core/repair_jobs/quality_upgrade.py b/core/repair_jobs/quality_upgrade.py index a6ee6522..00af7cf1 100644 --- a/core/repair_jobs/quality_upgrade.py +++ b/core/repair_jobs/quality_upgrade.py @@ -476,25 +476,26 @@ class QualityUpgradeJob(RepairJob): icon = 'repair-icon-lossy' default_enabled = False default_interval_hours = 168 - default_settings = {'scope': 'watchlist', 'min_confidence': 0.7, 'deep_audio_verify': False, 'require_top_target': False} - setting_options = {'scope': ['watchlist', 'all'], 'deep_audio_verify': [True, False], 'require_top_target': [True, False]} + default_settings = {'scope': 'all', 'min_confidence': 0.7, 'deep_audio_verify': False, 'require_top_target': False} + setting_options = {'scope': ['all', 'watchlist'], 'deep_audio_verify': [True, False], 'require_top_target': [True, False]} auto_fix = False def _get_settings(self, context: JobContext) -> Dict[str, Any]: - cfg = context.config_manager - scope = 'watchlist' - min_conf = 0.7 - deep_verify = False - require_top = False - if cfg: - scope = cfg.get(self.get_config_key('settings.scope'), 'watchlist') or 'watchlist' + merged = dict(self.default_settings) + if context.config_manager: try: - min_conf = float(cfg.get(self.get_config_key('settings.min_confidence'), 0.7)) - except (TypeError, ValueError): - min_conf = 0.7 - deep_verify = cfg.get(self.get_config_key('settings.deep_audio_verify'), False) is True - require_top = cfg.get(self.get_config_key('settings.require_top_target'), False) is True - return {'scope': scope, 'min_confidence': min_conf, 'deep_audio_verify': deep_verify, 'require_top_target': require_top} + cfg = context.config_manager.get(f'repair.jobs.{self.job_id}.settings', {}) + if isinstance(cfg, dict): + merged.update(cfg) + except Exception as e: + logger.debug("settings read failed: %s", e) + try: + 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 + return merged def _load_tracks(self, db: Any, scope: str) -> List[dict]: conn = db._get_connection() diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js index 23e8756e..64e34add 100644 --- a/webui/static/pages-extra.js +++ b/webui/static/pages-extra.js @@ -2611,6 +2611,7 @@ let _verifQuarLoading = false; // Expanded πŸ” detail panels, keyed by quarantine entry id β€” survives the // polling re-render (which rebuilds the rows every few seconds). const _verifQuarOpenDetails = new Set(); +const _verifQuarOpenGroups = new Set(); // group keys whose alt-members are expanded // null = not fetched yet (assume enabled). Without an AcoustID API key // nothing ever gets a verification status, so the review queue collapses // to quarantine-only. @@ -2668,7 +2669,7 @@ const _VERIF_QUAR_TRIGGERS = { bit_depth: ['BIT DEPTH FILTER', 'verif-rb-int'], }; -function _verifQuarRowHtml(q, idx) { +function _verifQuarRowHtml(q, idx, extraAction = '') { const title = _adlEsc(q.expected_track || q.original_filename || q.filename || 'Unknown file'); const meta = [_adlEsc(q.expected_artist || ''), _adlEsc(q.original_filename || '')].filter(Boolean).join(' β€” '); const [trigLabel, trigClass] = _VERIF_QUAR_TRIGGERS[q.trigger] || ['QUARANTINED', 'verif-rb-unv']; @@ -2702,10 +2703,21 @@ function _verifQuarRowHtml(q, idx) { ${approveBtn} + ${extraAction} `; } +function _verifQuarToggleGroup(btn) { + const key = btn.dataset.groupKey; + const open = !_verifQuarOpenGroups.has(key); + if (open) _verifQuarOpenGroups.add(key); else _verifQuarOpenGroups.delete(key); + const wrapper = btn.closest('.verif-quar-alt-wrapper'); + if (wrapper) wrapper.querySelector('.verif-quar-alt-members')?.classList.toggle('vqg-open', open); + btn.classList.toggle('open', open); + btn.textContent = open ? `β–΄ ${btn.dataset.altCount} more` : `β–Ύ ${btn.dataset.altCount} more`; +} + function _verifQuarRows() { if (!_verifQuarLoaded) return '
Loading quarantine…
'; if (!_verifQuarEntries.length) return ''; @@ -2718,20 +2730,20 @@ function _verifQuarRows() { if (group.members.length === 1) { html += _verifQuarRowHtml(group.members[0], idxById.get(group.members[0].id)); } else { + // First member shown as normal row; rest hidden under a toggle button. const first = group.members[0]; - const title = _adlEsc(first.expected_track || first.original_filename || 'Unknown'); - const artist = first.expected_artist ? ` β€” ${_adlEsc(first.expected_artist)}` : ''; - const n = group.members.length; - const groupId = `vqg-${_adlEsc(first.id)}`; - html += `
-
- ${title}${artist} - ${n} alternatives ▾ -
-
- ${group.members.map(m => _verifQuarRowHtml(m, idxById.get(m.id))).join('')} -
-
`; + const firstIdx = idxById.get(first.id); + const altCount = group.members.length - 1; + const groupKey = group.key || first.id; + const isOpen = _verifQuarOpenGroups.has(groupKey); + const altBtn = ``; + html += `
`; + html += _verifQuarRowHtml(first, firstIdx, altBtn); + html += `
`; + for (let i = 1; i < group.members.length; i++) { + html += _verifQuarRowHtml(group.members[i], idxById.get(group.members[i].id)); + } + html += `
`; } } return html; diff --git a/webui/static/style.css b/webui/static/style.css index 74c2f02b..468276d3 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -68215,13 +68215,12 @@ body.em-scroll-lock { overflow: hidden; } .verif-banner-spacer { flex: 1; } .verif-bulk-danger { border-color: rgba(248,113,113,0.4) !important; color: #f87171 !important; } -.verif-quar-group { margin-bottom: 4px; } -.verif-quar-group-hdr { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; cursor: pointer; user-select: none; } -.verif-quar-group-hdr:hover { background: rgba(255,255,255,0.07); } -.verif-quar-group-lbl { flex: 1; font-size: 13px; font-weight: 500; color: rgba(255,255,255,0.85); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.verif-quar-group-cnt { font-size: 12px; color: rgba(255,255,255,0.45); white-space: nowrap; } -.verif-quar-group-members { display: none; padding-left: 12px; padding-top: 2px; } -.verif-quar-group-members.vqg-open { display: block; } +.verif-quar-alt-wrapper { display: contents; } +.verif-quar-alt-btn { border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.5); border-radius: 6px; padding: 2px 7px; font-size: 11px; cursor: pointer; white-space: nowrap; line-height: 18px; } +.verif-quar-alt-btn:hover { background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.8); } +.verif-quar-alt-btn.open { color: rgba(255,255,255,0.75); border-color: rgba(255,255,255,0.25); } +.verif-quar-alt-members { display: none; padding-left: 20px; border-left: 2px solid rgba(255,255,255,0.07); margin-left: 8px; margin-bottom: 2px; } +.verif-quar-alt-members.vqg-open { display: block; } /* ── Security settings: grouped sub-sections + dependency visuals ── */ .security-subgroup {