mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
improved catalogfetching logic
This commit is contained in:
parent
4ac45a041a
commit
80d75a528f
2 changed files with 234 additions and 213 deletions
|
|
@ -83,215 +83,225 @@ export function useFeaturedContent() {
|
||||||
const signal = abortControllerRef.current.signal;
|
const signal = abortControllerRef.current.signal;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let formattedContent: StreamingContent[] = [];
|
// Load list of catalogs to fetch
|
||||||
|
const configs = await catalogService.resolveHomeCatalogsToFetch(selectedCatalogs);
|
||||||
|
|
||||||
{
|
if (signal.aborted) return;
|
||||||
// Load from installed catalogs with optimization
|
|
||||||
const tCats = Date.now();
|
|
||||||
// Pass selected catalogs to service for optimized fetching
|
|
||||||
const catalogs = await catalogService.getHomeCatalogs(selectedCatalogs);
|
|
||||||
|
|
||||||
if (signal.aborted) return;
|
// Prepare for incremental loading
|
||||||
|
const seenIds = new Set<string>();
|
||||||
|
let accumulatedContent: StreamingContent[] = [];
|
||||||
|
const TARGET_COUNT = 10;
|
||||||
|
let hasSetInitialContent = false;
|
||||||
|
|
||||||
// If no catalogs are installed, stop loading and return.
|
// Helper function to enrich items
|
||||||
if (catalogs.length === 0) {
|
const enrichItems = async (items: any[]): Promise<StreamingContent[]> => {
|
||||||
formattedContent = [];
|
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
||||||
} else {
|
|
||||||
// Use catalogs directly (filtering is now done in service)
|
|
||||||
const filteredCatalogs = catalogs;
|
|
||||||
|
|
||||||
// Flatten all catalog items into a single array, filter out items without posters
|
const enrichLogo = async (item: any): Promise<StreamingContent> => {
|
||||||
const tFlat = Date.now();
|
const base: StreamingContent = {
|
||||||
const allItems = filteredCatalogs.flatMap(catalog => catalog.items)
|
id: item.id,
|
||||||
.filter(item => item.poster)
|
type: item.type,
|
||||||
.filter((item, index, self) =>
|
name: item.name,
|
||||||
// Remove duplicates based on ID
|
poster: item.poster,
|
||||||
index === self.findIndex(t => t.id === item.id)
|
banner: (item as any).banner,
|
||||||
);
|
logo: (item as any).logo,
|
||||||
|
description: (item as any).description,
|
||||||
// Sort by popular, newest, etc. (possibly enhanced later) and take first 10
|
year: (item as any).year,
|
||||||
const topItems = allItems.sort(() => Math.random() - 0.5).slice(0, 10);
|
genres: (item as any).genres,
|
||||||
|
inLibrary: Boolean((item as any).inLibrary),
|
||||||
// Optionally enrich with logos (TMDB only) for tmdb/imdb sourced IDs
|
|
||||||
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
|
||||||
|
|
||||||
const enrichLogo = async (item: any): Promise<StreamingContent> => {
|
|
||||||
const base: StreamingContent = {
|
|
||||||
id: item.id,
|
|
||||||
type: item.type,
|
|
||||||
name: item.name,
|
|
||||||
poster: item.poster,
|
|
||||||
banner: (item as any).banner,
|
|
||||||
logo: (item as any).logo,
|
|
||||||
description: (item as any).description,
|
|
||||||
year: (item as any).year,
|
|
||||||
genres: (item as any).genres,
|
|
||||||
inLibrary: Boolean((item as any).inLibrary),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!settings.enrichMetadataWithTMDB) {
|
|
||||||
// When enrichment is OFF, keep addon logo or undefined
|
|
||||||
return { ...base, logo: base.logo || undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
// When enrichment is ON, fetch from TMDB with language preference
|
|
||||||
const rawId = String(item.id);
|
|
||||||
const isTmdb = rawId.startsWith('tmdb:');
|
|
||||||
const isImdb = rawId.startsWith('tt');
|
|
||||||
let tmdbId: string | null = null;
|
|
||||||
let imdbId: string | null = null;
|
|
||||||
|
|
||||||
if (isTmdb) tmdbId = rawId.split(':')[1];
|
|
||||||
if (isImdb) imdbId = rawId.split(':')[0];
|
|
||||||
if (!tmdbId && imdbId) {
|
|
||||||
const found = await tmdbService.findTMDBIdByIMDB(imdbId);
|
|
||||||
tmdbId = found ? String(found) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tmdbId) {
|
|
||||||
const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage);
|
|
||||||
return { ...base, logo: logoUrl || undefined }; // TMDB logo or undefined (no addon fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...base, logo: undefined }; // No TMDB ID means no logo
|
|
||||||
} catch (error) {
|
|
||||||
return { ...base, logo: undefined }; // Error means no logo
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only enrich with logos if enrichment is enabled
|
try {
|
||||||
if (settings.enrichMetadataWithTMDB) {
|
if (!settings.enrichMetadataWithTMDB) {
|
||||||
formattedContent = await Promise.all(topItems.map(enrichLogo));
|
return { ...base, logo: base.logo || undefined };
|
||||||
try {
|
}
|
||||||
const details = formattedContent.slice(0, 20).map((c) => ({
|
|
||||||
id: c.id,
|
|
||||||
name: c.name,
|
|
||||||
hasLogo: Boolean(c.logo),
|
|
||||||
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
|
|
||||||
logo: c.logo || undefined,
|
|
||||||
}));
|
|
||||||
} catch { }
|
|
||||||
} else {
|
|
||||||
// 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 base: StreamingContent = {
|
|
||||||
id: item.id,
|
|
||||||
type: item.type,
|
|
||||||
name: item.name,
|
|
||||||
poster: item.poster,
|
|
||||||
banner: (item as any).banner,
|
|
||||||
logo: (item as any).logo || undefined,
|
|
||||||
description: (item as any).description,
|
|
||||||
year: (item as any).year,
|
|
||||||
genres: (item as any).genres,
|
|
||||||
inLibrary: Boolean((item as any).inLibrary),
|
|
||||||
};
|
|
||||||
return base;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attempt to fill missing logos from addon meta details for a limited subset
|
const rawId = String(item.id);
|
||||||
const candidates = baseItems.filter(i => !i.logo).slice(0, 10);
|
const isTmdb = rawId.startsWith('tmdb:');
|
||||||
|
const isImdb = rawId.startsWith('tt');
|
||||||
|
let tmdbId: string | null = null;
|
||||||
|
let imdbId: string | null = null;
|
||||||
|
|
||||||
|
if (isTmdb) tmdbId = rawId.split(':')[1];
|
||||||
|
if (isImdb) imdbId = rawId.split(':')[0];
|
||||||
|
if (!tmdbId && imdbId) {
|
||||||
|
const found = await tmdbService.findTMDBIdByIMDB(imdbId);
|
||||||
|
tmdbId = found ? String(found) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tmdbId) {
|
||||||
|
const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage);
|
||||||
|
return { ...base, logo: logoUrl || undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...base, logo: undefined };
|
||||||
|
} catch (error) {
|
||||||
|
return { ...base, logo: undefined };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (settings.enrichMetadataWithTMDB) {
|
||||||
|
return Promise.all(items.map(enrichLogo));
|
||||||
|
} else {
|
||||||
|
// Fallback logic for when enrichment is disabled
|
||||||
|
const baseItems = items.map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
type: item.type,
|
||||||
|
name: item.name,
|
||||||
|
poster: item.poster,
|
||||||
|
banner: (item as any).banner,
|
||||||
|
logo: (item as any).logo || undefined,
|
||||||
|
description: (item as any).description,
|
||||||
|
year: (item as any).year,
|
||||||
|
genres: (item as any).genres,
|
||||||
|
inLibrary: Boolean((item as any).inLibrary),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Try to get logos for items missing them
|
||||||
|
const missingLogoCandidates = baseItems.filter((i: any) => !i.logo);
|
||||||
|
if (missingLogoCandidates.length > 0) {
|
||||||
try {
|
try {
|
||||||
const filled = await Promise.allSettled(candidates.map(async (item) => {
|
const filled = await Promise.allSettled(missingLogoCandidates.map(async (item: any) => {
|
||||||
try {
|
try {
|
||||||
const meta = await catalogService.getBasicContentDetails(item.type, item.id);
|
const meta = await catalogService.getBasicContentDetails(item.type, item.id);
|
||||||
if (meta?.logo) {
|
if (meta?.logo) return { id: item.id, logo: meta.logo };
|
||||||
return { id: item.id, logo: meta.logo } as { id: string; logo: string };
|
} catch { }
|
||||||
}
|
return { id: item.id, logo: undefined };
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
return { id: item.id, logo: undefined as any };
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const idToLogo = new Map<string, string>();
|
const idToLogo = new Map();
|
||||||
filled.forEach(res => {
|
filled.forEach((res: any) => {
|
||||||
if (res.status === 'fulfilled' && res.value && res.value.logo) {
|
if (res.status === 'fulfilled' && res.value?.logo) {
|
||||||
idToLogo.set(res.value.id, res.value.logo);
|
idToLogo.set(res.value.id, res.value.logo);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
formattedContent = baseItems.map(i => (
|
return baseItems.map((i: any) => idToLogo.has(i.id) ? { ...i, logo: idToLogo.get(i.id) } : i);
|
||||||
idToLogo.has(i.id) ? { ...i, logo: idToLogo.get(i.id)! } : i
|
|
||||||
));
|
|
||||||
} catch {
|
} catch {
|
||||||
formattedContent = baseItems;
|
return baseItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const details = formattedContent.slice(0, 20).map((c) => ({
|
|
||||||
id: c.id,
|
|
||||||
name: c.name,
|
|
||||||
hasLogo: Boolean(c.logo),
|
|
||||||
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
|
|
||||||
logo: c.logo || undefined,
|
|
||||||
}));
|
|
||||||
} catch { }
|
|
||||||
}
|
}
|
||||||
|
return baseItems;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process each catalog independently
|
||||||
|
const processCatalog = async (config: { addon: any, catalog: any }) => {
|
||||||
|
if (signal.aborted) return;
|
||||||
|
// Optimization: Stop fetching if we have enough items
|
||||||
|
// Note: We check length here but parallel requests might race. This is acceptable.
|
||||||
|
if (accumulatedContent.length >= TARGET_COUNT) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cat = await catalogService.fetchHomeCatalog(config.addon, config.catalog);
|
||||||
|
if (signal.aborted) return;
|
||||||
|
if (!cat || !cat.items || cat.items.length === 0) return;
|
||||||
|
|
||||||
|
// Deduplicate
|
||||||
|
const newItems = cat.items.filter(item => {
|
||||||
|
if (!item.poster) return false;
|
||||||
|
if (seenIds.has(item.id)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newItems.length === 0) return;
|
||||||
|
|
||||||
|
// Take only what we need (or a small batch)
|
||||||
|
const needed = TARGET_COUNT - accumulatedContent.length;
|
||||||
|
// Shuffle this batch locally just to mix it up a bit if the catalog returns strict order
|
||||||
|
const shuffledBatch = newItems.sort(() => Math.random() - 0.5).slice(0, needed);
|
||||||
|
|
||||||
|
if (shuffledBatch.length === 0) return;
|
||||||
|
|
||||||
|
shuffledBatch.forEach(item => seenIds.add(item.id));
|
||||||
|
|
||||||
|
// Enrich this batch
|
||||||
|
const enrichedBatch = await enrichItems(shuffledBatch);
|
||||||
|
if (signal.aborted) return;
|
||||||
|
|
||||||
|
// Update accumulated content
|
||||||
|
accumulatedContent = [...accumulatedContent, ...enrichedBatch];
|
||||||
|
|
||||||
|
// Update State
|
||||||
|
// Always update allFeaturedContent to show progress
|
||||||
|
setAllFeaturedContent([...accumulatedContent]);
|
||||||
|
|
||||||
|
// If this is the first batch, set initial state and UNBLOCK LOADING
|
||||||
|
if (!hasSetInitialContent && accumulatedContent.length > 0) {
|
||||||
|
hasSetInitialContent = true;
|
||||||
|
setFeaturedContent(accumulatedContent[0]);
|
||||||
|
persistentStore.featuredContent = accumulatedContent[0];
|
||||||
|
persistentStore.allFeaturedContent = accumulatedContent;
|
||||||
|
currentIndexRef.current = 0;
|
||||||
|
setLoading(false); // <--- Key improvement: Display content immediately
|
||||||
|
} else {
|
||||||
|
// Just update store for subsequent batches
|
||||||
|
persistentStore.allFeaturedContent = accumulatedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Error processing catalog in parallel', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If no catalogs to fetch, fallback immediately
|
||||||
|
if (configs.length === 0) {
|
||||||
|
// Fallback logic
|
||||||
|
} else {
|
||||||
|
// Run fetches in parallel
|
||||||
|
await Promise.all(configs.map(processCatalog));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signal.aborted) return;
|
if (signal.aborted) return;
|
||||||
|
|
||||||
// Safety guard: if nothing came back within a reasonable time, stop loading
|
// Handle case where we finished all fetches but found NOTHING
|
||||||
if (!formattedContent || formattedContent.length === 0) {
|
if (accumulatedContent.length === 0) {
|
||||||
// Fall back to any cached featured item so UI can render something
|
// Fall back to any cached featured item so UI can render something
|
||||||
const cachedJson = await mmkvStorage.getItem(STORAGE_KEY).catch(() => null);
|
const cachedJson = await mmkvStorage.getItem(STORAGE_KEY).catch(() => null);
|
||||||
if (cachedJson) {
|
if (cachedJson) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(cachedJson);
|
const parsed = JSON.parse(cachedJson);
|
||||||
if (parsed?.featuredContent) {
|
if (parsed?.featuredContent) {
|
||||||
formattedContent = Array.isArray(parsed.allFeaturedContent) && parsed.allFeaturedContent.length > 0
|
const fallback = Array.isArray(parsed.allFeaturedContent) && parsed.allFeaturedContent.length > 0
|
||||||
? parsed.allFeaturedContent
|
? parsed.allFeaturedContent
|
||||||
: [parsed.featuredContent];
|
: [parsed.featuredContent];
|
||||||
|
|
||||||
|
setAllFeaturedContent(fallback);
|
||||||
|
setFeaturedContent(fallback[0]);
|
||||||
|
setLoading(false);
|
||||||
|
return; // Done
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Update persistent store with the new data (no lastFetchTime when cache disabled)
|
// If still nothing
|
||||||
persistentStore.allFeaturedContent = formattedContent;
|
|
||||||
if (!DISABLE_CACHE) {
|
|
||||||
persistentStore.lastFetchTime = now;
|
|
||||||
}
|
|
||||||
persistentStore.isFirstLoad = false;
|
|
||||||
|
|
||||||
setAllFeaturedContent(formattedContent);
|
|
||||||
|
|
||||||
if (formattedContent.length > 0) {
|
|
||||||
persistentStore.featuredContent = formattedContent[0];
|
|
||||||
setFeaturedContent(formattedContent[0]);
|
|
||||||
currentIndexRef.current = 0;
|
|
||||||
// Persist cache for fast startup (skipped when cache disabled)
|
|
||||||
if (!DISABLE_CACHE) {
|
|
||||||
try {
|
|
||||||
await mmkvStorage.setItem(
|
|
||||||
STORAGE_KEY,
|
|
||||||
JSON.stringify({
|
|
||||||
ts: now,
|
|
||||||
featuredContent: formattedContent[0],
|
|
||||||
allFeaturedContent: formattedContent,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
persistentStore.featuredContent = null;
|
|
||||||
setFeaturedContent(null);
|
setFeaturedContent(null);
|
||||||
// Clear persisted cache on empty (skipped when cache disabled)
|
setAllFeaturedContent([]);
|
||||||
if (!DISABLE_CACHE) {
|
setLoading(false); // Ensure we don't hang in loading state
|
||||||
try { await mmkvStorage.removeItem(STORAGE_KEY); } catch { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final persistence
|
||||||
|
persistentStore.allFeaturedContent = accumulatedContent;
|
||||||
|
if (!DISABLE_CACHE && accumulatedContent.length > 0) {
|
||||||
|
persistentStore.lastFetchTime = now;
|
||||||
|
try {
|
||||||
|
await mmkvStorage.setItem(
|
||||||
|
STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
ts: now,
|
||||||
|
featuredContent: accumulatedContent[0],
|
||||||
|
allFeaturedContent: accumulatedContent,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (signal.aborted) {
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
setFeaturedContent(null);
|
|
||||||
setAllFeaturedContent([]);
|
|
||||||
} finally {
|
|
||||||
if (!signal.aborted) {
|
if (!signal.aborted) {
|
||||||
|
// Even on error, ensure we stop loading
|
||||||
|
setFeaturedContent(null);
|
||||||
|
setAllFeaturedContent([]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,7 @@ class CatalogService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHomeCatalogs(limitIds?: string[]): Promise<CatalogContent[]> {
|
async resolveHomeCatalogsToFetch(limitIds?: string[]): Promise<{ addon: StreamingAddon; catalog: any }[]> {
|
||||||
const addons = await this.getAllAddons();
|
const addons = await this.getAllAddons();
|
||||||
|
|
||||||
// Load enabled/disabled settings
|
// Load enabled/disabled settings
|
||||||
|
|
@ -360,59 +360,70 @@ class CatalogService {
|
||||||
catalogsToFetch = potentialCatalogs.sort(() => 0.5 - Math.random()).slice(0, 5);
|
catalogsToFetch = potentialCatalogs.sort(() => 0.5 - Math.random()).slice(0, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create promises for the selected catalogs
|
return catalogsToFetch;
|
||||||
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);
|
async fetchHomeCatalog(addon: StreamingAddon, catalog: any): Promise<CatalogContent | null> {
|
||||||
if (metas && metas.length > 0) {
|
try {
|
||||||
// Cap items per catalog to reduce memory and rendering load
|
// Hoist manifest list retrieval and find once
|
||||||
const limited = metas.slice(0, 12);
|
const addonManifests = await stremioService.getInstalledAddonsAsync();
|
||||||
const items = limited.map(meta => this.convertMetaToStreamingContent(meta));
|
const manifest = addonManifests.find(a => a.id === addon.id);
|
||||||
|
if (!manifest) return null;
|
||||||
|
|
||||||
// Get potentially custom display name; if customized, respect it as-is
|
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
|
||||||
const originalName = catalog.name || catalog.id;
|
if (metas && metas.length > 0) {
|
||||||
let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, originalName);
|
// Cap items per catalog to reduce memory and rendering load
|
||||||
const isCustom = displayName !== originalName;
|
const limited = metas.slice(0, 12);
|
||||||
|
const items = limited.map(meta => this.convertMetaToStreamingContent(meta));
|
||||||
|
|
||||||
if (!isCustom) {
|
// Get potentially custom display name; if customized, respect it as-is
|
||||||
// Remove duplicate words and clean up the name (case-insensitive)
|
const originalName = catalog.name || catalog.id;
|
||||||
const words = displayName.split(' ');
|
let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, originalName);
|
||||||
const uniqueWords: string[] = [];
|
const isCustom = displayName !== originalName;
|
||||||
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
|
if (!isCustom) {
|
||||||
const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows';
|
// Remove duplicate words and clean up the name (case-insensitive)
|
||||||
if (!displayName.toLowerCase().includes(contentType.toLowerCase())) {
|
const words = displayName.split(' ');
|
||||||
displayName = `${displayName} ${contentType}`;
|
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(' ');
|
||||||
|
|
||||||
return {
|
// Add content type if not present
|
||||||
addon: addon.id,
|
const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows';
|
||||||
type: catalog.type,
|
if (!displayName.toLowerCase().includes(contentType.toLowerCase())) {
|
||||||
id: catalog.id,
|
displayName = `${displayName} ${contentType}`;
|
||||||
name: displayName,
|
}
|
||||||
items
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
} catch (error) {
|
return {
|
||||||
logger.error(`Failed to load ${catalog.name} from ${addon.name}:`, error);
|
addon: addon.id,
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHomeCatalogs(limitIds?: string[]): Promise<CatalogContent[]> {
|
||||||
|
// Determine which catalogs to actually fetch
|
||||||
|
const catalogsToFetch = await this.resolveHomeCatalogsToFetch(limitIds);
|
||||||
|
|
||||||
|
// Create promises for the selected catalogs
|
||||||
|
const catalogPromises = catalogsToFetch.map(async ({ addon, catalog }) => {
|
||||||
|
return this.fetchHomeCatalog(addon, catalog);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for all selected catalog fetch promises to resolve in parallel
|
// Wait for all selected catalog fetch promises to resolve in parallel
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue