1676 lines
101 KiB
HTML
1676 lines
101 KiB
HTML
<!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>
|