YouTube End Time Display

Displays the actual end time of YouTube videos next to the time display

  1. // ==UserScript==
  2. // @name YouTube End Time Display
  3. // @namespace YouTube End Time Display
  4. // @version 1.0.0
  5. // @description Displays the actual end time of YouTube videos next to the time display
  6. // @author DumbGPT
  7. // @match https://www.youtube.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. let etaElement = null;
  15. let updateInterval = null;
  16.  
  17. function init() {
  18. if (location.href.includes('youtube.com/watch')) {
  19. const timeDisplay = document.querySelector('.ytp-time-display');
  20. if (timeDisplay) {
  21. setupEtaDisplay(timeDisplay);
  22. } else {
  23. setTimeout(init, 1000);
  24. }
  25. }
  26. }
  27.  
  28. function setupEtaDisplay(timeDisplay) {
  29. if (etaElement) {
  30. etaElement.remove();
  31. }
  32.  
  33. etaElement = document.createElement('span');
  34. etaElement.id = 'yt-eta-display';
  35.  
  36. const timeDisplayStyleRef = timeDisplay.querySelector('.ytp-time-current') || timeDisplay;
  37. const timeStyles = window.getComputedStyle(timeDisplayStyleRef);
  38.  
  39. etaElement.style.fontFamily = timeStyles.fontFamily;
  40. etaElement.style.fontSize = timeStyles.fontSize;
  41. etaElement.style.color = timeStyles.color;
  42. etaElement.style.fontWeight = timeStyles.fontWeight;
  43. etaElement.style.pointerEvents = 'none';
  44.  
  45. timeDisplay.appendChild(etaElement);
  46.  
  47. const video = document.querySelector('video');
  48. if (video) {
  49. updateEtaDisplay(video);
  50.  
  51. if (updateInterval) {
  52. clearInterval(updateInterval);
  53. }
  54. updateInterval = setInterval(() => updateEtaDisplay(video), 1000);
  55.  
  56. video.addEventListener('seeking', () => updateEtaDisplay(video));
  57. video.addEventListener('ratechange', () => updateEtaDisplay(video));
  58. } else {
  59. setTimeout(() => {
  60. const newVideo = document.querySelector('video');
  61. if (newVideo) setupEtaDisplay(timeDisplay);
  62. }, 1000);
  63. }
  64. }
  65.  
  66. function updateEtaDisplay(video) {
  67. if (!video || !video.duration || isNaN(video.duration) || !etaElement) return;
  68.  
  69. const isLive = document.querySelector('.ytp-live') !== null || video.duration === Infinity;
  70. if (isLive) {
  71. etaElement.textContent = "";
  72. return;
  73. }
  74.  
  75. const remainingTime = video.duration - video.currentTime;
  76.  
  77. const now = new Date();
  78. const endTime = new Date(now.getTime() + (remainingTime * 1000 / video.playbackRate));
  79.  
  80. const hours = endTime.getHours().toString().padStart(2, '0');
  81. const minutes = endTime.getMinutes().toString().padStart(2, '0');
  82.  
  83. etaElement.textContent = ` | ETA ${hours}:${minutes}`;
  84. }
  85.  
  86. let lastUrl = location.href;
  87. const observer = new MutationObserver(() => {
  88. if (location.href !== lastUrl) {
  89. lastUrl = location.href;
  90.  
  91. if (updateInterval) {
  92. clearInterval(updateInterval);
  93. updateInterval = null;
  94. }
  95.  
  96. if (etaElement) {
  97. etaElement.remove();
  98. etaElement = null;
  99. }
  100.  
  101. setTimeout(init, 1000);
  102. }
  103. });
  104.  
  105. observer.observe(document.body, { childList: true, subtree: true });
  106.  
  107. init();
  108.  
  109. window.addEventListener('beforeunload', () => {
  110. if (updateInterval) {
  111. clearInterval(updateInterval);
  112. }
  113. if (observer) {
  114. observer.disconnect();
  115. }
  116. });
  117. })();