jable加载本地字幕

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

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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