skipIntroPlugin.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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. async function injectSkipIntroHtml() {
  75. const playerContainer = await waitForElement('.upNextContainer', 5000);
  76. // inject only if it doesn't exist
  77. if (!document.querySelector('.skipIntro .btnSkipIntro')) {
  78. playerContainer.insertAdjacentHTML('afterend', skipIntroHtml);
  79. }
  80. document.querySelector('.skipIntro .btnSkipIntro').addEventListener('click', (e) => {
  81. e.preventDefault();
  82. e.stopPropagation();
  83. skipIntro();
  84. }, { useCapture: true });
  85. if (window.PointerEvent) {
  86. document.querySelector('.skipIntro .btnSkipIntro').addEventListener('pointerdown', (e) => {
  87. e.preventDefault();
  88. e.stopPropagation();
  89. }, { useCapture: true });
  90. }
  91. }
  92. function onPlayback(e, player, state) {
  93. if (state.NowPlayingItem) {
  94. getIntroTimestamps(state.NowPlayingItem);
  95. const onTimeUpdate = async () => {
  96. // Check if an introduction sequence was detected for this item.
  97. if (!tvIntro?.Valid) {
  98. return;
  99. }
  100. const seconds = playbackManager.currentTime(player) / 1000;
  101. await injectSkipIntroHtml(); // I have trust issues
  102. const skipIntro = document.querySelector(".skipIntro");
  103. // If the skip prompt should be shown, show it.
  104. if (seconds >= tvIntro.ShowSkipPromptAt && seconds < tvIntro.HideSkipPromptAt) {
  105. skipIntro.classList.remove("hide");
  106. return;
  107. }
  108. skipIntro.classList.add("hide");
  109. };
  110. events.on(player, 'timeupdate', onTimeUpdate);
  111. const onPlaybackStop = () => {
  112. events.off(player, 'timeupdate', onTimeUpdate);
  113. events.off(player, 'playbackstop', onPlaybackStop);
  114. };
  115. events.on(player, 'playbackstop', onPlaybackStop);
  116. }
  117. };
  118. events.on(playbackManager, 'playbackstart', onPlayback);
  119. function getIntroTimestamps(item) {
  120. const apiClient = ServerConnections
  121. ? ServerConnections.currentApiClient()
  122. : window.ApiClient;
  123. const address = apiClient.serverAddress();
  124. const url = `${address}/Episode/${item.Id}/IntroTimestamps`;
  125. const reqInit = {
  126. headers: {
  127. "Authorization": `MediaBrowser Token=${apiClient.accessToken()}`
  128. }
  129. };
  130. fetch(url, reqInit).then(r => {
  131. if (!r.ok) {
  132. tvIntro = null;
  133. return;
  134. }
  135. return r.json();
  136. }).then(intro => {
  137. tvIntro = intro;
  138. }).catch(err => { tvIntro = null; });
  139. }
  140. function skipIntro() {
  141. playbackManager.seekMs(tvIntro.IntroEnd * 1000);
  142. }
  143. })();
  144. }
  145. }
  146. window._skipIntroPlugin = skipIntroPlugin;