calender screen fix
|
|
@ -461,7 +461,7 @@
|
|||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||
PRODUCT_NAME = Nuvio;
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -493,7 +493,7 @@
|
|||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||
PRODUCT_NAME = Nuvio;
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 122 KiB |
|
|
@ -55,7 +55,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
try {
|
||||
// Check memory pressure and cleanup if needed
|
||||
memoryManager.checkMemoryPressure();
|
||||
|
||||
|
||||
if (!forceRefresh) {
|
||||
const cachedData = await robustCalendarCache.getCachedCalendarData(
|
||||
libraryItems,
|
||||
|
|
@ -65,7 +65,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
watched: watchedShows,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
if (cachedData) {
|
||||
setCalendarData(cachedData);
|
||||
setLoading(false);
|
||||
|
|
@ -156,11 +156,11 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
allSeries,
|
||||
async (series: StreamingContent, index: number) => {
|
||||
try {
|
||||
// Use the new memory-efficient method to fetch only upcoming episodes
|
||||
// Use the new memory-efficient method to fetch upcoming and recent episodes
|
||||
const episodeData = await stremioService.getUpcomingEpisodes(series.type, series.id, {
|
||||
daysBack: 14, // 2 weeks back
|
||||
daysAhead: 28, // 4 weeks ahead
|
||||
maxEpisodes: 25, // Limit episodes per series
|
||||
daysBack: 90, // 3 months back for recently released episodes
|
||||
daysAhead: 60, // 2 months ahead for upcoming episodes
|
||||
maxEpisodes: 50, // Increased limit to get more episodes per series
|
||||
});
|
||||
|
||||
if (episodeData && episodeData.episodes.length > 0) {
|
||||
|
|
@ -191,7 +191,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
// Transform episodes with memory-efficient processing
|
||||
const transformedEpisodes = episodeData.episodes.map(video => {
|
||||
const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {};
|
||||
return {
|
||||
const episode = {
|
||||
id: video.id,
|
||||
seriesId: series.id,
|
||||
title: tmdbEpisode.name || video.title || `Episode ${video.episode}`,
|
||||
|
|
@ -205,6 +205,15 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
still_path: tmdbEpisode.still_path || null,
|
||||
season_poster_path: tmdbEpisode.season_poster_path || null
|
||||
};
|
||||
|
||||
// Debug log for episodes
|
||||
if (episode.releaseDate) {
|
||||
logger.log(`[CalendarData] Episode with date: ${episode.seriesName} - ${episode.title} (${episode.releaseDate})`);
|
||||
} else {
|
||||
logger.log(`[CalendarData] Episode without date: ${episode.seriesName} - ${episode.title}`);
|
||||
}
|
||||
|
||||
return episode;
|
||||
});
|
||||
|
||||
// Clear references to help garbage collection
|
||||
|
|
@ -257,10 +266,17 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
|
||||
// Process results and separate episodes from no-episode series
|
||||
for (const result of processedSeries) {
|
||||
if (!result) {
|
||||
logger.error(`[CalendarData] Null/undefined result in processedSeries`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.type === 'episodes' && Array.isArray(result.data)) {
|
||||
allEpisodes.push(...result.data);
|
||||
} else if (result.type === 'no-episodes') {
|
||||
} else if (result.type === 'no-episodes' && result.data) {
|
||||
seriesWithoutEpisodes.push(result.data as CalendarEpisode);
|
||||
} else {
|
||||
logger.warn(`[CalendarData] Unexpected result type or missing data:`, result);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -271,35 +287,111 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
allEpisodes = memoryManager.limitArraySize(allEpisodes, 500);
|
||||
seriesWithoutEpisodes = memoryManager.limitArraySize(seriesWithoutEpisodes, 100);
|
||||
|
||||
// Sort episodes by release date
|
||||
allEpisodes.sort((a, b) => new Date(a.releaseDate).getTime() - new Date(b.releaseDate).getTime());
|
||||
|
||||
// Use memory-efficient filtering
|
||||
// Sort episodes by release date with error handling
|
||||
allEpisodes.sort((a, b) => {
|
||||
try {
|
||||
const dateA = new Date(a.releaseDate).getTime();
|
||||
const dateB = new Date(b.releaseDate).getTime();
|
||||
return dateA - dateB;
|
||||
} catch (error) {
|
||||
logger.warn(`[CalendarData] Error sorting episodes: ${a.releaseDate} vs ${b.releaseDate}`, error);
|
||||
return 0; // Keep original order if sorting fails
|
||||
}
|
||||
});
|
||||
|
||||
logger.log(`[CalendarData] Total episodes fetched: ${allEpisodes.length}`);
|
||||
|
||||
// Use memory-efficient filtering with error handling
|
||||
const thisWeekEpisodes = await memoryManager.filterLargeArray(
|
||||
allEpisodes,
|
||||
ep => isThisWeek(parseISO(ep.releaseDate))
|
||||
allEpisodes,
|
||||
ep => {
|
||||
try {
|
||||
if (!ep.releaseDate) return false;
|
||||
const parsed = parseISO(ep.releaseDate);
|
||||
return isThisWeek(parsed) && isAfter(parsed, new Date());
|
||||
} catch (error) {
|
||||
logger.warn(`[CalendarData] Error parsing date for this week filtering: ${ep.releaseDate}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
const upcomingEpisodes = await memoryManager.filterLargeArray(
|
||||
allEpisodes,
|
||||
ep => isAfter(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate))
|
||||
allEpisodes,
|
||||
ep => {
|
||||
try {
|
||||
if (!ep.releaseDate) return false;
|
||||
const parsed = parseISO(ep.releaseDate);
|
||||
return isAfter(parsed, new Date()) && !isThisWeek(parsed);
|
||||
} catch (error) {
|
||||
logger.warn(`[CalendarData] Error parsing date for upcoming filtering: ${ep.releaseDate}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
const recentEpisodes = await memoryManager.filterLargeArray(
|
||||
allEpisodes,
|
||||
ep => isBefore(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate))
|
||||
allEpisodes,
|
||||
ep => {
|
||||
try {
|
||||
if (!ep.releaseDate) return false;
|
||||
const parsed = parseISO(ep.releaseDate);
|
||||
return isBefore(parsed, new Date());
|
||||
} catch (error) {
|
||||
logger.warn(`[CalendarData] Error parsing date for recent filtering: ${ep.releaseDate}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
logger.log(`[CalendarData] Episode categorization: This Week: ${thisWeekEpisodes.length}, Upcoming: ${upcomingEpisodes.length}, Recently Released: ${recentEpisodes.length}, Series without episodes: ${seriesWithoutEpisodes.length}`);
|
||||
|
||||
// Debug: Show some example episodes from each category
|
||||
if (thisWeekEpisodes && thisWeekEpisodes.length > 0) {
|
||||
logger.log(`[CalendarData] This Week examples:`, thisWeekEpisodes.slice(0, 3).map(ep => ({
|
||||
title: ep.title,
|
||||
date: ep.releaseDate,
|
||||
series: ep.seriesName
|
||||
})));
|
||||
}
|
||||
if (recentEpisodes && recentEpisodes.length > 0) {
|
||||
logger.log(`[CalendarData] Recently Released examples:`, recentEpisodes.slice(0, 3).map(ep => ({
|
||||
title: ep.title,
|
||||
date: ep.releaseDate,
|
||||
series: ep.seriesName
|
||||
})));
|
||||
}
|
||||
|
||||
const sections: CalendarSection[] = [];
|
||||
if (thisWeekEpisodes.length > 0) sections.push({ title: 'This Week', data: thisWeekEpisodes });
|
||||
if (upcomingEpisodes.length > 0) sections.push({ title: 'Upcoming', data: upcomingEpisodes });
|
||||
if (recentEpisodes.length > 0) sections.push({ title: 'Recently Released', data: recentEpisodes });
|
||||
if (seriesWithoutEpisodes.length > 0) sections.push({ title: 'Series with No Scheduled Episodes', data: seriesWithoutEpisodes });
|
||||
|
||||
if (thisWeekEpisodes.length > 0) {
|
||||
sections.push({ title: 'This Week', data: thisWeekEpisodes });
|
||||
logger.log(`[CalendarData] Added 'This Week' section with ${thisWeekEpisodes.length} episodes`);
|
||||
}
|
||||
if (upcomingEpisodes.length > 0) {
|
||||
sections.push({ title: 'Upcoming', data: upcomingEpisodes });
|
||||
logger.log(`[CalendarData] Added 'Upcoming' section with ${upcomingEpisodes.length} episodes`);
|
||||
}
|
||||
if (recentEpisodes.length > 0) {
|
||||
sections.push({ title: 'Recently Released', data: recentEpisodes });
|
||||
logger.log(`[CalendarData] Added 'Recently Released' section with ${recentEpisodes.length} episodes`);
|
||||
}
|
||||
if (seriesWithoutEpisodes.length > 0) {
|
||||
sections.push({ title: 'Series with No Scheduled Episodes', data: seriesWithoutEpisodes });
|
||||
logger.log(`[CalendarData] Added 'Series with No Scheduled Episodes' section with ${seriesWithoutEpisodes.length} episodes`);
|
||||
}
|
||||
|
||||
// Log section details before setting
|
||||
logger.log(`[CalendarData] About to set calendarData with ${sections.length} sections:`);
|
||||
sections.forEach((section, index) => {
|
||||
logger.log(` Section ${index}: "${section.title}" with ${section.data?.length || 0} episodes`);
|
||||
});
|
||||
|
||||
setCalendarData(sections);
|
||||
|
||||
|
||||
// Clear large arrays to help garbage collection
|
||||
memoryManager.clearObjects(allEpisodes, thisWeekEpisodes, upcomingEpisodes, recentEpisodes);
|
||||
// Note: Don't clear the arrays that are referenced in sections (thisWeekEpisodes, upcomingEpisodes, recentEpisodes, seriesWithoutEpisodes)
|
||||
// as they would empty the section data
|
||||
memoryManager.clearObjects(allEpisodes);
|
||||
|
||||
await robustCalendarCache.setCachedCalendarData(
|
||||
sections,
|
||||
|
|
|
|||
|
|
@ -59,25 +59,16 @@ export function useFeaturedContent() {
|
|||
|
||||
const loadFeaturedContent = useCallback(async (forceRefresh = false) => {
|
||||
const t0 = Date.now();
|
||||
logger.info('[useFeaturedContent] load:start', { forceRefresh, contentSource: 'catalogs', selectedCatalogsCount: (selectedCatalogs || []).length });
|
||||
|
||||
// Check if we should use cached data (disabled if DISABLE_CACHE)
|
||||
const now = Date.now();
|
||||
const cacheAge = now - persistentStore.lastFetchTime;
|
||||
logger.debug('[useFeaturedContent] cache:status', {
|
||||
disabled: DISABLE_CACHE,
|
||||
hasFeatured: Boolean(persistentStore.featuredContent),
|
||||
allCount: persistentStore.allFeaturedContent?.length || 0,
|
||||
cacheAgeMs: cacheAge,
|
||||
timeoutMs: CACHE_TIMEOUT,
|
||||
});
|
||||
if (!DISABLE_CACHE) {
|
||||
if (!forceRefresh &&
|
||||
persistentStore.featuredContent &&
|
||||
persistentStore.allFeaturedContent.length > 0 &&
|
||||
cacheAge < CACHE_TIMEOUT) {
|
||||
// Use cached data
|
||||
logger.info('[useFeaturedContent] cache:use', { duration: `${Date.now() - t0}ms` });
|
||||
setFeaturedContent(persistentStore.featuredContent);
|
||||
setAllFeaturedContent(persistentStore.allFeaturedContent);
|
||||
setLoading(false);
|
||||
|
|
@ -86,7 +77,6 @@ export function useFeaturedContent() {
|
|||
}
|
||||
}
|
||||
|
||||
logger.info('[useFeaturedContent] fetch:start', { source: 'catalogs' });
|
||||
setLoading(true);
|
||||
cleanup();
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
|
@ -99,7 +89,6 @@ export function useFeaturedContent() {
|
|||
// Load from installed catalogs
|
||||
const tCats = Date.now();
|
||||
const catalogs = await catalogService.getHomeCatalogs();
|
||||
logger.info('[useFeaturedContent] catalogs:list', { count: catalogs?.length || 0, duration: `${Date.now() - tCats}ms` });
|
||||
|
||||
if (signal.aborted) return;
|
||||
|
||||
|
|
@ -114,7 +103,6 @@ export function useFeaturedContent() {
|
|||
return selectedCatalogs.includes(catalogId);
|
||||
})
|
||||
: catalogs; // Use all catalogs if none specifically selected
|
||||
logger.debug('[useFeaturedContent] catalogs:filtered', { filteredCount: filteredCatalogs.length, selectedCount: selectedCatalogs?.length || 0 });
|
||||
|
||||
// Flatten all catalog items into a single array, filter out items without posters
|
||||
const tFlat = Date.now();
|
||||
|
|
@ -124,7 +112,6 @@ export function useFeaturedContent() {
|
|||
// Remove duplicates based on ID
|
||||
index === self.findIndex(t => t.id === item.id)
|
||||
);
|
||||
logger.info('[useFeaturedContent] catalogs:items', { total: allItems.length, duration: `${Date.now() - tFlat}ms` });
|
||||
|
||||
// Sort by popular, newest, etc. (possibly enhanced later) and take first 10
|
||||
const topItems = allItems.sort(() => Math.random() - 0.5).slice(0, 10);
|
||||
|
|
@ -149,10 +136,8 @@ export function useFeaturedContent() {
|
|||
// If enrichment is disabled, use addon logo if available
|
||||
if (!settings.enrichMetadataWithTMDB) {
|
||||
if (base.logo && !isTmdbUrl(base.logo)) {
|
||||
logger.debug('[useFeaturedContent] enrichment disabled, using addon logo', { name: item.name, logo: base.logo });
|
||||
return base;
|
||||
}
|
||||
logger.debug('[useFeaturedContent] enrichment disabled, no addon logo available', { name: item.name });
|
||||
return { ...base, logo: undefined };
|
||||
}
|
||||
|
||||
|
|
@ -172,16 +157,13 @@ export function useFeaturedContent() {
|
|||
if (!tmdbId && !imdbId) return base;
|
||||
// Try TMDB if we have a TMDB id
|
||||
if (tmdbId) {
|
||||
logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage });
|
||||
const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage);
|
||||
if (logoUrl) {
|
||||
logger.debug('[useFeaturedContent] logo:tmdb:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage });
|
||||
return { ...base, logo: logoUrl };
|
||||
}
|
||||
}
|
||||
return base;
|
||||
} catch (error) {
|
||||
logger.error('[useFeaturedContent] logo:error', { name: item.name, id: item.id, error: String(error) });
|
||||
return base;
|
||||
}
|
||||
};
|
||||
|
|
@ -197,7 +179,6 @@ export function useFeaturedContent() {
|
|||
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
|
||||
logo: c.logo || undefined,
|
||||
}));
|
||||
logger.info('[useFeaturedContent] catalogs:logos:details (enrich=true)', { items: details });
|
||||
} catch {}
|
||||
} else {
|
||||
// When enrichment is disabled, prefer addon-provided logos; if missing, fetch basic meta to pull logo (like HeroSection)
|
||||
|
|
@ -219,18 +200,15 @@ export function useFeaturedContent() {
|
|||
|
||||
// Attempt to fill missing logos from addon meta details for a limited subset
|
||||
const candidates = baseItems.filter(i => !i.logo).slice(0, 10);
|
||||
logger.debug('[useFeaturedContent] catalogs:no-enrich:missing-logos', { count: candidates.length });
|
||||
|
||||
try {
|
||||
const filled = await Promise.allSettled(candidates.map(async (item) => {
|
||||
try {
|
||||
const meta = await catalogService.getBasicContentDetails(item.type, item.id);
|
||||
if (meta?.logo) {
|
||||
logger.debug('[useFeaturedContent] catalogs:no-enrich:filled-logo', { id: item.id, name: item.name, logo: meta.logo });
|
||||
return { id: item.id, logo: meta.logo } as { id: string; logo: string };
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('[useFeaturedContent] catalogs:no-enrich:fill-failed', { id: item.id, error: String(e) });
|
||||
}
|
||||
return { id: item.id, logo: undefined as any };
|
||||
}));
|
||||
|
|
@ -257,7 +235,6 @@ export function useFeaturedContent() {
|
|||
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
|
||||
logo: c.logo || undefined,
|
||||
}));
|
||||
logger.info('[useFeaturedContent] catalogs:logos:details (no-enrich)', { items: details });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
@ -267,7 +244,6 @@ export function useFeaturedContent() {
|
|||
|
||||
// Safety guard: if nothing came back within a reasonable time, stop loading
|
||||
if (!formattedContent || formattedContent.length === 0) {
|
||||
logger.warn('[useFeaturedContent] results:empty');
|
||||
// Fall back to any cached featured item so UI can render something
|
||||
const cachedJson = await AsyncStorage.getItem(STORAGE_KEY).catch(() => null);
|
||||
if (cachedJson) {
|
||||
|
|
@ -277,7 +253,6 @@ export function useFeaturedContent() {
|
|||
formattedContent = Array.isArray(parsed.allFeaturedContent) && parsed.allFeaturedContent.length > 0
|
||||
? parsed.allFeaturedContent
|
||||
: [parsed.featuredContent];
|
||||
logger.info('[useFeaturedContent] fallback:storage', { count: formattedContent.length });
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
|
@ -295,12 +270,6 @@ export function useFeaturedContent() {
|
|||
if (formattedContent.length > 0) {
|
||||
persistentStore.featuredContent = formattedContent[0];
|
||||
setFeaturedContent(formattedContent[0]);
|
||||
logger.info('[useFeaturedContent] setting featuredContent', {
|
||||
id: formattedContent[0].id,
|
||||
name: formattedContent[0].name,
|
||||
hasLogo: Boolean(formattedContent[0].logo),
|
||||
logo: formattedContent[0].logo
|
||||
});
|
||||
currentIndexRef.current = 0;
|
||||
// Persist cache for fast startup (skipped when cache disabled)
|
||||
if (!DISABLE_CACHE) {
|
||||
|
|
@ -313,7 +282,6 @@ export function useFeaturedContent() {
|
|||
allFeaturedContent: formattedContent,
|
||||
})
|
||||
);
|
||||
logger.debug('[useFeaturedContent] cache:written', { firstId: formattedContent[0]?.id });
|
||||
} catch {}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -326,16 +294,13 @@ export function useFeaturedContent() {
|
|||
}
|
||||
} catch (error) {
|
||||
if (signal.aborted) {
|
||||
logger.info('[useFeaturedContent] fetch:aborted');
|
||||
} else {
|
||||
logger.error('[useFeaturedContent] fetch:error', { error: String(error) });
|
||||
}
|
||||
setFeaturedContent(null);
|
||||
setAllFeaturedContent([]);
|
||||
} finally {
|
||||
if (!signal.aborted) {
|
||||
setLoading(false);
|
||||
logger.info('[useFeaturedContent] load:done', { duration: `${Date.now() - t0}ms` });
|
||||
}
|
||||
}
|
||||
}, [cleanup, genreMap, loadingGenres, selectedCatalogs]);
|
||||
|
|
@ -344,7 +309,6 @@ export function useFeaturedContent() {
|
|||
useEffect(() => {
|
||||
if (DISABLE_CACHE) {
|
||||
// Skip hydration entirely
|
||||
logger.debug('[useFeaturedContent] hydrate:skipped');
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
|
|
@ -364,7 +328,6 @@ export function useFeaturedContent() {
|
|||
setFeaturedContent(parsed.featuredContent);
|
||||
setAllFeaturedContent(persistentStore.allFeaturedContent);
|
||||
setLoading(false);
|
||||
logger.info('[useFeaturedContent] hydrate:storage', { allCount: persistentStore.allFeaturedContent.length });
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
|
@ -392,7 +355,6 @@ export function useFeaturedContent() {
|
|||
|
||||
// Force refresh if settings changed during app restart, but only if we have content
|
||||
if (settingsChanged && persistentStore.featuredContent) {
|
||||
logger.info('[useFeaturedContent] settings:changed', { selectedCount: settings.selectedHeroCatalogs?.length || 0 });
|
||||
loadFeaturedContent(true);
|
||||
}
|
||||
}, [settings, loadFeaturedContent]);
|
||||
|
|
@ -410,11 +372,6 @@ export function useFeaturedContent() {
|
|||
const tmdbLangChanged = persistentStore.lastSettings.tmdbLanguagePreference !== nextTmdbLang;
|
||||
|
||||
if (catalogsChanged || logoPrefChanged || tmdbLangChanged) {
|
||||
logger.info('[useFeaturedContent] event:settings-changed:immediate-refresh', {
|
||||
catalogsChanged,
|
||||
logoPrefChanged,
|
||||
tmdbLangChanged
|
||||
});
|
||||
|
||||
// Update internal state immediately so dependent effects are in sync
|
||||
setSelectedCatalogs(nextSelected);
|
||||
|
|
|
|||
|
|
@ -232,6 +232,20 @@ const CalendarScreen = () => {
|
|||
|
||||
// Log when rendering with relevant state info
|
||||
logger.log(`[Calendar] Rendering: loading=${loading}, calendarData sections=${calendarData.length}, allEpisodes=${allEpisodes.length}`);
|
||||
|
||||
// Log section details
|
||||
if (calendarData.length > 0) {
|
||||
calendarData.forEach((section, index) => {
|
||||
logger.log(`[Calendar] Section ${index}: "${section.title}" with ${section.data.length} episodes`);
|
||||
if (section.data && section.data.length > 0) {
|
||||
logger.log(`[Calendar] First episode in "${section.title}": ${section.data[0].seriesName} - ${section.data[0].title} (${section.data[0].releaseDate})`);
|
||||
} else {
|
||||
logger.log(`[Calendar] Section "${section.title}" has empty or undefined data array`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.log(`[Calendar] No calendarData sections available`);
|
||||
}
|
||||
|
||||
// Handle date selection from calendar
|
||||
const handleDateSelect = useCallback((date: Date) => {
|
||||
|
|
|
|||
|
|
@ -714,14 +714,6 @@ class CatalogService {
|
|||
if (!logoUrl || logoUrl.trim() === '' || logoUrl === 'null' || logoUrl === 'undefined') {
|
||||
logoUrl = undefined;
|
||||
}
|
||||
try {
|
||||
logger.debug('[CatalogService] convertMetaToStreamingContent:logo', {
|
||||
id: meta.id,
|
||||
name: meta.name,
|
||||
hasLogo: Boolean(logoUrl),
|
||||
logo: logoUrl || undefined,
|
||||
});
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
id: meta.id,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ interface TraktCollections {
|
|||
}
|
||||
|
||||
const THIS_WEEK_CACHE_KEY = 'this_week_episodes_cache';
|
||||
const CALENDAR_CACHE_KEY = 'calendar_data_cache';
|
||||
const CALENDAR_CACHE_KEY = 'calendar_data_cache_v2';
|
||||
const CACHE_DURATION_MS = 30 * 60 * 1000; // 30 minutes (increased to reduce API calls)
|
||||
const ERROR_CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes for error recovery
|
||||
|
||||
|
|
|
|||
|
|
@ -752,13 +752,11 @@ class StremioService {
|
|||
const urlPathStyle = `${baseUrl}/catalog/${type}/${encodedId}/skip=${pageSkip}.json${queryParams ? `?${queryParams}` : ''}`;
|
||||
// Add filters to path style (append with & or ? based on presence of queryParams)
|
||||
const urlPathWithFilters = urlPathStyle + (urlPathStyle.includes('?') ? filterQuery : (filterQuery ? `?${filterQuery.slice(1)}` : ''));
|
||||
try { logger.log('[StremioService] getCatalog URL (path-style)', { url: urlPathWithFilters, page, pageSize: this.DEFAULT_PAGE_SIZE, type, id, filters, manifest: manifest.id }); } catch {}
|
||||
|
||||
// Candidate 2: Query-style skip URL: /catalog/{type}/{id}.json?skip={N}&limit={PAGE_SIZE}
|
||||
let urlQueryStyle = `${baseUrl}/catalog/${type}/${encodedId}.json?skip=${pageSkip}&limit=${this.DEFAULT_PAGE_SIZE}`;
|
||||
if (queryParams) urlQueryStyle += `&${queryParams}`;
|
||||
urlQueryStyle += filterQuery;
|
||||
try { logger.log('[StremioService] getCatalog URL (query-style)', { url: urlQueryStyle, page, pageSize: this.DEFAULT_PAGE_SIZE, type, id, filters, manifest: manifest.id }); } catch {}
|
||||
|
||||
// Try path-style first, then fallback to query-style
|
||||
let response;
|
||||
|
|
@ -777,7 +775,6 @@ class StremioService {
|
|||
try {
|
||||
const key = `${manifest.id}|${type}|${id}`;
|
||||
if (typeof hasMore === 'boolean') this.catalogHasMore.set(key, hasMore);
|
||||
logger.log('[StremioService] getCatalog response meta', { hasMore, count: Array.isArray(response.data.metas) ? response.data.metas.length : 0 });
|
||||
} catch {}
|
||||
if (response.data.metas && Array.isArray(response.data.metas)) {
|
||||
return response.data.metas;
|
||||
|
|
@ -796,23 +793,18 @@ class StremioService {
|
|||
}
|
||||
|
||||
async getMetaDetails(type: string, id: string, preferredAddonId?: string): Promise<MetaDetails | null> {
|
||||
console.log(`🔍 [StremioService] getMetaDetails called:`, { type, id, preferredAddonId });
|
||||
try {
|
||||
// Validate content ID first
|
||||
const isValidId = await this.isValidContentId(type, id);
|
||||
console.log(`🔍 [StremioService] Content ID validation:`, { type, id, isValidId });
|
||||
|
||||
if (!isValidId) {
|
||||
console.log(`🔍 [StremioService] Invalid content ID, returning null`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const addons = this.getInstalledAddons();
|
||||
console.log(`🔍 [StremioService] Found ${addons.length} installed addons`);
|
||||
|
||||
// If a preferred addon is specified, try it first
|
||||
if (preferredAddonId) {
|
||||
console.log(`🔍 [StremioService] Preferred addon specified:`, { preferredAddonId });
|
||||
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
||||
|
||||
if (preferredAddon && preferredAddon.resources) {
|
||||
|
|
@ -857,49 +849,26 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
|
||||
console.log(`🔍 [StremioService] Preferred addon support check:`, {
|
||||
hasMetaSupport,
|
||||
supportsIdPrefix,
|
||||
addonId: preferredAddon.id,
|
||||
addonName: preferredAddon.name,
|
||||
hasDeclaredPrefixes: preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0
|
||||
});
|
||||
|
||||
// Only require ID prefix compatibility if the addon has declared specific prefixes
|
||||
const requiresIdPrefix = preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0;
|
||||
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
|
||||
|
||||
if (isSupported) {
|
||||
console.log(`🔍 [StremioService] Requesting metadata from preferred addon:`, { url });
|
||||
try {
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
||||
console.log(`🔍 [StremioService] Preferred addon response:`, {
|
||||
hasData: !!response.data,
|
||||
hasMeta: !!response.data?.meta,
|
||||
metaId: response.data?.meta?.id,
|
||||
metaName: response.data?.meta?.name
|
||||
});
|
||||
|
||||
if (response.data && response.data.meta) {
|
||||
console.log(`🔍 [StremioService] Successfully got metadata from preferred addon`);
|
||||
return response.data.meta;
|
||||
} else {
|
||||
console.log(`🔍 [StremioService] Preferred addon returned no metadata`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(`🔍 [StremioService] Preferred addon request failed:`, {
|
||||
errorMessage: error.message,
|
||||
isAxiosError: error.isAxiosError,
|
||||
responseStatus: error.response?.status,
|
||||
responseData: error.response?.data
|
||||
});
|
||||
// Continue trying other addons
|
||||
}
|
||||
} else {
|
||||
console.log(`🔍 [StremioService] Preferred addon doesn't support this content type${requiresIdPrefix ? ' or ID prefix' : ''}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -910,40 +879,23 @@ class StremioService {
|
|||
'http://v3-cinemeta.strem.io'
|
||||
];
|
||||
|
||||
console.log(`🔍 [StremioService] Trying Cinemeta URLs:`, { cinemetaUrls });
|
||||
|
||||
for (const baseUrl of cinemetaUrls) {
|
||||
try {
|
||||
const encodedId = encodeURIComponent(id);
|
||||
const url = `${baseUrl}/meta/${type}/${encodedId}.json`;
|
||||
|
||||
console.log(`🔍 [StremioService] Requesting from Cinemeta:`, { url });
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
||||
console.log(`🔍 [StremioService] Cinemeta response:`, {
|
||||
hasData: !!response.data,
|
||||
hasMeta: !!response.data?.meta,
|
||||
metaId: response.data?.meta?.id,
|
||||
metaName: response.data?.meta?.name
|
||||
});
|
||||
|
||||
if (response.data && response.data.meta) {
|
||||
console.log(`🔍 [StremioService] Successfully got metadata from Cinemeta`);
|
||||
return response.data.meta;
|
||||
} else {
|
||||
console.log(`🔍 [StremioService] Cinemeta returned no metadata`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(`🔍 [StremioService] Cinemeta request failed:`, {
|
||||
baseUrl,
|
||||
errorMessage: error.message,
|
||||
isAxiosError: error.isAxiosError,
|
||||
responseStatus: error.response?.status,
|
||||
responseData: error.response?.data
|
||||
});
|
||||
continue; // Try next URL
|
||||
}
|
||||
}
|
||||
|
|
@ -989,20 +941,12 @@ class StremioService {
|
|||
}
|
||||
|
||||
// Require meta support, but allow any ID if addon doesn't declare specific prefixes
|
||||
console.log(`🔍 [StremioService] Addon support check:`, {
|
||||
addonId: addon.id,
|
||||
addonName: addon.name,
|
||||
hasMetaSupport,
|
||||
supportsIdPrefix,
|
||||
hasDeclaredPrefixes: addon.idPrefixes && addon.idPrefixes.length > 0
|
||||
});
|
||||
|
||||
// Only require ID prefix compatibility if the addon has declared specific prefixes
|
||||
const requiresIdPrefix = addon.idPrefixes && addon.idPrefixes.length > 0;
|
||||
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
|
||||
|
||||
if (!isSupported) {
|
||||
console.log(`🔍 [StremioService] Addon doesn't support this content type${requiresIdPrefix ? ' or ID prefix' : ''}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1011,52 +955,23 @@ class StremioService {
|
|||
const encodedId = encodeURIComponent(id);
|
||||
const url = queryParams ? `${baseUrl}/meta/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/meta/${type}/${encodedId}.json`;
|
||||
|
||||
console.log(`🔍 [StremioService] Requesting from addon:`, {
|
||||
addonId: addon.id,
|
||||
addonName: addon.name,
|
||||
url
|
||||
});
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
||||
console.log(`🔍 [StremioService] Addon response:`, {
|
||||
addonId: addon.id,
|
||||
hasData: !!response.data,
|
||||
hasMeta: !!response.data?.meta,
|
||||
metaId: response.data?.meta?.id,
|
||||
metaName: response.data?.meta?.name
|
||||
});
|
||||
|
||||
if (response.data && response.data.meta) {
|
||||
console.log(`🔍 [StremioService] Successfully got metadata from addon:`, { addonId: addon.id });
|
||||
return response.data.meta;
|
||||
} else {
|
||||
console.log(`🔍 [StremioService] Addon returned no metadata:`, { addonId: addon.id });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(`🔍 [StremioService] Addon request failed:`, {
|
||||
addonId: addon.id,
|
||||
addonName: addon.name,
|
||||
errorMessage: error.message,
|
||||
isAxiosError: error.isAxiosError,
|
||||
responseStatus: error.response?.status,
|
||||
responseData: error.response?.data
|
||||
});
|
||||
continue; // Try next addon
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔍 [StremioService] No metadata found from any addon`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log(`🔍 [StremioService] getMetaDetails caught error:`, {
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
isAxiosError: (error as any)?.isAxiosError,
|
||||
responseStatus: (error as any)?.response?.status,
|
||||
responseData: (error as any)?.response?.data
|
||||
});
|
||||
logger.error('Error in getMetaDetails:', error);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1100,15 +1015,24 @@ class StremioService {
|
|||
|
||||
// Filter episodes to only include those within our date range
|
||||
// This is done immediately after fetching to reduce memory footprint
|
||||
logger.log(`[StremioService] Filtering ${metadata.videos.length} episodes for ${id}, date range: ${startDate.toISOString()} to ${endDate.toISOString()}`);
|
||||
|
||||
const filteredEpisodes = metadata.videos
|
||||
.filter(video => {
|
||||
if (!video.released) return false;
|
||||
if (!video.released) {
|
||||
logger.log(`[StremioService] Episode ${video.id} has no release date`);
|
||||
return false;
|
||||
}
|
||||
const releaseDate = new Date(video.released);
|
||||
return releaseDate >= startDate && releaseDate <= endDate;
|
||||
const inRange = releaseDate >= startDate && releaseDate <= endDate;
|
||||
logger.log(`[StremioService] Episode ${video.id}: released=${video.released}, inRange=${inRange}`);
|
||||
return inRange;
|
||||
})
|
||||
.sort((a, b) => new Date(a.released).getTime() - new Date(b.released).getTime())
|
||||
.slice(0, maxEpisodes); // Limit number of episodes to prevent memory overflow
|
||||
|
||||
logger.log(`[StremioService] After filtering: ${filteredEpisodes.length} episodes remain`);
|
||||
|
||||
return {
|
||||
seriesName: metadata.name,
|
||||
poster: metadata.poster || '',
|
||||
|
|
|
|||