mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-28 04:24:33 +00:00
Extracted DVR utils
This commit is contained in:
parent
bfcc47c331
commit
ae60f81314
2 changed files with 213 additions and 0 deletions
123
frontend/src/utils/dateTimeUtils.js
Normal file
123
frontend/src/utils/dateTimeUtils.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Flex,
|
||||
Badge,
|
||||
Group,
|
||||
Image,
|
||||
Modal,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
Switch,
|
||||
Select,
|
||||
MultiSelect,
|
||||
TextInput,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
AlertTriangle,
|
||||
SquarePlus,
|
||||
SquareX,
|
||||
} from 'lucide-react';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import useChannelsStore from '../store/channels';
|
||||
import useSettingsStore from '../store/settings';
|
||||
import useLocalStorage from '../hooks/useLocalStorage';
|
||||
import useVideoStore from '../store/useVideoStore';
|
||||
import RecordingForm from '../components/forms/Recording';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import API from '../api';
|
||||
import { DatePickerInput, TimeInput } from '@mantine/dates';
|
||||
import { useForm } from '@mantine/form';
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export const useUserTimeZone = () => {
|
||||
const settings = useSettingsStore((s) => s.settings);
|
||||
const [timeZone, setTimeZone] = useLocalStorage(
|
||||
'time-zone',
|
||||
dayjs.tz?.guess
|
||||
? dayjs.tz.guess()
|
||||
: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const tz = settings?.['system-time-zone']?.value;
|
||||
if (tz && tz !== timeZone) {
|
||||
setTimeZone(tz);
|
||||
}
|
||||
}, [settings, timeZone, setTimeZone]);
|
||||
|
||||
return timeZone;
|
||||
};
|
||||
|
||||
export const useTimeHelpers = () => {
|
||||
const timeZone = useUserTimeZone();
|
||||
|
||||
const toUserTime = useCallback(
|
||||
(value) => {
|
||||
if (!value) return dayjs.invalid();
|
||||
try {
|
||||
return dayjs(value).tz(timeZone);
|
||||
} catch (error) {
|
||||
return dayjs(value);
|
||||
}
|
||||
},
|
||||
[timeZone]
|
||||
);
|
||||
|
||||
const userNow = useCallback(() => dayjs().tz(timeZone), [timeZone]);
|
||||
|
||||
return { timeZone, toUserTime, userNow };
|
||||
};
|
||||
|
||||
export const RECURRING_DAY_OPTIONS = [
|
||||
{ value: 6, label: 'Sun' },
|
||||
{ value: 0, label: 'Mon' },
|
||||
{ value: 1, label: 'Tue' },
|
||||
{ value: 2, label: 'Wed' },
|
||||
{ value: 3, label: 'Thu' },
|
||||
{ value: 4, label: 'Fri' },
|
||||
{ value: 5, label: 'Sat' },
|
||||
];
|
||||
|
||||
export const useDateTimeFormat = () => {
|
||||
const [timeFormatSetting] = useLocalStorage('time-format', '12h');
|
||||
const [dateFormatSetting] = useLocalStorage('date-format', 'mdy');
|
||||
// Use user preference for time format
|
||||
const timeFormat = timeFormatSetting === '12h' ? 'h:mma' : 'HH:mm';
|
||||
const dateFormat = dateFormatSetting === 'mdy' ? 'MMM D' : 'D MMM';
|
||||
|
||||
return [timeFormat, dateFormat]
|
||||
};
|
||||
|
||||
export const toTimeString = (value) => {
|
||||
if (!value) return '00:00';
|
||||
if (typeof value === 'string') {
|
||||
const parsed = dayjs(value, ['HH:mm', 'HH:mm:ss', 'h:mm A'], true);
|
||||
if (parsed.isValid()) return parsed.format('HH:mm');
|
||||
return value;
|
||||
}
|
||||
const parsed = dayjs(value);
|
||||
return parsed.isValid() ? parsed.format('HH:mm') : '00:00';
|
||||
};
|
||||
|
||||
export const parseDate = (value) => {
|
||||
if (!value) return null;
|
||||
const parsed = dayjs(value, ['YYYY-MM-DD', dayjs.ISO_8601], true);
|
||||
return parsed.isValid() ? parsed.toDate() : null;
|
||||
};
|
||||
90
frontend/src/utils/pages/DVRUtils.js
Normal file
90
frontend/src/utils/pages/DVRUtils.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// Deduplicate in-progress and upcoming by program id or channel+slot
|
||||
const dedupeByProgramOrSlot = (arr) => {
|
||||
const out = [];
|
||||
const sigs = new Set();
|
||||
|
||||
for (const r of arr) {
|
||||
const cp = r.custom_properties || {};
|
||||
const pr = cp.program || {};
|
||||
const sig =
|
||||
pr?.id != null
|
||||
? `id:${pr.id}`
|
||||
: `slot:${r.channel}|${r.start_time}|${r.end_time}|${pr.title || ''}`;
|
||||
|
||||
if (sigs.has(sig)) continue;
|
||||
sigs.add(sig);
|
||||
out.push(r);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const dedupeById = (list, toUserTime, completed, now, inProgress, upcoming) => {
|
||||
// ID-based dedupe guard in case store returns duplicates
|
||||
const seenIds = new Set();
|
||||
for (const rec of list) {
|
||||
if (rec && rec.id != null) {
|
||||
const k = String(rec.id);
|
||||
if (seenIds.has(k)) continue;
|
||||
seenIds.add(k);
|
||||
}
|
||||
|
||||
const s = toUserTime(rec.start_time);
|
||||
const e = toUserTime(rec.end_time);
|
||||
const status = rec.custom_properties?.status;
|
||||
|
||||
if (status === 'interrupted' || status === 'completed') {
|
||||
completed.push(rec);
|
||||
} else {
|
||||
if (now.isAfter(s) && now.isBefore(e)) inProgress.push(rec);
|
||||
else if (now.isBefore(s)) upcoming.push(rec);
|
||||
else completed.push(rec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const categorizeRecordings = (recordings, toUserTime, now) => {
|
||||
const inProgress = [];
|
||||
const upcoming = [];
|
||||
const completed = [];
|
||||
const list = Array.isArray(recordings)
|
||||
? recordings
|
||||
: Object.values(recordings || {});
|
||||
|
||||
dedupeById(list, toUserTime, completed, now, inProgress, upcoming);
|
||||
|
||||
const inProgressDedup = dedupeByProgramOrSlot(inProgress).sort(
|
||||
(a, b) => toUserTime(b.start_time) - toUserTime(a.start_time)
|
||||
);
|
||||
|
||||
// Group upcoming by series title+tvg_id (keep only next episode)
|
||||
const upcomingDedup = dedupeByProgramOrSlot(upcoming).sort(
|
||||
(a, b) => toUserTime(a.start_time) - toUserTime(b.start_time)
|
||||
);
|
||||
const grouped = new Map();
|
||||
|
||||
for (const rec of upcomingDedup) {
|
||||
const cp = rec.custom_properties || {};
|
||||
const prog = cp.program || {};
|
||||
const key = `${prog.tvg_id || ''}|${(prog.title || '').toLowerCase()}`;
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, { rec, count: 1 });
|
||||
} else {
|
||||
const entry = grouped.get(key);
|
||||
entry.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const upcomingGrouped = Array.from(grouped.values()).map((e) => {
|
||||
const item = { ...e.rec };
|
||||
item._group_count = e.count;
|
||||
return item;
|
||||
});
|
||||
|
||||
completed.sort((a, b) => toUserTime(b.end_time) - toUserTime(a.end_time));
|
||||
|
||||
return {
|
||||
inProgress: inProgressDedup,
|
||||
upcoming: upcomingGrouped,
|
||||
completed,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue