mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
trailer ininital addition
This commit is contained in:
parent
5b71ccc56c
commit
face30c163
3 changed files with 587 additions and 4 deletions
|
|
@ -29,6 +29,8 @@ import { useTheme } from '../../contexts/ThemeContext';
|
|||
import { useTraktContext } from '../../contexts/TraktContext';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { TMDBService } from '../../services/tmdbService';
|
||||
import TrailerService from '../../services/trailerService';
|
||||
import TrailerPlayer from '../video/TrailerPlayer';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
|
@ -693,6 +695,11 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
// Image loading state with optimized management
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [trailerUrl, setTrailerUrl] = useState<string | null>(null);
|
||||
const [trailerLoading, setTrailerLoading] = useState(false);
|
||||
const [trailerError, setTrailerError] = useState(false);
|
||||
const [trailerMuted, setTrailerMuted] = useState(true);
|
||||
const [trailerPlaying, setTrailerPlaying] = useState(false);
|
||||
const imageOpacity = useSharedValue(1);
|
||||
const imageLoadOpacity = useSharedValue(0);
|
||||
const shimmerOpacity = useSharedValue(0.3);
|
||||
|
|
@ -722,6 +729,33 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
return () => timer.cancel();
|
||||
}, []);
|
||||
|
||||
// Fetch trailer URL when component mounts
|
||||
useEffect(() => {
|
||||
const fetchTrailer = async () => {
|
||||
if (!metadata?.name || !metadata?.year) return;
|
||||
|
||||
setTrailerLoading(true);
|
||||
setTrailerError(false);
|
||||
|
||||
try {
|
||||
const url = await TrailerService.getTrailerUrl(metadata.name, metadata.year);
|
||||
if (url) {
|
||||
setTrailerUrl(TrailerService.getBestFormatUrl(url));
|
||||
logger.info('HeroSection', `Trailer loaded for ${metadata.name}`);
|
||||
} else {
|
||||
logger.info('HeroSection', `No trailer found for ${metadata.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('HeroSection', 'Error fetching trailer:', error);
|
||||
setTrailerError(true);
|
||||
} finally {
|
||||
setTrailerLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTrailer();
|
||||
}, [metadata?.name, metadata?.year]);
|
||||
|
||||
// Optimized shimmer animation for loading state
|
||||
useEffect(() => {
|
||||
if (!shouldLoadSecondaryData) return;
|
||||
|
|
@ -903,7 +937,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
<View style={[styles.absoluteFill, { backgroundColor: themeColors.black }]} />
|
||||
|
||||
{/* Optimized shimmer loading effect */}
|
||||
{shouldLoadSecondaryData && ((imageSource && !imageLoaded) || loadingBanner) && (
|
||||
{shouldLoadSecondaryData && (trailerLoading || ((imageSource && !imageLoaded) || loadingBanner)) && (
|
||||
<Animated.View style={[styles.absoluteFill, {
|
||||
opacity: shimmerOpacity,
|
||||
}]}>
|
||||
|
|
@ -916,8 +950,24 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
</Animated.View>
|
||||
)}
|
||||
|
||||
{/* Optimized background image with lazy loading */}
|
||||
{shouldLoadSecondaryData && imageSource && !loadingBanner && (
|
||||
{/* Trailer player or background image */}
|
||||
{shouldLoadSecondaryData && trailerUrl && !trailerLoading && !trailerError ? (
|
||||
<TrailerPlayer
|
||||
trailerUrl={trailerUrl}
|
||||
autoPlay={true}
|
||||
muted={trailerMuted}
|
||||
style={styles.absoluteFill}
|
||||
onError={() => {
|
||||
logger.warn('HeroSection', 'Trailer playback failed, falling back to image');
|
||||
setTrailerError(true);
|
||||
}}
|
||||
onProgress={() => {
|
||||
if (!trailerPlaying) {
|
||||
setTrailerPlaying(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : shouldLoadSecondaryData && imageSource && !loadingBanner ? (
|
||||
<Animated.Image
|
||||
source={{ uri: imageSource }}
|
||||
style={[styles.absoluteFill, backdropImageStyle]}
|
||||
|
|
@ -925,7 +975,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
onError={handleImageError}
|
||||
onLoad={handleImageLoad}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<Animated.View style={styles.backButtonContainer}>
|
||||
<TouchableOpacity style={styles.backButton} onPress={handleBack}>
|
||||
|
|
|
|||
367
src/components/video/TrailerPlayer.tsx
Normal file
367
src/components/video/TrailerPlayer.tsx
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
import React, { useState, useEffect, useRef, useCallback, memo } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
Platform,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Video, { VideoRef, OnLoadData, OnProgressData } from 'react-native-video';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
withDelay,
|
||||
runOnJS,
|
||||
} from 'react-native-reanimated';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
||||
interface TrailerPlayerProps {
|
||||
trailerUrl: string;
|
||||
autoPlay?: boolean;
|
||||
muted?: boolean;
|
||||
onLoadStart?: () => void;
|
||||
onLoad?: () => void;
|
||||
onError?: (error: string) => void;
|
||||
onProgress?: (data: OnProgressData) => void;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
const TrailerPlayer: React.FC<TrailerPlayerProps> = memo(({
|
||||
trailerUrl,
|
||||
autoPlay = true,
|
||||
muted = true,
|
||||
onLoadStart,
|
||||
onLoad,
|
||||
onError,
|
||||
onProgress,
|
||||
style,
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isPlaying, setIsPlaying] = useState(autoPlay);
|
||||
const [isMuted, setIsMuted] = useState(muted);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [position, setPosition] = useState(0);
|
||||
|
||||
// Animated values
|
||||
const controlsOpacity = useSharedValue(0);
|
||||
const loadingOpacity = useSharedValue(1);
|
||||
const playButtonScale = useSharedValue(1);
|
||||
|
||||
// Auto-hide controls after 3 seconds
|
||||
const hideControlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const showControlsWithTimeout = useCallback(() => {
|
||||
setShowControls(true);
|
||||
controlsOpacity.value = withTiming(1, { duration: 200 });
|
||||
|
||||
// Clear existing timeout
|
||||
if (hideControlsTimeout.current) {
|
||||
clearTimeout(hideControlsTimeout.current);
|
||||
}
|
||||
|
||||
// Set new timeout to hide controls
|
||||
hideControlsTimeout.current = setTimeout(() => {
|
||||
setShowControls(false);
|
||||
controlsOpacity.value = withTiming(0, { duration: 200 });
|
||||
}, 3000);
|
||||
}, [controlsOpacity]);
|
||||
|
||||
const handleVideoPress = useCallback(() => {
|
||||
if (showControls) {
|
||||
// If controls are visible, toggle play/pause
|
||||
handlePlayPause();
|
||||
} else {
|
||||
// If controls are hidden, show them
|
||||
showControlsWithTimeout();
|
||||
}
|
||||
}, [showControls, showControlsWithTimeout]);
|
||||
|
||||
const handlePlayPause = useCallback(async () => {
|
||||
try {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
playButtonScale.value = withTiming(0.8, { duration: 100 }, () => {
|
||||
playButtonScale.value = withTiming(1, { duration: 100 });
|
||||
});
|
||||
|
||||
setIsPlaying(!isPlaying);
|
||||
|
||||
showControlsWithTimeout();
|
||||
} catch (error) {
|
||||
logger.error('TrailerPlayer', 'Error toggling playback:', error);
|
||||
}
|
||||
}, [isPlaying, playButtonScale, showControlsWithTimeout]);
|
||||
|
||||
const handleMuteToggle = useCallback(async () => {
|
||||
try {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
setIsMuted(!isMuted);
|
||||
showControlsWithTimeout();
|
||||
} catch (error) {
|
||||
logger.error('TrailerPlayer', 'Error toggling mute:', error);
|
||||
}
|
||||
}, [isMuted, showControlsWithTimeout]);
|
||||
|
||||
const handleLoadStart = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setHasError(false);
|
||||
loadingOpacity.value = 1;
|
||||
onLoadStart?.();
|
||||
logger.info('TrailerPlayer', 'Video load started');
|
||||
}, [loadingOpacity, onLoadStart]);
|
||||
|
||||
const handleLoad = useCallback((data: OnLoadData) => {
|
||||
setIsLoading(false);
|
||||
loadingOpacity.value = withTiming(0, { duration: 300 });
|
||||
setDuration(data.duration * 1000); // Convert to milliseconds
|
||||
onLoad?.();
|
||||
logger.info('TrailerPlayer', 'Video loaded successfully');
|
||||
}, [loadingOpacity, onLoad]);
|
||||
|
||||
const handleError = useCallback((error: string) => {
|
||||
setIsLoading(false);
|
||||
setHasError(true);
|
||||
loadingOpacity.value = withTiming(0, { duration: 300 });
|
||||
onError?.(error);
|
||||
logger.error('TrailerPlayer', 'Video error:', error);
|
||||
}, [loadingOpacity, onError]);
|
||||
|
||||
const handleProgress = useCallback((data: OnProgressData) => {
|
||||
setPosition(data.currentTime * 1000); // Convert to milliseconds
|
||||
onProgress?.(data);
|
||||
}, [onProgress]);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideControlsTimeout.current) {
|
||||
clearTimeout(hideControlsTimeout.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Animated styles
|
||||
const controlsAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: controlsOpacity.value,
|
||||
}));
|
||||
|
||||
const loadingAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: loadingOpacity.value,
|
||||
}));
|
||||
|
||||
const playButtonAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: playButtonScale.value }],
|
||||
}));
|
||||
|
||||
const progressPercentage = duration > 0 ? (position / duration) * 100 : 0;
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<View style={styles.errorContainer}>
|
||||
<MaterialIcons name="error-outline" size={48} color={currentTheme.colors.error} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<Video
|
||||
ref={videoRef}
|
||||
source={{ uri: trailerUrl }}
|
||||
style={styles.video}
|
||||
resizeMode="cover"
|
||||
paused={!isPlaying}
|
||||
repeat={true}
|
||||
muted={isMuted}
|
||||
onLoadStart={handleLoadStart}
|
||||
onLoad={handleLoad}
|
||||
onError={(error: any) => handleError(error?.error?.message || 'Unknown error')}
|
||||
onProgress={handleProgress}
|
||||
controls={false}
|
||||
onEnd={() => {
|
||||
// Auto-restart when video ends
|
||||
videoRef.current?.seek(0);
|
||||
setIsPlaying(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Loading indicator */}
|
||||
<Animated.View style={[styles.loadingContainer, loadingAnimatedStyle]}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
</Animated.View>
|
||||
|
||||
{/* Video controls overlay */}
|
||||
<TouchableOpacity
|
||||
style={styles.videoOverlay}
|
||||
onPress={handleVideoPress}
|
||||
activeOpacity={1}
|
||||
>
|
||||
<Animated.View style={[styles.controlsContainer, controlsAnimatedStyle]}>
|
||||
{/* Top gradient */}
|
||||
<LinearGradient
|
||||
colors={['rgba(0,0,0,0.6)', 'transparent']}
|
||||
style={styles.topGradient}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
{/* Center play/pause button */}
|
||||
<View style={styles.centerControls}>
|
||||
<Animated.View style={playButtonAnimatedStyle}>
|
||||
<TouchableOpacity style={styles.playButton} onPress={handlePlayPause}>
|
||||
<MaterialIcons
|
||||
name={isPlaying ? 'pause' : 'play-arrow'}
|
||||
size={isTablet ? 64 : 48}
|
||||
color="white"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* Bottom controls */}
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.8)']}
|
||||
style={styles.bottomGradient}
|
||||
>
|
||||
<View style={styles.bottomControls}>
|
||||
{/* Progress bar */}
|
||||
<View style={styles.progressContainer}>
|
||||
<View style={styles.progressBar}>
|
||||
<View
|
||||
style={[styles.progressFill, { width: `${progressPercentage}%` }]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Control buttons */}
|
||||
<View style={styles.controlButtons}>
|
||||
<TouchableOpacity style={styles.controlButton} onPress={handlePlayPause}>
|
||||
<MaterialIcons
|
||||
name={isPlaying ? 'pause' : 'play-arrow'}
|
||||
size={isTablet ? 32 : 24}
|
||||
color="white"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.controlButton} onPress={handleMuteToggle}>
|
||||
<MaterialIcons
|
||||
name={isMuted ? 'volume-off' : 'volume-up'}
|
||||
size={isTablet ? 32 : 24}
|
||||
color="white"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
video: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
videoOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
loadingContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
controlsContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
topGradient: {
|
||||
height: 100,
|
||||
width: '100%',
|
||||
},
|
||||
centerControls: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
playButton: {
|
||||
width: isTablet ? 100 : 80,
|
||||
height: isTablet ? 100 : 80,
|
||||
borderRadius: isTablet ? 50 : 40,
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(255,255,255,0.8)',
|
||||
},
|
||||
bottomGradient: {
|
||||
paddingBottom: Platform.OS === 'ios' ? 20 : 16,
|
||||
paddingTop: 20,
|
||||
},
|
||||
bottomControls: {
|
||||
paddingHorizontal: isTablet ? 32 : 16,
|
||||
},
|
||||
progressContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
progressBar: {
|
||||
height: 3,
|
||||
backgroundColor: 'rgba(255,255,255,0.3)',
|
||||
borderRadius: 1.5,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
progressFill: {
|
||||
height: '100%',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 1.5,
|
||||
},
|
||||
controlButtons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
controlButton: {
|
||||
padding: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
},
|
||||
});
|
||||
|
||||
export default TrailerPlayer;
|
||||
166
src/services/trailerService.ts
Normal file
166
src/services/trailerService.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import { logger } from '../utils/logger';
|
||||
|
||||
export interface TrailerData {
|
||||
url: string;
|
||||
title: string;
|
||||
year: number;
|
||||
}
|
||||
|
||||
export class TrailerService {
|
||||
private static readonly BASE_URL = 'https://db.xprime.tv/trailers';
|
||||
private static readonly TIMEOUT = 10000; // 10 seconds
|
||||
|
||||
/**
|
||||
* Fetches trailer URL for a given title and year
|
||||
* @param title - The movie/series title
|
||||
* @param year - The release year
|
||||
* @returns Promise<string | null> - The trailer URL or null if not found
|
||||
*/
|
||||
static async getTrailerUrl(title: string, year: number): Promise<string | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT);
|
||||
|
||||
const url = `${this.BASE_URL}?title=${encodeURIComponent(title)}&year=${year}`;
|
||||
|
||||
logger.info('TrailerService', `Fetching trailer for: ${title} (${year})`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'text/plain',
|
||||
'User-Agent': 'Nuvio/1.0',
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn('TrailerService', `Failed to fetch trailer: ${response.status} ${response.statusText}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const trailerUrl = await response.text();
|
||||
|
||||
// Validate the response is a valid URL
|
||||
if (!trailerUrl || !this.isValidTrailerUrl(trailerUrl.trim())) {
|
||||
logger.warn('TrailerService', `Invalid trailer URL received: ${trailerUrl}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cleanUrl = trailerUrl.trim();
|
||||
logger.info('TrailerService', `Successfully fetched trailer URL: ${cleanUrl}`);
|
||||
|
||||
return cleanUrl;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
logger.warn('TrailerService', 'Trailer fetch request timed out');
|
||||
} else {
|
||||
logger.error('TrailerService', 'Error fetching trailer:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the provided string is a valid trailer URL
|
||||
* @param url - The URL to validate
|
||||
* @returns boolean - True if valid, false otherwise
|
||||
*/
|
||||
private static isValidTrailerUrl(url: string): boolean {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// Check if it's a valid HTTP/HTTPS URL
|
||||
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for common video streaming domains/patterns
|
||||
const validDomains = [
|
||||
'theplatform.com',
|
||||
'youtube.com',
|
||||
'youtu.be',
|
||||
'vimeo.com',
|
||||
'dailymotion.com',
|
||||
'twitch.tv',
|
||||
'amazonaws.com',
|
||||
'cloudfront.net'
|
||||
];
|
||||
|
||||
const hostname = urlObj.hostname.toLowerCase();
|
||||
const isValidDomain = validDomains.some(domain =>
|
||||
hostname.includes(domain) || hostname.endsWith(domain)
|
||||
);
|
||||
|
||||
// Check for video file extensions or streaming formats
|
||||
const hasVideoFormat = /\.(mp4|m3u8|mpd|webm|mov|avi|mkv)$/i.test(urlObj.pathname) ||
|
||||
url.includes('formats=') ||
|
||||
url.includes('manifest') ||
|
||||
url.includes('playlist');
|
||||
|
||||
return isValidDomain || hasVideoFormat;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the best video format URL from a multi-format URL
|
||||
* @param url - The trailer URL that may contain multiple formats
|
||||
* @returns string - The best format URL for mobile playback
|
||||
*/
|
||||
static getBestFormatUrl(url: string): string {
|
||||
// If the URL contains format parameters, try to get the best one for mobile
|
||||
if (url.includes('formats=')) {
|
||||
// Prefer M3U (HLS) for better mobile compatibility
|
||||
if (url.includes('M3U')) {
|
||||
// Try to get M3U without encryption first, then with encryption
|
||||
const baseUrl = url.split('?')[0];
|
||||
return `${baseUrl}?formats=M3U+none,M3U+appleHlsEncryption`;
|
||||
}
|
||||
// Fallback to MP4 if available
|
||||
if (url.includes('MPEG4')) {
|
||||
const baseUrl = url.split('?')[0];
|
||||
return `${baseUrl}?formats=MPEG4`;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the original URL if no format optimization is needed
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a trailer is available for the given title and year
|
||||
* @param title - The movie/series title
|
||||
* @param year - The release year
|
||||
* @returns Promise<boolean> - True if trailer is available
|
||||
*/
|
||||
static async isTrailerAvailable(title: string, year: number): Promise<boolean> {
|
||||
const trailerUrl = await this.getTrailerUrl(title, year);
|
||||
return trailerUrl !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets trailer data with additional metadata
|
||||
* @param title - The movie/series title
|
||||
* @param year - The release year
|
||||
* @returns Promise<TrailerData | null> - Trailer data or null if not found
|
||||
*/
|
||||
static async getTrailerData(title: string, year: number): Promise<TrailerData | null> {
|
||||
const url = await this.getTrailerUrl(title, year);
|
||||
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
url: this.getBestFormatUrl(url),
|
||||
title,
|
||||
year
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default TrailerService;
|
||||
Loading…
Reference in a new issue