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