// ==UserScript==
// @name Missav字幕加载
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 一键搜索字幕,加载本地字幕,快捷键操作加速
// @author 月月小射
// @match *://missav.ws/*/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_openInTab
// @connect xunlei.com
// @connect geilijiasu.com
// @connect v.geilijiasu.com
// @license MIT
// @run-at document-end
// ==/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: 10px;
}
.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;
}
.subtitle-list {
position: fixed;
bottom: 60px;
left: 10px;
background: rgba(0, 0, 0, 0.8);
color: white;
max-height: 300px;
overflow-y: auto;
padding: 10px;
border-radius: 5px;
z-index: 10001;
}
.subtitle-item {
padding: 5px;
cursor: pointer;
border-bottom: 1px solid #444;
}
.subtitle-item:hover {
background: rgba(255, 255, 255, 0.1);
}
`);
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;
let subtitleList = null;
let originalSubtitleText = '';
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 group = document.createElement('div');
const label = document.createElement('label');
label.textContent = labelText;
const input = document.createElement('input');
input.type = inputType;
input.value = inputValue;
input.oninput = onInputHandler;
group.append(label, input);
return group;
};
controlPanel.append(
createInputGroup('加速键:', 'text', shortcutKeys.accelerate, (e) => {
shortcutKeys.accelerate = e.target.value.toLowerCase();
}),
createInputGroup('快进键:', 'text', shortcutKeys.forward, (e) => {
shortcutKeys.forward = e.target.value.toLowerCase();
}),
createInputGroup('倒退键:', 'text', shortcutKeys.backward, (e) => {
shortcutKeys.backward = e.target.value.toLowerCase();
})
);
controlPanel.append(
createInputGroup('加速倍率:', 'number', accelerationRate, (e) => {
accelerationRate = parseFloat(e.target.value);
}),
createInputGroup('快进(秒):', 'number', skipTime, (e) => {
skipTime = parseFloat(e.target.value);
}),
createInputGroup('字幕偏移:', 'number', subtitleOffset, async (e) => {
subtitleOffset = parseFloat(e.target.value);
if (originalSubtitleText) {
subtitles = await parseSRT(originalSubtitleText);
}
})
);
const buttonContainer = document.createElement('div');
buttonContainer.style.marginTop = '10px';
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 searchSubtitleButton = document.createElement('button');
searchSubtitleButton.textContent = '搜索字幕1';
searchSubtitleButton.onclick = searchSubtitle;
const searchSubtitle2Button = document.createElement('button');
searchSubtitle2Button.textContent = '搜索字幕2';
searchSubtitle2Button.onclick = searchSubtitle2;
const clearSubtitleButton = document.createElement('button');
clearSubtitleButton.textContent = '清除字幕';
clearSubtitleButton.onclick = () => {
subtitles = [];
subtitleElement.textContent = '';
originalSubtitleText = '';
};
const saveSettingsButton = document.createElement('button');
saveSettingsButton.textContent = '保存设置';
saveSettingsButton.onclick = saveSettings;
buttonContainer.append(
subtitleButton,
searchSubtitleButton,
searchSubtitle2Button,
clearSubtitleButton,
saveSettingsButton,
subtitleInput
);
controlPanel.appendChild(buttonContainer);
document.body.appendChild(controlPanel);
setupSubtitleHandler(subtitleInput);
}
function setupSubtitleHandler(inputElement) {
subtitleElement = document.createElement('div');
subtitleElement.className = 'custom-subtitle';
subtitleElement.style.display = 'none';
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();
originalSubtitleText = text;
subtitles = await parseSRT(text);
subtitleElement.style.display = 'block';
showToast('本地字幕加载成功');
} catch (error) {
showToast(`读取失败: ${error.message}`);
}
});
}
async function parseSRT(text) {
return text.replace(/\r/g, '').split(/\n\n+/).filter(Boolean).map(block => {
const [id, time, ...textLines] = block.split('\n');
const [start, end] = time.split(' --> ').map(parseTime);
return {
start: start + subtitleOffset,
end: end + subtitleOffset,
text: textLines.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 sub = subtitles.find(s => currentTime >= s.start && currentTime <= s.end);
subtitleElement.textContent = sub?.text || '';
subtitleElement.style.display = sub ? 'block' : 'none';
}
function initPlayer() {
const checkPlayer = setInterval(() => {
const video = document.querySelector('video');
if (video && unsafeWindow.player) {
clearInterval(checkPlayer);
plyrInstance = unsafeWindow.player;
video.addEventListener('timeupdate', () => {
requestAnimationFrame(updateSubtitle);
});
setInterval(updateSubtitle, 250);
showToast('播放器初始化完成');
}
}, 500);
}
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 += skipTime;
} else if (key === shortcutKeys.backward) {
plyrInstance.currentTime -= skipTime;
}
});
document.addEventListener('keyup', (e) => {
if (e.key.toLowerCase() === shortcutKeys.accelerate && isZKeyPressed) {
plyrInstance.speed = 1;
isZKeyPressed = false;
}
});
}
function searchSubtitle() {
const videoID = getCurrentVideoID();
GM_openInTab(`https://subtitlecat.com/index.php?search=${encodeURIComponent(videoID)}`, { active: true });
}
function fetchSubtitleAPI(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
"Cache-Control": "no-cache",
"Referer": location.href,
"Origin": location.origin,
"User-Agent": navigator.userAgent
},
timeout: 15000,
onload: (res) => {
if (res.status >= 200 && res.status < 300) {
try {
resolve(JSON.parse(res.responseText));
} catch (e) {
reject(new Error('JSON解析失败'));
}
} else {
reject(new Error(`HTTP错误 ${res.status}`));
}
},
onerror: () => reject(new Error('网络错误')),
ontimeout: () => reject(new Error('请求超时'))
});
});
}
async function searchSubtitle2() {
try {
const videoID = getCurrentVideoID();
if (!videoID) return showToast('无法获取视频ID');
showToast('正在搜索字幕...');
const data = await fetchSubtitleAPI(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name=${encodeURIComponent(videoID)}`);
if (data.code === 0) {
showSubtitleList(data.data.filter(item =>
item.url.includes('.srt') &&
item.name.toUpperCase().includes(videoID.toUpperCase())
));
} else {
showToast('未找到匹配字幕');
}
} catch (e) {
showToast(`错误: ${e.message}`);
}
}
function showSubtitleList(items) {
if (subtitleList) subtitleList.remove();
subtitleList = document.createElement('div');
subtitleList.className = 'subtitle-list';
subtitleList.innerHTML = '<div style="color:#ccc; margin-bottom:8px;">选择字幕:</div>';
items.forEach(item => {
const div = document.createElement('div');
div.className = 'subtitle-item';
div.textContent = `${item.name} (${item.extra_name})`;
div.onclick = () => loadRemoteSubtitle(item.url);
subtitleList.appendChild(div);
});
document.body.appendChild(subtitleList);
}
async function loadRemoteSubtitle(url) {
showToast('正在加载字幕...');
try {
const content = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"Referer": location.href,
"Origin": location.origin,
"User-Agent": navigator.userAgent
},
timeout: 15000,
onload: (res) => res.status === 200 ? resolve(res.responseText) : reject(),
onerror: reject,
ontimeout: () => reject(new Error('请求超时'))
});
});
originalSubtitleText = content;
subtitles = await parseSRT(content);
subtitleElement.style.display = 'block';
subtitleList.remove();
showToast('在线字幕加载成功');
} catch (e) {
showToast(`字幕加载失败: ${e.message}`);
}
}
function getCurrentVideoID() {
return location.pathname.split('/').pop();
}
function saveSettings() {
localStorage.setItem('missavAccelerationRate', accelerationRate);
localStorage.setItem('missavSkipTime', skipTime);
localStorage.setItem('missavAccelerateKey', shortcutKeys.accelerate);
localStorage.setItem('missavForwardKey', shortcutKeys.forward);
localStorage.setItem('missavBackwardKey', shortcutKeys.backward);
showToast('设置已保存');
}
function showToast(message) {
const toast = document.createElement('div');
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0,0,0,0.7);
color: white;
padding: 10px 20px;
border-radius: 5px;
z-index: 99999;
animation: fadeIn 0.3s;
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
(function init() {
videoContainer = document.querySelector('.plyr__video-wrapper');
if (!videoContainer) return setTimeout(init, 500);
createControlPanel();
setupShortcuts();
initPlayer();
})();
})();