您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Calculate the duration of a playlist and display it next to the number of videos. Also detects and displays video playback speed.
// ==UserScript== // @name Youtube Playlist Duration Calculator (Forked) // @namespace http://tampermonkey.net/ // @version 1.1.6 // @description Calculate the duration of a playlist and display it next to the number of videos. Also detects and displays video playback speed. // @author HellFiveOsborn (Forked from DenverCoder1) // @icon https://i.imgur.com/FwUCnbF.png // @source https://greasyfork.org/pt-BR/scripts/407457-youtube-playlist-duration-calculator // @match https://www.youtube.com/playlist?list=* // @match https://www.youtube.com/watch?v=*&list=* // @match https://www.youtube.com/watch?v=* // @exclude https://www.youtube.com/shorts/* // @exclude https://www.youtube.com/ // @exclude https://www.youtube.com/feed/* // @grant none // @license MIT // ==/UserScript== (function () { "use strict"; /** * Converts a time string in the format "HH:MM:SS" or "MM:SS" or "SS" to the total number of seconds. * * @param {string} timeString - The time string to convert, formatted as "HH:MM:SS", "MM:SS", or "SS". * @returns {number} The total number of seconds represented by the input time string. */ function timeToSeconds(timeString) { const parts = timeString.split(':').map(Number).reverse(); // Split by ':' and reverse the array let seconds = 0; if (parts[0]) seconds += parts[0]; // Add seconds if (parts[1]) seconds += parts[1] * 60; // Add minutes converted to seconds if (parts[2]) seconds += parts[2] * 3600; // Add hours converted to seconds return seconds; } /** * Calculate the duration of a playlist * * @returns {string} Duration of the playlist in a human readable format */ function calculateDuration() { // get data object stored on Youtube's website const data = window.ytInitialData; // Extracts the two main content structures from the data object using destructuring. const { twoColumnBrowseResultsRenderer, twoColumnWatchNextResults } = data.contents || {}; // Safely access nested properties with optional chaining to avoid runtime errors const browseContents = twoColumnBrowseResultsRenderer?.tabs[0]?.tabRenderer?.content; // Attempt to get the playlist content from the watch next results const watchNextContents = twoColumnWatchNextResults?.playlist?.playlist; // Try to extract the list of videos from one of the known content structures. const vids = browseContents?.sectionListRenderer?.contents[0]?.itemSectionRenderer?.contents[0]?.playlistVideoListRenderer?.contents || watchNextContents?.contents; // Calculate the total duration of all videos in seconds const seconds = vids.reduce(function (x, y) { const videoRenderer = y.playlistVideoRenderer || y.playlistPanelVideoRenderer; if (!videoRenderer) return x; // If 'lengthSeconds' is available and valid, use it directly if (!isNaN(videoRenderer.lengthSeconds)) { return x + parseInt(videoRenderer.lengthSeconds); } // If 'lengthText.simpleText' is available, convert it to seconds else if (videoRenderer.lengthText?.simpleText) { return x + timeToSeconds(videoRenderer.lengthText.simpleText); } // If neither is available, return the current total return x; }, 0); // divide by 60 and round to get the number of minutes const minutes = Math.round(seconds / 60); // if there is at least 1 hour, display hours and minutes, otherwise display minutes and seconds. const durationString = minutes >= 60 // if minutes is 60 or more ? Math.floor(minutes / 60) + "h " + (minutes % 60) + "m" // calculate hours and minutes : Math.floor(seconds / 60) + "m " + (seconds % 60) + "s"; // calculate minutes and seconds return durationString; } /** * Append the duration to the playlist metadata */ function appendDurationToPlaylistMetadata() { const metadataRow = document.querySelectorAll('div.yt-content-metadata-view-model-wiz__metadata-row')[3] if (!metadataRow) return; const durationString = calculateDuration(); // Create a new span for the duration const durationSpan = document.createElement('span'); 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'; durationSpan.setAttribute('dir', 'auto'); durationSpan.setAttribute('role', 'text'); durationSpan.textContent = durationString; durationSpan.style.color = '#ff0'; // Highlight color // Add a delimiter before the duration const delimiterSpan = document.createElement('span'); delimiterSpan.className = 'yt-content-metadata-view-model-wiz__delimiter'; delimiterSpan.setAttribute('aria-hidden', 'true'); delimiterSpan.textContent = '•'; // Append the delimiter and duration to the metadata row metadataRow.appendChild(delimiterSpan); metadataRow.appendChild(durationSpan); console.debug('Duration of playlist:', durationString); } /** * Append the duration to the playlist header when watching a video in a playlist */ function appendDurationToPlaylistHeader() { const headerDescription = document.querySelectorAll('div#publisher-container')[2]; if (!headerDescription) return; waitForElement('.html5-video-container > video').then(() => { const videoElement = document.querySelector('.html5-video-container > video'); if (!videoElement) return; const speed = videoElement.playbackRate; const durationString = calculateDuration(); // Calculate the adjusted duration if playback speed is not 1x const totalSeconds = parseInt(durationString.split(' ').reduce((acc, val) => { if (val.includes('h')) acc += parseInt(val) * 3600; if (val.includes('m')) acc += parseInt(val) * 60; if (val.includes('s')) acc += parseInt(val); return acc; }, 0)); const adjustedDuration = speed !== 1 ? totalSeconds / speed : totalSeconds; // Format adjusted duration into hours, minutes, and seconds const hours = Math.floor(adjustedDuration / 3600); const minutes = Math.floor((adjustedDuration % 3600) / 60); const seconds = Math.floor(adjustedDuration % 60); // Create a human-readable string const formattedDuration = hours > 0 ? `${hours}h ${minutes.toString().padStart(2, '0')}m` : `${minutes}m ${seconds.toString().padStart(2, '0')}s`; // Remove the previous duration if it exists if (document.querySelector('#playlist-duration-calculated')) { // If the duration is the same, don't update if (document.querySelector('#playlist-duration-calculated').textContent === formattedDuration) return; document.querySelector('#playlist-duration-calculated').remove(); } console.debug('Playback speed:', speed); console.debug('Duration of playlist:', durationString); console.debug('Adjusted duration:', formattedDuration); // Create a new span for the duration const durationSpan = document.createElement('div'); durationSpan.className = 'index-message-wrapper style-scope ytd-playlist-panel-renderer'; durationSpan.id = 'playlist-duration-calculated'; durationSpan.setAttribute('dir', 'auto'); durationSpan.setAttribute('role', 'text'); durationSpan.textContent = `${formattedDuration}`; durationSpan.style.marginLeft = '5px'; // Add margin to the left durationSpan.style.color = '#ff0'; // Highlight color // Append the duration to the header description headerDescription.appendChild(durationSpan); }); } /** * Detect video playback speed and display calculated duration */ function detectPlaybackSpeed() { const videoElement = document.querySelector('.html5-video-container > video'); if (!videoElement) return; const speed = videoElement.playbackRate; const currentTime = videoElement.currentTime; const duration = videoElement.duration; // Only display if playback speed is not 1x if (speed !== 1) { const calculatedDuration = (duration - currentTime) / speed; const timeWrapper = document.querySelector('.ytp-time-wrapper'); if (!timeWrapper) return; let calculatedDurationElement = timeWrapper.querySelector('.ytp-time-duration-calculed'); if (!calculatedDurationElement) { calculatedDurationElement = document.createElement('span'); calculatedDurationElement.className = 'ytp-time-duration-calculed'; calculatedDurationElement.style.color = '#ff0'; // Highlight color timeWrapper.appendChild(calculatedDurationElement); } const minutes = Math.floor(calculatedDuration / 60); const seconds = Math.floor(calculatedDuration % 60); calculatedDurationElement.textContent = ` (${minutes}:${seconds.toString().padStart(2, '0')})`; return calculatedDurationElement; } else { // Remove the calculated duration if speed is back to 1x const calculatedDurationElement = document.querySelector('.ytp-time-duration-calculed'); if (calculatedDurationElement) { calculatedDurationElement.remove(); } return null; } } /** * Wait for an element using an observer * * @param {string} selector Selector to wait for * * @see https://stackoverflow.com/a/61511955 */ function waitForElement(selector) { return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver((_) => { if (document.querySelector(selector)) { resolve(document.querySelector(selector)); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true, }); }); } /** * Check if the current page is a playlist or a single video */ function isPlaylistOrVideoPage() { const url = window.location.href; return ( url.includes('/playlist?list=') || // Playlist page url.includes('/watch?v=') // Single video page ); } // Only run the script on playlist or single video pages if (isPlaylistOrVideoPage()) { setTimeout(() => { if (window.location.href.includes('/playlist?list=')) { // Append duration to playlist metadata waitForElement('.yt-content-metadata-view-model-wiz__metadata-row').then(() => { appendDurationToPlaylistMetadata() }); } else if (window.location.href.includes('/watch?v=')) { // Append duration to playlist header when watching a video in a playlist if (window.location.href.includes('list=')) { waitForElement('div#publisher-container').then(() => { setInterval(appendDurationToPlaylistHeader, 1500); }); } // Detect playback speed and update calculated duration periodically setInterval(detectPlaybackSpeed, 1000); } }, 1500); } })();