Greasy Fork

Greasy Fork is available in English.

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

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

// ==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);
})();