// ==UserScript== // @name PEP WP Auto Play Helper // @namespace https://wp.pep.com.cn/ // @version 0.1.0 // @description Best-effort autoplay and next-item helper for wp.pep.com.cn using visible controls only. // @match https://wp.pep.com.cn/* // @match http://wp.pep.com.cn/* // @run-at document-idle // @allFrames true // @grant none // ==/UserScript== (function () { 'use strict'; const LOG = '[PEP Auto]'; const MIN_VISIBLE_WIDTH = 120; const MIN_VISIBLE_HEIGHT = 90; const prepared = new WeakSet(); const stateByVideo = new WeakMap(); let scanTimer = null; const PLAY_RE = /(继续播放|继续学习|开始播放|开始学习|播放|play|resume)/i; const NEXT_RE = /(下一(?:集|节|课|讲|页|章|个|视频|课时|单元|课程|门)|下一个|继续(?:学习|播放)?|next|continue)/i; const PREV_RE = /(上一(?:集|节|课|讲|页|章|个|视频|课时|单元|课程|门)|上一个|previous|prev|back)/i; const BAD_TEXT_RE = /(列表|设置|速度|画中画|全屏|音量|弹幕|字幕|收藏|分享|倍速|评论|刷新|下载|广告)/i; function log() { if (typeof console !== 'undefined' && console.debug) { console.debug(LOG, ...arguments); } } function textOf(el) { if (!el) return ''; const parts = [ el.getAttribute && el.getAttribute('aria-label'), el.getAttribute && el.getAttribute('title'), el.getAttribute && el.getAttribute('alt'), typeof el.value === 'string' ? el.value : '', el.innerText, el.textContent, ]; return parts.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); } function isVisible(el) { if (!el || !el.getClientRects || !el.getClientRects().length) return false; const style = getComputedStyle(el); return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || 1) > 0; } function isEnabled(el) { return !el.matches('[disabled], [aria-disabled="true"], .disabled, .is-disabled'); } function interactiveCandidates(root) { const scope = root && root.querySelectorAll ? root : document; return Array.from(scope.querySelectorAll('button, a, [role="button"], input[type="button"], input[type="submit"]')); } function scorePlayCandidate(el) { const text = textOf(el); if (!text || !PLAY_RE.test(text) || BAD_TEXT_RE.test(text)) return 0; let score = 10; if (/^播放$/.test(text)) score += 40; if (/继续播放|开始播放|继续学习/.test(text)) score += 30; if (/play/i.test(text)) score += 10; return score; } function scoreNextCandidate(el) { const text = textOf(el); let score = 0; if (el.matches('a[rel="next"]')) score += 60; if (/next/i.test(el.getAttribute('href') || '')) score += 20; if (NEXT_RE.test(text)) score += 50; if (/下一/.test(text)) score += 20; if (/继续/.test(text)) score += 10; if (PREV_RE.test(text)) score -= 100; if (BAD_TEXT_RE.test(text) && !NEXT_RE.test(text)) score -= 15; return score; } function findPlayTarget(root) { let best = null; let bestScore = 0; for (const el of interactiveCandidates(root)) { if (!isVisible(el) || !isEnabled(el)) continue; const score = scorePlayCandidate(el); if (score > bestScore) { bestScore = score; best = el; } } return best; } function findNextTarget(root) { let best = null; let bestScore = 0; for (const el of interactiveCandidates(root)) { if (!isVisible(el) || !isEnabled(el)) continue; const score = scoreNextCandidate(el); if (score > bestScore) { bestScore = score; best = el; } } return bestScore > 0 ? best : null; } function findSiblingNextTarget(root) { const active = root.querySelector('[aria-current="true"], .active, .current, .on, .selected, .playing'); if (!active) return null; const anchor = active.closest('a, button, [role="button"]') || active; const container = active.closest('ul, ol, nav, section, article, aside, div') || root; const items = interactiveCandidates(container).filter((el) => isVisible(el) && isEnabled(el)); const index = items.findIndex((el) => el === anchor || el.contains(anchor) || anchor.contains(el)); if (index >= 0) { for (let i = index + 1; i < items.length; i += 1) { const text = textOf(items[i]); if (!text) continue; if (PREV_RE.test(text)) continue; return items[i]; } } return null; } function scopeForVideo(video) { return ( video.closest('main, article, section, .player, .video-player, .lesson, .course, .content') || document ); } function videoState(video) { let state = stateByVideo.get(video); if (!state) { state = { lastSrc: '', gestureHooked: false, nextTriggered: false, autoplayAttempted: false, awaitingGesture: false, }; stateByVideo.set(video, state); } const src = video.currentSrc || video.src || ''; if (src && src !== state.lastSrc) { state.lastSrc = src; state.gestureHooked = false; state.nextTriggered = false; state.autoplayAttempted = false; state.awaitingGesture = false; } return state; } function clickElement(el) { if (!el) return false; try { el.click(); return true; } catch (err) { log('click failed', err); return false; } } function clickPlayButton(video) { const root = scopeForVideo(video); const target = findPlayTarget(root) || findPlayTarget(document); if (target) { log('click play target', textOf(target)); return clickElement(target); } return false; } function clickNextButton(video) { const root = scopeForVideo(video); const target = findNextTarget(root) || findNextTarget(document) || findSiblingNextTarget(root); if (target) { log('click next target', textOf(target)); return clickElement(target); } return false; } function hookGestureRetry(video) { const state = videoState(video); if (state.gestureHooked) return; state.gestureHooked = true; state.awaitingGesture = true; const retry = () => { if (!video.isConnected) return; state.gestureHooked = false; tryAutoplay(video, 'gesture'); }; const opts = { capture: true, once: true }; document.addEventListener('pointerdown', retry, opts); document.addEventListener('keydown', retry, opts); } function tryAutoplay(video, reason) { if (!video || !video.isConnected) return; const state = videoState(video); const isGestureRetry = reason === 'gesture' && state.awaitingGesture; const isHotkeyRetry = reason === 'hotkey'; if (state.autoplayAttempted && !isGestureRetry && !isHotkeyRetry) return; if (!video.paused && !video.ended) { state.autoplayAttempted = true; state.awaitingGesture = false; return; } state.autoplayAttempted = true; video.autoplay = true; video.setAttribute('playsinline', ''); video.setAttribute('webkit-playsinline', ''); video.preload = 'auto'; const playPromise = video.play(); if (playPromise && typeof playPromise.catch === 'function') { playPromise.catch((err) => { const name = err && err.name ? err.name : ''; if (name === 'NotAllowedError') { log('autoplay blocked, waiting for gesture', reason || ''); state.awaitingGesture = true; hookGestureRetry(video); clickPlayButton(video); } else { log('play failed', name || err); } }); } if (video.currentTime < 1) { state.nextTriggered = false; } } function prepareVideo(video) { if (!(video instanceof HTMLMediaElement) || prepared.has(video)) return; const rect = video.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0 && rect.width < MIN_VISIBLE_WIDTH && rect.height < MIN_VISIBLE_HEIGHT) { return; } prepared.add(video); const onReady = () => tryAutoplay(video, 'ready'); const onEnded = () => { const state = videoState(video); if (state.nextTriggered) return; state.nextTriggered = true; if (!clickNextButton(video)) { log('no next target found'); } }; const onPlay = () => { const state = videoState(video); state.gestureHooked = false; state.autoplayAttempted = true; state.awaitingGesture = false; if (video.currentTime < 1) { state.nextTriggered = false; } }; const onEmptied = () => { const state = videoState(video); state.lastSrc = video.currentSrc || video.src || ''; state.gestureHooked = false; state.nextTriggered = false; state.autoplayAttempted = false; state.awaitingGesture = false; }; ['loadedmetadata', 'loadeddata', 'canplay'].forEach((evt) => { video.addEventListener(evt, onReady, { passive: true }); }); video.addEventListener('ended', onEnded, { passive: true }); video.addEventListener('play', onPlay, { passive: true }); video.addEventListener('emptied', onEmptied, { passive: true }); tryAutoplay(video, 'scan'); } function scan() { for (const video of document.querySelectorAll('video')) { prepareVideo(video); } } function scheduleScan() { if (scanTimer) return; scanTimer = setTimeout(() => { scanTimer = null; scan(); }, 120); } document.addEventListener('keydown', (event) => { if (!event.altKey || !event.shiftKey || event.defaultPrevented) return; if (event.code === 'KeyP') { const video = document.querySelector('video'); if (video) { event.preventDefault(); tryAutoplay(video, 'hotkey'); } } else if (event.code === 'KeyN') { const video = document.querySelector('video'); if (video) { event.preventDefault(); clickNextButton(video); } } }, true); const observer = new MutationObserver(scheduleScan); const root = document.documentElement || document.body; if (root) { observer.observe(root, { childList: true, subtree: true }); } window.addEventListener('load', scheduleScan, { passive: true }); document.addEventListener('DOMContentLoaded', scheduleScan, { passive: true }); scheduleScan(); })();