7519f149f5
It isn't actually this easy, right? Revert "It isn't actually this easy, right?" This reverts commit d3cdb654689fd47e4ef65c7db05ee19be9368268. Revert "Update configPage.html" This reverts commit ae5d1828a4fa0dfd01f049a178bd839e9dbb5e25. dfgd
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 />
|
|
<i>
|
|
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.
|
|
</i>
|
|
</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">
|
|
<p>
|
|
<textarea id="timestampError" rows="2" cols="75" readonly></textarea>
|
|
</p>
|
|
</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" />
|
|
<p>
|
|
<span id="suggestedShifts">
|
|
<span>Suggested shifts: </span>
|
|
</span>
|
|
</p>
|
|
<canvas id="troubleshooter" style="display: none"></canvas>
|
|
<span id="timestampContainer">
|
|
<span id="timestamps"></span>
|
|
<br />
|
|
<span id="intros"></span>
|
|
</span>
|
|
<br />
|
|
</div>
|
|
|
|
<div id="eraseSeasonContainer" style="display: none">
|
|
<p>
|
|
<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>
|
|
</p>
|
|
</div>
|
|
|
|
<div id="eraseMovieContainer" style="display: none">
|
|
<p>
|
|
<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>
|
|
</p>
|
|
</div>
|
|
|
|
<div style="display: flex; align-items: center;">
|
|
<p>
|
|
<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>
|
|
</p>
|
|
<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>
|
|
</details>
|
|
|
|
<details id="support">
|
|
<summary>Intro Skipper Support Log</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);
|
|
|
|
var remainingSecondsOfIntro = document.getElementById("RemainingSecondsOfIntro")
|
|
|
|
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;
|
|
remainingSecondsOfIntro.value = 0;
|
|
}
|
|
}
|
|
|
|
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 segment 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>
|