YouTube Timestamp Saver (Optimized UI)

Save YouTube timestamps with simplified UI, exportable CSV, and persistent storage

  1. // ==UserScript==
  2. // @name YouTube Timestamp Saver (Optimized UI)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  5. // @description Save YouTube timestamps with simplified UI, exportable CSV, and persistent storage
  6. // @author You
  7. // @match https://www.youtube.com/watch*
  8. // @match https://www.youtube.com/shorts/*
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. const timeFields = [
  15. ['Start', 'startTime'],
  16. ['End', 'endTime'],
  17. ['Query', 'queryTime'],
  18. ['TgtStart', 'targetStartTime'],
  19. ['TgtEnd', 'targetEndTime'],
  20. ['TgtStart2', 'targetStart2'],
  21. ['TgtEnd2', 'targetEnd2'],
  22. ['TgtStart3', 'targetStart3'],
  23. ['TgtEnd3', 'targetEnd3']
  24. ];
  25.  
  26. let label = '', videoURL = location.href.split('&')[0];
  27. let values = Object.fromEntries(timeFields.map(([_, k]) => [k, '']));
  28. let savedRows = JSON.parse(localStorage.getItem('yt_savedRows') || '[]');
  29.  
  30. const saveToLocal = () => localStorage.setItem('yt_savedRows', JSON.stringify(savedRows));
  31.  
  32. const formatTime = (s) => {
  33. const h = Math.floor(s / 3600);
  34. const m = Math.floor((s % 3600) / 60);
  35. const sec = Math.floor(s % 60);
  36. const ms = Math.round((s % 1) * 1000);
  37. const formatted = `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
  38. return h > 0 ? `${h}:${formatted}` : formatted;
  39. };
  40.  
  41. const parseTime = (str) => {
  42. const p = str.split(/[:.]/).map(Number);
  43. return p.length === 4 ? p[0]*3600 + p[1]*60 + p[2] + p[3]/1000 : p[0]*60 + p[1] + p[2]/1000;
  44. };
  45.  
  46. const getTime = () => {
  47. const v = document.querySelector('video');
  48. return v ? formatTime(v.currentTime) : '';
  49. };
  50.  
  51. const seekTo = (key) => {
  52. const v = document.querySelector('video');
  53. const t = parseTime(inputs[key].value);
  54. if (v && !isNaN(t)) v.currentTime = t;
  55. };
  56.  
  57. const setTime = (key) => {
  58. values[key] = getTime();
  59. inputs[key].value = values[key];
  60. };
  61.  
  62. const saveRow = () => {
  63. const row = [videoURL, label, ...timeFields.map(([_, k]) => values[k])];
  64. savedRows.push(row);
  65. saveToLocal();
  66. preview.textContent = savedRows.map(r => r.join('\t')).join('\n');
  67. labelInput.value = '';
  68. timeFields.forEach(([_, k]) => inputs[k].value = values[k] = '');
  69. };
  70.  
  71. const downloadCSV = () => {
  72. const header = ['videoURL', 'label', ...timeFields.map(([_, k]) => k)];
  73. const csv = [header, ...savedRows].map(r => r.join(',')).join('\n');
  74. const blob = new Blob([csv], { type: 'text/csv' });
  75. const a = document.createElement('a');
  76. a.href = URL.createObjectURL(blob);
  77. a.download = 'timestamps.csv';
  78. a.click();
  79. savedRows = [];
  80. localStorage.removeItem('yt_savedRows');
  81. preview.textContent = '';
  82. };
  83.  
  84. // Create UI
  85. const bar = document.createElement('div');
  86. bar.style.cssText = `position:fixed;bottom:0;left:0;width:100%;background:#fff;padding:5px 8px;display:flex;flex-wrap:wrap;gap:4px;font:12px sans-serif;z-index:99999;box-shadow:0 -1px 6px rgba(0,0,0,0.1)`;
  87.  
  88. const inputs = {};
  89.  
  90. const labelInput = document.createElement('input');
  91. labelInput.placeholder = 'Label';
  92. labelInput.style.width = '80px';
  93. labelInput.oninput = () => label = labelInput.value;
  94. bar.appendChild(labelInput);
  95.  
  96. timeFields.forEach(([labelText, key]) => {
  97. const input = document.createElement('input');
  98. input.placeholder = labelText;
  99. input.style.width = '70px';
  100. inputs[key] = input;
  101. bar.appendChild(input);
  102. bar.appendChild(Object.assign(document.createElement('button'), {
  103. textContent: 'Set', onclick: () => setTime(key), style: 'font-size:11px'
  104. }));
  105. bar.appendChild(Object.assign(document.createElement('button'), {
  106. textContent: 'Go', onclick: () => seekTo(key), style: 'font-size:11px'
  107. }));
  108. });
  109.  
  110. bar.appendChild(Object.assign(document.createElement('button'), {
  111. textContent: 'Save Row', onclick: saveRow, style: 'background:#4caf50;color:#fff;padding:3px 8px'
  112. }));
  113.  
  114. bar.appendChild(Object.assign(document.createElement('button'), {
  115. textContent: 'Download CSV', onclick: downloadCSV, style: 'padding:3px 8px'
  116. }));
  117.  
  118. const preview = document.createElement('div');
  119. preview.style.cssText = 'white-space:pre;overflow:auto;max-height:60px;width:100%';
  120. bar.appendChild(preview);
  121.  
  122. document.body.appendChild(bar);
  123. preview.textContent = savedRows.map(r => r.join('\t')).join('\n');
  124.  
  125. // URL change detection
  126. let lastURL = videoURL;
  127. const observer = new MutationObserver(() => {
  128. const newURL = location.href.split('&')[0];
  129. if (newURL !== lastURL) {
  130. lastURL = videoURL = newURL;
  131. label = '';
  132. Object.keys(values).forEach(k => values[k] = '');
  133. timeFields.forEach(([_, k]) => inputs[k].value = '');
  134. }
  135. });
  136. observer.observe(document.body, { childList: true, subtree: true });
  137. })();

QingJ © 2025

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