skipIntroPlugin.js 6.1 KB

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