B站循环助手-稳定版

稳定可靠的AB点循环工具,适配最新B站页面结构

当前为 2025-02-21 提交的版本,查看 最新版本

// ==UserScript==
// @name         B站循环助手-稳定版
// @namespace    bilibili-replayer
// @version      1.0
// @description  稳定可靠的AB点循环工具,适配最新B站页面结构
// @author       dms
// @match        https://www.bilibili.com/video/BV*
// @match        https://www.bilibili.com/bangumi/play/ep*
// @match        https://www.bilibili.com/medialist/play/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
  'use strict';

  // 配置参数
  const CONFIG = {
    checkInterval: 500,      // 视频检查间隔(ms)
    loopInterval:  200,       // 循环检查间隔(ms)
    buttonHeight:  '26px',    // 按钮高度
    buttonSpacing: '4px'      // 按钮间距
  };

  // 核心功能初始化
  function initReplayer() {
    // 获取视频容器(兼容新旧版页面)
    const container = 
      document.querySelector('.bpx-player-video-wrap') ||  // 新版
      document.querySelector('#playerWrap') ||             // 旧版普通视频
      document.querySelector('#player_module');           // 旧版番剧

    if (!container) {
      console.warn('[循环助手] 未找到视频容器');
      return;
    }

    // 创建工具栏容器
    const toolbar = document.createElement('div');
    toolbar.style.cssText = `
      width: 100%;
      height: ${CONFIG.buttonHeight};
      margin: 2px 0;
      position: relative;
      z-index: 100;
    `;

    // 插入工具栏
    container.parentNode.insertBefore(toolbar, container.nextSibling);

    // 初始化工具栏
    createToolbar(toolbar);
  }

  // 创建工具栏
  function createToolbar(container) {
    const shadow = container.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        :host {
          display: block;
          font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif;
        }

        .toolbar {
          display: flex;
          align-items: center;
          gap: ${CONFIG.buttonSpacing};
          padding: 0 8px;
          height: 100%;
          background: rgba(255,255,255,0.9);
          backdrop-filter: blur(4px);
          border-radius: 4px;
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }

        .btn {
          padding: 0 8px;
          height: 22px;
          line-height: 22px;
          border: 1px solid #e7e7e7;
          border-radius: 4px;
          background: #fff;
          color: #212121;
          font-size: 12px;
          cursor: pointer;
          transition: all 0.2s;
          white-space: nowrap;
        }

        .btn:hover {
          background: #f1f1f1;
          border-color: #00aeec;
          color: #00aeec;
        }

        .btn.active {
          background: #00aeec;
          border-color: #00aeec;
          color: #fff;
        }

        .separator {
          width: 1px;
          height: 16px;
          background: #e0e0e0;
          margin: 0 4px;
        }
      </style>
      <div class="toolbar"></div>
    `;

    const toolbar = shadow.querySelector('.toolbar');
    const video = document.querySelector('video');

    // 状态管理
    const state = {
      points: [
        GM_getValue('pointA', 0),
        GM_getValue('pointB', video.duration || 0)
      ],
      looping: false,
      intervalId: null
    };

    // 创建按钮
    const createButton = (text, handler, isActive = false) => {
      const btn = document.createElement('div');
      btn.className = `btn${isActive ? ' active' : ''}`;
      btn.textContent = text;
      btn.addEventListener('click', handler);
      return btn;
    };

    // 功能按钮组
    toolbar.append(
      createButton('设A', () => setPoint(0)),
      createButton('跳A', () => seekToPoint(0)),
      createButton('链A', () => copyLink(0)),
      createSeparator(),
      createButton('设B', () => setPoint(1)),
      createButton('跳B', () => seekToPoint(1)),
      createButton('链B', () => copyLink(1)),
      createSeparator(),
      createButton('循环', toggleLoop),
      createSeparator(),
      createButton('当前', copyCurrentTime)
    );

    // 辅助函数
    function createSeparator() {
      const sep = document.createElement('div');
      sep.className = 'separator';
      return sep;
    }

    // 核心功能
    function setPoint(index) {
      const video = document.querySelector('video');
      if (!video) return;

      const input = prompt(
        `设置${index ? '结束' : '开始'}时间(示例):\n`
        + '120 → 120秒\n'
        + '1:30 → 1分30秒\n'
        + '2h3m → 2小时3分',
        formatTime(video.currentTime)
      );

      if (input === null) return;

      const seconds = parseTime(input);
      if (isNaN(seconds)) {
        showAlert('时间格式错误');
        return;
      }

      state.points[index] = Math.max(0, Math.min(seconds, video.duration));
      GM_setValue(index ? 'pointB' : 'pointA', state.points[index]);
      updateButtonStates();
    }

    function toggleLoop() {
      state.looping = !state.looping;
      const btn = toolbar.querySelector('.btn:nth-child(9)');
      
      if (state.looping) {
        btn.classList.add('active');
        state.intervalId = setInterval(checkLoop, CONFIG.loopInterval);
      } else {
        btn.classList.remove('active');
        clearInterval(state.intervalId);
      }
    }

    function checkLoop() {
      const video = document.querySelector('video');
      if (!video) return;

      const [start, end] = state.points.slice().sort((a, b) => a - b);
      if (video.currentTime >= end) {
        video.currentTime = start;
      }
    }

    // 工具函数
    function parseTime(input) {
      if (!input) return NaN;

      // 处理带冒号格式 (HH:MM:SS)
      if (input.includes(':')) {
        const parts = input.split(':').reverse();
        return parts.reduce((acc, val, idx) => 
          acc + parseFloat(val) * Math.pow(60, idx), 0);
      }

      // 处理字母格式 (1h2m3s)
      const hours = (input.match(/(\d+)h/i) || [0,0])[1];
      const minutes = (input.match(/(\d+)m/i) || [0,0])[1];
      const seconds = (input.match(/(\d+)(?:s|$)/i) || [0,0])[1];
      return hours * 3600 + minutes * 60 + parseFloat(seconds);
    }

    function formatTime(seconds) {
      const date = new Date(seconds * 1000);
      return date.toISOString().substr(11, 8).replace(/^00:/, '');
    }

    function copyLink(index) {
      const time = state.points[index];
      const cleanUrl = window.location.href.replace(/[?&]t=\d+/, '');
      const newUrl = `${cleanUrl}${cleanUrl.includes('?') ? '&' : '?'}t=${Math.floor(time)}`;
      copyToClipboard(newUrl);
      showAlert('链接已复制到剪贴板');
    }

    function copyCurrentTime() {
      const video = document.querySelector('video');
      if (!video) return;
      copyLink({ 0: video.currentTime });
    }

    function copyToClipboard(text) {
      const textarea = document.createElement('textarea');
      textarea.value = text;
      document.body.appendChild(textarea);
      textarea.select();
      
      try {
        document.execCommand('copy');
        if (!navigator.clipboard) return;
        navigator.clipboard.writeText(text);
      } catch (err) {
        console.error('复制失败:', err);
      } finally {
        document.body.removeChild(textarea);
      }
    }

    function showAlert(message) {
      if (typeof GM_notification === 'function') {
        GM_notification({
          text: message,
          timeout: 2000
        });
      } else {
        alert(message);
      }
    }
  }

  // 页面初始化
  function checkVideo() {
    const check = () => {
      if (document.querySelector('video')) {
        initReplayer();
      } else {
        setTimeout(check, CONFIG.checkInterval);
      }
    };
    check();
  }

  // 启动脚本
  if (document.readyState === 'complete') {
    checkVideo();
  } else {
    window.addEventListener('load', checkVideo);
  }
})();

QingJ © 2025

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