watchparty goes to next episode with host

This commit is contained in:
Pas 2025-10-23 09:50:51 -06:00
parent bf530902cc
commit 83840594a6
2 changed files with 136 additions and 17 deletions

View file

@ -1,5 +1,6 @@
import { t } from "i18next";
import { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { getRoomStatuses, sendPlayerStatus } from "@/backend/player/status";
import { usePlayerStatusPolling } from "@/components/player/hooks/usePlayerStatusPolling";
@ -24,6 +25,8 @@ export function WatchPartyReporter() {
const lastReportTime = useRef<number>(0);
const lastReportedStateRef = useRef<string>("");
const contentValidatedRef = useRef<boolean>(false);
const hostEpisodeRef = useRef<{ seasonId?: number; episodeId?: number }>({});
const navigate = useNavigate();
// Auth data
const account = useAuthStore((s) => s.account);
@ -45,6 +48,7 @@ export function WatchPartyReporter() {
useEffect(() => {
if (!watchPartyEnabled) {
contentValidatedRef.current = false;
hostEpisodeRef.current = {};
}
}, [watchPartyEnabled]);
@ -93,6 +97,14 @@ export function WatchPartyReporter() {
? parseInt(meta.episode.tmdbId, 10)
: undefined;
// Initialize host episode tracking
if (hostSeasonId && hostEpisodeId) {
hostEpisodeRef.current = {
seasonId: hostSeasonId,
episodeId: hostEpisodeId,
};
}
// Validate episode match (if host has this info)
if (
(hostSeasonId &&
@ -144,6 +156,112 @@ export function WatchPartyReporter() {
disable,
]);
// Monitor for episode changes from host and auto-navigate guests
useEffect(() => {
if (
!watchPartyEnabled ||
!roomCode ||
isHost ||
!meta?.tmdbId ||
meta.type !== "show"
) {
return;
}
const checkForEpisodeChange = async () => {
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.type !== "TV Show") return;
const hostSeasonId = hostUser.content.seasonId;
const hostEpisodeId = hostUser.content.episodeId;
// Initialize host episode ref on first check
if (
!hostEpisodeRef.current.seasonId &&
!hostEpisodeRef.current.episodeId
) {
hostEpisodeRef.current = {
seasonId: hostSeasonId,
episodeId: hostEpisodeId,
};
return;
}
// Check if host has changed episodes
const hasHostChangedEpisode =
hostSeasonId !== hostEpisodeRef.current.seasonId ||
hostEpisodeId !== hostEpisodeRef.current.episodeId;
if (hasHostChangedEpisode && hostSeasonId && hostEpisodeId) {
// Update our reference
hostEpisodeRef.current = {
seasonId: hostSeasonId,
episodeId: hostEpisodeId,
};
// Check if we're already on the correct episode
const currentSeasonId = meta.season?.tmdbId
? parseInt(meta.season.tmdbId, 10)
: undefined;
const currentEpisodeId = meta.episode?.tmdbId
? parseInt(meta.episode.tmdbId, 10)
: undefined;
if (
currentSeasonId === hostSeasonId &&
currentEpisodeId === hostEpisodeId
) {
// Already on the correct episode
return;
}
// Navigate to the new episode
// eslint-disable-next-line no-console
console.log("Host changed episode, following to new episode:", {
seasonId: hostSeasonId,
episodeId: hostEpisodeId,
});
const url = new URL(
`/media/tmdb-tv-${meta.tmdbId}/${hostSeasonId}/${hostEpisodeId}`,
window.location.origin,
);
url.searchParams.set("watchparty", roomCode);
// Reset content validation so it re-validates on the new episode
contentValidatedRef.current = false;
navigate(url.pathname + url.search);
}
} catch (error) {
console.error("Failed to check for episode change:", error);
}
};
// Check every 3 seconds for episode changes
const interval = setInterval(checkForEpisodeChange, 3000);
// Initial check
checkForEpisodeChange();
return () => clearInterval(interval);
}, [
watchPartyEnabled,
roomCode,
isHost,
meta?.tmdbId,
meta?.season?.tmdbId,
meta?.episode?.tmdbId,
meta?.type,
backendUrl,
account,
navigate,
]);
useEffect(() => {
// Skip if watch party is not enabled
if (

View file

@ -11,41 +11,42 @@ export function WatchPartyResetter() {
const meta = usePlayerStore((s) => s.meta);
const { disable } = useWatchPartyStore();
// Store the current meta to track changes
const previousMetaRef = useRef<string | null>(null);
// Store the current base media to track changes
const previousBaseMediaRef = useRef<string | null>(null);
// Memoize the metaId calculation
const metaId = useMemo(() => {
// Memoize the base media ID (without episode details for shows)
// This allows episode changes within the same show to keep the room active
const baseMediaId = useMemo(() => {
if (!meta) return null;
return meta.type === "show"
? `${meta.type}-${meta.tmdbId}-s${meta.season?.tmdbId || "0"}-e${meta.episode?.tmdbId || "0"}`
: `${meta.type}-${meta.tmdbId}`;
// For shows, only track the show ID, not the episode
// This allows episode navigation within the same show
return `${meta.type}-${meta.tmdbId}`;
}, [meta]);
useEffect(() => {
// If meta exists but has changed, reset watch party
// If base media has changed (different show/movie), reset watch party
if (
metaId &&
previousMetaRef.current &&
metaId !== previousMetaRef.current
baseMediaId &&
previousBaseMediaRef.current &&
baseMediaId !== previousBaseMediaRef.current
) {
// eslint-disable-next-line no-console
console.log("Media changed, disabling watch party:", {
previous: previousMetaRef.current,
current: metaId,
console.log("Base media changed, disabling watch party:", {
previous: previousBaseMediaRef.current,
current: baseMediaId,
});
disable();
}
// Update the ref with current meta
previousMetaRef.current = metaId;
// Update the ref with current base media
previousBaseMediaRef.current = baseMediaId;
// Also reset when component unmounts (player exited)
return () => {
disable();
};
}, [metaId, disable]);
}, [baseMediaId, disable]);
return null; // This component doesn't render anything
}