Copy YouTube URL w/ Timestamp & Details

Adds two buttons to manage and copy current YouTube live or video URL with it's timestamp and some other details to clipboard then turn into a shortened url (youtu.be) and 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)

Stan na 20-10-2023. Zobacz najnowsza wersja.

// ==UserScript==
// @name         Copy YouTube URL w/ Timestamp & Details
// @namespace    azb-copyurl
// @version      0.2
// @description  Adds two buttons to manage and copy current YouTube live or video URL with it's timestamp and some other details to clipboard then turn into a shortened url (youtu.be) and 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/watch*
// @license MIT
// ==/UserScript==
(function() {
	'use strict';
	let initialDurationInSeconds = 0,
		selectedOptions = localStorage.getItem("selectedOptions") || "1,2,3,4,5,6",
		startTime = new Date();

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

	function parseTime(time) {
		return time.split(':')
			.reverse()
			.reduce((acc, val, idx) => acc + (parseInt(val) || 0) * Math.pow(60, idx), 0);
	}

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

	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();
		date.setUTCSeconds(date.getUTCSeconds() + 3600 - (liveStartTimeInSeconds % 86400));
		let formattedTime = "";
		if (selectedOptions.includes("4")) {
			formattedTime += `${String(date.getUTCHours()).padStart(2, '0')}:`;
		}
		if (selectedOptions.includes("5")) {
			formattedTime += `${String(date.getUTCMinutes()).padStart(2, '0')}:`;
		}
		if (selectedOptions.includes("6")) {
			formattedTime += `${String(date.getUTCSeconds()).padStart(2, '0')}`;
		}
		return {
			timestamp: liveStartTimeInSeconds + currentInSeconds,
			formattedDate: `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`,
			formattedTime
		};
	}

	function getChannelName() {
		let channelNameElem = document.querySelector('yt-formatted-string.ytd-channel-name a');
		return channelNameElem ? channelNameElem.textContent : "";
	}

	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.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() {
		const cursorTimeResult = getCurrentTimestampInSeconds();
		const urlTimeInSeconds = getURLTimestampInSeconds();
		const channelName = getChannelName();
		if (cursorTimeResult) {
			let details = "";
			if (selectedOptions.includes("1")) {
				details += ` - ${cursorTimeResult.formattedDate}`;
			}
			if (selectedOptions.includes("2")) {
				details += ` @ ~${cursorTimeResult.formattedTime} UTC`;
			}
			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!");
				});
		}
	}

	function toggleOptionsMenu() {
		let menu = document.getElementById("optionsMenu");
		if (menu) {
			menu.parentNode.removeChild(menu);
		} else {
			createOptionsMenu();
		}
	}

	function createOptionsMenu() {
		const optionsBtn = document.getElementById('optionsBtn');
		const rect = optionsBtn.getBoundingClientRect();
		const menu = document.createElement("div");
		menu.id = "optionsMenu";
		menu.style.position = "fixed";
		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"
		}];

		function handleTimeOptionChange(checked) {
			["4", "5", "6"].forEach(id => {
				const elem = document.getElementById("option-" + id)
					.parentNode;
				const checkbox = document.getElementById("option-" + id);
				if (checked) {
					elem.style.display = "";
					checkbox.checked = true;
					selectedOptions += id;
				} else {
					elem.style.display = "none";
					checkbox.checked = false;
					selectedOptions = selectedOptions.replace(id, "");
				}
			});
		}
		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 (opt.onChange) {
					opt.onChange(this.checked);
				}
				if (this.checked) {
					selectedOptions += opt.id;
				} else {
					selectedOptions = selectedOptions.replace(opt.id, "");
				}
				localStorage.setItem("selectedOptions", selectedOptions);
			});
			const label = document.createElement("label");
			label.htmlFor = "option-" + opt.id;
			label.innerHTML = `${opt.emoji} ${opt.label}`;
			optionElem.appendChild(checkbox);
			optionElem.appendChild(label);
			if (opt.requires && !selectedOptions.includes(opt.requires)) {
				optionElem.style.display = "none";
			}
			menu.appendChild(optionElem);
		});
		document.body.appendChild(menu);
	}

	function handleOptionsButton() {
		toggleOptionsMenu();
	}

	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 (!document.getElementById('copyTimestampBtn')) {
				const copyBtn = createStyledButton("Copy URL", handleCopyButton);
				copyBtn.id = "copyTimestampBtn";
				innerActions.insertBefore(copyBtn, innerActions.firstChild);
			}
		}
	}
	const observer = new MutationObserver(addButton);
	observer.observe(document.body, {
		childList: true,
		subtree: true
	});
	setInterval(updateLiveStartTime, 1000);
})();