mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-08 03:00:41 +00:00
usemetadata maximum depth limit reached potential fix
This commit is contained in:
parent
43f6f056c0
commit
ccde944bfa
2 changed files with 121 additions and 68 deletions
|
|
@ -723,6 +723,13 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
const shimmerOpacity = useSharedValue(0.3);
|
const shimmerOpacity = useSharedValue(0.3);
|
||||||
const trailerOpacity = useSharedValue(0);
|
const trailerOpacity = useSharedValue(0);
|
||||||
const thumbnailOpacity = useSharedValue(1);
|
const thumbnailOpacity = useSharedValue(1);
|
||||||
|
// Scroll-based pause/resume control
|
||||||
|
const pausedByScrollSV = useSharedValue(0);
|
||||||
|
const scrollGuardEnabledSV = useSharedValue(0);
|
||||||
|
const isPlayingSV = useSharedValue(0);
|
||||||
|
// Guards to avoid repeated auto-starts
|
||||||
|
const startedOnFocusRef = useRef(false);
|
||||||
|
const startedOnReadyRef = useRef(false);
|
||||||
|
|
||||||
// Animation values for trailer unmute effects
|
// Animation values for trailer unmute effects
|
||||||
const actionButtonsOpacity = useSharedValue(1);
|
const actionButtonsOpacity = useSharedValue(1);
|
||||||
|
|
@ -753,14 +760,18 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
// Smooth transition: fade out thumbnail, fade in trailer
|
// Smooth transition: fade out thumbnail, fade in trailer
|
||||||
thumbnailOpacity.value = withTiming(0, { duration: 500 });
|
thumbnailOpacity.value = withTiming(0, { duration: 500 });
|
||||||
trailerOpacity.value = withTiming(1, { duration: 500 });
|
trailerOpacity.value = withTiming(1, { duration: 500 });
|
||||||
|
// Enable scroll guard after a brief delay to avoid immediate pause on entry
|
||||||
|
scrollGuardEnabledSV.value = 0;
|
||||||
|
setTimeout(() => { scrollGuardEnabledSV.value = 1; }, 1000);
|
||||||
}, [thumbnailOpacity, trailerOpacity, trailerPreloaded]);
|
}, [thumbnailOpacity, trailerOpacity, trailerPreloaded]);
|
||||||
|
|
||||||
// Ensure trailer state is properly synchronized when trailer becomes ready
|
// Auto-start trailer when ready on initial entry if enabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (trailerReady && settings?.showTrailers && !globalTrailerPlaying) {
|
if (trailerReady && settings?.showTrailers && !globalTrailerPlaying && !startedOnReadyRef.current) {
|
||||||
// Only start trailer if it's the initial load, not when returning from other screens
|
startedOnReadyRef.current = true;
|
||||||
// This prevents auto-starting when returning from StreamsScreen
|
logger.info('HeroSection', 'Trailer ready - auto-starting playback');
|
||||||
logger.info('HeroSection', 'Trailer ready but not playing - not auto-starting to prevent unwanted playback');
|
setTrailerPlaying(true);
|
||||||
|
isPlayingSV.value = 1;
|
||||||
}
|
}
|
||||||
}, [trailerReady, settings?.showTrailers, globalTrailerPlaying, setTrailerPlaying]);
|
}, [trailerReady, settings?.showTrailers, globalTrailerPlaying, setTrailerPlaying]);
|
||||||
|
|
||||||
|
|
@ -1036,25 +1047,61 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
// Screen is focused - only resume trailer if it was previously playing and got interrupted
|
// Screen is focused - only resume trailer if it was previously playing and got interrupted
|
||||||
logger.info('HeroSection', 'Screen focused');
|
logger.info('HeroSection', 'Screen focused');
|
||||||
|
// If trailers are enabled and not playing, start playback (unless scrolled past resume threshold)
|
||||||
// Don't automatically resume trailer when returning from other screens
|
if (settings?.showTrailers) {
|
||||||
// This prevents the trailer from starting when returning from StreamsScreen
|
setTimeout(() => {
|
||||||
// The trailer should only resume if the user explicitly wants it to play
|
try {
|
||||||
|
const y = (scrollY as any).value || 0;
|
||||||
|
const resumeThreshold = heroHeight.value * 0.4;
|
||||||
|
if (y < resumeThreshold && !startedOnFocusRef.current && isPlayingSV.value === 0) {
|
||||||
|
setTrailerPlaying(true);
|
||||||
|
isPlayingSV.value = 1;
|
||||||
|
startedOnFocusRef.current = true;
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
if (!startedOnFocusRef.current && isPlayingSV.value === 0) {
|
||||||
|
setTrailerPlaying(true);
|
||||||
|
isPlayingSV.value = 1;
|
||||||
|
startedOnFocusRef.current = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Stop trailer when leaving this screen to prevent background playback/heat
|
// Stop trailer when leaving this screen to prevent background playback/heat
|
||||||
logger.info('HeroSection', 'Screen unfocused - stopping trailer playback');
|
logger.info('HeroSection', 'Screen unfocused - stopping trailer playback');
|
||||||
setTrailerPlaying(false);
|
setTrailerPlaying(false);
|
||||||
|
isPlayingSV.value = 0;
|
||||||
|
startedOnFocusRef.current = false;
|
||||||
|
startedOnReadyRef.current = false;
|
||||||
};
|
};
|
||||||
}, [setTrailerPlaying])
|
}, [setTrailerPlaying, settings?.showTrailers])
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pause trailer when the hero is scrolled substantially off-screen
|
// Mirror playing state to shared value to use inside worklets
|
||||||
|
useEffect(() => {
|
||||||
|
isPlayingSV.value = globalTrailerPlaying ? 1 : 0;
|
||||||
|
}, [globalTrailerPlaying]);
|
||||||
|
|
||||||
|
// Pause/resume trailer based on scroll with hysteresis and guard
|
||||||
useDerivedValue(() => {
|
useDerivedValue(() => {
|
||||||
|
'worklet';
|
||||||
try {
|
try {
|
||||||
const threshold = heroHeight.value * 0.6;
|
if (!scrollGuardEnabledSV.value) return;
|
||||||
if (scrollY.value > threshold) {
|
const pauseThreshold = heroHeight.value * 0.7; // pause when beyond 70%
|
||||||
|
const resumeThreshold = heroHeight.value * 0.4; // resume when back within 40%
|
||||||
|
|
||||||
|
const y = scrollY.value;
|
||||||
|
|
||||||
|
if (y > pauseThreshold && isPlayingSV.value === 1 && pausedByScrollSV.value === 0) {
|
||||||
|
pausedByScrollSV.value = 1;
|
||||||
runOnJS(setTrailerPlaying)(false);
|
runOnJS(setTrailerPlaying)(false);
|
||||||
|
isPlayingSV.value = 0;
|
||||||
|
} else if (y < resumeThreshold && pausedByScrollSV.value === 1) {
|
||||||
|
pausedByScrollSV.value = 0;
|
||||||
|
runOnJS(setTrailerPlaying)(true);
|
||||||
|
isPlayingSV.value = 1;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// no-op
|
// no-op
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { StreamingContent } from '../services/catalogService';
|
import { StreamingContent } from '../services/catalogService';
|
||||||
import { catalogService } from '../services/catalogService';
|
import { catalogService } from '../services/catalogService';
|
||||||
import { stremioService } from '../services/stremioService';
|
import { stremioService } from '../services/stremioService';
|
||||||
|
|
@ -134,6 +134,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
const [availableStreams, setAvailableStreams] = useState<{ [sourceType: string]: Stream }>({});
|
const [availableStreams, setAvailableStreams] = useState<{ [sourceType: string]: Stream }>({});
|
||||||
const [scraperStatuses, setScraperStatuses] = useState<ScraperStatus[]>([]);
|
const [scraperStatuses, setScraperStatuses] = useState<ScraperStatus[]>([]);
|
||||||
const [activeFetchingScrapers, setActiveFetchingScrapers] = useState<string[]>([]);
|
const [activeFetchingScrapers, setActiveFetchingScrapers] = useState<string[]>([]);
|
||||||
|
// Prevent re-initializing season selection repeatedly for the same series
|
||||||
|
const initializedSeasonRef = useRef(false);
|
||||||
|
|
||||||
// Add hook for persistent seasons
|
// Add hook for persistent seasons
|
||||||
const { getSeason, saveSeason } = usePersistentSeasons();
|
const { getSeason, saveSeason } = usePersistentSeasons();
|
||||||
|
|
@ -591,12 +593,21 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
|
|
||||||
setGroupedEpisodes(groupedAddonEpisodes);
|
setGroupedEpisodes(groupedAddonEpisodes);
|
||||||
|
|
||||||
// Set the first available season
|
// Determine initial season only once per series
|
||||||
const seasons = Object.keys(groupedAddonEpisodes).map(Number);
|
const seasons = Object.keys(groupedAddonEpisodes).map(Number);
|
||||||
const firstSeason = Math.min(...seasons);
|
const firstSeason = Math.min(...seasons);
|
||||||
logger.log(`📺 Setting season ${firstSeason} as selected (${groupedAddonEpisodes[firstSeason]?.length || 0} episodes)`);
|
if (!initializedSeasonRef.current) {
|
||||||
setSelectedSeason(firstSeason);
|
const nextSeason = firstSeason;
|
||||||
setEpisodes(groupedAddonEpisodes[firstSeason] || []);
|
if (selectedSeason !== nextSeason) {
|
||||||
|
logger.log(`📺 Setting season ${nextSeason} as selected (${groupedAddonEpisodes[nextSeason]?.length || 0} episodes)`);
|
||||||
|
setSelectedSeason(nextSeason);
|
||||||
|
}
|
||||||
|
setEpisodes(groupedAddonEpisodes[nextSeason] || []);
|
||||||
|
initializedSeasonRef.current = true;
|
||||||
|
} else {
|
||||||
|
// Keep current selection; refresh episode list for selected season
|
||||||
|
setEpisodes(groupedAddonEpisodes[selectedSeason] || []);
|
||||||
|
}
|
||||||
|
|
||||||
// Try to get TMDB ID for additional metadata (cast, etc.) but don't override episodes
|
// Try to get TMDB ID for additional metadata (cast, etc.) but don't override episodes
|
||||||
const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id);
|
const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id);
|
||||||
|
|
@ -640,61 +651,55 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
// Get the first available season as fallback
|
// Get the first available season as fallback
|
||||||
const firstSeason = Math.min(...Object.keys(allEpisodes).map(Number));
|
const firstSeason = Math.min(...Object.keys(allEpisodes).map(Number));
|
||||||
|
|
||||||
// Check for watch progress to auto-select season
|
if (!initializedSeasonRef.current) {
|
||||||
let selectedSeasonNumber = firstSeason;
|
// Check for watch progress to auto-select season
|
||||||
|
let selectedSeasonNumber = firstSeason;
|
||||||
try {
|
try {
|
||||||
// Check watch progress for auto-season selection
|
const allProgress = await storageService.getAllWatchProgress();
|
||||||
const allProgress = await storageService.getAllWatchProgress();
|
let mostRecentEpisodeId = '';
|
||||||
|
let mostRecentTimestamp = 0;
|
||||||
// Find the most recently watched episode for this series
|
Object.entries(allProgress).forEach(([key, progress]) => {
|
||||||
let mostRecentEpisodeId = '';
|
if (key.includes(`series:${id}:`)) {
|
||||||
let mostRecentTimestamp = 0;
|
const episodeId = key.split(`series:${id}:`)[1];
|
||||||
|
if (progress.lastUpdated > mostRecentTimestamp && progress.currentTime > 0) {
|
||||||
Object.entries(allProgress).forEach(([key, progress]) => {
|
mostRecentTimestamp = progress.lastUpdated;
|
||||||
if (key.includes(`series:${id}:`)) {
|
mostRecentEpisodeId = episodeId;
|
||||||
const episodeId = key.split(`series:${id}:`)[1];
|
}
|
||||||
if (progress.lastUpdated > mostRecentTimestamp && progress.currentTime > 0) {
|
|
||||||
mostRecentTimestamp = progress.lastUpdated;
|
|
||||||
mostRecentEpisodeId = episodeId;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
if (mostRecentEpisodeId) {
|
||||||
|
const parts = mostRecentEpisodeId.split(':');
|
||||||
if (mostRecentEpisodeId) {
|
if (parts.length === 3) {
|
||||||
// Parse season number from episode ID
|
const watchProgressSeason = parseInt(parts[1], 10);
|
||||||
const parts = mostRecentEpisodeId.split(':');
|
if (transformedEpisodes[watchProgressSeason]) {
|
||||||
if (parts.length === 3) {
|
selectedSeasonNumber = watchProgressSeason;
|
||||||
const watchProgressSeason = parseInt(parts[1], 10);
|
logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for ${mostRecentEpisodeId}`);
|
||||||
if (transformedEpisodes[watchProgressSeason]) {
|
}
|
||||||
selectedSeasonNumber = watchProgressSeason;
|
} else {
|
||||||
logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for ${mostRecentEpisodeId}`);
|
const allEpisodesList = Object.values(transformedEpisodes).flat();
|
||||||
|
const episode = allEpisodesList.find(ep => ep.stremioId === mostRecentEpisodeId);
|
||||||
|
if (episode) {
|
||||||
|
selectedSeasonNumber = episode.season_number;
|
||||||
|
logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for episode with stremioId ${mostRecentEpisodeId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Try to find episode by stremioId to get season
|
selectedSeasonNumber = getSeason(id, firstSeason);
|
||||||
const allEpisodesList = Object.values(transformedEpisodes).flat();
|
logger.log(`[useMetadata] No watch progress found, using persistent season ${selectedSeasonNumber}`);
|
||||||
const episode = allEpisodesList.find(ep => ep.stremioId === mostRecentEpisodeId);
|
|
||||||
if (episode) {
|
|
||||||
selectedSeasonNumber = episode.season_number;
|
|
||||||
logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for episode with stremioId ${mostRecentEpisodeId}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
// No watch progress found, use persistent storage as fallback
|
logger.error('[useMetadata] Error checking watch progress for season selection:', error);
|
||||||
selectedSeasonNumber = getSeason(id, firstSeason);
|
selectedSeasonNumber = getSeason(id, firstSeason);
|
||||||
logger.log(`[useMetadata] No watch progress found, using persistent season ${selectedSeasonNumber}`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
if (selectedSeason !== selectedSeasonNumber) {
|
||||||
logger.error('[useMetadata] Error checking watch progress for season selection:', error);
|
setSelectedSeason(selectedSeasonNumber);
|
||||||
// Fall back to persistent storage
|
}
|
||||||
selectedSeasonNumber = getSeason(id, firstSeason);
|
setEpisodes(transformedEpisodes[selectedSeasonNumber] || []);
|
||||||
|
initializedSeasonRef.current = true;
|
||||||
|
} else {
|
||||||
|
// Keep existing selection stable and only refresh episode list for it
|
||||||
|
setEpisodes(transformedEpisodes[selectedSeason] || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the selected season
|
|
||||||
setSelectedSeason(selectedSeasonNumber);
|
|
||||||
|
|
||||||
// Set episodes for the selected season
|
|
||||||
setEpisodes(transformedEpisodes[selectedSeasonNumber] || []);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (__DEV__) console.error('Failed to load episodes:', error);
|
if (__DEV__) console.error('Failed to load episodes:', error);
|
||||||
|
|
@ -1082,6 +1087,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
// Reset load attempts when id or type changes
|
// Reset load attempts when id or type changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoadAttempts(0);
|
setLoadAttempts(0);
|
||||||
|
initializedSeasonRef.current = false;
|
||||||
}, [id, type]);
|
}, [id, type]);
|
||||||
|
|
||||||
// Auto-retry on error with delay
|
// Auto-retry on error with delay
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue