p-stream/src/components/player/internals/WebhookReporter.tsx
Pas fb3bc161ce Merge: Add Watch Party
commit 6034a1ebeaa97a97552dc249f97cc935dcbd1cd6
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 20:02:36 2025 -0600

    update stuff

commit 91d1370668a3e05fed3687ffef697a96c28a0b2c
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 19:47:34 2025 -0600

    Update Downloads.tsx

commit 1c25c88175abebe761d27194f22eec9fd3bcf2e1
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 19:46:27 2025 -0600

    clean some more stuff

commit d6c24e76348c135f3c1ae0444491ff0b302eca2e
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 19:38:07 2025 -0600

    clean this again

commit 6511de68a1b1397e4884dfc6e6f0599497b9afee
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 19:30:08 2025 -0600

    clean up a bit

commit 467358c1f50c1555b42f9ae8d22f955ebc9bba1b
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 17:45:04 2025 -0600

    validate content

commit 59d4a1665b32bdf4ca453859816a2245b184b025
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 17:35:22 2025 -0600

    add auto join link

commit 6f3c334d2157f1c82f9d26e9a7db49371e6a2b5e
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 17:08:35 2025 -0600

    watch partyyyy

commit 1497377692fba86ea1ef40191dbaa648abc38e2e
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 13:56:04 2025 -0600

    add watch party view stuff

commit 80003f78d0adf6071d4f2c6b6a6d8fdcb2aa204a
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 13:14:07 2025 -0600

    init sending webhooks

    testing on a discord hook

commit f5293c2eae5d5a12be6153ab37e50214289e20b6
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 12:31:41 2025 -0600

    remove duplicate class

commit 7871162e6b2c580eb2bcb9c8abc5ecc19d706a06
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 12:25:49 2025 -0600

    update legacy button

commit a2948f3aa1b7481a3ac5b69e2e4dab71552816de
Author: Pas <74743263+Pasithea0@users.noreply.github.com>
Date:   Sat May 17 12:21:52 2025 -0600

    move watchparty to new atoms menu
2025-05-17 20:04:40 -06:00

260 lines
8.2 KiB
TypeScript

