Feature: Watch Together - History, Titel-Fetch, Next-Button

Server:
- Video-History Tracking (max 50 Einträge pro Raum)
- History wird bei Skip und Play-Video gespeichert
- Server-seitiger Titel-Fetch via noembed.com als Fallback

Client:
- Aufklappbare History-Sektion im Queue-Panel
- "Weiter" Button mit Text-Label statt nur Icon
- YouTube-Thumbnails in der Warteschlange
- History in RoomState integriert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-07 15:31:56 +01:00
parent a1a1f31c8e
commit 6c57419959
3 changed files with 169 additions and 6 deletions

View file

@ -12,6 +12,13 @@ interface QueueItem {
addedBy: string;
}
interface HistoryItem {
url: string;
title: string;
addedBy: string;
playedAt: string; // ISO timestamp
}
interface RoomMember {
id: string;
name: string;
@ -29,6 +36,7 @@ interface Room {
currentTime: number;
lastSyncAt: number;
queue: QueueItem[];
history: HistoryItem[];
}
interface WtClient {
@ -78,6 +86,7 @@ function getPlaybackState(room: Room): Record<string, any> {
playing: room.playing,
currentTime,
updatedAt: Date.now(),
history: room.history,
};
}
@ -92,6 +101,7 @@ function serializeRoom(room: Room): Record<string, any> {
playing: room.playing,
currentTime: room.currentTime + (room.playing ? (Date.now() - room.lastSyncAt) / 1000 : 0),
queue: room.queue,
history: room.history,
};
}
@ -176,7 +186,7 @@ function handleDisconnect(client: WtClient): void {
// ── WebSocket Message Handler ──
function handleMessage(client: WtClient, msg: any): void {
async function handleMessage(client: WtClient, msg: any): Promise<void> {
switch (msg.type) {
case 'create_room': {
if (client.roomId) {
@ -203,6 +213,7 @@ function handleMessage(client: WtClient, msg: any): void {
currentTime: 0,
lastSyncAt: Date.now(),
queue: [],
history: [],
};
rooms.set(roomId, room);
@ -260,7 +271,17 @@ function handleMessage(client: WtClient, msg: any): void {
sendTo(client, { type: 'error', code: 'INVALID_URL', message: 'URL darf nicht leer sein.' });
return;
}
const title = String(msg.title || url).slice(0, 128);
let title = String(msg.title || '').trim();
if (!title || title === url) {
try {
const resp = await fetch(`https://noembed.com/embed?url=${encodeURIComponent(url)}`);
if (resp.ok) {
const data = await resp.json() as Record<string, any>;
if (data.title) title = String(data.title).slice(0, 128);
}
} catch {}
}
if (!title) title = url;
room.queue.push({ url, title, addedBy: client.name });
sendToRoom(room.id, { type: 'queue_updated', queue: room.queue });
@ -325,6 +346,17 @@ function handleMessage(client: WtClient, msg: any): void {
return;
}
// Track current video in history before replacing
if (room.currentVideo) {
room.history.push({
url: room.currentVideo.url,
title: room.currentVideo.title,
addedBy: '',
playedAt: new Date().toISOString(),
});
if (room.history.length > 50) room.history = room.history.slice(-50);
}
const index = msg.index != null ? Number(msg.index) : undefined;
if (index !== undefined) {
if (index < 0 || index >= room.queue.length) {
@ -405,6 +437,17 @@ function handleMessage(client: WtClient, msg: any): void {
sendTo(client, { type: 'error', code: 'NOT_HOST', message: 'Nur der Host kann die Wiedergabe steuern.' });
return;
}
// Track current video in history before skipping
if (room.currentVideo) {
room.history.push({
url: room.currentVideo.url,
title: room.currentVideo.title,
addedBy: '',
playedAt: new Date().toISOString(),
});
// Keep history max 50 items
if (room.history.length > 50) room.history = room.history.slice(-50);
}
if (room.queue.length > 0) {
const item = room.queue.shift()!;
room.currentVideo = { url: item.url, title: item.title };