Last active
July 28, 2024 17:23
-
-
Save vogler/451aa48d0af7b659e391fdbeeea0d9d8 to your computer and use it in GitHub Desktop.
Tampermonkey: YouTube: show time left in title
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name YouTube: show time left in title | |
// @namespace https://gist.github.com/vogler | |
// @downloadURL https://gist.github.com/vogler/451aa48d0af7b659e391fdbeeea0d9d8/raw/youtube-time-left.tamper.js | |
// @version 0.6 | |
// @description YouTube: show time left in title | |
// @author Ralf Vogler | |
// @ match https://www.youtube.com/watch?v=* // this will not work if you open youtube.com and then click on a video since it is a SPA | |
// @match https://www.youtube.com/* | |
// @grant window.onurlchange | |
// ==/UserScript== | |
// options | |
const opt = { | |
onLoad: true, // false: only update title after video starts playing | |
position: 'start', // add to 'start' | 'end' of document.title | |
withRate: true, // true: divide time left by playback speed, false: ignore playback speed | |
} | |
const durationRegex = '(\\d{1,2}:)*\\d{1,2}'; // regex as string used below -> need to escape \ | |
const titleRegex = new RegExp(opt.position == 'start' ? `^${durationRegex} - ` : ` - ${durationRegex}$`); | |
const setTitle = timeLeft => { | |
// if (!timeLeft) return; | |
const sep = timeLeft ? ' - ' : ''; | |
const title = document.title.replace(titleRegex, ''); // strip timeLeft: simpler and more reliable alternative to urlchange listener + MutationObserver on title | |
document.title = opt.position == 'start' ? timeLeft + sep + title : title + sep + timeLeft; | |
}; | |
const parseDuration = str => str.split(':').toReversed().reduce((a,x,i) => a + parseInt(x) * 60**i, 0); // to seconds | |
const formatDuration = seconds => new Date(1000 * seconds).toISOString().substr(11, 8).replace(/^[0:]+/, ""); | |
(async function() { | |
'use strict'; | |
// console.log('title onload:', document.title); | |
// let originalTitle = document.title; | |
// window.addEventListener('urlchange', e => { // update originalTitle after urlchange | |
// // console.log('urlchange', e.url, document.title); | |
// new MutationObserver((m, o) => { | |
// // console.log('title change:', document.title, m); | |
// originalTitle = document.title; | |
// o.disconnect(); // TODO seems like this does not work reliably | |
// }).observe(document.querySelector('title'), { childList: true }); | |
// }, { passive: true }); | |
// new MutationObserver((m, o) => console.log('title change:', document.title, m)).observe(document.querySelector('title'), { childList: true }); | |
// window.addEventListener('focus', e => console.log('window.focus')); // to debug opening page in background tab | |
// const v = document.querySelector('#movie_player video'); | |
// console.log('video:', v); | |
// console.log('duration:', v.duration, 'playbackRate:', v.playbackRate); | |
// duration is not set if page is loaded in background tab, playbackRate may still be 1 if Video Speed Controller extension runs afterwards | |
// update time left in title | |
// the surrounding MutationObserver is needed for when navigating from youtube.com to a video instead of opening it in a new tab | |
(new MutationObserver((mutations, observer) => { | |
if (document.location.pathname != '/watch') return; // not on a video page | |
// if (!document.querySelector('#ytd-player')) return; // not enough since setting the title here will revert it to just 'YouTube' | |
if (!mutations.some(m => m.target.className == 'ytp-large-play-button ytp-button')) return; | |
// console.log(mutations); | |
// observer.disconnect(); // problem: tabs loaded in background first had time left but then title was set again without it | |
// if (document.title != originalTitle) return; // problem: aborts if clicking on video on start page | |
if (document.title.match(titleRegex)) return; | |
const v = document.querySelector('#movie_player video'); | |
// console.log('video:', v); | |
const originalDuration = parseDuration(document.querySelector('.ytp-time-duration').innerText); | |
console.log('youtube-time-left:', 'duration:', v.duration, 'playbackRate:', v.playbackRate, 'originalDuration:', originalDuration); | |
const update = e => { | |
if (document.location.pathname != '/watch') return; | |
let timeLeft = (v.duration || originalDuration) - v.currentTime; | |
if (opt.withRate) timeLeft /= v.playbackRate; | |
timeLeft = formatDuration(timeLeft); | |
setTitle(timeLeft); | |
}; | |
if (opt.onLoad) update(); | |
v.addEventListener('timeupdate', update, { passive: true }); | |
})).observe(document, { subtree: true, childList: true }); // .querySelector('#page-manager') and #content was not reliable | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment