skipIntroPlugin.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. let tvIntro;
  2. class skipIntroPlugin {
  3. constructor({ events, playbackManager, ServerConnections }) {
  4. this.name = 'Skip Intro Plugin';
  5. this.type = 'input';
  6. this.id = 'skipIntroPlugin';
  7. (async() => {
  8. await window.initCompleted;
  9. const enabled = window.jmpInfo.settings.plugins.skipintro;
  10. console.log("Skip Intro Plugin enabled: " + enabled);
  11. if (!enabled) return;
  12. // Based on https://github.com/jellyfin/jellyfin-web/compare/release-10.8.z...ConfusedPolarBear:jellyfin-web:intros
  13. // Adapted for use in JMP
  14. const stylesheet = `
  15. <style>
  16. @media (hover:hover) and (pointer:fine) {
  17. .skipIntro .paper-icon-button-light:hover:not(:disabled) {
  18. color:black !important;
  19. background-color:rgba(47,93,98,0) !important;
  20. }
  21. }
  22. .skipIntro {
  23. padding: 0 1px;
  24. position: absolute;
  25. right: 10em;
  26. bottom: 9em;
  27. background-color:rgba(25, 25, 25, 0.66);
  28. border: 1px solid;
  29. border-radius: 0px;
  30. display: inline-block;
  31. cursor: pointer;
  32. box-shadow: inset 0 0 0 0 #f9f9f9;
  33. -webkit-transition: ease-out 0.4s;
  34. -moz-transition: ease-out 0.4s;
  35. transition: ease-out 0.4s;
  36. }
  37. @media (max-width: 1080px) {
  38. .skipIntro {
  39. right: 10%;
  40. }
  41. }
  42. .skipIntro:hover {
  43. box-shadow: inset 400px 0 0 0 #f9f9f9;
  44. -webkit-transition: ease-in 1s;
  45. -moz-transition: ease-in 1s;
  46. transition: ease-in 1s;
  47. }
  48. </style>
  49. `;
  50. document.head.insertAdjacentHTML('beforeend', stylesheet);
  51. const skipIntroHtml = `
  52. <div class="skipIntro hide">
  53. <button is="paper-icon-button-light" class="btnSkipIntro paper-icon-button-light">
  54. Skip Intro
  55. <span class="material-icons skip_next"></span>
  56. </button>
  57. </div>
  58. `;
  59. function waitForElement(element, maxWait = 10000) {
  60. return new Promise((resolve, reject) => {
  61. const interval = setInterval(() => {
  62. const result = document.querySelector(element);
  63. if (result) {
  64. clearInterval(interval);
  65. resolve(result);
  66. }
  67. }, 100);
  68. setTimeout(() => {
  69. clearInterval(interval);
  70. reject();
  71. }, maxWait);
  72. });
  73. }
  74. function handleClick(e) {
  75. e.preventDefault();
  76. e.stopPropagation();
  77. skipIntro();
  78. document.querySelector('.skipIntro .btnSkipIntro').removeEventListener('click', handleClick, { useCapture: true });
  79. }
  80. async function injectSkipIntroHtml() {
  81. const playerContainer = await waitForElement('.upNextContainer', 5000);
  82. // inject only if it doesn't exist
  83. if (!document.querySelector('.skipIntro .btnSkipIntro')) {
  84. playerContainer.insertAdjacentHTML('afterend', skipIntroHtml);
  85. }
  86. document.querySelector('.skipIntro .btnSkipIntro').addEventListener('click', handleClick, { useCapture: true });
  87. if (window.PointerEvent) {
  88. document.querySelector('.skipIntro .btnSkipIntro').addEventListener('pointerdown', (e) => {
  89. e.preventDefault();
  90. e.stopPropagation();
  91. }, { useCapture: true });
  92. }
  93. }
  94. function onPlayback(e, player, state) {
  95. if (state.NowPlayingItem) {
  96. getIntroTimestamps(state.NowPlayingItem);
  97. const onTimeUpdate = async () => {
  98. // Check if an introduction sequence was detected for this item.
  99. if (!tvIntro?.Valid) {
  100. return;
  101. }
  102. const seconds = playbackManager.currentTime(player) / 1000;
  103. await injectSkipIntroHtml(); // I have trust issues
  104. const skipIntro = document.querySelector(".skipIntro");
  105. // If the skip prompt should be shown, show it.
  106. if (seconds >= tvIntro.ShowSkipPromptAt && seconds < tvIntro.HideSkipPromptAt) {
  107. skipIntro.classList.remove("hide");
  108. return;
  109. }
  110. skipIntro.classList.add("hide");
  111. };
  112. events.on(player, 'timeupdate', onTimeUpdate);
  113. const onPlaybackStop = () => {
  114. events.off(player, 'timeupdate', onTimeUpdate);
  115. events.off(player, 'playbackstop', onPlaybackStop);
  116. };
  117. events.on(player, 'playbackstop', onPlaybackStop);
  118. }
  119. };
  120. events.on(playbackManager, 'playbackstart', onPlayback);
  121. function getIntroTimestamps(item) {
  122. const apiClient = ServerConnections
  123. ? ServerConnections.currentApiClient()
  124. : window.ApiClient;
  125. const address = apiClient.serverAddress();
  126. const url = `${address}/Episode/${item.Id}/IntroTimestamps`;
  127. const reqInit = {
  128. headers: {
  129. "Authorization": `MediaBrowser Token=${apiClient.accessToken()}`
  130. }
  131. };
  132. fetch(url, reqInit).then(r => {
  133. if (!r.ok) {
  134. tvIntro = null;
  135. return;
  136. }
  137. return r.json();
  138. }).then(intro => {
  139. tvIntro = intro;
  140. }).catch(err => { tvIntro = null; });
  141. }
  142. function skipIntro() {
  143. playbackManager.seekMs(tvIntro.IntroEnd * 1000);
  144. }
  145. })();
  146. }
  147. }
  148. window._skipIntroPlugin = skipIntroPlugin;