在B站(Bilibili)网页版自动显示歌词

自动歌词显示

// ==UserScript==
// @name 在B站(Bilibili)网页版自动显示歌词
// @version 1.0.1
// @description 自动歌词显示
// @author Tian
// @namespace BilibiliMusicLRC
// @license MIT
// @match https://www.bilibili.com/video/*
// @require https://static.hdslb.com/js/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @connect ark.cn-beijing.volces.com
// @connect api.52vmy.cn
// ==/UserScript==

(function () {
  'use strict';

  // 存储密钥(建议通过脚本设置页面输入,不要直接硬编码)
  // 首次使用请在控制台执行:
  // GM_setValue('tencentSecretId', '你的SecretId')
  // GM_setValue('tencentSecretKey', '你的SecretKey')
  const SECRET_ID = 'AKIDToxtgHj03va2HNHzUmtnUxFMG6jAPakl';
  const SECRET_KEY = 'tYzlMlz0sfOzOgu0ufDXdVCgz62sUt54';
  const TOKEN = "";

  // API配置
  const host = "ark.cn-beijing.volces.com/api/v3/chat/completions";


  // 添加所有的DOM元素
  // 创建主要容器
  const mainBox = document.createElement('div');
  // 创建标题
  const title = document.createElement('div');
  // 创建最小化按钮
  const minimizeButton = document.createElement('button');
  // 创建关闭按钮
  const closeButton = document.createElement('button');
  // 创建调用API按钮
  const apiButton = document.createElement('button');
  // 创建输入框
  const resultInput = document.createElement('input');
  // 创建结果显示区域
  const resultBox = document.createElement('div');
  // 创建歌词显示区域
  const resultMusicLrc = document.createElement('div');


  // 常量配置
  let music_Name = [];
  let music_Msg = [];
  let music_Msg_Time = [];
  let music_Chapter = [];
  let api_lock = false; // 防止重复请求锁

  /**
   * 初始化
   */
  function init() {
    // 关键步骤:注入 placeholder 样式(必须在输入框创建前执行,确保样式生效)
    const style = document.createElement('style');
    style.textContent = `
        .music-lyric-input::placeholder {
            color: #14ffec !important;
            opacity: 1;
        }
    `;
    document.head.appendChild(style);

    // 添加主盒子
    mainBox.id = 'music-lyric-main-box';
    mainBox.style.position = 'fixed';
    mainBox.style.width = '300px';
    mainBox.style.minHeight = '30px';
    mainBox.style.padding = '10px 15px';
    mainBox.style.top = '10px';
    mainBox.style.left = '10px';
    mainBox.style.zIndex = '9998';
    mainBox.style.backgroundColor = 'rgb(33, 33, 33)';
    mainBox.style.display = 'flex';
    mainBox.style.flexDirection = 'column';
    mainBox.style.alignItems = 'flex-start';
    mainBox.style.borderRadius = '8px';
    mainBox.style.fontSize = '14px';
    mainBox.style.lineHeight = '1.5';
    // 按住拖动
    mainBox.style.cursor = 'move';
    mainBox.onmousedown = function (event) {
      const shiftX = event.clientX - mainBox.getBoundingClientRect().left
      const shiftY = event.clientY - mainBox.getBoundingClientRect().top
      function moveAt(pageX, pageY) {
        mainBox.style.left = pageX - shiftX + 'px'
        mainBox.style.top = pageY - shiftY + 'px'
      }
      function onMouseMove(event) {
        moveAt(event.pageX, event.pageY)
      }
      document.addEventListener('mousemove', onMouseMove)
      mainBox.onmouseup = function () {
        document.removeEventListener('mousemove', onMouseMove)
        mainBox.onmouseup = null
      }
      mainBox.ondragstart = function () {
        return false
      }
    }
    document.body.appendChild(mainBox);

    // 添加标题
    title.textContent = '歌词助手';
    title.style.fontSize = '16px';
    title.style.fontWeight = 'bold';
    title.style.color = '#14ffec';
    title.style.position = 'absolute';
    title.style.top = '15px';
    title.style.left = '15px';
    title.style.userSelect = 'none';
    mainBox.appendChild(title);

    // 添加最小化
    minimizeButton.id = 'minimize-button';
    minimizeButton.textContent = '-';
    minimizeButton.style.position = 'absolute';
    minimizeButton.style.top = '10px';
    minimizeButton.style.right = '40px';
    minimizeButton.style.zIndex = '9999';
    minimizeButton.style.padding = '5px 10px';
    minimizeButton.style.cursor = 'pointer';
    minimizeButton.style.border = 'none';
    minimizeButton.style.borderRadius = '5px';
    minimizeButton.style.backgroundColor = '#323232';
    minimizeButton.style.color = '#14ffec';
    minimizeButton.addEventListener('click', () => {
      apiButton.style.display = apiButton.style.display === 'none' ? 'block' : 'none';
      resultInput.style.display = resultInput.style.display === 'none' ? 'block' : 'none';
      resultBox.style.display = resultBox.style.display === 'none' ? 'block' : 'none';
      minimizeButton.textContent = minimizeButton.textContent === '-' ? '+' : '-';
    });
    mainBox.appendChild(minimizeButton);

    // 添加关闭
    closeButton.id = 'close-button';
    closeButton.textContent = '×';
    closeButton.style.position = 'absolute';
    closeButton.style.top = '10px';
    closeButton.style.right = '10px';
    closeButton.style.zIndex = '9999';
    closeButton.style.padding = '5px 10px';
    closeButton.style.cursor = 'pointer';
    closeButton.style.border = 'none';
    closeButton.style.borderRadius = '5px';
    closeButton.style.backgroundColor = '#323232';
    closeButton.style.color = '#14ffec';
    closeButton.addEventListener('click', () => {
      mainBox.remove();
    });
    mainBox.appendChild(closeButton);

    // 也可以添加一个按钮,点击时调用API
    apiButton.id = 'call-nlp-api-button';
    apiButton.textContent = '重新查找歌词!';
    apiButton.style.padding = '10px 15px';
    apiButton.style.zIndex = '9998';
    apiButton.style.cursor = 'pointer';
    apiButton.style.border = 'none';
    apiButton.style.borderRadius = '5px';
    apiButton.style.backgroundColor = '#323232';
    apiButton.style.color = '#14ffec';
    apiButton.style.marginBottom = '10px';
    apiButton.style.marginTop = '40px';
    apiButton.addEventListener('click', callNlpApi);
    mainBox.appendChild(apiButton);

    // 输入框
    resultInput.type = 'text';
    resultInput.placeholder = '输入歌名获取歌词';
    resultInput.disabled = false;
    resultInput.readOnly = false;
    resultInput.autocomplete = 'off'; // 避免自动填充干扰
    // 添加input样式
    resultInput.className = 'music-lyric-input';
    resultInput.style.width = '150px';
    resultInput.style.padding = '5px';
    resultInput.style.paddingLeft = '13px'; // 确保placeholder有内边距
    resultInput.style.borderRadius = '5px';
    resultInput.style.border = '1px solid #323232'; // 原代码border: none可能导致视觉上不可见
    resultInput.style.boxSizing = 'border-box';
    resultInput.style.fontSize = '14px';
    resultInput.style.outline = 'none'; // 可选:去除聚焦边框
    resultInput.style.zIndex = '9999'; // 确保在最上层,避免被遮挡
    resultInput.style.backgroundColor = '#323232';
    resultInput.style.color = '#14ffec';
    resultInput.style.marginBottom = '10px';
    resultInput.addEventListener('keydown', (e) => {
      e.stopPropagation(); // 阻止事件冒泡到页面
    });
    resultInput.addEventListener('input', (e) => {
      e.stopPropagation();
    });
    resultInput.addEventListener('blur', (e) => {
      getMusic(e.target.value);
      e.stopPropagation();
    });
    mainBox.appendChild(resultInput);

    // 结果显示区域
    resultBox.className = 'music-lyric-result-box';
    resultBox.style.padding = '10px 15px';
    resultBox.style.backgroundColor = '#323232';
    resultBox.style.color = '#14ffec';
    resultBox.style.zIndex = '9999';
    resultBox.style.maxWidth = '300px';
    resultBox.style.maxHeight = '400px';
    resultBox.style.overflowY = 'auto';
    resultBox.style.borderRadius = '8px';
    resultBox.style.fontSize = '14px';
    resultBox.style.lineHeight = '1.5';
    resultBox.style.marginBottom = '10px';
    resultBox.style.fontWeight = '300';
    resultBox.innerHTML = `
    <strong>识别到的歌词/作品:</strong>
    <br>
    <div class="box_item"></div>
    `;
    mainBox.appendChild(resultBox);
  }

  /**
   * 发起API请求
   */
  async function callNlpApi() {
    if (api_lock) {
      return; // 如果锁定,则不执行
    }
    api_lock = true; // 上锁
    resetConstants();
    const box_item = document.querySelector('.box_item');
    if (box_item) {
      box_item.innerHTML = '识别中...';
    }
    try {
      const sampleText = $('.video-title').text();
      music_Chapter = [];
      let tempChapter = document.querySelectorAll('.bpx-player-ctrl-viewpoint-menu-item-content');
      for (let i = 0; i < tempChapter.length; i++) {
        music_Chapter.push(tempChapter[i].textContent);
      }
      console.log(music_Chapter);
      const payload = JSON.stringify({
        "model": "doubao-1-5-thinking-pro-250415",
        "messages": [
          { "role": "system", "content": "你是人工智能助手." },
          { "role": "user", "content": `请从以下文本中识别出歌词和作品名称,文本内容为:${sampleText + music_Chapter.join(',')}。只需要返回歌词和作品名称即可,用英文逗号分割,不需要其他多余的描述。如果没有识别出歌词或作品,请返回“ ”。` }
        ]
      });
      // 使用GM_xmlhttpRequest发送请求(避免跨域问题)
      GM_xmlhttpRequest({
        method: 'POST',
        url: `https://${host}`,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer 628cbe0e-210d-46e0-bd89-cee9ff9957e0`
        },
        data: payload,
        onload: function (response) {
          console.log("API响应:", JSON.parse(response.responseText));
          // 可以在这里处理返回结果,例如显示在页面上
          box_item.innerHTML = '';
          api_lock = false; // 解锁
          showResult(JSON.parse(response.responseText).choices[0].message.content);
        },
        onerror: function (error) {
          api_lock = false; // 解锁
          box_item.innerHTML = '请求出错,请稍后再试.';
        }
      });

    } catch (error) {
      console.error("调用API时出错:", error);
    }
  }

  /**
   * 获取歌曲
   */
  async function getMusic(musicName) {
    if (api_lock) {
      return; // 如果锁定,则不执行
    }
    api_lock = true; // 上锁
    if (document.querySelector('.music-lyric-result-lrc')) {
      document.querySelector('.music-lyric-result-lrc').remove();
    }
    try {
      // 使用GM_xmlhttpRequest发送请求(避免跨域问题)
      GM_xmlhttpRequest({
        url: 'https://api.52vmy.cn/api/music/lrc?msg=' + encodeURIComponent(musicName) + '&n=1',
        method: 'GET',
        headers: {
          'Content-Type': 'application/json'
        },
        onload: function (response) {
          // 处理数据 正则删除[00:00.35]
          const cleanedLyric = response.responseText.replace(/\[.*?\]/g, '');
          // 提取歌词
          music_Msg = cleanedLyric
            .split(/[\r\n]+/)  // 按任意换行符拆分(兼容所有系统)
            .filter(line => line.trim() !== '');  // 过滤空行/纯空白行
          // 提取时间
          music_Msg_Time = (response.responseText.match(/\[(\d{2}:\d{2}\.\d{2})\]/g) || []).map(t => t.replace(/[\[\]]/g, ''));
          console.log(music_Msg_Time);
          resultMusicLrc.className = 'music-lyric-result-lrc';
          resultMusicLrc.style.position = 'fixed';
          resultMusicLrc.style.top = '80px';
          resultMusicLrc.style.right = '10px';
          resultMusicLrc.style.padding = '10px 15px';
          resultMusicLrc.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
          resultMusicLrc.style.color = '#fff';
          resultMusicLrc.style.zIndex = '9999';
          resultMusicLrc.style.maxWidth = '200px';
          resultMusicLrc.style.maxHeight = '300px';
          resultMusicLrc.style.overflowY = 'auto';
          resultMusicLrc.style.borderRadius = '8px';
          resultMusicLrc.style.fontSize = '14px';
          resultMusicLrc.style.lineHeight = '1.5';
          // 隐藏滑动条
          resultMusicLrc.style.scrollbarWidth = 'none'; // Firefox
          resultMusicLrc.style.msOverflowStyle = 'none'; // IE 10+
          // 按住拖动
          resultMusicLrc.style.cursor = 'move';
          resultMusicLrc.onmousedown = function (event) {
            const shiftX = event.clientX - resultMusicLrc.getBoundingClientRect().left
            const shiftY = event.clientY - resultMusicLrc.getBoundingClientRect().top
            function moveAt(pageX, pageY) {
              resultMusicLrc.style.left = pageX - shiftX + 'px'
              resultMusicLrc.style.top = pageY - shiftY + 'px'
            }
            function onMouseMove(event) {
              moveAt(event.pageX, event.pageY)
            }
            document.addEventListener('mousemove', onMouseMove)
            resultMusicLrc.onmouseup = function () {
              document.removeEventListener('mousemove', onMouseMove)
              resultMusicLrc.onmouseup = null
            }
            resultMusicLrc.ondragstart = function () {
              return false
            }

          }
          document.body.appendChild(resultMusicLrc);
          for (let i = 0; i < music_Msg.length; i++) {
            // 创建歌词行元素
            const lineDiv = document.createElement('div');
            lineDiv.textContent = music_Msg[i];
            lineDiv.style.padding = '5px';
            lineDiv.style.fontSize = '16px';
            resultMusicLrc.appendChild(lineDiv);
          }
          apiButton.style.display = apiButton.style.display === 'none' ? 'block' : 'none';
          resultInput.style.display = resultInput.style.display === 'none' ? 'block' : 'none';
          resultBox.style.display = resultBox.style.display === 'none' ? 'block' : 'none';
          minimizeButton.textContent = minimizeButton.textContent === '-' ? '+' : '-';
          scrollLyric();
          api_lock = false; // 解锁
        },
        onerror: function (error) {
          console.error("API请求错误:", error);
          api_lock = false; // 解锁
        }
      });

    } catch (error) {
      console.error("调用API时出错:", error);
      api_lock = false; // 解锁
    }
  }

  /**
   * 在页面上显示结果
   * @param {object} result API返回的结果
   */
  function showResult(result) {
    const resultBoxItem = document.querySelector('.box_item');
    // 先处理数据 按照,分割
    music_Name = result.split(',');
    // 去重
    music_Name = Array.from(new Set(music_Name));
    // 去除空格
    music_Name = music_Name.map(name => name.trim()).filter(name => name !== '');
    // 显示在页面上
    resultBoxItem.innerHTML = '';
    for (let i = 0; i < music_Name.length; i++) {
      const resultNameItem = document.createElement('div');
      resultNameItem.style.display = 'inline-block';
      resultNameItem.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
      resultNameItem.style.padding = '5px 10px';
      resultNameItem.style.borderRadius = '5px';
      resultNameItem.style.margin = '2px 5px 2px 0';
      resultNameItem.style.userSelect = 'none';
      resultNameItem.style.cursor = 'pointer';
      resultNameItem.innerHTML = music_Name[i];
      resultNameItem.addEventListener('click', function () {
        getMusic(music_Name[i]);
      });
      resultBoxItem.appendChild(resultNameItem);
    }
    if (music_Name.length === 0) {
      resultBoxItem.innerHTML = '未识别到歌词或作品';
    }
  }

  /**
   * 歌词滚动
   */
  function scrollLyric() {
    // 时间格式为 mm:ss.xx 转换为毫秒
    function timeToSeconds(time) {
      const parts = time.split(':');
      const minutes = parseInt(parts[0], 10);
      const seconds = parseFloat(parts[1]);
      return (minutes * 60 + seconds) * 1000;
    }
    // 转换所有时间为毫秒
    music_Msg_Time = music_Msg_Time.map(timeToSeconds);
    // 到达时间后滚动到对应位置,打印对应歌词
    const interval = setInterval(() => {
      const currentTime = document.querySelector('video').currentTime * 1000; // 当前时间 毫秒
      for (let i = 0; i < music_Msg_Time.length; i++) {
        // 提前5毫秒
        if (currentTime >= (music_Msg_Time[i] - 5) && currentTime < ((music_Msg_Time[i + 1] || Infinity) - 5)) {
          // 滚动到对应位置
          const lines = resultMusicLrc.querySelectorAll('div');
          if (lines[i]) {
            lines[i].scrollIntoView({ behavior: 'smooth', block: 'center' });
            // 高亮当前行
            lines[i].style.color = '#ff0';
            // 取消上一个高亮
            if (i > 0 && lines[i - 1]) {
              lines[i - 1].style.color = '#fff';
            }
          }
          break;
        }
      }
    }, 100);
  }

  /**
   * 重置常量
   */
  function resetConstants() {
    music_Name = [];
    const resultBoxItem = document.querySelector('.box_item');
    if (resultBoxItem) {
      resultBoxItem.innerHTML = '';
    }
  }

  // 页面加载完成后执行
  $(document).ready(function () {
    init();
  });

})();