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 ? (