From cd13b1185efad389c34457893664c61b955b70ae Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 19 Jun 2026 08:43:47 -0700 Subject: [PATCH] video quality profile UI: rich-curated ladder + cutoff + rejects + soft prefs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilds the Video Quality Profile card to match the new Radarr-class model: - Quality ladder: one ranked, toggleable source×resolution list (Remux·4K … SDTV) reusing the .hybrid-source-item styling (parity with Download Source). The cutoff tier shows an accent rail + 'cutoff' tag. - Upgrade until: a styled dropdown of the enabled tiers (the cutoff). - Never grab: red toggle chips for the hard rejects (cam/screener/workprint/3d/x264). - Preferences: segmented controls for codec (Any/HEVC/AV1), HDR (Don't care/ Prefer/Require), audio (Any/Surround/Lossless/Atmos) + a Prefer REPACK check — all soft tie-breakers. - Size guard: min + max GB sliders (0 = no limit). Dropped the old resolutions/source_priority/single-codec/fallback widgets. JS delegation rewritten for the new data-vq-* hooks; same-verdict-as-HEAD balance, div balance clean. Pure UI on the isolated video Downloads tab. --- webui/index.html | 75 ++++++++---- webui/static/video/video-settings.js | 165 ++++++++++++++++----------- webui/static/video/video-side.css | 29 ++++- 3 files changed, 178 insertions(+), 91 deletions(-) diff --git a/webui/index.html b/webui/index.html index 5cfefd25..4950e8eb 100644 --- a/webui/index.html +++ b/webui/index.html @@ -6110,40 +6110,65 @@

Video Quality Profile

- One profile for every video source. Quality is read from the release name / filename — choose what you'll accept, then how to break ties. + One profile for every video source (slskd · torrent · usenet). Quality is read from the release name — pick the ladder you'll accept, where to stop upgrading, what to reject outright, then the soft tie-breakers.
-
Resolutions to accept toggle on what you want · order = preference (best first)
-
+
Quality ladder toggle what you'll accept · rank best→worst with the arrows
+
-
Preferred source tie-breaker when two releases match the same resolution
-
+
Upgrade until once the library holds this tier (or better) stop grabbing upgrades
+ +
+
+
Never grab hard rejects — skipped no matter what else matches
+
+
+
+
Preferences soft tie-breakers — they rank candidates, they never reject on their own
+
+
+ Codec +
+ + + +
+
+
+ HDR +
+ + + +
+
+
+ Audio +
+ + + + +
+
+
+
-
Codec
-
- - - -
+
Min size per item No limit
+ +
+
+
Max size per item No limit
+
-
-
-
Max size per item No limit
- -
-
diff --git a/webui/static/video/video-settings.js b/webui/static/video/video-settings.js index b5453232..0e9086f9 100644 --- a/webui/static/video/video-settings.js +++ b/webui/static/video/video-settings.js @@ -19,8 +19,18 @@ var QUALITY_URL = '/api/video/downloads/quality'; var SLSKD_URL = '/api/video/downloads/slskd'; var _videoQuality = null; - var RES_LABEL = { '2160p': '4K (2160p)', '1080p': '1080p', '720p': '720p', '480p': '480p (SD)' }; - var SRC_LABEL = { 'bluray': 'BluRay', 'web-dl': 'WEB-DL', 'webrip': 'WEBRip', 'hdtv': 'HDTV' }; + // Pretty labels for the source×resolution quality ladder (keys come from the backend). + var TIER_LABEL = { + 'remux-2160p': 'Remux · 4K', 'bluray-2160p': 'BluRay · 4K', 'web-2160p': 'WEB · 4K', + 'remux-1080p': 'Remux · 1080p', 'bluray-1080p': 'BluRay · 1080p', 'web-1080p': 'WEB-DL · 1080p', + 'webrip-1080p': 'WEBRip · 1080p', 'hdtv-1080p': 'HDTV · 1080p', + 'bluray-720p': 'BluRay · 720p', 'web-720p': 'WEB-DL · 720p', 'hdtv-720p': 'HDTV · 720p', + 'dvd': 'DVD', 'sdtv': 'SDTV' + }; + var REJECT_LABEL = { + 'cam': 'CAM / TS', 'screener': 'Screener', 'workprint': 'Workprint', '3d': '3D', 'x264': 'x264 / AVC' + }; + var REJECT_ORDER = ['cam', 'screener', 'workprint', '3d', 'x264']; function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>'); @@ -442,70 +452,89 @@ .catch(function () { /* ignore */ }); } + function _vqSizeLabel(id, v) { + var lab = document.getElementById(id); + if (lab) lab.textContent = v ? (v + ' GB') : 'No limit'; + } + + function _vqSeg(id, attr, value) { + var seg = document.getElementById(id); + if (!seg) return; + Array.prototype.forEach.call(seg.querySelectorAll('[' + attr + ']'), function (b) { + b.classList.toggle('active', b.getAttribute(attr) === value); + }); + } + function renderQuality() { var p = _videoQuality; if (!p) return; - var resHost = document.getElementById('vq-resolution-rows'); - if (resHost) { - var keys = Object.keys(p.resolutions).sort(function (a, b) { - return p.resolutions[a].priority - p.resolutions[b].priority; - }); - resHost.innerHTML = keys.map(function (k, i) { - var r = p.resolutions[k]; - return '
' + + var tiers = Array.isArray(p.tiers) ? p.tiers : []; + + // Quality ladder — ranked, toggleable (same .hybrid-source-item styling as Download Source). + var host = document.getElementById('vq-tier-rows'); + if (host) { + host.innerHTML = tiers.map(function (t, i) { + var cut = (p.cutoff === t.key); + return '
' + '' + - '' + - '' + + '' + + '' + '' + - '' + (RES_LABEL[k] || k) + '' + + '' + (TIER_LABEL[t.key] || t.key) + (cut ? ' cutoff' : '') + '' + '' + (i + 1) + '' + - '' + + '' + '
'; }).join(''); } - var srcHost = document.getElementById('vq-source-rows'); - if (srcHost) { - srcHost.innerHTML = p.source_priority.map(function (s, i) { - return '
' + - '' + - '' + - '' + - '' + - '' + (SRC_LABEL[s] || s) + '' + - '' + (i + 1) + '' + - '
'; + + // Cutoff dropdown — only the enabled tiers are sensible upgrade targets. + var cut = document.getElementById('vq-cutoff'); + if (cut) { + var enabled = tiers.filter(function (t) { return t.enabled; }); + cut.innerHTML = enabled.map(function (t) { + return ''; + }).join(''); + if (!enabled.length) cut.innerHTML = ''; + } + + // Hard rejects — toggle chips (on = blocked). + var rj = document.getElementById('vq-rejects'); + if (rj) { + var set = Array.isArray(p.rejects) ? p.rejects : []; + rj.innerHTML = REJECT_ORDER.map(function (k) { + var on = set.indexOf(k) !== -1; + return ''; }).join(''); } - var seg = document.getElementById('vq-codec'); - if (seg) { - Array.prototype.forEach.call(seg.querySelectorAll('[data-vq-codec]'), function (b) { - b.classList.toggle('active', b.getAttribute('data-vq-codec') === p.codec); - }); - } - var hdr = document.getElementById('vq-prefer-hdr'); if (hdr) hdr.checked = !!p.prefer_hdr; - var fb = document.getElementById('vq-fallback'); if (fb) fb.checked = p.fallback_enabled !== false; - var sl = document.getElementById('vq-max-size'); if (sl) sl.value = p.max_size_gb || 0; - var lab = document.getElementById('vq-size-label'); - if (lab) lab.textContent = p.max_size_gb ? (p.max_size_gb + ' GB') : 'No limit'; + + // Soft preferences. + _vqSeg('vq-codec', 'data-vq-codec', p.prefer_codec); + _vqSeg('vq-hdr', 'data-vq-hdr', p.prefer_hdr); + _vqSeg('vq-audio', 'data-vq-audio', p.prefer_audio); + var rep = document.getElementById('vq-prefer-repack'); if (rep) rep.checked = !!p.prefer_repack; + + // Size guard. + var mn = document.getElementById('vq-min-size'); if (mn) mn.value = p.min_size_gb || 0; + var mx = document.getElementById('vq-max-size'); if (mx) mx.value = p.max_size_gb || 0; + _vqSizeLabel('vq-min-label', p.min_size_gb || 0); + _vqSizeLabel('vq-max-label', p.max_size_gb || 0); } - function moveRes(k, dir) { - var p = _videoQuality; if (!p) return; - var keys = Object.keys(p.resolutions).sort(function (a, b) { - return p.resolutions[a].priority - p.resolutions[b].priority; - }); - var i = keys.indexOf(k), j = i + dir; - if (j < 0 || j >= keys.length) return; - keys[i] = keys[j]; keys[j] = k; // swap - keys.forEach(function (key, idx) { p.resolutions[key].priority = idx + 1; }); + function moveTier(k, dir) { + var p = _videoQuality; if (!p || !Array.isArray(p.tiers)) return; + var arr = p.tiers, i = -1; + for (var n = 0; n < arr.length; n++) { if (arr[n].key === k) { i = n; break; } } + var j = i + dir; + if (i < 0 || j < 0 || j >= arr.length) return; + var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; // swap renderQuality(); saveQuality(true); } - function moveSrc(s, dir) { + function toggleReject(k) { var p = _videoQuality; if (!p) return; - var arr = p.source_priority, i = arr.indexOf(s), j = i + dir; - if (j < 0 || j >= arr.length) return; - arr[i] = arr[j]; arr[j] = s; + if (!Array.isArray(p.rejects)) p.rejects = []; + var i = p.rejects.indexOf(k); + if (i === -1) p.rejects.push(k); else p.rejects.splice(i, 1); renderQuality(); saveQuality(true); } @@ -521,33 +550,41 @@ // Delegated handlers for the quality profile (rows re-render, so delegate). function wireQuality() { - var sec = document.getElementById('vq-resolution-rows'); + var sec = document.getElementById('vq-tier-rows'); if (!sec) return; var card = sec.closest('.settings-group'); if (!card || card._vqWired) return; card._vqWired = true; card.addEventListener('click', function (e) { - var rm = e.target.closest('[data-vq-res-move]'); - if (rm) { moveRes(rm.getAttribute('data-vq-res-move'), parseInt(rm.getAttribute('data-dir'), 10)); return; } - var sm = e.target.closest('[data-vq-src-move]'); - if (sm) { moveSrc(sm.getAttribute('data-vq-src-move'), parseInt(sm.getAttribute('data-dir'), 10)); return; } + var tm = e.target.closest('[data-vq-tier-move]'); + if (tm) { moveTier(tm.getAttribute('data-vq-tier-move'), parseInt(tm.getAttribute('data-dir'), 10)); return; } + var rj = e.target.closest('[data-vq-reject]'); + if (rj) { toggleReject(rj.getAttribute('data-vq-reject')); return; } + if (!_videoQuality) return; var cd = e.target.closest('[data-vq-codec]'); - if (cd && _videoQuality) { _videoQuality.codec = cd.getAttribute('data-vq-codec'); renderQuality(); saveQuality(true); } + if (cd) { _videoQuality.prefer_codec = cd.getAttribute('data-vq-codec'); renderQuality(); saveQuality(true); return; } + var hd = e.target.closest('[data-vq-hdr]'); + if (hd) { _videoQuality.prefer_hdr = hd.getAttribute('data-vq-hdr'); renderQuality(); saveQuality(true); return; } + var au = e.target.closest('[data-vq-audio]'); + if (au) { _videoQuality.prefer_audio = au.getAttribute('data-vq-audio'); renderQuality(); saveQuality(true); return; } }); card.addEventListener('change', function (e) { if (!_videoQuality) return; - var rt = e.target.closest('[data-vq-res-toggle]'); - if (rt) { _videoQuality.resolutions[rt.getAttribute('data-vq-res-toggle')].enabled = rt.checked; renderQuality(); saveQuality(true); return; } - if (e.target.id === 'vq-prefer-hdr') { _videoQuality.prefer_hdr = e.target.checked; saveQuality(true); return; } - if (e.target.id === 'vq-fallback') { _videoQuality.fallback_enabled = e.target.checked; saveQuality(true); return; } + var tt = e.target.closest('[data-vq-tier-toggle]'); + if (tt) { + var key = tt.getAttribute('data-vq-tier-toggle'); + var arr = _videoQuality.tiers || []; + for (var n = 0; n < arr.length; n++) { if (arr[n].key === key) { arr[n].enabled = tt.checked; break; } } + renderQuality(); saveQuality(true); return; + } + if (e.target.id === 'vq-cutoff') { _videoQuality.cutoff = e.target.value; renderQuality(); saveQuality(true); return; } + if (e.target.id === 'vq-prefer-repack') { _videoQuality.prefer_repack = e.target.checked; saveQuality(true); return; } + if (e.target.id === 'vq-min-size') { _videoQuality.min_size_gb = parseInt(e.target.value, 10) || 0; saveQuality(true); return; } if (e.target.id === 'vq-max-size') { _videoQuality.max_size_gb = parseInt(e.target.value, 10) || 0; saveQuality(true); return; } }); card.addEventListener('input', function (e) { - if (e.target.id === 'vq-max-size' && _videoQuality) { - var v = parseInt(e.target.value, 10) || 0; - var lab = document.getElementById('vq-size-label'); - if (lab) lab.textContent = v ? (v + ' GB') : 'No limit'; - } + if (e.target.id === 'vq-min-size') { _vqSizeLabel('vq-min-label', parseInt(e.target.value, 10) || 0); return; } + if (e.target.id === 'vq-max-size') { _vqSizeLabel('vq-max-label', parseInt(e.target.value, 10) || 0); return; } }); } diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index 6485a8a9..8f07f3b3 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -2969,8 +2969,33 @@ body[data-side="video"] #soulsync-toggle { display: none; } color: rgba(255,255,255,0.65); margin-bottom: 9px; display: flex; align-items: center; gap: 10px; } .vq-hint { font-size: 11px; font-weight: 500; text-transform: none; letter-spacing: 0; color: rgba(255,255,255,0.4); } -/* Resolution + source rows reuse music's .hybrid-source-item styling (set in the - shared style.css) for parity with the Download Source list — no custom row CSS. */ +/* Quality ladder rows reuse music's .hybrid-source-item styling (set in the shared + style.css) for parity with the Download Source list — no custom row CSS. The + cutoff tier gets a subtle accent rail + tag. */ +.hybrid-source-item.vq-cut { border-left: 3px solid rgb(var(--accent-rgb)); + background: rgba(var(--accent-rgb),0.07); } +.vq-cut-tag { display: inline-block; margin-left: 9px; padding: 2px 8px; border-radius: 999px; + font-size: 10px; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; + background: rgb(var(--accent-rgb)); color: #06210f; vertical-align: middle; } +/* cutoff dropdown */ +.vq-select { width: 100%; max-width: 360px; padding: 10px 13px; border-radius: 10px; + background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: #fff; + font-size: 13.5px; font-weight: 600; cursor: pointer; outline: none; transition: border-color 0.15s; } +.vq-select:hover, .vq-select:focus { border-color: rgba(var(--accent-rgb),0.6); } +.vq-select option { background: #14181f; color: #fff; } +/* hard-reject chips (on = blocked) */ +.vq-chips { display: flex; flex-wrap: wrap; gap: 8px; } +.vq-chip { padding: 7px 14px; border-radius: 999px; cursor: pointer; font-size: 12.5px; font-weight: 600; + background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12); + color: rgba(255,255,255,0.6); transition: all 0.15s; } +.vq-chip:hover { color: #fff; border-color: rgba(255,255,255,0.25); } +.vq-chip.on { background: rgba(239,68,68,0.18); border-color: rgba(239,68,68,0.55); color: #fecaca; } +.vq-chip.on::before { content: '⊘ '; font-weight: 800; } +/* preference rows (label + segmented control) */ +.vq-prefs { display: flex; flex-direction: column; gap: 11px; } +.vq-pref-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; } +.vq-pref-name { font-size: 13px; font-weight: 700; color: rgba(255,255,255,0.75); + min-width: 56px; } /* codec + hdr row */ .vq-grid { display: flex; align-items: flex-end; gap: 28px; flex-wrap: wrap; margin: 16px 0; } .vq-seg { display: inline-flex; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08);