Bilibili Video Screenshot Helper

Bilibili Video Screenshot Tool – supports screenshot button, hotkey capture, burst mode, customizable hotkeys and burst intervals, with menu language switch between Chinese and English.

Install this script?
Author's suggested script

You may also like Bilibili Live Screenshot Helper.

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