diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 85910ed8..e683c815 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -735,10 +735,6 @@ "device": "device", "enabled": "Casting to device 🎬" }, - "skipIntro": { - "feedback": "Was this skip helpful?", - "skip": "Skip Intro" - }, "menus": { "downloads": { "button": "Attempt download", diff --git a/src/components/player/atoms/SkipIntroButton.tsx b/src/components/player/atoms/SkipIntroButton.tsx index 3b030f74..2f95158e 100644 --- a/src/components/player/atoms/SkipIntroButton.tsx +++ b/src/components/player/atoms/SkipIntroButton.tsx @@ -1,6 +1,5 @@ import classNames from "classnames"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useCallback } from "react"; import { Icon, Icons } from "@/components/Icon"; import { useSkipTracking } from "@/components/player/hooks/useSkipTracking"; @@ -51,14 +50,6 @@ export function SkipIntroButton(props: { const display = usePlayerStore((s) => s.display); const meta = usePlayerStore((s) => s.meta); const { addSkipEvent } = useSkipTracking(20); - const [showFeedback, setShowFeedback] = useState(false); - const [feedbackSubmitted, setFeedbackSubmitted] = useState(false); - const timeoutRef = useRef | null>(null); - const pendingSkipDataRef = useRef<{ - startTime: number; - endTime: number; - skipDuration: number; - } | null>(null); const showingState = shouldShowSkipButton(time, props.skipTime); const animation = showingState === "hover" ? "slide-up" : "fade"; let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]"; @@ -68,19 +59,20 @@ export function SkipIntroButton(props: { : "bottom-[calc(3rem+env(safe-area-inset-bottom))]"; } - const { t } = useTranslation(); + const handleSkip = useCallback(() => { + if (typeof props.skipTime === "number" && display) { + const startTime = time; + const endTime = props.skipTime; + const skipDuration = endTime - startTime; - const reportSkip = useCallback( - (confidence: number) => { - if (!pendingSkipDataRef.current) return; - - const { startTime, endTime, skipDuration } = pendingSkipDataRef.current; + display.setTime(props.skipTime); + // Add manual skip event with high confidence (user explicitly clicked skip intro) addSkipEvent({ startTime, endTime, skipDuration, - confidence, + confidence: 0.95, // High confidence for explicit user action meta: meta ? { title: @@ -95,99 +87,15 @@ export function SkipIntroButton(props: { : undefined, }); - // eslint-disable-next-line no-console - console.log( - `Skip intro reported: ${skipDuration}s total, confidence: ${confidence}`, - ); - - // Clean up - pendingSkipDataRef.current = null; - setShowFeedback(false); - setFeedbackSubmitted(true); - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - }, - [addSkipEvent, meta], - ); - - const handleThumbsUp = useCallback(() => { - reportSkip(0.95); - }, [reportSkip]); - - const handleThumbsDown = useCallback(() => { - reportSkip(0.7); - }, [reportSkip]); - - const handleSkip = useCallback(() => { - if (typeof props.skipTime === "number" && display) { - const startTime = time; - const endTime = props.skipTime; - const skipDuration = endTime - startTime; - - display.setTime(props.skipTime); - - // Store skip data temporarily - pendingSkipDataRef.current = { - startTime, - endTime, - skipDuration, - }; - - // Show feedback UI - setShowFeedback(true); - setFeedbackSubmitted(false); - - // Start 10-second timeout - timeoutRef.current = setTimeout(() => { - // Hide component immediately to prevent flicker - setShowFeedback(false); - setFeedbackSubmitted(true); - reportSkip(0.8); - }, 10000); - // eslint-disable-next-line no-console console.log(`Skip intro button used: ${skipDuration}s total`); } - }, [props.skipTime, display, time, reportSkip]); - - // Reset feedback state when content changes - useEffect(() => { - setShowFeedback(false); - setFeedbackSubmitted(false); - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - pendingSkipDataRef.current = null; - }, [meta?.tmdbId]); - - // Cleanup timeout on unmount - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); - + }, [props.skipTime, display, time, addSkipEvent, meta]); if (!props.inControl) return null; let show = false; - // Don't show anything if feedback has been submitted - if (feedbackSubmitted) { - show = false; - } else if (showFeedback) { - // Always show feedback UI when active - show = true; - } else if (showingState === "always") { - // Show skip button when always visible - show = true; - } else if (showingState === "hover" && props.controlsShowing) { - // Show skip button on hover when controls are showing - show = true; - } + if (showingState === "always") show = true; + else if (showingState === "hover" && props.controlsShowing) show = true; if (status !== "playing") show = false; return ( @@ -198,52 +106,17 @@ export function SkipIntroButton(props: { >
- {showFeedback ? ( - <> -
- {t("player.skipIntro.feedback")} -
-
- - -
- - ) : ( - - )} +
); diff --git a/src/components/player/atoms/settings/CaptionSettingsView.tsx b/src/components/player/atoms/settings/CaptionSettingsView.tsx index 910821ae..8ca3c434 100644 --- a/src/components/player/atoms/settings/CaptionSettingsView.tsx +++ b/src/components/player/atoms/settings/CaptionSettingsView.tsx @@ -471,8 +471,8 @@ export function CaptionSettingsView({ setDelay(v)} value={delay} textTransformer={(s) => `${s}s`} diff --git a/src/components/player/atoms/settings/QualityView.tsx b/src/components/player/atoms/settings/QualityView.tsx index 0250c8ea..689c43ed 100644 --- a/src/components/player/atoms/settings/QualityView.tsx +++ b/src/components/player/atoms/settings/QualityView.tsx @@ -40,6 +40,7 @@ function useIsIosHls() { export function QualityView({ id }: { id: string }) { const router = useOverlayRouter(id); const isIosHls = useIsIosHls(); + const sourceType = usePlayerStore((s) => s.source?.type); const availableQualities = usePlayerStore((s) => s.qualities); const currentQuality = usePlayerStore((s) => s.currentQuality); const switchQuality = usePlayerStore((s) => s.switchQuality); @@ -50,14 +51,18 @@ export function QualityView({ id }: { id: string }) { const setLastChosenQuality = useQualityStore((s) => s.setLastChosenQuality); const autoQuality = useQualityStore((s) => s.quality.automaticQuality); + // Auto quality only makes sense for HLS sources + const supportsAutoQuality = sourceType === "hls"; + const change = useCallback( (q: SourceQuality) => { setLastChosenQuality(q); - setAutomaticQuality(false); + // Don't disable auto quality when manually selecting a quality + // Keep auto quality enabled by default unless user explicitly toggles it switchQuality(q); router.close(); }, - [router, switchQuality, setLastChosenQuality, setAutomaticQuality], + [router, switchQuality, setLastChosenQuality], ); const changeAutomatic = useCallback(() => { @@ -90,12 +95,18 @@ export function QualityView({ id }: { id: string }) { {qualityToString(v)} ))} - - } - > - {t("player.menus.quality.automaticLabel")} - + {supportsAutoQuality && ( + <> + + + } + > + {t("player.menus.quality.automaticLabel")} + + + )} { diff --git a/src/components/player/hooks/useSkipTracking.ts b/src/components/player/hooks/useSkipTracking.ts index 4d7d7c1b..3aad3aaa 100644 --- a/src/components/player/hooks/useSkipTracking.ts +++ b/src/components/player/hooks/useSkipTracking.ts @@ -29,33 +29,8 @@ interface SkipTrackingResult { } /** - * 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 - // 20s = 0.4, 40s = 0.6, 60s+ = 0.85 - const durationConfidence = Math.min(0.85, 0.4 + (skipDuration - 20) * 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; -} - -/** - * Hook that tracks rapid skipping sessions where users accumulate 20+ 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). + * Hook that tracks manual skip events and monitors user behavior patterns for confidence adjustment. + * Only processes skip events added via addSkipEvent (e.g., from skip intro button). * * @param minSkipThreshold Minimum total forward movement in 5-second window to start session (default: 20) * @param maxHistory Maximum number of skip events to keep in history (default: 50) @@ -74,7 +49,6 @@ export function useSkipTracking( // Get current player state const progress = usePlayerStore((s) => s.progress); const meta = usePlayerStore((s) => s.meta); - const duration = progress.duration; const clearHistory = useCallback(() => { setSkipHistory([]); @@ -149,60 +123,7 @@ export function useSkipTracking( ); 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; - } - - // Only report skips where end time is greater than start time - if (currentTime <= skipSessionStartRef.current) { - // 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: skipSessionStartRef.current, - endTime: currentTime, - skipDuration: sessionTotalRef.current, - timestamp: now, - confidence: calculateSkipConfidence( - sessionTotalRef.current, - skipSessionStartRef.current, - duration, - ), - 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, - }; - - setSkipHistory((prev) => { - const newHistory = [...prev, skipEvent]; - return newHistory.length > maxHistory - ? newHistory.slice(newHistory.length - maxHistory) - : newHistory; - }); - - // Reset session state + // Session ended - reset state but DON'T create skip event isInSkipSessionRef.current = false; skipSessionStartRef.current = 0; sessionTotalRef.current = 0; @@ -210,7 +131,7 @@ export function useSkipTracking( } previousTimeRef.current = currentTime; - }, [progress.time, duration, meta, minSkipThreshold, maxHistory]); + }, [progress.time, minSkipThreshold]); useEffect(() => { // Monitor time changes every 100ms to catch rapid skipping diff --git a/src/components/player/internals/Backend/SkipTracker.tsx b/src/components/player/internals/Backend/SkipTracker.tsx index cd4333f0..bf6bc1e9 100644 --- a/src/components/player/internals/Backend/SkipTracker.tsx +++ b/src/components/player/internals/Backend/SkipTracker.tsx @@ -82,13 +82,13 @@ export function SkipTracker() { return { skip, originalConfidence: skip.confidence, - startTime: progress.time, + startTime: skip.startTime, endTime: skip.endTime, hasBackwardMovement: false, timer, }; }, - [progress.time, sendSkipAnalytics], + [sendSkipAnalytics], ); useEffect(() => { diff --git a/src/pages/parts/settings/ConnectionsPart.tsx b/src/pages/parts/settings/ConnectionsPart.tsx index 9fc6e97a..f06e2a36 100644 --- a/src/pages/parts/settings/ConnectionsPart.tsx +++ b/src/pages/parts/settings/ConnectionsPart.tsx @@ -518,23 +518,23 @@ export function FebboxSetup({ ); })()} +
+
+

+ {t("fedapi.setup.useMp4")} +

+
+
+ + preferences.setFebboxUseMp4(!preferences.febboxUseMp4) + } + enabled={preferences.febboxUseMp4} + /> +
+
) : null} -
-
-

- {t("fedapi.setup.useMp4")} -

-
-
- - preferences.setFebboxUseMp4(!preferences.febboxUseMp4) - } - enabled={preferences.febboxUseMp4} - /> -
-
diff --git a/src/stores/player/utils/qualities.ts b/src/stores/player/utils/qualities.ts index c9df244c..125cdef6 100644 --- a/src/stores/player/utils/qualities.ts +++ b/src/stores/player/utils/qualities.ts @@ -99,7 +99,15 @@ export function selectQuality( const availableQualities = Object.entries(source.qualities) .filter((entry) => (entry[1].url.length ?? 0) > 0) .map((entry) => entry[0]) as SourceQuality[]; - const quality = getPreferredQuality(availableQualities, qualityPreferences); + // For file sources (MP4), always use manual quality selection since they don't support switching + const manualQualityPreferences = { + ...qualityPreferences, + automaticQuality: false, + }; + const quality = getPreferredQuality( + availableQualities, + manualQualityPreferences, + ); if (quality) { const stream = source.qualities[quality]; if (stream) {