mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 17:16:26 +00:00
Extracted utils
This commit is contained in:
parent
93a4846924
commit
30b97c4fc5
7 changed files with 865 additions and 0 deletions
|
|
@ -45,6 +45,32 @@ export const toFriendlyDuration = (dateTime, unit) =>
|
|||
|
||||
export const fromNow = (dateTime) => dayjs(dateTime).fromNow();
|
||||
|
||||
export const setTz = (dateTime, timeZone) => dayjs(dateTime).tz(timeZone);
|
||||
|
||||
export const setMonth = (dateTime, value) => dayjs(dateTime).month(value);
|
||||
|
||||
export const setYear = (dateTime, value) => dayjs(dateTime).year(value);
|
||||
|
||||
export const setDay = (dateTime, value) => dayjs(dateTime).date(value);
|
||||
|
||||
export const setHour = (dateTime, value) => dayjs(dateTime).hour(value);
|
||||
|
||||
export const setMinute = (dateTime, value) => dayjs(dateTime).minute(value);
|
||||
|
||||
export const setSecond = (dateTime, value) => dayjs(dateTime).second(value);
|
||||
|
||||
export const getMonth = (dateTime) => dayjs(dateTime).month();
|
||||
|
||||
export const getYear = (dateTime) => dayjs(dateTime).year();
|
||||
|
||||
export const getDay = (dateTime) => dayjs(dateTime).date();
|
||||
|
||||
export const getHour = (dateTime) => dayjs(dateTime).hour();
|
||||
|
||||
export const getMinute = (dateTime) => dayjs(dateTime).minute();
|
||||
|
||||
export const getSecond = (dateTime) => dayjs(dateTime).second();
|
||||
|
||||
export const getNowMs = () => Date.now();
|
||||
|
||||
export const roundToNearest = (dateTime, minutes) => {
|
||||
|
|
@ -281,3 +307,33 @@ export const getDefaultTimeZone = () => {
|
|||
return 'UTC';
|
||||
}
|
||||
};
|
||||
|
||||
export const MONTH_NAMES = [
|
||||
'january',
|
||||
'february',
|
||||
'march',
|
||||
'april',
|
||||
'may',
|
||||
'june',
|
||||
'july',
|
||||
'august',
|
||||
'september',
|
||||
'october',
|
||||
'november',
|
||||
'december',
|
||||
];
|
||||
|
||||
export const MONTH_ABBR = [
|
||||
'jan',
|
||||
'feb',
|
||||
'mar',
|
||||
'apr',
|
||||
'may',
|
||||
'jun',
|
||||
'jul',
|
||||
'aug',
|
||||
'sep',
|
||||
'oct',
|
||||
'nov',
|
||||
'dec',
|
||||
];
|
||||
54
frontend/src/utils/forms/AccountInfoModalUtils.js
Normal file
54
frontend/src/utils/forms/AccountInfoModalUtils.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Helper function to format timestamps
|
||||
import API from '../../api.js';
|
||||
|
||||
export const formatTimestamp = (timestamp) => {
|
||||
if (!timestamp) return 'Unknown';
|
||||
try {
|
||||
const date =
|
||||
typeof timestamp === 'string' && timestamp.includes('T')
|
||||
? new Date(timestamp) // This should handle ISO format properly
|
||||
: new Date(parseInt(timestamp) * 1000);
|
||||
|
||||
// Convert to user's local time and display with timezone
|
||||
return date.toLocaleString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
} catch {
|
||||
return 'Invalid date';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get time remaining
|
||||
export const getTimeRemaining = (expTimestamp) => {
|
||||
if (!expTimestamp) return null;
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
const expDate = new Date(parseInt(expTimestamp) * 1000);
|
||||
const diffMs = expDate - now;
|
||||
|
||||
if (diffMs <= 0) return 'Expired';
|
||||
|
||||
const MS_PER_HOUR = 1000 * 60 * 60;
|
||||
const MS_PER_DAY = MS_PER_HOUR * 24;
|
||||
|
||||
const days = Math.floor(diffMs / MS_PER_DAY);
|
||||
const hours = Math.floor((diffMs % MS_PER_DAY) / MS_PER_HOUR);
|
||||
|
||||
const dayLabel = `${days} ${days === 1 ? 'day' : 'days'}`;
|
||||
const hourLabel = `${hours} ${hours === 1 ? 'hour' : 'hours'}`;
|
||||
|
||||
return days > 0 ? `${dayLabel} ${hourLabel}` : hourLabel;
|
||||
} catch {
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export const refreshAccountInfo = (currentProfile) => {
|
||||
return API.refreshAccountInfo(currentProfile.id);
|
||||
};
|
||||
186
frontend/src/utils/forms/ChannelBatchUtils.js
Normal file
186
frontend/src/utils/forms/ChannelBatchUtils.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import API from '../../api.js';
|
||||
|
||||
export const getChannelGroupChange = (selectedChannelGroup, channelGroups) => {
|
||||
if (!selectedChannelGroup || selectedChannelGroup === '-1') return null;
|
||||
const groupName = channelGroups[selectedChannelGroup]?.name || 'Unknown';
|
||||
return `• Channel Group: ${groupName}`;
|
||||
};
|
||||
|
||||
export const getLogoChange = (selectedLogoId, channelLogos) => {
|
||||
if (!selectedLogoId || selectedLogoId === '-1') return null;
|
||||
if (selectedLogoId === '0') return `• Logo: Use Default`;
|
||||
const logoName = channelLogos[selectedLogoId]?.name || 'Selected Logo';
|
||||
return `• Logo: ${logoName}`;
|
||||
};
|
||||
|
||||
export const getStreamProfileChange = (streamProfileId, streamProfiles) => {
|
||||
if (!streamProfileId || streamProfileId === '-1') return null;
|
||||
if (streamProfileId === '0') return `• Stream Profile: Use Default`;
|
||||
const profile = streamProfiles.find(
|
||||
(p) => `${p.id}` === `${streamProfileId}`
|
||||
);
|
||||
return `• Stream Profile: ${profile?.name || 'Selected Profile'}`;
|
||||
};
|
||||
|
||||
export const getUserLevelChange = (userLevel, userLevelLabels) => {
|
||||
if (!userLevel || userLevel === '-1') return null;
|
||||
return `• User Level: ${userLevelLabels[userLevel] || userLevel}`;
|
||||
};
|
||||
|
||||
export const getMatureContentChange = (isAdult) => {
|
||||
if (!isAdult || isAdult === '-1') return null;
|
||||
return `• Mature Content: ${isAdult === 'true' ? 'Yes' : 'No'}`;
|
||||
};
|
||||
|
||||
export const getRegexNameChange = (regexFind, regexReplace) => {
|
||||
if (!regexFind?.trim()) return null;
|
||||
return `• Name Change: Apply regex find "${regexFind}" replace with "${regexReplace || ''}"`;
|
||||
};
|
||||
|
||||
export const getEpgChange = (selectedDummyEpgId, epgs) => {
|
||||
if (!selectedDummyEpgId) return null;
|
||||
if (selectedDummyEpgId === 'clear')
|
||||
return `• EPG: Clear Assignment (use default dummy)`;
|
||||
const epgName = epgs[selectedDummyEpgId]?.name || 'Selected EPG';
|
||||
return `• Dummy EPG: ${epgName}`;
|
||||
};
|
||||
|
||||
export const updateChannels = (channelIds, values) => {
|
||||
return API.updateChannels(channelIds, values);
|
||||
};
|
||||
|
||||
export const bulkRegexRenameChannels = (
|
||||
channelIds,
|
||||
regexFind,
|
||||
regexReplace,
|
||||
flags
|
||||
) => {
|
||||
return API.bulkRegexRenameChannels(
|
||||
channelIds,
|
||||
regexFind,
|
||||
regexReplace ?? '',
|
||||
flags
|
||||
);
|
||||
};
|
||||
|
||||
export const batchSetEPG = (associations) => {
|
||||
return API.batchSetEPG(associations);
|
||||
};
|
||||
|
||||
export const getEpgData = () => {
|
||||
return API.getEPGData();
|
||||
};
|
||||
|
||||
export const setChannelNamesFromEpg = (channelIds) => {
|
||||
return API.setChannelNamesFromEpg(channelIds);
|
||||
};
|
||||
|
||||
export const setChannelLogosFromEpg = (channelIds) => {
|
||||
return API.setChannelLogosFromEpg(channelIds);
|
||||
};
|
||||
|
||||
export const setChannelTvgIdsFromEpg = (channelIds) => {
|
||||
return API.setChannelTvgIdsFromEpg(channelIds);
|
||||
};
|
||||
|
||||
export const computeRegexPreview = (
|
||||
channelIds,
|
||||
nameById,
|
||||
find,
|
||||
replace,
|
||||
limit = 25
|
||||
) => {
|
||||
if (!find) return [];
|
||||
|
||||
let re;
|
||||
try {
|
||||
re = new RegExp(find, 'g');
|
||||
} catch (error) {
|
||||
console.error('Invalid regex:', error);
|
||||
return [{ before: 'Invalid regex', after: '' }];
|
||||
}
|
||||
|
||||
// Limit preview to items that exist on the current page
|
||||
const pageOnlyIds = channelIds.filter((id) => nameById[id] !== undefined);
|
||||
const items = [];
|
||||
|
||||
for (let i = 0; i < Math.min(pageOnlyIds.length, limit); i++) {
|
||||
const before = nameById[pageOnlyIds[i]] ?? '';
|
||||
const after = before.replace(re, replace ?? '');
|
||||
if (before !== after) items.push({ before, after });
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export const buildSubmitValues = (
|
||||
formValues,
|
||||
selectedChannelGroup,
|
||||
selectedLogoId
|
||||
) => {
|
||||
const values = { ...formValues };
|
||||
|
||||
// Handle channel group ID - convert to integer if it exists
|
||||
if (selectedChannelGroup && selectedChannelGroup !== '-1') {
|
||||
values.channel_group_id = parseInt(selectedChannelGroup);
|
||||
} else {
|
||||
delete values.channel_group_id;
|
||||
}
|
||||
|
||||
if (selectedLogoId && selectedLogoId !== '-1') {
|
||||
values.logo_id = selectedLogoId === '0' ? null : parseInt(selectedLogoId);
|
||||
}
|
||||
delete values.logo;
|
||||
// Remove the channel_group field from form values as we use channel_group_id
|
||||
delete values.channel_group;
|
||||
|
||||
// Handle stream profile ID - convert special values
|
||||
if (!values.stream_profile_id || values.stream_profile_id === '-1') {
|
||||
delete values.stream_profile_id;
|
||||
} else if (
|
||||
values.stream_profile_id === '0' ||
|
||||
values.stream_profile_id === 0
|
||||
) {
|
||||
values.stream_profile_id = null; // Convert "use default" to null
|
||||
}
|
||||
|
||||
if (values.user_level == '-1') delete values.user_level;
|
||||
|
||||
if (values.is_adult === '-1') {
|
||||
delete values.is_adult;
|
||||
} else {
|
||||
values.is_adult = values.is_adult === 'true';
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
export const buildEpgAssociations = async (
|
||||
selectedDummyEpgId,
|
||||
channelIds,
|
||||
epgs,
|
||||
tvgs
|
||||
) => {
|
||||
if (!selectedDummyEpgId) return null;
|
||||
|
||||
if (selectedDummyEpgId === 'clear') {
|
||||
// Clear EPG assignments
|
||||
return channelIds.map((id) => ({ channel_id: id, epg_data_id: null }));
|
||||
}
|
||||
|
||||
// Assign the selected dummy EPG
|
||||
const selectedEpg = epgs[selectedDummyEpgId];
|
||||
if (!selectedEpg?.epg_data_count) return null;
|
||||
|
||||
const epgSourceId = parseInt(selectedDummyEpgId, 10);
|
||||
// Check if we already have EPG data loaded in the store
|
||||
let epgData = tvgs.find((data) => data.epg_source === epgSourceId);
|
||||
|
||||
if (!epgData) {
|
||||
const epgDataList = await getEpgData();
|
||||
epgData = epgDataList.find((data) => data.epg_source === epgSourceId);
|
||||
}
|
||||
|
||||
if (!epgData) return null;
|
||||
return channelIds.map((id) => ({ channel_id: id, epg_data_id: epgData.id }));
|
||||
};
|
||||
97
frontend/src/utils/forms/ChannelUtils.js
Normal file
97
frontend/src/utils/forms/ChannelUtils.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import API from '../../api.js';
|
||||
|
||||
export const matchChannelEpg = (channel) => {
|
||||
return API.matchChannelEpg(channel.id);
|
||||
};
|
||||
export const createLogo = (newLogoData) => {
|
||||
return API.createLogo(newLogoData);
|
||||
};
|
||||
const setChannelEPG = (channel, values) => {
|
||||
return API.setChannelEPG(channel.id, values.epg_data_id);
|
||||
};
|
||||
const updateChannel = (values) => {
|
||||
return API.updateChannel(values);
|
||||
};
|
||||
export const addChannel = (channel) => {
|
||||
return API.addChannel(channel);
|
||||
};
|
||||
export const requeryChannels = () => {
|
||||
API.requeryChannels();
|
||||
};
|
||||
|
||||
export const getChannelFormDefaultValues = (channel, channelGroups) => {
|
||||
return {
|
||||
name: channel?.name || '',
|
||||
channel_number:
|
||||
channel?.channel_number !== null && channel?.channel_number !== undefined
|
||||
? channel.channel_number
|
||||
: '',
|
||||
channel_group_id: channel?.channel_group_id
|
||||
? `${channel.channel_group_id}`
|
||||
: Object.keys(channelGroups).length > 0
|
||||
? Object.keys(channelGroups)[0]
|
||||
: '',
|
||||
stream_profile_id: channel?.stream_profile_id
|
||||
? `${channel.stream_profile_id}`
|
||||
: '0',
|
||||
tvg_id: channel?.tvg_id || '',
|
||||
tvc_guide_stationid: channel?.tvc_guide_stationid || '',
|
||||
epg_data_id: channel?.epg_data_id ?? '',
|
||||
logo_id: channel?.logo_id ? `${channel.logo_id}` : '',
|
||||
user_level: `${channel?.user_level ?? '0'}`,
|
||||
is_adult: channel?.is_adult ?? false,
|
||||
};
|
||||
};
|
||||
|
||||
export const getFormattedValues = (values) => {
|
||||
const formattedValues = { ...values };
|
||||
|
||||
// Convert empty or "0" stream_profile_id to null for the API
|
||||
if (
|
||||
!formattedValues.stream_profile_id ||
|
||||
formattedValues.stream_profile_id === '0'
|
||||
) {
|
||||
formattedValues.stream_profile_id = null;
|
||||
}
|
||||
|
||||
// Ensure tvg_id is properly included (no empty strings)
|
||||
formattedValues.tvg_id = formattedValues.tvg_id || null;
|
||||
|
||||
// Ensure tvc_guide_stationid is properly included (no empty strings)
|
||||
formattedValues.tvc_guide_stationid =
|
||||
formattedValues.tvc_guide_stationid || null;
|
||||
|
||||
return formattedValues;
|
||||
};
|
||||
|
||||
export const handleEpgUpdate = async (
|
||||
channel,
|
||||
values,
|
||||
formattedValues,
|
||||
channelStreams
|
||||
) => {
|
||||
// If there's an EPG to set, use our enhanced endpoint
|
||||
if (values.epg_data_id !== (channel.epg_data_id ?? '')) {
|
||||
// Use the special endpoint to set EPG and trigger refresh
|
||||
await setChannelEPG(channel, values);
|
||||
|
||||
// Remove epg_data_id from values since we've handled it separately
|
||||
const { epg_data_id: _epg_data_id, ...otherValues } = formattedValues;
|
||||
|
||||
// Update other channel fields if needed
|
||||
if (Object.keys(otherValues).length > 0) {
|
||||
await updateChannel({
|
||||
id: channel.id,
|
||||
...otherValues,
|
||||
streams: channelStreams.map((stream) => stream.id),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No EPG change, regular update
|
||||
await updateChannel({
|
||||
id: channel.id,
|
||||
...formattedValues,
|
||||
streams: channelStreams.map((stream) => stream.id),
|
||||
});
|
||||
}
|
||||
};
|
||||
75
frontend/src/utils/forms/ConnectionUtils.js
Normal file
75
frontend/src/utils/forms/ConnectionUtils.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import API from '../../api.js';
|
||||
import { SUBSCRIPTION_EVENTS } from '../../constants.js';
|
||||
|
||||
export const EVENT_OPTIONS = Object.entries(SUBSCRIPTION_EVENTS).map(
|
||||
([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
})
|
||||
);
|
||||
|
||||
export const updateConnectIntegration = (connection, values, config) => {
|
||||
return API.updateConnectIntegration(connection.id, {
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
config,
|
||||
enabled: values.enabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const createConnectIntegration = (values, config) => {
|
||||
return API.createConnectIntegration({
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
config,
|
||||
enabled: values.enabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const setConnectSubscriptions = (connection, subs) => {
|
||||
return API.setConnectSubscriptions(connection.id, subs);
|
||||
};
|
||||
|
||||
const buildWebhookConfig = (url, headers) => {
|
||||
const hdrs = {};
|
||||
headers.forEach((h) => {
|
||||
if (h.key && h.key.trim()) hdrs[h.key] = h.value;
|
||||
});
|
||||
const config = { url };
|
||||
if (Object.keys(hdrs).length) config.headers = hdrs;
|
||||
return config;
|
||||
};
|
||||
|
||||
const buildScriptConfig = (scriptPath) => ({ path: scriptPath });
|
||||
|
||||
export const buildConfig = (values, headers) =>
|
||||
values.type === 'webhook'
|
||||
? buildWebhookConfig(values.url, headers)
|
||||
: buildScriptConfig(values.script_path);
|
||||
|
||||
export const buildSubscriptions = (selectedEvents, payloadTemplates) =>
|
||||
Object.keys(SUBSCRIPTION_EVENTS).map((event) => ({
|
||||
event,
|
||||
enabled: selectedEvents.includes(event),
|
||||
payload_template: payloadTemplates[event] ?? null,
|
||||
}));
|
||||
|
||||
export const parseApiError = (error) => {
|
||||
const body = error?.body;
|
||||
|
||||
if (!body || typeof body !== 'object') {
|
||||
return { fieldErrors: {}, apiError: error?.message ?? 'Unknown error' };
|
||||
}
|
||||
|
||||
const knownFields = ['name', 'type'];
|
||||
const fieldErrors = Object.fromEntries(
|
||||
knownFields.filter((f) => body[f]).map((f) => [f, body[f]])
|
||||
);
|
||||
|
||||
const nonField = body.non_field_errors ?? body.detail ?? null;
|
||||
const apiError =
|
||||
nonField ??
|
||||
(Object.keys(fieldErrors).length === 0 ? JSON.stringify(body) : '');
|
||||
|
||||
return { fieldErrors, apiError };
|
||||
};
|
||||
131
frontend/src/utils/forms/CronBuilderUtils.js
Normal file
131
frontend/src/utils/forms/CronBuilderUtils.js
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
export const PRESETS = [
|
||||
{
|
||||
label: 'Every hour',
|
||||
value: '0 * * * *',
|
||||
description: 'At the start of every hour',
|
||||
},
|
||||
{
|
||||
label: 'Every 6 hours',
|
||||
value: '0 */6 * * *',
|
||||
description: 'Every 6 hours starting at midnight',
|
||||
},
|
||||
{
|
||||
label: 'Every 12 hours',
|
||||
value: '0 */12 * * *',
|
||||
description: 'Twice daily at midnight and noon',
|
||||
},
|
||||
{
|
||||
label: 'Daily at midnight',
|
||||
value: '0 0 * * *',
|
||||
description: 'Once per day at 12:00 AM',
|
||||
},
|
||||
{
|
||||
label: 'Daily at 3 AM',
|
||||
value: '0 3 * * *',
|
||||
description: 'Once per day at 3:00 AM',
|
||||
},
|
||||
{
|
||||
label: 'Daily at noon',
|
||||
value: '0 12 * * *',
|
||||
description: 'Once per day at 12:00 PM',
|
||||
},
|
||||
{
|
||||
label: 'Weekly (Sunday midnight)',
|
||||
value: '0 0 * * 0',
|
||||
description: 'Once per week on Sunday',
|
||||
},
|
||||
{
|
||||
label: 'Weekly (Monday 3 AM)',
|
||||
value: '0 3 * * 1',
|
||||
description: 'Once per week on Monday',
|
||||
},
|
||||
{
|
||||
label: 'Monthly (1st at 2:30 AM)',
|
||||
value: '30 2 1 * *',
|
||||
description: 'First day of each month',
|
||||
},
|
||||
];
|
||||
|
||||
export const DAYS_OF_WEEK = [
|
||||
{ value: '*', label: 'Every day' },
|
||||
{ value: '0', label: 'Sunday' },
|
||||
{ value: '1', label: 'Monday' },
|
||||
{ value: '2', label: 'Tuesday' },
|
||||
{ value: '3', label: 'Wednesday' },
|
||||
{ value: '4', label: 'Thursday' },
|
||||
{ value: '5', label: 'Friday' },
|
||||
{ value: '6', label: 'Saturday' },
|
||||
];
|
||||
|
||||
export const FREQUENCY_OPTIONS = [
|
||||
{ value: 'hourly', label: 'Hourly' },
|
||||
{ value: 'daily', label: 'Daily' },
|
||||
{ value: 'weekly', label: 'Weekly' },
|
||||
{ value: 'monthly', label: 'Monthly' },
|
||||
];
|
||||
|
||||
export const buildCron = (frequency, minute, hour, dayOfWeek, dayOfMonth) => {
|
||||
switch (frequency) {
|
||||
case 'hourly':
|
||||
return `${minute} * * * *`;
|
||||
case 'daily':
|
||||
return `${minute} ${hour} * * *`;
|
||||
case 'weekly':
|
||||
return `${minute} ${hour} * * ${dayOfWeek === '*' ? '0' : dayOfWeek}`;
|
||||
case 'monthly':
|
||||
return `${minute} ${hour} ${dayOfMonth} * *`;
|
||||
default:
|
||||
return '* * * * *';
|
||||
}
|
||||
};
|
||||
|
||||
const parseHour = (hr) => parseInt(hr.replace('*/', '').replace('*', '0')) || 0;
|
||||
|
||||
export const parseCronPreset = (cron) => {
|
||||
const [min, hr, day, _month, weekday] = cron.split(' ');
|
||||
const minute = parseInt(min) || 0;
|
||||
const hour = parseHour(hr);
|
||||
|
||||
if (hr === '*')
|
||||
return { frequency: 'hourly', minute, hour, dayOfWeek: '*', dayOfMonth: 1 };
|
||||
if (weekday !== '*')
|
||||
return {
|
||||
frequency: 'weekly',
|
||||
minute,
|
||||
hour,
|
||||
dayOfWeek: weekday,
|
||||
dayOfMonth: 1,
|
||||
};
|
||||
if (day !== '*')
|
||||
return {
|
||||
frequency: 'monthly',
|
||||
minute,
|
||||
hour,
|
||||
dayOfWeek: '*',
|
||||
dayOfMonth: parseInt(day) || 1,
|
||||
};
|
||||
return { frequency: 'daily', minute, hour, dayOfWeek: '*', dayOfMonth: 1 };
|
||||
};
|
||||
|
||||
export const CRON_FIELDS = [
|
||||
{ index: 0, label: 'Minute (0-59)', placeholder: '*, 0, */15, 0,15,30,45' },
|
||||
{ index: 1, label: 'Hour (0-23)', placeholder: '*, 0, 9-17, */6, 2,4,16' },
|
||||
{
|
||||
index: 2,
|
||||
label: 'Day of Month (1-31)',
|
||||
placeholder: '*, 1, 1-15, */2, 1,15',
|
||||
},
|
||||
{ index: 3, label: 'Month (1-12)', placeholder: '*, 1, 1-6, */3, 6,12' },
|
||||
{
|
||||
index: 4,
|
||||
label: 'Day of Week (0-6, Sun-Sat)',
|
||||
placeholder: '*, 0, 1-5, 0,6',
|
||||
},
|
||||
];
|
||||
|
||||
export const updateCronPart = (cron, index, value) => {
|
||||
const parts =
|
||||
cron.split(' ').length >= 5 ? cron.split(' ') : ['*', '*', '*', '*', '*'];
|
||||
parts[index] = value || '*';
|
||||
return parts.join(' ');
|
||||
};
|
||||
266
frontend/src/utils/forms/DummyEpgUtils.js
Normal file
266
frontend/src/utils/forms/DummyEpgUtils.js
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
import API from '../../api.js';
|
||||
import {
|
||||
format,
|
||||
getDay,
|
||||
getHour,
|
||||
getMinute,
|
||||
getMonth,
|
||||
getNow,
|
||||
getYear,
|
||||
MONTH_ABBR,
|
||||
MONTH_NAMES,
|
||||
setHour,
|
||||
setMinute,
|
||||
setSecond,
|
||||
setTz,
|
||||
} from '../dateTimeUtils.js';
|
||||
|
||||
export const getTimezones = () => {
|
||||
return API.getTimezones();
|
||||
};
|
||||
export const updateEPG = (values, epg) => {
|
||||
return API.updateEPG({ ...values, id: epg.id });
|
||||
};
|
||||
export const addEPG = (values) => {
|
||||
return API.addEPG(values);
|
||||
};
|
||||
|
||||
export const getDummyEpgFormInitialValues = () => {
|
||||
return {
|
||||
name: '',
|
||||
is_active: true,
|
||||
source_type: 'dummy',
|
||||
custom_properties: buildCustomProperties({}),
|
||||
};
|
||||
};
|
||||
|
||||
export const buildCustomProperties = (custom = {}) => ({
|
||||
title_pattern: custom.title_pattern || '',
|
||||
time_pattern: custom.time_pattern || '',
|
||||
date_pattern: custom.date_pattern || '',
|
||||
timezone:
|
||||
custom.timezone || custom.timezone_offset?.toString() || 'US/Eastern',
|
||||
output_timezone: custom.output_timezone || '',
|
||||
program_duration: custom.program_duration || 180,
|
||||
sample_title: custom.sample_title || '',
|
||||
title_template: custom.title_template || '',
|
||||
subtitle_template: custom.subtitle_template || '',
|
||||
description_template: custom.description_template || '',
|
||||
upcoming_title_template: custom.upcoming_title_template || '',
|
||||
upcoming_description_template: custom.upcoming_description_template || '',
|
||||
ended_title_template: custom.ended_title_template || '',
|
||||
ended_description_template: custom.ended_description_template || '',
|
||||
fallback_title_template: custom.fallback_title_template || '',
|
||||
fallback_description_template: custom.fallback_description_template || '',
|
||||
channel_logo_url: custom.channel_logo_url || '',
|
||||
program_poster_url: custom.program_poster_url || '',
|
||||
name_source: custom.name_source || 'channel',
|
||||
stream_index: custom.stream_index || 1,
|
||||
category: custom.category || '',
|
||||
include_date: custom.include_date ?? true,
|
||||
include_live: custom.include_live ?? false,
|
||||
include_new: custom.include_new ?? false,
|
||||
});
|
||||
|
||||
export const validateCustomTitlePattern = (value) => {
|
||||
if (!value?.trim()) return 'Title pattern is required';
|
||||
try {
|
||||
new RegExp(value);
|
||||
return null;
|
||||
} catch (e) {
|
||||
return `Invalid regex: ${e.message}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const validateCustomNameSource = (value) => {
|
||||
if (!value) return 'Name source is required';
|
||||
return null;
|
||||
};
|
||||
|
||||
export const validateCustomStreamIndex = (values, value) => {
|
||||
if (values.custom_properties?.name_source === 'stream') {
|
||||
if (!value || value < 1) {
|
||||
return 'Stream index must be at least 1';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const matchPattern = (pattern, input, errorPrefix) => {
|
||||
if (!pattern || !input) return { matched: false, groups: {}, error: null };
|
||||
try {
|
||||
const match = input.match(new RegExp(pattern));
|
||||
return match
|
||||
? { matched: true, groups: match.groups || {}, error: null }
|
||||
: { matched: false, groups: {}, error: null };
|
||||
} catch (e) {
|
||||
return {
|
||||
matched: false,
|
||||
groups: {},
|
||||
error: `${errorPrefix}: ${e.message}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const addNormalizedGroups = (groups) => {
|
||||
const result = { ...groups };
|
||||
Object.keys(groups).forEach((key) => {
|
||||
if (groups[key]) {
|
||||
result[`${key}_normalize`] = String(groups[key])
|
||||
.replace(/[^a-zA-Z0-9\s]/g, '')
|
||||
.replace(/\s+/g, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const formatTime12 = (h, m) => {
|
||||
const period = h < 12 ? 'AM' : 'PM';
|
||||
let h12 = h % 12 || 12;
|
||||
return m > 0
|
||||
? `${h12}:${String(m).padStart(2, '0')} ${period}`
|
||||
: `${h12} ${period}`;
|
||||
};
|
||||
|
||||
const formatTime12Long = (h, m) => {
|
||||
const period = h < 12 ? 'AM' : 'PM';
|
||||
let h12 = h % 12 || 12;
|
||||
return `${h12}:${String(m).padStart(2, '0')} ${period}`;
|
||||
};
|
||||
|
||||
const formatTime24 = (h, m) =>
|
||||
m > 0
|
||||
? `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
|
||||
: `${String(h).padStart(2, '0')}:00`;
|
||||
|
||||
export const buildTimePlaceholders = (
|
||||
timeGroups,
|
||||
dateGroups,
|
||||
sourceTimezone,
|
||||
outputTimezone,
|
||||
programDuration
|
||||
) => {
|
||||
if (!timeGroups || !timeGroups.hour) return {};
|
||||
|
||||
try {
|
||||
let hour24 = parseInt(timeGroups.hour);
|
||||
const minute = timeGroups.minute ? parseInt(timeGroups.minute) : 0;
|
||||
const ampm = timeGroups.ampm?.toLowerCase();
|
||||
|
||||
if (ampm === 'pm' && hour24 !== 12) hour24 += 12;
|
||||
else if (ampm === 'am' && hour24 === 12) hour24 = 0;
|
||||
|
||||
let baseDate = setTz(getNow(), sourceTimezone);
|
||||
|
||||
if (dateGroups.month && dateGroups.day) {
|
||||
const monthValue = dateGroups.month;
|
||||
let extractedMonth;
|
||||
if (/^\d+$/.test(monthValue)) {
|
||||
extractedMonth = parseInt(monthValue);
|
||||
} else {
|
||||
const lower = monthValue.toLowerCase();
|
||||
const idx =
|
||||
MONTH_NAMES.indexOf(lower) !== -1
|
||||
? MONTH_NAMES.indexOf(lower)
|
||||
: MONTH_ABBR.indexOf(lower);
|
||||
extractedMonth = idx !== -1 ? idx + 1 : getMonth(getNow()) + 1;
|
||||
}
|
||||
const extractedDay = parseInt(dateGroups.day);
|
||||
const extractedYear = dateGroups.year
|
||||
? parseInt(dateGroups.year)
|
||||
: getYear(getNow());
|
||||
if (
|
||||
!isNaN(extractedMonth) &&
|
||||
!isNaN(extractedDay) &&
|
||||
!isNaN(extractedYear) &&
|
||||
extractedMonth >= 1 &&
|
||||
extractedMonth <= 12 &&
|
||||
extractedDay >= 1 &&
|
||||
extractedDay <= 31
|
||||
) {
|
||||
baseDate = setTz(
|
||||
`${extractedYear}-${String(extractedMonth).padStart(2, '0')}-${String(extractedDay).padStart(2, '0')}`,
|
||||
sourceTimezone
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let sourceDate = setHour(baseDate, hour24);
|
||||
sourceDate = setMinute(sourceDate, minute);
|
||||
sourceDate = setSecond(sourceDate, 0);
|
||||
const workDate =
|
||||
outputTimezone && outputTimezone !== sourceTimezone
|
||||
? setTz(sourceDate, outputTimezone)
|
||||
: sourceDate;
|
||||
|
||||
const h24 = getHour(workDate);
|
||||
const min = getMinute(workDate);
|
||||
|
||||
const endTotalMin = h24 * 60 + min + (programDuration || 180);
|
||||
const endH24 = Math.floor(endTotalMin / 60) % 24;
|
||||
const endMin = endTotalMin % 60;
|
||||
|
||||
return {
|
||||
starttime: formatTime12(h24, min),
|
||||
starttime_long: formatTime12Long(h24, min),
|
||||
starttime24: formatTime24(h24, min),
|
||||
starttime24_long: formatTime24(h24, min),
|
||||
endtime: formatTime12(endH24, endMin),
|
||||
endtime_long: formatTime12Long(endH24, endMin),
|
||||
endtime24: formatTime24(endH24, endMin),
|
||||
date: format(workDate, 'YYYY-MM-DD'),
|
||||
month: getMonth(workDate) + 1,
|
||||
day: getDay(workDate),
|
||||
year: getYear(workDate),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error building time placeholders:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const PLAIN_TEMPLATES = [
|
||||
{ stateKey: 'titleTemplate', resultKey: 'formattedTitle' },
|
||||
{ stateKey: 'subtitleTemplate', resultKey: 'formattedSubtitle' },
|
||||
{ stateKey: 'descriptionTemplate', resultKey: 'formattedDescription' },
|
||||
{ stateKey: 'upcomingTitleTemplate', resultKey: 'formattedUpcomingTitle' },
|
||||
{
|
||||
stateKey: 'upcomingDescriptionTemplate',
|
||||
resultKey: 'formattedUpcomingDescription',
|
||||
},
|
||||
{ stateKey: 'endedTitleTemplate', resultKey: 'formattedEndedTitle' },
|
||||
{
|
||||
stateKey: 'endedDescriptionTemplate',
|
||||
resultKey: 'formattedEndedDescription',
|
||||
},
|
||||
];
|
||||
|
||||
const URL_TEMPLATES = [
|
||||
{ stateKey: 'channelLogoUrl', resultKey: 'formattedChannelLogoUrl' },
|
||||
{ stateKey: 'programPosterUrl', resultKey: 'formattedProgramPosterUrl' },
|
||||
];
|
||||
|
||||
export const applyTemplates = (templateValues, groups, hasMatch) => {
|
||||
const result = {};
|
||||
if (!hasMatch) return result;
|
||||
|
||||
PLAIN_TEMPLATES.forEach(({ stateKey, resultKey }) => {
|
||||
if (templateValues[stateKey]) {
|
||||
result[resultKey] = templateValues[stateKey].replace(
|
||||
/\{(\w+)\}/g,
|
||||
(m, k) => groups[k] || m
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
URL_TEMPLATES.forEach(({ stateKey, resultKey }) => {
|
||||
if (templateValues[stateKey]) {
|
||||
result[resultKey] = templateValues[stateKey].replace(
|
||||
/\{(\w+)\}/g,
|
||||
(m, k) => (groups[k] ? encodeURIComponent(String(groups[k])) : m)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue