560 lines
22 KiB
HTML
Raw Normal View History

2020-07-04 23:33:19 +09:00
<!DOCTYPE html>
<html lang="en">
2022-05-30 02:23:36 -05:00
2020-07-04 23:33:19 +09:00
<head>
<meta charset="utf-8">
<title>Template</title>
</head>
2022-05-30 02:23:36 -05:00
2020-07-04 23:33:19 +09:00
<body>
2022-05-30 02:23:36 -05:00
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage"
data-require="emby-input,emby-button,emby-select,emby-checkbox">
2020-07-04 23:33:19 +09:00
<div data-role="content">
<div class="content-primary">
<form id="FingerprintConfigForm">
2021-01-10 00:25:51 +09:00
<div class="checkboxContainer checkboxContainer-withDescription">
2020-07-04 23:33:19 +09:00
<label class="emby-checkbox-label">
<input id="CacheFingerprints" type="checkbox" is="emby-checkbox" />
<span>Cache fingerprints to disk</span>
2020-07-04 23:33:19 +09:00
</label>
<div class="fieldDescription">
If checked, will store the fingerprints for all subsequently scanned files to disk.
2022-05-30 02:23:36 -05:00
Caching fingerprints avoids having to re-run fpcalc on each file, at the expense of disk
usage.
</div>
2020-07-04 23:33:19 +09:00
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="AnInteger">Show skip prompt at</label>
<input id="ShowPromptAdjustment" type="number" is="emby-input" min="0" />
<div class="fieldDescription">
Seconds before the introduction starts to display the skip prompt at.
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="AnInteger">Hide skip prompt at</label>
<input id="HidePromptAdjustment" type="number" is="emby-input" min="0" />
<div class="fieldDescription">
Seconds after the introduction starts to hide the skip prompt at.
</div>
</div>
2020-07-04 23:33:19 +09:00
<div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save</span>
</button>
</div>
</form>
</div>
2022-05-30 02:23:36 -05:00
<details>
<summary>Fingerprint Visualizer</summary>
<p>
Interactively compare the audio fingerprints of two episodes. <br />
The blue and red bar to the right of the fingerprint diff turns blue
when the corresponding fingerprint points are at least 80% similar.
</p>
<table>
<thead>
<td style="min-width: 100px; font-weight: bold">Key</td>
<td style="font-weight: bold">Function</td>
</thead>
<tbody>
<tr>
<td>Up arrow</td>
<td>
Shift the left episode up by 0.128 seconds.
Holding control will shift the episode by 10 seconds.
</td>
</tr>
<tr>
<td>Down arrow</td>
<td>
Shift the left episode down by 0.128 seconds.
Holding control will shift the episode by 10 seconds.
</td>
</tr>
<tr>
<td>Right arrow</td>
<td>Advance to the next pair of episodes.</td>
</tr>
<tr>
<td>Left arrow</td>
<td>Go back to the previous pair of episodes.</td>
</tr>
</tbody>
</table>
<br />
2022-05-30 02:23:36 -05:00
<select id="troubleshooterShow"></select>
<select id="troubleshooterSeason"></select>
<br />
<select id="troubleshooterEpisode1"></select>
<select id="troubleshooterEpisode2"></select>
<br />
<span>Shift amount:</span>
2022-05-30 02:23:36 -05:00
<input type="number" min="-3000" max="3000" value="0" id="offset">
<br />
<br />
<canvas id="troubleshooter"></canvas>
<span id="timestampContainer">
<span id="timestamps"></span> <br />
<span id="intros"></span>
</span>
</details>
2020-07-04 23:33:19 +09:00
</div>
2022-05-30 02:23:36 -05:00
<script>
// first and second episodes to fingerprint & compare
var lhs = [];
var rhs = [];
// fingerprint point comparison & miminum similarity threshold (at most 6 bits out of 32 can be different)
var fprDiffs = [];
var fprDiffMinimum = (1 - 6 / 32) * 100;
2022-05-30 02:23:36 -05:00
// seasons grouped by show
var shows = {};
// ui elements
const canvas = document.querySelector("canvas#troubleshooter");
const selectShow = document.querySelector("select#troubleshooterShow");
const selectSeason = document.querySelector("select#troubleshooterSeason");
const selectEpisode1 = document.querySelector("select#troubleshooterEpisode1");
const selectEpisode2 = document.querySelector("select#troubleshooterEpisode2");
const txtOffset = document.querySelector("input#offset");
const timeContainer = document.querySelector("span#timestampContainer");
2022-05-30 02:23:36 -05:00
// config page loaded, populate show names
async function onLoad() {
shows = await getJson("Intros/Shows");
var sorted = [];
for (var series in shows) { sorted.push(series); }
sorted.sort();
for (var show of sorted) {
2022-05-30 02:23:36 -05:00
addItem(selectShow, show, show);
}
selectShow.value = "";
}
// show changed, populate seasons
async function showChanged() {
clearSelect(selectSeason);
// add all seasons from this show to the season select
for (var season of shows[selectShow.value]) {
addItem(selectSeason, season, season);
}
selectSeason.value = "";
}
// season changed, reload all episodes
async function seasonChanged() {
const url = "Intros/Show/" + encodeURI(selectShow.value) + "/" + selectSeason.value;
const episodes = await getJson(url);
clearSelect(selectEpisode1);
clearSelect(selectEpisode2);
let i = 1;
for (let episode of episodes) {
const strI = i.toLocaleString("en", { minimumIntegerDigits: 2, maximumFractionDigits: 0 });
addItem(selectEpisode1, strI + ": " + episode.Name, episode.Id);
addItem(selectEpisode2, strI + ": " + episode.Name, episode.Id);
i++;
}
setTimeout(() => {
selectEpisode1.selectedIndex = 0;
selectEpisode2.selectedIndex = 1;
episodeChanged();
}, 100);
2022-05-30 02:23:36 -05:00
}
// episode changed, get fingerprints & calculate diff
async function episodeChanged() {
if (!selectEpisode1.value || !selectEpisode2.value) {
return;
}
Dashboard.showLoadingMsg();
lhs = await getJson("Intros/Fingerprint/" + selectEpisode1.value);
rhs = await getJson("Intros/Fingerprint/" + selectEpisode2.value);
Dashboard.hideLoadingMsg();
refreshBounds();
renderTroubleshooter();
}
// adds an item to a dropdown
function addItem(select, text, value) {
let item = new Option(text, value);
select.add(item);
}
// clear a select of items
function clearSelect(select) {
let i, L = select.options.length - 1;
for (i = L; i >= 0; i--) {
select.remove(i);
}
}
// re-render the troubleshooter with the latest offset
function renderTroubleshooter() {
paintFingerprintDiff(canvas, lhs, rhs, Number(offset.value));
findIntros();
2022-05-30 02:23:36 -05:00
}
// refresh the upper & lower bounds for the offset
function refreshBounds() {
const len = Math.min(lhs.length, rhs.length) - 1;
offset.min = -1 * len;
offset.max = len;
}
// make an authenticated GET to the server and parse the response as JSON
async function getJson(url) {
url = ApiClient.serverAddress() + "/" + url;
const reqInit = {
headers: {
"Authorization": "MediaBrowser Token=" + ApiClient.accessToken()
}
};
return await
fetch(url, reqInit)
.then(r => {
return r.json();
});
}
2020-07-04 23:33:19 +09:00
// key pressed
function keyDown(e) {
let episodeDelta = 0;
let offsetDelta = 0;
switch (e.key) {
case "ArrowDown":
// if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1.
offsetDelta = e.ctrlKey ? 10 / 0.128 : 1;
break;
case "ArrowUp":
offsetDelta = e.ctrlKey ? -10 / 0.128 : -1;
break;
case "ArrowRight":
episodeDelta = 2;
break;
case "ArrowLeft":
episodeDelta = -2;
break;
default:
return;
}
if (offsetDelta != 0) {
txtOffset.value = Number(txtOffset.value) + Math.floor(offsetDelta);
}
if (episodeDelta != 0) {
// calculate the number of episodes remaining in the LHS and RHS episode pickers
const lhsRemaining = selectEpisode1.selectedIndex;
const rhsRemaining = selectEpisode2.length - selectEpisode2.selectedIndex - 1;
// if we're moving forward and the right episode picker is close to the end, don't move.
if (episodeDelta > 0 && rhsRemaining <= 1) {
return;
} else if (episodeDelta < 0 && lhsRemaining <= 1) {
return;
}
selectEpisode1.selectedIndex += episodeDelta;
selectEpisode2.selectedIndex += episodeDelta;
episodeChanged();
}
renderTroubleshooter();
e.preventDefault();
}
function findIntros() {
let times = [];
// get the times of all similar fingerprint points
for (var i in fprDiffs) {
if (fprDiffs[i] > fprDiffMinimum) {
times.push(i * 0.128);
}
}
// always close the last range
times.push(Number.MAX_VALUE);
let last = times[0];
let start = last;
let end = last;
var ranges = [];
for (var t of times) {
const diff = t - last;
if (diff <= 3.5) {
end = t;
last = t;
continue;
}
const dur = Math.round(end - start);
if (dur >= 15) {
ranges.push({
"start": start,
"end": end,
"duration": dur
});
}
start = t;
end = t;
last = t;
}
const introsLog = document.querySelector("span#intros");
introsLog.style.position = "relative";
introsLog.style.left = "115px";
introsLog.innerHTML = "";
const offset = Number(txtOffset.value) * 0.128;
for (let r of ranges) {
let lStart, lEnd, rStart, rEnd;
if (offset < 0) {
// negative offset, the diff is aligned with the RHS
lStart = r.start - offset;
lEnd = r.end - offset;
rStart = r.start;
rEnd = r.end;
} else {
// positive offset, the diff is aligned with the LHS
lStart = r.start;
lEnd = r.end;
rStart = r.start + offset;
rEnd = r.end + offset;
}
const lTitle = selectEpisode1.options[selectEpisode1.selectedIndex].text;
const rTitle = selectEpisode2.options[selectEpisode2.selectedIndex].text;
introsLog.innerHTML += "<span>" + lTitle + ": " +
secondsToString(lStart) + " - " + secondsToString(lEnd) + "</span> <br />";
introsLog.innerHTML += "<span>" + rTitle + ": " +
secondsToString(rStart) + " - " + secondsToString(rEnd) + "</span> <br />";
}
}
// converts seconds to a readable timestamp (i.e. 127 becomes "02:07").
function secondsToString(seconds) {
return new Date(seconds * 1000).toISOString().substr(14, 5);
}
2020-12-02 17:47:22 -07:00
document.querySelector('#TemplateConfigPage')
2022-05-30 02:23:36 -05:00
.addEventListener('pageshow', function () {
2020-12-02 17:47:22 -07:00
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
document.querySelector('#CacheFingerprints').checked = config.CacheFingerprints;
document.querySelector('#ShowPromptAdjustment').value = config.ShowPromptAdjustment;
document.querySelector('#HidePromptAdjustment').value = config.HidePromptAdjustment;
2020-12-02 17:47:22 -07:00
Dashboard.hideLoadingMsg();
});
2021-01-10 00:25:51 +09:00
});
document.querySelector('#FingerprintConfigForm')
2022-05-30 02:23:36 -05:00
.addEventListener('submit', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
2022-05-30 02:23:36 -05:00
config.CacheFingerprints = document.querySelector('#CacheFingerprints').checked;
2022-05-30 02:23:36 -05:00
config.ShowPromptAdjustment = document.querySelector("#ShowPromptAdjustment").value;
config.HidePromptAdjustment = document.querySelector("#HidePromptAdjustment").value;
ApiClient.updatePluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b", config).then(function (result) {
2022-05-30 02:23:36 -05:00
Dashboard.processPluginConfigurationUpdateResult(result);
});
2020-07-04 23:33:19 +09:00
});
2022-05-30 02:23:36 -05:00
return false;
2020-07-04 23:33:19 +09:00
});
2021-01-10 00:25:51 +09:00
2022-05-30 02:23:36 -05:00
txtOffset.addEventListener("change", renderTroubleshooter);
selectShow.addEventListener("change", showChanged);
selectSeason.addEventListener("change", seasonChanged);
selectEpisode1.addEventListener("change", episodeChanged);
selectEpisode2.addEventListener("change", episodeChanged);
document.addEventListener("keydown", keyDown); // TODO: remove document wide listener when the user exits the page
2022-05-30 02:23:36 -05:00
canvas.addEventListener("mousemove", (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const y = e.clientY - rect.top;
const shift = Number(txtOffset.value);
2022-05-30 03:24:19 -05:00
let lTime, rTime, diffPos;
2022-05-30 02:23:36 -05:00
if (shift < 0) {
lTime = y * 0.128;
rTime = (y + shift) * 0.128;
2022-05-30 03:24:19 -05:00
diffPos = y + shift;
2022-05-30 02:23:36 -05:00
} else {
lTime = (y - shift) * 0.128;
rTime = y * 0.128;
2022-05-30 03:24:19 -05:00
diffPos = y - shift;
2022-05-30 02:23:36 -05:00
}
const diff = fprDiffs[Math.floor(diffPos)];
2022-05-30 02:23:36 -05:00
const times = document.querySelector("span#timestamps");
if (!diff) {
timeContainer.style.display = "none";
return;
} else {
timeContainer.style.display = "unset";
}
// LHS timestamp, RHS timestamp, percent similarity
times.textContent =
secondsToString(lTime) + ", " +
secondsToString(rTime) + ", " +
Math.round(diff) + "%";
2022-05-30 02:23:36 -05:00
timeContainer.style.position = "relative";
timeContainer.style.left = "25px";
timeContainer.style.top = (-1 * rect.height + y).toString() + "px";
2020-07-04 23:33:19 +09:00
});
2022-05-30 02:23:36 -05:00
// TODO: fix
setTimeout(onLoad, 250);
</script>
<script>
// Modified from https://github.com/dnknth/acoustid-match/blob/ffbf21d8c53c40d3b3b4c92238c35846545d3cd7/fingerprints/static/fingerprints/fputils.js
// Originally licensed as MIT.
2022-05-30 02:23:36 -05:00
function renderFingerprintData(ctx, fp, xor = false) {
const pixels = ctx.createImageData(32, fp.length);
let idx = 0;
for (let i = 0; i < fp.length; i++) {
for (let j = 0; j < 32; j++) {
if (fp[i] & (1 << j)) {
pixels.data[idx + 0] = 255;
pixels.data[idx + 1] = 255;
pixels.data[idx + 2] = 255;
} else {
pixels.data[idx + 0] = 0;
pixels.data[idx + 1] = 0;
pixels.data[idx + 2] = 0;
}
pixels.data[idx + 3] = 255;
idx += 4;
}
}
if (!xor) {
return pixels;
}
2022-05-30 03:24:19 -05:00
// if rendering the XOR of the fingerprints, count how many bits are different at each timecode
fprDiffs = [];
2022-05-30 03:24:19 -05:00
for (let i = 0; i < fp.length; i++) {
let count = 0;
2022-05-30 02:23:36 -05:00
for (let j = 0; j < 32; j++) {
if (fp[i] & (1 << j)) {
count++;
2022-05-30 02:23:36 -05:00
}
}
// push the percentage similarity
fprDiffs[i] = 100 - (count * 100) / 32;
2022-05-30 02:23:36 -05:00
}
return pixels;
}
function paintFingerprintDiff(canvas, fp1, fp2, offset) {
let leftOffset = 0, rightOffset = 0;
if (offset < 0) {
leftOffset -= offset;
} else {
rightOffset += offset;
}
let fpDiff = [];
fpDiff.length = Math.min(fp1.length, fp2.length) - Math.abs(offset);
for (let i = 0; i < fpDiff.length; i++) {
fpDiff[i] = fp1[i + leftOffset] ^ fp2[i + rightOffset];
}
const ctx = canvas.getContext('2d');
const pixels1 = renderFingerprintData(ctx, fp1);
const pixels2 = renderFingerprintData(ctx, fp2);
const pixelsDiff = renderFingerprintData(ctx, fpDiff, true);
const border = 4;
canvas.width = pixels1.width + border + // left fingerprint
pixels2.width + border + // right fingerprint
pixelsDiff.width + border // fingerprint diff
+ 4; // if diff[x] >= fprDiffMinimum
2022-05-30 02:23:36 -05:00
canvas.height = Math.max(pixels1.height, pixels2.height) + Math.abs(offset);
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#C5C5C5";
ctx.fill();
// draw left fingerprint
let dx = 0;
ctx.putImageData(pixels1, dx, rightOffset);
dx += pixels1.width + border;
// draw right fingerprint
ctx.putImageData(pixels2, dx, leftOffset);
dx += pixels2.width + border;
// draw fingerprint diff
ctx.putImageData(pixelsDiff, dx, Math.abs(offset));
dx += pixelsDiff.width + border;
// draw the fingerprint diff similarity indicator
// https://davidmathlogic.com/colorblind/#%23EA3535-%232C92EF
for (var i in fprDiffs) {
const j = Number(i);
const y = Math.abs(offset) + j;
ctx.fillStyle = fprDiffs[j] >= fprDiffMinimum ? "#2C92EF" : "#EA3535";
ctx.fillRect(dx, y, 4, 1);
}
2022-05-30 02:23:36 -05:00
}
2020-07-04 23:33:19 +09:00
</script>
</div>
</body>
2022-05-30 02:23:36 -05:00
2020-07-04 23:33:19 +09:00
</html>