UNPKG

peertube-plugin-youth-schedule

Version:

PeerTube plugin: youth protection schedule (configurable HH:mm slots and end time)

179 lines (156 loc) 7.58 kB
let __youthScheduleDecision = { decided: false, allowed: true, open: null, end: null } async function register({ registerHook }) { const config = await fetch('/plugins/youth-schedule/router/config') .then(r => r.json()) .catch(() => ({ timeSource: 'server', endTime: '06:00' })) const getNowMinutes = () => { const n = new Date() return n.getHours() * 60 + n.getMinutes() } async function checkAllowed(videoUuid) { try { const params = new URLSearchParams() if (config.timeSource === 'user') params.set('clientMinutes', String(getNowMinutes())) const url = `/plugins/youth-schedule/router/video/${videoUuid}/youth-allowed?${params}` const res = await fetch(url) if (!res.ok) return { allowed: true } const data = await res.json() return data } catch (_) { return { allowed: true } } } function getPlayerRoot() { // Приоритет: специальные контейнеры PeerTube и обёртки Video.js const candidates = [ '#plugin-selector-player-container', '#videojs-wrapper', '#video-wrapper', '#player' ] for (const sel of candidates) { const el = document.querySelector(sel) if (el) { console.log('[YouthSchedule] player root via selector:', sel) return el } } // Пробуем сам контейнер Video.js const vjsContainer = document.querySelector('.video-js.vjs-peertube-skin') || document.querySelector('[id^="vjs_video_"]') if (vjsContainer) { console.log('[YouthSchedule] player root via videojs container') // Если есть родитель-обёртка — используем её, иначе сам контейнер const wrapper = vjsContainer.closest('#videojs-wrapper, #video-wrapper') return wrapper || vjsContainer } // Пробуем найти <video> и подняться к обёртке const vid = document.querySelector('video.vjs-tech') || document.querySelector('video') if (vid) { const outer = vid.closest('#videojs-wrapper, #video-wrapper, .video-js') || vid.parentElement if (outer) { console.log('[YouthSchedule] player root via video parent') return outer } } console.warn('[YouthSchedule] player root not found; please share DOM around <video>.') return null } // CSS стили if (!document.getElementById('youth-schedule-style')) { const style = document.createElement('style') style.id = 'youth-schedule-style' style.textContent = ` .youth-schedule-init-hidden { visibility: hidden !important; } .youth-schedule-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.75); display: flex; align-items: center; justify-content: center; z-index: 2147483647; padding: 16px; text-align: center; } .youth-schedule-overlay .msg { max-width: 720px; color: #fff; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif; } .youth-schedule-overlay .msg .title { font-size: 1.1rem; margin-bottom: 8px; } .youth-schedule-overlay .msg .subtitle { font-size: .95rem; opacity: .9; } .youth-schedule-block-video video, .youth-schedule-block-video .video-js { pointer-events: none !important; } ` document.head.appendChild(style) } function applyOverlay(playerRoot, open, end) { if (!playerRoot) return const prevPos = getComputedStyle(playerRoot).position if (prevPos === 'static' || !prevPos) playerRoot.style.position = 'relative' playerRoot.classList.add('youth-schedule-block-video') let overlay = playerRoot.querySelector('.youth-schedule-overlay') if (!overlay) { overlay = document.createElement('div') overlay.className = 'youth-schedule-overlay' overlay.innerHTML = ` <div class="msg"> <div class="title">Dieses Video ist aus Jugendschutzgründen derzeit nicht verfügbar.</div> <div class="subtitle">Freigabe von ${open || '—'} bis ${end}.</div> </div> ` playerRoot.appendChild(overlay) } } // Важно: не деструктурируем параметры — некоторые версии PeerTube вызывают init без аргумента registerHook({ target: 'action:video-watch.init', handler: async (ctx) => { try { const playerRoot = getPlayerRoot() if (playerRoot) playerRoot.classList.add('youth-schedule-init-hidden') const videoUuid = ctx && ctx.video && ctx.video.uuid if (!videoUuid) { // Нет UUID на ранней стадии — просто покажем контейнер, проверим позже в player.loaded if (playerRoot) playerRoot.classList.remove('youth-schedule-init-hidden') return } const data = await checkAllowed(videoUuid) __youthScheduleDecision = { decided: true, allowed: !!data.allowed, open: data.openLabel || null, end: data.endLabel || config.endTime || '06:00' } console.log('[YouthSchedule:init] decision', __youthScheduleDecision) if (playerRoot) playerRoot.classList.remove('youth-schedule-init-hidden') if (!__youthScheduleDecision.allowed) { applyOverlay(playerRoot, __youthScheduleDecision.open, __youthScheduleDecision.end) const htmlVideo = playerRoot?.querySelector('video') try { htmlVideo && htmlVideo.pause && htmlVideo.pause() } catch (_) {} } } catch (e) { console.warn('[YouthSchedule:init] error', e) } } }) // Подстраховка: отключаем контролы при сборке опций плеера, если уже известно, что блокируем registerHook({ target: 'filter:internal.video-watch.player.build-options.result', handler: (options) => { try { if (__youthScheduleDecision.decided && __youthScheduleDecision.allowed === false) { options.controls = false options.responsive = false options.fluid = false options.autoplay = false if (!options.userActions) options.userActions = {} options.userActions.hotkeys = false } } catch (_) {} return options } }) // Основная логика: дублирующая проверка и оверлей на загруженном плеере registerHook({ target: 'action:video-watch.player.loaded', handler: async ({ video }) => { try { if (!__youthScheduleDecision.decided) { const data = await checkAllowed(video.uuid) __youthScheduleDecision = { decided: true, allowed: !!data.allowed, open: data.openLabel || null, end: data.endLabel || config.endTime || '06:00' } console.log('[YouthSchedule:loaded] decision', __youthScheduleDecision) } if (__youthScheduleDecision.allowed) return const playerRoot = getPlayerRoot() if (!playerRoot) return const htmlVideo = playerRoot.querySelector('video') try { htmlVideo && htmlVideo.pause && htmlVideo.pause() } catch (_) {} applyOverlay(playerRoot, __youthScheduleDecision.open, __youthScheduleDecision.end) } catch (_) { // игнорируем } } }) } export { register }