JavDB to JavTrailers

JavDB 跳转 JavTrailers; From JavDB jump to JavTrailers.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         JavDB to JavTrailers
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  JavDB 跳转 JavTrailers; From JavDB jump to JavTrailers.
// @author       Phoebe
// @match        https://javdb.com/*
// @match        https://javtrailers.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- JavTrailers 静音和全屏逻辑 ---
    if (location.hostname === 'javtrailers.com') {
        const STYLE_ID = 'javtrailers-web-fullscreen-style';

        // 用户交互时间戳(用于判断是否是用户手动操作)
        let lastUserInteraction = 0;
        const updateInteraction = () => { lastUserInteraction = Date.now(); };
        document.addEventListener('pointerdown', updateInteraction, true);
        document.addEventListener('keydown', updateInteraction, true);

        // 拦截页面脚本对视频 pause / currentTime 的操控
        function hookVideoMethods() {
            if (window.__jtVideoHooked) return;
            window.__jtVideoHooked = true;

            const originalPause = HTMLMediaElement.prototype.pause;
            HTMLMediaElement.prototype.pause = function() {
                if (this.tagName === 'VIDEO' && this.closest('.javtrailers-web-fullscreen')) {
                    if (Date.now() - lastUserInteraction < 800) return originalPause.apply(this, arguments);
                    if (this.ended || (this.duration && this.currentTime >= this.duration - 0.5)) return originalPause.apply(this, arguments);
                    console.log('JavTrailers: Blocked auto pause');
                    return;
                }
                return originalPause.apply(this, arguments);
            };

            const descriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'currentTime');
            if (descriptor && descriptor.set) {
                Object.defineProperty(HTMLMediaElement.prototype, 'currentTime', {
                    configurable: true,
                    enumerable: true,
                    get: descriptor.get,
                    set: function(value) {
                        if (this.tagName === 'VIDEO' && this.closest('.javtrailers-web-fullscreen')) {
                            if (value === 0 && Date.now() - lastUserInteraction > 800) {
                                const current = descriptor.get.call(this);
                                if (current > 2) {
                                    console.log('JavTrailers: Blocked currentTime reset to 0');
                                    return;
                                }
                            }
                        }
                        return descriptor.set.call(this, value);
                    }
                });
            }
        }

        // 确保全屏样式只注入一次
        function ensureFullscreenStyle() {
            if (document.getElementById(STYLE_ID)) return;
            const style = document.createElement('style');
            style.id = STYLE_ID;
            style.textContent = `
                .javtrailers-web-fullscreen {
                    position: fixed !important;
                    top: 0 !important;
                    left: 0 !important;
                    width: 100vw !important;
                    height: 100vh !important;
                    z-index: 2147483647 !important;
                    background: #000 !important;
                    margin: 0 !important;
                    padding: 0 !important;
                }
                .javtrailers-web-fullscreen video {
                    width: 100% !important;
                    height: 100% !important;
                    object-fit: contain !important;
                }
            `;
            document.head.appendChild(style);
        }

        // 设置视频默认静音,并防止页面脚本把它改回
        function muteVideos() {
            const videos = document.querySelectorAll('video');
            videos.forEach(video => {
                if (!video.dataset.jtMuted) {
                    video.muted = true;
                    video.dataset.jtMuted = 'true';
                    video.addEventListener('volumechange', function onVolumeChange() {
                        if (!video.muted) {
                            video.muted = true;
                            console.log('JavTrailers: Re-muted video');
                        }
                    });
                    console.log('JavTrailers: Video muted');
                }
            });
        }

        // 查找最合适的视频容器
        function findVideoContainer(video) {
            let container = video.parentElement;
            while (container && container !== document.body) {
                if (container.tagName === 'DIV' || container.tagName === 'SECTION') {
                    break;
                }
                container = container.parentElement;
            }
            return container;
        }

        // 设置视频网页全屏(每个视频只处理一次,避免重复触发播放器重置)
        function webFullscreenVideo() {
            ensureFullscreenStyle();
            const videos = document.querySelectorAll('video:not([data-jt-fullscreen])');
            videos.forEach(video => {
                if (video.readyState >= 2) {
                    const container = findVideoContainer(video);
                    if (container && container !== document.body) {
                        container.classList.add('javtrailers-web-fullscreen');
                        video.dataset.jtFullscreen = 'true';
                        video.play().catch(() => {});
                        console.log('JavTrailers: Web fullscreen enabled');
                    }
                }
            });
        }

        // 防止视频被页面脚本自动暂停并回到封面
        function preventAutoPause() {
            const videos = document.querySelectorAll('video:not([data-jt-pause-guard])');
            videos.forEach(video => {
                video.dataset.jtPauseGuard = 'true';
                video.addEventListener('pause', function onPause() {
                    if (video.ended || video.currentTime <= 0) return;
                    if (video.duration && video.currentTime >= video.duration - 0.5) return;
                    setTimeout(() => {
                        if (Date.now() - lastUserInteraction < 500) return;
                        if (video.paused && !video.ended) {
                            video.play().catch(() => {});
                        }
                    }, 100);
                });
            });
        }

        // 持续守护:防止页面脚本重置进度或自动暂停
        function keepVideoAlive() {
            const lastTimes = new WeakMap();
            setInterval(() => {
                const videos = document.querySelectorAll('video');
                videos.forEach(video => {
                    if (!video.duration) return;

                    const lastTime = lastTimes.get(video) || 0;
                    // 如果进度被非用户操作重置到 0,恢复
                    if (video.currentTime === 0 && lastTime > 2) {
                        video.currentTime = lastTime;
                        video.play().catch(() => {});
                        console.log('JavTrailers: Restored currentTime', lastTime);
                    }
                    lastTimes.set(video, video.currentTime);

                    // 如果页面脚本暂停了视频,恢复
                    if (video.paused && !video.ended && video.currentTime > 0 && video.currentTime < video.duration - 1) {
                        if (Date.now() - lastUserInteraction > 500) {
                            video.play().catch(() => {});
                            console.log('JavTrailers: Resumed video');
                        }
                    }
                });
            }, 500);
        }

        // 初始执行静音和全屏
        muteVideos();

        // 延迟执行全屏,等待视频加载
        setTimeout(() => {
            hookVideoMethods();
            webFullscreenVideo();
            preventAutoPause();
            keepVideoAlive();
        }, 1000);

        // 监听新视频元素的添加
        const videoObserver = new MutationObserver(() => {
            muteVideos();
            setTimeout(() => {
                webFullscreenVideo();
                preventAutoPause();
            }, 500);
        });
        videoObserver.observe(document.body, { childList: true, subtree: true });

        // --- JavTrailers 自动重定向逻辑---
        // 如 SNOS-001,依次尝试 snos00001, 1snos00001, 118snos00001,如果都不行则进入搜索页
        if (document.title.includes('Page not found') || document.title.includes('404')) {
            const currentUrl = location.href;
            const pathParts = location.pathname.split('/');
            let vid = pathParts[pathParts.length - 1];

            if (!vid) return;

            console.log('JavTrailers 404 detected for:', vid);

            const retryKey = 'javtrailers_retry_chain';
            const retryChain = JSON.parse(sessionStorage.getItem(retryKey) || '[]');
            if (!retryChain.includes(vid)) {
                retryChain.push(vid);
                sessionStorage.setItem(retryKey, JSON.stringify(retryChain));
            }

            let newVid = null;
            let attempt = 0;

            // 判断当前是第几次尝试
            if (vid.match(/^[a-z]+\d{5}$/i)) {
                attempt = 1;   // snos00001
                newVid = '1' + vid;
            }
            else if (vid.startsWith('1') && vid.match(/^1[a-z]+\d{5}$/i)) {
                attempt = 2;   // 1snos00001
                const withoutOne = vid.substring(1);
                newVid = '118' + withoutOne;   // 118snos00001
            }
            else if (vid.startsWith('118') && vid.match(/^118[a-z]+\d{5}$/i)) {
                attempt = 3;   // 118snos00001
            }

            if (newVid && attempt < 3 && !retryChain.includes(newVid)) {
                // 第1、2、3次重试
                const newUrl = currentUrl.replace('/' + vid, '/' + newVid);
                console.log(`Retry ${attempt}/3: ${vid} → ${newVid}`);
                location.replace(newUrl);
            }
            else {
                // 三次都失败 → 跳转搜索页

                const searchMatch = vid.replace(/^\d+/, '').match(/^([a-z]+)0*(\d+)$/i);
                let searchId = searchMatch
                    ? `${searchMatch[1].toLowerCase()}-${searchMatch[2].padStart(3, '0')}`
                    : vid.replace(/^\d+/, '').replace(/(\d+)$/, '-$1');

                const searchUrl = `https://javtrailers.com/search/${searchId}`;
                console.log(`All 3 attempts failed. Jumping to search: ${searchUrl}`);
                sessionStorage.removeItem(retryKey);
                location.replace(searchUrl);
            }
        } else {
            sessionStorage.removeItem('javtrailers_retry_chain');
        }
        return;
    }

    // --- JavDB 页面按钮插入逻辑---
    function formatId(rawId) {
        const match = rawId.match(/^([A-Za-z]+)-?(\d+)$/);
        if (match) {
            const prefix = match[1].toLowerCase();
            const number = match[2].padStart(5, '0');
            return prefix + number;
        }
        return rawId.replace('-', '').toLowerCase().trim();
    }

    function createJumpButton(id, isMini = false) {
        const formattedId = formatId(id);
        const url = `https://javtrailers.com/video/${formattedId}`;

        let btn;
        if (isMini) {
            btn = document.createElement('span');
            btn.innerHTML = '🎬 预告片';
            btn.style.cssText = 'margin-left:8px; padding:0 4px; font-size:11px; background-color:#3e8ed0; color:#fff; border-radius:3px; cursor:pointer; display:inline-block; vertical-align:middle;';
            btn.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                window.open(url, '_blank');
            });
        } else {
            btn = document.createElement('a');
            btn.href = url;
            btn.target = '_blank';
            btn.className = 'button is-info is-outlined is-small';
            btn.innerHTML = `<span class="icon is-small"><i class="icon-play-circle"></i></span><span>預告片</span>`;
            btn.style.marginLeft = '5px';
            btn.style.height = '2.25em';
        }
        return btn;
    }

    function handlePage() {
        // 详情页:直接定位番号所在的 panel-block,避免依赖 label 文本和 pathname
        const detailBlocks = document.querySelectorAll('.video-detail .panel-block.first-block:not([data-jt-done])');
        detailBlocks.forEach(block => {
            const valueSpan = block.querySelector('.value');
            if (!valueSpan) return;
            const id = valueSpan.textContent.trim();
            if (!id) return;
            const copyBtn = block.querySelector('.copy-to-clipboard');
            const anchor = copyBtn || valueSpan;
            if (block.querySelector('.jt-jump-button')) return;
            const btn = createJumpButton(id, false);
            btn.classList.add('jt-jump-button');
            anchor.after(btn);
            block.dataset.jtDone = 'true';
        });

        // 列表页
        const items = document.querySelectorAll('.item');
        items.forEach(item => {
            const scoreEl = item.querySelector('.score');
            const idEl = item.querySelector('.video-title strong');
            if (scoreEl && idEl && !scoreEl.dataset.jumpAdded) {
                const id = idEl.innerText.trim();
                scoreEl.appendChild(createJumpButton(id, true));
                scoreEl.dataset.jumpAdded = "true";
            }
        });
    }

    handlePage();

    let debounceTimer;
    const observer = new MutationObserver(() => {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(handlePage, 100);
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();