您需要先安装一个扩展,例如 篡改猴、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);
- }
- })();