Skip to content

Instantly share code, notes, and snippets.

@vogler
Last active January 21, 2024 21:30
Show Gist options
  • Save vogler/51d82f4887d75f718dddbcf45e8feb2f to your computer and use it in GitHub Desktop.
Save vogler/51d82f4887d75f718dddbcf45e8feb2f to your computer and use it in GitHub Desktop.
Tampermonkey: Video: show time left in title
// ==UserScript==
// @name Video: show time left in title
// @namespace https://gist.github.com/vogler
// @downloadURL https://gist.github.com/vogler/51d82f4887d75f718dddbcf45e8feb2f/raw/video-time-left.tamper.js
// @version 0.1
// @description Video: show time left in title
// @author Ralf Vogler
// @match https://*.zdf.de/*
// @grant window.onurlchange
// ==/UserScript==
// match all *://*/*
// 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 formatDuration = seconds => new Date(1000 * seconds).toISOString().substr(11, 8).replace(/^[0:]+/, "");
const parseDuration = str => str.split(':').toReversed().reduce((a,x,i) => a + parseInt(x) * 60**i, 0); // to seconds
(async function() {
'use strict';
// console.log('title onload:', document.title);
let originalTitle = document.title;
// update time left in title
const f = el => {
if (!el.getElementsByTagName) return; // Angular adds some weird #comment and #text elements which don't have this function...
const vs = el.tagName == 'VIDEO' ? [el] : [...el.getElementsByTagName('video')]; // find videos in element
if (vs.length < 1) return;
// console.log('video-time-left: found', vs.length, 'video(s)', vs, 'in', el);
vs.forEach(v => {
if (v.handled) return;
v.handled = true;
const update = e => {
let timeLeft = v.duration - v.currentTime;
// console.log('video-time-left:', 'duration:', v.duration, 'playbackRate:', v.playbackRate, 'timeLeft:', timeLeft, v);
if (!v.duration) return;
if (opt.withRate) timeLeft /= v.playbackRate;
timeLeft = formatDuration(timeLeft);
const sep = timeLeft ? ' - ' : '';
if (opt.position == 'start') {
originalTitle = document.title.replace(/^(\d{1,2}:)*\d{1,2} - /, ''); // strip timeLeft: simpler and more reliable alternative to urlchange listener + MutationObserver on title
document.title = timeLeft + sep + originalTitle;
} else {
originalTitle = document.title.replace(/ - (\d{1,2}:)*\d{1,2}$/, ''); // same as above, but at the end
document.title = originalTitle + sep + timeLeft;
}
};
if (opt.onLoad) update();
v.addEventListener('timeupdate', update, { passive: true });
});
};
f(document.body); // handle all videos
// setTimeout(() => f(document.body), 3000); // if our event listener gets removed, we add it again after 3s
// we also want to react to dynamically added videos, i.e., call f on any nodes added to document.body
(new MutationObserver((ms, ob) => ms.forEach(m => m.addedNodes.forEach(f)))).observe(document.body, { subtree: true , childList: true });
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment