// ==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 // @downloadURL https://update.greasyfork.icu/scripts/533053/bilibili%20%E5%AD%97%E5%B9%95%E4%B8%8B%E8%BD%BD%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/533053/bilibili%20%E5%AD%97%E5%B9%95%E4%B8%8B%E8%BD%BD%E5%99%A8.meta.js // ==/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 = ` `; // 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(); } } })();