Files
ytplayer/server/party-dj.js
2026-10-03 14:52:10 +08:00

27 lines
1.9 KiB
JavaScript

// Server-authoritative queue. Votes belong to the participant id across reconnects.
export function createDJQueue() { return { enabled: false, entries: [], sequence: 0 }; }
export function queueSnapshot(queue) {
return { enabled: queue.enabled, entries: [...queue.entries].sort((a, b) => Number(b.approved) - Number(a.approved) || b.votes.size - a.votes.size || a.order - b.order).map(({ votes, order, ...entry }) => ({ ...entry, votes: [...votes] })) };
}
export function changeDJQueue(queue, message, participant, host) {
if (message.action === 'enable') { if (!host) return false; queue.enabled = !!message.enabled; return true; }
if (!queue.enabled) return false;
const entry = queue.entries.find(e => e.video.id === message.id);
if (message.action === 'suggest') {
const v = message.video; if (!v || !/^[A-Za-z0-9_-]{11}$/.test(v.id) || queue.entries.length >= 100 || queue.entries.some(e => e.video.id === v.id)) return false;
const clean = value => String(value || '').replace(/[\u0000-\u001f<>]/g, '').slice(0, 200);
queue.entries.push({ video: { id: v.id, title: clean(v.title) || v.id, channel: clean(v.channel), thumbnail: `/api/thumb/${v.id}` }, from: participant, approved: host, votes: new Set(), order: queue.sequence++ }); return true;
}
if (!entry) return false;
if (message.action === 'vote') { if (entry.votes.has(participant)) return false; entry.votes.add(participant); return true; }
if (!host) return false;
if (message.action === 'approve') { entry.approved = true; return true; }
if (message.action === 'remove') { queue.entries = queue.entries.filter(e => e !== entry); return true; }
return false;
}
export function takeDJVideo(queue) {
if (!queue.enabled) return null;
const next = queueSnapshot(queue).entries.find(e => e.approved); if (!next) return null;
queue.entries = queue.entries.filter(e => e.video.id !== next.video.id); return next.video;
}