mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
seriescontent optimziation
This commit is contained in:
parent
4667357a25
commit
a5d2756854
5 changed files with 208 additions and 59 deletions
BIN
.App.tsx.swp
Normal file
BIN
.App.tsx.swp
Normal file
Binary file not shown.
|
|
@ -274,6 +274,19 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
||||||
}, [episodes, metadata?.id])
|
}, [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
|
// Add effect to scroll to selected season
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedSeason && seasonScrollViewRef.current && Object.keys(groupedEpisodes).length > 0) {
|
if (selectedSeason && seasonScrollViewRef.current && Object.keys(groupedEpisodes).length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -136,10 +136,86 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
const [activeFetchingScrapers, setActiveFetchingScrapers] = useState<string[]>([]);
|
const [activeFetchingScrapers, setActiveFetchingScrapers] = useState<string[]>([]);
|
||||||
// Prevent re-initializing season selection repeatedly for the same series
|
// Prevent re-initializing season selection repeatedly for the same series
|
||||||
const initializedSeasonRef = useRef(false);
|
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
|
// Add hook for persistent seasons
|
||||||
const { getSeason, saveSeason } = usePersistentSeasons();
|
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 processStremioSource = async (type: string, id: string, isEpisode = false) => {
|
||||||
const sourceStartTime = Date.now();
|
const sourceStartTime = Date.now();
|
||||||
const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams';
|
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 (__DEV__) logger.log(`✅ [${logPrefix}:${sourceName}] Received ${streams.length} streams from ${addonName} (${addonId}) after ${processTime}ms`);
|
||||||
|
|
||||||
if (streams.length > 0) {
|
if (streams.length > 0) {
|
||||||
// Use the streams directly as they are already processed by stremioService
|
// Memory optimization: Check total stream count and cleanup if needed
|
||||||
const updateState = (prevState: GroupedStreams): GroupedStreams => {
|
const currentTotalStreams = streamCountRef.current;
|
||||||
if (__DEV__) logger.log(`🔄 [${logPrefix}:${sourceName}] Updating state for addon ${addonName} (${addonId})`);
|
if (currentTotalStreams >= maxTotalStreams) {
|
||||||
return {
|
if (__DEV__) logger.log(`🧹 [${logPrefix}:${sourceName}] Memory limit reached (${currentTotalStreams} streams), cleaning up`);
|
||||||
...prevState,
|
cleanupStreams();
|
||||||
[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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
} else {
|
||||||
if (__DEV__) logger.log(`🤷 [${logPrefix}:${sourceName}] No streams found for addon ${addonName} (${addonId})`);
|
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(() => {
|
useEffect(() => {
|
||||||
setLoadAttempts(0);
|
setLoadAttempts(0);
|
||||||
initializedSeasonRef.current = false;
|
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
|
// Auto-retry on error with delay
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1230,6 +1328,21 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, [id]);
|
}, [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 {
|
return {
|
||||||
metadata,
|
metadata,
|
||||||
loading,
|
loading,
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,37 @@ const MetadataScreen: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [metadata]);
|
}, [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
|
// Memoized derived values for performance
|
||||||
const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]);
|
const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1120,67 +1120,59 @@ class StremioService {
|
||||||
const isDirectStreamingUrl = this.isDirectStreamingUrl(streamUrl);
|
const isDirectStreamingUrl = this.isDirectStreamingUrl(streamUrl);
|
||||||
const isMagnetStream = streamUrl?.startsWith('magnet:');
|
const isMagnetStream = streamUrl?.startsWith('magnet:');
|
||||||
|
|
||||||
// Determine the best title: Prioritize description if it seems detailed,
|
// Memory optimization: Limit title length to prevent memory bloat
|
||||||
// otherwise fall back to title or name.
|
|
||||||
let displayTitle = stream.title || stream.name || 'Unnamed Stream';
|
let displayTitle = stream.title || stream.name || 'Unnamed Stream';
|
||||||
if (stream.description && stream.description.includes('\n') && stream.description.length > (stream.title?.length || 0)) {
|
if (stream.description && stream.description.includes('\n') && stream.description.length > (stream.title?.length || 0)) {
|
||||||
// If description exists, contains newlines (likely formatted metadata),
|
// If description exists, contains newlines (likely formatted metadata),
|
||||||
// and is longer than the title, prefer it.
|
// and is longer than the title, prefer it but truncate if too long
|
||||||
displayTitle = stream.description;
|
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
|
// 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
|
// Extract size: Prefer behaviorHints.videoSize, fallback to top-level size
|
||||||
const sizeInBytes = stream.behaviorHints?.videoSize || stream.size || undefined;
|
const sizeInBytes = stream.behaviorHints?.videoSize || stream.size || undefined;
|
||||||
|
|
||||||
// Consolidate behavior hints, prioritizing specific data extraction
|
// Memory optimization: Minimize behaviorHints to essential data only
|
||||||
let behaviorHints: Stream['behaviorHints'] = {
|
const behaviorHints: Stream['behaviorHints'] = {
|
||||||
...(stream.behaviorHints || {}), // Start with existing hints
|
|
||||||
notWebReady: !isDirectStreamingUrl,
|
notWebReady: !isDirectStreamingUrl,
|
||||||
isMagnetStream,
|
cached: stream.behaviorHints?.cached || undefined,
|
||||||
// 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
|
|
||||||
bingeGroup: stream.behaviorHints?.bingeGroup || undefined,
|
bingeGroup: stream.behaviorHints?.bingeGroup || undefined,
|
||||||
// Add size here if extracted
|
// Only include essential torrent data for magnet streams
|
||||||
size: sizeInBytes,
|
...(isMagnetStream ? {
|
||||||
};
|
|
||||||
|
|
||||||
// Specific handling for magnet/torrent streams to extract more details
|
|
||||||
if (isMagnetStream) {
|
|
||||||
behaviorHints = {
|
|
||||||
...behaviorHints,
|
|
||||||
infoHash: stream.infoHash || streamUrl?.match(/btih:([a-zA-Z0-9]+)/)?.[1],
|
infoHash: stream.infoHash || streamUrl?.match(/btih:([a-zA-Z0-9]+)/)?.[1],
|
||||||
fileIdx: stream.fileIdx,
|
fileIdx: stream.fileIdx,
|
||||||
magnetUrl: streamUrl,
|
|
||||||
type: 'torrent',
|
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 = {
|
const processedStream: Stream = {
|
||||||
url: streamUrl,
|
url: streamUrl,
|
||||||
name: name, // Use the original name/title for primary ID
|
name: name,
|
||||||
title: displayTitle, // Use the potentially more detailed title from description
|
title: displayTitle,
|
||||||
addonName: addon.name,
|
addonName: addon.name,
|
||||||
addonId: addon.id,
|
addonId: addon.id,
|
||||||
// Map other potential top-level fields if they exist
|
// Memory optimization: Only include essential fields
|
||||||
description: stream.description || undefined, // Keep original description too
|
description: stream.description && stream.description.length <= 100
|
||||||
|
? stream.description
|
||||||
|
: undefined, // Skip long descriptions
|
||||||
infoHash: stream.infoHash || undefined,
|
infoHash: stream.infoHash || undefined,
|
||||||
fileIdx: stream.fileIdx,
|
fileIdx: stream.fileIdx,
|
||||||
size: sizeInBytes, // Assign the extracted size
|
size: sizeInBytes,
|
||||||
isFree: stream.isFree,
|
isFree: stream.isFree,
|
||||||
isDebrid: !!(stream.behaviorHints?.cached), // Map debrid status more reliably
|
isDebrid: !!(stream.behaviorHints?.cached),
|
||||||
// Assign the consolidated behaviorHints
|
|
||||||
behaviorHints: behaviorHints,
|
behaviorHints: behaviorHints,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue