Greasy Fork

Peacock字幕保存器 (Peacock Subtitle Saver)

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.

目前为 2025-03-01 提交的版本。查看 最新版本

// ==UserScript==
// @name         Peacock字幕保存器 (Peacock Subtitle Saver)
// @namespace    https://129899.xyz
// @version      0.4
// @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

// ==/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 = `
                <div style="margin-bottom: 10px; border-bottom: 1px solid #555; padding-bottom: 5px;">
                    已记录 ${this.savedSubtitles.length} 条字幕
                </div>
                ${lastSubtitles.map((subtitle, index) => `<div style="margin-bottom: 8px;">${subtitle}</div>`).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
            const subtitleContainer = document.querySelector('[data-t-subtitles="true"]');
            if (!subtitleContainer || subtitleContainer.style.display === 'none') return;

            // Target ALL line elements from the Peacock subtitle structure
            const subtitleLines = subtitleContainer.querySelectorAll('.video-player__subtitles__line');
            if (!subtitleLines || subtitleLines.length === 0) return;

            // Combine all subtitle lines into a single text
            const subtitleText = Array.from(subtitleLines)
            .map(line => line.innerText.trim())
            .filter(text => text) // Remove empty lines
            .join(' '); // Join with space

            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();
    }
})();