厂长资源观影记录脚本

一直使用 厂长资源 看电影连续剧,还挺高清的,但是现在发现他的观影历史不能正常使用了 因此用ai撸了个油猴脚本,代替他的观影记录,也分享给大家 // ==UserScript== // @name 厂长资源 观影历史记录增强版 // @namespace https://czzyv.com/ // ...
厂长资源观影记录脚本
厂长资源观影记录脚本

一直使用厂长资源看电影连续剧,还挺高清的,但是现在发现他的观影历史不能正常使用了

因此用ai撸了个油猴脚本,代替他的观影记录,也分享给大家

// ==UserScript==
// @name         厂长资源 观影历史记录增强版
// @namespace    https://czzyv.com/
// @version      1.0.0
// @description  为 厂长资源 影视站增加观影历史、播放进度记录、并支持从历史新窗口打开后自动跳转到上次播放时间
// @author       wg5945
// @match        *://czzyv.com/*
// @match        *://*.czzyv.com/*
// @match        *://plala.py1080p.com/*
// @match        *://*.plala.py1080p.com/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    /**
     * ================================================================
     * 配置
     * ================================================================
     */
    // 在 CONFIG 对象中添加新配置
    const CONFIG = {
        // 主站域名
        SITE_HOSTS: [
            'czzyv.com'
        ],

        // 播放器 iframe 域名
        PLAYER_HOSTS: [
            'plala.py1080p.com'
        ],

        // 观影历史 localStorage key
        STORAGE_KEY: 'CZzyv_Watch_History_v1',

        // 新窗口恢复播放进度用的 localStorage key
        RESUME_STORAGE_KEY: 'CZzyv_Watch_History_Resume_Target_v1',

        // 历史记录最大数量
        MAX_HISTORY: 200,

        // 播放页地址规则
        PLAY_PAGE_REG: /\/v_play\/[^/]+\.html/i,

        // 保存进度间隔(毫秒)
        SAVE_INTERVAL: 10000,

        // 定时记录间隔(毫秒)- 新增
        RECORD_INTERVAL: 10000,

        // iframe 向外层页面上报播放进度
        IFRAME_PROGRESS_MESSAGE: 'CZ_HISTORY_IFRAME_PROGRESS',

        // 外层页面向 iframe 发送恢复进度指令
        IFRAME_RESUME_MESSAGE: 'CZ_HISTORY_IFRAME_RESUME',

        // iframe 恢复完成后通知外层页面
        IFRAME_RESUME_ACK_MESSAGE: 'CZ_HISTORY_IFRAME_RESUME_ACK',

        // 恢复进度容错秒数
        RESUME_TOLERANCE: 3,

        // 刚进入页面时防止 0 秒覆盖历史进度的保护时间
        RESUME_PROTECT_MS: 15000,

        // 向 iframe 发送恢复指令的间隔
        RESUME_DISPATCH_INTERVAL: 800,

        // 最多发送恢复指令次数,防止影响后续正常播放
        RESUME_DISPATCH_MAX_COUNT: 15,

        // 恢复进度数据有效期
        RESUME_EXPIRE_MS: 10 * 60 * 1000,

        // 定时记录间隔(毫秒)
        RECORD_INTERVAL: 10000,

        // iframe 上报进度间隔(毫秒)- 新增
        IFRAME_REPORT_INTERVAL: 10000,

        // 定时记录间隔,毫秒
        RECORD_INTERVAL: 10000,
        // iframe 上报进度间隔,毫秒
        IFRAME_REPORT_INTERVAL: 10000,
        // 小于这个秒数认为还没有真正开始播放
        MIN_VALID_PROGRESS_SECONDS: 3,
        // 是否禁止 0 秒进度覆盖已有历史
        PREVENT_ZERO_PROGRESS_OVERWRITE: true,
        // 是否跳过未开始播放时创建历史记录
        SKIP_ZERO_PROGRESS_RECORD: true,
    };

    let lastSaveAt = 0;
    let latestIframeProgress = null;

    let pendingResumeTarget = null;
    let pendingResumeStartAt = 0;
    let pendingResumeDone = false;
    let resumeDispatchTimer = null;
    let resumeDispatchCount = 0;

    /**
     * ================================================================
     * 通用工具函数
     * ================================================================
     */

    function hostMatches(hostList) {
        const host = location.hostname;
        return hostList.some(item => host === item || host.endsWith('.' + item));
    }

    function isMainSiteHost() {
        return hostMatches(CONFIG.SITE_HOSTS);
    }

    function isPlayerHost() {
        return hostMatches(CONFIG.PLAYER_HOSTS);
    }

    function isInIframe() {
        return window.self !== window.top;
    }

    function isPlayPage() {
        return CONFIG.PLAY_PAGE_REG.test(location.pathname);
    }

    function normalizeUrl(url) {
        try {
            const u = new URL(url, location.href);
            u.hash = '';
            return u.href;
        } catch (e) {
            return String(url || '').split('#')[0];
        }
    }

    function parseTimeToSeconds(text) {
        if (!text) return 0;

        const arr = String(text)
            .trim()
            .split(':')
            .map(num => parseInt(num, 10))
            .filter(num => !Number.isNaN(num));

        if (arr.length === 2) {
            return arr[0] * 60 + arr[1];
        }

        if (arr.length === 3) {
            return arr[0] * 3600 + arr[1] * 60 + arr[2];
        }

        return 0;
    }

    function formatSeconds(seconds) {
        seconds = Math.floor(Number(seconds) || 0);

        const h = Math.floor(seconds / 3600);
        const m = Math.floor((seconds % 3600) / 60);
        const s = seconds % 60;

        if (h > 0) {
            return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
        }

        return `${m}:${String(s).padStart(2, '0')}`;
    }

    function parseProgressText(text) {
        if (!text) return null;

        const cleanText = String(text).replace(/\s+/g, ' ').trim();

        const match = cleanText.match(
            /(\d{1,2}:\d{2}(?::\d{2})?)\s*\/\s*(\d{1,2}:\d{2}(?::\d{2})?)/
        );

        if (!match) return null;

        const currentText = match[1];
        const durationText = match[2];

        return {
            currentTime: parseTimeToSeconds(currentText),
            duration: parseTimeToSeconds(durationText),
            progressText: `${currentText} / ${durationText}`
        };
    }

    function escapeHtml(str) {
        return String(str || '')
            .replace(/&/g, '&')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

    /**
     * 支持读取 Shadow DOM 内部节点。
     */
    function querySelectorAllDeep(selector, root = document) {
        const result = [];

        try {
            result.push(...root.querySelectorAll(selector));
        } catch (e) {}

        let all = [];

        try {
            all = Array.from(root.querySelectorAll('*'));
        } catch (e) {}

        for (const el of all) {
            if (el.shadowRoot) {
                result.push(...querySelectorAllDeep(selector, el.shadowRoot));
            }
        }

        return result;
    }

    /**
     * ================================================================
     * 播放进度读取
     * ================================================================
     */

    function getProgressFromVideo() {
        const videos = querySelectorAllDeep('video', document);
        const video = videos && videos.length ? videos[0] : null;

        if (!video) {
            return {
                currentTime: 0,
                duration: 0,
                progressText: ''
            };
        }

        const currentTime = Number.isFinite(video.currentTime) ? video.currentTime : 0;
        const duration = Number.isFinite(video.duration) ? video.duration : 0;

        if (!currentTime && !duration) {
            return {
                currentTime: 0,
                duration: 0,
                progressText: ''
            };
        }

        return {
            currentTime,
            duration,
            progressText: duration
                ? `${formatSeconds(currentTime)} / ${formatSeconds(duration)}`
                : formatSeconds(currentTime)
        };
    }

    function getProgressFromCurrentDocument() {
        const selectors = [
            '.art-control.art-control-time[data-index="30"]',
            '.art-control.art-control-time',
            '.art-controls-left .art-control-time',
            '.art-control-time',
            '[class*="art-control-time"]'
        ];

        for (const selector of selectors) {
            const nodes = querySelectorAllDeep(selector, document);

            for (const node of nodes) {
                const text =
                    node.textContent ||
                    node.innerText ||
                    node.getAttribute('aria-label') ||
                    '';

                const result = parseProgressText(text);

                if (result && result.progressText) {
                    return result;
                }
            }
        }

        const leftControls = querySelectorAllDeep('.art-controls-left', document);

        for (const node of leftControls) {
            const result = parseProgressText(node.textContent || node.innerText || '');

            if (result && result.progressText) {
                return result;
            }
        }

        return getProgressFromVideo();
    }

    /**
     * ================================================================
     * iframe 播放器内部逻辑
     *
     * 脚本运行在播放器 iframe 页面时:
     * 1. 定时读取 video / ArtPlayer 控件进度
     * 2. 上报给外层主站页面
     * 3. 接收外层页面发来的恢复进度指令
     * ================================================================
     */

    let iframeActiveSeekTimer = null;
    let iframeActiveSeekTarget = 0;
    let iframeSeekAttemptCount = 0;
    let iframeLastResumeKey = '';
    let iframeResumeFinished = false;

    function sendIframeResumeAck(seconds) {
        try {
            window.top.postMessage({
                type: CONFIG.IFRAME_RESUME_ACK_MESSAGE,
                currentTime: seconds,
                href: location.href,
                time: Date.now()
            }, '*');
        } catch (e) {}
    }

    function reportIframeProgressNow() {
        const progress = getProgressFromCurrentDocument();

        if (!progress || !progress.progressText) {
            return;
        }

        try {
            window.top.postMessage({
                type: CONFIG.IFRAME_PROGRESS_MESSAGE,
                progress,
                href: location.href,
                time: Date.now()
            }, '*');
        } catch (e) {}
    }

    function startIframeSeek(seconds, autoPlay = true) {
        seconds = Number(seconds) || 0;
        if (seconds <= 0) return;

        const resumeKey = `${Math.floor(seconds)}`;

        // 同一个恢复时间只处理一次,避免正常播放后被反复拉回旧进度。
        if (iframeResumeFinished && iframeLastResumeKey === resumeKey) {
            return;
        }

        iframeLastResumeKey = resumeKey;
        iframeResumeFinished = false;
        iframeActiveSeekTarget = seconds;
        iframeSeekAttemptCount = 0;

        if (iframeActiveSeekTimer) {
            clearInterval(iframeActiveSeekTimer);
            iframeActiveSeekTimer = null;
        }

        function doSeek() {
            iframeSeekAttemptCount++;

            const videos = querySelectorAllDeep('video', document);

            if (!videos.length) {
                if (iframeSeekAttemptCount >= 60) {
                    clearInterval(iframeActiveSeekTimer);
                    iframeActiveSeekTimer = null;
                }
                return;
            }

            let finished = false;

            for (const video of videos) {
                try {
                    let target = iframeActiveSeekTarget;
                    const duration = Number.isFinite(video.duration) ? video.duration : 0;

                    if (duration > 0 && target >= duration - 2) {
                        target = Math.max(0, duration - 5);
                    }

                    const current = Number(video.currentTime) || 0;

                    // 只允许从前往后恢复,不允许播放后再被拉回旧时间。
                    if (current < target - CONFIG.RESUME_TOLERANCE) {
                        video.currentTime = target;
                    } else {
                        finished = true;
                    }

                    if (autoPlay) {
                        const p = video.play && video.play();
                        if (p && typeof p.catch === 'function') {
                            p.catch(() => {});
                        }
                    }

                    const after = Number(video.currentTime) || 0;

                    if (after >= target - CONFIG.RESUME_TOLERANCE) {
                        finished = true;
                    }
                } catch (e) {}
            }

            reportIframeProgressNow();

            if (finished || iframeSeekAttemptCount >= 60) {
                iframeResumeFinished = true;

                if (iframeActiveSeekTimer) {
                    clearInterval(iframeActiveSeekTimer);
                    iframeActiveSeekTimer = null;
                }

                sendIframeResumeAck(iframeActiveSeekTarget);
            }
        }

        doSeek();

        iframeActiveSeekTimer = setInterval(doSeek, 500);

        setTimeout(doSeek, 300);
        setTimeout(doSeek, 1000);
        setTimeout(doSeek, 2000);
    }

    function startIframeProgressReporter() {
        let lastText = '';

        function reportProgress() {
            const progress = getProgressFromCurrentDocument();

            if (!progress || !progress.progressText) {
                return;
            }

            if (progress.progressText === lastText) {
                return;
            }

            lastText = progress.progressText;

            try {
                window.top.postMessage({
                    type: CONFIG.IFRAME_PROGRESS_MESSAGE,
                    progress,
                    href: location.href,
                    time: Date.now()
                }, '*');
            } catch (e) {}
        }

        window.addEventListener('message', function (event) {
            const data = event.data;

            if (!data || data.type !== CONFIG.IFRAME_RESUME_MESSAGE) {
                return;
            }

            const seconds = Number(data.currentTime) || 0;

            if (seconds > 0) {
                startIframeSeek(seconds, data.autoPlay !== false);
            }
        });

        setInterval(reportProgress, CONFIG.IFRAME_REPORT_INTERVAL);  // 改为使用配置参数

        const observer = new MutationObserver(() => {
            reportProgress();
        });

        if (document.body) {
            observer.observe(document.body, {
                childList: true,
                subtree: true,
                characterData: true
            });
        }

        document.addEventListener('loadedmetadata', function (e) {
            if (
                e.target &&
                e.target.tagName === 'VIDEO' &&
                iframeActiveSeekTarget > 0 &&
                !iframeResumeFinished
            ) {
                startIframeSeek(iframeActiveSeekTarget, true);
            }
        }, true);

        document.addEventListener('canplay', function (e) {
            if (
                e.target &&
                e.target.tagName === 'VIDEO' &&
                iframeActiveSeekTarget > 0 &&
                !iframeResumeFinished
            ) {
                startIframeSeek(iframeActiveSeekTarget, true);
            }
        }, true);

        setTimeout(reportProgress, 500);
        setTimeout(reportProgress, 1500);
        setTimeout(reportProgress, 3000);
    }

    // 当前脚本如果运行在播放器 iframe 中,只执行 iframe 逻辑。
    if (isInIframe() && isPlayerHost()) {
        startIframeProgressReporter();
        return;
    }

    // 非主站页面不执行外层历史记录逻辑。
    if (!isMainSiteHost()) {
        return;
    }

    /**
     * ================================================================
     * 历史记录读写
     * ================================================================
     */

    function getHistory() {
        try {
            const raw = localStorage.getItem(CONFIG.STORAGE_KEY);
            const list = raw ? JSON.parse(raw) : [];
            return Array.isArray(list) ? list : [];
        } catch (e) {
            console.error('[观影历史] 读取失败', e);
            return [];
        }
    }

    function saveHistory(list) {
        try {
            localStorage.setItem(
                CONFIG.STORAGE_KEY,
                JSON.stringify(list.slice(0, CONFIG.MAX_HISTORY))
            );
        } catch (e) {
            console.error('[观影历史] 保存失败', e);
        }
    }

    /**
     * 新窗口打开后也要恢复进度,因此这里使用 localStorage,
     * 而不是 sessionStorage。
     */
    function saveResumeTarget(item) {
        if (!item || !item.url) return;

        const currentTime = Number(item.currentTime) || 0;

        if (currentTime <= 0) return;

        try {
            localStorage.setItem(CONFIG.RESUME_STORAGE_KEY, JSON.stringify({
                url: item.url,
                currentTime,
                duration: Number(item.duration) || 0,
                title: item.title || '',
                time: Date.now()
            }));
        } catch (e) {}
    }

    function loadResumeTarget() {
        try {
            const raw = localStorage.getItem(CONFIG.RESUME_STORAGE_KEY);
            if (!raw) return null;

            const data = JSON.parse(raw);

            if (!data || !data.url || !(Number(data.currentTime) > 0)) {
                return null;
            }

            if (Date.now() - (Number(data.time) || 0) > CONFIG.RESUME_EXPIRE_MS) {
                clearResumeTarget();
                return null;
            }

            if (normalizeUrl(data.url) !== normalizeUrl(location.href)) {
                return null;
            }

            return {
                url: data.url,
                currentTime: Number(data.currentTime) || 0,
                duration: Number(data.duration) || 0,
                title: data.title || '',
                time: Number(data.time) || Date.now()
            };
        } catch (e) {
            return null;
        }
    }

    function clearResumeTarget() {
        try {
            localStorage.removeItem(CONFIG.RESUME_STORAGE_KEY);
        } catch (e) {}
    }

    /**
     * ================================================================
     * 恢复播放进度
     * ================================================================
     */

    function stopResumeDispatcher() {
        pendingResumeDone = true;
        clearResumeTarget();

        if (resumeDispatchTimer) {
            clearInterval(resumeDispatchTimer);
            resumeDispatchTimer = null;
        }
    }

    function sendResumeMessageToIframes() {
        if (!pendingResumeTarget || pendingResumeDone) return;

        const currentTime = Number(pendingResumeTarget.currentTime) || 0;
        if (currentTime <= 0) return;

        const iframes = document.querySelectorAll('iframe');

        for (const iframe of iframes) {
            try {
                iframe.contentWindow.postMessage({
                    type: CONFIG.IFRAME_RESUME_MESSAGE,
                    currentTime,
                    duration: Number(pendingResumeTarget.duration) || 0,
                    url: pendingResumeTarget.url,
                    autoPlay: true,
                    time: Date.now()
                }, '*');
            } catch (e) {}
        }
    }

    function startResumeDispatcher() {
        if (!isPlayPage()) return;

        const target = loadResumeTarget();

        if (!target || !(target.currentTime > 0)) return;

        pendingResumeTarget = target;
        pendingResumeStartAt = Date.now();
        pendingResumeDone = false;
        resumeDispatchCount = 0;

        function dispatch() {
            if (!pendingResumeTarget || pendingResumeDone) {
                stopResumeDispatcher();
                return;
            }

            resumeDispatchCount++;

            sendResumeMessageToIframes();

            // 只在进入页面初期尝试恢复,避免影响后续正常播放。
            if (resumeDispatchCount >= CONFIG.RESUME_DISPATCH_MAX_COUNT) {
                stopResumeDispatcher();
            }
        }

        dispatch();

        if (resumeDispatchTimer) {
            clearInterval(resumeDispatchTimer);
        }

        resumeDispatchTimer = setInterval(dispatch, CONFIG.RESUME_DISPATCH_INTERVAL);

        setTimeout(dispatch, 300);
        setTimeout(dispatch, 1000);
        setTimeout(dispatch, 2000);
    }

    /**
     * ================================================================
     * 外层页面接收 iframe 消息
     * ================================================================
     */

    function bindIframeProgressMessage() {
        window.addEventListener('message', function (event) {
            const data = event.data;

            if (!data) return;

            if (data.type === CONFIG.IFRAME_RESUME_ACK_MESSAGE) {
                stopResumeDispatcher();
                return;
            }

            if (data.type !== CONFIG.IFRAME_PROGRESS_MESSAGE) {
                return;
            }

            if (!data.progress || !data.progress.progressText) {
                return;
            }

            latestIframeProgress = {
                currentTime: data.progress.currentTime || 0,
                duration: data.progress.duration || 0,
                progressText: data.progress.progressText,
                time: Date.now(),
                iframeHref: data.href || ''
            };

            if (
                pendingResumeTarget &&
                !pendingResumeDone &&
                latestIframeProgress.currentTime >= pendingResumeTarget.currentTime - CONFIG.RESUME_TOLERANCE
            ) {
                stopResumeDispatcher();
            }

            recordCurrentPlayPage(false);  // 改为 false,让它走节流逻辑
        });
    }

    function getProgress() {
        const pageProgress = getProgressFromCurrentDocument();

        if (pageProgress && pageProgress.progressText) {
            return pageProgress;
        }

        if (latestIframeProgress && latestIframeProgress.progressText) {
            return latestIframeProgress;
        }

        return getProgressFromVideo();
    }

    /**
     * ================================================================
     * 标题、集数识别
     * ================================================================
     */

    function getMovieTitle() {
        const ptitLink = document.querySelector('.mi_cont .paycon h3.ptit a, h3.ptit a');

        if (ptitLink && ptitLink.textContent.trim()) {
            return ptitLink.textContent.trim();
        }

        return document.title
            .replace(/第\s*\d+\s*集/g, '')
            .replace(/在线观看.*$/g, '')
            .replace(/免费播放.*$/g, '')
            .replace(/在线播放.*$/g, '')
            .replace(/[-_].*$/g, '')
            .replace(/\s+/g, ' ')
            .trim() || location.href;
    }

    function getEpisodeText() {
        const ptitEpisode = document.querySelector('.mi_cont .paycon h3.ptit span, h3.ptit span');

        if (ptitEpisode && ptitEpisode.textContent.trim()) {
            return ptitEpisode.textContent.trim();
        }

        const currentEpisode = document.querySelector('.juji_list .pbplay');

        if (currentEpisode && currentEpisode.textContent.trim()) {
            return `第${currentEpisode.textContent.trim()}集`;
        }

        return '';
    }

    /**
     * ================================================================
     * 保存当前播放页进度
     * ================================================================
     */

    function recordCurrentPlayPage(force = false) {
        if (!isPlayPage()) return;

        const now = Date.now();

        if (!force && now - lastSaveAt < CONFIG.SAVE_INTERVAL) {
            return;
        }

        const progress = getProgress();

        const currentTime = Number(progress.currentTime) || 0;
        const duration = Number(progress.duration) || 0;

        let list = getHistory();
        const currentUrl = location.href;
        const oldItem = list.find(item => item.url === currentUrl);

        const oldCurrentTime = oldItem ? Number(oldItem.currentTime) || 0 : 0;

        /**
         * ================================================================
         * 0 秒进度保护
         *
         * 页面刚打开但还没播放时,播放器通常会上报:
         * currentTime = 0
         *
         * 如果历史里已经有这一集的有效进度,不能让 0 秒覆盖掉旧进度。
         * 如果历史里没有这一集,也不要在未播放时创建一条 0 秒记录。
         * ================================================================
         */
        const minValidSeconds = Number(CONFIG.MIN_VALID_PROGRESS_SECONDS) || 3;
        const isZeroLikeProgress = currentTime < minValidSeconds;
        const oldHasValidProgress = oldCurrentTime >= minValidSeconds;

        if (isZeroLikeProgress) {
            // 已有有效历史进度时,禁止被 0 秒或接近 0 秒覆盖
            if (
                CONFIG.PREVENT_ZERO_PROGRESS_OVERWRITE &&
                oldItem &&
                oldHasValidProgress
            ) {
                return;
            }

            // 没有历史记录时,页面刚打开但未播放,不创建 0 秒记录
            if (
                CONFIG.SKIP_ZERO_PROGRESS_RECORD &&
                !oldItem
            ) {
                return;
            }
        }

        // 恢复初期避免播放器短暂上报 0 秒覆盖已有历史。
        if (
            pendingResumeTarget &&
            !pendingResumeDone &&
            now - pendingResumeStartAt < CONFIG.RESUME_PROTECT_MS &&
            pendingResumeTarget.currentTime > 5 &&
            currentTime < pendingResumeTarget.currentTime - CONFIG.RESUME_TOLERANCE
        ) {
            return;
        }

        lastSaveAt = now;

        const movieTitle = getMovieTitle();
        const episodeText = getEpisodeText();

        const displayTitle = episodeText
            ? `${movieTitle} ${episodeText}`
            : movieTitle;

        const item = {
            url: currentUrl,
            title: displayTitle,
            movieTitle,
            episodeText,
            currentTime,
            duration,
            progressText: progress.progressText || '',
            time: now,
            host: location.hostname,
            pathname: location.pathname
        };

        list = list.filter(x => x.url !== item.url);
        list.unshift(item);

        saveHistory(list);
        renderHistoryList();
    }

    function deleteHistory(url) {
        const list = getHistory().filter(item => item.url !== url);
        saveHistory(list);
        renderHistoryList();
    }

    function clearHistory() {
        if (!confirm('确定要清空全部观影历史吗?')) return;
        saveHistory([]);
        renderHistoryList();
    }

    /**
     * ================================================================
     * UI 样式
     * ================================================================
     */

    function addStyle() {
        const style = document.createElement('style');

        style.textContent = `
            #czzyv-history-btn {
                position: fixed;
                right: 30px;
                top: 10px;
                z-index: 999999;
                width: 34px;          /* was 76px */
                height: 34px;         /* was 34px */
                border-radius: 50%;   /* was 18px — now a circle */
                background: #ff6500;
                color: #fff;
                cursor: pointer;
                box-shadow: 0 4px 14px rgba(0,0,0,.25);
                user-select: none;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            #czzyv-history-btn:hover {
                background: #ff7a22;
            }

            #czzyv-history-panel {
                position: fixed;
                right: 20px;
                top: 64px;
                width: 420px;
                max-width: calc(100vw - 40px);
                height: 560px;
                max-height: calc(100vh - 90px);
                background: #ffffff;
                color: #333;
                z-index: 999999;
                border-radius: 10px;
                box-shadow: 0 8px 32px rgba(0,0,0,.35);
                display: none;
                overflow: hidden;
                font-size: 14px;
            }

            #czzyv-history-panel.czzyv-show {
                display: block;
            }

            .czzyv-history-header {
                height: 48px;
                background: #20232a;
                color: #fff;
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 0 14px;
                box-sizing: border-box;
            }

            .czzyv-history-header strong {
                font-size: 16px;
            }

            .czzyv-history-actions {
                display: flex;
                align-items: center;
                gap: 8px;
            }

            .czzyv-history-actions button {
                border: none;
                background: #444b57;
                color: #fff;
                border-radius: 4px;
                cursor: pointer;
                padding: 4px 8px;
                font-size: 12px;
            }

            .czzyv-history-actions button:hover {
                background: #5a6473;
            }

            #czzyv-history-list {
                height: calc(100% - 48px);
                overflow-y: auto;
                padding: 8px;
                box-sizing: border-box;
                background: #f5f6f7;
            }

            .czzyv-history-empty {
                color: #888;
                text-align: center;
                padding: 70px 10px;
            }

            .czzyv-history-item {
                background: #fff;
                border-radius: 8px;
                margin-bottom: 8px;
                padding: 10px;
                box-sizing: border-box;
                box-shadow: 0 1px 5px rgba(0,0,0,.08);
            }

            .czzyv-history-title {
                color: #222;
                font-weight: 600;
                font-size: 15px;
                line-height: 1.45;
                margin-bottom: 8px;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }

            .czzyv-history-progress-text {
                color: #666;
                font-size: 13px;
                line-height: 1.8;
            }

            .czzyv-progress-wrap {
                height: 6px;
                background: #e5e5e5;
                border-radius: 10px;
                margin: 8px 0;
                overflow: hidden;
            }

            .czzyv-progress-bar {
                height: 100%;
                background: #ff6500;
                width: 0%;
            }

            .czzyv-history-op {
                margin-top: 8px;
                display: flex;
                gap: 8px;
            }

            .czzyv-history-op a,
            .czzyv-history-op button {
                font-size: 12px;
                border: none;
                text-decoration: none;
                cursor: pointer;
                border-radius: 4px;
                padding: 5px 10px;
            }

            .czzyv-history-op a {
                background: #ff6500;
                color: #fff;
            }

            .czzyv-history-op button {
                background: #eee;
                color: #555;
            }

            .czzyv-history-op a:hover {
                background: #ff7a22;
            }

            .czzyv-history-op button:hover {
                background: #ddd;
            }
        `;

        document.head.appendChild(style);
    }

    /**
     * ================================================================
     * UI 创建与渲染
     * ================================================================
     */

    function createUI() {
        if (document.querySelector('#czzyv-history-btn')) return;
        const btn = document.createElement('div');
        btn.id = 'czzyv-history-btn';
        btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <circle cx="12" cy="12" r="10"/>
          <polyline points="12 6 12 12 16 14"/>
        </svg>`;
        btn.title = '观影历史';
        const panel = document.createElement('div');
        panel.id = 'czzyv-history-panel';
        panel.innerHTML = `
            <div class="czzyv-history-header">
                <strong>观影历史</strong>
                <div class="czzyv-history-actions">
                    <button id="czzyv-history-clear">清空</button>
                    <button id="czzyv-history-close">关闭</button>
                </div>
            </div>
            <div id="czzyv-history-list"></div>
        `;
        document.body.appendChild(btn);
        document.body.appendChild(panel);
        // 鼠标移入按钮时显示面板
        btn.addEventListener('mouseenter', function () {
            panel.classList.add('czzyv-show');
            renderHistoryList();
        });
        // 鼠标移出按钮时,如果不在面板上则隐藏
        btn.addEventListener('mouseleave', function () {
            setTimeout(() => {
                if (!panel.matches(':hover') && !btn.matches(':hover')) {
                    panel.classList.remove('czzyv-show');
                }
            }, 100);
        });
        // 鼠标移出面板时隐藏
        panel.addEventListener('mouseleave', function () {
            setTimeout(() => {
                if (!panel.matches(':hover') && !btn.matches(':hover')) {
                    panel.classList.remove('czzyv-show');
                }
            }, 100);
        });
        // 阻止面板内部点击事件冒泡
        panel.addEventListener('click', function (e) {
            e.stopPropagation();
        });
        // 点击页面其他地方关闭面板
        document.addEventListener('click', function () {
            panel.classList.remove('czzyv-show');
        });
        panel.querySelector('#czzyv-history-close').addEventListener('click', function () {
            panel.classList.remove('czzyv-show');
        });
        panel.querySelector('#czzyv-history-clear').addEventListener('click', clearHistory);
        renderHistoryList();
    }

    function renderHistoryList() {
        const box = document.querySelector('#czzyv-history-list');
        if (!box) return;

        const list = getHistory();

        if (!list.length) {
            box.innerHTML = `<div class="czzyv-history-empty">暂无观影历史</div>`;
            return;
        }

        box.innerHTML = list.map((item, index) => {
            const progress = item.duration > 0
                ? Math.min(100, Math.max(0, item.currentTime / item.duration * 100))
                : 0;

            const progressText = item.progressText || '未记录进度';

            return `
                <div class="czzyv-history-item">
                    <div class="czzyv-history-title" title="${escapeHtml(item.title)}">
                        ${escapeHtml(item.title)}
                    </div>

                    <div class="czzyv-progress-wrap">
                        <div class="czzyv-progress-bar" style="width:${progress}%"></div>
                    </div>

                    <div class="czzyv-history-progress-text">
                        进度:${escapeHtml(progressText)}
                    </div>

                    <div class="czzyv-history-op">
                        <a href="${escapeHtml(item.url)}"
                           target="_blank"
                           rel="noopener noreferrer"
                           data-action="continue"
                           data-index="${index}">继续播放</a>
                        <button data-url="${escapeHtml(item.url)}" data-action="delete">删除</button>
                    </div>
                </div>
            `;
        }).join('');

        box.querySelectorAll('a[data-action="continue"]').forEach(link => {
            link.addEventListener('click', function (e) {
                e.stopPropagation();

                const index = parseInt(this.getAttribute('data-index'), 10);
                const item = getHistory()[index];

                if (!item) return;

                // 新窗口打开前保存恢复目标,新窗口读取后自动跳转进度。
                saveResumeTarget(item);
            });
        });

        box.querySelectorAll('button[data-action="delete"]').forEach(btn => {
            btn.addEventListener('click', function (e) {
                e.stopPropagation();
                const url = this.getAttribute('data-url');
                deleteHistory(url);
            });
        });
    }

    /**
     * ================================================================
     * 页面事件绑定
     * ================================================================
     */

    function bindVideoEvents() {
        const video = document.querySelector('video');

        if (!video || video.__czzyvHistoryBound) return;

        video.__czzyvHistoryBound = true;

        // 播放开始时记录,延迟 1 秒确保播放器稳定
        video.addEventListener('play', () => {
            setTimeout(() => recordCurrentPlayPage(true), 1000);
        });

        // 暂停时立即记录
        video.addEventListener('pause', () => {
            recordCurrentPlayPage(true);
        });

        // 播放结束时立即记录
        video.addEventListener('ended', () => {
            recordCurrentPlayPage(true);
        });

        // timeupdate 节流处理,每 10 秒记录一次
        let lastUpdateTime = 0;
        video.addEventListener('timeupdate', () => {
            const now = Date.now();
            if (now - lastUpdateTime >= 10000) {
                lastUpdateTime = now;
                recordCurrentPlayPage(false);
            }
        });

        // 元数据加载完成时记录
        video.addEventListener('loadedmetadata', () => {
            setTimeout(() => recordCurrentPlayPage(true), 1000);
        });

        // 用户拖动进度条后记录新位置
        video.addEventListener('seeked', () => {
            recordCurrentPlayPage(true);
        });
    }

    function observePageChange() {
        const observer = new MutationObserver(() => {
            bindVideoEvents();

            if (isPlayPage()) {
                recordCurrentPlayPage(false);
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            characterData: true
        });
    }

    function startIntervalRecord() {
        setInterval(() => {
            if (isPlayPage()) {
                recordCurrentPlayPage(true);
            }
        }, CONFIG.RECORD_INTERVAL);  // 改为使用 CONFIG.RECORD_INTERVAL
    }

    function bindBeforeUnload() {
        window.addEventListener('beforeunload', () => {
            recordCurrentPlayPage(true);
        });

        window.addEventListener('pagehide', () => {
            recordCurrentPlayPage(true);
        });
    }

    /**
     * ================================================================
     * 初始化
     * ================================================================
     */

    function init() {
        bindIframeProgressMessage();
        addStyle();
        createUI();

        if (isPlayPage()) {
            startResumeDispatcher();

            setTimeout(() => recordCurrentPlayPage(true), 1000);
            setTimeout(() => recordCurrentPlayPage(true), 3000);
            setTimeout(() => recordCurrentPlayPage(true), 6000);
        }

        bindVideoEvents();
        observePageChange();
        startIntervalRecord();
        bindBeforeUnload();
    }

    init();

})();

2 个帖子 - 2 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文