diff --git a/src/tv2-common/cues/ekstern.ts b/src/tv2-common/cues/ekstern.ts index 46cd4644..8ca1f383 100644 --- a/src/tv2-common/cues/ekstern.ts +++ b/src/tv2-common/cues/ekstern.ts @@ -1,14 +1,9 @@ -import { - IBlueprintPart, - PieceLifespan, - RemoteContent, - TimelineObjectCoreExt, - WithTimeline -} from 'blueprints-integration' +import { PieceLifespan, RemoteContent, TimelineObjectCoreExt, WithTimeline } from 'blueprints-integration' import { CueDefinitionEkstern, EvaluateCueResult, literal, + Part, PartDefinition, ShowStyleContext, TransitionStyle, @@ -33,7 +28,7 @@ export function EvaluateEksternBase< ShowStyleConfig extends TV2BlueprintConfigBase >( context: ShowStyleContext, - part: IBlueprintPart, + part: Part, partId: string, parsedCue: CueDefinitionEkstern, partDefinition: PartDefinition, @@ -46,6 +41,7 @@ export function EvaluateEksternBase< if (parsedCue.sourceDefinition.sourceType !== SourceType.REMOTE || sourceInfoEkstern === undefined) { context.core.notifyUserWarning(`EKSTERN source is not valid: "${parsedCue.sourceDefinition.raw}"`) part.invalid = true + part.invalidity = { reason: `No configuration found for the remote source '${parsedCue.sourceDefinition.raw}"'.` } return result } const switcherInput = sourceInfoEkstern.port diff --git a/src/tv2-common/getSegment.ts b/src/tv2-common/getSegment.ts index c5bd5af3..6e599762 100644 --- a/src/tv2-common/getSegment.ts +++ b/src/tv2-common/getSegment.ts @@ -25,6 +25,7 @@ import { TimeFromINewsField } from './inewsConversion' import { CreatePartInvalid, ServerPartProps } from './parts' +import { Invalidity } from './types/invalidity' export interface GetSegmentShowstyleOptions { CreatePartContinuity: ( @@ -85,14 +86,10 @@ export interface GetSegmentShowstyleOptions extends IBlueprintSegment { - invalidity?: SegmentInvalidity + invalidity?: Invalidity definesShowStyleVariant?: boolean } -interface SegmentInvalidity { - reason: string -} - interface SegmentMetadata { miniShelfVideoClipFile?: string } @@ -179,7 +176,13 @@ export async function getSegmentBase (c) => c.type === CueType.UNPAIRED_TARGET && IsTargetingFull(c.target) ) as CueDefinitionUnpairedTarget[] if (unpairedTargets.length) { - blueprintParts.push(CreatePartInvalid(part)) + blueprintParts.push( + CreatePartInvalid(part, { + reason: `The part has one or more unpaired targets: ${unpairedTargets + .map((cue) => cue.iNewsCommand) + .join(', ')}` + }) + ) unpairedTargets.forEach((cue) => { context.core.notifyUserWarning(`No graphic found after ${cue.iNewsCommand} cue`) }) @@ -408,10 +411,7 @@ export async function getSegmentBase } } -function getSegmentInvalidity( - segment: Segment, - parts: BlueprintResultPart[] -): SegmentInvalidity | undefined { +function getSegmentInvalidity(segment: Segment, parts: BlueprintResultPart[]): Invalidity | undefined { const doesSegmentHaveMiniShelf: boolean = !!segment.metaData?.miniShelfVideoClipFile const doesSegmentHaveValidParts: boolean = parts.length > 0 && parts.some((part) => part.pieces.length > 0) if (doesSegmentHaveMiniShelf && doesSegmentHaveValidParts) { diff --git a/src/tv2-common/parts/index.ts b/src/tv2-common/parts/index.ts index d4d4c577..e262a917 100644 --- a/src/tv2-common/parts/index.ts +++ b/src/tv2-common/parts/index.ts @@ -2,3 +2,4 @@ export * from './server' export * from './invalid' export * from './kam' export * from './effekt' +export { Part } from '../types/part' diff --git a/src/tv2-common/parts/invalid.ts b/src/tv2-common/parts/invalid.ts index 4b650bd3..653bfa22 100644 --- a/src/tv2-common/parts/invalid.ts +++ b/src/tv2-common/parts/invalid.ts @@ -1,12 +1,19 @@ -import { BlueprintResultPart, IBlueprintPart } from 'blueprints-integration' +import { BlueprintResultPart } from 'blueprints-integration' import { PartDefinition } from 'tv2-common' +import { Invalidity } from '../types/invalidity' +import { Part } from '../types/part' -export function CreatePartInvalid(ingestPart: PartDefinition, externalIdSuffix?: string): BlueprintResultPart { - const part: IBlueprintPart = { +export function CreatePartInvalid( + ingestPart: PartDefinition, + invalidity: Invalidity, + externalIdSuffix?: string +): BlueprintResultPart { + const part: Part = { externalId: ingestPart.externalId + (externalIdSuffix ? `_${externalIdSuffix}` : ''), title: ingestPart.rawType || 'Unknown', metaData: {}, - invalid: true + invalid: true, + invalidity } return { diff --git a/src/tv2-common/parts/server.ts b/src/tv2-common/parts/server.ts index 7373c4b7..8ddc8fe6 100644 --- a/src/tv2-common/parts/server.ts +++ b/src/tv2-common/parts/server.ts @@ -68,7 +68,14 @@ export async function CreatePartServerBase< ): Promise<{ part: BlueprintResultPart; file: string; duration: number; invalid?: true }> { if (isVideoIdMissing(partDefinition)) { context.core.notifyUserWarning('Video ID not set!') - return { part: CreatePartInvalid(partDefinition), file: '', duration: 0, invalid: true } + return { + part: CreatePartInvalid(partDefinition, { + reason: `The part is missing a video id.` + }), + file: '', + duration: 0, + invalid: true + } } const file = getVideoId(partDefinition) diff --git a/src/tv2-common/types/invalidity.ts b/src/tv2-common/types/invalidity.ts new file mode 100644 index 00000000..87a9170d --- /dev/null +++ b/src/tv2-common/types/invalidity.ts @@ -0,0 +1,3 @@ +export interface Invalidity { + reason: string +} diff --git a/src/tv2-common/types/part.ts b/src/tv2-common/types/part.ts new file mode 100644 index 00000000..2710b9b1 --- /dev/null +++ b/src/tv2-common/types/part.ts @@ -0,0 +1,6 @@ +import { IBlueprintPart } from 'blueprints-integration' +import { Invalidity } from './invalidity' + +export interface Part extends IBlueprintPart { + invalidity?: Invalidity +} diff --git a/src/tv2_afvd_showstyle/parts/evs.ts b/src/tv2_afvd_showstyle/parts/evs.ts index 37e19d46..82f333c8 100644 --- a/src/tv2_afvd_showstyle/parts/evs.ts +++ b/src/tv2_afvd_showstyle/parts/evs.ts @@ -3,7 +3,6 @@ import { HackPartMediaObjectSubscription, IBlueprintActionManifest, IBlueprintAdLibPiece, - IBlueprintPart, IBlueprintPiece, PieceLifespan, TimelineObjectCoreExt @@ -14,6 +13,7 @@ import { findSourceInfo, GetSisyfosTimelineObjForReplay, literal, + Part, PartDefinitionEVS, PartTime, PieceMetaData, @@ -39,7 +39,7 @@ export async function CreatePartEVS( const partTime = PartTime(context.config, partDefinition, totalWords, false) const title = partDefinition.sourceDefinition.name - let part: IBlueprintPart = { + let part: Part = { externalId: partDefinition.externalId, title, metaData: {}, @@ -55,7 +55,9 @@ export async function CreatePartEVS( const sourceInfoReplay = findSourceInfo(context.config.sources, partDefinition.sourceDefinition) if (sourceInfoReplay === undefined) { - return CreatePartInvalid(partDefinition) + return CreatePartInvalid(partDefinition, { + reason: `No configuration found for the replay source '${partDefinition.sourceDefinition.name}'.` + }) } const switcherInput = sourceInfoReplay.port @@ -94,6 +96,7 @@ export async function CreatePartEVS( if (pieces.length === 0) { part.invalid = true + part.invalidity = { reason: 'The part has no pieces.' } } return { diff --git a/src/tv2_afvd_showstyle/parts/grafik.ts b/src/tv2_afvd_showstyle/parts/grafik.ts index f2de5f92..7155052b 100644 --- a/src/tv2_afvd_showstyle/parts/grafik.ts +++ b/src/tv2_afvd_showstyle/parts/grafik.ts @@ -3,13 +3,13 @@ import { HackPartMediaObjectSubscription, IBlueprintActionManifest, IBlueprintAdLibPiece, - IBlueprintPart, IBlueprintPiece } from 'blueprints-integration' import { AddScript, applyFullGraphicPropertiesToPart, GraphicIsPilot, + Part, PartDefinition, PartTime, ShowStyleContext @@ -25,7 +25,7 @@ export async function CreatePartGrafik( totalWords: number ): Promise { const partTime = PartTime(context.config, partDefinition, totalWords, false) - const part: IBlueprintPart = { + const part: Part = { externalId: partDefinition.externalId, title: partDefinition.type + ' - ' + partDefinition.rawType, metaData: {} @@ -61,6 +61,7 @@ export async function CreatePartGrafik( if (pieces.length === 0) { part.invalid = true + part.invalidity = { reason: 'The part has no pieces.' } } return { diff --git a/src/tv2_afvd_showstyle/parts/intro.ts b/src/tv2_afvd_showstyle/parts/intro.ts index 3bcf33c1..3db7bd13 100644 --- a/src/tv2_afvd_showstyle/parts/intro.ts +++ b/src/tv2_afvd_showstyle/parts/intro.ts @@ -3,7 +3,6 @@ import { HackPartMediaObjectSubscription, IBlueprintActionManifest, IBlueprintAdLibPiece, - IBlueprintPart, IBlueprintPiece } from 'blueprints-integration' import { @@ -11,6 +10,7 @@ import { CreatePartInvalid, CueDefinitionJingle, GetJinglePartProperties, + Part, PartDefinition, PartTime, ShowStyleContext @@ -28,13 +28,12 @@ export async function CreatePartIntro( const partTime = PartTime(context.config, partDefinition, totalWords, false) const jingleCue = partDefinition.cues.find((cue) => { - const parsedCue = cue - return parsedCue.type === CueType.Jingle + return cue.type === CueType.Jingle }) if (!jingleCue) { context.core.notifyUserWarning(`Intro must contain a jingle`) - return CreatePartInvalid(partDefinition) + return CreatePartInvalid(partDefinition, { reason: 'Intro parts must contain a jingle.' }) } const parsedJingle = jingleCue as CueDefinitionJingle @@ -43,18 +42,20 @@ export async function CreatePartIntro( jngl.BreakerName ? jngl.BreakerName.toString().toUpperCase() === parsedJingle.clip.toString().toUpperCase() : false ) if (!jingle) { - context.core.notifyUserWarning(`Jingle ${parsedJingle.clip} is not configured`) - return CreatePartInvalid(partDefinition) + context.core.notifyUserWarning(`Jingle ${parsedJingle.clip} is not configured.`) + return CreatePartInvalid(partDefinition, { + reason: `No configuration found for the jingle '${parsedJingle.clip}'.` + }) } const overlapFrames = jingle.EndAlpha if (overlapFrames === undefined) { context.core.notifyUserWarning(`Jingle ${parsedJingle.clip} does not have an out-duration set.`) - return CreatePartInvalid(partDefinition) + return CreatePartInvalid(partDefinition, { reason: `No out-duration set for the jingle '${parsedJingle.clip}'.` }) } - let part: IBlueprintPart = { + let part: Part = { externalId: partDefinition.externalId, title: partDefinition.type + ' - ' + partDefinition.rawType, metaData: {} @@ -89,6 +90,7 @@ export async function CreatePartIntro( if (pieces.length === 0) { part.invalid = true + part.invalidity = { reason: 'The part has no pieces.' } } return { diff --git a/src/tv2_afvd_showstyle/parts/kam.ts b/src/tv2_afvd_showstyle/parts/kam.ts index f790c12f..6962fe04 100644 --- a/src/tv2_afvd_showstyle/parts/kam.ts +++ b/src/tv2_afvd_showstyle/parts/kam.ts @@ -16,6 +16,7 @@ import { findSourceInfo, GetSisyfosTimelineObjForCamera, literal, + Part, PartDefinitionKam, PieceMetaData, SegmentContext, @@ -37,7 +38,7 @@ export async function CreatePartKam( ): Promise { const partKamBase = CreatePartKamBase(context, partDefinition, totalWords) - let part = partKamBase.part.part + let part: Part = partKamBase.part.part const partTime = partKamBase.duration const adLibPieces: IBlueprintAdLibPiece[] = [] @@ -80,7 +81,9 @@ export async function CreatePartKam( const sourceInfoCam = findSourceInfo(context.config.sources, partDefinition.sourceDefinition) if (sourceInfoCam === undefined) { context.core.notifyUserWarning(`${partDefinition.rawType} does not exist in this studio`) - return CreatePartInvalid(partDefinition) + return CreatePartInvalid(partDefinition, { + reason: `No configuration found for the camera source '${partDefinition.rawType}'.` + }) } const switcherInput = sourceInfoCam.port @@ -136,6 +139,7 @@ export async function CreatePartKam( if (pieces.length === 0) { part.invalid = true + part.invalidity = { reason: 'The part has no pieces.' } } return { diff --git a/src/tv2_afvd_showstyle/parts/live.ts b/src/tv2_afvd_showstyle/parts/live.ts index 86d1d29e..19914fb9 100644 --- a/src/tv2_afvd_showstyle/parts/live.ts +++ b/src/tv2_afvd_showstyle/parts/live.ts @@ -3,10 +3,9 @@ import { HackPartMediaObjectSubscription, IBlueprintActionManifest, IBlueprintAdLibPiece, - IBlueprintPart, IBlueprintPiece } from 'blueprints-integration' -import { AddScript, CueDefinitionEkstern, PartDefinition, PartTime, SegmentContext } from 'tv2-common' +import { AddScript, CueDefinition, Part, PartDefinition, PartTime, SegmentContext } from 'tv2-common' import { CueType } from 'tv2-constants' import { GalleryBlueprintConfig } from '../../tv2_afvd_showstyle/helpers/config' import { EvaluateCues } from '../helpers/pieces/evaluateCues' @@ -19,7 +18,7 @@ export async function CreatePartLive( totalWords: number ): Promise { const partTime = PartTime(context.config, partDefinition, totalWords, false) - let part: IBlueprintPart = { + let part: Part = { externalId: partDefinition.externalId, title: partDefinition.title || 'Ekstern', metaData: {}, @@ -48,12 +47,8 @@ export async function CreatePartLive( part.hackListenToMediaObjectUpdates = mediaSubscriptions - const liveCue = partDefinition.cues.find((c) => c.type === CueType.Ekstern) as CueDefinitionEkstern - const livePiece = pieces.find((p) => p.sourceLayerId === SourceLayer.PgmLive) - - if (pieces.length === 0 || !liveCue || !livePiece) { - part.invalid = true - } + part.invalidity = getInvalidityReasonForLivePart(partDefinition, pieces) + part.invalid = part.invalidity !== undefined return { part, @@ -62,3 +57,24 @@ export async function CreatePartLive( actions } } + +function getInvalidityReasonForLivePart( + partDefinition: PartDefinition, + pieces: IBlueprintPiece[] +): Part['invalidity'] | undefined { + if (pieces.length === 0) { + return { reason: 'The part has no pieces.' } + } + + const liveCue: CueDefinition | undefined = partDefinition.cues.find((c) => c.type === CueType.Ekstern) + if (!liveCue) { + return { reason: 'The part has no cues with a remote source.' } + } + + const livePiece = pieces.find((p) => p.sourceLayerId === SourceLayer.PgmLive) + if (!livePiece) { + return { reason: 'The part has no pieces with a remote source.' } + } + + return undefined +} diff --git a/src/tv2_afvd_showstyle/parts/server.ts b/src/tv2_afvd_showstyle/parts/server.ts index 57cd4388..914d69a9 100644 --- a/src/tv2_afvd_showstyle/parts/server.ts +++ b/src/tv2_afvd_showstyle/parts/server.ts @@ -1,5 +1,5 @@ import { BlueprintResultPart, HackPartMediaObjectSubscription, IBlueprintActionManifest } from 'blueprints-integration' -import { AddScript, CreatePartServerBase, PartDefinition, SegmentContext, ServerPartProps } from 'tv2-common' +import { AddScript, CreatePartServerBase, Part, PartDefinition, SegmentContext, ServerPartProps } from 'tv2-common' import { CasparLLayer, SisyfosLLAyer } from '../../tv2_afvd_studio/layers' import { GalleryBlueprintConfig } from '../helpers/config' import { EvaluateCues } from '../helpers/pieces/evaluateCues' @@ -28,7 +28,7 @@ export async function CreatePartServer( return basePartProps.part } - let part = basePartProps.part.part + let part: Part = basePartProps.part.part const pieces = basePartProps.part.pieces const adLibPieces = basePartProps.part.adLibPieces const duration = basePartProps.duration @@ -57,6 +57,7 @@ export async function CreatePartServer( if (pieces.length === 0) { part.invalid = true + part.invalidity = { reason: 'The part has no pieces.' } } return { diff --git a/src/tv2_afvd_showstyle/parts/teknik.ts b/src/tv2_afvd_showstyle/parts/teknik.ts index 0263bac8..ad5d8039 100644 --- a/src/tv2_afvd_showstyle/parts/teknik.ts +++ b/src/tv2_afvd_showstyle/parts/teknik.ts @@ -3,10 +3,9 @@ import { HackPartMediaObjectSubscription, IBlueprintActionManifest, IBlueprintAdLibPiece, - IBlueprintPart, IBlueprintPiece } from 'blueprints-integration' -import { AddScript, PartDefinition, PartTime, SegmentContext } from 'tv2-common' +import { AddScript, Part, PartDefinition, PartTime, SegmentContext } from 'tv2-common' import { GalleryBlueprintConfig } from '../helpers/config' import { EvaluateCues } from '../helpers/pieces/evaluateCues' import { SourceLayer } from '../layers' @@ -17,7 +16,7 @@ export async function CreatePartTeknik( totalWords: number ): Promise { const partTime = PartTime(context.config, partDefinition, totalWords, false) - const part: IBlueprintPart = { + const part: Part = { externalId: partDefinition.externalId, title: partDefinition.type + ' - ' + partDefinition.rawType, metaData: {} @@ -45,6 +44,7 @@ export async function CreatePartTeknik( if (pieces.length === 0) { part.invalid = true + part.invalidity = { reason: 'The part has no pieces.' } } return { diff --git a/src/tv2_afvd_showstyle/parts/unknown.ts b/src/tv2_afvd_showstyle/parts/unknown.ts index d8aff93d..c66cf2ae 100644 --- a/src/tv2_afvd_showstyle/parts/unknown.ts +++ b/src/tv2_afvd_showstyle/parts/unknown.ts @@ -2,7 +2,6 @@ import { HackPartMediaObjectSubscription, IBlueprintActionManifest, IBlueprintAdLibPiece, - IBlueprintPart, IBlueprintPiece } from 'blueprints-integration' import { @@ -10,6 +9,7 @@ import { applyFullGraphicPropertiesToPart, GetJinglePartProperties, GraphicIsPilot, + Part, PartDefinition, PartTime, ShowStyleContext @@ -28,7 +28,7 @@ export async function CreatePartUnknown( ) { const partTime = PartTime(context.config, partDefinition, totalWords, false) - let part: IBlueprintPart = { + let part: Part = { externalId: partDefinition.externalId, title: partDefinition.type + ' - ' + partDefinition.rawType, metaData: {}, @@ -70,6 +70,7 @@ export async function CreatePartUnknown( if (pieces.length === 0) { part.invalid = true + part.invalidity = { reason: 'The part has no pieces.' } } part.hackListenToMediaObjectUpdates = mediaSubscriptions diff --git a/src/tv2_offtube_showstyle/parts/OfftubeKam.ts b/src/tv2_offtube_showstyle/parts/OfftubeKam.ts index 2dc07380..0fd21d2e 100644 --- a/src/tv2_offtube_showstyle/parts/OfftubeKam.ts +++ b/src/tv2_offtube_showstyle/parts/OfftubeKam.ts @@ -77,7 +77,9 @@ export async function OfftubeCreatePartKam( } else { const sourceInfoCam = findSourceInfo(context.config.sources, partDefinition.sourceDefinition) if (sourceInfoCam === undefined) { - return CreatePartInvalid(partDefinition) + return CreatePartInvalid(partDefinition, { + reason: `No configuration found for the camera source '${partDefinition.rawType}'.` + }) } const switcherInput = sourceInfoCam.port