From 8584aae675edc43057ffd3a49e311c631097cf7b Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 5 Aug 2025 12:53:22 -0500 Subject: [PATCH] Better naming for different functions. --- apps/vod/api_views.py | 76 ++++++++++++++++++++++++ core/api_urls.py | 1 - frontend/src/api.js | 44 +++++++------- frontend/src/pages/VODs.jsx | 47 ++++++++++----- frontend/src/store/useVODStore.jsx | 92 +++++++++++++++--------------- 5 files changed, 179 insertions(+), 81 deletions(-) diff --git a/apps/vod/api_views.py b/apps/vod/api_views.py index f2977237..6509522c 100644 --- a/apps/vod/api_views.py +++ b/apps/vod/api_views.py @@ -245,6 +245,82 @@ class SeriesViewSet(viewsets.ReadOnlyModelViewSet): serializer = EpisodeSerializer(episodes, many=True) return Response(serializer.data) + @action(detail=True, methods=['get'], url_path='provider-info') + def series_info(self, request, pk=None): + """Get detailed series information, refreshing from provider if needed""" + series = self.get_object() + + if not series.m3u_account: + return Response( + {'error': 'No M3U account associated with this series'}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + # Check if we should refresh data (optional force refresh parameter) + force_refresh = request.query_params.get('force_refresh', 'false').lower() == 'true' + refresh_interval_hours = int(request.query_params.get("refresh_interval", 24)) # Default to 24 hours + + now = timezone.now() + last_refreshed = series.updated_at or series.created_at or now - timedelta(days=1) + + if force_refresh or (now - last_refreshed) > timedelta(hours=refresh_interval_hours): + # Use existing refresh logic + from .tasks import refresh_series_episodes + account = series.m3u_account + if account and account.is_active: + refresh_series_episodes(account, series, series.series_id) + series.refresh_from_db() # Reload from database after refresh + + # Return the database data (which should now be fresh) + response_data = { + 'id': series.id, + 'series_id': series.series_id, + 'name': series.name, + 'description': series.description, + 'year': series.year, + 'genre': series.genre, + 'rating': series.rating, + 'tmdb_id': series.tmdb_id, + 'imdb_id': series.imdb_id, + 'category_id': series.category.id if series.category else None, + 'category_name': series.category.name if series.category else None, + 'cover': series.logo.url if series.logo else None, + 'last_refreshed': series.updated_at, + 'custom_properties': series.custom_properties or {}, + } + + # Add episodes info if requested + include_episodes = request.query_params.get('include_episodes', 'false').lower() == 'true' + if include_episodes: + episodes_by_season = {} + for episode in series.episodes.all().order_by('season_number', 'episode_number'): + season_key = str(episode.season_number or 0) + if season_key not in episodes_by_season: + episodes_by_season[season_key] = [] + + episodes_by_season[season_key].append({ + 'id': episode.stream_id, + 'title': episode.name, + 'episode_num': episode.episode_number, + 'season_number': episode.season_number, + 'plot': episode.description, + 'duration_secs': episode.duration * 60 if episode.duration else None, + 'rating': episode.rating, + 'container_extension': episode.container_extension, + }) + + response_data['episodes'] = episodes_by_season + + return Response(response_data) + + except Exception as e: + logger.error(f"Error fetching series info for series {pk}: {str(e)}") + return Response( + {'error': f'Failed to fetch series information: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + class VODCategoryFilter(django_filters.FilterSet): name = django_filters.CharFilter(lookup_expr="icontains") diff --git a/core/api_urls.py b/core/api_urls.py index 30714d44..00e20a6e 100644 --- a/core/api_urls.py +++ b/core/api_urls.py @@ -8,7 +8,6 @@ router = DefaultRouter() router.register(r'useragents', UserAgentViewSet, basename='useragent') router.register(r'streamprofiles', StreamProfileViewSet, basename='streamprofile') router.register(r'settings', CoreSettingsViewSet, basename='coresettings') -router.register(r'settings', CoreSettingsViewSet, basename='settings') urlpatterns = [ path('settings/env/', environment, name='token_refresh'), path('version/', version, name='version'), diff --git a/frontend/src/api.js b/frontend/src/api.js index 67cdec57..103d4857 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1700,28 +1700,47 @@ export default class API { } // VOD Methods - static async getVODs(params = new URLSearchParams()) { + static async getMovies(params = new URLSearchParams()) { try { const response = await request( `${host}/api/vod/movies/?${params.toString()}` ); return response; } catch (e) { - errorNotification('Failed to retrieve VODs', e); + errorNotification('Failed to retrieve movies', e); } } - static async getVODSeries(params = new URLSearchParams()) { + static async getSeries(params = new URLSearchParams()) { try { const response = await request( `${host}/api/vod/series/?${params.toString()}` ); return response; } catch (e) { - errorNotification('Failed to retrieve VOD series', e); + errorNotification('Failed to retrieve series', e); } } + static async getMovieDetails(movieId) { + try { + const response = await request(`${host}/api/vod/movies/${movieId}/`); + return response; + } catch (e) { + errorNotification('Failed to retrieve movie details', e); + } + } + + static async getMovieProviderInfo(movieId) { + try { + const response = await request(`${host}/api/vod/movies/${movieId}/provider-info/`); + return response; + } catch (e) { + errorNotification('Failed to retrieve movie provider info', e); + } + } + + static async getSeriesEpisodes(seriesId) { try { const response = await request( @@ -1742,23 +1761,6 @@ export default class API { } } - static async getVODInfo(vodId) { - try { - const response = await request(`${host}/api/vod/movies/${vodId}/`); - return response; - } catch (e) { - errorNotification('Failed to retrieve VOD info', e); - } - } - - 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 { diff --git a/frontend/src/pages/VODs.jsx b/frontend/src/pages/VODs.jsx index d89cf9b1..003e3191 100644 --- a/frontend/src/pages/VODs.jsx +++ b/frontend/src/pages/VODs.jsx @@ -213,8 +213,9 @@ const SeriesCard = ({ series, onClick }) => { }; const SeriesModal = ({ series, opened, onClose }) => { - const { fetchSeriesEpisodes, vods, loading } = useVODStore(); + const { fetchSeriesEpisodes, episodes, loading } = useVODStore(); const showVideo = useVideoStore((s) => s.showVideo); + const env_mode = useSettingsStore((s) => s.environment.env_mode); useEffect(() => { if (opened && series) { @@ -222,8 +223,8 @@ const SeriesModal = ({ series, opened, onClose }) => { } }, [opened, series, fetchSeriesEpisodes]); - const episodes = Object.values(vods).filter( - vod => vod.type === 'episode' && vod.series?.id === series?.id + const seriesEpisodes = Object.values(episodes).filter( + episode => episode.series?.id === series?.id ).sort((a, b) => { if (a.season_number !== b.season_number) { return (a.season_number || 0) - (b.season_number || 0); @@ -232,8 +233,13 @@ const SeriesModal = ({ series, opened, onClose }) => { }); const handlePlayEpisode = (episode) => { - const streamUrl = `${window.location.origin}${episode.stream_url}`; - showVideo(streamUrl, 'vod'); // Specify VOD content type + let streamUrl = `/proxy/vod/episode/${episode.uuid}`; + if (env_mode === 'dev') { + streamUrl = `${window.location.protocol}//${window.location.hostname}:5656${streamUrl}`; + } else { + streamUrl = `${window.location.origin}${streamUrl}`; + } + showVideo(streamUrl, 'vod', episode); }; if (!series) return null; @@ -267,7 +273,7 @@ const SeriesModal = ({ series, opened, onClose }) => { ) : ( - {episodes.map(episode => ( + {seriesEpisodes.map(episode => ( @@ -284,14 +290,14 @@ const VODModal = ({ vod, opened, onClose }) => { const [loadingDetails, setLoadingDetails] = useState(false); const [trailerModalOpened, setTrailerModalOpened] = useState(false); const [trailerUrl, setTrailerUrl] = useState(''); - const { fetchVODDetailsFromProvider } = useVODStore(); + const { fetchMovieDetailsFromProvider } = 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) + fetchMovieDetailsFromProvider(vod.id) .then((details) => { setDetailedVOD(details); }) @@ -303,7 +309,7 @@ const VODModal = ({ vod, opened, onClose }) => { setLoadingDetails(false); }); } - }, [opened, vod, detailedVOD, fetchVODDetailsFromProvider]); + }, [opened, vod, detailedVOD, fetchMovieDetailsFromProvider]); useEffect(() => { if (!opened) { @@ -661,8 +667,9 @@ const useCardColumns = () => { const VODsPage = () => { const { - vods, + movies, series, + episodes, categories, loading, filters, @@ -671,7 +678,7 @@ const VODsPage = () => { pageSize, setFilters, setPage, - fetchVODs, + fetchMovies, fetchSeries, fetchCategories } = useVODStore(); @@ -684,6 +691,18 @@ const VODsPage = () => { const [initialLoad, setInitialLoad] = useState(true); const columns = useCardColumns(); + // Helper function to get display data based on current filters + const getDisplayData = () => { + if (filters.type === 'series') { + return Object.values(series); + } else if (filters.type === 'movies') { + return Object.values(movies); + } else { + // 'all' - combine movies and series + return [...Object.values(movies), ...Object.values(series)]; + } + }; + useEffect(() => { fetchCategories(); }, [fetchCategories]); @@ -692,9 +711,9 @@ const VODsPage = () => { if (filters.type === 'series') { fetchSeries().finally(() => setInitialLoad(false)); } else { - fetchVODs().finally(() => setInitialLoad(false)); + fetchMovies().finally(() => setInitialLoad(false)); } - }, [filters, currentPage, fetchVODs, fetchSeries]); + }, [filters, currentPage, fetchMovies, fetchSeries]); const handleVODCardClick = (vod) => { setSelectedVOD(vod); @@ -777,7 +796,7 @@ const VODsPage = () => { ) : ( - {Object.values(vods).map(vod => ( + {getDisplayData().map(vod => ( ({ - vods: {}, + movies: {}, series: {}, + episodes: {}, categories: {}, loading: false, error: null, @@ -27,7 +28,7 @@ const useVODStore = create((set, get) => ({ currentPage: page, })), - fetchVODs: async () => { + fetchMovies: async () => { try { set({ loading: true, error: null }); const state = get(); @@ -44,27 +45,23 @@ const useVODStore = create((set, get) => ({ params.append('category', state.filters.category); } - if (state.filters.type === 'movies') { - params.append('type', 'movie'); - } - - const response = await api.getVODs(params); + const response = await api.getMovies(params); // Handle both paginated and non-paginated responses const results = response.results || response; const count = response.count || results.length; set({ - vods: results.reduce((acc, vod) => { - acc[vod.id] = vod; + movies: results.reduce((acc, movie) => { + acc[movie.id] = movie; return acc; }, {}), totalCount: count, loading: false, }); } catch (error) { - console.error('Failed to fetch VODs:', error); - set({ error: 'Failed to load VODs.', loading: false }); + console.error('Failed to fetch movies:', error); + set({ error: 'Failed to load movies.', loading: false }); } }, @@ -85,7 +82,7 @@ const useVODStore = create((set, get) => ({ params.append('category', state.filters.category); } - const response = await api.getVODSeries(params); + const response = await api.getSeries(params); // Handle both paginated and non-paginated responses const results = response.results || response; @@ -111,8 +108,8 @@ const useVODStore = create((set, get) => ({ const response = await api.getSeriesEpisodes(seriesId); set((state) => ({ - vods: { - ...state.vods, + episodes: { + ...state.episodes, ...response.reduce((acc, episode) => { acc[episode.id] = episode; return acc; @@ -120,61 +117,64 @@ const useVODStore = create((set, get) => ({ }, loading: false, })); + + return response; } catch (error) { console.error('Failed to fetch series episodes:', error); set({ error: 'Failed to load episodes.', loading: false }); + throw error; // Re-throw to allow calling component to handle } }, - fetchVODDetails: async (vodId) => { + fetchMovieDetails: async (movieId) => { set({ loading: true, error: null }); try { - const response = await api.getVODInfo(vodId); + const response = await api.getMovieDetails(movieId); // Transform the response data to match our expected format - const vodDetails = { - id: response.id || vodId, + const movieDetails = { + id: response.id || movieId, 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, + stream_url: response.url || '', + logo: response.logo_url || null, type: 'movie', director: response.director || '', actors: response.actors || '', country: response.country || '', tmdb_id: response.tmdb_id || '', - youtube_trailer: response.youtube_trailer || '', + imdb_id: response.imdb_id || '', m3u_account: response.m3u_account || '', }; - console.log('Fetched VOD Details:', vodDetails); + console.log('Fetched Movie Details:', movieDetails); set((state) => ({ - vods: { - ...state.vods, - [vodDetails.id]: vodDetails, + movies: { + ...state.movies, + [movieDetails.id]: movieDetails, }, loading: false, })); - return vodDetails; + return movieDetails; } catch (error) { - console.error('Failed to fetch VOD details:', error); - set({ error: 'Failed to load VOD details.', loading: false }); + console.error('Failed to fetch movie details:', error); + set({ error: 'Failed to load movie details.', loading: false }); throw error; } }, - fetchVODDetailsFromProvider: async (vodId) => { + fetchMovieDetailsFromProvider: async (movieId) => { set({ loading: true, error: null }); try { - const response = await api.getVODInfoFromProvider(vodId); + const response = await api.getMovieProviderInfo(movieId); // Transform the response data to match our expected format - const vodDetails = { - id: response.id || vodId, + const movieDetails = { + id: response.id || movieId, name: response.name || '', description: response.description || response.plot || '', year: response.year || null, @@ -201,13 +201,13 @@ const useVODStore = create((set, get) => ({ audio: response.audio || {}, }; - set({ loading: false }); // Only update loading state, do not update vods{ + set({ loading: false }); // Only update loading state - // Do NOT merge or overwrite the store VOD entry - return vodDetails; + // Do NOT merge or overwrite the store entry + return movieDetails; } catch (error) { - console.error('Failed to fetch VOD details from provider:', error); - set({ error: 'Failed to load VOD details from provider.', loading: false }); + console.error('Failed to fetch movie details from provider:', error); + set({ error: 'Failed to load movie details from provider.', loading: false }); throw error; } }, @@ -230,21 +230,21 @@ const useVODStore = create((set, get) => ({ } }, - addVOD: (vod) => + addMovie: (movie) => set((state) => ({ - vods: { ...state.vods, [vod.id]: vod }, + movies: { ...state.movies, [movie.id]: movie }, })), - updateVOD: (vod) => + updateMovie: (movie) => set((state) => ({ - vods: { ...state.vods, [vod.id]: vod }, + movies: { ...state.movies, [movie.id]: movie }, })), - removeVOD: (vodId) => + removeMovie: (movieId) => set((state) => { - const updatedVODs = { ...state.vods }; - delete updatedVODs[vodId]; - return { vods: updatedVODs }; + const updatedMovies = { ...state.movies }; + delete updatedMovies[movieId]; + return { movies: updatedMovies }; }), addSeries: (series) => @@ -263,6 +263,8 @@ const useVODStore = create((set, get) => ({ delete updatedSeries[seriesId]; return { series: updatedSeries }; }), + + })); export default useVODStore;