Greasy Fork

Peacock字幕保存器 (Peacock Subtitle Saver)

Peacock字幕保存器是一款专为 Peacock TV 设计的用户脚本,能够自动记录和保存视频播放过程中的字幕内容。通过该脚本,用户可以轻松捕获字幕文本,并将其导出为 .txt 文件以便后续使用 | Peacock Subtitle Saver is a user script designed specifically for Peacock TV, allowing users to automatically record and save subtitle content during video playback. With this script, users can effortlessly capture subtitle text and export it as a .txt file for later use.

目前为 2025-02-26 提交的版本。查看 最新版本

// ==UserScript==
// @name         Peacock字幕保存器 (Peacock Subtitle Saver)
// @namespace    https://129899.xyz
// @version      0.2
// @description  Peacock字幕保存器是一款专为 Peacock TV 设计的用户脚本,能够自动记录和保存视频播放过程中的字幕内容。通过该脚本,用户可以轻松捕获字幕文本,并将其导出为 .txt 文件以便后续使用 | Peacock Subtitle Saver is a user script designed specifically for Peacock TV, allowing users to automatically record and save subtitle content during video playback. With this script, users can effortlessly capture subtitle text and export it as a .txt file for later 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 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();
    }
})();