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