mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-14 13:40:21 +00:00
CP, changes to calenderscreen
This commit is contained in:
parent
fe8489fa18
commit
6493432099
6 changed files with 388 additions and 440 deletions
|
|
@ -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
|
||||
|
|
|
|||
261
src/hooks/useCalendarData.ts
Normal file
261
src/hooks/useCalendarData.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export interface StreamingContent {
|
|||
imdb_id?: string;
|
||||
slug?: string;
|
||||
releaseInfo?: string;
|
||||
traktSource?: 'watchlist' | 'continue-watching' | 'watched';
|
||||
}
|
||||
|
||||
export interface CatalogContent {
|
||||
|
|
|
|||
102
src/services/robustCalendarCache.ts
Normal file
102
src/services/robustCalendarCache.ts
Normal 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();
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue