Youtube Playlist Duration Calculator (Forked)

Calculate the duration of a playlist and display it next to the number of videos. Also detects and displays video playback speed.

  1. // ==UserScript==
  2. // @name Youtube Playlist Duration Calculator (Forked)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1.6
  5. // @description Calculate the duration of a playlist and display it next to the number of videos. Also detects and displays video playback speed.
  6. // @author HellFiveOsborn (Forked from DenverCoder1)
  7. // @icon https://i.imgur.com/FwUCnbF.png
  8. // @source https://greasyfork.org/pt-BR/scripts/407457-youtube-playlist-duration-calculator
  9. // @match https://www.youtube.com/playlist?list=*
  10. // @match https://www.youtube.com/watch?v=*&list=*
  11. // @match https://www.youtube.com/watch?v=*
  12. // @exclude https://www.youtube.com/shorts/*
  13. // @exclude https://www.youtube.com/
  14. // @exclude https://www.youtube.com/feed/*
  15. // @grant none
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19. (function () {
  20. "use strict";
  21.  
  22. /**
  23. * Converts a time string in the format "HH:MM:SS" or "MM:SS" or "SS" to the total number of seconds.
  24. *
  25. * @param {string} timeString - The time string to convert, formatted as "HH:MM:SS", "MM:SS", or "SS".
  26. * @returns {number} The total number of seconds represented by the input time string.
  27. */
  28. function timeToSeconds(timeString) {
  29. const parts = timeString.split(':').map(Number).reverse(); // Split by ':' and reverse the array
  30. let seconds = 0;
  31. if (parts[0]) seconds += parts[0]; // Add seconds
  32. if (parts[1]) seconds += parts[1] * 60; // Add minutes converted to seconds
  33. if (parts[2]) seconds += parts[2] * 3600; // Add hours converted to seconds
  34. return seconds;
  35. }
  36.  
  37. /**
  38. * Calculate the duration of a playlist
  39. *
  40. * @returns {string} Duration of the playlist in a human readable format
  41. */
  42. function calculateDuration() {
  43. // get data object stored on Youtube's website
  44. const data = window.ytInitialData;
  45.  
  46. // Extracts the two main content structures from the data object using destructuring.
  47. const { twoColumnBrowseResultsRenderer, twoColumnWatchNextResults } = data.contents || {};
  48.  
  49. // Safely access nested properties with optional chaining to avoid runtime errors
  50. const browseContents = twoColumnBrowseResultsRenderer?.tabs[0]?.tabRenderer?.content;
  51.  
  52. // Attempt to get the playlist content from the watch next results
  53. const watchNextContents = twoColumnWatchNextResults?.playlist?.playlist;
  54.  
  55. // Try to extract the list of videos from one of the known content structures.
  56. const vids = browseContents?.sectionListRenderer?.contents[0]?.itemSectionRenderer?.contents[0]?.playlistVideoListRenderer?.contents
  57. || watchNextContents?.contents;
  58.  
  59. // Calculate the total duration of all videos in seconds
  60. const seconds = vids.reduce(function (x, y) {
  61. const videoRenderer = y.playlistVideoRenderer || y.playlistPanelVideoRenderer;
  62. if (!videoRenderer) return x;
  63.  
  64. // If 'lengthSeconds' is available and valid, use it directly
  65. if (!isNaN(videoRenderer.lengthSeconds)) {
  66. return x + parseInt(videoRenderer.lengthSeconds);
  67. }
  68. // If 'lengthText.simpleText' is available, convert it to seconds
  69. else if (videoRenderer.lengthText?.simpleText) {
  70. return x + timeToSeconds(videoRenderer.lengthText.simpleText);
  71. }
  72. // If neither is available, return the current total
  73. return x;
  74. }, 0);
  75.  
  76. // divide by 60 and round to get the number of minutes
  77. const minutes = Math.round(seconds / 60);
  78.  
  79. // if there is at least 1 hour, display hours and minutes, otherwise display minutes and seconds.
  80. const durationString =
  81. minutes >= 60 // if minutes is 60 or more
  82. ? Math.floor(minutes / 60) + "h " + (minutes % 60) + "m" // calculate hours and minutes
  83. : Math.floor(seconds / 60) + "m " + (seconds % 60) + "s"; // calculate minutes and seconds
  84.  
  85. return durationString;
  86. }
  87.  
  88. /**
  89. * Append the duration to the playlist metadata
  90. */
  91. function appendDurationToPlaylistMetadata() {
  92. const metadataRow = document.querySelectorAll('div.yt-content-metadata-view-model-wiz__metadata-row')[3]
  93. if (!metadataRow) return;
  94.  
  95. const durationString = calculateDuration();
  96.  
  97. // Create a new span for the duration
  98. const durationSpan = document.createElement('span');
  99. durationSpan.className = 'yt-core-attributed-string yt-content-metadata-view-model-wiz__metadata-text yt-core-attributed-string--white-space-pre-wrap yt-core-attributed-string--link-inherit-color';
  100. durationSpan.setAttribute('dir', 'auto');
  101. durationSpan.setAttribute('role', 'text');
  102. durationSpan.textContent = durationString;
  103. durationSpan.style.color = '#ff0'; // Highlight color
  104.  
  105. // Add a delimiter before the duration
  106. const delimiterSpan = document.createElement('span');
  107. delimiterSpan.className = 'yt-content-metadata-view-model-wiz__delimiter';
  108. delimiterSpan.setAttribute('aria-hidden', 'true');
  109. delimiterSpan.textContent = '•';
  110.  
  111. // Append the delimiter and duration to the metadata row
  112. metadataRow.appendChild(delimiterSpan);
  113. metadataRow.appendChild(durationSpan);
  114.  
  115. console.debug('Duration of playlist:', durationString);
  116. }
  117.  
  118. /**
  119. * Append the duration to the playlist header when watching a video in a playlist
  120. */
  121. function appendDurationToPlaylistHeader() {
  122. const headerDescription = document.querySelectorAll('div#publisher-container')[2];
  123. if (!headerDescription) return;
  124.  
  125. waitForElement('.html5-video-container > video').then(() => {
  126. const videoElement = document.querySelector('.html5-video-container > video');
  127. if (!videoElement) return;
  128.  
  129. const speed = videoElement.playbackRate;
  130. const durationString = calculateDuration();
  131.  
  132. // Calculate the adjusted duration if playback speed is not 1x
  133. const totalSeconds = parseInt(durationString.split(' ').reduce((acc, val) => {
  134. if (val.includes('h')) acc += parseInt(val) * 3600;
  135. if (val.includes('m')) acc += parseInt(val) * 60;
  136. if (val.includes('s')) acc += parseInt(val);
  137. return acc;
  138. }, 0));
  139.  
  140. const adjustedDuration = speed !== 1 ? totalSeconds / speed : totalSeconds;
  141.  
  142. // Format adjusted duration into hours, minutes, and seconds
  143. const hours = Math.floor(adjustedDuration / 3600);
  144. const minutes = Math.floor((adjustedDuration % 3600) / 60);
  145. const seconds = Math.floor(adjustedDuration % 60);
  146.  
  147. // Create a human-readable string
  148. const formattedDuration =
  149. hours > 0
  150. ? `${hours}h ${minutes.toString().padStart(2, '0')}m`
  151. : `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
  152.  
  153. // Remove the previous duration if it exists
  154. if (document.querySelector('#playlist-duration-calculated')) {
  155. // If the duration is the same, don't update
  156. if (document.querySelector('#playlist-duration-calculated').textContent === formattedDuration) return;
  157.  
  158. document.querySelector('#playlist-duration-calculated').remove();
  159. }
  160.  
  161. console.debug('Playback speed:', speed);
  162. console.debug('Duration of playlist:', durationString);
  163. console.debug('Adjusted duration:', formattedDuration);
  164.  
  165. // Create a new span for the duration
  166. const durationSpan = document.createElement('div');
  167. durationSpan.className = 'index-message-wrapper style-scope ytd-playlist-panel-renderer';
  168. durationSpan.id = 'playlist-duration-calculated';
  169. durationSpan.setAttribute('dir', 'auto');
  170. durationSpan.setAttribute('role', 'text');
  171. durationSpan.textContent = `${formattedDuration}`;
  172. durationSpan.style.marginLeft = '5px'; // Add margin to the left
  173. durationSpan.style.color = '#ff0'; // Highlight color
  174.  
  175. // Append the duration to the header description
  176. headerDescription.appendChild(durationSpan);
  177. });
  178. }
  179.  
  180. /**
  181. * Detect video playback speed and display calculated duration
  182. */
  183. function detectPlaybackSpeed() {
  184. const videoElement = document.querySelector('.html5-video-container > video');
  185. if (!videoElement) return;
  186.  
  187. const speed = videoElement.playbackRate;
  188. const currentTime = videoElement.currentTime;
  189. const duration = videoElement.duration;
  190.  
  191. // Only display if playback speed is not 1x
  192. if (speed !== 1) {
  193. const calculatedDuration = (duration - currentTime) / speed;
  194.  
  195. const timeWrapper = document.querySelector('.ytp-time-wrapper');
  196. if (!timeWrapper) return;
  197.  
  198. let calculatedDurationElement = timeWrapper.querySelector('.ytp-time-duration-calculed');
  199. if (!calculatedDurationElement) {
  200. calculatedDurationElement = document.createElement('span');
  201. calculatedDurationElement.className = 'ytp-time-duration-calculed';
  202. calculatedDurationElement.style.color = '#ff0'; // Highlight color
  203. timeWrapper.appendChild(calculatedDurationElement);
  204. }
  205.  
  206. const minutes = Math.floor(calculatedDuration / 60);
  207. const seconds = Math.floor(calculatedDuration % 60);
  208. calculatedDurationElement.textContent = ` (${minutes}:${seconds.toString().padStart(2, '0')})`;
  209. return calculatedDurationElement;
  210. } else {
  211. // Remove the calculated duration if speed is back to 1x
  212. const calculatedDurationElement = document.querySelector('.ytp-time-duration-calculed');
  213. if (calculatedDurationElement) {
  214. calculatedDurationElement.remove();
  215. }
  216. return null;
  217. }
  218. }
  219.  
  220. /**
  221. * Wait for an element using an observer
  222. *
  223. * @param {string} selector Selector to wait for
  224. *
  225. * @see https://stackoverflow.com/a/61511955
  226. */
  227. function waitForElement(selector) {
  228. return new Promise((resolve) => {
  229. if (document.querySelector(selector)) {
  230. return resolve(document.querySelector(selector));
  231. }
  232. const observer = new MutationObserver((_) => {
  233. if (document.querySelector(selector)) {
  234. resolve(document.querySelector(selector));
  235. observer.disconnect();
  236. }
  237. });
  238. observer.observe(document.body, {
  239. childList: true,
  240. subtree: true,
  241. });
  242. });
  243. }
  244.  
  245. /**
  246. * Check if the current page is a playlist or a single video
  247. */
  248. function isPlaylistOrVideoPage() {
  249. const url = window.location.href;
  250. return (
  251. url.includes('/playlist?list=') || // Playlist page
  252. url.includes('/watch?v=') // Single video page
  253. );
  254. }
  255.  
  256. // Only run the script on playlist or single video pages
  257. if (isPlaylistOrVideoPage()) {
  258. setTimeout(() => {
  259. if (window.location.href.includes('/playlist?list=')) {
  260. // Append duration to playlist metadata
  261. waitForElement('.yt-content-metadata-view-model-wiz__metadata-row').then(() => {
  262. appendDurationToPlaylistMetadata()
  263. });
  264. } else if (window.location.href.includes('/watch?v=')) {
  265. // Append duration to playlist header when watching a video in a playlist
  266. if (window.location.href.includes('list=')) {
  267. waitForElement('div#publisher-container').then(() => {
  268. setInterval(appendDurationToPlaylistHeader, 1500);
  269. });
  270. }
  271.  
  272. // Detect playback speed and update calculated duration periodically
  273. setInterval(detectPlaybackSpeed, 1000);
  274. }
  275. }, 1500);
  276. }
  277. })();