<!doctype html> <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> <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> <details id="intro_reqs"> <summary>Modify Segment Parameters</summary> <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> </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> </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> <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> <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> <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 <b>will have to be deleted.</b> Increasing either of these settings will cause episode analysis to take much longer. </p> <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> <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. <b>Warning:</b> 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 <b>overwrite all existing media segments</b> for your episodes with currently detected introduction and credit timestamps during the next scan.</div> </div> </details> <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> </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> <details id="detection"> <summary>Process Configuration</summary> <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> </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> <p align="center" style="font-size: 0.75em"> EDL file generation has been removed. Please use endrl's <a href="https://github.com/endrl/jellyfin-plugin-edl">EDL plugin</a>. </p> </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 for <b>all</b> 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> <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"> <label class="emby-checkbox-label"> <input id="AutoSkipCredits" type="checkbox" is="emby-checkbox" /> <span>Automatically skip credits</span> </label> <div class="fieldDescription"> If checked, credits will be automatically skipped for <b>all</b> 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> <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> <br /> </div> <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> </label> <div class="fieldDescription"> (<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 <b>automatically</b>. <br /> </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> <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> <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"> 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> <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> <div> <button is="emby-button" type="submit" class="raised button-submit block emby-button"> <span>Save</span> </button> </div> <br /> <fieldset class="verticalSection-extrabottompadding"> <legend>Advanced</legend> <details id="support"> <summary>Support Bundle Info</summary> <textarea id="supportBundle" rows="20" cols="75" readonly></textarea> </details> <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 /> <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> </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 /> </div> <div id="timestampErrorDiv" style="display: none"> <br /> <textarea id="timestampError" rows="2" cols="75" readonly></textarea> <br /> <br /> </div> <div id="fingerprintVisualizer" style="display: none"> <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 /> <span>Shift amount:</span> <input type="number" min="-3000" max="3000" value="0" id="offset" /> <br /> <span id="suggestedShifts"> <span>Suggested shifts: </span> </span> <br /> <br /> <canvas id="troubleshooter" style="display: none"></canvas> <span id="timestampContainer"> <span id="timestamps"></span> <br /> <span id="intros"></span> </span> <br /> </div> <br /> <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> <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> <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> </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(); } autoSkipCredits.addEventListener("change", autoSkipCreditsChanged); skipButtonVisibleText(); // run once on launch for legacy installs function selectAllLibrariesChanged() { if (selectAllLibraries.checked) { librariesContainer.style.display = "none"; } else { librariesContainer.style.display = "unset"; } } selectAllLibraries.addEventListener("change", selectAllLibrariesChanged); function updateList(textField, container) { textField.value = Array.from(container.querySelectorAll('input[type="checkbox"]:checked')) .map((checkbox) => checkbox.nextElementSibling.textContent) .join(", "); } 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"); } 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"; } } persistSkip.addEventListener("change", persistSkipChanged); 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"); // 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; } // 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; } 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 = ""; } // 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); } // 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 += selectEpisode1.value + " fingerprints missing or incomplete.\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 += selectEpisode2.value + " fingerprints missing or incomplete.\n"; } if (timestampError.value == "") { timestampErrorDiv.style.display = "none"; } else { timestampErrorDiv.style.display = "unset"; } Dashboard.hideLoadingMsg(); txtOffset.value = "0"; refreshBounds(); renderTroubleshooter(); findExactMatches(); updateTimestampEditor(); } async function movieLoaded() { Dashboard.showLoadingMsg(); 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 += selectShow.value + " fingerprints missing or incomplete.\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); }); } function switchToEdit(displayInput, editInput) { displayInput.style.display = "none"; editInput.style.display = ""; editInput.focus(); } function switchToDisplay(displayInput, editInput) { editInput.style.display = "none"; displayInput.style.display = ""; displayInput.value = formatTime(parseFloat(editInput.value) || 0); } 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(" "); } // 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); }); setupTimeInputs(); } // 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) { 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 () { 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(); autoSkipCreditsChanged(); persistSkipChanged(); generateAutoSkipClientList(); Dashboard.hideLoadingMsg(); }); }); document.querySelector("#FingerprintConfigForm").addEventListener("submit", function (e) { 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); }); }); e.preventDefault(); return false; }); 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) => { if (!result) { return; } const show = selectShow.value; const season = selectSeason.value; const eraseCacheChecked = document.getElementById("eraseSeasonCacheCheckbox").checked; const url = "Intros/Show/" + encodeURIComponent(show) + "/" + encodeURIComponent(season); fetchWithAuth(url + "?eraseCache=" + eraseCacheChecked, "DELETE", null); 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; } const diff = fprDiffs[Math.floor(diffPos)]; if (!diff) { timeContainer.style.display = "none"; return; } else { timeContainer.style.display = "unset"; } const times = document.querySelector("span#timestamps"); // 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; } function getTimeInSeconds(time) { let [hours, minutes, seconds] = time.split(":").map(Number); return hours * 3600 + minutes * 60 + seconds; } </script> </div> </body> </html>