YouTube Timestamp Saver

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