YouTube Timestamp Tool by Vat5aL

Enhanced timestamp tool for YouTube videos

  1. // ==UserScript==
  2. // @name YouTube Timestamp Tool by Vat5aL
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  5. // @description Enhanced timestamp tool for YouTube videos
  6. // @author Vat5aL
  7. // @match https://www.youtube.com/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. function formatTime(e, t) {
  16. var n, a = Math.floor(t / 3600), o = Math.floor(t / 60) % 60, i = Math.floor(t) % 60;
  17. e.textContent = (a ? a + ":" + String(o).padStart(2, "0") : o) + ":" + String(i).padStart(2, "0");
  18. e.dataset.time = t;
  19. var vid = location.search.split(/.+v=|&/)[1] || location.href.split(/\/live\/|\/shorts\/|\?|&/)[1];
  20. e.href = "https://youtu.be/" + vid + "?t=" + t;
  21. }
  22.  
  23. function parseTimeRange(str) {
  24. var matches = str.match(/(\d*:*\d+):(\d+)(?:\s*[-–]\s*)(\d*:*\d+):(\d+)/);
  25. if (!matches) return null;
  26. var startParts = matches[1].split(':').map(Number),
  27. startSec = parseInt(matches[2]),
  28. endParts = matches[3].split(':').map(Number),
  29. endSec = parseInt(matches[4]);
  30. var startMin = startParts.length === 1 ? startParts[0] : startParts[0] * 60 + startParts[1],
  31. endMin = endParts.length === 1 ? endParts[0] : endParts[0] * 60 + endParts[1];
  32. return { start: startMin * 60 + startSec, end: endMin * 60 + endSec };
  33. }
  34.  
  35. function parseCopiedFormat(str) {
  36. var matches = str.match(/^(?:.*?\?v=[^&\s]+&.*?\s)?(\d*:*\d+:\d+)\s*-\s*(\d*:*\d+:\d+)\s*"(.+)"$/);
  37. if (!matches) return null;
  38. var startParts = matches[1].split(':').map(Number),
  39. endParts = matches[2].split(':').map(Number),
  40. comment = matches[3];
  41. var startMin = startParts.length === 2 ? startParts[0] : startParts[0] * 60 + startParts[1],
  42. startSec = startParts.length === 2 ? startParts[1] : startParts[2],
  43. endMin = endParts.length === 2 ? endParts[0] : endParts[0] * 60 + endParts[1],
  44. endSec = endParts.length === 2 ? endParts[1] : endParts[2];
  45. return { start: startMin * 60 + startSec, end: endMin * 60 + endSec, comment: comment };
  46. }
  47.  
  48. function handleClick(e) {
  49. if (e.target.dataset.time) {
  50. e.preventDefault();
  51. document.querySelector("video").currentTime = e.target.dataset.time;
  52. } else if (e.target.dataset.increment) {
  53. e.preventDefault();
  54. var t = e.target.parentElement.querySelector('a[data-time]');
  55. var currTime = parseInt(t.dataset.time);
  56. formatTime(t, Math.max(0, currTime + parseInt(e.target.dataset.increment)));
  57. } else if (e.target.dataset.action === "end") {
  58. e.preventDefault();
  59. var li = e.target.parentElement, startLink = li.querySelector('a[data-time]');
  60. if (!li.querySelector('.end-time')) {
  61. var endLink = document.createElement("a");
  62. formatTime(endLink, Math.floor(document.querySelector("video").currentTime));
  63. endLink.className = 'end-time';
  64. startLink.textContent += " - " + endLink.textContent;
  65. var timeRow = li.querySelector('.time-row');
  66. timeRow.insertBefore(endLink, e.target.nextSibling);
  67. e.target.remove();
  68. updateSeekbarMarkers();
  69. }
  70. } else if (e.target.dataset.action === "clear") {
  71. e.preventDefault();
  72. list.textContent = "";
  73. updateSeekbarMarkers();
  74. updateScroll();
  75. }
  76. }
  77.  
  78. function addTimestamp(e, t) {
  79. var li = document.createElement("li"), timeRow = document.createElement("div"), minus = document.createElement("span"),
  80. plus = document.createElement("span"), a = document.createElement("a"), endBtn = document.createElement("button"),
  81. commentInput = document.createElement("input"), del = document.createElement("button");
  82.  
  83. timeRow.className = "time-row";
  84. minus.textContent = "➖"; minus.dataset.increment = 1; minus.style.cursor = "pointer";
  85. plus.textContent = "➕"; plus.dataset.increment = -1; plus.style.cursor = "pointer";
  86. formatTime(a, e);
  87. endBtn.textContent = "End"; endBtn.dataset.action = "end"; endBtn.style = "background:#555;color:white;border:none;padding:2px 5px;border-radius:3px;cursor:pointer;";
  88. commentInput.value = t || ""; commentInput.style = "width:200px;margin-top:5px;display:block;";
  89. del.textContent = "🗑️"; del.style = "background:transparent;border:none;color:white;cursor:pointer;margin-left:5px;";
  90. del.onclick = () => { li.remove(); updateSeekbarMarkers(); updateScroll(); };
  91.  
  92. timeRow.append(minus, plus, a, endBtn, del);
  93. li.append(timeRow, commentInput);
  94. li.style = "display:flex;flex-direction:column;gap:5px;padding:5px;background:rgba(255,255,255,0.05);border-radius:3px;";
  95. list.appendChild(li);
  96. updateScroll();
  97. updateSeekbarMarkers();
  98. return commentInput;
  99. }
  100.  
  101. function updateScroll() {
  102. var tsCount = list.children.length;
  103. if (tsCount > 2) {
  104. list.style.maxHeight = "200px";
  105. list.style.overflowY = "auto";
  106. } else {
  107. list.style.maxHeight = "none";
  108. list.style.overflowY = "hidden";
  109. }
  110. }
  111.  
  112. function updateSeekbarMarkers() {
  113. var video = document.querySelector("video");
  114. var progressBar = document.querySelector(".ytp-progress-bar");
  115. if (!video || !progressBar || !isFinite(video.duration)) return;
  116.  
  117. var existingMarkers = document.querySelectorAll(".ytls-marker, .ytls-ts-bar");
  118. existingMarkers.forEach(marker => marker.remove());
  119.  
  120. var timestamps = Array.from(list.children).map(li => {
  121. var startLink = li.querySelector('a[data-time]');
  122. var endLink = li.querySelector('.end-time');
  123. var comment = li.querySelector('input').value;
  124. var startTime = parseInt(startLink.dataset.time);
  125. var endTime = endLink ? parseInt(endLink.dataset.time) : null;
  126. return { start: startTime, end: endTime, comment: comment };
  127. });
  128.  
  129. timestamps.forEach(ts => {
  130. if (ts.start) {
  131. var marker = document.createElement("div");
  132. marker.className = "ytls-marker";
  133. marker.style.position = "absolute";
  134. marker.style.height = "100%";
  135. marker.style.width = "2px";
  136. marker.style.backgroundColor = "#ff0000";
  137. marker.style.cursor = "pointer";
  138. marker.style.left = (ts.start / video.duration * 100) + "%";
  139. marker.dataset.time = ts.start;
  140. marker.addEventListener("click", () => video.currentTime = ts.start);
  141. progressBar.appendChild(marker);
  142.  
  143. if (ts.end) {
  144. var endMarker = document.createElement("div");
  145. endMarker.className = "ytls-marker end";
  146. endMarker.style.position = "absolute";
  147. endMarker.style.height = "100%";
  148. endMarker.style.width = "2px";
  149. endMarker.style.backgroundColor = "#00ff00";
  150. endMarker.style.cursor = "pointer";
  151. endMarker.style.left = (ts.end / video.duration * 100) + "%";
  152. endMarker.dataset.time = ts.end;
  153. endMarker.addEventListener("click", () => video.currentTime = ts.end);
  154. progressBar.appendChild(endMarker);
  155.  
  156. // Add TS bar between start and end
  157. var tsBar = document.createElement("div");
  158. tsBar.className = "ytls-ts-bar";
  159. tsBar.style.position = "absolute";
  160. tsBar.style.height = "100%";
  161. tsBar.style.backgroundColor = "rgba(255, 255, 0, 0.3)"; // Yellow with transparency
  162. tsBar.style.cursor = "pointer";
  163. var startPos = ts.start / video.duration * 100;
  164. var endPos = ts.end / video.duration * 100;
  165. tsBar.style.left = startPos + "%";
  166. tsBar.style.width = (endPos - startPos) + "%";
  167. tsBar.title = ts.comment; // Tooltip with comment on hover
  168. progressBar.appendChild(tsBar);
  169. }
  170. }
  171. });
  172. }
  173.  
  174. function resetCopy() { isCopyList = true; copyBtn.textContent = "Copy List"; }
  175.  
  176. function importTimestamps(text) {
  177. var lines = text.split("\n").map(line => line.trim()).filter(line => line);
  178. var i = 0;
  179. while (i < lines.length) {
  180. var copiedMatch = parseCopiedFormat(lines[i]);
  181. if (copiedMatch) {
  182. var start = copiedMatch.start, end = copiedMatch.end, comment = copiedMatch.comment;
  183. addTimestamp(start, comment);
  184. if (end) {
  185. var li = list.lastChild;
  186. var startLink = li.querySelector('a[data-time]');
  187. var endLink = document.createElement("a");
  188. formatTime(endLink, end);
  189. endLink.className = 'end-time';
  190. startLink.textContent += " - " + endLink.textContent;
  191. var timeRow = li.querySelector('.time-row');
  192. timeRow.insertBefore(endLink, timeRow.lastChild);
  193. }
  194. i++;
  195. } else {
  196. var timeMatch = lines[i].match(/(\d*:*\d+:\d+)(?:\s*[-–]\s*)(\d*:*\d+:\d+)/);
  197. if (timeMatch) {
  198. var timeRange = parseTimeRange(lines[i]);
  199. var start = timeRange.start, end = timeRange.end;
  200. var comment = "";
  201. i++;
  202. while (i < lines.length && !lines[i].match(/(\d*:*\d+:\d+)(?:\s*[-–]\s*)(\d*:*\d+:\d+)/) && !parseCopiedFormat(lines[i])) {
  203. comment += (comment ? " " : "") + lines[i];
  204. i++;
  205. }
  206. addTimestamp(start, comment);
  207. if (end) {
  208. var li = list.lastChild;
  209. var startLink = li.querySelector('a[data-time]');
  210. var endLink = document.createElement("a");
  211. formatTime(endLink, end);
  212. endLink.className = 'end-time';
  213. startLink.textContent += " - " + endLink.textContent;
  214. var timeRow = li.querySelector('.time-row');
  215. timeRow.insertBefore(endLink, timeRow.lastChild);
  216. }
  217. } else {
  218. i++;
  219. }
  220. }
  221. }
  222. updateScroll();
  223. updateSeekbarMarkers();
  224. }
  225.  
  226. if (!document.querySelector("#ytls-pane")) {
  227. var pane = document.createElement("div"), header = document.createElement("div"), close = document.createElement("span"),
  228. list = document.createElement("ul"), textarea = document.createElement("textarea"), btns = document.createElement("div"),
  229. importBtn = document.createElement("button"), addBtn = document.createElement("button"), isCopyList = true,
  230. copyBtn = document.createElement("button"), clearBtn = document.createElement("button"), timeDisplay = document.createElement("span"),
  231. credit = document.createElement("span"), style = document.createElement("style"), minimizeBtn = document.createElement("button");
  232.  
  233. pane.id = "ytls-pane";
  234. pane.classList.add("minimized");
  235. header.style = "display:flex;justify-content:space-between;align-items:center;padding-bottom:5px;padding-left:20px;";
  236. timeDisplay.id = "ytls-current-time"; timeDisplay.textContent = "CT: "; timeDisplay.style = "color:white;font-size:14px;";
  237. close.textContent = "×"; close.style = "cursor:pointer;font-size:18px;margin-left:5px;";
  238. credit.textContent = "Made By Vat5aL"; credit.style = "color:white;font-size:12px;margin-left:5px;";
  239. minimizeBtn.textContent = "▶️"; minimizeBtn.style = "background:transparent;border:none;color:white;cursor:pointer;font-size:16px;position:absolute;top:5px;left:5px;";
  240. minimizeBtn.id = "ytls-minimize";
  241. function updateTime() {
  242. var v = document.querySelector("video");
  243. if (v) {
  244. var t = Math.floor(v.currentTime), h = Math.floor(t / 3600), m = Math.floor(t / 60) % 60, s = t % 60;
  245. timeDisplay.textContent = `CT: ${h ? h + ":" + String(m).padStart(2, "0") : m}:${String(s).padStart(2, "0")}`;
  246. }
  247. requestAnimationFrame(updateTime);
  248. }
  249. updateTime();
  250. textarea.id = "ytls-box";
  251. btns.id = "ytls-buttons";
  252. importBtn.textContent = "Import List";
  253. addBtn.textContent = "Add TS";
  254. copyBtn.textContent = "Copy List";
  255. 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;";
  256. 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);}";
  257.  
  258. close.onclick = () => { if (confirm("Close timestamp tool?")) pane.remove(); };
  259. minimizeBtn.onclick = () => pane.classList.toggle("minimized");
  260. list.onclick = handleClick;
  261. list.ontouchstart = handleClick;
  262. importBtn.onclick = () => {
  263. var text = textarea.value;
  264. importTimestamps(text);
  265. };
  266. addBtn.onclick = () => {
  267. var timeStampBuffer = 2;
  268. var input = addTimestamp(Math.max(0, Math.floor(document.querySelector("video").currentTime - timeStampBuffer)));
  269. input.focus();
  270. };
  271. copyBtn.onclick = () => {
  272. var url = location.href;
  273. var text = url + "\n";
  274. if (isCopyList) {
  275. isCopyList = false; copyBtn.textContent = "Copy Links";
  276. setTimeout(resetCopy, 500);
  277. for (var i = 0; i < list.children.length; i++) {
  278. var start = list.children[i].querySelector('a[data-time]').textContent,
  279. comment = list.children[i].querySelector('input').value,
  280. end = list.children[i].querySelector('.end-time') ? list.children[i].querySelector('.end-time').textContent : "";
  281. text += (i ? "\n" : "") + `${start}${end ? " - " + end : ""} "${comment}"`;
  282. }
  283. } else {
  284. resetCopy();
  285. for (var j = 0; j < list.children.length; j++) {
  286. var commentText = list.children[j].querySelector('input').value,
  287. tsUrl = list.children[j].querySelector('a[data-time]').href;
  288. text += (j ? "\n" : "") + `${commentText} ${tsUrl}`;
  289. }
  290. }
  291. textarea.value = text; textarea.select(); document.execCommand("copy");
  292. };
  293.  
  294. header.append(timeDisplay, credit, close);
  295. var content = document.createElement("div"); content.id = "ytls-content";
  296. content.append(header, list, textarea, btns);
  297. pane.append(minimizeBtn, content, style);
  298. btns.append(importBtn, addBtn, copyBtn, clearBtn);
  299. document.body.appendChild(pane);
  300. updateSeekbarMarkers();
  301. }
  302. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址