diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9b6177b2..831c76bd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -44,7 +44,7 @@ importers:
version: 1.8.0
'@p-stream/providers':
specifier: github:p-stream/providers#production
- version: https://codeload.github.com/p-stream/providers/tar.gz/f86bacfb657781183c5ffc4e2dc68665cebc21bc
+ version: https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957
'@plasmohq/messaging':
specifier: ^0.6.2
version: 0.6.2(react@18.3.1)
@@ -1207,8 +1207,8 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
- '@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/f86bacfb657781183c5ffc4e2dc68665cebc21bc':
- resolution: {tarball: https://codeload.github.com/p-stream/providers/tar.gz/f86bacfb657781183c5ffc4e2dc68665cebc21bc}
+ '@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957':
+ resolution: {tarball: https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957}
version: 3.2.0
'@pkgjs/parseargs@0.11.0':
@@ -5524,7 +5524,7 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
- '@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/f86bacfb657781183c5ffc4e2dc68665cebc21bc':
+ '@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957':
dependencies:
abort-controller: 3.0.0
cheerio: 1.0.0-rc.12
diff --git a/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx b/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx
index ed2259f9..e229e60f 100644
--- a/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx
+++ b/src/components/overlays/detailsModal/components/sections/DetailsBody.tsx
@@ -103,10 +103,8 @@ export function DetailsBody({
if (hasDigitalRelease) {
const digitalReleaseDate = new Date(releaseInfo.digital_release_date!);
- const twoDaysAfter = new Date(digitalReleaseDate);
- twoDaysAfter.setDate(twoDaysAfter.getDate() + 2);
- if (new Date() >= twoDaysAfter) {
+ if (new Date() >= digitalReleaseDate) {
return HD;
}
}
@@ -115,10 +113,8 @@ export function DetailsBody({
const theatricalReleaseDate = new Date(
releaseInfo.theatrical_release_date!,
);
- const fortyFiveDaysAfter = new Date(theatricalReleaseDate);
- fortyFiveDaysAfter.setDate(fortyFiveDaysAfter.getDate() + 45);
- if (new Date() >= fortyFiveDaysAfter) {
+ if (new Date() >= theatricalReleaseDate) {
return (
HD
diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx
index 89ec5cde..3df4d517 100644
--- a/src/components/player/Player.tsx
+++ b/src/components/player/Player.tsx
@@ -12,3 +12,4 @@ export * from "./internals/BookmarkButton";
export * from "./internals/InfoButton";
export * from "./internals/SkipEpisodeButton";
export * from "./atoms/Chromecast";
+export * from "./atoms/Widescreen";
diff --git a/src/components/player/atoms/SkipIntroButton.tsx b/src/components/player/atoms/SkipIntroButton.tsx
index 4f8f08d6..b743622a 100644
--- a/src/components/player/atoms/SkipIntroButton.tsx
+++ b/src/components/player/atoms/SkipIntroButton.tsx
@@ -3,6 +3,7 @@ import { useCallback } from "react";
import { Icon, Icons } from "@/components/Icon";
import { Transition } from "@/components/utils/Transition";
+import { useAuthStore } from "@/stores/auth";
import { usePlayerStore } from "@/stores/player/store";
function shouldShowSkipButton(
@@ -47,6 +48,8 @@ export function SkipIntroButton(props: {
const time = usePlayerStore((s) => s.progress.time);
const status = usePlayerStore((s) => s.status);
const display = usePlayerStore((s) => s.display);
+ const meta = usePlayerStore((s) => s.meta);
+ const account = useAuthStore((s) => s.account);
const showingState = shouldShowSkipButton(time, props.skipTime);
const animation = showingState === "hover" ? "slide-up" : "fade";
let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]";
@@ -55,11 +58,47 @@ export function SkipIntroButton(props: {
? bottom
: "bottom-[calc(3rem+env(safe-area-inset-bottom))]";
}
+
+ const sendSkipAnalytics = useCallback(
+ async (startTime: number, endTime: number, skipDuration: number) => {
+ try {
+ await fetch("https://skips.pstream.mov/send", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ start_time: startTime,
+ end_time: endTime,
+ skip_duration: skipDuration,
+ content_id: meta?.tmdbId,
+ content_type: meta?.type,
+ season_id: meta?.season?.tmdbId,
+ episode_id: meta?.episode?.tmdbId,
+ user_id: account?.userId,
+ session_id: `session_${Date.now()}`,
+ turnstile_token: "",
+ }),
+ });
+ } catch (error) {
+ console.error("Failed to send skip analytics:", error);
+ }
+ },
+ [meta, account],
+ );
+
const handleSkip = useCallback(() => {
if (typeof props.skipTime === "number" && display) {
+ const startTime = time;
+ const endTime = props.skipTime;
+ const skipDuration = endTime - startTime;
+
display.setTime(props.skipTime);
+
+ // Send analytics for intro skip button usage
+ // eslint-disable-next-line no-console
+ console.log(`Skip intro button used: ${skipDuration}s total`);
+ sendSkipAnalytics(startTime, endTime, skipDuration);
}
- }, [props.skipTime, display]);
+ }, [props.skipTime, display, time, sendSkipAnalytics]);
if (!props.inControl) return null;
let show = false;
diff --git a/src/components/player/hooks/useSkipTime.ts b/src/components/player/hooks/useSkipTime.ts
index ebe2e0e4..8173490d 100644
--- a/src/components/player/hooks/useSkipTime.ts
+++ b/src/components/player/hooks/useSkipTime.ts
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
import { conf } from "@/setup/config";
import { usePreferencesStore } from "@/stores/preferences";
+import { getTurnstileToken } from "@/utils/turnstile";
// Thanks Nemo for this API
const BASE_URL = "https://fed-skips.pstream.mov";
@@ -20,8 +21,16 @@ export function useSkipTime() {
if (!febboxKey) return;
try {
+ const turnstileToken = await getTurnstileToken(
+ "0x4AAAAAAB6ocCCpurfWRZyC",
+ );
+
const apiUrl = `${BASE_URL}/${meta.imdbId}/${meta.season?.number}/${meta.episode?.number}`;
- const response = await fetch(apiUrl);
+ const response = await fetch(apiUrl, {
+ headers: {
+ "cf-turnstile-response": turnstileToken,
+ },
+ });
if (!response.ok) {
if (response.status === 500 && retries < MAX_RETRIES) {
diff --git a/src/components/player/hooks/useSkipTracking.ts b/src/components/player/hooks/useSkipTracking.ts
index e4747da8..c22253a0 100644
--- a/src/components/player/hooks/useSkipTracking.ts
+++ b/src/components/player/hooks/useSkipTracking.ts
@@ -26,77 +26,102 @@ interface SkipTrackingResult {
}
/**
- * Hook that tracks when users skip or scrub more than 15 seconds
- * Useful for gathering information about show intros and user behavior
+ * Hook that tracks rapid skipping sessions where users accumulate 30+ seconds of forward
+ * movement within a 5-second window. Sessions continue until 8 seconds pass without
+ * any forward movement, then report the total skip distance. Ignores skips that start
+ * after 20% of video duration (unlikely to be intro skipping).
*
- * @param minSkipThreshold Minimum skip duration in seconds to track (default: 15)
+ * @param minSkipThreshold Minimum total forward movement in 5-second window to start session (default: 30)
* @param maxHistory Maximum number of skip events to keep in history (default: 50)
*/
export function useSkipTracking(
- minSkipThreshold: number = 15,
+ minSkipThreshold: number = 30,
maxHistory: number = 50,
): SkipTrackingResult {
const [skipHistory, setSkipHistory] = useState
([]);
const previousTimeRef = useRef(0);
- const lastUpdateTimeRef = useRef(0);
- const isSeekingRef = useRef(false);
+ const skipWindowRef = useRef>([]);
+ const isInSkipSessionRef = useRef(false);
+ const skipSessionStartRef = useRef(0);
+ const sessionTotalRef = useRef(0);
// Get current player state
const progress = usePlayerStore((s) => s.progress);
- const mediaPlaying = usePlayerStore((s) => s.mediaPlaying);
const meta = usePlayerStore((s) => s.meta);
- const isSeeking = usePlayerStore((s) => s.interface.isSeeking);
-
- // Track seeking state to avoid false positives during drag seeking
- useEffect(() => {
- isSeekingRef.current = isSeeking;
- }, [isSeeking]);
+ const duration = progress.duration;
const clearHistory = useCallback(() => {
setSkipHistory([]);
previousTimeRef.current = 0;
- lastUpdateTimeRef.current = 0;
+ skipWindowRef.current = [];
+ isInSkipSessionRef.current = false;
+ skipSessionStartRef.current = 0;
+ sessionTotalRef.current = 0;
}, []);
const detectSkip = useCallback(() => {
const now = Date.now();
const currentTime = progress.time;
- // Don't track if video hasn't started playing or if we're actively seeking
- if (!mediaPlaying.hasPlayedOnce || isSeekingRef.current) {
- previousTimeRef.current = currentTime;
- lastUpdateTimeRef.current = now;
- return;
- }
-
// Initialize on first run
if (previousTimeRef.current === 0) {
previousTimeRef.current = currentTime;
- lastUpdateTimeRef.current = now;
return;
}
const timeDelta = currentTime - previousTimeRef.current;
- const realTimeDelta = now - lastUpdateTimeRef.current;
- // Calculate expected time change based on playback rate and real time passed
- const expectedTimeDelta = mediaPlaying.isPlaying
- ? (realTimeDelta / 1000) * mediaPlaying.playbackRate
- : 0;
+ // Track forward movements >= 1 second in sliding 5-second window
+ if (timeDelta >= 1) {
+ // Add forward movement to window and remove entries older than 5 seconds
+ skipWindowRef.current.push({ time: now, delta: timeDelta });
+ skipWindowRef.current = skipWindowRef.current.filter(
+ (entry) => entry.time > now - 5000,
+ );
- // Detect if the time jump is significantly different from expected
- // This accounts for normal playback, pausing, and small buffering hiccups
- const unexpectedJump = Math.abs(timeDelta - expectedTimeDelta);
+ // Calculate total forward movement in current window
+ const totalForwardMovement = skipWindowRef.current.reduce(
+ (sum, entry) => sum + entry.delta,
+ 0,
+ );
- // Only consider it a skip if:
- // 1. The time jump is greater than our threshold
- // 2. The unexpected jump is significant (more than 3 seconds difference from expected)
- // 3. We're not in a seeking state
- if (Math.abs(timeDelta) >= minSkipThreshold && unexpectedJump >= 3) {
+ // Start session when threshold exceeded
+ if (
+ totalForwardMovement >= minSkipThreshold &&
+ !isInSkipSessionRef.current
+ ) {
+ isInSkipSessionRef.current = true;
+ skipSessionStartRef.current = previousTimeRef.current;
+ sessionTotalRef.current = totalForwardMovement;
+ }
+ // Update session total while active
+ else if (isInSkipSessionRef.current) {
+ sessionTotalRef.current = totalForwardMovement;
+ }
+ }
+
+ // End session if no forward movement in last 8 seconds
+ const recentEntries = skipWindowRef.current.filter(
+ (entry) => entry.time > now - 8000,
+ );
+
+ if (isInSkipSessionRef.current && recentEntries.length === 0) {
+ // Ignore skips that start after 20% of video duration (likely not intro skipping)
+ const twentyPercentMark = duration * 0.2;
+ if (skipSessionStartRef.current > twentyPercentMark) {
+ // Reset session state without creating event
+ isInSkipSessionRef.current = false;
+ skipSessionStartRef.current = 0;
+ sessionTotalRef.current = 0;
+ skipWindowRef.current = [];
+ return;
+ }
+
+ // Create skip event for completed session
const skipEvent: SkipEvent = {
- startTime: previousTimeRef.current,
+ startTime: skipSessionStartRef.current,
endTime: currentTime,
- skipDuration: timeDelta,
+ skipDuration: sessionTotalRef.current,
timestamp: now,
meta: meta
? {
@@ -118,22 +143,22 @@ export function useSkipTracking(
? newHistory.slice(newHistory.length - maxHistory)
: newHistory;
});
+
+ // Reset session state
+ isInSkipSessionRef.current = false;
+ skipSessionStartRef.current = 0;
+ sessionTotalRef.current = 0;
+ skipWindowRef.current = [];
}
previousTimeRef.current = currentTime;
- lastUpdateTimeRef.current = now;
- }, [progress.time, mediaPlaying, meta, minSkipThreshold, maxHistory]);
+ }, [progress.time, duration, meta, minSkipThreshold, maxHistory]);
useEffect(() => {
- // Run detection every second when video is playing
- const interval = setInterval(() => {
- if (mediaPlaying.hasPlayedOnce) {
- detectSkip();
- }
- }, 1000);
-
+ // Monitor time changes every 100ms to catch rapid skipping
+ const interval = setInterval(detectSkip, 100);
return () => clearInterval(interval);
- }, [detectSkip, mediaPlaying.hasPlayedOnce]);
+ }, [detectSkip]);
// Reset tracking when content changes
useEffect(() => {
diff --git a/src/components/player/hooks/useSourceSelection.ts b/src/components/player/hooks/useSourceSelection.ts
index 428096f6..aa5cf1d0 100644
--- a/src/components/player/hooks/useSourceSelection.ts
+++ b/src/components/player/hooks/useSourceSelection.ts
@@ -22,6 +22,21 @@ import { convertRunoutputToSource } from "@/components/player/utils/convertRunou
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { metaToScrapeMedia } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
+import { usePreferencesStore } from "@/stores/preferences";
+import { useProgressStore } from "@/stores/progress";
+
+function getSavedProgress(items: Record, meta: any): number {
+ const item = items[meta?.tmdbId ?? ""];
+ if (!item || !meta) return 0;
+ if (meta.type === "movie") {
+ if (!item.progress) return 0;
+ return item.progress.watched;
+ }
+
+ const ep = item.episodes[meta.episode?.tmdbId ?? ""];
+ if (!ep) return 0;
+ return ep.progress.watched;
+}
export function useEmbedScraping(
routerId: string,
@@ -33,8 +48,8 @@ export function useEmbedScraping(
const setCaption = usePlayerStore((s) => s.setCaption);
const setSourceId = usePlayerStore((s) => s.setSourceId);
const setEmbedId = usePlayerStore((s) => (s as any).setEmbedId);
- const progress = usePlayerStore((s) => s.progress.time);
const meta = usePlayerStore((s) => s.meta);
+ const progressItems = useProgressStore((s) => s.items);
const router = useOverlayRouter(routerId);
const { report } = useReportProviders();
@@ -81,7 +96,7 @@ export function useEmbedScraping(
setSource(
convertRunoutputToSource({ stream: result.stream[0] }),
convertProviderCaption(result.stream[0].captions),
- progress,
+ getSavedProgress(progressItems, meta),
);
router.close();
}, [embedId, sourceId, meta, router, report, setCaption]);
@@ -99,7 +114,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
const setCaption = usePlayerStore((s) => s.setCaption);
const setSourceId = usePlayerStore((s) => s.setSourceId);
const setEmbedId = usePlayerStore((s) => (s as any).setEmbedId);
- const progress = usePlayerStore((s) => s.progress.time);
+ const progressItems = useProgressStore((s) => s.items);
const router = useOverlayRouter(routerId);
const { report } = useReportProviders();
@@ -144,7 +159,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
setSource(
convertRunoutputToSource({ stream: result.stream[0] }),
convertProviderCaption(result.stream[0].captions),
- progress,
+ getSavedProgress(progressItems, meta),
);
setSourceId(sourceId);
router.close();
@@ -201,7 +216,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
setSource(
convertRunoutputToSource({ stream: embedResult.stream[0] }),
convertProviderCaption(embedResult.stream[0].captions),
- progress,
+ getSavedProgress(progressItems, meta),
);
router.close();
}
diff --git a/src/components/player/internals/Backend/SkipTracker.tsx b/src/components/player/internals/Backend/SkipTracker.tsx
index 4b28ce08..b3000264 100644
--- a/src/components/player/internals/Backend/SkipTracker.tsx
+++ b/src/components/player/internals/Backend/SkipTracker.tsx
@@ -1,173 +1,64 @@
-import { useEffect, useRef } from "react";
+import { useCallback, useEffect, useRef } from "react";
import { useSkipTracking } from "@/components/player/hooks/useSkipTracking";
+import { useAuthStore } from "@/stores/auth";
import { usePlayerStore } from "@/stores/player/store";
/**
- * Component that tracks when users skip or scrub more than 15 seconds
- * Useful for gathering information about show intros and user behavior patterns
- * Currently logs to console - can be extended to send to backend analytics endpoint
+ * Component that tracks and reports completed skip sessions to analytics backend.
+ * Sessions are detected when users accumulate 30+ seconds of forward movement
+ * within a 5-second window and end after 8 seconds of no activity.
+ * Ignores skips that start after 20% of video duration (unlikely to be intro skipping).
*/
export function SkipTracker() {
- const { skipHistory, latestSkip } = useSkipTracking(15); // Track skips > 15 seconds
+ const { latestSkip } = useSkipTracking(30);
const lastLoggedSkipRef = useRef(0);
// Player metadata for context
const meta = usePlayerStore((s) => s.meta);
+ const account = useAuthStore((s) => s.account);
+ const turnstileToken = "";
+
+ const sendSkipAnalytics = useCallback(async () => {
+ if (!latestSkip) return;
+
+ try {
+ await fetch("https://skips.pstream.mov/send", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ start_time: latestSkip.startTime,
+ end_time: latestSkip.endTime,
+ skip_duration: latestSkip.skipDuration,
+ content_id: meta?.tmdbId,
+ content_type: meta?.type,
+ season_id: meta?.season?.tmdbId,
+ episode_id: meta?.episode?.tmdbId,
+ user_id: account?.userId,
+ session_id: `session_${Date.now()}`,
+ turnstile_token: turnstileToken ?? "",
+ }),
+ });
+ } catch (error) {
+ console.error("Failed to send skip analytics:", error);
+ }
+ }, [latestSkip, meta, account]);
useEffect(() => {
if (!latestSkip || !meta) return;
- // Avoid logging the same skip multiple times
+ // Avoid processing the same skip multiple times
if (latestSkip.timestamp === lastLoggedSkipRef.current) return;
- // Format skip duration for readability
- const formatDuration = (seconds: number): string => {
- const absSeconds = Math.abs(seconds);
- const minutes = Math.floor(absSeconds / 60);
- const remainingSeconds = Math.floor(absSeconds % 60);
-
- if (minutes > 0) {
- return `${minutes}m ${remainingSeconds}s`;
- }
- return `${remainingSeconds}s`;
- };
-
- // Format time position for readability
- const formatTime = (seconds: number): string => {
- const minutes = Math.floor(seconds / 60);
- const remainingSeconds = Math.floor(seconds % 60);
- return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
- };
-
- const skipDirection = latestSkip.skipDuration > 0 ? "forward" : "backward";
- const skipType = Math.abs(latestSkip.skipDuration) >= 30 ? "scrub" : "skip";
-
- // Log the skip event with detailed information
+ // Log completed skip session
// eslint-disable-next-line no-console
- console.log(`User ${skipType.toUpperCase()} detected`, {
- // Basic skip info
- direction: skipDirection,
- duration: formatDuration(latestSkip.skipDuration),
- from: formatTime(latestSkip.startTime),
- to: formatTime(latestSkip.endTime),
+ console.log(`Skip session completed: ${latestSkip.skipDuration}s total`);
- // Content context
- content: {
- title: latestSkip.meta?.title || "Unknown",
- type: latestSkip.meta?.type || "Unknown",
- tmdbId: latestSkip.meta?.tmdbId,
- },
-
- // Episode context (for TV shows)
- ...(meta.type === "show" && {
- episode: {
- season: latestSkip.meta?.seasonNumber,
- episode: latestSkip.meta?.episodeNumber,
- },
- }),
-
- // Analytics data that could be sent to backend
- analytics: {
- timestamp: new Date(latestSkip.timestamp).toISOString(),
- startTime: latestSkip.startTime,
- endTime: latestSkip.endTime,
- skipDuration: latestSkip.skipDuration,
- contentId: latestSkip.meta?.tmdbId,
- contentType: latestSkip.meta?.type,
- seasonId: meta.season?.tmdbId,
- episodeId: meta.episode?.tmdbId,
- },
- });
-
- // Log special cases that might indicate intro skipping
- if (
- meta.type === "show" &&
- latestSkip.startTime <= 30 && // Skip happened in first 30 seconds
- latestSkip.skipDuration > 15 && // Forward skip of at least 15 seconds
- latestSkip.skipDuration <= 120 // But not more than 2 minutes (reasonable intro length)
- ) {
- // eslint-disable-next-line no-console
- console.log(`Potential intro skip dete`, {
- show: latestSkip.meta?.title,
- season: latestSkip.meta?.seasonNumber,
- episode: latestSkip.meta?.episodeNumber,
- introSkipDuration: formatDuration(latestSkip.skipDuration),
- message: "User likely skipped intro sequence",
- });
- }
-
- // Log potential outro/credits skipping
- const progress = usePlayerStore.getState().progress;
- const timeRemaining = progress.duration - latestSkip.endTime;
- if (
- latestSkip.skipDuration > 0 && // Forward skip
- timeRemaining <= 300 && // Within last 5 minutes
- latestSkip.skipDuration >= 15
- ) {
- // eslint-disable-next-line no-console
- console.log(`Potential outro skip detected`, {
- content: latestSkip.meta?.title,
- timeRemaining: formatDuration(timeRemaining),
- skipDuration: formatDuration(latestSkip.skipDuration),
- message: "User likely skipped credits/outro",
- });
- }
+ // Send analytics data to backend
+ sendSkipAnalytics();
lastLoggedSkipRef.current = latestSkip.timestamp;
- }, [latestSkip, meta]);
-
- // Log summary statistics occasionally... just for testing, we likely wont use it unless useful.
- useEffect(() => {
- if (skipHistory.length > 0 && skipHistory.length % 5 === 0) {
- const forwardSkips = skipHistory.filter((s) => s.skipDuration > 0);
- const backwardSkips = skipHistory.filter((s) => s.skipDuration < 0);
- const avgSkipDuration =
- skipHistory.reduce((sum, s) => sum + Math.abs(s.skipDuration), 0) /
- skipHistory.length;
-
- // eslint-disable-next-line no-console
- console.log(`skip analytics`, {
- totalSkips: skipHistory.length,
- forwardSkips: forwardSkips.length,
- backwardSkips: backwardSkips.length,
- averageSkipDuration: `${Math.round(avgSkipDuration)}s`,
- content: meta?.title || "Unknown",
- });
- }
- }, [skipHistory.length, skipHistory, meta?.title]);
-
- // TODO: When backend endpoint is ready, replace console.log with API calls
- // Example implementation:
- /*
- useEffect(() => {
- if (!latestSkip || !account?.userId) return;
-
- // Send skip data to analytics endpoint
- const sendSkipAnalytics = async () => {
- try {
- await fetch(`${backendUrl}/api/analytics/skips`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- userId: account.userId,
- skipData: latestSkip,
- contentContext: {
- tmdbId: meta?.tmdbId,
- type: meta?.type,
- seasonId: meta?.season?.tmdbId,
- episodeId: meta?.episode?.tmdbId,
- }
- })
- });
- } catch (error) {
- console.error('Failed to send skip analytics:', error);
- }
- };
-
- sendSkipAnalytics();
- }, [latestSkip, account?.userId, meta]);
- */
+ }, [latestSkip, meta, sendSkipAnalytics]);
return null;
}
diff --git a/src/hooks/useWatchPartySync.ts b/src/hooks/useWatchPartySync.ts
index f69839d1..2a88ab71 100644
--- a/src/hooks/useWatchPartySync.ts
+++ b/src/hooks/useWatchPartySync.ts
@@ -82,6 +82,13 @@ export function useWatchPartySync(
// Get watch party state
const { roomCode, isHost, enabled, enableAsGuest } = useWatchPartyStore();
+ // Reset URL parameter checking when watch party is disabled
+ useEffect(() => {
+ if (!enabled) {
+ syncStateRef.current.checkedUrlParams = false;
+ }
+ }, [enabled]);
+
// Check URL parameters for watch party code
useEffect(() => {
if (syncStateRef.current.checkedUrlParams) return;
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index af65bdab..c590282f 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -481,6 +481,7 @@ export function SettingsPage() {
state.forceCompactEpisodeView.changed ||
state.enableLowPerformanceMode.changed ||
state.enableHoldToBoost.changed ||
+ state.homeSectionOrder.changed ||
state.manualSourceSelection.changed ||
state.enableDoubleClickToSeek
) {
@@ -505,6 +506,7 @@ export function SettingsPage() {
forceCompactEpisodeView: state.forceCompactEpisodeView.state,
enableLowPerformanceMode: state.enableLowPerformanceMode.state,
enableHoldToBoost: state.enableHoldToBoost.state,
+ homeSectionOrder: state.homeSectionOrder.state,
manualSourceSelection: state.manualSourceSelection.state,
enableDoubleClickToSeek: state.enableDoubleClickToSeek.state,
});
diff --git a/src/pages/discover/components/FeaturedCarousel.tsx b/src/pages/discover/components/FeaturedCarousel.tsx
index 90287d1f..c3ac8a7b 100644
--- a/src/pages/discover/components/FeaturedCarousel.tsx
+++ b/src/pages/discover/components/FeaturedCarousel.tsx
@@ -573,10 +573,8 @@ export function FeaturedCarousel({
if (hasDigitalRelease) {
const digitalReleaseDate = new Date(releaseInfo.digital_release_date!);
- const twoDaysAfter = new Date(digitalReleaseDate);
- twoDaysAfter.setDate(twoDaysAfter.getDate() + 2);
- if (new Date() >= twoDaysAfter) {
+ if (new Date() >= digitalReleaseDate) {
return HD;
}
}
@@ -585,10 +583,8 @@ export function FeaturedCarousel({
const theatricalReleaseDate = new Date(
releaseInfo.theatrical_release_date!,
);
- const fortyFiveDaysAfter = new Date(theatricalReleaseDate);
- fortyFiveDaysAfter.setDate(fortyFiveDaysAfter.getDate() + 45);
- if (new Date() >= fortyFiveDaysAfter) {
+ if (new Date() >= theatricalReleaseDate) {
return (
HD
diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx
index df502218..d0eb236e 100644
--- a/src/pages/parts/player/PlayerPart.tsx
+++ b/src/pages/parts/player/PlayerPart.tsx
@@ -5,7 +5,6 @@ import { Player } from "@/components/player";
import { SkipIntroButton } from "@/components/player/atoms/SkipIntroButton";
import { UnreleasedEpisodeOverlay } from "@/components/player/atoms/UnreleasedEpisodeOverlay";
import { WatchPartyStatus } from "@/components/player/atoms/WatchPartyStatus";
-import { Widescreen } from "@/components/player/atoms/Widescreen";
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
import { useSkipTime } from "@/components/player/hooks/useSkipTime";
import { useIsMobile } from "@/hooks/useIsMobile";
@@ -180,19 +179,13 @@ export function PlayerPart(props: PlayerPartProps) {
) : null}
{status === playerStatus.PLAYBACK_ERROR ||
status === playerStatus.PLAYING ? (
- <>
-
-
- >
+
) : null}
- {/* Fullscreen on when not shifting */}
- {!isShifting &&
}
-
- {/* Expand button visible when shifting */}
- {isShifting && (
-
-
-
+
+ {isShifting || isHoldingFullscreen ? (
+
+ ) : (
+
)}
@@ -205,13 +198,11 @@ export function PlayerPart(props: PlayerPartProps) {
)}
{status === playerStatus.PLAYING && (
@@ -221,7 +212,11 @@ export function PlayerPart(props: PlayerPartProps) {
className="select-none touch-none"
style={{ WebkitTapHighlightColor: "transparent" }}
>
- {isHoldingFullscreen ?
:
}
+ {isHoldingFullscreen ? (
+
+ ) : (
+
+ )}
)}
diff --git a/src/setup/config.ts b/src/setup/config.ts
index bf41548a..42b00a58 100644
--- a/src/setup/config.ts
+++ b/src/setup/config.ts
@@ -28,8 +28,8 @@ interface Config {
ALLOW_FEBBOX_KEY: boolean;
ALLOW_REAL_DEBRID_KEY: boolean;
SHOW_AD: boolean;
- AD_CONTENT_URL: string; // like
- TRACK_SCRIPT: string;
+ AD_CONTENT_URL: string;
+ TRACK_SCRIPT: string; // like
BANNER_MESSAGE: string;
BANNER_ID: string;
}
diff --git a/src/utils/turnstile.ts b/src/utils/turnstile.ts
new file mode 100644
index 00000000..0e16cf57
--- /dev/null
+++ b/src/utils/turnstile.ts
@@ -0,0 +1,160 @@
+/**
+ * Cloudflare Turnstile utility for handling invisible CAPTCHA verification
+ */
+
+/**
+ * Loads the Cloudflare Turnstile script if not already loaded
+ */
+function loadTurnstileScript(): Promise