mirror of
https://github.com/jellyfin/jellyfin-media-player.git
synced 2024-11-27 00:00:38 +00:00
Update skip intro to use jumoog/intro-skipper.
This commit is contained in:
parent
ce9846758a
commit
0c57c6d2ea
@ -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
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user