Make credit skipping functionality available
This commit is contained in:
parent
04903541e9
commit
8a9b630e68
@ -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.
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -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. */
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user