CP, changes to calenderscreen

This commit is contained in:
tapframe 2025-07-07 19:04:19 +05:30
parent fe8489fa18
commit 6493432099
6 changed files with 388 additions and 440 deletions

View file

@ -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<ThisWeekEpisode[]>([]);
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 (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>
Loading this week's episodes...
</Text>
</View>
);
}
if (episodes.length === 0) {
if (thisWeekEpisodes.length === 0) {
return null;
}
@ -335,7 +195,7 @@ export const ThisWeekSection = React.memo(() => {
</View>
<FlatList
data={episodes}
data={thisWeekEpisodes}
keyExtractor={(item) => item.id}
renderItem={renderEpisodeItem}
horizontal

View file

@ -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<CalendarSection[]>([]);
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,
};
};

View file

@ -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<NavigationProp<RootStackParamList>>();
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<CalendarSection[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [filteredEpisodes, setFilteredEpisodes] = useState<CalendarEpisode[]>([]);
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 = () => {
</View>
)}
<CalendarSection
<CalendarSectionComponent
episodes={allEpisodes}
onSelectDate={handleDateSelect}
/>

View file

@ -70,6 +70,7 @@ export interface StreamingContent {
imdb_id?: string;
slug?: string;
releaseInfo?: string;
traktSource?: 'watchlist' | 'continue-watching' | 'watched';
}
export interface CatalogContent {

View file

@ -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<T> {
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<T>(key: string, libraryItems: any[], traktCollections: TraktCollections): Promise<T | null> {
try {
const storedCache = await AsyncStorage.getItem(key);
if (!storedCache) return null;
const cache: CachedData<T> = 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<T>(key: string, data: T, libraryItems: any[], traktCollections: TraktCollections, isErrorRecovery = false): Promise<void> {
try {
const hash = this.generateHash(libraryItems, traktCollections);
const cache: CachedData<T> = {
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<any[] | null> {
return this.getCachedData<any[]>(THIS_WEEK_CACHE_KEY, libraryItems, traktCollections);
}
public async setCachedThisWeekData(data: any[], libraryItems: any[], traktCollections: TraktCollections, isErrorRecovery = false): Promise<void> {
await this.setCachedData<any[]>(THIS_WEEK_CACHE_KEY, data, libraryItems, traktCollections, isErrorRecovery);
}
// Methods for Calendar screen
public async getCachedCalendarData(libraryItems: any[], traktCollections: TraktCollections): Promise<any[] | null> {
return this.getCachedData<any[]>(CALENDAR_CACHE_KEY, libraryItems, traktCollections);
}
public async setCachedCalendarData(data: any[], libraryItems: any[], traktCollections: TraktCollections, isErrorRecovery = false): Promise<void> {
await this.setCachedData<any[]>(CALENDAR_CACHE_KEY, data, libraryItems, traktCollections, isErrorRecovery);
}
}
export const robustCalendarCache = new RobustCalendarCache();

View file

@ -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 = {