forked from Mirrors/Dispatcharr
Pull advanced info from provider when opening a movie.
This commit is contained in:
parent
e1f5cb24ec
commit
4accd2be85
9 changed files with 666 additions and 12 deletions
|
|
@ -5,6 +5,7 @@ from rest_framework.filters import SearchFilter, OrderingFilter
|
|||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.shortcuts import get_object_or_404
|
||||
import django_filters
|
||||
import logging
|
||||
from apps.accounts.permissions import (
|
||||
Authenticated,
|
||||
permission_classes_by_action,
|
||||
|
|
@ -17,6 +18,9 @@ from .serializers import (
|
|||
VODCategorySerializer,
|
||||
VODConnectionSerializer
|
||||
)
|
||||
from core.xtream_codes import Client as XtreamCodesClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MovieFilter(django_filters.FilterSet):
|
||||
|
|
@ -54,6 +58,105 @@ class MovieViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
'category', 'logo', 'm3u_account'
|
||||
).filter(m3u_account__is_active=True)
|
||||
|
||||
def _extract_year(self, date_string):
|
||||
"""Extract year from date string"""
|
||||
if not date_string:
|
||||
return None
|
||||
try:
|
||||
return int(date_string.split('-')[0])
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
def _convert_duration_to_minutes(self, duration_secs):
|
||||
"""Convert duration from seconds to minutes"""
|
||||
if not duration_secs:
|
||||
return 0
|
||||
try:
|
||||
return int(duration_secs) // 60
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
@action(detail=True, methods=['get'], url_path='provider-info')
|
||||
def provider_info(self, request, pk=None):
|
||||
"""Get detailed movie information from the original provider"""
|
||||
logger.debug(f"MovieViewSet.provider_info called for movie ID: {pk}")
|
||||
movie = self.get_object()
|
||||
logger.debug(f"Retrieved movie: {movie.name} (ID: {movie.id})")
|
||||
|
||||
if not movie.m3u_account:
|
||||
return Response(
|
||||
{'error': 'No M3U account associated with this movie'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
# Create XtreamCodes client
|
||||
with XtreamCodesClient(
|
||||
server_url=movie.m3u_account.server_url,
|
||||
username=movie.m3u_account.username,
|
||||
password=movie.m3u_account.password,
|
||||
user_agent=movie.m3u_account.user_agent
|
||||
) as client:
|
||||
# Get detailed VOD info from provider
|
||||
logger.debug(f"Fetching VOD info for movie {movie.id} with stream ID {movie.stream_id} from provider")
|
||||
vod_info = client.get_vod_info(movie.stream_id)
|
||||
|
||||
if not vod_info or 'info' not in vod_info:
|
||||
return Response(
|
||||
{'error': 'No information available from provider'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# Extract and format the info
|
||||
info = vod_info.get('info', {})
|
||||
movie_data = vod_info.get('movie_data', {})
|
||||
|
||||
# Build response with all available fields
|
||||
response_data = {
|
||||
'id': movie.id,
|
||||
'stream_id': movie.stream_id,
|
||||
'name': info.get('name', movie.name),
|
||||
'o_name': info.get('o_name', ''),
|
||||
'description': info.get('description', info.get('plot', '')),
|
||||
'plot': info.get('plot', info.get('description', '')),
|
||||
'year': self._extract_year(info.get('releasedate', '')),
|
||||
'release_date': info.get('releasedate', ''),
|
||||
'releasedate': info.get('releasedate', ''),
|
||||
'genre': info.get('genre', ''),
|
||||
'director': info.get('director', ''),
|
||||
'actors': info.get('actors', info.get('cast', '')),
|
||||
'cast': info.get('cast', info.get('actors', '')),
|
||||
'country': info.get('country', ''),
|
||||
'rating': info.get('rating', 0),
|
||||
'tmdb_id': info.get('tmdb_id', ''),
|
||||
'youtube_trailer': info.get('youtube_trailer', ''),
|
||||
'duration': self._convert_duration_to_minutes(info.get('duration_secs', 0)),
|
||||
'duration_secs': info.get('duration_secs', 0),
|
||||
'episode_run_time': info.get('episode_run_time', 0),
|
||||
'age': info.get('age', ''),
|
||||
'backdrop_path': info.get('backdrop_path', []),
|
||||
'cover': info.get('cover_big', ''),
|
||||
'cover_big': info.get('cover_big', ''),
|
||||
'movie_image': info.get('movie_image', ''),
|
||||
'bitrate': info.get('bitrate', 0),
|
||||
'video': info.get('video', {}),
|
||||
'audio': info.get('audio', {}),
|
||||
# Include movie_data fields
|
||||
'container_extension': movie_data.get('container_extension', 'mp4'),
|
||||
'direct_source': movie_data.get('direct_source', ''),
|
||||
'category_id': movie_data.get('category_id', ''),
|
||||
'added': movie_data.get('added', ''),
|
||||
}
|
||||
|
||||
return Response(response_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching VOD info from provider for movie {pk}: {str(e)}")
|
||||
return Response(
|
||||
{'error': f'Failed to fetch information from provider: {str(e)}'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
class EpisodeFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(lookup_expr="icontains")
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from datetime import timedelta
|
|||
from .models import Series, VODCategory, VODConnection, Movie, Episode
|
||||
from apps.m3u.models import M3UAccount
|
||||
from apps.channels.models import Logo
|
||||
from core.xtream_codes import Client as XtreamCodesClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -371,4 +372,4 @@ def cleanup_inactive_vod_connections():
|
|||
inactive_connections.delete()
|
||||
logger.info(f"Cleaned up {count} inactive VOD connections")
|
||||
|
||||
return count
|
||||
return count
|
||||
|
|
@ -196,6 +196,180 @@ class Client:
|
|||
"""Get the playback URL for a stream"""
|
||||
return f"{self.server_url}/live/{self.username}/{self.password}/{stream_id}.ts"
|
||||
|
||||
def get_vod_categories(self):
|
||||
"""Get VOD categories"""
|
||||
try:
|
||||
if not self.server_info:
|
||||
self.authenticate()
|
||||
|
||||
endpoint = "player_api.php"
|
||||
params = {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'action': 'get_vod_categories'
|
||||
}
|
||||
|
||||
categories = self._make_request(endpoint, params)
|
||||
|
||||
if not isinstance(categories, list):
|
||||
error_msg = f"Invalid VOD categories response: {categories}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
logger.info(f"Successfully retrieved {len(categories)} VOD categories")
|
||||
return categories
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get VOD categories: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def get_vod_streams(self, category_id=None):
|
||||
"""Get VOD streams for a specific category"""
|
||||
try:
|
||||
if not self.server_info:
|
||||
self.authenticate()
|
||||
|
||||
endpoint = "player_api.php"
|
||||
params = {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'action': 'get_vod_streams'
|
||||
}
|
||||
|
||||
if category_id:
|
||||
params['category_id'] = category_id
|
||||
|
||||
streams = self._make_request(endpoint, params)
|
||||
|
||||
if not isinstance(streams, list):
|
||||
error_msg = f"Invalid VOD streams response for category {category_id}: {streams}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
logger.info(f"Successfully retrieved {len(streams)} VOD streams for category {category_id}")
|
||||
return streams
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get VOD streams for category {category_id}: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def get_vod_info(self, vod_id):
|
||||
"""Get detailed information for a specific VOD"""
|
||||
try:
|
||||
if not self.server_info:
|
||||
self.authenticate()
|
||||
|
||||
endpoint = "player_api.php"
|
||||
params = {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'action': 'get_vod_info',
|
||||
'vod_id': vod_id
|
||||
}
|
||||
|
||||
vod_info = self._make_request(endpoint, params)
|
||||
|
||||
if not isinstance(vod_info, dict):
|
||||
error_msg = f"Invalid VOD info response for vod_id {vod_id}: {vod_info}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
logger.info(f"Successfully retrieved VOD info for vod_id {vod_id}")
|
||||
return vod_info
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get VOD info for vod_id {vod_id}: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def get_series_categories(self):
|
||||
"""Get series categories"""
|
||||
try:
|
||||
if not self.server_info:
|
||||
self.authenticate()
|
||||
|
||||
endpoint = "player_api.php"
|
||||
params = {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'action': 'get_series_categories'
|
||||
}
|
||||
|
||||
categories = self._make_request(endpoint, params)
|
||||
|
||||
if not isinstance(categories, list):
|
||||
error_msg = f"Invalid series categories response: {categories}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
logger.info(f"Successfully retrieved {len(categories)} series categories")
|
||||
return categories
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get series categories: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def get_series(self, category_id=None):
|
||||
"""Get series for a specific category"""
|
||||
try:
|
||||
if not self.server_info:
|
||||
self.authenticate()
|
||||
|
||||
endpoint = "player_api.php"
|
||||
params = {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'action': 'get_series'
|
||||
}
|
||||
|
||||
if category_id:
|
||||
params['category_id'] = category_id
|
||||
|
||||
series = self._make_request(endpoint, params)
|
||||
|
||||
if not isinstance(series, list):
|
||||
error_msg = f"Invalid series response for category {category_id}: {series}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
logger.info(f"Successfully retrieved {len(series)} series for category {category_id}")
|
||||
return series
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get series for category {category_id}: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def get_series_info(self, series_id):
|
||||
"""Get detailed information for a specific series including episodes"""
|
||||
try:
|
||||
if not self.server_info:
|
||||
self.authenticate()
|
||||
|
||||
endpoint = "player_api.php"
|
||||
params = {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'action': 'get_series_info',
|
||||
'series_id': series_id
|
||||
}
|
||||
|
||||
series_info = self._make_request(endpoint, params)
|
||||
|
||||
if not isinstance(series_info, dict):
|
||||
error_msg = f"Invalid series info response for series_id {series_id}: {series_info}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
logger.info(f"Successfully retrieved series info for series_id {series_id}")
|
||||
return series_info
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get series info for series_id {series_id}: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def get_vod_stream_url(self, vod_id, container_extension="mp4"):
|
||||
"""Get the playback URL for a VOD"""
|
||||
return f"{self.server_url}/movie/{self.username}/{self.password}/{vod_id}.{container_extension}"
|
||||
|
||||
def close(self):
|
||||
"""Close the session and cleanup resources"""
|
||||
if hasattr(self, 'session') and self.session:
|
||||
|
|
|
|||
|
|
@ -65,8 +65,7 @@ urlpatterns = [
|
|||
path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
|
||||
# Optionally, serve the raw Swagger JSON
|
||||
path("swagger.json", schema_view.without_ui(cache_timeout=0), name="schema-json"),
|
||||
# VOD
|
||||
path("api/vod/", include("apps.vod.urls")),
|
||||
|
||||
path("proxy/vod/", include("apps.proxy.vod_proxy.urls")),
|
||||
# Catch-all routes should always be last
|
||||
path("", TemplateView.as_view(template_name="index.html")), # React entry point
|
||||
|
|
|
|||
|
|
@ -1751,6 +1751,15 @@ export default class API {
|
|||
}
|
||||
}
|
||||
|
||||
static async getVODInfoFromProvider(vodId) {
|
||||
try {
|
||||
const response = await request(`${host}/api/vod/movies/${vodId}/provider-info/`);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to retrieve VOD info from provider', e);
|
||||
}
|
||||
}
|
||||
|
||||
static async getSeriesInfo(seriesId) {
|
||||
try {
|
||||
const response = await request(`${host}/api/vod/series/${seriesId}/`);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export default function FloatingVideo() {
|
|||
const isVisible = useVideoStore((s) => s.isVisible);
|
||||
const streamUrl = useVideoStore((s) => s.streamUrl);
|
||||
const contentType = useVideoStore((s) => s.contentType);
|
||||
const metadata = useVideoStore((s) => s.metadata);
|
||||
const hideVideo = useVideoStore((s) => s.hideVideo);
|
||||
const videoRef = useRef(null);
|
||||
const playerRef = useRef(null);
|
||||
|
|
@ -331,10 +332,34 @@ export default function FloatingVideo() {
|
|||
}}
|
||||
// Add poster for VOD if available
|
||||
{...(contentType === 'vod' && {
|
||||
poster: undefined, // Could add poster support later
|
||||
poster: metadata?.logo?.url, // Use VOD poster if available
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* VOD title overlay when not loading */}
|
||||
{!isLoading && metadata && contentType === 'vod' && (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
background: 'linear-gradient(transparent, rgba(0,0,0,0.8))',
|
||||
padding: '20px 10px 10px',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<Text size="sm" weight={500} style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.8)' }}>
|
||||
{metadata.name}
|
||||
</Text>
|
||||
{metadata.year && (
|
||||
<Text size="xs" color="dimmed" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.8)' }}>
|
||||
{metadata.year}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Loading overlay - only show when loading */}
|
||||
{isLoading && (
|
||||
<Box
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ const VODCard = ({ vod, onClick }) => {
|
|||
return <Text weight={500}>{vod.name}</Text>;
|
||||
};
|
||||
|
||||
const handleCardClick = async () => {
|
||||
// Just pass the basic vod info to the parent handler
|
||||
onClick(vod);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
shadow="sm"
|
||||
|
|
@ -57,7 +62,7 @@ const VODCard = ({ vod, onClick }) => {
|
|||
radius="md"
|
||||
withBorder
|
||||
style={{ cursor: 'pointer', backgroundColor: '#27272A' }}
|
||||
onClick={() => onClick(vod)}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
<Card.Section>
|
||||
<Box style={{ position: 'relative', height: 300 }}>
|
||||
|
|
@ -274,6 +279,236 @@ const SeriesModal = ({ series, opened, onClose }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const VODModal = ({ vod, opened, onClose }) => {
|
||||
const [detailedVOD, setDetailedVOD] = useState(null);
|
||||
const [loadingDetails, setLoadingDetails] = useState(false);
|
||||
const { fetchVODDetailsFromProvider } = useVODStore();
|
||||
const showVideo = useVideoStore((s) => s.showVideo);
|
||||
const env_mode = useSettingsStore((s) => s.environment.env_mode);
|
||||
|
||||
useEffect(() => {
|
||||
if (opened && vod && !detailedVOD) {
|
||||
setLoadingDetails(true);
|
||||
fetchVODDetailsFromProvider(vod.id)
|
||||
.then((details) => {
|
||||
setDetailedVOD(details);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Failed to fetch provider details, using basic info:', error);
|
||||
setDetailedVOD(vod); // Fallback to basic data
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingDetails(false);
|
||||
});
|
||||
}
|
||||
}, [opened, vod, detailedVOD, fetchVODDetailsFromProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!opened) {
|
||||
setDetailedVOD(null);
|
||||
setLoadingDetails(false);
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
const handlePlayVOD = () => {
|
||||
const vodToPlay = detailedVOD || vod;
|
||||
if (!vodToPlay) return;
|
||||
|
||||
let streamUrl = vodToPlay.stream_url;
|
||||
if (env_mode === 'dev') {
|
||||
streamUrl = `${window.location.protocol}//${window.location.hostname}:5656${vodToPlay.stream_url}`;
|
||||
} else {
|
||||
streamUrl = `${window.location.origin}${vodToPlay.stream_url}`;
|
||||
}
|
||||
showVideo(streamUrl, 'vod', vodToPlay);
|
||||
};
|
||||
|
||||
const formatDuration = (minutes) => {
|
||||
if (!minutes) return '';
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
||||
};
|
||||
|
||||
if (!vod) return null;
|
||||
|
||||
// Use detailed data if available, otherwise use basic vod data
|
||||
const displayVOD = detailedVOD || vod;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={displayVOD.name}
|
||||
size="xl"
|
||||
centered
|
||||
>
|
||||
<Stack spacing="md">
|
||||
{loadingDetails && (
|
||||
<Group spacing="xs" mb="sm">
|
||||
<Loader size="xs" />
|
||||
<Text size="xs" color="dimmed">Loading additional details...</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{/* Backdrop image if available */}
|
||||
{displayVOD.backdrop_path && displayVOD.backdrop_path.length > 0 && (
|
||||
<Box style={{ position: 'relative', height: 200, borderRadius: '8px', overflow: 'hidden' }}>
|
||||
<Image
|
||||
src={displayVOD.backdrop_path[0]}
|
||||
height={200}
|
||||
alt={`${displayVOD.name} backdrop`}
|
||||
fit="cover"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Movie poster and basic info */}
|
||||
<Flex gap="md">
|
||||
{/* Use movie_image or logo */}
|
||||
{(displayVOD.movie_image || displayVOD.logo?.url) ? (
|
||||
<Box style={{ flexShrink: 0 }}>
|
||||
<Image
|
||||
src={displayVOD.movie_image || displayVOD.logo.url}
|
||||
width={200}
|
||||
height={300}
|
||||
alt={displayVOD.name}
|
||||
fit="contain"
|
||||
style={{ borderRadius: '8px' }}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
style={{
|
||||
width: 200,
|
||||
height: 300,
|
||||
backgroundColor: '#404040',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '8px',
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<Play size={48} color="#666" />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Stack spacing="md" style={{ flex: 1 }}>
|
||||
<Title order={3}>{displayVOD.name}</Title>
|
||||
|
||||
{/* Original name if different */}
|
||||
{displayVOD.o_name && displayVOD.o_name !== displayVOD.name && (
|
||||
<Text size="sm" color="dimmed" style={{ fontStyle: 'italic' }}>
|
||||
Original: {displayVOD.o_name}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Group spacing="md">
|
||||
{displayVOD.year && <Badge color="blue">{displayVOD.year}</Badge>}
|
||||
{displayVOD.duration && <Badge color="gray">{formatDuration(displayVOD.duration)}</Badge>}
|
||||
{displayVOD.rating && <Badge color="yellow">{displayVOD.rating}</Badge>}
|
||||
{displayVOD.age && <Badge color="orange">{displayVOD.age}</Badge>}
|
||||
<Badge color="green">Movie</Badge>
|
||||
</Group>
|
||||
|
||||
{/* Release date */}
|
||||
{displayVOD.release_date && (
|
||||
<Text size="sm" color="dimmed">
|
||||
<strong>Release Date:</strong> {displayVOD.release_date}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{displayVOD.genre && (
|
||||
<Text size="sm" color="dimmed">
|
||||
<strong>Genre:</strong> {displayVOD.genre}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{displayVOD.director && (
|
||||
<Text size="sm" color="dimmed">
|
||||
<strong>Director:</strong> {displayVOD.director}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{displayVOD.actors && (
|
||||
<Text size="sm" color="dimmed">
|
||||
<strong>Cast:</strong> {displayVOD.actors}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{displayVOD.country && (
|
||||
<Text size="sm" color="dimmed">
|
||||
<strong>Country:</strong> {displayVOD.country}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Technical info */}
|
||||
{(displayVOD.bitrate || displayVOD.video || displayVOD.audio) && (
|
||||
<Stack spacing={4}>
|
||||
<Text size="sm" weight={500}>Technical Details:</Text>
|
||||
{displayVOD.bitrate && displayVOD.bitrate > 0 && (
|
||||
<Text size="xs" color="dimmed">
|
||||
Bitrate: {displayVOD.bitrate} kbps
|
||||
</Text>
|
||||
)}
|
||||
{displayVOD.video && Object.keys(displayVOD.video).length > 0 && (
|
||||
<Text size="xs" color="dimmed">
|
||||
Video: {JSON.stringify(displayVOD.video)}
|
||||
</Text>
|
||||
)}
|
||||
{displayVOD.audio && Object.keys(displayVOD.audio).length > 0 && (
|
||||
<Text size="xs" color="dimmed">
|
||||
Audio: {JSON.stringify(displayVOD.audio)}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Button
|
||||
leftSection={<Play size={16} />}
|
||||
variant="filled"
|
||||
color="blue"
|
||||
size="md"
|
||||
onClick={handlePlayVOD}
|
||||
style={{ marginTop: 'auto', alignSelf: 'flex-start' }}
|
||||
>
|
||||
Play Movie
|
||||
</Button>
|
||||
</Stack>
|
||||
</Flex>
|
||||
|
||||
{/* Description */}
|
||||
{displayVOD.description && (
|
||||
<Box>
|
||||
<Text size="sm" weight={500} mb={8}>Description</Text>
|
||||
<Text size="sm">
|
||||
{displayVOD.description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* YouTube trailer if available */}
|
||||
{displayVOD.youtube_trailer && (
|
||||
<Box>
|
||||
<Text size="sm" weight={500} mb={8}>Trailer</Text>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="red"
|
||||
component="a"
|
||||
href={displayVOD.youtube_trailer}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Watch Trailer on YouTube
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const MIN_CARD_WIDTH = 260;
|
||||
const MAX_CARD_WIDTH = 320;
|
||||
|
||||
|
|
@ -316,7 +551,10 @@ const VODsPage = () => {
|
|||
|
||||
const showVideo = useVideoStore((s) => s.showVideo);
|
||||
const [selectedSeries, setSelectedSeries] = useState(null);
|
||||
const [selectedVOD, setSelectedVOD] = useState(null);
|
||||
const [seriesModalOpened, { open: openSeriesModal, close: closeSeriesModal }] = useDisclosure(false);
|
||||
const [vodModalOpened, { open: openVODModal, close: closeVODModal }] = useDisclosure(false);
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
const columns = useCardColumns();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -325,9 +563,9 @@ const VODsPage = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (filters.type === 'series') {
|
||||
fetchSeries();
|
||||
fetchSeries().finally(() => setInitialLoad(false));
|
||||
} else {
|
||||
fetchVODs();
|
||||
fetchVODs().finally(() => setInitialLoad(false));
|
||||
}
|
||||
}, [filters, currentPage, fetchVODs, fetchSeries]);
|
||||
|
||||
|
|
@ -339,7 +577,12 @@ const VODsPage = () => {
|
|||
} else {
|
||||
streamUrl = `${window.location.origin}${vod.stream_url}`;
|
||||
}
|
||||
showVideo(streamUrl, 'vod'); // Specify VOD content type
|
||||
showVideo(streamUrl, 'vod', vod);
|
||||
};
|
||||
|
||||
const handleVODCardClick = (vod) => {
|
||||
setSelectedVOD(vod);
|
||||
openVODModal();
|
||||
};
|
||||
|
||||
const handleSeriesClick = (series) => {
|
||||
|
|
@ -395,7 +638,7 @@ const VODsPage = () => {
|
|||
</Group>
|
||||
|
||||
{/* Content */}
|
||||
{loading ? (
|
||||
{initialLoad ? (
|
||||
<Flex justify="center" py="xl">
|
||||
<Loader size="lg" />
|
||||
</Flex>
|
||||
|
|
@ -424,7 +667,7 @@ const VODsPage = () => {
|
|||
key={vod.id}
|
||||
style={{ minWidth: MIN_CARD_WIDTH, maxWidth: MAX_CARD_WIDTH, margin: '0 auto' }}
|
||||
>
|
||||
<VODCard vod={vod} onClick={handlePlayVOD} />
|
||||
<VODCard vod={vod} onClick={handleVODCardClick} />
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
|
|
@ -450,6 +693,13 @@ const VODsPage = () => {
|
|||
opened={seriesModalOpened}
|
||||
onClose={closeSeriesModal}
|
||||
/>
|
||||
|
||||
{/* VOD Details Modal */}
|
||||
<VODModal
|
||||
vod={selectedVOD}
|
||||
opened={vodModalOpened}
|
||||
onClose={closeVODModal}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ const useVODStore = create((set, get) => ({
|
|||
})),
|
||||
|
||||
fetchVODs: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
const state = get();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
|
|
@ -126,6 +126,96 @@ const useVODStore = create((set, get) => ({
|
|||
}
|
||||
},
|
||||
|
||||
fetchVODDetails: async (vodId) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await api.getVODInfo(vodId);
|
||||
|
||||
// Transform the response data to match our expected format
|
||||
const vodDetails = {
|
||||
id: response.id || vodId,
|
||||
name: response.name || '',
|
||||
description: response.description || '',
|
||||
year: response.year || null,
|
||||
genre: response.genre || '',
|
||||
rating: response.rating || '',
|
||||
duration: response.duration || null,
|
||||
stream_url: response.stream_url || '',
|
||||
logo: response.logo || null,
|
||||
type: 'movie',
|
||||
director: response.director || '',
|
||||
actors: response.actors || '',
|
||||
country: response.country || '',
|
||||
tmdb_id: response.tmdb_id || '',
|
||||
youtube_trailer: response.youtube_trailer || '',
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
vods: {
|
||||
...state.vods,
|
||||
[vodDetails.id]: vodDetails,
|
||||
},
|
||||
loading: false,
|
||||
}));
|
||||
|
||||
return vodDetails;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch VOD details:', error);
|
||||
set({ error: 'Failed to load VOD details.', loading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
fetchVODDetailsFromProvider: async (vodId) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await api.getVODInfoFromProvider(vodId);
|
||||
|
||||
// Transform the response data to match our expected format
|
||||
const vodDetails = {
|
||||
id: response.id || vodId,
|
||||
name: response.name || '',
|
||||
description: response.description || response.plot || '',
|
||||
year: response.year || null,
|
||||
genre: response.genre || '',
|
||||
rating: response.rating || '',
|
||||
duration: response.duration || null,
|
||||
stream_url: response.stream_url || '',
|
||||
logo: response.logo || response.cover || null,
|
||||
type: 'movie',
|
||||
director: response.director || '',
|
||||
actors: response.actors || response.cast || '',
|
||||
country: response.country || '',
|
||||
tmdb_id: response.tmdb_id || '',
|
||||
youtube_trailer: response.youtube_trailer || '',
|
||||
// Additional provider fields
|
||||
backdrop_path: response.backdrop_path || [],
|
||||
release_date: response.release_date || response.releasedate || '',
|
||||
movie_image: response.movie_image || null,
|
||||
o_name: response.o_name || '',
|
||||
age: response.age || '',
|
||||
episode_run_time: response.episode_run_time || null,
|
||||
bitrate: response.bitrate || 0,
|
||||
video: response.video || {},
|
||||
audio: response.audio || {},
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
vods: {
|
||||
...state.vods,
|
||||
[vodDetails.id]: vodDetails,
|
||||
},
|
||||
loading: false,
|
||||
}));
|
||||
|
||||
return vodDetails;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch VOD details from provider:', error);
|
||||
set({ error: 'Failed to load VOD details from provider.', loading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
fetchCategories: async () => {
|
||||
try {
|
||||
const response = await api.getVODCategories();
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ const useVideoStore = create((set) => ({
|
|||
isVisible: false,
|
||||
streamUrl: null,
|
||||
contentType: 'live', // 'live' for MPEG-TS streams, 'vod' for MP4/MKV files
|
||||
metadata: null, // Store additional metadata for VOD content
|
||||
|
||||
showVideo: (url, type = 'live') =>
|
||||
showVideo: (url, type = 'live', metadata = null) =>
|
||||
set({
|
||||
isVisible: true,
|
||||
streamUrl: url,
|
||||
contentType: type,
|
||||
metadata: metadata,
|
||||
}),
|
||||
|
||||
hideVideo: () =>
|
||||
|
|
@ -21,6 +23,7 @@ const useVideoStore = create((set) => ({
|
|||
isVisible: false,
|
||||
streamUrl: null,
|
||||
contentType: 'live',
|
||||
metadata: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue