mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
continue watching fix
This commit is contained in:
parent
980e6d9484
commit
c21f279aa3
4 changed files with 253 additions and 161 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit db674925bbf74e1240cc0625d531853505ca941f
|
Subproject commit 46fce12a69ce684962a76893520e89fec18e0989
|
||||||
|
|
@ -23,6 +23,7 @@ import { storageService } from '../../services/storageService';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { TraktService } from '../../services/traktService';
|
import { TraktService } from '../../services/traktService';
|
||||||
|
import { stremioService } from '../../services/stremioService';
|
||||||
|
|
||||||
// Define interface for continue watching items
|
// Define interface for continue watching items
|
||||||
interface ContinueWatchingItem extends StreamingContent {
|
interface ContinueWatchingItem extends StreamingContent {
|
||||||
|
|
@ -86,6 +87,38 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
// Use a state to track if a background refresh is in progress
|
// Use a state to track if a background refresh is in progress
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
// Cache for metadata to avoid redundant API calls
|
||||||
|
const metadataCache = useRef<Record<string, { metadata: any; basicContent: StreamingContent | null; timestamp: number }>>({});
|
||||||
|
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
// Helper function to get cached or fetch metadata
|
||||||
|
const getCachedMetadata = useCallback(async (type: string, id: string) => {
|
||||||
|
const cacheKey = `${type}:${id}`;
|
||||||
|
const cached = metadataCache.current[cacheKey];
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (cached && (now - cached.timestamp) < CACHE_DURATION) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [metadata, basicContent] = await Promise.all([
|
||||||
|
stremioService.getMetaDetails(type, id),
|
||||||
|
catalogService.getBasicContentDetails(type, id)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (basicContent) {
|
||||||
|
const result = { metadata, basicContent, timestamp: now };
|
||||||
|
metadataCache.current[cacheKey] = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to fetch metadata for ${type}:${id}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Modified loadContinueWatching to be more efficient
|
// Modified loadContinueWatching to be more efficient
|
||||||
const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => {
|
const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => {
|
||||||
// Prevent multiple concurrent refreshes
|
// Prevent multiple concurrent refreshes
|
||||||
|
|
@ -106,153 +139,163 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
|
|
||||||
const progressItems: ContinueWatchingItem[] = [];
|
const progressItems: ContinueWatchingItem[] = [];
|
||||||
const latestEpisodes: Record<string, ContinueWatchingItem> = {};
|
const latestEpisodes: Record<string, ContinueWatchingItem> = {};
|
||||||
const contentPromises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
// Process each saved progress
|
// Group progress items by content ID to batch API calls
|
||||||
|
const contentGroups: Record<string, { type: string; id: string; episodes: Array<{ key: string; episodeId?: string; progress: any; progressPercent: number }> }> = {};
|
||||||
|
|
||||||
|
// First pass: group by content ID
|
||||||
for (const key in allProgress) {
|
for (const key in allProgress) {
|
||||||
// Parse the key to get type and id
|
|
||||||
const keyParts = key.split(':');
|
const keyParts = key.split(':');
|
||||||
const [type, id, ...episodeIdParts] = keyParts;
|
const [type, id, ...episodeIdParts] = keyParts;
|
||||||
const episodeId = episodeIdParts.length > 0 ? episodeIdParts.join(':') : undefined;
|
const episodeId = episodeIdParts.length > 0 ? episodeIdParts.join(':') : undefined;
|
||||||
const progress = allProgress[key];
|
const progress = allProgress[key];
|
||||||
|
|
||||||
// For series, skip episodes that are essentially finished (≥85%)
|
|
||||||
// For movies we still include them so users can "Watch Again"
|
|
||||||
const progressPercent = (progress.currentTime / progress.duration) * 100;
|
const progressPercent = (progress.currentTime / progress.duration) * 100;
|
||||||
|
|
||||||
// Skip fully watched movies
|
// Skip fully watched movies
|
||||||
if (type === 'movie' && progressPercent >= 85) {
|
if (type === 'movie' && progressPercent >= 85) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'series' && progressPercent >= 85) {
|
const contentKey = `${type}:${id}`;
|
||||||
// Determine next episode ID by incrementing episode number
|
if (!contentGroups[contentKey]) {
|
||||||
let nextSeason: number | undefined;
|
contentGroups[contentKey] = { type, id, episodes: [] };
|
||||||
let nextEpisode: number | undefined;
|
|
||||||
let nextEpisodeId: string | undefined;
|
|
||||||
|
|
||||||
if (episodeId) {
|
|
||||||
// Pattern 1: s1e1
|
|
||||||
const match = episodeId.match(/s(\d+)e(\d+)/i);
|
|
||||||
if (match) {
|
|
||||||
const currentSeason = parseInt(match[1], 10);
|
|
||||||
const currentEpisode = parseInt(match[2], 10);
|
|
||||||
nextSeason = currentSeason;
|
|
||||||
nextEpisode = currentEpisode + 1;
|
|
||||||
nextEpisodeId = `s${nextSeason}e${nextEpisode}`;
|
|
||||||
} else {
|
|
||||||
// Pattern 2: id:season:episode
|
|
||||||
const parts = episodeId.split(':');
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
const seasonNum = parseInt(parts[parts.length - 2], 10);
|
|
||||||
const episodeNum = parseInt(parts[parts.length - 1], 10);
|
|
||||||
if (!isNaN(seasonNum) && !isNaN(episodeNum)) {
|
|
||||||
nextSeason = seasonNum;
|
|
||||||
nextEpisode = episodeNum + 1;
|
|
||||||
nextEpisodeId = `${id}:${nextSeason}:${nextEpisode}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push placeholder for next episode with 0% progress
|
|
||||||
if (nextEpisodeId !== undefined) {
|
|
||||||
const basicContent = await catalogService.getBasicContentDetails(type, id);
|
|
||||||
const nextEpisodeItem = {
|
|
||||||
...basicContent,
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
progress: 0,
|
|
||||||
lastUpdated: progress.lastUpdated,
|
|
||||||
season: nextSeason,
|
|
||||||
episode: nextEpisode,
|
|
||||||
episodeTitle: `Episode ${nextEpisode}`,
|
|
||||||
} as ContinueWatchingItem;
|
|
||||||
|
|
||||||
// Store in latestEpisodes to ensure single entry per show
|
|
||||||
const existingLatest = latestEpisodes[id];
|
|
||||||
if (!existingLatest || existingLatest.lastUpdated < nextEpisodeItem.lastUpdated) {
|
|
||||||
latestEpisodes[id] = nextEpisodeItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip adding the finished episode itself
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentPromise = (async () => {
|
contentGroups[contentKey].episodes.push({ key, episodeId, progress, progressPercent });
|
||||||
try {
|
}
|
||||||
// Validate IMDB ID format before attempting to fetch
|
|
||||||
if (!isValidImdbId(id)) {
|
// Second pass: process each content group with batched API calls
|
||||||
return;
|
const contentPromises = Object.values(contentGroups).map(async (group) => {
|
||||||
}
|
try {
|
||||||
|
// Validate IMDB ID format before attempting to fetch
|
||||||
|
if (!isValidImdbId(group.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get metadata once per content
|
||||||
|
const cachedData = await getCachedMetadata(group.type, group.id);
|
||||||
|
if (!cachedData?.basicContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { metadata, basicContent } = cachedData;
|
||||||
|
|
||||||
|
// Process all episodes for this content
|
||||||
|
for (const episode of group.episodes) {
|
||||||
|
const { key, episodeId, progress, progressPercent } = episode;
|
||||||
|
|
||||||
let content: StreamingContent | null = null;
|
if (group.type === 'series' && progressPercent >= 85) {
|
||||||
|
// Handle next episode logic for completed episodes
|
||||||
// Get basic content details using catalogService (no enhanced metadata needed for continue watching)
|
let nextSeason: number | undefined;
|
||||||
content = await catalogService.getBasicContentDetails(type, id);
|
let nextEpisode: number | undefined;
|
||||||
|
|
||||||
if (content) {
|
if (episodeId) {
|
||||||
// Extract season and episode info from episodeId if available
|
// Pattern 1: s1e1
|
||||||
let season: number | undefined;
|
const match = episodeId.match(/s(\d+)e(\d+)/i);
|
||||||
let episode: number | undefined;
|
|
||||||
let episodeTitle: string | undefined;
|
|
||||||
|
|
||||||
if (episodeId && type === 'series') {
|
|
||||||
// Try different episode ID formats
|
|
||||||
let match = episodeId.match(/s(\d+)e(\d+)/i); // Format: s1e1
|
|
||||||
if (match) {
|
if (match) {
|
||||||
season = parseInt(match[1], 10);
|
const currentSeason = parseInt(match[1], 10);
|
||||||
episode = parseInt(match[2], 10);
|
const currentEpisode = parseInt(match[2], 10);
|
||||||
episodeTitle = `Episode ${episode}`;
|
nextSeason = currentSeason;
|
||||||
|
nextEpisode = currentEpisode + 1;
|
||||||
} else {
|
} else {
|
||||||
// Try format: seriesId:season:episode (e.g., tt0108778:4:6)
|
// Pattern 2: id:season:episode
|
||||||
const parts = episodeId.split(':');
|
const parts = episodeId.split(':');
|
||||||
if (parts.length >= 3) {
|
if (parts.length >= 2) {
|
||||||
const seasonPart = parts[parts.length - 2]; // Second to last part
|
const seasonNum = parseInt(parts[parts.length - 2], 10);
|
||||||
const episodePart = parts[parts.length - 1]; // Last part
|
const episodeNum = parseInt(parts[parts.length - 1], 10);
|
||||||
|
|
||||||
const seasonNum = parseInt(seasonPart, 10);
|
|
||||||
const episodeNum = parseInt(episodePart, 10);
|
|
||||||
|
|
||||||
if (!isNaN(seasonNum) && !isNaN(episodeNum)) {
|
if (!isNaN(seasonNum) && !isNaN(episodeNum)) {
|
||||||
season = seasonNum;
|
nextSeason = seasonNum;
|
||||||
episode = episodeNum;
|
nextEpisode = episodeNum + 1;
|
||||||
episodeTitle = `Episode ${episode}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const continueWatchingItem: ContinueWatchingItem = {
|
// Check if next episode exists using cached metadata
|
||||||
...content,
|
if (nextSeason !== undefined && nextEpisode !== undefined && metadata?.videos && Array.isArray(metadata.videos)) {
|
||||||
progress: progressPercent,
|
const nextEpisodeExists = metadata.videos.some((video: any) =>
|
||||||
lastUpdated: progress.lastUpdated,
|
video.season === nextSeason && video.episode === nextEpisode
|
||||||
season,
|
);
|
||||||
episode,
|
|
||||||
episodeTitle
|
if (nextEpisodeExists) {
|
||||||
};
|
const nextEpisodeItem = {
|
||||||
|
...basicContent,
|
||||||
if (type === 'series') {
|
id: group.id,
|
||||||
// For series, keep only the latest watched episode for each show
|
type: group.type,
|
||||||
if (!latestEpisodes[id] || latestEpisodes[id].lastUpdated < progress.lastUpdated) {
|
progress: 0,
|
||||||
latestEpisodes[id] = continueWatchingItem;
|
lastUpdated: progress.lastUpdated,
|
||||||
|
season: nextSeason,
|
||||||
|
episode: nextEpisode,
|
||||||
|
episodeTitle: `Episode ${nextEpisode}`,
|
||||||
|
} as ContinueWatchingItem;
|
||||||
|
|
||||||
|
// Store in latestEpisodes to ensure single entry per show
|
||||||
|
const existingLatest = latestEpisodes[group.id];
|
||||||
|
if (!existingLatest || existingLatest.lastUpdated < nextEpisodeItem.lastUpdated) {
|
||||||
|
latestEpisodes[group.id] = nextEpisodeItem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle in-progress episodes
|
||||||
|
let season: number | undefined;
|
||||||
|
let episodeNumber: number | undefined;
|
||||||
|
let episodeTitle: string | undefined;
|
||||||
|
|
||||||
|
if (episodeId && group.type === 'series') {
|
||||||
|
// Try different episode ID formats
|
||||||
|
let match = episodeId.match(/s(\d+)e(\d+)/i); // Format: s1e1
|
||||||
|
if (match) {
|
||||||
|
season = parseInt(match[1], 10);
|
||||||
|
episodeNumber = parseInt(match[2], 10);
|
||||||
|
episodeTitle = `Episode ${episodeNumber}`;
|
||||||
} else {
|
} else {
|
||||||
// For movies, add to the list directly
|
// Try format: seriesId:season:episode (e.g., tt0108778:4:6)
|
||||||
progressItems.push(continueWatchingItem);
|
const parts = episodeId.split(':');
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
const seasonPart = parts[parts.length - 2]; // Second to last part
|
||||||
|
const episodePart = parts[parts.length - 1]; // Last part
|
||||||
|
|
||||||
|
const seasonNum = parseInt(seasonPart, 10);
|
||||||
|
const episodeNum = parseInt(episodePart, 10);
|
||||||
|
|
||||||
|
if (!isNaN(seasonNum) && !isNaN(episodeNum)) {
|
||||||
|
season = seasonNum;
|
||||||
|
episodeNumber = episodeNum;
|
||||||
|
episodeTitle = `Episode ${episodeNumber}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to get content details for ${type}:${id}`, error);
|
const continueWatchingItem: ContinueWatchingItem = {
|
||||||
|
...basicContent,
|
||||||
|
progress: progressPercent,
|
||||||
|
lastUpdated: progress.lastUpdated,
|
||||||
|
season,
|
||||||
|
episode: episodeNumber,
|
||||||
|
episodeTitle
|
||||||
|
};
|
||||||
|
|
||||||
|
if (group.type === 'series') {
|
||||||
|
// For series, keep only the latest watched episode for each show
|
||||||
|
if (!latestEpisodes[group.id] || latestEpisodes[group.id].lastUpdated < progress.lastUpdated) {
|
||||||
|
latestEpisodes[group.id] = continueWatchingItem;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For movies, add to the list directly
|
||||||
|
progressItems.push(continueWatchingItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
} catch (error) {
|
||||||
|
logger.error(`Failed to process content group ${group.type}:${group.id}:`, error);
|
||||||
contentPromises.push(contentPromise);
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Wait for all content to be processed
|
// Wait for all content to be processed
|
||||||
await Promise.all(contentPromises);
|
await Promise.all(contentPromises);
|
||||||
|
|
||||||
// -------------------- TRAKT HISTORY INTEGRATION --------------------
|
// -------------------- TRAKT HISTORY INTEGRATION --------------------
|
||||||
try {
|
try {
|
||||||
|
|
@ -278,29 +321,40 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create placeholders (or update) for each show based on Trakt history
|
// Process Trakt shows in batches using cached metadata
|
||||||
for (const [showId, info] of Object.entries(latestWatchedByShow)) {
|
const traktPromises = Object.entries(latestWatchedByShow).map(async ([showId, info]) => {
|
||||||
const nextEpisode = info.episode + 1;
|
|
||||||
const nextEpisodeId = `${showId}:${info.season}:${nextEpisode}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const basicContent = await catalogService.getBasicContentDetails('series', showId);
|
const nextEpisode = info.episode + 1;
|
||||||
if (!basicContent) continue;
|
|
||||||
|
// Use cached metadata to validate next episode exists
|
||||||
|
const cachedData = await getCachedMetadata('series', showId);
|
||||||
|
if (!cachedData?.basicContent) return;
|
||||||
|
|
||||||
|
const { metadata, basicContent } = cachedData;
|
||||||
|
let nextEpisodeExists = false;
|
||||||
|
|
||||||
|
if (metadata?.videos && Array.isArray(metadata.videos)) {
|
||||||
|
nextEpisodeExists = metadata.videos.some((video: any) =>
|
||||||
|
video.season === info.season && video.episode === nextEpisode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextEpisodeExists) {
|
||||||
|
const placeholder: ContinueWatchingItem = {
|
||||||
|
...basicContent,
|
||||||
|
id: showId,
|
||||||
|
type: 'series',
|
||||||
|
progress: 0,
|
||||||
|
lastUpdated: info.watchedAt,
|
||||||
|
season: info.season,
|
||||||
|
episode: nextEpisode,
|
||||||
|
episodeTitle: `Episode ${nextEpisode}`,
|
||||||
|
} as ContinueWatchingItem;
|
||||||
|
|
||||||
const placeholder: ContinueWatchingItem = {
|
const existing = latestEpisodes[showId];
|
||||||
...basicContent,
|
if (!existing || existing.lastUpdated < info.watchedAt) {
|
||||||
id: showId,
|
latestEpisodes[showId] = placeholder;
|
||||||
type: 'series',
|
}
|
||||||
progress: 0,
|
|
||||||
lastUpdated: info.watchedAt,
|
|
||||||
season: info.season,
|
|
||||||
episode: nextEpisode,
|
|
||||||
episodeTitle: `Episode ${nextEpisode}`,
|
|
||||||
} as ContinueWatchingItem;
|
|
||||||
|
|
||||||
const existing = latestEpisodes[showId];
|
|
||||||
if (!existing || existing.lastUpdated < info.watchedAt) {
|
|
||||||
latestEpisodes[showId] = placeholder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist "watched" progress for the episode that Trakt reported
|
// Persist "watched" progress for the episode that Trakt reported
|
||||||
|
|
@ -325,7 +379,9 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Failed to build placeholder from history:', err);
|
logger.error('Failed to build placeholder from history:', err);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
await Promise.all(traktPromises);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error merging Trakt history:', err);
|
logger.error('Error merging Trakt history:', err);
|
||||||
|
|
@ -345,7 +401,14 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
}
|
}
|
||||||
}, [isRefreshing]);
|
}, [isRefreshing, getCachedMetadata]);
|
||||||
|
|
||||||
|
// Clear cache when component unmounts or when needed
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
metadataCache.current = {};
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Function to handle app state changes
|
// Function to handle app state changes
|
||||||
const handleAppStateChange = useCallback((nextAppState: AppStateStatus) => {
|
const handleAppStateChange = useCallback((nextAppState: AppStateStatus) => {
|
||||||
|
|
@ -816,4 +879,4 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default React.memo(ContinueWatchingSection);
|
export default React.memo(ContinueWatchingSection);
|
||||||
|
|
@ -67,6 +67,7 @@ interface HeroSectionProps {
|
||||||
getPlayButtonText: () => string;
|
getPlayButtonText: () => string;
|
||||||
setBannerImage: (bannerImage: string | null) => void;
|
setBannerImage: (bannerImage: string | null) => void;
|
||||||
setLogoLoadError: (error: boolean) => void;
|
setLogoLoadError: (error: boolean) => void;
|
||||||
|
groupedEpisodes?: { [seasonNumber: number]: any[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ultra-optimized ActionButtons Component - minimal re-renders
|
// Ultra-optimized ActionButtons Component - minimal re-renders
|
||||||
|
|
@ -80,7 +81,8 @@ const ActionButtons = React.memo(({
|
||||||
playButtonText,
|
playButtonText,
|
||||||
animatedStyle,
|
animatedStyle,
|
||||||
isWatched,
|
isWatched,
|
||||||
watchProgress
|
watchProgress,
|
||||||
|
groupedEpisodes
|
||||||
}: {
|
}: {
|
||||||
handleShowStreams: () => void;
|
handleShowStreams: () => void;
|
||||||
toggleLibrary: () => void;
|
toggleLibrary: () => void;
|
||||||
|
|
@ -92,6 +94,7 @@ const ActionButtons = React.memo(({
|
||||||
animatedStyle: any;
|
animatedStyle: any;
|
||||||
isWatched: boolean;
|
isWatched: boolean;
|
||||||
watchProgress: any;
|
watchProgress: any;
|
||||||
|
groupedEpisodes?: { [seasonNumber: number]: any[] };
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
|
|
@ -147,17 +150,13 @@ const ActionButtons = React.memo(({
|
||||||
}, [isWatched, type]);
|
}, [isWatched, type]);
|
||||||
|
|
||||||
const finalPlayButtonText = useMemo(() => {
|
const finalPlayButtonText = useMemo(() => {
|
||||||
if (!isWatched) {
|
// For movies, handle watched state
|
||||||
return playButtonText;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If content is a movie, keep existing "Watch Again" label
|
|
||||||
if (type === 'movie') {
|
if (type === 'movie') {
|
||||||
return 'Watch Again';
|
return isWatched ? 'Watch Again' : playButtonText;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For series, attempt to show the next episode label (e.g., "Play S02E05")
|
// For series, validate next episode existence for both watched and resume cases
|
||||||
if (type === 'series' && watchProgress?.episodeId) {
|
if (type === 'series' && watchProgress?.episodeId && groupedEpisodes) {
|
||||||
let seasonNum: number | null = null;
|
let seasonNum: number | null = null;
|
||||||
let episodeNum: number | null = null;
|
let episodeNum: number | null = null;
|
||||||
|
|
||||||
|
|
@ -181,20 +180,47 @@ const ActionButtons = React.memo(({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seasonNum !== null && episodeNum !== null && !isNaN(seasonNum) && !isNaN(episodeNum)) {
|
if (seasonNum !== null && episodeNum !== null && !isNaN(seasonNum) && !isNaN(episodeNum)) {
|
||||||
// For watched episodes, show the NEXT episode number
|
if (isWatched) {
|
||||||
const nextEpisode = episodeNum + 1;
|
// For watched episodes, check if next episode exists
|
||||||
const seasonStr = seasonNum.toString().padStart(2, '0');
|
const nextEpisode = episodeNum + 1;
|
||||||
const episodeStr = nextEpisode.toString().padStart(2, '0');
|
const currentSeasonEpisodes = groupedEpisodes[seasonNum] || [];
|
||||||
return `Play S${seasonStr}E${episodeStr}`;
|
const nextEpisodeExists = currentSeasonEpisodes.some(ep =>
|
||||||
|
ep.episode_number === nextEpisode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextEpisodeExists) {
|
||||||
|
// Show the NEXT episode number only if it exists
|
||||||
|
const seasonStr = seasonNum.toString().padStart(2, '0');
|
||||||
|
const episodeStr = nextEpisode.toString().padStart(2, '0');
|
||||||
|
return `Play S${seasonStr}E${episodeStr}`;
|
||||||
|
} else {
|
||||||
|
// If next episode doesn't exist, show generic text
|
||||||
|
return 'Completed';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-watched episodes, check if current episode exists
|
||||||
|
const currentSeasonEpisodes = groupedEpisodes[seasonNum] || [];
|
||||||
|
const currentEpisodeExists = currentSeasonEpisodes.some(ep =>
|
||||||
|
ep.episode_number === episodeNum
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentEpisodeExists) {
|
||||||
|
// Current episode exists, use original button text
|
||||||
|
return playButtonText;
|
||||||
|
} else {
|
||||||
|
// Current episode doesn't exist, fallback to generic play
|
||||||
|
return 'Play';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback label if parsing fails
|
// Fallback label if parsing fails
|
||||||
return 'Play Next Episode';
|
return isWatched ? 'Play Next Episode' : playButtonText;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default fallback
|
// Default fallback for non-series or missing data
|
||||||
return 'Play';
|
return isWatched ? 'Play' : playButtonText;
|
||||||
}, [isWatched, playButtonText, type, watchProgress]);
|
}, [isWatched, playButtonText, type, watchProgress, groupedEpisodes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.actionButtons, animatedStyle]}>
|
<Animated.View style={[styles.actionButtons, animatedStyle]}>
|
||||||
|
|
@ -620,6 +646,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
getPlayButtonText,
|
getPlayButtonText,
|
||||||
setBannerImage,
|
setBannerImage,
|
||||||
setLogoLoadError,
|
setLogoLoadError,
|
||||||
|
groupedEpisodes,
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { isAuthenticated: isTraktAuthenticated } = useTraktContext();
|
const { isAuthenticated: isTraktAuthenticated } = useTraktContext();
|
||||||
|
|
@ -875,6 +902,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
animatedStyle={buttonsAnimatedStyle}
|
animatedStyle={buttonsAnimatedStyle}
|
||||||
isWatched={isWatched}
|
isWatched={isWatched}
|
||||||
watchProgress={watchProgress}
|
watchProgress={watchProgress}
|
||||||
|
groupedEpisodes={groupedEpisodes}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
@ -1277,4 +1305,4 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default React.memo(HeroSection);
|
export default React.memo(HeroSection);
|
||||||
|
|
@ -432,6 +432,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
getPlayButtonText={watchProgressData.getPlayButtonText}
|
getPlayButtonText={watchProgressData.getPlayButtonText}
|
||||||
setBannerImage={assetData.setBannerImage}
|
setBannerImage={assetData.setBannerImage}
|
||||||
setLogoLoadError={assetData.setLogoLoadError}
|
setLogoLoadError={assetData.setLogoLoadError}
|
||||||
|
groupedEpisodes={groupedEpisodes}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main Content - Optimized */}
|
{/* Main Content - Optimized */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue