Make credit skipping functionality available

This commit is contained in:
ConfusedPolarBear 2023-03-04 00:15:26 -06:00
parent 04903541e9
commit 8a9b630e68
5 changed files with 111 additions and 34 deletions

View File

@ -163,9 +163,14 @@ public class PluginConfiguration : BasePluginConfiguration
// ===== Localization support ===== // ===== Localization support =====
/// <summary> /// <summary>
/// Gets or sets the text to display in the Skip Intro button. /// Gets or sets the text to display in the skip button in introduction mode.
/// </summary> /// </summary>
public string SkipButtonText { get; set; } = "Skip Intro"; public string SkipButtonIntroText { get; set; } = "Skip Intro";
/// <summary>
/// Gets or sets the text to display in the skip button in end credits mode.
/// </summary>
public string SkipButtonEndCreditsText { get; set; } = "Next";
/// <summary> /// <summary>
/// Gets or sets the notification text sent after automatically skipping an introduction. /// Gets or sets the notification text sent after automatically skipping an introduction.

View File

@ -9,11 +9,13 @@ public class UserInterfaceConfiguration
/// Initializes a new instance of the <see cref="UserInterfaceConfiguration"/> class. /// Initializes a new instance of the <see cref="UserInterfaceConfiguration"/> class.
/// </summary> /// </summary>
/// <param name="visible">Skip button visibility.</param> /// <param name="visible">Skip button visibility.</param>
/// <param name="text">Skip button text.</param> /// <param name="introText">Skip button intro text.</param>
public UserInterfaceConfiguration(bool visible, string text) /// <param name="creditsText">Skip button end credits text.</param>
public UserInterfaceConfiguration(bool visible, string introText, string creditsText)
{ {
SkipButtonVisible = visible; SkipButtonVisible = visible;
SkipButtonText = text; SkipButtonIntroText = introText;
SkipButtonEndCreditsText = creditsText;
} }
/// <summary> /// <summary>
@ -22,7 +24,12 @@ public class UserInterfaceConfiguration
public bool SkipButtonVisible { get; set; } public bool SkipButtonVisible { get; set; }
/// <summary> /// <summary>
/// Gets or sets the text to display in the skip intro button. /// Gets or sets the text to display in the skip intro button in introduction mode.
/// </summary> /// </summary>
public string SkipButtonText { get; set; } public string SkipButtonIntroText { get; set; }
/// <summary>
/// Gets or sets the text to display in the skip intro button in end credits mode.
/// </summary>
public string SkipButtonEndCreditsText { get; set; }
} }

View File

@ -265,15 +265,25 @@
<summary>User Interface Customization</summary> <summary>User Interface Customization</summary>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel" for="SkipButtonText"> <label class="inputLabel" for="SkipButtonIntroText">
Skip intro button text Skip intro button text
</label> </label>
<input id="SkipButtonText" type="text" is="emby-input" /> <input id="SkipButtonIntroText" type="text" is="emby-input" />
<div class="fieldDescription"> <div class="fieldDescription">
Text to display in the skip intro button. Text to display in the skip intro button.
</div> </div>
</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 class="inputContainer"> <div class="inputContainer">
<label class="inputLabel" for="AutoSkipNotificationText"> <label class="inputLabel" for="AutoSkipNotificationText">
Automatic skip notification message Automatic skip notification message
@ -440,7 +450,9 @@
// internals // internals
"SilenceDetectionMaximumNoise", "SilenceDetectionMaximumNoise",
"SilenceDetectionMinimumDuration", "SilenceDetectionMinimumDuration",
"SkipButtonText", // UI customization
"SkipButtonIntroText",
"SkipButtonEndCreditsText",
"AutoSkipNotificationText" "AutoSkipNotificationText"
] ]

View File

@ -34,7 +34,7 @@ introSkipper.fetchWrapper = async function (...args) {
introSkipper.d(path); introSkipper.d(path);
let id = path.split("/")[2]; let id = path.split("/")[2];
introSkipper.skipSegments = await introSkipper.secureFetch(`Episode/${id}/IntroTimestamps/v1`); introSkipper.skipSegments = await introSkipper.secureFetch(`Episode/${id}/IntroSkipperSegments`);
introSkipper.d("successfully retrieved skip segments"); introSkipper.d("successfully retrieved skip segments");
introSkipper.d(introSkipper.skipSegments); introSkipper.d(introSkipper.skipSegments);
@ -151,10 +151,12 @@ introSkipper.injectButton = async function () {
button.addEventListener("click", introSkipper.doSkip); button.addEventListener("click", introSkipper.doSkip);
button.innerHTML = ` button.innerHTML = `
<button is="paper-icon-button-light" class="btnSkipIntro paper-icon-button-light"> <button is="paper-icon-button-light" class="btnSkipIntro paper-icon-button-light">
<span id="btnSkipIntroText"></span> <span id="btnSkipSegmentText"></span>
<span class="material-icons skip_next"></span> <span class="material-icons skip_next"></span>
</button> </button>
`; `;
button.dataset["intro_text"] = config.SkipButtonIntroText;
button.dataset["credits_text"] = config.SkipButtonEndCreditsText;
/* /*
* Alternative workaround for #44. Jellyfin's video component registers a global click handler * Alternative workaround for #44. Jellyfin's video component registers a global click handler
@ -166,37 +168,60 @@ introSkipper.injectButton = async function () {
// Append the button to the video OSD // Append the button to the video OSD
let controls = document.querySelector("div#videoOsdPage"); let controls = document.querySelector("div#videoOsdPage");
controls.appendChild(button); controls.appendChild(button);
}
document.querySelector("#btnSkipIntroText").textContent = config.SkipButtonText; /** Get the currently playing skippable segment. */
introSkipper.getCurrentSegment = function (position) {
for (let key in introSkipper.skipSegments) {
const segment = introSkipper.skipSegments[key];
if (position >= segment.ShowSkipPromptAt && position < segment.HideSkipPromptAt) {
segment["SegmentType"] = key;
return segment;
}
}
return { "SegmentType": "None" };
} }
/** Playback position changed, check if the skip button needs to be displayed. */ /** Playback position changed, check if the skip button needs to be displayed. */
introSkipper.videoPositionChanged = function () { introSkipper.videoPositionChanged = function () {
// Ensure a skip segment was found.
if (!introSkipper.skipSegments || !introSkipper.skipSegments.Valid) {
return;
}
const skipButton = document.querySelector("#skipIntro"); const skipButton = document.querySelector("#skipIntro");
if (!skipButton) { if (!skipButton) {
return; return;
} }
const position = introSkipper.videoPlayer.currentTime; const segment = introSkipper.getCurrentSegment(introSkipper.videoPlayer.currentTime);
if (position >= introSkipper.skipSegments.ShowSkipPromptAt && switch (segment["SegmentType"]) {
position < introSkipper.skipSegments.HideSkipPromptAt) { case "None":
skipButton.classList.remove("hide"); skipButton.classList.add("hide");
return; return;
case "Introduction":
skipButton.querySelector("#btnSkipSegmentText").textContent =
skipButton.dataset["intro_text"];
break;
case "Credits":
skipButton.querySelector("#btnSkipSegmentText").textContent =
skipButton.dataset["credits_text"];
break;
} }
skipButton.classList.add("hide"); skipButton.classList.remove("hide");
} }
/** Seeks to the end of the intro. */ /** Seeks to the end of the intro. */
introSkipper.doSkip = function (e) { introSkipper.doSkip = function (e) {
introSkipper.d("Skipping intro"); introSkipper.d("Skipping intro");
introSkipper.d(introSkipper.skipSegments); introSkipper.d(introSkipper.skipSegments);
introSkipper.videoPlayer.currentTime = introSkipper.skipSegments.IntroEnd;
const segment = introSkipper.getCurrentSegment(introSkipper.videoPlayer.currentTime);
if (segment["SegmentType"] === "None") {
console.warn("[intro skipper] doSkip() called without an active segment");
return;
}
introSkipper.videoPlayer.currentTime = segment["IntroEnd"];
} }
/** Tests if an element with the provided selector exists. */ /** Tests if an element with the provided selector exists. */

View File

@ -44,15 +44,33 @@ public class SkipIntroController : ControllerBase
return NotFound(); return NotFound();
} }
// Populate the prompt show/hide times.
var config = Plugin.Instance!.Configuration;
intro.ShowSkipPromptAt = Math.Max(0, intro.IntroStart - config.ShowPromptAdjustment);
intro.HideSkipPromptAt = intro.IntroStart + config.HidePromptAdjustment;
intro.IntroEnd -= config.SecondsOfIntroToPlay;
return intro; return intro;
} }
/// <summary>
/// Gets a dictionary of all skippable segments.
/// </summary>
/// <param name="id">Media ID.</param>
/// <response code="200">Skippable segments dictionary.</response>
/// <returns>Dictionary of skippable segments.</returns>
[HttpGet("Episode/{id}/IntroSkipperSegments")]
public ActionResult<Dictionary<AnalysisMode, Intro>> GetSkippableSegments([FromRoute] Guid id)
{
var segments = new Dictionary<AnalysisMode, Intro>();
if (GetIntro(id, AnalysisMode.Introduction) is Intro intro)
{
segments[AnalysisMode.Introduction] = intro;
}
if (GetIntro(id, AnalysisMode.Credits) is Intro credits)
{
segments[AnalysisMode.Credits] = credits;
}
return segments;
}
/// <summary>Lookup and return the skippable timestamps for the provided item.</summary> /// <summary>Lookup and return the skippable timestamps for the provided item.</summary>
/// <param name="id">Unique identifier of this episode.</param> /// <param name="id">Unique identifier of this episode.</param>
/// <param name="mode">Mode.</param> /// <param name="mode">Mode.</param>
@ -65,8 +83,15 @@ public class SkipIntroController : ControllerBase
Plugin.Instance!.Intros[id] : Plugin.Instance!.Intros[id] :
Plugin.Instance!.Credits[id]; Plugin.Instance!.Credits[id];
// A copy is returned to avoid mutating the original Intro object stored in the dictionary. // Operate on a copy to avoid mutating the original Intro object stored in the dictionary.
return new(timestamp); var segment = new Intro(timestamp);
var config = Plugin.Instance!.Configuration;
segment.ShowSkipPromptAt = Math.Max(0, segment.IntroStart - config.ShowPromptAdjustment);
segment.HideSkipPromptAt = segment.IntroStart + config.HidePromptAdjustment;
segment.IntroEnd -= config.SecondsOfIntroToPlay;
return segment;
} }
catch (KeyNotFoundException) catch (KeyNotFoundException)
{ {
@ -145,6 +170,9 @@ public class SkipIntroController : ControllerBase
public ActionResult<UserInterfaceConfiguration> GetUserInterfaceConfiguration() public ActionResult<UserInterfaceConfiguration> GetUserInterfaceConfiguration()
{ {
var config = Plugin.Instance!.Configuration; var config = Plugin.Instance!.Configuration;
return new UserInterfaceConfiguration(config.SkipButtonVisible, config.SkipButtonText); return new UserInterfaceConfiguration(
config.SkipButtonVisible,
config.SkipButtonIntroText,
config.SkipButtonEndCreditsText);
} }
} }