1108 lines
52 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">
</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">
2024-03-01 19:56:43 -05:00
<style>
summary {
cursor: pointer;
padding: 10px;
width: inherit;
margin: auto;
border: none;
text-align: center;
outline: none;
font-size: 1.0em;
outline: 2px solid rgba(155, 155, 155, 0.5);
}
</style>
2020-07-04 23:33:19 +09:00
<div class="content-primary">
<form id="FingerprintConfigForm">
<fieldset class="verticalSection-extrabottompadding">
<legend>Analysis</legend>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AutomaticAnalysis" type="checkbox" is="emby-checkbox" />
<span>Automatic analysis</span>
</label>
<div class="fieldDescription">
If checked, newly added items will be automatically analyzed.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
2024-03-04 11:51:55 -05:00
<span>Analyze season 0</span>
</label>
<div class="fieldDescription">
2024-03-04 11:51:55 -05:00
If checked, season 0 (specials / extras) will be included in analysis.
<br />
Note: Shows containing both a specials and extra folder will identify extras as season 0
and ignore specials, regardless of this setting.
</div>
</div>
<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">
2024-03-04 18:23:42 -05:00
Maximum number of simultaneous async episode analysis operations.
</div>
</div>
<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-29 03:34:55 -05:00
<details id="intro_reqs">
2024-03-04 08:20:46 -05:00
<summary>Modify Intro Parameters</summary>
2022-06-26 22:54:47 -05:00
2024-03-01 19:56:43 -05:00
<br />
2022-06-26 22:54:47 -05:00
<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>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MinimumIntroDuration">
Minimum introduction duration (in seconds)
</label>
<input id="MinimumIntroDuration" type="number" is="emby-input" min="1" />
<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-09-03 00:39:35 -05:00
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaximumIntroDuration">
Maximum introduction duration (in seconds)
</label>
<input id="MaximumIntroDuration" type="number" is="emby-input" min="1" />
<div class="fieldDescription">
Similar sounding audio which is longer than this duration will not be considered an
introduction.
</div>
</div>
<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>
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-09-02 01:27:49 -05:00
2024-03-04 07:08:11 -05:00
<details id="edl">
<summary>EDL File Generation</summary>
<br />
<div class="selectContainer">
<label class="selectLabel" for="EdlAction">EDL Action</label>
<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>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="RegenerateEdlFiles" 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-09-02 01:27:49 -05:00
<details id="silence">
2024-03-03 21:46:17 -05:00
<summary>Silence Detection Options</summary>
2022-09-02 01:27:49 -05:00
2024-03-01 19:56:43 -05:00
<br />
2022-09-02 01:27:49 -05:00
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="SilenceDetectionMaximumNoise">
Noise tolerance
</label>
<input id="SilenceDetectionMaximumNoise" type="number" is="emby-input" min="-90"
max="0" />
<div class="fieldDescription">
Noise tolerance in negative decibels.
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="SilenceDetectionMinimumDuration">
Minimum silence duration
</label>
<input id="SilenceDetectionMinimumDuration" type="number" is="emby-input" min="0"
step="0.01" />
<div class="fieldDescription">
Minimum silence duration in seconds before adjusting introduction end time.
</div>
</div>
</details>
2024-03-03 21:46:17 -05:00
<details id="detection">
2024-03-04 07:08:11 -05:00
<summary>Process Configuration</summary>
2024-03-03 21:46:17 -05:00
<br/>
2024-03-04 17:50:12 -05:00
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="UseChromaprint" type="checkbox" is="emby-checkbox" />
<span>Chromaprint analysis</span>
</label>
<div class="fieldDescription">
If checked, analysis will use Chromaprint to compare episode audio and identify intros.
<br />
<strong>WARNING: Disabling this option may result in incomplete or innaccurate analysis!</strong>
<br />
</div>
</div>
2024-03-03 21:46:17 -05:00
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="CacheFingerprints" type="checkbox" is="emby-checkbox" />
<span>Cache episode fingerprints</span>
</label>
<div class="fieldDescription">
If checked, episode fingerprints will be saved on the filesystem to improve analysis speed.
<br />
<strong>WARNING: May result in lengthy detection! Not recommended for large libraries!</strong>
<br />
</div>
</div>
<div class="selectContainer">
<label class="selectLabel" for="ProcessPriority">ffmpeg Priority</label>
<select is="emby-select" id="ProcessPriority" class="emby-select-withcolor emby-select">
<option value="Idle">
Idle
</option>
<option value="BelowNormal">
Below Normal
</option>
<option value="Normal">
Normal
</option>
<option value="AboveNormal">
Above Normal
</option>
<option value="High">
High
</option>
<option value="RealTime">
Highest
</option>
</select>
<div class="fieldDescription">
Sets the relative priority of the analysis ffmpeg process to other parallel operations
(ie. transcoding, chapter detection, etc).
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ProcessThreads">
ffmpeg Threads
</label>
<input id="ProcessThreads" type="number" is="emby-input" min="0"
max="16" />
<div class="fieldDescription">
Number of simultaneous processes to use for ffmpeg operations.
<br />
This value is most often defined as 1 thread per CPU core,
but setting a value of 0 (default) will use the maximum threads available.
</div>
</div>
</details>
</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. If you access Jellyfin through a
reverse proxy, it must be configured to proxy web
sockets.<br />
</div>
</div>
<div id="divSkipFirstEpisode" class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
<span>Ignore intro in the first episode of a season</span>
</label>
<div class="fieldDescription">
If checked, auto skip will ignore introduction in the first episode of a season.<br />
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="SkipButtonVisible" type="checkbox" is="emby-checkbox" />
<span>Show skip intro button</span>
</label>
<div class="fieldDescription">
If checked, a skip button will be displayed at the start of an episode's introduction.
<strong>Only applies to the web interface and compatible applications.</strong>
<br />
</div>
</div>
2024-03-01 09:19:12 -05:00
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="PersistSkipButton" type="checkbox" is="emby-checkbox" />
2024-03-01 14:45:27 -05:00
<span>Display button for intro duration</span>
2024-03-01 09:19:12 -05:00
</label>
<div class="fieldDescription">
If checked, skip button will remain visible throught the intro (offset and timeout are ignored).
<br />
Note: If unchecked, button will only appear in the player controls after the set timeout.
2024-03-01 09:19:12 -05:00
</div>
</div>
<div id="divShowPromptAdjustment" class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ShowPromptAdjustment">
Skip prompt offset (in seoncds)
</label>
<input id="ShowPromptAdjustment" type="number" is="emby-input" min="0" />
<div class="fieldDescription">
Seconds to display skip prompt before introduction begins.
</div>
</div>
2024-04-02 21:09:41 -04:00
<br />
<div id="divHidePromptAdjustment" class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="HidePromptAdjustment">
Skip prompt timeout (in seconds)
</label>
<input id="HidePromptAdjustment" type="number" is="emby-input" min="2" />
<div class="fieldDescription">
Seconds after introduction before skip prompt is hidden.
</div>
</div>
2024-04-02 21:09:41 -04:00
<br />
2022-09-02 01:27:49 -05:00
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="SecondsOfIntroToPlay">
2024-03-01 09:19:12 -05:00
Intro playback duration (in seconds)
2022-09-02 01:27:49 -05:00
</label>
<input id="SecondsOfIntroToPlay" type="number" is="emby-input" min="0" />
<div class="fieldDescription">
2024-03-01 09:19:12 -05:00
Seconds of introduction ending that should be played. Defaults to 2.
2022-09-02 01:27:49 -05:00
</div>
</div>
<details>
<summary>User Interface Customization</summary>
2024-03-01 19:56:43 -05:00
<br />
<div class="inputContainer">
<label class="inputLabel" for="SkipButtonIntroText">
Skip intro button text
</label>
<input id="SkipButtonIntroText" type="text" is="emby-input" />
<div class="fieldDescription">
Text to display in the skip intro button.
</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SkipButtonEndCreditsText">
Skip end credits button text
</label>
<input id="SkipButtonEndCreditsText" type="text" is="emby-input" />
<div class="fieldDescription">
Text to display in the skip end credits button.
</div>
</div>
<div id="divAutoSkipNotificationText" class="inputContainer">
<label class="inputLabel" for="AutoSkipNotificationText">
Automatic skip notification message
</label>
<input id="AutoSkipNotificationText" type="text" is="emby-input" />
<div class="fieldDescription">
Message shown after automatically skipping an introduction. Leave blank to disable notification.
</div>
</div>
</details>
</fieldset>
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>
2022-07-17 02:13:02 -05:00
<br />
2024-03-01 19:56:43 -05:00
<fieldset class="verticalSection-extrabottompadding">
<legend>Advanced</legend>
<details id="support">
2024-03-02 20:30:30 -05:00
<summary>Support Bundle Info</summary>
2024-03-01 19:56:43 -05:00
<textarea id="supportBundle" rows="20" cols="75" readonly></textarea>
</details>
<details id="visualizer">
<summary>Manage Fingerprints</summary>
2024-03-01 19:56:43 -05:00
<br />
<h3 style="margin:0">Select episodes to manage</h3>
<br />
<select id="troubleshooterShow"></select>
<select id="troubleshooterSeason"></select>
<br />
<select id="troubleshooterEpisode1"></select>
<select id="troubleshooterEpisode2"></select>
<br />
<br />
<div id="timestampEditor" style="display:none">
<h3 style="margin:0">Introduction timestamp editor</h3>
<p style="margin:0">All times are in seconds.</p>
<p id="editLeftEpisodeTitle" style="margin-bottom:0"></p>
<input style="width:4em" type="number" min="0" id="editLeftEpisodeStart"> to
<input style="width:4em;margin-bottom:10px" type="number" min="0" id="editLeftEpisodeEnd">
<p id="editRightEpisodeTitle" style="margin-top:0;margin-bottom:0"></p>
<input style="width:4em" type="number" min="0" id="editRightEpisodeStart"> to
<input style="width:4em;margin-bottom:10px" type="number" min="0" id="editRightEpisodeEnd">
<br />
2024-03-05 23:32:51 -05:00
<br />
2024-03-01 19:56:43 -05:00
<button id="btnUpdateTimestamps" type="button">
Update timestamps
</button>
<br />
</div>
2022-05-30 02:23:36 -05:00
<div id="timestampErrorDiv" style="display:none">
<textarea id="timestampError" rows="2" cols="75" readonly></textarea>
<br />
<br />
</div>
2024-03-04 10:41:14 -05:00
2024-03-01 19:56:43 -05:00
<h3>Fingerprint Visualizer</h3>
<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 />
<span>Shift amount:</span>
<input type="number" min="-3000" max="3000" value="0" id="offset">
<br />
<span id="suggestedShifts">Suggested shifts:</span>
<br />
<br />
2024-03-05 23:32:51 -05:00
<canvas id="troubleshooter" style="display:none;"></canvas>
2024-03-01 19:56:43 -05:00
<span id="timestampContainer">
<span id="timestamps"></span> <br />
<span id="intros"></span>
</span>
2024-03-05 23:32:51 -05:00
<br />
<br />
<button id="btnEraseSeasonTimestamps" type="button" style="display:none;">
Erase all timestamps for this season
</button>
<hr />
<button id="btnEraseIntroTimestamps">
Erase all introduction timestamps (globally)
</button>
<br />
<button id="btnEraseCreditTimestamps">
Erase all end credits timestamps (globally)
</button>
2024-03-01 19:56:43 -05:00
</details>
</fieldset>
</div>
2020-07-04 23:33:19 +09:00
</div>
<script src="configurationpage?name=visualizer.js"></script>
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 = {};
2022-07-03 01:20:33 -05:00
// settings elements
2022-06-29 20:52:16 -05:00
var visualizer = document.querySelector("details#visualizer");
var support = document.querySelector("details#support");
var btnEraseIntroTimestamps = document.querySelector("button#btnEraseIntroTimestamps");
var btnEraseCreditTimestamps = document.querySelector("button#btnEraseCreditTimestamps");
2022-07-03 01:20:33 -05:00
// all plugin configuration fields that can be get or set with .value (i.e. strings or numbers).
var configurationFields = [
2022-09-02 01:27:49 -05:00
// analysis
"MaxParallelism",
"SelectedLibraries",
"AnalysisPercent",
"AnalysisLengthLimit",
"MinimumIntroDuration",
2022-09-03 00:39:35 -05:00
"MaximumIntroDuration",
"EdlAction",
2024-03-03 21:46:17 -05:00
"ProcessPriority",
"ProcessThreads",
2022-09-02 01:27:49 -05:00
// playback
"ShowPromptAdjustment",
2022-09-02 01:27:49 -05:00
"HidePromptAdjustment",
"SecondsOfIntroToPlay",
// internals
"SilenceDetectionMaximumNoise",
"SilenceDetectionMinimumDuration",
// UI customization
"SkipButtonIntroText",
"SkipButtonEndCreditsText",
"AutoSkipNotificationText"
]
var booleanConfigurationFields = [
"AutomaticAnalysis",
"AnalyzeSeasonZero",
2022-09-25 00:11:07 -05:00
"RegenerateEdlFiles",
2024-03-04 17:50:12 -05:00
"UseChromaprint",
2024-03-03 21:46:17 -05:00
"CacheFingerprints",
"AutoSkip",
"SkipFirstEpisode",
"PersistSkipButton",
2024-03-01 12:46:23 -05:00
"SkipButtonVisible"
]
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");
var txtSuggested = document.querySelector("span#suggestedShifts");
2022-07-03 01:20:33 -05:00
var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
2024-03-04 10:41:14 -05:00
var timestampError = document.querySelector("textarea#timestampError");
2024-03-05 23:32:51 -05:00
var timestampEditor = document.querySelector("#timestampEditor");
var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
2022-06-01 23:53:12 -05:00
var timeContainer = document.querySelector("span#timestampContainer");
2022-07-03 01:20:33 -05:00
var windowHashInterval = 0;
2022-05-30 02:23:36 -05:00
var autoSkip = document.querySelector("input#AutoSkip");
var skipFirstEpisode = document.querySelector("div#divSkipFirstEpisode");
var autoSkipNotificationText = document.querySelector("div#divAutoSkipNotificationText");
async function autoSkipChanged() {
if (autoSkip.checked) {
skipFirstEpisode.style.display = 'unset';
autoSkipNotificationText.style.display = 'unset';
} else {
skipFirstEpisode.style.display = 'none';
autoSkipNotificationText.style.display = 'none';
}
}
autoSkip.addEventListener("change", autoSkipChanged);
2024-03-01 14:45:27 -05:00
var persistSkip = document.querySelector("input#PersistSkipButton");
var showAdjustment = document.querySelector("div#divShowPromptAdjustment");
var hideAdjustment = document.querySelector("div#divHidePromptAdjustment");
2024-03-01 14:45:27 -05:00
// prevent setting unavailable options
async function persistSkipChanged() {
if (persistSkip.checked) {
showAdjustment.style.display = 'none';
hideAdjustment.style.display = 'none';
} else {
showAdjustment.style.display = 'unset';
hideAdjustment.style.display = 'unset';
}
2024-03-01 14:45:27 -05:00
}
persistSkip.addEventListener("change", persistSkipChanged);
2022-06-29 20:52:16 -05:00
// when the fingerprint visualizer opens, populate show names
async function visualizerToggled() {
if (!visualizer.open) {
return;
}
// ensure the series select is empty
while (selectShow.options.length > 0) {
selectShow.remove(0);
}
2022-06-29 20:52:16 -05:00
Dashboard.showLoadingMsg();
2022-05-30 02:23:36 -05:00
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 = "";
2022-06-29 20:52:16 -05:00
Dashboard.hideLoadingMsg();
2022-05-30 02:23:36 -05:00
}
// fetch the support bundle whenever the detail section is opened.
async function supportToggled() {
if (!support.open) {
return;
}
// Fetch the support bundle
const bundle = await fetchWithAuth("IntroSkipper/SupportBundle", "GET", null);
const bundleText = await bundle.text();
2022-10-26 19:30:21 -05:00
// Display it to the user and select all
const ta = document.querySelector("textarea#supportBundle");
ta.value = bundleText;
ta.focus();
ta.setSelectionRange(0, ta.value.length);
2022-10-26 19:30:21 -05:00
// Attempt to copy it to the clipboard automatically, falling back
// to prompting the user to press Ctrl + C.
try {
2022-10-26 19:30:21 -05:00
navigator.clipboard.writeText(bundleText);
Dashboard.alert("Support bundle copied to clipboard");
} catch {
2022-10-26 19:30:21 -05:00
Dashboard.alert("Press Ctrl+C to copy support bundle");
}
}
2022-05-30 02:23:36 -05:00
// show changed, populate seasons
async function showChanged() {
clearSelect(selectSeason);
2024-03-04 07:08:11 -05:00
btnSeasonEraseTimestamps.style.display = "none";
2024-03-04 10:41:14 -05:00
clearSelect(selectEpisode1);
clearSelect(selectEpisode2);
2022-05-30 02:23:36 -05:00
// 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);
2024-03-05 23:32:51 -05:00
btnSeasonEraseTimestamps.style.display = "unset";
2022-05-30 02:23:36 -05:00
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();
2024-03-04 10:41:14 -05:00
timestampError.value = "";
2024-03-05 23:32:51 -05:00
canvas.style.display = "none";
2024-03-04 10:41:14 -05:00
lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
2024-03-17 19:04:57 -04:00
if (lhs === undefined) {
2024-03-04 11:51:55 -05:00
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints failed!\n";
2024-03-17 19:04:57 -04:00
} else if (lhs === null) {
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints missing!\n";
2024-03-04 10:41:14 -05:00
}
2024-03-05 17:33:51 -05:00
2024-03-17 19:04:57 -04:00
rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint");
2024-03-04 10:41:14 -05:00
if (rhs === undefined) {
2024-03-04 11:51:55 -05:00
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
2024-03-17 19:04:57 -04:00
} else if (rhs === null) {
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints missing!\n";
2024-03-04 10:41:14 -05:00
}
2024-03-05 17:33:51 -05:00
2024-03-04 10:41:14 -05:00
if (timestampError.value == "") {
timestampErrorDiv.style.display = "none";
2024-03-04 10:41:14 -05:00
} else {
timestampErrorDiv.style.display = "unset";
2024-03-04 10:41:14 -05:00
}
2022-05-30 02:23:36 -05:00
Dashboard.hideLoadingMsg();
txtOffset.value = "0";
2022-05-30 02:23:36 -05:00
refreshBounds();
renderTroubleshooter();
findExactMatches();
updateTimestampEditor();
}
// updates the timestamp editor
async function updateTimestampEditor() {
// Get the title and ID of the left and right episodes
const leftEpisode = selectEpisode1.options[selectEpisode1.selectedIndex];
const rightEpisode = selectEpisode2.options[selectEpisode2.selectedIndex];
// Try to get the timestamps of each intro, falling back a default value of zero if no intro was found
2024-03-19 20:58:48 -04:00
let leftEpisodeIntro = await getJson("Episode/" + leftEpisode.value + "/IntroTimestamps/v1");
if (leftEpisodeIntro === null) {
2022-07-18 01:15:33 -05:00
leftEpisodeIntro = { IntroStart: 0, IntroEnd: 0 };
}
2024-03-19 20:58:48 -04:00
let rightEpisodeIntro = await getJson("Episode/" + rightEpisode.value + "/IntroTimestamps/v1");
if (rightEpisodeIntro === null) {
2022-07-18 01:15:33 -05:00
rightEpisodeIntro = { IntroStart: 0, IntroEnd: 0 };
}
// Update the editor for the first and second episodes
2024-03-05 23:32:51 -05:00
timestampEditor.style.display = "unset";
document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text;
document.querySelector("#editLeftEpisodeStart").value = Math.round(leftEpisodeIntro.IntroStart);
document.querySelector("#editLeftEpisodeEnd").value = Math.round(leftEpisodeIntro.IntroEnd);
document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text;
document.querySelector("#editRightEpisodeStart").value = Math.round(rightEpisodeIntro.IntroStart);
document.querySelector("#editRightEpisodeEnd").value = Math.round(rightEpisodeIntro.IntroEnd);
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) {
2024-03-04 10:41:14 -05:00
timestampError.value = "";
timestampErrorDiv.style.display = "none";
2024-03-05 23:32:51 -05:00
timestampEditor.style.display = "none";
timeContainer.style.display = "none";
canvas.style.display = "none";
2022-05-30 02:23:36 -05:00
let i, L = select.options.length - 1;
for (i = L; i >= 0; i--) {
select.remove(i);
}
}
// make an authenticated GET to the server and parse the response as JSON
2022-07-18 00:38:38 -05:00
async function getJson(url) {
2024-03-10 21:31:50 -04:00
return await fetchWithAuth(url, "GET")
.then(r => {
2024-03-19 20:58:48 -04:00
if (r.ok) {
2024-03-10 21:31:50 -04:00
return r.json();
2024-03-19 20:58:48 -04:00
} else {
return null;
2024-03-10 21:31:50 -04:00
}
})
2024-03-17 19:04:57 -04:00
.catch(err => {
console.debug(err);
});
}
// make an authenticated fetch to the server
async function fetchWithAuth(url, method, body) {
2022-05-30 02:23:36 -05:00
url = ApiClient.serverAddress() + "/" + url;
const reqInit = {
method: method,
2022-05-30 02:23:36 -05:00
headers: {
"Authorization": "MediaBrowser Token=" + ApiClient.accessToken()
},
body: body,
2022-05-30 02:23:36 -05:00
};
if (method === "POST") {
reqInit.headers["Content-Type"] = "application/json";
}
return await fetch(url, reqInit);
2022-05-30 02:23:36 -05:00
}
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 (timestampError.value != "") {
// if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1.
offsetDelta = e.ctrlKey ? 10 / 0.128 : 1;
}
break;
case "ArrowUp":
if (timestampError.value != "") {
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();
}
// check that the user is still on the configuration page
function checkWindowHash() {
const h = location.hash;
if (h === "#!/configurationpage?name=Intro%20Skipper" || h.includes("#!/dialog")) {
return;
}
console.debug("navigated away from intro skipper configuration page");
document.removeEventListener("keydown", keyDown);
clearInterval(windowHashInterval);
}
// converts seconds to a readable timestamp (i.e. 127 becomes "02:07").
function secondsToString(seconds) {
return new Date(seconds * 1000).toISOString().substr(14, 5);
}
// erase all intro/credits timestamps
function eraseTimestamps(mode) {
const lower = mode.toLocaleLowerCase();
const title = "Confirm timestamp erasure";
const body = "Are you sure you want to erase all previously discovered " +
mode.toLocaleLowerCase() +
" timestamps?";
Dashboard.confirm(
body,
title,
(result) => {
if (!result) {
return;
}
fetchWithAuth("Intros/EraseTimestamps?mode=" + mode, "POST", null);
Dashboard.alert(mode + " timestamps erased");
});
}
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) {
for (const field of configurationFields) {
document.querySelector("#" + field).value = config[field];
}
for (const field of booleanConfigurationFields) {
document.querySelector("#" + field).checked = config[field];
}
autoSkipChanged();
2024-03-01 14:45:27 -05:00
persistSkipChanged();
2020-12-02 17:47:22 -07:00
Dashboard.hideLoadingMsg();
});
2021-01-10 00:25:51 +09:00
});
document.querySelector('#FingerprintConfigForm')
.addEventListener('submit', function (e) {
2022-05-30 02:23:36 -05:00
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
for (const field of configurationFields) {
config[field] = document.querySelector("#" + field).value;
}
for (const field of booleanConfigurationFields) {
config[field] = document.querySelector("#" + field).checked;
}
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
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);
support.addEventListener("toggle", supportToggled);
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);
btnEraseIntroTimestamps.addEventListener("click", (e) => {
eraseTimestamps("Introduction");
e.preventDefault();
});
btnEraseCreditTimestamps.addEventListener("click", (e) => {
eraseTimestamps("Credits");
e.preventDefault();
});
2022-07-03 01:20:33 -05:00
btnSeasonEraseTimestamps.addEventListener("click", () => {
2022-07-17 02:13:02 -05:00
Dashboard.confirm(
"Are you sure you want to erase all timestamps for this season?",
"Confirm timestamp erasure",
(result) => {
if (!result) {
return;
}
2022-07-03 01:20:33 -05:00
2022-07-17 02:13:02 -05:00
const show = selectShow.value;
const season = selectSeason.value;
2022-07-03 01:20:33 -05:00
2022-07-17 02:13:02 -05:00
const url = "Intros/Show/" + encodeURIComponent(show) + "/" + encodeURIComponent(season);
2022-07-18 00:38:38 -05:00
fetchWithAuth(url, "DELETE", null);
2022-07-17 02:13:02 -05:00
Dashboard.alert("Erased timestamps for " + season + " of " + show);
}
);
2022-07-03 01:20:33 -05:00
});
btnUpdateTimestamps.addEventListener("click", () => {
const lhsId = selectEpisode1.options[selectEpisode1.selectedIndex].value;
const newLhsIntro = {
IntroStart: document.querySelector("#editLeftEpisodeStart").value,
IntroEnd: document.querySelector("#editLeftEpisodeEnd").value,
};
const rhsId = selectEpisode2.options[selectEpisode2.selectedIndex].value;
const newRhsIntro = {
IntroStart: document.querySelector("#editRightEpisodeStart").value,
IntroEnd: document.querySelector("#editRightEpisodeEnd").value,
};
fetchWithAuth("Intros/Episode/" + lhsId + "/UpdateIntroTimestamps", "POST", JSON.stringify(newLhsIntro));
fetchWithAuth("Intros/Episode/" + rhsId + "/UpdateIntroTimestamps", "POST", JSON.stringify(newRhsIntro));
Dashboard.alert("New introduction timestamps saved");
});
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
}
const diff = fprDiffs[Math.floor(diffPos)];
2022-05-30 02:23:36 -05:00
if (!diff) {
timeContainer.style.display = "none";
return;
} else {
timeContainer.style.display = "unset";
}
2022-06-01 23:53:12 -05:00
const times = document.querySelector("span#timestamps");
// 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
</script>
2020-07-04 23:33:19 +09:00
</div>
</body>
2022-05-30 02:23:36 -05:00
2020-07-04 23:33:19 +09:00
</html>