<!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 Analyze New Media</span>
                                </label>

                                <div class="fieldDescription">If enabled, new media will be automatically analyzed for skippable segments when added to the library
                                    <br />
                                    <br />
                                    Note: 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="UpdateMediaSegments" type="checkbox" is="emby-checkbox" />
                                    <span>Update Missing Segments During Scan</span>
                                </label>

                                <div class="fieldDescription">
                                    Enable this option to update media segments for any uncached media during a library scan. <br />
                                    This includes recently added, modified, or previously skipped (but not ignored) files.<br />
                                    <b>Warning:</b> This should be disabled if you're using media segment providers other than Intro Skipper.
                                </div>
                            </div>

                            <div class="checkboxContainer">
                                <label class="emby-checkbox-label">
                                    <input id="AnalyzeMovies" type="checkbox" is="emby-checkbox" />
                                    <span>Analyze Movies</span>
                                </label>
                            </div>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
                                    <span>Analyze Season 0 (Specials / Extras)</span>
                                </label>

                                <div class="fieldDescription">
                                    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="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>
                                    <b style="color: orange">Changing segment parameters requires regenerating media segments before changes take effect.</b>
                                    <br />
                                    Per the jellyfin MediaSegments API, records must be updated individually and may be slow to regenerate.
                                </p>

                                <div class="checkboxContainer">
                                    <label class="emby-checkbox-label">
                                        <input id="ScanIntroduction" type="checkbox" is="emby-checkbox" />
                                        <span>Identify Introductions</span>
                                    </label>
                                </div>

                                <div class="checkboxContainer">
                                    <label class="emby-checkbox-label">
                                        <input id="ScanCredits" type="checkbox" is="emby-checkbox" />
                                        <span>Identify Credits</span>
                                    </label>
                                </div>

                                <div class="checkboxContainer">
                                    <label class="emby-checkbox-label">
                                        <input id="ScanRecap" type="checkbox" is="emby-checkbox" />
                                        <span>Identify Recaps</span>
                                    </label>
                                </div>

                                <div class="checkboxContainer">
                                    <label class="emby-checkbox-label">
                                        <input id="ScanPreview" type="checkbox" is="emby-checkbox" />
                                        <span>Identify Previews</span>
                                    </label>
                                </div>

                                <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>

                                <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>

                                <p>
                                    The amount of each item's audio or content that will be analyzed is determined using both the percentage of audio and maximum runtime to analyze. The minimum of (duration * percent, maximum runtime) is the amount that will be analyzed.
                                </p>

                                <p>
                                    If the audio percentage or maximum runtime settings are modified, the cached fingerprints and timestamps for each series, season, or movie you want to analyze with the modified settings <b>will have to be deleted</b>.
                                    <br />
                                    Increasing either of these settings will cause episode analysis to take much longer.
                                </p>

                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="RebuildMediaSegments" type="checkbox" is="emby-checkbox" />
                                        <span>Regenerate All Media Segments on Next Run</span>
                                    </label>

                                    <div class="fieldDescription">When enabled, this option will <b>overwrite all existing media segments</b> with your current Intro Skipper timestamps during the next analysis. Upon completion, this option will be automatically disabled.</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="chapters">
                                <summary>Chapter Detection Options</summary>

                                <br />
                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerIntroductionPattern"> Introductions </label>
                                    <input id="ChapterAnalyzerIntroductionPattern" type="text" placeholder="(^|\s)(Intro|Introduction|OP|Opening)(?!\sEnd)(\s|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect introduction chapters.
                                        <br />Default: <code>(^|\s)(Intro|Introduction|OP|Opening)(?!\sEnd)(\s|$)</code>
                                    </div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerEndCreditsPattern"> Credits </label>
                                    <input id="ChapterAnalyzerEndCreditsPattern" type="text" placeholder="(^|\s)(Credits?|ED|Ending|Outro)(?!\sEnd)(\s|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect credits chapters.
                                        <br />Default: <code>(^|\s)(Credits?|ED|Ending|Outro)(?!\sEnd)(\s|$)</code>
                                    </div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerPreviewPattern"> Preview </label>
                                    <input id="ChapterAnalyzerPreviewPattern" type="text" placeholder="(^|\s)(Preview|PV|Sneak\s?Peek|Coming\s?(Up|Soon)|Next\s+(time|on|episode)|Teaser|Trailer)(?!\sEnd)(\s|:|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect preview chapters.
                                        <br />Default: <code>(^|\s)(Preview|PV|Sneak\s?Peek|Coming\s?(Up|Soon)|Next\s+(time|on|episode)|Teaser|Trailer)(?!\sEnd)(\s|:|$)</code>
                                    </div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerRecapPattern"> Recaps </label>
                                    <input id="ChapterAnalyzerRecapPattern" type="text" placeholder="(^|\s)(Re?cap|Sum{1,2}ary|Prev(ious(ly)?)?|(Last|Earlier)(\s\w+)?|Catch[ -]up)(?!\sEnd)(\s|:|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect recap chapters.
                                        <br />Default: <code>(^|\s)(Re?cap|Sum{1,2}ary|Prev(ious(ly)?)?|(Last|Earlier)(\s\w+)?|Catch[ -]up)(?!\sEnd)(\s|:|$)</code>
                                    </div>
                                </div>
                            </details>

                            <details id="detection">
                                <summary>Process Configuration</summary>

                                <br />
                                <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="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 />
                                        <b>WARNING: Disabling the cache will cause all fingerprints to be recreated during analysis. For debug use only!</b>
                                        <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>

                        <p>
                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="PluginSkip" type="checkbox" is="emby-checkbox" />
                                <span>Enable injected server-side skip <b style="color: red;">Restart required!</b></span>
                            </label>
                        </div>
                        </p>

                        <div id="ServerSkipSettings" style="display: none">
                            <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 for All Clients</span>
                                    </label>
                                </div>

                                <div class="AutoSkipClientListContainer">
                                    <div class="AutoSkipClientList">
                                        <h3 class="checkboxListLabel">Limit auto skip to the following clients</h3>
                                        <div class="checkboxList paperList" style="padding: 0.5em 1em" id="autoSkipCheckboxes"></div>
                                    </div>
                                    <label class="inputLabel" for="ClientList"></label>
                                    <input id="ClientList" type="hidden" is="emby-input" />
                                </div>

                                <div class="AutoSkipTypeListContainer">
                                    <div class="AutoSkipTypeList">
                                        <h3 class="checkboxListLabel">Auto skip the following types</h3>
                                        <div class="checkboxList paperList" style="padding: 0.5em 1em" id="autoSkipTypeCheckboxes"></div>
                                    </div>
                                    <label class="inputLabel" for="TypeList"></label>
                                    <input id="TypeList" type="hidden" is="emby-input" />
                                </div>

                                <div id="divSkipFirstEpisode" class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
                                        <span>Play Segments for First Episode of a Season</span>
                                    </label>

                                    <div class="fieldDescription">If checked, auto skip will play the segments of the first episode in a season.</div>
                                </div>

                                <div id="divSecondsOfIntroStartToPlay" class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="SecondsOfIntroStartToPlay"> Segment skip delay (in seconds) </label>
                                    <input id="SecondsOfIntroStartToPlay" type="number" is="emby-input" min="0" />
                                    <div class="fieldDescription">Seconds of segment start that should be played. Defaults to 0.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="RemainingSecondsOfIntro"> Segment playback duration (in seconds) </label>
                                    <input id="RemainingSecondsOfIntro" type="number" is="emby-input" min="0" />
                                    <div class="fieldDescription">Seconds of segment ending that should be played. Defaults to 2.</div>
                                </div>

                                <div id="SkipButtonContainer" class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="SkipButtonEnabled" type="checkbox" is="emby-checkbox" />
                                        <span id="SkipButtonVisibleLabel">Show Segment Skip Buttons</span>
                                    </label>

                                    <div class="fieldDescription">
                                        <b style="color: red">Restart required!</b> If checked, a skip button will be added <b>to the server</b> according to the UI settings.<br />
                                        This button is <b>separate</b> from the Media Segment Actions in Jellyfin 10.10 and compatible clients.<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>

                                <details>
                                    <summary>User Interface Customization</summary>

                                    <p>
                                        <b style="color: orange">These settings do not apply to Media Segment Actions in Jellyfin 10.10 and compatible clients.</b>
                                    </p>

                                    <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 Segment Duration</span>
                                            </label>

                                            <div class="fieldDescription">
                                                If checked, skip button will remain visible for the entire 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 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>

                                    <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>
                                </details>
                            </fieldset>
                        </div>

                        <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="visualizer">
                                <summary>Edit 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="analyzerActionsSection" style="display: none">
                                    <h3 style="margin: 0">Analyzer actions</h3>
                                    <p style="margin: 0">
                                        Choose how segments should be analyzed for this season.<br />
                                        Default uses all available detection methods (Chromaprint, Chapter, and BlackFrame for credits).<br />
                                        Select specific methods to limit analysis, or None to skip detection entirely.
                                    </p>
                                    <br />

                                    <div id="analyzerActionsContainer">
                                        <label for="actionRecap" style="margin-right: 1.5em; display: inline-block">
                                            <span>Recap analysis</span>
                                            <select is="emby-select" id="actionRecap" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                        <label for="actionIntro" style="margin-right: 1.5em; display: inline-block">
                                            <span>Introduction analysis</span>
                                            <select is="emby-select" id="actionIntro" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="Chromaprint">Chromaprint</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                        <label for="actionCredits" style="margin-right: 1.5em; display: inline-block">
                                            <span>Credits (Outro) analysis</span>
                                            <select is="emby-select" id="actionCredits" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="Chromaprint">Chromaprint</option>
                                                <option value="BlackFrame">BlackFrame</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                        <label for="actionPreview" style="margin-right: 1.5em; display: inline-block">
                                            <span>Preview analysis</span>
                                            <select is="emby-select" id="actionPreview" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                    </div>
                                    <br />

                                    <button is="emby-button" id="saveAnalyzerActions" class="raised button-submit block emby-button" style="display: none">Apply Changes</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="recapStart">Recap Start</label>
                                            <input type="text" id="editLeftRecapEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftRecapEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused" for="recapEnd">Recap End</label>
                                            <input type="text" id="editLeftRecapEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftRecapEpisodeEndEdit" 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="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 (Outro) 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 (Outro) 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>
                                    <div class="inlineForm">
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused" for="previewStart">Preview Start</label>
                                            <input type="text" id="editLeftPreviewEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftPreviewEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused" for="previewEnd">Preview End</label>
                                            <input type="text" id="editLeftPreviewEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftPreviewEpisodeEndEdit" 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="recapStart">Recap Start</label>
                                                <input type="text" id="editRightRecapEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightRecapEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused" for="recapEnd">Recap End</label>
                                                <input type="text" id="editRightRecapEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightRecapEpisodeEndEdit" 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="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 (Outro) 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 (Outro) 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>
                                        <div class="inlineForm">
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused" for="previewStart">Preview Start</label>
                                                <input type="text" id="editRightPreviewEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightPreviewEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused" for="previewEnd">Preview End</label>
                                                <input type="text" id="editRightPreviewEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightPreviewEpisodeEndEdit" 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 />
                                    <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 />
                                    <br />
                                </div>

                                <div style="display: flex; align-items: center;">
                                    <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="btnEraseRecapTimestamps">Erase all recap timestamps (globally)</button>
                                        <br />

                                        <button is="emby-button" class="button-submit emby-button" id="btnEraseCreditTimestamps">Erase all credits (outro) timestamps (globally)</button>
                                        <br />

                                        <button is="emby-button" class="button-submit emby-button" id="btnErasePreviewTimestamps">Erase all preview timestamps (globally)</button>
                                    </div>
                                    <div>
                                        <input type="checkbox" id="eraseModeCacheCheckbox" style="margin-left: 10px" />
                                        <label for="eraseModeCacheCheckbox" style="margin-left: 5px">Erase global cached fingerprint files</label>
                                    </div>
                                </div>
                                <br />
                                <br />
                            </details>

                            <details id="support">
                                <summary>Support Bundle Info</summary>

                                <textarea id="supportBundle" rows="20" cols="75" readonly></textarea>
                            </details>

                            <details id="storage">
                                <br />
                                <summary>Storage Usage</summary>
                                <div class="fieldDescription">See how much space each library uses.</div>
                                <textarea id="storageText" 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 = {};

                // 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 btnEraseRecapTimestamps = document.querySelector("button#btnEraseRecapTimestamps");
                var btnEraseCreditTimestamps = document.querySelector("button#btnEraseCreditTimestamps");
                var btnErasePreviewTimestamps = document.querySelector("button#btnErasePreviewTimestamps");

                // 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",
                    // internals
                    "SilenceDetectionMaximumNoise",
                    "SilenceDetectionMinimumDuration",
                    "ChapterAnalyzerIntroductionPattern",
                    "ChapterAnalyzerEndCreditsPattern",
                    "ChapterAnalyzerPreviewPattern",
                    "ChapterAnalyzerRecapPattern",
                    "TypeList",
                    // UI customization
                    "SkipButtonIntroText",
                    "SkipButtonEndCreditsText",
                    "AutoSkipNotificationText",
                ];

                var booleanConfigurationFields = ["AutoDetectIntros", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "UpdateMediaSegments", "RebuildMediaSegments", "ScanIntroduction", "ScanCredits", "ScanRecap", "ScanPreview", "CacheFingerprints", "PluginSkip", "AutoSkip", "SkipFirstEpisode", "PersistSkipButton", "SkipButtonEnabled"];

                // visualizer elements
                var analyzerActionsSection = document.querySelector("div#analyzerActionsSection");
                var actionIntro = analyzerActionsSection.querySelector("select#actionIntro");
                var actionCredits = analyzerActionsSection.querySelector("select#actionCredits");
                var actionRecap = analyzerActionsSection.querySelector("select#actionRecap");
                var actionPreview = analyzerActionsSection.querySelector("select#actionPreview");
                var saveAnalyzerActionsButton = analyzerActionsSection.querySelector("button#saveAnalyzerActions");
                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 pluginSkip = document.getElementById("PluginSkip");
                var serverSkipSettings = document.getElementById("ServerSkipSettings");
                var autoSkip = document.getElementById("AutoSkip");
                var skipButtonVisible = document.getElementById("SkipButtonEnabled");
                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.getElementById("divSkipFirstEpisode");
                var secondsOfIntroStartToPlay = document.getElementById("divSecondsOfIntroStartToPlay");
                var autoSkipClientList = document.querySelector("div.AutoSkipClientListContainer");
                var movieCreditsDuration = document.getElementById("movieCreditsDuration");

                function skipButtonVisibleChanged() {
                    if (autoSkip.checked) {
                        skipButtonSettings.style.display = "none";
                    } else if (skipButtonVisible.checked) {
                        skipButtonSettings.style.display = "unset";
                    } else {
                        skipButtonSettings.style.display = "none";
                    }
                }

                skipButtonVisible.addEventListener("change", skipButtonVisibleChanged);

                function autoSkipChanged() {
                    if (autoSkip.checked) {
                        autoSkipClientList.style.display = "none";
                        skipButtonVisibleLabel.textContent = "Button unavailable due to auto skip";
                    } else {
                        autoSkipClientList.style.display = "unset";
                        autoSkipClientList.style.width = "100%";
                        skipButtonVisibleLabel.textContent = "Show Segment Skip Buttons";
                    }
                    skipButtonVisibleChanged();
                }

                autoSkip.addEventListener("change", autoSkipChanged);

                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();
                    for (const item of items) {
                        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 generateAutoSkipTypeList() {
                    const types = ["Introduction", "Credits", "Recap", "Preview"];
                    generateCheckboxList(types, "autoSkipTypeCheckboxes", "TypeList");
                }

                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.getElementById("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 pluginSkipSettingChanged() {
                    if (pluginSkip.checked) {
                        serverSkipSettings.style.display = "unset";
                    } else {
                        serverSkipSettings.style.display = "none";
                        // TODO: Reset everything to default
                        autoSkip.checked = false;
                        skipButtonVisible.checked = false;
                        persistSkip.checked = false;
                    }
                }

                pluginSkip.addEventListener("change", pluginSkipSettingChanged);

                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) {
                        analyzerActionsSection.style.display = "none";
                        saveAnalyzerActionsButton.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#storageText");
                    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);

                    if (shows[selectShow.value].IsMovie) {
                        movieLoaded();
                        return;
                    }

                    // 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 analyzer actions editor.
                    saveAnalyzerActionsButton.style.display = "block";
                    const analyzerActions = await getJson("Intros/AnalyzerActions/" + encodeURI(selectSeason.value));
                    actionIntro.value = analyzerActions.Introduction || "Default";
                    actionCredits.value = analyzerActions.Credits || "Default";
                    actionRecap.value = analyzerActions.Recap || "Default";
                    actionPreview.value = analyzerActions.Preview || "Default";
                    analyzerActionsSection.style.display = "unset";

                    // 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();

                    saveAnalyzerActionsButton.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 movie
                    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;
                    document.querySelector("#editLeftRecapEpisodeStartEdit").value = leftEpisodeJson.Recap.Start;
                    document.querySelector("#editLeftRecapEpisodeEndEdit").value = leftEpisodeJson.Recap.End;
                    document.querySelector("#editLeftPreviewEpisodeStartEdit").value = leftEpisodeJson.Preview.Start;
                    document.querySelector("#editLeftPreviewEpisodeEndEdit").value = leftEpisodeJson.Preview.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("#editLeftRecapEpisodeStartEdit").value = leftEpisodeJson.Recap.Start;
                    document.querySelector("#editLeftRecapEpisodeEndEdit").value = leftEpisodeJson.Recap.End;
                    document.querySelector("#editLeftPreviewEpisodeStartEdit").value = leftEpisodeJson.Preview.Start;
                    document.querySelector("#editLeftPreviewEpisodeEndEdit").value = leftEpisodeJson.Preview.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;
                    document.querySelector("#editRightRecapEpisodeStartEdit").value = rightEpisodeJson.Recap.Start;
                    document.querySelector("#editRightRecapEpisodeEndEdit").value = rightEpisodeJson.Recap.End;
                    document.querySelector("#editRightPreviewEpisodeStartEdit").value = rightEpisodeJson.Preview.Start;
                    document.querySelector("#editRightPreviewEpisodeEndEdit").value = rightEpisodeJson.Preview.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.getElementById("eraseModeCacheCheckbox").checked = false;
                    });
                }

                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("warningMessage").style.display = "unset";
                        } else {
                            document.getElementById("warningMessage").style.display = "none";
                        }

                        populateLibraries();
                        selectAllLibrariesChanged();
                        autoSkipChanged();
                        persistSkipChanged();
                        generateAutoSkipTypeList();
                        generateAutoSkipClientList();
                        const pluginSkip = document.getElementById("PluginSkip");
                        pluginSkip.checked = pluginSkip.checked
                            || document.getElementById("AutoSkip").checked
                            || document.getElementById("SkipButtonEnabled").checked
                        pluginSkipSettingChanged();

                        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();
                });
                btnEraseRecapTimestamps.addEventListener("click", (e) => {
                    eraseTimestamps("Recap");
                    e.preventDefault();
                });
                btnEraseCreditTimestamps.addEventListener("click", (e) => {
                    eraseTimestamps("Credits");
                    e.preventDefault();
                });
                btnErasePreviewTimestamps.addEventListener("click", (e) => {
                    eraseTimestamps("Preview");
                    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;
                    });
                });
                saveAnalyzerActionsButton.addEventListener("click", () => {
                    Dashboard.showLoadingMsg();

                    var url = "Intros/AnalyzerActions/UpdateSeason";
                    const actions = {
                        id: selectSeason.value,
                        analyzerActions: {
                            Introduction: actionIntro.value,
                            Credits: actionCredits.value,
                            Recap: actionRecap.value,
                            Preview: actionPreview.value,
                        },
                    };

                    fetchWithAuth(url, "POST", JSON.stringify(actions));

                    Dashboard.alert("Analyzer actions updated for " + selectSeason.value + " of " + 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: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftIntroEpisodeStartEdit"),
                            End: getEditValue("editLeftIntroEpisodeEndEdit"),
                        },
                        Credits: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftCreditEpisodeStartEdit"),
                            End: getEditValue("editLeftCreditEpisodeEndEdit"),
                        },
                        Recap: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftRecapEpisodeStartEdit"),
                            End: getEditValue("editLeftRecapEpisodeEndEdit"),
                        },
                        Preview: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftPreviewEpisodeStartEdit"),
                            End: getEditValue("editLeftPreviewEpisodeEndEdit"),
                        },
                    };

                    const rhsId = selectEpisode2.options[selectEpisode2.selectedIndex].value;
                    const newRhs = {
                        Introduction: {
                            ItemId: rhsId,
                            Start: getEditValue("editRightIntroEpisodeStartEdit"),
                            End: getEditValue("editRightIntroEpisodeEndEdit"),
                        },
                        Credits: {
                            ItemId: rhsId,
                            Start: getEditValue("editRightCreditEpisodeStartEdit"),
                            End: getEditValue("editRightCreditEpisodeEndEdit"),
                        },
                        Recap: {
                            ItemId: rhsId,
                            Start: getEditValue("editRightRecapEpisodeStartEdit"),
                            End: getEditValue("editRightRecapEpisodeEndEdit"),
                        },
                        Preview: {
                            ItemId: rhsId,
                            Start: getEditValue("editRightPreviewEpisodeStartEdit"),
                            End: getEditValue("editRightPreviewEpisodeEndEdit"),
                        },
                    };

                    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>