1542 lines
89 KiB
HTML
Raw Normal View History

<!doctype html>
2020-07-04 23:33:19 +09:00
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox,emby-linkbutton">
<div data-role="content">
<style>
summary {
cursor: pointer;
padding: 10px;
width: inherit;
margin: auto;
border: none;
text-align: center;
outline: none;
font-size: 1em;
outline: 2px solid rgba(155, 155, 155, 0.5);
}
h3.checkboxListLabel {
font-size: 1em;
margin-bottom: 4px;
}
</style>
<div class="content-primary">
<form id="FingerprintConfigForm">
<fieldset class="verticalSection-extrabottompadding">
<legend>Analysis</legend>
2022-05-30 02:23:36 -05:00
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AutoDetectIntros" type="checkbox" is="emby-checkbox" />
<span>Automatically Scan Intros</span>
</label>
<div class="fieldDescription">If enabled, introductions will be automatically analyzed for new media</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AutoDetectCredits" type="checkbox" is="emby-checkbox" />
<span>Automatically Scan Credits</span>
</label>
<div class="fieldDescription">
If enabled, credits will be automatically analyzed for new media
<br />
<br />
Note: Not selecting at least one automatic detection type will disable automatic scans. To configure the scheduled task, see <a is="emby-linkbutton" class="button-link" href="#/dashboard/tasks">scheduled tasks</a>.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AnalyzeMovies" type="checkbox" is="emby-checkbox" />
<span>Analyze Movies</span>
</label>
<div class="fieldDescription">If checked, movies will be included in analysis.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
<span>Analyze season 0</span>
</label>
<div class="fieldDescription">
If checked, season 0 (specials / extras) will be included in analysis.
<br />
<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">Maximum number of simultaneous async episode analysis operations.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="SelectAllLibraries" type="checkbox" is="emby-checkbox" />
<span>Enable analysis for all libraries containing television episodes</span>
</label>
<div class="folderAccessListContainer">
<div class="folderAccess">
<h3 class="checkboxListLabel">Limit analysis to the following libraries</h3>
<div class="checkboxList paperList" style="padding: 0.5em 1em" id="libraryCheckboxes"></div>
</div>
<label class="inputLabel" for="SelectedLibraries"></label>
<input id="SelectedLibraries" type="hidden" is="emby-input" />
</div>
</div>
2022-07-08 00:57:12 -05:00
<details id="intro_reqs">
<summary>Modify Segment Parameters</summary>
2022-06-26 22:54:47 -05:00
<p>
<strong style="color:orange">Changing segment parameters requires regenerating media segments before changes take effect.</strong>
<br />
Per the jellyfin MediaSegments API, records must be updated individually and may be slow to regenerate.
</p>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="AnalysisPercent"> 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>
2022-06-26 22:54:47 -05:00
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="AnalysisLengthLimit"> 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>
2022-06-26 22:54:47 -05:00
</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
<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>
2022-09-03 00:39:35 -05:00
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MinimumCreditsDuration"> Minimum credits duration (in seconds) </label>
<input id="MinimumCreditsDuration" type="number" is="emby-input" min="1" />
<div class="fieldDescription">Similar sounding audio which is shorter than this duration will not be considered credits.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaximumCreditsDuration"> Maximum credits duration (in seconds) </label>
<input id="MaximumCreditsDuration" type="number" is="emby-input" min="1" />
<div class="fieldDescription">Similar sounding audio which is longer than this duration will not be considered credits.</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 be analyzed.</p>
2024-03-04 07:08:11 -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>
2024-03-04 07:08:11 -05:00
Increasing either of these settings will cause episode analysis to take much longer.
</p>
2024-10-20 07:53:48 -04:00
<div class="inputContainer" id="movieCreditsDuration">
<label class="inputLabel inputLabelUnfocused" for="MaximumMovieCreditsDuration"> Maximum movie credits duration (in seconds) </label>
<input id="MaximumMovieCreditsDuration" type="number" is="emby-input" min="1" />
<div class="fieldDescription">Segments longer than this duration will not be considered movie credits.</div>
<br />
</div>
</details>
2024-03-04 07:08:11 -05:00
<details id="mediasegment">
<summary>Jellyfin MediaSegment Generation</summary>
<br />
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="UpdateMediaSegments" type="checkbox" is="emby-checkbox" />
<span>Update Media Segments for Newly Added Files During Scan</span>
</label>
<div class="fieldDescription">Enable this option to update media segments for newly added files during a scan. <strong>Warning:</strong> This should be disabled if you're using media segment providers other than Intro Skipper.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="RegenerateMediaSegments" type="checkbox" is="emby-checkbox" />
<span>Regenerate All Media Segments on Next Scan</span>
</label>
<div class="fieldDescription">When enabled, this option will <strong>overwrite all existing media segments</strong> for your episodes with currently detected introduction and credit timestamps during the next scan.</div>
</div>
</details>
<details id="edl">
<summary>EDL File Generation</summary>
2024-03-04 07:08:11 -05:00
<br />
<div class="selectContainer">
The EDL file generation has been removed. Please use endrl's <a href="https://github.com/endrl/jellyfin-plugin-edl">EDL plugin</a>.
2022-09-02 01:27:49 -05:00
</div>
</details>
2022-09-02 01:27:49 -05:00
<details id="silence">
<summary>Silence Detection Options</summary>
<br />
<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>
2022-09-02 01:27:49 -05:00
</div>
2024-03-03 21:46:17 -05:00
<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">
<summary>Process Configuration</summary>
2024-03-03 21:46:17 -05:00
<br />
<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: Disabling the cache will cause all libraries to be re-scanned, which can take a very long time!</strong>
<br />
</div>
2024-03-03 21:46:17 -05:00
</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>
2024-03-03 21:46:17 -05:00
<option value="BelowNormal">Below Normal</option>
2024-03-03 21:46:17 -05:00
<option value="Normal">Normal</option>
2024-03-03 21:46:17 -05:00
<option value="AboveNormal">Above Normal</option>
2024-03-03 21:46:17 -05:00
<option value="High">High</option>
2024-03-03 21:46:17 -05:00
<option value="RealTime">Highest</option>
</select>
2024-03-03 21:46:17 -05:00
<div class="fieldDescription">Sets the relative priority of the analysis ffmpeg process to other parallel operations (ie. transcoding, chapter detection, etc).</div>
2024-03-03 21:46:17 -05:00
</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>
2024-03-03 21:46:17 -05:00
</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>
2024-05-13 14:25:52 +02:00
<div class="fieldDescription">
If checked, intros will be automatically skipped for <strong>all</strong> clients. Note: Clients cannot disable this setting from the player popup (gear icon).<br />
If you access Jellyfin through a reverse proxy, it must be configured to proxy websockets.<br />
</div>
</div>
2024-05-13 14:25:52 +02:00
<div id="divSkipFirstEpisode" class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
<span>Play intro for first episode of a season</span>
</label>
<div class="fieldDescription">If checked, auto skip will play the introduction of the first episode in a season.<br /></div>
<br />
</div>
<div id="divSecondsOfIntroStartToPlay" class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="SecondsOfIntroStartToPlay"> Intro skip delay (in seconds) </label>
<input id="SecondsOfIntroStartToPlay" type="number" is="emby-input" min="0" />
<div class="fieldDescription">Seconds of introduction start that should be played. Defaults to 0.</div>
<br />
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
2024-10-09 06:42:51 -04:00
<label class="emby-checkbox-label">
<input id="AutoSkipCredits" type="checkbox" is="emby-checkbox" />
<span>Automatically skip credits</span>
2024-10-09 06:42:51 -04:00
</label>
<div class="fieldDescription">
If checked, credits will be automatically skipped for <strong>all</strong> clients. Note: Clients cannot disable this setting from the player popup (gear icon).<br />
If you access Jellyfin through a reverse proxy, it must be configured to proxy websockets.<br />
2024-10-09 06:42:51 -04:00
</div>
</div>
<div id="divSecondsOfCreditsStartToPlay" class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="SecondsOfCreditsStartToPlay"> Credit skip delay (in seconds) </label>
<input id="SecondsOfCreditsStartToPlay" type="number" is="emby-input" min="0" />
<div class="fieldDescription">Seconds of credits start that should be played. Defaults to 0.</div>
2024-10-09 06:42:51 -04:00
<br />
</div>
2024-10-12 06:35:46 -04:00
<details id="AutoSkipClientList" style="padding-bottom: 1em">
<summary>Auto Skip Client List</summary>
<br />
<div class="checkboxList paperList" style="padding: 0.5em 1em" id="autoSkipCheckboxes"></div>
<label class="inputLabel" for="ClientList"></label>
<input id="ClientList" type="hidden" is="emby-input" />
</details>
<div id="SkipButtonContainer" class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="SkipButtonVisible" type="checkbox" is="emby-checkbox" />
<span id="SkipButtonVisibleLabel">Show skip intro / credit button</span>
2024-10-09 06:42:51 -04:00
</label>
2024-10-09 06:42:51 -04:00
<div class="fieldDescription">
2024-10-12 06:35:46 -04:00
(<strong>Restart required!</strong>) If checked, a skip button will be displayed according to the settings below, while clients selected in the Auto Skip Client List will still skip <strong>automatically</strong>.
<br />
2024-10-09 06:42:51 -04:00
</div>
</div>
<div id="warningMessage" style="color: #721c24; background-color: #f7cf1f; border: 1px solid #f5c6cb; border-radius: 4px; padding: 10px; margin-bottom: 10px">Failed to add skip button to web interface. See <a href="https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting#skip-button-is-not-visible" target="_blank" rel="noopener noreferrer">troubleshooting guide</a> for the most common issues.</div>
2022-09-02 01:27:49 -05:00
<div id="SkipButtonSettings">
<div id="PersistContainer" class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="PersistSkipButton" type="checkbox" is="emby-checkbox" />
<span>Display button for intro duration</span>
</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.
</div>
</div>
<div id="divShowPromptAdjustment" class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ShowPromptAdjustment"> Skip prompt offset (in seconds) </label>
<input id="ShowPromptAdjustment" type="number" is="emby-input" min="0" />
<div class="fieldDescription">Seconds to display skip prompt before introduction begins.</div>
<br />
</div>
<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>
<br />
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="RemainingSecondsOfIntro"> Intro playback duration (in seconds) </label>
<input id="RemainingSecondsOfIntro" type="number" is="emby-input" min="0" />
<div class="fieldDescription">Seconds of introduction ending that should be played. Defaults to 2.</div>
</div>
<details>
<summary>User Interface Customization</summary>
<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>
2024-05-13 14:25:52 +02:00
<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>
2024-05-13 14:25:52 +02:00
</div>
<div id="divAutoSkipNotificationText" class="inputContainer">
<label class="inputLabel" for="AutoSkipNotificationText"> Auto skip intro 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>
2022-07-17 02:13:02 -05:00
<div id="divAutoSkipCreditsNotificationText" class="inputContainer">
<label class="inputLabel" for="AutoSkipCreditsNotificationText"> Auto skip credits notification message </label>
<input id="AutoSkipCreditsNotificationText" type="text" is="emby-input" />
<div class="fieldDescription">Message shown after automatically skipping credits. Leave blank to disable notification.</div>
</div>
</details>
</fieldset>
2024-03-01 19:56:43 -05:00
<div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save</span>
</button>
</div>
<br />
2024-03-01 19:56:43 -05:00
<fieldset class="verticalSection-extrabottompadding">
<legend>Advanced</legend>
2024-03-01 19:56:43 -05:00
<details id="support">
<summary>Support Bundle Info</summary>
2024-03-01 19:56:43 -05:00
<textarea id="supportBundle" rows="20" cols="75" readonly></textarea>
</details>
2024-03-01 19:56:43 -05:00
<details id="visualizer">
<summary>Manage Timestamps & Fingerprints</summary>
<br />
<label class="inputLabel" for="troubleshooterShow">Select TV series / movie to manage</label>
<select is="emby-select" id="troubleshooterShow" class="emby-select-withcolor emby-select"></select>
<div id="seasonSelection">
<label class="inputLabel" for="troubleshooterSeason">Select season to manage</label>
<select is="emby-select" id="troubleshooterSeason" class="emby-select-withcolor emby-select"></select>
</div>
<br />
<div id="ignorelistSection" style="display: none">
<h3 style="margin: 0">Ignore list editor</h3>
<p style="margin: 0">
Add or remove items from the ignore list. Items on the ignore list will not be analyzed.<br />
You can apply the changes for the entire series or just the selected season.
</p>
<br />
2024-03-01 19:56:43 -05:00
<div id="ignoreListCheckboxContainer">
<label for="ignorelistIntro" style="margin-right: 1.5em; display: inline-block">
<span>Ignore intros</span>
<input type="checkbox" id="ignorelistIntro" />
</label>
<label for="ignorelistCredits" style="margin-right: 1.5em; display: inline-block">
<span>Ignore credits</span>
<input type="checkbox" id="ignorelistCredits" />
</label>
</div>
<br />
<button is="emby-button" id="saveIgnoreListSeries" class="raised button-submit block emby-button">Apply to series / movie</button>
<button is="emby-button" id="saveIgnoreListSeason" class="raised button-submit block emby-button" style="display: none">Apply to season</button>
2024-10-06 12:13:30 +02:00
</div>
<br />
<div id="episodeSelection">
<label class="inputLabel" for="troubleshooterEpisode1">Select the first episode</label>
<select is="emby-select" id="troubleshooterEpisode1" class="emby-select-withcolor emby-select"></select>
<label class="inputLabel" for="troubleshooterEpisode1">Select the second episode</label>
<select is="emby-select" id="troubleshooterEpisode2" class="emby-select-withcolor emby-select"></select>
<br />
</div>
<div id="timestampEditor" style="display: none">
<h3 style="margin: 0">Introduction timestamp editor</h3>
<br />
<h4 style="margin: 0" id="editLeftEpisodeTitle"></h4>
<br />
<div class="inlineForm">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="introStart">Intro Start</label>
<input type="text" id="editLeftIntroEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
<input type="number" id="editLeftIntroEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="introEnd">Intro End</label>
<input type="text" id="editLeftIntroEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
<input type="number" id="editLeftIntroEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
</div>
</div>
<div class="inlineForm">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="creditsStart">Credits Start</label>
<input type="text" id="editLeftCreditEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
<input type="number" id="editLeftCreditEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="creditsEnd">Credits End</label>
<input type="text" id="editLeftCreditEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
<input type="number" id="editLeftCreditEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
</div>
</div>
<br />
<div id="rightEpisodeEditor">
<h4 style="margin: 0" id="editRightEpisodeTitle"></h4>
<br />
<div class="inlineForm">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="introStart">Intro Start</label>
<input type="text" id="editRightIntroEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
<input type="number" id="editRightIntroEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="introEnd">Intro End</label>
<input type="text" id="editRightIntroEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
<input type="number" id="editRightIntroEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
</div>
</div>
<div class="inlineForm">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="creditsStart">Credits Start</label>
<input type="text" id="editRightCreditEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
<input type="number" id="editRightCreditEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="creditsEnd">Credits End</label>
<input type="text" id="editRightCreditEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
<input type="number" id="editRightCreditEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
</div>
</div>
<br />
</div>
<button is="emby-button" id="btnUpdateTimestamps" class="raised button-submit block emby-button" type="button">Update timestamps</button>
<br />
2024-10-06 12:13:30 +02:00
</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
<div id="fingerprintVisualizer">
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>
<tr>
<td style="min-width: 100px; font-weight: bold">Key</td>
<td style="font-weight: bold">Function</td>
</tr>
</thead>
<tbody>
<tr>
<td>Up arrow</td>
<td>Shift the left episode up by 0.1238 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.1238 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 />
2024-03-01 19:56:43 -05:00
<span>Shift amount:</span>
<input type="number" min="-3000" max="3000" value="0" id="offset" />
<br />
<span id="suggestedShifts">
<span>Suggested shifts:</span>
<br />
<br />
</span>
<canvas id="troubleshooter" style="display: none"></canvas>
<span id="timestampContainer">
<span id="timestamps"></span> <br />
<span id="intros"></span>
</span>
</div>
<br />
<br />
2024-03-05 23:32:51 -05:00
<div id="eraseSeasonContainer" style="display: none">
<button is="emby-button" id="btnEraseSeasonTimestamps" class="button-submit emby-button" type="button">Erase all timestamps for this season</button>
<input type="checkbox" id="eraseSeasonCacheCheckbox" style="margin-left: 10px" />
<label for="eraseSeasonCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
<br />
</div>
2024-03-05 23:32:51 -05:00
<div id="eraseMovieContainer" style="display: none">
<button is="emby-button" id="btnEraseMovieTimestamps" class="button-submit emby-button" type="button">Erase all timestamps for this movie</button>
2024-03-05 23:32:51 -05:00
<input type="checkbox" id="eraseMovieCacheCheckbox" style="margin-left: 10px" />
<label for="eraseMovieCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
<br />
</div>
<button is="emby-button" class="button-submit emby-button" id="btnEraseIntroTimestamps">Erase all introduction timestamps (globally)</button>
<br />
<button is="emby-button" class="button-submit emby-button" id="btnEraseCreditTimestamps">Erase all end credits timestamps (globally)</button>
<br />
<br />
<input type="checkbox" id="eraseModeCacheCheckbox" style="margin-left: 10px" />
<label for="eraseModeCacheCheckbox" style="margin-left: 5px">Erase cached fingerprint files</label>
<br />
<br />
</details>
<details id="storage">
<br />
<summary>Storage Usage</summary>
<div class="fieldDescription">See how much space each library uses.</div>
<textarea id="storage" rows="20" cols="75" readonly></textarea>
</details>
</fieldset>
</form>
</div>
2024-03-01 19:56:43 -05:00
</div>
<script src="configurationpage?name=visualizer.js"></script>
<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;
// seasons grouped by show
var shows = {};
var IgnoreListSeasonId;
// settings elements
var visualizer = document.querySelector("details#visualizer");
var support = document.querySelector("details#support");
var storage = document.querySelector("details#storage");
var btnEraseIntroTimestamps = document.querySelector("button#btnEraseIntroTimestamps");
var btnEraseCreditTimestamps = document.querySelector("button#btnEraseCreditTimestamps");
// all plugin configuration fields that can be get or set with .value (i.e. strings or numbers).
var configurationFields = [
// analysis
"MaxParallelism",
"SelectedLibraries",
"ClientList",
"AnalysisPercent",
"AnalysisLengthLimit",
"MinimumIntroDuration",
"MaximumIntroDuration",
"MinimumCreditsDuration",
"MaximumCreditsDuration",
"MaximumMovieCreditsDuration",
"ProcessPriority",
"ProcessThreads",
// playback
"ShowPromptAdjustment",
"HidePromptAdjustment",
"RemainingSecondsOfIntro",
"SecondsOfIntroStartToPlay",
"SecondsOfCreditsStartToPlay",
// internals
"SilenceDetectionMaximumNoise",
"SilenceDetectionMinimumDuration",
// UI customization
"SkipButtonIntroText",
"SkipButtonEndCreditsText",
"AutoSkipNotificationText",
"AutoSkipCreditsNotificationText",
];
var booleanConfigurationFields = ["AutoDetectIntros", "AutoDetectCredits", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "UpdateMediaSegments", "RegenerateMediaSegments", "CacheFingerprints", "AutoSkip", "AutoSkipCredits", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonVisible"];
// visualizer elements
var ignorelistSection = document.querySelector("div#ignorelistSection");
var ignorelistIntro = ignorelistSection.querySelector("input#ignorelistIntro");
var ignorelistCredits = ignorelistSection.querySelector("input#ignorelistCredits");
var saveIgnoreListSeasonButton = ignorelistSection.querySelector("button#saveIgnoreListSeason");
var saveIgnoreListSeriesButton = ignorelistSection.querySelector("button#saveIgnoreListSeries");
var canvas = document.querySelector("canvas#troubleshooter");
var selectShow = document.querySelector("select#troubleshooterShow");
var seasonSelection = document.getElementById("seasonSelection");
var selectSeason = document.querySelector("select#troubleshooterSeason");
var episodeSelection = document.getElementById("episodeSelection");
var selectEpisode1 = document.querySelector("select#troubleshooterEpisode1");
var selectEpisode2 = document.querySelector("select#troubleshooterEpisode2");
var txtOffset = document.querySelector("input#offset");
var txtSuggested = document.querySelector("span#suggestedShifts");
var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
var eraseSeasonContainer = document.getElementById("eraseSeasonContainer");
var btnMovieEraseTimestamps = document.querySelector("button#btnEraseMovieTimestamps");
var eraseMovieContainer = document.getElementById("eraseMovieContainer");
var timestampError = document.querySelector("textarea#timestampError");
var timestampEditor = document.querySelector("#timestampEditor");
var rightEpisodeEditor = document.getElementById("rightEpisodeEditor");
var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
var timeContainer = document.querySelector("span#timestampContainer");
var fingerprintVisualizer = document.getElementById("fingerprintVisualizer");
var windowHashInterval = 0;
var analyzeMovies = document.getElementById("AnalyzeMovies");
var autoSkip = document.querySelector("input#AutoSkip");
var skipButtonVisible = document.getElementById("SkipButtonVisible");
var skipButtonVisibleLabel = document.getElementById("SkipButtonVisibleLabel");
var skipButtonSettings = document.getElementById("SkipButtonSettings");
var selectAllLibraries = document.querySelector("input#SelectAllLibraries");
var librariesContainer = document.querySelector("div.folderAccessListContainer");
var skipFirstEpisode = document.querySelector("div#divSkipFirstEpisode");
var secondsOfIntroStartToPlay = document.querySelector("div#divSecondsOfIntroStartToPlay");
var autoSkipClientList = document.getElementById("AutoSkipClientList");
var secondsOfCreditsStartToPlay = document.querySelector("div#divSecondsOfCreditsStartToPlay");
var movieCreditsDuration = document.getElementById("movieCreditsDuration");
var autoSkipNotificationText = document.querySelector("div#divAutoSkipNotificationText");
var autoSkipCredits = document.querySelector("input#AutoSkipCredits");
var autoSkipCreditsNotificationText = document.querySelector("div#divAutoSkipCreditsNotificationText");
function skipButtonVisibleChanged() {
if (autoSkip.checked && autoSkipCredits.checked) {
skipButtonSettings.style.display = "none";
} else if (skipButtonVisible.checked) {
skipButtonSettings.style.display = "unset";
} else {
skipButtonSettings.style.display = "none";
}
}
skipButtonVisible.addEventListener("change", skipButtonVisibleChanged);
function skipButtonVisibleText() {
if (autoSkip.checked && autoSkipCredits.checked) {
autoSkipClientList.style.display = "none";
skipButtonVisibleLabel.textContent = "Button unavailable due to auto skip";
} else if (autoSkip.checked) {
autoSkipClientList.style.display = "unset";
autoSkipClientList.style.width = "100%";
skipButtonVisibleLabel.textContent = "Show skip credit button";
} else if (autoSkipCredits.checked) {
autoSkipClientList.style.display = "unset";
autoSkipClientList.style.width = "100%";
skipButtonVisibleLabel.textContent = "Show skip intro button";
} else {
autoSkipClientList.style.display = "unset";
autoSkipClientList.style.width = "100%";
skipButtonVisibleLabel.textContent = "Show skip intro / credit button";
}
skipButtonVisibleChanged();
}
function autoSkipChanged() {
if (autoSkip.checked) {
skipFirstEpisode.style.display = "unset";
autoSkipNotificationText.style.display = "unset";
secondsOfIntroStartToPlay.style.display = "unset";
} else {
skipFirstEpisode.style.display = "none";
autoSkipNotificationText.style.display = "none";
secondsOfIntroStartToPlay.style.display = "none";
}
skipButtonVisibleText();
}
autoSkip.addEventListener("change", autoSkipChanged);
function autoSkipCreditsChanged() {
if (autoSkipCredits.checked) {
autoSkipCreditsNotificationText.style.display = "unset";
secondsOfCreditsStartToPlay.style.display = "unset";
} else {
autoSkipCreditsNotificationText.style.display = "none";
secondsOfCreditsStartToPlay.style.display = "none";
}
skipButtonVisibleText();
2024-05-13 14:25:52 +02:00
}
autoSkipCredits.addEventListener("change", autoSkipCreditsChanged);
2024-05-13 14:25:52 +02:00
skipButtonVisibleText(); // run once on launch for legacy installs
function selectAllLibrariesChanged() {
if (selectAllLibraries.checked) {
librariesContainer.style.display = "none";
} else {
librariesContainer.style.display = "unset";
}
}
2024-03-01 14:45:27 -05:00
selectAllLibraries.addEventListener("change", selectAllLibrariesChanged);
2024-03-01 14:45:27 -05:00
function updateList(textField, container) {
textField.value = Array.from(container.querySelectorAll('input[type="checkbox"]:checked'))
.map((checkbox) => checkbox.nextElementSibling.textContent)
.join(", ");
2022-06-29 20:52:16 -05:00
}
function generateCheckboxList(items, containerId, textFieldId) {
const container = document.getElementById(containerId);
const checkedItems = new Set(document.getElementById(textFieldId).value.split(", ").filter(Boolean));
const fragment = document.createDocumentFragment();
items.forEach((item) => {
const label = document.createElement("label");
label.className = "emby-checkbox-label";
label.innerHTML = '<input type="checkbox" is="emby-checkbox"' + (checkedItems.has(item) ? " checked" : "") + ">" + '<span class="checkboxLabel">' + item + "</span>";
fragment.appendChild(label);
});
container.innerHTML = "";
container.appendChild(fragment);
container.addEventListener(
"change",
(e) => {
if (e.target.type === "checkbox") updateList(document.getElementById(textFieldId), container);
},
{ passive: true },
);
}
async function generateAutoSkipClientList() {
const response = await getJson("Devices");
const devices = [...new Set(response.Items.map((item) => item.AppName))];
generateCheckboxList(devices, "autoSkipCheckboxes", "ClientList");
}
2022-05-30 02:23:36 -05:00
async function populateLibraries() {
const response = await getJson("Library/VirtualFolders");
const tvLibraries = response.filter((item) => item.CollectionType === undefined || item.CollectionType === "tvshows" || item.CollectionType === "movies");
const libraryNames = tvLibraries.map((lib) => lib.Name || "Unnamed Library");
generateCheckboxList(libraryNames, "libraryCheckboxes", "SelectedLibraries");
}
var persistSkip = document.querySelector("input#PersistSkipButton");
var showAdjustment = document.querySelector("div#divShowPromptAdjustment");
var hideAdjustment = document.querySelector("div#divHidePromptAdjustment");
// 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";
}
2022-05-30 02:23:36 -05:00
}
persistSkip.addEventListener("change", persistSkipChanged);
2022-06-29 20:52:16 -05:00
async function analyzeMoviesChanged() {
if (analyzeMovies.checked) {
movieCreditsDuration.style.display = "unset";
} else {
movieCreditsDuration.style.display = "none";
}
}
analyzeMovies.addEventListener("change", analyzeMoviesChanged);
// when the fingerprint visualizer opens, populate show names
async function visualizerToggled() {
if (!visualizer.open) {
ignorelistSection.style.display = "none";
saveIgnoreListSeasonButton.style.display = "none";
return;
}
// ensure the series select is empty
selectShow.innerHTML = "";
Dashboard.showLoadingMsg();
shows = await getJson("Intros/Shows");
2022-05-30 02:23:36 -05:00
// Create an object to store shows by library
let showsByLibrary = {};
// Categorize shows by LibraryName
for (const show in shows) {
const libraryName = shows[show].LibraryName || "Uncategorized";
if (!showsByLibrary[libraryName]) {
showsByLibrary[libraryName] = [];
}
showsByLibrary[libraryName].push({
value: show,
text: shows[show].SeriesName + " (" + shows[show].ProductionYear + ")",
});
}
// Add categorized shows to the select element
for (const library in showsByLibrary) {
const optgroup = document.createElement("optgroup");
optgroup.label = library;
showsByLibrary[library].forEach(function (show) {
const option = document.createElement("option");
option.value = show.value;
option.textContent = show.text;
optgroup.appendChild(option);
});
selectShow.appendChild(optgroup);
}
selectShow.value = "";
Dashboard.hideLoadingMsg();
}
// 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();
// 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);
// Attempt to copy it to the clipboard automatically, falling back
// to prompting the user to press Ctrl + C.
try {
navigator.clipboard.writeText(bundleText);
Dashboard.alert("Support bundle copied to clipboard");
} catch {
Dashboard.alert("Press Ctrl+C to copy support bundle");
}
}
// fetch the storage whenever the detail section is opened.
async function storageToggled() {
if (!storage.open) {
return;
}
// Fetch the support bundle
const bundle = await fetchWithAuth("IntroSkipper/Storage", "GET", null);
const bundleText = await bundle.text();
// Display it to the user
const ta = document.querySelector("textarea#storage");
ta.value = bundleText;
2022-05-30 02:23:36 -05:00
}
// show changed, populate seasons
async function showChanged() {
seasonSelection.style.display = "unset";
clearSelect(selectSeason);
eraseSeasonContainer.style.display = "none";
eraseMovieContainer.style.display = "none";
episodeSelection.style.display = "unset";
clearSelect(selectEpisode1);
clearSelect(selectEpisode2);
// show the ignore list editor.
Dashboard.showLoadingMsg();
const IgnoreList = await getJson("Intros/IgnoreListSeries/" + encodeURI(selectShow.value));
ignorelistIntro.checked = IgnoreList.IgnoreIntro;
ignorelistCredits.checked = IgnoreList.IgnoreCredits;
ignorelistSection.style.display = "unset";
saveIgnoreListSeasonButton.style.display = "none";
Dashboard.hideLoadingMsg();
if (shows[selectShow.value].IsMovie) {
movieLoaded();
return;
}
2024-10-20 07:53:48 -04:00
saveIgnoreListSeriesButton.textContent = "Apply to series";
// add all seasons from this show to the season select
for (const season in shows[selectShow.value].Seasons) {
addItem(selectSeason, "Season " + shows[selectShow.value].Seasons[season], season);
}
selectSeason.value = "";
2022-05-30 02:23:36 -05:00
}
// season changed, reload all episodes
async function seasonChanged() {
const seasonData = encodeURI(selectShow.value) + "/" + encodeURI(selectSeason.value);
Dashboard.showLoadingMsg();
// show the ignore list editor.
saveIgnoreListSeasonButton.style.display = "block";
const IgnoreList = await getJson("Intros/IgnoreListSeason/" + encodeURI(selectSeason.value));
ignorelistIntro.checked = IgnoreList.IgnoreIntro;
ignorelistCredits.checked = IgnoreList.IgnoreCredits;
IgnoreListSeasonId = IgnoreList.SeasonId;
// show the erase season button
eraseSeasonContainer.style.display = "unset";
clearSelect(selectEpisode1);
clearSelect(selectEpisode2);
let i = 1;
const episodes = await getJson("Intros/Show/" + seasonData);
for (const episode in episodes) {
const strI = i.toLocaleString("en", { minimumIntegerDigits: 2, maximumFractionDigits: 0 });
addItem(selectEpisode1, strI + ": " + episodes[episode].Name, episodes[episode].Id);
addItem(selectEpisode2, strI + ": " + episodes[episode].Name, episodes[episode].Id);
i++;
}
Dashboard.hideLoadingMsg();
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();
timestampError.value = "";
fingerprintVisualizer.style.display = "unset";
canvas.style.display = "none";
lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
if (lhs === undefined) {
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints failed!\n";
} else if (lhs === null) {
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints missing!\n";
}
rightEpisodeEditor.style.display = "unset";
rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint");
if (rhs === undefined) {
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
} else if (rhs === null) {
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints missing!\n";
}
if (timestampError.value == "") {
timestampErrorDiv.style.display = "none";
} else {
timestampErrorDiv.style.display = "unset";
}
2022-05-30 02:23:36 -05:00
Dashboard.hideLoadingMsg();
2024-03-04 10:41:14 -05:00
txtOffset.value = "0";
refreshBounds();
renderTroubleshooter();
findExactMatches();
updateTimestampEditor();
2024-03-04 10:41:14 -05:00
}
2024-03-05 17:33:51 -05:00
async function movieLoaded() {
Dashboard.showLoadingMsg();
2024-10-20 07:53:48 -04:00
saveIgnoreListSeriesButton.textContent = "Apply to movie";
seasonSelection.style.display = "none";
episodeSelection.style.display = "none";
eraseMovieContainer.style.display = "unset";
timestampError.value = "";
fingerprintVisualizer.style.display = "none";
lhs = await getJson("Intros/Episode/" + selectShow.value + "/Chromaprint");
if (lhs === undefined) {
timestampError.value += "Error: " + selectShow.value + " fingerprints failed!\n";
} else if (lhs === null) {
timestampError.value += "Error: " + selectShow.value + " fingerprints missing!\n";
}
rightEpisodeEditor.style.display = "none";
if (timestampError.value == "") {
timestampErrorDiv.style.display = "none";
} else {
timestampErrorDiv.style.display = "unset";
}
Dashboard.hideLoadingMsg();
txtOffset.value = "0";
// Try to get the timestamps of each intro, falling back a default value of zero if no intro was found
const leftEpisodeJson = await getJson("Episode/" + selectShow.value + "/Timestamps");
// Update the editor for the first and second episodes
timestampEditor.style.display = "unset";
document.querySelector("#editLeftEpisodeTitle").textContent = selectShow.value;
document.querySelector("#editLeftIntroEpisodeStartEdit").value = leftEpisodeJson.Introduction.Start;
document.querySelector("#editLeftIntroEpisodeEndEdit").value = leftEpisodeJson.Introduction.End;
document.querySelector("#editLeftCreditEpisodeStartEdit").value = leftEpisodeJson.Credits.Start;
document.querySelector("#editLeftCreditEpisodeEndEdit").value = leftEpisodeJson.Credits.End
// Update display inputs
const inputs = document.querySelectorAll('#timestampEditor input[type="number"]');
inputs.forEach((input) => {
const displayInput = document.getElementById(input.id.replace("Edit", "Display"));
displayInput.value = formatTime(parseFloat(input.value) || 0);
});
setupTimeInputs();
}
function setupTimeInputs() {
const timestampEditor = document.getElementById("timestampEditor");
timestampEditor.querySelectorAll(".inputContainer").forEach((container) => {
const displayInput = container.querySelector('[id$="Display"]');
const editInput = container.querySelector('[id$="Edit"]');
displayInput.addEventListener("pointerdown", (e) => {
e.preventDefault();
switchToEdit(displayInput, editInput);
});
editInput.addEventListener("blur", () => switchToDisplay(displayInput, editInput));
displayInput.value = formatTime(parseFloat(editInput.value) || 0);
});
2024-03-04 10:41:14 -05:00
}
2024-03-05 17:33:51 -05:00
function switchToEdit(displayInput, editInput) {
displayInput.style.display = "none";
editInput.style.display = "";
editInput.focus();
2024-03-04 10:41:14 -05:00
}
function switchToDisplay(displayInput, editInput) {
editInput.style.display = "none";
displayInput.style.display = "";
2024-10-06 12:13:30 +02:00
displayInput.value = formatTime(parseFloat(editInput.value) || 0);
}
2024-10-06 12:13:30 +02:00
function formatTime(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = Math.floor(totalSeconds % 60);
let result = [];
if (hours > 0) result.push(hours + " hour" + (hours !== 1 ? "s" : ""));
if (minutes > 0) result.push(minutes + " minute" + (minutes !== 1 ? "s" : ""));
if (seconds > 0 || result.length === 0) result.push(seconds + " second" + (seconds !== 1 ? "s" : ""));
return result.join(" ");
2022-05-30 02:23:36 -05:00
}
// 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
const leftEpisodeJson = await getJson("Episode/" + leftEpisode.value + "/Timestamps");
const rightEpisodeJson = await getJson("Episode/" + rightEpisode.value + "/Timestamps");
// Update the editor for the first and second episodes
timestampEditor.style.display = "unset";
document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text;
document.querySelector("#editLeftIntroEpisodeStartEdit").value = leftEpisodeJson.Introduction.Start;
document.querySelector("#editLeftIntroEpisodeEndEdit").value = leftEpisodeJson.Introduction.End;
document.querySelector("#editLeftCreditEpisodeStartEdit").value = leftEpisodeJson.Credits.Start;
document.querySelector("#editLeftCreditEpisodeEndEdit").value = leftEpisodeJson.Credits.End;
document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text;
document.querySelector("#editRightIntroEpisodeStartEdit").value = rightEpisodeJson.Introduction.Start;
document.querySelector("#editRightIntroEpisodeEndEdit").value = rightEpisodeJson.Introduction.End;
document.querySelector("#editRightCreditEpisodeStartEdit").value = rightEpisodeJson.Credits.Start;
document.querySelector("#editRightCreditEpisodeEndEdit").value = rightEpisodeJson.Credits.End;
// Update display inputs
const inputs = document.querySelectorAll('#timestampEditor input[type="number"]');
inputs.forEach((input) => {
const displayInput = document.getElementById(input.id.replace("Edit", "Display"));
displayInput.value = formatTime(parseFloat(input.value) || 0);
2024-05-13 14:25:52 +02:00
});
setupTimeInputs();
}
// adds an item to a dropdown
function addItem(select, text, value) {
let item = new Option(text, value);
select.add(item);
}
2020-07-04 23:33:19 +09:00
// clear a select of items
function clearSelect(select) {
timestampError.value = "";
timestampErrorDiv.style.display = "none";
timestampEditor.style.display = "none";
timeContainer.style.display = "none";
canvas.style.display = "none";
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
async function getJson(url) {
return await fetchWithAuth(url, "GET")
.then((r) => {
if (r.ok) {
return r.json();
} else {
return null;
}
})
.catch((err) => {
console.debug(err);
});
}
// make an authenticated fetch to the server
async function fetchWithAuth(url, method, body) {
url = ApiClient.serverAddress() + "/" + url;
const reqInit = {
method: method,
headers: {
Authorization: "MediaBrowser Token=" + ApiClient.accessToken(),
},
body: body,
};
if (method === "POST") {
reqInit.headers["Content-Type"] = "application/json";
}
return await fetch(url, reqInit);
}
// 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.1238 : 1;
}
break;
case "ArrowUp":
if (timestampError.value != "") {
offsetDelta = e.ctrlKey ? -10 / 0.1238 : -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().slice(14, 19);
}
// 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?";
const eraseCacheChecked = document.getElementById("eraseModeCacheCheckbox").checked;
Dashboard.confirm(body, title, (result) => {
if (!result) {
return;
}
fetchWithAuth("Intros/EraseTimestamps?mode=" + mode + "&eraseCache=" + eraseCacheChecked, "POST", null);
Dashboard.alert(mode + " timestamps erased");
});
}
document.querySelector("#TemplateConfigPage").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];
}
if (config["SkipButtonWarning"]) {
document.getElementById("SkipButtonContainer").style.display = "none";
document.getElementById("PersistContainer").style.display = "none";
} else {
document.getElementById("warningMessage").style.display = "none";
}
populateLibraries();
selectAllLibrariesChanged();
autoSkipChanged();
2024-05-13 14:25:52 +02:00
autoSkipCreditsChanged();
2024-03-01 14:45:27 -05:00
persistSkipChanged();
generateAutoSkipClientList();
2024-03-01 14:45:27 -05:00
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
visualizer.addEventListener("toggle", visualizerToggled);
support.addEventListener("toggle", supportToggled);
storage.addEventListener("toggle", storageToggled);
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();
});
btnSeasonEraseTimestamps.addEventListener("click", () => {
Dashboard.confirm("Are you sure you want to erase all timestamps for this season?", "Confirm timestamp erasure", (result) => {
2022-07-17 02:13:02 -05:00
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;
const eraseCacheChecked = document.getElementById("eraseSeasonCacheCheckbox").checked;
2022-07-03 01:20:33 -05:00
2022-07-17 02:13:02 -05:00
const url = "Intros/Show/" + encodeURIComponent(show) + "/" + encodeURIComponent(season);
fetchWithAuth(url + "?eraseCache=" + eraseCacheChecked, "DELETE", null);
2022-07-17 02:13:02 -05:00
Dashboard.alert("Erased timestamps for " + season + " of " + show);
document.getElementById("eraseSeasonCacheCheckbox").checked = false;
});
});
btnMovieEraseTimestamps.addEventListener("click", () => {
Dashboard.confirm("Are you sure you want to erase all timestamps for this movie?", "Confirm timestamp erasure", (result) => {
if (!result) {
return;
}
const show = selectShow.value;
const eraseCacheChecked = document.getElementById("eraseMovieCacheCheckbox").checked;
const url = "Intros/Show/" + encodeURIComponent(show);
fetchWithAuth(url + "?eraseCache=" + eraseCacheChecked, "DELETE", null);
Dashboard.alert("Erased timestamps for " + show);
document.getElementById("eraseMovieCacheCheckbox").checked = false;
});
});
saveIgnoreListSeasonButton.addEventListener("click", () => {
Dashboard.showLoadingMsg();
var url = "Intros/IgnoreList/UpdateSeason";
const newRhs = {
IgnoreIntro: ignorelistIntro.checked,
IgnoreCredits: ignorelistCredits.checked,
SeasonId: IgnoreListSeasonId,
};
fetchWithAuth(url, "POST", JSON.stringify(newRhs));
Dashboard.alert("Ignore list updated for " + selectSeason.value + " of " + selectShow.value);
Dashboard.hideLoadingMsg();
});
saveIgnoreListSeriesButton.addEventListener("click", () => {
Dashboard.showLoadingMsg();
var url = "Intros/IgnoreList/UpdateSeries" + "/" + encodeURIComponent(selectShow.value);
const newRhs = {
IgnoreIntro: ignorelistIntro.checked,
IgnoreCredits: ignorelistCredits.checked,
};
fetchWithAuth(url, "POST", JSON.stringify(newRhs));
Dashboard.alert("Ignore list updated for " + selectShow.value);
Dashboard.hideLoadingMsg();
});
btnUpdateTimestamps.addEventListener("click", () => {
const getEditValue = (id) => parseFloat(document.getElementById(id).value) || 0;
const lhsId = selectEpisode1.options[selectEpisode1.selectedIndex].value;
const newLhs = {
Introduction: {
Start: getEditValue("editLeftIntroEpisodeStartEdit"),
End: getEditValue("editLeftIntroEpisodeEndEdit"),
},
Credits: {
Start: getEditValue("editLeftCreditEpisodeStartEdit"),
End: getEditValue("editLeftCreditEpisodeEndEdit"),
},
};
const rhsId = selectEpisode2.options[selectEpisode2.selectedIndex].value;
const newRhs = {
Introduction: {
Start: getEditValue("editRightIntroEpisodeStartEdit"),
End: getEditValue("editRightIntroEpisodeEndEdit"),
},
Credits: {
Start: getEditValue("editRightCreditEpisodeStartEdit"),
End: getEditValue("editRightCreditEpisodeEndEdit"),
},
};
fetchWithAuth("Episode/" + lhsId + "/Timestamps", "POST", JSON.stringify(newLhs));
fetchWithAuth("Episode/" + rhsId + "/Timestamps", "POST", JSON.stringify(newRhs));
Dashboard.alert("New introduction timestamps saved");
});
document.addEventListener("keydown", keyDown);
windowHashInterval = setInterval(checkWindowHash, 2500);
canvas.addEventListener("mousemove", (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const y = e.clientY - rect.top;
const shift = Number(txtOffset.value);
let lTime, rTime, diffPos;
if (shift < 0) {
lTime = y * 0.1238;
rTime = (y + shift) * 0.1238;
diffPos = y + shift;
} else {
lTime = (y - shift) * 0.1238;
rTime = y * 0.1238;
diffPos = y - shift;
2022-07-17 02:13:02 -05:00
}
const diff = fprDiffs[Math.floor(diffPos)];
if (!diff) {
timeContainer.style.display = "none";
return;
} else {
timeContainer.style.display = "unset";
}
2022-05-30 02:23:36 -05:00
const times = document.querySelector("span#timestamps");
2022-05-30 02:23:36 -05:00
// LHS timestamp, RHS timestamp, percent similarity
times.textContent = secondsToString(lTime) + ", " + secondsToString(rTime) + ", " + Math.round(diff) + "%";
timeContainer.style.position = "relative";
timeContainer.style.left = "25px";
timeContainer.style.top = (-1 * rect.height + y).toString() + "px";
});
function setTime(seconds) {
// Calculate hours, minutes, and remaining seconds
let hours = Math.floor(seconds / 3600);
let minutes = Math.floor((seconds % 3600) / 60);
let remainingSeconds = seconds % 60;
// Format as HH:MM:SS
let formattedTime = String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(remainingSeconds).padStart(2, "0");
// Set the value of the time input
return formattedTime;
}
2022-05-30 02:23:36 -05:00
function getTimeInSeconds(time) {
let [hours, minutes, seconds] = time.split(":").map(Number);
return hours * 3600 + minutes * 60 + seconds;
}
</script>
</div>
</body>
2020-07-04 23:33:19 +09:00
</html>