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" >
2022-05-05 18:10:34 -05:00
< form id = "FingerprintConfigForm" >
2022-06-09 20:34:18 -05:00
< fieldset class = "verticalSection-extrabottompadding" >
< legend > Analysis< / legend >
< div class = "checkboxContainer checkboxContainer-withDescription" >
< label class = "emby-checkbox-label" >
< input id = "CacheFingerprints" type = "checkbox" is = "emby-checkbox" / >
< span > Cache fingerprints to disk< / span >
< / label >
< div class = "fieldDescription" >
If checked, will store the audio fingerprints for all subsequently scanned files to
2022-06-10 23:08:27 -05:00
disk. Caching fingerprints avoids having to re-run ffmpeg on each file, at the expense
2022-06-09 20:34:18 -05:00
of disk usage.
< / div >
2022-06-07 18:33:59 -05:00
< / div >
2022-05-05 18:10:34 -05:00
2022-06-09 20:34:18 -05:00
< div class = "inputContainer" >
< label class = "inputLabel inputLabelUnfocused" for = "MaxParallelism" >
Maximum degree of parallelism
< / label >
< input id = "MaxParallelism" type = "number" is = "emby-input" min = "1" / >
< div class = "fieldDescription" >
Maximum degree of parallelism to use when analyzing episodes.
< / div >
< / div >
2022-06-27 00:21:30 -05:00
< div class = "inputContainer" >
< label class = "inputLabel inputLabelUnfocused" for = "SelectedLibraries" >
Limit analysis to the following libraries
< / label >
< input id = "SelectedLibraries" type = "text" is = "emby-input" / >
< div class = "fieldDescription" >
Enter the names of libraries to analyze, separated by commas. If this field is left
blank, all libraries on the server containing television episodes will be analyzed.
< / div >
< / div >
2022-07-08 00:57:12 -05:00
2022-07-08 01:02:50 -05:00
< details >
< summary > EDL file generation< / summary >
2022-06-20 01:39:56 -05:00
2022-07-08 01:02:50 -05:00
< div class = "selectContainer" >
2022-07-16 21:53:08 -05:00
< label class = "selectLabel" for = "EdlAction" > EDL Action< / label >
2022-07-08 01:02:50 -05:00
< select is = "emby-select" id = "EdlAction" class = "emby-select-withcolor emby-select" >
< option value = "None" >
None (do not create or modify EDL files)
< / option >
< option value = "CommercialBreak" >
Commercial Break (recommended, skips past the intro once)
< / option >
< option value = "Cut" >
Cut (player will remove the intro from the video)
< / option >
< option value = "Intro" >
Intro (show a skip button, *experimental*)
< / option >
< option value = "Mute" >
Mute (audio will be muted)
< / option >
< option value = "SceneMarker" >
Scene Marker (create a chapter marker)
< / option >
< / select >
< div class = "fieldDescription" >
If set to a value other than None, specifies which action to write to
< a href = "https://kodi.wiki/view/Edit_decision_list" > MPlayer compatible EDL files< / a >
alongside your episode files. < br / >
If this value is changed after EDL files are generated, you must check the "Regenerate EDL files" checkbox below.
< / div >
2022-06-15 01:00:03 -05:00
< / div >
2022-07-08 01:02:50 -05:00
< div class = "checkboxContainer checkboxContainer-withDescription" >
< label class = "emby-checkbox-label" >
< input id = "RegenerateEdl" type = "checkbox" is = "emby-checkbox" / >
< span > Regenerate EDL files during next scan< / span >
< / label >
< div class = "fieldDescription" >
If checked, the plugin will < strong > overwrite all EDL files< / strong > associated with your episodes with the currently discovered introduction timestamps and EDL action.
< / div >
< / div >
< / details >
2022-06-26 22:54:47 -05:00
< details >
< summary > Modify introduction requirements< / summary >
< div class = "inputContainer" >
2022-07-16 21:53:08 -05:00
< label class = "inputLabel inputLabelUnfocused" for = "AnalysisPercent" >
2022-06-26 22:54:47 -05:00
Percent of audio to analyze
< / label >
< input id = "AnalysisPercent" type = "number" is = "emby-input" min = "1" max = "90" / >
< div class = "fieldDescription" >
Analysis will be limited to this percentage of each episode's audio. For example, a
value of 25 (the default) will limit analysis to the first quarter of each episode.
< / div >
< / div >
< div class = "inputContainer" >
2022-07-16 21:53:08 -05:00
< label class = "inputLabel inputLabelUnfocused" for = "AnalysisLengthLimit" >
2022-06-26 22:54:47 -05:00
Maximum runtime of audio to analyze (in minutes)
< / label >
< input id = "AnalysisLengthLimit" type = "number" is = "emby-input" min = "1" / >
< div class = "fieldDescription" >
Analysis will be limited to this amount of each episode's audio. For example, a
value of 10 (the default) will limit analysis to the first 10 minutes of each
episode.
< / div >
< / div >
2022-07-03 01:59:16 -05:00
< div class = "inputContainer" >
2022-07-16 22:12:42 -05:00
< label class = "inputLabel inputLabelUnfocused" for = "MinimumIntroDuration" >
2022-07-03 01:59:16 -05:00
Minimum introduction duration (in seconds)
< / label >
2022-07-16 22:12:42 -05:00
< input id = "MinimumIntroDuration" type = "number" is = "emby-input" min = "1" / >
2022-07-03 01:59:16 -05:00
< div class = "fieldDescription" >
Similar sounding audio which is shorter than this duration will not be considered an
introduction.
< / div >
< / div >
2022-06-26 22:54:47 -05:00
2022-07-03 01:59:16 -05:00
< p >
The amount of each episode's audio that will be analyzed is determined using both
the percentage of audio and maximum runtime of audio to analyze. The minimum of
(episode duration * percent, maximum runtime) is the amount of audio that will
2022-07-08 00:57:12 -05:00
be analyzed.
2022-06-26 22:54:47 -05:00
< / p >
2022-07-08 00:57:12 -05:00
< p >
If the audio percentage or maximum runtime settings are modified, the cached
fingerprints and introduction timestamps for each season you want to analyze with the
modified settings < strong > will have to be deleted.< / strong >
2022-07-03 01:59:16 -05:00
Increasing either of these settings will cause episode analysis to take much longer.
2022-07-08 00:57:12 -05:00
< / p >
2022-06-26 22:54:47 -05:00
< / details >
2022-06-09 20:34:18 -05:00
< / fieldset >
< fieldset class = "verticalSection-extrabottompadding" >
< legend > Playback< / legend >
< div class = "checkboxContainer checkboxContainer-withDescription" >
< label class = "emby-checkbox-label" >
< input id = "AutoSkip" type = "checkbox" is = "emby-checkbox" / >
< span > Automatically skip intros< / span >
< / label >
< div class = "fieldDescription" >
If checked, intros will be automatically skipped. Will only work if web
sockets are configured correctly.< br / >
< / div >
2022-05-05 18:10:34 -05:00
< / div >
2022-05-05 18:26:02 -05:00
2022-06-09 20:34:18 -05:00
< div class = "inputContainer" >
< label class = "inputLabel inputLabelUnfocused" for = "ShowPromptAdjustment" >
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 >
2022-05-05 18:26:02 -05:00
< / div >
2022-06-09 20:34:18 -05:00
< div class = "inputContainer" >
< label class = "inputLabel inputLabelUnfocused" for = "HidePromptAdjustment" >
Hide skip prompt after
< / label >
< input id = "HidePromptAdjustment" type = "number" is = "emby-input" min = "2" / >
< div class = "fieldDescription" >
Seconds after the introduction starts to hide the skip prompt at.
< / div >
2022-05-05 18:26:02 -05:00
< / div >
2022-06-09 20:34:18 -05:00
< / fieldset >
2022-05-05 18:26:02 -05:00
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 >
2022-06-07 12:18:03 -05:00
< br / >
< button id = "btnEraseTimestamps" is = "emby-button" class = "raised block emby-button" >
< span > Erase introduction timestamps< / span >
< / button >
< p >
Erasing introduction timestamps is only necessary after upgrading the plugin if specifically
2022-06-09 20:34:18 -05:00
requested to do so in the plugin's changelog. After the timestamps are erased, run the
Analyze episodes scheduled task to re-analyze all media on the server.
2022-06-07 12:18:03 -05:00
< / p >
2020-07-04 23:33:19 +09:00
< / div >
< / form >
< / div >
2022-05-30 02:23:36 -05:00
2022-07-04 02:03:10 -05:00
< details id = "statistics" >
2022-07-05 16:16:48 -05:00
< summary > Analysis Statistics (experimental)< / summary >
2022-07-04 02:03:10 -05:00
< pre id = "statisticsText" style = "font-size: larger" > < / p >
< / details >
2022-06-29 20:52:16 -05:00
< details id = "visualizer" >
2022-05-31 16:12:11 -05:00
< 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
2022-06-01 21:52:40 -05:00
when the corresponding fingerprint points are at least 80% similar.
2022-05-31 16:12:11 -05:00
< / 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 / >
2022-05-31 16:12:11 -05:00
< span > Shift amount:< / span >
2022-05-30 02:23:36 -05:00
< input type = "number" min = "-3000" max = "3000" value = "0" id = "offset" >
< br / >
2022-07-03 01:47:55 -05:00
< span id = "suggestedShifts" > Suggested shifts:< / span >
< br / >
2022-05-30 02:23:36 -05:00
< br / >
2022-07-03 01:20:33 -05:00
< button id = "btnEraseSeasonTimestamps" type = "button" >
Erase Season Timestamps
< / button >
< br / >
< br / >
2022-05-30 02:23:36 -05:00
< canvas id = "troubleshooter" > < / canvas >
2022-06-01 21:52:40 -05:00
< span id = "timestampContainer" >
< span id = "timestamps" > < / span > < br / >
< span id = "intros" > < / span >
< / span >
2022-05-31 16:12:11 -05:00
< / 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 = [];
2022-05-31 16:12:11 -05:00
2022-06-01 21:52:40 -05:00
// fingerprint point comparison & miminum similarity threshold (at most 6 bits out of 32 can be different)
2022-05-31 16:12:11 -05:00
var fprDiffs = [];
2022-06-01 21:52:40 -05:00
var fprDiffMinimum = (1 - 6 / 32) * 100;
2022-05-30 02:23:36 -05:00
// seasons grouped by show
var shows = {};
2022-07-03 01:20:33 -05:00
// settings elements
2022-06-29 20:52:16 -05:00
var visualizer = document.querySelector("details#visualizer");
2022-07-04 02:03:10 -05:00
var statistics = document.querySelector("details#statistics");
2022-07-03 01:20:33 -05:00
var btnEraseTimestamps = document.querySelector("button#btnEraseTimestamps");
2022-07-16 22:12:42 -05:00
// all plugin configuration fields that can be get or set with .value (i.e. strings or numbers).
var configurationFields = [
"MaxParallelism",
"SelectedLibraries",
"AnalysisPercent",
"AnalysisLengthLimit",
"MinimumIntroDuration",
"EdlAction",
"ShowPromptAdjustment",
"HidePromptAdjustment"
]
2022-07-03 01:20:33 -05:00
// visualizer elements
2022-06-01 23:53:12 -05:00
var canvas = document.querySelector("canvas#troubleshooter");
var selectShow = document.querySelector("select#troubleshooterShow");
var selectSeason = document.querySelector("select#troubleshooterSeason");
var selectEpisode1 = document.querySelector("select#troubleshooterEpisode1");
var selectEpisode2 = document.querySelector("select#troubleshooterEpisode2");
var txtOffset = document.querySelector("input#offset");
2022-07-03 01:47:55 -05:00
var txtSuggested = document.querySelector("span#suggestedShifts");
2022-07-03 01:20:33 -05:00
var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
2022-06-01 23:53:12 -05:00
var timeContainer = document.querySelector("span#timestampContainer");
2022-07-03 01:20:33 -05:00
2022-06-02 01:06:15 -05:00
var windowHashInterval = 0;
2022-05-30 02:23:36 -05:00
2022-06-29 20:52:16 -05:00
// when the fingerprint visualizer opens, populate show names
async function visualizerToggled() {
if (!visualizer.open) {
return;
}
Dashboard.showLoadingMsg();
2022-05-30 02:23:36 -05:00
shows = await getJson("Intros/Shows");
2022-05-31 16:12:11 -05:00
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 = "";
2022-06-29 20:52:16 -05:00
Dashboard.hideLoadingMsg();
2022-05-30 02:23:36 -05:00
}
2022-07-04 02:03:10 -05:00
async function statisticsToggled() {
if (!statistics.open) {
return;
}
// Blank any old statistics
const text = document.querySelector("pre#statisticsText");
2022-07-05 16:16:48 -05:00
text.textContent = "All CPU times are displayed as seconds.\n\n";
2022-07-04 02:03:10 -05:00
Dashboard.showLoadingMsg();
// Load the statistics from the server
let stats = await getJson("Intros/Statistics");
// Select which fields to print and label them with more friendly descriptions
2022-07-05 16:16:48 -05:00
let fields = "TotalCPUTime,FingerprintCPUTime,FirstPassCPUTime,SecondPassCPUTime,IndexSearches," +
"QuickScans,FullScans,TotalQueuedEpisodes";
2022-07-04 02:03:10 -05:00
let friendlyNames = {
2022-07-05 16:16:48 -05:00
TotalCPUTime: "Total CPU time",
FingerprintCPUTime: "Fingerprint CPU time",
FirstPassCPUTime: "First pass CPU time",
SecondPassCPUTime: "Second pass CPU time",
IndexSearches: "Index searches",
2022-07-04 02:03:10 -05:00
QuickScans: "Quick scans",
FullScans: "Full scans",
TotalQueuedEpisodes: "Episodes queued",
};
// Print all friendly names and data points
for (var f of fields.split(",")) {
2022-07-05 16:16:48 -05:00
let name = friendlyNames[f].padEnd(25);
let value = stats[f];
// If this statistic is a measure of CPU time, divide by 1,000 to turn milliseconds into seconds.
2022-07-08 00:57:12 -05:00
if (name.includes("time")) {
2022-07-05 16:16:48 -05:00
value = Math.round(value / 1000);
}
text.textContent += name + value + "\n";
2022-07-04 02:03:10 -05:00
}
2022-07-05 16:16:48 -05:00
// Calculate the percentage of time (to two decimal places) spent waiting for fingerprints
const percentWait = Math.round((stats.FingerprintCPUTime * 10_000) / stats.TotalCPUTime) / 100;
// Breakdown CPU time by analysis component
text.textContent += "\nCPU time breakdown:\n";
text.textContent += "Fingerprint generation " + percentWait + "%\n";
2022-07-04 02:03:10 -05:00
Dashboard.hideLoadingMsg();
}
2022-05-30 02:23:36 -05:00
// 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++;
}
2022-05-31 16:12:11 -05:00
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();
2022-07-03 01:47:55 -05:00
findExactMatches();
2022-05-30 02:23:36 -05:00
}
// 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));
2022-06-01 21:52:40 -05:00
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
2022-06-07 12:18:03 -05:00
async function getJson(url, method = "GET") {
2022-05-30 02:23:36 -05:00
url = ApiClient.serverAddress() + "/" + url;
const reqInit = {
2022-06-07 12:18:03 -05:00
method: method,
2022-05-30 02:23:36 -05:00
headers: {
"Authorization": "MediaBrowser Token=" + ApiClient.accessToken()
}
};
return await
fetch(url, reqInit)
.then(r => {
return r.json();
});
}
2020-07-04 23:33:19 +09:00
2022-05-31 16:12:11 -05: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();
}
2022-06-01 21:52:40 -05:00
function findIntros() {
let times = [];
// get the times of all similar fingerprint points
2022-06-01 23:53:12 -05:00
for (let i in fprDiffs) {
2022-06-01 21:52:40 -05:00
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;
2022-06-01 23:53:12 -05:00
let ranges = [];
2022-06-01 21:52:40 -05:00
2022-06-01 23:53:12 -05:00
for (let t of times) {
2022-06-01 21:52:40 -05:00
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 / > ";
}
}
2022-07-03 01:47:55 -05:00
// find all shifts which align exact matches of audio.
function findExactMatches() {
let shifts = [];
for (let lhsIndex in lhs) {
let lhsPoint = lhs[lhsIndex];
let rhsIndex = rhs.findIndex((x) => x === lhsPoint);
if (rhsIndex === -1) {
continue;
}
let shift = rhsIndex - lhsIndex;
if (shifts.includes(shift)) {
continue;
}
shifts.push(shift);
}
txtSuggested.textContent = "Suggested shifts: ";
if (shifts.length === 0) {
txtSuggested.textContent += "none available";
} else {
2022-07-07 23:31:06 -05:00
shifts.sort((a, b) => { return a - b });
2022-07-03 01:47:55 -05:00
txtSuggested.textContent += shifts.join(", ");
}
}
2022-06-02 01:06:15 -05:00
// check that the user is still on the configuration page
function checkWindowHash() {
2022-06-07 12:18:03 -05:00
const h = location.hash;
if (h === "#!/configurationpage?name=Intro%20Skipper" || h.includes("#!/dialog")) {
2022-06-02 01:06:15 -05:00
return;
}
console.debug("navigated away from intro skipper configuration page");
document.removeEventListener("keydown", keyDown);
clearInterval(windowHashInterval);
}
2022-05-31 16:12:11 -05:00
// 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();
2022-05-31 16:12:11 -05:00
ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
2022-06-07 18:33:59 -05:00
document.querySelector('#AutoSkip').checked = config.AutoSkip;
2022-06-24 00:02:08 -05:00
document.querySelector('#RegenerateEdl').checked = config.RegenerateEdlFiles;
2022-06-09 20:34:18 -05:00
document.querySelector('#CacheFingerprints').checked = config.CacheFingerprints;
2022-07-16 22:12:42 -05:00
for (const field of configurationFields) {
document.querySelector("#" + field).value = config[field];
}
2022-05-05 18:26:02 -05:00
2020-12-02 17:47:22 -07:00
Dashboard.hideLoadingMsg();
2022-05-05 18:10:34 -05:00
});
2021-01-10 00:25:51 +09:00
});
2022-05-05 18:10:34 -05:00
document.querySelector('#FingerprintConfigForm')
2022-06-02 01:06:15 -05:00
.addEventListener('submit', function (e) {
2022-05-30 02:23:36 -05:00
Dashboard.showLoadingMsg();
2022-05-31 16:12:11 -05:00
ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
2022-06-07 18:33:59 -05:00
config.AutoSkip = document.querySelector('#AutoSkip').checked;
2022-06-24 00:02:08 -05:00
config.RegenerateEdlFiles = document.querySelector('#RegenerateEdl').checked;
2022-06-09 20:34:18 -05:00
config.CacheFingerprints = document.querySelector('#CacheFingerprints').checked;
2022-07-16 22:12:42 -05:00
for (const field of configurationFields) {
config[field] = document.querySelector("#" + field).value;
}
2022-05-05 18:26:02 -05:00
2022-06-20 01:39:56 -05:00
ApiClient.updatePluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b", config)
.then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
2020-07-04 23:33:19 +09:00
});
2022-05-30 02:23:36 -05:00
2022-06-02 01:06:15 -05:00
e.preventDefault();
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-06-29 20:52:16 -05:00
visualizer.addEventListener("toggle", visualizerToggled);
2022-07-04 02:03:10 -05:00
statistics.addEventListener("toggle", statisticsToggled);
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);
2022-06-07 12:18:03 -05:00
btnEraseTimestamps.addEventListener("click", (e) => {
Dashboard.confirm(
"Are you sure you want to erase all previously discovered introduction timestamps?",
"Confirm timestamp erasure",
(result) => {
if (!result) {
return;
}
// reset all intro timestamps on the server so a new fingerprint comparison algorithm can be tested
getJson("Intros/EraseTimestamps", "POST");
});
e.preventDefault();
});
2022-07-03 01:20:33 -05:00
btnSeasonEraseTimestamps.addEventListener("click", () => {
const show = selectShow.value;
const season = selectSeason.value;
const url = "Intros/Show/" + encodeURIComponent(show) + "/" + encodeURIComponent(season);
getJson(url, "DELETE");
Dashboard.alert("Erased timestamps for " + season + " of " + show);
});
2022-06-02 01:06:15 -05:00
document.addEventListener("keydown", keyDown);
windowHashInterval = setInterval(checkWindowHash, 2500);
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
}
2022-05-31 16:12:11 -05:00
const diff = fprDiffs[Math.floor(diffPos)];
2022-05-30 02:23:36 -05:00
2022-05-31 16:12:11 -05:00
if (!diff) {
2022-06-01 21:52:40 -05:00
timeContainer.style.display = "none";
2022-05-31 16:12:11 -05:00
return;
} else {
2022-06-01 21:52:40 -05:00
timeContainer.style.display = "unset";
2022-05-31 16:12:11 -05:00
}
2022-06-01 23:53:12 -05:00
const times = document.querySelector("span#timestamps");
2022-05-31 16:12:11 -05:00
// LHS timestamp, RHS timestamp, percent similarity
times.textContent =
secondsToString(lTime) + ", " +
secondsToString(rTime) + ", " +
Math.round(diff) + "%";
2022-05-30 02:23:36 -05:00
2022-06-01 21:52:40 -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
< / script >
< script >
2022-05-31 16:12:11 -05:00
// 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;
}
}
2022-05-31 16:12:11 -05:00
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
2022-05-31 16:12:11 -05:00
fprDiffs = [];
2022-05-30 03:24:19 -05:00
2022-05-31 16:12:11 -05:00
for (let i = 0; i < fp.length ; i + + ) {
let count = 0;
2022-05-30 02:23:36 -05:00
2022-05-31 16:12:11 -05:00
for (let j = 0; j < 32 ; j + + ) {
if (fp[i] & (1 < < j ) ) {
count++;
2022-05-30 02:23:36 -05:00
}
}
2022-05-31 16:12:11 -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) {
2022-06-02 01:06:15 -05:00
if (fp1.length == 0) {
return;
}
2022-05-30 02:23:36 -05:00
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);
2022-05-31 16:12:11 -05:00
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();
2022-05-31 16:12:11 -05:00
// 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
2022-06-01 23:53:12 -05:00
for (let i in fprDiffs) {
2022-05-31 16:12:11 -05:00
const j = Number(i);
const y = Math.abs(offset) + j;
2022-07-03 01:47:55 -05:00
const point = fprDiffs[j];
if (point >= 100) {
ctx.fillStyle = "#002FFF"
} else if (point >= fprDiffMinimum) {
ctx.fillStyle = "#2C92EF";
} else {
ctx.fillStyle = "#EA3535";
}
2022-05-31 16:12:11 -05:00
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 >