mirror of
https://github.com/p-stream/p-stream.git
synced 2026-01-11 20:10:32 +00:00
Merge branch 'production' into dev
This commit is contained in:
commit
2017cb2f26
10 changed files with 194 additions and 230 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <span className="text-green-400">HD</span>;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="px-2 py-1 rounded-lg backdrop-blur-sm bg-gray-600/40">
|
||||
<span className="text-green-400">HD</span>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<SkipEvent[]>([]);
|
||||
const previousTimeRef = useRef<number>(0);
|
||||
const lastUpdateTimeRef = useRef<number>(0);
|
||||
const isSeekingRef = useRef<boolean>(false);
|
||||
const skipWindowRef = useRef<Array<{ time: number; delta: number }>>([]);
|
||||
const isInSkipSessionRef = useRef<boolean>(false);
|
||||
const skipSessionStartRef = useRef<number>(0);
|
||||
const sessionTotalRef = useRef<number>(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(() => {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,20 @@ 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<string, any>, 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,
|
||||
|
|
@ -34,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();
|
||||
const setLastSuccessfulSource = usePreferencesStore(
|
||||
|
|
@ -88,7 +102,7 @@ export function useEmbedScraping(
|
|||
setSource(
|
||||
convertRunoutputToSource({ stream: result.stream[0] }),
|
||||
convertProviderCaption(result.stream[0].captions),
|
||||
progress,
|
||||
getSavedProgress(progressItems, meta),
|
||||
);
|
||||
// Save the last successful source when manually selected
|
||||
if (enableLastSuccessfulSource) {
|
||||
|
|
@ -119,7 +133,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();
|
||||
const setLastSuccessfulSource = usePreferencesStore(
|
||||
|
|
@ -170,7 +184,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);
|
||||
// Save the last successful source when manually selected
|
||||
|
|
@ -231,7 +245,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
|
|||
setSource(
|
||||
convertRunoutputToSource({ stream: embedResult.stream[0] }),
|
||||
convertProviderCaption(embedResult.stream[0].captions),
|
||||
progress,
|
||||
getSavedProgress(progressItems, meta),
|
||||
);
|
||||
// Save the last successful source when manually selected
|
||||
if (enableLastSuccessfulSource) {
|
||||
|
|
|
|||
|
|
@ -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<number>(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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 <span className="text-green-400">HD</span>;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="px-2 py-1 rounded-lg backdrop-blur-sm bg-gray-600/40">
|
||||
<span className="text-green-400">HD</span>
|
||||
|
|
|
|||
|
|
@ -179,11 +179,9 @@ export function PlayerPart(props: PlayerPartProps) {
|
|||
) : null}
|
||||
{status === playerStatus.PLAYBACK_ERROR ||
|
||||
status === playerStatus.PLAYING ? (
|
||||
<>
|
||||
<Player.Captions />
|
||||
<Player.Settings />
|
||||
</>
|
||||
<Player.Captions />
|
||||
) : null}
|
||||
<Player.Settings />
|
||||
{isShifting || isHoldingFullscreen ? (
|
||||
<Player.Widescreen />
|
||||
) : (
|
||||
|
|
@ -200,13 +198,11 @@ export function PlayerPart(props: PlayerPartProps) {
|
|||
)}
|
||||
<Player.Episodes inControl={inControl} />
|
||||
{status === playerStatus.PLAYING ? (
|
||||
<>
|
||||
<div className="hidden ssm:block">
|
||||
<Player.Captions />
|
||||
</div>
|
||||
<Player.Settings />
|
||||
</>
|
||||
<div className="hidden ssm:block">
|
||||
<Player.Captions />
|
||||
</div>
|
||||
) : null}
|
||||
<Player.Settings />
|
||||
</div>
|
||||
<div>
|
||||
{status === playerStatus.PLAYING && (
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ interface Config {
|
|||
ALLOW_FEBBOX_KEY: boolean;
|
||||
ALLOW_REAL_DEBRID_KEY: boolean;
|
||||
SHOW_AD: boolean;
|
||||
AD_CONTENT_URL: string; // like <script src="https://umami.com/script.js"></script>
|
||||
TRACK_SCRIPT: string;
|
||||
AD_CONTENT_URL: string;
|
||||
TRACK_SCRIPT: string; // like <script src="https://umami.com/script.js"></script>
|
||||
BANNER_MESSAGE: string;
|
||||
BANNER_ID: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue