diff --git a/local-scrapers-repo b/local-scrapers-repo index 2310c9bec..aefc318d6 160000 --- a/local-scrapers-repo +++ b/local-scrapers-repo @@ -1 +1 @@ -Subproject commit 2310c9bec1b0abaa48304ba2a64561da15ebceb6 +Subproject commit aefc318d6be92936d4e3512388bffc13d0862eb3 diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 26e6f4d5d..1696b4dd0 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -4,6 +4,7 @@ import { catalogService } from '../services/catalogService'; import { stremioService } from '../services/stremioService'; import { tmdbService } from '../services/tmdbService'; import { cacheService } from '../services/cacheService'; +import { localScraperService, ScraperInfo } from '../services/localScraperService'; import { Cast, Episode, GroupedEpisodes, GroupedStreams } from '../types/metadata'; import { TMDBService } from '../services/tmdbService'; import { logger } from '../utils/logger'; @@ -62,6 +63,16 @@ interface UseMetadataProps { addonId?: string; } +interface ScraperStatus { + id: string; + name: string; + isLoading: boolean; + hasCompleted: boolean; + error: string | null; + startTime: number; + endTime: number | null; +} + interface UseMetadataReturn { metadata: StreamingContent | null; loading: boolean; @@ -92,6 +103,8 @@ interface UseMetadataReturn { loadingRecommendations: boolean; setMetadata: React.Dispatch>; imdbId: string | null; + scraperStatuses: ScraperStatus[]; + activeFetchingScrapers: string[]; } export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadataReturn => { @@ -119,6 +132,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const [imdbId, setImdbId] = useState(null); const [isLoading, setIsLoading] = useState(false); const [availableStreams, setAvailableStreams] = useState<{ [sourceType: string]: Stream }>({}); + const [scraperStatuses, setScraperStatuses] = useState([]); + const [activeFetchingScrapers, setActiveFetchingScrapers] = useState([]); // Add hook for persistent seasons const { getSeason, saveSeason } = usePersistentSeasons(); @@ -134,10 +149,36 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat await stremioService.getStreams(type, id, (streams, addonId, addonName, error) => { const processTime = Date.now() - sourceStartTime; + + // Update scraper status when we get a callback + if (addonId && addonName) { + setScraperStatuses(prevStatuses => { + const existingIndex = prevStatuses.findIndex(s => s.id === addonId); + const newStatus: ScraperStatus = { + id: addonId, + name: addonName, + isLoading: false, + hasCompleted: true, + error: error ? error.message : null, + startTime: sourceStartTime, + endTime: Date.now() + }; + + if (existingIndex >= 0) { + const updated = [...prevStatuses]; + updated[existingIndex] = newStatus; + return updated; + } else { + return [...prevStatuses, newStatus]; + } + }); + + // Remove from active fetching list + setActiveFetchingScrapers(prev => prev.filter(name => name !== addonName)); + } + if (error) { logger.error(`❌ [${logPrefix}:${sourceName}] Error for addon ${addonName} (${addonId}):`, error); - // Optionally update state to show error for this specific addon? - // For now, just log the error. } else if (streams && addonId && addonName) { logger.log(`✅ [${logPrefix}:${sourceName}] Received ${streams.length} streams from ${addonName} (${addonId}) after ${processTime}ms`); @@ -685,6 +726,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat try { console.log('🚀 [loadStreams] START - Loading streams for:', id); updateLoadingState(); + + // Reset scraper tracking + setScraperStatuses([]); + setActiveFetchingScrapers([]); // Get TMDB ID for external sources and determine the correct ID for Stremio addons console.log('🔍 [loadStreams] Getting TMDB ID for:', id); @@ -735,6 +780,80 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat console.log('â„šī¸ [loadStreams] Using ID as both TMDB and Stremio ID:', tmdbId); } + // Initialize scraper tracking + try { + const allStremioAddons = await stremioService.getInstalledAddons(); + const localScrapers = await localScraperService.getInstalledScrapers(); + + // Filter Stremio addons to only include those that provide streams for this content type + const streamAddons = allStremioAddons.filter(addon => { + if (!addon.resources || !Array.isArray(addon.resources)) { + return false; + } + + let hasStreamResource = false; + + for (const resource of addon.resources) { + // Check if the current element is a ResourceObject + if (typeof resource === 'object' && resource !== null && 'name' in resource) { + const typedResource = resource as any; + if (typedResource.name === 'stream' && + Array.isArray(typedResource.types) && + typedResource.types.includes(type)) { + hasStreamResource = true; + break; + } + } + // Check if the element is the simple string "stream" AND the addon has a top-level types array + else if (typeof resource === 'string' && resource === 'stream' && addon.types) { + if (Array.isArray(addon.types) && addon.types.includes(type)) { + hasStreamResource = true; + break; + } + } + } + + return hasStreamResource; + }); + + // Initialize scraper statuses for tracking + const initialStatuses: ScraperStatus[] = []; + const initialActiveFetching: string[] = []; + + // Add stream-capable Stremio addons only + streamAddons.forEach(addon => { + initialStatuses.push({ + id: addon.id, + name: addon.name, + isLoading: true, + hasCompleted: false, + error: null, + startTime: Date.now(), + endTime: null + }); + initialActiveFetching.push(addon.name); + }); + + // Add local scrapers if enabled + localScrapers.filter((scraper: ScraperInfo) => scraper.enabled).forEach((scraper: ScraperInfo) => { + initialStatuses.push({ + id: scraper.id, + name: scraper.name, + isLoading: true, + hasCompleted: false, + error: null, + startTime: Date.now(), + endTime: null + }); + initialActiveFetching.push(scraper.name); + }); + + setScraperStatuses(initialStatuses); + setActiveFetchingScrapers(initialActiveFetching); + } catch (error) { + console.error('Failed to initialize scraper tracking:', error); + } + // Start Stremio request using the converted ID format console.log('đŸŽŦ [loadStreams] Using ID for Stremio addons:', stremioId); processStremioSource(type, stremioId, false); @@ -756,6 +875,84 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat try { console.log('🚀 [loadEpisodeStreams] START - Loading episode streams for:', episodeId); updateEpisodeLoadingState(); + + // Reset scraper tracking for episodes + setScraperStatuses([]); + setActiveFetchingScrapers([]); + + // Initialize scraper tracking for episodes + try { + const allStremioAddons = await stremioService.getInstalledAddons(); + const localScrapers = await localScraperService.getInstalledScrapers(); + + // Filter Stremio addons to only include those that provide streams for series content + const streamAddons = allStremioAddons.filter(addon => { + if (!addon.resources || !Array.isArray(addon.resources)) { + return false; + } + + let hasStreamResource = false; + + for (const resource of addon.resources) { + // Check if the current element is a ResourceObject + if (typeof resource === 'object' && resource !== null && 'name' in resource) { + const typedResource = resource as any; + if (typedResource.name === 'stream' && + Array.isArray(typedResource.types) && + typedResource.types.includes('series')) { + hasStreamResource = true; + break; + } + } + // Check if the element is the simple string "stream" AND the addon has a top-level types array + else if (typeof resource === 'string' && resource === 'stream' && addon.types) { + if (Array.isArray(addon.types) && addon.types.includes('series')) { + hasStreamResource = true; + break; + } + } + } + + return hasStreamResource; + }); + + // Initialize scraper statuses for tracking + const initialStatuses: ScraperStatus[] = []; + const initialActiveFetching: string[] = []; + + // Add stream-capable Stremio addons only + streamAddons.forEach(addon => { + initialStatuses.push({ + id: addon.id, + name: addon.name, + isLoading: true, + hasCompleted: false, + error: null, + startTime: Date.now(), + endTime: null + }); + initialActiveFetching.push(addon.name); + }); + + // Add local scrapers if enabled + localScrapers.filter((scraper: ScraperInfo) => scraper.enabled).forEach((scraper: ScraperInfo) => { + initialStatuses.push({ + id: scraper.id, + name: scraper.name, + isLoading: true, + hasCompleted: false, + error: null, + startTime: Date.now(), + endTime: null + }); + initialActiveFetching.push(scraper.name); + }); + + setScraperStatuses(initialStatuses); + setActiveFetchingScrapers(initialActiveFetching); + } catch (error) { + console.error('Failed to initialize episode scraper tracking:', error); + } // Get TMDB ID for external sources and determine the correct ID for Stremio addons console.log('🔍 [loadEpisodeStreams] Getting TMDB ID for:', id); @@ -987,5 +1184,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat loadingRecommendations, setMetadata, imdbId, + scraperStatuses, + activeFetchingScrapers, }; -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 01bb587e5..8b542f8b6 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -71,16 +71,32 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the const streamInfo = useMemo(() => { const title = stream.title || ''; const name = stream.name || ''; + + // Helper function to format size from bytes + const formatSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + // Get size from title (legacy format) or from stream.size field + let sizeDisplay = title.match(/💾\s*([\d.]+\s*[GM]B)/)?.[1]; + if (!sizeDisplay && stream.size && typeof stream.size === 'number' && stream.size > 0) { + sizeDisplay = formatSize(stream.size); + } + return { quality: title.match(/(\d+)p/)?.[1] || null, isHDR: title.toLowerCase().includes('hdr'), isDolby: title.toLowerCase().includes('dolby') || title.includes('DV'), - size: title.match(/💾\s*([\d.]+\s*[GM]B)/)?.[1], + size: sizeDisplay, isDebrid: stream.behaviorHints?.cached, displayName: name || title || 'Unnamed Stream', subTitle: title && title !== name ? title : null }; - }, [stream.name, stream.title, stream.behaviorHints]); + }, [stream.name, stream.title, stream.behaviorHints, stream.size]); // Animation delay based on index - stagger effect const enterDelay = 100 + (index * 30); @@ -134,7 +150,7 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the {streamInfo.size && ( - {streamInfo.size} + 💾 {streamInfo.size} )} @@ -274,6 +290,8 @@ export const StreamsScreen = () => { setSelectedEpisode, groupedEpisodes, imdbId, + scraperStatuses, + activeFetchingScrapers, } = useMetadata({ id, type }); // Get backdrop from metadata assets @@ -1106,6 +1124,23 @@ export const StreamsScreen = () => { )} + {/* Active Scrapers Status */} + {activeFetchingScrapers.length > 0 && ( + + 🔄 Fetching from: + + {activeFetchingScrapers.map((scraperName, index) => ( + + {scraperName} + + ))} + + + )} + {/* Update the streams/loading state display logic */} { showNoSourcesError ? ( StyleSheet.create({ fontSize: 14, fontWeight: '600', }, + activeScrapersContainer: { + paddingHorizontal: 16, + paddingBottom: 12, + backgroundColor: colors.elevation1, + marginHorizontal: 16, + marginBottom: 8, + borderRadius: 8, + paddingVertical: 12, + }, + activeScrapersTitle: { + color: colors.primary, + fontSize: 13, + fontWeight: '600', + marginBottom: 8, + }, + activeScrapersRow: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 6, + }, + activeScraperChip: { + backgroundColor: colors.surfaceVariant, + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + borderWidth: 1, + borderColor: colors.primary + '40', + }, + activeScraperText: { + color: colors.highEmphasis, + fontSize: 12, + fontWeight: '500', + }, }); export default memo(StreamsScreen); \ No newline at end of file