Greasy Fork

Greasy Fork is available in English.

B站视频解析脚本[v1.5]

在B站视频页添加解析视频按钮

当前为 2025-05-17 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
// ==/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 = '<div style="text-align:center;">加载中...</div>';
        
        try {
            outputArea.innerText = `正在解析 BV号: ${bvId}...`;
            
            const data = await fetchWithCache(
                `video-info-${bvId}`,
                () => ApiService.getVideoInfo(bvId)
            );

            if (data.code === 0) {
                const cid = data.data.cid;
                const playData = await fetchWithCache(
                    `play-url-${bvId}-${cid}`,
                    () => ApiService.getPlayUrl(bvId, cid)
                );
                
                if (playData.code === 0) {
                    // 确保URL从顶部开始显示并自动换行
                    outputArea.innerHTML = '';
                    const urlText = document.createElement('div');
                    urlText.style.whiteSpace = 'pre-wrap';
                    urlText.style.wordBreak = 'break-all';
                    urlText.style.textAlign = 'left';
                    urlText.style.overflowAnchor = 'none';
                    urlText.style.color = '#333';
                    urlText.textContent = playData.data.durl[0].url;
                    outputArea.appendChild(urlText);
                    outputArea.scrollTop = 0; // 确保滚动到顶部
                } else {
                    outputArea.innerText = '获取播放链接失败。';
                }
            } else {
                outputArea.innerText = '解析失败,无法获取视频信息。';
            }
        } catch (error) {
            outputArea.innerText = '请求失败,请检查网络。';
            console.error('Error:', error);
        }
    });

    const copyButton = createButton('复制URL', {
        width: btnStyles.width,
        marginRight: btnStyles.marginRight,
        display: btnStyles.display
    }, () => {
        const videoUrl = outputArea.innerText.trim();
        if (videoUrl) {
            navigator.clipboard.writeText(videoUrl).then(() => {
                outputArea.innerText = '视频链接已复制到剪贴板!';
            }).catch(() => {
                outputArea.innerText = '复制失败,请手动复制。';
            });
        } else {
            outputArea.innerText = '没有视频链接可复制。';
        }
    });

    const closeButton = createButton('关闭', {
        width: btnStyles.width,
        display: btnStyles.display
    }, () => {
        modal.style.display = 'none';
        outputArea.innerText = '';
    });

    // 创建输出区域(更新样式确保URL正确显示)
    const outputStyles = positionCtrl.getOutputAreaStyles();
    const outputArea = document.createElement('div');
    Object.assign(outputArea.style, {
        marginTop: `${outputStyles.marginTop}px`,
        height: `${outputStyles.height}px`,
        border: '1px solid #ccc',
        padding: '5px',
        overflowY: 'auto',
        whiteSpace: 'pre-wrap',
        wordBreak: 'break-all',
        textAlign: 'left',
        overflowAnchor: 'none'
    });

    // 组装UI
    const buttonContainer = document.createElement('div');
    buttonContainer.style.marginBottom = '10px';
    buttonContainer.appendChild(startButton);
    buttonContainer.appendChild(copyButton);
    buttonContainer.appendChild(closeButton);

    modal.appendChild(buttonContainer);
    modal.appendChild(outputArea);

    document.body.appendChild(button);
    document.body.appendChild(modal);

    // 主按钮点击事件
    button.onclick = () => {
        modal.style.display = modal.style.display === 'none' ? 'block' : 'none';
        if (modal.style.display === 'block') {
            outputArea.innerHTML = '';
        }
    };

    // 窗口大小变化时重新计算位置
    window.addEventListener('resize', () => {
        const newBtnPos = positionCtrl.getMainButtonPosition();
        button.style.right = `${newBtnPos.right}px`;
        button.style.bottom = `${newBtnPos.bottom}px`;
        
        const newModalPos = positionCtrl.getModalPosition(newBtnPos.bottom, newBtnPos.right);
        modal.style.right = `${newModalPos.right}px`;
        modal.style.bottom = `${newModalPos.bottom}px`;
    });

    // 全局样式
    const style = document.createElement('style');
    style.textContent = `
        button:hover {
            background-color: #ff6b81;
            transform: scale(1.05);
        }
        div {
            font-family: Arial, sans-serif;
        }
    `;
    document.head.appendChild(style);
})();