// ==UserScript== // @name YouTube - Playlist Utils // @description Adds a length calculation to playlists. // @version 2026.07.04.01.41 // @author MetalTxus // @namespace https://github.com/jesuscc1993 // @grant GM_registerMenuCommand // @icon https://www.youtube.com/favicon.ico // @match https://www.youtube.com/* // ==/UserScript== (() => { 'use strict'; const INTERACTION_INTERVAL = 250; let intervalId; let durationElement; let extraStatsElement; const getPlaylistLength = () => { let seconds = 0; const badges = document.querySelectorAll( 'ytd-playlist-video-list-renderer ytd-thumbnail-overlay-time-status-renderer .ytBadgeShapeText', ); badges.forEach((el) => { if (el.innerText.includes(':')) { const timeString = el.innerText.replace(/\s*/g, '').split(':'); if (timeString.length) seconds += parseInt(timeString.pop(), 10); if (timeString.length) seconds += parseInt(timeString.pop(), 10) * 60; if (timeString.length) seconds += parseInt(timeString.pop(), 10) * 3600; } }); return { seconds, videos: badges.length, }; }; const formatLength = (length) => { const hours = Math.floor(length / 3600); const minutes = Math.floor((length % 3600) / 60); const seconds = length % 60; const minUnit = hours ? 3 : minutes ? 2 : 1; const formattedHours = minUnit > 2 ? `${formatTimeToken(hours, false)}:` : ''; const formattedMinutes = minUnit > 1 ? `${formatTimeToken(minutes, !!hours)}:` : ''; const formattedSeconds = formatTimeToken(seconds, !!minutes); return `${formattedHours}${formattedMinutes}${formattedSeconds}`; }; const formatTimeToken = (token, shouldPad) => { return shouldPad ? String(token).padStart(2, '0') : token; }; const calculateExtraPlaylistStats = () => { const containerElement = document.querySelector( 'ytd-playlist-byline-renderer', ); if (containerElement && !containerElement.querySelector('.extra-stats')) { containerElement .querySelector('.metadata-stats') .prepend(extraStatsElement); } const playlistLength = getPlaylistLength(); console.log(`Extra playlist stats: Videos: ${playlistLength.videos} Length: ${formatLength(playlistLength.seconds)} Length on average: ${formatLength( Math.round(playlistLength.seconds / playlistLength.videos), )}`); durationElement.innerText = `Duration: ${formatLength( playlistLength.seconds, )} `; }; const queryDropdownDeleteItem = () => { return ( document.querySelector( 'tp-yt-iron-dropdown:not([style*="display: none;"]):has(:nth-child(8)) ytd-menu-service-item-renderer:nth-child(4)', ) || document.querySelector( 'tp-yt-iron-dropdown:not([style*="display: none;"]):has(:nth-child(7)) ytd-menu-service-item-renderer:nth-child(3)', ) || document.querySelector( 'tp-yt-iron-dropdown:not([style*="display: none;"]):has(:nth-child(4)) ytd-menu-service-item-renderer:nth-child(2)', ) ); }; const deleteVideoMatches = (queryMatch) => { intervalId = setInterval(() => { const dropdownItem = queryDropdownDeleteItem(); if (dropdownItem) { dropdownItem.click(); return; } const match = queryMatch(); if (!match) { clearInterval(intervalId); return; } const matchTitle = match.querySelector('#video-title'); console.info(`Deleting "${matchTitle?.innerText}" (${matchTitle?.href})`); match.querySelector('ytd-menu-renderer button').click(); }, INTERACTION_INTERVAL); }; const deleteWatched = () => { clearInterval(intervalId); deleteVideoMatches(() => document.querySelector( 'ytd-playlist-video-renderer:has(:where(.ytd-thumbnail-overlay-resume-playback-renderer, .ytThumbnailOverlayProgressBarHost)), ytd-playlist-panel-video-renderer:has(:where(.ytd-thumbnail-overlay-resume-playback-renderer, .ytThumbnailOverlayProgressBarHost))', ), ); }; const deleteByText = (...texts) => { clearInterval(intervalId); deleteVideoMatches(() => Array.from( document.querySelectorAll( 'ytd-playlist-video-renderer, ytd-playlist-panel-video-renderer', ), ).find((el) => { const titleEl = el.querySelector('#video-title'); const title = titleEl?.innerText.normalize('NFKC').toLowerCase(); return texts.some((text) => title?.includes(text.toLowerCase())); }), ); }; const deleteUnavailable = () => { clearInterval(intervalId); intervalId = setInterval(() => { let element = document.querySelector( 'tp-yt-iron-dropdown:not([style*="display: none;"]) ytd-menu-service-item-renderer:nth-child(1)', ) || document.querySelector( 'ytd-playlist-video-renderer:has([src="https://i.ytimg.com/img/no_thumbnail.jpg"]) ytd-menu-renderer button', ); element ? element.click() : clearInterval(intervalId); }, INTERACTION_INTERVAL); }; const saveToWatchLater = () => { clearInterval(intervalId); const videos = document.querySelectorAll( '#contents > ytd-rich-item-renderer.ytd-rich-grid-renderer:not(:has(:where(.ytd-thumbnail-overlay-resume-playback-renderer, .ytThumbnailOverlayProgressBarHost)))', ); let i = 0; intervalId = setInterval(() => { let element = document.querySelector( 'tp-yt-iron-dropdown:not([style*="display: none;"]) yt-list-item-view-model:nth-child(2)', ); while (!element && i < videos.length) { const button = videos[i++].querySelector( '.ytLockupMetadataViewModelMenuButton button', ); if (button) { element = button; break; } } element ? element.click() : clearInterval(intervalId); }, INTERACTION_INTERVAL); }; const initialize = () => { durationElement = document.createElement('span'); extraStatsElement = document.createElement('span'); extraStatsElement.className = 'extra-stats byline-item style-scope ytd-playlist-byline-renderer'; extraStatsElement.appendChild(durationElement); unsafeWindow.calculateExtraPlaylistStats = calculateExtraPlaylistStats; unsafeWindow.deleteByText = deleteByText; unsafeWindow.deleteUnavailable = deleteUnavailable; unsafeWindow.deleteWatched = deleteWatched; unsafeWindow.saveToWatchLater = saveToWatchLater; GM_registerMenuCommand( 'Calculate playlist duration', calculateExtraPlaylistStats, ); GM_registerMenuCommand('Delete watched videos', deleteWatched); GM_registerMenuCommand('Delete unavailable videos', deleteUnavailable); GM_registerMenuCommand('Save from grid to Watch Later', saveToWatchLater); bindForwardButton(); }; const bindForwardButton = () => { window.addEventListener( 'mouseup', (e) => { if (e.button === 4) { const currentVideoEl = document.querySelector( 'ytd-playlist-panel-video-renderer[selected]', ); const nextEl = currentVideoEl ? currentVideoEl?.nextElementSibling?.querySelector('a') : document.querySelector('.ytp-next-button'); nextEl ? nextEl.click() : history.forward(); } }, true, ); }; initialize(); })();