peertube-plugin-youth-schedule
Version:
PeerTube plugin: youth protection schedule (configurable HH:mm slots and end time)
239 lines (213 loc) • 8.36 kB
JavaScript
async function register({ registerHook, storageManager, getRouter, registerSetting, settingsManager }) {
// Настройки плагина: источник времени (user/server)
if (registerSetting) {
registerSetting({
name: 'timeSource',
label: 'Zeitquelle',
type: 'select',
default: 'server',
descriptionHTML: 'Welche Zeit wird verwendet, um Verfügbarkeit zu prüfen?',
options: [
{ value: 'server', label: 'Serverzeit' },
{ value: 'user', label: 'Browserzeit (Benutzer)' }
]
})
// Редактируемые временные слоты, CSV часов/минут, например: 20,22:30,23:15
registerSetting({
name: 'timeSlots',
label: 'Sendezeit-Slots (CSV HH:mm oder HH)',
type: 'input',
default: '20,22,23',
descriptionHTML: 'Zulässige Startzeiten als CSV, z.B. 20,22:30,23:15. Gültig 00:00–23:59.'
})
// Конец окна доступности
registerSetting({
name: 'endTime',
label: 'Endzeit (HH:mm)',
type: 'input',
default: '06:00',
descriptionHTML: 'Zeitpunkt, bis wann Inhalte sichtbar sind. Format HH:mm. Kann kleiner als Startzeit sein (über Mitternacht).'
})
}
const router = getRouter()
// Роут: получить конфиг плагина для клиента
router.get('/config', async (_req, res) => {
try {
const timeSource = (await getSetting(settingsManager, 'timeSource')) || 'server'
const slotsCsv = (await getSetting(settingsManager, 'timeSlots')) || '20,22,23'
const endTimeStr = normalizeTimeString((await getSetting(settingsManager, 'endTime')) || '06:00')
const slotMinutes = parseSlotsToMinutes(slotsCsv)
const slotLabels = slotMinutes.map(m => minutesToLabel(m))
res.json({ timeSource, slots: slotLabels, endTime: endTimeStr })
} catch (err) {
res.status(500).json({ error: err.message })
}
})
// Роут: проверка доступности видео
router.get('/video/:uuid/youth-allowed', async (req, res) => {
try {
const videoUuid = req.params.uuid
const meta = await storageManager.getData('youth-schedule-' + videoUuid)
// Блокируем только если в метаданных включён чекбокс
if (!meta || !meta.youthSensitive) return res.json({ allowed: true, debug: { reason: 'not-sensitive', youthSensitive: !!(meta && meta.youthSensitive) } })
const timeSource = (await getSetting(settingsManager, 'timeSource')) || 'server'
const endTimeStr = normalizeTimeString((await getSetting(settingsManager, 'endTime')) || '06:00')
const endMinutes = timeStringToMinutes(endTimeStr)
// Текущее время в минутах суток
const now = new Date()
let currentMinutes
if (timeSource === 'server') {
currentMinutes = now.getHours() * 60 + now.getMinutes()
} else {
const cm = Number(req.query.clientMinutes)
const ch = Number(req.query.clientHour)
if (Number.isFinite(cm)) currentMinutes = cm
else if (Number.isFinite(ch)) currentMinutes = ch * 60
else currentMinutes = now.getHours() * 60 + now.getMinutes()
}
// Время старта из меты (минуты). Поддерживаем старый формат часов
let openMinutes = 0
if (meta.youthOpenMinutes) {
const parsed = Number(meta.youthOpenMinutes)
openMinutes = Number.isFinite(parsed) ? clampMinutes(parsed) : 0
} else if (meta.youthOpenHour) {
const h = parseInt(meta.youthOpenHour, 10)
openMinutes = Number.isFinite(h) ? clampMinutes(h * 60) : 0
}
if (!openMinutes) return res.json({ allowed: true, debug: { reason: 'no-openMinutes', youthSensitive: true } })
const allowed = isWithinAllowedWindowMinutes(currentMinutes, openMinutes, endMinutes)
return res.json({
allowed,
openLabel: minutesToLabel(openMinutes),
endLabel: endTimeStr,
debug: { timeSource, currentMinutes, openMinutes, endMinutes }
})
} catch (err) {
res.status(500).json({ error: err.message })
}
})
// Сохранение и восстановление метаданных при апдейте видео
registerHook({
target: 'action:api.video.updated',
handler: ({ video, body }) => {
const pluginData = body?.pluginData || {}
const json = {
youthSensitive: !!pluginData.youthSensitive,
youthOpenMinutes: normalizeOpenMinutes(pluginData)
}
if (json.youthSensitive || json.youthOpenMinutes) {
storageManager.storeData('youth-schedule-' + video.uuid, json)
}
}
})
// При создании тоже сохраняем
registerHook({
target: 'action:api.video.uploaded',
handler: ({ video, body }) => {
const pluginData = body?.pluginData || {}
const json = {
youthSensitive: !!pluginData.youthSensitive,
youthOpenMinutes: normalizeOpenMinutes(pluginData)
}
if (json.youthSensitive || json.youthOpenMinutes) {
storageManager.storeData('youth-schedule-' + video.uuid, json)
}
}
})
// Отдаём данные вместе с видео
registerHook({
target: 'filter:api.video.get.result',
handler: async (video) => {
if (!video) return video
if (!video.pluginData) video.pluginData = {}
const result = await storageManager.getData('youth-schedule-' + video.uuid)
if (result) {
Object.assign(video.pluginData, result)
// Совместимость: для старых форм с часами
if (!video.pluginData.youthOpenMinutes && video.pluginData.youthOpenHour) {
const h = parseInt(video.pluginData.youthOpenHour, 10)
if (Number.isFinite(h)) video.pluginData.youthOpenMinutes = h * 60
}
}
return video
}
})
}
function isWithinAllowedWindowMinutes(currentMinutes, openMinutes, endMinutes) {
// Если окно не пересекает полночь
if (openMinutes < endMinutes) {
return currentMinutes >= openMinutes && currentMinutes < endMinutes
}
// Окно через полночь: [open..24h) U [0..end)
if (openMinutes > endMinutes) {
return currentMinutes >= openMinutes || currentMinutes < endMinutes
}
// Равенство: трактуем как 24 часа доступности
return true
}
function parseSlotsToMinutes(csv) {
return String(csv)
.split(',')
.map(s => s.trim())
.filter(Boolean)
.map(timeStringToMinutes)
.filter(n => Number.isFinite(n))
.map(clampMinutes)
}
function clampMinutes(v) {
if (!Number.isFinite(v)) return 0
if (v < 0) return 0
if (v >= 24 * 60) return 24 * 60 - 1
return v
}
function minutesToLabel(minutes) {
const h = Math.floor(minutes / 60)
const m = minutes % 60
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
}
function normalizeTimeString(str) {
const m = timeStringToMinutes(str)
return minutesToLabel(m)
}
function timeStringToMinutes(str) {
// Поддержка 'H', 'HH', 'H:mm', 'HH:mm'
const s = String(str).trim()
if (!s) return 0
if (/^\d{1,2}$/.test(s)) {
const h = Math.min(23, Math.max(0, parseInt(s, 10)))
return h * 60
}
const m = s.match(/^(\d{1,2}):(\d{1,2})$/)
if (m) {
const h = Math.min(23, Math.max(0, parseInt(m[1], 10)))
const mm = Math.min(59, Math.max(0, parseInt(m[2], 10)))
return h * 60 + mm
}
const n = Number(s)
if (Number.isFinite(n)) return clampMinutes(n)
return 0
}
function normalizeOpenMinutes(pluginData) {
if (!pluginData) return 0
// Новый формат: минутный
if (pluginData.youthOpenMinutes != null && pluginData.youthOpenMinutes !== '') {
const v = Number(pluginData.youthOpenMinutes)
if (Number.isFinite(v)) return clampMinutes(v)
}
// Старый формат: час
if (pluginData.youthOpenHour != null && pluginData.youthOpenHour !== '') {
const h = parseInt(pluginData.youthOpenHour, 10)
if (Number.isFinite(h)) return clampMinutes(h * 60)
}
return 0
}
async function getSetting(settingsManager, name) {
if (!settingsManager || !settingsManager.getSetting) return null
try {
return await settingsManager.getSetting(name)
} catch (_) {
return null
}
}
async function unregister() { return }
module.exports = { register, unregister }