pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'production' into dev

Pas 2017cb2f 81b67dae

+194 -230
+4 -4
pnpm-lock.yaml
··· 44 44 version: 1.8.0 45 45 '@p-stream/providers': 46 46 specifier: github:p-stream/providers#production 47 - version: https://codeload.github.com/p-stream/providers/tar.gz/f86bacfb657781183c5ffc4e2dc68665cebc21bc 47 + version: https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957 48 48 '@plasmohq/messaging': 49 49 specifier: ^0.6.2 50 50 version: 0.6.2(react@18.3.1) ··· 1207 1207 resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} 1208 1208 engines: {node: '>=12.4.0'} 1209 1209 1210 - '@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/f86bacfb657781183c5ffc4e2dc68665cebc21bc': 1211 - resolution: {tarball: https://codeload.github.com/p-stream/providers/tar.gz/f86bacfb657781183c5ffc4e2dc68665cebc21bc} 1210 + '@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957': 1211 + resolution: {tarball: https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957} 1212 1212 version: 3.2.0 1213 1213 1214 1214 '@pkgjs/parseargs@0.11.0': ··· 5524 5524 5525 5525 '@nolyfill/is-core-module@1.0.39': {} 5526 5526 5527 - '@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/f86bacfb657781183c5ffc4e2dc68665cebc21bc': 5527 + '@p-stream/providers@https://codeload.github.com/p-stream/providers/tar.gz/f8c0aa098c7a68e325c7af23b4175cf323c69957': 5528 5528 dependencies: 5529 5529 abort-controller: 3.0.0 5530 5530 cheerio: 1.0.0-rc.12
+2 -6
src/components/overlays/detailsModal/components/sections/DetailsBody.tsx
··· 103 103 104 104 if (hasDigitalRelease) { 105 105 const digitalReleaseDate = new Date(releaseInfo.digital_release_date!); 106 - const twoDaysAfter = new Date(digitalReleaseDate); 107 - twoDaysAfter.setDate(twoDaysAfter.getDate() + 2); 108 106 109 - if (new Date() >= twoDaysAfter) { 107 + if (new Date() >= digitalReleaseDate) { 110 108 return <span className="text-green-400">HD</span>; 111 109 } 112 110 } ··· 115 113 const theatricalReleaseDate = new Date( 116 114 releaseInfo.theatrical_release_date!, 117 115 ); 118 - const fortyFiveDaysAfter = new Date(theatricalReleaseDate); 119 - fortyFiveDaysAfter.setDate(fortyFiveDaysAfter.getDate() + 45); 120 116 121 - if (new Date() >= fortyFiveDaysAfter) { 117 + if (new Date() >= theatricalReleaseDate) { 122 118 return ( 123 119 <div className="px-2 py-1 rounded-lg backdrop-blur-sm bg-gray-600/40"> 124 120 <span className="text-green-400">HD</span>
+40 -1
src/components/player/atoms/SkipIntroButton.tsx
··· 3 3 4 4 import { Icon, Icons } from "@/components/Icon"; 5 5 import { Transition } from "@/components/utils/Transition"; 6 + import { useAuthStore } from "@/stores/auth"; 6 7 import { usePlayerStore } from "@/stores/player/store"; 7 8 8 9 function shouldShowSkipButton( ··· 47 48 const time = usePlayerStore((s) => s.progress.time); 48 49 const status = usePlayerStore((s) => s.status); 49 50 const display = usePlayerStore((s) => s.display); 51 + const meta = usePlayerStore((s) => s.meta); 52 + const account = useAuthStore((s) => s.account); 50 53 const showingState = shouldShowSkipButton(time, props.skipTime); 51 54 const animation = showingState === "hover" ? "slide-up" : "fade"; 52 55 let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]"; ··· 55 58 ? bottom 56 59 : "bottom-[calc(3rem+env(safe-area-inset-bottom))]"; 57 60 } 61 + 62 + const sendSkipAnalytics = useCallback( 63 + async (startTime: number, endTime: number, skipDuration: number) => { 64 + try { 65 + await fetch("https://skips.pstream.mov/send", { 66 + method: "POST", 67 + headers: { "Content-Type": "application/json" }, 68 + body: JSON.stringify({ 69 + start_time: startTime, 70 + end_time: endTime, 71 + skip_duration: skipDuration, 72 + content_id: meta?.tmdbId, 73 + content_type: meta?.type, 74 + season_id: meta?.season?.tmdbId, 75 + episode_id: meta?.episode?.tmdbId, 76 + user_id: account?.userId, 77 + session_id: `session_${Date.now()}`, 78 + turnstile_token: "", 79 + }), 80 + }); 81 + } catch (error) { 82 + console.error("Failed to send skip analytics:", error); 83 + } 84 + }, 85 + [meta, account], 86 + ); 87 + 58 88 const handleSkip = useCallback(() => { 59 89 if (typeof props.skipTime === "number" && display) { 90 + const startTime = time; 91 + const endTime = props.skipTime; 92 + const skipDuration = endTime - startTime; 93 + 60 94 display.setTime(props.skipTime); 95 + 96 + // Send analytics for intro skip button usage 97 + // eslint-disable-next-line no-console 98 + console.log(`Skip intro button used: ${skipDuration}s total`); 99 + sendSkipAnalytics(startTime, endTime, skipDuration); 61 100 } 62 - }, [props.skipTime, display]); 101 + }, [props.skipTime, display, time, sendSkipAnalytics]); 63 102 if (!props.inControl) return null; 64 103 65 104 let show = false;
+72 -47
src/components/player/hooks/useSkipTracking.ts
··· 26 26 } 27 27 28 28 /** 29 - * Hook that tracks when users skip or scrub more than 15 seconds 30 - * Useful for gathering information about show intros and user behavior 29 + * Hook that tracks rapid skipping sessions where users accumulate 30+ seconds of forward 30 + * movement within a 5-second window. Sessions continue until 8 seconds pass without 31 + * any forward movement, then report the total skip distance. Ignores skips that start 32 + * after 20% of video duration (unlikely to be intro skipping). 31 33 * 32 - * @param minSkipThreshold Minimum skip duration in seconds to track (default: 15) 34 + * @param minSkipThreshold Minimum total forward movement in 5-second window to start session (default: 30) 33 35 * @param maxHistory Maximum number of skip events to keep in history (default: 50) 34 36 */ 35 37 export function useSkipTracking( 36 - minSkipThreshold: number = 15, 38 + minSkipThreshold: number = 30, 37 39 maxHistory: number = 50, 38 40 ): SkipTrackingResult { 39 41 const [skipHistory, setSkipHistory] = useState<SkipEvent[]>([]); 40 42 const previousTimeRef = useRef<number>(0); 41 - const lastUpdateTimeRef = useRef<number>(0); 42 - const isSeekingRef = useRef<boolean>(false); 43 + const skipWindowRef = useRef<Array<{ time: number; delta: number }>>([]); 44 + const isInSkipSessionRef = useRef<boolean>(false); 45 + const skipSessionStartRef = useRef<number>(0); 46 + const sessionTotalRef = useRef<number>(0); 43 47 44 48 // Get current player state 45 49 const progress = usePlayerStore((s) => s.progress); 46 - const mediaPlaying = usePlayerStore((s) => s.mediaPlaying); 47 50 const meta = usePlayerStore((s) => s.meta); 48 - const isSeeking = usePlayerStore((s) => s.interface.isSeeking); 49 - 50 - // Track seeking state to avoid false positives during drag seeking 51 - useEffect(() => { 52 - isSeekingRef.current = isSeeking; 53 - }, [isSeeking]); 51 + const duration = progress.duration; 54 52 55 53 const clearHistory = useCallback(() => { 56 54 setSkipHistory([]); 57 55 previousTimeRef.current = 0; 58 - lastUpdateTimeRef.current = 0; 56 + skipWindowRef.current = []; 57 + isInSkipSessionRef.current = false; 58 + skipSessionStartRef.current = 0; 59 + sessionTotalRef.current = 0; 59 60 }, []); 60 61 61 62 const detectSkip = useCallback(() => { 62 63 const now = Date.now(); 63 64 const currentTime = progress.time; 64 65 65 - // Don't track if video hasn't started playing or if we're actively seeking 66 - if (!mediaPlaying.hasPlayedOnce || isSeekingRef.current) { 67 - previousTimeRef.current = currentTime; 68 - lastUpdateTimeRef.current = now; 69 - return; 70 - } 71 - 72 66 // Initialize on first run 73 67 if (previousTimeRef.current === 0) { 74 68 previousTimeRef.current = currentTime; 75 - lastUpdateTimeRef.current = now; 76 69 return; 77 70 } 78 71 79 72 const timeDelta = currentTime - previousTimeRef.current; 80 - const realTimeDelta = now - lastUpdateTimeRef.current; 73 + 74 + // Track forward movements >= 1 second in sliding 5-second window 75 + if (timeDelta >= 1) { 76 + // Add forward movement to window and remove entries older than 5 seconds 77 + skipWindowRef.current.push({ time: now, delta: timeDelta }); 78 + skipWindowRef.current = skipWindowRef.current.filter( 79 + (entry) => entry.time > now - 5000, 80 + ); 81 + 82 + // Calculate total forward movement in current window 83 + const totalForwardMovement = skipWindowRef.current.reduce( 84 + (sum, entry) => sum + entry.delta, 85 + 0, 86 + ); 87 + 88 + // Start session when threshold exceeded 89 + if ( 90 + totalForwardMovement >= minSkipThreshold && 91 + !isInSkipSessionRef.current 92 + ) { 93 + isInSkipSessionRef.current = true; 94 + skipSessionStartRef.current = previousTimeRef.current; 95 + sessionTotalRef.current = totalForwardMovement; 96 + } 97 + // Update session total while active 98 + else if (isInSkipSessionRef.current) { 99 + sessionTotalRef.current = totalForwardMovement; 100 + } 101 + } 81 102 82 - // Calculate expected time change based on playback rate and real time passed 83 - const expectedTimeDelta = mediaPlaying.isPlaying 84 - ? (realTimeDelta / 1000) * mediaPlaying.playbackRate 85 - : 0; 103 + // End session if no forward movement in last 8 seconds 104 + const recentEntries = skipWindowRef.current.filter( 105 + (entry) => entry.time > now - 8000, 106 + ); 86 107 87 - // Detect if the time jump is significantly different from expected 88 - // This accounts for normal playback, pausing, and small buffering hiccups 89 - const unexpectedJump = Math.abs(timeDelta - expectedTimeDelta); 108 + if (isInSkipSessionRef.current && recentEntries.length === 0) { 109 + // Ignore skips that start after 20% of video duration (likely not intro skipping) 110 + const twentyPercentMark = duration * 0.2; 111 + if (skipSessionStartRef.current > twentyPercentMark) { 112 + // Reset session state without creating event 113 + isInSkipSessionRef.current = false; 114 + skipSessionStartRef.current = 0; 115 + sessionTotalRef.current = 0; 116 + skipWindowRef.current = []; 117 + return; 118 + } 90 119 91 - // Only consider it a skip if: 92 - // 1. The time jump is greater than our threshold 93 - // 2. The unexpected jump is significant (more than 3 seconds difference from expected) 94 - // 3. We're not in a seeking state 95 - if (Math.abs(timeDelta) >= minSkipThreshold && unexpectedJump >= 3) { 120 + // Create skip event for completed session 96 121 const skipEvent: SkipEvent = { 97 - startTime: previousTimeRef.current, 122 + startTime: skipSessionStartRef.current, 98 123 endTime: currentTime, 99 - skipDuration: timeDelta, 124 + skipDuration: sessionTotalRef.current, 100 125 timestamp: now, 101 126 meta: meta 102 127 ? { ··· 118 143 ? newHistory.slice(newHistory.length - maxHistory) 119 144 : newHistory; 120 145 }); 146 + 147 + // Reset session state 148 + isInSkipSessionRef.current = false; 149 + skipSessionStartRef.current = 0; 150 + sessionTotalRef.current = 0; 151 + skipWindowRef.current = []; 121 152 } 122 153 123 154 previousTimeRef.current = currentTime; 124 - lastUpdateTimeRef.current = now; 125 - }, [progress.time, mediaPlaying, meta, minSkipThreshold, maxHistory]); 155 + }, [progress.time, duration, meta, minSkipThreshold, maxHistory]); 126 156 127 157 useEffect(() => { 128 - // Run detection every second when video is playing 129 - const interval = setInterval(() => { 130 - if (mediaPlaying.hasPlayedOnce) { 131 - detectSkip(); 132 - } 133 - }, 1000); 134 - 158 + // Monitor time changes every 100ms to catch rapid skipping 159 + const interval = setInterval(detectSkip, 100); 135 160 return () => clearInterval(interval); 136 - }, [detectSkip, mediaPlaying.hasPlayedOnce]); 161 + }, [detectSkip]); 137 162 138 163 // Reset tracking when content changes 139 164 useEffect(() => {
+19 -5
src/components/player/hooks/useSourceSelection.ts
··· 23 23 import { metaToScrapeMedia } from "@/stores/player/slices/source"; 24 24 import { usePlayerStore } from "@/stores/player/store"; 25 25 import { usePreferencesStore } from "@/stores/preferences"; 26 + import { useProgressStore } from "@/stores/progress"; 27 + 28 + function getSavedProgress(items: Record<string, any>, meta: any): number { 29 + const item = items[meta?.tmdbId ?? ""]; 30 + if (!item || !meta) return 0; 31 + if (meta.type === "movie") { 32 + if (!item.progress) return 0; 33 + return item.progress.watched; 34 + } 35 + 36 + const ep = item.episodes[meta.episode?.tmdbId ?? ""]; 37 + if (!ep) return 0; 38 + return ep.progress.watched; 39 + } 26 40 27 41 export function useEmbedScraping( 28 42 routerId: string, ··· 34 48 const setCaption = usePlayerStore((s) => s.setCaption); 35 49 const setSourceId = usePlayerStore((s) => s.setSourceId); 36 50 const setEmbedId = usePlayerStore((s) => (s as any).setEmbedId); 37 - const progress = usePlayerStore((s) => s.progress.time); 38 51 const meta = usePlayerStore((s) => s.meta); 52 + const progressItems = useProgressStore((s) => s.items); 39 53 const router = useOverlayRouter(routerId); 40 54 const { report } = useReportProviders(); 41 55 const setLastSuccessfulSource = usePreferencesStore( ··· 88 102 setSource( 89 103 convertRunoutputToSource({ stream: result.stream[0] }), 90 104 convertProviderCaption(result.stream[0].captions), 91 - progress, 105 + getSavedProgress(progressItems, meta), 92 106 ); 93 107 // Save the last successful source when manually selected 94 108 if (enableLastSuccessfulSource) { ··· 119 133 const setCaption = usePlayerStore((s) => s.setCaption); 120 134 const setSourceId = usePlayerStore((s) => s.setSourceId); 121 135 const setEmbedId = usePlayerStore((s) => (s as any).setEmbedId); 122 - const progress = usePlayerStore((s) => s.progress.time); 136 + const progressItems = useProgressStore((s) => s.items); 123 137 const router = useOverlayRouter(routerId); 124 138 const { report } = useReportProviders(); 125 139 const setLastSuccessfulSource = usePreferencesStore( ··· 170 184 setSource( 171 185 convertRunoutputToSource({ stream: result.stream[0] }), 172 186 convertProviderCaption(result.stream[0].captions), 173 - progress, 187 + getSavedProgress(progressItems, meta), 174 188 ); 175 189 setSourceId(sourceId); 176 190 // Save the last successful source when manually selected ··· 231 245 setSource( 232 246 convertRunoutputToSource({ stream: embedResult.stream[0] }), 233 247 convertProviderCaption(embedResult.stream[0].captions), 234 - progress, 248 + getSavedProgress(progressItems, meta), 235 249 ); 236 250 // Save the last successful source when manually selected 237 251 if (enableLastSuccessfulSource) {
+40 -149
src/components/player/internals/Backend/SkipTracker.tsx
··· 1 - import { useEffect, useRef } from "react"; 1 + import { useCallback, useEffect, useRef } from "react"; 2 2 3 3 import { useSkipTracking } from "@/components/player/hooks/useSkipTracking"; 4 + import { useAuthStore } from "@/stores/auth"; 4 5 import { usePlayerStore } from "@/stores/player/store"; 5 6 6 7 /** 7 - * Component that tracks when users skip or scrub more than 15 seconds 8 - * Useful for gathering information about show intros and user behavior patterns 9 - * Currently logs to console - can be extended to send to backend analytics endpoint 8 + * Component that tracks and reports completed skip sessions to analytics backend. 9 + * Sessions are detected when users accumulate 30+ seconds of forward movement 10 + * within a 5-second window and end after 8 seconds of no activity. 11 + * Ignores skips that start after 20% of video duration (unlikely to be intro skipping). 10 12 */ 11 13 export function SkipTracker() { 12 - const { skipHistory, latestSkip } = useSkipTracking(15); // Track skips > 15 seconds 14 + const { latestSkip } = useSkipTracking(30); 13 15 const lastLoggedSkipRef = useRef<number>(0); 14 16 15 17 // Player metadata for context 16 18 const meta = usePlayerStore((s) => s.meta); 19 + const account = useAuthStore((s) => s.account); 20 + const turnstileToken = ""; 21 + 22 + const sendSkipAnalytics = useCallback(async () => { 23 + if (!latestSkip) return; 24 + 25 + try { 26 + await fetch("https://skips.pstream.mov/send", { 27 + method: "POST", 28 + headers: { "Content-Type": "application/json" }, 29 + body: JSON.stringify({ 30 + start_time: latestSkip.startTime, 31 + end_time: latestSkip.endTime, 32 + skip_duration: latestSkip.skipDuration, 33 + content_id: meta?.tmdbId, 34 + content_type: meta?.type, 35 + season_id: meta?.season?.tmdbId, 36 + episode_id: meta?.episode?.tmdbId, 37 + user_id: account?.userId, 38 + session_id: `session_${Date.now()}`, 39 + turnstile_token: turnstileToken ?? "", 40 + }), 41 + }); 42 + } catch (error) { 43 + console.error("Failed to send skip analytics:", error); 44 + } 45 + }, [latestSkip, meta, account]); 17 46 18 47 useEffect(() => { 19 48 if (!latestSkip || !meta) return; 20 49 21 - // Avoid logging the same skip multiple times 50 + // Avoid processing the same skip multiple times 22 51 if (latestSkip.timestamp === lastLoggedSkipRef.current) return; 23 52 24 - // Format skip duration for readability 25 - const formatDuration = (seconds: number): string => { 26 - const absSeconds = Math.abs(seconds); 27 - const minutes = Math.floor(absSeconds / 60); 28 - const remainingSeconds = Math.floor(absSeconds % 60); 29 - 30 - if (minutes > 0) { 31 - return `${minutes}m ${remainingSeconds}s`; 32 - } 33 - return `${remainingSeconds}s`; 34 - }; 35 - 36 - // Format time position for readability 37 - const formatTime = (seconds: number): string => { 38 - const minutes = Math.floor(seconds / 60); 39 - const remainingSeconds = Math.floor(seconds % 60); 40 - return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; 41 - }; 42 - 43 - const skipDirection = latestSkip.skipDuration > 0 ? "forward" : "backward"; 44 - const skipType = Math.abs(latestSkip.skipDuration) >= 30 ? "scrub" : "skip"; 45 - 46 - // Log the skip event with detailed information 53 + // Log completed skip session 47 54 // eslint-disable-next-line no-console 48 - console.log(`User ${skipType.toUpperCase()} detected`, { 49 - // Basic skip info 50 - direction: skipDirection, 51 - duration: formatDuration(latestSkip.skipDuration), 52 - from: formatTime(latestSkip.startTime), 53 - to: formatTime(latestSkip.endTime), 55 + console.log(`Skip session completed: ${latestSkip.skipDuration}s total`); 54 56 55 - // Content context 56 - content: { 57 - title: latestSkip.meta?.title || "Unknown", 58 - type: latestSkip.meta?.type || "Unknown", 59 - tmdbId: latestSkip.meta?.tmdbId, 60 - }, 61 - 62 - // Episode context (for TV shows) 63 - ...(meta.type === "show" && { 64 - episode: { 65 - season: latestSkip.meta?.seasonNumber, 66 - episode: latestSkip.meta?.episodeNumber, 67 - }, 68 - }), 69 - 70 - // Analytics data that could be sent to backend 71 - analytics: { 72 - timestamp: new Date(latestSkip.timestamp).toISOString(), 73 - startTime: latestSkip.startTime, 74 - endTime: latestSkip.endTime, 75 - skipDuration: latestSkip.skipDuration, 76 - contentId: latestSkip.meta?.tmdbId, 77 - contentType: latestSkip.meta?.type, 78 - seasonId: meta.season?.tmdbId, 79 - episodeId: meta.episode?.tmdbId, 80 - }, 81 - }); 82 - 83 - // Log special cases that might indicate intro skipping 84 - if ( 85 - meta.type === "show" && 86 - latestSkip.startTime <= 30 && // Skip happened in first 30 seconds 87 - latestSkip.skipDuration > 15 && // Forward skip of at least 15 seconds 88 - latestSkip.skipDuration <= 120 // But not more than 2 minutes (reasonable intro length) 89 - ) { 90 - // eslint-disable-next-line no-console 91 - console.log(`Potential intro skip dete`, { 92 - show: latestSkip.meta?.title, 93 - season: latestSkip.meta?.seasonNumber, 94 - episode: latestSkip.meta?.episodeNumber, 95 - introSkipDuration: formatDuration(latestSkip.skipDuration), 96 - message: "User likely skipped intro sequence", 97 - }); 98 - } 99 - 100 - // Log potential outro/credits skipping 101 - const progress = usePlayerStore.getState().progress; 102 - const timeRemaining = progress.duration - latestSkip.endTime; 103 - if ( 104 - latestSkip.skipDuration > 0 && // Forward skip 105 - timeRemaining <= 300 && // Within last 5 minutes 106 - latestSkip.skipDuration >= 15 107 - ) { 108 - // eslint-disable-next-line no-console 109 - console.log(`Potential outro skip detected`, { 110 - content: latestSkip.meta?.title, 111 - timeRemaining: formatDuration(timeRemaining), 112 - skipDuration: formatDuration(latestSkip.skipDuration), 113 - message: "User likely skipped credits/outro", 114 - }); 115 - } 57 + // Send analytics data to backend 58 + sendSkipAnalytics(); 116 59 117 60 lastLoggedSkipRef.current = latestSkip.timestamp; 118 - }, [latestSkip, meta]); 119 - 120 - // Log summary statistics occasionally... just for testing, we likely wont use it unless useful. 121 - useEffect(() => { 122 - if (skipHistory.length > 0 && skipHistory.length % 5 === 0) { 123 - const forwardSkips = skipHistory.filter((s) => s.skipDuration > 0); 124 - const backwardSkips = skipHistory.filter((s) => s.skipDuration < 0); 125 - const avgSkipDuration = 126 - skipHistory.reduce((sum, s) => sum + Math.abs(s.skipDuration), 0) / 127 - skipHistory.length; 128 - 129 - // eslint-disable-next-line no-console 130 - console.log(`skip analytics`, { 131 - totalSkips: skipHistory.length, 132 - forwardSkips: forwardSkips.length, 133 - backwardSkips: backwardSkips.length, 134 - averageSkipDuration: `${Math.round(avgSkipDuration)}s`, 135 - content: meta?.title || "Unknown", 136 - }); 137 - } 138 - }, [skipHistory.length, skipHistory, meta?.title]); 139 - 140 - // TODO: When backend endpoint is ready, replace console.log with API calls 141 - // Example implementation: 142 - /* 143 - useEffect(() => { 144 - if (!latestSkip || !account?.userId) return; 145 - 146 - // Send skip data to analytics endpoint 147 - const sendSkipAnalytics = async () => { 148 - try { 149 - await fetch(`${backendUrl}/api/analytics/skips`, { 150 - method: 'POST', 151 - headers: { 'Content-Type': 'application/json' }, 152 - body: JSON.stringify({ 153 - userId: account.userId, 154 - skipData: latestSkip, 155 - contentContext: { 156 - tmdbId: meta?.tmdbId, 157 - type: meta?.type, 158 - seasonId: meta?.season?.tmdbId, 159 - episodeId: meta?.episode?.tmdbId, 160 - } 161 - }) 162 - }); 163 - } catch (error) { 164 - console.error('Failed to send skip analytics:', error); 165 - } 166 - }; 167 - 168 - sendSkipAnalytics(); 169 - }, [latestSkip, account?.userId, meta]); 170 - */ 61 + }, [latestSkip, meta, sendSkipAnalytics]); 171 62 172 63 return null; 173 64 }
+7
src/hooks/useWatchPartySync.ts
··· 82 82 // Get watch party state 83 83 const { roomCode, isHost, enabled, enableAsGuest } = useWatchPartyStore(); 84 84 85 + // Reset URL parameter checking when watch party is disabled 86 + useEffect(() => { 87 + if (!enabled) { 88 + syncStateRef.current.checkedUrlParams = false; 89 + } 90 + }, [enabled]); 91 + 85 92 // Check URL parameters for watch party code 86 93 useEffect(() => { 87 94 if (syncStateRef.current.checkedUrlParams) return;
+2 -6
src/pages/discover/components/FeaturedCarousel.tsx
··· 573 573 574 574 if (hasDigitalRelease) { 575 575 const digitalReleaseDate = new Date(releaseInfo.digital_release_date!); 576 - const twoDaysAfter = new Date(digitalReleaseDate); 577 - twoDaysAfter.setDate(twoDaysAfter.getDate() + 2); 578 576 579 - if (new Date() >= twoDaysAfter) { 577 + if (new Date() >= digitalReleaseDate) { 580 578 return <span className="text-green-400">HD</span>; 581 579 } 582 580 } ··· 585 583 const theatricalReleaseDate = new Date( 586 584 releaseInfo.theatrical_release_date!, 587 585 ); 588 - const fortyFiveDaysAfter = new Date(theatricalReleaseDate); 589 - fortyFiveDaysAfter.setDate(fortyFiveDaysAfter.getDate() + 45); 590 586 591 - if (new Date() >= fortyFiveDaysAfter) { 587 + if (new Date() >= theatricalReleaseDate) { 592 588 return ( 593 589 <div className="px-2 py-1 rounded-lg backdrop-blur-sm bg-gray-600/40"> 594 590 <span className="text-green-400">HD</span>
+6 -10
src/pages/parts/player/PlayerPart.tsx
··· 179 179 ) : null} 180 180 {status === playerStatus.PLAYBACK_ERROR || 181 181 status === playerStatus.PLAYING ? ( 182 - <> 183 - <Player.Captions /> 184 - <Player.Settings /> 185 - </> 182 + <Player.Captions /> 186 183 ) : null} 184 + <Player.Settings /> 187 185 {isShifting || isHoldingFullscreen ? ( 188 186 <Player.Widescreen /> 189 187 ) : ( ··· 200 198 )} 201 199 <Player.Episodes inControl={inControl} /> 202 200 {status === playerStatus.PLAYING ? ( 203 - <> 204 - <div className="hidden ssm:block"> 205 - <Player.Captions /> 206 - </div> 207 - <Player.Settings /> 208 - </> 201 + <div className="hidden ssm:block"> 202 + <Player.Captions /> 203 + </div> 209 204 ) : null} 205 + <Player.Settings /> 210 206 </div> 211 207 <div> 212 208 {status === playerStatus.PLAYING && (
+2 -2
src/setup/config.ts
··· 28 28 ALLOW_FEBBOX_KEY: boolean; 29 29 ALLOW_REAL_DEBRID_KEY: boolean; 30 30 SHOW_AD: boolean; 31 - AD_CONTENT_URL: string; // like <script src="https://umami.com/script.js"></script> 32 - TRACK_SCRIPT: string; 31 + AD_CONTENT_URL: string; 32 + TRACK_SCRIPT: string; // like <script src="https://umami.com/script.js"></script> 33 33 BANNER_MESSAGE: string; 34 34 BANNER_ID: string; 35 35 }