// ==UserScript== // @name B站视频解析脚本[v1.5] // @namespace http://tampermonkey.net/ // @version 1.5 // @description 在B站视频页添加解析视频按钮 // @author Waves_Man // @author-github https://github.com/WavesMan // @author-homepage https://home.waveyo.cn // @match https://www.bilibili.com/video/* // @icon https://cloud.waveyo.cn//Services/websites/home/images/icon/favicon.ico // @original-script https://scriptcat.org/zh-CN/script-show-page/2682/ // @grant none // @license GPL-2.0 license // @downloadURL https://update.greasyfork.icu/scripts/536307/B%E7%AB%99%E8%A7%86%E9%A2%91%E8%A7%A3%E6%9E%90%E8%84%9A%E6%9C%AC%5Bv15%5D.user.js // @updateURL https://update.greasyfork.icu/scripts/536307/B%E7%AB%99%E8%A7%86%E9%A2%91%E8%A7%A3%E6%9E%90%E8%84%9A%E6%9C%AC%5Bv15%5D.meta.js // ==/UserScript== (function() { 'use strict'; // ====================== 位置控制器 ====================== class PositionController { constructor() { this.config = { mainButton: { right: '5%', bottom: '5%', minMargin: 50 }, modal: { width: 300, offsetY: 40, padding: 20 }, actionButtons: { spacing: 10, width: 'auto', marginRight: 10 }, outputArea: { height: 100, marginTop: 10 } }; } calculateValue(value, base) { if (typeof value === 'string' && value.endsWith('%')) { return (parseFloat(value) / 100) * base; } return parseFloat(value); } getMainButtonPosition() { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const cfg = this.config.mainButton; const right = Math.max( this.calculateValue(cfg.right, viewportWidth), cfg.minMargin ); const bottom = Math.max( this.calculateValue(cfg.bottom, viewportHeight), cfg.minMargin ); return { right, bottom }; } getModalPosition(buttonBottom, buttonRight) { const cfg = this.config.modal; return { right: buttonRight, bottom: buttonBottom + cfg.offsetY, width: cfg.width, padding: cfg.padding }; } getActionButtonStyles() { const cfg = this.config.actionButtons; return { width: cfg.width, marginRight: cfg.marginRight, display: 'inline-block' }; } getOutputAreaStyles() { const cfg = this.config.outputArea; return { height: cfg.height, marginTop: cfg.marginTop }; } } // ====================== 主要逻辑 ====================== const positionCtrl = new PositionController(); const cache = new Map(); // API服务 const ApiService = { async getVideoInfo(bvid) { const response = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`); return response.json(); }, async getPlayUrl(bvid, cid) { const url = `https://api.bilibili.com/x/player/playurl?bvid=${bvid}&cid=${cid}&qn=64&fnval=1&fnver=0&fourk=0&platform=html5`; const headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3', 'Referer': 'https://www.bilibili.com/' }; const response = await fetch(url, { headers }); return response.json(); } }; // 带缓存的请求 async function fetchWithCache(key, fetchFn) { if (cache.has(key)) { return cache.get(key); } const result = await fetchFn(); cache.set(key, result); return result; } // 创建按钮 function createButton(text, styles = {}, onClick = null) { const button = document.createElement('button'); button.innerText = text; Object.assign(button.style, { position: 'relative', zIndex: '9999', padding: '10px 15px', backgroundColor: '#4CAF50', color: '#fff', border: 'none', borderRadius: '5px', cursor: 'pointer', transition: 'all 0.3s', ...styles }); if (onClick) button.onclick = onClick; return button; } // 主逻辑 const bvId = window.location.pathname.split('/')[2]; const btnPos = positionCtrl.getMainButtonPosition(); // 创建主按钮 const button = createButton('解析视频', { position: 'fixed', right: `${btnPos.right}px`, bottom: `${btnPos.bottom}px` }); // 创建弹窗 const modal = document.createElement('div'); const modalPos = positionCtrl.getModalPosition(btnPos.bottom, btnPos.right); Object.assign(modal.style, { display: 'none', position: 'fixed', right: `${modalPos.right}px`, bottom: `${modalPos.bottom}px`, width: `${modalPos.width}px`, padding: `${modalPos.padding}px`, backgroundColor: '#fff', boxShadow: '0 0 10px rgba(0,0,0,0.5)', zIndex: '10000' }); // 创建功能按钮 const btnStyles = positionCtrl.getActionButtonStyles(); const startButton = createButton('开始解析', { width: btnStyles.width, marginRight: btnStyles.marginRight, display: btnStyles.display }, async () => { outputArea.innerHTML = '