您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Save timestamps on YouTube videos with a bottom toolbar, persist across videos, and export CSV
// ==UserScript== // @name YouTube Timestamp Saver // @namespace http://tampermonkey.net/ // @version 1.2 // @description Save timestamps on YouTube videos with a bottom toolbar, persist across videos, and export CSV // @author You // @match https://www.youtube.com/watch* // @grant none // @license MIT // ==/UserScript== (function () { let startTime = ''; let endTime = ''; let queryTime = ''; let targetStartTime = ''; let targetEndTime = ''; let targetStart2 = ''; let targetEnd2 = ''; let targetStart3 = ''; let targetEnd3 = ''; let label = 'label'; let videoURL = location.href.split('&')[0]; let savedRows = JSON.parse(localStorage.getItem('yt_savedRows') || '[]'); const saveToLocal = () => { localStorage.setItem('yt_savedRows', JSON.stringify(savedRows)); }; const formatTime = (seconds) => { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); const ms = Math.round((seconds % 1) * 1000); // rounding ensures .809 not .808999999 const msStr = ms.toString().padStart(3, '0'); const timeStr = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}.${msStr}`; return h > 0 ? `${h}:${timeStr}` : timeStr; }; const getPlayerTime = () => { const video = document.querySelector('video'); return video ? formatTime(video.currentTime) : ''; }; const formatTimeString = (timeStr) => { if (!timeStr) return ''; const parts = timeStr.split(/[:.]/).map(Number); let seconds = 0; if (parts.length === 3) { // MM:SS.mmm seconds = parts[0] * 60 + parts[1] + parts[2] / 1000; } else if (parts.length === 4) { // H:MM:SS.mmm seconds = parts[0] * 3600 + parts[1] * 60 + parts[2] + parts[3] / 1000; } return formatTime(seconds); }; const saveRow = () => { const row = [ videoURL, label, formatTimeString(startTime), formatTimeString(endTime), formatTimeString(queryTime), formatTimeString(targetStartTime), formatTimeString(targetEndTime), formatTimeString(targetStart2), formatTimeString(targetEnd2), formatTimeString(targetStart3), formatTimeString(targetEnd3) ]; savedRows.push(row); saveToLocal(); updatePreview(); }; const downloadCSV = () => { const header = [ 'videoURL', 'label', 'startTime', 'endTime', 'queryTime', 'targetStartTime', 'targetEndTime', 'targetStart2', 'targetEnd2', 'targetStart3', 'targetEnd3' ]; const allRows = [header, ...savedRows]; // Wrap each cell in double quotes to ensure they are saved as text const csvContent = allRows .map(row => row.map(cell => `${cell}`).join(',')) .join('\n'); const blob = new Blob([csvContent], { type: 'text/csv' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'timestamps.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); savedRows = []; localStorage.removeItem('yt_savedRows'); updatePreview(); }; const updatePreview = () => { previewArea.textContent = savedRows.map(r => r.join('\t')).join('\n'); }; const setAndShow = (fieldName) => { const time = getPlayerTime(); if (fieldName === 'start') startTime = time; if (fieldName === 'end') endTime = time; if (fieldName === 'query') queryTime = time; if (fieldName === 'targetStart') targetStartTime = time; if (fieldName === 'targetEnd') targetEndTime = time; if (fieldName === 'targetStart2') targetStart2 = time; if (fieldName === 'targetEnd2') targetEnd2 = time; if (fieldName === 'targetStart3') targetStart3 = time; if (fieldName === 'targetEnd3') targetEnd3 = time; labelInput.value = label; updateInputs(); hidePanel(); }; const updateInputs = () => { startInput.value = startTime; endInput.value = endTime; queryInput.value = queryTime; targetStartInput.value = targetStartTime; targetEndInput.value = targetEndTime; targetStart2Input.value = targetStart2; targetEnd2Input.value = targetEnd2; targetStart3Input.value = targetStart3; targetEnd3Input.value = targetEnd3; }; const hidePanel = () => { container.style.display = 'none'; setTimeout(() => container.style.display = 'flex', 100); }; const container = document.createElement('div'); container.id = 'yt-time-btns'; container.style.position = 'fixed'; container.style.bottom = '0'; container.style.left = '0'; container.style.width = '100%'; container.style.zIndex = '99999'; container.style.backgroundColor = 'rgba(255,255,255,0.95)'; container.style.padding = '6px 10px'; container.style.borderTop = '1px solid #ccc'; container.style.display = 'flex'; container.style.flexWrap = 'nowrap'; container.style.alignItems = 'center'; container.style.fontFamily = 'sans-serif'; container.style.fontSize = '12px'; container.style.boxShadow = '0 -2px 6px rgba(0,0,0,0.1)'; container.style.gap = '6px'; container.style.overflowX = 'auto'; container.style.maxHeight = '140px'; const createBtn = (text, onClick) => { const btn = document.createElement('button'); btn.textContent = text; btn.style.padding = '4px 6px'; btn.style.fontSize = '12px'; btn.style.cursor = 'pointer'; btn.onclick = onClick; return btn; }; const labelInput = document.createElement('input'); labelInput.placeholder = 'Label'; labelInput.style.width = '60px'; labelInput.oninput = () => label = labelInput.value; const startInput = document.createElement('input'); startInput.placeholder = 'Start'; startInput.style.width = '60px'; const endInput = document.createElement('input'); endInput.placeholder = 'End'; endInput.style.width = '60px'; const queryInput = document.createElement('input'); queryInput.placeholder = 'Query'; queryInput.style.width = '60px'; const targetStartInput = document.createElement('input'); targetStartInput.placeholder = 'Target Start'; targetStartInput.style.width = '80px'; const targetEndInput = document.createElement('input'); targetEndInput.placeholder = 'Target End'; targetEndInput.style.width = '80px'; const targetStart2Input = document.createElement('input'); targetStart2Input.placeholder = 'TargetStart2'; targetStart2Input.style.width = '80px'; const targetEnd2Input = document.createElement('input'); targetEnd2Input.placeholder = 'TargetEnd2'; targetEnd2Input.style.width = '80px'; const targetStart3Input = document.createElement('input'); targetStart3Input.placeholder = 'TargetStart3'; targetStart3Input.style.width = '80px'; const targetEnd3Input = document.createElement('input'); targetEnd3Input.placeholder = 'TargetEnd3'; targetEnd3Input.style.width = '80px'; const previewArea = document.createElement('div'); previewArea.style.flex = '1'; previewArea.style.maxHeight = '80px'; previewArea.style.overflowY = 'auto'; previewArea.style.whiteSpace = 'pre-wrap'; previewArea.style.padding = '6px'; previewArea.style.border = '1px solid #ddd'; previewArea.style.backgroundColor = '#f9f9f9'; previewArea.style.minWidth = '300px'; previewArea.style.fontSize = '11px'; // Add all components to the toolbar container.appendChild(labelInput); container.appendChild(startInput); container.appendChild(createBtn('Set Start', () => setAndShow('start'))); container.appendChild(endInput); container.appendChild(createBtn('Set End', () => setAndShow('end'))); container.appendChild(queryInput); container.appendChild(createBtn('Set Query', () => setAndShow('query'))); container.appendChild(targetStartInput); container.appendChild(createBtn('Set TgtStart', () => setAndShow('targetStart'))); container.appendChild(targetEndInput); container.appendChild(createBtn('Set TgtEnd', () => setAndShow('targetEnd'))); container.appendChild(targetStart2Input); container.appendChild(createBtn('Set TgtStart2', () => setAndShow('targetStart2'))); container.appendChild(targetEnd2Input); container.appendChild(createBtn('Set TgtEnd2', () => setAndShow('targetEnd2'))); container.appendChild(targetStart3Input); container.appendChild(createBtn('Set TgtStart3', () => setAndShow('targetStart3'))); container.appendChild(targetEnd3Input); container.appendChild(createBtn('Set TgtEnd3', () => setAndShow('targetEnd3'))); container.appendChild(createBtn('Save Row', () => { saveRow(); hidePanel(); })); container.appendChild(createBtn('Download CSV', () => { downloadCSV(); hidePanel(); })); container.appendChild(previewArea); document.body.appendChild(container); updateInputs(); updatePreview(); const observer = new MutationObserver(() => { if (location.href.split('&')[0] !== videoURL) { videoURL = location.href.split('&')[0]; startTime = endTime = queryTime = targetStartTime = targetEndTime = targetStart2 = targetEnd2 = targetStart3 = targetEnd3 = ''; updateInputs(); } }); observer.observe(document.body, { childList: true, subtree: true }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址