NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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 }); }); }