neoliu / bilibili音频封面下载

// ==UserScript==
// @name       bilibili音频封面下载
// @namespace   Violentmonkey Scripts
// @license MIT
// @match       https://www.bilibili.com/video/*
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        unsafeWindow
// @connect      *
// @connect      unpkg.com
// @connect      cdn.jsdelivr.net
// @author      neoliu
// @description 2025/4/1 20:39:01
// ==/UserScript==

(function () {
  'use strict';

  // 配置参数 (根据实际情况修改)
  const TARGET_PROPERTY = 'unsafeWindow.__INITIAL_STATE__.videoData.pic'; // 要监控的属性路径
  const CHECK_INTERVAL = 500; // 检查间隔(毫秒)
  const TIMEOUT = 30000; // 超时时间(毫秒)

  // 创建状态按钮
  const statusBtn = document.createElement('button');
  statusBtn.style = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        padding: 10px 20px;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        font-family: Arial;
        transition: all 0.3s;
        z-index: 99999;
    `;
  // statusBtn.onclick = process;

  // 初始状态:解析中
  let isChecking = true;
  let checkTimer = null;
  let timeoutTimer = null;
  updateButtonState('pending');

  // 添加按钮到页面
  document.body.appendChild(statusBtn);

  // 启动检查流程
  startPropertyCheck();

  /********************
   * 核心功能函数
   ********************/
  function startPropertyCheck() {
    // 第一次立即检查
    checkProperty();

    // 设置周期检查
    checkTimer = setInterval(checkProperty, CHECK_INTERVAL);

    // 设置超时终止
    timeoutTimer = setTimeout(() => {
      if (isChecking) {
        stopCheck();
        updateButtonState('timeout');
      }
    }, TIMEOUT);
  }

  function checkProperty() {
    console.log('checking', unsafeWindow.__INITIAL_STATE__.videoData.pic)
    try {
      // 使用安全访问方式 (支持多级属性如 'a.b.c')
      const propPath = TARGET_PROPERTY.replace(/^unsafeWindow\.?/, '').split('.');
      let value = unsafeWindow;
      for (const key of propPath) {
        value = value?.[key];
        if (value === undefined) break;
      }

      if (value !== undefined) {
        stopCheck();
        updateButtonState('success');
      }
    }
    catch (e) {
      console.error('属性检查错误:', e);
      stopCheck();
      updateButtonState('error');
    }
  }

  function stopCheck() {
    isChecking = false;
    clearInterval(checkTimer);
    clearTimeout(timeoutTimer);
  }

  function updateButtonState(state) {
    const states = {
      pending: {
        text: '🔄 解析中...',
        color: '#ffffff',
        bgColor: '#2196f3',
        hoverEffect: false
      },
      success: {
        text: '✅ 解析成功!',
        color: '#ffffff',
        bgColor: '#4caf50',
        hoverEffect: true
      },
      timeout: {
        text: '⛔ 解析失败',
        color: '#ffffff',
        bgColor: '#f44336',
        hoverEffect: false
      },
      error: {
        text: '⚠️ 发生错误',
        color: '#ffffff',
        bgColor: '#ff9800',
        hoverEffect: false
      },
      download_error: {
        text: '⚠️ 下载发生错误',
        color: '#ffffff',
        bgColor: '#ff9800',
        hoverEffect: false
      }
    };

    const config = states[state] || states.pending;

    // 更新样式
    statusBtn.textContent = config.text;
    statusBtn.style.backgroundColor = config.bgColor;
    statusBtn.style.color = config.color;
    statusBtn.style.cursor = config.hoverEffect ? 'pointer' : 'default';

    // 添加/移除悬停效果
    if (config.hoverEffect) {
      statusBtn.addEventListener('mouseover', hoverEffect);
      statusBtn.addEventListener('mouseout', resetEffect);
    }
    else {
      statusBtn.removeEventListener('mouseover', hoverEffect);
      statusBtn.removeEventListener('mouseout', resetEffect);
    }

    // 点击事件处理
    statusBtn.onclick = () => {
      if (state === 'success') {
        // 成功后的操作示例:显示属性值
        downloadAndZip()
      }
      else if (state === 'timeout') {
        // 失败后的操作示例:重新尝试
        if (confirm('加载超时,是否重试?')) {
          resetCheck();
        }
      }
    };
  }

  /********************
   * 辅助函数
   ********************/

  function resetCheck() {
    isChecking = true;
    updateButtonState('pending');
    startPropertyCheck();
  }

  function hoverEffect() {
    this.style.transform = 'scale(1.05)';
    this.style.boxShadow = '0 4px 15px rgba(0,0,0,0.2)';
  }

  function resetEffect() {
    this.style.transform = 'scale(1)';
    this.style.boxShadow = 'none';
  }

  function downloadAndZip() {

    var pubdate = unsafeWindow.__INITIAL_STATE__.videoData.pubdate;
    var d = new Date(pubdate * 1000).toLocaleString('zh-CN', {
      timeZone: 'Asia/Shanghai'
    });

    var audioURL = unsafeWindow.__playinfo__.data.dash.audio[0].base_url
    var coverURL = unsafeWindow.__INITIAL_STATE__.videoData.pic
    var title = unsafeWindow.__INITIAL_STATE__.videoData.title
    var author = unsafeWindow.__INITIAL_STATE__.videoData.owner.name

    console.log("音频链接: " + audioURL);
    console.log("封面: " + coverURL);
    console.log("标题: " + title);
    console.log("作者: " + author);
    console.log("发布时间: " + d);

    const meta = {}
    meta.title = title;
    meta.author = author;
    meta.coverURL = coverURL;
    meta.audioURL = audioURL;
    meta.audio_filename = title + ".m4s";
    meta.cover_filename = title + ".jpg";
    meta.timestamp = pubdate;
    meta.time = d;
    if (unsafeWindow.__INITIAL_STATE__.videoData.hasOwnProperty('ugc_season')) {
      meta.collection = unsafeWindow.__INITIAL_STATE__.videoData.ugc_season.title
    }
    else {
      meta.collection = author + "的默认专辑"
    }

    console.log(meta)

    const urls = [{
        u: audioURL,
        name: meta.audio_filename
      },
      {
        u: coverURL,
        name: meta.cover_filename
      },
    ]

    // 并行请求所有资源
    const downloadPromises = urls.map(url => {
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: 'GET',
          url: url.u,
          responseType: 'blob', // 关键参数:将响应数据转为Blob对象[1,6](@ref)
          onload: (response) => {
            if (response.status === 200) {
              const blob = response.response;
              resolve({
                url: url,
                data: blob,
                size: blob.size,
                type: blob.type
              });
            }
            else {
              reject(`下载失败: HTTP ${response.status}`);
            }
          },
          onerror: (err) => reject(`网络错误: ${err}`),
          ontimeout: () => reject('请求超时')
        });
      });
    });

    Promise.all(downloadPromises)
      .then(results => {
        results.forEach(file => {
          console.log('文件已加载到内存:', file);

          GM_download({
            url: URL.createObjectURL(file.data),
            name: file.url.name,
            onerror: (e) => console.error('保存失败:', e)
          });

        });
      })
      .catch(error => updateButtonState("download_error"));

    const ctt = JSON.stringify(meta);
    const blob = new Blob([ctt], {
      type: 'text/plain'
    });

    GM_download({
      url: URL.createObjectURL(blob),
      name: title + ".json",
    });
  }

})();

function fetchBlob(url) {
  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method: "GET",
      url,
      responseType: "blob",
      onload: (res) => res.status === 200 ? resolve(res.response) : reject(res.status),
      onerror: reject
    });
  });
}