// ==UserScript== // @name Peacock字幕保存器 (Peacock Subtitle Saver) // @namespace https://129899.xyz // @version 0.3 // @description Peacock字幕保存器是一款专为Peacock TV设计的用户脚本,可自动记录视频播放中的字幕并保存为.txt文件,方便后续使用。| Peacock Subtitle Saver is a user script for Peacock TV that automatically records subtitles during playback and saves them as .txt files for future use. // @author aka1298 // @match https://www.peacocktv.com/watch/playback* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; class SubtitleSaver { constructor() { this.savedSubtitles = []; this.recordStatus = false; this.lastSavedSubtitle = ''; this.subtitleContainerClass = 'video-player__subtitles'; this.buttonGroup = null; this.startButton = null; this.autoScrollStatus = true; this.previewPanel = null; this.autoScrollButton = null; this.startTime = null; this.lastSubtitleTime = null; this.downloadSubtitles = this.downloadSubtitles.bind(this); this.toggleRecording = this.toggleRecording.bind(this); this.clearSubtitles = this.clearSubtitles.bind(this); this.subtitleObserverCallback = this.subtitleObserverCallback.bind(this); this.toggleAutoScroll = this.toggleAutoScroll.bind(this); this.updatePreviewPanel = this.updatePreviewPanel.bind(this); this.createUI(); this.initializeObserver(); this.createPreviewPanel(); } static get BUTTON_STYLE() { return ` padding: 8px 16px; font-size: 14px; font-weight: bold; color: white; border: none; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); cursor: pointer; transition: all 0.2s ease-in-out; margin-right: 5px; `; } static get PREVIEW_PANEL_STYLE() { return ` position: fixed; top: 5%; right: 20px; width: 300px; max-height: 400px; background-color: rgba(0, 0, 0, 0.8); color: white; padding: 15px; border-radius: 8px; overflow-y: auto; z-index: 10000; font-size: 14px; line-height: 1.5; `; } createButton(text, baseColor, hoverColor, clickHandler) { const button = document.createElement('button'); button.textContent = text; button.style.cssText = SubtitleSaver.BUTTON_STYLE + `background-color: ${baseColor};`; button.onmouseover = () => { button.style.backgroundColor = hoverColor; button.style.transform = 'scale(1.05)'; }; button.onmouseout = () => { button.style.backgroundColor = baseColor; button.style.transform = 'scale(1)'; }; button.onclick = clickHandler; return button; } createPreviewPanel() { this.previewPanel = document.createElement('div'); this.previewPanel.style.cssText = SubtitleSaver.PREVIEW_PANEL_STYLE; this.previewPanel.style.display = 'none'; document.body.appendChild(this.previewPanel); } updatePreviewPanel() { if (!this.autoScrollStatus) { this.previewPanel.style.display = 'none'; return; } this.previewPanel.style.display = 'block'; const lastSubtitles = this.savedSubtitles.slice(-5); this.previewPanel.innerHTML = `
已记录 ${this.savedSubtitles.length} 条字幕
${lastSubtitles.map((subtitle, index) => `
${subtitle}
`).join('')} `; // 只在自动滚动开启时才滚动到底部 if (this.autoScrollStatus) { requestAnimationFrame(() => { this.previewPanel.scrollTop = this.previewPanel.scrollHeight; }); } } createUI() { this.buttonGroup = document.createElement('div'); Object.assign(this.buttonGroup.style, { position: 'fixed', top: '5%', left: '50%', transform: 'translateX(-50%)', zIndex: '10000', display: 'flex', gap: '10px' }); this.startButton = this.createButton( '开始记录', '#4CAF50', '#367c39', this.toggleRecording ); const clearButton = this.createButton( '停止并清除', '#ff9800', '#cc7a00', this.clearSubtitles ); const downloadButton = this.createButton( '下载', '#f44336', '#c8352e', this.downloadSubtitles ); const autoScrollButton = this.createButton( '自动滚动: 开', '#2196F3', '#1976D2', this.toggleAutoScroll ); this.autoScrollButton = autoScrollButton; this.buttonGroup.append(this.startButton, clearButton, downloadButton, autoScrollButton); document.body.appendChild(this.buttonGroup); } toggleAutoScroll() { this.autoScrollStatus = !this.autoScrollStatus; this.autoScrollButton.textContent = `自动滚动: ${this.autoScrollStatus ? '开' : '关'}`; this.previewPanel.style.display = !this.autoScrollStatus ? 'none' : 'block'; } toggleRecording() { this.recordStatus = !this.recordStatus; this.startButton.textContent = this.recordStatus ? '暂停' : '开始记录'; if (this.recordStatus) { this.startTime = new Date(); this.startButton.style.backgroundColor = '#ff4444'; } else { this.startTime = null; this.startButton.style.backgroundColor = '#4CAF50'; } this.updatePreviewPanel(); } clearSubtitles() { this.recordStatus = false; this.startButton.textContent = '开始记录'; this.startButton.style.backgroundColor = '#4CAF50'; this.savedSubtitles = []; this.lastSavedSubtitle = ''; this.startTime = null; this.lastSubtitleTime = null; this.updatePreviewPanel(); console.log('已清除保存的字幕并重置变量。'); } downloadSubtitles() { if (this.savedSubtitles.length === 0) { alert('没有可下载的字幕!'); return; } const timestamp = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(/[/:]/g, '-'); const metadata = [ '==================', `记录时间: ${timestamp}`, `字幕数量: ${this.savedSubtitles.length}`, '==================\n' ]; const content = [...metadata, ...this.savedSubtitles]; const blob = new Blob([content.join('\n')], { type: 'text/plain;charset=utf-8' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `peacock_subtitles_${timestamp}.txt`; link.click(); URL.revokeObjectURL(link.href); } subtitleObserverCallback(mutationsList) { if (!this.recordStatus) return; // Look for Peacock subtitle containers based on the example const subtitleContainer = document.querySelector('[data-t-subtitles="true"]'); if (!subtitleContainer || subtitleContainer.style.display === 'none') return; // Target the specific line element from the Peacock subtitle structure const subtitleLine = subtitleContainer.querySelector('.video-player__subtitles__line'); if (!subtitleLine) return; const subtitleText = subtitleLine.innerText.trim(); if (!subtitleText || subtitleText === this.lastSavedSubtitle) return; this.lastSavedSubtitle = subtitleText; this.savedSubtitles.push(subtitleText); this.updatePreviewPanel(); } initializeObserver() { const observerConfig = { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }; const subtitleObserver = new MutationObserver(this.subtitleObserverCallback); // Function to initialize observer on player and subtitle elements const initializePlayerObserver = () => { // Find Peacock player element - may need adjustment const videoPlayer = document.querySelector('.video-player') || document.querySelector('[data-t-subtitles="true"]'); if (videoPlayer) { subtitleObserver.observe(videoPlayer, observerConfig); // Also observe body for dynamic changes subtitleObserver.observe(document.body, observerConfig); return true; } return false; }; if (!initializePlayerObserver()) { // If player not found, observe body until it appears const bodyObserver = new MutationObserver((mutations, observer) => { if (initializePlayerObserver()) { observer.disconnect(); } }); bodyObserver.observe(document.body, observerConfig); } } } // Run the script after page load if (document.readyState === 'loading') { window.addEventListener('load', () => new SubtitleSaver()); } else { new SubtitleSaver(); } })();