diff --git a/apps/vod/api_views.py b/apps/vod/api_views.py
index c7cd81b8..d0fc7668 100644
--- a/apps/vod/api_views.py
+++ b/apps/vod/api_views.py
@@ -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")
diff --git a/apps/vod/tasks.py b/apps/vod/tasks.py
index 8702806c..f237ac77 100644
--- a/apps/vod/tasks.py
+++ b/apps/vod/tasks.py
@@ -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
\ No newline at end of file
diff --git a/core/xtream_codes.py b/core/xtream_codes.py
index d068bacb..b8b4d862 100644
--- a/core/xtream_codes.py
+++ b/core/xtream_codes.py
@@ -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:
diff --git a/dispatcharr/urls.py b/dispatcharr/urls.py
index 143b6e4c..94742024 100644
--- a/dispatcharr/urls.py
+++ b/dispatcharr/urls.py
@@ -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
diff --git a/frontend/src/api.js b/frontend/src/api.js
index 748754c8..67cdec57 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -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}/`);
diff --git a/frontend/src/components/FloatingVideo.jsx b/frontend/src/components/FloatingVideo.jsx
index f09f384f..1a1b628a 100644
--- a/frontend/src/components/FloatingVideo.jsx
+++ b/frontend/src/components/FloatingVideo.jsx
@@ -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' && (
+
+
+ {metadata.name}
+
+ {metadata.year && (
+
+ {metadata.year}
+
+ )}
+
+ )}
+
{/* Loading overlay - only show when loading */}
{isLoading && (
{
return {vod.name};
};
+ const handleCardClick = async () => {
+ // Just pass the basic vod info to the parent handler
+ onClick(vod);
+ };
+
return (
{
radius="md"
withBorder
style={{ cursor: 'pointer', backgroundColor: '#27272A' }}
- onClick={() => onClick(vod)}
+ onClick={handleCardClick}
>
@@ -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 (
+
+
+ {loadingDetails && (
+
+
+ Loading additional details...
+
+ )}
+
+ {/* Backdrop image if available */}
+ {displayVOD.backdrop_path && displayVOD.backdrop_path.length > 0 && (
+
+
+
+ )}
+
+ {/* Movie poster and basic info */}
+
+ {/* Use movie_image or logo */}
+ {(displayVOD.movie_image || displayVOD.logo?.url) ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {displayVOD.name}
+
+ {/* Original name if different */}
+ {displayVOD.o_name && displayVOD.o_name !== displayVOD.name && (
+
+ Original: {displayVOD.o_name}
+
+ )}
+
+
+ {displayVOD.year && {displayVOD.year}}
+ {displayVOD.duration && {formatDuration(displayVOD.duration)}}
+ {displayVOD.rating && {displayVOD.rating}}
+ {displayVOD.age && {displayVOD.age}}
+ Movie
+
+
+ {/* Release date */}
+ {displayVOD.release_date && (
+
+ Release Date: {displayVOD.release_date}
+
+ )}
+
+ {displayVOD.genre && (
+
+ Genre: {displayVOD.genre}
+
+ )}
+
+ {displayVOD.director && (
+
+ Director: {displayVOD.director}
+
+ )}
+
+ {displayVOD.actors && (
+
+ Cast: {displayVOD.actors}
+
+ )}
+
+ {displayVOD.country && (
+
+ Country: {displayVOD.country}
+
+ )}
+
+ {/* Technical info */}
+ {(displayVOD.bitrate || displayVOD.video || displayVOD.audio) && (
+
+ Technical Details:
+ {displayVOD.bitrate && displayVOD.bitrate > 0 && (
+
+ Bitrate: {displayVOD.bitrate} kbps
+
+ )}
+ {displayVOD.video && Object.keys(displayVOD.video).length > 0 && (
+
+ Video: {JSON.stringify(displayVOD.video)}
+
+ )}
+ {displayVOD.audio && Object.keys(displayVOD.audio).length > 0 && (
+
+ Audio: {JSON.stringify(displayVOD.audio)}
+
+ )}
+
+ )}
+
+ }
+ variant="filled"
+ color="blue"
+ size="md"
+ onClick={handlePlayVOD}
+ style={{ marginTop: 'auto', alignSelf: 'flex-start' }}
+ >
+ Play Movie
+
+
+
+
+ {/* Description */}
+ {displayVOD.description && (
+
+ Description
+
+ {displayVOD.description}
+
+
+ )}
+
+ {/* YouTube trailer if available */}
+ {displayVOD.youtube_trailer && (
+
+ Trailer
+
+
+ )}
+
+
+ );
+};
+
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 = () => {
{/* Content */}
- {loading ? (
+ {initialLoad ? (
@@ -424,7 +667,7 @@ const VODsPage = () => {
key={vod.id}
style={{ minWidth: MIN_CARD_WIDTH, maxWidth: MAX_CARD_WIDTH, margin: '0 auto' }}
>
-
+
))}
@@ -450,6 +693,13 @@ const VODsPage = () => {
opened={seriesModalOpened}
onClose={closeSeriesModal}
/>
+
+ {/* VOD Details Modal */}
+
);
};
diff --git a/frontend/src/store/useVODStore.jsx b/frontend/src/store/useVODStore.jsx
index cb79701b..249182a8 100644
--- a/frontend/src/store/useVODStore.jsx
+++ b/frontend/src/store/useVODStore.jsx
@@ -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();
diff --git a/frontend/src/store/useVideoStore.jsx b/frontend/src/store/useVideoStore.jsx
index 4aa721ed..1ac21542 100644
--- a/frontend/src/store/useVideoStore.jsx
+++ b/frontend/src/store/useVideoStore.jsx
@@ -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,
}),
}));