feat: prioritise and blacklist media and overview in setting page of progress in adding info
This commit is contained in:
@@ -39,6 +39,66 @@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2 style="margin-top:2em;">Coverage</h2>
|
||||
<p class="fieldDescription">
|
||||
How much of your library has scene-actor truth data. Ignored items are
|
||||
excluded from the "to do" total, so blacklisting a genre or series
|
||||
won't drag your progress down.
|
||||
</p>
|
||||
<div id="JRayCoverageSummary" style="margin-bottom:1em;">Loading…</div>
|
||||
<div id="JRayCoverageBar" style="height:1.25em;border-radius:0.25em;overflow:hidden;background:rgba(127,127,127,0.25);display:flex;margin-bottom:0.5em;"></div>
|
||||
<div id="JRayCoverageLegend" class="fieldDescription" style="margin-bottom:1.5em;"></div>
|
||||
|
||||
<details style="margin-bottom:1.5em;">
|
||||
<summary style="cursor:pointer;">By media type</summary>
|
||||
<div id="JRayCoverageByType" style="margin-top:0.5em;"></div>
|
||||
</details>
|
||||
<details style="margin-bottom:1.5em;">
|
||||
<summary style="cursor:pointer;">By genre</summary>
|
||||
<div id="JRayCoverageByGenre" style="margin-top:0.5em;"></div>
|
||||
</details>
|
||||
|
||||
<h2 style="margin-top:2em;">Prioritise / ignore rules</h2>
|
||||
<p class="fieldDescription">
|
||||
Shape the work-discovery API (<code>/Plugins/JRay/Tasks/Pending</code>).
|
||||
<strong>Ignore</strong> hides matching items from extraction workers;
|
||||
<strong>Prioritise</strong> pushes them to the front of the queue. A more
|
||||
specific rule wins (item > series > genre). Setting a
|
||||
rule for a target that already has one replaces it, so nothing can be both
|
||||
prioritised and ignored.
|
||||
</p>
|
||||
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.5em;align-items:flex-end;margin-bottom:1em;">
|
||||
<div class="selectContainer" style="margin:0;">
|
||||
<label class="selectLabel" for="JRayRuleScope">Scope</label>
|
||||
<select id="JRayRuleScope" is="emby-select">
|
||||
<option value="Genre">Genre</option>
|
||||
<option value="Series">Series</option>
|
||||
<option value="Item">Item</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="JRayItemSearchContainer" class="inputContainer" style="margin:0;flex:1;min-width:14em;display:none;">
|
||||
<label class="inputLabel inputLabelUnfocused" for="JRayItemSearch">Search title</label>
|
||||
<input id="JRayItemSearch" type="text" is="emby-input" placeholder="Type a movie or episode name…" />
|
||||
</div>
|
||||
<div class="selectContainer" style="margin:0;flex:1;min-width:14em;">
|
||||
<label class="selectLabel" for="JRayRuleValue">Target</label>
|
||||
<select id="JRayRuleValue" is="emby-select"></select>
|
||||
</div>
|
||||
<div class="selectContainer" style="margin:0;">
|
||||
<label class="selectLabel" for="JRayRuleAction">Action</label>
|
||||
<select id="JRayRuleAction" is="emby-select">
|
||||
<option value="Ignore">Ignore</option>
|
||||
<option value="Prioritise">Prioritise</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="JRayAddRule" is="emby-button" type="button" class="raised emby-button">
|
||||
<span>Add rule</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="JRayRulesList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
@@ -46,6 +106,230 @@
|
||||
pluginUniqueId: '96a22d9d-23fd-49bb-8970-5e153817d223'
|
||||
};
|
||||
|
||||
function jrayUrl(path) {
|
||||
return ApiClient.getUrl('Plugins/JRay/' + path);
|
||||
}
|
||||
|
||||
function jrayApi(path, method, body) {
|
||||
var opts = {
|
||||
url: jrayUrl(path),
|
||||
type: method || 'GET'
|
||||
};
|
||||
if (body !== undefined) {
|
||||
opts.data = JSON.stringify(body);
|
||||
opts.contentType = 'application/json';
|
||||
}
|
||||
if (!method || method === 'GET') {
|
||||
opts.dataType = 'json';
|
||||
}
|
||||
return ApiClient.ajax(opts);
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ---- Coverage ----
|
||||
|
||||
var JRayCoverColors = {
|
||||
covered: '#4caf50',
|
||||
prioritised: '#ff9800',
|
||||
pending: '#9e9e9e',
|
||||
ignored: 'rgba(127,127,127,0.35)'
|
||||
};
|
||||
|
||||
function renderBar(el, counts) {
|
||||
el.innerHTML = '';
|
||||
var order = [
|
||||
['covered', counts.covered],
|
||||
['prioritised', counts.prioritised],
|
||||
['pending', counts.pending - counts.prioritised],
|
||||
['ignored', counts.ignored]
|
||||
];
|
||||
order.forEach(function (seg) {
|
||||
if (counts.total <= 0 || seg[1] <= 0) { return; }
|
||||
var d = document.createElement('div');
|
||||
d.style.background = JRayCoverColors[seg[0]];
|
||||
d.style.width = (100 * seg[1] / counts.total) + '%';
|
||||
el.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
function pct(counts) {
|
||||
var denom = counts.total - counts.ignored;
|
||||
if (denom <= 0) { return '—'; }
|
||||
return Math.round(100 * counts.covered / denom) + '%';
|
||||
}
|
||||
|
||||
function renderBreakdown(el, rows) {
|
||||
if (!rows || !rows.length) {
|
||||
el.textContent = 'Nothing to show.';
|
||||
return;
|
||||
}
|
||||
var html = '<table style="width:100%;border-collapse:collapse;">' +
|
||||
'<thead><tr style="text-align:left;">' +
|
||||
'<th>Name</th><th>Covered</th><th>Pending</th><th>Ignored</th><th>Total</th><th>%</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
rows.forEach(function (r) {
|
||||
var c = r.counts;
|
||||
html += '<tr>' +
|
||||
'<td>' + escapeHtml(r.label) + '</td>' +
|
||||
'<td>' + c.covered + '</td>' +
|
||||
'<td>' + c.pending + (c.prioritised ? ' (' + c.prioritised + ' ★)' : '') + '</td>' +
|
||||
'<td>' + c.ignored + '</td>' +
|
||||
'<td>' + c.total + '</td>' +
|
||||
'<td>' + pct(c) + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function loadCoverage() {
|
||||
document.querySelector('#JRayCoverageSummary').textContent = 'Loading…';
|
||||
jrayApi('Coverage').then(function (report) {
|
||||
var t = report.total;
|
||||
document.querySelector('#JRayCoverageSummary').innerHTML =
|
||||
'<strong>' + pct(t) + '</strong> covered — ' +
|
||||
t.covered + ' of ' + (t.total - t.ignored) + ' items processed' +
|
||||
(t.ignored ? ' (' + t.ignored + ' ignored)' : '') + '.';
|
||||
renderBar(document.querySelector('#JRayCoverageBar'), t);
|
||||
document.querySelector('#JRayCoverageLegend').innerHTML =
|
||||
'<span style="color:' + JRayCoverColors.covered + '">■</span> covered ' +
|
||||
'<span style="color:' + JRayCoverColors.prioritised + '">■</span> prioritised ' +
|
||||
'<span style="color:' + JRayCoverColors.pending + '">■</span> pending ' +
|
||||
'<span>▨</span> ignored';
|
||||
renderBreakdown(document.querySelector('#JRayCoverageByType'), report.by_media_type);
|
||||
renderBreakdown(document.querySelector('#JRayCoverageByGenre'), report.by_genre);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Rules ----
|
||||
|
||||
var JRayOptionCache = { Genre: null, Series: null };
|
||||
|
||||
function loadOptionsFor(scope) {
|
||||
if (JRayOptionCache[scope]) {
|
||||
return Promise.resolve(JRayOptionCache[scope]);
|
||||
}
|
||||
var path = scope === 'Series' ? 'Coverage/Series' : 'Coverage/Genres';
|
||||
return jrayApi(path).then(function (opts) {
|
||||
JRayOptionCache[scope] = opts;
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
function fillSelect(select, opts, emptyText) {
|
||||
if (!opts.length) {
|
||||
select.innerHTML = '<option value="">' + emptyText + '</option>';
|
||||
return;
|
||||
}
|
||||
select.innerHTML = opts.map(function (o) {
|
||||
return '<option value="' + escapeHtml(o.value) + '">' + escapeHtml(o.label) + '</option>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function populateValueSelect() {
|
||||
var scope = document.querySelector('#JRayRuleScope').value;
|
||||
var select = document.querySelector('#JRayRuleValue');
|
||||
var isItem = scope === 'Item';
|
||||
document.querySelector('#JRayItemSearchContainer').style.display = isItem ? '' : 'none';
|
||||
|
||||
if (isItem) {
|
||||
// The target dropdown is filled from a live search instead of a full list.
|
||||
select.innerHTML = '<option value="">Type a title to search…</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
select.innerHTML = '<option value="">Loading…</option>';
|
||||
loadOptionsFor(scope).then(function (opts) {
|
||||
fillSelect(select, opts, '(none found)');
|
||||
});
|
||||
}
|
||||
|
||||
var JRayItemSearchTimer = null;
|
||||
function onItemSearch() {
|
||||
var term = document.querySelector('#JRayItemSearch').value.trim();
|
||||
var select = document.querySelector('#JRayRuleValue');
|
||||
clearTimeout(JRayItemSearchTimer);
|
||||
if (!term) {
|
||||
select.innerHTML = '<option value="">Type a title to search…</option>';
|
||||
return;
|
||||
}
|
||||
select.innerHTML = '<option value="">Searching…</option>';
|
||||
JRayItemSearchTimer = setTimeout(function () {
|
||||
jrayApi('Coverage/Items?search=' + encodeURIComponent(term)).then(function (opts) {
|
||||
fillSelect(select, opts, '(no matches)');
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function labelForRule(rule) {
|
||||
if (rule.label) { return rule.label; }
|
||||
var cache = JRayOptionCache[rule.scope];
|
||||
if (cache) {
|
||||
var hit = cache.filter(function (o) { return o.value === rule.value; })[0];
|
||||
if (hit) { return hit.label; }
|
||||
}
|
||||
return rule.value;
|
||||
}
|
||||
|
||||
function loadRules() {
|
||||
jrayApi('Policy/Rules').then(function (rules) {
|
||||
var el = document.querySelector('#JRayRulesList');
|
||||
if (!rules.length) {
|
||||
el.innerHTML = '<p class="fieldDescription">No rules yet.</p>';
|
||||
return;
|
||||
}
|
||||
var html = '<table style="width:100%;border-collapse:collapse;">' +
|
||||
'<thead><tr style="text-align:left;"><th>Action</th><th>Scope</th><th>Target</th><th></th></tr></thead><tbody>';
|
||||
rules.forEach(function (r) {
|
||||
var badge = r.action === 'Prioritise'
|
||||
? '<span style="color:' + JRayCoverColors.prioritised + '">★ Prioritise</span>'
|
||||
: '<span>⦸ Ignore</span>';
|
||||
html += '<tr>' +
|
||||
'<td>' + badge + '</td>' +
|
||||
'<td>' + escapeHtml(r.scope) + '</td>' +
|
||||
'<td>' + escapeHtml(labelForRule(r)) + '</td>' +
|
||||
'<td><button is="emby-button" type="button" class="emby-button jray-del" ' +
|
||||
'data-scope="' + escapeHtml(r.scope) + '" data-value="' + escapeHtml(r.value) + '">Remove</button></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
el.innerHTML = html;
|
||||
|
||||
Array.prototype.forEach.call(el.querySelectorAll('.jray-del'), function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var q = 'Policy/Rules?scope=' + encodeURIComponent(btn.getAttribute('data-scope')) +
|
||||
'&value=' + encodeURIComponent(btn.getAttribute('data-value'));
|
||||
jrayApi(q, 'DELETE').then(function () {
|
||||
loadRules();
|
||||
loadCoverage();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addRule() {
|
||||
var scope = document.querySelector('#JRayRuleScope').value;
|
||||
var valueSelect = document.querySelector('#JRayRuleValue');
|
||||
var value = valueSelect.value;
|
||||
if (!value) { return; }
|
||||
var rule = {
|
||||
scope: scope,
|
||||
value: value,
|
||||
action: document.querySelector('#JRayRuleAction').value,
|
||||
label: valueSelect.options[valueSelect.selectedIndex].text
|
||||
};
|
||||
jrayApi('Policy/Rules', 'PUT', rule).then(function () {
|
||||
loadRules();
|
||||
loadCoverage();
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelector('#JRayConfigPage')
|
||||
.addEventListener('pageshow', function() {
|
||||
Dashboard.showLoadingMsg();
|
||||
@@ -55,8 +339,16 @@
|
||||
document.querySelector('#EnableOverlay').checked = config.EnableOverlay;
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
|
||||
populateValueSelect();
|
||||
loadRules();
|
||||
loadCoverage();
|
||||
});
|
||||
|
||||
document.querySelector('#JRayRuleScope').addEventListener('change', populateValueSelect);
|
||||
document.querySelector('#JRayItemSearch').addEventListener('input', onItemSearch);
|
||||
document.querySelector('#JRayAddRule').addEventListener('click', addRule);
|
||||
|
||||
document.querySelector('#JRayConfigForm')
|
||||
.addEventListener('submit', function(e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
Reference in New Issue
Block a user