mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 08:41:57 +00:00
verbose logs cleanup
This commit is contained in:
parent
674dbcf818
commit
2314d1db86
9 changed files with 265 additions and 266 deletions
|
|
@ -38,36 +38,36 @@ interface StreamCardProps {
|
||||||
parentImdbId?: string;
|
parentImdbId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StreamCard = memo(({
|
const StreamCard = memo(({
|
||||||
stream,
|
stream,
|
||||||
onPress,
|
onPress,
|
||||||
index,
|
index,
|
||||||
isLoading,
|
isLoading,
|
||||||
statusMessage,
|
statusMessage,
|
||||||
theme,
|
theme,
|
||||||
showLogos,
|
showLogos,
|
||||||
scraperLogo,
|
scraperLogo,
|
||||||
showAlert,
|
showAlert,
|
||||||
parentTitle,
|
parentTitle,
|
||||||
parentType,
|
parentType,
|
||||||
parentSeason,
|
parentSeason,
|
||||||
parentEpisode,
|
parentEpisode,
|
||||||
parentEpisodeTitle,
|
parentEpisodeTitle,
|
||||||
parentPosterUrl,
|
parentPosterUrl,
|
||||||
providerName,
|
providerName,
|
||||||
parentId,
|
parentId,
|
||||||
parentImdbId
|
parentImdbId
|
||||||
}: StreamCardProps) => {
|
}: StreamCardProps) => {
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const { startDownload } = useDownloads();
|
const { startDownload } = useDownloads();
|
||||||
const { showSuccess, showInfo } = useToast();
|
const { showSuccess, showInfo } = useToast();
|
||||||
|
|
||||||
// Handle long press to copy stream URL to clipboard
|
// Handle long press to copy stream URL to clipboard
|
||||||
const handleLongPress = useCallback(async () => {
|
const handleLongPress = useCallback(async () => {
|
||||||
if (stream.url) {
|
if (stream.url) {
|
||||||
try {
|
try {
|
||||||
await Clipboard.setString(stream.url);
|
await Clipboard.setString(stream.url);
|
||||||
|
|
||||||
// Use toast for Android, custom alert for iOS
|
// Use toast for Android, custom alert for iOS
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
showSuccess('URL Copied', 'Stream URL copied to clipboard!');
|
showSuccess('URL Copied', 'Stream URL copied to clipboard!');
|
||||||
|
|
@ -85,13 +85,13 @@ const StreamCard = memo(({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [stream.url, showAlert, showSuccess, showInfo]);
|
}, [stream.url, showAlert, showSuccess, showInfo]);
|
||||||
|
|
||||||
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
||||||
|
|
||||||
const streamInfo = useMemo(() => {
|
const streamInfo = useMemo(() => {
|
||||||
const title = stream.title || '';
|
const title = stream.title || '';
|
||||||
const name = stream.name || '';
|
const name = stream.name || '';
|
||||||
|
|
||||||
// Helper function to format size from bytes
|
// Helper function to format size from bytes
|
||||||
const formatSize = (bytes: number): string => {
|
const formatSize = (bytes: number): string => {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
@ -100,16 +100,16 @@ const StreamCard = memo(({
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get size from title (legacy format) or from stream.size field
|
// Get size from title (legacy format) or from stream.size field
|
||||||
let sizeDisplay = title.match(/💾\s*([\d.]+\s*[GM]B)/)?.[1];
|
let sizeDisplay = title.match(/💾\s*([\d.]+\s*[GM]B)/)?.[1];
|
||||||
if (!sizeDisplay && stream.size && typeof stream.size === 'number' && stream.size > 0) {
|
if (!sizeDisplay && stream.size && typeof stream.size === 'number' && stream.size > 0) {
|
||||||
sizeDisplay = formatSize(stream.size);
|
sizeDisplay = formatSize(stream.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract quality for badge display
|
// Extract quality for badge display
|
||||||
const basicQuality = title.match(/(\d+)p/)?.[1] || null;
|
const basicQuality = title.match(/(\d+)p/)?.[1] || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
quality: basicQuality,
|
quality: basicQuality,
|
||||||
isHDR: title.toLowerCase().includes('hdr'),
|
isHDR: title.toLowerCase().includes('hdr'),
|
||||||
|
|
@ -120,7 +120,7 @@ const StreamCard = memo(({
|
||||||
subTitle: title && title !== name ? title : null
|
subTitle: title && title !== name ? title : null
|
||||||
};
|
};
|
||||||
}, [stream.name, stream.title, stream.behaviorHints, stream.size]);
|
}, [stream.name, stream.title, stream.behaviorHints, stream.size]);
|
||||||
|
|
||||||
const handleDownload = useCallback(async () => {
|
const handleDownload = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const url = stream.url;
|
const url = stream.url;
|
||||||
|
|
@ -132,9 +132,10 @@ const StreamCard = memo(({
|
||||||
showAlert('Already Downloading', 'This download has already started for this exact link.');
|
showAlert('Already Downloading', 'This download has already started for this exact link.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
// Show immediate feedback on both platforms
|
// Show immediate feedback on both platforms
|
||||||
showAlert('Starting Download', 'Download will be started.');
|
// Show immediate feedback on both platforms
|
||||||
|
// showAlert('Starting Download', 'Download will be started.');
|
||||||
const parent: any = stream as any;
|
const parent: any = stream as any;
|
||||||
const inferredTitle = parentTitle || stream.name || stream.title || parent.metaName || 'Content';
|
const inferredTitle = parentTitle || stream.name || stream.title || parent.metaName || 'Content';
|
||||||
const inferredType: 'movie' | 'series' = parentType || (parent.kind === 'series' || parent.type === 'series' ? 'series' : 'movie');
|
const inferredType: 'movie' | 'series' = parentType || (parent.kind === 'series' || parent.type === 'series' ? 'series' : 'movie');
|
||||||
|
|
@ -143,10 +144,10 @@ const StreamCard = memo(({
|
||||||
const episodeTitle = parentEpisodeTitle || parent.episodeTitle || parent.episode_name;
|
const episodeTitle = parentEpisodeTitle || parent.episodeTitle || parent.episode_name;
|
||||||
// Prefer the stream's display name (often includes provider + resolution)
|
// Prefer the stream's display name (often includes provider + resolution)
|
||||||
const provider = (stream.name as any) || (stream.title as any) || providerName || parent.addonName || parent.addonId || (stream.addonName as any) || (stream.addonId as any) || 'Provider';
|
const provider = (stream.name as any) || (stream.title as any) || providerName || parent.addonName || parent.addonId || (stream.addonName as any) || (stream.addonId as any) || 'Provider';
|
||||||
|
|
||||||
// Use parentId first (from route params), fallback to stream metadata
|
// Use parentId first (from route params), fallback to stream metadata
|
||||||
const idForContent = parentId || parent.imdbId || parent.tmdbId || parent.addonId || inferredTitle;
|
const idForContent = parentId || parent.imdbId || parent.tmdbId || parent.addonId || inferredTitle;
|
||||||
|
|
||||||
// Extract tmdbId if available (from parentId or parent metadata)
|
// Extract tmdbId if available (from parentId or parent metadata)
|
||||||
let tmdbId: number | undefined = undefined;
|
let tmdbId: number | undefined = undefined;
|
||||||
if (parentId && parentId.startsWith('tmdb:')) {
|
if (parentId && parentId.startsWith('tmdb:')) {
|
||||||
|
|
@ -172,99 +173,101 @@ const StreamCard = memo(({
|
||||||
tmdbId: tmdbId,
|
tmdbId: tmdbId,
|
||||||
});
|
});
|
||||||
showAlert('Download Started', 'Your download has been added to the queue.');
|
showAlert('Download Started', 'Your download has been added to the queue.');
|
||||||
} catch {}
|
} catch (e: any) {
|
||||||
|
showAlert('Download Failed', e.message || 'Could not start download.');
|
||||||
|
}
|
||||||
}, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title, parentId, parentImdbId, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName]);
|
}, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title, parentId, parentImdbId, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName]);
|
||||||
|
|
||||||
const isDebrid = streamInfo.isDebrid;
|
const isDebrid = streamInfo.isDebrid;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.streamCard,
|
styles.streamCard,
|
||||||
isLoading && styles.streamCardLoading,
|
isLoading && styles.streamCardLoading,
|
||||||
isDebrid && styles.streamCardHighlighted
|
isDebrid && styles.streamCardHighlighted
|
||||||
]}
|
]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={handleLongPress}
|
onLongPress={handleLongPress}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
{/* Scraper Logo */}
|
{/* Scraper Logo */}
|
||||||
{showLogos && scraperLogo && (
|
{showLogos && scraperLogo && (
|
||||||
<View style={styles.scraperLogoContainer}>
|
<View style={styles.scraperLogoContainer}>
|
||||||
{scraperLogo.toLowerCase().endsWith('.svg') || scraperLogo.toLowerCase().includes('.svg?') ? (
|
{scraperLogo.toLowerCase().endsWith('.svg') || scraperLogo.toLowerCase().includes('.svg?') ? (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: scraperLogo }}
|
source={{ uri: scraperLogo }}
|
||||||
style={styles.scraperLogo}
|
style={styles.scraperLogo}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: scraperLogo }}
|
|
||||||
style={styles.scraperLogo}
|
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View style={styles.streamDetails}>
|
|
||||||
<View style={styles.streamNameRow}>
|
|
||||||
<View style={styles.streamTitleContainer}>
|
|
||||||
<Text style={[styles.streamName, { color: theme.colors.highEmphasis }]}>
|
|
||||||
{streamInfo.displayName}
|
|
||||||
</Text>
|
|
||||||
{streamInfo.subTitle && (
|
|
||||||
<Text style={[styles.streamAddonName, { color: theme.colors.mediumEmphasis }]}>
|
|
||||||
{streamInfo.subTitle}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Show loading indicator if stream is loading */}
|
|
||||||
{isLoading && (
|
|
||||||
<View style={styles.loadingIndicator}>
|
|
||||||
<ActivityIndicator size="small" color={theme.colors.primary} />
|
|
||||||
<Text style={[styles.loadingText, { color: theme.colors.primary }]}>
|
|
||||||
{statusMessage || "Loading..."}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.streamMetaRow}>
|
|
||||||
{streamInfo.isDolby && (
|
|
||||||
<QualityBadge type="VISION" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{streamInfo.size && (
|
|
||||||
<View style={[styles.chip, { backgroundColor: theme.colors.darkGray }]}>
|
|
||||||
<Text style={[styles.chipText, { color: theme.colors.white }]}>💾 {streamInfo.size}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{streamInfo.isDebrid && (
|
|
||||||
<View style={[styles.chip, { backgroundColor: theme.colors.success }]}>
|
|
||||||
<Text style={[styles.chipText, { color: theme.colors.white }]}>DEBRID</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
|
|
||||||
{settings?.enableDownloads !== false && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.streamAction, { marginLeft: 8, backgroundColor: theme.colors.elevation2 }]}
|
|
||||||
onPress={handleDownload}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
|
||||||
name="download"
|
|
||||||
size={20}
|
|
||||||
color={theme.colors.highEmphasis}
|
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
) : (
|
||||||
)}
|
<FastImage
|
||||||
</TouchableOpacity>
|
source={{ uri: scraperLogo }}
|
||||||
|
style={styles.scraperLogo}
|
||||||
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.streamDetails}>
|
||||||
|
<View style={styles.streamNameRow}>
|
||||||
|
<View style={styles.streamTitleContainer}>
|
||||||
|
<Text style={[styles.streamName, { color: theme.colors.highEmphasis }]}>
|
||||||
|
{streamInfo.displayName}
|
||||||
|
</Text>
|
||||||
|
{streamInfo.subTitle && (
|
||||||
|
<Text style={[styles.streamAddonName, { color: theme.colors.mediumEmphasis }]}>
|
||||||
|
{streamInfo.subTitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Show loading indicator if stream is loading */}
|
||||||
|
{isLoading && (
|
||||||
|
<View style={styles.loadingIndicator}>
|
||||||
|
<ActivityIndicator size="small" color={theme.colors.primary} />
|
||||||
|
<Text style={[styles.loadingText, { color: theme.colors.primary }]}>
|
||||||
|
{statusMessage || "Loading..."}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.streamMetaRow}>
|
||||||
|
{streamInfo.isDolby && (
|
||||||
|
<QualityBadge type="VISION" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{streamInfo.size && (
|
||||||
|
<View style={[styles.chip, { backgroundColor: theme.colors.darkGray }]}>
|
||||||
|
<Text style={[styles.chipText, { color: theme.colors.white }]}>💾 {streamInfo.size}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{streamInfo.isDebrid && (
|
||||||
|
<View style={[styles.chip, { backgroundColor: theme.colors.success }]}>
|
||||||
|
<Text style={[styles.chipText, { color: theme.colors.white }]}>DEBRID</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
|
||||||
|
{settings?.enableDownloads !== false && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.streamAction, { marginLeft: 8, backgroundColor: theme.colors.elevation2 }]}
|
||||||
|
onPress={handleDownload}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<MaterialIcons
|
||||||
|
name="download"
|
||||||
|
size={20}
|
||||||
|
color={theme.colors.highEmphasis}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -446,7 +446,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
||||||
if (url) {
|
if (url) {
|
||||||
const bestUrl = TrailerService.getBestFormatUrl(url);
|
const bestUrl = TrailerService.getBestFormatUrl(url);
|
||||||
setTrailerUrl(bestUrl);
|
setTrailerUrl(bestUrl);
|
||||||
logger.info('[AppleTVHero] Trailer URL loaded:', bestUrl);
|
// logger.info('[AppleTVHero] Trailer URL loaded:', bestUrl);
|
||||||
} else {
|
} else {
|
||||||
logger.info('[AppleTVHero] No trailer found for:', currentItem.name);
|
logger.info('[AppleTVHero] No trailer found for:', currentItem.name);
|
||||||
setTrailerUrl(null);
|
setTrailerUrl(null);
|
||||||
|
|
@ -997,7 +997,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
||||||
{/* Background Images with Crossfade */}
|
{/* Background Images with Crossfade */}
|
||||||
<View style={styles.backgroundContainer}>
|
<View style={styles.backgroundContainer}>
|
||||||
{/* Current Image - Always visible as base */}
|
{/* Current Image - Always visible as base */}
|
||||||
<Animated.View style={[styles.imageWrapper, backgroundParallaxStyle]}>
|
<Animated.View style={[styles.imageWrapper, backgroundParallaxStyle, { opacity: thumbnailOpacity }]}>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{
|
source={{
|
||||||
uri: bannerUrl,
|
uri: bannerUrl,
|
||||||
|
|
@ -1441,7 +1441,7 @@ const styles = StyleSheet.create({
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: 40,
|
height: 400, // Increased to cover action buttons with dark background
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
},
|
},
|
||||||
// Loading & Empty States
|
// Loading & Empty States
|
||||||
|
|
|
||||||
|
|
@ -351,7 +351,6 @@ const CompactCommentCard: React.FC<{
|
||||||
onPressIn={() => setIsPressed(true)}
|
onPressIn={() => setIsPressed(true)}
|
||||||
onPressOut={() => setIsPressed(false)}
|
onPressOut={() => setIsPressed(false)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
console.log('CompactCommentCard: TouchableOpacity pressed for comment:', comment.id);
|
|
||||||
onPress();
|
onPress();
|
||||||
}}
|
}}
|
||||||
activeOpacity={1}
|
activeOpacity={1}
|
||||||
|
|
@ -789,26 +788,21 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
||||||
}, [loading]);
|
}, [loading]);
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
console.log('CommentsSection: Comments data:', comments);
|
// Debug logging removed per user request
|
||||||
console.log('CommentsSection: Comments length:', comments?.length);
|
|
||||||
console.log('CommentsSection: Loading:', loading);
|
|
||||||
console.log('CommentsSection: Error:', error);
|
|
||||||
|
|
||||||
const renderComment = useCallback(({ item }: { item: TraktContentComment }) => {
|
const renderComment = useCallback(({ item }: { item: TraktContentComment }) => {
|
||||||
// Safety check for null/undefined items
|
// Safety check for null/undefined items
|
||||||
if (!item || !item.id) {
|
if (!item || !item.id) {
|
||||||
console.log('CommentsSection: Invalid comment item:', item);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('CommentsSection: Rendering comment:', item.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CompactCommentCard
|
<CompactCommentCard
|
||||||
comment={item}
|
comment={item}
|
||||||
theme={currentTheme}
|
theme={currentTheme}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
console.log('CommentsSection: Comment pressed:', item.id);
|
|
||||||
onCommentPress?.(item);
|
onCommentPress?.(item);
|
||||||
}}
|
}}
|
||||||
isSpoilerRevealed={true}
|
isSpoilerRevealed={true}
|
||||||
|
|
|
||||||
|
|
@ -925,7 +925,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
// Handle trailer preload completion
|
// Handle trailer preload completion
|
||||||
const handleTrailerPreloaded = useCallback(() => {
|
const handleTrailerPreloaded = useCallback(() => {
|
||||||
setTrailerPreloaded(true);
|
setTrailerPreloaded(true);
|
||||||
logger.info('HeroSection', 'Trailer preloaded successfully');
|
// logger.info('HeroSection', 'Trailer preloaded successfully');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle smooth transition when trailer is ready to play
|
// Handle smooth transition when trailer is ready to play
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { isTrailerPlaying: globalTrailerPlaying } = useTrailer();
|
const { isTrailerPlaying: globalTrailerPlaying } = useTrailer();
|
||||||
const videoRef = useRef<VideoRef>(null);
|
const videoRef = useRef<VideoRef>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isPlaying, setIsPlaying] = useState(autoPlay);
|
const [isPlaying, setIsPlaying] = useState(autoPlay);
|
||||||
const [isMuted, setIsMuted] = useState(muted);
|
const [isMuted, setIsMuted] = useState(muted);
|
||||||
|
|
@ -90,16 +90,16 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
// Pause the video
|
// Pause the video
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
|
|
||||||
// Seek to beginning to stop any background processing
|
// Seek to beginning to stop any background processing
|
||||||
videoRef.current.seek(0);
|
videoRef.current.seek(0);
|
||||||
|
|
||||||
// Clear any pending timeouts
|
// Clear any pending timeouts
|
||||||
if (hideControlsTimeout.current) {
|
if (hideControlsTimeout.current) {
|
||||||
clearTimeout(hideControlsTimeout.current);
|
clearTimeout(hideControlsTimeout.current);
|
||||||
hideControlsTimeout.current = null;
|
hideControlsTimeout.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('TrailerPlayer', 'Video cleanup completed');
|
logger.info('TrailerPlayer', 'Video cleanup completed');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -138,7 +138,7 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
// Component mount/unmount tracking
|
// Component mount/unmount tracking
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsComponentMounted(true);
|
setIsComponentMounted(true);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setIsComponentMounted(false);
|
setIsComponentMounted(false);
|
||||||
cleanupVideo();
|
cleanupVideo();
|
||||||
|
|
@ -185,15 +185,15 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
|
|
||||||
const showControlsWithTimeout = useCallback(() => {
|
const showControlsWithTimeout = useCallback(() => {
|
||||||
if (!isComponentMounted) return;
|
if (!isComponentMounted) return;
|
||||||
|
|
||||||
setShowControls(true);
|
setShowControls(true);
|
||||||
controlsOpacity.value = withTiming(1, { duration: 200 });
|
controlsOpacity.value = withTiming(1, { duration: 200 });
|
||||||
|
|
||||||
// Clear existing timeout
|
// Clear existing timeout
|
||||||
if (hideControlsTimeout.current) {
|
if (hideControlsTimeout.current) {
|
||||||
clearTimeout(hideControlsTimeout.current);
|
clearTimeout(hideControlsTimeout.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set new timeout to hide controls
|
// Set new timeout to hide controls
|
||||||
hideControlsTimeout.current = setTimeout(() => {
|
hideControlsTimeout.current = setTimeout(() => {
|
||||||
if (isComponentMounted) {
|
if (isComponentMounted) {
|
||||||
|
|
@ -205,7 +205,7 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
|
|
||||||
const handleVideoPress = useCallback(() => {
|
const handleVideoPress = useCallback(() => {
|
||||||
if (!isComponentMounted) return;
|
if (!isComponentMounted) return;
|
||||||
|
|
||||||
if (showControls) {
|
if (showControls) {
|
||||||
// If controls are visible, toggle play/pause
|
// If controls are visible, toggle play/pause
|
||||||
handlePlayPause();
|
handlePlayPause();
|
||||||
|
|
@ -218,7 +218,7 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
const handlePlayPause = useCallback(async () => {
|
const handlePlayPause = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (!videoRef.current || !isComponentMounted) return;
|
if (!videoRef.current || !isComponentMounted) return;
|
||||||
|
|
||||||
playButtonScale.value = withTiming(0.8, { duration: 100 }, () => {
|
playButtonScale.value = withTiming(0.8, { duration: 100 }, () => {
|
||||||
if (isComponentMounted) {
|
if (isComponentMounted) {
|
||||||
playButtonScale.value = withTiming(1, { duration: 100 });
|
playButtonScale.value = withTiming(1, { duration: 100 });
|
||||||
|
|
@ -226,7 +226,7 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsPlaying(!isPlaying);
|
setIsPlaying(!isPlaying);
|
||||||
|
|
||||||
showControlsWithTimeout();
|
showControlsWithTimeout();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('TrailerPlayer', 'Error toggling playback:', error);
|
logger.error('TrailerPlayer', 'Error toggling playback:', error);
|
||||||
|
|
@ -236,7 +236,7 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
const handleMuteToggle = useCallback(async () => {
|
const handleMuteToggle = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (!videoRef.current || !isComponentMounted) return;
|
if (!videoRef.current || !isComponentMounted) return;
|
||||||
|
|
||||||
setIsMuted(!isMuted);
|
setIsMuted(!isMuted);
|
||||||
showControlsWithTimeout();
|
showControlsWithTimeout();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -246,28 +246,28 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
|
|
||||||
const handleLoadStart = useCallback(() => {
|
const handleLoadStart = useCallback(() => {
|
||||||
if (!isComponentMounted) return;
|
if (!isComponentMounted) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setHasError(false);
|
setHasError(false);
|
||||||
// Only show loading spinner if not hidden
|
// Only show loading spinner if not hidden
|
||||||
loadingOpacity.value = hideLoadingSpinner ? 0 : 1;
|
loadingOpacity.value = hideLoadingSpinner ? 0 : 1;
|
||||||
onLoadStart?.();
|
onLoadStart?.();
|
||||||
logger.info('TrailerPlayer', 'Video load started');
|
// logger.info('TrailerPlayer', 'Video load started');
|
||||||
}, [loadingOpacity, onLoadStart, hideLoadingSpinner, isComponentMounted]);
|
}, [loadingOpacity, onLoadStart, hideLoadingSpinner, isComponentMounted]);
|
||||||
|
|
||||||
const handleLoad = useCallback((data: OnLoadData) => {
|
const handleLoad = useCallback((data: OnLoadData) => {
|
||||||
if (!isComponentMounted) return;
|
if (!isComponentMounted) return;
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
loadingOpacity.value = withTiming(0, { duration: 300 });
|
loadingOpacity.value = withTiming(0, { duration: 300 });
|
||||||
setDuration(data.duration * 1000); // Convert to milliseconds
|
setDuration(data.duration * 1000); // Convert to milliseconds
|
||||||
onLoad?.();
|
onLoad?.();
|
||||||
logger.info('TrailerPlayer', 'Video loaded successfully');
|
// logger.info('TrailerPlayer', 'Video loaded successfully');
|
||||||
}, [loadingOpacity, onLoad, isComponentMounted]);
|
}, [loadingOpacity, onLoad, isComponentMounted]);
|
||||||
|
|
||||||
const handleError = useCallback((error: any) => {
|
const handleError = useCallback((error: any) => {
|
||||||
if (!isComponentMounted) return;
|
if (!isComponentMounted) return;
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
loadingOpacity.value = withTiming(0, { duration: 300 });
|
loadingOpacity.value = withTiming(0, { duration: 300 });
|
||||||
|
|
@ -278,10 +278,10 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
|
|
||||||
const handleProgress = useCallback((data: OnProgressData) => {
|
const handleProgress = useCallback((data: OnProgressData) => {
|
||||||
if (!isComponentMounted) return;
|
if (!isComponentMounted) return;
|
||||||
|
|
||||||
setPosition(data.currentTime * 1000); // Convert to milliseconds
|
setPosition(data.currentTime * 1000); // Convert to milliseconds
|
||||||
onProgress?.(data);
|
onProgress?.(data);
|
||||||
|
|
||||||
if (onPlaybackStatusUpdate) {
|
if (onPlaybackStatusUpdate) {
|
||||||
onPlaybackStatusUpdate({
|
onPlaybackStatusUpdate({
|
||||||
isLoaded: data.currentTime > 0,
|
isLoaded: data.currentTime > 0,
|
||||||
|
|
@ -304,7 +304,7 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
clearTimeout(hideControlsTimeout.current);
|
clearTimeout(hideControlsTimeout.current);
|
||||||
hideControlsTimeout.current = null;
|
hideControlsTimeout.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset all animated values to prevent memory leaks
|
// Reset all animated values to prevent memory leaks
|
||||||
try {
|
try {
|
||||||
controlsOpacity.value = 0;
|
controlsOpacity.value = 0;
|
||||||
|
|
@ -313,7 +313,7 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('TrailerPlayer', 'Error cleaning up animation values:', error);
|
logger.error('TrailerPlayer', 'Error cleaning up animation values:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure video is stopped
|
// Ensure video is stopped
|
||||||
cleanupVideo();
|
cleanupVideo();
|
||||||
};
|
};
|
||||||
|
|
@ -420,9 +420,9 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Video controls overlay */}
|
{/* Video controls overlay */}
|
||||||
{!hideControls && (
|
{!hideControls && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.videoOverlay}
|
style={styles.videoOverlay}
|
||||||
onPress={handleVideoPress}
|
onPress={handleVideoPress}
|
||||||
activeOpacity={1}
|
activeOpacity={1}
|
||||||
|
|
@ -439,10 +439,10 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
<View style={styles.centerControls}>
|
<View style={styles.centerControls}>
|
||||||
<Animated.View style={playButtonAnimatedStyle}>
|
<Animated.View style={playButtonAnimatedStyle}>
|
||||||
<TouchableOpacity style={styles.playButton} onPress={handlePlayPause}>
|
<TouchableOpacity style={styles.playButton} onPress={handlePlayPause}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={isPlaying ? 'pause' : 'play-arrow'}
|
name={isPlaying ? 'pause' : 'play-arrow'}
|
||||||
size={isTablet ? 64 : 48}
|
size={isTablet ? 64 : 48}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -457,8 +457,8 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<View style={styles.progressContainer}>
|
<View style={styles.progressContainer}>
|
||||||
<View style={styles.progressBar}>
|
<View style={styles.progressBar}>
|
||||||
<View
|
<View
|
||||||
style={[styles.progressFill, { width: `${progressPercentage}%` }]}
|
style={[styles.progressFill, { width: `${progressPercentage}%` }]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -466,27 +466,27 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
|
||||||
{/* Control buttons */}
|
{/* Control buttons */}
|
||||||
<View style={styles.controlButtons}>
|
<View style={styles.controlButtons}>
|
||||||
<TouchableOpacity style={styles.controlButton} onPress={handlePlayPause}>
|
<TouchableOpacity style={styles.controlButton} onPress={handlePlayPause}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={isPlaying ? 'pause' : 'play-arrow'}
|
name={isPlaying ? 'pause' : 'play-arrow'}
|
||||||
size={isTablet ? 32 : 24}
|
size={isTablet ? 32 : 24}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.controlButton} onPress={handleMuteToggle}>
|
<TouchableOpacity style={styles.controlButton} onPress={handleMuteToggle}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={isMuted ? 'volume-off' : 'volume-up'}
|
name={isMuted ? 'volume-off' : 'volume-up'}
|
||||||
size={isTablet ? 32 : 24}
|
size={isTablet ? 32 : 24}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{onFullscreenToggle && (
|
{onFullscreenToggle && (
|
||||||
<TouchableOpacity style={styles.controlButton} onPress={onFullscreenToggle}>
|
<TouchableOpacity style={styles.controlButton} onPress={onFullscreenToggle}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="fullscreen"
|
name="fullscreen"
|
||||||
size={isTablet ? 32 : 24}
|
size={isTablet ? 32 : 24}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ async function getExtensionFromHeaders(url: string, headers?: Record<string, str
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, { method: 'HEAD', headers });
|
const response = await fetch(url, { method: 'HEAD', headers });
|
||||||
const contentType = response.headers.get('content-type');
|
const contentType = response.headers.get('content-type');
|
||||||
|
|
||||||
if (contentType) {
|
if (contentType) {
|
||||||
// Map common content types to extensions
|
// Map common content types to extensions
|
||||||
if (contentType.includes('video/mp4')) return 'mp4';
|
if (contentType.includes('video/mp4')) return 'mp4';
|
||||||
|
|
@ -90,15 +90,15 @@ async function getExtensionFromHeaders(url: string, headers?: Record<string, str
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[DownloadsContext] Could not get content-type from HEAD request', error);
|
console.warn('[DownloadsContext] Could not get content-type from HEAD request', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDownloadableUrl(url: string): boolean {
|
function isDownloadableUrl(url: string): boolean {
|
||||||
if (!url) return false;
|
if (!url) return false;
|
||||||
|
|
||||||
const lower = url.toLowerCase();
|
const lower = url.toLowerCase();
|
||||||
|
|
||||||
// Check for streaming formats that should NOT be downloadable (only m3u8 and DASH)
|
// Check for streaming formats that should NOT be downloadable (only m3u8 and DASH)
|
||||||
const streamingFormats = [
|
const streamingFormats = [
|
||||||
'.m3u8', // HLS streaming
|
'.m3u8', // HLS streaming
|
||||||
|
|
@ -106,15 +106,15 @@ function isDownloadableUrl(url: string): boolean {
|
||||||
'm3u8', // HLS without extension
|
'm3u8', // HLS without extension
|
||||||
'mpd', // DASH without extension
|
'mpd', // DASH without extension
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check if URL contains streaming format indicators
|
// Check if URL contains streaming format indicators
|
||||||
const isStreamingFormat = streamingFormats.some(format =>
|
const isStreamingFormat = streamingFormats.some(format =>
|
||||||
lower.includes(format) ||
|
lower.includes(format) ||
|
||||||
lower.includes(`ext=${format}`) ||
|
lower.includes(`ext=${format}`) ||
|
||||||
lower.includes(`format=${format}`) ||
|
lower.includes(`format=${format}`) ||
|
||||||
lower.includes(`container=${format}`)
|
lower.includes(`container=${format}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Return true if it's NOT a streaming format (m3u8 or DASH)
|
// Return true if it's NOT a streaming format (m3u8 or DASH)
|
||||||
return !isStreamingFormat;
|
return !isStreamingFormat;
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +183,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
});
|
});
|
||||||
setDownloads(restored);
|
setDownloads(restored);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -209,18 +209,18 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
if (d.progress <= prev || d.progress - prev < 2) return; // notify every 2%
|
if (d.progress <= prev || d.progress - prev < 2) return; // notify every 2%
|
||||||
lastNotifyRef.current.set(d.id, d.progress);
|
lastNotifyRef.current.set(d.id, d.progress);
|
||||||
await notificationService.notifyDownloadProgress(d.title, d.progress, d.downloadedBytes, d.totalBytes);
|
await notificationService.notifyDownloadProgress(d.title, d.progress, d.downloadedBytes, d.totalBytes);
|
||||||
} catch {}
|
} catch { }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const notifyCompleted = useCallback(async (d: DownloadItem) => {
|
const notifyCompleted = useCallback(async (d: DownloadItem) => {
|
||||||
try {
|
try {
|
||||||
if (appStateRef.current === 'active') return;
|
if (appStateRef.current === 'active') return;
|
||||||
await notificationService.notifyDownloadComplete(d.title);
|
await notificationService.notifyDownloadComplete(d.title);
|
||||||
} catch {}
|
} catch { }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mmkvStorage.setItem(STORAGE_KEY, JSON.stringify(downloads)).catch(() => {});
|
mmkvStorage.setItem(STORAGE_KEY, JSON.stringify(downloads)).catch(() => { });
|
||||||
}, [downloads]);
|
}, [downloads]);
|
||||||
|
|
||||||
const updateDownload = useCallback((id: string, updater: (d: DownloadItem) => DownloadItem) => {
|
const updateDownload = useCallback((id: string, updater: (d: DownloadItem) => DownloadItem) => {
|
||||||
|
|
@ -247,7 +247,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
// No need to recreate it
|
// No need to recreate it
|
||||||
} else {
|
} else {
|
||||||
console.log(`[DownloadsContext] Creating new resumable for download: ${id}`);
|
console.log(`[DownloadsContext] Creating new resumable for download: ${id}`);
|
||||||
|
|
||||||
// Use the exact same file URI that was used initially
|
// Use the exact same file URI that was used initially
|
||||||
const fileUri = item.fileUri;
|
const fileUri = item.fileUri;
|
||||||
if (!fileUri) {
|
if (!fileUri) {
|
||||||
|
|
@ -322,13 +322,13 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
if (fileInfo.size === 0) {
|
if (fileInfo.size === 0) {
|
||||||
throw new Error('Downloaded file is empty (0 bytes)');
|
throw new Error('Downloaded file is empty (0 bytes)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL FIX: Check if file size matches expected size (if known)
|
// CRITICAL FIX: Check if file size matches expected size (if known)
|
||||||
const currentItem = downloadsRef.current.find(d => d.id === id);
|
const currentItem = downloadsRef.current.find(d => d.id === id);
|
||||||
if (currentItem && currentItem.totalBytes > 0) {
|
if (currentItem && currentItem.totalBytes > 0) {
|
||||||
const sizeDifference = Math.abs(fileInfo.size - currentItem.totalBytes);
|
const sizeDifference = Math.abs(fileInfo.size - currentItem.totalBytes);
|
||||||
const percentDifference = (sizeDifference / currentItem.totalBytes) * 100;
|
const percentDifference = (sizeDifference / currentItem.totalBytes) * 100;
|
||||||
|
|
||||||
// Allow up to 1% difference to account for potential header/metadata variations
|
// Allow up to 1% difference to account for potential header/metadata variations
|
||||||
if (percentDifference > 1) {
|
if (percentDifference > 1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -336,7 +336,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DownloadsContext] File validation passed: ${result.uri} (${fileInfo.size} bytes)`);
|
console.log(`[DownloadsContext] File validation passed: ${result.uri} (${fileInfo.size} bytes)`);
|
||||||
} catch (validationError) {
|
} catch (validationError) {
|
||||||
console.error(`[DownloadsContext] File validation failed: ${validationError}`);
|
console.error(`[DownloadsContext] File validation failed: ${validationError}`);
|
||||||
|
|
@ -373,15 +373,15 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
|
|
||||||
// Only mark as error and clean up if it's a real error (not pause-related)
|
// Only mark as error and clean up if it's a real error (not pause-related)
|
||||||
console.log(`[DownloadsContext] Marking download as error: ${id}`);
|
console.log(`[DownloadsContext] Marking download as error: ${id}`);
|
||||||
|
|
||||||
// For validation errors, clear resumeData and allow fresh restart
|
// For validation errors, clear resumeData and allow fresh restart
|
||||||
if (e.message && e.message.includes('validation failed')) {
|
if (e.message && e.message.includes('validation failed')) {
|
||||||
console.log(`[DownloadsContext] Validation error - clearing resume data for fresh start: ${id}`);
|
console.log(`[DownloadsContext] Validation error - clearing resume data for fresh start: ${id}`);
|
||||||
updateDownload(id, (d) => ({
|
updateDownload(id, (d) => ({
|
||||||
...d,
|
...d,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
resumeData: undefined, // Clear corrupted resume data
|
resumeData: undefined, // Clear corrupted resume data
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
}));
|
}));
|
||||||
// Clean up resumable to force fresh download on retry
|
// Clean up resumable to force fresh download on retry
|
||||||
resumablesRef.current.delete(id);
|
resumablesRef.current.delete(id);
|
||||||
|
|
@ -389,13 +389,13 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
} else if (e.message && (e.message.includes('size mismatch') || e.message.includes('empty'))) {
|
} else if (e.message && (e.message.includes('size mismatch') || e.message.includes('empty'))) {
|
||||||
// File corruption detected - clear everything for fresh start
|
// File corruption detected - clear everything for fresh start
|
||||||
console.log(`[DownloadsContext] File corruption detected - clearing for fresh start: ${id}`);
|
console.log(`[DownloadsContext] File corruption detected - clearing for fresh start: ${id}`);
|
||||||
updateDownload(id, (d) => ({
|
updateDownload(id, (d) => ({
|
||||||
...d,
|
...d,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
downloadedBytes: 0,
|
downloadedBytes: 0,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
resumeData: undefined, // Clear corrupted resume data
|
resumeData: undefined, // Clear corrupted resume data
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
}));
|
}));
|
||||||
resumablesRef.current.delete(id);
|
resumablesRef.current.delete(id);
|
||||||
lastBytesRef.current.delete(id);
|
lastBytesRef.current.delete(id);
|
||||||
|
|
@ -443,7 +443,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
const fileUri = extension ? `${baseDir}downloads/${uniqueId}.${extension}` : `${baseDir}downloads/${uniqueId}`;
|
const fileUri = extension ? `${baseDir}downloads/${uniqueId}.${extension}` : `${baseDir}downloads/${uniqueId}`;
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
await FileSystem.makeDirectoryAsync(`${baseDir}downloads`, { intermediates: true }).catch(() => {});
|
await FileSystem.makeDirectoryAsync(`${baseDir}downloads`, { intermediates: true }).catch(() => { });
|
||||||
|
|
||||||
const createdAt = Date.now();
|
const createdAt = Date.now();
|
||||||
const newItem: DownloadItem = {
|
const newItem: DownloadItem = {
|
||||||
|
|
@ -515,9 +515,9 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
resumablesRef.current.set(compoundId, resumable);
|
resumablesRef.current.set(compoundId, resumable);
|
||||||
lastBytesRef.current.set(compoundId, { bytes: 0, time: Date.now() });
|
lastBytesRef.current.set(compoundId, { bytes: 0, time: Date.now() });
|
||||||
|
|
||||||
try {
|
// Start download in background (non-blocking) to allow UI success alert
|
||||||
const result = await resumable.downloadAsync();
|
resumable.downloadAsync().then(async (result) => {
|
||||||
|
|
||||||
// Check if download was paused during download
|
// Check if download was paused during download
|
||||||
const currentItem = downloadsRef.current.find(d => d.id === compoundId);
|
const currentItem = downloadsRef.current.find(d => d.id === compoundId);
|
||||||
if (currentItem && currentItem.status === 'paused') {
|
if (currentItem && currentItem.status === 'paused') {
|
||||||
|
|
@ -536,7 +536,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
// Don't delete resumable - keep it for resume
|
// Don't delete resumable - keep it for resume
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result) throw new Error('Download failed');
|
if (!result) throw new Error('Download failed');
|
||||||
|
|
||||||
// Validate the downloaded file
|
// Validate the downloaded file
|
||||||
|
|
@ -548,13 +548,13 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
if (fileInfo.size === 0) {
|
if (fileInfo.size === 0) {
|
||||||
throw new Error('Downloaded file is empty (0 bytes)');
|
throw new Error('Downloaded file is empty (0 bytes)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL FIX: Check if file size matches expected size (if known)
|
// CRITICAL FIX: Check if file size matches expected size (if known)
|
||||||
const currentItem = downloadsRef.current.find(d => d.id === compoundId);
|
const currentItem = downloadsRef.current.find(d => d.id === compoundId);
|
||||||
if (currentItem && currentItem.totalBytes > 0) {
|
if (currentItem && currentItem.totalBytes > 0) {
|
||||||
const sizeDifference = Math.abs(fileInfo.size - currentItem.totalBytes);
|
const sizeDifference = Math.abs(fileInfo.size - currentItem.totalBytes);
|
||||||
const percentDifference = (sizeDifference / currentItem.totalBytes) * 100;
|
const percentDifference = (sizeDifference / currentItem.totalBytes) * 100;
|
||||||
|
|
||||||
// Allow up to 1% difference to account for potential header/metadata variations
|
// Allow up to 1% difference to account for potential header/metadata variations
|
||||||
if (percentDifference > 1) {
|
if (percentDifference > 1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -562,7 +562,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DownloadsContext] File validation passed: ${result.uri} (${fileInfo.size} bytes)`);
|
console.log(`[DownloadsContext] File validation passed: ${result.uri} (${fileInfo.size} bytes)`);
|
||||||
} catch (validationError) {
|
} catch (validationError) {
|
||||||
console.error(`[DownloadsContext] File validation failed: ${validationError}`);
|
console.error(`[DownloadsContext] File validation failed: ${validationError}`);
|
||||||
|
|
@ -581,7 +581,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
if (done) notifyCompleted({ ...done, status: 'completed', progress: 100, fileUri: result.uri } as DownloadItem);
|
if (done) notifyCompleted({ ...done, status: 'completed', progress: 100, fileUri: result.uri } as DownloadItem);
|
||||||
resumablesRef.current.delete(compoundId);
|
resumablesRef.current.delete(compoundId);
|
||||||
lastBytesRef.current.delete(compoundId);
|
lastBytesRef.current.delete(compoundId);
|
||||||
} catch (e: any) {
|
}).catch(async (e: any) => {
|
||||||
// If user paused, keep paused state, else error
|
// If user paused, keep paused state, else error
|
||||||
const current = downloadsRef.current.find(d => d.id === compoundId);
|
const current = downloadsRef.current.find(d => d.id === compoundId);
|
||||||
if (current && current.status === 'paused') {
|
if (current && current.status === 'paused') {
|
||||||
|
|
@ -602,15 +602,15 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DownloadsContext] Marking initial download as error: ${compoundId}`);
|
console.log(`[DownloadsContext] Marking initial download as error: ${compoundId}`);
|
||||||
|
|
||||||
// For validation errors, clear resumeData and allow fresh restart
|
// For validation errors, clear resumeData and allow fresh restart
|
||||||
if (e.message && e.message.includes('validation failed')) {
|
if (e.message && e.message.includes('validation failed')) {
|
||||||
console.log(`[DownloadsContext] Validation error - clearing resume data for fresh start: ${compoundId}`);
|
console.log(`[DownloadsContext] Validation error - clearing resume data for fresh start: ${compoundId}`);
|
||||||
updateDownload(compoundId, (d) => ({
|
updateDownload(compoundId, (d) => ({
|
||||||
...d,
|
...d,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
resumeData: undefined, // Clear corrupted resume data
|
resumeData: undefined, // Clear corrupted resume data
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
}));
|
}));
|
||||||
// Clean up resumable to force fresh download on retry
|
// Clean up resumable to force fresh download on retry
|
||||||
resumablesRef.current.delete(compoundId);
|
resumablesRef.current.delete(compoundId);
|
||||||
|
|
@ -618,13 +618,13 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
} else if (e.message && (e.message.includes('size mismatch') || e.message.includes('empty'))) {
|
} else if (e.message && (e.message.includes('size mismatch') || e.message.includes('empty'))) {
|
||||||
// File corruption detected - clear everything for fresh start
|
// File corruption detected - clear everything for fresh start
|
||||||
console.log(`[DownloadsContext] File corruption detected - clearing for fresh start: ${compoundId}`);
|
console.log(`[DownloadsContext] File corruption detected - clearing for fresh start: ${compoundId}`);
|
||||||
updateDownload(compoundId, (d) => ({
|
updateDownload(compoundId, (d) => ({
|
||||||
...d,
|
...d,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
downloadedBytes: 0,
|
downloadedBytes: 0,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
resumeData: undefined, // Clear corrupted resume data
|
resumeData: undefined, // Clear corrupted resume data
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
}));
|
}));
|
||||||
resumablesRef.current.delete(compoundId);
|
resumablesRef.current.delete(compoundId);
|
||||||
lastBytesRef.current.delete(compoundId);
|
lastBytesRef.current.delete(compoundId);
|
||||||
|
|
@ -634,27 +634,27 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
updateDownload(compoundId, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
updateDownload(compoundId, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
||||||
// Keep resumable for potential retry
|
// Keep resumable for potential retry
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}, [updateDownload, resumeDownload]);
|
}, [updateDownload, resumeDownload]);
|
||||||
|
|
||||||
const pauseDownload = useCallback(async (id: string) => {
|
const pauseDownload = useCallback(async (id: string) => {
|
||||||
console.log(`[DownloadsContext] Pausing download: ${id}`);
|
console.log(`[DownloadsContext] Pausing download: ${id}`);
|
||||||
|
|
||||||
// First, update the status to 'paused' immediately
|
// First, update the status to 'paused' immediately
|
||||||
// This will cause any ongoing download/resume operations to check status and exit gracefully
|
// This will cause any ongoing download/resume operations to check status and exit gracefully
|
||||||
updateDownload(id, (d) => ({ ...d, status: 'paused', updatedAt: Date.now() }));
|
updateDownload(id, (d) => ({ ...d, status: 'paused', updatedAt: Date.now() }));
|
||||||
|
|
||||||
const resumable = resumablesRef.current.get(id);
|
const resumable = resumablesRef.current.get(id);
|
||||||
if (resumable) {
|
if (resumable) {
|
||||||
try {
|
try {
|
||||||
// CRITICAL FIX: Get the pause state which contains resumeData
|
// CRITICAL FIX: Get the pause state which contains resumeData
|
||||||
const pauseResult = await resumable.pauseAsync();
|
const pauseResult = await resumable.pauseAsync();
|
||||||
console.log(`[DownloadsContext] Successfully paused download: ${id}`);
|
console.log(`[DownloadsContext] Successfully paused download: ${id}`);
|
||||||
|
|
||||||
// CRITICAL FIX: Save the resumeData from pauseAsync result or savable()
|
// CRITICAL FIX: Save the resumeData from pauseAsync result or savable()
|
||||||
// The pauseAsync returns a DownloadPauseState object with resumeData
|
// The pauseAsync returns a DownloadPauseState object with resumeData
|
||||||
const savableState = resumable.savable();
|
const savableState = resumable.savable();
|
||||||
|
|
||||||
// Update the download item with the critical resumeData for future resume
|
// Update the download item with the critical resumeData for future resume
|
||||||
updateDownload(id, (d) => ({
|
updateDownload(id, (d) => ({
|
||||||
...d,
|
...d,
|
||||||
|
|
@ -662,9 +662,9 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
resumeData: savableState.resumeData || pauseResult.resumeData, // Store resume data
|
resumeData: savableState.resumeData || pauseResult.resumeData, // Store resume data
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(`[DownloadsContext] Saved resume data for download: ${id}`);
|
console.log(`[DownloadsContext] Saved resume data for download: ${id}`);
|
||||||
|
|
||||||
// Keep the resumable in memory for resume - DO NOT DELETE
|
// Keep the resumable in memory for resume - DO NOT DELETE
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`[DownloadsContext] Pause async failed (this is normal if already paused): ${id}`, error);
|
console.log(`[DownloadsContext] Pause async failed (this is normal if already paused): ${id}`, error);
|
||||||
|
|
@ -691,7 +691,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
const resumable = resumablesRef.current.get(id);
|
const resumable = resumablesRef.current.get(id);
|
||||||
try {
|
try {
|
||||||
if (resumable) {
|
if (resumable) {
|
||||||
try { await resumable.pauseAsync(); } catch {}
|
try { await resumable.pauseAsync(); } catch { }
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
resumablesRef.current.delete(id);
|
resumablesRef.current.delete(id);
|
||||||
|
|
@ -700,7 +700,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
|
|
||||||
const item = downloadsRef.current.find(d => d.id === id);
|
const item = downloadsRef.current.find(d => d.id === id);
|
||||||
if (item?.fileUri) {
|
if (item?.fileUri) {
|
||||||
await FileSystem.deleteAsync(item.fileUri, { idempotent: true }).catch(() => {});
|
await FileSystem.deleteAsync(item.fileUri, { idempotent: true }).catch(() => { });
|
||||||
}
|
}
|
||||||
setDownloads(prev => prev.filter(d => d.id !== id));
|
setDownloads(prev => prev.filter(d => d.id !== id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -708,7 +708,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
const removeDownload = useCallback(async (id: string) => {
|
const removeDownload = useCallback(async (id: string) => {
|
||||||
const item = downloadsRef.current.find(d => d.id === id);
|
const item = downloadsRef.current.find(d => d.id === id);
|
||||||
if (item?.fileUri && item.status === 'completed') {
|
if (item?.fileUri && item.status === 'completed') {
|
||||||
await FileSystem.deleteAsync(item.fileUri, { idempotent: true }).catch(() => {});
|
await FileSystem.deleteAsync(item.fileUri, { idempotent: true }).catch(() => { });
|
||||||
}
|
}
|
||||||
setDownloads(prev => prev.filter(d => d.id !== id));
|
setDownloads(prev => prev.filter(d => d.id !== id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export const useTraktComments = ({
|
||||||
const traktService = TraktService.getInstance();
|
const traktService = TraktService.getInstance();
|
||||||
let fetchedComments: TraktContentComment[] = [];
|
let fetchedComments: TraktContentComment[] = [];
|
||||||
|
|
||||||
console.log(`[useTraktComments] Loading comments for ${type} - IMDb: ${imdbId}, TMDB: ${tmdbId}, page: ${pageNum}`);
|
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'movie':
|
case 'movie':
|
||||||
|
|
@ -87,10 +87,10 @@ export const useTraktComments = ({
|
||||||
setComments(prevComments => {
|
setComments(prevComments => {
|
||||||
if (append) {
|
if (append) {
|
||||||
const newComments = [...prevComments, ...fetchedComments];
|
const newComments = [...prevComments, ...fetchedComments];
|
||||||
console.log(`[useTraktComments] Appended ${fetchedComments.length} comments, total: ${newComments.length}`);
|
|
||||||
return newComments;
|
return newComments;
|
||||||
} else {
|
} else {
|
||||||
console.log(`[useTraktComments] Loaded ${fetchedComments.length} comments`);
|
|
||||||
return fetchedComments;
|
return fetchedComments;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,10 @@ export class TrailerService {
|
||||||
// Try local server first, fallback to XPrime if it fails
|
// Try local server first, fallback to XPrime if it fails
|
||||||
const localResult = await this.getTrailerFromLocalServer(title, year, tmdbId, type);
|
const localResult = await this.getTrailerFromLocalServer(title, year, tmdbId, type);
|
||||||
if (localResult) {
|
if (localResult) {
|
||||||
logger.info('TrailerService', 'Returning trailer URL from local server');
|
// logger.info('TrailerService', 'Returning trailer URL from local server');
|
||||||
return localResult;
|
return localResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('TrailerService', `Local server failed, falling back to XPrime for: ${title} (${year})`);
|
logger.info('TrailerService', `Local server failed, falling back to XPrime for: ${title} (${year})`);
|
||||||
return this.getTrailerFromXPrime(title, year);
|
return this.getTrailerFromXPrime(title, year);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -59,11 +59,11 @@ export class TrailerService {
|
||||||
|
|
||||||
// Build URL with parameters
|
// Build URL with parameters
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
// Always send title and year for logging and fallback
|
// Always send title and year for logging and fallback
|
||||||
params.append('title', title);
|
params.append('title', title);
|
||||||
params.append('year', year.toString());
|
params.append('year', year.toString());
|
||||||
|
|
||||||
if (tmdbId) {
|
if (tmdbId) {
|
||||||
params.append('tmdbId', tmdbId);
|
params.append('tmdbId', tmdbId);
|
||||||
params.append('type', type || 'movie');
|
params.append('type', type || 'movie');
|
||||||
|
|
@ -76,9 +76,9 @@ export class TrailerService {
|
||||||
logger.info('TrailerService', `Local server request URL: ${url}`);
|
logger.info('TrailerService', `Local server request URL: ${url}`);
|
||||||
logger.info('TrailerService', `Local server timeout set to ${this.TIMEOUT}ms`);
|
logger.info('TrailerService', `Local server timeout set to ${this.TIMEOUT}ms`);
|
||||||
logger.info('TrailerService', `Making fetch request to: ${url}`);
|
logger.info('TrailerService', `Making fetch request to: ${url}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -87,24 +87,26 @@ export class TrailerService {
|
||||||
},
|
},
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info('TrailerService', `Fetch request completed. Response status: ${response.status}`);
|
// logger.info('TrailerService', `Fetch request completed. Response status: ${response.status}`);
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
const elapsed = Date.now() - startTime;
|
||||||
const contentType = response.headers.get('content-type') || 'unknown';
|
const contentType = response.headers.get('content-type') || 'unknown';
|
||||||
logger.info('TrailerService', `Local server response: status=${response.status} ok=${response.ok} content-type=${contentType} elapsedMs=${elapsed}`);
|
// logger.info('TrailerService', `Local server response: status=${response.status} ok=${response.ok} content-type=${contentType} elapsedMs=${elapsed}`);
|
||||||
|
|
||||||
// Read body as text first so we can log it even on non-200s
|
// Read body as text first so we can log it even on non-200s
|
||||||
let rawText = '';
|
let rawText = '';
|
||||||
try {
|
try {
|
||||||
rawText = await response.text();
|
rawText = await response.text();
|
||||||
if (rawText) {
|
if (rawText) {
|
||||||
|
/*
|
||||||
const preview = rawText.length > 200 ? `${rawText.slice(0, 200)}...` : rawText;
|
const preview = rawText.length > 200 ? `${rawText.slice(0, 200)}...` : rawText;
|
||||||
logger.info('TrailerService', `Local server body preview: ${preview}`);
|
logger.info('TrailerService', `Local server body preview: ${preview}`);
|
||||||
|
*/
|
||||||
} else {
|
} else {
|
||||||
logger.info('TrailerService', 'Local server body is empty');
|
// logger.info('TrailerService', 'Local server body is empty');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
const msg = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
||||||
|
|
@ -120,20 +122,20 @@ export class TrailerService {
|
||||||
let data: any = null;
|
let data: any = null;
|
||||||
try {
|
try {
|
||||||
data = rawText ? JSON.parse(rawText) : null;
|
data = rawText ? JSON.parse(rawText) : null;
|
||||||
const keys = typeof data === 'object' && data !== null ? Object.keys(data).join(',') : typeof data;
|
// const keys = typeof data === 'object' && data !== null ? Object.keys(data).join(',') : typeof data;
|
||||||
logger.info('TrailerService', `Local server JSON parsed. Keys/Type: ${keys}`);
|
// logger.info('TrailerService', `Local server JSON parsed. Keys/Type: ${keys}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
const msg = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
||||||
logger.warn('TrailerService', `Failed to parse local server JSON: ${msg}`);
|
logger.warn('TrailerService', `Failed to parse local server JSON: ${msg}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.url || !this.isValidTrailerUrl(data.url)) {
|
if (!data.url || !this.isValidTrailerUrl(data.url)) {
|
||||||
logger.warn('TrailerService', `Invalid trailer URL from auto-search: ${data.url}`);
|
logger.warn('TrailerService', `Invalid trailer URL from auto-search: ${data.url}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('TrailerService', `Successfully found trailer: ${String(data.url).substring(0, 80)}...`);
|
// logger.info('TrailerService', `Successfully found trailer: ${String(data.url).substring(0, 80)}...`);
|
||||||
return data.url;
|
return data.url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
|
@ -164,11 +166,11 @@ export class TrailerService {
|
||||||
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT);
|
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT);
|
||||||
|
|
||||||
const url = `${this.XPRIME_URL}?title=${encodeURIComponent(title)}&year=${year}`;
|
const url = `${this.XPRIME_URL}?title=${encodeURIComponent(title)}&year=${year}`;
|
||||||
|
|
||||||
logger.info('TrailerService', `Fetching trailer from XPrime for: ${title} (${year})`);
|
logger.info('TrailerService', `Fetching trailer from XPrime for: ${title} (${year})`);
|
||||||
logger.info('TrailerService', `XPrime request URL: ${url}`);
|
logger.info('TrailerService', `XPrime request URL: ${url}`);
|
||||||
logger.info('TrailerService', `XPrime timeout set to ${this.TIMEOUT}ms`);
|
logger.info('TrailerService', `XPrime timeout set to ${this.TIMEOUT}ms`);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -188,7 +190,7 @@ export class TrailerService {
|
||||||
|
|
||||||
const trailerUrl = await response.text();
|
const trailerUrl = await response.text();
|
||||||
logger.info('TrailerService', `XPrime raw URL length: ${trailerUrl ? trailerUrl.length : 0}`);
|
logger.info('TrailerService', `XPrime raw URL length: ${trailerUrl ? trailerUrl.length : 0}`);
|
||||||
|
|
||||||
if (!trailerUrl || !this.isValidTrailerUrl(trailerUrl.trim())) {
|
if (!trailerUrl || !this.isValidTrailerUrl(trailerUrl.trim())) {
|
||||||
logger.warn('TrailerService', `Invalid trailer URL from XPrime: ${trailerUrl}`);
|
logger.warn('TrailerService', `Invalid trailer URL from XPrime: ${trailerUrl}`);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -196,7 +198,7 @@ export class TrailerService {
|
||||||
|
|
||||||
const cleanUrl = trailerUrl.trim();
|
const cleanUrl = trailerUrl.trim();
|
||||||
logger.info('TrailerService', `Successfully fetched trailer from XPrime: ${cleanUrl}`);
|
logger.info('TrailerService', `Successfully fetched trailer from XPrime: ${cleanUrl}`);
|
||||||
|
|
||||||
return cleanUrl;
|
return cleanUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
|
@ -218,7 +220,7 @@ export class TrailerService {
|
||||||
private static isValidTrailerUrl(url: string): boolean {
|
private static isValidTrailerUrl(url: string): boolean {
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
|
|
||||||
// Check if it's a valid HTTP/HTTPS URL
|
// Check if it's a valid HTTP/HTTPS URL
|
||||||
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -242,19 +244,19 @@ export class TrailerService {
|
||||||
];
|
];
|
||||||
|
|
||||||
const hostname = urlObj.hostname.toLowerCase();
|
const hostname = urlObj.hostname.toLowerCase();
|
||||||
const isValidDomain = validDomains.some(domain =>
|
const isValidDomain = validDomains.some(domain =>
|
||||||
hostname.includes(domain) || hostname.endsWith(domain)
|
hostname.includes(domain) || hostname.endsWith(domain)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Special check for Google Video CDN (YouTube direct streaming URLs)
|
// Special check for Google Video CDN (YouTube direct streaming URLs)
|
||||||
const isGoogleVideoCDN = hostname.includes('googlevideo.com') ||
|
const isGoogleVideoCDN = hostname.includes('googlevideo.com') ||
|
||||||
hostname.includes('sn-') && hostname.includes('.googlevideo.com');
|
hostname.includes('sn-') && hostname.includes('.googlevideo.com');
|
||||||
|
|
||||||
// Check for video file extensions or streaming formats
|
// Check for video file extensions or streaming formats
|
||||||
const hasVideoFormat = /\.(mp4|m3u8|mpd|webm|mov|avi|mkv)$/i.test(urlObj.pathname) ||
|
const hasVideoFormat = /\.(mp4|m3u8|mpd|webm|mov|avi|mkv)$/i.test(urlObj.pathname) ||
|
||||||
url.includes('formats=') ||
|
url.includes('formats=') ||
|
||||||
url.includes('manifest') ||
|
url.includes('manifest') ||
|
||||||
url.includes('playlist');
|
url.includes('playlist');
|
||||||
|
|
||||||
return isValidDomain || hasVideoFormat || isGoogleVideoCDN;
|
return isValidDomain || hasVideoFormat || isGoogleVideoCDN;
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -286,9 +288,9 @@ export class TrailerService {
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the original URL if no format optimization is needed
|
// Return the original URL if no format optimization is needed
|
||||||
logger.info('TrailerService', 'No format optimization applied');
|
// logger.info('TrailerService', 'No format optimization applied');
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,7 +316,7 @@ export class TrailerService {
|
||||||
static async getTrailerData(title: string, year: number): Promise<TrailerData | null> {
|
static async getTrailerData(title: string, year: number): Promise<TrailerData | null> {
|
||||||
logger.info('TrailerService', `getTrailerData for: ${title} (${year})`);
|
logger.info('TrailerService', `getTrailerData for: ${title} (${year})`);
|
||||||
const url = await this.getTrailerUrl(title, year);
|
const url = await this.getTrailerUrl(title, year);
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
logger.info('TrailerService', 'No trailer URL found for getTrailerData');
|
logger.info('TrailerService', 'No trailer URL found for getTrailerData');
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -433,9 +435,9 @@ export class TrailerService {
|
||||||
signal: AbortSignal.timeout(5000) // 5 second timeout
|
signal: AbortSignal.timeout(5000) // 5 second timeout
|
||||||
});
|
});
|
||||||
if (response.ok || response.status === 404) { // 404 is ok, means server is running
|
if (response.ok || response.status === 404) { // 404 is ok, means server is running
|
||||||
results.localServer = {
|
results.localServer = {
|
||||||
status: 'online',
|
status: 'online',
|
||||||
responseTime: Date.now() - startTime
|
responseTime: Date.now() - startTime
|
||||||
};
|
};
|
||||||
logger.info('TrailerService', `Local server online. Response time: ${results.localServer.responseTime}ms`);
|
logger.info('TrailerService', `Local server online. Response time: ${results.localServer.responseTime}ms`);
|
||||||
}
|
}
|
||||||
|
|
@ -452,9 +454,9 @@ export class TrailerService {
|
||||||
signal: AbortSignal.timeout(5000) // 5 second timeout
|
signal: AbortSignal.timeout(5000) // 5 second timeout
|
||||||
});
|
});
|
||||||
if (response.ok || response.status === 404) { // 404 is ok, means server is running
|
if (response.ok || response.status === 404) { // 404 is ok, means server is running
|
||||||
results.xprimeServer = {
|
results.xprimeServer = {
|
||||||
status: 'online',
|
status: 'online',
|
||||||
responseTime: Date.now() - startTime
|
responseTime: Date.now() - startTime
|
||||||
};
|
};
|
||||||
logger.info('TrailerService', `XPrime server online. Response time: ${results.xprimeServer.responseTime}ms`);
|
logger.info('TrailerService', `XPrime server online. Response time: ${results.xprimeServer.responseTime}ms`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1463,7 +1463,7 @@ export class TraktService {
|
||||||
if (matchingResult) {
|
if (matchingResult) {
|
||||||
const traktId = matchingResult[type]?.ids?.trakt;
|
const traktId = matchingResult[type]?.ids?.trakt;
|
||||||
if (traktId) {
|
if (traktId) {
|
||||||
logger.log(`[TraktService] Found Trakt ID: ${traktId} for IMDb ID: ${fullImdbId}`);
|
// logger.log(`[TraktService] Found Trakt ID: ${traktId} for IMDb ID: ${fullImdbId}`);
|
||||||
return traktId;
|
return traktId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1471,7 +1471,7 @@ export class TraktService {
|
||||||
// Fallback: try the first result if type filtering didn't work
|
// Fallback: try the first result if type filtering didn't work
|
||||||
const traktId = data[0][type]?.ids?.trakt;
|
const traktId = data[0][type]?.ids?.trakt;
|
||||||
if (traktId) {
|
if (traktId) {
|
||||||
logger.log(`[TraktService] Found Trakt ID (fallback): ${traktId} for IMDb ID: ${fullImdbId}`);
|
// logger.log(`[TraktService] Found Trakt ID (fallback): ${traktId} for IMDb ID: ${fullImdbId}`);
|
||||||
return traktId;
|
return traktId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2860,7 +2860,7 @@ export class TraktService {
|
||||||
if (data && data.length > 0) {
|
if (data && data.length > 0) {
|
||||||
const traktId = data[0][type === 'show' ? 'show' : type]?.ids?.trakt;
|
const traktId = data[0][type === 'show' ? 'show' : type]?.ids?.trakt;
|
||||||
if (traktId) {
|
if (traktId) {
|
||||||
logger.log(`[TraktService] Found Trakt ID via TMDB: ${traktId} for TMDB ID: ${tmdbId}`);
|
// logger.log(`[TraktService] Found Trakt ID via TMDB: ${traktId} for TMDB ID: ${tmdbId}`);
|
||||||
return traktId;
|
return traktId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2893,7 +2893,7 @@ export class TraktService {
|
||||||
|
|
||||||
const endpoint = `/movies/${traktId}/comments?page=${page}&limit=${limit}`;
|
const endpoint = `/movies/${traktId}/comments?page=${page}&limit=${limit}`;
|
||||||
const result = await this.apiRequest<TraktContentComment[]>(endpoint, 'GET');
|
const result = await this.apiRequest<TraktContentComment[]>(endpoint, 'GET');
|
||||||
console.log(`[TraktService] Movie comments response:`, result);
|
// console.log(`[TraktService] Movie comments response:`, result);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[TraktService] Failed to get movie comments:', error);
|
logger.error('[TraktService] Failed to get movie comments:', error);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue