Skip to content
This repository has been archived by the owner on Sep 25, 2023. It is now read-only.

Commit

Permalink
Inject skip button into web interface
Browse files Browse the repository at this point in the history
Text for the skip button and auto skip message can now be customized

Closes #104, 105
  • Loading branch information
ConfusedPolarBear committed Nov 11, 2022
1 parent ce52a0b commit f4e84d4
Show file tree
Hide file tree
Showing 9 changed files with 435 additions and 24 deletions.
5 changes: 5 additions & 0 deletions ACKNOWLEDGEMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Intro Skipper is made possible by the following open source projects:

* License: MIT
* [acoustid-match](https://github.com/dnknth/acoustid-match)
* [JellyScrub](https://github.com/nicknsy/jellyscrub)
9 changes: 6 additions & 3 deletions ConfusedPolarBear.Plugin.IntroSkipper/AutoSkip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,18 +163,21 @@ private void PlaybackTimer_Elapsed(object? sender, ElapsedEventArgs e)
}

// Notify the user that an introduction is being skipped for them.
_sessionManager.SendMessageCommand(
var notificationText = Plugin.Instance!.Configuration.AutoSkipNotificationText;
if (!string.IsNullOrWhiteSpace(notificationText))
{
_sessionManager.SendMessageCommand(
session.Id,
session.Id,
new MessageCommand()
{
Header = string.Empty, // some clients require header to be a string instead of null
Text = "Automatically skipped intro",
Text = notificationText,
TimeoutMs = 2000,
},
CancellationToken.None);
}

// Send the seek command
_logger.LogDebug("Sending seek command to {Session}", deviceId);

var introEnd = (long)intro.IntroEnd - Plugin.Instance!.Configuration.SecondsOfIntroToPlay;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ public PluginConfiguration()

// ===== Playback settings =====

/// <summary>
/// Gets or sets a value indicating whether to show the skip intro button.
/// </summary>
public bool SkipButtonVisible { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether introductions should be automatically skipped.
/// </summary>
Expand Down Expand Up @@ -127,4 +132,16 @@ public PluginConfiguration()
/// Gets or sets the minimum duration of audio (in seconds) that is considered silent.
/// </summary>
public double SilenceDetectionMinimumDuration { get; set; } = 0.33;

// ===== Localization support =====

/// <summary>
/// Gets or sets the text to display in the Skip Intro button.
/// </summary>
public string SkipButtonText { get; set; } = "Skip Intro";

/// <summary>
/// Gets or sets the notification text sent after automatically skipping an introduction.
/// </summary>
public string AutoSkipNotificationText { get; set; } = "Automatically skipped intro";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace ConfusedPolarBear.Plugin.IntroSkipper.Configuration;

/// <summary>
/// User interface configuration.
/// </summary>
public class UserInterfaceConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="UserInterfaceConfiguration"/> class.
/// </summary>
/// <param name="visible">Skip button visibility.</param>
/// <param name="text">Skip button text.</param>
public UserInterfaceConfiguration(bool visible, string text)
{
SkipButtonVisible = visible;
SkipButtonText = text;
}

/// <summary>
/// Gets or sets a value indicating whether to show the skip intro button.
/// </summary>
public bool SkipButtonVisible { get; set; }

/// <summary>
/// Gets or sets the text to display in the skip intro button.
/// </summary>
public string SkipButtonText { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,28 @@
<fieldset class="verticalSection-extrabottompadding">
<legend>Playback</legend>

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

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

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

<div class="fieldDescription">
If checked, intros will be automatically skipped. If you access Jellyfin through a reverse proxy, it must be configured to proxy web
If checked, intros will be automatically skipped. If you access Jellyfin through a
reverse proxy, it must be configured to proxy web
sockets.<br />
</div>
</div>
Expand Down Expand Up @@ -246,6 +260,30 @@
Seconds of introduction that should be played. Defaults to 2.
</div>
</div>

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

<div class="inputContainer">
<label class="inputLabel" for="SkipButtonText">
Skip intro button text
</label>
<input id="SkipButtonText" 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="AutoSkipNotificationText">
Automatic skip notification message
</label>
<input id="AutoSkipNotificationText" type="text" is="emby-input" />
<div class="fieldDescription">
Message sent to a user after automatically skipping an introduction. Leave blank to skip sending a notification.
</div>
</div>
</details>
</fieldset>

<div>
Expand Down Expand Up @@ -395,13 +433,16 @@ <h3>Fingerprint Visualizer</h3>
// internals
"SilenceDetectionMaximumNoise",
"SilenceDetectionMinimumDuration",
"SkipButtonText",
"AutoSkipNotificationText"
]

var booleanConfigurationFields = [
"AnalyzeSeasonZero",
"RegenerateEdlFiles",
"AutoSkip",
"SkipFirstEpisode"
"SkipFirstEpisode",
"SkipButtonVisible",
]

// visualizer elements
Expand Down
224 changes: 224 additions & 0 deletions ConfusedPolarBear.Plugin.IntroSkipper/Configuration/inject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
let nowPlayingItemSkipSegments = {};
let videoPlayer = {};

function d(msg) {
console.debug("[intro skipper]", msg);
}

/** Setup event listeners */
function setup() {
document.addEventListener("viewshow", viewshow);
d("Registered hooks");
}

/**
* Event handler that runs whenever the current view changes.
* Used to detect the start of video playback.
*/
function viewshow() {
const location = window.location.hash;
d("Location changed to " + location);

if (location !== "#!/video") {
d("Ignoring location change");
return;
}

d("Adding button CSS and element");
injectSkipButtonCss();
injectSkipButtonElement();

d("Hooking video timeupdate");
videoPlayer = document.querySelector("video");
videoPlayer.addEventListener("timeupdate", videoPositionChanged);

d("Getting timestamps of introduction");
getIntroTimestamps();
}

/** Get skip button UI configuration */
async function getUserInterfaceConfiguration() {
const reqInit = {
headers: {
"Authorization": "MediaBrowser Token=" + ApiClient.accessToken()
}
}

const res = await fetch("/Intros/UserInterfaceConfiguration", reqInit);
return await res.json();
}

/**
* Injects the CSS used by the skip intro button.
* Calling this function is a no-op if the CSS has already been injected.
*/
function injectSkipButtonCss() {
if (testElement("style#introSkipperCss"))
{
d("CSS already added");
return;
}

d("Adding CSS");

let styleElement = document.createElement("style");
styleElement.id = "introSkipperCss";
styleElement.innerText = `
@media (hover:hover) and (pointer:fine) {
#skipIntro .paper-icon-button-light:hover:not(:disabled) {
color: black !important;
background-color: rgba(47, 93, 98, 0) !important;
}
}
#skipIntro.upNextContainer {
width: unset;
}
#skipIntro {
padding: 0 1px;
position: absolute;
right: 10em;
bottom: 9em;
background-color: rgba(25, 25, 25, 0.66);
border: 1px solid;
border-radius: 0px;
display: inline-block;
cursor: pointer;
box-shadow: inset 0 0 0 0 #f9f9f9;
-webkit-transition: ease-out 0.4s;
-moz-transition: ease-out 0.4s;
transition: ease-out 0.4s;
}
@media (max-width: 1080px) {
#skipIntro {
right: 10%;
}
}
#skipIntro:hover {
box-shadow: inset 400px 0 0 0 #f9f9f9;
-webkit-transition: ease-in 1s;
-moz-transition: ease-in 1s;
transition: ease-in 1s;
}
`;
document.querySelector("head").appendChild(styleElement);
}

/**
* Inject the skip intro button into the video player.
* Calling this function is a no-op if the CSS has already been injected.
*/
async function injectSkipButtonElement() {
if (testElement(".btnSkipIntro")) {
d("Button already added");
return;
}

d("Adding button");

let config = await getUserInterfaceConfiguration();
if (!config.SkipButtonVisible) {
d("Not adding button: not visible");
return;
}

// Construct the skip button div
const button = document.createElement("div");
button.id = "skipIntro"
button.classList.add("hide");
button.addEventListener("click", skipIntro);
button.innerHTML = `
<button is="paper-icon-button-light" class="btnSkipIntro paper-icon-button-light">
<span id="btnSkipIntroText"></span>
<span class="material-icons skip_next"></span>
</button>
`;

/*
* Alternative workaround for #44. Jellyfin's video component registers a global click handler
* (located at src/controllers/playback/video/index.js:1492) that pauses video playback unless
* the clicked element has a parent with the class "videoOsdBottom" or "upNextContainer".
*/
button.classList.add("upNextContainer");

// Append the button to the video OSD
let controls = document.querySelector("div#videoOsdPage");
controls.appendChild(button);

document.querySelector("#btnSkipIntroText").textContent = config.SkipButtonText;
}

/** Gets the introduction timestamps of the currently playing item. */
async function getIntroTimestamps() {
let id = await getNowPlayingItemId();

const address = ApiClient.serverAddress();

const url = `${address}/Episode/${id}/IntroTimestamps`;
const reqInit = {
headers: {
"Authorization": `MediaBrowser Token=${ApiClient.accessToken()}`
}
};

fetch(url, reqInit).then(r => {
if (!r.ok) {
return;
}

return r.json();
}).then(intro => {
nowPlayingItemSkipSegments = intro;
});
}

/** Playback position changed, check if the skip button needs to be displayed. */
function videoPositionChanged() {
// Ensure a skip segment was found.
if (!nowPlayingItemSkipSegments?.Valid) {
return;
}

const skipButton = document.querySelector("#skipIntro");
if (!skipButton) {
return;
}

const position = videoPlayer.currentTime;
if (position >= nowPlayingItemSkipSegments.ShowSkipPromptAt &&
position < nowPlayingItemSkipSegments.HideSkipPromptAt) {
skipButton.classList.remove("hide");
return;
}

skipButton.classList.add("hide");
}

/** Seeks to the end of the intro. */
function skipIntro(e) {
d("Skipping intro");
d(nowPlayingItemSkipSegments);
videoPlayer.currentTime = nowPlayingItemSkipSegments.IntroEnd;
}

/** Looks up the ID of the currently playing item. */
async function getNowPlayingItemId() {
d("Looking up ID of currently playing item");

let id = await ApiClient.getCurrentUserId();

let sessions = await ApiClient.getSessions();
let filtered = sessions.filter(x => (x.UserId === id) && x.NowPlayingItem);

d("Filtered " + sessions.length + " sessions down to " + filtered.length);

return filtered[0].NowPlayingItem.Id;
}

/** Tests if an element with the provided selector exists. */
function testElement(selector) { return document.querySelector(selector); }

setup();

0 comments on commit f4e84d4

Please sign in to comment.