diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index bb135468..ec3b664b 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -36,6 +36,7 @@ export interface AppSettings { tmdbLanguagePreference: string; // Preferred language for TMDB logos (ISO 639-1 code) enableInternalProviders: boolean; // Toggle for internal providers like HDRezka episodeLayoutStyle: 'vertical' | 'horizontal'; // Layout style for episode cards + autoplayBestStream: boolean; // Automatically play the best available stream } export const DEFAULT_SETTINGS: AppSettings = { @@ -54,6 +55,7 @@ export const DEFAULT_SETTINGS: AppSettings = { tmdbLanguagePreference: 'en', // Default to English enableInternalProviders: true, // Enable internal providers by default episodeLayoutStyle: 'horizontal', // Default to the new horizontal layout + autoplayBestStream: false, // Disabled by default for user choice }; const SETTINGS_STORAGE_KEY = 'app_settings'; diff --git a/src/screens/PlayerSettingsScreen.tsx b/src/screens/PlayerSettingsScreen.tsx index 93fd3425..31a18bab 100644 --- a/src/screens/PlayerSettingsScreen.tsx +++ b/src/screens/PlayerSettingsScreen.tsx @@ -8,6 +8,7 @@ import { Platform, TouchableOpacity, StatusBar, + Switch, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { useSettings, AppSettings } from '../hooks/useSettings'; @@ -219,6 +220,68 @@ const PlayerSettingsScreen: React.FC = () => { ))} + + + + PLAYBACK OPTIONS + + + + + + + + + + Auto-play Best Stream + + + Automatically play the highest quality stream when available + + + updateSetting('autoplayBestStream', value)} + trackColor={{ + false: 'rgba(255,255,255,0.2)', + true: currentTheme.colors.primary + '40' + }} + thumbColor={settings.autoplayBestStream ? currentTheme.colors.primary : 'rgba(255,255,255,0.8)'} + ios_backgroundColor="rgba(255,255,255,0.2)" + /> + + + + ); diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 70d74754..e09c4ee5 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -302,6 +302,10 @@ export const StreamsScreen = () => { } }>({}); + // Add state for autoplay functionality + const [autoplayTriggered, setAutoplayTriggered] = useState(false); + const [isAutoplayWaiting, setIsAutoplayWaiting] = useState(false); + // Monitor streams loading start and completion - FIXED to prevent loops useEffect(() => { // Skip processing if component is unmounting @@ -438,6 +442,8 @@ export const StreamsScreen = () => { } }, [type, groupedStreams, episodeStreams, loadingStreams, loadingEpisodeStreams, selectedProvider]); + + React.useEffect(() => { if (type === 'series' && episodeId) { logger.log(`🎬 Loading episode streams for: ${episodeId}`); @@ -455,7 +461,16 @@ export const StreamsScreen = () => { // }); loadStreams(); } - }, [type, episodeId]); + + // Reset autoplay state when content changes + setAutoplayTriggered(false); + if (settings.autoplayBestStream) { + setIsAutoplayWaiting(true); + logger.log('🔄 Autoplay enabled, waiting for best stream...'); + } else { + setIsAutoplayWaiting(false); + } + }, [type, episodeId, settings.autoplayBestStream]); React.useEffect(() => { // Trigger entrance animations @@ -500,6 +515,101 @@ export const StreamsScreen = () => { setSelectedProvider(provider); }, []); + // Function to determine the best stream based on quality, provider priority, and other factors + const getBestStream = useCallback((streamsData: typeof groupedStreams): Stream | null => { + if (!streamsData || Object.keys(streamsData).length === 0) { + return null; + } + + // Helper function to extract quality as number + const getQualityNumeric = (title: string | undefined): number => { + if (!title) return 0; + const matchWithP = title.match(/(\d+)p/i); + if (matchWithP) return parseInt(matchWithP[1], 10); + + const qualityPatterns = [ + /\b(240|360|480|720|1080|1440|2160|4320|8000)\b/i + ]; + + for (const pattern of qualityPatterns) { + const match = title.match(pattern); + if (match) { + const quality = parseInt(match[1], 10); + if (quality >= 240 && quality <= 8000) return quality; + } + } + return 0; + }; + + // Provider priority (higher number = higher priority) + const getProviderPriority = (addonId: string): number => { + if (addonId === 'hdrezka') return 100; // HDRezka highest priority + + // Get Stremio addon installation order (earlier = higher priority) + const installedAddons = stremioService.getInstalledAddons(); + const addonIndex = installedAddons.findIndex(addon => addon.id === addonId); + + if (addonIndex !== -1) { + // Higher priority for addons installed earlier (reverse index) + return 50 - addonIndex; + } + + return 0; // Unknown providers get lowest priority + }; + + // Collect all streams with metadata + const allStreams: Array<{ + stream: Stream; + quality: number; + providerPriority: number; + isDebrid: boolean; + isCached: boolean; + }> = []; + + Object.entries(streamsData).forEach(([addonId, { streams }]) => { + streams.forEach(stream => { + const quality = getQualityNumeric(stream.name || stream.title); + const providerPriority = getProviderPriority(addonId); + const isDebrid = stream.behaviorHints?.cached || false; + const isCached = isDebrid; + + allStreams.push({ + stream, + quality, + providerPriority, + isDebrid, + isCached, + }); + }); + }); + + if (allStreams.length === 0) return null; + + // Sort streams by multiple criteria (best first) + allStreams.sort((a, b) => { + // 1. Prioritize cached/debrid streams + if (a.isCached !== b.isCached) { + return a.isCached ? -1 : 1; + } + + // 2. Prioritize higher quality + if (a.quality !== b.quality) { + return b.quality - a.quality; + } + + // 3. Prioritize better providers + if (a.providerPriority !== b.providerPriority) { + return b.providerPriority - a.providerPriority; + } + + return 0; + }); + + logger.log(`🎯 Best stream selected: ${allStreams[0].stream.name || allStreams[0].stream.title} (Quality: ${allStreams[0].quality}p, Provider Priority: ${allStreams[0].providerPriority}, Cached: ${allStreams[0].isCached})`); + + return allStreams[0].stream; + }, []); + const currentEpisode = useMemo(() => { if (!selectedEpisode) return null; @@ -710,6 +820,48 @@ export const StreamsScreen = () => { } }, [settings.preferredPlayer, settings.useExternalPlayer, navigateToPlayer]); + // Autoplay effect - triggers when streams are available and autoplay is enabled + useEffect(() => { + if ( + settings.autoplayBestStream && + !autoplayTriggered && + !loadingStreams && + !loadingEpisodeStreams && + isAutoplayWaiting + ) { + const streams = type === 'series' ? episodeStreams : groupedStreams; + + if (Object.keys(streams).length > 0) { + const bestStream = getBestStream(streams); + + if (bestStream) { + logger.log('🚀 Autoplay: Best stream found, starting playback...'); + setAutoplayTriggered(true); + setIsAutoplayWaiting(false); + + // Add a small delay to let the UI settle + setTimeout(() => { + handleStreamPress(bestStream); + }, 500); + } else { + logger.log('⚠️ Autoplay: No suitable stream found'); + setIsAutoplayWaiting(false); + } + } + } + }, [ + settings.autoplayBestStream, + autoplayTriggered, + loadingStreams, + loadingEpisodeStreams, + isAutoplayWaiting, + type, + episodeStreams, + groupedStreams, + getBestStream, + handleStreamPress + ]); + const filterItems = useMemo(() => { const installedAddons = stremioService.getInstalledAddons(); const streams = type === 'series' ? episodeStreams : groupedStreams; @@ -1140,7 +1292,17 @@ export const StreamsScreen = () => { style={styles.loadingContainer} > - Finding available streams... + + {isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'} + + + ) : isAutoplayWaiting && !autoplayTriggered ? ( + + + Starting best stream... ) : Object.keys(streams).length === 0 && !loadingStreams && !loadingEpisodeStreams ? (