import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native'; import NetInfo from '@react-native-community/netinfo'; import { MaterialIcons } from '@expo/vector-icons'; import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutRight, } from 'react-native-reanimated'; import { useTranslation } from 'react-i18next'; import { Episode } from '../../../types/metadata'; import { Stream } from '../../../types/streams'; import { stremioService } from '../../../services/stremioService'; import { logger } from '../../../utils/logger'; import { estimateNetworkProfile, getPlaybackViabilityFromStream, rankStreamsByPlaybackViability, } from '../../../screens/streams/utils'; interface EpisodeStreamsModalProps { visible: boolean; episode: Episode | null; onClose: () => void; onSelectStream: (stream: Stream) => void; metadata?: { id?: string; name?: string }; } const QualityBadge = ({ quality }: { quality: string | null | undefined }) => { if (!quality) return null; const qualityNum = parseInt(quality); let color = '#8B5CF6'; let label = `${quality}p`; if (qualityNum >= 2160) { color = '#F59E0B'; label = '4K'; } else if (qualityNum >= 1080) { color = '#3B82F6'; label = '1080p'; } else if (qualityNum >= 720) { color = '#10B981'; label = '720p'; } return ( {label} ); }; export const EpisodeStreamsModal: React.FC = ({ visible, episode, onClose, onSelectStream, metadata, }) => { const { t } = useTranslation(); const { width } = useWindowDimensions(); const MENU_WIDTH = Math.min(width * 0.85, 400); const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: Stream[]; addonName: string } }>({}); const [isLoading, setIsLoading] = useState(false); const [hasErrors, setHasErrors] = useState([]); const [networkMbps, setNetworkMbps] = useState(20); useEffect(() => { if (visible && episode && metadata?.id) { fetchStreams(); } else { setAvailableStreams({}); setIsLoading(false); setHasErrors([]); } }, [visible, episode, metadata?.id]); useEffect(() => { const unsubscribe = NetInfo.addEventListener(state => { setNetworkMbps(estimateNetworkProfile(state as any).estimatedDownlinkMbps); }); NetInfo.fetch() .then(state => setNetworkMbps(estimateNetworkProfile(state as any).estimatedDownlinkMbps)) .catch(() => { // Keep default profile. }); return () => unsubscribe(); }, []); const fetchStreams = async () => { if (!episode || !metadata?.id) return; setIsLoading(true); setHasErrors([]); setAvailableStreams({}); try { const episodeId = episode.stremioId || `${metadata.id}:${episode.season_number}:${episode.episode_number}`; let completedProviders = 0; const expectedProviders = new Set(); const respondedProviders = new Set(); const installedAddons = stremioService.getInstalledAddons(); const streamAddons = installedAddons.filter((addon: any) => addon.resources && addon.resources.includes('stream') ); streamAddons.forEach((addon: any) => expectedProviders.add(addon.id)); logger.log(`[EpisodeStreamsModal] Fetching streams for ${episodeId}, expecting ${expectedProviders.size} providers`); await stremioService.getStreams('series', episodeId, (streams: any, addonId: any, addonName: any, error: any) => { completedProviders++; respondedProviders.add(addonId); if (error) { setHasErrors(prev => [...prev, `${addonName || addonId}: ${error.message || 'Unknown error'}`]); } else if (streams && streams.length > 0) { setAvailableStreams(prev => ({ ...prev, [addonId]: { streams: streams, addonName: addonName || addonId } })); } if (completedProviders >= expectedProviders.size) { setIsLoading(false); } }); setTimeout(() => { if (respondedProviders.size === 0) { setIsLoading(false); } }, 8000); } catch (error) { setIsLoading(false); } }; const getQualityFromTitle = (title?: string): string | null => { if (!title) return null; const match = title.match(/(\d+)p/); return match ? match[1] : null; }; const sortedProviders = useMemo>(() => { return Object.entries(availableStreams).map(([providerId, providerData]) => [ providerId, { ...providerData, streams: rankStreamsByPlaybackViability((providerData.streams as any) || [], networkMbps) as any as Stream[], }, ]); }, [availableStreams, networkMbps]); if (!visible) return null; return ( {/* Backdrop */} {/* Header */} {episode?.name || t('player_ui.sources')} {episode && ( S{episode.season_number} • E{episode.episode_number} )} {isLoading && sortedProviders.length === 0 && ( {t('player_ui.finding_sources')} )} {sortedProviders.map(([providerId, providerData]) => ( {providerData.addonName} {providerData.streams.map((stream, index) => { const quality = getQualityFromTitle(stream.title) || stream.quality; const viability = getPlaybackViabilityFromStream(stream as any); return ( { onSelectStream(stream); onClose(); }} activeOpacity={0.7} > {stream.name || t('player_ui.unknown_source')} {viability?.label ? ( {viability.label.toUpperCase()} ) : null} {stream.title && ( {stream.title} )} ); })} ))} {!isLoading && sortedProviders.length === 0 && ( {t('player_ui.no_sources_found')} )} {hasErrors.length > 0 && ( {t('player_ui.sources_limited')} )} ); }; export default EpisodeStreamsModal;