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