Extracted utils

This commit is contained in:
Nick Sandstrom 2026-02-24 00:59:41 -08:00
parent a9402207fb
commit b595e845bb
3 changed files with 414 additions and 0 deletions

View 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 };
};

View 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}`
: '')
);
};

View 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(', ');
};