嗶哩嗶哩視頻下載助手📥

一鍵下載嗶哩嗶哩視頻,界面簡潔易用

// ==UserScript==
// @name         bilibili哔哩哔哩视频下载 📥
// @name:zh-CN   哔哩哔哩视频下载助手📥
// @name:zh-TW   嗶哩嗶哩視頻下載助手📥
// @name:en      Bilibili Video Downloader 📥
// @name:ja      ビリビリ動画ダウンローダー 📥
// @name:ko      비리비리 비디오 다운로더 📥
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  在哔哩哔哩视频页面添加下载按钮,支持多种清晰度和格式
// @description:zh-CN  一键下载哔哩哔哩视频,界面简洁易用
// @description:zh-TW  一鍵下載嗶哩嗶哩視頻,界面簡潔易用
// @description:en  Download Bilibili videos with one click, clean and easy-to-use interface
// @description:ja  ビリビリ動画を1クリックでダウンロード、シンプルで使いやすいインターフェース
// @description:ko  비리비리 동영상 원클릭 다운로드, 깔끔하고 사용하기 쉬운 인터페이스
// @author       youhou
// @match        https://www.bilibili.com/video/*
// @grant        none
// @license      MIT
// @homepage     https://saveany.cn
// @supportURL   https://saveany.cn
// @keywords     bilibili,哔哩哔哩,视频下载,B站下载,bilibili下载,哔哩哔哩视频下载,B站视频下载器,bilibili视频下载,B站,下载视频,下载,ビリビリ,ダウンロード,비리비리,다운로드
// @icon         https://www.bilibili.com/favicon.ico
// ==/UserScript==

(function() {
    'use strict';

    const PARSE_APIS = [
        'https://api.injahow.cn/bparse/',
        'https://jx.jsonplayer.com/player/',
        'https://jx.bozrc.com:4433/player/',
        'https://jx.parwix.com:4433/player/'
    ];

    function createDownloadButton() {
        const downloadBtn = document.createElement('button');
        downloadBtn.textContent = '下载';
        downloadBtn.style.cssText = `
            margin-left: 10px;
            padding: 5px 12px;
            background: #00aeec;
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 13px;
            height: 32px;
            line-height: 18px;
            min-width: 50px;
        `;
        downloadBtn.onclick = startDownload;
        return downloadBtn;
    }

    function getBiliVideoInfo() {
        try {
            let initialState = window.__INITIAL_STATE__;
            let videoData = initialState?.videoData;

            if (!videoData) {
                // 尝试从 window.__playinfo__ 获取
                const playInfo = window.__playinfo__;
                if (playInfo) {
                    videoData = {
                        bvid: document.querySelector('meta[itemprop="url"]')?.content?.split('/').pop(),
                        aid: playInfo.aid,
                        cid: playInfo.cid,
                        title: document.querySelector('h1.video-title')?.textContent?.trim(),
                        desc: document.querySelector('.desc-info-text')?.textContent?.trim(),
                        pic: document.querySelector('meta[itemprop="image"]')?.content,
                        owner: {
                            name: document.querySelector('.up-name')?.textContent?.trim(),
                            face: document.querySelector('.up-avatar img')?.src,
                            mid: document.querySelector('.up-name')?.href?.match(/\d+/)?.[0]
                        }
                    };
                }
            }

            if (!videoData) {
                const bvid = location.pathname.match(/BV\w+/)?.[0];
                videoData = {
                    bvid: bvid,
                    title: document.title.replace(' - 哔哩哔哩', '').trim(),
                    pic: document.querySelector('meta[property="og:image"]')?.content,
                    desc: document.querySelector('meta[property="og:description"]')?.content,
                    owner: {
                        name: document.querySelector('.up-name')?.textContent?.trim(),
                        face: document.querySelector('.up-avatar img')?.src,
                        mid: document.querySelector('.up-name')?.href?.match(/\d+/)?.[0]
                    }
                };
            }

            if (!videoData || !videoData.bvid) {
                throw new Error('无法获取视频信息');
            }

            return {
                bvid: videoData.bvid,
                pic: videoData.pic || '',
                title: videoData.title || document.title,
                pubdate: videoData.pubdate,
                desc: videoData.desc || '',
                duration: videoData.duration,
                owner: {
                    mid: videoData.owner?.mid || '',
                    name: videoData.owner?.name || '未知用户',
                    face: videoData.owner?.face || ''
                },
                aid: videoData.aid,
                cid: videoData.cid || videoData.pages?.[0]?.cid
            };
        } catch (error) {
            console.error('获取视频信息失败:', error);
            // 添加更详细的错误信息
            console.log('当前页面URL:', location.href);
            console.log('window.__INITIAL_STATE__:', window.__INITIAL_STATE__);
            console.log('window.__playinfo__:', window.__playinfo__);
            throw error;
        }
    }

    async function getVideoUrl(aid, cid, quality) {
        const apiUrl = 'https://api.bilibili.com/x/player/playurl';
        const params = {
            otype: 'json',
            platform: 'html5',
            avid: aid,
            cid: cid,
            qn: quality || window.__playinfo__?.data?.accept_quality?.[0] || 80,
            fnver: 0,
            fnval: 4048,
            high_quality: window.__playinfo__?.data?.quality || 1
        };

        const queryString = Object.entries(params)
            .map(([key, value]) => `${key}=${value}`)
            .join('&');

        const response = await fetch(`${apiUrl}?${queryString}`, {
            credentials: 'include'
        });

        const data = await response.json();

        if (data.code !== 0) {
            throw new Error(data.message || '获取下载地址失败');
        }

        return data.data.durl[0].url;
    }

    async function parseVideoUrl(bvid, apiIndex = 0, usedQuality = null) {
        if (apiIndex >= PARSE_APIS.length) {
            throw new Error('所有解析接口都失败了');
        }

        try {
            const quality = usedQuality || window.__playinfo__?.data?.quality || 80;

            const apiUrl = `${PARSE_APIS[apiIndex]}?bv=${bvid}&q=${quality}`;


            const response = await fetch(apiUrl);
            const data = await response.json();

            if (!data.url && !data.data?.url) {
                if (quality !== 80) {
                    return parseVideoUrl(bvid, apiIndex, 80);
                }
                throw new Error('解析接口返回数据格式错误');
            }

            return {
                url: data.url || data.data.url,
                quality: quality
            };
        } catch (error) {
            return parseVideoUrl(bvid, apiIndex + 1, usedQuality);
        }
    }

    async function constructDownloadInfo() {
        try {
            const videoInfo = getBiliVideoInfo();

            let downloadUrl;
            let usedQuality;  // 添加变量记录使用的清晰度

            try {
                if (videoInfo.aid && videoInfo.cid) {
                    const quality = window.__playinfo__?.data?.accept_quality?.[0] || 80;
                    downloadUrl = await getVideoUrl(videoInfo.aid, videoInfo.cid, quality);
                    usedQuality = quality;
                }
            } catch (error) {
            }

            if (!downloadUrl) {
                const result = await parseVideoUrl(videoInfo.bvid, 0, window.__playinfo__?.data?.quality);
                downloadUrl = result.url;
                usedQuality = result.quality;
            }

            return {
                bvid: videoInfo.bvid,
                downloadUrl: downloadUrl,
                title: videoInfo.title,
                desc: videoInfo.desc,
                pic: videoInfo.pic,
                aid: videoInfo.aid,
                cid: videoInfo.cid,
                owner: videoInfo.owner,
                face: videoInfo.face,
                downloadUrl,
                usedQuality,  // 将清晰度信息添加到返回对象中
            };
        } catch (error) {
            throw error;
        }
    }

    async function startDownload() {
        try {
            const downloadInfo = await constructDownloadInfo();

            // 在控制台打印下载信息
            console.group('视频下载信息');
            console.log('标题:', downloadInfo.title);
            console.log('描述:', downloadInfo.desc);
            console.log('封面:', downloadInfo.pic);
            console.log('下载地址:', downloadInfo.downloadUrl);
            console.log('UP主:', downloadInfo.owner?.name);
            console.log('UP主头像:', downloadInfo.owner?.face);
            console.log('BV号:', downloadInfo.bvid);
            console.log('AV号:', downloadInfo.aid);
            console.log('CID:', downloadInfo.cid);

            console.group('清晰度信息');
            console.log('支持的清晰度列表:', window.__playinfo__?.data?.accept_quality?.map(qn => ({
                qn,
                desc: {
                    120: '4K',
                    116: '1080P60帧',
                    112: '1080P+高码率',
                    80: '1080P',
                    64: '720P',
                    32: '480P',
                    16: '360P'
            }[qn] || `未知(${qn})`
            })));
            console.log('当前播放清晰度:', window.__playinfo__?.data?.quality);

            if (downloadInfo.isOfficialApi) {
                console.log('下载使用的清晰度:', `${downloadInfo.usedQuality} (${
                    {
                        120: '4K',
                        116: '1080P60帧',
                        112: '1080P+高码率',
                        80: '1080P',
                        64: '720P',
                        32: '480P',
                        16: '360P'
                    }[downloadInfo.usedQuality] || '未知清晰度'
                })`);
                console.log('使用接口: 官方API');
            } else {
                console.log('下载使用的清晰度:', `${downloadInfo.usedQuality} (${
                    {
                        120: '4K',
                        116: '1080P60帧',
                        112: '1080P+高码率',
                        80: '1080P',
                        64: '720P',
                        32: '480P',
                        16: '360P'
                    }[downloadInfo.usedQuality] || '未知清晰度'
                })`);
                console.log('使用接口: 第三方接口');
                console.log('提示: 如需更高清晰度,建议登录后使用官方API下载');
            }
            console.groupEnd();

            console.groupEnd();

            const params = new URLSearchParams();
            params.append('title', downloadInfo.title || '');
            params.append('desc', downloadInfo.desc || '');
            params.append('pic', downloadInfo.pic || '');
            params.append('downloadUrl', downloadInfo.downloadUrl);
            params.append('owner', downloadInfo.owner?.name || '');
            params.append('face', downloadInfo.owner?.face || '');

            const baseUrl = 'https://saveany.cn/get_video_info';
            const finalUrl = `${baseUrl}?${params.toString()}`;

            console.log('最终请求URL:', finalUrl);

            const downloadWindow = window.open(finalUrl, '_blank');
            if (downloadWindow) {
                downloadWindow.focus();
            }
        } catch (error) {
            console.error('下载失败:', error);
            alert('下载失败: ' + error.message);
        }
    }

    function addDownloadButton() {
        const targetArea = document.querySelector("#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-sending-area > div");

        if (targetArea && !targetArea.querySelector('.download-btn')) {
            const downloadBtn = createDownloadButton();
            downloadBtn.classList.add('download-btn');
            targetArea.appendChild(downloadBtn);
        }
    }

    function observeDOM() {
        const targetNode = document.body;
        const config = { childList: true, subtree: true };
        const observer = new MutationObserver((mutationsList, observer) => {
            for(let mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    addDownloadButton();
                }
            }
        });
        observer.observe(targetNode, config);
    }

    window.addEventListener('load', () => {
        addDownloadButton();
        observeDOM();
    });

    setInterval(addDownloadButton, 5000);
})();