Bilibili 视频截图助手

B站视频截图工具,支援截图按钮、快捷键截图、连拍功能,自定义快捷键、连拍间隔设定、中英菜单切换

安装此脚本?
作者推荐脚本

您可能也喜欢Bilibili 直播截图助手

安装此脚本
// ==UserScript==
// @name         Bilibili Video Screenshot Helper
// @name:zh-TW   Bilibili 影片截圖助手
// @name:zh-CN   Bilibili 视频截图助手
// @namespace    https://www.tampermonkey.net/
// @version      2.4
// @description  Bilibili Video Screenshot Tool – supports screenshot button, hotkey capture, burst mode, customizable hotkeys and burst intervals, with menu language switch between Chinese and English.
// @description:zh-TW B站影片截圖工具,支援截圖按鈕、快捷鍵截圖、連拍功能,自定義快捷鍵、連拍間隔設定、中英菜單切換
// @description:zh-CN B站视频截图工具,支援截图按钮、快捷键截图、连拍功能,自定义快捷键、连拍间隔设定、中英菜单切换
// @author       ChatGPT
// @match        https://www.bilibili.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // ====== 預設設定 ======
  const DEFAULTS = {
    key: 'S',
    interval: 1000,
    minInterval: 100,
    lang: 'EN',
    lockKey: 'bili_screenshot_prompt_lock'
  };

  // ====== 語言包 ======
  const LANGS = {
    EN: {
      screenshot: 'Screenshot',
      keySetting: key => `Set Screenshot Key (Current: ${key})`,
      intervalSetting: val => `Set Burst Interval (Current: ${val}ms)`,
      langSwitch: 'Switch to 中文',
      keyPrompt: 'Enter new key (A-Z)',
      intervalPrompt: 'Enter new interval in ms (>= 100)'
    },
    ZH: {
      screenshot: '截圖',
      keySetting: key => `設定截圖快捷鍵(目前:${key})`,
      intervalSetting: val => `設定連拍間隔(目前:${val} 毫秒)`,
      langSwitch: '切換到 English',
      keyPrompt: '輸入新快捷鍵(A-Z)',
      intervalPrompt: '輸入新的連拍間隔(最小 100ms)'
    }
  };

  // ====== 狀態管理 ======
  let lang = GM_getValue('lang', DEFAULTS.lang);
  let hotkey = GM_getValue('hotkey', DEFAULTS.key);
  let interval = GM_getValue('interval', DEFAULTS.interval);

  function getLangPack() {
    return LANGS[lang];
  }

  // ====== 設定選單註冊 ======
  function safePrompt(action) {
    if (window.top !== window.self) return;
    if (sessionStorage.getItem(DEFAULTS.lockKey) === '1') return;
    sessionStorage.setItem(DEFAULTS.lockKey, '1');
    try {
      action();
    } finally {
      sessionStorage.removeItem(DEFAULTS.lockKey);
    }
  }

  GM_registerMenuCommand(getLangPack().keySetting(hotkey), () => {
    safePrompt(() => {
      const input = prompt(getLangPack().keyPrompt);
      if (input && /^[a-zA-Z]$/.test(input)) {
        const newKey = input.toUpperCase();
        if (newKey !== hotkey) {
          GM_setValue('hotkey', newKey);
          location.reload();
        }
      }
    });
  });

  GM_registerMenuCommand(getLangPack().intervalSetting(interval), () => {
    safePrompt(() => {
      const input = prompt(getLangPack().intervalPrompt);
      const val = parseInt(input, 10);
      if (!isNaN(val) && val >= DEFAULTS.minInterval && val !== interval) {
        GM_setValue('interval', val);
        location.reload();
      }
    });
  });

  GM_registerMenuCommand(getLangPack().langSwitch, () => {
    safePrompt(() => {
      const newLang = lang === 'EN' ? 'ZH' : 'EN';
      GM_setValue('lang', newLang);
      location.reload();
    });
  });

  // ====== 取得影片標題 ======
  function getVideoTitle() {
    // 新播放器
    let title = document.querySelector('h1[data-v-1c684a5a]')?.innerText
      || document.querySelector('h1.video-title')?.innerText
      || document.querySelector('h1')?.innerText;
    // 備用:<title>
    if (!title) {
      title = document.title.replace(/_.*$/, '').trim();
    }
    // 過濾非法檔名字元
    if (title) {
      title = title.replace(/[\\/:*?"<>|]/g, '_').replace(/\s+/g, '_');
    } else {
      title = 'UnknownTitle';
    }
    return title;
  }

  // ====== 截圖邏輯 ======
  function takeScreenshot() {
    const video = document.querySelector('video');
    const match = location.pathname.match(/\/video\/(BV\w+)/);
    if (!match || !video || video.paused) 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 pad = (n, len = 2) => n.toString().padStart(len, '0');
    const padMs = n => pad(n, 3);
    const bvId = match[1];
    const t = video.currentTime;
    const h = pad(Math.floor(t / 3600));
    const m = pad(Math.floor((t % 3600) / 60));
    const s = pad(Math.floor(t % 60));
    const ms = padMs(Math.floor((t * 1000) % 1000));
    const res = `${canvas.width}x${canvas.height}`;
    const title = getVideoTitle();
    // 修改命名規則:影片標題_小時_分鐘_秒_毫秒_BV號_解析度
    const filename = `${title}_${h}_${m}_${s}_${ms}_${bvId}_${res}.png`;

    canvas.toBlob(blob => {
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      a.click();
      setTimeout(() => URL.revokeObjectURL(url), 100);
    }, 'image/png');
  }

  // ====== 插入截圖按鈕 ======
  function insertScreenshotButton() {
    const qualityBtn = document.querySelector('.bpx-player-ctrl-quality');
    if (!qualityBtn || document.querySelector('.bili-screenshot-btn')) return;

    const btn = document.createElement('div');
    btn.className = 'bpx-player-ctrl-btn bili-screenshot-btn';
    Object.assign(btn.style, {
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      cursor: 'pointer',
      fontSize: '18px',
      marginRight: '6px'
    });
    btn.title = getLangPack().screenshot;
    btn.innerHTML = '📸';
    btn.addEventListener('click', takeScreenshot);
    qualityBtn.parentNode.insertBefore(btn, qualityBtn);
  }

  // ====== 監聽 DOM 插入按鈕(全程監聽,SPA跳轉也能偵測) ======
  const observer = new MutationObserver(() => {
    insertScreenshotButton();
    // 動態更新 title
    const btn = document.querySelector('.bili-screenshot-btn');
    if (btn) btn.title = getLangPack().screenshot;
  });
  observer.observe(document.body, { childList: true, subtree: true });

  // ====== 快捷鍵與連拍 ======
  let holdTimer = null;
  document.addEventListener('keydown', e => {
    if (e.repeat) return;
    if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.isContentEditable) return;
    hotkey = GM_getValue('hotkey', DEFAULTS.key);
    interval = GM_getValue('interval', DEFAULTS.interval);
    if (e.key.toUpperCase() === hotkey && !holdTimer) {
      takeScreenshot();
      holdTimer = setInterval(takeScreenshot, interval);
    }
  });
  document.addEventListener('keyup', e => {
    hotkey = GM_getValue('hotkey', DEFAULTS.key);
    if (e.key.toUpperCase() === hotkey && holdTimer) {
      clearInterval(holdTimer);
      holdTimer = null;
    }
  });

  // ====== SPA 路徑變化偵測(不 reload,只更新語言包) ======
  let lastPath = location.pathname;
  function onPathChange() {
    if (location.pathname !== lastPath) {
      lastPath = location.pathname;
      // 只要是影片頁就更新按鈕 title
      setTimeout(() => {
        const btn = document.querySelector('.bili-screenshot-btn');
        if (btn) btn.title = getLangPack().screenshot;
      }, 500);
    }
  }
  (function(history){
    const pushState = history.pushState;
    const replaceState = history.replaceState;
    history.pushState = function() {
      pushState.apply(this, arguments);
      setTimeout(onPathChange, 100);
    };
    history.replaceState = function() {
      replaceState.apply(this, arguments);
      setTimeout(onPathChange, 100);
    };
  })(window.history);
  window.addEventListener('popstate', () => setTimeout(onPathChange, 100));

  // ====== 初始化 ======
  insertScreenshotButton();
})();