diff --git a/frontend/src/pages/VODs.jsx b/frontend/src/pages/VODs.jsx
index 303cd147..5fa99afd 100644
--- a/frontend/src/pages/VODs.jsx
+++ b/frontend/src/pages/VODs.jsx
@@ -1,26 +1,26 @@
import React, { useState, useEffect } from 'react';
import {
-Box,
-Button,
-Card,
-Flex,
-Group,
-Image,
-Text,
-Title,
-Select,
-TextInput,
-Pagination,
-Badge,
-Grid,
-Loader,
-Stack,
-SegmentedControl,
-ActionIcon,
-Modal,
-Tabs,
-Table,
-Divider,
+ Box,
+ Button,
+ Card,
+ Flex,
+ Group,
+ Image,
+ Text,
+ Title,
+ Select,
+ TextInput,
+ Pagination,
+ Badge,
+ Grid,
+ Loader,
+ Stack,
+ SegmentedControl,
+ ActionIcon,
+ Modal,
+ Tabs,
+ Table,
+ Divider,
} from '@mantine/core';
import { Search, Play, Calendar, Clock, Star } from 'lucide-react';
import { useDisclosure } from '@mantine/hooks';
@@ -29,575 +29,575 @@ import useVideoStore from '../store/useVideoStore';
import useSettingsStore from '../store/settings';
const imdbUrl = (imdb_id) =>
-imdb_id ? `https://www.imdb.com/title/${imdb_id}` : '';
+ imdb_id ? `https://www.imdb.com/title/${imdb_id}` : '';
const tmdbUrl = (tmdb_id, type = 'movie') =>
-tmdb_id ? `https://www.themoviedb.org/${type}/${tmdb_id}` : '';
+ tmdb_id ? `https://www.themoviedb.org/${type}/${tmdb_id}` : '';
const formatDuration = (seconds) => {
-if (!seconds) return '';
-const hours = Math.floor(seconds / 3600);
-const mins = Math.floor((seconds % 3600) / 60);
-const secs = seconds % 60;
-return hours > 0 ? `${hours}h ${mins}m` : `${mins}m ${secs}s`;
+ if (!seconds) return '';
+ const hours = Math.floor(seconds / 3600);
+ const mins = Math.floor((seconds % 3600) / 60);
+ const secs = seconds % 60;
+ return hours > 0 ? `${hours}h ${mins}m` : `${mins}m ${secs}s`;
};
const formatStreamLabel = (relation) => {
-// Create a label for the stream that includes provider name and stream-specific info
-const provider = relation.m3u_account.name;
-const streamId = relation.stream_id;
+ // Create a label for the stream that includes provider name and stream-specific info
+ const provider = relation.m3u_account.name;
+ const streamId = relation.stream_id;
-// Try to extract quality info - prioritizing the new quality_info field from backend
-let qualityInfo = '';
+ // Try to extract quality info - prioritizing the new quality_info field from backend
+ let qualityInfo = '';
-// 1. Check the new quality_info field from backend (PRIMARY)
-if (relation.quality_info) {
+ // 1. Check the new quality_info field from backend (PRIMARY)
+ if (relation.quality_info) {
if (relation.quality_info.quality) {
- qualityInfo = ` - ${relation.quality_info.quality}`;
+ qualityInfo = ` - ${relation.quality_info.quality}`;
} else if (relation.quality_info.resolution) {
- qualityInfo = ` - ${relation.quality_info.resolution}`;
+ qualityInfo = ` - ${relation.quality_info.resolution}`;
} else if (relation.quality_info.bitrate) {
- qualityInfo = ` - ${relation.quality_info.bitrate}`;
+ qualityInfo = ` - ${relation.quality_info.bitrate}`;
}
-}
+ }
-// 2. Fallback: Check custom_properties detailed info structure
-if (qualityInfo === '' && relation.custom_properties) {
+ // 2. Fallback: Check custom_properties detailed info structure
+ if (qualityInfo === '' && relation.custom_properties) {
const props = relation.custom_properties;
// Check detailed_info structure (where the real data is!)
if (qualityInfo === '' && props.detailed_info) {
- const detailedInfo = props.detailed_info;
+ const detailedInfo = props.detailed_info;
- // Extract from video resolution
- if (
+ // Extract from video resolution
+ if (
detailedInfo.video &&
detailedInfo.video.width &&
detailedInfo.video.height
- ) {
+ ) {
const width = detailedInfo.video.width;
const height = detailedInfo.video.height;
// Prioritize width for quality detection (handles ultrawide/cinematic aspect ratios)
if (width >= 3840) {
- qualityInfo = ' - 4K';
+ qualityInfo = ' - 4K';
} else if (width >= 1920) {
- qualityInfo = ' - 1080p';
+ qualityInfo = ' - 1080p';
} else if (width >= 1280) {
- qualityInfo = ' - 720p';
+ qualityInfo = ' - 720p';
} else if (width >= 854) {
- qualityInfo = ' - 480p';
+ qualityInfo = ' - 480p';
} else {
- qualityInfo = ` - ${width}x${height}`;
+ qualityInfo = ` - ${width}x${height}`;
}
- }
+ }
- // Extract from movie name in detailed_info
- if (qualityInfo === '' && detailedInfo.name) {
+ // Extract from movie name in detailed_info
+ if (qualityInfo === '' && detailedInfo.name) {
const name = detailedInfo.name;
if (name.includes('4K') || name.includes('2160p')) {
- qualityInfo = ' - 4K';
+ qualityInfo = ' - 4K';
} else if (name.includes('1080p') || name.includes('FHD')) {
- qualityInfo = ' - 1080p';
+ qualityInfo = ' - 1080p';
} else if (name.includes('720p') || name.includes('HD')) {
- qualityInfo = ' - 720p';
+ qualityInfo = ' - 720p';
} else if (name.includes('480p')) {
- qualityInfo = ' - 480p';
+ qualityInfo = ' - 480p';
}
- }
+ }
- // Extract from bitrate in detailed_info
- if (
+ // Extract from bitrate in detailed_info
+ if (
qualityInfo === '' &&
detailedInfo.bitrate &&
detailedInfo.bitrate > 0
- ) {
+ ) {
const bitrate = detailedInfo.bitrate;
if (bitrate >= 6000) {
- qualityInfo = ' - 4K';
+ qualityInfo = ' - 4K';
} else if (bitrate >= 3000) {
- qualityInfo = ' - 1080p';
+ qualityInfo = ' - 1080p';
} else if (bitrate >= 1500) {
- qualityInfo = ' - 720p';
+ qualityInfo = ' - 720p';
} else {
- qualityInfo = ` - ${Math.round(bitrate / 1000)}Mbps`;
+ qualityInfo = ` - ${Math.round(bitrate / 1000)}Mbps`;
}
- }
+ }
}
// Check basic_data structure as another fallback
if (qualityInfo === '' && props.basic_data && props.basic_data.name) {
- const name = props.basic_data.name;
- if (name.includes('4K') || name.includes('2160p')) {
+ const name = props.basic_data.name;
+ if (name.includes('4K') || name.includes('2160p')) {
qualityInfo = ' - 4K';
- } else if (name.includes('1080p') || name.includes('FHD')) {
+ } else if (name.includes('1080p') || name.includes('FHD')) {
qualityInfo = ' - 1080p';
- } else if (name.includes('720p') || name.includes('HD')) {
+ } else if (name.includes('720p') || name.includes('HD')) {
qualityInfo = ' - 720p';
- } else if (name.includes('480p')) {
+ } else if (name.includes('480p')) {
qualityInfo = ' - 480p';
+ }
}
- }
-}
+ }
-// 3. Final fallback: Try to extract from movie/episode name
-if (qualityInfo === '') {
+ // 3. Final fallback: Try to extract from movie/episode name
+ if (qualityInfo === '') {
const content = relation.movie || relation.episode;
if (content && content.name) {
- const name = content.name;
- if (name.includes('4K') || name.includes('2160p')) {
+ const name = content.name;
+ if (name.includes('4K') || name.includes('2160p')) {
qualityInfo = ' - 4K';
- } else if (name.includes('1080p') || name.includes('FHD')) {
+ } else if (name.includes('1080p') || name.includes('FHD')) {
qualityInfo = ' - 1080p';
- } else if (name.includes('720p') || name.includes('HD')) {
+ } else if (name.includes('720p') || name.includes('HD')) {
qualityInfo = ' - 720p';
- } else if (name.includes('480p')) {
+ } else if (name.includes('480p')) {
qualityInfo = ' - 480p';
+ }
}
- }
-}
+ }
-// If no quality info and multiple streams from same provider, show stream ID
-const finalLabel = `${provider}${qualityInfo}${qualityInfo === '' && streamId ? ` - Stream ${streamId}` : ''}`;
-return finalLabel;
+ // If no quality info and multiple streams from same provider, show stream ID
+ const finalLabel = `${provider}${qualityInfo}${qualityInfo === '' && streamId ? ` - Stream ${streamId}` : ''}`;
+ return finalLabel;
};
// Helper function to get technical details from selected provider or fallback to default VOD
const getTechnicalDetails = (selectedProvider, defaultVOD) => {
-let source = defaultVOD; // Default fallback
+ let source = defaultVOD; // Default fallback
-// If a provider is selected, try to get technical details from various locations
-if (selectedProvider) {
+ // If a provider is selected, try to get technical details from various locations
+ if (selectedProvider) {
// 1. First try the movie/episode relation content
const content = selectedProvider.movie || selectedProvider.episode;
if (content && (content.bitrate || content.video || content.audio)) {
- source = content;
+ source = content;
}
// 2. Try technical details directly on the relation object
else if (
- selectedProvider.bitrate ||
- selectedProvider.video ||
- selectedProvider.audio
+ selectedProvider.bitrate ||
+ selectedProvider.video ||
+ selectedProvider.audio
) {
- source = selectedProvider;
+ source = selectedProvider;
}
// 3. Try to extract from custom_properties detailed_info (where quality data is stored)
else if (selectedProvider.custom_properties?.detailed_info) {
- const detailedInfo = selectedProvider.custom_properties.detailed_info;
+ const detailedInfo = selectedProvider.custom_properties.detailed_info;
- // Create a synthetic source from detailed_info
- const syntheticSource = {
+ // Create a synthetic source from detailed_info
+ const syntheticSource = {
bitrate: detailedInfo.bitrate || null,
video: detailedInfo.video || null,
audio: detailedInfo.audio || null,
- };
+ };
- if (
+ if (
syntheticSource.bitrate ||
syntheticSource.video ||
syntheticSource.audio
- ) {
+ ) {
source = syntheticSource;
+ }
}
- }
-}
+ }
-return {
+ return {
bitrate: source?.bitrate,
video: source?.video,
audio: source?.audio,
-};
+ };
};
const VODCard = ({ vod, onClick }) => {
-const isEpisode = vod.type === 'episode';
+ const isEpisode = vod.type === 'episode';
-const getDisplayTitle = () => {
+ const getDisplayTitle = () => {
if (isEpisode && vod.series) {
- const seasonEp =
+ const seasonEp =
vod.season_number && vod.episode_number
- ? `S${vod.season_number.toString().padStart(2, '0')}E${vod.episode_number.toString().padStart(2, '0')}`
- : '';
- return (
+ ? `S${vod.season_number.toString().padStart(2, '0')}E${vod.episode_number.toString().padStart(2, '0')}`
+ : '';
+ return (
-
+
{vod.series.name}
-
-
+
+
{seasonEp} - {vod.name}
-
+
- );
+ );
}
return {vod.name};
-};
+ };
-const handleCardClick = async () => {
+ const handleCardClick = async () => {
// Just pass the basic vod info to the parent handler
onClick(vod);
-};
+ };
-return (
+ return (
-
+
- {vod.logo?.url ? (
+ {vod.logo?.url ? (
- ) : (
+ ) : (
-
+
- )}
+ )}
- {
- e.stopPropagation();
- onClick(vod);
+ e.stopPropagation();
+ onClick(vod);
}}
- >
+ >
-
+
-
+ >
{isEpisode ? 'Episode' : 'Movie'}
-
+
-
+
-
+
{getDisplayTitle()}
- {vod.year && (
+ {vod.year && (
-
-
+
+
{vod.year}
-
+
- )}
+ )}
- {vod.duration && (
+ {vod.duration && (
-
-
+
+
{formatDuration(vod.duration_secs)}
-
+
- )}
+ )}
- {vod.rating && (
+ {vod.rating && (
-
-
+
+
{vod.rating}
-
+
- )}
+ )}
{vod.genre && (
-
+
{vod.genre}
-
+
)}
-
+
-);
+ );
};
const SeriesCard = ({ series, onClick }) => {
-return (
+ return (
onClick(series)}
+ shadow="sm"
+ padding="md"
+ radius="md"
+ withBorder
+ style={{ cursor: 'pointer', backgroundColor: '#27272A' }}
+ onClick={() => onClick(series)}
>
-
+
- {series.logo?.url ? (
+ {series.logo?.url ? (
- ) : (
+ ) : (
-
+
- )}
- {/* Add Series badge in the same position as Movie badge */}
-
+ >
Series
-
+
-
+
-
+
{series.name}
- {series.year && (
+ {series.year && (
-
-
+
+
{series.year}
-
+
- )}
- {series.rating && (
+ )}
+ {series.rating && (
-
-
+
+
{series.rating}
-
+
- )}
+ )}
{series.genre && (
-
+
{series.genre}
-
+
)}
-
+
-);
+ );
};
const SeriesModal = ({ series, opened, onClose }) => {
-const { fetchSeriesInfo, fetchSeriesProviders } = useVODStore();
-const showVideo = useVideoStore((s) => s.showVideo);
-const env_mode = useSettingsStore((s) => s.environment.env_mode);
-const [detailedSeries, setDetailedSeries] = useState(null);
-const [loadingDetails, setLoadingDetails] = useState(false);
-const [activeTab, setActiveTab] = useState(null);
-const [expandedEpisode, setExpandedEpisode] = useState(null);
-const [trailerModalOpened, setTrailerModalOpened] = useState(false);
-const [trailerUrl, setTrailerUrl] = useState('');
-const [providers, setProviders] = useState([]);
-const [selectedProvider, setSelectedProvider] = useState(null);
-const [loadingProviders, setLoadingProviders] = useState(false);
+ const { fetchSeriesInfo, fetchSeriesProviders } = useVODStore();
+ const showVideo = useVideoStore((s) => s.showVideo);
+ const env_mode = useSettingsStore((s) => s.environment.env_mode);
+ const [detailedSeries, setDetailedSeries] = useState(null);
+ const [loadingDetails, setLoadingDetails] = useState(false);
+ const [activeTab, setActiveTab] = useState(null);
+ const [expandedEpisode, setExpandedEpisode] = useState(null);
+ const [trailerModalOpened, setTrailerModalOpened] = useState(false);
+ const [trailerUrl, setTrailerUrl] = useState('');
+ const [providers, setProviders] = useState([]);
+ const [selectedProvider, setSelectedProvider] = useState(null);
+ const [loadingProviders, setLoadingProviders] = useState(false);
-useEffect(() => {
+ useEffect(() => {
if (opened && series) {
- // Fetch detailed series info which now includes episodes
- setLoadingDetails(true);
- fetchSeriesInfo(series.id)
+ // Fetch detailed series info which now includes episodes
+ setLoadingDetails(true);
+ fetchSeriesInfo(series.id)
.then((details) => {
- setDetailedSeries(details);
- // Check if episodes were fetched
- if (!details.episodes_fetched) {
+ setDetailedSeries(details);
+ // Check if episodes were fetched
+ if (!details.episodes_fetched) {
// Episodes not yet fetched, may need to wait for background fetch
- }
+ }
})
.catch((error) => {
- console.warn(
+ console.warn(
'Failed to fetch series details, using basic info:',
error
- );
- setDetailedSeries(series); // Fallback to basic data
+ );
+ setDetailedSeries(series); // Fallback to basic data
})
.finally(() => {
- setLoadingDetails(false);
+ setLoadingDetails(false);
});
- // Fetch available providers
- setLoadingProviders(true);
- fetchSeriesProviders(series.id)
+ // Fetch available providers
+ setLoadingProviders(true);
+ fetchSeriesProviders(series.id)
.then((providersData) => {
- setProviders(providersData);
- // Set the first provider as default if none selected
- if (providersData.length > 0 && !selectedProvider) {
+ setProviders(providersData);
+ // Set the first provider as default if none selected
+ if (providersData.length > 0 && !selectedProvider) {
setSelectedProvider(providersData[0]);
- }
+ }
})
.catch((error) => {
- console.error('Failed to fetch series providers:', error);
- setProviders([]);
+ console.error('Failed to fetch series providers:', error);
+ setProviders([]);
})
.finally(() => {
- setLoadingProviders(false);
+ setLoadingProviders(false);
});
}
-}, [opened, series, fetchSeriesInfo, fetchSeriesProviders, selectedProvider]);
+ }, [opened, series, fetchSeriesInfo, fetchSeriesProviders, selectedProvider]);
-useEffect(() => {
+ useEffect(() => {
if (!opened) {
- setDetailedSeries(null);
- setLoadingDetails(false);
- setProviders([]);
- setSelectedProvider(null);
- setLoadingProviders(false);
+ setDetailedSeries(null);
+ setLoadingDetails(false);
+ setProviders([]);
+ setSelectedProvider(null);
+ setLoadingProviders(false);
}
-}, [opened]);
+ }, [opened]);
-// Get episodes from the store based on the series ID
-const seriesEpisodes = React.useMemo(() => {
+ // Get episodes from the store based on the series ID
+ const seriesEpisodes = React.useMemo(() => {
if (!detailedSeries) return [];
// Try to get episodes from the fetched data
if (detailedSeries.episodesList) {
- return detailedSeries.episodesList.sort((a, b) => {
+ return detailedSeries.episodesList.sort((a, b) => {
if (a.season_number !== b.season_number) {
- return (a.season_number || 0) - (b.season_number || 0);
+ return (a.season_number || 0) - (b.season_number || 0);
}
return (a.episode_number || 0) - (b.episode_number || 0);
- });
+ });
}
// If no episodes in detailed series, return empty array
return [];
-}, [detailedSeries]);
+ }, [detailedSeries]);
-// Group episodes by season
-const episodesBySeason = React.useMemo(() => {
+ // Group episodes by season
+ const episodesBySeason = React.useMemo(() => {
const grouped = {};
seriesEpisodes.forEach((episode) => {
- const season = episode.season_number || 1;
- if (!grouped[season]) {
+ const season = episode.season_number || 1;
+ if (!grouped[season]) {
grouped[season] = [];
- }
- grouped[season].push(episode);
+ }
+ grouped[season].push(episode);
});
return grouped;
-}, [seriesEpisodes]);
+ }, [seriesEpisodes]);
-// Get available seasons sorted
-const seasons = React.useMemo(() => {
+ // Get available seasons sorted
+ const seasons = React.useMemo(() => {
return Object.keys(episodesBySeason)
- .map(Number)
- .sort((a, b) => a - b);
-}, [episodesBySeason]);
+ .map(Number)
+ .sort((a, b) => a - b);
+ }, [episodesBySeason]);
-// Update active tab when seasons change or modal opens
-React.useEffect(() => {
+ // Update active tab when seasons change or modal opens
+ React.useEffect(() => {
if (seasons.length > 0) {
- if (
+ if (
!activeTab ||
!seasons.includes(parseInt(activeTab.replace('season-', '')))
- ) {
+ ) {
setActiveTab(`season-${seasons[0]}`);
+ }
}
- }
-}, [seasons, activeTab]);
+ }, [seasons, activeTab]);
-// Reset tab when modal closes
-React.useEffect(() => {
+ // Reset tab when modal closes
+ React.useEffect(() => {
if (!opened) {
- setActiveTab(null);
+ setActiveTab(null);
}
-}, [opened]);
+ }, [opened]);
-const handlePlayEpisode = (episode) => {
+ const handlePlayEpisode = (episode) => {
let streamUrl = `/proxy/vod/episode/${episode.uuid}`;
// Add selected provider as query parameter if available
if (selectedProvider) {
- // Use stream_id for most specific selection, fallback to account_id
- if (selectedProvider.stream_id) {
+ // Use stream_id for most specific selection, fallback to account_id
+ if (selectedProvider.stream_id) {
streamUrl += `?stream_id=${encodeURIComponent(selectedProvider.stream_id)}`;
- } else {
+ } else {
streamUrl += `?m3u_account_id=${selectedProvider.m3u_account.id}`;
- }
+ }
}
if (env_mode === 'dev') {
- streamUrl = `${window.location.protocol}//${window.location.hostname}:5656${streamUrl}`;
+ streamUrl = `${window.location.protocol}//${window.location.hostname}:5656${streamUrl}`;
} else {
- streamUrl = `${window.location.origin}${streamUrl}`;
+ streamUrl = `${window.location.origin}${streamUrl}`;
}
showVideo(streamUrl, 'vod', episode);
-};
+ };
-const handleEpisodeRowClick = (episode) => {
+ const handleEpisodeRowClick = (episode) => {
setExpandedEpisode(expandedEpisode === episode.id ? null : episode.id);
-};
+ };
-// Helper to get embeddable YouTube URL
-const getEmbedUrl = (url) => {
+ // Helper to get embeddable YouTube URL
+ const getEmbedUrl = (url) => {
if (!url) return '';
// Accepts full YouTube URLs or just IDs
const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
const videoId = match ? match[1] : url;
return `https://www.youtube.com/embed/${videoId}`;
-};
+ };
-if (!series) return null;
+ if (!series) return null;
-// Use detailed data if available, otherwise use basic series data
-const displaySeries = detailedSeries || series;
+ // Use detailed data if available, otherwise use basic series data
+ const displaySeries = detailedSeries || series;
-return (
+ return (
<>
-
+ >
- {/* Backdrop image as background */}
- {displaySeries.backdrop_path &&
+ {/* Backdrop image as background */}
+ {displaySeries.backdrop_path &&
displaySeries.backdrop_path.length > 0 && (
- <>
+ <>
{/* Overlay for readability */}
- >
+ >
)}
- {/* Modal content above backdrop */}
-
+ {/* Modal content above backdrop */}
+
- {loadingDetails && (
+ {loadingDetails && (
-
-
+
+
Loading series details and episodes...
-
+
- )}
+ )}
- {/* Series poster and basic info */}
-
+ {/* Series poster and basic info */}
+
{displaySeries.series_image || displaySeries.logo?.url ? (
-
+
-
+
) : (
-
+ >
-
+
)}
- {displaySeries.name}
+ {displaySeries.name}
- {/* Original name if different */}
- {displaySeries.o_name &&
+ {/* Original name if different */}
+ {displaySeries.o_name &&
displaySeries.o_name !== displaySeries.name && (
-
+ >
Original: {displaySeries.o_name}
-
+
)}
-
+
{displaySeries.year && (
- {displaySeries.year}
+ {displaySeries.year}
)}
{displaySeries.rating && (
- {displaySeries.rating}
+ {displaySeries.rating}
)}
{displaySeries.age && (
- {displaySeries.age}
+ {displaySeries.age}
)}
Series
{displaySeries.episode_count && (
-
+
{displaySeries.episode_count} episodes
-
+
)}
{/* imdb_id and tmdb_id badges */}
{displaySeries.imdb_id && (
-
+ >
IMDb
-
+
)}
{displaySeries.tmdb_id && (
-
+ >
TMDb
-
+
)}
-
+
- {/* Release date */}
- {displaySeries.release_date && (
+ {/* Release date */}
+ {displaySeries.release_date && (
- Release Date:{' '}
- {displaySeries.release_date}
+ Release Date:{' '}
+ {displaySeries.release_date}
- )}
+ )}
- {displaySeries.genre && (
+ {displaySeries.genre && (
- Genre: {displaySeries.genre}
+ Genre: {displaySeries.genre}
- )}
+ )}
- {displaySeries.director && (
+ {displaySeries.director && (
- Director: {displaySeries.director}
+ Director: {displaySeries.director}
- )}
+ )}
- {displaySeries.cast && (
+ {displaySeries.cast && (
- Cast: {displaySeries.cast}
+ Cast: {displaySeries.cast}
- )}
+ )}
- {displaySeries.country && (
+ {displaySeries.country && (
- Country: {displaySeries.country}
+ Country: {displaySeries.country}
- )}
+ )}
- {/* Description */}
- {displaySeries.description && (
+ {/* Description */}
+ {displaySeries.description && (
-
+
Description
-
- {displaySeries.description}
+
+ {displaySeries.description}
- )}
+ )}
- {/* Watch Trailer button if available */}
- {displaySeries.youtube_trailer && (
+ {/* Watch Trailer button if available */}
+ {displaySeries.youtube_trailer && (
- )}
+ )}
-
+
- {/* Provider Information */}
-
+ {/* Provider Information */}
+
- Stream Selection
- {loadingProviders && (
+ Stream Selection
+ {loadingProviders && (
- )}
+ )}
{providers.length === 0 &&
!loadingProviders &&
displaySeries.m3u_account ? (
-
+
- {displaySeries.m3u_account.name}
+ {displaySeries.m3u_account.name}
{displaySeries.m3u_account.account_type && (
-
+
{displaySeries.m3u_account.account_type === 'XC'
- ? 'Xtream Codes'
- : 'Standard M3U'}
-
+ ? 'Xtream Codes'
+ : 'Standard M3U'}
+
)}
-
+
) : providers.length === 1 ? (
-
+
- {providers[0].m3u_account.name}
+ {providers[0].m3u_account.name}
{providers[0].m3u_account.account_type && (
-
+
{providers[0].m3u_account.account_type === 'XC'
- ? 'Xtream Codes'
- : 'Standard M3U'}
-
+ ? 'Xtream Codes'
+ : 'Standard M3U'}
+
)}
{providers[0].stream_id && (
-
+
Stream {providers[0].stream_id}
-
+
)}
-
+
) : providers.length > 1 ? (
-
+
-
+
-
+
Episodes
{seriesEpisodes.length > 0 && <> ({seriesEpisodes.length})>}
-
+
- {loadingDetails ? (
+ {loadingDetails ? (
-
+
- ) : seasons.length > 0 ? (
+ ) : seasons.length > 0 ? (
-
+
{seasons.map((season) => (
-
+
Season {season}
-
+
))}
-
+
- {seasons.map((season) => (
+ {seasons.map((season) => (
-
+
-
+
Ep
Title
- Duration
+ Duration
Date
- Action
+ Action
-
+
- {episodesBySeason[season]?.map((episode) => (
+ {episodesBySeason[season]?.map((episode) => (
- handleEpisodeRowClick(episode)}
- >
+ >
-
+
{episode.episode_number || '?'}
-
+
-
+
- {episode.name}
+ {episode.name}
{episode.genre && (
-
+
{episode.genre}
-
+
)}
-
+
-
+
{formatDuration(episode.duration_secs)}
-
+
-
+
{episode.air_date
- ? new Date(
- episode.air_date
+ ? new Date(
+ episode.air_date
).toLocaleDateString()
- : 'N/A'}
-
+ : 'N/A'}
+
- 0 && !selectedProvider
+ providers.length > 0 && !selectedProvider
}
onClick={(e) => {
- e.stopPropagation();
- handlePlayEpisode(episode);
+ e.stopPropagation();
+ handlePlayEpisode(episode);
}}
- >
+ >
-
+
-
- {expandedEpisode === episode.id && (
+
+ {expandedEpisode === episode.id && (
-
+ >
- {/* Episode Image and Description Row */}
-
+ {/* Episode Image and Description Row */}
+
{/* Episode Image */}
{episode.movie_image && (
-
+
-
+
)}
{/* Episode Description */}
- {episode.description && (
+ {episode.description && (
-
+ >
Description
-
-
+
+
{episode.description}
-
+
- )}
+ )}
-
+
- {/* Additional Episode Details */}
-
+ {/* Additional Episode Details */}
+
{episode.rating && (
-
+
- Rating
+ Rating
- {episode.rating}
+ {episode.rating}
-
+
)}
{/* IMDb and TMDb badges for episode */}
{(episode.imdb_id ||
- displaySeries.tmdb_id) && (
-
+ displaySeries.tmdb_id) && (
+
- Links
+ Links
{episode.imdb_id && (
-
+ >
IMDb
-
+
)}
{displaySeries.tmdb_id && (
-
+ >
TMDb
-
+
)}
-
+
)}
{episode.director && (
-
+
- Director
+ Director
- {episode.director}
+ {episode.director}
-
+
)}
{episode.actors && (
-
+
- Cast
+ Cast
- {episode.actors}
+ {episode.actors}
-
+
)}
-
+
- {/* Technical Details */}
- {(episode.bitrate ||
+ {/* Technical Details */}
+ {(episode.bitrate ||
episode.video ||
episode.audio) && (
-
+ >
Technical Details
-
-
+
+
{episode.bitrate &&
- episode.bitrate > 0 && (
+ episode.bitrate > 0 && (
- Bitrate:{' '}
- {episode.bitrate} kbps
+ Bitrate:{' '}
+ {episode.bitrate} kbps
- )}
+ )}
{episode.video &&
- Object.keys(episode.video)
+ Object.keys(episode.video)
.length > 0 && (
- Video:{' '}
- {episode.video
+ Video:{' '}
+ {episode.video
.codec_long_name ||
episode.video.codec_name}
- {episode.video.width &&
- episode.video.height
+ {episode.video.width &&
+ episode.video.height
? `, ${episode.video.width}x${episode.video.height}`
: ''}
- )}
+ )}
{episode.audio &&
- Object.keys(episode.audio)
+ Object.keys(episode.audio)
.length > 0 && (
- Audio:{' '}
- {episode.audio
+ Audio:{' '}
+ {episode.audio
.codec_long_name ||
episode.audio.codec_name}
- {episode.audio.channels
+ {episode.audio.channels
? `, ${episode.audio.channels} channels`
: ''}
- )}
-
+ )}
+
- )}
+ )}
- {/* Provider Information */}
- {episode.m3u_account && (
+ {/* Provider Information */}
+ {episode.m3u_account && (
-
+ >
Provider:
-
-
+
+ >
{episode.m3u_account.name ||
- episode.m3u_account}
-
+ episode.m3u_account}
+
- )}
+ )}
-
+
- )}
+ )}
- ))}
+ ))}
-
+
- ))}
+ ))}
- ) : (
+ ) : (
- No episodes found for this series.
+ No episodes found for this series.
- )}
+ )}
+
-
-
+
- {/* YouTube Trailer Modal */}
- setTrailerModalOpened(false)}
title="Trailer"
size="xl"
centered
- >
+ >
- {trailerUrl && (
+ {trailerUrl && (
- )}
+ )}
-
+
>
-);
+ );
};
const VODModal = ({ vod, opened, onClose }) => {
-const [detailedVOD, setDetailedVOD] = useState(null);
-const [loadingDetails, setLoadingDetails] = useState(false);
-const [trailerModalOpened, setTrailerModalOpened] = useState(false);
-const [trailerUrl, setTrailerUrl] = useState('');
-const [providers, setProviders] = useState([]);
-const [selectedProvider, setSelectedProvider] = useState(null);
-const [loadingProviders, setLoadingProviders] = useState(false);
-const { fetchMovieDetailsFromProvider, fetchMovieProviders } = useVODStore();
-const showVideo = useVideoStore((s) => s.showVideo);
-const env_mode = useSettingsStore((s) => s.environment.env_mode);
+ const [detailedVOD, setDetailedVOD] = useState(null);
+ const [loadingDetails, setLoadingDetails] = useState(false);
+ const [trailerModalOpened, setTrailerModalOpened] = useState(false);
+ const [trailerUrl, setTrailerUrl] = useState('');
+ const [providers, setProviders] = useState([]);
+ const [selectedProvider, setSelectedProvider] = useState(null);
+ const [loadingProviders, setLoadingProviders] = useState(false);
+ const { fetchMovieDetailsFromProvider, fetchMovieProviders } = useVODStore();
+ const showVideo = useVideoStore((s) => s.showVideo);
+ const env_mode = useSettingsStore((s) => s.environment.env_mode);
-useEffect(() => {
+ useEffect(() => {
if (opened && vod) {
- // Fetch detailed VOD info if not already loaded
- if (!detailedVOD) {
+ // Fetch detailed VOD info if not already loaded
+ if (!detailedVOD) {
setLoadingDetails(true);
fetchMovieDetailsFromProvider(vod.id)
- .then((details) => {
+ .then((details) => {
setDetailedVOD(details);
- })
- .catch((error) => {
+ })
+ .catch((error) => {
console.warn(
- 'Failed to fetch provider details, using basic info:',
- error
+ 'Failed to fetch provider details, using basic info:',
+ error
);
setDetailedVOD(vod); // Fallback to basic data
- })
- .finally(() => {
+ })
+ .finally(() => {
setLoadingDetails(false);
- });
- }
+ });
+ }
- // Fetch available providers
- setLoadingProviders(true);
- fetchMovieProviders(vod.id)
+ // Fetch available providers
+ setLoadingProviders(true);
+ fetchMovieProviders(vod.id)
.then((providersData) => {
- setProviders(providersData);
- // Set the first provider as default if none selected
- if (providersData.length > 0 && !selectedProvider) {
+ setProviders(providersData);
+ // Set the first provider as default if none selected
+ if (providersData.length > 0 && !selectedProvider) {
setSelectedProvider(providersData[0]);
- }
+ }
})
.catch((error) => {
- console.error('Failed to fetch providers:', error);
- setProviders([]);
+ console.error('Failed to fetch providers:', error);
+ setProviders([]);
})
.finally(() => {
- setLoadingProviders(false);
+ setLoadingProviders(false);
});
}
-}, [
+ }, [
opened,
vod,
detailedVOD,
fetchMovieDetailsFromProvider,
fetchMovieProviders,
selectedProvider,
-]);
+ ]);
-useEffect(() => {
+ useEffect(() => {
if (!opened) {
- setDetailedVOD(null);
- setLoadingDetails(false);
- setTrailerModalOpened(false);
- setTrailerUrl('');
- setProviders([]);
- setSelectedProvider(null);
- setLoadingProviders(false);
+ setDetailedVOD(null);
+ setLoadingDetails(false);
+ setTrailerModalOpened(false);
+ setTrailerUrl('');
+ setProviders([]);
+ setSelectedProvider(null);
+ setLoadingProviders(false);
}
-}, [opened]);
+ }, [opened]);
-const handlePlayVOD = () => {
+ const handlePlayVOD = () => {
const vodToPlay = detailedVOD || vod;
if (!vodToPlay) return;
@@ -1290,714 +1290,714 @@ const handlePlayVOD = () => {
// Add selected provider as query parameter if available
if (selectedProvider) {
- // Use stream_id for most specific selection, fallback to account_id
- if (selectedProvider.stream_id) {
+ // Use stream_id for most specific selection, fallback to account_id
+ if (selectedProvider.stream_id) {
streamUrl += `?stream_id=${encodeURIComponent(selectedProvider.stream_id)}`;
- } else {
+ } else {
streamUrl += `?m3u_account_id=${selectedProvider.m3u_account.id}`;
- }
+ }
}
if (env_mode === 'dev') {
- streamUrl = `${window.location.protocol}//${window.location.hostname}:5656${streamUrl}`;
+ streamUrl = `${window.location.protocol}//${window.location.hostname}:5656${streamUrl}`;
} else {
- streamUrl = `${window.location.origin}${streamUrl}`;
+ streamUrl = `${window.location.origin}${streamUrl}`;
}
showVideo(streamUrl, 'vod', vodToPlay);
-};
+ };
-// Helper to get embeddable YouTube URL
-const getEmbedUrl = (url) => {
+ // Helper to get embeddable YouTube URL
+ const getEmbedUrl = (url) => {
if (!url) return '';
// Accepts full YouTube URLs or just IDs
const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
const videoId = match ? match[1] : url;
return `https://www.youtube.com/embed/${videoId}`;
-};
+ };
-if (!vod) return null;
+ if (!vod) return null;
-// Use detailed data if available, otherwise use basic vod data
-const displayVOD = detailedVOD || vod;
+ // Use detailed data if available, otherwise use basic vod data
+ const displayVOD = detailedVOD || vod;
-return (
+ return (
<>
-
+ >
- {/* Backdrop image as background */}
- {displayVOD.backdrop_path && displayVOD.backdrop_path.length > 0 && (
+ {/* Backdrop image as background */}
+ {displayVOD.backdrop_path && displayVOD.backdrop_path.length > 0 && (
<>
-
- {/* Overlay for readability */}
-
+ {/* Overlay for readability */}
+
+ />
>
- )}
- {/* Modal content above backdrop */}
-
+ )}
+ {/* Modal content above backdrop */}
+
- {loadingDetails && (
+ {loadingDetails && (
-
-
+
+
Loading additional details...
-
+
- )}
+ )}
- {/* Movie poster and basic info */}
-
+ {/* Movie poster and basic info */}
+
{/* Use movie_image or logo */}
{displayVOD.movie_image || displayVOD.logo?.url ? (
-
+
-
+
) : (
-
+ >
-
+
)}
- {displayVOD.name}
+ {displayVOD.name}
- {/* Original name if different */}
- {displayVOD.o_name &&
+ {/* Original name if different */}
+ {displayVOD.o_name &&
displayVOD.o_name !== displayVOD.name && (
-
+ >
Original: {displayVOD.o_name}
-
+
)}
-
+
{displayVOD.year && (
- {displayVOD.year}
+ {displayVOD.year}
)}
{displayVOD.duration_secs && (
-
+
{formatDuration(displayVOD.duration_secs)}
-
+
)}
{displayVOD.rating && (
- {displayVOD.rating}
+ {displayVOD.rating}
)}
{displayVOD.age && (
- {displayVOD.age}
+ {displayVOD.age}
)}
Movie
{/* imdb_id and tmdb_id badges */}
{displayVOD.imdb_id && (
-
+ >
IMDb
-
+
)}
{displayVOD.tmdb_id && (
-
+ >
TMDb
-
+
)}
-
+
- {/* Release date */}
- {displayVOD.release_date && (
+ {/* Release date */}
+ {displayVOD.release_date && (
- Release Date: {displayVOD.release_date}
+ Release Date: {displayVOD.release_date}
- )}
+ )}
- {displayVOD.genre && (
+ {displayVOD.genre && (
- Genre: {displayVOD.genre}
+ Genre: {displayVOD.genre}
- )}
+ )}
- {displayVOD.director && (
+ {displayVOD.director && (
- Director: {displayVOD.director}
+ Director: {displayVOD.director}
- )}
+ )}
- {displayVOD.actors && (
+ {displayVOD.actors && (
- Cast: {displayVOD.actors}
+ Cast: {displayVOD.actors}
- )}
+ )}
- {displayVOD.country && (
+ {displayVOD.country && (
- Country: {displayVOD.country}
+ Country: {displayVOD.country}
- )}
+ )}
- {/* Description */}
- {displayVOD.description && (
+ {/* Description */}
+ {displayVOD.description && (
-
+
Description
-
- {displayVOD.description}
+
+ {displayVOD.description}
- )}
+ )}
- {/* Watch Trailer button at top */}
- {displayVOD.youtube_trailer && (
+ {/* Watch Trailer button at top */}
+ {displayVOD.youtube_trailer && (
- )}
+ )}
-
+
- {/* Provider Information & Play Button Row */}
-
+ {/* Provider Information & Play Button Row */}
+
{/* Provider Selection */}
{providers.length > 0 && (
-
+
- Stream Selection
- {loadingProviders && (
+ Stream Selection
+ {loadingProviders && (
- )}
+ )}
{providers.length === 1 ? (
-
+
- {providers[0].m3u_account.name}
+ {providers[0].m3u_account.name}
{providers[0].m3u_account.account_type && (
-
+
{providers[0].m3u_account.account_type === 'XC'
- ? 'Xtream Codes'
- : 'Standard M3U'}
-
+ ? 'Xtream Codes'
+ : 'Standard M3U'}
+
)}
{providers[0].stream_id && (
-
+
Stream {providers[0].stream_id}
-
+
)}
-
+
) : (
-
+
)}
{/* Fallback provider info if no providers loaded yet */}
{providers.length === 0 &&
- !loadingProviders &&
- vod?.m3u_account && (
+ !loadingProviders &&
+ vod?.m3u_account && (
-
+
Stream Selection
-
-
+
+
- {vod.m3u_account.name}
+ {vod.m3u_account.name}
{vod.m3u_account.account_type && (
-
+
{vod.m3u_account.account_type === 'XC'
- ? 'Xtream Codes'
- : 'Standard M3U'}
-
+ ? 'Xtream Codes'
+ : 'Standard M3U'}
+
)}
-
+
- )}
+ )}
{/* Play Button */}
}
- variant="filled"
- color="blue"
- size="md"
- onClick={handlePlayVOD}
- disabled={providers.length > 0 && !selectedProvider}
+ leftSection={}
+ variant="filled"
+ color="blue"
+ size="md"
+ onClick={handlePlayVOD}
+ disabled={providers.length > 0 && !selectedProvider}
>
- Play Movie
- {selectedProvider && (
+ Play Movie
+ {selectedProvider && (
- (
- {selectedProvider.stream_id
+ (
+ {selectedProvider.stream_id
? `Stream ${selectedProvider.stream_id}`
: selectedProvider.m3u_account.name}
- )
+ )
- )}
+ )}
-
+
- {/* Technical Details */}
- {(() => {
+ {/* Technical Details */}
+ {(() => {
const techDetails = getTechnicalDetails(
- selectedProvider,
- displayVOD
+ selectedProvider,
+ displayVOD
);
const hasDetails =
- techDetails.bitrate || techDetails.video || techDetails.audio;
+ techDetails.bitrate || techDetails.video || techDetails.audio;
return (
- hasDetails && (
+ hasDetails && (
-
+
Technical Details:
{selectedProvider && (
-
+ >
(from {selectedProvider.m3u_account.name}
{selectedProvider.stream_id &&
- ` - Stream ${selectedProvider.stream_id}`}
+ ` - Stream ${selectedProvider.stream_id}`}
)
-
+
)}
-
- {techDetails.bitrate && techDetails.bitrate > 0 && (
+
+ {techDetails.bitrate && techDetails.bitrate > 0 && (
- Bitrate: {techDetails.bitrate} kbps
+ Bitrate: {techDetails.bitrate} kbps
- )}
- {techDetails.video &&
+ )}
+ {techDetails.video &&
Object.keys(techDetails.video).length > 0 && (
-
+
Video:{' '}
{techDetails.video.codec_long_name &&
techDetails.video.codec_long_name !== 'unknown'
- ? techDetails.video.codec_long_name
- : techDetails.video.codec_name}
+ ? techDetails.video.codec_long_name
+ : techDetails.video.codec_name}
{techDetails.video.profile
- ? ` (${techDetails.video.profile})`
- : ''}
+ ? ` (${techDetails.video.profile})`
+ : ''}
{techDetails.video.width && techDetails.video.height
- ? `, ${techDetails.video.width}x${techDetails.video.height}`
- : ''}
+ ? `, ${techDetails.video.width}x${techDetails.video.height}`
+ : ''}
{techDetails.video.display_aspect_ratio
- ? `, Aspect Ratio: ${techDetails.video.display_aspect_ratio}`
- : ''}
+ ? `, Aspect Ratio: ${techDetails.video.display_aspect_ratio}`
+ : ''}
{techDetails.video.bit_rate
- ? `, Bitrate: ${Math.round(Number(techDetails.video.bit_rate) / 1000)} kbps`
- : ''}
+ ? `, Bitrate: ${Math.round(Number(techDetails.video.bit_rate) / 1000)} kbps`
+ : ''}
{techDetails.video.r_frame_rate
- ? `, Frame Rate: ${techDetails.video.r_frame_rate.replace('/', '/')} fps`
- : ''}
+ ? `, Frame Rate: ${techDetails.video.r_frame_rate.replace('/', '/')} fps`
+ : ''}
{techDetails.video.tags?.encoder
- ? `, Encoder: ${techDetails.video.tags.encoder}`
- : ''}
-
+ ? `, Encoder: ${techDetails.video.tags.encoder}`
+ : ''}
+
)}
- {techDetails.audio &&
+ {techDetails.audio &&
Object.keys(techDetails.audio).length > 0 && (
-
+
Audio:{' '}
{techDetails.audio.codec_long_name &&
techDetails.audio.codec_long_name !== 'unknown'
- ? techDetails.audio.codec_long_name
- : techDetails.audio.codec_name}
+ ? techDetails.audio.codec_long_name
+ : techDetails.audio.codec_name}
{techDetails.audio.profile
- ? ` (${techDetails.audio.profile})`
- : ''}
+ ? ` (${techDetails.audio.profile})`
+ : ''}
{techDetails.audio.channel_layout
- ? `, Channels: ${techDetails.audio.channel_layout}`
- : techDetails.audio.channels
+ ? `, Channels: ${techDetails.audio.channel_layout}`
+ : techDetails.audio.channels
? `, Channels: ${techDetails.audio.channels}`
: ''}
{techDetails.audio.sample_rate
- ? `, Sample Rate: ${techDetails.audio.sample_rate} Hz`
- : ''}
+ ? `, Sample Rate: ${techDetails.audio.sample_rate} Hz`
+ : ''}
{techDetails.audio.bit_rate
- ? `, Bitrate: ${Math.round(Number(techDetails.audio.bit_rate) / 1000)} kbps`
- : ''}
+ ? `, Bitrate: ${Math.round(Number(techDetails.audio.bit_rate) / 1000)} kbps`
+ : ''}
{techDetails.audio.tags?.handler_name
- ? `, Handler: ${techDetails.audio.tags.handler_name}`
- : ''}
-
+ ? `, Handler: ${techDetails.audio.tags.handler_name}`
+ : ''}
+
)}
- )
+ )
);
- })()}
- {/* YouTube trailer if available */}
+ })()}
+ {/* YouTube trailer if available */}
+
-
-
- {/* YouTube Trailer Modal */}
-
+ {/* YouTube Trailer Modal */}
+ setTrailerModalOpened(false)}
title="Trailer"
size="xl"
centered
withCloseButton
- >
+ >
- {trailerUrl && (
+ {trailerUrl && (
- )}
+ )}
-
+
>
-);
+ );
};
const MIN_CARD_WIDTH = 260;
const MAX_CARD_WIDTH = 320;
const useCardColumns = () => {
-const [columns, setColumns] = useState(4);
+ const [columns, setColumns] = useState(4);
-useEffect(() => {
+ useEffect(() => {
const calcColumns = () => {
- const container = document.getElementById('vods-container');
- const width = container ? container.offsetWidth : window.innerWidth;
- let colCount = Math.floor(width / MIN_CARD_WIDTH);
- if (colCount < 1) colCount = 1;
- if (colCount > 6) colCount = 6;
- setColumns(colCount);
+ const container = document.getElementById('vods-container');
+ const width = container ? container.offsetWidth : window.innerWidth;
+ let colCount = Math.floor(width / MIN_CARD_WIDTH);
+ if (colCount < 1) colCount = 1;
+ if (colCount > 6) colCount = 6;
+ setColumns(colCount);
};
calcColumns();
window.addEventListener('resize', calcColumns);
return () => window.removeEventListener('resize', calcColumns);
-}, []);
+ }, []);
-return columns;
+ return columns;
};
const VODsPage = () => {
-const movies = useVODStore((s) => s.movies);
-const series = useVODStore((s) => s.series);
-const allCategories = useVODStore((s) => s.categories);
-const filters = useVODStore((s) => s.filters);
-const currentPage = useVODStore((s) => s.currentPage);
-const totalCount = useVODStore((s) => s.totalCount);
-const pageSize = useVODStore((s) => s.pageSize);
-const setFilters = useVODStore((s) => s.setFilters);
-const setPage = useVODStore((s) => s.setPage);
-const fetchMovies = useVODStore((s) => s.fetchMovies);
-const fetchSeries = useVODStore((s) => s.fetchSeries);
-const fetchCategories = useVODStore((s) => s.fetchCategories);
+ const movies = useVODStore((s) => s.movies);
+ const series = useVODStore((s) => s.series);
+ const allCategories = useVODStore((s) => s.categories);
+ const filters = useVODStore((s) => s.filters);
+ const currentPage = useVODStore((s) => s.currentPage);
+ const totalCount = useVODStore((s) => s.totalCount);
+ const pageSize = useVODStore((s) => s.pageSize);
+ const setFilters = useVODStore((s) => s.setFilters);
+ const setPage = useVODStore((s) => s.setPage);
+ const fetchMovies = useVODStore((s) => s.fetchMovies);
+ const fetchSeries = useVODStore((s) => s.fetchSeries);
+ const fetchCategories = useVODStore((s) => s.fetchCategories);
-// const showVideo = useVideoStore((s) => s.showVideo); - removed as unused
-const [selectedSeries, setSelectedSeries] = useState(null);
-const [selectedVOD, setSelectedVOD] = useState(null);
-const [
+ // const showVideo = useVideoStore((s) => s.showVideo); - removed as unused
+ 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 [vodModalOpened, { open: openVODModal, close: closeVODModal }] =
useDisclosure(false);
-const [initialLoad, setInitialLoad] = useState(true);
-const columns = useCardColumns();
-const [categories, setCategories] = useState({});
+ const [initialLoad, setInitialLoad] = useState(true);
+ const columns = useCardColumns();
+ const [categories, setCategories] = useState({});
-// Helper function to get display data based on current filters
-const getDisplayData = () => {
+ // Helper function to get display data based on current filters
+ const getDisplayData = () => {
if (filters.type === 'series') {
- return Object.values(series).map((item) => ({
+ return Object.values(series).map((item) => ({
...item,
_vodType: 'series',
- }));
+ }));
} else if (filters.type === 'movies') {
- return Object.values(movies).map((item) => ({
+ return Object.values(movies).map((item) => ({
...item,
_vodType: 'movie',
- }));
+ }));
} else {
- // 'all' - combine movies and series, tagging each with its type
- return [
+ // 'all' - combine movies and series, tagging each with its type
+ return [
...Object.values(movies).map((item) => ({
- ...item,
- _vodType: 'movie',
+ ...item,
+ _vodType: 'movie',
})),
...Object.values(series).map((item) => ({
- ...item,
- _vodType: 'series',
+ ...item,
+ _vodType: 'series',
})),
- ];
+ ];
}
-};
+ };
-useEffect(() => {
+ useEffect(() => {
// setCategories(allCategories)
setCategories(
- Object.keys(allCategories).reduce((acc, key) => {
+ Object.keys(allCategories).reduce((acc, key) => {
const enabled = allCategories[key].m3u_accounts.find(
- (account) => account.enabled === true
+ (account) => account.enabled === true
);
if (enabled) {
- acc[key] = allCategories[key];
+ acc[key] = allCategories[key];
}
return acc;
- }, {})
+ }, {})
);
-}, [allCategories]);
+ }, [allCategories]);
-useEffect(() => {
+ useEffect(() => {
fetchCategories();
-}, [fetchCategories]);
+ }, [fetchCategories]);
-useEffect(() => {
+ useEffect(() => {
if (filters.type === 'series') {
- fetchSeries().finally(() => setInitialLoad(false));
+ fetchSeries().finally(() => setInitialLoad(false));
} else {
- fetchMovies().finally(() => setInitialLoad(false));
+ fetchMovies().finally(() => setInitialLoad(false));
}
-}, [filters, currentPage, fetchMovies, fetchSeries]);
+ }, [filters, currentPage, fetchMovies, fetchSeries]);
-const handleVODCardClick = (vod) => {
+ const handleVODCardClick = (vod) => {
setSelectedVOD(vod);
openVODModal();
-};
+ };
-const handleSeriesClick = (series) => {
+ const handleSeriesClick = (series) => {
setSelectedSeries(series);
openSeriesModal();
-};
+ };
-const onCategoryChange = (value) => {
+ const onCategoryChange = (value) => {
setFilters({ category: value });
setPage(1);
-};
+ };
-const categoryOptions = [
+ const categoryOptions = [
{ value: '', label: 'All Categories' },
...Object.values(categories).map((cat) => ({
- value: `${cat.name}|${cat.category_type}`, // Combine name and type for uniqueness
- label: `${cat.name} (${cat.category_type})`, // Show type in label for clarity
+ value: `${cat.name}|${cat.category_type}`, // Combine name and type for uniqueness
+ label: `${cat.name} (${cat.category_type})`, // Show type in label for clarity
})),
-];
+ ];
-const totalPages = Math.ceil(totalCount / pageSize);
+ const totalPages = Math.ceil(totalCount / pageSize);
-return (
+ return (
-
+
- Video on Demand
+ Video on Demand
{/* Filters */}
- setFilters({ type: value })}
data={[
- { label: 'All', value: 'all' },
- { label: 'Movies', value: 'movies' },
- { label: 'Series', value: 'series' },
+ { label: 'All', value: 'all' },
+ { label: 'Movies', value: 'movies' },
+ { label: 'Series', value: 'series' },
]}
- />
+ />
- }
value={filters.search}
onChange={(e) => setFilters({ search: e.target.value })}
style={{ minWidth: 200 }}
- />
+ />
-
+ />
{/* Content */}
{initialLoad ? (
-
+
-
+
) : (
- <>
+ <>
{filters.type === 'series' ? (
-
+
{Object.values(series).map((seriesItem) => (
-
+ >
-
+
))}
-
+
) : (
-
+
{getDisplayData().map((item) => (
-
+ >
{item._vodType === 'series' ? (
-
+
) : (
-
+
)}
-
+
))}
-
+
)}
{/* Pagination */}
{totalPages > 1 && (
-
+
-
+
)}
- >
+ >
)}
-
+
- {/* Series Episodes Modal */}
-
+ />
- {/* VOD Details Modal */}
-
+ />
-);
+ );
};
export default VODsPage;