2022-11-25 01:37:27 -06:00
|
|
|
let introSkipper = {
|
|
|
|
skipSegments: {},
|
|
|
|
videoPlayer: {},
|
2022-11-06 21:20:52 -06:00
|
|
|
|
2022-11-25 01:37:27 -06:00
|
|
|
// .bind() is used here to prevent illegal invocation errors
|
|
|
|
originalFetch: window.fetch.bind(window),
|
|
|
|
};
|
|
|
|
|
|
|
|
introSkipper.d = function (msg) {
|
2022-11-06 21:20:52 -06:00
|
|
|
console.debug("[intro skipper]", msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Setup event listeners */
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.setup = function () {
|
|
|
|
document.addEventListener("viewshow", introSkipper.viewShow);
|
|
|
|
window.fetch = introSkipper.fetchWrapper;
|
|
|
|
introSkipper.d("Registered hooks");
|
2022-11-06 21:20:52 -06:00
|
|
|
}
|
|
|
|
|
2022-11-17 01:19:44 -06:00
|
|
|
/** Wrapper around fetch() that retrieves skip segments for the currently playing item. */
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.fetchWrapper = async function (...args) {
|
2022-11-17 01:19:44 -06:00
|
|
|
// Based on JellyScrub's trickplay.js
|
|
|
|
let [resource, options] = args;
|
2022-11-25 01:37:27 -06:00
|
|
|
let response = await introSkipper.originalFetch(resource, options);
|
2022-11-17 01:19:44 -06:00
|
|
|
|
|
|
|
// Bail early if this isn't a playback info URL
|
2022-11-25 01:37:27 -06:00
|
|
|
try {
|
|
|
|
let path = new URL(resource).pathname;
|
|
|
|
if (!path.includes("/PlaybackInfo")) {
|
|
|
|
return response;
|
|
|
|
}
|
2022-11-17 01:19:44 -06:00
|
|
|
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.d("retrieving skip segments from URL");
|
|
|
|
introSkipper.d(path);
|
2022-11-17 01:19:44 -06:00
|
|
|
|
|
|
|
let id = path.split("/")[2];
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.skipSegments = await introSkipper.secureFetch(`Episode/${id}/IntroTimestamps/v1`);
|
2022-11-17 01:19:44 -06:00
|
|
|
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.d("successfully retrieved skip segments");
|
|
|
|
introSkipper.d(introSkipper.skipSegments);
|
2022-11-17 01:19:44 -06:00
|
|
|
}
|
2022-11-25 01:37:27 -06:00
|
|
|
catch (e) {
|
|
|
|
console.error("unable to get skip segments from", resource, e);
|
2022-11-17 01:19:44 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
2022-11-06 21:20:52 -06:00
|
|
|
/**
|
|
|
|
* Event handler that runs whenever the current view changes.
|
|
|
|
* Used to detect the start of video playback.
|
|
|
|
*/
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.viewShow = function () {
|
2022-11-06 21:20:52 -06:00
|
|
|
const location = window.location.hash;
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.d("Location changed to " + location);
|
2022-11-06 21:20:52 -06:00
|
|
|
|
|
|
|
if (location !== "#!/video") {
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.d("Ignoring location change");
|
2022-11-06 21:20:52 -06:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.d("Adding button CSS and element");
|
|
|
|
introSkipper.injectCss();
|
|
|
|
introSkipper.injectButton();
|
2022-11-06 21:20:52 -06:00
|
|
|
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.d("Hooking video timeupdate");
|
|
|
|
introSkipper.videoPlayer = document.querySelector("video");
|
|
|
|
introSkipper.videoPlayer.addEventListener("timeupdate", introSkipper.videoPositionChanged);
|
2022-11-06 21:20:52 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Injects the CSS used by the skip intro button.
|
|
|
|
* Calling this function is a no-op if the CSS has already been injected.
|
|
|
|
*/
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.injectCss = function () {
|
|
|
|
if (introSkipper.testElement("style#introSkipperCss")) {
|
|
|
|
introSkipper.d("CSS already added");
|
2022-11-06 21:20:52 -06:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.d("Adding CSS");
|
2022-11-06 21:20:52 -06:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.injectButton = async function () {
|
|
|
|
if (introSkipper.testElement(".btnSkipIntro")) {
|
|
|
|
introSkipper.d("Button already added");
|
2022-11-06 21:20:52 -06:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.d("Adding button");
|
2022-11-06 21:20:52 -06:00
|
|
|
|
2022-11-25 01:37:27 -06:00
|
|
|
let config = await introSkipper.secureFetch("Intros/UserInterfaceConfiguration");
|
2022-11-06 21:20:52 -06:00
|
|
|
if (!config.SkipButtonVisible) {
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.d("Not adding button: not visible");
|
2022-11-06 21:20:52 -06:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Construct the skip button div
|
|
|
|
const button = document.createElement("div");
|
|
|
|
button.id = "skipIntro"
|
|
|
|
button.classList.add("hide");
|
2022-11-25 01:37:27 -06:00
|
|
|
button.addEventListener("click", introSkipper.doSkip);
|
2022-11-06 21:20:52 -06:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Playback position changed, check if the skip button needs to be displayed. */
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.videoPositionChanged = function () {
|
2022-11-06 21:20:52 -06:00
|
|
|
// Ensure a skip segment was found.
|
2022-11-25 01:37:27 -06:00
|
|
|
if (!introSkipper.skipSegments?.Valid) {
|
2022-11-06 21:20:52 -06:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const skipButton = document.querySelector("#skipIntro");
|
|
|
|
if (!skipButton) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-11-25 01:37:27 -06:00
|
|
|
const position = introSkipper.videoPlayer.currentTime;
|
|
|
|
if (position >= introSkipper.skipSegments.ShowSkipPromptAt &&
|
|
|
|
position < introSkipper.skipSegments.HideSkipPromptAt) {
|
|
|
|
skipButton.classList.remove("hide");
|
|
|
|
return;
|
|
|
|
}
|
2022-11-06 21:20:52 -06:00
|
|
|
|
|
|
|
skipButton.classList.add("hide");
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Seeks to the end of the intro. */
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.doSkip = function (e) {
|
|
|
|
introSkipper.d("Skipping intro");
|
|
|
|
introSkipper.d(introSkipper.skipSegments);
|
|
|
|
introSkipper.videoPlayer.currentTime = introSkipper.skipSegments.IntroEnd;
|
2022-11-06 21:20:52 -06:00
|
|
|
}
|
|
|
|
|
2022-11-17 01:19:44 -06:00
|
|
|
/** Tests if an element with the provided selector exists. */
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.testElement = function (selector) { return document.querySelector(selector); }
|
2022-11-06 21:20:52 -06:00
|
|
|
|
2022-11-17 01:19:44 -06:00
|
|
|
/** Make an authenticated fetch to the Jellyfin server and parse the response body as JSON. */
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.secureFetch = async function (url) {
|
2022-11-17 01:19:44 -06:00
|
|
|
url = ApiClient.serverAddress() + "/" + url;
|
2022-11-06 21:20:52 -06:00
|
|
|
|
2022-11-17 01:19:44 -06:00
|
|
|
const reqInit = {
|
|
|
|
headers: {
|
|
|
|
"Authorization": "MediaBrowser Token=" + ApiClient.accessToken()
|
|
|
|
}
|
|
|
|
};
|
2022-11-06 21:20:52 -06:00
|
|
|
|
2022-11-17 01:19:44 -06:00
|
|
|
const res = await fetch(url, reqInit);
|
2022-11-06 21:20:52 -06:00
|
|
|
|
2022-11-17 01:19:44 -06:00
|
|
|
if (res.status !== 200) {
|
|
|
|
throw new Error(`Expected status 200 from ${url}, but got ${res.status}`);
|
|
|
|
}
|
2022-11-06 21:20:52 -06:00
|
|
|
|
2022-11-17 01:19:44 -06:00
|
|
|
return await res.json();
|
|
|
|
}
|
2022-11-06 21:20:52 -06:00
|
|
|
|
2022-11-25 01:37:27 -06:00
|
|
|
introSkipper.setup();
|