seriescontent optimziation

This commit is contained in:
tapframe 2025-09-13 12:18:19 +05:30
parent 4667357a25
commit a5d2756854
5 changed files with 208 additions and 59 deletions

BIN
.App.tsx.swp Normal file

Binary file not shown.

View file

@ -274,6 +274,19 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
}, [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) {

View file

@ -136,10 +136,86 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
const [activeFetchingScrapers, setActiveFetchingScrapers] = useState<string[]>([]);
// 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<NodeJS.Timeout | null>(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,

View file

@ -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]);

View file

@ -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,
};