<!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: 1.0em;
                   outline: 2px solid rgba(155, 155, 155, 0.5);
                }
            </style>
            <div class="content-primary">
                <form id="FingerprintConfigForm">
                    <fieldset class="verticalSection-extrabottompadding">
                        <legend>Analysis</legend>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="AutoDetectIntros" type="checkbox" is="emby-checkbox" />
                                <span>Automatically Scan Intros</span>
                            </label>

                            <div class="fieldDescription">
                                If enabled, introductions will be automatically analyzed for new media
                            </div>
                        </div>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="AutoDetectCredits" type="checkbox" is="emby-checkbox" />
                                <span>Automatically Scan Credits</span>
                            </label>

                            <div class="fieldDescription">
                                If enabled, credits will be automatically analyzed for new media
                                <br />
                                <br />
                                Note: Not selecting at least one automatic detection type will disable automatic scans. To configure the scheduled task, see <a is="emby-linkbutton" class="button-link" href="scheduledtasks.html">scheduled tasks</a>.
                            </div>
                        </div>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
                                <span>Analyze season 0</span>
                            </label>

                            <div class="fieldDescription">
                                If checked, season 0 (specials / extras) will be included in analysis.
                                <br />
                                <br />
                                Note: Shows containing both a specials and extra folder will identify extras as season 0
                                and ignore specials, regardless of this setting.
                            </div>
                        </div>

                        <div class="inputContainer">
                            <label class="inputLabel inputLabelUnfocused" for="MaxParallelism">
                                Maximum degree of parallelism
                            </label>
                            <input id="MaxParallelism" type="number" is="emby-input" min="1" />
                            <div class="fieldDescription">
                                Maximum number of simultaneous async episode analysis operations.
                            </div>
                        </div>

                        <div class="inputContainer">
                            <label class="inputLabel inputLabelUnfocused" for="SelectedLibraries">
                                Limit analysis to the following libraries
                            </label>
                            <input id="SelectedLibraries" type="text" is="emby-input" />
                            <div class="fieldDescription">
                                Enter the names of libraries to analyze, separated by commas. If this field is left
                                blank, all libraries on the server containing television episodes will be analyzed.
                            </div>
                        </div>

                        <details id="intro_reqs">
                            <summary>Modify Segment Parameters</summary>

                            <br />
                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="AnalysisPercent">
                                    Percent of audio to analyze
                                </label>
                                <input id="AnalysisPercent" type="number" is="emby-input" min="1" max="90" />
                                <div class="fieldDescription">
                                    Analysis will be limited to this percentage of each episode's audio. For example, a
                                    value of 25 (the default) will limit analysis to the first quarter of each episode.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="AnalysisLengthLimit">
                                    Maximum runtime of audio to analyze (in minutes)
                                </label>
                                <input id="AnalysisLengthLimit" type="number" is="emby-input" min="1" />
                                <div class="fieldDescription">
                                    Analysis will be limited to this amount of each episode's audio. For example, a
                                    value of 10 (the default) will limit analysis to the first 10 minutes of each
                                    episode.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="MinimumIntroDuration">
                                    Minimum introduction duration (in seconds)
                                </label>
                                <input id="MinimumIntroDuration" type="number" is="emby-input" min="1" />
                                <div class="fieldDescription">
                                    Similar sounding audio which is shorter than this duration will not be considered an
                                    introduction.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="MaximumIntroDuration">
                                    Maximum introduction duration (in seconds)
                                </label>
                                <input id="MaximumIntroDuration" type="number" is="emby-input" min="1" />
                                <div class="fieldDescription">
                                    Similar sounding audio which is longer than this duration will not be considered an
                                    introduction.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="MinimumCreditsDuration">
                                    Minimum credits duration (in seconds)
                                </label>
                                <input id="MinimumCreditsDuration" type="number" is="emby-input" min="1" />
                                <div class="fieldDescription">
                                    Similar sounding audio which is shorter than this duration will not be considered credits.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="MaximumCreditsDuration">
                                    Maximum credits duration (in seconds)
                                </label>
                                <input id="MaximumCreditsDuration" type="number" is="emby-input" min="1" />
                                <div class="fieldDescription">
                                    Similar sounding audio which is longer than this duration will not be considered credits.
                                </div>
                            </div>

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

                            <p>
                                If the audio percentage or maximum runtime settings are modified, the cached
                                fingerprints and introduction timestamps for each season you want to analyze with the
                                modified settings <strong>will have to be deleted.</strong>

                                Increasing either of these settings will cause episode analysis to take much longer.
                            </p>
                        </details>

                        <details id="edl">
                            <summary>EDL File Generation</summary>

                            <br />
                            <div class="selectContainer">
                                <label class="selectLabel" for="EdlAction">EDL Action</label>
                                <select is="emby-select" id="EdlAction" class="emby-select-withcolor emby-select">
                                    <option value="None">
                                        None (do not create or modify EDL files)
                                    </option>

                                    <option value="CommercialBreak">
                                        Commercial Break (recommended, skips past the intro once)
                                    </option>

                                    <option value="Cut">
                                        Cut (player will remove the intro from the video)
                                    </option>

                                    <option value="Intro">
                                        Intro/Credit (show a skip button, *experimental*)
                                    </option>

                                    <option value="Mute">
                                        Mute (audio will be muted)
                                    </option>

                                    <option value="SceneMarker">
                                        Scene Marker (create a chapter marker)
                                    </option>
                                </select>

                                <div class="fieldDescription">
                                    If set to a value other than None, specifies which action to write to
                                    <a href="https://kodi.wiki/view/Edit_decision_list">MPlayer compatible EDL files</a>
                                    alongside your episode files. <br />

                                    If this value is changed after EDL files are generated, you must check the
                                    "Regenerate EDL files" checkbox below.
                                </div>
                            </div>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="RegenerateEdlFiles" type="checkbox" is="emby-checkbox" />
                                    <span>Regenerate EDL files during next scan</span>
                                </label>

                                <div class="fieldDescription">
                                    If checked, the plugin will <strong>overwrite all EDL files</strong> associated with
                                    your episodes with the currently discovered introduction/credit timestamps and EDL action.
                                </div>
                            </div>
                        </details>

                        <details id="silence">
                            <summary>Silence Detection Options</summary>

                            <br />
                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="SilenceDetectionMaximumNoise">
                                    Noise tolerance
                                </label>
                                <input id="SilenceDetectionMaximumNoise" type="number" is="emby-input" min="-90"
                                       max="0" />
                                <div class="fieldDescription">
                                    Noise tolerance in negative decibels.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="SilenceDetectionMinimumDuration">
                                    Minimum silence duration
                                </label>
                                <input id="SilenceDetectionMinimumDuration" type="number" is="emby-input" min="0"
                                       step="0.01" />
                                <div class="fieldDescription">
                                    Minimum silence duration in seconds before adjusting introduction end time.
                                </div>
                            </div>
                        </details>

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

                            <br />
                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="UseChromaprint" type="checkbox" is="emby-checkbox" />
                                    <span>Chromaprint analysis</span>
                                </label>

                                <div class="fieldDescription">
                                    If checked, analysis will use Chromaprint to compare episode audio and identify intros.
                                    <br />
                                    <strong>WARNING: Disabling this option may result in incomplete or innaccurate analysis!</strong>
                                    <br />
                                </div>
                            </div>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="CacheFingerprints" type="checkbox" is="emby-checkbox" />
                                    <span>Cache episode fingerprints</span>
                                </label>

                                <div class="fieldDescription">
                                    If checked, episode fingerprints will be saved on the filesystem to improve analysis speed.
                                    <br />
                                    <strong>WARNING: May result in lengthy detection! Not recommended for large libraries!</strong>
                                    <br />
                                </div>
                            </div>

                            <div class="selectContainer">
                                <label class="selectLabel" for="ProcessPriority">ffmpeg Priority</label>
                                <select is="emby-select" id="ProcessPriority" class="emby-select-withcolor emby-select">
                                    <option value="Idle">
                                        Idle
                                    </option>

                                    <option value="BelowNormal">
                                        Below Normal
                                    </option>

                                    <option value="Normal">
                                        Normal
                                    </option>

                                    <option value="AboveNormal">
                                        Above Normal
                                    </option>

                                    <option value="High">
                                        High
                                    </option>

                                    <option value="RealTime">
                                        Highest
                                    </option>
                                </select>

                                <div class="fieldDescription">
                                    Sets the relative priority of the analysis ffmpeg process to other parallel operations
                                    (ie. transcoding, chapter detection, etc).
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="ProcessThreads">
                                    ffmpeg Threads
                                </label>
                                <input id="ProcessThreads" type="number" is="emby-input" min="0"
                                       max="16" />
                                <div class="fieldDescription">
                                    Number of simultaneous processes to use for ffmpeg operations.
                                    <br />
                                    This value is most often defined as 1 thread per CPU core,
                                    but setting a value of 0 (default) will use the maximum threads available.
                                </div>
                            </div>
                        </details>
                    </fieldset>

                    <fieldset class="verticalSection-extrabottompadding">
                        <legend>Playback</legend>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="AutoSkip" type="checkbox" is="emby-checkbox" />
                                <span>Automatically skip intros</span>
                            </label>

                            <div class="fieldDescription">
                                If checked, intros will be automatically skipped. If you access Jellyfin through a
                                reverse proxy, it must be configured to proxy web
                                sockets.<br />
                            </div>
                        </div>

                        <div id="divSkipFirstEpisode" class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
                                <span>Play intro for first episode of a season</span>
                            </label>

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

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="AutoSkipCredits" type="checkbox" is="emby-checkbox" />
                                <span>Automatically skip credits</span>
                            </label>

                            <div class="fieldDescription">
                                If checked, credits will be automatically skipped. If you access Jellyfin through a
                                reverse proxy, it must be configured to proxy web
                                sockets.<br />
                            </div>
                        </div>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="SkipButtonVisible" type="checkbox" is="emby-checkbox" />
                                <span>Show skip intro button</span>
                            </label>

                            <div class="fieldDescription">
                                If checked, a skip button will be displayed at the start of an episode's introduction.
                                <strong>Only applies to the web interface and compatible applications.</strong>
                                <br />
                            </div>
                        </div>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="PersistSkipButton" type="checkbox" is="emby-checkbox" />
                                <span>Display button for intro duration</span>
                            </label>

                            <div class="fieldDescription">
                                If checked, skip button will remain visible throught the intro (offset and timeout are ignored).
                                <br />
                                Note: If unchecked, button will only appear in the player controls after the set timeout.
                            </div>
                        </div>

                        <div id="divShowPromptAdjustment" class="inputContainer">
                            <label class="inputLabel inputLabelUnfocused" for="ShowPromptAdjustment">
                                Skip prompt offset (in seoncds)
                            </label>
                            <input id="ShowPromptAdjustment" type="number" is="emby-input" min="0" />
                            <div class="fieldDescription">
                                Seconds to display skip prompt before introduction begins.
                            </div>
                        </div>
                        <br />

                        <div id="divHidePromptAdjustment" class="inputContainer">
                            <label class="inputLabel inputLabelUnfocused" for="HidePromptAdjustment">
                                Skip prompt timeout (in seconds)
                            </label>
                            <input id="HidePromptAdjustment" type="number" is="emby-input" min="2" />
                            <div class="fieldDescription">
                                Seconds after introduction before skip prompt is hidden.
                            </div>
                        </div>
                        <br />

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

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

                            <br />
                            <div class="inputContainer">
                                <label class="inputLabel" for="SkipButtonIntroText">
                                    Skip intro button text
                                </label>
                                <input id="SkipButtonIntroText" type="text" is="emby-input" />
                                <div class="fieldDescription">
                                    Text to display in the skip intro button.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel" for="SkipButtonEndCreditsText">
                                    Skip end credits button text
                                </label>
                                <input id="SkipButtonEndCreditsText" type="text" is="emby-input" />
                                <div class="fieldDescription">
                                    Text to display in the skip end credits button.
                                </div>
                            </div>

                            <div id="divAutoSkipNotificationText" class="inputContainer">
                                <label class="inputLabel" for="AutoSkipNotificationText">
                                    Automatic skip notification message
                                </label>
                                <input id="AutoSkipNotificationText" type="text" is="emby-input" />
                                <div class="fieldDescription">
                                    Message shown after automatically skipping an introduction. Leave blank to disable notification.
                                </div>
                            </div>

                            <div id="divAutoSkipCreditsNotificationText" class="inputContainer">
                                <label class="inputLabel" for="AutoSkipCreditsNotificationText">
                                    Automatic skip notification message
                                </label>
                                <input id="AutoSkipCreditsNotificationText" type="text" is="emby-input" />
                                <div class="fieldDescription">
                                    Message shown after automatically skipping credits. Leave blank to disable notification.
                                </div>
                            </div>
                        </details>
                    </fieldset>

                    <div>
                        <button is="emby-button" type="submit" class="raised button-submit block emby-button">
                            <span>Save</span>
                        </button>
                    </div>
                </form>
                <br />

                <fieldset class="verticalSection-extrabottompadding">
                    <legend>Advanced</legend>

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

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

                    <details id="visualizer">
                        <summary>Manage Fingerprints</summary>

                        <br />
                        <h3 style="margin:0">Select episodes to manage</h3>
                        <br />
                        <select id="troubleshooterShow"></select>
                        <select id="troubleshooterSeason"></select>
                        <br />

                        <select id="troubleshooterEpisode1"></select>
                        <select id="troubleshooterEpisode2"></select>
                        <br />
                        <br />

                        <div id="timestampEditor" style="display:none">
                            <h3 style="margin:0">Introduction timestamp editor</h3>
                            <p style="margin:0">All times are in seconds.</p>

                            <p id="editLeftEpisodeTitle" style="margin-bottom:0"></p>
                            <input style="width:4em" type="number" min="0" id="editLeftEpisodeStart"> to
                            <input style="width:4em;margin-bottom:10px" type="number" min="0" id="editLeftEpisodeEnd">

                            <p id="editRightEpisodeTitle" style="margin-top:0;margin-bottom:0"></p>
                            <input style="width:4em" type="number" min="0" id="editRightEpisodeStart"> to
                            <input style="width:4em;margin-bottom:10px" type="number" min="0" id="editRightEpisodeEnd">
                            <br />
                            <br />

                            <button id="btnUpdateTimestamps" type="button">
                                Update timestamps
                            </button>
                            <br />
                        </div>

                        <div id="timestampErrorDiv" style="display:none">
                          <textarea id="timestampError" rows="2" cols="75" readonly></textarea>
                          <br />
                          <br />
                        </div>

                        <h3>Fingerprint Visualizer</h3>
                        <p>
                            Interactively compare the audio fingerprints of two episodes. <br />
                            The blue and red bar to the right of the fingerprint diff turns blue
                            when the corresponding fingerprint points are at least 80% similar.
                        </p>
                        <table>
                            <thead>
                                <td style="min-width: 100px; font-weight: bold">Key</td>
                                <td style="font-weight: bold">Function</td>
                            </thead>
                            <tbody>
                                <tr>
                                    <td>Up arrow</td>
                                    <td>
                                        Shift the left episode up by 0.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">Suggested shifts:</span>
                        <br />
                        <br />

                        <canvas id="troubleshooter" style="display:none;"></canvas>
                        <span id="timestampContainer">
                            <span id="timestamps"></span> <br />
                            <span id="intros"></span>
                        </span>
                        <br />
                        <br />

                        <button id="btnEraseSeasonTimestamps" type="button" style="display:none;">
                            Erase all timestamps for this season
                        </button>
                        <hr />

                        <button id="btnEraseIntroTimestamps">
                            Erase all introduction timestamps (globally)
                        </button>
                        <br />

                        <button id="btnEraseCreditTimestamps">
                            Erase all end credits timestamps (globally)
                        </button>
                    </details>
                </fieldset>
            </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 btnEraseIntroTimestamps = document.querySelector("button#btnEraseIntroTimestamps");
            var btnEraseCreditTimestamps = document.querySelector("button#btnEraseCreditTimestamps");

            // all plugin configuration fields that can be get or set with .value (i.e. strings or numbers).
            var configurationFields = [
                // analysis
                "MaxParallelism",
                "SelectedLibraries",
                "AnalysisPercent",
                "AnalysisLengthLimit",
                "MinimumIntroDuration",
                "MaximumIntroDuration",
                "MinimumCreditsDuration",
                "MaximumCreditsDuration",
                "EdlAction",
                "ProcessPriority",
                "ProcessThreads",
                // playback
                "ShowPromptAdjustment",
                "HidePromptAdjustment",
                "SecondsOfIntroToPlay",
                // internals
                "SilenceDetectionMaximumNoise",
                "SilenceDetectionMinimumDuration",
                // UI customization
                "SkipButtonIntroText",
                "SkipButtonEndCreditsText",
                "AutoSkipNotificationText",
                "AutoSkipCreditsNotificationText"
            ]

            var booleanConfigurationFields = [
                "AutoDetectIntros",
                "AutoDetectCredits",
                "AnalyzeSeasonZero",
                "RegenerateEdlFiles",
                "UseChromaprint",
                "CacheFingerprints",
                "AutoSkip",
                "AutoSkipCredits",
                "SkipFirstEpisode",
                "PersistSkipButton",
                "SkipButtonVisible"
            ]

            // visualizer elements
            var canvas = document.querySelector("canvas#troubleshooter");
            var selectShow = document.querySelector("select#troubleshooterShow");
            var selectSeason = document.querySelector("select#troubleshooterSeason");
            var selectEpisode1 = document.querySelector("select#troubleshooterEpisode1");
            var selectEpisode2 = document.querySelector("select#troubleshooterEpisode2");
            var txtOffset = document.querySelector("input#offset");
            var txtSuggested = document.querySelector("span#suggestedShifts");
            var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
            var timestampError = document.querySelector("textarea#timestampError");
            var timestampEditor = document.querySelector("#timestampEditor");
            var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
            var timeContainer = document.querySelector("span#timestampContainer");

            var windowHashInterval = 0;

            var autoSkip = document.querySelector("input#AutoSkip");
            var skipFirstEpisode = document.querySelector("div#divSkipFirstEpisode");
            var autoSkipNotificationText = document.querySelector("div#divAutoSkipNotificationText");
            var autoSkipCredits = document.querySelector("input#AutoSkipCredits");
            var autoSkipCreditsNotificationText = document.querySelector("div#divAutoSkipCreditsNotificationText");

            async function autoSkipChanged() {
                if (autoSkip.checked) {
                    skipFirstEpisode.style.display = 'unset';
                    autoSkipNotificationText.style.display = 'unset';
                } else {
                    skipFirstEpisode.style.display = 'none';
                    autoSkipNotificationText.style.display = 'none';
                }
            }

            autoSkip.addEventListener("change", autoSkipChanged);

            async function autoSkipCreditsChanged() {
                if (autoSkipCredits.checked) {
                    autoSkipCreditsNotificationText.style.display = 'unset';
                } else {
                    autoSkipCreditsNotificationText.style.display = 'none';
                }
            }

            autoSkipCredits.addEventListener("change", autoSkipCreditsChanged);

            var persistSkip = document.querySelector("input#PersistSkipButton");
            var showAdjustment = document.querySelector("div#divShowPromptAdjustment");
            var hideAdjustment = document.querySelector("div#divHidePromptAdjustment");

            // prevent setting unavailable options
            async function persistSkipChanged() {
                if (persistSkip.checked) {
                    showAdjustment.style.display = 'none';
                    hideAdjustment.style.display = 'none';
                } else {
                    showAdjustment.style.display = 'unset';
                    hideAdjustment.style.display = 'unset';
                }
            }

            persistSkip.addEventListener("change", persistSkipChanged);

            // when the fingerprint visualizer opens, populate show names
            async function visualizerToggled() {
                if (!visualizer.open) {
                    return;
                }

                // ensure the series select is empty
                while (selectShow.options.length > 0) {
                    selectShow.remove(0);
                }

                Dashboard.showLoadingMsg();

                shows = await getJson("Intros/Shows");

                var sorted = [];
                for (var series in shows) { sorted.push(series); }
                sorted.sort();

                for (var show of sorted) {
                    addItem(selectShow, show, show);
                }

                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");
                }
            }

            // show changed, populate seasons
            async function showChanged() {
                clearSelect(selectSeason);
                btnSeasonEraseTimestamps.style.display = "none";
                clearSelect(selectEpisode1);
                clearSelect(selectEpisode2);

                // add all seasons from this show to the season select
                for (var season of shows[selectShow.value]) {
                    addItem(selectSeason, season, season);
                }

                selectSeason.value = "";
            }

            // season changed, reload all episodes
            async function seasonChanged() {
                const url = "Intros/Show/" + encodeURI(selectShow.value) + "/" + selectSeason.value;
                const episodes = await getJson(url);

                clearSelect(selectEpisode1);
                clearSelect(selectEpisode2);
                btnSeasonEraseTimestamps.style.display = "unset";

                let i = 1;
                for (let episode of episodes) {
                    const strI = i.toLocaleString("en", { minimumIntegerDigits: 2, maximumFractionDigits: 0 });
                    addItem(selectEpisode1, strI + ": " + episode.Name, episode.Id);
                    addItem(selectEpisode2, strI + ": " + episode.Name, episode.Id);
                    i++;
                }

                setTimeout(() => {
                    selectEpisode1.selectedIndex = 0;
                    selectEpisode2.selectedIndex = 1;
                    episodeChanged();
                }, 100);
            }

            // episode changed, get fingerprints & calculate diff
            async function episodeChanged() {
                if (!selectEpisode1.value || !selectEpisode2.value) {
                    return;
                }

                Dashboard.showLoadingMsg();

                timestampError.value = "";
                canvas.style.display = "none";

                lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
                if (lhs === undefined) {
                    timestampError.value += "Error: " + selectEpisode1.value + " fingerprints failed!\n";
                } else if (lhs === null) {
                    timestampError.value += "Error: " + selectEpisode1.value + " fingerprints missing!\n";
                }

                rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint");
                if (rhs === undefined) {
                    timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
                } else if (rhs === null) {
                    timestampError.value += "Error: " + selectEpisode2.value + " fingerprints missing!\n";
                }

                if (timestampError.value == "") {
                    timestampErrorDiv.style.display = "none";
                } else {
                    timestampErrorDiv.style.display = "unset";
                }

                Dashboard.hideLoadingMsg();

                txtOffset.value = "0";
                refreshBounds();
                renderTroubleshooter();
                findExactMatches();
                updateTimestampEditor();
            }

            // updates the timestamp editor
            async function updateTimestampEditor() {
                // Get the title and ID of the left and right episodes
                const leftEpisode = selectEpisode1.options[selectEpisode1.selectedIndex];
                const rightEpisode = selectEpisode2.options[selectEpisode2.selectedIndex];

                // Try to get the timestamps of each intro, falling back a default value of zero if no intro was found
                let leftEpisodeIntro = await getJson("Episode/" + leftEpisode.value + "/IntroTimestamps/v1");
                if (leftEpisodeIntro === null) {
                    leftEpisodeIntro = { IntroStart: 0, IntroEnd: 0 };
                }

                let rightEpisodeIntro = await getJson("Episode/" + rightEpisode.value + "/IntroTimestamps/v1");
                if (rightEpisodeIntro === null) {
                    rightEpisodeIntro = { IntroStart: 0, IntroEnd: 0 };
                }

                // Update the editor for the first and second episodes
                timestampEditor.style.display = "unset";
                document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text;
                document.querySelector("#editLeftEpisodeStart").value = Math.round(leftEpisodeIntro.IntroStart);
                document.querySelector("#editLeftEpisodeEnd").value = Math.round(leftEpisodeIntro.IntroEnd);

                document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text;
                document.querySelector("#editRightEpisodeStart").value = Math.round(rightEpisodeIntro.IntroStart);
                document.querySelector("#editRightEpisodeEnd").value = Math.round(rightEpisodeIntro.IntroEnd);
            }

            // 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().substr(14, 5);
            }

            // erase all intro/credits timestamps
            function eraseTimestamps(mode) {
                const lower = mode.toLocaleLowerCase();
                const title = "Confirm timestamp erasure";
                const body = "Are you sure you want to erase all previously discovered " +
                    mode.toLocaleLowerCase() +
                    " timestamps?";

                Dashboard.confirm(
                    body,
                    title,
                    (result) => {
                        if (!result) {
                            return;
                        }

                        fetchWithAuth("Intros/EraseTimestamps?mode=" + mode, "POST", null);

                        Dashboard.alert(mode + " timestamps erased");
                    });
            }

            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];
                        }

                        autoSkipChanged();
                        autoSkipCreditsChanged();
                        persistSkipChanged();

                        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);
            txtOffset.addEventListener("change", renderTroubleshooter);
            selectShow.addEventListener("change", showChanged);
            selectSeason.addEventListener("change", seasonChanged);
            selectEpisode1.addEventListener("change", episodeChanged);
            selectEpisode2.addEventListener("change", episodeChanged);
            btnEraseIntroTimestamps.addEventListener("click", (e) => {
                eraseTimestamps("Introduction");
                e.preventDefault();
            });
            btnEraseCreditTimestamps.addEventListener("click", (e) => {
                eraseTimestamps("Credits");
                e.preventDefault();
            });
            btnSeasonEraseTimestamps.addEventListener("click", () => {
                Dashboard.confirm(
                    "Are you sure you want to erase all timestamps for this season?",
                    "Confirm timestamp erasure",
                    (result) => {
                        if (!result) {
                            return;
                        }

                        const show = selectShow.value;
                        const season = selectSeason.value;

                        const url = "Intros/Show/" + encodeURIComponent(show) + "/" + encodeURIComponent(season);
                        fetchWithAuth(url, "DELETE", null);

                        Dashboard.alert("Erased timestamps for " + season + " of " + show);
                    }
                );
            });
            btnUpdateTimestamps.addEventListener("click", () => {
                const lhsId = selectEpisode1.options[selectEpisode1.selectedIndex].value;
                const newLhsIntro = {
                    IntroStart: document.querySelector("#editLeftEpisodeStart").value,
                    IntroEnd: document.querySelector("#editLeftEpisodeEnd").value,
                };

                const rhsId = selectEpisode2.options[selectEpisode2.selectedIndex].value;
                const newRhsIntro = {
                    IntroStart: document.querySelector("#editRightEpisodeStart").value,
                    IntroEnd: document.querySelector("#editRightEpisodeEnd").value,
                };

                fetchWithAuth("Intros/Episode/" + lhsId + "/UpdateIntroTimestamps", "POST", JSON.stringify(newLhsIntro));
                fetchWithAuth("Intros/Episode/" + rhsId + "/UpdateIntroTimestamps", "POST", JSON.stringify(newRhsIntro));

                Dashboard.alert("New introduction timestamps saved");
            });
            document.addEventListener("keydown", keyDown);
            windowHashInterval = setInterval(checkWindowHash, 2500);

            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";
            });
        </script>
    </div>
</body>

</html>