Extracted DVR utils

This commit is contained in:
Nick Sandstrom 2025-12-13 06:32:16 -08:00
parent bfcc47c331
commit ae60f81314
2 changed files with 213 additions and 0 deletions

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

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