// ==UserScript== // @name YouTube Screenshot Helper // @name:zh-TW YouTube 截圖助手 // @name:zh-CN YouTube 截图助手 // @namespace https://www.tampermonkey.net/ // @version 1.9 // @description YouTube Screenshot Tool – supports hotkey capture, burst mode, customizable hotkeys, burst interval settings, and menu language switch between Chinese and English. // @description:zh-TW YouTube截圖工具,支援快捷鍵截圖、連拍模式,自定義快捷鍵、連拍間隔設定、中英菜單切換 // @description:zh-CN YouTube截图工具,支援快捷键截图、连拍模式,自定义快捷键、连拍间隔设定、中英菜单切换 // @author ChatGPT // @match https://www.youtube.com/* // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 預設參數 const CONFIG = { defaultHotkey: 's', defaultInterval: 1000, minInterval: 100, defaultLang: 'EN', }; // 取得設定值 let screenshotKey = GM_getValue('screenshotKey', CONFIG.defaultHotkey); let interval = Math.max(parseInt(GM_getValue('captureInterval', CONFIG.defaultInterval)), CONFIG.minInterval); let lang = GM_getValue('lang', CONFIG.defaultLang); // 多語系 const I18N = { EN: { langToggle: 'LANG EN', setHotkey: `Set Screenshot Key (Now: ${screenshotKey.toUpperCase()})`, setInterval: `Set Burst Interval (Now: ${interval}ms)`, promptKey: 'Enter new hotkey (a-z):', promptInterval: `Enter new interval (min ${CONFIG.minInterval}ms):`, }, ZH: { langToggle: '語言 中文', setHotkey: `設定截圖快捷鍵(目前:${screenshotKey.toUpperCase()})`, setInterval: `設定連拍間隔(目前:${interval}ms)`, promptKey: '請輸入新的快捷鍵(單一字母):', promptInterval: `請輸入新的連拍間隔(單位ms,最低 ${CONFIG.minInterval}ms):`, }, }; const t = I18N[lang]; // 狀態變數 let keyDown = false; let intervalId = null; // Shorts 影片切換偵測 let lastShortsUrl = ''; function onShortsVideoChange() { // 目前不需特別重設,因為 takeScreenshot 會即時抓取 } // 監聽網址變化 setInterval(() => { if (window.location.href.includes('/shorts/')) { if (window.location.href !== lastShortsUrl) { lastShortsUrl = window.location.href; onShortsVideoChange(); } } }, 300); // 監聽 DOM 變化 if (window.location.href.includes('/shorts/')) { const observer = new MutationObserver(() => { onShortsVideoChange(); }); observer.observe(document.body, { childList: true, subtree: true }); } // 取得影片元素 function getVideoElement() { // Shorts 影片有時會有多個 video,只取可見的那個 const videos = Array.from(document.querySelectorAll('video')); if (window.location.href.includes('/shorts/')) { // 只取在畫面上的 video return videos.find(v => v.offsetParent !== null); } return videos[0] || null; } // 格式化時間 function formatTime(seconds) { const h = String(Math.floor(seconds / 3600)).padStart(2, '0'); const m = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0'); const s = String(Math.floor(seconds % 60)).padStart(2, '0'); const ms = String(Math.floor((seconds % 1) * 1000)).padStart(3, '0'); return `${h}_${m}_${s}_${ms}`; } // 取得影片ID function getVideoID() { let match = window.location.href.match(/\/shorts\/([a-zA-Z0-9_-]+)/); if (match) return match[1]; match = window.location.href.match(/\/live\/([a-zA-Z0-9_-]+)/); if (match) return match[1]; match = window.location.href.match(/[?&]v=([^&]+)/); return match ? match[1] : 'unknown'; } // 取得影片標題(重點修正:Shorts 動態標題) function getVideoTitle() { if (window.location.href.includes('/shorts/')) { // 取得目前 active 的 Shorts 標題 let h2 = document.querySelector('ytd-reel-video-renderer[is-active] h2'); if (h2 && h2.textContent.trim()) return h2.textContent.trim().replace(/[\\/:*?"<>|]/g, '').trim(); // Fallback: 取第一個 h2 h2 = document.querySelector('ytd-reel-video-renderer h2'); if (h2 && h2.textContent.trim()) return h2.textContent.trim().replace(/[\\/:*?"<>|]/g, '').trim(); // Fallback: meta let meta = document.querySelector('meta[name="title"]'); if (meta) return meta.getAttribute('content').replace(/[\\/:*?"<>|]/g, '').trim(); return (document.title || 'unknown').replace(/[\\/:*?"<>|]/g, '').trim(); } // Live if (window.location.href.includes('/live/')) { let title = document.querySelector('meta[name="title"]')?.getAttribute('content') || document.title || 'unknown'; return title.replace(/[\\/:*?"<>|]/g, '').trim(); } // 一般影片 let title = document.querySelector('h1.ytd-watch-metadata')?.textContent || document.querySelector('h1.title')?.innerText || document.querySelector('h1')?.innerText || document.querySelector('meta[name="title"]')?.getAttribute('content') || document.title || 'unknown'; return title.replace(/[\\/:*?"<>|]/g, '').trim(); } // 取得 Shorts 影片唯一標識(用 video.src 或影片ID) function getCurrentShortsUniqueKey() { const video = getVideoElement(); if (video && video.src) return video.src; return getVideoID(); } // 等待 Shorts 影片切換完成再截圖 function waitForShortsReadyAndScreenshot(prevKey, tryCount = 0) { const newKey = getCurrentShortsUniqueKey(); if (newKey && newKey !== prevKey) { takeScreenshot(); } else if (tryCount < 20) { // 最多等2秒 setTimeout(() => waitForShortsReadyAndScreenshot(prevKey, tryCount + 1), 100); } // 超時則放棄 } // 截圖主程式 function takeScreenshot() { const video = getVideoElement(); if (!video || video.videoWidth === 0 || video.videoHeight === 0) { // video 尚未載入,略過這次 return; } const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); const link = document.createElement('a'); const timestamp = formatTime(video.currentTime); const title = getVideoTitle(); const id = getVideoID(); const resolution = `${canvas.width}x${canvas.height}`; link.download = `${timestamp}_${title}_${id}_${resolution}.png`; link.href = canvas.toDataURL('image/png'); link.click(); } // 快捷鍵事件 document.addEventListener('keydown', (e) => { if ( e.key.toLowerCase() === screenshotKey && !keyDown && !['INPUT', 'TEXTAREA'].includes(e.target.tagName) ) { keyDown = true; if (window.location.href.includes('/shorts/')) { // Shorts 模式下,等待新影片載入 const prevKey = getCurrentShortsUniqueKey(); waitForShortsReadyAndScreenshot(prevKey); intervalId = setInterval(() => waitForShortsReadyAndScreenshot(getCurrentShortsUniqueKey()), interval); } else { takeScreenshot(); intervalId = setInterval(takeScreenshot, interval); } } }); document.addEventListener('keyup', (e) => { if (e.key.toLowerCase() === screenshotKey) { keyDown = false; clearInterval(intervalId); } }); // 油猴選單 GM_registerMenuCommand(t.setHotkey, () => { const input = prompt(t.promptKey, screenshotKey); if (input && /^[a-zA-Z]$/.test(input)) { GM_setValue('screenshotKey', input.toLowerCase()); location.reload(); } }); GM_registerMenuCommand(t.setInterval, () => { const input = parseInt(prompt(t.promptInterval, interval)); if (!isNaN(input) && input >= CONFIG.minInterval) { GM_setValue('captureInterval', input); location.reload(); } }); GM_registerMenuCommand(t.langToggle, () => { GM_setValue('lang', lang === 'EN' ? 'ZH' : 'EN'); location.reload(); }); })();