一直使用厂长资源看电影连续剧,还挺高清的,但是现在发现他的观影历史不能正常使用了
因此用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, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* 支持读取 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 位参与者