episodes not fetching backdrop fix tablet layout

This commit is contained in:
tapframe 2025-10-23 01:57:04 +05:30
parent ef1c34a9c0
commit 786e06b27f
4 changed files with 75 additions and 39 deletions

View file

@ -128,6 +128,7 @@ const TabletStreamsLayout: React.FC<TabletStreamsLayoutProps> = ({
const backdropOpacity = useSharedValue(0);
const backdropScale = useSharedValue(1.05);
const [backdropLoaded, setBackdropLoaded] = useState(false);
const [backdropError, setBackdropError] = useState(false);
// Animation values for content panels
const leftPanelOpacity = useSharedValue(0);
@ -135,10 +136,46 @@ const TabletStreamsLayout: React.FC<TabletStreamsLayoutProps> = ({
const rightPanelOpacity = useSharedValue(0);
const rightPanelTranslateX = useSharedValue(30);
// Get the backdrop source
const backdropSource = episodeImage ? { uri: episodeImage } :
bannerImage ? { uri: bannerImage } :
metadata?.poster ? { uri: metadata.poster } : undefined;
// Get the backdrop source - prioritize episode thumbnail, then show backdrop, then poster
// For episodes without thumbnails, use show's backdrop instead of poster
const backdropSource = React.useMemo(() => {
// Debug logging
if (__DEV__) {
console.log('[TabletStreamsLayout] Backdrop source selection:', {
episodeImage,
bannerImage,
metadataPoster: metadata?.poster,
episodeImageIsPoster: episodeImage === metadata?.poster,
backdropError
});
}
// If episodeImage failed to load, skip it and use backdrop
if (backdropError && episodeImage && episodeImage !== metadata?.poster) {
if (__DEV__) console.log('[TabletStreamsLayout] Episode thumbnail failed, falling back to backdrop');
if (bannerImage) {
if (__DEV__) console.log('[TabletStreamsLayout] Using show backdrop (episode failed):', bannerImage);
return { uri: bannerImage };
}
}
// If episodeImage exists and is not the same as poster, use it (real episode thumbnail)
if (episodeImage && episodeImage !== metadata?.poster && !backdropError) {
if (__DEV__) console.log('[TabletStreamsLayout] Using episode thumbnail:', episodeImage);
return { uri: episodeImage };
}
// If episodeImage is the same as poster (fallback case), prioritize backdrop
if (bannerImage) {
if (__DEV__) console.log('[TabletStreamsLayout] Using show backdrop:', bannerImage);
return { uri: bannerImage };
}
// No fallback to poster images
if (__DEV__) console.log('[TabletStreamsLayout] No backdrop source found');
return undefined;
}, [episodeImage, bannerImage, metadata?.poster, backdropError]);
// Animate backdrop when it loads
useEffect(() => {
@ -173,18 +210,17 @@ const TabletStreamsLayout: React.FC<TabletStreamsLayoutProps> = ({
}
}, [backdropSource?.uri, backdropLoaded]);
// Reset animation when source changes
// Reset animation when episode changes
useEffect(() => {
if (backdropSource?.uri) {
backdropOpacity.value = 0;
backdropScale.value = 1.05;
leftPanelOpacity.value = 0;
leftPanelTranslateX.value = -30;
rightPanelOpacity.value = 0;
rightPanelTranslateX.value = 30;
setBackdropLoaded(false);
}
}, [backdropSource?.uri]);
backdropOpacity.value = 0;
backdropScale.value = 1.05;
leftPanelOpacity.value = 0;
leftPanelTranslateX.value = -30;
rightPanelOpacity.value = 0;
rightPanelTranslateX.value = 30;
setBackdropLoaded(false);
setBackdropError(false);
}, [episodeImage]);
// Animated styles for backdrop
const backdropAnimatedStyle = useAnimatedStyle(() => ({
@ -206,6 +242,12 @@ const TabletStreamsLayout: React.FC<TabletStreamsLayoutProps> = ({
const handleBackdropLoad = () => {
setBackdropLoaded(true);
};
const handleBackdropError = () => {
if (__DEV__) console.log('[TabletStreamsLayout] Backdrop image failed to load:', backdropSource?.uri);
setBackdropError(true);
setBackdropLoaded(false);
};
const renderStreamContent = () => {
if (showNoSourcesError) {
@ -284,7 +326,7 @@ const TabletStreamsLayout: React.FC<TabletStreamsLayoutProps> = ({
theme={currentTheme}
showLogos={settings.showScraperLogos}
scraperLogo={(item.addonId && scraperLogos[item.addonId]) || (item as any).addon ? scraperLogos[(item.addonId || (item as any).addon) as string] || null : null}
showAlert={(t, m) => openAlert(t, m)}
showAlert={(t: string, m: string) => openAlert(t, m)}
parentTitle={metadata?.name}
parentType={type as 'movie' | 'series'}
parentSeason={(type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined}
@ -332,6 +374,7 @@ const TabletStreamsLayout: React.FC<TabletStreamsLayoutProps> = ({
style={StyleSheet.absoluteFillObject}
resizeMode={FastImage.resizeMode.cover}
onLoad={handleBackdropLoad}
onError={handleBackdropError}
/>
</Animated.View>
<LinearGradient

View file

@ -875,6 +875,13 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
}
// Commit final metadata once and cache it
// Clear banner field if TMDB enrichment is enabled to prevent flash
if (settings.enrichMetadataWithTMDB) {
finalMetadata = {
...finalMetadata,
banner: undefined, // Let useMetadataAssets handle banner via TMDB
};
}
setMetadata(finalMetadata);
cacheService.setMetadata(id, type, finalMetadata);
const isInLib = catalogService.getLibraryItems().some(item => item.id === id);

View file

@ -251,16 +251,10 @@ export const useMetadataAssets = (
setLoadingBanner(true);
// Show fallback banner immediately to prevent blank state
const fallbackBanner = metadata?.banner || metadata?.poster || null;
if (fallbackBanner && !bannerImage) {
setBannerImage(fallbackBanner);
setBannerSource('default');
}
// If enrichment is disabled, use addon banner and don't fetch from external sources
if (!settings.enrichMetadataWithTMDB) {
const addonBanner = metadata?.banner || metadata?.poster || null;
const addonBanner = metadata?.banner || null;
if (addonBanner && addonBanner !== bannerImage) {
setBannerImage(addonBanner);
setBannerSource('default');
@ -312,15 +306,6 @@ export const useMetadataAssets = (
finalBanner = tmdbService.getImageUrl(details.backdrop_path);
bannerSourceType = 'tmdb';
// Preload the image
if (finalBanner) {
FastImage.preload([{ uri: finalBanner }]);
}
}
else if (details?.poster_path) {
finalBanner = tmdbService.getImageUrl(details.poster_path);
bannerSourceType = 'tmdb';
// Preload the image
if (finalBanner) {
FastImage.preload([{ uri: finalBanner }]);
@ -332,9 +317,9 @@ export const useMetadataAssets = (
}
}
// Final fallback to metadata
// Final fallback to metadata banner only
if (!finalBanner) {
finalBanner = metadata?.banner || metadata?.poster || null;
finalBanner = metadata?.banner || null;
bannerSourceType = 'default';
}
@ -346,8 +331,8 @@ export const useMetadataAssets = (
forcedBannerRefreshDone.current = true;
} catch (error) {
// Use default banner on error
const defaultBanner = metadata?.banner || metadata?.poster || null;
// Use default banner on error (only addon banner)
const defaultBanner = metadata?.banner || null;
if (defaultBanner !== bannerImage) {
setBannerImage(defaultBanner);
setBannerSource('default');

View file

@ -227,7 +227,7 @@ export const StreamsScreen = () => {
// Get backdrop from metadata assets
const setMetadataStub = useCallback(() => {}, []);
const memoizedSettings = useMemo(() => settings, [settings.logoSourcePreference, settings.tmdbLanguagePreference]);
const memoizedSettings = useMemo(() => settings, [settings.logoSourcePreference, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB]);
const { bannerImage } = useMetadataAssets(metadata, id, type, imdbId, memoizedSettings, setMetadataStub);
// Create styles using current theme colors
@ -1532,8 +1532,9 @@ export const StreamsScreen = () => {
const path = currentEpisode.still_path || hydratedStill || '';
return tmdbService.getImageUrl(path, 'original');
}
return metadata?.poster || null;
}, [currentEpisode, metadata, episodeThumbnail, tmdbEpisodeOverride?.still_path]);
// No poster fallback
return null;
}, [currentEpisode, metadata, episodeThumbnail, tmdbEpisodeOverride?.still_path, settings.enrichMetadataWithTMDB]);
// Effective TMDB fields for hero (series)
const effectiveEpisodeVote = useMemo(() => {