diff --git a/meteor/server/publications/lib/quickLoop.ts b/meteor/server/publications/lib/quickLoop.ts index 73a3b5f1dd..272a554ac9 100644 --- a/meteor/server/publications/lib/quickLoop.ts +++ b/meteor/server/publications/lib/quickLoop.ts @@ -9,7 +9,7 @@ import { MarkerPosition, compareMarkerPositions } from '@sofie-automation/coreli import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { DEFAULT_FALLBACK_PART_DURATION } from '@sofie-automation/shared-lib/dist/core/constants' import { getCurrentTime } from '../../lib/lib' -import { generateTranslation } from '@sofie-automation/meteor-lib/dist/lib' +import { generateTranslation } from '@sofie-automation/corelib/dist/lib' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' diff --git a/packages/corelib/src/dataModel/Notes.ts b/packages/corelib/src/dataModel/Notes.ts index 280a421a19..7d097323b2 100644 --- a/packages/corelib/src/dataModel/Notes.ts +++ b/packages/corelib/src/dataModel/Notes.ts @@ -24,6 +24,11 @@ export interface GenericNote extends INoteBase { name: string } } +export interface RundownPlaylistNote extends INoteBase { + origin: { + name: string + } +} export interface RundownNote extends INoteBase { origin: { name: string diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index a2ba8cccb1..241e0c3895 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -10,6 +10,7 @@ import { StudioId, RundownId, } from './Ids' +import { RundownPlaylistNote } from './Notes' /** Details of an ab-session requested by the blueprints in onTimelineGenerate */ export interface ABSessionInfo { @@ -152,6 +153,9 @@ export interface DBRundownPlaylist { */ queuedSegmentId?: SegmentId + /** Holds notes (warnings / errors) thrown by the blueprints during creation */ + notes?: Array + quickLoop?: QuickLoopProps /** Actual time of playback starting */ diff --git a/packages/corelib/src/lib.ts b/packages/corelib/src/lib.ts index c32833abe0..399db4fead 100644 --- a/packages/corelib/src/lib.ts +++ b/packages/corelib/src/lib.ts @@ -7,6 +7,7 @@ import { Timecode } from 'timecode' import { iterateDeeply, iterateDeeplyEnum, Time } from '@sofie-automation/blueprints-integration' import { IStudioSettings } from './dataModel/Studio' import { customAlphabet as createNanoid } from 'nanoid' +import type { ITranslatableMessage } from './TranslatableMessage' /** * Limited character set to use for id generation @@ -455,3 +456,16 @@ export function stringifyObjects(objs: unknown): string { return objs + '' } } + +/** Generate the translation for a string, to be applied later when it gets rendered */ +export function generateTranslation( + key: string, + args?: { [k: string]: any }, + namespaces?: string[] +): ITranslatableMessage { + return { + key, + args, + namespaces, + } +} diff --git a/packages/job-worker/src/blueprints/context/StudioUserContext.ts b/packages/job-worker/src/blueprints/context/StudioUserContext.ts index 1ed5a483cb..be2c471dc4 100644 --- a/packages/job-worker/src/blueprints/context/StudioUserContext.ts +++ b/packages/job-worker/src/blueprints/context/StudioUserContext.ts @@ -3,20 +3,18 @@ import { ReadonlyDeep } from 'type-fest' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ProcessedStudioConfig } from '../config' import { INoteBase } from '@sofie-automation/corelib/dist/dataModel/Notes' -import { UserContextInfo } from './CommonContext' +import { ContextInfo } from './CommonContext' import { StudioContext } from './StudioContext' export class StudioUserContext extends StudioContext implements IStudioUserContext { public readonly notes: INoteBase[] = [] - private readonly tempSendNotesIntoBlackHole: boolean constructor( - contextInfo: UserContextInfo, + contextInfo: ContextInfo, studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig ) { super(contextInfo, studio, studioBlueprintConfig) - this.tempSendNotesIntoBlackHole = contextInfo.tempSendUserNotesIntoBlackHole ?? false } notifyUserError(message: string, params?: { [key: string]: any }): void { @@ -30,16 +28,12 @@ export class StudioUserContext extends StudioContext implements IStudioUserConte this.addNote(NoteSeverity.INFO, message, params) } private addNote(type: NoteSeverity, message: string, params?: { [key: string]: any }) { - if (this.tempSendNotesIntoBlackHole) { - this.logNote(`UserNotes: "${message}", ${JSON.stringify(params)}`, type) - } else { - this.notes.push({ - type: type, - message: { - key: message, - args: params, - }, - }) - } + this.notes.push({ + type: type, + message: { + key: message, + args: params, + }, + }) } } diff --git a/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts b/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts index 9fd132c416..003379d9aa 100644 --- a/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts +++ b/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts @@ -22,7 +22,6 @@ describe('selectShowStyleVariant', () => { { name: 'test', identifier: 'test', - tempSendUserNotesIntoBlackHole: true, }, context.studio, context.getStudioBlueprintConfig() diff --git a/packages/job-worker/src/ingest/generationRundown.ts b/packages/job-worker/src/ingest/generationRundown.ts index f41ed7db49..daf23cfd53 100644 --- a/packages/job-worker/src/ingest/generationRundown.ts +++ b/packages/job-worker/src/ingest/generationRundown.ts @@ -58,12 +58,11 @@ export async function updateRundownFromIngestData( { name: 'selectShowStyleVariant', identifier: `studioId=${context.studio._id},rundownId=${ingestModel.rundownId},ingestRundownId=${ingestModel.rundownExternalId}`, - tempSendUserNotesIntoBlackHole: true, }, context.studio, context.getStudioBlueprintConfig() ) - // TODO-CONTEXT save any user notes from selectShowStyleContext + const showStyle = await selectShowStyleVariant( context, selectShowStyleContext, @@ -80,6 +79,14 @@ export async function updateRundownFromIngestData( const showStyleBlueprint = await context.getShowStyleBlueprint(showStyle.base._id) const allRundownWatchedPackages = await pAllRundownWatchedPackages + const extraRundownNotes: RundownNote[] = selectShowStyleContext.notes.map((note) => ({ + type: note.type, + message: wrapTranslatableMessageFromBlueprints(note.message, [showStyleBlueprint.blueprintId]), + origin: { + name: 'selectShowStyleVariant', + }, + })) + // Call blueprints, get rundown const dbRundown = await regenerateRundownAndBaselineFromIngestData( context, @@ -88,7 +95,8 @@ export async function updateRundownFromIngestData( rundownSource, showStyle, showStyleBlueprint, - allRundownWatchedPackages + allRundownWatchedPackages, + extraRundownNotes ) if (!dbRundown) { // We got no rundown, abort: @@ -147,13 +155,11 @@ export async function updateRundownMetadataFromIngestData( { name: 'selectShowStyleVariant', identifier: `studioId=${context.studio._id},rundownId=${ingestModel.rundownId},ingestRundownId=${ingestModel.rundownExternalId}`, - tempSendUserNotesIntoBlackHole: true, }, context.studio, context.getStudioBlueprintConfig() ) - // TODO-CONTEXT save any user notes from selectShowStyleContext const showStyle = await selectShowStyleVariant( context, selectShowStyleContext, @@ -170,6 +176,14 @@ export async function updateRundownMetadataFromIngestData( const showStyleBlueprint = await context.getShowStyleBlueprint(showStyle.base._id) const allRundownWatchedPackages = await pAllRundownWatchedPackages + const extraRundownNotes: RundownNote[] = selectShowStyleContext.notes.map((note) => ({ + type: note.type, + message: wrapTranslatableMessageFromBlueprints(note.message, [showStyleBlueprint.blueprintId]), + origin: { + name: 'selectShowStyleVariant', + }, + })) + // Call blueprints, get rundown const dbRundown = await regenerateRundownAndBaselineFromIngestData( context, @@ -178,7 +192,8 @@ export async function updateRundownMetadataFromIngestData( rundownSource, showStyle, showStyleBlueprint, - allRundownWatchedPackages + allRundownWatchedPackages, + extraRundownNotes ) if (!dbRundown) { // We got no rundown, abort: @@ -225,6 +240,7 @@ export async function updateRundownMetadataFromIngestData( * @param showStyle ShowStyle to regenerate for * @param showStyleBlueprint ShowStyle Blueprint to regenerate with * @param allRundownWatchedPackages WatchedPackagesHelper for all packages belonging to the rundown + * @param extraRundownNotes Additional notes to add to the Rundown, produced earlier in the ingest process * @returns Generated documents or null if Blueprints reject the Rundown */ export async function regenerateRundownAndBaselineFromIngestData( @@ -234,7 +250,8 @@ export async function regenerateRundownAndBaselineFromIngestData( rundownSource: RundownSource, showStyle: SelectedShowStyleVariant, showStyleBlueprint: ReadonlyDeep, - allRundownWatchedPackages: WatchedPackagesHelper + allRundownWatchedPackages: WatchedPackagesHelper, + extraRundownNotes: RundownNote[] ): Promise | null> { const rundownBaselinePackages = allRundownWatchedPackages.filter( context, @@ -297,15 +314,17 @@ export async function regenerateRundownAndBaselineFromIngestData( } // Ensure the ids in the notes are clean - const rundownNotes = blueprintContext.notes.map((note) => - literal({ - type: note.type, - message: wrapTranslatableMessageFromBlueprints(note.message, translationNamespaces), - origin: { - name: `${showStyle.base.name}-${showStyle.variant.name}`, - }, - }) - ) + const rundownNotes = blueprintContext.notes + .map((note) => + literal({ + type: note.type, + message: wrapTranslatableMessageFromBlueprints(note.message, translationNamespaces), + origin: { + name: `${showStyle.base.name}-${showStyle.variant.name}`, + }, + }) + ) + .concat(extraRundownNotes) ingestModel.setRundownData( rundownRes.rundown, diff --git a/packages/job-worker/src/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap b/packages/job-worker/src/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap index d949eb684e..651d6405e1 100644 --- a/packages/job-worker/src/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap +++ b/packages/job-worker/src/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap @@ -9,6 +9,7 @@ exports[`Test recieved mos ingest payloads mosRoCreate 1`] = ` "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -306,6 +307,7 @@ exports[`Test recieved mos ingest payloads mosRoCreate: replace existing 1`] = ` "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -594,6 +596,7 @@ exports[`Test recieved mos ingest payloads mosRoFullStory: Valid data 1`] = ` "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -904,6 +907,7 @@ exports[`Test recieved mos ingest payloads mosRoReadyToAir: Update ro 1`] = ` "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -1204,6 +1208,7 @@ exports[`Test recieved mos ingest payloads mosRoStatus: Update ro 1`] = ` "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -1502,6 +1507,7 @@ exports[`Test recieved mos ingest payloads mosRoStoryDelete: Remove segment 1`] "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -1766,6 +1772,7 @@ exports[`Test recieved mos ingest payloads mosRoStoryInsert: Into segment 1`] = "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -2077,6 +2084,7 @@ exports[`Test recieved mos ingest payloads mosRoStoryInsert: New segment 1`] = ` "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -2397,6 +2405,7 @@ exports[`Test recieved mos ingest payloads mosRoStoryMove: Move whole segment to "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -2699,6 +2708,7 @@ exports[`Test recieved mos ingest payloads mosRoStoryMove: Within segment 1`] = "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -3001,6 +3011,7 @@ exports[`Test recieved mos ingest payloads mosRoStoryReplace: Same segment 1`] = "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -3302,6 +3313,7 @@ exports[`Test recieved mos ingest payloads mosRoStorySwap: Swap across segments "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -3595,6 +3607,7 @@ exports[`Test recieved mos ingest payloads mosRoStorySwap: Swap across segments2 "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -3924,6 +3937,7 @@ exports[`Test recieved mos ingest payloads mosRoStorySwap: With first in same se "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ @@ -4226,6 +4240,7 @@ exports[`Test recieved mos ingest payloads mosRoStorySwap: Within same segment 1 "modified": 0, "name": "All effect1 into clip combinations", "nextPartInfo": null, + "notes": [], "organizationId": null, "previousPartInfo": null, "rundownIdsInOrder": [ diff --git a/packages/job-worker/src/rundownPlaylists.ts b/packages/job-worker/src/rundownPlaylists.ts index dce9f538dd..a8b96d37d0 100644 --- a/packages/job-worker/src/rundownPlaylists.ts +++ b/packages/job-worker/src/rundownPlaylists.ts @@ -5,7 +5,13 @@ import { ForceQuickLoopAutoNext, QuickLoopMarkerType, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { clone, getHash, getRandomString, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' +import { + clone, + getHash, + getRandomString, + normalizeArrayToMap, + generateTranslation, +} from '@sofie-automation/corelib/dist/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' @@ -16,7 +22,11 @@ import { RemovePlaylistProps, } from '@sofie-automation/corelib/dist/worker/studio' import { ReadonlyDeep } from 'type-fest' -import { BlueprintResultRundownPlaylist, IBlueprintRundown } from '@sofie-automation/blueprints-integration' +import { + BlueprintResultRundownPlaylist, + IBlueprintRundown, + NoteSeverity, +} from '@sofie-automation/blueprints-integration' import { JobContext } from './jobs' import { logger } from './logging' import { resetRundownPlaylist } from './playout/lib' @@ -38,6 +48,7 @@ import { RundownLock } from './jobs/lock' import { runWithRundownLock } from './ingest/lock' import { convertRundownToBlueprints } from './blueprints/context/lib' import { sortRundownIDsInPlaylist } from '@sofie-automation/corelib/dist/playout/playlist' +import { INoteBase } from '@sofie-automation/corelib/dist/dataModel/Notes' /** * Debug: Remove a Playlist and all its contents @@ -171,27 +182,38 @@ export function produceRundownPlaylistInfoFromRundown( rundowns: ReadonlyDeep> ): DBRundownPlaylist { let playlistInfo: BlueprintResultRundownPlaylist | null = null + + let notes: INoteBase[] = [] + try { if (studioBlueprint?.blueprint?.getRundownPlaylistInfo) { + const blueprintContext = new StudioUserContext( + { + name: 'produceRundownPlaylistInfoFromRundown', + identifier: `studioId=${context.studioId},playlistId=${playlistId},rundownIds=${rundowns + .map((r) => r._id) + .join(',')}`, + }, + context.studio, + context.getStudioBlueprintConfig() + ) + playlistInfo = studioBlueprint.blueprint.getRundownPlaylistInfo( - new StudioUserContext( - { - name: 'produceRundownPlaylistInfoFromRundown', - identifier: `studioId=${context.studioId},playlistId=${playlistId},rundownIds=${rundowns - .map((r) => r._id) - .join(',')}`, - tempSendUserNotesIntoBlackHole: true, - }, - context.studio, - context.getStudioBlueprintConfig() - ), + blueprintContext, rundowns.map(convertRundownToBlueprints), playlistExternalId ) + + notes = blueprintContext.notes } } catch (err) { logger.error(`Error in studioBlueprint.getRundownPlaylistInfo: ${stringifyError(err)}`) playlistInfo = null + + notes.push({ + type: NoteSeverity.ERROR, + message: generateTranslation(`Internal Error generating RundownPlaylist`), + }) } const rundownsInDefaultOrder = sortDefaultRundownInPlaylistOrder(rundowns) @@ -240,6 +262,14 @@ export function produceRundownPlaylistInfoFromRundown( } } + // Update the notes on the playlist + newPlaylist.notes = notes.map((note) => ({ + ...note, + origin: { + name: 'produceRundownPlaylistInfoFromRundown', + }, + })) + if (!newPlaylist.rundownRanksAreSetInSofie) { if (playlistInfo?.order) { // The blueprints gave us an order diff --git a/packages/meteor-lib/src/lib.ts b/packages/meteor-lib/src/lib.ts index c2af24b1f4..bbe5a9daa6 100644 --- a/packages/meteor-lib/src/lib.ts +++ b/packages/meteor-lib/src/lib.ts @@ -1,5 +1,3 @@ -import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' - export enum LogLevel { SILLY = 'silly', DEBUG = 'debug', @@ -9,16 +7,3 @@ export enum LogLevel { ERROR = 'error', NONE = 'crit', } - -/** Generate the translation for a string, to be applied later when it gets rendered */ -export function generateTranslation( - key: string, - args?: { [k: string]: any }, - namespaces?: string[] -): ITranslatableMessage { - return { - key, - args, - namespaces, - } -} diff --git a/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts b/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts index e63e456f05..eb2b9e47cb 100644 --- a/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts +++ b/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts @@ -23,8 +23,7 @@ import { ReactivePlaylistActionContext } from './actionFactory' import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { IWrappedAdLibBase } from '@sofie-automation/shared-lib/dist/input-gateway/deviceTriggerPreviews' import { MountedAdLibTriggerType } from '../api/MountedTriggers' -import { assertNever } from '@sofie-automation/corelib/dist/lib' -import { generateTranslation } from '../lib' +import { assertNever, generateTranslation } from '@sofie-automation/corelib/dist/lib' import { FindOptions } from '../collections/lib' import { TriggersContext } from './triggersContext' diff --git a/packages/webui/src/client/lib/tempLib.ts b/packages/webui/src/client/lib/tempLib.ts index b173dabb98..494b3d6ce7 100644 --- a/packages/webui/src/client/lib/tempLib.ts +++ b/packages/webui/src/client/lib/tempLib.ts @@ -28,6 +28,7 @@ export { groupByToMapFunc, formatDurationAsTimecode, formatDateAsTimecode, + generateTranslation, } from '@sofie-automation/corelib/dist/lib' export type { Complete } from '@sofie-automation/corelib/dist/lib' -export { LogLevel, generateTranslation } from '@sofie-automation/meteor-lib/dist/lib' +export { LogLevel } from '@sofie-automation/meteor-lib/dist/lib' diff --git a/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx b/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx index 803c25f742..3438e37c51 100644 --- a/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx @@ -195,6 +195,28 @@ class RundownViewNotifier extends WithManagedTracker { const playlist = RundownPlaylists.findOne(playlistId) const rundowns = rRundowns.get() + if (playlist?.notes) { + const playlistNotesId = playlist._id + '_playlistnotes_' + playlist.notes.forEach((note) => { + const noteId = playlistNotesId + note.origin.name + '_' + note.message + '_' + note.type + const notificationFromNote = new Notification( + noteId, + getNoticeLevelForNoteSeverity(note.type), + note.message, + 'RundownPlaylist', + getCurrentTime(), + true, + [], + -1 + ) + if (!Notification.isEqual(this._rundownStatus[noteId], notificationFromNote)) { + this._rundownStatus[noteId] = notificationFromNote + this._rundownStatusDep.changed() + } + newNoteIds.push(noteId) + }) + } + if (playlist && rundowns) { rundowns.forEach((rundown) => { const unsyncedId = rundown._id + '_unsynced'