Teachable Time Helper

Calculate and display amount of time spent watching lectures and how much time remains to be spent watching lectures for the course.

// ==UserScript==
// @name         Teachable Time Helper
// @namespace    http://tampermonkey.net/
// @version      2024-05-05
// @description  Calculate and display amount of time spent watching lectures and how much time remains to be spent watching lectures for the course.
// @author       Daniel Hillis (https://github.com/dhillis)
// @match        https://learn.cantrill.io/courses/enrolled/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=cantrill.io
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    class Section {
        constructor(sectionName){
            this.name = sectionName;
            this.totalSeconds = 0;
            this.completedSeconds = 0;
        }
        get remainingSeconds(){
            return this.totalSeconds - this.completedSeconds;
        }
    }

    function getDurationString(timeInSeconds) {
        let hours = Math.trunc(timeInSeconds/3600);
        let hoursRemainder = timeInSeconds%3600;
        let minutes = Math.trunc(hoursRemainder/60);
        let seconds = hoursRemainder%60;
        if (hours > 0){
            return (hours.toString()
                    .concat(':')
                    .concat(String(minutes).padStart(2, '0'))
                    .concat(':')
                    .concat(String(seconds).padStart(2, '0')));
        }
        else {
            return (String(minutes)
                    .concat(':')
                    .concat(String(seconds).padStart(2, '0')));
        }
    }

    var sections = $('.course-mainbar div.row div.course-section');
    var sectionCount = sections.length;
    var sectionCourses = $('.course-mainbar div.row div.course-section ul.section-list');
    var lectureCount = $('span.lecture-name').length;

    const regexpCourseDurationMMSS = /([0-9]{1,2})(?=:):([0-9]{2})/;
    const sectionMap = new Map();

    let overallDurationSeconds = 0;
    let overallDurationCompletedSeconds = 0;

    for(let sec of document.querySelectorAll(".course-mainbar div.row div.course-section")) {
        let currentSection = sec.querySelector("div.section-title");
        let currentSectionTitle = currentSection.innerText;

        // store the map for this section
        let currentSectionCounts = new Section(currentSectionTitle);
        sectionMap.set(currentSectionTitle, currentSectionCounts);

        for(let c of sec.querySelectorAll("ul.section-list li.section-item")) {
            let courseName = c.querySelector("span.lecture-name").innerText;
            let completed = c.classList.contains("completed");

            // courseName sample:
            // [ASSOCIATESHARED] [DEMO] Automated EC2 Control using Lambda and Events - PART2 (18:49 )

            // get duration from the course title
            let duration = courseName.match(regexpCourseDurationMMSS);

            if (duration && duration.length === 3){
                let minutes = parseInt(duration[1]);
                let seconds = parseInt(duration[2]);
                let courseTotalSeconds = seconds + ( minutes * 60 );

                currentSectionCounts.totalSeconds+= courseTotalSeconds;
                overallDurationSeconds += courseTotalSeconds;
                if (completed){
                    currentSectionCounts.completedSeconds+= courseTotalSeconds;
                    overallDurationCompletedSeconds+= courseTotalSeconds;
                }
            }
        }

        currentSection.innerText += ` (${getDurationString(currentSectionCounts.remainingSeconds)} remaining)`;
    }

    var tbl = document.createElement('table');
    tbl.style.width = '100%';
    tbl.setAttribute('border', '1');
    var tbdy = document.createElement('tbody');

    let playback_speeds = [0.50, 0.75, 1.00, 1.25, 1.50, 2.00];

    const cellpadding = "10px";
    const headfootbgcolor = "#5a1d82";
    const headfootfontsize = "120%";
    const cellhighlightcolor = "#e0d7e6";
    const white = "#FFFFFF";
    const speed1fontsize = "115%";
    const speed1fontweight = "900";

    let table = document.createElement('TABLE');
    table.border = '1';
    table.style.fontSize = "50%";
    table.style.textAlign = "center";
    let tableHead = document.createElement('THEAD');
    tableHead.style.backgroundColor = headfootbgcolor;
    tableHead.style.color = white;

    let headrow1 = document.createElement('TR');

    let headrow1_th1 = document.createElement('TH');
    headrow1_th1.appendChild(document.createTextNode('Section'))
    headrow1_th1.style.textAlign = "center";
    headrow1_th1.style.paddingLeft = cellpadding;
    headrow1_th1.style.paddingRight = cellpadding;
    headrow1_th1.rowSpan = 2;
    headrow1.appendChild(headrow1_th1);

    let headrow1_th2 = document.createElement('TH');
    headrow1_th2.appendChild(document.createTextNode('Playback Speeds'))
    headrow1_th2.style.textAlign = "center";
    headrow1_th2.style.paddingLeft = cellpadding;
    headrow1_th2.style.paddingRight = cellpadding;
    headrow1_th2.colSpan = playback_speeds.length;
    headrow1.appendChild(headrow1_th2);

    tableHead.appendChild(headrow1);

    let headrow2 = document.createElement('TR');
    for(let speed of playback_speeds){
        let th = document.createElement('TH');
        th.appendChild(document.createTextNode(`${speed.toFixed(2)}X`));
        th.style.textAlign = "center";
        th.style.paddingLeft = cellpadding;
        th.style.paddingRight = cellpadding;
        if (speed == 1.0) {
            th.style.fontWeight = speed1fontweight;
            th.style.fontSize = speed1fontsize;
        }
        tableHead.style.fontSize = headfootfontsize;
        headrow2.appendChild(th);
    }
    tableHead.appendChild(headrow2);

    table.appendChild(tableHead);

    let tableBody = document.createElement('TBODY');
    for (const [key, value] of sectionMap) {
        var sectionTR = document.createElement('TR');
        let sectionNameTD = document.createElement('TD');
        sectionNameTD.appendChild(document.createTextNode(key));
        sectionNameTD.style.paddingLeft = cellpadding; sectionNameTD.style.paddingRight = cellpadding;
        sectionNameTD.style.textAlign = "center";
        sectionTR.appendChild(sectionNameTD);
        for (let speed of playback_speeds) {
            var td = document.createElement('TD');
            td.style.textAlign = "center";
            td.style.paddingLeft = cellpadding; td.style.paddingRight = cellpadding;
            if (speed == 1.0) {
                td.style.backgroundColor = cellhighlightcolor;
                td.style.fontWeight = speed1fontweight;
                td.style.fontSize = speed1fontsize;
            }
            td.appendChild(document.createTextNode(getDurationString(Math.round(value.remainingSeconds / speed))));
            sectionTR.appendChild(td);
        }
        tableBody.appendChild(sectionTR);
    }
    table.appendChild(tableBody);

    const overall_rt_secs = overallDurationSeconds - overallDurationCompletedSeconds;

    let tableFoot = document.createElement('TFOOT');
    tableFoot.style.backgroundColor = headfootbgcolor;
    tableFoot.style.color = white;
    let tfth1 = document.createElement('TH');
    tfth1.appendChild(document.createTextNode('Overall Remaining Time'));
    tfth1.style.textAlign = "center";
    tfth1.style.paddingLeft = cellpadding; tfth1.style.paddingRight = cellpadding;
    tableFoot.appendChild(tfth1);
    for(let speed of playback_speeds){
        let th = document.createElement('TH');
        th.appendChild(document.createTextNode(`${getDurationString(Math.round(overall_rt_secs / speed))}`));
        th.style.textAlign = "center";
        th.style.paddingLeft = cellpadding; th.style.paddingRight = cellpadding;
        if (speed == 1.0) {
            th.style.fontWeight = speed1fontweight;
        }
        tableFoot.style.fontSize = headfootfontsize;
        tableFoot.appendChild(th);
    }

    table.appendChild(tableFoot);

    $('.course-mainbar .section-title').first().append(table);

})();