From bc8a5482c2857a5f3cbfb7d483e26b72342cf0a9 Mon Sep 17 00:00:00 2001 From: Christian Benincasa Date: Sat, 27 Apr 2024 10:56:43 -0400 Subject: [PATCH] Allow program order selection in time slot config. Fixes #251 (#393) --- shared/src/index.ts | 6 ++- shared/src/services/ProgramIterator.ts | 3 +- shared/src/services/slotSchedulerUtil.ts | 53 ++++++++++++++++--- .../channel_config/ChannelProgrammingList.tsx | 17 +++++- web/src/pages/channels/TimeSlotEditorPage.tsx | 40 +++++++++++++- 5 files changed, 107 insertions(+), 12 deletions(-) diff --git a/shared/src/index.ts b/shared/src/index.ts index e65f1fb12..2e162a31a 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -1,4 +1,4 @@ -import { ExternalId } from '@tunarr/types'; +import { ExternalId, MultiExternalId } from '@tunarr/types'; export { scheduleRandomSlots } from './services/randomSlotsService.js'; export { scheduleTimeSlots } from './services/timeSlotService.js'; @@ -13,6 +13,10 @@ export function createExternalId( return `${sourceType}|${sourceId}|${itemId}`; } +export function createExternalIdFromMulti(multi: MultiExternalId) { + return createExternalId(multi.source, multi.sourceId, multi.id); +} + // We could type this better if we reuse the other ExternalId // types in createExternalId export function containsMultiExternalId( diff --git a/shared/src/services/ProgramIterator.ts b/shared/src/services/ProgramIterator.ts index 58b3f8ac4..dec18e438 100644 --- a/shared/src/services/ProgramIterator.ts +++ b/shared/src/services/ProgramIterator.ts @@ -63,6 +63,7 @@ export class ProgramOrderer implements ProgramIterator { orderer: (program: ContentProgram) => string | number = getProgramOrder, ) { this.#programs = sortBy(programs, orderer); + console.log(this.#programs); } current(): ChannelProgram | null { @@ -80,7 +81,7 @@ export function getProgramOrder(program: ContentProgram): string | number { return program.title; case 'episode': // Hacky thing from original code... - return program.seasonNumber! * 100000 + program.episodeNumber!; + return program.seasonNumber! * 1e5 + program.episodeNumber!; case 'track': // A-z for now return program.title; diff --git a/shared/src/services/slotSchedulerUtil.ts b/shared/src/services/slotSchedulerUtil.ts index 658fd9532..1a9a32833 100644 --- a/shared/src/services/slotSchedulerUtil.ts +++ b/shared/src/services/slotSchedulerUtil.ts @@ -1,16 +1,27 @@ import { ChannelProgram, + MultiExternalId, isContentProgram, isRedirectProgram, } from '@tunarr/types'; -import { TimeSlot, RandomSlot } from '@tunarr/types/api'; -import { reduce, isNull, first } from 'lodash-es'; +import { RandomSlot, TimeSlot } from '@tunarr/types/api'; import { - slotIteratorKey, - StaticProgramIterator, + filter, + first, + forEach, + isNull, + isUndefined, + map, + reduce, + some, +} from 'lodash-es'; +import { createExternalIdFromMulti } from '../index.js'; +import { + ProgramIterator, ProgramOrderer, ProgramShuffler, - ProgramIterator, + StaticProgramIterator, + slotIteratorKey, } from './ProgramIterator.js'; export type SlotLike = { @@ -101,10 +112,38 @@ export function createProgramIterators( } } else { const programs = programBySlotType['content'][slotId] ?? []; + // Remove any duplicates. + // We don't need to go through and remove flex since + // they will just be ignored during schedule generation + const seenDBIds = new Set(); + const seenIds = new Set(); + const uniquePrograms = filter(programs, (p) => { + if (p.persisted && !isUndefined(p.id) && !seenDBIds.has(p.id)) { + seenDBIds.add(p.id); + forEach(p.externalIds, (eid) => { + if (eid.type === 'multi') { + seenIds.add(createExternalIdFromMulti(eid)); + } + }); + return true; + } + + const externalIds = filter( + p.externalIds, + (eid): eid is MultiExternalId => eid.type === 'multi', + ); + const eids = map(externalIds, createExternalIdFromMulti); + if (some(eids, (eid) => seenIds.has(eid))) { + return false; + } + + forEach(eids, (eid) => seenIds.add(eid)); + return true; + }); acc[id] = slot.order === 'next' - ? new ProgramOrderer(programs) - : new ProgramShuffler(programs); + ? new ProgramOrderer(uniquePrograms) + : new ProgramShuffler(uniquePrograms); } } diff --git a/web/src/components/channel_config/ChannelProgrammingList.tsx b/web/src/components/channel_config/ChannelProgrammingList.tsx index d5deb09ad..221accca9 100644 --- a/web/src/components/channel_config/ChannelProgrammingList.tsx +++ b/web/src/components/channel_config/ChannelProgrammingList.tsx @@ -96,8 +96,21 @@ const programListItemTitleFormatter = (() => { switch (p.subtype) { case 'movie': return p.title; - case 'episode': - return p.episodeTitle ? `${p.title} - ${p.episodeTitle}` : p.title; + case 'episode': { + // TODO: this makes some assumptions about number of seasons + // and episodes... it may break + const epPart = + p.seasonNumber && p.episodeNumber + ? ` S${p.seasonNumber + .toString() + .padStart(2, '0')}E${p.episodeNumber + .toString() + .padStart(2, '0')}` + : ''; + return p.episodeTitle + ? `${p.title}${epPart} - ${p.episodeTitle}` + : p.title; + } case 'track': { return join( reject( diff --git a/web/src/pages/channels/TimeSlotEditorPage.tsx b/web/src/pages/channels/TimeSlotEditorPage.tsx index 277821fce..02abca543 100644 --- a/web/src/pages/channels/TimeSlotEditorPage.tsx +++ b/web/src/pages/channels/TimeSlotEditorPage.tsx @@ -137,6 +137,17 @@ const defaultTimeSlotSchedule: TimeSlotSchedule = { timeZoneOffset: new Date().getTimezoneOffset(), }; +const showOrderOptions = [ + { + value: 'next', + description: 'Next Episode', + }, + { + value: 'shuffle', + description: 'Shuffle', + }, +]; + const lineupItemAppearsInSchedule = ( slots: TimeSlot[], item: ChannelProgram, @@ -314,8 +325,15 @@ const TimeSlotRow = ({ break; } } - const showInputSize = currentPeriod === 'week' ? 7 : 9; + + const isShowType = slot.programming.type === 'show'; + let showInputSize = currentPeriod === 'week' ? 7 : 9; + if (isShowType) { + showInputSize -= 3; + } + const dayOfTheWeek = Math.floor(slot.startTime / OneDayMillis); + return ( {currentPeriod === 'week' ? ( @@ -358,6 +376,26 @@ const TimeSlotRow = ({ + {isShowType && ( + + + Order + ( + + )} + /> + + + )} removeSlot(index)} color="error">