mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
FIxed bug
Fixed bug that stopped stream from ending
This commit is contained in:
parent
eee4ab0725
commit
6536f35dc0
4 changed files with 93 additions and 71 deletions
|
|
@ -528,26 +528,28 @@ class RecurringRecordingRuleSerializer(serializers.ModelSerializer):
|
|||
return sorted(set(cleaned))
|
||||
|
||||
def validate(self, attrs):
|
||||
from django.utils import timezone
|
||||
start = attrs.get("start_time") or getattr(self.instance, "start_time", None)
|
||||
end = attrs.get("end_time") or getattr(self.instance, "end_time", None)
|
||||
if start and end and end <= start:
|
||||
raise serializers.ValidationError("End time must be after start time")
|
||||
start_date = attrs.get("start_date") if "start_date" in attrs else getattr(self.instance, "start_date", None)
|
||||
end_date = attrs.get("end_date") if "end_date" in attrs else getattr(self.instance, "end_date", None)
|
||||
if start_date is None:
|
||||
existing_start = getattr(self.instance, "start_date", None)
|
||||
if existing_start is None:
|
||||
raise serializers.ValidationError("Start date is required")
|
||||
if start_date and end_date and end_date < start_date:
|
||||
raise serializers.ValidationError("End date must be on or after start date")
|
||||
if end_date is None:
|
||||
existing_end = getattr(self.instance, "end_date", None)
|
||||
if existing_end is None:
|
||||
raise serializers.ValidationError("End date is required")
|
||||
# Normalize empty strings to None for dates
|
||||
if attrs.get("end_date") == "":
|
||||
attrs["end_date"] = None
|
||||
if attrs.get("start_date") == "":
|
||||
attrs["start_date"] = None
|
||||
if attrs.get("start_date") is None and not getattr(self.instance, "start_date", None):
|
||||
attrs["start_date"] = timezone.localdate()
|
||||
return super().validate(attrs)
|
||||
|
||||
def create(self, validated_data):
|
||||
from django.utils import timezone
|
||||
if not validated_data.get("start_date"):
|
||||
validated_data["start_date"] = timezone.localdate()
|
||||
return super().create(validated_data)
|
||||
|
|
|
|||
|
|
@ -1145,9 +1145,12 @@ def sync_recurring_rule_impl(rule_id: int, drop_existing: bool = True, horizon_d
|
|||
end_limit = rule.end_date
|
||||
horizon = now + timedelta(days=horizon_days)
|
||||
start_window = max(start_limit, now.date())
|
||||
end_window = horizon.date()
|
||||
if end_limit and end_limit < end_window:
|
||||
if drop_existing and end_limit:
|
||||
end_window = end_limit
|
||||
else:
|
||||
end_window = horizon.date()
|
||||
if end_limit and end_limit < end_window:
|
||||
end_window = end_limit
|
||||
if end_window < start_window:
|
||||
return 0
|
||||
total_created = 0
|
||||
|
|
@ -1163,7 +1166,9 @@ def sync_recurring_rule_impl(rule_id: int, drop_existing: bool = True, horizon_d
|
|||
end_dt = timezone.make_aware(datetime.combine(target_date, rule.end_time), tz)
|
||||
except Exception:
|
||||
continue
|
||||
if end_dt <= start_dt or start_dt <= now:
|
||||
if end_dt <= start_dt:
|
||||
end_dt = end_dt + timedelta(days=1)
|
||||
if start_dt <= now:
|
||||
continue
|
||||
exists = Recording.objects.filter(
|
||||
channel=rule.channel,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
SegmentedControl,
|
||||
MultiSelect,
|
||||
Group,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { DateTimePicker, TimeInput, DatePickerInput } from '@mantine/dates';
|
||||
|
|
@ -41,13 +40,12 @@ const toIsoIfDate = (value) => {
|
|||
return dt ? dt.toISOString() : value;
|
||||
};
|
||||
|
||||
// Accepts "h:mm A"/"hh:mm A"/"HH:mm"/Date, returns "HH:mm"
|
||||
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');
|
||||
}
|
||||
const parsed = dayjs(value, ['HH:mm', 'hh:mm A', 'h:mm A', 'HH:mm:ss'], true);
|
||||
if (parsed.isValid()) return parsed.format('HH:mm');
|
||||
return value;
|
||||
}
|
||||
const dt = asDate(value);
|
||||
|
|
@ -69,12 +67,16 @@ const createRoundedDate = (minutesAhead = 0) => {
|
|||
dt.setSeconds(0);
|
||||
dt.setMilliseconds(0);
|
||||
dt.setMinutes(Math.ceil(dt.getMinutes() / 30) * 30);
|
||||
if (minutesAhead) {
|
||||
dt.setMinutes(dt.getMinutes() + minutesAhead);
|
||||
}
|
||||
if (minutesAhead) dt.setMinutes(dt.getMinutes() + minutesAhead);
|
||||
return dt;
|
||||
};
|
||||
|
||||
// robust onChange for TimeInput (string or event)
|
||||
const timeChange = (setter) => (valOrEvent) => {
|
||||
if (typeof valOrEvent === 'string') setter(valOrEvent);
|
||||
else if (valOrEvent?.currentTarget) setter(valOrEvent.currentTarget.value);
|
||||
};
|
||||
|
||||
const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) => {
|
||||
const channels = useChannelsStore((s) => s.channels);
|
||||
const fetchRecordings = useChannelsStore((s) => s.fetchRecordings);
|
||||
|
|
@ -87,14 +89,11 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
const defaultEnd = createRoundedDate(60);
|
||||
const defaultDate = new Date();
|
||||
|
||||
// One-time form
|
||||
const singleForm = useForm({
|
||||
mode: 'controlled',
|
||||
initialValues: {
|
||||
channel_id: recording
|
||||
? `${recording.channel}`
|
||||
: channel
|
||||
? `${channel.id}`
|
||||
: '',
|
||||
channel_id: recording ? `${recording.channel}` : channel ? `${channel.id}` : '',
|
||||
start_time: recording ? asDate(recording.start_time) || defaultStart : defaultStart,
|
||||
end_time: recording ? asDate(recording.end_time) || defaultEnd : defaultEnd,
|
||||
},
|
||||
|
|
@ -111,8 +110,11 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
},
|
||||
});
|
||||
|
||||
// Recurring form stores times as "HH:mm" strings for stable editing
|
||||
const recurringForm = useForm({
|
||||
mode: 'controlled',
|
||||
validateInputOnChange: false,
|
||||
validateInputOnBlur: true,
|
||||
initialValues: {
|
||||
channel_id: channel ? `${channel.id}` : '',
|
||||
days_of_week: [],
|
||||
|
|
@ -120,34 +122,38 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
end_time: dayjs(defaultEnd).format('HH:mm'),
|
||||
rule_name: '',
|
||||
start_date: defaultDate,
|
||||
end_date: null,
|
||||
end_date: defaultDate,
|
||||
},
|
||||
validate: {
|
||||
channel_id: isNotEmpty('Select a channel'),
|
||||
days_of_week: (value) => (value && value.length ? null : 'Pick at least one day'),
|
||||
start_time: isNotEmpty('Select a start time'),
|
||||
start_time: (value) => (value ? null : 'Select a start time'),
|
||||
end_time: (value, values) => {
|
||||
const start = asDate(values.start_time);
|
||||
const end = asDate(value);
|
||||
if (!end) return 'Select an end time';
|
||||
if (start && end <= start) return 'End time must be after start time';
|
||||
if (!value) return 'Select an end time';
|
||||
const start = dayjs(values.start_time, ['HH:mm', 'hh:mm A', 'h:mm A'], true);
|
||||
const end = dayjs(value, ['HH:mm', 'hh:mm A', 'h:mm A'], true);
|
||||
if (start.isValid() && end.isValid() && end.diff(start, 'minute') === 0) {
|
||||
return 'End time must differ from start time';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
end_date: (value, values) => {
|
||||
const end = asDate(value);
|
||||
const start = asDate(values.start_date);
|
||||
if (end && start && end < start) {
|
||||
return 'End date cannot be before start date';
|
||||
}
|
||||
if (!end) return 'Select an end date';
|
||||
if (start && end < start) return 'End date cannot be before start date';
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
if (!isOpen) return;
|
||||
|
||||
const freshStart = createRoundedDate();
|
||||
const freshEnd = createRoundedDate(60);
|
||||
const freshDate = new Date();
|
||||
|
||||
if (recording && recording.id) {
|
||||
setMode('single');
|
||||
singleForm.setValues({
|
||||
|
|
@ -156,23 +162,22 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
end_time: asDate(recording.end_time) || defaultEnd,
|
||||
});
|
||||
} else {
|
||||
// Reset forms to defaults when opening fresh
|
||||
const freshStart = createRoundedDate();
|
||||
const freshEnd = createRoundedDate(60);
|
||||
const freshDate = new Date();
|
||||
// Reset forms for fresh open
|
||||
singleForm.setValues({
|
||||
channel_id: channel ? `${channel.id}` : '',
|
||||
start_time: freshStart,
|
||||
end_time: freshEnd,
|
||||
});
|
||||
|
||||
const startStr = dayjs(freshStart).format('HH:mm');
|
||||
recurringForm.setValues({
|
||||
channel_id: channel ? `${channel.id}` : '',
|
||||
days_of_week: [],
|
||||
start_time: dayjs(freshStart).format('HH:mm'),
|
||||
start_time: startStr,
|
||||
end_time: dayjs(freshEnd).format('HH:mm'),
|
||||
rule_name: channel?.name || '',
|
||||
start_date: freshDate,
|
||||
end_date: null,
|
||||
end_date: freshDate,
|
||||
});
|
||||
setMode('single');
|
||||
}
|
||||
|
|
@ -184,9 +189,7 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
list.sort((a, b) => {
|
||||
const aNum = Number(a.channel_number) || 0;
|
||||
const bNum = Number(b.channel_number) || 0;
|
||||
if (aNum === bNum) {
|
||||
return (a.name || '').localeCompare(b.name || '');
|
||||
}
|
||||
if (aNum === bNum) return (a.name || '').localeCompare(b.name || '');
|
||||
return aNum - bNum;
|
||||
});
|
||||
return list.map((item) => ({ value: `${item.id}`, label: item.name || `Channel ${item.id}` }));
|
||||
|
|
@ -252,6 +255,7 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
end_date: toDateString(values.end_date),
|
||||
name: values.rule_name?.trim() || '',
|
||||
});
|
||||
|
||||
await Promise.all([fetchRecurringRules(), fetchRecordings()]);
|
||||
notifications.show({
|
||||
title: 'Recurring rule saved',
|
||||
|
|
@ -267,13 +271,12 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
}
|
||||
};
|
||||
|
||||
const onSubmit = mode === 'single'
|
||||
? singleForm.onSubmit(handleSingleSubmit)
|
||||
: recurringForm.onSubmit(handleRecurringSubmit);
|
||||
const onSubmit =
|
||||
mode === 'single'
|
||||
? singleForm.onSubmit(handleSingleSubmit)
|
||||
: recurringForm.onSubmit(handleRecurringSubmit);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<Modal opened={isOpen} onClose={handleClose} title="Channel Recording">
|
||||
|
|
@ -326,15 +329,15 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
{...singleForm.getInputProps('start_time')}
|
||||
key={singleForm.key('start_time')}
|
||||
label="Start"
|
||||
valueFormat="MMM D, YYYY hh:mm A"
|
||||
timeInputProps={{ format: '12', amLabel: 'AM', pmLabel: 'PM', withSeconds: false }}
|
||||
valueFormat="MMM D, YYYY h:mm A"
|
||||
timeInputProps={{ format: '12', withSeconds: false, amLabel: 'AM', pmLabel: 'PM' }}
|
||||
/>
|
||||
<DateTimePicker
|
||||
{...singleForm.getInputProps('end_time')}
|
||||
key={singleForm.key('end_time')}
|
||||
label="End"
|
||||
valueFormat="MMM D, YYYY hh:mm A"
|
||||
timeInputProps={{ format: '12', amLabel: 'AM', pmLabel: 'PM', withSeconds: false }}
|
||||
valueFormat="MMM D, YYYY h:mm A"
|
||||
timeInputProps={{ format: '12', withSeconds: false, amLabel: 'AM', pmLabel: 'PM' }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -353,41 +356,52 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
data={DAY_OPTIONS}
|
||||
searchable
|
||||
clearable
|
||||
nothingFound="No match"
|
||||
nothingFoundMessage="No match"
|
||||
/>
|
||||
|
||||
<Group grow>
|
||||
<DatePickerInput
|
||||
label="Start date"
|
||||
value={recurringForm.values.start_date}
|
||||
onChange={(value) => recurringForm.setFieldValue('start_date', value || new Date())}
|
||||
onChange={(value) =>
|
||||
recurringForm.setFieldValue('start_date', value || new Date())
|
||||
}
|
||||
valueFormat="MMM D, YYYY"
|
||||
/>
|
||||
<DatePickerInput
|
||||
label="End date"
|
||||
placeholder="No end date"
|
||||
clearable
|
||||
value={recurringForm.values.end_date}
|
||||
onChange={(value) => recurringForm.setFieldValue('end_date', value)}
|
||||
valueFormat="MMM D, YYYY"
|
||||
minDate={recurringForm.values.start_date || undefined}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group grow>
|
||||
<TimeInput
|
||||
value={recurringForm.values.start_time}
|
||||
onChange={(value) => recurringForm.setFieldValue('start_time', value)}
|
||||
label="Start time"
|
||||
value={recurringForm.values.start_time}
|
||||
onChange={timeChange((val) =>
|
||||
recurringForm.setFieldValue('start_time', toTimeString(val))
|
||||
)}
|
||||
onBlur={() => recurringForm.validateField('start_time')}
|
||||
withSeconds={false}
|
||||
format="12"
|
||||
format="12" // shows 12-hour (so "00:00" renders "12:00 AM")
|
||||
inputMode="numeric"
|
||||
amLabel="AM"
|
||||
pmLabel="PM"
|
||||
/>
|
||||
|
||||
<TimeInput
|
||||
value={recurringForm.values.end_time}
|
||||
onChange={(value) => recurringForm.setFieldValue('end_time', value)}
|
||||
label="End time"
|
||||
value={recurringForm.values.end_time}
|
||||
onChange={timeChange((val) =>
|
||||
recurringForm.setFieldValue('end_time', toTimeString(val))
|
||||
)}
|
||||
onBlur={() => recurringForm.validateField('end_time')}
|
||||
withSeconds={false}
|
||||
format="12"
|
||||
inputMode="numeric"
|
||||
amLabel="AM"
|
||||
pmLabel="PM"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -375,23 +375,26 @@ const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence }) => {
|
|||
start_time: dayjs().startOf('hour').format('HH:mm'),
|
||||
end_time: dayjs().startOf('hour').add(1, 'hour').format('HH:mm'),
|
||||
start_date: dayjs().toDate(),
|
||||
end_date: null,
|
||||
end_date: dayjs().toDate(),
|
||||
enabled: true,
|
||||
},
|
||||
validate: {
|
||||
channel_id: (value) => (value ? null : 'Select a channel'),
|
||||
days_of_week: (value) => (value && value.length ? null : 'Pick at least one day'),
|
||||
end_time: (value, values) => {
|
||||
const startValue = dayjs(values.start_time);
|
||||
const endValue = dayjs(value);
|
||||
if (!value) return 'Select an end time';
|
||||
if (endValue.isSameOrBefore(startValue)) return 'End time must be after start time';
|
||||
const startValue = dayjs(values.start_time, ['HH:mm', 'hh:mm A', 'h:mm A'], true);
|
||||
const endValue = dayjs(value, ['HH:mm', 'hh:mm A', 'h:mm A'], true);
|
||||
if (startValue.isValid() && endValue.isValid() && endValue.diff(startValue, 'minute') === 0) {
|
||||
return 'End time must differ from start time';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
end_date: (value, values) => {
|
||||
const endDate = dayjs(value);
|
||||
const startDate = dayjs(values.start_date);
|
||||
if (value && startDate.isValid() && endDate.isBefore(startDate, 'day')) {
|
||||
if (!value) return 'Select an end date';
|
||||
if (startDate.isValid() && endDate.isBefore(startDate, 'day')) {
|
||||
return 'End date cannot be before start date';
|
||||
}
|
||||
return null;
|
||||
|
|
@ -564,8 +567,6 @@ const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence }) => {
|
|||
/>
|
||||
<DatePickerInput
|
||||
label="End date"
|
||||
placeholder="No end date"
|
||||
clearable
|
||||
value={form.values.end_date}
|
||||
onChange={(value) => form.setFieldValue('end_date', value)}
|
||||
valueFormat="MMM D, YYYY"
|
||||
|
|
@ -576,7 +577,7 @@ const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence }) => {
|
|||
<TimeInput
|
||||
label="Start time"
|
||||
value={form.values.start_time}
|
||||
onChange={(value) => form.setFieldValue('start_time', value)}
|
||||
onChange={(value) => form.setFieldValue('start_time', toTimeString(value))}
|
||||
withSeconds={false}
|
||||
format="12"
|
||||
amLabel="AM"
|
||||
|
|
@ -585,7 +586,7 @@ const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence }) => {
|
|||
<TimeInput
|
||||
label="End time"
|
||||
value={form.values.end_time}
|
||||
onChange={(value) => form.setFieldValue('end_time', value)}
|
||||
onChange={(value) => form.setFieldValue('end_time', toTimeString(value))}
|
||||
withSeconds={false}
|
||||
format="12"
|
||||
amLabel="AM"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue