diff --git a/src/components/home/ThisWeekSection.tsx b/src/components/home/ThisWeekSection.tsx index 42a45d60..892ffff6 100644 --- a/src/components/home/ThisWeekSection.tsx +++ b/src/components/home/ThisWeekSection.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { View, Text, @@ -21,6 +21,7 @@ import { useLibrary } from '../../hooks/useLibrary'; import { RootStackParamList } from '../../navigation/AppNavigator'; import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns'; import Animated, { FadeIn, FadeInRight } from 'react-native-reanimated'; +import { useCalendarData } from '../../hooks/useCalendarData'; const { width } = Dimensions.get('window'); const ITEM_WIDTH = width * 0.75; // Reduced width for better spacing @@ -53,148 +54,18 @@ export const ThisWeekSection = React.memo(() => { continueWatching, loadAllCollections } = useTraktContext(); - const [episodes, setEpisodes] = useState([]); - const [loading, setLoading] = useState(true); const { currentTheme } = useTheme(); + const { calendarData, loading } = useCalendarData(); - const fetchThisWeekEpisodes = useCallback(async () => { - setLoading(true); - - try { - // Combine library series with Trakt series - const librarySeries = libraryItems.filter(item => item.type === 'series'); - let allSeries = [...librarySeries]; - - // Add Trakt watchlist and continue watching shows if authenticated - if (traktAuthenticated) { - const traktSeriesIds = new Set(); - - // Add watchlist shows - if (watchlistShows) { - for (const item of watchlistShows) { - if (item.show && item.show.ids.imdb) { - const imdbId = item.show.ids.imdb; - if (!librarySeries.some(s => s.id === imdbId)) { - traktSeriesIds.add(imdbId); - allSeries.push({ - id: imdbId, - name: item.show.title, - type: 'series', - poster: '', // Will try to fetch from TMDB - year: item.show.year, - traktSource: 'watchlist' - }); - } - } - } - } - - // Add continue watching shows - if (continueWatching) { - for (const item of continueWatching) { - if (item.type === 'episode' && item.show && item.show.ids.imdb) { - const imdbId = item.show.ids.imdb; - if (!librarySeries.some(s => s.id === imdbId) && !traktSeriesIds.has(imdbId)) { - traktSeriesIds.add(imdbId); - allSeries.push({ - id: imdbId, - name: item.show.title, - type: 'series', - poster: '', // Will try to fetch from TMDB - year: item.show.year, - traktSource: 'continue-watching' - }); - } - } - } - } - } - - console.log(`[ThisWeekSection] Checking ${allSeries.length} series (Library: ${librarySeries.length}, Trakt: ${allSeries.length - librarySeries.length})`); - let allEpisodes: ThisWeekEpisode[] = []; - - for (const series of allSeries) { - try { - const metadata = await stremioService.getMetaDetails(series.type, series.id); - - if (metadata?.videos) { - // Get TMDB ID for additional metadata - const tmdbId = await tmdbService.findTMDBIdByIMDB(series.id); - let tmdbEpisodes: { [key: string]: any } = {}; - - if (tmdbId) { - const allTMDBEpisodes = await tmdbService.getAllEpisodes(tmdbId); - // Flatten episodes into a map for easy lookup - Object.values(allTMDBEpisodes).forEach(seasonEpisodes => { - seasonEpisodes.forEach(episode => { - const key = `${episode.season_number}:${episode.episode_number}`; - tmdbEpisodes[key] = episode; - }); - }); - } - - const thisWeekEpisodes = metadata.videos - .filter(video => { - if (!video.released) return false; - const releaseDate = parseISO(video.released); - return isThisWeek(releaseDate); - }) - .map(video => { - const releaseDate = parseISO(video.released); - const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {}; - - return { - id: video.id, - seriesId: series.id, - seriesName: series.name || metadata.name, - title: tmdbEpisode.name || video.title || `Episode ${video.episode}`, - poster: series.poster || metadata.poster || '', - releaseDate: video.released, - season: video.season || 0, - episode: video.episode || 0, - isReleased: isBefore(releaseDate, new Date()), - overview: tmdbEpisode.overview || '', - vote_average: tmdbEpisode.vote_average || 0, - still_path: tmdbEpisode.still_path || null, - season_poster_path: tmdbEpisode.season_poster_path || null - }; - }); - - allEpisodes = [...allEpisodes, ...thisWeekEpisodes]; - } - } catch (error) { - console.error(`Error fetching episodes for ${series.name}:`, error); - } - } - - // Sort episodes by release date - allEpisodes.sort((a, b) => { - return new Date(a.releaseDate).getTime() - new Date(b.releaseDate).getTime(); - }); - - setEpisodes(allEpisodes); - } catch (error) { - console.error('Error fetching this week episodes:', error); - } finally { - setLoading(false); - } - }, [libraryItems, traktAuthenticated, watchlistShows, continueWatching]); - - // Load episodes when library items or Trakt data changes - useEffect(() => { - if (!libraryLoading && !traktLoading) { - if (traktAuthenticated && (!watchlistShows || !continueWatching)) { - console.log('[ThisWeekSection] Loading Trakt collections for this week data'); - loadAllCollections(); - } else { - console.log('[ThisWeekSection] Data ready, refreshing episodes. Library items:', libraryItems.length); - fetchThisWeekEpisodes(); - } - } else if (!libraryLoading && !traktAuthenticated) { - console.log('[ThisWeekSection] Not authenticated with Trakt, using library only. Items count:', libraryItems.length); - fetchThisWeekEpisodes(); - } - }, [libraryLoading, traktLoading, traktAuthenticated, libraryItems, watchlistShows, continueWatching, fetchThisWeekEpisodes, loadAllCollections]); + const thisWeekEpisodes = useMemo(() => { + const thisWeekSection = calendarData.find(section => section.title === 'This Week'); + if (!thisWeekSection) return []; + + return thisWeekSection.data.map(episode => ({ + ...episode, + isReleased: isBefore(parseISO(episode.releaseDate), new Date()), + })); + }, [calendarData]); const handleEpisodePress = (episode: ThisWeekEpisode) => { // For upcoming episodes, go to the metadata screen @@ -221,18 +92,7 @@ export const ThisWeekSection = React.memo(() => { navigation.navigate('Calendar' as any); }; - if (loading) { - return ( - - - - Loading this week's episodes... - - - ); - } - - if (episodes.length === 0) { + if (thisWeekEpisodes.length === 0) { return null; } @@ -335,7 +195,7 @@ export const ThisWeekSection = React.memo(() => { item.id} renderItem={renderEpisodeItem} horizontal diff --git a/src/hooks/useCalendarData.ts b/src/hooks/useCalendarData.ts new file mode 100644 index 00000000..b13e9fc2 --- /dev/null +++ b/src/hooks/useCalendarData.ts @@ -0,0 +1,261 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useLibrary } from './useLibrary'; +import { useTraktContext } from '../contexts/TraktContext'; +import { robustCalendarCache } from '../services/robustCalendarCache'; +import { stremioService } from '../services/stremioService'; +import { tmdbService } from '../services/tmdbService'; +import { logger } from '../utils/logger'; +import { parseISO, isBefore, isAfter, startOfToday, addWeeks, isThisWeek } from 'date-fns'; +import { StreamingContent } from '../services/catalogService'; + +interface CalendarEpisode { + id: string; + seriesId: string; + title: string; + seriesName: string; + poster: string; + releaseDate: string; + season: number; + episode: number; + overview: string; + vote_average: number; + still_path: string | null; + season_poster_path: string | null; + } + + interface CalendarSection { + title: string; + data: CalendarEpisode[]; + } + +interface UseCalendarDataReturn { + calendarData: CalendarSection[]; + loading: boolean; + refresh: (force?: boolean) => void; +} + +export const useCalendarData = (): UseCalendarDataReturn => { + const [calendarData, setCalendarData] = useState([]); + const [loading, setLoading] = useState(true); + + const { libraryItems, loading: libraryLoading } = useLibrary(); + const { + isAuthenticated: traktAuthenticated, + isLoading: traktLoading, + watchedShows, + watchlistShows, + continueWatching, + loadAllCollections, + } = useTraktContext(); + + const fetchCalendarData = useCallback(async (forceRefresh = false) => { + logger.log("[CalendarData] Starting to fetch calendar data"); + setLoading(true); + + try { + if (!forceRefresh) { + const cachedData = await robustCalendarCache.getCachedCalendarData( + libraryItems, + { + watchlist: watchlistShows, + continueWatching: continueWatching, + watched: watchedShows, + } + ); + + if (cachedData) { + logger.log(`[CalendarData] Using cached data with ${cachedData.length} sections`); + setCalendarData(cachedData); + setLoading(false); + return; + } + } + + logger.log("[CalendarData] Fetching fresh data from APIs"); + + const librarySeries = libraryItems.filter(item => item.type === 'series'); + let allSeries: StreamingContent[] = [...librarySeries]; + + if (traktAuthenticated) { + const traktSeriesIds = new Set(); + + if (watchlistShows) { + for (const item of watchlistShows) { + if (item.show && item.show.ids.imdb) { + const imdbId = item.show.ids.imdb; + if (!librarySeries.some(s => s.id === imdbId)) { + traktSeriesIds.add(imdbId); + allSeries.push({ + id: imdbId, + name: item.show.title, + type: 'series', + poster: '', + year: item.show.year, + traktSource: 'watchlist' + }); + } + } + } + } + + if (continueWatching) { + for (const item of continueWatching) { + if (item.type === 'episode' && item.show && item.show.ids.imdb) { + const imdbId = item.show.ids.imdb; + if (!librarySeries.some(s => s.id === imdbId) && !traktSeriesIds.has(imdbId)) { + traktSeriesIds.add(imdbId); + allSeries.push({ + id: imdbId, + name: item.show.title, + type: 'series', + poster: '', + year: item.show.year, + traktSource: 'continue-watching' + }); + } + } + } + } + + if (watchedShows) { + const recentWatched = watchedShows.slice(0, 20); + for (const item of recentWatched) { + if (item.show && item.show.ids.imdb) { + const imdbId = item.show.ids.imdb; + if (!librarySeries.some(s => s.id === imdbId) && !traktSeriesIds.has(imdbId)) { + traktSeriesIds.add(imdbId); + allSeries.push({ + id: imdbId, + name: item.show.title, + type: 'series', + poster: '', + year: item.show.year, + traktSource: 'watched' + }); + } + } + } + } + } + + logger.log(`[CalendarData] Total series to check: ${allSeries.length} (Library: ${librarySeries.length}, Trakt: ${allSeries.length - librarySeries.length})`); + + let allEpisodes: CalendarEpisode[] = []; + let seriesWithoutEpisodes: CalendarEpisode[] = []; + + for (const series of allSeries) { + try { + const metadata = await stremioService.getMetaDetails(series.type, series.id); + + if (metadata?.videos && metadata.videos.length > 0) { + const today = startOfToday(); + const fourWeeksLater = addWeeks(today, 4); + const twoWeeksAgo = addWeeks(today, -2); + + const tmdbId = await tmdbService.findTMDBIdByIMDB(series.id); + let tmdbEpisodes: { [key: string]: any } = {}; + + if (tmdbId) { + const allTMDBEpisodes = await tmdbService.getAllEpisodes(tmdbId); + Object.values(allTMDBEpisodes).forEach(seasonEpisodes => { + seasonEpisodes.forEach(episode => { + const key = `${episode.season_number}:${episode.episode_number}`; + tmdbEpisodes[key] = episode; + }); + }); + } + + const upcomingEpisodes = metadata.videos + .filter(video => { + if (!video.released) return false; + const releaseDate = parseISO(video.released); + return isBefore(releaseDate, fourWeeksLater) && isAfter(releaseDate, twoWeeksAgo); + }) + .map(video => { + const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {}; + return { + id: video.id, + seriesId: series.id, + title: tmdbEpisode.name || video.title || `Episode ${video.episode}`, + seriesName: series.name || metadata.name, + poster: series.poster || metadata.poster || '', + releaseDate: video.released, + season: video.season || 0, + episode: video.episode || 0, + overview: tmdbEpisode.overview || '', + vote_average: tmdbEpisode.vote_average || 0, + still_path: tmdbEpisode.still_path || null, + season_poster_path: tmdbEpisode.season_poster_path || null + }; + }); + + if (upcomingEpisodes.length > 0) { + allEpisodes = [...allEpisodes, ...upcomingEpisodes]; + } else { + seriesWithoutEpisodes.push({ id: series.id, seriesId: series.id, title: 'No upcoming episodes', seriesName: series.name || (metadata?.name || ''), poster: series.poster || (metadata?.poster || ''), releaseDate: '', season: 0, episode: 0, overview: '', vote_average: 0, still_path: null, season_poster_path: null }); + } + } else { + seriesWithoutEpisodes.push({ id: series.id, seriesId: series.id, title: 'No upcoming episodes', seriesName: series.name || (metadata?.name || ''), poster: series.poster || (metadata?.poster || ''), releaseDate: '', season: 0, episode: 0, overview: '', vote_average: 0, still_path: null, season_poster_path: null }); + } + } catch (error) { + logger.error(`Error fetching episodes for ${series.name}:`, error); + } + } + + allEpisodes.sort((a, b) => new Date(a.releaseDate).getTime() - new Date(b.releaseDate).getTime()); + + const thisWeekEpisodes = allEpisodes.filter(ep => isThisWeek(parseISO(ep.releaseDate))); + const upcomingEpisodes = allEpisodes.filter(ep => isAfter(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate))); + const recentEpisodes = allEpisodes.filter(ep => isBefore(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate))); + + 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 }); + + setCalendarData(sections); + + await robustCalendarCache.setCachedCalendarData( + sections, + libraryItems, + { watchlist: watchlistShows, continueWatching: continueWatching, watched: watchedShows } + ); + + } catch (error) { + logger.error('Error fetching calendar data:', error); + await robustCalendarCache.setCachedCalendarData( + [], + libraryItems, + { watchlist: watchlistShows, continueWatching: continueWatching, watched: watchedShows }, + true + ); + } finally { + setLoading(false); + } + }, [libraryItems, traktAuthenticated, watchlistShows, continueWatching, watchedShows]); + + useEffect(() => { + if (!libraryLoading && !traktLoading) { + if (traktAuthenticated && (!watchlistShows || !continueWatching || !watchedShows)) { + loadAllCollections(); + } else { + fetchCalendarData(); + } + } else if (!libraryLoading && !traktAuthenticated) { + fetchCalendarData(); + } + }, [libraryItems, libraryLoading, traktLoading, traktAuthenticated, watchlistShows, continueWatching, watchedShows, fetchCalendarData, loadAllCollections]); + + const refresh = useCallback((force = false) => { + fetchCalendarData(force); + }, [fetchCalendarData]); + + return { + calendarData, + loading, + refresh, + }; +}; + + \ No newline at end of file diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index c07018b7..3ca02627 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -20,14 +20,14 @@ import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { useTheme } from '../contexts/ThemeContext'; import { RootStackParamList } from '../navigation/AppNavigator'; -import { stremioService } from '../services/stremioService'; import { useLibrary } from '../hooks/useLibrary'; import { useTraktContext } from '../contexts/TraktContext'; import { format, parseISO, isThisWeek, isAfter, startOfToday, addWeeks, isBefore, isSameDay } from 'date-fns'; import Animated, { FadeIn } from 'react-native-reanimated'; -import { CalendarSection } from '../components/calendar/CalendarSection'; +import { CalendarSection as CalendarSectionComponent } from '../components/calendar/CalendarSection'; import { tmdbService } from '../services/tmdbService'; import { logger } from '../utils/logger'; +import { useCalendarData } from '../hooks/useCalendarData'; const { width } = Dimensions.get('window'); const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; @@ -56,6 +56,7 @@ const CalendarScreen = () => { const navigation = useNavigation>(); const { libraryItems, loading: libraryLoading } = useLibrary(); const { currentTheme } = useTheme(); + const { calendarData, loading, refresh } = useCalendarData(); const { isAuthenticated: traktAuthenticated, isLoading: traktLoading, @@ -66,254 +67,15 @@ const CalendarScreen = () => { } = useTraktContext(); logger.log(`[Calendar] Initial load - Library has ${libraryItems?.length || 0} items, loading: ${libraryLoading}`); - const [calendarData, setCalendarData] = useState([]); - const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [selectedDate, setSelectedDate] = useState(null); const [filteredEpisodes, setFilteredEpisodes] = useState([]); - const fetchCalendarData = useCallback(async () => { - logger.log("[Calendar] Starting to fetch calendar data"); - setLoading(true); - - try { - // Combine library series with Trakt series - const librarySeries = libraryItems.filter(item => item.type === 'series'); - let allSeries = [...librarySeries]; - - // Add Trakt watchlist and watched shows if authenticated - if (traktAuthenticated) { - const traktSeriesIds = new Set(); - - // Add watchlist shows - if (watchlistShows) { - for (const item of watchlistShows) { - if (item.show && item.show.ids.imdb) { - const imdbId = item.show.ids.imdb; - if (!librarySeries.some(s => s.id === imdbId)) { - traktSeriesIds.add(imdbId); - allSeries.push({ - id: imdbId, - name: item.show.title, - type: 'series', - poster: '', // Will try to fetch from TMDB - year: item.show.year, - traktSource: 'watchlist' - }); - } - } - } - } - - // Add continue watching shows - if (continueWatching) { - for (const item of continueWatching) { - if (item.type === 'episode' && item.show && item.show.ids.imdb) { - const imdbId = item.show.ids.imdb; - if (!librarySeries.some(s => s.id === imdbId) && !traktSeriesIds.has(imdbId)) { - traktSeriesIds.add(imdbId); - allSeries.push({ - id: imdbId, - name: item.show.title, - type: 'series', - poster: '', // Will try to fetch from TMDB - year: item.show.year, - traktSource: 'continue-watching' - }); - } - } - } - } - - // Add watched shows (only recent ones to avoid too much data) - if (watchedShows) { - const recentWatched = watchedShows.slice(0, 20); // Limit to recent 20 - for (const item of recentWatched) { - if (item.show && item.show.ids.imdb) { - const imdbId = item.show.ids.imdb; - if (!librarySeries.some(s => s.id === imdbId) && !traktSeriesIds.has(imdbId)) { - traktSeriesIds.add(imdbId); - allSeries.push({ - id: imdbId, - name: item.show.title, - type: 'series', - poster: '', // Will try to fetch from TMDB - year: item.show.year, - traktSource: 'watched' - }); - } - } - } - } - } - - logger.log(`[Calendar] Total series to check: ${allSeries.length} (Library: ${librarySeries.length}, Trakt: ${allSeries.length - librarySeries.length})`); - - let allEpisodes: CalendarEpisode[] = []; - let seriesWithoutEpisodes: CalendarEpisode[] = []; - - // For each series, fetch upcoming episodes - for (const series of allSeries) { - try { - logger.log(`[Calendar] Fetching episodes for series: ${series.name} (${series.id})`); - const metadata = await stremioService.getMetaDetails(series.type, series.id); - logger.log(`[Calendar] Metadata fetched:`, metadata ? 'success' : 'null'); - - if (metadata?.videos && metadata.videos.length > 0) { - logger.log(`[Calendar] Series ${series.name} has ${metadata.videos.length} videos`); - // Filter for upcoming episodes or recently released - const today = startOfToday(); - const fourWeeksLater = addWeeks(today, 4); - const twoWeeksAgo = addWeeks(today, -2); - - // Get TMDB ID for additional metadata - const tmdbId = await tmdbService.findTMDBIdByIMDB(series.id); - let tmdbEpisodes: { [key: string]: any } = {}; - - if (tmdbId) { - const allTMDBEpisodes = await tmdbService.getAllEpisodes(tmdbId); - // Flatten episodes into a map for easy lookup - Object.values(allTMDBEpisodes).forEach(seasonEpisodes => { - seasonEpisodes.forEach(episode => { - const key = `${episode.season_number}:${episode.episode_number}`; - tmdbEpisodes[key] = episode; - }); - }); - } - - const upcomingEpisodes = metadata.videos - .filter(video => { - if (!video.released) return false; - const releaseDate = parseISO(video.released); - return isBefore(releaseDate, fourWeeksLater) && isAfter(releaseDate, twoWeeksAgo); - }) - .map(video => { - const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {}; - return { - id: video.id, - seriesId: series.id, - title: tmdbEpisode.name || video.title || `Episode ${video.episode}`, - seriesName: series.name || metadata.name, - poster: series.poster || metadata.poster || '', - releaseDate: video.released, - season: video.season || 0, - episode: video.episode || 0, - overview: tmdbEpisode.overview || '', - vote_average: tmdbEpisode.vote_average || 0, - still_path: tmdbEpisode.still_path || null, - season_poster_path: tmdbEpisode.season_poster_path || null - }; - }); - - if (upcomingEpisodes.length > 0) { - allEpisodes = [...allEpisodes, ...upcomingEpisodes]; - } else { - // Add to series without episode dates - seriesWithoutEpisodes.push({ - id: series.id, - seriesId: series.id, - title: 'No upcoming episodes', - seriesName: series.name || (metadata?.name || ''), - poster: series.poster || (metadata?.poster || ''), - releaseDate: '', - season: 0, - episode: 0, - overview: '', - vote_average: 0, - still_path: null, - season_poster_path: null - }); - } - } else { - // Add to series without episode dates - seriesWithoutEpisodes.push({ - id: series.id, - seriesId: series.id, - title: 'No upcoming episodes', - seriesName: series.name || (metadata?.name || ''), - poster: series.poster || (metadata?.poster || ''), - releaseDate: '', - season: 0, - episode: 0, - overview: '', - vote_average: 0, - still_path: null, - season_poster_path: null - }); - } - } catch (error) { - logger.error(`Error fetching episodes for ${series.name}:`, error); - } - } - - // Sort episodes by release date - allEpisodes.sort((a, b) => { - return new Date(a.releaseDate).getTime() - new Date(b.releaseDate).getTime(); - }); - - // Group episodes into sections - const thisWeekEpisodes = allEpisodes.filter( - episode => isThisWeek(parseISO(episode.releaseDate)) - ); - - const upcomingEpisodes = allEpisodes.filter( - episode => isAfter(parseISO(episode.releaseDate), new Date()) && - !isThisWeek(parseISO(episode.releaseDate)) - ); - - const recentEpisodes = allEpisodes.filter( - episode => isBefore(parseISO(episode.releaseDate), new Date()) && - !isThisWeek(parseISO(episode.releaseDate)) - ); - - logger.log(`[Calendar] Episodes summary: All episodes: ${allEpisodes.length}, This Week: ${thisWeekEpisodes.length}, Upcoming: ${upcomingEpisodes.length}, Recent: ${recentEpisodes.length}, No Schedule: ${seriesWithoutEpisodes.length}`); - - 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 }); - } - - setCalendarData(sections); - } catch (error) { - logger.error('Error fetching calendar data:', error); - } finally { - setLoading(false); - setRefreshing(false); - } - }, [libraryItems, traktAuthenticated, watchlistShows, continueWatching, watchedShows]); - - useEffect(() => { - if (!libraryLoading && !traktLoading) { - if (traktAuthenticated && (!watchlistShows || !continueWatching || !watchedShows)) { - logger.log(`[Calendar] Loading Trakt collections for calendar data`); - loadAllCollections(); - } else { - logger.log(`[Calendar] Data ready, fetching calendar data - Library: ${libraryItems.length} items`); - fetchCalendarData(); - } - } else if (!libraryLoading && !traktAuthenticated) { - logger.log(`[Calendar] Not authenticated with Trakt, using library only (${libraryItems.length} items)`); - fetchCalendarData(); - } - }, [libraryItems, libraryLoading, traktLoading, traktAuthenticated, watchlistShows, continueWatching, watchedShows, fetchCalendarData, loadAllCollections]); - const onRefresh = useCallback(() => { setRefreshing(true); - fetchCalendarData(); - }, [fetchCalendarData]); + refresh(true); + setRefreshing(false); + }, [refresh]); const handleSeriesPress = useCallback((seriesId: string, episode?: CalendarEpisode) => { navigation.navigate('Metadata', { @@ -445,7 +207,7 @@ const CalendarScreen = () => { ); // Process all episodes once data is loaded - const allEpisodes = calendarData.reduce((acc, section) => + const allEpisodes = calendarData.reduce((acc: CalendarEpisode[], section: CalendarSection) => [...acc, ...section.data], [] as CalendarEpisode[]); // Log when rendering with relevant state info @@ -549,7 +311,7 @@ const CalendarScreen = () => { )} - diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index d8ede4b2..8769b208 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -70,6 +70,7 @@ export interface StreamingContent { imdb_id?: string; slug?: string; releaseInfo?: string; + traktSource?: 'watchlist' | 'continue-watching' | 'watched'; } export interface CatalogContent { diff --git a/src/services/robustCalendarCache.ts b/src/services/robustCalendarCache.ts new file mode 100644 index 00000000..b50ebe56 --- /dev/null +++ b/src/services/robustCalendarCache.ts @@ -0,0 +1,102 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { logger } from '../utils/logger'; + +// Define the structure of cached data +interface CachedData { + timestamp: number; + hash: string; + data: T; +} + +// Define the structure for Trakt collections +interface TraktCollections { + watchlist: any[]; + continueWatching: any[]; + watched?: any[]; +} + +const THIS_WEEK_CACHE_KEY = 'this_week_episodes_cache'; +const CALENDAR_CACHE_KEY = 'calendar_data_cache'; +const CACHE_DURATION_MS = 15 * 60 * 1000; // 15 minutes +const ERROR_CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes for error recovery + +class RobustCalendarCache { + private generateHash(libraryItems: any[], traktCollections: TraktCollections): string { + const libraryIds = libraryItems.map(item => item.id).sort().join('|'); + const watchlistIds = (traktCollections.watchlist || []).map(item => item.show?.ids?.imdb || '').filter(Boolean).sort().join('|'); + const continueWatchingIds = (traktCollections.continueWatching || []).map(item => item.show?.ids?.imdb || '').filter(Boolean).sort().join('|'); + const watchedIds = (traktCollections.watched || []).map(item => item.show?.ids?.imdb || '').filter(Boolean).sort().join('|'); + + return `${libraryIds}:${watchlistIds}:${continueWatchingIds}:${watchedIds}`; + } + + private async getCachedData(key: string, libraryItems: any[], traktCollections: TraktCollections): Promise { + try { + const storedCache = await AsyncStorage.getItem(key); + if (!storedCache) return null; + + const cache: CachedData = JSON.parse(storedCache); + const currentHash = this.generateHash(libraryItems, traktCollections); + + if (cache.hash !== currentHash) { + logger.log(`[Cache] Hash mismatch for key ${key}, cache invalidated`); + return null; + } + + const isCacheExpired = Date.now() - cache.timestamp > CACHE_DURATION_MS; + if (isCacheExpired) { + logger.log(`[Cache] Cache expired for key ${key}`); + return null; + } + + logger.log(`[Cache] Valid cache found for key ${key}`); + return cache.data; + } catch (error) { + logger.error(`[Cache] Error getting cached data for key ${key}:`, error); + return null; + } + } + + private async setCachedData(key: string, data: T, libraryItems: any[], traktCollections: TraktCollections, isErrorRecovery = false): Promise { + try { + const hash = this.generateHash(libraryItems, traktCollections); + const cache: CachedData = { + timestamp: Date.now(), + hash, + data, + }; + + if (isErrorRecovery) { + // Use a shorter cache duration for error states + cache.timestamp = Date.now() - CACHE_DURATION_MS + ERROR_CACHE_DURATION_MS; + logger.log(`[Cache] Saving error recovery cache for key ${key}`); + } else { + logger.log(`[Cache] Saving successful data to cache for key ${key}`); + } + + await AsyncStorage.setItem(key, JSON.stringify(cache)); + } catch (error) { + logger.error(`[Cache] Error setting cached data for key ${key}:`, error); + } + } + + // Methods for This Week section + public async getCachedThisWeekData(libraryItems: any[], traktCollections: TraktCollections): Promise { + return this.getCachedData(THIS_WEEK_CACHE_KEY, libraryItems, traktCollections); + } + + public async setCachedThisWeekData(data: any[], libraryItems: any[], traktCollections: TraktCollections, isErrorRecovery = false): Promise { + await this.setCachedData(THIS_WEEK_CACHE_KEY, data, libraryItems, traktCollections, isErrorRecovery); + } + + // Methods for Calendar screen + public async getCachedCalendarData(libraryItems: any[], traktCollections: TraktCollections): Promise { + return this.getCachedData(CALENDAR_CACHE_KEY, libraryItems, traktCollections); + } + + public async setCachedCalendarData(data: any[], libraryItems: any[], traktCollections: TraktCollections, isErrorRecovery = false): Promise { + await this.setCachedData(CALENDAR_CACHE_KEY, data, libraryItems, traktCollections, isErrorRecovery); + } +} + +export const robustCalendarCache = new RobustCalendarCache(); \ No newline at end of file diff --git a/src/types/metadata.ts b/src/types/metadata.ts index aff9d856..de2338a3 100644 --- a/src/types/metadata.ts +++ b/src/types/metadata.ts @@ -1,4 +1,5 @@ import { TMDBEpisode } from '../services/tmdbService'; +import { StreamingContent } from '../services/catalogService'; // Types for route params export type RouteParams = { @@ -74,46 +75,7 @@ export interface Cast { known_for_department?: string; } -// Streaming content type -export interface StreamingContent { - id: string; - type: string; - name: string; - description?: string; - poster?: string; - posterShape?: string; - banner?: string; - logo?: string; - year?: string | number; - runtime?: string; - imdbRating?: string; - genres?: string[]; - director?: string; - writer?: string[]; - cast?: string[]; - releaseInfo?: string; - directors?: string[]; - creators?: string[]; - certification?: string; - released?: string; - trailerStreams?: any[]; - videos?: any[]; - inLibrary?: boolean; - // Enhanced metadata from addons - country?: string; - links?: Array<{ - name: string; - category: string; - url: string; - }>; - behaviorHints?: { - defaultVideoId?: string; - hasScheduledVideos?: boolean; - [key: string]: any; - }; - imdb_id?: string; - slug?: string; -} +// Streaming content type - REMOVED AND IMPORTED FROM catalogService.ts // Navigation types export type RootStackParamList = {