From a5d2756854b7eeb8d2fc1565b71343889182d1b4 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 13 Sep 2025 12:18:19 +0530 Subject: [PATCH] seriescontent optimziation --- .App.tsx.swp | Bin 0 -> 16384 bytes src/components/metadata/SeriesContent.tsx | 13 ++ src/hooks/useMetadata.ts | 155 +++++++++++++++++++--- src/screens/MetadataScreen.tsx | 31 +++++ src/services/stremioService.ts | 68 +++++----- 5 files changed, 208 insertions(+), 59 deletions(-) create mode 100644 .App.tsx.swp diff --git a/.App.tsx.swp b/.App.tsx.swp new file mode 100644 index 0000000000000000000000000000000000000000..2d308d86590f553fb5455b3b24279bb5a62b7514 GIT binary patch literal 16384 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 = ({ }, [episodes, metadata?.id]) ); + // Memory optimization: Cleanup on unmount + useEffect(() => { + return () => { + // Clear any pending timeouts + if (__DEV__) console.log('[SeriesContent] Component unmounted, cleaning up memory'); + + // Force garbage collection if available (development only) + if (__DEV__ && global.gc) { + global.gc(); + } + }; + }, []); + // Add effect to scroll to selected season useEffect(() => { if (selectedSeason && seasonScrollViewRef.current && Object.keys(groupedEpisodes).length > 0) { diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index e06de2c24..2103a5721 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -136,10 +136,86 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const [activeFetchingScrapers, setActiveFetchingScrapers] = useState([]); // Prevent re-initializing season selection repeatedly for the same series const initializedSeasonRef = useRef(false); + + // Memory optimization: Track stream counts and implement cleanup + const streamCountRef = useRef(0); + const maxStreamsPerAddon = 50; // Limit streams per addon to prevent memory bloat + const maxTotalStreams = 200; // Maximum total streams across all addons + const cleanupTimeoutRef = useRef(null); // Add hook for persistent seasons const { getSeason, saveSeason } = usePersistentSeasons(); + // Memory optimization: Stream cleanup and garbage collection + const cleanupStreams = useCallback(() => { + if (__DEV__) console.log('[useMetadata] Running stream cleanup to free memory'); + + // Clear preloaded streams cache + setPreloadedStreams({}); + setPreloadedEpisodeStreams({}); + + // Reset stream count + streamCountRef.current = 0; + + // Force garbage collection if available (development only) + if (__DEV__ && global.gc) { + global.gc(); + } + }, []); + + // Memory optimization: Debounced stream state updates + const debouncedStreamUpdate = useCallback((updateFn: () => void) => { + // Clear existing timeout + if (cleanupTimeoutRef.current) { + clearTimeout(cleanupTimeoutRef.current); + } + + // Set new timeout for cleanup + cleanupTimeoutRef.current = setTimeout(() => { + cleanupStreams(); + }, 30000); // Cleanup after 30 seconds of inactivity + + // Execute the update + updateFn(); + }, [cleanupStreams]); + + // Memory optimization: Limit and optimize stream data + const optimizeStreams = useCallback((streams: Stream[]): Stream[] => { + if (!streams || streams.length === 0) return streams; + + // Sort streams by quality/priority and limit count + const sortedStreams = streams + .sort((a, b) => { + // Prioritize free streams, then debrid, then by size + if (a.isFree && !b.isFree) return -1; + if (!a.isFree && b.isFree) return 1; + if (a.isDebrid && !b.isDebrid) return -1; + if (!a.isDebrid && b.isDebrid) return 1; + + // Sort by size (larger files often better quality) + const sizeA = a.size || 0; + const sizeB = b.size || 0; + return sizeB - sizeA; + }) + .slice(0, maxStreamsPerAddon); // Limit streams per addon + + // Optimize individual stream objects + return sortedStreams.map(stream => ({ + ...stream, + // Truncate long descriptions to prevent memory bloat + description: stream.description && stream.description.length > 200 + ? stream.description.substring(0, 200) + '...' + : stream.description, + // Simplify behaviorHints to essential data only + behaviorHints: stream.behaviorHints ? { + cached: stream.behaviorHints.cached, + notWebReady: stream.behaviorHints.notWebReady, + bingeGroup: stream.behaviorHints.bingeGroup, + // Remove large objects like magnetUrl, sources, etc. + } : undefined, + })); + }, [maxStreamsPerAddon]); + const processStremioSource = async (type: string, id: string, isEpisode = false) => { const sourceStartTime = Date.now(); const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams'; @@ -185,27 +261,40 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (__DEV__) logger.log(`โœ… [${logPrefix}:${sourceName}] Received ${streams.length} streams from ${addonName} (${addonId}) after ${processTime}ms`); if (streams.length > 0) { - // Use the streams directly as they are already processed by stremioService - const updateState = (prevState: GroupedStreams): GroupedStreams => { - if (__DEV__) logger.log(`๐Ÿ”„ [${logPrefix}:${sourceName}] Updating state for addon ${addonName} (${addonId})`); - return { - ...prevState, - [addonId]: { - addonName: addonName, - streams: streams // Use the received streams directly - } - }; - }; - - if (isEpisode) { - setEpisodeStreams(updateState); - // Turn off loading when we get streams - setLoadingEpisodeStreams(false); - } else { - setGroupedStreams(updateState); - // Turn off loading when we get streams - setLoadingStreams(false); + // Memory optimization: Check total stream count and cleanup if needed + const currentTotalStreams = streamCountRef.current; + if (currentTotalStreams >= maxTotalStreams) { + if (__DEV__) logger.log(`๐Ÿงน [${logPrefix}:${sourceName}] Memory limit reached (${currentTotalStreams} streams), cleaning up`); + cleanupStreams(); } + + // Optimize streams before storing + const optimizedStreams = optimizeStreams(streams); + streamCountRef.current += optimizedStreams.length; + + if (__DEV__) logger.log(`๐Ÿ“Š [${logPrefix}:${sourceName}] Optimized ${streams.length} โ†’ ${optimizedStreams.length} streams, total: ${streamCountRef.current}`); + + // Use debounced update to prevent rapid state changes + debouncedStreamUpdate(() => { + const updateState = (prevState: GroupedStreams): GroupedStreams => { + if (__DEV__) logger.log(`๐Ÿ”„ [${logPrefix}:${sourceName}] Updating state for addon ${addonName} (${addonId})`); + return { + ...prevState, + [addonId]: { + addonName: addonName, + streams: optimizedStreams // Use optimized streams + } + }; + }; + + if (isEpisode) { + setEpisodeStreams(updateState); + setLoadingEpisodeStreams(false); + } else { + setGroupedStreams(updateState); + setLoadingStreams(false); + } + }); } else { if (__DEV__) logger.log(`๐Ÿคท [${logPrefix}:${sourceName}] No streams found for addon ${addonName} (${addonId})`); } @@ -1088,7 +1177,16 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat useEffect(() => { setLoadAttempts(0); initializedSeasonRef.current = false; - }, [id, type]); + + // Memory optimization: Clean up streams when content changes + cleanupStreams(); + + // Clear any pending cleanup timeouts + if (cleanupTimeoutRef.current) { + clearTimeout(cleanupTimeoutRef.current); + cleanupTimeoutRef.current = null; + } + }, [id, type, cleanupStreams]); // Auto-retry on error with delay useEffect(() => { @@ -1230,6 +1328,21 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat return () => unsubscribe(); }, [id]); + // Memory optimization: Cleanup on unmount + useEffect(() => { + return () => { + // Clear cleanup timeout + if (cleanupTimeoutRef.current) { + clearTimeout(cleanupTimeoutRef.current); + } + + // Force cleanup + cleanupStreams(); + + if (__DEV__) console.log('[useMetadata] Component unmounted, memory cleaned up'); + }; + }, [cleanupStreams]); + return { metadata, loading, diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index d4a917ed7..7bce60875 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -322,6 +322,37 @@ const MetadataScreen: React.FC = () => { } }, [metadata]); + // Memory monitoring and cleanup + useEffect(() => { + if (__DEV__) { + const memoryMonitor = () => { + // Check if we have access to memory info + if (performance && (performance as any).memory) { + const memory = (performance as any).memory; + const usedMB = Math.round(memory.usedJSHeapSize / 1048576); + const totalMB = Math.round(memory.totalJSHeapSize / 1048576); + const limitMB = Math.round(memory.jsHeapSizeLimit / 1048576); + + if (__DEV__) console.log(`[MetadataScreen] Memory usage: ${usedMB}MB / ${totalMB}MB (limit: ${limitMB}MB)`); + + // Trigger cleanup if memory usage is high + if (usedMB > limitMB * 0.8) { + if (__DEV__) console.warn(`[MetadataScreen] High memory usage detected (${usedMB}MB), triggering cleanup`); + // Force garbage collection if available + if (global.gc) { + global.gc(); + } + } + } + }; + + // Monitor memory every 10 seconds + const interval = setInterval(memoryMonitor, 10000); + + return () => clearInterval(interval); + } + }, []); + // Memoized derived values for performance const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]); diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index a8b26b9b9..c4ba56c0c 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -1120,67 +1120,59 @@ class StremioService { const isDirectStreamingUrl = this.isDirectStreamingUrl(streamUrl); const isMagnetStream = streamUrl?.startsWith('magnet:'); - // Determine the best title: Prioritize description if it seems detailed, - // otherwise fall back to title or name. + // Memory optimization: Limit title length to prevent memory bloat let displayTitle = stream.title || stream.name || 'Unnamed Stream'; if (stream.description && stream.description.includes('\n') && stream.description.length > (stream.title?.length || 0)) { // If description exists, contains newlines (likely formatted metadata), - // and is longer than the title, prefer it. - displayTitle = stream.description; + // and is longer than the title, prefer it but truncate if too long + displayTitle = stream.description.length > 150 + ? stream.description.substring(0, 150) + '...' + : stream.description; + } + + // Truncate display title if still too long + if (displayTitle.length > 100) { + displayTitle = displayTitle.substring(0, 100) + '...'; } // Use the original name field for the primary identifier if available - const name = stream.name || stream.title || 'Unnamed Stream'; + let name = stream.name || stream.title || 'Unnamed Stream'; + if (name.length > 80) { + name = name.substring(0, 80) + '...'; + } // Extract size: Prefer behaviorHints.videoSize, fallback to top-level size const sizeInBytes = stream.behaviorHints?.videoSize || stream.size || undefined; - // Consolidate behavior hints, prioritizing specific data extraction - let behaviorHints: Stream['behaviorHints'] = { - ...(stream.behaviorHints || {}), // Start with existing hints + // Memory optimization: Minimize behaviorHints to essential data only + const behaviorHints: Stream['behaviorHints'] = { notWebReady: !isDirectStreamingUrl, - isMagnetStream, - // Addon Info - addonName: addon.name, - addonId: addon.id, - // Extracted data (provide defaults or undefined) - cached: stream.behaviorHints?.cached || undefined, // For RD/AD detection - filename: stream.behaviorHints?.filename || undefined, // Filename if available + cached: stream.behaviorHints?.cached || undefined, bingeGroup: stream.behaviorHints?.bingeGroup || undefined, - // Add size here if extracted - size: sizeInBytes, - }; - - // Specific handling for magnet/torrent streams to extract more details - if (isMagnetStream) { - behaviorHints = { - ...behaviorHints, + // Only include essential torrent data for magnet streams + ...(isMagnetStream ? { infoHash: stream.infoHash || streamUrl?.match(/btih:([a-zA-Z0-9]+)/)?.[1], fileIdx: stream.fileIdx, - magnetUrl: streamUrl, type: 'torrent', - sources: stream.sources || [], - seeders: stream.seeders, // Explicitly map seeders if present - size: sizeInBytes || stream.seeders, // Use extracted size, fallback for torrents - title: stream.title, // Torrent title might be different - }; - } + } : {}), + }; - // Explicitly construct the final Stream object + // Explicitly construct the final Stream object with minimal data const processedStream: Stream = { url: streamUrl, - name: name, // Use the original name/title for primary ID - title: displayTitle, // Use the potentially more detailed title from description + name: name, + title: displayTitle, addonName: addon.name, addonId: addon.id, - // Map other potential top-level fields if they exist - description: stream.description || undefined, // Keep original description too + // Memory optimization: Only include essential fields + description: stream.description && stream.description.length <= 100 + ? stream.description + : undefined, // Skip long descriptions infoHash: stream.infoHash || undefined, fileIdx: stream.fileIdx, - size: sizeInBytes, // Assign the extracted size + size: sizeInBytes, isFree: stream.isFree, - isDebrid: !!(stream.behaviorHints?.cached), // Map debrid status more reliably - // Assign the consolidated behaviorHints + isDebrid: !!(stream.behaviorHints?.cached), behaviorHints: behaviorHints, };