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