Copy YouTube URL w/ Timestamp & Details

Adds two buttons to manage and copy current YouTube live or video URL with its timestamp and some other details to clipboard then turn into a shortened URL (youtu.be) style will auto switch color depending on your theme color, has a ~15 to 30 seconds delay for the time capture. (the Date and Time capture only works if this is an ongoing stream)

Per 29-10-2023. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Copy YouTube URL w/ Timestamp & Details
// @namespace    azb-copyurl
// @version      0.3.7
// @description  Adds two buttons to manage and copy current YouTube live or video URL with its timestamp and some other details to clipboard then turn into a shortened URL (youtu.be) style will auto switch color depending on your theme color, has a ~15 to 30 seconds delay for the time capture. (the Date and Time capture only works if this is an ongoing stream)
// @author       Azb
// @match        *://www.youtube.com/*
// @license MIT
// ==/UserScript==
(function () {
    'use strict';
    let initialDurationInSeconds = 0,
        selectedOptions = (localStorage.getItem("selectedOptions") || "1,2,3,4,5,6").split(","),
        startTime = new Date(),
        selectedTimezone = localStorage.getItem("selectedTimezone") || "UTC+0";
    let previousTitle = document.title;
    let maxChecks = 20;
    let titleChanged = false;
    let realReferenceTime = null;
    let videoReferenceTime = null;

    function getChannelName() {
        let channelNameElem = document.querySelector("#upload-info a.yt-simple-endpoint.style-scope.yt-formatted-string");
        return channelNameElem ? channelNameElem.textContent.trim() : "";
    }

    function detectVideoChange() {
        if (document.title !== previousTitle) {
            realReferenceTime = null;
            videoReferenceTime = null;
            initialDurationInSeconds = 0;
            startTime = new Date();
            previousTitle = document.title;
        }
    }

    function isStreamReady() {
        const videoElem = document.querySelector('video');
        return videoElem && videoElem.readyState > 3;
    }

    function isDarkMode() {
        return !['#fff', '#ffffff'].includes(getComputedStyle(document.documentElement).getPropertyValue('--yt-spec-general-background-a').trim());
    }

    function parseTime(time) {
        const timeParts = time.split(':').reverse();
        let seconds = 0;
        if (timeParts.length === 2) {
            seconds = (parseInt(timeParts[0]) || 0) + (parseInt(timeParts[1] || 0) * 60);
        } else if (timeParts.length === 3) {
            seconds = (parseInt(timeParts[0]) || 0) + (parseInt(timeParts[1] || 0) * 60) + (parseInt(timeParts[2] || 0) * 3600);
        } else if (timeParts.length === 4) {
            seconds = (parseInt(timeParts[0]) || 0) + (parseInt(timeParts[1] || 0) * 60) + (parseInt(timeParts[2] || 0) * 3600) + (parseInt(timeParts[3] || 0) * 86400);
        }
        return seconds;
    }

    function updateLiveStartTime() {
        const duration = document.querySelector('.ytp-time-duration');
        if (duration)
            initialDurationInSeconds = parseTime(duration.textContent);
    }

    function updateReferences() {
        const currentTimeElem = document.querySelector('.ytp-time-current');
        const timeParts = currentTimeElem.textContent.split(':').reverse();
        const videoCurrentTime = (parseInt(timeParts[0]) || 0) + (parseInt(timeParts[1] || 0) * 60) + (parseInt(timeParts[2] || 0) * 3600) + (parseInt(timeParts[3] || 0) * 86400);
        realReferenceTime = Date.now();
        videoReferenceTime = videoCurrentTime;
    }

    function convertToTimezone(date) {
        const timezoneOffset = parseInt(selectedTimezone.replace("UTC", "")) || 0;
        const year = date.getUTCFullYear();
        const month = date.getUTCMonth();
        const day = date.getUTCDate();
        const hours = date.getUTCHours();
        const minutes = date.getUTCMinutes();
        const seconds = date.getUTCSeconds();
        const adjustedDate = new Date(Date.UTC(year, month, day, hours + timezoneOffset, minutes, seconds));
        return adjustedDate;
    }

    function handleMinutesCheckboxChange() {
        if (!minutesCheckbox.checked) {
            secondsCheckbox.checked = false;
            secondsCheckbox.disabled = true;
            selectedOptions = selectedOptions.filter(option => option !== "6");
        } else {
            secondsCheckbox.disabled = false;
        }
        saveCheckboxState();
    }

    function handleHoursCheckboxChange() {
        if (!hoursCheckbox.checked) {
            minutesCheckbox.checked = false;
            minutesCheckbox.disabled = true;
            secondsCheckbox.checked = false;
            secondsCheckbox.disabled = true;
            selectedOptions = selectedOptions.filter(option => option !== "5" && option !== "6");
        } else {
            minutesCheckbox.disabled = false;
        }
        saveCheckboxState();
    }

    function getSimpleTimeInSeconds() {
        const currentTimeElem = document.querySelector('.ytp-time-current');
        if (!currentTimeElem)
            return null;

        return parseTime(currentTimeElem.textContent);
    }

    function getURLTimestampInSeconds() {
        const videoElem = document.querySelector('video');
        if (!videoElem)
            return null;

        const currentTimeElem = document.querySelector('.ytp-time-current');
        if (!currentTimeElem)
            return null;

        const timeParts = currentTimeElem.textContent.split(':').reverse();
        const videoCurrentTime = (parseInt(timeParts[0]) || 0) + (parseInt(timeParts[1] || 0) * 60) + (parseInt(timeParts[2] || 0) * 3600) + (parseInt(timeParts[3] || 0) * 86400);
        if (!realReferenceTime) {
            realReferenceTime = Date.now();
            videoReferenceTime = videoCurrentTime;
        }
        const realElapsedTime = (Date.now() - realReferenceTime) / 1000;
        return videoReferenceTime + realElapsedTime;
    }

    function getCurrentTimestampInSeconds() {
        const currentTimeElem = document.querySelector('.ytp-time-current');
        if (!currentTimeElem)
            return null;

        const currentInSeconds = parseTime(currentTimeElem.textContent);
        const liveStartTimeInSeconds = (initialDurationInSeconds + (new Date() - startTime) / 1000) - currentInSeconds;
        const date = new Date();
        const daysPassed = Math.floor(liveStartTimeInSeconds / 86400);
        date.setUTCDate(date.getUTCDate() + daysPassed);
        date.setUTCSeconds(date.getUTCSeconds() + 3600 - (liveStartTimeInSeconds % 86400));
        const adjustedDate = convertToTimezone(date);
        let formattedTime = "";
        if (selectedOptions.includes("4")) {
            formattedTime += `${
                String(adjustedDate.getUTCHours()).padStart(2, '0')
            }`;
        }
        if (selectedOptions.includes("5")) {
            if (formattedTime) {
                formattedTime += `:${
                    String(adjustedDate.getUTCMinutes()).padStart(2, '0')
                }`;
            } else {
                formattedTime += `${
                    String(adjustedDate.getUTCMinutes()).padStart(2, '0')
                }`;
            }
        }
        if (selectedOptions.includes("6")) {
            if (formattedTime) {
                formattedTime += `:${
                    String(adjustedDate.getUTCSeconds()).padStart(2, '0')
                }`;
            } else {
                formattedTime += `00:${
                    String(adjustedDate.getUTCSeconds()).padStart(2, '0')
                }`;
            }
        }
        if (formattedTime && !formattedTime.includes(":")) {
            formattedTime += "h";
        }
        return {
            timestamp: liveStartTimeInSeconds + currentInSeconds,
            formattedDate: `${
                adjustedDate.getUTCFullYear()
            }-${
                String(adjustedDate.getUTCMonth() + 1).padStart(2, '0')
            }-${
                String(adjustedDate.getUTCDate()).padStart(2, '0')
            }`,
            formattedTime: `${formattedTime} ${selectedTimezone}`
        };
    }

    function showCopyAlert(btn, message) {
        const alertDiv = document.createElement("div");
        alertDiv.innerHTML = message;
        alertDiv.style.position = 'fixed';
        alertDiv.style.background = isDarkMode() ? '#212121' : '#f8f8f8';
        alertDiv.style.color = isDarkMode() ? '#fff' : '#000';
        alertDiv.style.padding = '6px 10px';
        alertDiv.style.borderRadius = '5px';
        alertDiv.style.top = (btn.getBoundingClientRect().top - 40) + 'px';
        alertDiv.style.left = (btn.getBoundingClientRect().left + btn.offsetWidth / 2) + 'px';
        alertDiv.style.transform = 'translateX(-50%)';
        alertDiv.style.fontSize = '0.9rem';
        alertDiv.style.fontFamily = 'Roboto, sans-serif';
        alertDiv.style.zIndex = '1000';
        alertDiv.style.transition = 'opacity 0.3s';
        document.body.appendChild(alertDiv);
        setTimeout(() => {
            alertDiv.style.opacity = '0';
            setTimeout(() => {
                document.body.removeChild(alertDiv);
            }, 300);
        }, 2000);
    }

    function createStyledButton(text, handler) {
        const btn = document.createElement("a");
        btn.className = "yt-simple-endpoint style-scope ytd-toggle-button-renderer";
        btn.style.position = 'relative';
        btn.style.display = 'flex';
        btn.style.alignItems = 'center';
        btn.style.padding = "0 16px";
        btn.style.height = "36px";
        btn.style.borderRadius = "18px";
        btn.style.fontSize = "14px";
        btn.style.lineHeight = "2rem";
        btn.style.fontWeight = "500";
        btn.style.marginRight = "8px";
        btn.style.whiteSpace = "nowrap";
        btn.style.transition = "background 0.2s";
        btn.style.fontFamily = "Roboto, sans-serif";
        btn.style.flex = 'none';
        if (isDarkMode()) {
            btn.style.background = "#212121";
            btn.style.color = "#fff";
            btn.addEventListener("mouseenter", function () {
                btn.style.background = "#3e3e3e";
            });
            btn.addEventListener("mouseleave", function () {
                btn.style.background = "#212121";
            });
        } else {
            btn.style.background = "#f8f8f8";
            btn.style.color = "#000";
            btn.addEventListener("mouseenter", function () {
                btn.style.background = "#e0e0e0";
            });
            btn.addEventListener("mouseleave", function () {
                btn.style.background = "#f8f8f8";
            });
        }
        btn.appendChild(document.createTextNode(text));
        btn.addEventListener('click', handler);
        return btn;
    }

    function handleCopyButton() {
        let cursorTimeResult = getCurrentTimestampInSeconds();
        let urlTimeInSeconds = getSimpleTimeInSeconds();
        let channelName = getChannelName();
        if (cursorTimeResult) {
            let details = "";
            if (selectedOptions.includes("1")) {
                details += ` | ${cursorTimeResult.formattedDate}`;
            }
            if ((selectedOptions.includes("4") || selectedOptions.includes("5") || selectedOptions.includes("6")) && selectedOptions.includes("2")) {
                let timeDetail = cursorTimeResult.formattedTime.split(" ")[0];
                if (!selectedOptions.includes("6") && timeDetail.endsWith(":00")) {
                    timeDetail = timeDetail.substring(0, timeDetail.length - 3);
                }
                details += ` | ~${timeDetail} ${selectedTimezone}`;
            }
            if (selectedOptions.includes("3")) {
                details += ` | ${channelName}`;
            }
            const videoID = new URL(window.location.href).searchParams.get("v");
            const fullURL = `https://youtu.be/${videoID}?t=${Math.round(urlTimeInSeconds)}s${details}`;
            navigator.clipboard.writeText(fullURL).then(() => {
                showCopyAlert(document.getElementById('copyTimestampBtn'), "URL Copied!");
            });
        }
        titleChanged = false;
    }


    function toggleOptionsMenu() {
        let menu = document.getElementById("optionsMenu");
        if (menu) {
            menu.parentNode.removeChild(menu);
        } else {
            createOptionsMenu();
        }
    }
    document.body.addEventListener('click', function (event) {
        const menu = document.getElementById('optionsMenu');
        const optionsBtn = document.getElementById('optionsBtn');
        if (menu && event.target !== menu && event.target !== optionsBtn && !menu.contains(event.target)) {
            menu.remove();
        }
    }, true);

    function handleTimeOptionChange(checked) {
        if (checked) {
            if (localStorage.getItem('hoursCheckbox') === 'true') {
                hoursCheckbox.checked = true;
            }
            if (localStorage.getItem('minutesCheckbox') === 'true') {
                minutesCheckbox.checked = true;
            }
            if (localStorage.getItem('secondsCheckbox') === 'true') {
                secondsCheckbox.checked = true;
            }
        } else {
            hoursCheckbox.checked = false;
            minutesCheckbox.checked = false;
            secondsCheckbox.checked = false;
        }
        updateCheckboxStates();
        if (hoursCheckbox.parentNode && minutesCheckbox.parentNode && secondsCheckbox.parentNode) {
            const displayStyle = checked ? "" : "none";
            hoursCheckbox.parentNode.style.display = displayStyle;
            minutesCheckbox.parentNode.style.display = displayStyle;
            secondsCheckbox.parentNode.style.display = displayStyle;
            timezoneDiv.style.display = checked ? "" : "none";
        }
    }

    function updateSecondsCheckboxState() {
        if (!minutesCheckbox || !secondsCheckbox)
            return;

        const minutesChecked = minutesCheckbox.checked;
        if (!minutesChecked) {
            secondsCheckbox.checked = false;
            secondsCheckbox.disabled = true;
        } else {
            secondsCheckbox.disabled = false;
        }
    }

    let timezoneDiv;
    let hoursCheckbox;
    let minutesCheckbox;
    let secondsCheckbox;

    function updateCheckboxStates() {
        if (!hoursCheckbox || !minutesCheckbox || !secondsCheckbox)
            return;

        minutesCheckbox.disabled = false;
        secondsCheckbox.disabled = false;
        if (!hoursCheckbox.checked) {
            minutesCheckbox.checked = false;
            minutesCheckbox.disabled = true;
            secondsCheckbox.checked = false;
            secondsCheckbox.disabled = true;
        } else {
            if (!minutesCheckbox.checked) {
                secondsCheckbox.checked = false;
                secondsCheckbox.disabled = true;
            }
        }
    }

    function saveCheckboxState() {
        localStorage.setItem('hoursCheckbox', hoursCheckbox.checked);
        localStorage.setItem('minutesCheckbox', minutesCheckbox.checked);
        localStorage.setItem('secondsCheckbox', secondsCheckbox.checked);
    }

    function restoreCheckboxState() {
        if (localStorage.getItem('hoursCheckbox') !== null) {
            hoursCheckbox.checked = (localStorage.getItem('hoursCheckbox') === 'true');
        }
        if (localStorage.getItem('minutesCheckbox') !== null) {
            minutesCheckbox.checked = (localStorage.getItem('minutesCheckbox') === 'true');
        }
        if (localStorage.getItem('secondsCheckbox') !== null) {
            secondsCheckbox.checked = (localStorage.getItem('secondsCheckbox') === 'true');
        }
        handleHoursCheckboxChange
        handleMinutesCheckboxChange();
    }

    function bindCheckboxEvents() {
        hoursCheckbox = document.getElementById("option-4");
        minutesCheckbox = document.getElementById("option-5");
        secondsCheckbox = document.getElementById("option-6");
        if (hoursCheckbox) {
            hoursCheckbox.addEventListener("change", function () {
                saveCheckboxState();
                handleHoursCheckboxChange();
                updateCheckboxStates();
            });
        }
        if (minutesCheckbox) {
            minutesCheckbox.addEventListener("change", function () {
                saveCheckboxState();
                handleMinutesCheckboxChange();
                updateCheckboxStates();
            });
        }
        if (secondsCheckbox) {
            secondsCheckbox.addEventListener("change", function () {
                saveCheckboxState();
                updateCheckboxStates();
            });
        }
        restoreCheckboxState();
        updateCheckboxStates();
    }

    function refreshMenuState() {
        [
            "1",
            "2",
            "3",
            "4",
            "5",
            "6"
        ].forEach(id => {
            const elem = document.getElementById("option-" + id);
            elem.checked = selectedOptions.includes(id);
            if (selectedOptions.includes("2")) {
                timezoneDiv.style.display = "";
                if (["4", "5", "6"].includes(id)) {
                    elem.parentNode.style.display = "";
                }
            } else {
                timezoneDiv.style.display = "none";
                if (["4", "5", "6"].includes(id)) {
                    elem.parentNode.style.display = "none";
                }
            }
        });
        bindCheckboxEvents();
        updateCheckboxStates();
        updateSecondsCheckboxState();
    }

    function createOptionsMenu() {
        const optionsBtn = document.getElementById('optionsBtn');
        const rect = optionsBtn.getBoundingClientRect();
        const menu = document.createElement("div");
        menu.id = "optionsMenu";
        menu.style.position = "absolute";
        menu.style.top = rect.bottom + window.scrollY + "px";
        menu.style.left = rect.left + "px";
        menu.style.color = isDarkMode() ? '#fff' : '#000';
        menu.style.background = isDarkMode() ? "#212121" : "#f8f8f8";
        menu.style.border = "1px solid #ccc";
        menu.style.borderRadius = "5px";
        menu.style.zIndex = "1000";
        menu.style.padding = "5px";
        menu.style.marginTop = "10px";
        menu.style.fontFamily = "Roboto";
        menu.style.fontSize = "14px";
        menu.style.fontWeight = "500";

        const optionsData = [{
                id: "1",
                emoji: "📅",
                label: "Date"
            },
            {
                id: "2",
                emoji: "🕙",
                label: "Time",
                onChange: handleTimeOptionChange
            },
            {
                id: "3",
                emoji: "👤",
                label: "Channel"
            },
            {
                id: "4",
                emoji: "⏳",
                label: "Hours",
                requires: "2"
            }, {
                id: "5",
                emoji: "⌛",
                label: "Minutes",
                requires: "2"
            }, {
                id: "6",
                emoji: "⏲️",
                label: "Seconds",
                requires: "2"
            }
        ];
        optionsData.forEach(opt => {
            const optionElem = document.createElement("div");
            const checkbox = document.createElement("input");
            checkbox.type = "checkbox";
            checkbox.id = "option-" + opt.id;
            checkbox.checked = selectedOptions.includes(opt.id);
            checkbox.addEventListener("change", function () {
                if (this.checked) {
                    selectedOptions.push(opt.id);
                } else {
                    selectedOptions = selectedOptions.filter(option => option !== opt.id);
                }
                localStorage.setItem("selectedOptions", selectedOptions.join(","));
                if (opt.onChange) {
                    opt.onChange(this.checked);
                }
            });
            const label = document.createElement("label");
            label.htmlFor = "option-" + opt.id;
            label.innerHTML = `${
                opt.emoji
            } ${
                opt.label
            }`;
            optionElem.appendChild(checkbox);
            optionElem.appendChild(label);
            menu.appendChild(optionElem);
        });
        timezoneDiv = document.createElement("div");
        timezoneDiv.innerHTML = "🌍 Timezone: ";
        const timezoneSelect = document.createElement("select");
        if (isDarkMode()) {
            timezoneSelect.style.background = "#212121";
            timezoneSelect.style.color = "#fff";
            timezoneSelect.style.border = "1px solid #fff";
        } else {
            timezoneSelect.style.background = "#f8f8f8";
            timezoneSelect.style.color = "#000";
            timezoneSelect.style.border = "1px solid #000";
        }
        const timezones = [
            "UTC-11",
            "UTC-10",
            "UTC-9",
            "UTC-8",
            "UTC-7",
            "UTC-6",
            "UTC-5",
            "UTC-4",
            "UTC-3",
            "UTC-2",
            "UTC-1",
            "UTC+0",
            "UTC+1",
            "UTC+2",
            "UTC+3",
            "UTC+4",
            "UTC+5",
            "UTC+6",
            "UTC+7",
            "UTC+8",
            "UTC+9",
            "UTC+10",
            "UTC+11",
            "UTC+12",
            "UTC+13",
            "UTC+14"
        ];
        timezones.forEach(zone => {
            const option = document.createElement("option");
            option.value = zone;
            option.text = zone;
            if (zone === selectedTimezone) {
                option.selected = true;
            }
            timezoneSelect.appendChild(option);
        });
        timezoneSelect.onchange = function () {
            selectedTimezone = this.value;
            localStorage.setItem("selectedTimezone", selectedTimezone);
        };
        timezoneDiv.appendChild(timezoneSelect);
        menu.appendChild(timezoneDiv);
        document.body.appendChild(menu);
        bindCheckboxEvents();
        updateSecondsCheckboxState();
        refreshMenuState();
    }

    function handleOptionsButton() {
        toggleOptionsMenu();
        event.stopPropagation();
    }

    function addButton() {
        const actionsBar = document.querySelector('#actions');
        const innerActions = document.querySelector('ytd-watch-metadata[flex-menu-enabled] #actions.ytd-watch-metadata ytd-menu-renderer.ytd-watch-metadata');

        if (actionsBar && innerActions) {
            if (!document.getElementById('optionsBtn')) {
                const optionsBtn = createStyledButton("Settings", handleOptionsButton);
                optionsBtn.id = "optionsBtn";
                innerActions.insertBefore(optionsBtn, innerActions.firstChild);
            }
            if (isStreamReady() && !document.getElementById('copyTimestampBtn')) {
                const copyBtn = createStyledButton("Copy URL", handleCopyButton);
                copyBtn.id = "copyTimestampBtn";
                innerActions.insertBefore(copyBtn, innerActions.firstChild);
            }
            const videoElem = document.querySelector('video');
            videoElem.addEventListener('play', updateReferences);
            videoElem.addEventListener('pause', updateReferences);
            videoElem.addEventListener('seeked', updateReferences);
        }
    }
    const videoObserver = new MutationObserver(function () {
        if (isStreamReady() && !document.getElementById('copyTimestampBtn')) {
            addButton();
        }
    });

    const videoElem = document.querySelector('video');
    if (videoElem) {
        videoObserver.observe(videoElem, {
            attributes: true,
            attributeFilter: ['readyState']
        });
    }
    new MutationObserver(detectVideoChange).observe(document.querySelector('title'), {
        childList: true
    });
    const observer = new MutationObserver(addButton);
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
    setInterval(updateLiveStartTime, 1000);
})();