mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-20 23:12:16 +00:00
add skip confidence
This commit is contained in:
parent
aeb131b26d
commit
fcf9fbb56e
3 changed files with 73 additions and 31 deletions
|
|
@ -2,8 +2,8 @@ import classNames from "classnames";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { useSkipTracking } from "@/components/player/hooks/useSkipTracking";
|
||||||
import { Transition } from "@/components/utils/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
function shouldShowSkipButton(
|
function shouldShowSkipButton(
|
||||||
|
|
@ -49,7 +49,7 @@ export function SkipIntroButton(props: {
|
||||||
const status = usePlayerStore((s) => s.status);
|
const status = usePlayerStore((s) => s.status);
|
||||||
const display = usePlayerStore((s) => s.display);
|
const display = usePlayerStore((s) => s.display);
|
||||||
const meta = usePlayerStore((s) => s.meta);
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
const account = useAuthStore((s) => s.account);
|
const { addSkipEvent } = useSkipTracking(30);
|
||||||
const showingState = shouldShowSkipButton(time, props.skipTime);
|
const showingState = shouldShowSkipButton(time, props.skipTime);
|
||||||
const animation = showingState === "hover" ? "slide-up" : "fade";
|
const animation = showingState === "hover" ? "slide-up" : "fade";
|
||||||
let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]";
|
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))]";
|
: "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(() => {
|
const handleSkip = useCallback(() => {
|
||||||
if (typeof props.skipTime === "number" && display) {
|
if (typeof props.skipTime === "number" && display) {
|
||||||
const startTime = time;
|
const startTime = time;
|
||||||
|
|
@ -93,12 +67,30 @@ export function SkipIntroButton(props: {
|
||||||
|
|
||||||
display.setTime(props.skipTime);
|
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
|
// eslint-disable-next-line no-console
|
||||||
console.log(`Skip intro button used: ${skipDuration}s total`);
|
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;
|
if (!props.inControl) return null;
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ interface SkipEvent {
|
||||||
endTime: number;
|
endTime: number;
|
||||||
skipDuration: number;
|
skipDuration: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
confidence: number; // 0.0-1.0 confidence score
|
||||||
meta?: {
|
meta?: {
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -23,6 +24,31 @@ interface SkipTrackingResult {
|
||||||
latestSkip: SkipEvent | null;
|
latestSkip: SkipEvent | null;
|
||||||
/** Clear the skip history */
|
/** Clear the skip history */
|
||||||
clearHistory: () => void;
|
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;
|
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 detectSkip = useCallback(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const currentTime = progress.time;
|
const currentTime = progress.time;
|
||||||
|
|
@ -123,6 +166,11 @@ export function useSkipTracking(
|
||||||
endTime: currentTime,
|
endTime: currentTime,
|
||||||
skipDuration: sessionTotalRef.current,
|
skipDuration: sessionTotalRef.current,
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
|
confidence: calculateSkipConfidence(
|
||||||
|
sessionTotalRef.current,
|
||||||
|
skipSessionStartRef.current,
|
||||||
|
duration,
|
||||||
|
),
|
||||||
meta: meta
|
meta: meta
|
||||||
? {
|
? {
|
||||||
title:
|
title:
|
||||||
|
|
@ -170,5 +218,6 @@ export function useSkipTracking(
|
||||||
latestSkip:
|
latestSkip:
|
||||||
skipHistory.length > 0 ? skipHistory[skipHistory.length - 1] : null,
|
skipHistory.length > 0 ? skipHistory[skipHistory.length - 1] : null,
|
||||||
clearHistory,
|
clearHistory,
|
||||||
|
addSkipEvent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export function SkipTracker() {
|
||||||
content_type: meta?.type,
|
content_type: meta?.type,
|
||||||
season_id: meta?.season?.tmdbId,
|
season_id: meta?.season?.tmdbId,
|
||||||
episode_id: meta?.episode?.tmdbId,
|
episode_id: meta?.episode?.tmdbId,
|
||||||
|
confidence: latestSkip.confidence,
|
||||||
turnstile_token: turnstileToken ?? "",
|
turnstile_token: turnstileToken ?? "",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue