bilibili 字幕下载器

Download subtitles from Bilibili videos using the AI assistant feature and clicking the subtitle list

// ==UserScript==
// @name         bilibili 字幕下载器
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Download subtitles from Bilibili videos using the AI assistant feature and clicking the subtitle list
// @author       Claude
// @match        https://www.bilibili.com/video/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Wait for the video page to fully load before adding our button
    window.addEventListener('load', function() {
        // Add a small delay to ensure all elements are loaded
        setTimeout(addDownloadButton, 2000);
    });

    // Function to add our download button to the page
    function addDownloadButton() {
        // Check if our button already exists to avoid duplicates
        if (document.querySelector('#subtitle-download-container')) {
            return;
        }

        // Create a floating button container
        const downloadContainer = document.createElement('div');
        downloadContainer.id = 'subtitle-download-container';
        downloadContainer.style.position = 'fixed';
        downloadContainer.style.left = '0';
        downloadContainer.style.top = '50%';
        downloadContainer.style.transform = 'translateY(-50%)';
        downloadContainer.style.backgroundColor = 'rgba(251, 114, 153, 0.7)'; // Bilibili pink with transparency
        downloadContainer.style.color = 'white';
        downloadContainer.style.padding = '5px 8px'; // 50% of original padding
        downloadContainer.style.borderRadius = '0 4px 4px 0';
        downloadContainer.style.cursor = 'pointer';
        downloadContainer.style.zIndex = '999';
        downloadContainer.style.display = 'flex';
        downloadContainer.style.alignItems = 'center';
        downloadContainer.style.boxShadow = '2px 2px 10px rgba(0, 0, 0, 0.2)';
        downloadContainer.style.transition = 'all 0.3s ease';
        downloadContainer.style.fontSize = '12px'; // Smaller font size

        // Create the icon element
        const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        iconSvg.setAttribute('width', '10'); // 50% of original size
        iconSvg.setAttribute('height', '10'); // 50% of original size
        iconSvg.setAttribute('viewBox', '0 0 24 24');
        iconSvg.setAttribute('fill', 'none');
        iconSvg.style.marginRight = '4px'; // 50% of original margin
        iconSvg.id = 'subtitle-download-icon';

        // Add SVG content for a download icon
        iconSvg.innerHTML = `
            <path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C12.5523 4 13 4.44772 13 5V13.5858L15.2929 11.2929C15.6834 10.9024 16.3166 10.9024 16.7071 11.2929C17.0976 11.6834 17.0976 12.3166 16.7071 12.7071L12.7071 16.7071C12.3166 17.0976 11.6834 17.0976 11.2929 16.7071L7.29289 12.7071C6.90237 12.3166 6.90237 11.6834 7.29289 11.2929C7.68342 10.9024 8.31658 10.9024 8.70711 11.2929L11 13.5858V5C11 4.44772 11.4477 4 12 4ZM4 14C4.55228 14 5 14.4477 5 15V17C5 17.5523 5.44772 18 6 18H18C18.5523 18 19 17.5523 19 17V15C19 14.4477 19.4477 14 20 14C20.5523 14 21 14.4477 21 15V17C21 18.6569 19.6569 20 18 20H6C4.34315 20 3 18.6569 3 17V15C3 14.4477 3.44772 14 4 14Z" fill="white"/>
        `;

        // Add the icon to the container
        downloadContainer.appendChild(iconSvg);

        // Add text label
        const textLabel = document.createElement('span');
        textLabel.textContent = '下载字幕';
        textLabel.style.fontSize = '12px'; // 50% of original font size
        textLabel.id = 'subtitle-download-text';
        downloadContainer.appendChild(textLabel);

        // Add click event to the container
        downloadContainer.addEventListener('click', extractAndDownloadSubtitles);

        // Add the button to the body
        document.body.appendChild(downloadContainer);

        console.log('Subtitle download button added successfully');
    }

    // Function to extract and download subtitles
    function extractAndDownloadSubtitles() {
        console.log('Extracting subtitles...');

        // Show a loading indicator
        const downloadContainer = document.querySelector('#subtitle-download-container');
        const textLabel = document.querySelector('#subtitle-download-text');
        const originalText = textLabel.textContent;
        textLabel.textContent = '下载中...';
        
        // Store original color and set loading color
        const originalColor = downloadContainer.style.backgroundColor;
        downloadContainer.style.backgroundColor = 'rgba(251, 114, 153, 0.9)'; // More opaque while loading

        // Create loading animation - small dot that pulses
        const loadingDot = document.createElement('span');
        loadingDot.textContent = ' •';
        loadingDot.style.animation = 'pulse 1s infinite';
        loadingDot.id = 'loading-dot';
        textLabel.appendChild(loadingDot);

        // Add the keyframe animation to the document
        const style = document.createElement('style');
        style.innerHTML = `
            @keyframes pulse {
                0% { opacity: 0.2; }
                50% { opacity: 1; }
                100% { opacity: 0.2; }
            }
        `;
        document.head.appendChild(style);

        // Find and click the AI assistant button to open the panel
        const aiAssistantContainer = document.querySelector('.video-ai-assistant');
        if (aiAssistantContainer) {
            aiAssistantContainer.click(); // Open the AI assistant panel

            // Wait for AI assistant panel to load
            setTimeout(() => {
                try {
                    // Find and click the subtitle list button
                    const subtitleListButton = findSubtitleListButton();
                    if (!subtitleListButton) {
                        alert('无法找到"字幕列表"按钮,请确保AI小助手面板已正确加载');
                        downloadContainer.style.backgroundColor = originalColor;
                        textLabel.textContent = originalText;
                        
                        // Remove loading dot
                        const loadingDot = document.querySelector('#loading-dot');
                        if (loadingDot) loadingDot.remove();
                        
                        // Close the AI panel
                        const closeButton = document.querySelector('.close-btn');
                        if (closeButton) closeButton.click();
                        return;
                    }

                    console.log('找到字幕列表按钮,点击中...');
                    subtitleListButton.click();

                    // Wait for subtitles to load after clicking the subtitle list button
                    setTimeout(() => {
                        try {
                            // Find all subtitle text spans
                            const subtitleItems = document.querySelectorAll('._Text_1iu0q_64');
                            if (!subtitleItems || subtitleItems.length === 0) {
                                // Try alternative selectors
                                console.log('尝试使用备用选择器查找字幕...');
                                downloadSubtitlesWithAlternativeSelectors(originalColor, downloadContainer, originalText);
                                return;
                            }

                            // Extract timestamps and subtitle text
                            let subtitles = [];
                            document.querySelectorAll('._Part_1iu0q_16').forEach(part => {
                                const timeElem = part.querySelector('._TimeText_1iu0q_35');
                                const textElem = part.querySelector('._Text_1iu0q_64');

                                if (timeElem && textElem) {
                                    subtitles.push(`${timeElem.textContent}: ${textElem.textContent}`);
                                }
                            });

                            // If no subtitles found, try alternative selectors
                            if (subtitles.length === 0) {
                                console.log('通过主选择器未找到字幕,尝试备用选择器...');
                                downloadSubtitlesWithAlternativeSelectors(originalColor, downloadContainer, originalText);
                                return;
                            }

                            // Save the subtitles to file
                            saveSubtitlesToFile(subtitles, originalColor, downloadContainer, originalText);

                        } catch (error) {
                            console.error('提取字幕时出错:', error);
                            downloadContainer.style.backgroundColor = originalColor;
                            textLabel.textContent = originalText;
                            
                            // Remove loading dot
                            const loadingDot = document.querySelector('#loading-dot');
                            if (loadingDot) loadingDot.remove();

                            // Close the AI panel
                            const closeButton = document.querySelector('.close-btn');
                            if (closeButton) closeButton.click();
                        }
                    }, 2000); // Wait 2 seconds for subtitles to load after clicking subtitle list
                } catch (error) {
                    console.error('点击字幕列表按钮时出错:', error);
                    downloadContainer.style.backgroundColor = originalColor;
                    textLabel.textContent = originalText;
                    
                    // Remove loading dot
                    const loadingDot = document.querySelector('#loading-dot');
                    if (loadingDot) loadingDot.remove();

                    // Close the AI panel
                    const closeButton = document.querySelector('.close-btn');
                    if (closeButton) closeButton.click();
                }
            }, 2000); // Wait 2 seconds for AI assistant panel to load
        } else {
            alert('无法找到AI小助手按钮,请确保您在Bilibili视频页面上');
            downloadContainer.style.backgroundColor = originalColor;
            textLabel.textContent = originalText;
            
            // Remove loading dot
            const loadingDot = document.querySelector('#loading-dot');
            if (loadingDot) loadingDot.remove();
        }
    }

    // Function to find the subtitle list button
    function findSubtitleListButton() {
        // First try with the class mentioned in the user's message
        const buttonByClass = document.querySelector('span._Label_krx6h_18');
        if (buttonByClass && buttonByClass.textContent === '字幕列表') {
            return buttonByClass;
        }

        // Try with general selectors and text content
        const allLabels = [
            ...document.querySelectorAll('span[class*="Label"]'),
            ...document.querySelectorAll('div[class*="Label"]'),
            ...document.querySelectorAll('button[class*="Label"]'),
            ...document.querySelectorAll('span'),
            ...document.querySelectorAll('button')
        ];

        for (const element of allLabels) {
            if (element.textContent.includes('字幕列表')) {
                return element;
            }
        }

        // As a last resort, try to find elements with certain classes that might contain the subtitle list button
        const panelElements = document.querySelectorAll('[class*="panel"], [class*="container"], [class*="ai"]');
        for (const panel of panelElements) {
            const children = panel.querySelectorAll('*');
            for (const child of children) {
                if (child.textContent === '字幕列表') {
                    return child;
                }
            }
        }

        return null;
    }

    // Function to try alternative selectors for finding subtitles
    function downloadSubtitlesWithAlternativeSelectors(originalColor, downloadContainer, originalText) {
        console.log('使用备用选择器提取字幕...');
        let subtitles = [];

        // Try different selectors that might contain subtitle content
        // Method 1: Look for elements with time-like text and adjacent text
        document.querySelectorAll('[class*="time"], [class*="Time"]').forEach(timeElem => {
            // Check if it has time format (00:00)
            if (/^\d+:\d+$/.test(timeElem.textContent.trim())) {
                // Find the closest text element (usually a sibling or parent's child)
                let textElem = timeElem.nextElementSibling;
                if (textElem) {
                    subtitles.push(`${timeElem.textContent}: ${textElem.textContent}`);
                }
            }
        });

        // Method 2: Look for container elements that might have both time and text
        document.querySelectorAll('[class*="subtitle"], [class*="Subtitle"], [class*="Part"], [class*="part"], [class*="Line"], [class*="line"]').forEach(container => {
            const children = container.children;
            if (children.length >= 2) {
                const firstChild = children[0];
                const secondChild = children[1];
                
                // Check if first child might be a timestamp
                if (firstChild && /^\d+:\d+$/.test(firstChild.textContent.trim())) {
                    subtitles.push(`${firstChild.textContent}: ${secondChild.textContent}`);
                }
            }
        });

        // Method 3: Look at all spans for timestamp-like content
        const allSpans = document.querySelectorAll('span');
        for (let i = 0; i < allSpans.length; i++) {
            const span = allSpans[i];
            if (/^\d+:\d+$/.test(span.textContent.trim()) && allSpans[i+1]) {
                subtitles.push(`${span.textContent}: ${allSpans[i+1].textContent}`);
            }
        }

        // Check if we found any subtitles
        if (subtitles.length === 0) {
            // Last resort: grab any text that might be subtitle content
            const allText = document.querySelectorAll('[class*="text"], [class*="Text"], [class*="content"], [class*="Content"]');
            allText.forEach(elem => {
                if (elem.textContent.length > 0 && !subtitles.includes(elem.textContent)) {
                    subtitles.push(elem.textContent);
                }
            });
        }

        // If still no subtitles found
        if (subtitles.length === 0) {
            alert('无法提取字幕,请尝试刷新页面后重试');
            downloadContainer.style.backgroundColor = originalColor;
            
            // Update button text back to original
            const textLabel = document.querySelector('#subtitle-download-text');
            if (textLabel) textLabel.textContent = originalText;
            
            // Remove loading dot
            const loadingDot = document.querySelector('#loading-dot');
            if (loadingDot) loadingDot.remove();
            
            // Close the AI panel
            const closeButton = document.querySelector('.close-btn');
            if (closeButton) closeButton.click();
            
            return;
        }

        // Save the subtitles to file
        saveSubtitlesToFile(subtitles, originalColor, downloadContainer, originalText);
    }

    // Function to save subtitles to a file
    function saveSubtitlesToFile(subtitles, originalColor, downloadContainer, originalText) {
        try {
            // Remove duplicates
            subtitles = [...new Set(subtitles)];

            // Create the subtitle content
            const subtitleContent = subtitles.join('\n');
            
            // Copy to clipboard
            navigator.clipboard.writeText(subtitleContent).then(function() {
                // Get the position of the download button
                const buttonRect = downloadContainer.getBoundingClientRect();
                
                // Update button text back to original
                const textLabel = document.querySelector('#subtitle-download-text');
                if (textLabel) textLabel.textContent = originalText;
                
                // Remove loading dot
                const loadingDot = document.querySelector('#loading-dot');
                if (loadingDot) loadingDot.remove();
                
                // Create and show a temporary notification
                const notification = document.createElement('div');
                notification.textContent = '字幕已复制到剪贴板';
                notification.style.position = 'fixed';
                notification.style.top = `${buttonRect.top}px`;
                notification.style.left = `${buttonRect.right + 10}px`; // 10px to the right of the button
                notification.style.padding = '5px 10px';
                notification.style.backgroundColor = '#fb7299'; // Bilibili pink color
                notification.style.color = 'white';
                notification.style.borderRadius = '4px';
                notification.style.zIndex = '9999';
                notification.style.fontSize = '12px';
                notification.style.boxShadow = '2px 2px 10px rgba(0, 0, 0, 0.2)';
                notification.style.whiteSpace = 'nowrap';
                
                document.body.appendChild(notification);
                
                // Remove the notification after 1.5 seconds
                setTimeout(() => {
                    document.body.removeChild(notification);
                }, 1500);
                
                console.log(`成功复制了 ${subtitles.length} 行字幕到剪贴板`);
            }).catch(function(error) {
                console.error('复制到剪贴板时出错:', error);
                alert('复制到剪贴板失败,请检查浏览器权限');
                
                // Update button text back to original
                const textLabel = document.querySelector('#subtitle-download-text');
                if (textLabel) textLabel.textContent = originalText;
                
                // Remove loading dot
                const loadingDot = document.querySelector('#loading-dot');
                if (loadingDot) loadingDot.remove();
            }).finally(function() {
                // Close the AI panel
                const closeButton = document.querySelector('.close-btn');
                if (closeButton) closeButton.click();
                
                // Reset button color
                downloadContainer.style.backgroundColor = originalColor;
            });
        } catch (error) {
            console.error('处理字幕时出错:', error);
            downloadContainer.style.backgroundColor = originalColor;
            
            // Update button text back to original
            const textLabel = document.querySelector('#subtitle-download-text');
            if (textLabel) textLabel.textContent = originalText;
            
            // Remove loading dot
            const loadingDot = document.querySelector('#loading-dot');
            if (loadingDot) loadingDot.remove();
            
            // Close the AI panel
            const closeButton = document.querySelector('.close-btn');
            if (closeButton) closeButton.click();
        }
    }
})();