mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
Merge pull request #1285 from nemesbak:fix/vod-series-provider-switch
fix(vod): respect relation_id when switching providers in Series/Movie modal
This commit is contained in:
commit
1d5a005888
6 changed files with 93 additions and 44 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
<Title order={3}>{displayVOD.name}</Title>
|
||||
|
||||
{/* Original name if different */}
|
||||
{displayVOD.o_name &&
|
||||
displayVOD.o_name !== displayVOD.name && (
|
||||
<Text size="sm" c="dimmed" fs="italic">
|
||||
Original: {displayVOD.o_name}
|
||||
</Text>
|
||||
)}
|
||||
{displayVOD.o_name && displayVOD.o_name !== displayVOD.name && (
|
||||
<Text size="sm" c="dimmed" fs="italic">
|
||||
Original: {displayVOD.o_name}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Group spacing="md">
|
||||
{displayVOD.year && (
|
||||
<Badge color="blue">{displayVOD.year}</Badge>
|
||||
)}
|
||||
{displayVOD.year && <Badge color="blue">{displayVOD.year}</Badge>}
|
||||
{displayVOD.duration_secs && (
|
||||
<Badge color="gray">
|
||||
{formatDuration(displayVOD.duration_secs)}
|
||||
</Badge>
|
||||
)}
|
||||
{displayVOD.rating && (
|
||||
<Badge color="yellow">{displayVOD.rating}</Badge>
|
||||
)}
|
||||
{displayVOD.age && (
|
||||
<Badge color="orange">{displayVOD.age}</Badge>
|
||||
<Badge color="gray">{formatDuration(displayVOD.duration_secs)}</Badge>
|
||||
)}
|
||||
{displayVOD.rating && <Badge color="yellow">{displayVOD.rating}</Badge>}
|
||||
{displayVOD.age && <Badge color="orange">{displayVOD.age}</Badge>}
|
||||
<Badge color="green">Movie</Badge>
|
||||
{/* imdb_id and tmdb_id badges */}
|
||||
{displayVOD.imdb_id && (
|
||||
|
|
@ -203,12 +194,15 @@ const Movie = ({
|
|||
|
||||
const MovieTechnicalDetails = ({ selectedProvider, displayVOD }) => {
|
||||
const techDetails = getTechnicalDetails(selectedProvider, displayVOD);
|
||||
const hasDetails = techDetails.bitrate || techDetails.video || techDetails.audio;
|
||||
const hasDetails =
|
||||
techDetails.bitrate || techDetails.video || techDetails.audio;
|
||||
|
||||
if (!hasDetails) return null;
|
||||
|
||||
const hasVideo = techDetails.video && Object.keys(techDetails.video).length > 0;
|
||||
const hasAudio = techDetails.audio && Object.keys(techDetails.audio).length > 0;
|
||||
const hasVideo =
|
||||
techDetails.video && Object.keys(techDetails.video).length > 0;
|
||||
const hasAudio =
|
||||
techDetails.audio && Object.keys(techDetails.audio).length > 0;
|
||||
|
||||
return (
|
||||
<Stack spacing={4} mt="xs">
|
||||
|
|
@ -217,7 +211,9 @@ const MovieTechnicalDetails = ({ selectedProvider, displayVOD }) => {
|
|||
{selectedProvider && (
|
||||
<Text size="xs" c="dimmed" weight="normal" span ml={8}>
|
||||
(from {selectedProvider.m3u_account.name}
|
||||
{selectedProvider.stream_id && ` - Stream ${selectedProvider.stream_id}`})
|
||||
{selectedProvider.stream_id &&
|
||||
` - Stream ${selectedProvider.stream_id}`}
|
||||
)
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
|
|
@ -315,16 +311,21 @@ const VODModal = ({ vod, opened, onClose }) => {
|
|||
}, [opened]);
|
||||
|
||||
const onClickYouTubeTrailer = () => {
|
||||
setTrailerUrl(
|
||||
getYouTubeEmbedUrl(displayVOD.youtube_trailer)
|
||||
);
|
||||
setTrailerUrl(getYouTubeEmbedUrl(displayVOD.youtube_trailer));
|
||||
setTrailerModalOpened(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeSelectedProvider = (value) => {
|
||||
const provider = providers.find((p) => p.id.toString() === value);
|
||||
setSelectedProvider(provider);
|
||||
}
|
||||
if (provider) {
|
||||
setLoadingDetails(true);
|
||||
fetchMovieDetailsFromProvider(vod.id, provider.id)
|
||||
.then((details) => setDetailedVOD(details))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoadingDetails(false));
|
||||
}
|
||||
};
|
||||
|
||||
if (!vod) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -240,10 +240,10 @@ const useVODStore = create((set, get) => ({
|
|||
}
|
||||
},
|
||||
|
||||
fetchMovieDetailsFromProvider: async (movieId) => {
|
||||
fetchMovieDetailsFromProvider: async (movieId, relationId = null) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await api.getMovieProviderInfo(movieId);
|
||||
const response = await api.getMovieProviderInfo(movieId, relationId);
|
||||
|
||||
// Transform the response data to match our expected format
|
||||
const movieDetails = getMovieDetailsWithProvider(response, movieId);
|
||||
|
|
@ -346,10 +346,10 @@ const useVODStore = create((set, get) => ({
|
|||
return { content: updatedContent };
|
||||
}),
|
||||
|
||||
fetchSeriesInfo: async (seriesId) => {
|
||||
fetchSeriesInfo: async (seriesId, relationId = null) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await api.getSeriesInfo(seriesId);
|
||||
const response = await api.getSeriesInfo(seriesId, relationId);
|
||||
|
||||
// Transform the response data to match our expected format
|
||||
const seriesInfo = getSeriesDetails(response, seriesId);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue