mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-06 08:09:24 +00:00
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
260 lines
8.2 KiB
TypeScript
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;
|
|
}
|