featured content fix

This commit is contained in:
tapframe 2025-12-09 12:57:14 +05:30
parent 22d8fe311a
commit d457db5053
4 changed files with 235 additions and 203 deletions

View file

@ -49,7 +49,7 @@
"permissions": [ "permissions": [
"INTERNET", "INTERNET",
"WAKE_LOCK", "WAKE_LOCK",
"WRITE_SETTINGS" "android.permission.WRITE_SETTINGS"
], ],
"package": "com.nuvio.app", "package": "com.nuvio.app",
"versionCode": 25, "versionCode": 25,

View file

@ -569,6 +569,33 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
); );
}, [currentIndex, setTrailerPlaying, trailerOpacity, thumbnailOpacity]); }, [currentIndex, setTrailerPlaying, trailerOpacity, thumbnailOpacity]);
// Preload next and previous images for instant swiping
useEffect(() => {
if (items.length <= 1) return;
const prevIdx = (currentIndex - 1 + items.length) % items.length;
const nextIdx = (currentIndex + 1) % items.length;
const prevItem = items[prevIdx];
const nextItem = items[nextIdx];
const urlsToPreload: { uri: string }[] = [];
if (prevItem) {
const url = prevItem.banner || prevItem.poster;
if (url) urlsToPreload.push({ uri: url });
}
if (nextItem) {
const url = nextItem.banner || nextItem.poster;
if (url) urlsToPreload.push({ uri: url });
}
if (urlsToPreload.length > 0) {
FastImage.preload(urlsToPreload);
}
}, [currentIndex, items]);
// Callback for updating interaction time // Callback for updating interaction time
const updateInteractionTime = useCallback(() => { const updateInteractionTime = useCallback(() => {
lastInteractionRef.current = Date.now(); lastInteractionRef.current = Date.now();

View file

@ -65,9 +65,9 @@ export function useFeaturedContent() {
const cacheAge = now - persistentStore.lastFetchTime; const cacheAge = now - persistentStore.lastFetchTime;
if (!DISABLE_CACHE) { if (!DISABLE_CACHE) {
if (!forceRefresh && if (!forceRefresh &&
persistentStore.featuredContent && persistentStore.featuredContent &&
persistentStore.allFeaturedContent.length > 0 && persistentStore.allFeaturedContent.length > 0 &&
cacheAge < CACHE_TIMEOUT) { cacheAge < CACHE_TIMEOUT) {
// Use cached data // Use cached data
setFeaturedContent(persistentStore.featuredContent); setFeaturedContent(persistentStore.featuredContent);
setAllFeaturedContent(persistentStore.allFeaturedContent); setAllFeaturedContent(persistentStore.allFeaturedContent);
@ -86,9 +86,10 @@ export function useFeaturedContent() {
let formattedContent: StreamingContent[] = []; let formattedContent: StreamingContent[] = [];
{ {
// Load from installed catalogs // Load from installed catalogs with optimization
const tCats = Date.now(); const tCats = Date.now();
const catalogs = await catalogService.getHomeCatalogs(); // Pass selected catalogs to service for optimized fetching
const catalogs = await catalogService.getHomeCatalogs(selectedCatalogs);
if (signal.aborted) return; if (signal.aborted) return;
@ -96,13 +97,8 @@ export function useFeaturedContent() {
if (catalogs.length === 0) { if (catalogs.length === 0) {
formattedContent = []; formattedContent = [];
} else { } else {
// Filter catalogs based on user selection if any catalogs are selected // Use catalogs directly (filtering is now done in service)
const filteredCatalogs = selectedCatalogs && selectedCatalogs.length > 0 const filteredCatalogs = catalogs;
? catalogs.filter(catalog => {
const catalogId = `${catalog.addon}:${catalog.type}:${catalog.id}`;
return selectedCatalogs.includes(catalogId);
})
: catalogs; // Use all catalogs if none specifically selected
// Flatten all catalog items into a single array, filter out items without posters // Flatten all catalog items into a single array, filter out items without posters
const tFlat = Date.now(); const tFlat = Date.now();
@ -175,7 +171,7 @@ export function useFeaturedContent() {
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none', logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
logo: c.logo || undefined, logo: c.logo || undefined,
})); }));
} catch {} } catch { }
} else { } else {
// When enrichment is disabled, prefer addon-provided logos; if missing, fetch basic meta to pull logo (like HeroSection) // When enrichment is disabled, prefer addon-provided logos; if missing, fetch basic meta to pull logo (like HeroSection)
const baseItems = topItems.map((item: any) => { const baseItems = topItems.map((item: any) => {
@ -231,7 +227,7 @@ export function useFeaturedContent() {
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none', logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
logo: c.logo || undefined, logo: c.logo || undefined,
})); }));
} catch {} } catch { }
} }
} }
} }
@ -250,7 +246,7 @@ export function useFeaturedContent() {
? parsed.allFeaturedContent ? parsed.allFeaturedContent
: [parsed.featuredContent]; : [parsed.featuredContent];
} }
} catch {} } catch { }
} }
} }
@ -278,14 +274,14 @@ export function useFeaturedContent() {
allFeaturedContent: formattedContent, allFeaturedContent: formattedContent,
}) })
); );
} catch {} } catch { }
} }
} else { } else {
persistentStore.featuredContent = null; persistentStore.featuredContent = null;
setFeaturedContent(null); setFeaturedContent(null);
// Clear persisted cache on empty (skipped when cache disabled) // Clear persisted cache on empty (skipped when cache disabled)
if (!DISABLE_CACHE) { if (!DISABLE_CACHE) {
try { await mmkvStorage.removeItem(STORAGE_KEY); } catch {} try { await mmkvStorage.removeItem(STORAGE_KEY); } catch { }
} }
} }
} catch (error) { } catch (error) {
@ -326,7 +322,7 @@ export function useFeaturedContent() {
setLoading(false); setLoading(false);
} }
} }
} catch {} } catch { }
})(); })();
return () => { cancelled = true; }; return () => { cancelled = true; };
}, []); }, []);
@ -394,13 +390,9 @@ export function useFeaturedContent() {
return unsubscribe; return unsubscribe;
}, [loadFeaturedContent, settings, contentSource, selectedCatalogs]); }, [loadFeaturedContent, settings, contentSource, selectedCatalogs]);
// Load featured content initially and when catalogs selection changes
useEffect(() => { useEffect(() => {
// Always use catalogs // Always use catalogs
setAllFeaturedContent([]); // Don't clear content here to prevent flashing
setFeaturedContent(null);
persistentStore.allFeaturedContent = [];
persistentStore.featuredContent = null;
loadFeaturedContent(true); loadFeaturedContent(true);
}, [loadFeaturedContent, selectedCatalogs]); }, [loadFeaturedContent, selectedCatalogs]);

View file

@ -323,17 +323,16 @@ class CatalogService {
}; };
} }
async getHomeCatalogs(): Promise<CatalogContent[]> { async getHomeCatalogs(limitIds?: string[]): Promise<CatalogContent[]> {
const addons = await this.getAllAddons(); const addons = await this.getAllAddons();
// Load enabled/disabled settings // Load enabled/disabled settings
const catalogSettingsJson = await mmkvStorage.getItem(CATALOG_SETTINGS_KEY); const catalogSettingsJson = await mmkvStorage.getItem(CATALOG_SETTINGS_KEY);
const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {}; const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
// Create an array of promises for all catalog fetches // Collect all potential catalogs first
const catalogPromises: Promise<CatalogContent | null>[] = []; const potentialCatalogs: { addon: StreamingAddon; catalog: any }[] = [];
// Process addons in order (they're already returned in order from getAllAddons)
for (const addon of addons) { for (const addon of addons) {
if (addon.catalogs) { if (addon.catalogs) {
for (const catalog of addon.catalogs) { for (const catalog of addon.catalogs) {
@ -341,68 +340,82 @@ class CatalogService {
const isEnabled = catalogSettings[settingKey] ?? true; const isEnabled = catalogSettings[settingKey] ?? true;
if (isEnabled) { if (isEnabled) {
// Create a promise for each catalog fetch potentialCatalogs.push({ addon, catalog });
const catalogPromise = (async () => {
try {
// Hoist manifest list retrieval and find once
const addonManifests = await stremioService.getInstalledAddonsAsync();
const manifest = addonManifests.find(a => a.id === addon.id);
if (!manifest) return null;
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
if (metas && metas.length > 0) {
// Cap items per catalog to reduce memory and rendering load
const limited = metas.slice(0, 12);
const items = limited.map(meta => this.convertMetaToStreamingContent(meta));
// Get potentially custom display name; if customized, respect it as-is
const originalName = catalog.name || catalog.id;
let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, originalName);
const isCustom = displayName !== originalName;
if (!isCustom) {
// Remove duplicate words and clean up the name (case-insensitive)
const words = displayName.split(' ');
const uniqueWords: string[] = [];
const seenWords = new Set<string>();
for (const word of words) {
const lowerWord = word.toLowerCase();
if (!seenWords.has(lowerWord)) {
uniqueWords.push(word);
seenWords.add(lowerWord);
}
}
displayName = uniqueWords.join(' ');
// Add content type if not present
const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows';
if (!displayName.toLowerCase().includes(contentType.toLowerCase())) {
displayName = `${displayName} ${contentType}`;
}
}
return {
addon: addon.id,
type: catalog.type,
id: catalog.id,
name: displayName,
items
};
}
return null;
} catch (error) {
logger.error(`Failed to load ${catalog.name} from ${addon.name}:`, error);
return null;
}
})();
catalogPromises.push(catalogPromise);
} }
} }
} }
} }
// Wait for all catalog fetch promises to resolve in parallel // Determine which catalogs to actually fetch
let catalogsToFetch: { addon: StreamingAddon; catalog: any }[] = [];
if (limitIds && limitIds.length > 0) {
// User selected specific catalogs - strict filtering
catalogsToFetch = potentialCatalogs.filter(item => {
const catalogId = `${item.addon.id}:${item.catalog.type}:${item.catalog.id}`;
return limitIds.includes(catalogId);
});
} else {
// "All" mode - Smart Sample: Pick 5 random catalogs to avoid waterfall
catalogsToFetch = potentialCatalogs.sort(() => 0.5 - Math.random()).slice(0, 5);
}
// Create promises for the selected catalogs
const catalogPromises = catalogsToFetch.map(async ({ addon, catalog }) => {
try {
// Hoist manifest list retrieval and find once
const addonManifests = await stremioService.getInstalledAddonsAsync();
const manifest = addonManifests.find(a => a.id === addon.id);
if (!manifest) return null;
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
if (metas && metas.length > 0) {
// Cap items per catalog to reduce memory and rendering load
const limited = metas.slice(0, 12);
const items = limited.map(meta => this.convertMetaToStreamingContent(meta));
// Get potentially custom display name; if customized, respect it as-is
const originalName = catalog.name || catalog.id;
let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, originalName);
const isCustom = displayName !== originalName;
if (!isCustom) {
// Remove duplicate words and clean up the name (case-insensitive)
const words = displayName.split(' ');
const uniqueWords: string[] = [];
const seenWords = new Set<string>();
for (const word of words) {
const lowerWord = word.toLowerCase();
if (!seenWords.has(lowerWord)) {
uniqueWords.push(word);
seenWords.add(lowerWord);
}
}
displayName = uniqueWords.join(' ');
// Add content type if not present
const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows';
if (!displayName.toLowerCase().includes(contentType.toLowerCase())) {
displayName = `${displayName} ${contentType}`;
}
}
return {
addon: addon.id,
type: catalog.type,
id: catalog.id,
name: displayName,
items
};
}
return null;
} catch (error) {
logger.error(`Failed to load ${catalog.name} from ${addon.name}:`, error);
return null;
}
});
// Wait for all selected catalog fetch promises to resolve in parallel
const catalogResults = await Promise.all(catalogPromises); const catalogResults = await Promise.all(catalogPromises);
// Filter out null results // Filter out null results
@ -963,7 +976,7 @@ class CatalogService {
await this.saveLibrary(); await this.saveLibrary();
logger.log(`[CatalogService] addToLibrary() completed for: ${content.type}:${content.id}`); logger.log(`[CatalogService] addToLibrary() completed for: ${content.type}:${content.id}`);
this.notifyLibrarySubscribers(); this.notifyLibrarySubscribers();
try { this.libraryAddListeners.forEach(l => l(content)); } catch {} try { this.libraryAddListeners.forEach(l => l(content)); } catch { }
// Auto-setup notifications for series when added to library // Auto-setup notifications for series when added to library
if (content.type === 'series') { if (content.type === 'series') {
@ -989,7 +1002,7 @@ class CatalogService {
await this.saveLibrary(); await this.saveLibrary();
logger.log(`[CatalogService] removeFromLibrary() completed for: ${type}:${id}`); logger.log(`[CatalogService] removeFromLibrary() completed for: ${type}:${id}`);
this.notifyLibrarySubscribers(); this.notifyLibrarySubscribers();
try { this.libraryRemoveListeners.forEach(l => l(type, id)); } catch {} try { this.libraryRemoveListeners.forEach(l => l(type, id)); } catch { }
// Cancel notifications for series when removed from library // Cancel notifications for series when removed from library
if (type === 'series') { if (type === 'series') {
@ -1103,7 +1116,7 @@ class CatalogService {
// Check if 'search' is in extraSupported or extra // Check if 'search' is in extraSupported or extra
return extraSupported.includes('search') || return extraSupported.includes('search') ||
extra.some((e: any) => e.name === 'search'); extra.some((e: any) => e.name === 'search');
}); });
}); });
@ -1115,7 +1128,7 @@ class CatalogService {
const extraSupported = catalog.extraSupported || []; const extraSupported = catalog.extraSupported || [];
const extra = catalog.extra || []; const extra = catalog.extra || [];
return extraSupported.includes('search') || return extraSupported.includes('search') ||
extra.some((e: any) => e.name === 'search'); extra.some((e: any) => e.name === 'search');
}); });
// Search all catalogs for this addon in parallel // Search all catalogs for this addon in parallel