diff --git a/CHANGELOG.md b/CHANGELOG.md index afd013ac..496121b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Switching providers in the VOD or Series detail modal had no effect.** The `provider-info` endpoints for movies and series always fetched data from the highest-priority provider, ignoring the `relation_id` the frontend sent when the user selected a different provider from the dropdown. The endpoints now accept an optional `relation_id` query parameter and fetch from that specific relation. The VOD modal also now shows the "Loading additional details..." indicator while the provider switch is in flight, matching the existing behaviour in the Series modal. (Fixes #1285) - Thanks [@nemesbak](https://github.com/nemesbak) - **Per-channel stream profile override was ignored during streaming.** `Channel.get_stream_profile()` read `self.stream_profile` directly, bypassing any `ChannelOverride.stream_profile` set by the user on auto-synced channels. The method now resolves through `effective_stream_profile_obj`, which checks the channel's override record first and falls back to the channel's own field. Channels without an override continue to behave identically to before. (Fixes #1268) - Thanks [@nemesbak](https://github.com/nemesbak) - **Web-player output profile was ignored for live streams started outside the Channels page.** `getShowVideoUrl` in `RecordingCardUtils.js` returned a raw proxy URL without calling `buildLiveStreamUrl`, so the output profile preference stored in `localStorage` was not applied when launching a stream from the TV Guide, Program Detail modal, DVR page, Recording Details modal, or Recording Card. Only the Channels page (StreamsTable) was calling `buildLiveStreamUrl` correctly. One additional import and one changed return value fix all affected entry points. (Fixes #1304) - Thanks [@nemesbak](https://github.com/nemesbak) - **Plugins with available updates were not sorting to the top of the Plugin Browse list.** The sort weight function previously treated `update_available` plugins the same as any other installed plugin, leaving them buried. They now receive the highest sort priority (below the search/filter results header) so users can spot pending updates immediately. — Thanks [@sethwv](https://github.com/sethwv) diff --git a/apps/vod/api_views.py b/apps/vod/api_views.py index 67813251..07f5b6fe 100644 --- a/apps/vod/api_views.py +++ b/apps/vod/api_views.py @@ -120,11 +120,30 @@ class MovieViewSet(viewsets.ReadOnlyModelViewSet): """Get detailed movie information from the original provider, throttled to 24h.""" movie = self.get_object() - # Get the highest priority active relation - relation = M3UMovieRelation.objects.filter( + relation_id = request.query_params.get('relation_id') + if relation_id is not None: + try: + relation_id = int(relation_id) + except (TypeError, ValueError): + return Response( + {'error': 'Invalid relation_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + qs = M3UMovieRelation.objects.filter( movie=movie, m3u_account__is_active=True - ).select_related('m3u_account').order_by('-m3u_account__priority', 'id').first() + ).select_related('m3u_account') + + if relation_id is not None: + relation = qs.filter(id=relation_id).first() + if not relation: + return Response( + {'error': 'Relation not found or not active'}, + status=status.HTTP_404_NOT_FOUND + ) + else: + relation = qs.order_by('-m3u_account__priority', 'id').first() if not relation: return Response( @@ -326,11 +345,30 @@ class SeriesViewSet(viewsets.ReadOnlyModelViewSet): series = self.get_object() logger.debug(f"Retrieved series: {series.name} (ID: {series.id})") - # Get the highest priority active relation - relation = M3USeriesRelation.objects.filter( + relation_id = request.query_params.get('relation_id') + if relation_id is not None: + try: + relation_id = int(relation_id) + except (TypeError, ValueError): + return Response( + {'error': 'Invalid relation_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + qs = M3USeriesRelation.objects.filter( series=series, m3u_account__is_active=True - ).select_related('m3u_account').order_by('-m3u_account__priority', 'id').first() + ).select_related('m3u_account') + + if relation_id is not None: + relation = qs.filter(id=relation_id).first() + if not relation: + return Response( + {'error': 'Relation not found or not active'}, + status=status.HTTP_404_NOT_FOUND + ) + else: + relation = qs.order_by('-m3u_account__priority', 'id').first() if not relation: return Response( diff --git a/frontend/src/api.js b/frontend/src/api.js index 2a357a81..def75df5 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -3526,10 +3526,11 @@ export default class API { } } - static async getMovieProviderInfo(movieId) { + static async getMovieProviderInfo(movieId, relationId = null) { try { + const params = relationId ? `?relation_id=${relationId}` : ''; const response = await request( - `${host}/api/vod/movies/${movieId}/provider-info/` + `${host}/api/vod/movies/${movieId}/provider-info/${params}` ); return response; } catch (e) { @@ -3568,11 +3569,12 @@ export default class API { } } - static async getSeriesInfo(seriesId) { + static async getSeriesInfo(seriesId, relationId = null) { try { - // Call the provider-info endpoint that includes episodes + const params = new URLSearchParams({ include_episodes: 'true' }); + if (relationId) params.set('relation_id', relationId); const response = await request( - `${host}/api/vod/series/${seriesId}/provider-info/?include_episodes=true` + `${host}/api/vod/series/${seriesId}/provider-info/?${params}` ); return response; } catch (e) { diff --git a/frontend/src/components/SeriesModal.jsx b/frontend/src/components/SeriesModal.jsx index d22e6630..45cb37cd 100644 --- a/frontend/src/components/SeriesModal.jsx +++ b/frontend/src/components/SeriesModal.jsx @@ -470,6 +470,13 @@ const SeriesModal = ({ series, opened, onClose }) => { const onChangeSelectedProvider = (value) => { const provider = providers.find((p) => p.id.toString() === value); setSelectedProvider(provider); + if (provider) { + setLoadingDetails(true); + fetchSeriesInfo(series.id, provider.id) + .then((details) => setDetailedSeries(details)) + .catch(() => {}) + .finally(() => setLoadingDetails(false)); + } }; if (!series) return null; diff --git a/frontend/src/components/VODModal.jsx b/frontend/src/components/VODModal.jsx index c4f5f0cd..da4ee13f 100644 --- a/frontend/src/components/VODModal.jsx +++ b/frontend/src/components/VODModal.jsx @@ -23,7 +23,7 @@ import { formatStreamLabel, getYouTubeEmbedUrl, imdbUrl, - tmdbUrl + tmdbUrl, } from '../utils/components/SeriesModalUtils.js'; import { YouTubeTrailerModal } from './modals/YouTubeTrailerModal.jsx'; import { @@ -38,7 +38,7 @@ const Movie = ({ hasMultipleProviders, selectedProvider, detailedVOD, - vod + vod, }) => { const showVideo = useVideoStore((s) => s.showVideo); const env_mode = useSettingsStore((s) => s.environment.env_mode); @@ -71,28 +71,19 @@ const Movie = ({