YouTube Timestamp Tool by Vat5aL

Enhanced timestamp tool for YouTube videos

// ==UserScript==
// @name         YouTube Timestamp Tool by Vat5aL
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Enhanced timestamp tool for YouTube videos
// @author       Vat5aL
// @match        https://www.youtube.com/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    function formatTime(e, t) {
        var n, a = Math.floor(t / 3600), o = Math.floor(t / 60) % 60, i = Math.floor(t) % 60;
        e.textContent = (a ? a + ":" + String(o).padStart(2, "0") : o) + ":" + String(i).padStart(2, "0");
        e.dataset.time = t;
        var vid = location.search.split(/.+v=|&/)[1] || location.href.split(/\/live\/|\/shorts\/|\?|&/)[1];
        e.href = "https://youtu.be/" + vid + "?t=" + t;
    }

    function parseTimeRange(str) {
        var matches = str.match(/(\d*:*\d+):(\d+)(?:\s*[-–]\s*)(\d*:*\d+):(\d+)/);
        if (!matches) return null;
        var startParts = matches[1].split(':').map(Number),
            startSec = parseInt(matches[2]),
            endParts = matches[3].split(':').map(Number),
            endSec = parseInt(matches[4]);
        var startMin = startParts.length === 1 ? startParts[0] : startParts[0] * 60 + startParts[1],
            endMin = endParts.length === 1 ? endParts[0] : endParts[0] * 60 + endParts[1];
        return { start: startMin * 60 + startSec, end: endMin * 60 + endSec };
    }

    function parseCopiedFormat(str) {
        var matches = str.match(/^(?:.*?\?v=[^&\s]+&.*?\s)?(\d*:*\d+:\d+)\s*-\s*(\d*:*\d+:\d+)\s*"(.+)"$/);
        if (!matches) return null;
        var startParts = matches[1].split(':').map(Number),
            endParts = matches[2].split(':').map(Number),
            comment = matches[3];
        var startMin = startParts.length === 2 ? startParts[0] : startParts[0] * 60 + startParts[1],
            startSec = startParts.length === 2 ? startParts[1] : startParts[2],
            endMin = endParts.length === 2 ? endParts[0] : endParts[0] * 60 + endParts[1],
            endSec = endParts.length === 2 ? endParts[1] : endParts[2];
        return { start: startMin * 60 + startSec, end: endMin * 60 + endSec, comment: comment };
    }

    function handleClick(e) {
        if (e.target.dataset.time) {
            e.preventDefault();
            document.querySelector("video").currentTime = e.target.dataset.time;
        } else if (e.target.dataset.increment) {
            e.preventDefault();
            var t = e.target.parentElement.querySelector('a[data-time]');
            var currTime = parseInt(t.dataset.time);
            formatTime(t, Math.max(0, currTime + parseInt(e.target.dataset.increment)));
        } else if (e.target.dataset.action === "end") {
            e.preventDefault();
            var li = e.target.parentElement, startLink = li.querySelector('a[data-time]');
            if (!li.querySelector('.end-time')) {
                var endLink = document.createElement("a");
                formatTime(endLink, Math.floor(document.querySelector("video").currentTime));
                endLink.className = 'end-time';
                startLink.textContent += " - " + endLink.textContent;
                var timeRow = li.querySelector('.time-row');
                timeRow.insertBefore(endLink, e.target.nextSibling);
                e.target.remove();
                updateSeekbarMarkers();
            }
        } else if (e.target.dataset.action === "clear") {
            e.preventDefault();
            list.textContent = "";
            updateSeekbarMarkers();
            updateScroll();
        }
    }

    function addTimestamp(e, t) {
        var li = document.createElement("li"), timeRow = document.createElement("div"), minus = document.createElement("span"),
            plus = document.createElement("span"), a = document.createElement("a"), endBtn = document.createElement("button"),
            commentInput = document.createElement("input"), del = document.createElement("button");

        timeRow.className = "time-row";
        minus.textContent = "➖"; minus.dataset.increment = 1; minus.style.cursor = "pointer";
        plus.textContent = "➕"; plus.dataset.increment = -1; plus.style.cursor = "pointer";
        formatTime(a, e);
        endBtn.textContent = "End"; endBtn.dataset.action = "end"; endBtn.style = "background:#555;color:white;border:none;padding:2px 5px;border-radius:3px;cursor:pointer;";
        commentInput.value = t || ""; commentInput.style = "width:200px;margin-top:5px;display:block;";
        del.textContent = "🗑️"; del.style = "background:transparent;border:none;color:white;cursor:pointer;margin-left:5px;";
        del.onclick = () => { li.remove(); updateSeekbarMarkers(); updateScroll(); };

        timeRow.append(minus, plus, a, endBtn, del);
        li.append(timeRow, commentInput);
        li.style = "display:flex;flex-direction:column;gap:5px;padding:5px;background:rgba(255,255,255,0.05);border-radius:3px;";
        list.appendChild(li);
        updateScroll();
        updateSeekbarMarkers();
        return commentInput;
    }

    function updateScroll() {
        var tsCount = list.children.length;
        if (tsCount > 2) {
            list.style.maxHeight = "200px";
            list.style.overflowY = "auto";
        } else {
            list.style.maxHeight = "none";
            list.style.overflowY = "hidden";
        }
    }

    function updateSeekbarMarkers() {
        var video = document.querySelector("video");
        var progressBar = document.querySelector(".ytp-progress-bar");
        if (!video || !progressBar || !isFinite(video.duration)) return;

        var existingMarkers = document.querySelectorAll(".ytls-marker, .ytls-ts-bar");
        existingMarkers.forEach(marker => marker.remove());

        var timestamps = Array.from(list.children).map(li => {
            var startLink = li.querySelector('a[data-time]');
            var endLink = li.querySelector('.end-time');
            var comment = li.querySelector('input').value;
            var startTime = parseInt(startLink.dataset.time);
            var endTime = endLink ? parseInt(endLink.dataset.time) : null;
            return { start: startTime, end: endTime, comment: comment };
        });

        timestamps.forEach(ts => {
            if (ts.start) {
                var marker = document.createElement("div");
                marker.className = "ytls-marker";
                marker.style.position = "absolute";
                marker.style.height = "100%";
                marker.style.width = "2px";
                marker.style.backgroundColor = "#ff0000";
                marker.style.cursor = "pointer";
                marker.style.left = (ts.start / video.duration * 100) + "%";
                marker.dataset.time = ts.start;
                marker.addEventListener("click", () => video.currentTime = ts.start);
                progressBar.appendChild(marker);

                if (ts.end) {
                    var endMarker = document.createElement("div");
                    endMarker.className = "ytls-marker end";
                    endMarker.style.position = "absolute";
                    endMarker.style.height = "100%";
                    endMarker.style.width = "2px";
                    endMarker.style.backgroundColor = "#00ff00";
                    endMarker.style.cursor = "pointer";
                    endMarker.style.left = (ts.end / video.duration * 100) + "%";
                    endMarker.dataset.time = ts.end;
                    endMarker.addEventListener("click", () => video.currentTime = ts.end);
                    progressBar.appendChild(endMarker);

                    // Add TS bar between start and end
                    var tsBar = document.createElement("div");
                    tsBar.className = "ytls-ts-bar";
                    tsBar.style.position = "absolute";
                    tsBar.style.height = "100%";
                    tsBar.style.backgroundColor = "rgba(255, 255, 0, 0.3)"; // Yellow with transparency
                    tsBar.style.cursor = "pointer";
                    var startPos = ts.start / video.duration * 100;
                    var endPos = ts.end / video.duration * 100;
                    tsBar.style.left = startPos + "%";
                    tsBar.style.width = (endPos - startPos) + "%";
                    tsBar.title = ts.comment; // Tooltip with comment on hover
                    progressBar.appendChild(tsBar);
                }
            }
        });
    }

    function resetCopy() { isCopyList = true; copyBtn.textContent = "Copy List"; }

    function importTimestamps(text) {
        var lines = text.split("\n").map(line => line.trim()).filter(line => line);
        var i = 0;
        while (i < lines.length) {
            var copiedMatch = parseCopiedFormat(lines[i]);
            if (copiedMatch) {
                var start = copiedMatch.start, end = copiedMatch.end, comment = copiedMatch.comment;
                addTimestamp(start, comment);
                if (end) {
                    var li = list.lastChild;
                    var startLink = li.querySelector('a[data-time]');
                    var endLink = document.createElement("a");
                    formatTime(endLink, end);
                    endLink.className = 'end-time';
                    startLink.textContent += " - " + endLink.textContent;
                    var timeRow = li.querySelector('.time-row');
                    timeRow.insertBefore(endLink, timeRow.lastChild);
                }
                i++;
            } else {
                var timeMatch = lines[i].match(/(\d*:*\d+:\d+)(?:\s*[-–]\s*)(\d*:*\d+:\d+)/);
                if (timeMatch) {
                    var timeRange = parseTimeRange(lines[i]);
                    var start = timeRange.start, end = timeRange.end;
                    var comment = "";
                    i++;
                    while (i < lines.length && !lines[i].match(/(\d*:*\d+:\d+)(?:\s*[-–]\s*)(\d*:*\d+:\d+)/) && !parseCopiedFormat(lines[i])) {
                        comment += (comment ? " " : "") + lines[i];
                        i++;
                    }
                    addTimestamp(start, comment);
                    if (end) {
                        var li = list.lastChild;
                        var startLink = li.querySelector('a[data-time]');
                        var endLink = document.createElement("a");
                        formatTime(endLink, end);
                        endLink.className = 'end-time';
                        startLink.textContent += " - " + endLink.textContent;
                        var timeRow = li.querySelector('.time-row');
                        timeRow.insertBefore(endLink, timeRow.lastChild);
                    }
                } else {
                    i++;
                }
            }
        }
        updateScroll();
        updateSeekbarMarkers();
    }

    if (!document.querySelector("#ytls-pane")) {
        var pane = document.createElement("div"), header = document.createElement("div"), close = document.createElement("span"),
            list = document.createElement("ul"), textarea = document.createElement("textarea"), btns = document.createElement("div"),
            importBtn = document.createElement("button"), addBtn = document.createElement("button"), isCopyList = true,
            copyBtn = document.createElement("button"), clearBtn = document.createElement("button"), timeDisplay = document.createElement("span"),
            credit = document.createElement("span"), style = document.createElement("style"), minimizeBtn = document.createElement("button");

        pane.id = "ytls-pane";
        pane.classList.add("minimized");
        header.style = "display:flex;justify-content:space-between;align-items:center;padding-bottom:5px;padding-left:20px;";
        timeDisplay.id = "ytls-current-time"; timeDisplay.textContent = "CT: "; timeDisplay.style = "color:white;font-size:14px;";
        close.textContent = "×"; close.style = "cursor:pointer;font-size:18px;margin-left:5px;";
        credit.textContent = "Made By Vat5aL"; credit.style = "color:white;font-size:12px;margin-left:5px;";
        minimizeBtn.textContent = "▶️"; minimizeBtn.style = "background:transparent;border:none;color:white;cursor:pointer;font-size:16px;position:absolute;top:5px;left:5px;";
        minimizeBtn.id = "ytls-minimize";
        function updateTime() {
            var v = document.querySelector("video");
            if (v) {
                var t = Math.floor(v.currentTime), h = Math.floor(t / 3600), m = Math.floor(t / 60) % 60, s = t % 60;
                timeDisplay.textContent = `CT: ${h ? h + ":" + String(m).padStart(2, "0") : m}:${String(s).padStart(2, "0")}`;
            }
            requestAnimationFrame(updateTime);
        }
        updateTime();
        textarea.id = "ytls-box";
        btns.id = "ytls-buttons";
        importBtn.textContent = "Import List";
        addBtn.textContent = "Add TS";
        copyBtn.textContent = "Copy List";
        clearBtn.textContent = "Clear"; clearBtn.dataset.action = "clear"; clearBtn.style = "background:#555;color:white;font-size:12px;padding:5px 10px;border:none;border-radius:5px;cursor:pointer;";
        style.textContent = "#ytls-pane{background:rgba(0,0,0,0.8);text-align:right;position:fixed;bottom:0;right:0;padding:10px;border-radius:10px 0 0 0;opacity:0.9;z-index:5000;font-family:Arial,sans-serif;width:300px;}#ytls-pane.minimized{width:30px;height:30px;overflow:hidden;background:rgba(0,0,0,0.8);padding:0;}#ytls-pane.minimized #ytls-content{display:none;}#ytls-pane.minimized #ytls-minimize{display:block;}#ytls-pane:hover{opacity:1;}#ytls-pane ul{list-style:none;padding:0;margin:0;}#ytls-pane li{display:flex;flex-direction:column;gap:5px;margin:5px 0;background:rgba(255,255,255,0.05);padding:5px;border-radius:3px;}#ytls-pane .time-row{display:flex;gap:5px;align-items:center;}#ytls-pane .ytls-marker{position:absolute;height:100%;width:2px;background-color:#ff0000;cursor:pointer;}#ytls-pane .ytls-marker.end{background-color:#00ff00;}#ytls-pane .ytls-ts-bar{position:absolute;height:100%;background-color:rgba(255,255,0,0.3);cursor:pointer;}#ytls-pane span,#ytls-pane a,#ytls-pane input{background:none;color:white;font-family:inherit;font-size:14px;text-decoration:none;border:none;outline:none;}#ytls-box{font-family:monospace;width:100%;display:block;padding:5px;border:none;outline:none;resize:none;background:rgba(255,255,255,0.1);color:white;border-radius:5px;}#ytls-buttons{display:flex;gap:5px;justify-content:space-between;margin-top:10px;}#ytls-buttons button{background:rgba(255,255,255,0.1);color:white;font-size:12px;padding:5px 10px;border:none;border-radius:5px;cursor:pointer;}#ytls-buttons button:hover{background:rgba(255,255,255,0.2);}";

        close.onclick = () => { if (confirm("Close timestamp tool?")) pane.remove(); };
        minimizeBtn.onclick = () => pane.classList.toggle("minimized");
        list.onclick = handleClick;
        list.ontouchstart = handleClick;
        importBtn.onclick = () => {
            var text = textarea.value;
            importTimestamps(text);
        };
        addBtn.onclick = () => {
            var timeStampBuffer = 2;
            var input = addTimestamp(Math.max(0, Math.floor(document.querySelector("video").currentTime - timeStampBuffer)));
            input.focus();
        };
        copyBtn.onclick = () => {
            var url = location.href;
            var text = url + "\n";
            if (isCopyList) {
                isCopyList = false; copyBtn.textContent = "Copy Links";
                setTimeout(resetCopy, 500);
                for (var i = 0; i < list.children.length; i++) {
                    var start = list.children[i].querySelector('a[data-time]').textContent,
                        comment = list.children[i].querySelector('input').value,
                        end = list.children[i].querySelector('.end-time') ? list.children[i].querySelector('.end-time').textContent : "";
                    text += (i ? "\n" : "") + `${start}${end ? " - " + end : ""} "${comment}"`;
                }
            } else {
                resetCopy();
                for (var j = 0; j < list.children.length; j++) {
                    var commentText = list.children[j].querySelector('input').value,
                        tsUrl = list.children[j].querySelector('a[data-time]').href;
                    text += (j ? "\n" : "") + `${commentText} ${tsUrl}`;
                }
            }
            textarea.value = text; textarea.select(); document.execCommand("copy");
        };

        header.append(timeDisplay, credit, close);
        var content = document.createElement("div"); content.id = "ytls-content";
        content.append(header, list, textarea, btns);
        pane.append(minimizeBtn, content, style);
        btns.append(importBtn, addBtn, copyBtn, clearBtn);
        document.body.appendChild(pane);
        updateSeekbarMarkers();
    }
})();