import { t } from "i18next";
import { useEffect, useRef } from "react";
import { getRoomStatuses, sendPlayerStatus } from "@/backend/player/status";
import { usePlayerStatusPolling } from "@/components/player/hooks/usePlayerStatusPolling";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { useAuthStore } from "@/stores/auth";
import { usePlayerStore } from "@/stores/player/store";
import { useWatchPartyStore } from "@/stores/watchParty";
// Event for content validation status
const VALIDATION_EVENT = "watchparty:validation";
export const emitValidationStatus = (success: boolean) => {
window.dispatchEvent(
new CustomEvent(VALIDATION_EVENT, { detail: { success } }),
);
};
/**
* Component that sends player status to the backend when watch party is enabled
*/
export function WebhookReporter() {
const { statusHistory, latestStatus } = usePlayerStatusPolling(5); // Keep last 5 status points
const lastReportTime = useRef<number>(0);
const lastReportedStateRef = useRef<string>("");
const contentValidatedRef = useRef<boolean>(false);
// Auth data
const account = useAuthStore((s) => s.account);
const userId = account?.userId || "guest";
const backendUrl = useBackendUrl();
// Player metadata
const meta = usePlayerStore((s) => s.meta);
// Watch party state
const {
enabled: watchPartyEnabled,
roomCode,
isHost,
disable,
} = useWatchPartyStore();
// Reset validation state when watch party is disabled
useEffect(() => {
if (!watchPartyEnabled) {
contentValidatedRef.current = false;
}
}, [watchPartyEnabled]);
// Validate content matches when joining a room
useEffect(() => {
const validateContent = async () => {
if (
!watchPartyEnabled ||
!roomCode ||
!meta?.tmdbId ||
isHost ||
contentValidatedRef.current
)
return;
try {
const roomData = await getRoomStatuses(backendUrl, account, roomCode);
const users = Object.values(roomData.users).flat();
const hostUser = users.find((user) => user.isHost);
if (hostUser && hostUser.content.tmdbId) {
const hostTmdbId = hostUser.content.tmdbId;
const currentTmdbId = parseInt(meta.tmdbId, 10);
// Check base content ID (movie or show)
if (hostTmdbId !== currentTmdbId) {
console.error("Content mismatch - disconnecting from watch party", {
hostContent: hostTmdbId,
currentContent: currentTmdbId,
});
disable();
emitValidationStatus(false);
// eslint-disable-next-line no-alert
alert(t("watchParty.contentMismatch"));
return;
}
// Check season and episode IDs for TV shows
if (meta.type === "show" && hostUser.content.type === "TV Show") {
const hostSeasonId = hostUser.content.seasonId;
const hostEpisodeId = hostUser.content.episodeId;
const currentSeasonId = meta.season?.tmdbId
? parseInt(meta.season.tmdbId, 10)
: undefined;
const currentEpisodeId = meta.episode?.tmdbId
? parseInt(meta.episode.tmdbId, 10)
: undefined;
// Validate episode match (if host has this info)
if (
(hostSeasonId &&
currentSeasonId &&
hostSeasonId !== currentSeasonId) ||
(hostEpisodeId &&
currentEpisodeId &&
hostEpisodeId !== currentEpisodeId)
) {
console.error(
"Episode mismatch - disconnecting from watch party",
{
host: { seasonId: hostSeasonId, episodeId: hostEpisodeId },
current: {
seasonId: currentSeasonId,
episodeId: currentEpisodeId,
},
},
);
disable();
emitValidationStatus(false);
// eslint-disable-next-line no-alert
alert(t("watchParty.episodeMismatch"));
return;
}
}
}
contentValidatedRef.current = true;
emitValidationStatus(true);
} catch (error) {
console.error("Failed to validate watch party content:", error);
disable();
emitValidationStatus(false);
}
};
validateContent();
}, [
watchPartyEnabled,
roomCode,
meta?.tmdbId,
meta?.season?.tmdbId,
meta?.episode?.tmdbId,
meta?.type,
isHost,
backendUrl,
account,
disable,
]);
useEffect(() => {
// Skip if watch party is not enabled
if (
!watchPartyEnabled ||
!latestStatus ||
!latestStatus.hasPlayedOnce ||
!roomCode ||
(!isHost && !contentValidatedRef.current) // Don't send updates until content is validated for non-hosts
)
return;
const now = Date.now();
// Create a state fingerprint to detect meaningful changes
// Use more precise time tracking (round to nearest second) to detect smaller changes
const stateFingerprint = JSON.stringify({
isPlaying: latestStatus.isPlaying,
isPaused: latestStatus.isPaused,
isLoading: latestStatus.isLoading,
time: Math.floor(latestStatus.time), // Track seconds directly
// volume: Math.round(latestStatus.volume * 100),
playbackRate: latestStatus.playbackRate,
});
// Check if state has changed meaningfully OR
// it's been at least 2 seconds since last report
const hasStateChanged = stateFingerprint !== lastReportedStateRef.current;
const timeThresholdMet = now - lastReportTime.current >= 10000; // Less frequent updates (10s)
// Always update more frequently if we're the host to ensure guests stay in sync
const shouldUpdateForHost = isHost && now - lastReportTime.current >= 1000;
if (!hasStateChanged && !timeThresholdMet && !shouldUpdateForHost) return;
// Prepare content information
let contentTitle = "Unknown content";
let contentType = "Unknown";
if (meta) {
if (meta.type === "movie") {
contentTitle = meta.title;
contentType = "Movie";
} else if (meta.type === "show" && meta.episode) {
contentTitle = `${meta.title} - S${meta.season?.number || 0}E${meta.episode.number || 0}`;
contentType = "TV Show";
}
}
// Send player status to backend API
const sendStatusToBackend = async () => {
try {
await sendPlayerStatus(backendUrl, account, {
userId,
roomCode,
isHost,
content: {
title: contentTitle,
type: contentType,
tmdbId: meta?.tmdbId ? Number(meta.tmdbId) : 0,
seasonId: meta?.season?.tmdbId
? Number(meta.season.tmdbId)
: undefined,
episodeId: meta?.episode?.tmdbId
? Number(meta.episode.tmdbId)
: undefined,
seasonNumber: meta?.season?.number,
episodeNumber: meta?.episode?.number,
},
player: {
isPlaying: latestStatus.isPlaying,
isPaused: latestStatus.isPaused,
isLoading: latestStatus.isLoading,
hasPlayedOnce: latestStatus.hasPlayedOnce,
time: latestStatus.time,
duration: latestStatus.duration,
// volume: latestStatus.volume,
playbackRate: latestStatus.playbackRate,
buffered: latestStatus.buffered,
},
});
// Update last report time and fingerprint
lastReportTime.current = now;
lastReportedStateRef.current = stateFingerprint;
// eslint-disable-next-line no-console
console.log("Sent player status update to backend", {
time: new Date().toISOString(),
isPlaying: latestStatus.isPlaying,
currentTime: Math.floor(latestStatus.time),
userId,
content: contentTitle,
roomCode,
});
} catch (error) {
console.error("Failed to send player status to backend", error);
}
};
sendStatusToBackend();
}, [
latestStatus,
statusHistory.length,
userId,
account,
meta,
watchPartyEnabled,
roomCode,
isHost,
backendUrl,
]);
return null;
}