YouTube Remaining Time

Show the remaining time of a YouTube video inside the player.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         YouTube Remaining Time
// @namespace    http://the6p4c.com/
// @version      1.3
// @description  Show the remaining time of a YouTube video inside the player.
// @author       The6P4C
// @match        *://www.youtube.com/*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js
// @grant        none
// ==/UserScript==

// ***********************************************************************
// CHANGE TO true TO DISPLAY REMAINING TIME COMPENSATING FOR PLAYBACK RATE
// ***********************************************************************
var SHOW_REAL_TIME_REMAINING = false;

var TIME_KEYS_ORDER = ["days", "hours", "minutes", "seconds"];
var TIME_KEYS_ORDER_REV = [].concat(TIME_KEYS_ORDER).reverse();

function parseTime(timeString) {
    // reversed so seconds first
    var parts = timeString.split(":").reverse();
    var time = {};

    for (var i = 0; i < TIME_KEYS_ORDER_REV.length; ++i) {
        var key = TIME_KEYS_ORDER_REV[i];

        if (i < parts.length) {
            time[key] = parseInt(parts[i]);
        } else {
            time[key] = 0;
        }
    }

    return time;
}

function pad2(n) {
    if (n >= 10) {
        return n;
    } else {
        return "0" + n;
    }
}

function stringifyTime(time) {
    var timeString = "";
    var nonZeroEncountered = false;

    for (var i = 0; i < TIME_KEYS_ORDER.length; ++i) {
        var key = TIME_KEYS_ORDER[i];
        var currentValue = time[key];

        // Only start adding values from the first non zero
        // But... always show minutes, even if they're zero.
        if (currentValue !== 0 || nonZeroEncountered || key == "minutes") {
            // We don't want to pad the first value:
            // If there's hours, don't pad days
            // If there's minutes, don't pad hours
            // If there's seconds, don't pad minutes
            if (nonZeroEncountered) {
                currentValue = pad2(currentValue);
            }

            timeString += currentValue;

            if (key != "seconds") {
                timeString += ":";
            }

            nonZeroEncountered = true;
        }
    }

    return timeString;
}

function subtractTime(a, b) {
    var secondsDiff = a.seconds - b.seconds;
    var minutesDiff = a.minutes - b.minutes;
    var hoursDiff = a.hours - b.hours;
    var daysDiff = a.days - b.days;

    if (secondsDiff < 0) {
        minutesDiff -= 1;
        secondsDiff += 60;
    }

    if (minutesDiff < 0) {
        hoursDiff -= 1;
        minutesDiff += 60;
    }

    if (hoursDiff < 0) {
        daysDiff -= 1;
        hoursDiff += 24;
    }

    return {
        days: daysDiff,
        hours: hoursDiff,
        minutes: minutesDiff,
        seconds: secondsDiff
    };
}

function divideTime(time, divisor) {
    var timeSeconds = time.seconds + ((time.minutes + (time.hours + time.days * 24) * 60) * 60);
    var newTimeSeconds = Math.floor(timeSeconds / divisor);

    return {
        days: Math.floor(newTimeSeconds / (24 * 60 * 60)),
        hours: Math.floor(newTimeSeconds / (60 * 60)),
        minutes: Math.floor(newTimeSeconds / 60),
        seconds: newTimeSeconds % 60
    };
}

function tryGetPlaybackRate($timeDisplay) {
    // Try and find the playback speed from the settings UI
    var $ytdPlayer = $timeDisplay.parents(".html5-video-player");
    if ($ytdPlayer.length != 1) {
        // If we can't find the player, fall back to a "safe" alternative
        return 1;
    }

    $ytdPlayer = $ytdPlayer[0];

    // We can't be sure that this will work - if YouTube changes their player API somehow
    // we don't want our entire script to break. Again, fall back to a "safe" alternative.
    try {
        return $ytdPlayer.getPlaybackRate();
    } catch (e) {
       return 1;
    }
}

function onTimeChange() {
    var $this = $(this);

    // If this event was called in the context of the .ytp-time-duration event,
    // we need to set $timeCurrent not to $this but to the actual
    // .ytp-time-current element.
    var $timeCurrent = $this;
    if (!$this.hasClass("ytp-time-current")) {
        $timeCurrent = $this.parent().find(".ytp-time-current");
    }

    var $timeDisplay = $timeCurrent.parent();

    // For some reason a time will show on a live video, but I don't think it's very meaningful
    if ($timeDisplay.hasClass("ytp-live")) {
        return;
    }

    var $timeDuration = $timeDisplay.parent().find(".ytp-time-duration");

    if ($timeDisplay.attr("____remaining-added") != "____remaining-added") {
        var $timeRemainingWrapper = $("<div style='display: inline-block; width: 5px;'></div><span style='color: #ddd; text-shadow: 0 0 2px rgba(0,0,0,.5)'>(&minus;<span class='____time-remaining'></span>)</span>");
        $timeDuration.after($timeRemainingWrapper);

        $timeDisplay.attr("____remaining-added", "____remaining-added");
    }

    var $timeRemaining = $timeDisplay.find(".____time-remaining");

    var currentTime = parseTime($timeCurrent.text());
    var durationTime = parseTime($timeDuration.text());
    var remainingTime = subtractTime(durationTime, currentTime);

    if (SHOW_REAL_TIME_REMAINING) {
        var playbackRate = tryGetPlaybackRate($timeDisplay);
        var remainingRealTime = divideTime(remainingTime, playbackRate);

        var suffix = playbackRate == 1 ? "" : (" at x" + playbackRate);
        $timeRemaining.text(stringifyTime(remainingRealTime) + suffix);
    } else {
        $timeRemaining.text(stringifyTime(remainingTime));
    }
}

$(document).ready(function() {
    $("body").on("DOMSubtreeModified", ".ytp-time-current", onTimeChange);
    $("body").on("DOMSubtreeModified", ".ytp-time-duration", onTimeChange);
});