mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-03 16:59:08 +00:00
featured content fix
This commit is contained in:
parent
22d8fe311a
commit
d457db5053
4 changed files with 235 additions and 203 deletions
2
app.json
2
app.json
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue