Update skip intro to use jumoog/intro-skipper.

This commit is contained in:
Izzie Walton 2024-05-17 18:22:04 -04:00
parent ce9846758a
commit 0c57c6d2ea
2 changed files with 95 additions and 40 deletions

View File

@ -186,7 +186,7 @@ This file can also be printed at runtime when using the ``--licenses`` option.
## Unofficial Plugin Support ## Unofficial Plugin Support
You can enable experimental support for [Skip Intro](https://github.com/ConfusedPolarBear/intro-skipper) in client settings. These are included for convenience only and is not an endorsement or long-term commitment to ensure functionality. See `src/native` for details on what the plugins modify code-wise. You can enable experimental support for [Skip Intro](https://github.com/jumoog/intro-skipper/) in client settings. These are included for convenience only and is not an endorsement or long-term commitment to ensure functionality. See `src/native` for details on what the plugins modify code-wise.
## Known Issues ## Known Issues

View File

@ -1,4 +1,7 @@
let tvIntro; let tvIntro;
let currentSegment;
let currentPlayer;
let skipIntroConfig;
class skipIntroPlugin { class skipIntroPlugin {
constructor({ events, playbackManager, ServerConnections }) { constructor({ events, playbackManager, ServerConnections }) {
@ -14,6 +17,7 @@ class skipIntroPlugin {
if (!enabled) return; if (!enabled) return;
// Based on https://github.com/jellyfin/jellyfin-web/compare/release-10.8.z...ConfusedPolarBear:jellyfin-web:intros // Based on https://github.com/jellyfin/jellyfin-web/compare/release-10.8.z...ConfusedPolarBear:jellyfin-web:intros
// Updated from https://github.com/jumoog/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/inject.js
// Adapted for use in JMP // Adapted for use in JMP
const stylesheet = ` const stylesheet = `
<style> <style>
@ -23,7 +27,7 @@ class skipIntroPlugin {
background-color:rgba(47,93,98,0) !important; background-color:rgba(47,93,98,0) !important;
} }
} }
.skipIntro { .skipIntro {
padding: 0 1px; padding: 0 1px;
position: absolute; position: absolute;
@ -39,13 +43,13 @@ class skipIntroPlugin {
-moz-transition: ease-out 0.4s; -moz-transition: ease-out 0.4s;
transition: ease-out 0.4s; transition: ease-out 0.4s;
} }
@media (max-width: 1080px) { @media (max-width: 1080px) {
.skipIntro { .skipIntro {
right: 10%; right: 10%;
} }
} }
.skipIntro:hover { .skipIntro:hover {
box-shadow: inset 400px 0 0 0 #f9f9f9; box-shadow: inset 400px 0 0 0 #f9f9f9;
-webkit-transition: ease-in 1s; -webkit-transition: ease-in 1s;
@ -61,7 +65,7 @@ class skipIntroPlugin {
const skipIntroHtml = ` const skipIntroHtml = `
<div class="skipIntro hide"> <div class="skipIntro hide">
<button is="paper-icon-button-light" class="btnSkipIntro paper-icon-button-light"> <button is="paper-icon-button-light" class="btnSkipIntro paper-icon-button-light">
Skip Intro <span id="btnSkipSegmentText"></span>
<span class="material-icons skip_next"></span> <span class="material-icons skip_next"></span>
</button> </button>
</div> </div>
@ -87,50 +91,100 @@ class skipIntroPlugin {
function handleClick(e) { function handleClick(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
skipIntro(); doSkip();
document.querySelector('.skipIntro .btnSkipIntro').removeEventListener('click', handleClick, { useCapture: true }); document.querySelector('.skipIntro .btnSkipIntro').removeEventListener('click', handleClick, { useCapture: true });
} }
function secureFetch(url) {
const apiClient = ServerConnections
? ServerConnections.currentApiClient()
: window.ApiClient;
const address = apiClient.serverAddress();
const reqInit = {
headers: {
"Authorization": `MediaBrowser Token=${apiClient.accessToken()}`
}
};
return fetch(`${address}${url}`, reqInit).then(r => {
if (!r.ok) {
tvIntro = null;
return;
}
return r.json();
});
}
async function injectSkipIntroHtml() { async function injectSkipIntroHtml() {
const playerContainer = await waitForElement('.upNextContainer', 5000); const playerContainer = await waitForElement('.upNextContainer', 5000);
// inject only if it doesn't exist // inject only if it doesn't exist
if (!document.querySelector('.skipIntro .btnSkipIntro')) { if (!document.querySelector('.skipIntro .btnSkipIntro')) {
playerContainer.insertAdjacentHTML('afterend', skipIntroHtml); playerContainer.insertAdjacentHTML('afterend', skipIntroHtml);
skipIntroConfig = await secureFetch("/Intros/UserInterfaceConfiguration");
if (!skipIntroConfig.SkipButtonVisible) {
console.info("[intro skipper] Skip button is disabled by the server.");
return;
}
} }
document.querySelector('.skipIntro .btnSkipIntro').addEventListener('click', handleClick, { useCapture: true }); const button = document.querySelector('.skipIntro .btnSkipIntro');
button.addEventListener('click', handleClick, { useCapture: true });
if (window.PointerEvent) { if (window.PointerEvent) {
document.querySelector('.skipIntro .btnSkipIntro').addEventListener('pointerdown', (e) => { button.addEventListener('pointerdown', (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}, { useCapture: true }); }, { useCapture: true });
} }
} }
function onPlayback(e, player, state) { function onPlayback(e, player, state) {
if (state.NowPlayingItem) { if (state.NowPlayingItem) {
getIntroTimestamps(state.NowPlayingItem); getIntroTimestamps(state.NowPlayingItem);
const onTimeUpdate = async () => { const onTimeUpdate = async () => {
// Check if an introduction sequence was detected for this item. // Check if an introduction sequence was detected for this item.
if (!tvIntro?.Valid) { if (!tvIntro) {
return; return;
} }
const seconds = playbackManager.currentTime(player) / 1000;
await injectSkipIntroHtml(); // I have trust issues await injectSkipIntroHtml(); // I have trust issues
const skipIntro = document.querySelector(".skipIntro"); const skipIntro = document.querySelector(".skipIntro");
if (!skipIntro) {
// If the skip prompt should be shown, show it.
if (seconds >= tvIntro.ShowSkipPromptAt && seconds < tvIntro.HideSkipPromptAt) {
skipIntro.classList.remove("hide");
return; return;
} }
skipIntro.classList.add("hide"); const segment = getCurrentSegment(playbackManager.currentTime(player) / 1000);
currentSegment = segment;
currentPlayer = player;
switch (segment["SegmentType"]) {
case "None":
if (skipIntro.style.opacity === '0') return;
skipIntro.style.opacity = '0';
skipIntro.addEventListener("transitionend", () => {
skipIntro.classList.add("hide");
}, { once: true });
return;
case "Introduction":
skipIntro.querySelector("#btnSkipSegmentText").textContent = skipIntroConfig.SkipButtonIntroText;
break;
case "Credits":
skipIntro.querySelector("#btnSkipSegmentText").textContent = skipIntroConfig.SkipButtonEndCreditsText;
break;
}
if (!skipIntro.classList.contains("hide")) return;
skipIntro.classList.remove("hide");
requestAnimationFrame(() => {
requestAnimationFrame(() => {
skipIntro.style.opacity = '1';
});
});
}; };
events.on(player, 'timeupdate', onTimeUpdate); events.on(player, 'timeupdate', onTimeUpdate);
@ -143,36 +197,37 @@ class skipIntroPlugin {
} }
}; };
events.on(playbackManager, 'playbackstart', onPlayback); events.on(playbackManager, 'playbackstart', onPlayback);
function getIntroTimestamps(item) { function getIntroTimestamps(item) {
const apiClient = ServerConnections secureFetch(`/Episode/${item.Id}/IntroSkipperSegments`).then(intro => {
? ServerConnections.currentApiClient()
: window.ApiClient;
const address = apiClient.serverAddress();
const url = `${address}/Episode/${item.Id}/IntroTimestamps`;
const reqInit = {
headers: {
"Authorization": `MediaBrowser Token=${apiClient.accessToken()}`
}
};
fetch(url, reqInit).then(r => {
if (!r.ok) {
tvIntro = null;
return;
}
return r.json();
}).then(intro => {
tvIntro = intro; tvIntro = intro;
}).catch(err => { tvIntro = null; }); }).catch(err => { tvIntro = null; });
} }
function skipIntro() { function osdVisible() {
playbackManager.seekMs(tvIntro.IntroEnd * 1000); const osd = document.querySelector("div.videoOsdBottom");
return osd ? !osd.classList.contains("hide") : false;
} }
function getCurrentSegment(position) {
for (let key in tvIntro) {
const segment = tvIntro[key];
if ((position >= segment.ShowSkipPromptAt && position < segment.HideSkipPromptAt) || (osdVisible() && position >= segment.IntroStart && position < segment.IntroEnd)) {
segment["SegmentType"] = key;
return segment;
}
}
return { "SegmentType": "None" };
}
function doSkip(e) {
if (currentSegment["SegmentType"] === "None") {
console.warn("[intro skipper] doSkip() called without an active segment");
return;
}
currentPlayer.currentTime(currentSegment["IntroEnd"] * 1000);
}
})(); })();
} }
} }