mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-19 01:25:06 +00:00
Extracted utils
This commit is contained in:
parent
a9402207fb
commit
b595e845bb
3 changed files with 414 additions and 0 deletions
124
frontend/src/utils/components/FloatingVideoUtils.js
Normal file
124
frontend/src/utils/components/FloatingVideoUtils.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
export const getLivePlayerErrorMessage = (errorType, errorDetail) => {
|
||||
if (errorType !== 'MediaError') {
|
||||
return errorDetail
|
||||
? `Error: ${errorType} - ${errorDetail}`
|
||||
: `Error: ${errorType}`;
|
||||
}
|
||||
|
||||
const errorString = errorDetail?.toLowerCase() || '';
|
||||
|
||||
if (
|
||||
errorString.includes('audio') ||
|
||||
errorString.includes('ac3') ||
|
||||
errorString.includes('ac-3')
|
||||
) {
|
||||
return 'Audio codec not supported by your browser. Try Chrome or Edge for better audio codec support.';
|
||||
}
|
||||
|
||||
if (
|
||||
errorString.includes('video') ||
|
||||
errorString.includes('h264') ||
|
||||
errorString.includes('h.264')
|
||||
) {
|
||||
return 'Video codec not supported by your browser. Try Chrome or Edge for better video codec support.';
|
||||
}
|
||||
|
||||
if (errorString.includes('mse')) {
|
||||
return "Your browser doesn't support the codecs used in this stream. Try Chrome or Edge for better compatibility.";
|
||||
}
|
||||
|
||||
return 'Media codec not supported by your browser. This may be due to unsupported audio (AC3) or video codecs. Try Chrome or Edge.';
|
||||
};
|
||||
|
||||
export const getVODPlayerErrorMessage = (error) => {
|
||||
if (!error) return 'Video playback error';
|
||||
|
||||
switch (error.code) {
|
||||
case error.MEDIA_ERR_ABORTED:
|
||||
return 'Video playback was aborted';
|
||||
case error.MEDIA_ERR_NETWORK:
|
||||
return 'Network error while loading video';
|
||||
case error.MEDIA_ERR_DECODE:
|
||||
return 'Video codec not supported by your browser';
|
||||
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
||||
return 'Video format not supported by your browser';
|
||||
default:
|
||||
return error.message || 'Unknown video error';
|
||||
}
|
||||
};
|
||||
|
||||
export const getClientCoordinates = (event) => ({
|
||||
clientX: event.touches?.[0]?.clientX ?? event.clientX,
|
||||
clientY: event.touches?.[0]?.clientY ?? event.clientY,
|
||||
});
|
||||
|
||||
export const calculateNewDimensions = (
|
||||
deltaX,
|
||||
deltaY,
|
||||
startWidth,
|
||||
startHeight,
|
||||
handle,
|
||||
ratio
|
||||
) => {
|
||||
const widthDelta = deltaX * handle.xDir;
|
||||
const heightDelta = deltaY * handle.yDir;
|
||||
|
||||
let width = startWidth + widthDelta;
|
||||
let height = width / ratio;
|
||||
|
||||
// Use vertical-driven resize if user drags mostly vertically
|
||||
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
||||
height = startHeight + heightDelta;
|
||||
width = height * ratio;
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
export const applyConstraints = (
|
||||
width,
|
||||
height,
|
||||
ratio,
|
||||
startPos,
|
||||
handle,
|
||||
minWidth,
|
||||
minHeight,
|
||||
visibleMargin
|
||||
) => {
|
||||
// Apply minimum constraints
|
||||
if (width < minWidth) {
|
||||
width = minWidth;
|
||||
height = width / ratio;
|
||||
}
|
||||
if (height < minHeight) {
|
||||
height = minHeight;
|
||||
width = height * ratio;
|
||||
}
|
||||
|
||||
// Apply viewport constraints
|
||||
const posX = startPos?.x ?? 0;
|
||||
const posY = startPos?.y ?? 0;
|
||||
const maxWidth = !handle.isLeft
|
||||
? Math.max(minWidth, window.innerWidth - posX - visibleMargin)
|
||||
: null;
|
||||
const maxHeight = !handle.isTop
|
||||
? Math.max(minHeight, window.innerHeight - posY - visibleMargin)
|
||||
: null;
|
||||
|
||||
if (maxWidth && width > maxWidth) {
|
||||
width = maxWidth;
|
||||
height = width / ratio;
|
||||
}
|
||||
if (maxHeight && height > maxHeight) {
|
||||
height = maxHeight;
|
||||
width = height * ratio;
|
||||
}
|
||||
|
||||
// Final adjustment to maintain aspect ratio
|
||||
if (maxWidth && width > maxWidth) {
|
||||
width = maxWidth;
|
||||
height = width / ratio;
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
166
frontend/src/utils/components/SeriesModalUtils.js
Normal file
166
frontend/src/utils/components/SeriesModalUtils.js
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
export const imdbUrl = (imdb_id) =>
|
||||
imdb_id ? `https://www.imdb.com/title/${imdb_id}` : '';
|
||||
|
||||
export const tmdbUrl = (tmdb_id, type = 'movie') =>
|
||||
tmdb_id ? `https://www.themoviedb.org/${type}/${tmdb_id}` : '';
|
||||
|
||||
export 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`;
|
||||
};
|
||||
|
||||
export const formatStreamLabel = (relation) => {
|
||||
const provider = relation.m3u_account.name;
|
||||
const streamId = relation.stream_id;
|
||||
const quality = extractQuality(relation);
|
||||
|
||||
return `${provider}${quality ?? ''}${streamId ? ` (Stream ${streamId})` : ''}`;
|
||||
};
|
||||
|
||||
const extractQuality = (relation) => {
|
||||
// 1. Primary: Backend quality_info field
|
||||
const fromQualityInfo = getQualityFromBackend(relation.quality_info);
|
||||
if (fromQualityInfo) return fromQualityInfo;
|
||||
|
||||
// 2. Secondary: Custom properties detailed_info
|
||||
if (relation.custom_properties?.detailed_info) {
|
||||
const fromDetailedInfo = getQualityFromDetailedInfo(relation.custom_properties.detailed_info);
|
||||
if (fromDetailedInfo) return fromDetailedInfo;
|
||||
}
|
||||
|
||||
// 3. Fallback: Stream name
|
||||
if (relation.stream_name) {
|
||||
return getQualityFromStreamName(relation.stream_name);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const getQualityFromBackend = (qualityInfo) => {
|
||||
if (!qualityInfo) return '';
|
||||
|
||||
if (qualityInfo.quality) {
|
||||
return ` - ${qualityInfo.quality}`;
|
||||
} else if (qualityInfo.resolution) {
|
||||
return ` - ${qualityInfo.resolution}`;
|
||||
} else if (qualityInfo.bitrate) {
|
||||
return ` - ${qualityInfo.bitrate}`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getQualityFromDetailedInfo = (detailedInfo) => {
|
||||
// Check video dimensions first
|
||||
if (detailedInfo.video?.width && detailedInfo.video?.height) {
|
||||
return getQualityInfoFromDimensions(detailedInfo.video.width, detailedInfo.video.height);
|
||||
}
|
||||
|
||||
// Check name field
|
||||
if (detailedInfo.name) {
|
||||
return parseQualityFromText(detailedInfo.name);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const getQualityFromStreamName = (streamName) => {
|
||||
return parseQualityFromText(streamName);
|
||||
};
|
||||
|
||||
const parseQualityFromText = (text) => {
|
||||
if (text.includes('4K') || text.includes('2160p')) return ' - 4K';
|
||||
if (text.includes('1080p') || text.includes('FHD')) return ' - 1080p';
|
||||
if (text.includes('720p') || text.includes('HD')) return ' - 720p';
|
||||
if (text.includes('480p')) return ' - 480p';
|
||||
return '';
|
||||
};
|
||||
|
||||
const getQualityInfoFromDimensions = (width, height) => {
|
||||
// Prioritize width for quality detection (handles ultrawide/cinematic aspect ratios)
|
||||
if (width >= 3840) {
|
||||
return ' - 4K';
|
||||
} else if (width >= 1920) {
|
||||
return ' - 1080p';
|
||||
} else if (width >= 1280) {
|
||||
return ' - 720p';
|
||||
} else if (width >= 854) {
|
||||
return ' - 480p';
|
||||
} else {
|
||||
return ` - ${width}x${height}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const sortEpisodesList = (episodesList) => {
|
||||
return episodesList.sort((a, b) => {
|
||||
if (a.season_number !== b.season_number) {
|
||||
return (a.season_number || 0) - (b.season_number || 0);
|
||||
}
|
||||
return (a.episode_number || 0) - (b.episode_number || 0);
|
||||
});
|
||||
};
|
||||
|
||||
export const groupEpisodesBySeason = (seriesEpisodes) => {
|
||||
const grouped = {};
|
||||
seriesEpisodes.forEach((episode) => {
|
||||
const season = episode.season_number || 1;
|
||||
if (!grouped[season]) {
|
||||
grouped[season] = [];
|
||||
}
|
||||
grouped[season].push(episode);
|
||||
});
|
||||
return grouped;
|
||||
};
|
||||
|
||||
export const sortBySeasonNumber = (episodesBySeason) => {
|
||||
return Object.keys(episodesBySeason)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
};
|
||||
|
||||
export const getEpisodeStreamUrl = (episode, selectedProvider, env_mode) => {
|
||||
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) {
|
||||
streamUrl += `?stream_id=${encodeURIComponent(selectedProvider.stream_id)}`;
|
||||
} else {
|
||||
streamUrl += `?m3u_account_id=${selectedProvider.m3u_account.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (env_mode === 'dev') {
|
||||
streamUrl = `${window.location.protocol}//${window.location.hostname}:5656${streamUrl}`;
|
||||
} else {
|
||||
streamUrl = `${window.location.origin}${streamUrl}`;
|
||||
}
|
||||
return streamUrl;
|
||||
};
|
||||
|
||||
// Helper to get embeddable YouTube URL
|
||||
export const getYouTubeEmbedUrl = (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}`;
|
||||
};
|
||||
|
||||
export const getEpisodeAirdate = (episode) => {
|
||||
return episode.air_date
|
||||
? new Date(episode.air_date).toLocaleDateString()
|
||||
: 'N/A';
|
||||
};
|
||||
|
||||
export const getTmdbUrlLink = (displaySeries, episode) => {
|
||||
return (
|
||||
tmdbUrl(displaySeries.tmdb_id, 'tv') +
|
||||
(episode.season_number && episode.episode_number
|
||||
? `/season/${episode.season_number}/episode/${episode.episode_number}`
|
||||
: '')
|
||||
);
|
||||
};
|
||||
124
frontend/src/utils/components/VODModalUtils.js
Normal file
124
frontend/src/utils/components/VODModalUtils.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
const hasValidTechnicalDetails = (obj) => {
|
||||
return obj?.bitrate || obj?.video || obj?.audio;
|
||||
};
|
||||
|
||||
const extractFromDetailedInfo = (customProperties) => {
|
||||
const detailedInfo = customProperties?.detailed_info;
|
||||
if (!detailedInfo) return null;
|
||||
|
||||
return {
|
||||
bitrate: detailedInfo.bitrate || null,
|
||||
video: detailedInfo.video || null,
|
||||
audio: detailedInfo.audio || null,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTechnicalDetails = (selectedProvider, defaultVOD) => {
|
||||
if (!selectedProvider) {
|
||||
return {
|
||||
bitrate: defaultVOD?.bitrate,
|
||||
video: defaultVOD?.video,
|
||||
audio: defaultVOD?.audio,
|
||||
};
|
||||
}
|
||||
|
||||
// Try movie/episode content first
|
||||
const content = selectedProvider.movie || selectedProvider.episode;
|
||||
if (content && hasValidTechnicalDetails(content)) {
|
||||
return {
|
||||
bitrate: content.bitrate,
|
||||
video: content.video,
|
||||
audio: content.audio,
|
||||
};
|
||||
}
|
||||
|
||||
// Try provider object directly
|
||||
if (hasValidTechnicalDetails(selectedProvider)) {
|
||||
return {
|
||||
bitrate: selectedProvider.bitrate,
|
||||
video: selectedProvider.video,
|
||||
audio: selectedProvider.audio,
|
||||
};
|
||||
}
|
||||
|
||||
// Try custom_properties.detailed_info
|
||||
const detailedInfo = extractFromDetailedInfo(
|
||||
selectedProvider.custom_properties
|
||||
);
|
||||
if (detailedInfo && hasValidTechnicalDetails(detailedInfo)) {
|
||||
return detailedInfo;
|
||||
}
|
||||
|
||||
// Fallback to defaultVOD
|
||||
return {
|
||||
bitrate: defaultVOD?.bitrate,
|
||||
video: defaultVOD?.video,
|
||||
audio: defaultVOD?.audio,
|
||||
};
|
||||
};
|
||||
|
||||
export const getMovieStreamUrl = (vod, selectedProvider, env_mode) => {
|
||||
let streamUrl = `/proxy/vod/movie/${vod.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) {
|
||||
streamUrl += `?stream_id=${encodeURIComponent(selectedProvider.stream_id)}`;
|
||||
} else {
|
||||
streamUrl += `?m3u_account_id=${selectedProvider.m3u_account.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (env_mode === 'dev') {
|
||||
streamUrl = `${window.location.protocol}//${window.location.hostname}:5656${streamUrl}`;
|
||||
} else {
|
||||
streamUrl = `${window.location.origin}${streamUrl}`;
|
||||
}
|
||||
return streamUrl;
|
||||
};
|
||||
|
||||
export const formatVideoDetails = (video) => {
|
||||
const parts = [];
|
||||
|
||||
const codec =
|
||||
video.codec_long_name && video.codec_long_name !== 'unknown'
|
||||
? video.codec_long_name
|
||||
: video.codec_name;
|
||||
parts.push(codec);
|
||||
|
||||
if (video.profile) parts.push(`(${video.profile})`);
|
||||
if (video.width && video.height) parts.push(`${video.width}x${video.height}`);
|
||||
if (video.display_aspect_ratio)
|
||||
parts.push(`Aspect Ratio: ${video.display_aspect_ratio}`);
|
||||
if (video.bit_rate)
|
||||
parts.push(`Bitrate: ${Math.round(Number(video.bit_rate) / 1000)} kbps`);
|
||||
if (video.r_frame_rate) parts.push(`Frame Rate: ${video.r_frame_rate} fps`);
|
||||
if (video.tags?.encoder) parts.push(`Encoder: ${video.tags.encoder}`);
|
||||
|
||||
return parts.join(', ');
|
||||
};
|
||||
|
||||
export const formatAudioDetails = (audio) => {
|
||||
const parts = [];
|
||||
|
||||
const codec =
|
||||
audio.codec_long_name && audio.codec_long_name !== 'unknown'
|
||||
? audio.codec_long_name
|
||||
: audio.codec_name;
|
||||
parts.push(codec);
|
||||
|
||||
if (audio.profile) parts.push(`(${audio.profile})`);
|
||||
|
||||
const channels =
|
||||
audio.channel_layout || (audio.channels ? `${audio.channels}` : null);
|
||||
if (channels) parts.push(`Channels: ${channels}`);
|
||||
|
||||
if (audio.sample_rate) parts.push(`Sample Rate: ${audio.sample_rate} Hz`);
|
||||
if (audio.bit_rate)
|
||||
parts.push(`Bitrate: ${Math.round(Number(audio.bit_rate) / 1000)} kbps`);
|
||||
if (audio.tags?.handler_name)
|
||||
parts.push(`Handler: ${audio.tags.handler_name}`);
|
||||
|
||||
return parts.join(', ');
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue