哔哩哔哩国际版(B-Global)字幕下载

下载bilibili国际版字幕;注意:切勿频繁请求,可能会造成风控!

// ==UserScript==
// @name        哔哩哔哩国际版(B-Global)字幕下载
// @namespace   https://github.com/AreCie
// @version     1.2
// @description 下载bilibili国际版字幕;注意:切勿频繁请求,可能会造成风控!
// @license     AGPL-3.0-or-later
// @homepage    https://github.com/AreCie/B-Global-SubtitleDown
// @supportURL  https://github.com/AreCie/B-Global-SubtitleDown/issues
// @author      AreCie
// @match       https://www.bilibili.tv/*play/*
// @icon        https://p.bstarstatic.com/fe-static/deps/bilibili_tv.ico?v=1
// @require     https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @grant       none
// ==/UserScript==

(() => {
    'use strict';

    function applyStyles(element, styles) {
        Object.assign(element.style, styles);
    }

    const buttonStyles = {
        padding: '0 8px',
        height: '32px',
        backgroundColor: '#4C93FF',
        color: 'white',
        fontSize: 'inherit',
        fontWeight: '700',
        border: 'none',
        borderRadius: '5px',
        marginLeft: '10px',
        cursor: 'pointer'
    };

    const getSubBtn = document.createElement('button');
    getSubBtn.id = 'getSubBtn';
    getSubBtn.innerText = '获取字幕';
    applyStyles(getSubBtn, buttonStyles);

    const getAllSubBtn = document.createElement('button');
    getAllSubBtn.id = 'getAllSubBtn';
    getAllSubBtn.innerText = '获取全部字幕';
    applyStyles(getAllSubBtn, buttonStyles);

    const popupContainer = document.createElement("div");
    applyStyles(popupContainer, {
        position: "fixed",
        top: "50%",
        left: "50%",
        transform: "translate(-50%, -50%)",
        padding: "50px 10px 20px 10px",
        backgroundColor: "#ffffff",
        border: "1px solid #ccc",
        borderRadius: "8px",
        boxShadow: "0 4px 8px rgba(0, 0, 0, 0.2)",
        width: "380px",
        zIndex: "9999",
        cursor: "move"
    });

    const popupChildContainer = document.createElement("div");
    applyStyles(popupChildContainer, {
        maxHeight: "50vh",
        overflowY: "auto"
    });

    let isDragging = false;
    let offsetX, offsetY;

    popupContainer.addEventListener("mousedown", e => {
        isDragging = true;
        offsetX = e.clientX - popupContainer.getBoundingClientRect().left;
        offsetY = e.clientY - popupContainer.getBoundingClientRect().top;
        popupContainer.style.cursor = "grabbing";
    });

    document.addEventListener("mousemove", e => {
        if (isDragging) {
            popupContainer.style.left = `${e.clientX - offsetX}px`;
            popupContainer.style.top = `${e.clientY - offsetY}px`;
            popupContainer.style.transform = "none";
        }
    });

    document.addEventListener("mouseup", () => {
        isDragging = false;
        popupContainer.style.cursor = "move";
    });

    const closeButton = document.createElement("div");
    applyStyles(closeButton, {
        position: "absolute",
        top: "8px",
        right: "8px",
        width: "25px",
        height: "25px",
        borderRadius: "50%",
        backgroundColor: "#ff5e5e",
        cursor: "pointer",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        color: "#fff"
    });
    closeButton.textContent = "×";
    closeButton.onclick = () => document.body.removeChild(popupContainer);

    function insertButton() {
        const appendBaseBtn = document.querySelector('.interactive__btn.interactive__fav');
        if (appendBaseBtn && !document.querySelector("#getSubBtn")) {
            appendBaseBtn.insertAdjacentElement('afterend', getAllSubBtn);
            appendBaseBtn.insertAdjacentElement('afterend', getSubBtn);
        }
    }

    insertButton();

    const observer = new MutationObserver(insertButton);
    observer.observe(document.body, { childList: true, subtree: true });

    // 将 JSON 字幕转换为 SRT 格式
    function convertJsonToSrt(jsonContent) {
        // 将秒转换为 SRT 时间格式 (hh:mm:ss,ms)
        function secondsToSrtTime(seconds) {
            const hours = Math.floor(seconds / 3600).toString().padStart(2, '0');
            const minutes = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
            const secs = Math.floor(seconds % 60).toString().padStart(2, '0');
            const millis = Math.round((seconds % 1) * 1000).toString().padStart(3, '0');
            return `${hours}:${minutes}:${secs},${millis}`;
        }

        const subtitles = jsonContent.body;
        let srtContent = "";

        subtitles.forEach((subtitle, index) => {
            const startTime = secondsToSrtTime(subtitle.from);
            const endTime = secondsToSrtTime(subtitle.to);
            srtContent += `${index + 1}\n`; // 编号
            srtContent += `${startTime} --> ${endTime}\n`; // 时间
            srtContent += `${subtitle.content}\n\n`; // 内容和换行
        });

        return srtContent.trim(); // 去掉末尾的多余换行
    }

    function processSrtFile(blob, filename) {
        const reader = new FileReader();
        reader.onload = function () {
            try {
                const jsonContent = JSON.parse(reader.result);
                const srtContent = convertJsonToSrt(jsonContent);
                const srtBlob = new Blob([srtContent], { type: 'text/plain' });
                triggerDownload(srtBlob, filename);
            } catch (error) {
                console.error('解析或转换失败:', error);
            }
        };
        reader.readAsText(blob);
    }

    function triggerDownload(blob, filename) {
        const downloadUrl = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = downloadUrl;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(downloadUrl);
    }

    function downloadFile(url, filename) {
        fetch(url)
            .then(response => response.blob())
            .then(blob => {
                if (filename.toLowerCase().endsWith('.srt')) {
                    processSrtFile(blob, filename);
                } else {
                    triggerDownload(blob, filename);
                }
            })
            .catch(error => console.error('下载失败:', error));
    }

    const langDict = {
        'zh-Hans': '简体中文',
        'id': '印尼语',
        'th': '泰语',
        'en': '英语',
        'vi': '越南语'
    };

    function addSubItem(subs) {
        const urlWithoutParams = window.location.origin + window.location.pathname;
        const match = urlWithoutParams.match(/\/play\/(\d+)/);
        let anime_id = match ? match[1] : null;

        popupContainer.innerHTML = "";
        popupChildContainer.innerHTML = "";
        popupContainer.appendChild(closeButton);

        let downs = [];

        subs.data.forEach(item => {
            if (subs.type === 1) {
                const itemDiv = document.createElement("div");
                applyStyles(itemDiv, {
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "space-between",
                    padding: "5px 10px"
                });

                itemDiv.className = "sub-item";
                itemDiv.innerHTML = '';

                // const checkElem = document.createElement('input');
                // checkElem.type = "checkbox"
                // itemDiv.appendChild(checkElem);
                const langSpan = document.createElement('span');
                langSpan.style.width = '120px';
                langSpan.textContent = item.lang;
                itemDiv.appendChild(langSpan);

                if (item.srt) {
                    const srtButton = document.createElement('button');
                    srtButton.textContent = '下载SRT';
                    srtButton.style.backgroundColor = '#4CAF50';
                    srtButton.style.marginLeft = 'auto';
                    srtButton.style.color = 'white';
                    srtButton.style.padding = '5px 10px';
                    srtButton.style.cursor = 'pointer';
                    srtButton.style.borderRadius = '4px';
                    srtButton.onclick = () => downloadFile(item.srt, `${item.lang}.srt`);
                    itemDiv.appendChild(srtButton);
                }

                if (item.ass) {
                    const assButton = document.createElement('button');
                    assButton.textContent = '下载ASS';
                    assButton.style.backgroundColor = '#4C93FF';
                    assButton.style.marginLeft = '10px';
                    assButton.style.color = 'white';
                    assButton.style.padding = '5px 10px';
                    assButton.style.cursor = 'pointer';
                    assButton.style.borderRadius = '4px';
                    assButton.onclick = () => downloadFile(item.ass, `${item.lang}.ass`);
                    itemDiv.appendChild(assButton);
                }

                popupChildContainer.appendChild(itemDiv);
            } else {
                const epDiv = document.createElement("div");
                epDiv.style.border = "1px solid #ccc";
                epDiv.style.borderRadius = "4px";
                epDiv.style.width = "100%";
                epDiv.style.margin = "10px 0";
                epDiv.style.padding = "5px";

                const epSpan = document.createElement("span");
                epSpan.textContent = item.title;
                epDiv.appendChild(epSpan);

                item.ep_subs.forEach(s => {
                    const itemDiv = document.createElement("div");
                    applyStyles(itemDiv, {
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "space-between",
                        padding: "5px 0"
                    });
                    itemDiv.className = "sub-item";

                    const langSpan = document.createElement('span');
                    langSpan.style.width = '120px';
                    langSpan.textContent = s.lang;
                    itemDiv.appendChild(langSpan);

                    if (s.srt) {
                        downs.push({
                            title: `${item.title}_${s.lang}.srt`,
                            url: s.srt
                        });

                        const srtButton = document.createElement('button');
                        srtButton.textContent = '下载SRT';
                        srtButton.style.backgroundColor = '#4CAF50';
                        srtButton.style.marginLeft = 'auto';
                        srtButton.style.color = 'white';
                        srtButton.style.padding = '5px 10px';
                        srtButton.style.cursor = 'pointer';
                        srtButton.style.borderRadius = '4px';
                        srtButton.onclick = () => downloadFile(s.srt, `${item.title}_${s.lang}.srt`);
                        itemDiv.appendChild(srtButton);
                    }

                    if (s.ass) {
                        downs.push({
                            title: `${item.title}_${s.lang}.ass`,
                            url: s.ass
                        });

                        const assButton = document.createElement('button');
                        assButton.textContent = '下载ASS';
                        assButton.style.backgroundColor = '#4C93FF';
                        assButton.style.marginLeft = '10px';
                        assButton.style.color = 'white';
                        assButton.style.padding = '5px 10px';
                        assButton.style.cursor = 'pointer';
                        assButton.style.borderRadius = '4px';
                        assButton.onclick = () => downloadFile(s.ass, `${item.title}_${s.lang}.ass`);
                        itemDiv.appendChild(assButton);
                    }

                    epDiv.appendChild(itemDiv);
                });
                popupChildContainer.appendChild(epDiv);
            }
        });

        if (subs.type === 2) {
            const batchDownBtn = document.createElement("button");
            batchDownBtn.textContent = "下载全部";

            Object.assign(batchDownBtn.style, {
                position: "absolute",
                top: "8px",
                left: "8px",
                width: "130px",
                height: "30px",
                borderRadius: "4px",
                background: "#4C93FF",
                cursor: "pointer",
                display: "flex",
                justifyContent: "center",
                alignItems: "center",
                color: "white",
            });

            batchDownBtn.onclick = async () => {
                batchDownBtn.textContent = "请稍等...";
                const zip = new JSZip();

                const filePromises = downs.map(d =>
                    fetch(d.url)
                        .then(response => response.blob())
                        .then(blob => {
                            if (d.title.toLowerCase().endsWith('.srt')) {
                                const reader = new FileReader();
                                reader.onload = function () {
                                    try {
                                        const jsonContent = JSON.parse(reader.result);
                                        const srtContent = convertJsonToSrt(jsonContent);
                                        const srtBlob = new Blob([srtContent], { type: 'text/plain' });
                                        zip.file(d.title, srtBlob);
                                    } catch (error) {
                                        console.error('解析或转换失败:', error);
                                    }
                                };
                                reader.readAsText(blob);
                            } else {
                                zip.file(d.title, blob);
                            }
                        })
                );

                await Promise.all(filePromises);

                // 生成 ZIP 文件并下载
                zip.generateAsync({ type: "blob" }).then(content => {
                    const downloadUrl = URL.createObjectURL(content);
                    const a = document.createElement('a');
                    a.href = downloadUrl;
                    a.download = `${anime_id}_subs.zip`;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(downloadUrl);
                }).then(() => batchDownBtn.textContent = "下载全部");
            };


            popupContainer.appendChild(batchDownBtn);
        }


        popupContainer.appendChild(popupChildContainer);
        document.body.appendChild(popupContainer);
    }

    getSubBtn.addEventListener('click', () => {
        const urlWithoutParams = window.location.origin + window.location.pathname;
        const match = urlWithoutParams.match(/\/play\/\d+\/(\d+)/);
        let ep_id = match ? match[1] : null;

        if (!ep_id) {
            const firspEp = document.querySelector('.ep-list').firstElementChild;
            ep_id = firspEp.getAttribute('href').match(/\/play\/\d+\/(\d+)/)[1];
        }
        getSubBtn.innerText = "请稍等...";
        fetch(`https://api.bilibili.tv/intl/gateway/web/v2/subtitle?s_locale=en_US&platform=web&episode_id=${ep_id}`)
            .then(resp => resp.json())
            .then(data => {
                const subs = {
                    type: 1, data: data.data.video_subtitle.map(subtitle => ({
                        lang: langDict[subtitle.lang_key] ? langDict[subtitle.lang_key] : subtitle.lang,
                        srt: subtitle.srt ? subtitle.srt.url : null,
                        ass: subtitle.ass ? subtitle.ass.url : null
                    }))
                };
                addSubItem(subs);
                getSubBtn.innerText = "获取字幕";
            });
    });

    getAllSubBtn.addEventListener('click', () => {
        getAllSubBtn.innerText = "请稍等...";
        const epListDiv = document.querySelector('.ep-list');
        const links = epListDiv.querySelectorAll('a[href]');
        const subs = { type: 2, data: [] };

        Promise.all([...links].map(link => {
            const hrefMatch = link.getAttribute('href').match(/\/play\/\d+\/(\d+)/);
            if (hrefMatch) {
                const episodeId = hrefMatch[1];
                return fetch(`https://api.bilibili.tv/intl/gateway/web/v2/subtitle?s_locale=en_US&platform=web&episode_id=${episodeId}`)
                    .then(resp => resp.json())
                    .then(data => {
                        subs.data.push({
                            title: link.innerText,
                            ep_subs: data.data.video_subtitle.map(subtitle => ({
                                lang: langDict[subtitle.lang_key] ? langDict[subtitle.lang_key] : subtitle.lang,
                                srt: subtitle.srt ? subtitle.srt.url : null,
                                ass: subtitle.ass ? subtitle.ass.url : null
                            }))
                        });
                    });
            }
        })).then(() => {
            subs.data.sort((a, b) => {
                const aNum = a.title.match(/^E(\d+)$/)?.[1];
                const bNum = b.title.match(/^E(\d+)$/)?.[1];

                return aNum && bNum
                    ? aNum - bNum
                    : aNum
                        ? -1
                        : bNum
                            ? 1
                            : a.title.localeCompare(b.title);
            });

            addSubItem(subs);
            getAllSubBtn.innerText = "获取全部字幕";
        });
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址