Skip to content

Instantly share code, notes, and snippets.

@vogler
Last active July 28, 2024 17:23
Show Gist options
  • Save vogler/451aa48d0af7b659e391fdbeeea0d9d8 to your computer and use it in GitHub Desktop.
Save vogler/451aa48d0af7b659e391fdbeeea0d9d8 to your computer and use it in GitHub Desktop.
Tampermonkey: YouTube: show time left in title
// ==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