add skip confidence

This commit is contained in:
Pas 2025-11-10 14:52:31 -07:00
parent aeb131b26d
commit fcf9fbb56e
3 changed files with 73 additions and 31 deletions

View file

@ -2,8 +2,8 @@ import classNames from "classnames";
import { useCallback } from "react";
import { Icon, Icons } from "@/components/Icon";
import { useSkipTracking } from "@/components/player/hooks/useSkipTracking";
import { Transition } from "@/components/utils/Transition";
import { useAuthStore } from "@/stores/auth";
import { usePlayerStore } from "@/stores/player/store";
function shouldShowSkipButton(
@ -49,7 +49,7 @@ export function SkipIntroButton(props: {
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 { addSkipEvent } = useSkipTracking(30);
const showingState = shouldShowSkipButton(time, props.skipTime);
const animation = showingState === "hover" ? "slide-up" : "fade";
let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]";
@ -59,32 +59,6 @@ export function SkipIntroButton(props: {
: "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;
@ -93,12 +67,30 @@ export function SkipIntroButton(props: {
display.setTime(props.skipTime);
// Send analytics for intro skip button usage
// Add manual skip event with high confidence (user explicitly clicked skip intro)
addSkipEvent({
startTime,
endTime,
skipDuration,
confidence: 0.95, // High confidence for explicit user action
meta: meta
? {
title:
meta.type === "show" && meta.episode
? `${meta.title} - S${meta.season?.number || 0}E${meta.episode.number || 0}`
: meta.title,
type: meta.type === "movie" ? "Movie" : "TV Show",
tmdbId: meta.tmdbId,
seasonNumber: meta.season?.number,
episodeNumber: meta.episode?.number,
}
: undefined,
});
// eslint-disable-next-line no-console
console.log(`Skip intro button used: ${skipDuration}s total`);
sendSkipAnalytics(startTime, endTime, skipDuration);
}
}, [props.skipTime, display, time, sendSkipAnalytics]);
}, [props.skipTime, display, time, addSkipEvent, meta]);
if (!props.inControl) return null;
let show = false;

View file

@ -7,6 +7,7 @@ interface SkipEvent {
endTime: number;
skipDuration: number;
timestamp: number;
confidence: number; // 0.0-1.0 confidence score
meta?: {
title: string;
type: string;
@ -23,6 +24,31 @@ interface SkipTrackingResult {
latestSkip: SkipEvent | null;
/** Clear the skip history */
clearHistory: () => void;
/** Add a manual skip event (e.g., from skip intro button) */
addSkipEvent: (event: Omit<SkipEvent, "timestamp">) => void;
}
/**
* Calculate confidence score for automatic skip detection
* Based on skip duration and timing within the video
*/
function calculateSkipConfidence(
skipDuration: number,
startTime: number,
duration: number,
): number {
// Duration confidence: longer skips are more confident
// 30s = 0.5, 60s = 0.75, 90s+ = 0.85
const durationConfidence = Math.min(0.85, 0.5 + (skipDuration - 30) * 0.01);
// Timing confidence: earlier skips are more confident
// Start time as percentage of total duration
const startPercentage = startTime / duration;
// Higher confidence for earlier starts (0% = 1.0, 20% = 0.8)
const timingConfidence = Math.max(0.7, 1.0 - startPercentage * 0.75);
// Combine factors (weighted average)
return durationConfidence * 0.6 + timingConfidence * 0.4;
}
/**
@ -59,6 +85,23 @@ export function useSkipTracking(
sessionTotalRef.current = 0;
}, []);
const addSkipEvent = useCallback(
(event: Omit<SkipEvent, "timestamp">) => {
const skipEvent: SkipEvent = {
...event,
timestamp: Date.now(),
};
setSkipHistory((prev) => {
const newHistory = [...prev, skipEvent];
return newHistory.length > maxHistory
? newHistory.slice(newHistory.length - maxHistory)
: newHistory;
});
},
[maxHistory],
);
const detectSkip = useCallback(() => {
const now = Date.now();
const currentTime = progress.time;
@ -123,6 +166,11 @@ export function useSkipTracking(
endTime: currentTime,
skipDuration: sessionTotalRef.current,
timestamp: now,
confidence: calculateSkipConfidence(
sessionTotalRef.current,
skipSessionStartRef.current,
duration,
),
meta: meta
? {
title:
@ -170,5 +218,6 @@ export function useSkipTracking(
latestSkip:
skipHistory.length > 0 ? skipHistory[skipHistory.length - 1] : null,
clearHistory,
addSkipEvent,
};
}

View file

@ -32,6 +32,7 @@ export function SkipTracker() {
content_type: meta?.type,
season_id: meta?.season?.tmdbId,
episode_id: meta?.episode?.tmdbId,
confidence: latestSkip.confidence,
turnstile_token: turnstileToken ?? "",
}),
});