YouTube Playlist Calculator

Get the total length/duration of a YouTube playlist.

  1. // ==UserScript==
  2. // @name YouTube Playlist Calculator
  3. // @version 1.0.3
  4. // @author sapondanaisriwan
  5. // @description Get the total length/duration of a YouTube playlist.
  6. // @match https://www.youtube.com/*
  7. // @grant none
  8. // @license MIT
  9. // @namespace https://greasyfork.org/en/scripts/465609-youtube-playlist-calculator
  10. // @homepageURL https://github.com/sapondanaisriwan/Youtube-Playlist-Calculator
  11. // @supportURL https://github.com/sapondanaisriwan/Youtube-Playlist-Calculator/issues
  12. // @icon https://i.imgur.com/I9uDrsq.png
  13. // ==/UserScript==
  14.  
  15. /*
  16. If you want to submit a bug or request a feature please report via github issue. Since I receive so many emails, I can't reply to them all.
  17. Contact: sapondanaisriwan@gmail.com
  18. Support me: https://ko-fi.com/sapondanaisriwan
  19. Support me: https://ko-fi.com/sapondanaisriwan
  20. Support me: https://ko-fi.com/sapondanaisriwan
  21. Support me: https://ko-fi.com/sapondanaisriwan
  22. Support me: https://ko-fi.com/sapondanaisriwan
  23. */
  24.  
  25. "use strict";
  26.  
  27. const config = { childList: true, subtree: true };
  28.  
  29. const selectors = {
  30. watchPage: "ytd-watch-flexy[playlist]:not([hidden])",
  31. wpPLContainer:
  32. "ytd-playlist-panel-renderer[collapsible] #publisher-container.ytd-playlist-panel-renderer",
  33. wpPLText:
  34. "ytd-playlist-panel-renderer[collapsible] #publisher-container.ytd-playlist-panel-renderer .wp-text",
  35. playlistPage: "ytd-browse[page-subtype='playlist']:not([hidden])",
  36. overlayTime: "ytd-playlist-header-renderer #overlays .duration-text",
  37. playlistOverlay: "ytd-playlist-header-renderer #overlays",
  38. timestampOverlay:
  39. "ytd-playlist-video-list-renderer #text.ytd-thumbnail-overlay-time-status-renderer",
  40. thumbnail:
  41. "ytd-playlist-video-list-renderer ytd-thumbnail-overlay-hover-text-renderer",
  42. };
  43.  
  44. const styles = {
  45. log: "color: #fff; font-size: 16px;",
  46. duration: `
  47. .duration-overlay {
  48. margin: 4px;
  49. position: absolute;
  50. bottom: 0;
  51. right: 0;
  52. color: var(--yt-spec-static-brand-white);
  53. background-color: var(--yt-spec-static-overlay-background-heavy);
  54. padding: 3px 4px;
  55. height: 12px;
  56. border-radius: 2px;
  57. font-size: var(--yt-badge-font-size,1.2rem);
  58. font-weight: var(--yt-badge-font-weight,500);
  59. line-height: var(--yt-badge-line-height-size,1.2rem);
  60. letter-spacing: var(--yt-badge-letter-spacing,0.5px);
  61. display: flex;
  62. flex-direction: row;
  63. align-items: center;
  64. }
  65. .duration-text {
  66. max-height: 1.2rem;
  67. overflow: hidden;
  68. }
  69.  
  70. .wp-container::before {
  71. color: var(--yt-spec-text-secondary);
  72. content: "-";
  73. padding: 0 4px;
  74. }
  75. `,
  76. };
  77.  
  78. const cLog = (msg) => console.log(`%c${msg}`, styles.log);
  79.  
  80. const select = (selector) => document.querySelector(selector);
  81.  
  82. const selectAll = (selector) => document.querySelectorAll(selector);
  83.  
  84. const addStyles = (css) => {
  85. const style = document.createElement("style");
  86. style.type = "text/css";
  87. style.textContent = css;
  88. document.documentElement.appendChild(style);
  89. };
  90.  
  91. const getDataPlaylist = (selector) => {
  92. return selector?.__data?.data?.contents?.twoColumnBrowseResultsRenderer
  93. ?.tabs[0]?.tabRenderer?.content?.sectionListRenderer?.contents[0]
  94. ?.itemSectionRenderer?.contents[0]?.playlistVideoListRenderer?.contents;
  95. };
  96.  
  97. const getDataWatchPage = (selector) => {
  98. return selector.__data.playlistData.contents;
  99. };
  100.  
  101. const formatDuration = (sum) => {
  102. const hours = Math.floor(sum / 3600);
  103. const minutes = Math.floor((sum % 3600) / 60);
  104. const seconds = sum % 60;
  105. let formattedDuration = "";
  106. if (hours > 0) {
  107. formattedDuration += hours + ":";
  108. }
  109. if (minutes < 10 && hours > 0) {
  110. formattedDuration += "0";
  111. }
  112. formattedDuration += minutes + ":";
  113. if (seconds < 10) {
  114. formattedDuration += "0";
  115. }
  116. formattedDuration += seconds;
  117. return formattedDuration;
  118. };
  119.  
  120. const newOverlayContainer = document.createElement("div");
  121. newOverlayContainer.setAttribute("class", "duration-overlay");
  122.  
  123. const newTextEle = document.createElement("span");
  124. newTextEle.setAttribute("class", "duration-text");
  125.  
  126. newOverlayContainer.appendChild(newTextEle);
  127.  
  128. const addDurationOverlay = (duration) => {
  129. const overlayTimeEle = select(selectors.overlayTime);
  130. const overlayCon = select(selectors.playlistOverlay);
  131.  
  132. if (!overlayCon) return;
  133. if (!overlayTimeEle) return overlayCon.prepend(newOverlayContainer);
  134. overlayTimeEle.textContent = duration;
  135. };
  136.  
  137. const newWPContainer = document.createElement("div");
  138. newWPContainer.setAttribute(
  139. "class",
  140. "wp-container index-message-wrapper style-scope ytd-playlist-panel-renderer"
  141. );
  142.  
  143. const newWPText = document.createElement("span");
  144. newWPText.setAttribute("class", "wp-text");
  145.  
  146. newWPContainer.appendChild(newWPText);
  147.  
  148. const addDurationWP = (duration) => {
  149. const wpDurationEle = select(selectors.wpPLText);
  150. const wpContainerEle = select(selectors.wpPLContainer);
  151. if (!wpContainerEle) return;
  152. if (!wpDurationEle) return wpContainerEle.appendChild(newWPContainer);
  153. wpDurationEle.textContent = `[ ${duration} ]`;
  154. };
  155.  
  156. const sumResult = (data) =>
  157. data.reduce((pre, cur) => {
  158. return (
  159. pre +
  160. (!!cur.playlistVideoRenderer
  161. ? +cur.playlistVideoRenderer.lengthSeconds
  162. : 0)
  163. );
  164. }, 0);
  165.  
  166. const sumResultText = (overlays) => {
  167. let totalSeconds = 0;
  168. overlays.forEach((overlay) => {
  169. if (!overlay.playlistPanelVideoRenderer) return;
  170. const timeArr = overlay?.playlistPanelVideoRenderer?.lengthText?.simpleText
  171. .split(":")
  172. .map(Number);
  173.  
  174. let timestampSeconds = 0;
  175. if (timeArr.length === 3) {
  176. timestampSeconds = timeArr[0] * 3600 + timeArr[1] * 60 + timeArr[2];
  177. } else if (timeArr.length === 2) {
  178. timestampSeconds = timeArr[0] * 60 + timeArr[1];
  179. } else {
  180. timestampSeconds = timeArr[0];
  181. }
  182.  
  183. totalSeconds += timestampSeconds;
  184. });
  185. return totalSeconds;
  186. };
  187.  
  188. const main = () => {
  189. const playlistEle = select(selectors.playlistPage);
  190. const watchPageEle = select(selectors.watchPage);
  191. if (playlistEle) {
  192. const data = getDataPlaylist(playlistEle);
  193. const sum = sumResult(data);
  194. const duration = formatDuration(sum);
  195. addDurationOverlay(duration);
  196. }
  197. if (watchPageEle) {
  198. const data = getDataWatchPage(watchPageEle);
  199. const sum = sumResultText(data);
  200. const duration = formatDuration(sum);
  201. addDurationWP(duration);
  202. }
  203. };
  204.  
  205. const run = () => {
  206. addStyles(styles.duration);
  207. const observer = new MutationObserver(main);
  208. observer.observe(document.body, config);
  209. };
  210.  
  211. run();