Greasy Fork

Missav加载本地字幕

一键搜索字幕,加载本地字幕,快捷键操作加速

目前为 2025-02-11 提交的版本。查看 最新版本

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