jable加载本地字幕

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         jable加载本地字幕
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  一键搜索字幕,加载本地字幕,快捷键操作加速
// @author       月月小射
// @match        *://jable.tv/videos/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_openInTab
// @license      MIT
// @connect      xunlei.com
// @connect      geilijiasu.com
// @connect      v.geilijiasu.com
// ==/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('jableAccelerationRate')) || 3;
    let skipTime = parseFloat(localStorage.getItem('jableSkipTime')) || 5;
    let subtitleOffset = 6;
    let isZKeyPressed = false;
    let player = null;
    let subtitles = [];
    let currentSubtitle = null;
    let shortcutKeys = {
        accelerate: localStorage.getItem('jableAccelerateKey') || 'z',
        forward: localStorage.getItem('jableForwardKey') || 'x',
        backward: localStorage.getItem('jableBackwardKey') || 'c'
    };

    let controlPanel;
    let subtitleElement;
    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';
        const videoWrapper = document.querySelector('.plyr__video-wrapper');
        if (videoWrapper) {
            videoWrapper.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 (!player || !subtitles.length) return;
        const currentTime = 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 && video.plyr) {
                clearInterval(checkPlayer);
                player = video.plyr;

                player.on('timeupdate', () => {
                    requestAnimationFrame(updateSubtitle);
                });

                setInterval(updateSubtitle, 250);
                showToast('播放器初始化完成');
            }
        }, 500);
    }

    function setupShortcuts() {
        document.addEventListener('keydown', (e) => {
            if (!player) return;

            const key = e.key.toLowerCase();
            if (key === shortcutKeys.accelerate) {
                player.speed = accelerationRate;
                isZKeyPressed = true;
            } else if (key === shortcutKeys.forward) {
                player.currentTime = Math.min(player.currentTime + skipTime, player.duration);
            } else if (key === shortcutKeys.backward) {
                player.currentTime = Math.max(player.currentTime - skipTime, 0);
            }
        });

        document.addEventListener('keyup', (e) => {
            if (e.key.toLowerCase() === shortcutKeys.accelerate && isZKeyPressed) {
                player.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() {
        const match = location.href.match(/https:\/\/jable\.tv\/videos\/([^\/]+)/);
        return match ? match[1] : null;
    }

    function saveSettings() {
        localStorage.setItem('jableAccelerationRate', accelerationRate);
        localStorage.setItem('jableSkipTime', skipTime);
        localStorage.setItem('jableAccelerateKey', shortcutKeys.accelerate);
        localStorage.setItem('jableForwardKey', shortcutKeys.forward);
        localStorage.setItem('jableBackwardKey', 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() {
        const checkInterval = setInterval(() => {
            if (document.querySelector('video')) {
                clearInterval(checkInterval);
                createControlPanel();
                setupShortcuts();
                initPlayer();
            }
        }, 500);
    })();
})();