// ==UserScript==
// @name Missav加载本地字幕
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 一键搜索字幕,加载本地字幕,快捷键操作加速
// @author 月月小射
// @match https://missav.ws/*/*
// @grant GM_addStyle
// @grant unsafeWindow
// @grant GM_openInTab
// @license MIT
// ==/UserScript==
(function () {
'use strict';
GM_addStyle(`
.custom-control-panel {
position: fixed;
bottom: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.5);
color: white;
padding: 10px;
z-index: 9999;
border-radius: 5px;
min-width: 300px;
}
.custom-control-panel label {
margin-right: 5px;
}
.custom-control-panel input[type="number"] {
width: 60px;
margin-right: 5px;
color: white;
background: rgba(0, 0, 0, 0.3);
}
.custom-control-panel input[type="text"] {
width: 60px;
margin-right: 5px;
color: white;
background: rgba(0, 0, 0, 0.3);
}
.custom-control-panel button {
background: #2196F3;
border: none;
color: white;
padding: 5px 5px;
border-radius: 3px;
cursor: pointer;
margin-right: 30px;
}
.custom-subtitle {
position: absolute;
bottom: 15%;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
background: rgba(0,0,0,0.0);
padding: 4px 8px;
border-radius: 4px;
max-width: 80%;
text-align: center;
transition: opacity 0.3s;
z-index: 10000;
}
`);
let accelerationRate = parseFloat(localStorage.getItem('missavAccelerationRate')) || 3;
let skipTime = parseFloat(localStorage.getItem('missavSkipTime')) || 5;
let subtitleOffset = 0;
let isZKeyPressed = false;
let plyrInstance = null;
let subtitles = [];
let currentSubtitle = null;
let shortcutKeys = {
accelerate: localStorage.getItem('missavAccelerateKey') || 'z',
forward: localStorage.getItem('missavForwardKey') || 'x',
backward: localStorage.getItem('missavBackwardKey') || 'c'
};
let controlPanel;
let subtitleElement;
let videoContainer;
function createControlPanel() {
if (document.querySelector('.custom-control-panel')) return;
controlPanel = document.createElement('div');
controlPanel.className = 'custom-control-panel';
const createInputGroup = (labelText, inputType, inputValue, onInputHandler) => {
const label = document.createElement('label');
label.textContent = labelText;
const input = document.createElement('input');
input.type = inputType;
input.value = inputValue;
input.oninput = onInputHandler;
return [label, input];
};
const [accelerateShortcutLabel, accelerateShortcutInput] = createInputGroup(
'加速快捷键:',
'text',
shortcutKeys.accelerate,
() => {
shortcutKeys.accelerate = accelerateShortcutInput.value.toLowerCase();
}
);
const [forwardShortcutLabel, forwardShortcutInput] = createInputGroup(
'快进快捷键:',
'text',
shortcutKeys.forward,
() => {
shortcutKeys.forward = forwardShortcutInput.value.toLowerCase();
}
);
const [backwardShortcutLabel, backwardShortcutInput] = createInputGroup(
'倒退快捷键:',
'text',
shortcutKeys.backward,
() => {
shortcutKeys.backward = backwardShortcutInput.value.toLowerCase();
}
);
const [accelerationLabel, accelerationInput] = createInputGroup(
'加速倍率:',
'number',
accelerationRate,
() => {
accelerationRate = parseFloat(accelerationInput.value);
}
);
const [skipTimeLabel, skipTimeInput] = createInputGroup(
'快进(秒):',
'number',
skipTime,
() => {
skipTime = parseFloat(skipTimeInput.value);
}
);
const [subtitleOffsetLabel, subtitleOffsetInput] = createInputGroup(
'字幕偏移(秒):',
'number',
subtitleOffset,
() => {
subtitleOffset = parseFloat(subtitleOffsetInput.value);
}
);
const subtitleInput = document.createElement('input');
subtitleInput.type = 'file';
subtitleInput.accept = '.srt';
subtitleInput.style.display = 'none';
const subtitleButton = document.createElement('button');
subtitleButton.textContent = '加载字幕';
subtitleButton.onclick = () => subtitleInput.click();
const clearSubtitleButton = document.createElement('button');
clearSubtitleButton.textContent = '清除字幕';
clearSubtitleButton.onclick = () => {
subtitles = [];
subtitleElement.textContent = '';
};
const searchSubtitleButton = document.createElement('button');
searchSubtitleButton.id = 'searchSubtitle';
searchSubtitleButton.textContent = '搜索字幕';
const saveSettingsButton = document.createElement('button');
saveSettingsButton.textContent = '保存设置';
saveSettingsButton.onclick = () => {
localStorage.setItem('missavAccelerationRate', accelerationRate);
localStorage.setItem('missavSkipTime', skipTime);
localStorage.setItem('missavAccelerateKey', shortcutKeys.accelerate);
localStorage.setItem('missavForwardKey', shortcutKeys.forward);
localStorage.setItem('missavBackwardKey', shortcutKeys.backward);
showToast('设置已保存');
};
controlPanel.appendChild(accelerateShortcutLabel);
controlPanel.appendChild(accelerateShortcutInput);
controlPanel.appendChild(forwardShortcutLabel);
controlPanel.appendChild(forwardShortcutInput);
controlPanel.appendChild(backwardShortcutLabel);
controlPanel.appendChild(backwardShortcutInput);
controlPanel.appendChild(document.createElement('br'));
controlPanel.appendChild(accelerationLabel);
controlPanel.appendChild(accelerationInput);
controlPanel.appendChild(skipTimeLabel);
controlPanel.appendChild(skipTimeInput);
controlPanel.appendChild(subtitleOffsetLabel);
controlPanel.appendChild(subtitleOffsetInput);
controlPanel.appendChild(document.createElement('br'));
controlPanel.appendChild(subtitleButton);
controlPanel.appendChild(clearSubtitleButton);
controlPanel.appendChild(searchSubtitleButton);
controlPanel.appendChild(saveSettingsButton);
controlPanel.appendChild(subtitleInput);
document.body.appendChild(controlPanel);
controlPanel.querySelector('#searchSubtitle').addEventListener('click', searchSubtitle);
setupSubtitleHandler(subtitleInput);
}
function setupSubtitleHandler(inputElement) {
subtitleElement = document.createElement('div');
subtitleElement.className = 'custom-subtitle';
if (videoContainer) {
videoContainer.appendChild(subtitleElement);
}
inputElement.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
subtitles = await parseSRT(text);
subtitleElement.style.display = 'block';
console.log("加载字幕成功");
} catch (error) {
showToast('读取字幕文件失败: ' + error.message);
}
});
}
async function parseSRT(text) {
return text
.replace(/\r/g, '')
.split(/\n\n+/)
.filter(Boolean)
.map(block => {
const [id, time, ...text] = block.split('\n');
const [start, end] = time.split(' --> ').map(parseTime);
return { start, end, text: text.join('\n').trim() };
});
}
function parseTime(timeStr) {
const [hms, ms] = timeStr.split(/[,.]/);
const [h, m, s] = hms.split(':');
return (+h * 3600) + (+m * 60) + (+s) + (+ms / 1000);
}
function updateSubtitle() {
if (!plyrInstance || !subtitles.length) return;
const currentTime = unsafeWindow.player.currentTime;
const adjustedTime = currentTime + subtitleOffset;
const sub = subtitles.find(s => adjustedTime >= s.start && adjustedTime <= s.end);
subtitleElement.textContent = sub?.text || '';
}
function initPlayer() {
const video = document.querySelector('video');
if (video && !plyrInstance) {
const checkPlyrInterval = setInterval(() => {
const player = unsafeWindow.player;
if (player && typeof player.currentTime !== 'undefined') {
clearInterval(checkPlyrInterval);
plyrInstance = player;
console.log('Player instance found:', plyrInstance);
video.addEventListener('timeupdate', () => {
requestAnimationFrame(updateSubtitle);
});
setInterval(updateSubtitle, 250);
}
}, 500);
} else {
showToast('无法初始化播放器');
}
}
function setupShortcuts() {
document.addEventListener('keydown', (e) => {
if (!plyrInstance) return;
const key = e.key.toLowerCase();
if (key === shortcutKeys.accelerate) {
plyrInstance.speed = accelerationRate;
isZKeyPressed = true;
} else if (key === shortcutKeys.forward) {
plyrInstance.currentTime = Math.min(plyrInstance.currentTime + skipTime, plyrInstance.duration);
} else if (key === shortcutKeys.backward) {
plyrInstance.currentTime = Math.max(plyrInstance.currentTime - skipTime, 0);
}
});
document.addEventListener('keyup', (e) => {
if (e.key.toLowerCase() === shortcutKeys.accelerate && isZKeyPressed) {
plyrInstance.speed = 1;
isZKeyPressed = false;
}
});
}
function searchSubtitle() {
const videoID = getCurrentVideoID();
if (!videoID) {
showToast('无法获取视频ID');
return;
}
const searchUrl = `https://subtitlecat.com/index.php?search=${encodeURIComponent(videoID)}`;
GM_openInTab(searchUrl, { active: true });
}
function getCurrentVideoID() {
const pathSegments = location.pathname.split('/');
return pathSegments[pathSegments.length - 1];
}
function showToast(message) {
const toast = document.createElement('div');
toast.textContent = message;
toast.style.position = 'fixed';
toast.style.bottom = '20px';
toast.style.right = '20px';
toast.style.background = 'rgba(0, 0, 0, 0.7)';
toast.style.color = 'white';
toast.style.padding = '10px';
toast.style.borderRadius = '5px';
toast.style.zIndex = '9999';
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
(function init() {
videoContainer = document.querySelector('.plyr__video-wrapper');
if (!videoContainer) {
console.log('Video container not found. Retrying in 1 second...');
setTimeout(init, 1000);
return;
}
createControlPanel();
setupShortcuts();
initPlayer();
})();
})();