// ==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 });
})();