Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/src/db/derived_types/Lineup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const OnDemandChannelConfigSchema = z.object({

export type OnDemandChannelConfig = z.infer<typeof OnDemandChannelConfigSchema>;

export const CurrentLineupSchemaVersion = 5;
export const CurrentLineupSchemaVersion = 6;

export const LineupSchema = z.object({
version: z
Expand Down
20 changes: 20 additions & 0 deletions server/src/migration/lineups/AddOverflowMigration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { injectable } from 'inversify';
import { JsonObject } from '../../types/schemas.ts';
import { ChannelLineupMigration } from './ChannelLineupMigration.ts';

@injectable()
export class AddOverflowMigration extends ChannelLineupMigration<5, 6> {
readonly from = 5;
readonly to = 6;

migrate(lineup: JsonObject): Promise<void> {
const schedule = lineup['schedule'] as JsonObject | undefined;
if (!schedule || schedule['type'] !== 'time') {
return Promise.resolve();
}

schedule['overflow'] = { type: 'duration', maxMs: 0 };

return Promise.resolve();
}
}
2 changes: 2 additions & 0 deletions server/src/migration/lineups/ChannelLineupMigrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { FileSystemService } from '../../services/FileSystemService.ts';
import { parseIntOrNull } from '../../util/index.ts';
import { getFirstValue } from '../../util/json.ts';
import { JsonFileMigrator } from '../JsonFileMigrator.ts';
import { AddOverflowMigration } from './AddOverflowMigration.ts';
import { AddSlotIdMigration } from './AddSlotIdMigration.ts';
import { ChannelLineupMigration } from './ChannelLineupMigration.ts';
import { SlotProgrammingMigration } from './SlotProgrammingMigration.ts';
Expand All @@ -30,6 +31,7 @@ const MigrationSteps: ServiceIdentifier<
RandomSlotDurationSpecMigration,
SlotProgrammingMigration,
AddSlotIdMigration,
AddOverflowMigration,
];

/**
Expand Down
17 changes: 15 additions & 2 deletions server/src/services/TvGuideService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export class TVGuideService {
private accumulateTable: Record<string, number[]> = {};
private channelsById?: Record<string, ChannelWithLineup>;

@InjectLogger() private declare readonly logger: Logger;
@InjectLogger() declare private readonly logger: Logger;

constructor(
@inject(XmlTvWriter) xmltv: XmlTvWriter,
Expand Down Expand Up @@ -710,7 +710,20 @@ export class TVGuideService {
let melded = 0;

const push = (program: GuideItem) => {
const currentProgram = program.lineupItem;
// Normalize filler items to offline so they always participate
// in offline melding and never appear as content in the EPG.
let currentProgram = program.lineupItem;
if (
currentProgram.type === 'content' &&
isNonEmptyString(currentProgram.fillerListId)
) {
currentProgram = {
type: 'offline',
durationMs: currentProgram.durationMs,
};
program = { ...program, lineupItem: currentProgram };
}

const previousProgramIndex =
!isUndefined(program.index) &&
inRange(program.index - 1, 0, programs.length)
Expand Down
8 changes: 8 additions & 0 deletions server/src/services/scheduling/TimeSlotImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,12 @@ export class TimeSlotImpl<
}
return pad > 0 ? pad : undefined;
}

get overflow() {
return this.slot.overflow;
}

get latenessMs() {
return this.slot.latenessMs;
}
}
13 changes: 10 additions & 3 deletions server/src/services/scheduling/TimeSlotService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,10 @@ export async function scheduleTimeSlots(
slotDuration: slotDuration,
});

const effectiveLateness = currSlot.latenessMs ?? schedule.latenessMs;
if (
!isNull(lateMillis) &&
lateMillis >= schedule.latenessMs + constants.SLACK
lateMillis >= effectiveLateness + constants.SLACK
) {
pushFlex(slotDuration);
continue;
Expand Down Expand Up @@ -282,18 +283,24 @@ export async function scheduleTimeSlots(
);
let totalAddedDuration = paddedProgram.totalDuration;

const effectiveOverflow = currSlot.overflow ?? schedule.overflow;

for (;;) {
const nextProgram = currSlot.getNextProgram({
timeCursor: +timeCursor + totalAddedDuration,
slotDuration: slotDuration,
});
if (isNull(nextProgram)) break;
if (

if (effectiveOverflow.type === 'oneExtra') {
if (totalAddedDuration >= slotDuration) break;
} else if (
totalAddedDuration + nextProgram.duration >
slotDuration + schedule.latenessMs
slotDuration + effectiveOverflow.maxMs
) {
break;
}

const nextPadded = createPaddedProgram(nextProgram, schedule.padMs);
paddedPrograms.push(nextPadded);
currSlot.advanceIterator();
Expand Down
10 changes: 10 additions & 0 deletions types/src/api/TimeSlots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ import {
//
// Time slots
//
export const OverflowConfig = z.discriminatedUnion('type', [
z.object({ type: z.literal('duration'), maxMs: z.number().nonnegative() }),
z.object({ type: z.literal('oneExtra') }),
]);

export type OverflowConfig = z.infer<typeof OverflowConfig>;

const BaseTimeSlot = z.object({
startTime: z.number(), // Offset from midnight in millis
padMs: z.number().optional(),
overflow: OverflowConfig.optional(),
latenessMs: z.number().optional(),
});

export const MovieProgrammingTimeSlotSchema = z.object({
Expand Down Expand Up @@ -158,6 +167,7 @@ export const TimeSlotScheduleSchema = z.object({
flexPreference: z.enum(['distribute', 'end']),
latenessMs: z.number(), // max lateness in millis
maxDays: z.number(), // days to pregenerate schedule for
overflow: OverflowConfig.default({ type: 'duration', maxMs: 0 }),
padMs: z.number(), // Pad time in millis
period: z.enum(['day', 'week']),
slots: z.array(TimeSlotSchema),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ export const SlotProgrammingTooLongWarningDetails = ({
sx={{ mr: 1, color: (theme) => theme.palette.warning.main }}
/>
)}
<Typography><Trans>Programs Too Long</Trans></Typography>
<Typography>
<Trans>Programs Too Long</Trans>
</Typography>
</AccordionSummary>
<AccordionDetails>
<Stack>
Expand All @@ -158,22 +160,34 @@ export const SlotProgrammingTooLongWarningDetails = ({
<div>
<p>
<Trans>
{warning.programs.length} of {slot.programCount}{' '}
{plural(slot.programCount, { one: 'program', other: 'programs' })} exceed the length of
this slot ({betterHumanize(dayjs.duration(slot.durationMs ?? 0))}
). Average program length: {averageLength.humanize()}
{warning.programs.length} of {slot.programCount}{' '}
{plural(slot.programCount, {
one: 'program',
other: 'programs',
})}{' '}
exceed the length of this slot (
{betterHumanize(dayjs.duration(slot.durationMs ?? 0))}
). Average program length: {averageLength.humanize()}
</Trans>
<br />
<Trans>This could cause the following slot's programs to go unscheduled.
Possible solutions include:</Trans>
<Trans>
This could cause the following slot's programs to go
unscheduled. Possible solutions include:
</Trans>
</p>
<ul>
{}
{slotType === 'time' && (
<li><Trans>Increasing "Max Lateness" for the schedule.</Trans></li>
<li>
<Trans>Increasing "Max Overflow" for the schedule.</Trans>
</li>
)}
<li><Trans>Increasing the slot duration.</Trans></li>
<li><Trans>Removing overrun programs from the channel.</Trans></li>
<li>
<Trans>Increasing the slot duration.</Trans>
</li>
<li>
<Trans>Removing overrun programs from the channel.</Trans>
</li>
</ul>
</div>
<Box sx={{ width: '100%', height: 400 }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import {
Stack,
} from '@mui/material';
import { Controller, useFormContext } from 'react-hook-form';
import { padOptions } from '../../helpers/slotSchedulerUtil.ts';
import {
dropdownValueToOverflow,
latenessOptions,
overflowOptions,
overflowToDropdownValue,
padOptions,
} from '../../helpers/slotSchedulerUtil.ts';
import type { TimeSlotViewModel } from '../../model/TimeSlotModels.ts';

export const TimeSlotConfigDialogPanel = () => {
Expand Down Expand Up @@ -37,6 +43,70 @@ export const TimeSlotConfigDialogPanel = () => {
<Trans>Override how programs within this slot are padded.</Trans>
</FormHelperText>
</FormControl>
<FormControl fullWidth margin="normal">
<InputLabel>{t`Max Overflow`}</InputLabel>
<Controller
control={control}
name="overflow"
render={({ field }) => (
<Select
label={t`Max Overflow`}
value={
field.value !== undefined
? overflowToDropdownValue(field.value)
: ''
}
onChange={(e) => {
const val = e.target.value;
field.onChange(
val === '' ? undefined : dropdownValueToOverflow(val),
);
}}
>
<MenuItem value="">{t`Use schedule default`}</MenuItem>
{overflowOptions.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.description}
</MenuItem>
))}
</Select>
)}
/>
<FormHelperText>
<Trans>
Override how far past its boundary this slot's content can extend.
</Trans>
</FormHelperText>
</FormControl>
<FormControl fullWidth margin="normal">
<InputLabel>{t`Max Lateness`}</InputLabel>
<Controller
control={control}
name="latenessMs"
render={({ field }) => (
<Select
label={t`Max Lateness`}
value={field.value ?? ''}
onChange={(e) => {
const val = e.target.value;
field.onChange(val === '' ? undefined : Number(val));
}}
>
<MenuItem value="">{t`Use schedule default`}</MenuItem>
{latenessOptions.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.description}
</MenuItem>
))}
</Select>
)}
/>
<FormHelperText>
<Trans>
Override how late this slot can start if a previous slot ran long.
</Trans>
</FormHelperText>
</FormControl>
</Stack>
);
};
23 changes: 11 additions & 12 deletions web/src/components/slot_scheduler/TimeSlotTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export const TimeSlotTable = () => {
const providedDjs = useDayjs();
const localeData = useMemo(() => providedDjs().localeData(), [providedDjs]);
const { watch, slotArray, setValue, getValues } = useTimeSlotFormContext();
const [currentPeriod, latenessMs] = watch(['period', 'latenessMs']);
const [currentPeriod, overflow] = watch(['period', 'overflow']);
const programOptions = useSlotProgramOptionsContext();
const startOfPeriod = dayjs().startOf(currentPeriod);
const slotIds = useMemo(
Expand Down Expand Up @@ -156,10 +156,15 @@ export const TimeSlotTable = () => {
const slotDetails = detailsBySlotId[slotId];
let programCount = 0;
if (slotDetails) {
const overDuration = filter(
slotDetails.programDurations,
({ duration }) => duration > slotDuration + latenessMs,
);
const effectiveOverflow = slot.overflow ?? overflow;
const overDuration =
effectiveOverflow.type === 'oneExtra'
? []
: filter(
slotDetails.programDurations,
({ duration }) =>
duration > slotDuration + effectiveOverflow.maxMs,
);

if (overDuration.length > 0) {
warnings.push({
Expand All @@ -179,13 +184,7 @@ export const TimeSlotTable = () => {
} satisfies TimeSlotTableRowType;
},
);
}, [
currentPeriod,
detailsBySlotId,
latenessMs,
selectedDay,
slotArray.fields,
]);
}, [currentPeriod, detailsBySlotId, overflow, selectedDay, slotArray.fields]);

const columns = useMemo<MRT_ColumnDef<TimeSlotTableRowType>[]>(() => {
return [
Expand Down
Loading
Loading