From 38fcdc94d675b5fb51536be798912cc2827fe254 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Mon, 23 Mar 2026 14:54:58 +0100 Subject: [PATCH 01/10] chore: Added definition for a specific background and text color for the new class for the segment-event-identifier styling. --- packages/webui/src/client/styles/_colorScheme.scss | 3 +++ packages/webui/src/client/styles/defaultColors.scss | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/packages/webui/src/client/styles/_colorScheme.scss b/packages/webui/src/client/styles/_colorScheme.scss index eea945a755c..5c6e9db5397 100644 --- a/packages/webui/src/client/styles/_colorScheme.scss +++ b/packages/webui/src/client/styles/_colorScheme.scss @@ -31,6 +31,9 @@ $general-timecode-color: var(--general-timecode-color); $part-identifier: var(--part-identifier); $part-identifier-text: var(--part-identifier-text); +$segment-event-identifier: var(--segment-event-identifier); +$segment-event-identifier-text: var(--segment-event-identifier-text); + $ui-button-primary: var(--ui-button-primary); $ui-button-primary--hover: var(--ui-button-primary--hover); $ui-button-primary--translucent: var(--ui-button-primary--translucent); diff --git a/packages/webui/src/client/styles/defaultColors.scss b/packages/webui/src/client/styles/defaultColors.scss index 41be88ec005..fe6792e9493 100644 --- a/packages/webui/src/client/styles/defaultColors.scss +++ b/packages/webui/src/client/styles/defaultColors.scss @@ -31,6 +31,10 @@ --part-identifier: #363636; --part-identifier-text: #eee; + + --segment-event-identifier: #363636; + --segment-event-identifier-text: #eee; + --general-timecode-color: #ffff00; --ui-button-primary: #1769ff; From b50a2b8380461d24f5ba57e3e7300430a2e677fd Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Mon, 23 Mar 2026 16:21:38 +0100 Subject: [PATCH 02/10] chore: Refactored and modernized the font styling of the "parts-identifier" shown in the segment header and on the take lines on each part. --- .../webui/src/client/styles/rundownView.scss | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index 0044011c414..595c5c313b1 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -596,18 +596,38 @@ svg.icon { grid-row: title-identifiers / end; .segment-timeline__part-identifiers__identifier { - font-weight: 300; - text-shadow: none; - margin: 2.5px; - padding: 1px 4px; + margin: 0.1rem; + padding: 0.1rem 0.35rem 0.1rem 0.35rem; background-color: $part-identifier; color: $part-identifier-text; - border-radius: 10px; - font-size: 0.85rem; + border-radius: 99px; + font-family: "Roboto Flex", "Roboto", sans-serif; + font-style: normal; + font-size: 0.9rem; + letter-spacing: 0.03em; + line-height: 100%; + text-shadow: none; + font-variant-numeric: proportional-nums; + font-variation-settings: + 'wdth' 90, + 'wght' 500, + 'opsz' 120, + 'slnt' 0, + 'GRAD' 0, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 514, + 'YTUC' 712; &:hover { - background-color: #8c8c8c; + background-color: #6f6f6f; color: #fff; + font-weight: 700; + box-shadow: 0px 0px 4px 0px #fff; } } } @@ -2501,12 +2521,34 @@ svg.icon { .segment-timeline__identifier { z-index: -1; - padding: 0 4px 0 10px; + padding: 0.1rem 0.35rem 0.1rem 0.6rem; box-sizing: border-box; + border-radius: 0 99px 99px 0; + margin: 0rem; + background-color: $part-identifier; - border-radius: 0 8px 8px 0; color: $part-identifier-text; - font-size: 0.85rem; + font-family: "Roboto Flex", "Roboto", sans-serif; + font-style: normal; + font-size: 0.9rem; + letter-spacing: 0.03em; + line-height: 100%; + text-shadow: none; + font-variant-numeric: proportional-nums; + font-variation-settings: + 'wdth' 90, + 'wght' 500, + 'opsz' 120, + 'slnt' 0, + 'GRAD' 0, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 514, + 'YTUC' 712; } &.gap { From d08616718bec0d89639007ac2f5304c3b1c91b5f Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Tue, 24 Mar 2026 12:16:06 +0100 Subject: [PATCH 03/10] chore: Made the parts Identifiers vertically aligned to the bottom of the segment header. --- .../webui/src/client/styles/rundownView.scss | 17 +++++++++++------ .../ui/SegmentTimeline/SegmentTimeline.scss | 4 ---- .../ui/SegmentTimeline/SegmentTimeline.tsx | 1 + 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index 595c5c313b1..98704693eb7 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -548,9 +548,8 @@ svg.icon { } .segment-timeline__title { - display: grid; - grid-template-columns: auto; - grid-template-rows: [title-title] min-content [title-notifications] 1fr [title-identifiers] min-content [end]; + display: flex; + flex-direction: column; margin: 0; padding: 0; @@ -571,6 +570,12 @@ svg.icon { grid-row: title / timeline-header; } + .segment-timeline__title__content { + display: flex; + flex-direction: column; + flex-grow: 1; + } + h2 { margin: 0; padding: 0.2em; @@ -580,7 +585,6 @@ svg.icon { hyphens: auto; -webkit-hyphens: auto; -ms-hyphens: auto; - grid-row: title-title / title-notifications; } .segment-timeline__title__notes { @@ -589,13 +593,13 @@ svg.icon { line-height: 1.4em; font-size: 0.75em; font-weight: 400; - grid-row: title-notifications / title-identifiers; } .segment-timeline__part-identifiers { - grid-row: title-identifiers / end; + margin-top: auto; .segment-timeline__part-identifiers__identifier { + // The pill-shaped labels that appear in the title area of a segment, showing and linking to the part name or number margin: 0.1rem; padding: 0.1rem 0.35rem 0.1rem 0.35rem; background-color: $part-identifier; @@ -2520,6 +2524,7 @@ svg.icon { } .segment-timeline__identifier { + // The Parts identifier label on the right side of the part, showing the Part name or number z-index: -1; padding: 0.1rem 0.35rem 0.1rem 0.6rem; box-sizing: border-box; diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.scss b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.scss index d49920a8d40..753593d3060 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.scss +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.scss @@ -29,10 +29,6 @@ $timeline-layer-height: 1em; vertical-align: text-bottom; } .segment-timeline__title__user-edit-states { - position: absolute; - bottom: 0; - left: 0; - right: 0; display: flex; flex-flow: row nowrap; } diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index 1f1c54b9d90..b262a1648da 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -1013,6 +1013,7 @@ export class SegmentTimelineClass extends React.Component
{ if (this.props.studio.settings.enableUserEdits) { const segment = this.props.segment From 15474a4d0b351763c7871627f27ce9628b22e905 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Tue, 24 Mar 2026 12:45:01 +0100 Subject: [PATCH 04/10] chore: Created a separate class for the segment-event-identifier, as well as mock data in the form of the string "Seg. Identifier". --- .../src/client/styles/defaultColors.scss | 4 +-- .../webui/src/client/styles/rundownView.scss | 34 ++++++++++++++++++- .../SegmentStoryboard/SegmentStoryboard.tsx | 1 + .../ui/SegmentTimeline/SegmentTimeline.tsx | 1 + 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/webui/src/client/styles/defaultColors.scss b/packages/webui/src/client/styles/defaultColors.scss index fe6792e9493..a483f496d07 100644 --- a/packages/webui/src/client/styles/defaultColors.scss +++ b/packages/webui/src/client/styles/defaultColors.scss @@ -32,8 +32,8 @@ --part-identifier: #363636; --part-identifier-text: #eee; - --segment-event-identifier: #363636; - --segment-event-identifier-text: #eee; + --segment-event-identifier: #ffee00; + --segment-event-identifier-text: #000000; --general-timecode-color: #ffff00; diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index 98704693eb7..a036fad9ff3 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -599,7 +599,7 @@ svg.icon { margin-top: auto; .segment-timeline__part-identifiers__identifier { - // The pill-shaped labels that appear in the title area of a segment, showing and linking to the part name or number + // The pill-shaped labels that appear in the segment header, showing and linking to the part name or number margin: 0.1rem; padding: 0.1rem 0.35rem 0.1rem 0.35rem; background-color: $part-identifier; @@ -635,6 +635,38 @@ svg.icon { } } } + .segment-timeline__segment-event-identifier{ + // The pill-shaped labels that appear in the segment header, showing extra information about the segment. + align-self: flex-start; + display: inline-block; + margin-top: auto; + margin: 0.25rem; + padding: 0.1rem 0.35rem 0.1rem 0.35rem; + background-color: $segment-event-identifier; + color: $segment-event-identifier-text; + border-radius: 99px; + font-family: "Roboto Flex", "Roboto", sans-serif; + font-style: normal; + font-size: 0.9rem; + letter-spacing: 0.03em; + line-height: 100%; + text-shadow: none; + font-variant-numeric: proportional-nums; + font-variation-settings: + 'wdth' 70, + 'wght' 500, + 'opsz' 120, + 'slnt' 0, + 'GRAD' 0, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 514, + 'YTUC' 712; + } } &.has-identifiers { diff --git a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx index 6490240b81b..7fdec3ad7b5 100644 --- a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx +++ b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx @@ -602,6 +602,7 @@ export const SegmentStoryboard = React.memo( )}
)} +
Seg. Identifier
{identifiers.length > 0 && (
{identifiers.map((ident) => ( diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index b262a1648da..03914c75cfa 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -1069,6 +1069,7 @@ export class SegmentTimelineClass extends React.Component )} +
Seg. Identifier
{identifiers.length > 0 && (
{identifiers.map((ident) => ( From 8c25369b74b9438c845b3dcde29b5405eddfb0a6 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Wed, 25 Mar 2026 10:46:20 +0100 Subject: [PATCH 05/10] chore: Updated the colours of the segment event identifier. --- packages/webui/src/client/styles/defaultColors.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/styles/defaultColors.scss b/packages/webui/src/client/styles/defaultColors.scss index a483f496d07..f6d7d40f4e6 100644 --- a/packages/webui/src/client/styles/defaultColors.scss +++ b/packages/webui/src/client/styles/defaultColors.scss @@ -32,8 +32,8 @@ --part-identifier: #363636; --part-identifier-text: #eee; - --segment-event-identifier: #ffee00; - --segment-event-identifier-text: #000000; + --segment-event-identifier: #363636; + --segment-event-identifier-text: #fff; --general-timecode-color: #ffff00; From 466641d15b28f0643758329fd830c23a29488f9e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 25 Mar 2026 13:02:03 +0000 Subject: [PATCH 06/10] chore: ui refactoring --- .../SegmentAdlibTesting.tsx | 33 +--- .../SegmentAdlibTestingContainer.tsx | 1 - .../getReactivePieceNoteCountsForSegment.tsx | 104 ---------- .../SegmentContainer/withResolvedSegment.ts | 11 -- .../ui/SegmentHeader/SegmentHeaderNotes.tsx | 183 ++++++++++++++++++ .../src/client/ui/SegmentList/SegmentList.tsx | 4 +- .../ui/SegmentList/SegmentListContainer.tsx | 1 - .../ui/SegmentList/SegmentListHeader.tsx | 40 +--- .../SegmentStoryboard/SegmentStoryboard.tsx | 35 +--- .../SegmentStoryboardContainer.tsx | 1 - .../ui/SegmentTimeline/SegmentTimeline.tsx | 43 +--- .../SegmentTimelineContainer.tsx | 1 - 12 files changed, 206 insertions(+), 251 deletions(-) delete mode 100644 packages/webui/src/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx create mode 100644 packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx diff --git a/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx b/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx index 84a0c7bfb71..c64fb081bbf 100644 --- a/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx +++ b/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsx @@ -1,9 +1,8 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { IContextMenuContext } from '../RundownView.js' -import { IOutputLayerUi, PartUi, SegmentNoteCounts, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' +import { IOutputLayerUi, PartUi, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' -import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifications.js' import { contextMenuHoldToDisplayTime, useCombinedRefs, useRundownViewEventBusListener } from '../../lib/lib.js' import { useTranslation } from 'react-i18next' import { literal } from '@sofie-automation/corelib/dist/lib' @@ -28,6 +27,7 @@ import { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' import { PieceUi } from '@sofie-automation/corelib/src/dataModel/Piece.js' import { isLoopRunning } from '@sofie-automation/corelib/src/playout/stateCacheResolver.js' +import { SegmentHeaderNotes } from '../SegmentHeader/SegmentHeaderNotes.js' interface IProps { id: string @@ -36,7 +36,6 @@ interface IProps { playlist: DBRundownPlaylist studio: UIStudio parts: Array - segmentNoteCounts: SegmentNoteCounts hasAlreadyPlayed: boolean hasGuestItems: boolean hasRemoteItems: boolean @@ -79,9 +78,6 @@ export const SegmentAdlibTesting = React.memo( const [squishedHover, setSquishedHover] = useState(null) const squishedHoverTimeout = useRef(null) - const criticalNotes = props.segmentNoteCounts.criticial - const warningNotes = props.segmentNoteCounts.warning - const getSegmentContext = () => { const ctx = literal({ segment: props.segment, @@ -460,30 +456,7 @@ export const SegmentAdlibTesting = React.memo( > {t('Adlib Testing')} - {(criticalNotes > 0 || warningNotes > 0) && ( -
- {criticalNotes > 0 && ( -
props.onHeaderNoteClick?.(props.segment._id, NoteSeverity.ERROR)} - aria-label={t('Critical problems')} - > - -
{criticalNotes}
-
- )} - {warningNotes > 0 && ( -
props.onHeaderNoteClick?.(props.segment._id, NoteSeverity.WARNING)} - aria-label={t('Warnings')} - > - -
{warningNotes}
-
- )} -
- )} +
{Object.values(props.segment.outputLayers) diff --git a/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTestingContainer.tsx b/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTestingContainer.tsx index 048ae91000b..619df076340 100644 --- a/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTestingContainer.tsx +++ b/packages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTestingContainer.tsx @@ -209,7 +209,6 @@ export const SegmentAdlibTestingContainer = withResolvedSegment(function segment={props.segmentui} studio={props.studio} parts={props.parts} - segmentNoteCounts={props.segmentNoteCounts} onItemClick={props.onPieceClick} onItemDoubleClick={props.onPieceDoubleClick} playlist={props.playlist} diff --git a/packages/webui/src/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx b/packages/webui/src/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx deleted file mode 100644 index 12ada53dabc..00000000000 --- a/packages/webui/src/client/ui/SegmentContainer/getReactivePieceNoteCountsForSegment.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { NoteSeverity } from '@sofie-automation/blueprints-integration' -import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' -import { MongoFieldSpecifierOnes } from '@sofie-automation/corelib/dist/mongo' -import { UISegmentPartNote } from '@sofie-automation/meteor-lib/dist/api/rundownNotifications' -import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { getIgnorePieceContentStatus } from '../../lib/localStorage.js' -import { UIPartInstances, UIPieceContentStatuses, UISegmentPartNotes } from '../Collections.js' -import { SegmentNoteCounts, SegmentUi } from './withResolvedSegment.js' -import { Notifications } from '../../collections/index.js' -import { DBNotificationObj } from '@sofie-automation/corelib/dist/dataModel/Notifications' -import { UIPieceContentStatus } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' - -export function getReactivePieceNoteCountsForSegment(segment: SegmentUi): SegmentNoteCounts { - const segmentNoteCounts: SegmentNoteCounts = { - criticial: 0, - warning: 0, - } - - const rawNotes = UISegmentPartNotes.find({ segmentId: segment._id }, { fields: { note: 1 } }).fetch() as Pick< - UISegmentPartNote, - 'note' - >[] - for (const note of rawNotes) { - if (note.note.type === NoteSeverity.ERROR) { - segmentNoteCounts.criticial++ - } else if (note.note.type === NoteSeverity.WARNING) { - segmentNoteCounts.warning++ - } - } - - const mediaObjectStatuses = UIPieceContentStatuses.find( - { - rundownId: segment.rundownId, - segmentId: segment._id, - }, - { - fields: literal>({ - _id: 1, - // @ts-expect-error deep property - 'status.status': 1, - }), - } - ).fetch() as Array & { status: Pick }> - - if (!getIgnorePieceContentStatus()) { - for (const obj of mediaObjectStatuses) { - switch (obj.status.status) { - case PieceStatusCode.OK: - case PieceStatusCode.SOURCE_NOT_READY: - case PieceStatusCode.UNKNOWN: - // Ignore - break - case PieceStatusCode.SOURCE_NOT_SET: - segmentNoteCounts.criticial++ - break - case PieceStatusCode.SOURCE_HAS_ISSUES: - case PieceStatusCode.SOURCE_BROKEN: - case PieceStatusCode.SOURCE_MISSING: - case PieceStatusCode.SOURCE_UNKNOWN_STATE: - segmentNoteCounts.warning++ - break - default: - assertNever(obj.status.status) - segmentNoteCounts.warning++ - break - } - } - } - - // Find any relevant notifications - const partInstancesForSegment = UIPartInstances.find( - { segmentId: segment._id, reset: { $ne: true } }, - { - fields: { - _id: 1, - }, - } - ).fetch() as Array> - const rawNotifications = Notifications.find( - { - $or: [ - { 'relatedTo.segmentId': segment._id }, - { - 'relatedTo.partInstanceId': { $in: partInstancesForSegment.map((p) => p._id) }, - }, - ], - }, - { - fields: { - severity: 1, - }, - } - ).fetch() as Array> - for (const notification of rawNotifications) { - if (notification.severity === NoteSeverity.ERROR) { - segmentNoteCounts.criticial++ - } else if (notification.severity === NoteSeverity.WARNING) { - segmentNoteCounts.warning++ - } - } - - return segmentNoteCounts -} diff --git a/packages/webui/src/client/ui/SegmentContainer/withResolvedSegment.ts b/packages/webui/src/client/ui/SegmentContainer/withResolvedSegment.ts index 30e3a6e7bbd..1ce74fa1d2c 100644 --- a/packages/webui/src/client/ui/SegmentContainer/withResolvedSegment.ts +++ b/packages/webui/src/client/ui/SegmentContainer/withResolvedSegment.ts @@ -15,7 +15,6 @@ import { RundownLayoutFilterBase, RundownViewLayout, } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { getReactivePieceNoteCountsForSegment } from './getReactivePieceNoteCountsForSegment.js' import { SegmentViewMode } from './SegmentViewModes.js' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' import { AdlibSegmentUi } from '../../lib/shelf.js' @@ -93,15 +92,9 @@ export interface IResolvedSegmentProps { showDurationSourceLayers?: Set } -export interface SegmentNoteCounts { - criticial: number - warning: number -} - export interface ITrackedResolvedSegmentProps { segmentui: SegmentUi | undefined parts: Array - segmentNoteCounts: SegmentNoteCounts hasRemoteItems: boolean hasGuestItems: boolean hasAlreadyPlayed: boolean @@ -125,7 +118,6 @@ export function withResolvedSegment 0; i--) { @@ -280,7 +270,6 @@ export function withResolvedSegment void +} +interface SegmentNoteCounts { + criticalNotes: number + warningNotes: number + headerNotes: ITranslatableMessage[] +} + +export function SegmentHeaderNotes({ classname, segmentId, onHeaderNoteClick }: SegmentHeaderNotesProps): JSX.Element { + const { t } = useTranslation() + + const { criticalNotes, warningNotes, headerNotes } = useTracker( + () => getReactivePieceNoteCountsForSegment(segmentId), + [segmentId], + { criticalNotes: 0, warningNotes: 0, headerNotes: [] } + ) + + return ( + <> + {(criticalNotes > 0 || warningNotes > 0) && ( +
+ {criticalNotes > 0 && ( +
onHeaderNoteClick?.(segmentId, NoteSeverity.ERROR)} + aria-label={t('Critical problems')} + > + +
{criticalNotes}
+
+ )} + {warningNotes > 0 && ( +
onHeaderNoteClick?.(segmentId, NoteSeverity.WARNING)} + aria-label={t('Warnings')} + > + +
{warningNotes}
+
+ )} +
+ )} + + {headerNotes.map((event, index) => ( +
+ {event.key} +
+ ))} + + ) +} + +function getReactivePieceNoteCountsForSegment(segmentId: SegmentId): SegmentNoteCounts { + const segmentNoteCounts: SegmentNoteCounts = { + criticalNotes: 0, + warningNotes: 0, + headerNotes: [], // TODO - define + } + + const rawNotes = UISegmentPartNotes.find({ segmentId }, { fields: { note: 1 } }).fetch() as Pick< + UISegmentPartNote, + 'note' + >[] + for (const note of rawNotes) { + switch (note.note.type) { + case NoteSeverity.ERROR: + segmentNoteCounts.criticalNotes++ + break + case NoteSeverity.WARNING: + segmentNoteCounts.warningNotes++ + break + case NoteSeverity.INFO: + // Ignore + break + default: + assertNever(note.note.type) + } + } + + const mediaObjectStatuses = UIPieceContentStatuses.find( + { + segmentId, + }, + { + fields: literal>({ + _id: 1, + // @ts-expect-error deep property + 'status.status': 1, + }), + } + ).fetch() as Array & { status: Pick }> + + if (!getIgnorePieceContentStatus()) { + for (const obj of mediaObjectStatuses) { + switch (obj.status.status) { + case PieceStatusCode.OK: + case PieceStatusCode.SOURCE_NOT_READY: + case PieceStatusCode.UNKNOWN: + // Ignore + break + case PieceStatusCode.SOURCE_NOT_SET: + segmentNoteCounts.criticalNotes++ + break + case PieceStatusCode.SOURCE_HAS_ISSUES: + case PieceStatusCode.SOURCE_BROKEN: + case PieceStatusCode.SOURCE_MISSING: + case PieceStatusCode.SOURCE_UNKNOWN_STATE: + segmentNoteCounts.warningNotes++ + break + default: + assertNever(obj.status.status) + segmentNoteCounts.warningNotes++ + break + } + } + } + + // Find any relevant notifications + const partInstancesForSegment = UIPartInstances.find( + { segmentId: segmentId, reset: { $ne: true } }, + { + fields: { + _id: 1, + }, + } + ).fetch() as Array> + const rawNotifications = Notifications.find( + { + $or: [ + { 'relatedTo.segmentId': segmentId }, + { + 'relatedTo.partInstanceId': { $in: partInstancesForSegment.map((p) => p._id) }, + }, + ], + }, + { + fields: { + severity: 1, + message: 1, + }, + } + ).fetch() as Array> + for (const notification of rawNotifications) { + switch (notification.severity) { + case NoteSeverity.ERROR: + segmentNoteCounts.criticalNotes++ + break + case NoteSeverity.WARNING: + segmentNoteCounts.warningNotes++ + break + case NoteSeverity.INFO: + // Ignore + break + default: + assertNever(notification.severity) + } + } + + return segmentNoteCounts +} diff --git a/packages/webui/src/client/ui/SegmentList/SegmentList.tsx b/packages/webui/src/client/ui/SegmentList/SegmentList.tsx index 136e995c180..aac2e270012 100644 --- a/packages/webui/src/client/ui/SegmentList/SegmentList.tsx +++ b/packages/webui/src/client/ui/SegmentList/SegmentList.tsx @@ -2,7 +2,7 @@ import React, { ReactNode, useLayoutEffect, useMemo, useRef, useState } from 're import classNames from 'classnames' import { DBRundownPlaylist, RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { UIStateStorage } from '../../lib/UIStateStorage.js' -import { PartUi, SegmentNoteCounts, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' +import { PartUi, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' import { IContextMenuContext } from '../RundownView.js' import { useCombinedRefs } from '../../lib/lib.js' import { literal } from '@sofie-automation/corelib/dist/lib' @@ -37,7 +37,6 @@ interface IProps { segment: SegmentUi playlist: DBRundownPlaylist parts: Array - segmentNoteCounts: SegmentNoteCounts fixedSegmentDuration: boolean showCountdownToSegment: boolean @@ -237,7 +236,6 @@ const SegmentListInner = React.forwardRef(function Segme parts={props.parts} segment={props.segment} playlist={props.playlist} - segmentNoteCounts={props.segmentNoteCounts} highlight={highlight} isLiveSegment={props.isLiveSegment} isNextSegment={props.isNextSegment} diff --git a/packages/webui/src/client/ui/SegmentList/SegmentListContainer.tsx b/packages/webui/src/client/ui/SegmentList/SegmentListContainer.tsx index 4bd07d4c8e6..5fed38f774d 100644 --- a/packages/webui/src/client/ui/SegmentList/SegmentListContainer.tsx +++ b/packages/webui/src/client/ui/SegmentList/SegmentListContainer.tsx @@ -201,7 +201,6 @@ export const SegmentListContainer = withResolvedSegment(function Segment parts={props.parts} playlist={props.playlist} currentPartWillAutoNext={currentPartWillAutoNext} - segmentNoteCounts={props.segmentNoteCounts} isLiveSegment={isLiveSegment} isNextSegment={isNextSegment} isQueuedSegment={props.playlist.queuedSegmentId === props.segmentui._id} diff --git a/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx b/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx index 7a791cbe5d9..6baebaf2f94 100644 --- a/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx +++ b/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx @@ -5,7 +5,7 @@ import { contextMenuHoldToDisplayTime } from '../../lib/lib.js' import { ErrorBoundary } from '../../lib/ErrorBoundary.js' import { SwitchViewModeButton } from '../SegmentContainer/SwitchViewModeButton.js' import { SegmentViewMode } from '../SegmentContainer/SegmentViewModes.js' -import { PartUi, SegmentNoteCounts, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' +import { PartUi, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' import { PartCountdown } from '../RundownView/RundownTiming/PartCountdown.js' import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration.js' import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -13,8 +13,8 @@ import { useTranslation } from 'react-i18next' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { IContextMenuContext } from '../RundownView.js' import { NoteSeverity } from '@sofie-automation/blueprints-integration' -import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifications.js' import { SegmentTimeAnchorTime } from '../RundownView/RundownTiming/SegmentTimeAnchorTime.js' +import { SegmentHeaderNotes } from '../SegmentHeader/SegmentHeaderNotes.js' export function SegmentListHeader({ isDetached, @@ -23,7 +23,6 @@ export function SegmentListHeader({ parts, playlist, highlight, - segmentNoteCounts, isLiveSegment, isNextSegment, isQueuedSegment, @@ -42,7 +41,6 @@ export function SegmentListHeader({ segment: SegmentUi playlist: DBRundownPlaylist parts: Array - segmentNoteCounts: SegmentNoteCounts highlight: boolean isLiveSegment: boolean isNextSegment: boolean @@ -78,9 +76,6 @@ export function SegmentListHeader({ // setDetached(shouldDetach) // } - const criticalNotes = segmentNoteCounts.criticial - const warningNotes = segmentNoteCounts.warning - const contents = ( )}
- {(criticalNotes > 0 || warningNotes > 0) && ( -
- {criticalNotes > 0 && ( -
onHeaderNoteClick?.(segment._id, NoteSeverity.ERROR)} - aria-label={t('Critical problems')} - > - -
{criticalNotes}
-
- )} - {warningNotes > 0 && ( -
onHeaderNoteClick?.(segment._id, NoteSeverity.WARNING)} - aria-label={t('Warnings')} - > - -
{warningNotes}
-
- )} -
- )} + + + diff --git a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx index 7fdec3ad7b5..510dc513c3f 100644 --- a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx +++ b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx @@ -2,9 +2,8 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { IContextMenuContext } from '../RundownView.js' -import { IOutputLayerUi, PartUi, SegmentNoteCounts, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' +import { IOutputLayerUi, PartUi, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' -import { CriticalIconSmall, WarningIconSmall } from '../../lib/ui/icons/notifications.js' import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration.js' import { PartCountdown } from '../RundownView/RundownTiming/PartCountdown.js' import { contextMenuHoldToDisplayTime, useCombinedRefs, useRundownViewEventBusListener } from '../../lib/lib.js' @@ -48,6 +47,7 @@ import { isQuickLoopEnd as getIsQuickLoopEnd, isEntirePlaylistLooping as getIsEntirePlaylistLooping, } from '@sofie-automation/corelib/src/playout/stateCacheResolver.js' +import { SegmentHeaderNotes } from '../SegmentHeader/SegmentHeaderNotes.js' interface IProps { id: string @@ -56,7 +56,6 @@ interface IProps { playlist: DBRundownPlaylist studio: UIStudio parts: Array - segmentNoteCounts: SegmentNoteCounts // timeScale: number // maxTimeScale: number // onRecalculateMaxTimeScale: () => Promise @@ -132,9 +131,6 @@ export const SegmentStoryboard = React.memo( } } - const criticalNotes = props.segmentNoteCounts.criticial - const warningNotes = props.segmentNoteCounts.warning - const [useTimeOfDayCountdowns, setUseTimeOfDayCountdowns] = useState( UIStateStorage.getItemBoolean( `rundownView.${props.playlist._id}`, @@ -578,31 +574,8 @@ export const SegmentStoryboard = React.memo( > {props.segment.name} - {(criticalNotes > 0 || warningNotes > 0) && ( -
- {criticalNotes > 0 && ( -
props.onHeaderNoteClick?.(props.segment._id, NoteSeverity.ERROR)} - aria-label={t('Critical problems')} - > - -
{criticalNotes}
-
- )} - {warningNotes > 0 && ( -
props.onHeaderNoteClick?.(props.segment._id, NoteSeverity.WARNING)} - aria-label={t('Warnings')} - > - -
{warningNotes}
-
- )} -
- )} -
Seg. Identifier
+ + {identifiers.length > 0 && (
{identifiers.map((ident) => ( diff --git a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx index 169f08c9230..0bd4581207e 100644 --- a/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx +++ b/packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboardContainer.tsx @@ -211,7 +211,6 @@ export const SegmentStoryboardContainer = withResolvedSegment(function S segment={props.segmentui} studio={props.studio} parts={props.parts} - segmentNoteCounts={props.segmentNoteCounts} onItemClick={props.onPieceClick} onItemDoubleClick={props.onPieceDoubleClick} playlist={props.playlist} diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index 03914c75cfa..2f3a45a4682 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -27,7 +27,6 @@ import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString, unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { isPartPlayable, PartExtended } from '@sofie-automation/corelib/dist/dataModel/Part' import { contextMenuHoldToDisplayTime } from '../../lib/lib.js' -import { WarningIconSmall, CriticalIconSmall } from '../../lib/ui/icons/notifications.js' import RundownViewEventBus, { RundownViewEvents, HighlightEvent, @@ -42,7 +41,6 @@ import { SegmentViewMode } from '../SegmentContainer/SegmentViewModes.js' import { SwitchViewModeButton } from '../SegmentContainer/SwitchViewModeButton.js' import { PartId, PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { SegmentNoteCounts } from '../SegmentContainer/withResolvedSegment.js' import { withTiming, TimingTickResolution, @@ -58,6 +56,7 @@ import { hasUserEditableContent } from '../UserEditOperations/PropertiesPanel.js import { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' import { PieceUi } from '@sofie-automation/corelib/src/dataModel/Piece.js' import { isLoopRunning, wrapPartToTemporaryInstance } from '@sofie-automation/corelib/src/playout/stateCacheResolver.js' +import { SegmentHeaderNotes } from '../SegmentHeader/SegmentHeaderNotes.js' interface IProps { id: string @@ -67,7 +66,6 @@ interface IProps { followLiveSegments: boolean studio: UIStudio parts: Array - segmentNoteCounts: SegmentNoteCounts timeScale: number maxTimeScale: number onRecalculateMaxTimeScale: () => Promise @@ -942,9 +940,6 @@ export class SegmentTimelineClass extends React.Component = this.props.parts .map((p) => p.instance.part.identifier @@ -1039,37 +1034,11 @@ export class SegmentTimelineClass extends React.Component {this.props.segment.name} - {(criticalNotes > 0 || warningNotes > 0) && ( -
- {criticalNotes > 0 && ( -
- this.props.onHeaderNoteClick && - this.props.onHeaderNoteClick(this.props.segment._id, NoteSeverity.ERROR) - } - aria-label={t('Critical problems')} - > - -
{criticalNotes}
-
- )} - {warningNotes > 0 && ( -
- this.props.onHeaderNoteClick && - this.props.onHeaderNoteClick(this.props.segment._id, NoteSeverity.WARNING) - } - aria-label={t('Warnings')} - > - -
{warningNotes}
-
- )} -
- )} -
Seg. Identifier
+ + {identifiers.length > 0 && (
{identifiers.map((ident) => ( diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx index e541773d0d1..f578288872a 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx @@ -658,7 +658,6 @@ const SegmentTimelineContainerContent = withResolvedSegment( segment={this.props.segmentui} studio={this.props.studio} parts={this.props.parts} - segmentNoteCounts={this.props.segmentNoteCounts} timeScale={this.state.timeScale} maxTimeScale={this.state.maxTimeScale} onRecalculateMaxTimeScale={this.updateMaxTimeScale} From 151cf93aa157aee8499b4e47f6e3f43a171d75f1 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 31 Mar 2026 14:23:43 +0100 Subject: [PATCH 07/10] feat: use segmentHeaderNotes --- .../src/documents/part.ts | 3 ++ .../job-worker/src/blueprints/context/lib.ts | 11 ++++- .../src/ingest/generationSegment.ts | 3 ++ .../ui/SegmentHeader/SegmentHeaderNotes.tsx | 45 +++++++++++++++++-- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/blueprints-integration/src/documents/part.ts b/packages/blueprints-integration/src/documents/part.ts index 42283ac3609..cf778b25c7e 100644 --- a/packages/blueprints-integration/src/documents/part.ts +++ b/packages/blueprints-integration/src/documents/part.ts @@ -91,6 +91,9 @@ export interface IBlueprintMutatablePart): IBlueprintP displayDurationGroup: part.displayDurationGroup, displayDuration: part.displayDuration, identifier: part.identifier, + segmentHeaderNotes: clone(part.segmentHeaderNotes), hackListenToMediaObjectUpdates: clone( part.hackListenToMediaObjectUpdates ), @@ -705,7 +708,13 @@ export function convertPartialBlueprintMutablePartToCore( blueprintId, ]) } else { - delete playoutUpdatePart.userEditOperations + delete playoutUpdatePart.userEditProperties + } + + if ('segmentHeaderNotes' in updatePart) { + playoutUpdatePart.segmentHeaderNotes = updatePart.segmentHeaderNotes?.map((note) => + wrapTranslatableMessageFromBlueprints(note, [blueprintId]) + ) } return playoutUpdatePart diff --git a/packages/job-worker/src/ingest/generationSegment.ts b/packages/job-worker/src/ingest/generationSegment.ts index e3583e8515f..f563b6db1d9 100644 --- a/packages/job-worker/src/ingest/generationSegment.ts +++ b/packages/job-worker/src/ingest/generationSegment.ts @@ -377,6 +377,9 @@ function updateModelWithGeneratedPart( ]), } : undefined, + segmentHeaderNotes: blueprintPart.part.segmentHeaderNotes?.map((note) => + wrapTranslatableMessageFromBlueprints(note, [blueprintId]) + ), userEditOperations: translateUserEditsFromBlueprint(blueprintPart.part.userEditOperations, [blueprintId]), userEditProperties: translateUserEditPropertiesFromBlueprint(blueprintPart.part.userEditProperties, [ blueprintId, diff --git a/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx b/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx index 7672455aaf2..a0b5c20448b 100644 --- a/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx +++ b/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx @@ -12,9 +12,10 @@ import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/Part import { literal } from 'shuttle-webhid' import { Notifications } from '../../collections' import { getIgnorePieceContentStatus } from '../../lib/localStorage' -import { UISegmentPartNotes, UIPieceContentStatuses, UIPartInstances } from '../Collections' +import { UISegmentPartNotes, UIPieceContentStatuses, UIPartInstances, UIParts } from '../Collections' import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import type { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' +import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' export interface SegmentHeaderNotesProps { /** Override the classname of the root div */ @@ -77,7 +78,7 @@ function getReactivePieceNoteCountsForSegment(segmentId: SegmentId): SegmentNote const segmentNoteCounts: SegmentNoteCounts = { criticalNotes: 0, warningNotes: 0, - headerNotes: [], // TODO - define + headerNotes: [], } const rawNotes = UISegmentPartNotes.find({ segmentId }, { fields: { note: 1 } }).fetch() as Pick< @@ -144,9 +145,15 @@ function getReactivePieceNoteCountsForSegment(segmentId: SegmentId): SegmentNote { fields: { _id: 1, + // @ts-expect-error deep property + 'part._id': 1, + 'part._rank': 1, + 'part.segmentHeaderNotes': 1, }, } - ).fetch() as Array> + ).fetch() as Array< + Pick & { part: Pick } + > const rawNotifications = Notifications.find( { $or: [ @@ -179,5 +186,37 @@ function getReactivePieceNoteCountsForSegment(segmentId: SegmentId): SegmentNote } } + const partsForSegment = UIParts.find( + { segmentId }, + { + fields: { + _id: 1, + _rank: 1, + segmentHeaderNotes: 1, + }, + } + ).fetch() as Array> + + // Collect the segment header notes from the parts in part rank order, with partinstance taking priority over the part + const partIdsWithInstance = new Set(partInstancesForSegment.map((pi) => pi.part._id)) + + const mergedNoteEntries: Array<{ rank: number; notes: ITranslatableMessage[] }> = [] + for (const partInstance of partInstancesForSegment) { + if (partInstance.part.segmentHeaderNotes?.length) { + mergedNoteEntries.push({ rank: partInstance.part._rank, notes: partInstance.part.segmentHeaderNotes }) + } + } + for (const part of partsForSegment) { + if (partIdsWithInstance.has(part._id)) continue + if (part.segmentHeaderNotes?.length) { + mergedNoteEntries.push({ rank: part._rank, notes: part.segmentHeaderNotes }) + } + } + + mergedNoteEntries.sort((a, b) => a.rank - b.rank) + for (const entry of mergedNoteEntries) { + segmentNoteCounts.headerNotes.push(...entry.notes) + } + return segmentNoteCounts } From 71e94e3b00c768e4565fa4e21f8e3bb100fb73fc Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 13 Apr 2026 10:30:14 +0100 Subject: [PATCH 08/10] rename --- .../webui/src/client/styles/_colorScheme.scss | 4 ++-- .../src/client/styles/defaultColors.scss | 4 ++-- .../webui/src/client/styles/rundownView.scss | 21 +++++++++++-------- .../ui/SegmentHeader/SegmentHeaderNotes.tsx | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/webui/src/client/styles/_colorScheme.scss b/packages/webui/src/client/styles/_colorScheme.scss index 5c6e9db5397..1967769b3aa 100644 --- a/packages/webui/src/client/styles/_colorScheme.scss +++ b/packages/webui/src/client/styles/_colorScheme.scss @@ -31,8 +31,8 @@ $general-timecode-color: var(--general-timecode-color); $part-identifier: var(--part-identifier); $part-identifier-text: var(--part-identifier-text); -$segment-event-identifier: var(--segment-event-identifier); -$segment-event-identifier-text: var(--segment-event-identifier-text); +$segment-header-note-identifier: var(--segment-header-note-identifier); +$segment-header-note-identifier-text: var(--segment-header-note-identifier-text); $ui-button-primary: var(--ui-button-primary); $ui-button-primary--hover: var(--ui-button-primary--hover); diff --git a/packages/webui/src/client/styles/defaultColors.scss b/packages/webui/src/client/styles/defaultColors.scss index f6d7d40f4e6..51ceac9472b 100644 --- a/packages/webui/src/client/styles/defaultColors.scss +++ b/packages/webui/src/client/styles/defaultColors.scss @@ -32,8 +32,8 @@ --part-identifier: #363636; --part-identifier-text: #eee; - --segment-event-identifier: #363636; - --segment-event-identifier-text: #fff; + --segment-header-note-identifier: #363636; + --segment-header-note-identifier-text: #fff; --general-timecode-color: #ffff00; diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index a036fad9ff3..79f32bbff4a 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -605,7 +605,7 @@ svg.icon { background-color: $part-identifier; color: $part-identifier-text; border-radius: 99px; - font-family: "Roboto Flex", "Roboto", sans-serif; + font-family: 'Roboto Flex', 'Roboto', sans-serif; font-style: normal; font-size: 0.9rem; letter-spacing: 0.03em; @@ -635,17 +635,17 @@ svg.icon { } } } - .segment-timeline__segment-event-identifier{ + .segment-timeline__segment-header-note-identifier { // The pill-shaped labels that appear in the segment header, showing extra information about the segment. align-self: flex-start; display: inline-block; margin-top: auto; margin: 0.25rem; padding: 0.1rem 0.35rem 0.1rem 0.35rem; - background-color: $segment-event-identifier; - color: $segment-event-identifier-text; + background-color: $segment-header-note-identifier; + color: $segment-header-note-identifier-text; border-radius: 99px; - font-family: "Roboto Flex", "Roboto", sans-serif; + font-family: 'Roboto Flex', 'Roboto', sans-serif; font-style: normal; font-size: 0.9rem; letter-spacing: 0.03em; @@ -1169,7 +1169,8 @@ svg.icon { } .segment-timeline__part { .segment-timeline__part__invalid-cover { - background-image: repeating-linear-gradient( + background-image: + repeating-linear-gradient( 45deg, var(--invalid-reason-color-transparent) 0%, var(--invalid-reason-color-transparent) 4px, @@ -1451,7 +1452,8 @@ svg.icon { left: 2px; right: 2px; z-index: 3; - background: repeating-linear-gradient( + background: + repeating-linear-gradient( 45deg, var(--invalid-reason-color-opaque) 0, var(--invalid-reason-color-opaque) 5px, @@ -1633,7 +1635,8 @@ svg.icon { right: 1px; z-index: 10; pointer-events: all; - background-image: repeating-linear-gradient( + background-image: + repeating-linear-gradient( 45deg, var(--invalid-reason-color-transparent) 0%, var(--invalid-reason-color-transparent) 5px, @@ -2565,7 +2568,7 @@ svg.icon { background-color: $part-identifier; color: $part-identifier-text; - font-family: "Roboto Flex", "Roboto", sans-serif; + font-family: 'Roboto Flex', 'Roboto', sans-serif; font-style: normal; font-size: 0.9rem; letter-spacing: 0.03em; diff --git a/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx b/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx index a0b5c20448b..83bf081f9d9 100644 --- a/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx +++ b/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx @@ -66,7 +66,7 @@ export function SegmentHeaderNotes({ classname, segmentId, onHeaderNoteClick }: )} {headerNotes.map((event, index) => ( -
+
{event.key}
))} From f0ced6c93c23eb5215b8b621820c34c431da5c9a Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 13 Apr 2026 10:47:42 +0100 Subject: [PATCH 09/10] lint --- .../webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx b/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx index 83bf081f9d9..74cd02aefa8 100644 --- a/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx +++ b/packages/webui/src/client/ui/SegmentHeader/SegmentHeaderNotes.tsx @@ -8,7 +8,6 @@ import { UIPieceContentStatus } from '@sofie-automation/corelib/dist/dataModel/P import { assertNever } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnes } from '@sofie-automation/corelib/dist/mongo' import { UISegmentPartNote } from '@sofie-automation/meteor-lib/dist/api/rundownNotifications' -import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' import { literal } from 'shuttle-webhid' import { Notifications } from '../../collections' import { getIgnorePieceContentStatus } from '../../lib/localStorage' @@ -16,6 +15,7 @@ import { UISegmentPartNotes, UIPieceContentStatuses, UIPartInstances, UIParts } import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import type { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { PartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' export interface SegmentHeaderNotesProps { /** Override the classname of the root div */ From e8ba1f73cd87d7298f6ff0015922054f05ebba2d Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:09:03 +0100 Subject: [PATCH 10/10] Merge remote-tracking branch 'origin/main' into feat/segment-event-identifier --- .github/ISSUE_TEMPLATE/deprecation_rfc.yml | 46 + .github/actions/setup-meteor/action.yaml | 2 +- .github/workflows/audit.yaml | 8 +- .github/workflows/deploy-docs.yml | 4 +- .github/workflows/node.yaml | 20 +- .github/workflows/prune-tags.yml | 2 +- .github/workflows/publish-libs.yml | 8 +- .github/workflows/sonar.yaml | 4 +- .husky/pre-commit | 0 .yarnrc.yml | 6 +- DEVELOPER.md | 4 +- meteor/.gitignore | 6 + meteor/.meteor/packages | 11 +- meteor/.meteor/release | 2 +- meteor/.meteor/versions | 30 +- ...meteorjs-rspack-npm-2.0.1-d001eb481c.patch | 13 + meteor/Dockerfile | 3 +- meteor/__mocks__/defaultCollectionObjects.ts | 4 +- meteor/__mocks__/helpers/database.ts | 3 +- meteor/eslint.config.mjs | 2 +- meteor/i18n/.gitignore | 1 + meteor/i18n/{template.pot => en.po} | 3577 +++++--- meteor/i18n/nb.mo | Bin 75894 -> 0 bytes meteor/i18n/nb.po | 6101 +++++++------ meteor/i18n/nn.mo | Bin 75883 -> 0 bytes meteor/i18n/nn.po | 6098 +++++++------ meteor/i18n/sv.po | 4536 ++++++++++ meteor/jest.config.js | 2 + meteor/package.json | 64 +- meteor/rspack.config.cjs | 6 + meteor/scripts/extract-i18next-po.mjs | 4 + meteor/scripts/extract-i18next-pot.mjs | 119 - meteor/scripts/i18n-compile-json.mjs | 59 +- meteor/scripts/translation/bundle.mjs | 59 + meteor/scripts/translation/config.mjs | 9 + meteor/scripts/translation/extract.mjs | 121 + meteor/server/__tests__/cronjobs.test.ts | 3 + ...ripheralDevice.resolveActionResult.test.ts | 220 + .../api/__tests__/peripheralDevice.test.ts | 3 +- .../api/blueprints/__tests__/api.test.ts | 36 +- .../api/blueprints/__tests__/http.test.ts | 244 + meteor/server/api/blueprints/api.ts | 29 +- meteor/server/api/blueprints/http.ts | 19 +- meteor/server/api/buckets.ts | 2 +- meteor/server/api/cleanup.ts | 2 +- meteor/server/api/client.ts | 4 +- .../deviceTriggers/RundownContentObserver.ts | 4 +- .../api/deviceTriggers/StudioObserver.ts | 13 +- .../__tests__/StudioObserver.test.ts | 156 + .../__tests__/TagsService.test.ts | 2 +- meteor/server/api/deviceTriggers/observer.ts | 7 +- .../deviceTriggers/reactiveContentCache.ts | 2 +- .../reactiveContentCacheForPieceInstances.ts | 2 +- .../api/deviceTriggers/triggersContext.ts | 8 +- meteor/server/api/evaluations.ts | 2 +- meteor/server/api/peripheralDevice.ts | 285 +- .../api/rest/v1/__tests__/playlists.spec.ts | 228 + meteor/server/api/rest/v1/ingest.ts | 2 +- meteor/server/api/rest/v1/playlists.ts | 470 +- meteor/server/api/rest/v1/showstyles.ts | 2 +- meteor/server/api/rest/v1/studios.ts | 2 +- meteor/server/api/rest/v1/typeConversion.ts | 4 +- meteor/server/api/snapshot.ts | 67 +- meteor/server/api/studio/api.ts | 2 + meteor/server/api/studio/lib.ts | 2 +- meteor/server/api/userActions.ts | 2 +- meteor/server/collections/rundown.ts | 7 +- meteor/server/coreSystem/index.ts | 30 +- meteor/server/cronjobs.ts | 2 +- meteor/server/lib/rest/v1/playlists.ts | 143 + meteor/server/lib/rest/v1/studios.ts | 3 + meteor/server/migration/0_1_0.ts | 2 + meteor/server/migration/X_X_X.ts | 67 +- meteor/server/publications/_publications.ts | 1 + .../externalEventSubscriptions.ts | 179 + .../ingestStatus/reactiveContentCache.ts | 2 +- meteor/server/publications/lib/lib.ts | 2 +- meteor/server/publications/lib/quickLoop.ts | 2 +- .../expectedPackages/contentCache.ts | 5 +- .../expectedPackages/generate.ts | 1 + .../packageManager/playoutContext.ts | 2 +- .../partInstancesUI/publication.ts | 2 +- .../partInstancesUI/reactiveContentCache.ts | 2 +- .../publications/partsUI/publication.ts | 2 +- .../partsUI/reactiveContentCache.ts | 2 +- .../__tests__/checkPieceContentStatus.test.ts | 470 + .../checkPieceContentStatus.ts | 252 +- .../rundown/publication.ts | 2 +- meteor/server/publications/rundown.ts | 3 + meteor/server/publications/rundownPlaylist.ts | 2 +- .../__tests__/generateNotesForSegment.test.ts | 117 + .../__tests__/publication.test.ts | 7 +- .../generateNotesForSegment.ts | 26 + .../segmentPartNotesUI/publication.ts | 10 +- .../reactiveContentCache.ts | 10 +- .../rundownContentObserver.ts | 8 +- meteor/server/security/check.ts | 2 +- .../__tests__/systemStatus.test.ts | 2 + meteor/server/webmanifest.ts | 2 +- .../server/worker/__tests__/jobQueue.test.ts | 169 + meteor/server/worker/jobQueue.ts | 40 + meteor/server/worker/worker.ts | 27 + meteor/tsconfig-meteor.json | 2 +- meteor/yarn.lock | 6011 +++++++++---- package.json | 6 +- .../blueprints-integration/jest.config.js | 5 +- packages/blueprints-integration/package.json | 2 +- .../src/api/showStyle.ts | 66 +- .../blueprints-integration/src/api/studio.ts | 102 + .../blueprints-integration/src/api/system.ts | 22 + .../src/context/adlibActionContext.ts | 21 +- .../src/context/executeTsrActionContext.ts | 2 +- .../src/context/externalEventContext.ts | 19 + .../src/context/index.ts | 4 + .../src/context/onSetAsNextContext.ts | 11 +- .../src/context/partsAndPieceActionContext.ts | 11 +- .../src/context/playoutActionContext.ts | 15 + .../src/context/rundownContext.ts | 4 + .../src/context/snapshotContext.ts | 88 + .../src/context/syncIngestChangesContext.ts | 12 +- .../src/context/tTimersContext.ts | 175 +- .../src/documents/partInstance.ts | 19 +- .../src/documents/playlistTiming.ts | 28 +- .../src/documents/rundown.ts | 3 + .../src/documents/segment.ts | 15 +- .../src/externalEvent.ts | 40 + packages/blueprints-integration/src/index.ts | 1 + .../blueprints-integration/src/userEditing.ts | 27 + .../tsconfig.build.json | 4 +- packages/corelib/jest.config.js | 3 + packages/corelib/package.json | 9 +- packages/corelib/src/StatusMessageResolver.ts | 159 + packages/corelib/src/TranslatableMessage.ts | 5 +- .../__tests__/StatusMessageResolver.test.ts | 296 + packages/corelib/src/__tests__/lib.spec.ts | 2 +- .../corelib/src/dataModel/PartInstance.ts | 9 +- .../src/dataModel/PieceContentStatus.ts | 12 + packages/corelib/src/dataModel/Rundown.ts | 9 +- .../{ => RundownPlaylist}/RundownPlaylist.ts | 143 +- .../src/dataModel/RundownPlaylist/TTimers.ts | 166 + packages/corelib/src/dataModel/Segment.ts | 5 +- .../src/dataModel/UserEditingDefinitions.ts | 19 + packages/corelib/src/error.ts | 4 +- packages/corelib/src/index.ts | 11 +- packages/corelib/src/lib.ts | 2 +- .../src/playout/__tests__/timings.test.ts | 2 +- packages/corelib/src/playout/rundownTiming.ts | 56 +- .../corelib/src/playout/stateCacheResolver.ts | 2 +- .../src/playout/stateCacheResolverTypes.ts | 2 +- packages/corelib/src/playout/timings.ts | 2 +- packages/corelib/src/protectedString.ts | 8 +- packages/corelib/src/pubsub.ts | 2 +- packages/corelib/src/snapshots.ts | 2 +- packages/corelib/src/timecode.ts | 257 + packages/corelib/src/typings/Timecode.d.ts | 15 - packages/corelib/src/worker/studio.ts | 127 +- packages/corelib/tsconfig.build.json | 5 +- .../error-message-customization.md | 214 + .../for-blueprint-developers/intro.md | 8 +- .../snapshot-hooks.md | 133 + .../splits-box-previews.md | 169 + .../for-developers/url-query-parameters.md | 3 +- .../features/sofie-views-and-screens.mdx | 20 +- .../user-guide/installation/quick-install.md | 6 +- packages/documentation/package.json | 10 +- packages/documentation/releases/releases.mdx | 26 +- ...mepageFeatures.js => HomepageFeatures.jsx} | 0 .../src/components/HomepagePRs.css | 109 + .../src/components/HomepagePRs.jsx | 174 + .../src/components/LatestVersionNumber.jsx | 20 + packages/documentation/src/pages/index.js | 2 + packages/eslint.config.mjs | 23 + packages/job-worker/jest.config.js | 3 + packages/job-worker/package.json | 14 +- .../job-worker/src/__mocks__/collection.ts | 8 +- packages/job-worker/src/__mocks__/context.ts | 2 +- .../src/__mocks__/defaultCollectionObjects.ts | 4 +- .../src/__mocks__/helpers/snapshot.ts | 2 +- .../src/__mocks__/presetCollections.ts | 1 + .../src/__tests__/rundownPlaylist.test.ts | 2 +- .../src/blueprints/__tests__/config.test.ts | 3 + .../context-OnSetAsNextContext.test.ts | 22 +- .../__tests__/context-OnTakeContext.test.ts | 22 +- .../__tests__/context-adlibActions.test.ts | 22 +- .../__tests__/context-events.test.ts | 2 +- .../blueprints/context/GetRundownContext.ts | 2 +- .../blueprints/context/OnSetAsNextContext.ts | 8 +- .../src/blueprints/context/OnTakeContext.ts | 8 +- .../context/OnTimelineGenerateContext.ts | 13 +- .../blueprints/context/PartEventContext.ts | 10 +- .../context/PlaylistSnapshotCreatedContext.ts | 44 + .../context/RundownActivationContext.ts | 7 +- .../src/blueprints/context/RundownContext.ts | 5 + .../SyncIngestUpdateToPartInstanceContext.ts | 37 +- .../context/SystemSnapshotCreatedContext.ts | 42 + .../src/blueprints/context/adlibActions.ts | 8 +- .../job-worker/src/blueprints/context/lib.ts | 68 +- .../PartAndPieceInstanceActionService.ts | 22 +- .../context/services/TTimersService.ts | 155 +- .../PartAndPieceInstanceActionService.test.ts | 63 +- .../services/__tests__/TTimersService.test.ts | 379 +- packages/job-worker/src/db/collection.ts | 4 +- packages/job-worker/src/db/collections.ts | 6 +- .../__tests__/externalMessageQueue.test.ts | 2 +- packages/job-worker/src/events/handle.ts | 2 +- .../src/ingest/__tests__/ingest.test.ts | 2 +- .../ingest/__tests__/showShelfCompat.test.ts | 182 + .../syncChangesToPartInstance.test.ts | 5 +- .../src/ingest/__tests__/updateNext.test.ts | 42 + packages/job-worker/src/ingest/commit.ts | 2 +- .../src/ingest/generationRundown.ts | 3 +- .../src/ingest/generationSegment.ts | 30 +- .../src/ingest/model/IngestModel.ts | 5 +- .../model/implementation/IngestModelImpl.ts | 6 +- .../mosDevice/__tests__/mosIngest.test.ts | 2 +- .../src/ingest/syncChangesToPartInstance.ts | 20 +- packages/job-worker/src/ingest/updateNext.ts | 46 +- packages/job-worker/src/peripheralDevice.ts | 37 +- .../src/playout/__tests__/actions.test.ts | 2 +- .../src/playout/__tests__/infinites.test.ts | 2 +- .../job-worker/src/playout/__tests__/lib.ts | 2 +- .../src/playout/__tests__/playout.test.ts | 2 +- .../playout/__tests__/selectNextPart.test.ts | 2 +- .../playout/__tests__/snapshotHooks.test.ts | 212 + .../src/playout/__tests__/tTimers.test.ts | 33 +- .../src/playout/__tests__/tTimersJobs.test.ts | 118 +- .../src/playout/__tests__/timeline.test.ts | 2 +- .../abPlayback/__tests__/abPlayback.spec.ts | 2 +- .../__tests__/abPlaybackResolver.spec.ts | 50 + .../__tests__/abSessionHelper.spec.ts | 2 +- .../__tests__/applyAssignments.spec.ts | 2 +- .../playout/abPlayback/abPlaybackResolver.ts | 2 + .../playout/abPlayback/abPlaybackSessions.ts | 2 +- .../src/playout/abPlayback/abSessionHelper.ts | 2 +- .../playout/abPlayback/applyAssignments.ts | 5 +- .../src/playout/abPlayback/index.ts | 2 +- .../src/playout/activePlaylistJobs.ts | 2 +- .../job-worker/src/playout/adlibAction.ts | 36 +- packages/job-worker/src/playout/adlibJobs.ts | 2 +- packages/job-worker/src/playout/adlibUtils.ts | 2 +- .../job-worker/src/playout/bucketAdlibJobs.ts | 2 +- .../job-worker/src/playout/externalEvents.ts | 127 + packages/job-worker/src/playout/holdJobs.ts | 2 +- packages/job-worker/src/playout/lock.ts | 2 +- .../lookahead/__tests__/lookahead.test.ts | 5 +- .../playout/lookahead/__tests__/util.test.ts | 2 +- .../job-worker/src/playout/lookahead/index.ts | 2 +- .../src/playout/model/PlayoutModel.ts | 4 +- .../playout/model/PlayoutPartInstanceModel.ts | 9 + .../model/implementation/LoadPlayoutModel.ts | 2 +- .../model/implementation/PlayoutModelImpl.ts | 4 +- .../PlayoutPartInstanceModelImpl.ts | 6 +- .../__tests__/PlayoutModelImpl.spec.ts | 11 +- .../model/services/QuickLoopService.ts | 8 +- .../src/playout/quickLoopMarkers.ts | 5 +- .../job-worker/src/playout/selectNextPart.ts | 5 +- packages/job-worker/src/playout/setNext.ts | 2 +- .../job-worker/src/playout/setNextJobs.ts | 2 +- packages/job-worker/src/playout/snapshot.ts | 15 +- .../job-worker/src/playout/snapshotHooks.ts | 163 + packages/job-worker/src/playout/tTimers.ts | 51 +- .../job-worker/src/playout/tTimersJobs.ts | 112 +- packages/job-worker/src/playout/take.ts | 9 +- .../timeline/__tests__/rundown.test.ts | 5 +- .../src/playout/timeline/rundown.ts | 5 +- .../playout/timings/timelineTriggerTime.ts | 2 +- packages/job-worker/src/playout/upgrade.ts | 3 +- packages/job-worker/src/rundown.ts | 2 +- packages/job-worker/src/rundownPlaylists.ts | 5 +- packages/job-worker/src/studio/lib.ts | 2 +- .../src/studio/model/StudioPlayoutModel.ts | 2 +- .../studio/model/StudioPlayoutModelImpl.ts | 2 +- .../job-worker/src/workers/studio/jobs.ts | 27 +- packages/job-worker/tsconfig.build.json | 3 +- .../live-status-gateway-api/api/asyncapi.yaml | 2 + .../outputLayer/outputLayer-example.yaml | 6 + .../layers/outputLayer/outputLayer.yaml | 27 + .../sourceLayer/sourceLayer-example.yaml | 6 + .../layers/sourceLayer/sourceLayer.yaml | 28 + .../partInvalidReason-example.yaml | 3 + .../partInvalidReason/partInvalidReason.yaml | 19 + .../resolvedPart/resolvedPart-example.yaml | 17 + .../part/resolvedPart/resolvedPart.yaml | 56 + .../resolvedPiece/resolvedPiece-example.yaml | 17 + .../piece/resolvedPiece/resolvedPiece.yaml | 51 + .../activePlaylistEvent-example.yaml | 2 + .../activePlaylistEvent.yaml | 9 +- .../playlistStatus/playlistStatus.yaml | 1 - .../messages/resolvedPlaylistMessage.yaml | 11 + .../resolvedPlaylistEvent-example.yaml | 18 + .../resolvedPlaylistEvent.yaml | 51 + .../resolvedRundown-example.yaml | 9 + .../resolvedRundown/resolvedRundown.yaml | 32 + .../resolvedSegment-example.yaml | 16 + .../resolvedSegment/resolvedSegment.yaml | 47 + .../subscriptions/subscriptionName.yaml | 1 + .../api/components/tTimers/tTimerIndex.yaml | 6 + ...tTimerStatus-countdownRunning-example.yaml | 12 + .../tTimerStatus-freeRunRunning-example.yaml | 11 + .../tTimerStatus-unconfigured-example.yaml | 7 + .../api/components/tTimers/tTimerStatus.yaml | 3 + .../tTimerStatus-array-example.yaml | 34 + .../tTimerStatus-configured-example.yaml | 16 + .../tTimerStatus-unconfigured-example.yaml | 7 + .../tTimers/tTimerStatus/tTimerStatus.yaml | 48 + .../api/components/tTimers/timerMode.yaml | 21 + .../countdown/timerModeCountdown-example.yaml | 3 + .../countdown/timerModeCountdown.yaml | 20 + .../freeRun/timerModeFreeRun-example.yaml | 1 + .../timerMode/freeRun/timerModeFreeRun.yaml | 13 + .../timeOfDay/timerModeTimeOfDay-example.yaml | 3 + .../timeOfDay/timerModeTimeOfDay.yaml | 23 + .../tTimers/timerModeCountdown-example.yaml | 3 + .../tTimers/timerModeFreeRun-example.yaml | 1 + .../tTimers/timerModeTimeOfDay-example.yaml | 3 + .../api/components/tTimers/timerState.yaml | 19 + .../paused/timerStatePaused-example.yaml | 3 + .../timerState/paused/timerStatePaused.yaml | 27 + .../running/timerStateRunning-example.yaml | 3 + .../timerState/running/timerStateRunning.yaml | 28 + .../tTimers/timerStatePaused-example.yaml | 3 + .../tTimers/timerStateRunning-example.yaml | 3 + .../activePlaylistTimingMode.yaml | 1 + .../resolvedPartTiming-example.yaml | 7 + .../resolvedPartTiming.yaml | 31 + .../resolvedPieceTiming-example.yaml | 3 + .../resolvedPieceTiming.yaml | 18 + .../resolvedPlaylistTiming-example.yaml | 4 + .../resolvedPlaylistTiming.yaml | 35 + .../resolvedSegmentTiming-example.yaml | 3 + .../resolvedSegmentTiming.yaml | 19 + .../resolvedPlaylistTopic.yaml | 8 + packages/live-status-gateway-api/package.json | 9 +- .../scripts/generate-schema-types.mjs | 30 +- .../src/generated/asyncapi.yaml | 1017 ++- .../src/generated/schema.ts | 613 +- .../tsconfig.build.json | 7 +- packages/live-status-gateway/jest.config.js | 5 + packages/live-status-gateway/package.json | 4 +- .../sample-client/index.html | 4 + .../sample-client/script.js | 120 + .../playlistNotificationsHandler.ts | 3 +- .../rundownNotificationsHandler.ts | 2 +- .../src/collections/partHandler.ts | 2 +- .../src/collections/partInstancesHandler.ts | 3 +- .../partInstancesInPlaylistHandler.ts | 103 + .../pieceContentStatusesHandler.ts | 3 +- .../src/collections/pieceInstancesHandler.ts | 3 +- .../pieceInstancesInPlaylistHandler.ts | 125 + .../collections/piecesInPlaylistHandler.ts | 72 + .../src/collections/playlistHandler.ts | 2 +- .../collections/rundownContentHandlerBase.ts | 5 +- .../src/collections/rundownHandler.ts | 2 +- .../src/collections/segmentHandler.ts | 2 +- .../src/collections/showStyleBasesHandler.ts | 91 + packages/live-status-gateway/src/config.ts | 8 +- packages/live-status-gateway/src/connector.ts | 35 +- .../live-status-gateway/src/coreHandler.ts | 52 +- .../src/liveStatusServer.ts | 29 +- packages/live-status-gateway/src/process.ts | 31 - .../src/publicationCollection.ts | 2 +- .../src/topics/__tests__/activePieces.spec.ts | 2 +- .../topics/__tests__/activePlaylist.spec.ts | 61 +- .../topics/__tests__/packagesTopic.spec.ts | 2 +- .../src/topics/__tests__/utils.ts | 3 +- .../src/topics/activePiecesTopic.ts | 2 +- .../src/topics/activePlaylistTopic.ts | 71 +- .../src/topics/adLibsTopic.ts | 2 +- .../quickLoop/toQuickLoopStatus.ts | 71 + .../timers/__tests__/toTTimers.spec.ts | 51 + .../timers/toTTimers.ts | 50 + .../timing/toActivePlaylistTiming.ts | 42 + .../timing/translatePlaylistTimingType.ts | 20 + .../notification/toNotificationStatus.ts | 2 +- .../src/topics/helpers/pieceStatus.ts | 2 +- .../__tests__/conversionContext.spec.ts | 191 + .../resolvedPlaylistConversionTestUtils.ts | 134 + .../serializeResolvedSegment.spec.ts | 47 + .../__tests__/toResolvedPartStatus.spec.ts | 108 + .../__tests__/toResolvedPieceStatus.spec.ts | 69 + .../toResolvedPlaylistStatus.spec.ts | 121 + .../__tests__/toResolvedRundownStatus.spec.ts | 72 + .../__tests__/toResolvedSegmentStatus.spec.ts | 118 + .../context/conversionContext.ts | 232 + .../events/toResolvedPlaylistStatus.ts | 121 + .../parts/toResolvedPartStatus.ts | 65 + .../pieces/toResolvedPieceStatus.ts | 78 + .../rundowns/toResolvedRundownStatus.ts | 25 + .../segments/serializeResolvedSegment.ts | 54 + .../segments/toResolvedSegmentStatus.ts | 114 + .../src/topics/packagesTopic.ts | 2 +- .../src/topics/resolvedPlaylistTopic.ts | 147 + .../live-status-gateway/src/topics/root.ts | 14 + .../src/topics/segmentsTopic.ts | 2 +- .../src/topics/studioTopic.ts | 2 +- packages/live-status-gateway/src/wsHandler.ts | 5 + packages/live-status-gateway/src/wsMetrics.ts | 17 + .../live-status-gateway/tsconfig.build.json | 3 +- packages/meteor-lib/jest.config.js | 3 + packages/meteor-lib/package.json | 10 +- packages/meteor-lib/src/api/userActions.ts | 2 +- .../src/collections/RundownLayouts.ts | 34 - .../meteor-lib/src/triggers/actionFactory.ts | 2 +- .../triggers/actionFilterChainCompilers.ts | 2 +- .../src/triggers/triggersContext.ts | 2 +- packages/meteor-lib/tsconfig.build.json | 5 +- packages/mos-gateway/jest.config.js | 5 + packages/mos-gateway/package.json | 4 +- .../mos-gateway/src/CoreMosDeviceHandler.ts | 46 +- packages/mos-gateway/src/connector.ts | 6 +- packages/mos-gateway/src/coreHandler.ts | 35 +- packages/mos-gateway/src/mosHandler.ts | 26 +- packages/mos-gateway/src/mosMetrics.ts | 48 + packages/mos-gateway/src/mosStatus/handler.ts | 22 +- packages/mos-gateway/tsconfig.build.json | 3 +- packages/openapi/api/actions.yaml | 18 + packages/openapi/api/definitions/buckets.yaml | 6 +- .../openapi/api/definitions/playlists.yaml | 334 + packages/openapi/package.json | 18 +- packages/openapi/scripts/bundle-openapi.mjs | 35 + packages/openapi/src/generated/openapi.yaml | 7789 +++++++++++++++++ packages/openapi/tsconfig.build.json | 3 +- packages/package.json | 31 +- packages/playout-gateway/package.json | 6 +- packages/playout-gateway/src/connector.ts | 7 +- packages/playout-gateway/src/coreHandler.ts | 58 +- packages/playout-gateway/src/index.ts | 2 + .../playout-gateway/src/playoutMetrics.ts | 53 + packages/playout-gateway/src/tsrHandler.ts | 149 +- packages/playout-gateway/tsconfig.build.json | 3 +- .../examples/client.ts | 6 +- .../{jest.config.js => jest.config.cjs} | 5 + packages/server-core-integration/package.json | 28 +- .../__mocks__/{faye-websocket.ts => ws.ts} | 141 +- .../src/__tests__/index.spec.ts | 27 +- packages/server-core-integration/src/index.ts | 31 +- .../src/integrationTests/index.spec.ts | 20 +- .../src/lib/CoreConnectionChild.ts | 24 +- .../src/lib/__tests__/ddpClient.spec.ts | 4 +- .../src/lib/configManifest.ts | 2 +- .../src/lib/coreConnection.ts | 41 +- .../src/lib/ddpClient.ts | 105 +- .../src/lib/ddpConnector.ts | 13 +- .../src/lib/gateway-types.ts | 4 +- .../server-core-integration/src/lib/health.ts | 36 +- .../src/lib/methods.ts | 12 +- .../src/lib/process.ts | 22 +- .../src/lib/prometheus.ts | 34 + .../src/lib/subscriptions.ts | 6 +- .../src/types/faye-websocket.d.ts | 22 - .../tsconfig.build.json | 10 +- .../server-core-integration/tsconfig.json | 5 +- .../{jest.config.js => jest.config.cjs} | 2 +- packages/shared-lib/package.json | 20 +- .../src/core/deviceConfigManifest.ts | 8 +- packages/shared-lib/src/core/model/Ids.ts | 2 +- .../shared-lib/src/core/model/MediaObjects.ts | 4 +- .../src/core/model/PackageContainer.ts | 2 +- .../src/core/model/PeripheralDeviceCommand.ts | 4 +- .../src/core/model/StudioRouteSet.ts | 4 +- .../src/core/model/StudioSettings.ts | 11 + .../shared-lib/src/core/model/Timeline.ts | 4 +- .../src/core/model/TimelineDatastore.ts | 4 +- .../src/core/model/peripheralDevice.ts | 4 +- .../shared-lib/src/expectedPlayoutItem.ts | 9 +- .../input-gateway/deviceTriggerPreviews.ts | 10 +- packages/shared-lib/src/lib/JSONSchemaUtil.ts | 26 +- .../src/lib/__tests__/protectedString.spec.ts | 2 +- packages/shared-lib/src/lib/lib.ts | 2 +- .../shared-lib/src/lib/protectedString.ts | 2 +- packages/shared-lib/src/lib/translations.ts | 2 +- .../__tests__/splitBoxMedia.spec.ts | 61 + .../shared-lib/src/package-manager/helpers.ts | 2 +- .../shared-lib/src/package-manager/package.ts | 4 +- .../src/package-manager/publications.ts | 11 +- .../src/package-manager/splitBoxMedia.ts | 75 + .../src/peripheralDevice/externalEvents.ts | 44 + .../src/peripheralDevice/methodsAPI.ts | 26 +- .../peripheralDevice/peripheralDeviceAPI.ts | 16 +- .../shared-lib/src/pubsub/peripheralDevice.ts | 41 +- .../shared-lib/src/systemErrorMessages.ts | 40 + packages/shared-lib/src/tsr.ts | 2 +- packages/shared-lib/tsconfig.build.json | 12 +- packages/shared-lib/tsconfig.json | 3 +- packages/tsconfig.build.json | 1 + packages/tsconfig.test.json | 3 +- packages/webui/jest.config.cjs | 3 + packages/webui/package.json | 43 +- .../webui/public/locales/nb/translations.json | 1678 ++-- .../webui/public/locales/nn/translations.json | 1677 ++-- .../webui/public/locales/sv/translations.json | 24 +- .../src/__mocks__/defaultCollectionObjects.ts | 22 +- .../webui/src/__mocks__/helpers/database.ts | 42 +- packages/webui/src/__mocks__/mongo.ts | 14 +- .../webui/src/client/collections/index.ts | 64 +- packages/webui/src/client/collections/lib.ts | 33 +- .../client/collections/rundownPlaylistUtil.ts | 10 +- .../lib/Components/BlueprintAssetIcon.tsx | 2 +- .../src/client/lib/Components/Button.tsx | 0 .../src/client/lib/Components/Checkbox.tsx | 2 +- .../lib/Components/CounterComponents.tsx | 32 +- .../client/lib/Components/DropdownInput.tsx | 2 +- .../src/client/lib/Components/FloatInput.tsx | 30 +- .../src/client/lib/Components/IntInput.tsx | 17 +- .../client/lib/Components/JsonTextInput.tsx | 2 +- .../lib/Components/LabelAndOverrides.tsx | 8 +- .../lib/Components/MultiLineTextInput.tsx | 13 +- .../lib/Components/MultiSelectInput.tsx | 4 +- .../client/lib/Components/OverUnderChip.scss | 49 + .../client/lib/Components/OverUnderChip.tsx | 72 + .../client/lib/Components/PromiseButton.tsx | 2 +- .../src/client/lib/Components/TextInput.tsx | 19 +- .../src/client/lib/Components/TimeMsInput.tsx | 187 + .../client/lib/Components/ToggleSwitch.tsx | 2 +- .../webui/src/client/lib/Components/util.tsx | 4 +- .../lib/ConnectionStatusNotification.tsx | 14 +- .../src/client/lib/DocumentTitleProvider.tsx | 4 +- .../webui/src/client/lib/EditAttribute.tsx | 8 +- packages/webui/src/client/lib/Escape.tsx | 12 +- .../src/client/lib/KeyboardFocusIndicator.tsx | 2 +- .../webui/src/client/lib/LottieButton.tsx | 2 +- packages/webui/src/client/lib/ModalDialog.tsx | 10 +- packages/webui/src/client/lib/Moment.tsx | 2 +- .../lib/ReactMeteorData/ReactMeteorData.tsx | 4 +- packages/webui/src/client/lib/Settings.ts | 2 +- .../src/client/lib/SettingsNavigation.tsx | 2 +- .../webui/src/client/lib/SorensenContext.tsx | 2 +- packages/webui/src/client/lib/Spinner.tsx | 2 +- .../webui/src/client/lib/SplitDropdown.tsx | 2 +- .../webui/src/client/lib/SplitPreviewBox.tsx | 82 +- .../webui/src/client/lib/StyledTimecode.tsx | 3 +- .../src/client/lib/VideoPreviewPlayer.tsx | 2 +- .../webui/src/client/lib/VirtualElement.tsx | 168 +- .../src/client/lib/__tests__/rundown.test.ts | 28 +- .../lib/__tests__/rundownTiming.test.ts | 19 +- packages/webui/src/client/lib/clientAPI.ts | 6 +- .../webui/src/client/lib/clientUserAction.ts | 2 +- .../webui/src/client/lib/collapseJSON.tsx | 2 +- .../lib/data/nora/browser-plugin-data.ts | 6 +- .../lib/forms/SchemaFormForCollection.tsx | 12 +- .../client/lib/forms/SchemaFormInPlace.tsx | 4 +- .../SchemaFormOneOfButtons/OneOfButtons.scss | 44 + .../SchemaFormOneOfButtons/OneOfButtons.tsx | 332 + .../lib/forms/SchemaFormTable/ArrayTable.tsx | 6 +- .../SchemaFormTable/ArrayTableOpHelper.tsx | 2 +- .../forms/SchemaFormTable/ArrayTableRow.tsx | 10 +- .../lib/forms/SchemaFormTable/ObjectTable.tsx | 12 +- .../SchemaFormTable/ObjectTableDeletedRow.tsx | 2 +- .../SchemaFormTable/ObjectTableOpHelper.tsx | 2 +- .../forms/SchemaFormTable/TableEditRow.tsx | 8 +- .../lib/forms/SchemaFormWithOverrides.tsx | 59 +- .../client/lib/forms/SchemaFormWithState.tsx | 6 +- .../lib/forms/SchemaTableSummaryRow.tsx | 4 +- .../src/client/lib/forms/schemaFormUtil.tsx | 2 +- packages/webui/src/client/lib/iconPicker.tsx | 6 +- packages/webui/src/client/lib/language.ts | 2 +- packages/webui/src/client/lib/lib.tsx | 6 +- packages/webui/src/client/lib/logging.ts | 2 +- packages/webui/src/client/lib/meteorApi.ts | 2 +- .../notifications/NotificationCenterPanel.tsx | 11 +- .../lib/notifications/ReactNotification.tsx | 7 +- .../client/lib/notifications/notifications.ts | 10 +- .../client/lib/notifications/warningIcon.tsx | 2 +- .../webui/src/client/lib/partInstanceUtil.ts | 67 + .../src/client/lib/polyfill/polyfills.ts | 1 - .../client/lib/polyfill/promise.allSettled.ts | 11 - packages/webui/src/client/lib/popperUtils.ts | 2 +- .../client/lib/reactiveData/reactiveData.ts | 10 +- .../lib/reactiveData/reactiveDataHelper.ts | 24 +- packages/webui/src/client/lib/rundown.ts | 36 +- .../webui/src/client/lib/rundownLayouts.ts | 128 +- .../src/client/lib/rundownPlaylistUtil.ts | 33 +- .../webui/src/client/lib/rundownTiming.ts | 37 +- packages/webui/src/client/lib/shelf.ts | 16 +- packages/webui/src/client/lib/systemTime.ts | 2 +- packages/webui/src/client/lib/tTimerUtils.ts | 5 +- .../lib/triggers/ActionAdLibHotkeyPreview.tsx | 12 +- .../client/lib/triggers/TriggersHandler.tsx | 28 +- .../client/lib/triggers/triggersContext.ts | 22 +- .../src/client/lib/ui/icons/freezeFrame.tsx | 2 - .../src/client/lib/ui/icons/listView.tsx | 2 - .../webui/src/client/lib/ui/icons/looping.tsx | 4 +- .../src/client/lib/ui/icons/mediaStatus.tsx | 2 +- .../src/client/lib/ui/icons/notifications.tsx | 2 +- .../webui/src/client/lib/ui/icons/segment.tsx | 2 - .../webui/src/client/lib/ui/icons/shelf.tsx | 39 + .../webui/src/client/lib/ui/icons/sorting.tsx | 2 +- .../src/client/lib/ui/icons/switchboard.tsx | 2 +- .../src/client/lib/ui/icons/useredits.tsx | 2 +- .../src/client/lib/ui/pieceUiClassNames.ts | 10 +- .../webui/src/client/lib/ui/splitPreview.ts | 25 +- .../src/client/lib/ui/splitsPreviewVideo.ts | 47 + .../src/client/lib/ui/videoPreviewScrub.ts | 16 + .../src/client/lib/uncaughtErrorHandler.ts | 2 +- .../webui/src/client/lib/utilComponents.tsx | 2 +- packages/webui/src/client/lib/viewPort.ts | 42 +- .../src/client/styles/_checkerboard.scss | 25 + .../webui/src/client/styles/_colorScheme.scss | 1 + .../webui/src/client/styles/_cssVariables.ts | 2 +- .../src/client/styles/countdown/director.scss | 107 +- .../client/styles/countdown/presenter.scss | 127 +- .../src/client/styles/counterComponents.scss | 49 +- .../src/client/styles/defaultColors.scss | 1 + packages/webui/src/client/styles/main.scss | 65 +- .../src/client/styles/notifications.scss | 3 - .../webui/src/client/styles/prompter.scss | 41 +- .../src/client/styles/propertiesPanel.scss | 5 + .../src/client/styles/rundownStatusBar.scss | 42 + .../webui/src/client/styles/rundownView.scss | 482 +- .../styles/shelf/dashboard-rundownView.scss | 158 +- .../styles/shelf/dashboard-streamdeck.scss | 60 +- .../src/client/styles/shelf/dashboard.scss | 876 +- .../client/styles/shelf/endWordsPanel.scss | 18 - .../rundownViewShelfAdlibSizeToggle.scss | 48 + .../client/styles/shelf/showStylePanel.scss | 34 - .../styles/shelf/systemStatusPanel.scss | 14 - .../src/client/styles/tTimerDisplay.scss | 80 + .../webui/src/client/ui/ActiveRundownView.tsx | 11 +- .../src/client/ui/AfterBroadcastForm.tsx | 15 +- packages/webui/src/client/ui/App.tsx | 47 +- .../ui/ClipTrimPanel/ClipTrimDialog.tsx | 16 +- .../client/ui/ClipTrimPanel/ClipTrimPanel.tsx | 6 +- .../ui/ClipTrimPanel/VideoEditMonitor.tsx | 4 +- .../client/ui/ClockView/CameraConfigForm.tsx | 4 +- .../CameraScreen/OrderedPartsProvider.tsx | 4 +- .../client/ui/ClockView/CameraScreen/Part.tsx | 6 +- .../ui/ClockView/CameraScreen/Piece.tsx | 10 +- .../ui/ClockView/CameraScreen/Rundown.tsx | 8 +- .../ui/ClockView/CameraScreen/Segment.tsx | 4 +- .../ui/ClockView/CameraScreen/index.tsx | 36 +- .../src/client/ui/ClockView/ClockView.tsx | 2 +- .../client/ui/ClockView/ClockViewIndex.tsx | 2 +- .../ClockViewFreezeCount.tsx | 4 +- .../ClockViewPieceIcon.tsx | 14 +- .../ClockViewPieceName.tsx | 12 +- .../ClockViewRenderers/RemoteInputIcon.tsx | 2 - .../ClockViewRenderers/SplitInputIcon.tsx | 6 +- .../ui/ClockView/ClockViewPieceIcons/utils.ts | 12 +- .../DirectorScreen/DirectorScreen.tsx | 57 +- .../DirectorScreen/DirectorScreenTop.tsx | 117 +- .../client/ui/ClockView/FullscreenLink.tsx | 2 +- .../client/ui/ClockView/MultiviewScreen.tsx | 2 +- .../src/client/ui/ClockView/OverlayScreen.tsx | 4 +- .../ui/ClockView/OverlayScreenSaver.tsx | 4 +- .../ui/ClockView/PresenterConfigForm.tsx | 2 +- .../client/ui/ClockView/PresenterScreen.tsx | 65 +- .../ui/ClockView/PrompterConfigForm.tsx | 2 +- .../client/ui/ClockView/RundownStatusBar.tsx | 35 + .../src/client/ui/ClockView/TTimerDisplay.tsx | 52 +- .../webui/src/client/ui/FloatingInspector.tsx | 1 - .../src/client/ui/MediaStatus/MediaStatus.tsx | 20 +- .../ui/MediaStatus/MediaStatusIndicator.tsx | 2 +- .../client/ui/MediaStatus/SortOrderButton.tsx | 2 +- .../client/ui/PieceIcons/PieceCountdown.tsx | 4 +- .../src/client/ui/PieceIcons/PieceIcon.tsx | 14 +- .../src/client/ui/PieceIcons/PieceName.tsx | 10 +- .../PieceIcons/Renderers/RemoteInputIcon.tsx | 2 - .../PieceIcons/Renderers/SplitInputIcon.tsx | 6 +- .../webui/src/client/ui/PieceIcons/utils.ts | 12 +- .../client/ui/PreviewPopUp/PreviewPopUp.scss | 29 +- .../client/ui/PreviewPopUp/PreviewPopUp.tsx | 91 +- .../ui/PreviewPopUp/PreviewPopUpContent.tsx | 12 +- .../ui/PreviewPopUp/PreviewPopUpContext.tsx | 196 +- .../Previews/BoxLayoutPreview.tsx | 64 +- .../PreviewPopUp/Previews/IFramePreview.tsx | 30 +- .../Previews/LayerInfoPreview.tsx | 2 +- .../ui/PreviewPopUp/Previews/VTPreview.tsx | 18 +- .../FloatingInspectorTimeInformationRow.tsx | 8 +- .../ui/Prompter/Formatted/MdDisplay.tsx | 2 +- .../mdParser/__tests__/mdParser.test.ts | 4 +- .../Formatted/mdParser/constructs/colour.ts | 4 +- .../mdParser/constructs/emphasisAndStrong.ts | 4 +- .../Formatted/mdParser/constructs/escape.ts | 2 +- .../mdParser/constructs/paragraph.ts | 4 +- .../Formatted/mdParser/constructs/reverse.ts | 4 +- .../mdParser/constructs/screenMarker.ts | 2 +- .../mdParser/constructs/underlineOrHide.ts | 4 +- .../ui/Prompter/Formatted/mdParser/index.ts | 4 +- .../Formatted/mdParser/parserState.ts | 2 +- .../src/client/ui/Prompter/OverUnderTimer.tsx | 32 - .../src/client/ui/Prompter/PrompterView.tsx | 49 +- .../customizable-shuttle-webhid-device.ts | 2 +- .../ui/Prompter/controller/joycon-device.ts | 2 +- .../ui/Prompter/controller/keyboard-device.ts | 2 +- .../client/ui/Prompter/controller/manager.ts | 4 +- .../Prompter/controller/midi-pedal-device.ts | 4 +- .../Prompter/controller/mouse-ish-device.ts | 2 +- .../controller/shuttle-keyboard-device.ts | 2 +- .../controller/shuttle-webhid-device.ts | 4 +- .../controller/xbox-controller-device.ts | 2 +- .../webui/src/client/ui/Prompter/prompter.ts | 16 +- packages/webui/src/client/ui/RundownList.tsx | 2 +- .../ui/RundownList/ActiveProgressBar.tsx | 2 +- .../CreateAdlibTestingRundownPanel.tsx | 4 +- .../ui/RundownList/DisplayFormattedTime.tsx | 2 +- .../RundownList/DisplayFormattedTimeInner.ts | 2 +- .../client/ui/RundownList/DragAndDropTypes.ts | 4 +- .../client/ui/RundownList/RegisterHelp.tsx | 2 +- .../client/ui/RundownList/RundownDropZone.tsx | 2 +- .../ui/RundownList/RundownListFooter.tsx | 4 +- .../client/ui/RundownList/RundownListItem.tsx | 12 +- .../RundownList/RundownListItemProblems.tsx | 2 +- .../ui/RundownList/RundownListItemView.tsx | 4 +- .../RundownList/RundownPlaylistDragLayer.tsx | 6 +- .../ui/RundownList/RundownPlaylistUi.tsx | 10 +- .../RundownViewLayoutSelection.tsx | 8 +- .../__tests__/DisplayFormattedTime.test.ts | 6 +- .../webui/src/client/ui/RundownList/icons.tsx | 2 - .../webui/src/client/ui/RundownList/util.ts | 8 +- packages/webui/src/client/ui/RundownView.tsx | 160 +- .../ui/RundownView/CasparCGRestartButtons.tsx | 4 +- .../src/client/ui/RundownView/DataMissing.tsx | 6 +- .../src/client/ui/RundownView/DragContext.ts | 4 +- .../ui/RundownView/DragContextProvider.tsx | 17 +- .../MediaStatusPopUpHeader.tsx | 2 +- .../MediaStatusPopUp/MediaStatusPopUpItem.tsx | 6 +- .../MediaStatusPopUpSegmentRule.tsx | 2 +- .../ui/RundownView/MediaStatusPopUp/index.tsx | 8 +- .../src/client/ui/RundownView/PopUpPanel.tsx | 3 +- .../ui/RundownView/RundownDetachedShelf.tsx | 12 +- .../ui/RundownView/RundownDividerHeader.tsx | 4 +- .../RundownView/RundownHeader/Countdown.scss | 2 + .../RundownView/RundownHeader/Countdown.tsx | 3 +- .../CurrentPartOrSegmentRemaining.tsx | 14 +- .../RundownHeader/HeaderFreezeFrameIcon.tsx | 9 +- .../RundownHeader/RundownContextMenu.tsx | 30 +- .../RundownHeader/RundownHeader.scss | 225 +- .../RundownHeader/RundownHeader.tsx | 79 +- .../RundownHeader/RundownHeaderDurations.tsx | 30 +- .../RundownHeaderExpectedEnd.tsx | 23 +- .../RundownHeaderPlannedStart.tsx | 25 +- .../RundownHeader/RundownHeaderTimers.tsx | 29 +- .../RundownHeaderTimingDisplay.tsx | 18 +- .../RundownHeader/RundownReloadResponse.ts | 8 +- .../ui/RundownView/RundownHeader/_shared.scss | 2 +- .../useRundownPlaylistOperations.tsx | 16 +- .../client/ui/RundownView/RundownNotifier.tsx | 25 +- .../ui/RundownView/RundownRightHandButton.tsx | 3 +- .../RundownView/RundownRightHandControls.tsx | 10 +- .../ui/RundownView/RundownSorensenContext.tsx | 8 +- .../ui/RundownView/RundownSystemStatus.tsx | 6 +- .../RundownTiming/CurrentPartElapsed.tsx | 2 +- .../RundownTiming/NextBreakTiming.tsx | 2 +- .../RundownTiming/PartCountdown.tsx | 6 +- .../RundownTiming/PartDuration.tsx | 4 +- .../RundownTiming/PlaylistEndTiming.tsx | 2 +- .../RundownTiming/PlaylistStartTiming.tsx | 2 +- .../RundownView/RundownTiming/RundownName.tsx | 4 +- .../RundownTiming/RundownTiming.ts | 4 +- .../RundownTiming/RundownTimingProvider.tsx | 22 +- .../RundownTiming/SegmentDuration.tsx | 6 +- .../RundownTiming/SegmentTimeAnchorTime.tsx | 2 +- .../RundownView/RundownTiming/withTiming.tsx | 2 +- .../RundownViewContextProviders.tsx | 7 +- .../ui/RundownView/RundownViewShelf.tsx | 58 +- .../RundownViewShelfAdlibSizeToggle.tsx | 45 + .../RundownView/RundownViewSubscriptions.ts | 6 +- .../RundownView/SelectedElementsContext.tsx | 10 +- .../client/ui/RundownView/StudioContext.tsx | 2 +- .../ui/RundownView/SwitchboardPopUp.tsx | 6 +- .../client/ui/RundownView/WarningDisplay.tsx | 2 +- .../selectedElementsContext.test.tsx | 3 +- .../ui/RundownView/useQueueMiniShelfAdlib.ts | 20 +- .../SegmentAdlibTesting.tsx | 72 +- .../SegmentAdlibTestingContainer.tsx | 8 +- .../ui/SegmentContainer/PieceElement.tsx | 6 +- .../PieceMultistepChevron.tsx | 4 +- .../ui/SegmentContainer/getSplitItems.tsx | 19 +- .../SegmentContainer/withResolvedSegment.ts | 32 +- .../ui/SegmentHeader/SegmentHeaderNotes.tsx | 10 +- .../src/client/ui/SegmentList/LinePart.tsx | 20 +- .../LinePartMainPiece/LinePartMainPiece.tsx | 32 +- .../LinePartAdLibIndicator.tsx | 22 +- .../LinePartIndicator.tsx | 17 +- .../LinePartPieceIndicator.tsx | 8 +- .../LinePartScriptPiece.tsx | 22 +- .../PieceIndicatorMenu.tsx | 12 +- .../SegmentList/LinePartPieceIndicators.tsx | 7 +- .../LinePartSecondaryPiece.tsx | 22 +- .../ui/SegmentList/LinePartTimeline.tsx | 15 +- .../client/ui/SegmentList/LinePartTitle.tsx | 2 +- .../LinePartTransitionPiece.tsx | 3 +- .../src/client/ui/SegmentList/OnAirLine.tsx | 4 +- .../client/ui/SegmentList/OvertimeShadow.tsx | 2 +- .../ui/SegmentList/PartAutoNextMarker.tsx | 2 +- .../client/ui/SegmentList/QuickLoopEnd.tsx | 2 +- .../src/client/ui/SegmentList/SegmentList.tsx | 34 +- .../ui/SegmentList/SegmentListContainer.tsx | 6 +- .../ui/SegmentList/SegmentListHeader.tsx | 8 +- .../src/client/ui/SegmentList/TakeLine.tsx | 1 - .../ui/SegmentStoryboard/SegmentScrollbar.tsx | 2 +- .../SegmentStoryboard/SegmentStoryboard.scss | 33 +- .../SegmentStoryboard/SegmentStoryboard.tsx | 75 +- .../SegmentStoryboardContainer.tsx | 10 +- .../ui/SegmentStoryboard/StoryboardPart.tsx | 32 +- .../Renderers/DefaultRenderer.tsx | 8 +- .../Renderers/GraphicsRenderer.tsx | 2 +- .../Renderers/ScriptRenderer.tsx | 2 +- .../Renderers/SplitsRenderer.tsx | 10 +- .../Renderers/VTRenderer.tsx | 2 +- .../StoryboardPartSecondaryPieces.tsx | 6 +- .../StoryboardSecondaryPiece.tsx | 33 +- .../StoryboardSourceLayer.tsx | 12 +- .../StoryboardSourceLayerItem.tsx | 6 +- .../Renderers/CameraThumbnailRenderer.tsx | 2 +- .../Renderers/DefaultThumbnailRenderer.tsx | 2 +- .../Renderers/GraphicsThumbnailRenderer.tsx | 2 +- .../Renderers/LocalThumbnailRenderer.tsx | 4 +- .../Renderers/SplitsThumbnailRenderer.tsx | 17 +- .../Renderers/ThumbnailRendererFactory.tsx | 10 +- .../Renderers/VTThumbnailRenderer.tsx | 10 +- .../StoryboardPartThumbnail.scss | 70 +- .../StoryboardPartThumbnail.tsx | 4 +- .../StoryboardPartThumbnailInner.tsx | 79 +- .../StoryboardPartTransitions.tsx | 6 +- .../Parts/FlattenedSourceLayers.tsx | 6 +- .../Parts/InvalidPartCover.tsx | 39 +- .../ui/SegmentTimeline/Parts/OutputGroup.tsx | 15 +- .../Parts/SegmentTimelinePart.tsx | 84 +- .../ui/SegmentTimeline/Parts/SourceLayer.tsx | 16 +- .../Renderers/CustomLayerItemRenderer.tsx | 75 +- .../Renderers/DefaultLayerItemRenderer.tsx | 8 +- .../Renderers/L3rdSourceRenderer.tsx | 3 +- .../Renderers/LocalLayerItemRenderer.tsx | 5 +- .../Renderers/MicSourceRenderer.tsx | 59 +- .../Renderers/SplitsSourceRenderer.tsx | 7 +- .../Renderers/TransitionSourceRenderer.tsx | 4 +- .../Renderers/VTSourceRenderer.tsx | 176 +- .../ui/SegmentTimeline/SegmentContextMenu.tsx | 20 +- .../ui/SegmentTimeline/SegmentTimeline.tsx | 108 +- .../SegmentTimelineContainer.tsx | 58 +- .../SegmentTimelineZoomButtons.tsx | 1 - .../SegmentTimelineZoomControls.tsx | 38 +- .../SegmentTimelinePartHoverPreview.tsx | 8 +- .../SegmentTimelineSmallPartFlag.tsx | 12 +- .../SegmentTimelineSmallPartFlagIcon.tsx | 5 +- .../ui/SegmentTimeline/SourceLayerItem.tsx | 74 +- .../SourceLayerItemContainer.tsx | 6 +- .../ui/SegmentTimeline/TimelineGrid.tsx | 9 +- .../SegmentTimeline/withMediaObjectStatus.tsx | 18 +- .../BlueprintConfigSchema/CategoryEntry.tsx | 8 +- .../Settings/BlueprintConfigSchema/index.tsx | 18 +- .../client/ui/Settings/BlueprintSettings.tsx | 6 +- .../Settings/DevicePackageManagerSettings.tsx | 4 +- .../src/client/ui/Settings/DeviceSettings.tsx | 6 +- .../src/client/ui/Settings/Migration.tsx | 4 +- .../ui/Settings/RundownLayoutEditor.tsx | 25 +- .../src/client/ui/Settings/SettingsMenu.tsx | 16 +- .../Settings/ShowStyle/AbChannelDisplay.tsx | 8 +- .../SelectBlueprint.tsx | 4 +- .../SelectConfigPreset.tsx | 4 +- .../BlueprintConfiguration/index.tsx | 10 +- .../client/ui/Settings/ShowStyle/Generic.tsx | 4 +- .../ui/Settings/ShowStyle/HotkeyLegend.tsx | 5 +- .../ui/Settings/ShowStyle/OutputLayer.tsx | 13 +- .../ui/Settings/ShowStyle/SourceLayer.tsx | 13 +- .../ui/Settings/ShowStyle/VariantListItem.tsx | 16 +- .../ui/Settings/ShowStyle/VariantSettings.tsx | 16 +- .../ui/Settings/ShowStyleBaseSettings.tsx | 4 +- .../src/client/ui/Settings/SnapshotsView.tsx | 7 +- .../client/ui/Settings/Studio/Baseline.tsx | 2 +- .../SelectBlueprint.tsx | 4 +- .../SelectConfigPreset.tsx | 6 +- .../Studio/BlueprintConfiguration/index.tsx | 6 +- .../Studio/Devices/GenericSubDevices.tsx | 12 +- .../Studio/Devices/IngestSubDevices.tsx | 15 +- .../Studio/Devices/InputSubDevices.tsx | 15 +- .../Settings/Studio/Devices/ParentDevices.tsx | 24 +- .../Studio/Devices/PlayoutSubDevices.tsx | 15 +- .../ui/Settings/Studio/Devices/index.tsx | 2 +- .../src/client/ui/Settings/Studio/Generic.tsx | 38 +- .../client/ui/Settings/Studio/Mappings.tsx | 18 +- .../Studio/PackageManager/AccessorTable.tsx | 4 +- .../PackageManager/AccessorTableRow.tsx | 4 +- .../PackageContainerPickers.tsx | 14 +- .../PackageManager/PackageContainers.tsx | 14 +- .../Settings/Studio/PackageManager/index.tsx | 2 +- .../Studio/Routings/ExclusivityGroups.tsx | 14 +- .../Studio/Routings/RouteSetAbPlayers.tsx | 4 +- .../ui/Settings/Studio/Routings/RouteSets.tsx | 34 +- .../ui/Settings/Studio/Routings/index.tsx | 4 +- .../src/client/ui/Settings/StudioSettings.tsx | 8 +- .../client/ui/Settings/SystemManagement.tsx | 9 +- .../Settings/SystemManagement/Blueprint.tsx | 6 +- .../ui/Settings/Upgrades/Components.tsx | 5 +- .../src/client/ui/Settings/Upgrades/View.tsx | 2 +- .../components/ConfigManifestOAuthFlow.tsx | 299 +- .../ui/Settings/components/FilterEditor.tsx | 84 +- .../GenericDeviceSettingsComponent.tsx | 2 +- .../RundownHeaderLayoutSettings.tsx | 79 - .../RundownViewLayoutSettings.tsx | 17 +- .../rundownLayouts/ShelfLayoutSettings.tsx | 2 +- .../triggeredActions/TriggeredActionEntry.tsx | 20 +- .../TriggeredActionsEditor.tsx | 17 +- .../actionEditors/ActionEditor.tsx | 16 +- .../actionSelector/ActionSelector.tsx | 8 +- .../actionEditors/AdLibActionEditor.tsx | 4 +- .../actionEditors/SwitchRouteSetEditor.tsx | 6 +- .../filterPreviews/AdLibFilter.tsx | 15 +- .../filterPreviews/FilterEditor.tsx | 6 +- .../filterPreviews/RundownPlaylistFilter.tsx | 8 +- .../filterPreviews/SwitchFilterType.tsx | 2 +- .../filterPreviews/ViewFilter.tsx | 4 +- .../triggerEditors/DeviceEditor.tsx | 4 +- .../triggerEditors/HotkeyEditor.tsx | 6 +- .../triggerEditors/HotkeyTrigger.tsx | 2 +- .../triggerEditors/TriggerEditor.tsx | 8 +- .../useDebugStatesForPlayoutDevice.tsx | 6 +- .../ui/Settings/util/OverrideOpHelper.tsx | 10 +- .../src/client/ui/Shelf/AdLibListItem.tsx | 6 +- .../src/client/ui/Shelf/AdLibListView.tsx | 16 +- .../webui/src/client/ui/Shelf/AdLibPanel.tsx | 44 +- .../src/client/ui/Shelf/AdLibPanelToolbar.tsx | 1 - .../src/client/ui/Shelf/AdLibRegionPanel.tsx | 24 +- .../webui/src/client/ui/Shelf/BucketPanel.tsx | 67 +- .../src/client/ui/Shelf/BucketPieceButton.tsx | 72 +- .../src/client/ui/Shelf/ColoredBoxPanel.tsx | 4 +- .../client/ui/Shelf/DashboardActionButton.tsx | 6 +- .../ui/Shelf/DashboardActionButtonGroup.tsx | 14 +- .../src/client/ui/Shelf/DashboardPanel.tsx | 19 +- .../client/ui/Shelf/DashboardPieceButton.tsx | 477 - .../DashboardPieceButton.tsx | 271 + .../subcomponents/DashboardButtonSubLabel.tsx | 13 + .../DashboardButtonTagStrip.scss | 131 + .../subcomponents/DashboardButtonTagStrip.tsx | 11 + .../DashboardButtonThumbnail.tsx | 7 + .../subcomponents/EditableLabel.tsx | 49 + .../subcomponents/HotkeyBadge.tsx | 4 + .../subcomponents/MediaBox.tsx | 88 + .../SplitButtonLayerBackground.scss | 24 + .../SplitButtonLayerBackground.tsx | 60 + .../ui/Shelf/DashboardPieceButton/types.ts | 35 + .../useDashboardButtonInteractions.ts | 208 + .../usePreviewPopUpSession.ts | 117 + .../DashboardPieceButtonSplitPreview.tsx | 19 +- .../src/client/ui/Shelf/EndWordsPanel.tsx | 103 - .../client/ui/Shelf/ExternalFramePanel.tsx | 35 +- .../src/client/ui/Shelf/GlobalAdLibPanel.tsx | 16 +- .../src/client/ui/Shelf/HotkeyHelpPanel.tsx | 10 +- .../ItemRenderers/ActionItemRenderer.tsx | 26 +- .../ItemRenderers/DefaultItemRenderer.tsx | 16 +- .../ItemRenderers/InspectorTitle.tsx | 12 +- .../ItemRenderers/ItemRendererFactory.ts | 18 +- .../ItemRenderers/NoraItemEditor.tsx | 25 +- .../ItemRenderers/NoraItemRenderer.tsx | 12 +- .../ui/Shelf/Inspector/ShelfInspector.tsx | 12 +- .../src/client/ui/Shelf/MiniRundownPanel.tsx | 12 +- .../src/client/ui/Shelf/NextInfoPanel.tsx | 8 +- .../src/client/ui/Shelf/PartNamePanel.tsx | 8 +- .../src/client/ui/Shelf/PartTimingPanel.tsx | 11 +- .../client/ui/Shelf/PieceCountdownPanel.tsx | 17 +- .../client/ui/Shelf/PlaylistEndTimerPanel.tsx | 4 +- .../src/client/ui/Shelf/PlaylistNamePanel.tsx | 6 +- .../ui/Shelf/PlaylistStartTimerPanel.tsx | 4 +- .../Renderers/DefaultListItemRenderer.tsx | 3 +- .../ui/Shelf/Renderers/ItemRendererFactory.ts | 14 +- .../Shelf/Renderers/L3rdListItemRenderer.tsx | 33 +- .../ui/Shelf/Renderers/VTListItemRenderer.tsx | 25 +- .../client/ui/Shelf/RundownViewBuckets.tsx | 52 +- .../src/client/ui/Shelf/SegmentNamePanel.tsx | 8 +- .../client/ui/Shelf/SegmentTimingPanel.tsx | 19 +- packages/webui/src/client/ui/Shelf/Shelf.tsx | 50 +- .../src/client/ui/Shelf/ShelfContextMenu.tsx | 11 +- .../client/ui/Shelf/ShelfDashboardLayout.tsx | 55 +- .../client/ui/Shelf/ShelfRundownLayout.tsx | 16 +- .../src/client/ui/Shelf/ShowStylePanel.tsx | 46 - .../src/client/ui/Shelf/StudioNamePanel.tsx | 4 +- .../src/client/ui/Shelf/SystemStatusPanel.tsx | 82 - .../src/client/ui/Shelf/TextLabelPanel.tsx | 4 +- .../src/client/ui/Shelf/TimeOfDayPanel.tsx | 4 +- .../ui/Shelf/TimelineDashboardPanel.tsx | 19 +- .../src/client/ui/Status/Evaluations.tsx | 4 +- .../src/client/ui/Status/ExternalMessages.tsx | 2 +- .../src/client/ui/Status/StatusCodePill.tsx | 9 +- .../ui/Status/SystemStatus/CoreItem.tsx | 4 +- .../ui/Status/SystemStatus/DeviceItem.tsx | 9 +- .../ui/Status/SystemStatus/SystemStatus.tsx | 6 +- .../src/client/ui/Status/UserActivity.tsx | 4 +- .../media-status/MediaStatusListHeader.tsx | 2 +- .../media-status/MediaStatusListItem.tsx | 4 +- .../client/ui/Status/media-status/index.tsx | 8 +- .../Status/package-status/JobStatusIcon.tsx | 4 +- .../package-status/PackageContainerStatus.tsx | 7 +- .../Status/package-status/PackageStatus.tsx | 6 +- .../package-status/PackageWorkStatus.tsx | 4 +- .../client/ui/Status/package-status/index.tsx | 10 +- .../StudioScreenSaver/StudioScreenSaver.tsx | 8 +- .../client/ui/TestTools/DeviceTriggers.tsx | 11 +- .../ui/TestTools/IngestRundownStatus.tsx | 4 +- .../src/client/ui/TestTools/Mappings.tsx | 2 +- .../src/client/ui/TestTools/Timeline.tsx | 16 +- .../ui/UserEditOperations/PropertiesPanel.tsx | 25 +- .../RenderUserEditOperations.tsx | 9 +- .../__tests__/PropertiesPanel.test.tsx | 13 +- .../webui/src/client/ui/UserPermissions.tsx | 2 +- packages/webui/src/client/ui/i18n.ts | 29 +- .../src/client/ui/util/AdjustLabelFit.tsx | 4 +- .../useRundownAndShowStyleIdsForPlaylist.ts | 4 +- .../src/client/ui/util/useSetDocumentClass.ts | 35 +- .../client/ui/util/useToggleExpandHelper.tsx | 2 +- packages/webui/tsconfig.app.json | 3 +- packages/webui/tsconfig.jest.json | 3 +- packages/yarn.lock | 3144 +++---- scripts/reformat.mjs | 331 - yarn.lock | 266 +- 1005 files changed, 51116 insertions(+), 19467 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/deprecation_rfc.yml mode change 100644 => 100755 .husky/pre-commit create mode 100644 meteor/.yarn/patches/@meteorjs-rspack-npm-2.0.1-d001eb481c.patch create mode 100644 meteor/i18n/.gitignore rename meteor/i18n/{template.pot => en.po} (64%) delete mode 100644 meteor/i18n/nb.mo delete mode 100644 meteor/i18n/nn.mo create mode 100644 meteor/i18n/sv.po create mode 100644 meteor/rspack.config.cjs create mode 100644 meteor/scripts/extract-i18next-po.mjs delete mode 100644 meteor/scripts/extract-i18next-pot.mjs create mode 100644 meteor/scripts/translation/bundle.mjs create mode 100644 meteor/scripts/translation/config.mjs create mode 100644 meteor/scripts/translation/extract.mjs create mode 100644 meteor/server/api/__tests__/peripheralDevice.resolveActionResult.test.ts create mode 100644 meteor/server/api/deviceTriggers/__tests__/StudioObserver.test.ts create mode 100644 meteor/server/api/rest/v1/__tests__/playlists.spec.ts create mode 100644 meteor/server/publications/externalEventSubscriptions.ts create mode 100644 packages/blueprints-integration/src/context/externalEventContext.ts create mode 100644 packages/blueprints-integration/src/context/playoutActionContext.ts create mode 100644 packages/blueprints-integration/src/context/snapshotContext.ts create mode 100644 packages/blueprints-integration/src/externalEvent.ts create mode 100644 packages/corelib/src/StatusMessageResolver.ts create mode 100644 packages/corelib/src/__tests__/StatusMessageResolver.test.ts rename packages/corelib/src/dataModel/{ => RundownPlaylist}/RundownPlaylist.ts (60%) create mode 100644 packages/corelib/src/dataModel/RundownPlaylist/TTimers.ts create mode 100644 packages/corelib/src/timecode.ts delete mode 100644 packages/corelib/src/typings/Timecode.d.ts create mode 100644 packages/documentation/docs/for-developers/for-blueprint-developers/error-message-customization.md create mode 100644 packages/documentation/docs/for-developers/for-blueprint-developers/snapshot-hooks.md create mode 100644 packages/documentation/docs/for-developers/for-blueprint-developers/splits-box-previews.md rename packages/documentation/src/components/{HomepageFeatures.js => HomepageFeatures.jsx} (100%) create mode 100644 packages/documentation/src/components/HomepagePRs.css create mode 100644 packages/documentation/src/components/HomepagePRs.jsx create mode 100644 packages/documentation/src/components/LatestVersionNumber.jsx create mode 100644 packages/job-worker/src/blueprints/context/PlaylistSnapshotCreatedContext.ts create mode 100644 packages/job-worker/src/blueprints/context/SystemSnapshotCreatedContext.ts create mode 100644 packages/job-worker/src/ingest/__tests__/showShelfCompat.test.ts create mode 100644 packages/job-worker/src/playout/__tests__/snapshotHooks.test.ts create mode 100644 packages/job-worker/src/playout/externalEvents.ts create mode 100644 packages/job-worker/src/playout/snapshotHooks.ts create mode 100644 packages/live-status-gateway-api/api/components/layers/outputLayer/outputLayer-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/layers/outputLayer/outputLayer.yaml create mode 100644 packages/live-status-gateway-api/api/components/layers/sourceLayer/sourceLayer-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/layers/sourceLayer/sourceLayer.yaml create mode 100644 packages/live-status-gateway-api/api/components/part/partInvalidReason/partInvalidReason-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/part/partInvalidReason/partInvalidReason.yaml create mode 100644 packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart.yaml create mode 100644 packages/live-status-gateway-api/api/components/piece/resolvedPiece/resolvedPiece-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/piece/resolvedPiece/resolvedPiece.yaml create mode 100644 packages/live-status-gateway-api/api/components/resolvedPlaylist/messages/resolvedPlaylistMessage.yaml create mode 100644 packages/live-status-gateway-api/api/components/resolvedPlaylist/resolvedPlaylistEvent/resolvedPlaylistEvent-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/resolvedPlaylist/resolvedPlaylistEvent/resolvedPlaylistEvent.yaml create mode 100644 packages/live-status-gateway-api/api/components/rundown/resolvedRundown/resolvedRundown-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/rundown/resolvedRundown/resolvedRundown.yaml create mode 100644 packages/live-status-gateway-api/api/components/segment/resolvedSegment/resolvedSegment-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/segment/resolvedSegment/resolvedSegment.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-countdownRunning-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-freeRunRunning-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-unconfigured-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-array-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-configured-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-unconfigured-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerMode.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerMode/countdown/timerModeCountdown-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerMode/countdown/timerModeCountdown.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerMode/freeRun/timerModeFreeRun-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerMode/freeRun/timerModeFreeRun.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerMode/timeOfDay/timerModeTimeOfDay-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerMode/timeOfDay/timerModeTimeOfDay.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerModeCountdown-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerModeFreeRun-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerModeTimeOfDay-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerState.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerState/paused/timerStatePaused-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerState/paused/timerStatePaused.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerState/running/timerStateRunning-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerState/running/timerStateRunning.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerStatePaused-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/timerStateRunning-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/timing/resolvedPartTiming/resolvedPartTiming-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/timing/resolvedPartTiming/resolvedPartTiming.yaml create mode 100644 packages/live-status-gateway-api/api/components/timing/resolvedPieceTiming/resolvedPieceTiming-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/timing/resolvedPieceTiming/resolvedPieceTiming.yaml create mode 100644 packages/live-status-gateway-api/api/components/timing/resolvedPlaylistTiming/resolvedPlaylistTiming-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/timing/resolvedPlaylistTiming/resolvedPlaylistTiming.yaml create mode 100644 packages/live-status-gateway-api/api/components/timing/resolvedSegmentTiming/resolvedSegmentTiming-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/timing/resolvedSegmentTiming/resolvedSegmentTiming.yaml create mode 100644 packages/live-status-gateway-api/api/topics/resolvedPlaylist/resolvedPlaylistTopic.yaml create mode 100644 packages/live-status-gateway/src/collections/partInstancesInPlaylistHandler.ts create mode 100644 packages/live-status-gateway/src/collections/pieceInstancesInPlaylistHandler.ts create mode 100644 packages/live-status-gateway/src/collections/piecesInPlaylistHandler.ts create mode 100644 packages/live-status-gateway/src/collections/showStyleBasesHandler.ts delete mode 100644 packages/live-status-gateway/src/process.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/quickLoop/toQuickLoopStatus.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timers/__tests__/toTTimers.spec.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timers/toTTimers.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timing/toActivePlaylistTiming.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timing/translatePlaylistTimingType.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/conversionContext.spec.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/resolvedPlaylistConversionTestUtils.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/serializeResolvedSegment.spec.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPartStatus.spec.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPieceStatus.spec.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPlaylistStatus.spec.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedRundownStatus.spec.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedSegmentStatus.spec.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/context/conversionContext.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/events/toResolvedPlaylistStatus.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/parts/toResolvedPartStatus.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/pieces/toResolvedPieceStatus.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/rundowns/toResolvedRundownStatus.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/segments/serializeResolvedSegment.ts create mode 100644 packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/segments/toResolvedSegmentStatus.ts create mode 100644 packages/live-status-gateway/src/topics/resolvedPlaylistTopic.ts create mode 100644 packages/live-status-gateway/src/wsMetrics.ts create mode 100644 packages/mos-gateway/src/mosMetrics.ts create mode 100644 packages/openapi/scripts/bundle-openapi.mjs create mode 100644 packages/openapi/src/generated/openapi.yaml create mode 100644 packages/playout-gateway/src/playoutMetrics.ts rename packages/server-core-integration/{jest.config.js => jest.config.cjs} (65%) rename packages/server-core-integration/src/__mocks__/{faye-websocket.ts => ws.ts} (68%) create mode 100644 packages/server-core-integration/src/lib/prometheus.ts delete mode 100644 packages/server-core-integration/src/types/faye-websocket.d.ts rename packages/shared-lib/{jest.config.js => jest.config.cjs} (91%) create mode 100644 packages/shared-lib/src/package-manager/__tests__/splitBoxMedia.spec.ts create mode 100644 packages/shared-lib/src/package-manager/splitBoxMedia.ts create mode 100644 packages/shared-lib/src/peripheralDevice/externalEvents.ts create mode 100644 packages/shared-lib/src/systemErrorMessages.ts create mode 100644 packages/webui/src/client/lib/Components/Button.tsx create mode 100644 packages/webui/src/client/lib/Components/OverUnderChip.scss create mode 100644 packages/webui/src/client/lib/Components/OverUnderChip.tsx create mode 100644 packages/webui/src/client/lib/Components/TimeMsInput.tsx create mode 100644 packages/webui/src/client/lib/forms/SchemaFormOneOfButtons/OneOfButtons.scss create mode 100644 packages/webui/src/client/lib/forms/SchemaFormOneOfButtons/OneOfButtons.tsx create mode 100644 packages/webui/src/client/lib/partInstanceUtil.ts delete mode 100644 packages/webui/src/client/lib/polyfill/promise.allSettled.ts create mode 100644 packages/webui/src/client/lib/ui/splitsPreviewVideo.ts create mode 100644 packages/webui/src/client/lib/ui/videoPreviewScrub.ts create mode 100644 packages/webui/src/client/styles/_checkerboard.scss create mode 100644 packages/webui/src/client/styles/rundownStatusBar.scss delete mode 100644 packages/webui/src/client/styles/shelf/endWordsPanel.scss create mode 100644 packages/webui/src/client/styles/shelf/rundownViewShelfAdlibSizeToggle.scss delete mode 100644 packages/webui/src/client/styles/shelf/showStylePanel.scss delete mode 100644 packages/webui/src/client/styles/shelf/systemStatusPanel.scss create mode 100644 packages/webui/src/client/styles/tTimerDisplay.scss create mode 100644 packages/webui/src/client/ui/ClockView/RundownStatusBar.tsx delete mode 100644 packages/webui/src/client/ui/Prompter/OverUnderTimer.tsx create mode 100644 packages/webui/src/client/ui/RundownView/RundownViewShelfAdlibSizeToggle.tsx delete mode 100644 packages/webui/src/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx delete mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton.tsx create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/DashboardPieceButton.tsx create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/subcomponents/DashboardButtonSubLabel.tsx create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/subcomponents/DashboardButtonTagStrip.scss create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/subcomponents/DashboardButtonTagStrip.tsx create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/subcomponents/DashboardButtonThumbnail.tsx create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/subcomponents/EditableLabel.tsx create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/subcomponents/HotkeyBadge.tsx create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/subcomponents/MediaBox.tsx create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/subcomponents/SplitButtonLayerBackground.scss create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/subcomponents/SplitButtonLayerBackground.tsx create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/types.ts create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/useDashboardButtonInteractions.ts create mode 100644 packages/webui/src/client/ui/Shelf/DashboardPieceButton/usePreviewPopUpSession.ts delete mode 100644 packages/webui/src/client/ui/Shelf/EndWordsPanel.tsx delete mode 100644 packages/webui/src/client/ui/Shelf/ShowStylePanel.tsx delete mode 100644 packages/webui/src/client/ui/Shelf/SystemStatusPanel.tsx delete mode 100644 scripts/reformat.mjs diff --git a/.github/ISSUE_TEMPLATE/deprecation_rfc.yml b/.github/ISSUE_TEMPLATE/deprecation_rfc.yml new file mode 100644 index 00000000000..651c6d8deca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/deprecation_rfc.yml @@ -0,0 +1,46 @@ +name: Deprecation RFC ❗ +description: Use this to get approval to remove existing features. +title: "Deprecation RFC: [Short description of the feature/change]" +labels: + - Deprecation RFC + +body: + - type: markdown + attributes: + value: | + Before you post, be sure to read our Contribution guidelines: + https://sofie-automation.github.io/sofie-core/docs/for-developers/contribution-guidelines + + - type: textarea + attributes: + label: Suggested by + description: Tell us who / which organization you are representing, and how the Sofie team will be able to contact you. + placeholder: Example "This RFC is posted on behalf of the NRK." + validations: + required: true + + - type: textarea + attributes: + label: Use Case + description: "Please write some background information here, such as: What is your use case? What problem are you trying to solve?" + validations: + required: true + + - type: textarea + attributes: + label: Proposal + description: Please describe your proposal here + validations: + required: true + + - type: textarea + attributes: + label: Process + description: Please add the deadline one month from today, in bold. Leave the rest as is. + value: | + The Sofie Team will evaluate this Deprecation RFC following their approval workflow. + + Stakeholders have until **YYYY-MM-DD** to comment or challenge the proposal + + - [ ] Outcome: A unison approval from stakeholders has been reached. + - [ ] Outcome: No objections raised within the 1 month limit, so the proposal is implicitly approved. diff --git a/.github/actions/setup-meteor/action.yaml b/.github/actions/setup-meteor/action.yaml index cabb63ed3b4..c4e0f5c8971 100644 --- a/.github/actions/setup-meteor/action.yaml +++ b/.github/actions/setup-meteor/action.yaml @@ -3,5 +3,5 @@ description: "Setup Meteor" runs: using: "composite" steps: - - run: curl "https://install.meteor.com/?release=3.3.2" | sh + - run: curl "https://install.meteor.com/?release=3.4.1" | sh shell: bash diff --git a/.github/workflows/audit.yaml b/.github/workflows/audit.yaml index e1135e3b9f1..b83fbb9ac5a 100644 --- a/.github/workflows/audit.yaml +++ b/.github/workflows/audit.yaml @@ -14,7 +14,7 @@ jobs: continue-on-error: true timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -44,7 +44,7 @@ jobs: continue-on-error: true timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -87,7 +87,7 @@ jobs: - live-status-gateway steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -120,7 +120,7 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 26c9b324dc4..e10d7fbf7b7 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -20,7 +20,7 @@ jobs: name: Build Docusaurus runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false fetch-depth: 0 @@ -64,7 +64,7 @@ jobs: CI: true - name: Upload Build Artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: packages/documentation/build diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 76a8ffb3631..7eef4354ced 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -59,7 +59,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -111,7 +111,7 @@ jobs: packages: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -281,7 +281,7 @@ jobs: packages: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false @@ -414,7 +414,7 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -494,7 +494,7 @@ jobs: send-coverage: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js ${{ matrix.node-version }} @@ -539,7 +539,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -569,7 +569,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -605,7 +605,7 @@ jobs: # This is just to ensure the docs build, another job performs the build & publish steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -652,7 +652,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js diff --git a/.github/workflows/prune-tags.yml b/.github/workflows/prune-tags.yml index 117c2c77007..0eeb177c712 100644 --- a/.github/workflows/prune-tags.yml +++ b/.github/workflows/prune-tags.yml @@ -26,7 +26,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout repo with all tags - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index 110c02e805f..662b36191b6 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -53,7 +53,7 @@ jobs: if: ${{ needs.check-publish.outputs.can-publish == '1' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -103,7 +103,7 @@ jobs: node-version: [22.x] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js ${{ matrix.node-version }} @@ -141,7 +141,7 @@ jobs: if: ${{ needs.check-publish.outputs.can-publish == '1' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Use Node.js @@ -221,7 +221,7 @@ jobs: id-token: write # scoped for as short as possible, as this gives write access to npm steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml index 1753c637904..7ca17dbfa21 100644 --- a/.github/workflows/sonar.yaml +++ b/.github/workflows/sonar.yaml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: # Disabling shallow clone is recommended for improving relevancy of reporting fetch-depth: 0 @@ -54,6 +54,6 @@ jobs: env: CI: true - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v7 + uses: SonarSource/sonarqube-scan-action@v8 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/.yarnrc.yml b/.yarnrc.yml index 3bdf82f7357..882e7b6a209 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -14,6 +14,6 @@ npmMinimalAgeGate: 7 npmPreapprovedPackages: - "@sofie-automation/*" - "@mos-connection/*" - - "timeline-state-resolver" - - "timeline-state-resolver-types" - - "timeline-state-resolver-api" \ No newline at end of file + - timeline-state-resolver + - timeline-state-resolver-types + - timeline-state-resolver-api diff --git a/DEVELOPER.md b/DEVELOPER.md index bae711b60d1..f64318bf92d 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -118,10 +118,10 @@ For support of various languages in the GUI, Sofie uses the _i18next_ framework. ```bash cd meteor -yarn i18n-extract-pot +yarn i18n-extract-po ``` -Find the created `template.pot` file in `meteor/i18n` folder. Create a new PO file based on that template using a PO editor of your choice. Save it in the `meteor/i18n` folder using your [ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) of choice as the filename. +Find the created `.po` files in `meteor/i18n` folder. Edit the appropriate PO file using a PO editor of your choice. Then, run the compilation script: diff --git a/meteor/.gitignore b/meteor/.gitignore index f9fbba253b2..5afcd4a74c6 100644 --- a/meteor/.gitignore +++ b/meteor/.gitignore @@ -19,3 +19,9 @@ client/ui/Shelf/Keyboard !.yarn/sdks !.yarn/versions + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/meteor/.meteor/packages b/meteor/.meteor/packages index 5e49caae6e3..cbecb7860fa 100644 --- a/meteor/.meteor/packages +++ b/meteor/.meteor/packages @@ -8,15 +8,16 @@ # but you can also edit it by hand. -meteor@2.1.1 -webapp@2.0.7 +meteor@2.3.0 +webapp@2.1.2 ddp@1.4.2 -mongo@2.1.4 # The database Meteor supports right now +mongo@2.3.0 # The database Meteor supports right now -ecmascript@0.16.13 # Enable ECMAScript2015+ syntax in app code -typescript@5.6.6 # Enable TypeScript syntax in .ts and .tsx modules +ecmascript@0.18.0 # Enable ECMAScript2015+ syntax in app code +typescript@5.10.0 # Enable TypeScript syntax in .ts and .tsx modules tracker@1.3.4 # Meteor's client-side reactive programming library zodern:types +rspack diff --git a/meteor/.meteor/release b/meteor/.meteor/release index 4876d6ff64c..49a715aec23 100644 --- a/meteor/.meteor/release +++ b/meteor/.meteor/release @@ -1 +1 @@ -METEOR@3.3.2 +METEOR@3.4.1 diff --git a/meteor/.meteor/versions b/meteor/.meteor/versions index 5b4f25da332..d8186678778 100644 --- a/meteor/.meteor/versions +++ b/meteor/.meteor/versions @@ -1,21 +1,21 @@ allow-deny@2.1.0 -babel-compiler@7.12.2 +babel-compiler@7.14.0 babel-runtime@1.5.2 base64@1.0.13 binary-heap@1.0.12 -boilerplate-generator@2.0.2 -callback-hook@1.6.1 -check@1.4.4 +boilerplate-generator@2.1.0 +callback-hook@1.7.0 +check@1.5.0 core-runtime@1.0.0 ddp@1.4.2 -ddp-client@3.1.1 +ddp-client@3.2.0 ddp-common@1.4.4 -ddp-server@3.1.2 +ddp-server@3.2.0 diff-sequence@1.1.3 dynamic-import@0.7.4 -ecmascript@0.16.13 +ecmascript@0.18.0 ecmascript-runtime@0.8.3 -ecmascript-runtime-client@0.12.3 +ecmascript-runtime-client@0.13.0 ecmascript-runtime-server@0.11.1 ejson@1.1.5 facts-base@1.0.2 @@ -24,12 +24,12 @@ geojson-utils@1.0.12 id-map@1.2.0 inter-process-messaging@0.1.2 logging@1.3.6 -meteor@2.1.1 -minimongo@2.0.4 +meteor@2.3.0 +minimongo@2.1.0 modern-browsers@0.2.3 modules@0.20.3 modules-runtime@0.13.2 -mongo@2.1.4 +mongo@2.3.0 mongo-decimal@0.2.0 mongo-dev-server@1.1.1 mongo-id@1.0.9 @@ -37,13 +37,15 @@ npm-mongo@6.16.1 ordered-dict@1.2.0 promise@1.0.0 random@1.2.2 -react-fast-refresh@0.2.9 +react-fast-refresh@0.3.0 reload@1.3.2 retry@1.1.1 routepolicy@1.1.2 +rspack@1.1.0 socket-stream-client@0.6.1 +tools-core@1.1.0 tracker@1.3.4 -typescript@5.6.6 -webapp@2.0.7 +typescript@5.10.0 +webapp@2.1.2 webapp-hashing@1.1.2 zodern:types@1.0.13 diff --git a/meteor/.yarn/patches/@meteorjs-rspack-npm-2.0.1-d001eb481c.patch b/meteor/.yarn/patches/@meteorjs-rspack-npm-2.0.1-d001eb481c.patch new file mode 100644 index 00000000000..6496907a30b --- /dev/null +++ b/meteor/.yarn/patches/@meteorjs-rspack-npm-2.0.1-d001eb481c.patch @@ -0,0 +1,13 @@ +diff --git a/rspack.config.js b/rspack.config.js +index 76ad29ad8a9b5bf31309a09c88e1c00c53193a2a..1df53de9ff941bca8291547443ad99860b131dff 100644 +--- a/rspack.config.js ++++ b/rspack.config.js +@@ -135,7 +135,7 @@ function createSwcConfig({ + ...(isJsxEnabled && { jsx: true }), + ...(isAngularEnabled && { decorators: true }), + }, +- target: 'es2015', ++ target: 'es2024', // We don't care about browsers, so can target current nodejs and avoid issues with transpiling + ...(isReactEnabled && { + transform: { + react: { diff --git a/meteor/Dockerfile b/meteor/Dockerfile index 3e89b0e3eba..4c7b2fd26cd 100644 --- a/meteor/Dockerfile +++ b/meteor/Dockerfile @@ -8,6 +8,7 @@ WORKDIR /opt/core/packages RUN rm -R *-gateway documentation openapi RUN corepack enable RUN yarn install && yarn build +RUN cd webui && yarn build # Install production dependencies for the worker # HACK: @@ -15,7 +16,7 @@ RUN yarn install && yarn build # BUILD IMAGE FROM node:22 -RUN curl "https://install.meteor.com/?release=3.3.2" | sh +RUN curl "https://install.meteor.com/?release=3.4.1" | sh # Temporary change the NODE_ENV env variable, so that all libraries are installed: ENV NODE_ENV_TMP $NODE_ENV diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index a9c1b5ba846..450e8751a7f 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -1,11 +1,12 @@ import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { clone } from '@sofie-automation/corelib/dist/lib' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { IBlueprintPieceType, PieceLifespan } from '@sofie-automation/blueprints-integration' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { Piece, EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { getRundownId } from '../server/api/ingest/lib' @@ -116,6 +117,7 @@ export function defaultStudio(_id: StudioId): DBStudio { allowPieceDirectPlay: false, enableBuckets: false, enableEvaluationForm: true, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, }), _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index b79d86d5976..fd84d253c7a 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -45,7 +45,7 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { EmptyPieceTimelineObjectsBlob, Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { restartRandomId } from '../random' @@ -127,6 +127,7 @@ export async function setupMockPeripheralDevice( created: 1234, status: { statusCode: StatusCode.GOOD, + statusDetails: [], }, lastSeen: 1234, lastConnected: 1234, diff --git a/meteor/eslint.config.mjs b/meteor/eslint.config.mjs index 7f4a7a7e275..727aefc8e1f 100644 --- a/meteor/eslint.config.mjs +++ b/meteor/eslint.config.mjs @@ -14,7 +14,7 @@ const tmpRules = { const extendedRules = await generateEslintConfig({ // tsconfigName: 'tsconfig.eslint.json', - ignores: ['.meteor', 'public', 'scripts', 'server/_force_restart.js', '/packages/'], + ignores: ['.meteor', 'public', 'scripts', 'server/_force_restart.js', '/packages/', '_build'], // disableNodeRules: true, }) diff --git a/meteor/i18n/.gitignore b/meteor/i18n/.gitignore new file mode 100644 index 00000000000..94a2dd146a2 --- /dev/null +++ b/meteor/i18n/.gitignore @@ -0,0 +1 @@ +*.json \ No newline at end of file diff --git a/meteor/i18n/template.pot b/meteor/i18n/en.po similarity index 64% rename from meteor/i18n/template.pot rename to meteor/i18n/en.po index ac4e0e454e4..f47dfd79c54 100644 --- a/meteor/i18n/template.pot +++ b/meteor/i18n/en.po @@ -5,3333 +5,4532 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2023-02-09T08:18:53.604Z\n" -"PO-Revision-Date: 2023-02-09T08:18:53.604Z\n" +"POT-Creation-Date: 2026-04-14T14:17:21.116Z\n" +"PO-Revision-Date: 2026-04-14T14:17:21.116Z\n" +"Language: en\n" -msgid "Account Page" +msgid " The index of the Atem media/clip banks" msgstr "" -msgid "Name:" +msgid "({{time}} ago)" msgstr "" -msgid "Email:" +msgid "({{timecode}})" msgstr "" -msgid "Old Password" +msgid "" +"(Comma separated list. Empty - will store snapshots of all Rundown " +"Playlists)" msgstr "" -msgid "New Password" +msgid "(Default)" msgstr "" -msgid "Save Changes" +msgid "(in: {{time}})" msgstr "" -msgid "Edit Account" +msgid "(Optional) A name/identifier of the local network where the Atem is located" msgstr "" -msgid "Organization" +msgid "(Optional) A name/identifier of the local network where the share is located" msgstr "" -msgid "User roles in organization" +msgid "" +"(Optional) A name/identifier of the local network where the share is " +"located, leave empty if globally accessible" msgstr "" -msgid "Studio" +msgid "(Optional) This could be the name of the compute" msgstr "" -msgid "Configurator" +msgid "" +"(Optional) This could be the name of the computer on which the local folder " +"is on" msgstr "" -msgid "Developer" +msgid "(Unknown playlist)" msgstr "" -msgid "Admin" +msgid "(Unknown rundown)" msgstr "" -msgid "Remove Self" +msgid "" +"(what happened and when, what should have happened, what could have " +"triggered the problems, etcetera...)" msgstr "" -msgid "Email Address" +msgid "{{actionName}} failed! More information can be found in the system log." msgstr "" -msgid "Password" +msgid "{{currentRundownName}} - {{rundownPlaylistName}}" msgstr "" -msgid "Sign in" +msgid "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)" msgstr "" -msgid "Create New Account" +msgid "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago" msgstr "" -msgid "Lost password?" +msgid "{{frames}} black frames detected in the clip" msgstr "" -msgid "Send reset email" +msgid "{{frames}} black frames detected within the clip" msgstr "" -msgid "Go back" +msgid "{{frames}} freeze frames detected in the clip" msgstr "" -msgid "Password must be atleast 5 characters long" +msgid "{{frames}} freeze frames detected within the clip" msgstr "" -msgid "Enter your new password" +msgid "{{hours}} h {{minutes}} min {{seconds}} s ago" msgstr "" -msgid "Set new password" +msgid "{{indexCount}} indexes was removed." msgstr "" -msgid "Your Account" +msgid "{{minutes}} min {{seconds}} s ago" msgstr "" -msgid "About Your Organization" +msgid "{{nrcsName}} Connection" msgstr "" -msgid "We are mainly" +msgid "{{prevStatements}} and {{finalStatement}}" msgstr "" -msgid "Areas" +msgid "{{prevStatements}} or {{finalStatement}}" msgstr "" -msgid "Invite User" +msgid "" +"{{reason}} {{sourceLayer}} exists, but is not yet ready on the playout " +"system" msgstr "" -msgid "New User's Email" +msgid "{{rundownPlaylistName}} (Looping)" msgstr "" -msgid "New User's Name" +msgid "{{seconds}} s ago" msgstr "" -msgid "Create New User & Send Enrollment Email" +msgid "{{showStyleVariant}} – {{showStyleBase}}" msgstr "" -msgid "Users in organization" +msgid "{{sourceLayer}} can't be found on the playout system" msgstr "" -msgid "Return to list" +msgid "{{sourceLayer}} doesn't have both audio & video" msgstr "" -msgid "There is no rundown active in this studio." +msgid "{{sourceLayer}} has {{audioStreams}} audio streams" msgstr "" -msgid "This studio doesn't exist." +msgid "{{sourceLayer}} has the wrong format: {{format}}" msgstr "" -msgid "There are no active rundowns." +msgid "{{sourceLayer}} has unsupported source: {{containerLabels}}" msgstr "" -msgid "Evaluation" +msgid "{{sourceLayer}} is being ingested" msgstr "" -msgid "Please take a minute to fill in this form." +msgid "" +"{{sourceLayer}} is in a placeholder state for an unknown workflow-defined " +"reason" msgstr "" -msgid "" -"Be aware that while filling out the form keyboard and streamdeck commands " -"will not be executed!" +msgid "{{sourceLayer}} is in an unknown state: \"{{status}}\"" msgstr "" -msgid "Did you have any problems with the broadcast?" +msgid "{{sourceLayer}} is missing" msgstr "" -msgid "" -"Please explain the problems you experienced (what happened and when, what " -"should have happened, what could have triggered the problems, etcetera...)" +msgid "{{sourceLayer}} is missing a file path" msgstr "" -msgid "Your name" +msgid "{{sourceLayer}} is not yet ready on the playout system" msgstr "" -msgid "Save message" +msgid "{{sourceLayer}} is transferring to the playout system" msgstr "" -msgid "Save message and Deactivate Rundown" +msgid "" +"{{sourceLayer}} is transferring to the playout system but cannot be played " +"yet" msgstr "" -msgid "No problems" +msgid "{{studioName}}: Active Rundown" msgstr "" -msgid "Something went wrong, but it didn't affect the output" +msgid "{{studioName}}: Presenter screen" msgstr "" -msgid "Something went wrong, and it affected the output" +msgid "{{studioName}}: Prompter" msgstr "" -msgid "Are you sure?" +msgid "14 = 7 lines, 20 = 5 lines" msgstr "" -msgid "" -"Trimming this clip has timed out. It's possible that the story is currently " -"locked for writing in {{nrcsName}} and will eventually be updated. Make " -"sure that the story is not being edited by other users." +msgid "A device must be assigned to the config to edit the settings" msgstr "" -msgid "Trimming this clip has failed due to an error: {{error}}." +msgid "" +"A Full System Snapshot contains all system settings (studios, showstyles, " +"blueprints, devices, etc.)" msgstr "" -msgid "Trimmed succesfully." +msgid "a second" msgstr "" msgid "" -"Trimming this clip is taking longer than expected. It's possible that the " -"story is locked for writing in {{nrcsName}}." +"A snapshot of the current Running Order has been created for " +"troubleshooting." msgstr "" -msgid "Trim \"{{name}}\"" +msgid "A Studio Snapshot contains all system settings related to that studio" msgstr "" -msgid "OK" +msgid "AB Channel Display" msgstr "" -msgid "Cancel" +msgid "AB Playout devices" msgstr "" -msgid "Remove in-trimming" +msgid "AB Resolver Channel Display" msgstr "" -msgid "In" +msgid "Abort" msgstr "" -msgid "Remove all trimming" +msgid "Aborting all Media Workflows" msgstr "" -msgid "Duration" +msgid "Aborting Media Workflow" msgstr "" -msgid "Remove out-trimming" +msgid "Accessor ID" msgstr "" -msgid "Out" +msgid "Accessor Type" msgstr "" -msgid "Next" +msgid "Accessors" msgstr "" -msgid "Test test" +msgid "Action" msgstr "" -msgid "Until next take" +msgid "Action {{actionName}} done!" msgstr "" -msgid "Until next segment" +msgid "Action {{actionName}} failed: {{error}}" msgstr "" -msgid "Until end of segment" +msgid "Action Buttons" msgstr "" -msgid "Until next rundown" +msgid "Action Triggers" msgstr "" -msgid "Until end of showstyle" +msgid "Activate \"On Air\"" msgstr "" -msgid "Script is empty" +msgid "Activate \"Rehearsal\"" msgstr "" -msgid "Clip:" +msgid "Activate (On-Air)" msgstr "" -msgid "Home" +msgid "Activate (Rehearsal)" msgstr "" -msgid "Rundowns" +msgid "Activate On Air" msgstr "" -msgid "Test Tools" +msgid "Activate Rundown" msgstr "" -msgid "Status" +msgid "Activating Hold" msgstr "" -msgid "Settings" +msgid "Activating Rundown Playlist" msgstr "" -msgid "Account" +msgid "Active" msgstr "" -msgid "Logout" +msgid "Active Rundown" msgstr "" -msgid "My name is {{name}}" +msgid "Active Rundown View" msgstr "" -msgid "Operating Mode" +msgid "Ad-Lib" msgstr "" -msgid "Switching operating mode to {{mode}}" +msgid "Ad-Lib Action" msgstr "" -msgid "Prompter" +msgid "Add" msgstr "" -msgid "End of script" +msgid "Add {{filtersTitle}}" msgstr "" -msgid "Could not get system status. Please consult system administrator." +msgid "Add a playout device to the studio in order to configure the route sets" msgstr "" -msgid "There are no rundowns ingested into Sofie." +msgid "Add a playout device to the studio in order to edit the layer mappings" msgstr "" -msgid "Click on a rundown to control your studio" +msgid "Add Action Trigger" msgstr "" -msgid "Rundown" +msgid "Add button" msgstr "" -msgid "Problems" +msgid "Add filter" msgstr "" -msgid "Show Style" +msgid "Add some source layers (e.g. Graphics) for your data to appear in rundowns" msgstr "" -msgid "On Air Start Time" +msgid "AdLib" msgstr "" -msgid "Expected End Time" +msgid "AdLib Actions are not supported in the current Rundown" msgstr "" -msgid "Last updated" +msgid "AdLib could not be found!" msgstr "" -msgid "View Layout" +msgid "AdLib filter" msgstr "" -msgid "Today" +msgid "Adlib Rank" msgstr "" -msgid "Yesterday" +msgid "Adlib rundowns are not supported for this ShowStyle!" msgstr "" -msgid "Tomorrow" +msgid "Adlib Testing" msgstr "" -msgid "Last" +msgid "AdLib Testing" msgstr "" -msgid "Getting Started" +msgid "AdLibs can be only placed in a currently playing part!" msgstr "" -msgid "" -"Start with giving this browser configuration permissions by adding this to " -"the URL: " +msgid "AdLibs on this layer can be queued" msgstr "" -msgid "Start Here!" +msgid "All additional source layers must have active pieces" msgstr "" -msgid "Then, run the migrations script:" +msgid "All connections working correctly" msgstr "" -msgid "Run Migrations to get set up" +msgid "All devices working correctly" msgstr "" -msgid "Migrations" +msgid "All is well, go get a" msgstr "" -msgid "Documentation is available at" +msgid "All Screens in a MultiViewer" msgstr "" -msgid "Use {{nrcsName}} order" +msgid "All steps" msgstr "" -msgid "Reset Sort Order" +msgid "Allow direct playing pieces" msgstr "" -msgid "Enable configuration mode by adding ?configure=1 to the address bar." +msgid "Allow disabling of Pieces" msgstr "" -msgid "You need to run migrations to set the system up for operation." +msgid "Allow HOLD mode" msgstr "" -msgid "Drop Rundown here to move it out of its current Playlist" +msgid "Allow infinites from AdLib testing to persist" msgstr "" -msgid "Sofie Automation" +msgid "Allow Read access" msgstr "" -msgid "version" +msgid "Allow Rundowns to be reset while on-air" msgstr "" -msgid "System Status" +msgid "Allow Write access" msgstr "" -msgid "System has issues which need to be resolved" +msgid "Also Require Source Layers" msgstr "" -msgid "Status Messages:" +msgid "Amount of entries exceeds the limt of 10 000 items." msgstr "" -msgid "{{showStyleVariant}} – {{showStyleBase}}" +msgid "An error while performing the take, playout may be impacted" msgstr "" -msgid "Drag to reorder or move out of playlist" +msgid "An error while setting the next Part, playout may be impacted" msgstr "" -msgid "This rundown is currently active" +msgid "An internal error occured!" msgstr "" -msgid "Not set" +msgid "Another Rundown is Already Active!" msgstr "" -msgid "This rundown will loop indefinitely" +msgid "Answers" msgstr "" -msgid "({{timecode}})" +msgid "" +"Any AB Playout devices here will only be active when this or another " +"RouteSet that includes them is active" msgstr "" -msgid "Re-sync rundown data with {{nrcsName}}" +msgid "APM Enabled" msgstr "" -msgid "Delete" +msgid "APM Transaction Sample Rate" msgstr "" -msgid "Standalone Shelf" +msgid "Append" msgstr "" -msgid "Rundown & Shelf" +msgid "Append or Replace" msgstr "" -msgid "Default" +msgid "Append rows" msgstr "" -msgid "Delete rundown?" +msgid "Application credentials" msgstr "" -msgid "Are you sure you want to delete the \"{{name}}\" rundown?" +msgid "Application Performance Monitoring" msgstr "" -msgid "Please note: This action is irreversible!" +msgid "Apply" msgstr "" -msgid "Re-Sync rundown?" +msgid "Apply blueprint upgrades" msgstr "" -msgid "Re-Sync" +msgid "Apply Config" msgstr "" -msgid "Are you sure you want to re-sync the \"{{name}}\" rundown?" +msgid "Are you sure you want to activate Rehearsal Mode?" msgstr "" -msgid "Start time is close" +msgid "" +"Are you sure you want to deactivate this Rundown\n" +"(This will clear the outputs)" msgstr "" -msgid "Yes" +msgid "" +"Are you sure you want to deactivate this rundown?\n" +"(This will clear the outputs.)" msgstr "" -msgid "No" +msgid "Are you sure you want to delete output layer \"{{outputId}}\"?" msgstr "" -msgid "" -"You are in rehearsal mode, the broadcast starts in less than 1 minute. Do " -"you want to reset the rundown and go into On-Air mode?" +msgid "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?" msgstr "" -msgid "Hold" +msgid "Are you sure you want to delete the \"{{name}}\" rundown?" msgstr "" -msgid "Could not find a Piece that can be disabled." +msgid "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?" msgstr "" -msgid "Failed to execute take" +msgid "Are you sure you want to delete the shelf layout \"{{name}}\"?" msgstr "" -msgid "" -"The rundown you are trying to execute a take on is inactive, would you like " -"to activate this rundown?" +msgid "Are you sure you want to delete the show style \"{{showStyleId}}\"?" msgstr "" -msgid "Activate (Rehearsal)" +msgid "Are you sure you want to delete the studio \"{{studioId}}\"?" msgstr "" -msgid "Activate (On-Air)" +msgid "Are you sure you want to delete this AdLib?" msgstr "" -msgid "Failed to activate" +msgid "Are you sure you want to delete this Bucket?" msgstr "" -msgid "" -"Something went wrong, please contact the system administrator if the " -"problem persists." +msgid "Are you sure you want to delete this device: \"{{deviceId}}\"?" msgstr "" -msgid "Another Rundown is Already Active!" +msgid "Are you sure you want to empty (remove all adlibs inside) this Bucket?" msgstr "" msgid "" -"The rundown \"{{rundownName}}\" will need to be deactivated in order to " -"activate this one.\n" -"\n" -"Are you sure you want to activate this one anyway?" +"Are you sure you want to force the migration? This will bypass the " +"migration checks, so be sure to verify that the values in the settings are " +"correct!" msgstr "" -msgid "Activate Anyway (Rehearsal)" +msgid "Are you sure you want to import the contents of the file \"{{fileName}}\"?" msgstr "" -msgid "Activate Anyway (On-Air)" +msgid "Are you sure you want to re-sync the \"{{name}}\" rundown?" msgstr "" -msgid "Do you want to activate this Rundown?" +msgid "" +"Are you sure you want to re-sync the Rundown?\n" +"(If the currently playing Part has been changed, this can affect the output)" msgstr "" -msgid "" -"The planned end time has passed, are you sure you want to activate this " -"Rundown?" +msgid "Are you sure you want to remove {{type}} \"{{deviceId}}\"?" msgstr "" -msgid "Are you sure you want to activate Rehearsal Mode?" +msgid "Are you sure you want to remove all Variants in the table?" msgstr "" msgid "" -"Are you sure you want to deactivate this Rundown?\n" -"(This will clear the outputs)" +"Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\n" +"Route Sets assigned to this group will be reset to no group." msgstr "" -msgid "The rundown can not be reset while it is active" +msgid "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?" msgstr "" -msgid "" -"A snapshot of the current Running Order has been created for " -"troubleshooting." +msgid "Are you sure you want to remove the AB Player \"{{playerId}}\"?" msgstr "" -msgid "Prepare Studio and Activate (Rehearsal)" +msgid "" +"Are you sure you want to remove the device \"{{deviceName}}\" and all of " +"it's sub-devices?" msgstr "" -msgid "Deactivate" +msgid "Are you sure you want to remove the Package Container \"{{containerId}}\"?" msgstr "" -msgid "Take" +msgid "" +"Are you sure you want to remove the Package Container Accessor " +"\"{{accessorId}}\"?" msgstr "" -msgid "Reset Rundown" +msgid "" +"Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to " +"\"{{newLayerId}}\"?" msgstr "" -msgid "Reload {{nrcsName}} Data" +msgid "Are you sure you want to remove the Route Set \"{{routeId}}\"?" msgstr "" -msgid "Store Snapshot" +msgid "Are you sure you want to remove the Variant \"{{showStyleVariantId}}\"?" msgstr "" -msgid "No actions available" +msgid "" +"Are you sure you want to replace the blueprints with the file " +"\"{{fileName}}\"?" msgstr "" -msgid "Add ?studio=1 to the URL to enter studio mode" +msgid "" +"Are you sure you want to reset all overrides for Packing Container " +"\"{{id}}\"?" msgstr "" -msgid "Exit" +msgid "" +"Are you sure you want to reset all overrides for the mapping for layer " +"\"{{mappingId}}\"?" msgstr "" -msgid "Error" +msgid "" +"Are you sure you want to reset all overrides for the output layer " +"\"{{outputLayerId}}\"?" msgstr "" -msgid "This rundown is now active. Are you sure you want to exit this screen?" +msgid "Are you sure you want to reset all overrides for the selected row?" msgstr "" -msgid "Invalid AdLib" +msgid "" +"Are you sure you want to reset all overrides for the source layer " +"\"{{sourceLayerId}}\"?" msgstr "" -msgid "Cannot play this AdLib because it is marked as Invalid" +msgid "" +"Are you sure you want to reset the database version?\n" +"Only do this if you plan on running the migration right after." msgstr "" -msgid "Floated Adlib" +msgid "Are you sure you want to restart this device?" msgstr "" -msgid "Cannot play this AdLib because it is marked as Floated" +msgid "" +"Are you sure you want to restart this Sofie Automation Server Core: " +"{{name}}?" msgstr "" -msgid "Not queueable" +msgid "" +"Are you sure you want to restore the system from the snapshot file " +"\"{{fileName}}\"?" msgstr "" -msgid "Cannot play this adlib because source layer is not queueable" +msgid "Are you sure you want to skip the fix up config step for {{name}}" msgstr "" msgid "" -"There are no Playout Gateways connected and attached to this studio. Please " -"contact the system administrator to start the Playout Gateway." +"Are you sure you want to update the blueprints from the file " +"\"{{fileName}}\"?" msgstr "" -msgid "Playout Gateway \"{{playoutDeviceName}}\" is now restarting." +msgid "" +"Are you sure you want to upload the shelf layout from the file " +"\"{{fileName}}\"?" msgstr "" -msgid "Could not restart Playout Gateway \"{{playoutDeviceName}}\"." +msgid "" +"Are you sure, do you really want to REMOVE the Snapshot " +"\"{{snapshotName}}\"?\r\n" +"This cannot be undone!!" msgstr "" -msgid "Restart Playout" +msgid "Are you sure?" msgstr "" -msgid "Do you want to restart the Playout Gateway?" +msgid "" +"Are you sure? This will cause the whole Sofie system to be unresponsive " +"several seconds!" msgstr "" -msgid "Restart CasparCG Server" +msgid "Around 10 minutes ago" msgstr "" -msgid "Do you want to restart CasparCG Server \"{{device}}\"?" +msgid "Assign" msgstr "" -msgid "CasparCG on device \"{{deviceName}}\" restarting..." +msgid "Assigned Show Styles" msgstr "" -msgid "Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}" +msgid "Assigned Studios" msgstr "" -msgid "Cancel currently pressed hotkey" +msgid "Attached Subdevices" msgstr "" -msgid "Change to fullscreen mode" +msgid "Audio Mixing" msgstr "" -msgid "Show Hotkeys" +msgid "Authorize App Access" msgstr "" -msgid "Take a Snapshot" +msgid "Auto" msgstr "" -msgid "Restart {{device}}" +msgid "AutoNext in QuickLoop behavior" msgstr "" -msgid "Rundown not found" +msgid "Available Screens for Studio {{studioId}}" msgstr "" -msgid "Close" +msgid "Bad" msgstr "" -msgid "Rundown for piece \"{{pieceLabel}}\" could not be found." +msgid "Bank Index" msgstr "" -msgid "This rundown has been unpublished from Sofie." +msgid "Base URL" msgstr "" -msgid "Error: The studio of this Rundown was not found." +msgid "Base url to the resource (example: http://myserver/folder)" msgstr "" -msgid "This playlist is empty" +msgid "Baseline needs reload, this studio may not work until reloaded" msgstr "" -msgid "Error: The ShowStyle of this Rundown was not found." +msgid "Behavior" msgstr "" -msgid "Unknown error" +msgid "Blueprint" msgstr "" -msgid "" -"Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data " -"from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced " -"or remove the rundown from Sofie. What do you want to do?" +msgid "Blueprint config has changed" msgstr "" -msgid "(Unknown rundown)" +msgid "Blueprint config preset" msgstr "" -msgid "(Unknown playlist)" +msgid "" +"Blueprint config preset has been changed. From \"{{ oldValue }}\", to \"{{ " +"newValue }}\"" msgstr "" -msgid "Leave Unsynced" +msgid "Blueprint config preset is missing" msgstr "" -msgid "Remove" +msgid "Blueprint config preset not set" msgstr "" -msgid "Remove rundown" +msgid "Blueprint Configuration" msgstr "" -msgid "" -"Do you really want to remove just the rundown \"{{rundownName}}\" in the " -"playlist {{playlistName}} from Sofie? This cannot be undone!" +msgid "Blueprint has a new version" msgstr "" -msgid "Loop Start" +msgid "Blueprint has been changed. From \"{{ oldValue }}\", to \"{{ newValue }}\"" msgstr "" -msgid "Loop End" +msgid "Blueprint ID" msgstr "" -msgid "(in: {{time}})" +msgid "Blueprint Name" msgstr "" -msgid "({{time}} ago)" +msgid "Blueprint not found!" msgstr "" -msgid "Planned Start" +msgid "Blueprint not set" msgstr "" -msgid "Planned Duration" +msgid "Blueprint Type" msgstr "" -msgid "Planned End" +msgid "Blueprint Version" msgstr "" -msgid "Time to planned end" +msgid "Blueprints" msgstr "" -msgid "Time since planned end" +msgid "Blueprints updated successfully." msgstr "" -msgid "Over/Under" +msgid "Bottom" msgstr "" -msgid "" -"The rundown \"{{rundownName}}\" is not published or activated in " -"{{nrcsName}}! No data updates will currently come through." +msgid "Box color" msgstr "" -msgid "Re-sync" +msgid "BREAK" msgstr "" -msgid "Re-sync Rundown" +msgid "Break In" msgstr "" -msgid "" -"Are you sure you want to re-sync the Rundown?\n" -"(If the currently playing Part has been changed, this can affect the output)" +msgid "Bucket AdLib is not compatible with this Rundown!" msgstr "" -msgid "Restart" +msgid "Bucket not found!" msgstr "" -msgid "" -"Fixing this problem requires a restart to the host device. Are you sure you " -"want to restart {{device}}?\n" -"(This might affect output)" +msgid "Button" msgstr "" -msgid "Device \"{{deviceName}}\" restarting..." +msgid "Button height scale factor" msgstr "" -msgid "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}" +msgid "Button mapping" msgstr "" -msgid "There is an unknown problem with the part." +msgid "Button width scale factor" msgstr "" -msgid "Show issue" +msgid "By Parts" msgstr "" -msgid "There is an unspecified problem with the source." +msgid "By Segments" msgstr "" -msgid "External message queue has unsent messages." +msgid "Camera" msgstr "" -msgid "" -"The system configuration has been changed since importing this rundown. It " -"might not run correctly" +msgid "Camera Screen" msgstr "" -msgid "Unable to check the system configuration for changes" +msgid "Can Generate Adlib Testing Rundown" msgstr "" -msgid "The Studio configuration is missing some required fields:" +msgid "Can not be used during a hold!" msgstr "" -msgid "The Show Style configuration \"{{name}}\" could not be validated" +msgid "Cancel" msgstr "" -msgid "The ShowStyle \"{{name}}\" configuration is missing some required fields:" +msgid "Cancel currently pressed hotkey" msgstr "" -msgid "Unable to validate the system configuration" +msgid "Cannot activate HOLD before a part has been taken!" msgstr "" -msgid "Device {{deviceName}} is disconnected" +msgid "Cannot activate HOLD between the current and next parts" msgstr "" -msgid "Critical Problems" +msgid "Cannot activate HOLD once an adlib has been used" msgstr "" -msgid "Warnings" +msgid "Cannot cancel HOLD once it has been taken" msgstr "" -msgid "Notifications" +msgid "Cannot connect to the {{platformName}}: {{reason}}" msgstr "" -msgid "Rewind all Segments" +msgid "Cannot perform take for {{duration}}ms" msgstr "" -msgid "Go to On Air Segment" +msgid "Cannot play this AdLib because it is marked as Floated" msgstr "" -msgid "Switch Segment View Mode" +msgid "Cannot play this AdLib because it is marked as Invalid" msgstr "" -msgid "Toggle Switchboard Panel" +msgid "Cannot play this adlib because source layer is not queueable" msgstr "" -msgid "Toggle Support Panel" +msgid "Cannot remove the rundown \"{{name}}\" while it is on-air." msgstr "" -msgid "Just now" +msgid "Cannot take close to an AUTO" msgstr "" -msgid "Less than a minute ago" +msgid "Cannot take during a transition" msgstr "" -msgid "Less than five minutes ago" +msgid "Cannot take unplayable AdLib" msgstr "" -msgid "Around 10 minutes ago" +msgid "CasparCG on device \"{{deviceName}}\" restarting..." msgstr "" -msgid "More than 10 minutes ago" +msgid "Center" msgstr "" -msgid "More than 30 minutes ago" +msgid "Change to fullscreen mode" msgstr "" -msgid "More than 2 hours ago" +msgid "Channel Name" msgstr "" -msgid "More than 5 hours ago" +msgid "" +"Check layer types to select all layers of that type, or check individual " +"layers for more specific filtering." msgstr "" -msgid "More than a day ago" +msgid "Check the console for troubleshooting data from device \"{{deviceName}}\"!" msgstr "" -msgid "{{nrcsName}} Connection" +msgid "Cleanup" msgstr "" -msgid "Last update" +msgid "Cleanup old data" msgstr "" -msgid "Off-line devices" +msgid "Cleanup old database indexes" msgstr "" -msgid "Devices with issues" +msgid "Clear {{layerName}}" msgstr "" -msgid "All connections working correctly" +msgid "Clear filter" msgstr "" -msgid "Play-out" +msgid "Clear queued segment" msgstr "" -msgid "All devices working correctly" +msgid "Clear QuickLoop" msgstr "" -msgid "Auto" +msgid "Clear QuickLoop End" msgstr "" -msgid "Expected End" +msgid "Clear QuickLoop Start" msgstr "" -msgid "Next Loop at" +msgid "Clear Source Layer" msgstr "" -msgid "Diff" +msgid "Clear value" msgstr "" -msgid "Started" +msgid "Clearing SourceLayer" msgstr "" -msgid "Expected Start" +msgid "Click anywhere for fullscreen" msgstr "" -msgid "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)" +msgid "Click on a rundown to control your studio" msgstr "" -msgid "{{currentRundownName}} - {{rundownPlaylistName}}" +msgid "Click or press Enter for fullscreen" msgstr "" -msgid "{{rundownPlaylistName}} (Looping)" +msgid "Click to show available Package Containers" msgstr "" -msgid "Floated AdLib" +msgid "Click to show available Show Styles" msgstr "" -msgid "Switchboard" +msgid "Client IP" msgstr "" -msgid "This is not in it's normal setting" +msgid "Clip starts with {{frames}} black frames" msgstr "" -msgid "Off" +msgid "Clip starts with {{frames}} freeze frames" msgstr "" -msgid "Switch to Timeline View" +msgid "Clips" msgstr "" -msgid "Switch to Storyboard View" +msgid "Close" msgstr "" -msgid "Switch to List View" +msgid "Close Properties Panel" msgstr "" -msgid "On Air" +msgid "Close Rundown" msgstr "" -msgid "On Air At" +msgid "Comma-separated list of studio labels to filter by. Leave empty for all." msgstr "" -msgid "On Air In" +msgid "Comma-separated speeds in px/frame" msgstr "" -msgid "Unsynced" +msgid "Compatible Studios" msgstr "" -msgid "Critical problems" +msgid "Completed with warnings" msgstr "" -msgid "segment" +msgid "Config Fix Up must be run or ignored before the configuration can be edited" msgstr "" -msgid "Sources" +msgid "Config for {{name}} fix failed" msgstr "" -msgid "Loops to top" +msgid "Config for {{name}} fixed successfully" msgstr "" -msgid "Show End" +msgid "Config for {{name}} upgraded failed" msgstr "" -msgid "BREAK" +msgid "Config for {{name}} upgraded successfully" msgstr "" -msgid "Break In" +msgid "Config has not been applied before" msgstr "" -msgid "part" +msgid "Config ID: " msgstr "" -msgid "Set segment as Next" +msgid "Config looks good" msgstr "" -msgid "Queue segment" +msgid "Config preset" msgstr "" -msgid "Clear queued segment" +msgid "Config preset is missing" msgstr "" -msgid "Set this part as Next" +msgid "Config preset not set" msgstr "" -msgid "Set Next Here" +msgid "Config requires fixing up before it can be validated" msgstr "" -msgid "Play from Here" +msgid "" +"Config value \"{{ name }}\" has changed. From \"{{ oldValue }}\", to \"{{ " +"newValue }}\"" msgstr "" -msgid "Zoom Out" +msgid "Configurable Screens" msgstr "" -msgid "Show All" +msgid "" +"Configuration for this Gateway has moved to the Studio Peripheral Device " +"settings" msgstr "" -msgid "Zoom In" +msgid "Configure display options" msgstr "" -msgid "Parts Duration" +msgid "" +"Configure which pieces should display their assigned AB resolver channel " +"(e.g., \"Server A\") on various screens. This helps operators identify " +"which video server is playing each clip." msgstr "" -msgid "System Settings" +msgid "Confirm" msgstr "" -msgid "Add config item" +msgid "Connect some devices to the playout gateway" msgstr "" -msgid "Add" +msgid "Connect to {{deviceName}}" msgstr "" -msgid "Item" +msgid "Connected" msgstr "" -msgid "Value" +msgid "Connected App Containers" msgstr "" -msgid "true" +msgid "Connected Expectation Managers" msgstr "" -msgid "false" +msgid "Connected to the {{platformName}}." msgstr "" -msgid "{{count}} rows" +msgid "Connected Workers" msgstr "" -msgid "Failed to update config: {{errorMessage}}" +msgid "Connecting to the {{platformName}}" msgstr "" -msgid "Export" +msgid "Container Status" msgstr "" -msgid "Import" +msgid "Control mode" msgstr "" -msgid "Reset this item?" +msgid "Control modes" msgstr "" -msgid "Reset" +msgid "" +"Controls for exposed Route Sets will be displayed to the producer within " +"the Rundown View in the Switchboard." msgstr "" -msgid "Are you sure you want to reset this config item \"{{configId}}\"?" +msgid "Core" msgstr "" -msgid "Blueprint Configuration" +msgid "Core + Worker processing time" msgstr "" -msgid "More settings specific to this studio can be found here" +msgid "Core System settings" msgstr "" -msgid "Update Blueprints?" +msgid "" +"Could not create a snapshot for the evaluation, because the previous one " +"was created just moments ago. If you want another snapshot, try again in a " +"couple of seconds." msgstr "" -msgid "Update" +msgid "Could not get system status. Please consult system administrator." msgstr "" -msgid "" -"Are you sure you want to update the blueprints from the file " -"\"{{fileName}}\"?" +msgid "Could not restart core: {{err}}" msgstr "" -msgid "Blueprints updated successfully." +msgid "Could not restart Playout Gateway \"{{playoutDeviceName}}\"." msgstr "" -msgid "Replace Blueprints?" +msgid "Create Adlib Testing Rundown" msgstr "" -msgid "Replace" +msgid "Create new Bucket" msgstr "" -msgid "" -"Are you sure you want to replace the blueprints with the file " -"\"{{fileName}}\"?" +msgid "Created" msgstr "" -msgid "Failed to update blueprints: {{errorMessage}}" +msgid "Creating a new Bucket" msgstr "" -msgid "Assigned Show Styles:" +msgid "Creating Adlib Testing Rundown" msgstr "" -msgid "This Blueprint is not being used by any Show Style" +msgid "Creating Snapshot for debugging" msgstr "" -msgid "Assigned Studios:" +msgid "Critical problems" msgstr "" -msgid "This Blueprint is not compatible with any Studio" +msgid "Critical Problems" msgstr "" -msgid "Unassign" +msgid "Cron jobs" msgstr "" -msgid "Assign" +msgid "Current Part" msgstr "" -msgid "Blueprint ID" +msgid "Current part can contain next pieces" msgstr "" -msgid "Blueprint Name" +msgid "Current Segment" msgstr "" -msgid "No name set" +msgid "Custom Classes" msgstr "" -msgid "Blueprint Type" +msgid "Custom Hotkey Labels" msgstr "" -msgid "Upload a new blueprint" +msgid "Deactivate" msgstr "" -msgid "Last modified" +msgid "Deactivate \"On Air\"" msgstr "" -msgid "Blueprint Id" +msgid "Deactivate Rundown" msgstr "" -msgid "Blueprint Version" +msgid "Deactivate Studio" msgstr "" -msgid "Disable version check" +msgid "Deactivating other Rundown Playlist, and activating this one" msgstr "" -msgid "Upload Blueprints" +msgid "Deactivating Rundown Playlist" msgstr "" -msgid "Unknown table type" +msgid "Debug" msgstr "" -msgid "Unknown type" +msgid "Debug mode" msgstr "" -msgid "Defaults to '{{defaultVal}}' if left empty" +msgid "Debug State" msgstr "" -msgid "OAuth credentials succesfully uploaded." +msgid "Default" msgstr "" -msgid "Failed to upload OAuth credentials: {{errorMessage}}" +msgid "Default (hide)" msgstr "" -msgid "OAuth credentials successfuly reset" +msgid "Default Layout" msgstr "" -msgid "Failed to reset OAuth credentials: {{errorMessage}}" +msgid "Default shelf height" msgstr "" -msgid "Reset App Credentials" +msgid "Default State" msgstr "" -msgid "Reset User Credentials" +msgid "Delete" msgstr "" -msgid "Application credentials" +msgid "Delete Action Trigger" msgstr "" -msgid "Authorize App Access" +msgid "Delete layout?" msgstr "" -msgid "Waiting for gateway to generate URL..." +msgid "Delete mapping" msgstr "" -msgid "Only Match Global AdLibs" +msgid "Delete output layer" msgstr "" -msgid "Name" +msgid "Delete rundown?" msgstr "" -msgid "Display Style" +msgid "Delete source layer" msgstr "" -msgid "Show thumbnails next to list items" +msgid "Delete this AdLib" msgstr "" -msgid "Button width scale factor" +msgid "Delete this Blueprint?" msgstr "" -msgid "Button height scale factor" +msgid "Delete this Bucket" msgstr "" -msgid "Only Display AdLibs from Current Segment" +msgid "Delete this item?" msgstr "" -msgid "Include Global AdLibs" +msgid "Delete this output?" msgstr "" -msgid "Filter Disabled" +msgid "Delete this Show Style?" msgstr "" -msgid "Include Clear Source Layer in Ad-Libs" +msgid "Delete this Studio?" msgstr "" -msgid "Source Layers" +msgid "Device" msgstr "" -msgid "Source Layer Types" +msgid "Device \"{{deviceName}}\" restarting..." msgstr "" -msgid "Filter disabled" +msgid "Device {{deviceName}} is disconnected" msgstr "" -msgid "Output Channels" +msgid "Device ID" msgstr "" -msgid "Label contains" +msgid "Device is already attached to another studio." msgstr "" -msgid "Tags must contain" +msgid "Device is missing configuration schema" msgstr "" -msgid "Hide Panel from view" +msgid "Device is of unknown type" msgstr "" -msgid "Show panel as a timeline" +msgid "Device Name" msgstr "" -msgid "Enable search toolbar" +msgid "Device not found" msgstr "" -msgid "Overflow horizontally" +msgid "Device Triggers" msgstr "" -msgid "Display Take buttons" +msgid "Device Type" msgstr "" -msgid "Queue all adlibs" +msgid "Devices" msgstr "" -msgid "Toggle AdLibs on single mouse click" +msgid "Devices with issues" msgstr "" -msgid "Disable the hover Inspector when hovering over the button" +msgid "Did you have any problems with the broadcast?" msgstr "" -msgid "Current part can contain next pieces" +msgid "Diff" msgstr "" -msgid "Indicate only one next piece per source layer" +msgid "Director Screen" msgstr "" -msgid "Hide duplicated AdLibs" +msgid "Director's Screen" msgstr "" -msgid "" -"Picks the first instance of an adLib per rundown, identified by uniqueness " -"Id" +msgid "Disable" msgstr "" -msgid "URL" +msgid "Disable Context Menu" msgstr "" -msgid "Display Rank" +msgid "Disable follow take" msgstr "" -msgid "Role" +msgid "Disable hints by adding this to the URL:" msgstr "" -msgid "Adlib Rank" +msgid "Disable next Piece" msgstr "" -msgid "Place label below panel" +msgid "Disable the hover Inspector when hovering over the button" msgstr "" -msgid "Disabled" +msgid "Disable the next element" msgstr "" -msgid "Show segment name" +msgid "Disable version check" msgstr "" -msgid "Show part title" +msgid "Disabled" msgstr "" -msgid "Hide for dynamically inserted parts" +msgid "Disabling next Piece" msgstr "" -msgid "Planned Start Text" +msgid "Disconnected" msgstr "" -msgid "Text to show above show start time" +msgid "Dismiss" msgstr "" -msgid "Hide Diff" +msgid "Dismiss all notifications" msgstr "" -msgid "Hide Planned Start" +msgid "Display AB channel assignments on:" msgstr "" -msgid "Planned End text" +msgid "Display AdLibs in a column in List View" msgstr "" -msgid "Text to show above show end time" +msgid "Display in a column in List View" msgstr "" -msgid "Hide Planned End Label" +msgid "Display name of the Package Container" msgstr "" -msgid "Hide Diff Label" +msgid "Display piece duration for source layers" msgstr "" -msgid "Hide Countdown" +msgid "Display Rank" msgstr "" -msgid "Hide End Time" +msgid "Display Style" msgstr "" -msgid "Hide Label" +msgid "Display Take buttons" msgstr "" -msgid "Script Source Layers" +msgid "" +"Do you really want to remove just the rundown \"{{rundownName}}\" in the " +"playlist {{playlistName}} from Sofie? \n" +"\n" +"This cannot be undone!" msgstr "" -msgid "Source layers containing script" +msgid "Do you really want to restore the snapshot \"{{snapshotName}}\"?" msgstr "" -msgid "Type" +msgid "Do you want to activate this Rundown?" msgstr "" -msgid "Require Piece on Source Layer" +msgid "" +"Do you want to append these to existing Action Triggers, or do you want to " +"replace them?" msgstr "" -msgid "Text" +msgid "Do you want to do this?" msgstr "" -msgid "Show Rundown Name" +msgid "Do you want to execute {{actionName}}? This may the disrupt the output" msgstr "" -msgid "Segment" +msgid "Do you want to restart CasparCG Server \"{{device}}\"?" msgstr "" -msgid "Part" +msgid "Do you want to restart the Playout Gateway?" msgstr "" -msgid "Show Piece Icon Color" +msgid "Documentation is available at" msgstr "" -msgid "Use color of primary piece as background of panel" +msgid "Documents to be removed:" msgstr "" -msgid "Box color" +msgid "Does NOT support HEAD requests" msgstr "" -msgid "Also Require Source Layers" +msgid "Don't treat the end of the last rundown in a playlist as a break" msgstr "" -msgid "Specify additional layers where at least one layer must have an active piece" +msgid "Done" msgstr "" -msgid "Require All Additional Source Layers" +msgid "Download Action Triggers" msgstr "" -msgid "All additional source layers must have active pieces" +msgid "Drag to reorder or move out of playlist" msgstr "" -msgid "X" +msgid "Drop Rundown here to move it out of its current Playlist" msgstr "" -msgid "Y" +msgid "Dropzone URL" msgstr "" -msgid "Width" +msgid "Duplicate Action Trigger" msgstr "" -msgid "Height" +msgid "Duration" msgstr "" -msgid "Scale" +msgid "DURATION" msgstr "" -msgid "Custom Classes" +msgid "e.g., Studio A,Studio B" msgstr "" -msgid "Device ID" +msgid "Edit" msgstr "" -msgid "Device Type" +msgid "Edit Action Trigger" msgstr "" -msgid "Remove this item?" +msgid "Edit in Nora" msgstr "" -msgid "Are you sure you want to remove {{type}} \"{{deviceId}}\"?" +msgid "Edit mapping" msgstr "" -msgid "Attached Subdevices" +msgid "Edit Mode" msgstr "" -msgid "Expected End text" +msgid "Edit output layer" msgstr "" -msgid "Text to show above countdown to end of show" +msgid "Edit Part Properties" msgstr "" -msgid "Hide Expected End timing when a break is next" +msgid "Edit Piece Properties" msgstr "" -msgid "" -"While there are still breaks coming up in the show, hide the Expected End " -"timers" +msgid "Edit Segment Properties" msgstr "" -msgid "Show next break timing" +msgid "Edit source layer" msgstr "" -msgid "Whether to show countdown to next break" +msgid "Edit Support Panel" msgstr "" -msgid "Last rundown is not break" +msgid "Empty" msgstr "" -msgid "Don't treat the end of the last rundown in a playlist as a break" +msgid "Empty this Bucket" msgstr "" -msgid "Next Break text" +msgid "Emptying Bucket" msgstr "" -msgid "Text to show above countdown to next break" +msgid "Enable" msgstr "" -msgid "Expose as user selectable layout" +msgid "Enable \"Play from Anywhere\"" msgstr "" -msgid "Shelf Layout" +msgid "Enable AdLib Testing, for testing AdLibs before taking the first Part" msgstr "" -msgid "Mini Shelf Layout" +msgid "Enable automatic storage of Rundown Playlist snapshots periodically" msgstr "" -msgid "Rundown Header Layout" +msgid "Enable Buckets" msgstr "" -msgid "Live line countdown requires Source Layer" +msgid "Enable CasparCG restart job" msgstr "" -msgid "" -"One of these source layers must have an active piece for the live line " -"countdown to be show" +msgid "Enable configuration mode by adding ?configure=1 to the address bar." msgstr "" -msgid "Hide Rundown Divider" +msgid "Enable Evaluation Form" msgstr "" -msgid "Hide rundown divider between rundowns in a playlist" +msgid "Enable hints by adding this to the URL:" msgstr "" -msgid "Show Breaks as Segments" +msgid "Enable QuickLoop" msgstr "" -msgid "Segment countdown requires source layer" +msgid "Enable search toolbar" msgstr "" -msgid "" -"One of these source layers must have a piece for the countdown to segment " -"on-air to be show" +msgid "Enable User Editing" msgstr "" -msgid "Fixed duration in Segment header" +msgid "Enabled" msgstr "" -msgid "" -"The segment duration in the segment header always displays the planned " -"duration instead of acting as a counter" +msgid "Enabled on all Parts, applying QuickLoop Fallback Part Duration if needed" msgstr "" -msgid "Select visible Source Layers" +msgid "Enabled, but skipping parts with undefined or 0 duration" msgstr "" -msgid "Select visible Output Groups" +msgid "" +"Enables internal monitoring of blocked main thread. Logs when there is an " +"issue, but (unverified) might cause issues in itself." msgstr "" -msgid "Display piece duration for source layers" +msgid "End of script" msgstr "" -msgid "Piece on selected source layers will have a duration label shown" +msgid "End Words" msgstr "" -msgid "Expose layout as a standalone page" +msgid "Error" msgstr "" -msgid "Open shelf by default" +msgid "Error when checking for cleaning up" msgstr "" -msgid "Default shelf height" +msgid "Error: The ShowStyle of this Rundown was not found." msgstr "" -msgid "Disable Context Menu" +msgid "Error: The studio of this Rundown was not found." msgstr "" -msgid "Show Inspector" +msgid "Est. End" msgstr "" -msgid "Hide default AdLib Start/Execute options" +msgid "Evaluations" msgstr "" -msgid "Only custom trigger modes will be shown" +msgid "Exclusivity group" msgstr "" -msgid "This action has an invalid combination of filters" +msgid "Exclusivity Group ID" msgstr "" -msgid "Use Trigger Mode" +msgid "Exclusivity Group Name" msgstr "" -msgid "Trigger Mode" +msgid "Exclusivity Groups" +msgstr "" + +msgid "Execute" +msgstr "" + +msgid "Execute User Operation" +msgstr "" + +msgid "Executed {{actionName}} on device \"{{deviceName}}\": {{response}}" +msgstr "" + +msgid "Executed {{actionName}} on device \"{{deviceName}}\"..." +msgstr "" + +msgid "Executes within the currently open Rundown, requires a Client-side trigger." +msgstr "" + +msgid "Execution times" +msgstr "" + +msgid "Exit" +msgstr "" + +msgid "Expectation Manager" +msgstr "" + +msgid "Expected End" +msgstr "" + +msgid "Expected End text" +msgstr "" + +msgid "Expected End Time" +msgstr "" + +msgid "Expected Start" +msgstr "" + +msgid "Export" +msgstr "" + +msgid "Export visible" +msgstr "" + +msgid "Expose as user selectable layout" +msgstr "" + +msgid "Expose layout as a standalone page" +msgstr "" + +msgid "External message queue has unsent messages." +msgstr "" + +msgid "Failed to activate" +msgstr "" + +msgid "Failed to add a new Show Style Variant: {{errorMessage}}" +msgstr "" + +msgid "Failed to assign AB player for {{pieceNames}}" +msgstr "" + +msgid "Failed to assign non-critical AB player for {{pieceNames}}" +msgstr "" + +msgid "Failed to compare config changes" +msgstr "" + +msgid "Failed to copy Show Style Variant: {{errorMessage}}" +msgstr "" + +msgid "Failed to delete Show Style Variant: {{errorMessage}}" +msgstr "" + +msgid "" +"Failed to execute {{actionName}} on device: \"{{deviceName}}\": " +"{{errorMessage}}" +msgstr "" + +msgid "Failed to execute take" +msgstr "" + +msgid "Failed to generate adlib rundown! {{message}}" +msgstr "" + +msgid "Failed to import new Show Style Variants: {{errorMessage}}" +msgstr "" + +msgid "" +"Failed to import Show Style Variant {{name}}. Make sure it is not already " +"imported." +msgstr "" + +msgid "Failed to remove all Show Style Variants: {{errorMessage}}" +msgstr "" + +msgid "Failed to reorderShow Style Variant: {{errorMessage}}" +msgstr "" + +msgid "Failed to reset OAuth credentials: {{errorMessage}}" +msgstr "" + +msgid "Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}" +msgstr "" + +msgid "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}" +msgstr "" + +msgid "Failed to update blueprints: {{errorMessage}}" +msgstr "" + +msgid "Failed to update config: {{errorMessage}}" +msgstr "" + +msgid "Failed to upload OAuth credentials: {{errorMessage}}" +msgstr "" + +msgid "Failed to upload shelf layout: {{errorMessage}}" +msgstr "" + +msgid "Failed to validate config" +msgstr "" + +msgid "Fatal" +msgstr "" + +msgid "File path to the folder of the local folder" +msgstr "" + +msgid "Filter by Output Layer" +msgstr "" + +msgid "Filter by Source Layer" +msgstr "" + +msgid "Filter disabled" +msgstr "" + +msgid "Filter Disabled" +msgstr "" + +msgid "Filter..." +msgstr "" + +msgid "Filters" +msgstr "" + +msgid "Find Trigger..." +msgstr "" + +msgid "Fine scroll" +msgstr "" + +msgid "Fix Up Config" +msgstr "" + +msgid "Fixed duration in Segment header" +msgstr "" + +msgid "" +"Fixing this problem requires a restart to the host device. Are you sure you " +"want to restart {{device}}?\n" +"(This might affect output)" +msgstr "" + +msgid "Floated Adlib" +msgstr "" + +msgid "Floated AdLib" +msgstr "" + +msgid "Folder path" +msgstr "" + +msgid "Folder path to shared folder" +msgstr "" + +msgid "Following" +msgstr "" + +msgid "Font size" +msgstr "" + +msgid "for {{name}} fix skipped successfully" +msgstr "" + +msgid "Force" +msgstr "" + +msgid "Force (deactivate others)" +msgstr "" + +msgid "Force Migration" +msgstr "" + +msgid "Force Migration (unsafe)" +msgstr "" + +msgid "Force the Multi-gateway-mode" +msgstr "" + +msgid "Forward" +msgstr "" + +msgid "Forward: {{forward}}" +msgstr "" + +msgid "Found no future pieces" +msgstr "" + +msgid "Frame Rate" +msgstr "" + +msgid "From" +msgstr "" + +msgid "Full System Snapshot" +msgstr "" + +msgid "fullscreen" +msgstr "" + +msgid "Gateway" +msgstr "" + +msgid "General" +msgstr "" + +msgid "Generated URL" +msgstr "" + +msgid "Generating restart token" +msgstr "" + +msgid "Generic Properties" +msgstr "" + +msgid "Generic Script" +msgstr "" + +msgid "Getting Started" +msgstr "" + +msgid "Global AdLib" +msgstr "" + +msgid "Global AdLibs" +msgstr "" + +msgid "Go to Live" +msgstr "" + +msgid "Go to On Air line" +msgstr "" + +msgid "Go to On Air Segment" +msgstr "" + +msgid "Good" +msgstr "" + +msgid "Graphics" +msgstr "" + +msgid "GUI" +msgstr "" + +msgid "he default state of this Route Set" +msgstr "" + +msgid "Heading" +msgstr "" + +msgid "Height" +msgstr "" + +msgid "Help & Support" +msgstr "" + +msgid "Hide" +msgstr "" + +msgid "Hide Countdown" +msgstr "" + +msgid "Hide default AdLib Start/Execute options" +msgstr "" + +msgid "Hide Diff" +msgstr "" + +msgid "Hide Diff Label" +msgstr "" + +msgid "Hide duplicated AdLibs" +msgstr "" + +msgid "Hide End Time" +msgstr "" + +msgid "Hide Expected End timing when a break is next" +msgstr "" + +msgid "Hide for dynamically inserted parts" +msgstr "" + +msgid "Hide Label" +msgstr "" + +msgid "Hide over/under timer" +msgstr "" + +msgid "Hide Panel from view" +msgstr "" + +msgid "Hide Planned End Label" +msgstr "" + +msgid "Hide Planned Start" +msgstr "" + +msgid "Hide Rundown Divider" +msgstr "" + +msgid "Hide rundown divider between rundowns in a playlist" +msgstr "" + +msgid "Hide scrollbar" +msgstr "" + +msgid "Hold" +msgstr "" + +msgid "Hostname or IP address of the Atem" +msgstr "" + +msgid "Hotkey" +msgstr "" + +msgid "How did the show go?" +msgstr "" + +msgid "" +"How many of the transactions to monitor. Set to -1 to log nothing (max " +"performance), 0.5 to log 50% of the transactions, 1 to log all transactions" +msgstr "" + +msgid "" +"How much preparation time to add to global pieces on the timeline before " +"they are played" +msgstr "" + +msgid "HTML that will be shown in the Support Panel" +msgstr "" + +msgid "Human-readable name of the layer" +msgstr "" + +msgid "Icon" +msgstr "" + +msgid "Icon color" +msgstr "" + +msgid "Id" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "" +"ID of the device (corresponds to the device ID in the peripheralDevice " +"settings)" +msgstr "" + +msgid "ID of the timeline-layer to map to some output" +msgstr "" + +msgid "Idempotency-Key is already used" +msgstr "" + +msgid "Idempotency-Key is missing" +msgstr "" + +msgid "If set, only one Route Set will be active per exclusivity group" +msgstr "" + +msgid "" +"If set, Package Manager assumes that the source doesn't support HEAD " +"requests and will use GET instead. If false, HEAD requests will be sent to " +"check availability." +msgstr "" + +msgid "Ignore and apply" +msgstr "" + +msgid "Ignore QuickLoop" +msgstr "" + +msgid "Ignoring take as playing part has changed since TAKE was requested." +msgstr "" + +msgid "Ignoring TAKES that are too quick after eachother ({{duration}} ms)" +msgstr "" + +msgid "Import" +msgstr "" + +msgid "Import error: {{errorMessage}}" +msgstr "" + +msgid "Import file?" +msgstr "" + +msgid "Importing an AdLib to the Bucket" +msgstr "" + +msgid "In" +msgstr "" + +msgid "IN" +msgstr "" + +msgid "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s" +msgstr "" + +msgid "in {{hours}} h {{minutes}} min {{seconds}} s" +msgstr "" + +msgid "in {{minutes}} min {{seconds}} s" +msgstr "" + +msgid "in {{seconds}} s" +msgstr "" + +msgid "In rehearsal" +msgstr "" + +msgid "Include Clear Source Layer in Ad-Libs" +msgstr "" + +msgid "Include Global AdLibs" +msgstr "" + +msgid "Indicate only one next piece per source layer" +msgstr "" + +msgid "Ingest Devices" +msgstr "" + +msgid "Ingest devices are needed to create rundowns" +msgstr "" + +msgid "Ingest from Snapshot" +msgstr "" + +msgid "Ingest Rundown Status" +msgstr "" + +msgid "Ingest Rundown Statuses" +msgstr "" + +msgid "Input Devices" +msgstr "" + +msgid "Input devices allow you to trigger Sofie actions remotely" +msgstr "" + +msgid "Installation name" +msgstr "" + +msgid "Internal Error generating RundownPlaylist" +msgstr "" + +msgid "Internal ID" +msgstr "" + +msgid "Invalid AdLib" +msgstr "" + +msgid "Invalid blueprint: \"{{blueprintId}}\"" +msgstr "" + +msgid "" +"Invalid config preset for blueprint: \"{{configPresetId}}\" " +"({{blueprintId}})" +msgstr "" + +msgid "Invert joystick" +msgstr "" + +msgid "Is a Guest Input" +msgstr "" + +msgid "Is a Live Remote Input" +msgstr "" + +msgid "Is collapsed by default" +msgstr "" + +msgid "Is flattened" +msgstr "" + +msgid "Is hidden" +msgstr "" + +msgid "Is Immutable" +msgstr "" + +msgid "Is PGM Output" +msgstr "" + +msgid "ISA URLs" +msgstr "" + +msgid "Job Status" +msgstr "" + +msgid "Just now" +msgstr "" + +msgid "Key" +msgstr "" + +msgid "Keyboard" +msgstr "" + +msgid "" +"Keyboard shortcuts and Stream Deck buttons will not work while filling out " +"the form!" +msgstr "" + +msgid "Kill (debug)" +msgstr "" + +msgid "Label" +msgstr "" + +msgid "Label contains" +msgstr "" + +msgid "Last" +msgstr "" + +msgid "Last {{layerName}}" +msgstr "" + +msgid "Last modified" +msgstr "" + +msgid "Last rundown is not break" +msgstr "" + +msgid "Last seen" +msgstr "" + +msgid "Last Seen" +msgstr "" + +msgid "Last update" +msgstr "" + +msgid "Last updated" +msgstr "" + +msgid "Layer does not allow sticky pieces!" +msgstr "" + +msgid "Layer ID" +msgstr "" + +msgid "Layer Mappings" +msgstr "" + +msgid "Layer Name" +msgstr "" + +msgid "Leave Unsynced" +msgstr "" + +msgid "Less than a minute ago" +msgstr "" + +msgid "Less than five minutes ago" +msgstr "" + +msgid "Lighting" +msgstr "" + +msgid "Limit" +msgstr "" + +msgid "Live line countdown requires Source Layer" +msgstr "" + +msgid "Live Speak" +msgstr "" + +msgid "Loading" +msgstr "" + +msgid "Loading..." +msgstr "" + +msgid "Local" +msgstr "" + +msgid "Local Time" +msgstr "" + +msgid "Logging level" +msgstr "" + +msgid "Logo" +msgstr "" + +msgid "Lookahead Maximum Search Distance (Undefined = {{limit}})" +msgstr "" + +msgid "Lookahead Mode" +msgstr "" + +msgid "Lookahead Target Objects (Undefined = 1)" +msgstr "" + +msgid "Loop End" +msgstr "" + +msgid "Loop Start" +msgstr "" + +msgid "Loops to Start" +msgstr "" + +msgid "Lower Third" +msgstr "" + +msgid "Manage Snapshots" +msgstr "" + +msgid "Mapping cannot be reset as it has no default values" +msgstr "" + +msgid "Mapping Type" +msgstr "" + +msgid "Mappings" +msgstr "" + +msgid "Margin (%)" +msgstr "" + +msgid "Maximum register limit" +msgstr "" + +msgid "Media" +msgstr "" + +msgid "Media Preview URL" +msgstr "" + +msgid "Media Status" +msgstr "" + +msgid "Media Type" +msgstr "" + +msgid "Memory troubleshooting" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Message" +msgstr "" + +msgid "Message Queue" +msgstr "" + +msgid "Message shown to users in the Evaluations form" +msgstr "" + +msgid "Messages" +msgstr "" + +msgid "Method" +msgstr "" + +msgid "Method ${method}" +msgstr "" + +msgid "MIDI Pedal" +msgstr "" + +msgid "Migrate database" +msgstr "" + +msgid "Migrations" +msgstr "" + +msgid "Mini Shelf Layout" +msgstr "" + +msgid "Mini Shelf Layouts" +msgstr "" + +msgid "Minimum register limit" +msgstr "" + +msgid "Minimum Take Span" +msgstr "" + +msgid "Minor Warning" +msgstr "" + +msgid "Mirror horizontally" +msgstr "" + +msgid "Mirror vertically" +msgstr "" + +msgid "Mock Piece Content Status" +msgstr "" + +msgid "Mode: {{triggerMode}}" +msgstr "" + +msgid "Modify Shift register" +msgstr "" + +msgid "Modifying Bucket" +msgstr "" + +msgid "Modifying Bucket AdLib" +msgstr "" + +msgid "Monitor blocked thread" +msgstr "" + +msgid "More documentation available at:" +msgstr "" + +msgid "More than 10 minutes ago" +msgstr "" + +msgid "More than 2 hours ago" +msgstr "" + +msgid "More than 30 minutes ago" +msgstr "" + +msgid "More than 5 hours ago" +msgstr "" + +msgid "More than a day ago" +msgstr "" + +msgid "Mouse" +msgstr "" + +msgid "Move Next" +msgstr "" + +msgid "Move Next backwards" +msgstr "" + +msgid "Move Next forwards" +msgstr "" + +msgid "Move Next to the following segment" +msgstr "" + +msgid "Move Next to the previous segment" +msgstr "" + +msgid "Move Parts" +msgstr "" + +msgid "Move Segments" +msgstr "" + +msgid "Moving Next" +msgstr "" + +msgid "Multi-gateway-mode delay time" +msgstr "" + +msgid "Multilingual description, editing will overwrite" +msgstr "" + +msgid "My name is {{name}}" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Network address" +msgstr "" + +msgid "Network Id" +msgstr "" + +msgid "New Bucket" +msgstr "" + +msgid "New Filter" +msgstr "" + +msgid "New Layer" +msgstr "" + +msgid "New Layout" +msgstr "" + +msgid "New Output" +msgstr "" + +msgid "New Source" +msgstr "" + +msgid "Next" +msgstr "" + +msgid "Next Break text" +msgstr "" + +msgid "Next Loop at" +msgstr "" + +msgid "Next Part" +msgstr "" + +msgid "Next scheduled show" +msgstr "" + +msgid "Next Segment" +msgstr "" + +msgid "Nintendo Joy-Con" +msgstr "" + +msgid "No" +msgstr "" + +msgid "No Action Triggers set up." +msgstr "" + +msgid "No actions available" +msgstr "" + +msgid "" +"No Ad-Lib matches in the current state of Rundown: " +"\"{{rundownPlaylistName}}\"" +msgstr "" + +msgid "No camera-related source layers found" +msgstr "" + +msgid "No changes" +msgstr "" + +msgid "No gateways are configured" +msgstr "" + +msgid "No matching Action Trigger." +msgstr "" + +msgid "No matching Rundowns available to be used for preview" +msgstr "" + +msgid "No Media matches this filter" +msgstr "" + +msgid "No Media required by this system" +msgstr "" + +msgid "No Media required for this Rundown" +msgstr "" + +msgid "No migrations to apply" +msgstr "" + +msgid "No name set" +msgstr "" + +msgid "No Next point found, please set a part as Next before doing a TAKE." msgstr "" -msgid "Force" +msgid "No notifications" msgstr "" -msgid "Rehearsal" +msgid "No output channels set" msgstr "" -msgid "Mode: {{triggerMode}}" +msgid "No output layers available" msgstr "" -msgid "Undo" +msgid "No PGM output" msgstr "" -msgid "Segments: {{delta}}" +msgid "No problems" msgstr "" -msgid "Parts: {{delta}}" +msgid "No schema has been provided for this mapping" msgstr "" -msgid "Open" +msgid "No source layers available" msgstr "" -msgid "Toggle" +msgid "No source layers set" +msgstr "" + +msgid "No status loaded" +msgstr "" + +msgid "None" +msgstr "" + +msgid "Normal scrolling" +msgstr "" + +msgid "Not Active" +msgstr "" + +msgid "Not Connected" +msgstr "" + +msgid "Not defined" +msgstr "" + +msgid "Not Global" +msgstr "" + +msgid "Not in rehearsal" +msgstr "" + +msgid "Not queueable" +msgstr "" + +msgid "Not set" +msgstr "" + +msgid "Note: Core needs to be restarted to apply these settings" +msgstr "" + +msgid "Notes" +msgstr "" + +msgid "Nothing to cleanup!" +msgstr "" + +msgid "Nothing was found on layer!" +msgstr "" + +msgid "Now Active Rundown" +msgstr "" + +msgid "NRCS Name" +msgstr "" + +msgid "OAuth credentials successfully reset" +msgstr "" + +msgid "OAuth credentials successfully uploaded." +msgstr "" + +msgid "Off" +msgstr "" + +msgid "Off-line devices" +msgstr "" + +msgid "OK" msgstr "" msgid "On" msgstr "" -msgid "Forward: {{forward}}" +msgid "On Air" msgstr "" -msgid "Activate Rundown" +msgid "On Air At" +msgstr "" + +msgid "On Air In" +msgstr "" + +msgid "On Air Start Time" +msgstr "" + +msgid "On release" +msgstr "" + +msgid "OnAir" +msgstr "" + +msgid "" +"One of these source layers must have a piece for the countdown to segment " +"on-air to be show" +msgstr "" + +msgid "" +"One of these source layers must have an active piece for the live line " +"countdown to be show" +msgstr "" + +msgid "Only custom trigger modes will be shown" +msgstr "" + +msgid "Only Display AdLibs from Current Segment" +msgstr "" + +msgid "Only Global" +msgstr "" + +msgid "Only Match Global AdLibs" +msgstr "" + +msgid "" +"Only one rundown can be active at the same time. Currently active rundowns: " +"{{names}}" +msgstr "" + +msgid "Only Pieces present in rundown are sticky" +msgstr "" + +msgid "Open" +msgstr "" + +msgid "Open Camera Screen" +msgstr "" + +msgid "Open Fullscreen" +msgstr "" + +msgid "Open Presenter Screen" +msgstr "" + +msgid "Open Prompter" +msgstr "" + +msgid "Open shelf by default" +msgstr "" + +msgid "Operating Mode" +msgstr "" + +msgid "Operation" +msgstr "" + +msgid "Optional description of the action" +msgstr "" + +msgid "" +"Optionally restrict AB channel display to specific output layers (e.g., " +"only PGM). Leave empty to show for all output layers." +msgstr "" + +msgid "Order 66?" +msgstr "" + +msgid "Original Layer" +msgstr "" + +msgid "Original Layer not found" +msgstr "" + +msgid "Other" +msgstr "" + +msgid "Out" +msgstr "" + +msgid "OUT" +msgstr "" + +msgid "Output channels" +msgstr "" + +msgid "Output Channels" +msgstr "" + +msgid "Output channels are required for your studio to work" +msgstr "" + +msgid "Output Layer" +msgstr "" + +msgid "Over" +msgstr "" + +msgid "Over/Under" +msgstr "" + +msgid "Overflow horizontally" +msgstr "" + +msgid "Overlay Screen" +msgstr "" + +msgid "Package Container ID" +msgstr "" + +msgid "Package Containers" +msgstr "" + +msgid "Package Containers to use for previews" +msgstr "" + +msgid "Package Containers to use for thumbnails" +msgstr "" + +msgid "Package Manager" +msgstr "" + +msgid "Package Manager is offline" +msgstr "" + +msgid "Package Manager status" +msgstr "" + +msgid "Package Manager: Restart Package Container" +msgstr "" + +msgid "Package Manager: Restart work" +msgstr "" + +msgid "Package Status" +msgstr "" + +msgid "Packages" +msgstr "" + +msgid "Parameters" +msgstr "" + +msgid "Parent Config ID" +msgstr "" + +msgid "Parent device is missing" +msgstr "" + +msgid "Parent Devices" +msgstr "" + +msgid "part" +msgstr "" + +msgid "Part" +msgstr "" + +msgid "Part Count Down" +msgstr "" + +msgid "Part Count Up" +msgstr "" + +msgid "Part duration is 0." +msgstr "" + +msgid "Parts Duration" +msgstr "" + +msgid "Parts: {{delta}}" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password for authentication" +msgstr "" + +msgid "Peripheral Device is outdated" +msgstr "" + +msgid "Peripheral Device not found!" +msgstr "" + +msgid "Peripheral Devices" +msgstr "" + +msgid "Pick" +msgstr "" + +msgid "Pick last" +msgstr "" + +msgid "" +"Picks the first instance of an adLib per rundown, identified by uniqueness " +"Id" +msgstr "" + +msgid "Piece on selected source layers will have a duration label shown" msgstr "" -msgid "Ad-Lib" +msgid "Piece to take is already live!" msgstr "" -msgid "Deactivate Rundown" +msgid "Piece to take is not directly playable!" msgstr "" -msgid "Disable next Piece" +msgid "Piece to take was not found!" msgstr "" -msgid "Move Next" +msgid "Pieces on this layer are sticky" msgstr "" -msgid "Reload NRCS Data" +msgid "Pieces on this layer can be cleared" msgstr "" -msgid "Resync with NRCS" +msgid "Place label below panel" msgstr "" -msgid "Shelf" +msgid "Plan. Dur" msgstr "" -msgid "Rewind Segments to start" +msgid "Plan. End" msgstr "" -msgid "Go to On Air line" +msgid "Plan. Start" msgstr "" -msgid "Show entire On Air Segment" +msgid "Planned Duration" msgstr "" -msgid "Queue AdLib from Minishelf" +msgid "Planned End" msgstr "" -msgid "Force (deactivate others)" +msgid "Planned End text" msgstr "" -msgid "Move Segments" +msgid "Planned Start" msgstr "" -msgid "By Segments" +msgid "Planned Start Text" msgstr "" -msgid "Move Parts" +msgid "Play-out" msgstr "" -msgid "By Parts" +msgid "Playlist" msgstr "" -msgid "State" +msgid "Playout Devices" msgstr "" -msgid "Forward" +msgid "Playout devices are needed to control your studio hardware" msgstr "" -msgid "Action" +msgid "Playout devices which uses this package container" msgstr "" -msgid "Ad-Lib Action" +msgid "Playout Gateway \"{{playoutDeviceName}}\" is now restarting." msgstr "" -msgid "Clear Source Layer" +msgid "" +"Please check the database related to the warnings above. If neccessary, you " +"can" msgstr "" -msgid "Sticky Piece" +msgid "Please explain the problems you experienced" msgstr "" -msgid "Global AdLibs" +msgid "Please note: This action is irreversible!" msgstr "" -msgid "Label" +msgid "Pool name" msgstr "" -msgid "Limit" +msgid "Pool PlayerId" msgstr "" -msgid "Output Layer" +msgid "Prepare Studio and Activate (Rehearsal)" msgstr "" -msgid "Pick" +msgid "Preparing for broadcast" msgstr "" -msgid "Pick last" +msgid "Preparing, please wait..." msgstr "" -msgid "Source Layer" +msgid "Presenter Layout" msgstr "" -msgid "Source Layer Type" +msgid "Presenter screen" msgstr "" -msgid "Tag" +msgid "Presenter Screen" msgstr "" -msgid "Not Global" +msgid "Presenter View Layouts" msgstr "" -msgid "Only Global" +msgid "Preserve position of segments when unsynced relative to other segments" msgstr "" -msgid "OnAir" +msgid "Previous" msgstr "" -msgid "Now active rundown" +msgid "Previous work status reasons" msgstr "" -msgid "View" +msgid "Prioritizing Media Workflow" msgstr "" -msgid "Executes within the currently open Rundown, requires a Client-side trigger." +msgid "Priority" msgstr "" -msgid "Unknown" +msgid "Problems" msgstr "" -msgid "Restore Deleted Action" +msgid "Profile name to be used by FileFlow when exporting the clips" msgstr "" -msgid "Select Action" +msgid "Prompter" msgstr "" -msgid "Reset Action" +msgid "Prompter Screen" msgstr "" -msgid "Duplicate Action Trigger" +msgid "Properties" msgstr "" -msgid "Edit Action Trigger" +msgid "Quantel FileFlow Profile name" msgstr "" -msgid "Delete Action Trigger" +msgid "Quantel FileFlow URL" msgstr "" -msgid "" -"No Ad-Lib matches in the current state of Rundown: " -"\"{{rundownPlaylistName}}\"" +msgid "Quantel gateway URL" msgstr "" -msgid "No matching Rundowns available to be used for preview" +msgid "Quantel transformer URL" msgstr "" -msgid "Multilingual description, editing will overwrite" +msgid "Quantel Zone ID" msgstr "" -msgid "Optional description of the action" +msgid "Queue AdLib from Minishelf" msgstr "" -msgid "Triggered Actions uploaded successfully." +msgid "Queue all adlibs" msgstr "" -msgid "Triggered Actions failed to upload: {{errorMessage}}" +msgid "Queue segment" msgstr "" -msgid "Append or Replace" +msgid "Queue this AdLib" msgstr "" -msgid "" -"Do you want to append these to existing Action Triggers, or do you want to " -"replace them?" +msgid "Queued Messages" msgstr "" -msgid "Append" +msgid "Queueing next Segment" msgstr "" -msgid "Action Triggers" +msgid "Quick Links" msgstr "" -msgid "Find Trigger..." +msgid "QuickLoop Fallback Part Duration" msgstr "" -msgid "No matching Action Trigger." +msgid "Range: Forward max" msgstr "" -msgid "No Action Triggers set up." +msgid "Range: Neutral max" msgstr "" -msgid "System-wide" +msgid "Range: Neutral min" msgstr "" -msgid "Upload stored Action Triggers" +msgid "Range: Reverse min" msgstr "" -msgid "Download Action Triggers" +msgid "Rate limit exceeded" msgstr "" -msgid "On release" +msgid "Re-check" msgstr "" -msgid "Empty" +msgid "Re-sync" msgstr "" -msgid "Hotkey" +msgid "Re-Sync" msgstr "" -msgid "Device" +msgid "Re-sync Rundown" msgstr "" -msgid "There was an error: {{error}}" +msgid "Re-sync rundown data with {{nrcsName}}" msgstr "" -msgid "Package Manager status" +msgid "Re-Sync rundown?" msgstr "" -msgid "Reload statuses" +msgid "Re-Syncing Rundown" msgstr "" -msgid "Updated" +msgid "Re-Syncing Rundown Playlist" msgstr "" -msgid "Package Manager" +msgid "Read marker position" msgstr "" -msgid "Expectation Manager" +msgid "Reads the ingest (NRCS) data, and pipes it through the blueprints" msgstr "" -msgid "Statistics" +msgid "Ready" msgstr "" -msgid "Times" +msgid "Reconnect now" msgstr "" -msgid "Connected Workers" +msgid "Reconnecting to the {{platformName}}" msgstr "" -msgid "Work-in-progress" +msgid "Refreshing debug states" msgstr "" -msgid "WorkForce" +msgid "Register ID" msgstr "" -msgid "Kill (debug)" +msgid "Rehearsal" msgstr "" -msgid "Connected Expectation Managers" +msgid "Rehearsal mode is already active" msgstr "" -msgid "Connected App Containers" +msgid "Rehearsal mode is not allowed" msgstr "" -msgid "No status loaded" +msgid "Rehearsal State" msgstr "" -msgid "Peripheral Device is outdated" +msgid "Reload {{nrcsName}} Data" msgstr "" -msgid "" -"The config UI is now driven by manifests fed by the device. This device " -"needs updating to provide the configManifest to be configurable" +msgid "Reload Baseline" msgstr "" -msgid "Are you sure you want to restart this device?" +msgid "Reload NRCS Data" msgstr "" -msgid "Restart this Device?" +msgid "Reload statuses" msgstr "" -msgid "Check the console for troubleshooting data from device \"{{deviceName}}\"!" +msgid "Reloading Rundown Playlist Data" msgstr "" -msgid "" -"There was an error when troubleshooting the device: \"{{deviceName}}\": " -"{{errorMessage}}" +msgid "Rem. Dur" msgstr "" -msgid "Generic Properties" +msgid "Remote" msgstr "" -msgid "Device Name" +msgid "Remote Source" msgstr "" -msgid "Restart Device" +msgid "Remote Speak" msgstr "" -msgid "Troubleshoot" +msgid "Remove" msgstr "" -msgid "Reset Database Version" +msgid "Remove all Show Style Variants?" msgstr "" -msgid "" -"Are you sure you want to reset the database version?\n" -"Only do this if you plan on running the migration right after." +msgid "Remove all trimming" msgstr "" -msgid "Version for {{name}}: From {{fromVersion}} to {{toVersion}}" +msgid "Remove in-trimming" msgstr "" -msgid "Re-check" +msgid "Remove indexes" msgstr "" -msgid "Reset All Versions" +msgid "Remove old data" msgstr "" -msgid "Migrate database" +msgid "Remove old data from database" msgstr "" -msgid "All steps" +msgid "Remove out-trimming" msgstr "" -msgid "" -"The migration consists of several phases, you will get more options after " -"you've this migration" +msgid "Remove rundown" msgstr "" -msgid "The migration can be completed automatically." +msgid "Remove Snapshot" msgstr "" -msgid "Run automatic migration procedure" +msgid "Remove this AB PLayers from this Route Set?" msgstr "" -msgid "" -"The migration procedure needs some help from you in order to complete, see " -"below:" +msgid "Remove this device?" msgstr "" -msgid "Double-check Values" +msgid "Remove this Device?" msgstr "" -msgid "Are you sure the values you have entered are correct?" +msgid "Remove this Exclusivity Group?" msgstr "" -msgid "Run Migration Procedure" +msgid "Remove this item?" msgstr "" -msgid "Warnings During Migration" +msgid "Remove this mapping?" msgstr "" -msgid "" -"Please check the database related to the warnings above. If neccessary, you " -"can" +msgid "Remove this Package Container Accessor?" msgstr "" -msgid "Force Migration" +msgid "Remove this Package Container?" msgstr "" -msgid "" -"Are you sure you want to force the migration? This will bypass the " -"migration checks, so be sure to verify that the values in the settings are " -"correct!" +msgid "Remove this Route from this Route Set?" msgstr "" -msgid "Force Migration (unsafe)" +msgid "Remove this Route Set?" msgstr "" -msgid "The migration was completed successfully!" +msgid "Remove this Show Style Variant?" msgstr "" -msgid "All is well, go get a" +msgid "Removing Bucket" msgstr "" -msgid "New Layout" +msgid "Removing Bucket AdLib" msgstr "" -msgid "Button" +msgid "Removing Rundown" msgstr "" -msgid "New Filter" +msgid "Removing Rundown Playlist" msgstr "" -msgid "Delete layout?" +msgid "Rename this AdLib" msgstr "" -msgid "Are you sure you want to delete the shelf layout \"{{name}}\"?" +msgid "Rename this Bucket" msgstr "" -msgid "Action Buttons" +msgid "Reording Rundowns in Playlist" msgstr "" -msgid "Toggled Label" +msgid "Replace" msgstr "" -msgid "Icon" +msgid "Replace Blueprints?" msgstr "" -msgid "Icon color" +msgid "Replace rows" msgstr "" -msgid "Use as default" +msgid "Require All Additional Source Layers" msgstr "" -msgid "Filters" +msgid "Require Piece on Source Layer" msgstr "" -msgid "There are no filters set up yet" +msgid "Reset" msgstr "" -msgid "Default Layout" +msgid "Reset Action" msgstr "" -msgid "Add {{filtersTitle}}" +msgid "Reset All Versions" msgstr "" -msgid "Add filter" +msgid "Reset and Activate \"On Air\"" msgstr "" -msgid "Add button" +msgid "Reset App Credentials" msgstr "" -msgid "Upload Layout?" +msgid "Reset Database Version" msgstr "" -msgid "Upload" +msgid "Reset mapping to default values" msgstr "" -msgid "" -"Are you sure you want to upload the shelf layout from the file " -"\"{{fileName}}\"?" +msgid "Reset Package Container to default values" msgstr "" -msgid "Shelf layout uploaded successfully." +msgid "Reset row to default values" msgstr "" -msgid "Failed to upload shelf layout: {{errorMessage}}" +msgid "Reset Rundown" msgstr "" -msgid "Studios" +msgid "Reset Sort Order" msgstr "" -msgid "Show Styles" +msgid "Reset source layer to default values" msgstr "" -msgid "Blueprints" +msgid "Reset this item?" msgstr "" -msgid "Devices" +msgid "Reset this mapping?" msgstr "" -msgid "Tools" +msgid "Reset this Package Container?" msgstr "" -msgid "Core System settings" +msgid "Reset to default" msgstr "" -msgid "Upgrade Database" +msgid "Reset User Credentials" msgstr "" -msgid "Manage Snapshots" +msgid "Resetting and activating Rundown Playlist" msgstr "" -msgid "Delete this Studio?" +msgid "Resetting Playlist to default order" msgstr "" -msgid "Are you sure you want to delete the studio \"{{studioId}}\"?" +msgid "Resetting Rundown Playlist" msgstr "" -msgid "Attached Devices" +msgid "Resource Id" msgstr "" -msgid "Layer Mappings" +msgid "Restart" msgstr "" -msgid "Route Sets" +msgid "Restart {{device}}" msgstr "" -msgid "Unnamed Studio" +msgid "Restart All Jobs" msgstr "" -msgid "Delete this Show Style?" +msgid "Restart CasparCG Server" msgstr "" -msgid "Are you sure you want to delete the show style \"{{showStyleId}}\"?" +msgid "Restart Container" msgstr "" -msgid "Source/Output Layers" +msgid "Restart Device" msgstr "" -msgid "Custom Hotkey Labels" +msgid "Restart Playout" msgstr "" -msgid "Variants" +msgid "Restart this Device?" msgstr "" -msgid "Unnamed Show Style" +msgid "Restart this system?" msgstr "" -msgid "Delete this Blueprint?" +msgid "Restarting Media Workflow" msgstr "" -msgid "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?" +msgid "Restarting Sofie Core" msgstr "" -msgid "Unnamed blueprint" +msgid "Restore" msgstr "" -msgid "Version" +msgid "Restore Deleted Action" msgstr "" -msgid "Remove this Device?" +msgid "Restore from Snapshot File" msgstr "" -msgid "" -"Are you sure you want to remove the device \"{{deviceName}}\" and all of " -"it's sub-devices?" +msgid "Restore from Stored Snapshots" msgstr "" -msgid "Connected" +msgid "Restore from this Snapshot file?" msgstr "" -msgid "Disconnected" +msgid "Restore Part from NRCS" msgstr "" -msgid "Good" +msgid "Restore Segment from NRCS" msgstr "" -msgid "Minor Warning" +msgid "Restore Snapshot" msgstr "" -msgid "Warning" +msgid "Resync with NRCS" msgstr "" -msgid "Bad" +msgid "Retry" msgstr "" -msgid "Fatal" +msgid "Return to list" msgstr "" -msgid "Show Style Base Name" +msgid "Reveal in Shelf" msgstr "" -msgid "Blueprint" +msgid "Reverse speed map" msgstr "" -msgid "Blueprint not set" +msgid "Reverse speed map (left trigger)" msgstr "" -msgid "Blueprint config preset" +msgid "Rewind all Segments" msgstr "" -msgid "Blueprint config preset not set" +msgid "Rewind segments to start" msgstr "" -msgid "Blueprint config preset is missing" +msgid "Rewind Segments to start" msgstr "" -msgid "Compatible Studios:" +msgid "Right hand offset" msgstr "" -msgid "This Show Style is not compatible with any Studio" +msgid "Role" msgstr "" -msgid "Key" +msgid "Route Set" msgstr "" -msgid "Host Key" +msgid "Route Set ID" msgstr "" -msgid "Source Layer type" +msgid "Route Set Name" msgstr "" -msgid "Key color" +msgid "Route Sets" msgstr "" -msgid "AHK" +msgid "Route Type" msgstr "" -msgid "New Output" +msgid "Routed Mappings" msgstr "" -msgid "Output channels are required for your studio to work" +msgid "Routes" msgstr "" -msgid "Output channels" +msgid "Row cannot be reset as it has no default values" msgstr "" -msgid "No output channels set" +msgid "Run automatic migration procedure" msgstr "" -msgid "No PGM output" +msgid "Run Migrations to get set up" msgstr "" -msgid "Delete this output?" +msgid "Rundown" msgstr "" -msgid "Are you sure you want to delete output layer \"{{outputId}}\"?" +msgid "" +"Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data " +"from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced " +"or remove the rundown from Sofie. What do you want to do?" msgstr "" -msgid "Channel Name" +msgid "Rundown & Shelf" msgstr "" -msgid "Internal ID" +msgid "Rundown filter" msgstr "" -msgid "Is PGM Output" +msgid "Rundown for piece \"{{pieceLabel}}\" could not be found." msgstr "" -msgid "Is collapsed by default" +msgid "Rundown Global Piece Prepare Time" msgstr "" -msgid "Is flattened" +msgid "Rundown Header Layout" msgstr "" -msgid "Camera" +msgid "Rundown Header Layouts" msgstr "" -msgid "Graphics" +msgid "Rundown is already doing a HOLD!" msgstr "" -msgid "Live Speak" +msgid "Rundown must be active!" msgstr "" -msgid "Lower Third" +msgid "Rundown must be playing or have a next!" msgstr "" -msgid "Studio Microphone" +msgid "Rundown must be playing!" msgstr "" -msgid "Remote Source" +msgid "Rundown Name" msgstr "" -msgid "Generic Script" +msgid "Rundown not found" msgstr "" -msgid "Split Screen" +msgid "" +"Rundown Playlist is active, please deactivate before preparing it for " +"broadcast" msgstr "" -msgid "Clips" +msgid "Rundown Playlist is active, please deactivate it before regenerating it." msgstr "" -msgid "Unknown Layer" +msgid "Rundown Playlist names to store" msgstr "" -msgid "Audio Mixing" +msgid "Rundown Playlist not found!" msgstr "" -msgid "Transition" +msgid "Rundown View Layouts" msgstr "" -msgid "Lights" +msgid "" +"RundownPlaylist is active but not in rehearsal, please deactivate it or set " +"in in rehearsal to be able to reset it." msgstr "" -msgid "Local" +msgid "Rundowns" msgstr "" -msgid "New Source" +msgid "Save" msgstr "" -msgid "Add some source layers (e.g. Graphics) for your data to appear in rundowns" +msgid "Save Changes" msgstr "" -msgid "No source layers set" +msgid "Save to Bucket" msgstr "" -msgid "Delete this item?" +msgid "Saving AdLib to Bucket" msgstr "" -msgid "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?" +msgid "Saving Evaluation" msgstr "" -msgid "Source Name" +msgid "Scale" msgstr "" -msgid "Source Abbreviation" +msgid "Script is empty" msgstr "" -msgid "Source Type" +msgid "Script Source Layers" msgstr "" -msgid "Is a Live Remote Input" +msgid "Search..." msgstr "" -msgid "Is a Guest Input" +msgid "Seg. Budg." msgstr "" -msgid "Is hidden" +msgid "segment" msgstr "" -msgid "Display in a column in List View" +msgid "Segment" msgstr "" -msgid "Display AdLibs in a column in List View" +msgid "Segment Count Down" msgstr "" -msgid "Pieces on this layer can be cleared" +msgid "Segment Count Up" msgstr "" -msgid "Pieces on this layer are sticky" +msgid "Segment countdown requires source layer" msgstr "" -msgid "Only Pieces present in rundown are sticky" +msgid "Segment no longer exists in {{nrcs}}" msgstr "" -msgid "Allow disabling of Pieces" +msgid "Segment was hidden in {{nrcs}}" msgstr "" -msgid "AdLibs on this layer can be queued" +msgid "Segments: {{delta}}" msgstr "" -msgid "Exclusivity group" +msgid "" +"Select a presenter layout. Leave as default to use the first available " +"layout." msgstr "" -msgid "Unnamed variant" +msgid "Select Action" msgstr "" -msgid "" -"Failed to import Show Style Variant {{name}}. Make sure it is not already " -"imported." +msgid "Select Compatible Show Styles" msgstr "" -msgid "Failed to import new Show Style Variants: {{errorMessage}}" +msgid "Select image" msgstr "" -msgid "Failed to copy Show Style Variant: {{errorMessage}}" +msgid "" +"Select one or more control modes. Leave all unchecked for default (mouse + " +"keyboard)." msgstr "" -msgid "Failed to add a new Show Style Variant: {{errorMessage}}" +msgid "" +"Select source layers to display. Leave all unchecked to show all " +"camera-related layers." msgstr "" -msgid "Remove this Show Style Variant?" +msgid "Select visible Output Groups" msgstr "" -msgid "Failed to delete Show Style Variant: {{errorMessage}}" +msgid "Select visible Source Layers" msgstr "" -msgid "Are you sure you want to remove the Variant \"{{showStyleVariantId}}\"?" +msgid "Select which playout devices are using this package container" msgstr "" -msgid "Failed to remove all Show Style Variants: {{errorMessage}}" +msgid "Send message" msgstr "" -msgid "Remove all Show Style Variants?" +msgid "Send message and Deactivate Rundown" msgstr "" -msgid "Are you sure you want to remove all Variants in the table?" +msgid "Sent Messages" msgstr "" -msgid "Failed to reorderShow Style Variant: {{errorMessage}}" +msgid "Server" msgstr "" -msgid "Show Style Variants" +msgid "Server {{id}}" msgstr "" -msgid "Restore from this Snapshot file?" +msgid "Server ID" msgstr "" msgid "" -"Are you sure you want to restore the system from the snapshot file " -"\"{{fileName}}\"?" +"Server ID. For sources, this should generally be omitted (or set to 0) so " +"clip-searches are zone-wide. If set, clip-searches are limited to that " +"server." msgstr "" -msgid "Successfully restored snapshot" +msgid "Set" msgstr "" -msgid "Snapshot restore failed: {{errorMessage}}" +msgid "Set as QuickLoop End" msgstr "" -msgid "Full System Snapshot" +msgid "Set as QuickLoop Start" msgstr "" -msgid "" -"A Full System Snapshot contains all system settings (studios, showstyles, " -"blueprints, devices, etc.)" +msgid "Set In & Out points" msgstr "" -msgid "Take a Full System Snapshot" +msgid "Set part as Next" msgstr "" -msgid "Studio Snapshot" +msgid "Set segment as Next" msgstr "" -msgid "A Studio Snapshot contains all system settings related to that studio" +msgid "Setting as QuickLoop End" msgstr "" -msgid "Take a Snapshot for studio \"{{studioName}}\" only" +msgid "Setting as QuickLoop Start" msgstr "" -msgid "Restore from Snapshot File" +msgid "Setting Next" msgstr "" -msgid "Upload Snapshot" +msgid "Setting Next Segment" msgstr "" -msgid "Restore from Stored Snapshots" +msgid "Settings" msgstr "" -msgid "Restore" +msgid "Shelf" msgstr "" -msgid "Show \"Remove snapshots\"-buttons" +msgid "Shelf Layout" msgstr "" -msgid "Studio Baseline needs update: " +msgid "Shelf layout uploaded successfully." msgstr "" -msgid "Baseline needs reload, this studio may not work until reloaded" +msgid "Shelf Layouts" msgstr "" -msgid "Reload Baseline" +msgid "Shortcuts" msgstr "" -msgid "Remove this device?" +msgid "Show \"Remove snapshots\"-buttons" msgstr "" -msgid "Are you sure you want to remove device \"{{deviceId}}\"?" +msgid "Show All" msgstr "" -msgid "Devices are needed to control your studio hardware" +msgid "Show Breaks as Segments" msgstr "" -msgid "No devices connected" +msgid "Show config changes" msgstr "" -msgid "Playout gateway not connected" +msgid "Show End" msgstr "" -msgid "None" +msgid "Show entire On Air Segment" msgstr "" -msgid "Studio Name" +msgid "Show Hotkeys" msgstr "" -msgid "Select Compatible Show Styles" +msgid "Show Inspector" msgstr "" -msgid "Show style not set" +msgid "Show issue" msgstr "" -msgid "Click to show available Show Styles" +msgid "Show next break timing" msgstr "" -msgid "Frame Rate" +msgid "Show panel as a timeline" msgstr "" -msgid "Enable \"Play from Anywhere\"" +msgid "Show part title" msgstr "" -msgid "Media Preview URL" +msgid "Show Piece Icon Color" msgstr "" -msgid "Slack Webhook URLs" +msgid "Show Rundown Name" msgstr "" -msgid "Supported Media Formats" +msgid "Show segment name" msgstr "" -msgid "Supported Audio Formats" +msgid "Show Style" msgstr "" -msgid "Force the Multi-gateway-mode" +msgid "Show Style Base Name" msgstr "" -msgid "Multi-gateway-mode delay time" +msgid "Show style not set" msgstr "" -msgid "Preserve contents of playing segment when unsynced" +msgid "Show Style Variant" msgstr "" -msgid "Allow Rundowns to be reset while on-air" +msgid "Show Style Variants" msgstr "" -msgid "" -"Preserve position of segments when unsynced relative to other segments. " -"Note: this has only been tested for the iNews gateway" +msgid "Show Styles" msgstr "" -msgid "Add a playout device to the studio in order to edit the layer mappings" +msgid "Show thumbnails next to list items" msgstr "" -msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" +msgid "ShowStyleBase not found!" msgstr "" -msgid "Remove this mapping?" +msgid "Shuttle Keyboard (Contour ShuttleXpress / X-keys)" msgstr "" -msgid "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?" +msgid "Shuttle WebHID (Contour ShuttleXpress via browser)" msgstr "" -msgid "Layer ID" +msgid "Skip Fix Up Step" msgstr "" -msgid "ID of the timeline-layer to map to some output" +msgid "Slack Webhook URLs" msgstr "" -msgid "Layer Name" +msgid "Smooth scrolling" msgstr "" -msgid "Human-readable name of the layer" +msgid "Snapshot remove failed: {{errorMessage}}" msgstr "" -msgid "The type of device to use for the output" +msgid "Snapshot restore failed: {{errorMessage}}" msgstr "" -msgid "" -"ID of the device (corresponds to the device ID in the peripheralDevice " -"settings)" +msgid "Snapshot restored!" msgstr "" -msgid "Lookahead Mode" +msgid "Sofie" msgstr "" -msgid "Lookahead Target Objects (Undefined = 1)" +msgid "Sofie Automation" msgstr "" -msgid "Lookahead Maximum Search Distance (Undefined = {{limit}})" +msgid "Sofie Automation Server" msgstr "" -msgid "Remove this Package Container?" +msgid "Sofie Automation Server Core" msgstr "" -msgid "Are you sure you want to remove the Package Container \"{{containerId}}\"?" +msgid "Sofie Automation Server Core will restart in {{time}}s..." msgstr "" -msgid "There are no Package Containers set up." +msgid "Sofie Automation Server Core: {{name}}" msgstr "" -msgid "Package Container ID" +msgid "Sofie logo to be displayed in the header. Requires a page refresh." msgstr "" -msgid "Display name/label of the Package Container" +msgid "some invalid reason" msgstr "" -msgid "Playout devices which uses this package container" +msgid "some message" msgstr "" -msgid "Select playout devices" +msgid "some reason" msgstr "" -msgid "Select which playout devices are using this package container" +msgid "something changed" msgstr "" -msgid "Accessors" +msgid "" +"Something went wrong when creating the snapshot. Please contact the system " +"administrator if the problem persists." msgstr "" -msgid "Remove this Package Container Accessor?" +msgid "Something went wrong, and it affected the output" msgstr "" -msgid "" -"Are you sure you want to remove the Package Container Accessor " -"\"{{accessorId}}\"?" +msgid "Something went wrong, but it didn't affect the output" msgstr "" -msgid "There are no Accessors set up." +msgid "" +"Something went wrong, please contact the system administrator if the " +"problem persists." msgstr "" -msgid "Accessor ID" +msgid "Source Abbreviation" msgstr "" -msgid "Display name of the Package Container" +msgid "Source Layer" msgstr "" -msgid "Accessor Type" +msgid "Source layer cannot be reset as it has no default values" msgstr "" -msgid "Folder path" +msgid "Source Layer Type" msgstr "" -msgid "File path to the folder of the local folder" +msgid "Source Layer Types" msgstr "" -msgid "Resource Id" +msgid "Source Layers" msgstr "" -msgid "" -"(Optional) This could be the name of the computer on which the local folder " -"is on" +msgid "Source layers containing script" msgstr "" -msgid "Base URL" +msgid "Source Name" msgstr "" -msgid "Base url to the resource (example: http://myserver/folder)" +msgid "Source Type" msgstr "" -msgid "Network Id" +msgid "Source/Output Layers" msgstr "" -msgid "" -"(Optional) A name/identifier of the local network where the share is " -"located, leave empty if globally accessible" +msgid "Sources" msgstr "" -msgid "Folder path to shared folder" +msgid "Space separated list of style class names to use when displaying the action" msgstr "" -msgid "UserName" +msgid "Specify additional layers where at least one layer must have an active piece" msgstr "" -msgid "Username for athuentication" +msgid "Speed control" msgstr "" -msgid "Password for authentication" +msgid "Speed map" msgstr "" -msgid "(Optional) A name/identifier of the local network where the share is located" +msgid "Speed map (forward, right trigger)" msgstr "" -msgid "Quantel gateway URL" +msgid "Split Screen" msgstr "" -msgid "URL to the Quantel Gateway" +msgid "Splits" msgstr "" -msgid "ISA URLs" +msgid "Standalone Shelf" msgstr "" -msgid "URLs to the ISAs, in order of importance (comma separated)" +msgid "Start Here!" msgstr "" -msgid "Zone ID" +msgid "Start In" msgstr "" -msgid "Zone ID (default value: \"default\")" +msgid "Start this AdLib" msgstr "" -msgid "Server ID" +msgid "Start time is close" msgstr "" msgid "" -"Server ID. For sources, this should generally be omitted (or set to 0) so " -"clip-searches are zone-wide. If set, clip-searches are limited to that " -"server." +"Start with giving this browser configuration permissions by adding this to " +"the URL: " msgstr "" -msgid "Quantel transformer URL" +msgid "Started" msgstr "" -msgid "URL to the Quantel HTTP transformer" +msgid "Starting AdLib" msgstr "" -msgid "Quantel FileFlow URL" +msgid "Starting Bucket AdLib" msgstr "" -msgid "URL to the Quantel FileFlow Manager" +msgid "Starting Global AdLib" msgstr "" -msgid "Quantel FileFlow Profile name" +msgid "Starting Sticky Piece" msgstr "" -msgid "Profile name to be used by FileFlow when exporting the clips" +msgid "State" msgstr "" -msgid "Allow Read access" +msgid "State \"{{state}}\"" msgstr "" -msgid "Allow Write access" +msgid "Statistics" msgstr "" -msgid "Studio Settings" +msgid "Status" msgstr "" -msgid "Package Containers to use for previews" +msgid "Status Messages:" msgstr "" -msgid "Click to show available Package Containers" +msgid "Sticky Piece" msgstr "" -msgid "Package Containers to use for thumbnails" +msgid "Store Snapshot" msgstr "" -msgid "Package Containers" +msgid "Studio" msgstr "" -msgid "Remove this Exclusivity Group?" +msgid "Studio Baseline needs update: " msgstr "" -msgid "" -"Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\n" -"Route Sets assigned to this group will be reset to no group." +msgid "Studio Labels" msgstr "" -msgid "Remove this Route from this Route Set?" +msgid "Studio Name" msgstr "" -msgid "" -"Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to " -"\"{{newLayerId}}\"?" +msgid "Studio not found!" msgstr "" -msgid "Remove this Route Set?" +msgid "Studio Screen Graphics" msgstr "" -msgid "Are you sure you want to remove the Route Set \"{{routeId}}\"?" +msgid "Studio Settings" msgstr "" -msgid "Routes" +msgid "Studio Snapshot" msgstr "" -msgid "There are no routes set up yet" +msgid "Studios" msgstr "" -msgid "Original Layer" +msgid "Style class names" msgstr "" -msgid "New Layer" +msgid "Subtract" msgstr "" -msgid "Route Type" +msgid "Successfully restored snapshot" msgstr "" -msgid "Source Layer not found" +msgid "Successfully stored snapshot" msgstr "" -msgid "There are no exclusivity groups set up." +msgid "Support Panel" msgstr "" -msgid "Exclusivity Group ID" +msgid "Supported Audio Formats" msgstr "" -msgid "Exclusivity Group Name" +msgid "Supported Media Formats" msgstr "" -msgid "Display name of the Exclusivity Group" +msgid "Switch Route Set" msgstr "" -msgid "Active" +msgid "Switch Segment View Mode" msgstr "" -msgid "Not Active" +msgid "Switch to List View" msgstr "" -msgid "Not defined" +msgid "Switch to Storyboard View" msgstr "" -msgid "There are no Route Sets set up." +msgid "Switch to Timeline View" msgstr "" -msgid "Route Set ID" +msgid "Switchboard" msgstr "" -msgid "Is this Route Set currently active" +msgid "Switchboard Panel" msgstr "" -msgid "Default State" +msgid "Switching operating mode to {{mode}}" msgstr "" -msgid "The default state of this Route Set" +msgid "Switching routing" msgstr "" -msgid "Route Set Name" +msgid "System" msgstr "" -msgid "Display name of the Route Set" +msgid "System has issues which need to be resolved" msgstr "" -msgid "If set, only one Route Set will be active per exclusivity group" +msgid "System must have exactly one studio" msgstr "" -msgid "Behavior" +msgid "System Status" msgstr "" -msgid "The way this Route Set should behave towards the user" +msgid "System-wide" msgstr "" -msgid "Add a playout device to the studio in order to configure the route sets" +msgid "System-wide Notification Message" msgstr "" -msgid "" -"Controls for exposed Route Sets will be displayed to the producer within " -"the Rundown View in the Switchboard." +msgid "Table is not allowed to have `properties` defined" msgstr "" -msgid "Exclusivity Groups" +msgid "Table is only allowed the wildcard `patternProperties`" msgstr "" -msgid "Remove indexes" +msgid "Tables are not supported here" msgstr "" -msgid "This will remove {{indexCount}} old indexes, do you want to continue?" +msgid "Tag" msgstr "" -msgid "{{indexCount}} indexes was removed." +msgid "Tags must contain" msgstr "" -msgid "Installation name" +msgid "Take" msgstr "" -msgid "This name will be shown in the title bar of the window" +msgid "Take a Full System Snapshot" msgstr "" -msgid "Logging level" +msgid "Take a Snapshot" msgstr "" -msgid "This affects how much is logged to the console on the server" +msgid "Take a Snapshot for studio \"{{studioName}}\" only" msgstr "" -msgid "System-wide Notification Message" +msgid "Take and Download Memory Heap Snapshot" msgstr "" -msgid "Message" +msgid "Take System Snapshot" msgstr "" -msgid "Enabled" +msgid "Take System Snapshot failed: {{errorMessage}}" msgstr "" -msgid "Edit Support Panel" +msgid "Taking Piece" msgstr "" -msgid "HTML that will be shown in the Support Panel" +msgid "Technical reason: {{reason}}" msgstr "" -msgid "Application Performance Monitoring" +msgid "test" msgstr "" -msgid "APM Enabled" +msgid "Test ERROR message" msgstr "" -msgid "APM Transaction Sample Rate" +msgid "Test Info message" msgstr "" -msgid "" -"How many of the transactions to monitor. Set to -1 to log nothing (max " -"performance), 0.5 to log 50% of the transactions, 1 to log all transactions" +msgid "test partInstance" msgstr "" -msgid "Note: Core needs to be restarted to apply these settings" +msgid "test pieceInstance" msgstr "" -msgid "Monitor blocked thread" +msgid "test playlist" msgstr "" -msgid "Enable" +msgid "test rundown" msgstr "" -msgid "" -"Enables internal monitoring of blocked main thread. Logs when there is an " -"issue, but (unverified) might cause issues in itself." +msgid "Test test" msgstr "" -msgid "Cron jobs" +msgid "Test Tools" msgstr "" -msgid "Enable CasparCG restart job" +msgid "test2" msgstr "" -msgid "Cleanup" +msgid "Text" msgstr "" -msgid "Cleanup old database indexes" +msgid "Text to show above countdown to end of show" msgstr "" -msgid "Cleanup old data" +msgid "Text to show above countdown to next break" msgstr "" -msgid "Disable CasparCG restart job" +msgid "Text to show above show end time" msgstr "" -msgid "Enable automatic storage of Rundown Playlist snapshots periodically" +msgid "Text to show above show start time" msgstr "" -msgid "Filter: If set, only store snapshots for certain rundowns" +msgid "" +"The config UI is now driven by manifests fed by the device. This device " +"needs updating to provide the configManifest to be configurable" msgstr "" -msgid "" -"(Comma separated list. Empty - will store snapshots of all Rundown " -"Playlists)" +msgid "The following parts no longer exist in {{nrcs}}: {{partNames}}" msgstr "" -msgid "Error when checking for cleaning up" +msgid "The migration can be completed automatically." msgstr "" -msgid "Remove old data from database" +msgid "" +"The migration consists of several phases, you will get more options after " +"you've this migration" msgstr "" -msgid "There are {{count}} documents that can be removed, do you want to continue?" +msgid "The migration was completed successfully!" msgstr "" -msgid "Documents to be removed:" +msgid "The old data was removed." msgstr "" -msgid "Retry" +msgid "" +"The planned end time has passed, are you sure you want to activate this " +"Rundown?" msgstr "" -msgid "Remove old data" +msgid "The progress of all steps" msgstr "" -msgid "The old data was removed." +msgid "The progress of steps required for playout" msgstr "" -msgid "Failed to check status." +msgid "" +"The rundown \"{{rundownName}}\" is not published or activated in " +"{{nrcsName}}! No data updates will currently come through." msgstr "" -msgid "Apply blueprint upgrades" +msgid "The rundown can not be reset while it is active" msgstr "" -msgid "Upgrade config for {{name}}" +msgid "" +"The Rundown was attempted to be moved out of the Playlist when it was on " +"Air. Move it back and try again later." msgstr "" -msgid "Config looks good" +msgid "" +"The rundown you are trying to execute a take on is inactive, would you like " +"to activate this rundown?" msgstr "" -msgid "Apply" +msgid "" +"The rundown: \"{{rundownName}}\" will need to be deactivated in order to " +"activate this one.\n" +"\n" +"Are you sure you want to activate this one anyway?" msgstr "" -msgid "Ignore and apply" +msgid "" +"The segment duration in the segment header always displays the planned " +"duration instead of acting as a counter" msgstr "" -msgid "Config for {{name}} upgraded successfully" +msgid "The selected part cannot be played" msgstr "" -msgid "Config for {{name}} upgraded failed" +msgid "The selected part does not exist" msgstr "" -msgid "Failed to validate config" +msgid "" +"The system configuration has been changed since importing this rundown. It " +"might not run correctly" msgstr "" -msgid "No changes" +msgid "The type of device to use for the output" msgstr "" -msgid "Dismiss" +msgid "The type of mapping to use" msgstr "" -msgid "Unable to upgrade" +msgid "The way this Route Set should behave towards the user" msgstr "" -msgid "Upgrade required" +msgid "Then, run the migrations script:" msgstr "" -msgid "Show config changes" +msgid "There are no AB Playout devices set up yet" msgstr "" -msgid "Validate Config" +msgid "There are no Accessors set up." msgstr "" -msgid "Last {{layerName}}" +msgid "There are no active rundowns." msgstr "" -msgid "Clear {{layerName}}" +msgid "There are no exclusivity groups set up." msgstr "" -msgid "Search..." +msgid "There are no filters set up yet" msgstr "" msgid "" -"Are you sure you want to deactivate this Rundown\n" -"(This will clear the outputs)" +"There are no Playout Gateways connected and attached to this studio. Please " +"contact the system administrator to start the Playout Gateway." msgstr "" -msgid "Successfully stored snapshot" +msgid "There are no Route Sets set up." msgstr "" -msgid "End Words" +msgid "There are no routes set up yet" msgstr "" -msgid "Global AdLib" +msgid "There are no rundowns ingested into Sofie." msgstr "" -msgid "AdLib does not provide any options" +msgid "There are no sub-devices for this gateway" msgstr "" -msgid "Execute" +msgid "There is an unknown problem with the part." msgstr "" -msgid "Save to Bucket" +msgid "There is an unspecified problem with the source." msgstr "" -msgid "Reveal in Shelf" +msgid "There is no rundown active in this studio." msgstr "" -msgid "Edit in Nora" +msgid "" +"There was an error when troubleshooting the device: \"{{deviceName}}\": " +"{{errorMessage}}" msgstr "" -msgid "Current Part" +msgid "There was an error: {{error}}" msgstr "" -msgid "Next Part" +msgid "This action has an invalid combination of filters" msgstr "" -msgid "Part Count Down" +msgid "This affects how much is logged to the console on the server" msgstr "" -msgid "Part Count Up" +msgid "This blueprint has not provided a valid config schema" msgstr "" -msgid "Until end of rundown" +msgid "This Blueprint is not being used by any Show Style" msgstr "" -msgid "New Bucket" +msgid "This Blueprint is not compatible with any Studio" msgstr "" -msgid "Are you sure you want to delete this AdLib?" +msgid "This clip ends with black frames after {{seconds}} seconds" msgstr "" -msgid "Are you sure you want to delete this Bucket?" +msgid "This clip ends with freeze frames after {{seconds}} seconds" msgstr "" -msgid "Are you sure you want to empty (remove all adlibs inside) this Bucket?" +msgid "This could leave the configuration in a broken state" msgstr "" -msgid "Current Segment" +msgid "This enables or disables buckets in the UI - enabled is the default behavior" msgstr "" -msgid "Next Segment" +msgid "" +"This enables or disables the evaluationform in the UI - enabled is the " +"default behavior" msgstr "" -msgid "Segment Count Down" +msgid "This feature enables the use of the Properties Panel and the Edit Mode" msgstr "" -msgid "Segment Count Up" +msgid "This has only been tested for the iNews gateway" msgstr "" -msgid "Start this AdLib" +msgid "This is not in it's normal setting" +msgstr "" + +msgid "" +"This migration consists of {{stepCount}} steps ({{ignoredStepCount}} steps " +"are ignored)." msgstr "" -msgid "Queue this AdLib" +msgid "This must be assigned to a device to be able to edit the settings" msgstr "" -msgid "Inspect this AdLib" +msgid "This name will be shown in the title bar of the window" msgstr "" -msgid "Rename this AdLib" +msgid "This playlist is empty" msgstr "" -msgid "Delete this AdLib" +msgid "" +"This requires the blueprints to implement the " +"`generateAdlibTestingIngestRundown` method" msgstr "" -msgid "Empty this Bucket" +msgid "This rundown has been unpublished from Sofie." msgstr "" -msgid "Rename this Bucket" +msgid "This rundown is currently active" msgstr "" -msgid "Delete this Bucket" +msgid "This rundown is now active. Are you sure you want to exit this screen?" msgstr "" -msgid "Create new Bucket" +msgid "This rundown will loop indefinitely" msgstr "" -msgid "AdLib" +msgid "This Show Style is not compatible with any Studio" msgstr "" -msgid "Shortcuts" +msgid "This step is required for playout" msgstr "" -msgid "Show Style Variant" +msgid "This studio doesn't exist." msgstr "" -msgid "Local Time" +msgid "This will remove {{indexCount}} old indexes, do you want to continue?" msgstr "" -msgid "System" +msgid "Time from platform user event to Action received by Core" msgstr "" -msgid "Media" +msgid "Time since planned end" msgstr "" -msgid "Packages" +msgid "Time since rehearsal end" msgstr "" -msgid "Messages" +msgid "Time to planned end" msgstr "" -msgid "User Log" +msgid "Time to planned start" msgstr "" -msgid "Evaluations" +msgid "Time to rehearsal end" msgstr "" -msgid "Debug State" +msgid "Timeline" msgstr "" -msgid "Timestamp" +msgid "Timeline Datastore" msgstr "" -msgid "User Name" +msgid "Times" msgstr "" -msgid "Answers" +msgid "Timestamp" msgstr "" -msgid "Message Queue" +msgid "To inspect the memory heap snapshot, use Chrome DevTools" msgstr "" -msgid "Queued Messages" +msgid "Today" msgstr "" -msgid "Sent Messages" +msgid "Toggle" msgstr "" -msgid "File Copy" +msgid "Toggle AdLibs on single mouse click" msgstr "" -msgid "File Delete" +msgid "Toggle Shelf" msgstr "" -msgid "Check file size" +msgid "Toggle Support Panel" msgstr "" -msgid "Scan File" +msgid "Toggled Label" msgstr "" -msgid "Generate Thumbnail" +msgid "Tomorrow" msgstr "" -msgid "Generate Preview" +msgid "Tools" msgstr "" -msgid "Unknown action: {{action}}" +msgid "Top" msgstr "" -msgid "Done" +msgid "Transition" msgstr "" -msgid "Failed" +msgid "Treat as Main content" msgstr "" -msgid "Working, Media Available" +msgid "Trigger dead zone" msgstr "" -msgid "Working" +msgid "Trigger Mode" msgstr "" -msgid "Pending" +msgid "Triggered Actions failed to upload: {{errorMessage}}" msgstr "" -msgid "Blocked" +msgid "Triggered Actions uploaded successfully." msgstr "" -msgid "Canceled" +msgid "Trim \"{{name}}\"" msgstr "" -msgid "Idle" +msgid "Trimmed successfully." msgstr "" -msgid "Skipped" +msgid "Trimming this clip has failed due to an error: {{error}}." msgstr "" -msgid "Step progress: {{progress}}" +msgid "" +"Trimming this clip has timed out. It's possible that the story is currently " +"locked for writing in {{nrcsName}} and will eventually be updated. Make " +"sure that the story is not being edited by other users." msgstr "" -msgid "Processing" +msgid "" +"Trimming this clip is taking longer than expected. It's possible that the " +"story is locked for writing in {{nrcsName}}." msgstr "" -msgid "Unknown: {{status}}" +msgid "Troubleshoot" msgstr "" -msgid "Collapse" +msgid "TSR" msgstr "" -msgid "Details" +msgid "Type" msgstr "" -msgid "Abort" +msgid "Unable to check the system configuration for changes" msgstr "" -msgid "Prioritize" +msgid "Unable to upgrade" msgstr "" -msgid "Media Transfer Status" +msgid "Unassign" msgstr "" -msgid "Abort All" +msgid "Unconfigured" msgstr "" -msgid "Restart All" +msgid "Under" msgstr "" -msgid "Unknown Package \"{{packageId}}\"" +msgid "Undo" msgstr "" -msgid "Package Status" +msgid "Undo Disable the next element" msgstr "" -msgid "Package container status" +msgid "Undo Hold" msgstr "" -msgid "Id" +msgid "Unknown" msgstr "" -msgid "Work status" +msgid "Unknown action" msgstr "" -msgid "Restart All jobs" +msgid "Unknown error" msgstr "" -msgid "Created" +msgid "Unknown Layer" msgstr "" -msgid "Ready" +msgid "Unknown Package \"{{packageId}}\"" msgstr "" -msgid "The progress of steps required for playout" +msgid "Unnamed blueprint" msgstr "" -msgid "The progress of all steps" +msgid "Unnamed Show Style" msgstr "" -msgid "This step is required for playout" +msgid "Unnamed Studio" msgstr "" -msgid "Work description" +msgid "Unnamed variant" msgstr "" -msgid "Work status reason" +msgid "Unsupported array type \"{{ type }}\"" msgstr "" -msgid "Technical reason: {{reason}}" +msgid "Unsupported field type \"{{ type }}\"" msgstr "" -msgid "Previous work status reasons" +msgid "Unsyncing Rundown" msgstr "" -msgid "Priority" +msgid "Until" msgstr "" -msgid "Not Connected" +msgid "Until end of rundown" msgstr "" -msgid "MOS Gateway" +msgid "Until End of Rundown" msgstr "" -msgid "Play-out Gateway" +msgid "Until end of segment" msgstr "" -msgid "Media Manager" +msgid "Until End of Segment" msgstr "" -msgid "Unknown Device" +msgid "Until end of showstyle" msgstr "" -msgid "Executed {{actionName}} on device \"{{deviceName}}\": {{response}}" +msgid "Until End of Showstyle" msgstr "" -msgid "Executed {{actionName}} on device \"{{deviceName}}\"..." +msgid "Until next rundown" msgstr "" -msgid "" -"Failed to execute {{actionName}} on device: \"{{deviceName}}\": " -"{{errorMessage}}" +msgid "Until Next Rundown" msgstr "" -msgid "Do you want to execute {{actionName}}? This may the disrupt the output" +msgid "Until next segment" msgstr "" -msgid "Last seen" +msgid "Until Next Segment" msgstr "" -msgid "Connect some devices to the playout gateway" +msgid "Until next take" msgstr "" -msgid "Are you sure you want to delete this device: \"{{deviceId}}\"?" +msgid "Until Next Take" msgstr "" -msgid "Sofie Automation Server Core: {{name}}" +msgid "Update" msgstr "" -msgid "Restart this system?" +msgid "Update Blueprints?" msgstr "" -msgid "" -"Are you sure you want to restart this Sofie Automation Server Core: " -"{{name}}?" +msgid "Updated" msgstr "" -msgid "Could not generate restart token!" +msgid "Upgrade config for {{name}}" msgstr "" -msgid "Could not generate restart core: {{err}}" +msgid "Upgrade Database" msgstr "" -msgid "Sofie Automation Server Core will restart in {{time}}s..." +msgid "Upgrade required" msgstr "" -msgid "Execution times" +msgid "Upgrade Status" msgstr "" -msgid "User ID" +msgid "Upload" msgstr "" -msgid "Client IP" +msgid "Upload a new blueprint" msgstr "" -msgid "Method" +msgid "Upload a snapshot file" msgstr "" -msgid "Parameters" +msgid "" +"Upload a snapshot file (restores additional info not directly related to a " +"Playlist / Rundown, such as Packages, PackageWorkStatuses etc" msgstr "" -msgid "Time from platform user event to Action received by Core" +msgid "Upload Blueprints" msgstr "" -msgid "GUI" +msgid "Upload Layout?" msgstr "" -msgid "Core + Worker processing time" +msgid "Upload Snapshot" msgstr "" -msgid "Core" +msgid "Upload Snapshot (for debugging)" msgstr "" -msgid "Worker" +msgid "Upload stored Action Triggers" msgstr "" -msgid "Gateway" +msgid "URL" msgstr "" -msgid "TSR" +msgid "URL to the Quantel FileFlow Manager" msgstr "" -msgid "User Activity Log" +msgid "URL to the Quantel Gateway" msgstr "" -msgid "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s" +msgid "URL to the Quantel HTTP transformer" msgstr "" -msgid "in {{hours}} h {{minutes}} min {{seconds}} s" +msgid "URLs to the ISAs, in order of importance (comma separated)" msgstr "" -msgid "in {{minutes}} min {{seconds}} s" +msgid "Use {{nrcsName}} order" msgstr "" -msgid "in {{seconds}} s" +msgid "Use as default" msgstr "" -msgid "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago" +msgid "Use color of primary piece as background of panel" msgstr "" -msgid "{{hours}} h {{minutes}} min {{seconds}} s ago" +msgid "Use Trigger Mode" msgstr "" -msgid "{{minutes}} min {{seconds}} s ago" +msgid "User Activity Log" msgstr "" -msgid "{{seconds}} s ago" +msgid "User ID" msgstr "" -msgid "Next scheduled show" +msgid "User Log" msgstr "" -msgid "Help & Support" +msgid "User Name" msgstr "" -msgid "Disable hints by adding this to the URL:" +msgid "Username for authentication" msgstr "" -msgid "Enable hints by adding this to the URL:" +msgid "Validate and Apply Config" msgstr "" -msgid "More documentation available at:" +msgid "Validation failed!" msgstr "" -msgid "Device Triggers" +msgid "Value" msgstr "" -msgid "Timeline" +msgid "Value between 0 and 1" msgstr "" -msgid "Timeline Datastore" +msgid "Variants" msgstr "" -msgid "Mappings" +msgid "version" msgstr "" -msgid "Routed Mappings" +msgid "Version" msgstr "" -msgid "{{sourceLayer}} is missing a file path" +msgid "Version for {{name}}: From {{fromVersion}} to {{toVersion}}" msgstr "" -msgid "{{sourceLayer}} is not yet ready on the playout system" +msgid "View" msgstr "" -msgid "{{sourceLayer}} is being ingested" +msgid "View Layout" msgstr "" -msgid "Source is missing" +msgid "Waiting for action: {{actionName}}..." msgstr "" -msgid "Clip can't be played because it doesn't exist on the playout system" +msgid "Waiting for gateway to generate URL..." msgstr "" -msgid "{{sourceLayer}} is transferring to the playout system" +msgid "Warning" msgstr "" -msgid "" -"{{sourceLayer}} is transferring to the playout system and cannot be " -"played yet" +msgid "Warnings" msgstr "" -msgid "{{sourceLayer}} is in an unknown state" +msgid "Warnings During Migration" msgstr "" -msgid "{{sourceLayer}} doesn't have both audio & video" +msgid "What type of bank" msgstr "" -msgid "{{sourceLayer}} has the wrong format: {{format}}" +msgid "When" msgstr "" -msgid "{{sourceLayer}} has {{audioStreams}} audio streams" +msgid "When disabled, any HOLD operations will be silently ignored" msgstr "" -msgid "Clip starts with {{frames}} black frames" +msgid "" +"When enabled, double clicking on certain pieces in the GUI will play them " +"as adlibs" msgstr "" -msgid "This clip ends with black frames after {{seconds}} seconds" +msgid "" +"When enabled, this will override the piece content statuses to have no " +"errors or warnings and display a mock preview. This should only be used for " +"development!" msgstr "" -msgid "{{frames}} black frames detected within the clip" +msgid "When set, resources are considered immutable, ie they will not change" msgstr "" -msgid "{{frames}} black frames detected in the clip" +msgid "Whether to show countdown to next break" msgstr "" -msgid "Clip starts with {{frames}} freeze frames" +msgid "" +"While there are still breaks coming up in the show, hide the Expected End " +"timers" msgstr "" -msgid "This clip ends with freeze frames after {{seconds}} seconds" +msgid "Width" msgstr "" -msgid "{{frames}} freeze frames detected within the clip" +msgid "Work description" msgstr "" -msgid "{{frames}} freeze frames detected in the clip" +msgid "Work status" msgstr "" -msgid "Toggle Shelf" +msgid "Work status reason" msgstr "" -msgid "Undo Hold" +msgid "Work-in-progress" msgstr "" -msgid "Disable the next element" +msgid "Worker" msgstr "" -msgid "Undo Disable the next element" +msgid "WorkForce" msgstr "" -msgid "Move Next forwards" +msgid "X" msgstr "" -msgid "Move Next to the following segment" +msgid "Xbox Controller" msgstr "" -msgid "Move Next backwards" +msgid "Y" msgstr "" -msgid "Move Next to the previous segment" +msgid "Yes" msgstr "" -msgid "Rewind segments to start" +msgid "Yes, Take and Download Memory Heap Snapshot" msgstr "" -msgid "Invalid blueprint: \"{{blueprintId}}\"" +msgid "Yesterday" msgstr "" msgid "" -"Invalid config preset for blueprint: \"{{configPresetId}}\" " -"({{blueprintId}})" +"You are in rehearsal mode, the broadcast starts in less than 1 minute. Do " +"you want to go into On-Air mode?" msgstr "" -msgid "Config has not been applied before" +msgid "You need to run migrations to set the system up for operation." msgstr "" -msgid "Blueprint has been changed. From \"{{ oldValue }}\", to \"{{ newValue }}\"" +msgid "Your machine is offline and cannot connect to the {{platformName}}." msgstr "" -msgid "" -"Blueprint config preset has been changed. From \"{{ oldValue }}\", to \"{{ " -"newValue }}\"" +msgid "Your name" msgstr "" -msgid "Blueprint has a new version" +msgid "Zone ID" msgstr "" -msgid "Blueprint config has changed" +msgid "Zoom In" msgstr "" -msgid "Test Info message" +msgid "Zoom Out" msgstr "" -msgid "Test ERROR message" +msgctxt "one" +msgid "{{count}} items" msgstr "" -msgid "" -"Config value \"{{ name }}\" has changed. From \"{{ oldValue }}\", to \"{{ " -"newValue }}\"" +msgctxt "one" +msgid "There are {{count}} documents that can be removed, do you want to continue?" msgstr "" -msgctxt "°°°°°°plural" -msgid "{{count}} rows°°°°°°" +msgctxt "one" +msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" msgstr "" -msgctxt "°°°°°°plural" -msgid "This layer is now rerouted by an active Route Set: {{routeSets}}°°°°°°" +msgctxt "other" +msgid "{{count}} items" msgstr "" -msgctxt "°°°°°°plural" -msgid "" -"There are {{count}} documents that can be removed, do you want to " -"continue?°°°°°°" +msgctxt "other" +msgid "There are {{count}} documents that can be removed, do you want to continue?" +msgstr "" + +msgctxt "other" +msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" msgstr "" diff --git a/meteor/i18n/nb.mo b/meteor/i18n/nb.mo deleted file mode 100644 index 6f2e844d16a0fcc03c9c9c2e1651b032133acf6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75894 zcmcG%34k0$`TsqEaD_V%?xPcqB#_-45RQZpawp_kvI*f7W_M;clbxMeW@a~8LIjmV zP8EY3Ti=D_#&4sVS;GTga11G{1{`GJYTn=Z!FW|;-=A1-gM|dP$54OXN;1akkJPmFFuYd}FBit0; z1r_cWa0B>TsBnLT>%&)}^7AI#9R3Gx2{)=IuW(1W9UKOA|7mawn1+hq5x5lUey2j^ z?|e7_UJ`f@r05ba!W#GroC3F)n@CKCN5DStDyV$i7~DUDhvI%1ZVk7X=i&B%y3a7! zAMOWthDX4DumkG3&w+ZLi{Llii^>I+;oPs0a zNpLv44N~Qa*Wf{L-}!`vC&2yT?eGxz7S#12NBH#&9EJNPxHtS890L0u>EXvhg+B$V zeC~r6!QMwD5(mJ`;EC`lxF4K-G-2WCa6H`Lm_%YKJQUId6Bj_G_ZlQCiT=k@Rz_i^WA6s9 z|4rbwxVM2yzcz3*RJ)%VI3FrsO@WJ{+T&R;g5QO@|6Nee^8nld{t|8vUk?7CLbcy@ z8a-TJxGV1Mq2eD1RbS%+JE7XqGN^Q3fmRMs>1>2hP(5xB^;`{5<+c#+1eZXy=S!ga z$4yY>`5+t&pMrbAFW_!)aN5gdCR9DnhsxiLP~mdvFiVK1pfh*pMEV~4}+lk<2bk>oDWq_#|O5;O>v(H z_1xcty6+W%E8xz!{|@(t8)V(R5Bvu1!=S=H4JB7Tg@}~ITq=EEcpDrDUk}_o=kfQ4 zYA5sHAutE^9JfL}-yPxlub|?;43+`P|vw*aF2j`jK-KdssPvoQJ}?W1!K+~(_ypV-z5rFO zZ$Pz|&!O_Y^$DJ@fpBx&2SU~JG&mg|7W}V(DwiL_P2g>COZY(G6L2%!uR=ZddoY6k zfdk+U1+TX;P}eh1_iuyB&o|&S_&qokz6SL?HANpclCTE%m2fb83=V}KLOuVUB`=>L zP|v#`R67|D^<25YuR=ZFdBJ@VjBx(|ejPpnRlbL|dpbuz$+;BNeZB@IC(nRN_Zq17 z^Ao6WzlM6A*P+Vk3wQwBxWnsXBHSAHY`6tX!M?CP@S9NOco|gsH$tWJGpKw$0+s)# z;g;~vQ1$W-RJ;5D4uPLTmGfSUJiXz8hd{NX*-+^p36*XJs(cqgwVUgq^0^Fd10ROU z=L^CAo#5YlvCH)x;XYg+3zeT@;9{tJEQRO8Q(*+R?R35109cFrOsM=Xhl>AmsC@hq zt^=P4{!hWNxSxeecYBl&U60@%a5vZwPJ_ztaZu${fNB?~LOt)dgZs+hz7eWEmP0-7 zqi|dJ7pU_87^>XXTjK3`bEtfdfXes&a9wx^RQQQd?d342`kW6Hz6t97EpU6-36<^z zQ2Dz8D*lh4@_!dp`j0}T^CVQcyd2#BfXeTua0l4;Brlg5sQd2=6@FB3PYUkoQ296< zsvJ{rf7k+bpKIZs@HVLXKMR%4D^U4+8!FrRMN={y9L?o%*=uRuM=x?lBvIU4FdbAx+f zaDM}Ch5w~c<^SX0{wY+s{{~8Ky$JimUMKr~cZ0fLE!-I%1c$++pwc@Vs-CZcdhY9> z@_S?8oq-QR-S;=a{dC}8pvw2H;Qkouxz|0#`|H+F>F*9z?t`KF@kpq6lc3U_6Wqr@ zuE-amr}z}{c?avuYg&pN37R1cNze5mJJ0M~)-Q0-zd)bpMk zcqUZ4xB%+@*F)9AGI%t+3l4&PQ5uww(QrFB1rCPCL*@T0sCM%$sCIrm902ct%EvRo z|24QS?)RbU;UlQ)8=M;I3+{`1cc}D^getE*R69Ers-Im9m9P7u(s>=KoIirP|7NFo zK6i%dANxT)-&oii9tst2HdJ{Z2Nkass{BrZ*011>xG#cx!P}tf;m=U*`L9svt$Vtc z>xOV9?!ItWcs$$*`Ye>>Fu z?uDwC=YsnksD88QS^k{;q4Iek)N>sg+($r_dj@U*JA?n%pzeQu@V^Wy+_i8|cspDV zJ_k2~e+k#$gu2fs!M*X>Uhg|X-FIKOJ{%7>hSQ+(IS;CwGf?Gx64d?9hI+ospu*i9 zu0I~!FTh>!e;2A8H$TVS`$M(A!=duq3RONALZ$N~sD5@|@c&(KzXp|$524CqqjNp{ zmQenCK&5*C+!W4+dfsE<#xMsJ@5JE#dboZbRC~J$>iK>O^?bh!{(pqZ-#-J_JzTQ1-=B8{=f@7++?Wx&WCDmCqX^$wNUkP zGgNus2KBrTK&AI8lw5inD*UDh36=XcQ0`i&c>BXG;Y8R69uBvLN!S;5K()U!q1wrJ zp~5YLYOnV|wSy<2%JVI#^ge}(zv*{S=VH%iKdAB^0`)wj0w+U-p92*?1(n_j zQ299xD*Q#k{rzzLb~qRRN1&c-r%Sv(c84mTy`b9rNT_rUf*N0DLft0=)n7WGo^J_M z__Ltu|2t6mS_XChhoH*$w}CH0-S?kR;Xj1i!hV-JYvA^{CqczO4yqlsK;7rtQ0ZL^ zRW3h-d&4`S!oLh7_%>V@ZuULTPhY5f?+*1mgQ3!y1c$+SQ0@M7sB*X#sy*HU6>mAz zeeQ#M!N0=@Zhe`zlacUX+($t5hl}BX@FjQ#-2QTJ=hr}mdl=Th-dFI9a0EOIo)6Wo zUW41i&93x$)gUN$C)E8ffSvGucn&<^DxW_;4AZ#(4Ri45tFiIHU&BGL<{EE@hr zIdB*FV>l2#2G52cz+K@f-*@?NHPrR{q5S^_6>t4(JwJQE>A35H`*NsqeI&R)hKJ+c z?K)5IL^uNXB~amh1Bb)U0&B1Las7BW3jZtMX!s{M0B-#QWH8(x?gtmcZQ!j?<@!q) z!4KeM*zbp4{>Q-nxKDvQ!0*FJ@IEN{vhEGuA2xxi|GvS!6I>tn0Js|*4AmcI!VTe3 za6Q-v)qXNi*Gq6;_%%2P-UZd}UV>Z0cc9uy?;lYH@L<>h``qa9zXp5bJ`b)7FMzuL z#ZdXbC%FFzRZgEk-EZVg-d-2N4RK!tmCjXgXLudd^F0JNfR91-=cl0h)vHkT{4c2c zY-^Zuxi8f9 z?V<8F1WF$58?H}+y59__`+o&0ek)Y^OQD|g!r;CQD*U5R={*mXzgMB+{R65zK7)GR z^>6X({o#(d_k#W4WT<*R25tfypwh`hrMnoaot^?!-uJ?d;m_fo@NuYcA3(+X460wR zcdM80u5cjk8dwMCLEY~MP|treRC@P8mCvKW{{^W0zYUfCM{qm%U)T$7x6HW%R6E)U zDtrV_fqTJ&;Z0D_^=7#K5mb7;e&YRZBPjO}sQm8>mA^586QKIZG^lc@gG1msFoJi( zE#Pxd(Sg3y60q4MFa2j0q zE_5$A6CMed!{cDzpL)IuP~onIhru`Dwp7qT%M*#?anHS*F%;ejXTd2y^Lo4pZiV|+ zsPb76_$1sK_sg&eeh3eR$KK=dZ-A=LkKpdG_q|>}gQ42dFxVF!1b2k9gMSNDyhX4N zJQb>*zXdmk--W9G>!IrN7N~R{fXdI4Q2Biw?gT%DN`JfiJlwue=}ifo3H4m{!QB9r z&H@<0rBL^~5>A0Xg!AA#untbT-{W5dH7;EbmCs+mneZvNJsj|WKhHtX?hm)ceIDiaHw)VF1Qy#mFFo??d;;f z8==D82m8RMq3Yo;a6S0f;C>5ki2GfreEbKhzwGmvKj-(L;{OFI+*@!X_yN>?zkr*- zevdl`K;?I6;25ZWFdeGh9s`x$32=LO5>&buLFMZz*biPGcn@43_Y+X<_D{k629z9s z2W|j2{H>Ss7EsSo4~N5Zq2fINmG9p{^}lDK>g8>y{0#e@-*+^Oa32OYg$1}JJO!$J z&xdNK*TbRkMYtE-?DwAjf$#|26X0&}a##Z&4E!h5^X&A5Kksf(^)eXhIclNqvmaEs zPJ?RShePH6T&Qxp7^*yf0(0<}P;zAClir^WgM)E@6%K$mzzOg-a3b9N4}QP7Q1y5W z)bkYKmhf7r``rmQh7Z8a;csAH_-sCu0SH-mHF7BC5w{{mEf zp959S=fkbx)o?3#Csh4D8r-iylP^&1W3#6{-@8M(M?>B3FsOVV1$T$3;64+oo-YWz z5=#F52&&wF3Dq8c3;V$>|LF2-AXIv@;Ba^Z)bpPO^*mR=ZQw0X=|2n={`XMnJO@>e z??RQs=Wqnv$F-t(?6 zZ3)>Rlc6~9_eXfPN{|!*jy&P^2 z9}oVoL)Gg?a4_ukqPK^=;l8+!g}Qz|)O|09JHcg8<^MQTeeL*?%co=E7~B^_wTGvn zo^QjyczqrWRi0U>=Pf~{e==15&V;I`D+8B7J>Nr6?ebB$GkgwS3qOXIua~{NmqFFr z-BA7TF{pBU5~>{DgeuQZ0=Ig_%VP-C^~rD_csNwP&VtI}gYK4k_GE{k;3fF<>K|S|(;8E~WsDAMQTo-;0BiQS8&({E`=N=7J|8tbflB{vsP@|XAD*A>p~_QH`Mbz9Q=P9_$Rmt{;xn?e+Md`A45HVpSL~T z9iZ|NLEU#K)N@XPy3Y|%=`=us@XG8U)pFp*i$0p?mOUE+&_mZpI+~IefEQj zKM)=O_lNt!HmK*h9;!dz0aZ?qLyfb~LOu8UP|y1bRK50o-^*#kz#ZWh_}2!Gg{qG_ zsC15iDu)78dKW|0>y=Q?|1+rjJ_zT*$Dp2nzz5!bCPIZ@2)Bf%L*@GtsC@r0xE~De z-$6a^8-X7~mD7g*cK@y5KDc*-y3c$#8g{}N@J@ItT<1gA-_C#vcPrF$JOLy4Aym0+ z`;oJM;NDQrGZIRl7zfqAPKJ7pZ^B*Rbx?A31(bYx9qK;oeeC5q5UQOV0QKAppq}eQ zsPxZ*1K>qa<#9LM3jPZ21z&)YQycupU-$-8d;Juu{x|x>pKAxG{2mD9KOHLmF;MwAA@CBY=Y9aHJpK%8;b(9g zxc8?XZ#DA7Xipz3YC|9ZRG z4i3RR8Y&+P;P2tdP;#d((aYM~u~7Bg3YGp6sPg$CRQp&C)xPhCYM+ll-S-)&{{9l| z2mcoM8Ps$3?bXZL=>dU_P~~$T)cvl7s*j&Q<@+(H=X?n&z5l=oaPxI~S$)()&cEToaKm+bncZj#)cwwddY;Rm@^KAR|GNvSJpKlEf$u{-Z{OaY zj~$`PcQ{nN90YZrSy1^%LZ#mVmA}s5e->1F7edwJ?NIgm0Mv6o1C`!;Q0etvua~uh zZK3+f-cb2C3Mzgh+zIC4zVIBV=lMBQdw2w@KK=o9pZDQlxbgb_oFm~R+-azM-UOBY zy-@Py;lQV$%IDQ^{bQ)-?6rY&W2oot2X(*QpxW3s^7zjZeB=hzCWoc4x#-Z4=5od6Yn4vgTD!T&_4^v;CJ*M-6V2B`2q zg({c(q1xFSQ04UnRKMP4BQNg}Q1?9;DqbB_JHW18syxnxk~=rSG<*gs{FuHT zE)56aJ`L^(ZwY)F>N!4x>hF7Q)n3RRBx2lt=hWZdiQ*vsZG)1bng z2-T0Tf_koHQ2pmFsQ&pVRDPd=YWL3v|2LrI-1|`Fw8c(d9z&qw9{|Q1wy=b-$y+^;~c-g1Z0N zQ0ZI_)jn>4(z70g8t31Jl2@DW;_2-I_56dN;_VaMlc4fj7u;E>a##eF?+c;I@yF1{ z6R7+>03}Z!g`?pU!M*9OexH6&^&LSy-xO%i1yxQ*LFJI%wq*+_yuecQ2IO`wxuZo&&tTra*;10!l7_1?oPHQ2nC?4ueae z`p?a90{k0PdVB5W{eFJnnSu8}$?3PD>c9W)F2^Q8$~_k+7`U@x-+p9R&fUWN+yIh4HI ze~{Ntl1lm%_n+Wv@F6M*k%~TsgWr7|m#xcujN`YQ&ypv95zZ>xXgkw{VZxL@G$4>~8<~SZd4VsN0#qklx4BVsOH#y?pe)#D(1kMTR zBi0kA<98-${F(DdIhKTL>%qT=^8<{Z@Y@{U;i%){eW zEWDs9J%v9P_q7~X$-%FMaJRw}IiA5Ewc38?!G+=cB=<^OhWlZ{e+Bp8a7~wb<97}2 z<3c#$DEuCS`{RE%EN~n_elF+G?`!yv;`j;Y3kkcF<82Q8wt-*aIE?d~;6FH|>*)6* z;vC7LA5){mtz3(L(i7GT>6;(di(K!|F+K#^mg~ph{~i4P2yft67_NPr^A#K;InKxL zHymR)avZ3m<=-66zFKx6ySX+F_qp-2k#_a(aoht4BfU(&qpQ-A`)eWIj|g`$?mM`C z4t`99Yz!XG`95457s90i7r<>ejtTBPiQC5co#Fo5g!_!b?+08Pi2vpABmAWk7CB63 zgQG&6y*Qu6^`%h1?{Gc?e!%s6I6sKvZH`ky_|r&xU(OfdcL3ow=KNI7ujY7&;~V&W z4_*Ql610Mn>cpm{HI*MUXj8tNw}F@`!+0c=ywd)d*c`X zCgZ*W$Hn;dCd{RrUDoc|8ihxB&m{35RX8~0uu*K@uTe*X;DpMZOYdvM9tZlC6Q z{5zGC(S-R6#~U2agbQtO^Qv(2zL@JHLR#l>z7N-T#a+*FM2Ht-53Xyz_bOppInQzo z=X@N;UHG5J(S+X*I8MWF49D4VWcVol`sGM_Uyc@zFL2M}ID}&>+|LkZ9pdfA`MLNF z;Lz`7{C|L-euFt?Nk%tUnk8|IX2*E;)s7Ext7Ph zF2@siUWa@As_ReU_g#*e_$PGmJDs#A_^Ev_VJP8m3~@H%ntmG)_NVyW1?Ry-IR4A| zdGJ*@04^rnweSHr6~Dhg{Tevm1b_WD;=KBICXVen)(!X3wQEAytq6M?{{Q7zw<_MC za9)C!ay-cO_25+EF5~<+oL@y6Z{R)>_QHLW5)Qu-en)fuEdKGYM7&NM-wpSU{U?Pm zpK^T=$Njkb5awzEyvKQt<0kxnihGx;xP9bHQfC-|xTyAxr~;VeU# znIV2(+*88&ZwTLxyNRO}zx@cO-!B5)wer93;q?vTo=>=+!zu7r9D8xT1L@oz;+F__ zFn&M5eE|OTod1V&eRHUm^RIBs!rj5K6u%pB>vu2w9ezvPS^hUen7Lg02Asq7F;KsA zIKL8}5Zv;ci{Imf9UapAJAT`9ehgf}aWBVd9K#5AM~F8N_fg@z1|H0{3*n_4-{;zf z9AD-9VW{5*4x8uwjj&ge{t0kfxQuI?aQvR*W8CvOE+(#iFLOSYBmT+zhH&lo_-{`d zlW~6=>bEiHwc)zlw{gCNYqxOh7{dGr{~I|!yDHsj;hN$s4*t6Y{+?@xa=aVD{1pGI zIPVwkb4T!3+<$QG3*0w_YrhC-yu!8hIsX>Nz8qT;=HK}3$+>Jt6X8U-b%^&o=iepF zF#PqqF8J?;duRO5$9)9n`*Xe_=luvX2Y!xwU(Wvu|G=T&fq^FwJ{!{c73V{O|3#c* zqDuUMupLmpi6PttfxpGSjpJLy*)YWOkp6E!{GR6YV~zzJi*SFP@b|&t9Fs_AefSQ3 zyTbUF3@5MP-$|HZ;ePVogX4P~KPUVV9Dn2d*YN9bGr~{i_&3K4j$Jthg}5hiz8PV5 z;rJQHp}41U{Fw9E9PuwnTKY}r+HyZN`{d^Mzoh$ce8}-8FplTAb|%+OgfHU$AskN{`hDg| zyoH~B8^bNb`4xdbB<%g1-^lf!asFR;H0k|>BmS)j>Agv~E5i9FTzmlcq!8$nkX}Aq zzpE;~g4Bd?KZNgdY)iOo@Dqx}{}%jR@H-yvM3}>2Z;p1-TS}O}GuJC-^ASqKEkn_^G5iyaIfvF?y2yj^$)+p2y+c#^_vm67#_}X2*jeE<*4ImtctTI=YJ!-e%HZm;6eD^7Q!5bdsaxR0BaSH z-!HlT6vsaJ)w@^XbvTWqlXUjrIE!o3LpqP*|2cje;dc+`Cv*NMu073hALsFJI476k zcM#z&2yx$mTM%YBe#@^AJO29Z!!eulpK+~@^ZhwLFoLaV=>3~ zLi+NLe@AliO|BhF{IOg=o8y=G4TUFj?7*=L$1m`E9o|W}yW#U32Zu!J@EeRfF=%#M zDU;77vxB2?Q7+k<9-2v|bEQmECS8c~O;Kqj@DV_R1O>9V%O(#3jQM$FQ)EQ-(qULPAA(_o~M#;v;bg`Ie$fm1rIClXVY0S50 zQ&EFwPfrs+LSw$Qt-Yj2%JBf1#syUm)|AhtxI*-NE-`3ct|gaWl#AN3$L&FTV=dD!q_asXEGp$Ghh!=8+mT8!&R(ec#`Z#i<`B(k&*jM3Ls!o( zsH_$win)##gE?8Sju){M9ulAIbDj976+NpMlQ11{Gw=1I!Pf~ z#)SovH{GDA^V%=rb1sB6?zrbx$BCh6&QnKZH_F^;B%a93wn zvy?;}m!*TJI;}Bwr27-3xX8K(MU|s58K{3Uhrn#4!L+qyGmX|?qq=l~GHy-gsDqjL zT&9#qcr|wkg8Z=ICYj~-G#lz8D#q=nBZQpgP~xIY_0mX?1|A zRu~Ofvcw}!OD$bGV6~a)F|sE%LoIqOQ&3*Ht42fa7&q-T-&yg>BaTuiJHEt?iwn}( zCUroGk4i4q?(Q1a^`|$lp_1-nj*UnHK>qo&7)BzX~s#3up*+a$S^Y4M~pQ-1f7|ltaj0wX)ahF z9~)J=Mnh*?vWP6Iz8*C$NH?}fk1+}5T{|Cjqzjp*PJ*I)D17MYNM5BmP=3ZDU;F~rOrx`n+o|>#qKV#H8$a$ zOD|e0DPEOs_tp~X8ZD49kxtD>o+%A1GJrPJc-Z1e{7LAGIkda}CKNG6g+_S;N>q zvvYLflx8|KeI}9`qUqD9t)}Q{USl!KLM>tvumBCL33X$&eW6&ANU(J4^URo|aqXpi zt1CbC=>j>RX&2I(E4ox#ZFN+o=0&)M(4=hn4wFY~ZzewDIoy{k!uB?b&Qq+ak8-MC zHnD6TZPl_yn#pBQH%Eda?@FIB;`gW{+vU!O(e zQz|7HiBi!-pEg%K>f0MaS4oW1lslT4S!_xwHJKPcXVSRoiSfx42Q7+OlAp8}vg*Zp zI&^MV?*^q8+mv=xw4hXK8#Q!jYiH4#z)-g@47R9f+-WQ+=~R)JDxxrc#B(mDkec6_ zp@o@iCL4k=c^l8jy+|XXjhe3IQz@>)qg^RCQn#a)bZ0|8SwJRGZp9J`d~1q97?rsd zU7S0c;MZ*s{^`Z(MoKl+pY&0(Gx&Hc` zR}>Apy&leHj}$=)In(9J1TtDkCLBx^I?}DohjBA~$p%DKS^+g*q(x!Np}@m+N{#S( z1St(v2@=QjNQ$~K*-jf`UW;FAvOozlFh`SV1Tvj{$!ODZs7x6}E4C#I6Q)R(c{{F1 z;COf-qmom!>e|}c#01k@)O=NVMJ8aFacr)yE4g&mYixq_w#YhtF>_*?XmYEKF+Lab zDo|#9-Aq0{leRv-ip=y+Oke_)Yj2AgVSKY_D|wg@&*W0H(4t)|MD^a)Y}^c9ZrPy4 zG<$8?^lZrzQ%bx52$&)36i_mr{OIYWW)$*Sw*Ywe3n95^Gmm6PGLubWy;@mMQHGT0 z%0a6Hk;2G!Hmy!)rlYAm<7z==9B5roO@+o#)k0+F(Z%#)43NsX%3Z-Ui*j-ANyY(f zdYm-*wPz&DP~NXw#fL!^Ge&_XUSyCikJje%C4$;>w~ewGI?oqr8To7$o2E`$+mH(? zu=-f=R^$bJ4~I-7%muK`1gRU#(fH|_=?xgt;;`x&9*Yvfwh-ZR?U37-FU-f@BCCWh zQl~|0o9V@EWG@w!Rkis4W(OEB^9?3gnzQsUaXDk=oQGGMtW|0wtO`y+Z7Fr`{vPeDk;QysPh*0 z(A87CQY2bJJx#Q6G&E)6B7iaEI-*$=STMO(d(C4!jP_(oLb*)JQBPTQ*=SNL-7F4= zRvaeN!H4=U&;b_a8>rr3jMhY~?5K}>etQw!E1HmHwj+Vz&Z#DyqZ!GDG!~|bWdl#u zX>9Jot0_tG#3YjXTiIDpVW!J2H*EL_gQsjY0m0faHg*==npSpK28B3;`IN`Rt^s{g z-X&y3#B>enovy2&Oru?WcqGhL@}nPFnRK9tv|%>&I$hKF5kfKGhch*T_}oVK;2%!} zypd?sqv6v8B?9Qy9!hm$3mInAlVU`V?O8fA^)95O0F|TITp?2|VkAsV%%m!YLYc)e zkt2y(t7a4p1@x`PWKj>FX=>shnLyJK9iB;Y0}D&3l+OjoYY+^me51+%bLqt;@6Tl~ z2?g_}1=s)-CmLoSZvTjb^VD66z{QYTlDVS3hB||N;4~VaoD>JSDhQs(8616fTGey2il#PwArYmcGNIB&$YDhBZ zhnufa1cq=$7C*Fhp3i5A9%83pDnbWK@yK3-Oe->NC0trhq-PWml8Yjlhnd+DzED6N zw0}ooKt_v-txjJ3lu@jGEuywfo01iP{3@o$2~%ckFRO_l)3VQ2CzVghXLqrw8SKiQ zk})fLca3f)QvxxpnZ*?u!OjG^amMsvC}@$DNhITLE`HP@w8<0NGt>>u+oujSvd+^# zY)zVnw=}hSuAK+UMTRn7+-3u6MA3I~L~{w$)u+S&R`EfhL60K7R^J5#9i`1Il{H|- zvqlVQ439KRsX-xtyA&=d^)i=AX6b`eEi3Dj7E|49C5gyfQFdLMoSDhIJ*R;ozEG@9 zOh)-Y3vj4I#!|V;i4w2r*ASa%imSa<2<^gP4IxcxRxjK27`~LuCML^5J0aiJ z>1RO+Hecp}k`gwyhwU}7T(`YM?x%PX5a=~(X|x7+GZ>O_*5Mn5l?Zvi67qEK=UHa}oP_B3WYb$rCf|2uVzxJCkiNnyJT2W3mIW4rii%eVC8A4p-wQ>N@-ZAzGdv~!rwy1L+wNs6x9UHGu7_;`ARGXig??YX^^eHm86zVkJ^}zYHS-*Hs35o0Zpbk8q}Ix9MNvbt?fP; zJR}-cJ5pg4YUHpzdP;goR8B{WY1Ma4Ol>Ef8dT1d4T(H|c>>0ONSRHW7$@m+BpPHJ zRElkR8Qi(CI%2p)*wCWLVx|pEFPWWa!&iCbdvH~P8tc_qYwS)+i-lIf%xErd_3+9!nUZfI{FobYaH z|C-4;Nsqy^Sh9bluWPGq%3X2K$+*tza~uVx?#PXu`B+|k9wK)&de2a^lv3*_Guhil z&(N*qFXbpP!}izC%W3n3a-c~8(odb!m)dNlt>WFJ(yU&Do1t-!BoS3Sj;_|TfvzPn zLu0)E>*8%jzPVZbGMmQM#D!)llr!@AmZZk(Xl8P8rnQ~Du|B&%9YC>Ur^%pTk&DJK z=w@{n?Y6CoryYtF=iFpL8|P*>EToCC-LDdRxW#HS6`AmDK4>Uvn|IE98xD#&o}rAz z$v!u2ZehAIb9Q|^)lJMy=8|Z(@iyKf9-(AsvS~4yuzz_Xg(?JB80T#L(68H0*d}g( z7{)?NI8&HoMu%y?X6(}C5|XE+)JqE(v3&SPpoF#V;K9s*u-J5&Pifl~b*+NN2dOELb*jsJy?U}f$^*pq>`48U7Rv4#$uJsbn05tQc9Sj6YO?C_to?LbtF=4Y zg%PZlwX+jbE4UTkeY!5~RxXU}F*vpnCd!@!O>UM(M6K4@nGN(937Lvk{1^thdj*M% z(#6h7HO+VdsS+y$X_JzYZ|A{-q1i&n1RZV>R8L$h=~;-Etd;>a5wojWtZiq4gmuTX z7;20i+*lZt1yeeBkisHu*G$Z0DLBbELz&0h2NKqA_0Ooptgsy)0WW)duqo)&wP^FT z-X5Q+E$Dg>nL4eDXXJ9YD7PSzu-Ne2G_PyH!|$&WSFs0En4kL^tj#d(n)Ne`Kjtut zy`6>9a_^$n8OSVCp@sCBAwCNEVR)a{k)eak$gF<7I z!5~s)T#@le4}b+siD~qpNHkdBk~F9LR18=0^Z{G16~tC+JninPuW?-rftJDYwUC9X z^wfb^{9$%2UB7^R+Ld2Y%&D4rOx@CRgxT2EV%6%*JGN#kNZ4?|6S3wM2S8*ZNYHPX z|Ii|27n{W*l=9f&rxU6kq7?bKY+)D!Q7;rpggDUK*cDx!Dw`dC(iq+0%s+op@j+JP;;M=X$0?a6a))b1%7D0+7 zR+c*xvoQ_XKlBX=^D1S9OEON<1ht`=Kx(>OrAn-gdb3V+d5vN=_N0u$&oI>$agcD4 z5QQB8#n@dAKqo0T$@pTqiE2+uFkN}mHiKUnCul3$%)%d{Mjh8K_62MLKT(UgOdk>} zttLogS;@25gO{p^zEs_n))cH1&n_zg<8eB^WpIau)ADV@C>@7F9QYb^w{S{Vb6?a~ zJ1xmw^66kCi*&VnUYog!QQ6XLV~VUTj2fjRhN^N0iBIfllkIdnwg&ko`*NVjS~?OY z$x8}S8Aqzd3N!#o35;~jiMn)73tTXsc0^&`!5D+!(f+SgqK@Wg|6ut;O_fu6Aw``j zkSzMEYiG>Pq$_Hdw9Oep8y*rdiS!r9Ov7%^WiSKgq+Bvog{Gg^jGCKTC#`PvJzfvG zhHbr92KAx=QMD??L7Qqz$`X&i&3G#XTA2cuFKRk$gH#;aETi zoTlx_SwoqbzfQ^;qOQmt4<7!y8ErMpCECJTT)7ol5{eN_G-mIv3^^gaHLk*&d4R$Q za+->+C1qduFK^z7HAV?g)wa5k>K6S-+9Bgr0~$ze#B`B$TD@nNEOZXB#Vu+fPA|Q< zEz3m4V#Xtrh2a8iE3NrNG-#15DGL}WG^w)9XtHJu!KQ&fvQ>K@Oftt|!X*TAd?lE^ zl}|Dm-^yNkQ!h(mZW^5Dx4zn!n(9h z>^(+x$O5}ZF{YX#Kz?F90hy07m&TO1$SpyXFUERFew9ZfyNYxZ_T5kziP-pDj#8cM z%2&0UF^&>%@Th7#m|5eial5RJVdfDP6_XhEXU0Z6I8YXuLa9g0<2IkX7XiN*oVz!21DnH$i!9877mP4T%~l`^eNp_^mB zOsehzvYsx>m>aK=`fb-S0^jcr>D{vqU?pbUGUa1F_;%dV_4GIA~r zH>Q%WKKC6^Ay8Cau&TE$#^r!stz~BzI?C?2Lx=8O_V3mtEF<0~e6yiFvgzNI#Se>& zEg`12$Jj1na;w`Vc9|i_;}Ggl?ASuYRw_NdX8TUT%$MpPF%8fA&8{v3v6dilpCB>(27ISqv9mf^e_p7_FPn6 zgo#X)vm8apYJaiQnXsWGp z#rB;zl+7w_ilcpcIx(}OUi$MPl@U@yi`i^y%ZbU?yvjb^HsPpf)slMNe?uo~We=9d zq@WR)0Jci4mX)>9k?fF>Dwk%DC8Gwa36e+qp{Y50dqNvrv@=?sEHKBGNo}wx#B!=U zwG8b|5o@FQQYpG^!b#<6L0*u>M7{2%=}j}0XO6H&-Nj+Ud}Whh6xV}ApuGjzP_GT; z3I5G_GxrrK>0kDuUBc~{S_+HjhJn>nfv#T624auOytKR@pn6JM4wutv2G1jFJ^jGU zAzt829pc2aeZpu#p?P{=DQ(6*(tU{X2Vs^vpGN)_G0 zg{1m*IAgEipb69-ogt4^S$eQKj~6p9D^h6Et!jL`3yOOZF~y`cUBi0=X;jX7I`lzo z(4zbgmRakdp}|4dL<;C~4y##PY+jQ`F_;)gyvL{(%Or7|W{s;0=H?cc3HEO+3fvKD zg%v!r26cN`g1t6{68kDBbk*p9Fh&-`TSLVGHSvB!yRLD_PRzhZQLwcyh*tM!M&&2I zd>4|9g_6lni*HTGL)vL%@6>d=&i*ZX6|E{O<8>WYy7TsSfOLX_3GRp6nxdmBissUMl z*V8UtS~OX-R&S_<8%)(|i1hk;7G`Ob^u0yuQ;XE(=^#jI32A%dHO$T!7-dWCIRZ^e z5mK3ycKogMrtB(W@r95iHXfTPhc|1(%WhrvuIr_~s`;U;5k*Nv<3VqxvRHCJSf{SXh73DQ}P|+HJ8tt77_cO(*nN|2$aWE3s-_f@^Jzr7_Qv7l!PFXP$1o2vIm5`M0_ z(H0lC=PHom@iH}MDkl3dPyl^Bevt4nAuI9tsy@o&yW#Z>5KPZ1E2YLTM-Ak&?70nL zZ5*pUk7n`B9+vs6i^Nr!n44_Yfrck5x^+VYoA#vJKSUfDWEOz=w&s)^EW`-;& zzP=Sl{+K^y7uV%AhOG=FJq5KpC^^a3RR$<}SG6FRYbv8)w+=`lXuMfTQMXt8lCsIp z4bgqz>=M4ZCs%}wZw{$g`DzLbO8r6x~p35{* z5tz?tb9hJ|`@Fo?ZF26gHpw1Sy=!b@DK7zG-!f7RzXMiyODweAa!#?~*%O3K8`I%`eu^&Ia}H`@iFtw)6opz2t)-GoHa=&*9xu z6=_vygs3UwYG<-M(uUjUnEFx|s5y*0xL@@>CeJnB-$?TwiHzmm2+HYW`PKSn{BYIp)%Lf0$Mz=+=+iLvVe6?; z)6L!<_XFGL+ospAc&k|&vvOl!{IDnQ{#hQcG@qc2^#sFEp-xKSQos86k=?55iuocK zY_D`!OLdLcf)+f*o?a#KT{Ufdq%6aLi!3OVcO9e7#M2{}ME#M7W_Js(9EX<&E8Bl4 zGuh3U+cYom@>unq{e|m_G_LIHwELE;OLlAf#F&*0qgidAKI7J|APJ0Crf-mWS;m>s zBsI+Y83Sl|{^B?a2wmRp=Y1*;uc}hrVQ2~cGv0-b>MXg=(hV_LKeUx)O!j82SF~(~ z%ZS;oPZ1LKT*c}&#tt88D0TJw*x$GBQu#cGy)UMzpw>+)t*t65ErH!bYAoc1an$sC zllWM})og;YSQa3*$E@=0Ix`2b_aLLqMj6}SX3s3$DrB3L5c;a^?^P+{OJ|8c>ZM!g zvlq%j5N|wwyeg?o2c1{}^>w(eaw=W}Y0!)JDmmMvVh>9i_5`e!=$1;V++$htx4k3Q zk*O)w7Sb&?#@dbm+c3dhE0gvP`<02vI{BE)4YyGH_=dwg!MP9uL##M#7-{px{+QYbtR9JbjB~o<`zNFL1 zEU!yC^l*K3VwIPJiHRNi6M3rwxZh5DI|G42z{AbTR^QsTfYev6St*u zk2Z~79jl%XRK@ixCD=72`h!$#6{krOv(V^`HHlg^ELrlU#VczXBg{6>TrI*x^~X>a`b2QvVx6LZND4(9SDOO*KH2|@5kl`{`%qpKD( z%vWqhbS@{2f3236-k0pOQW1Z2t96=^h$OSAG`qhYF}TVd23x^=i+D-soTR!>##| zblb{DgcD{!lNFL97LD;5W_g!GyoalYFtI_chf7oE&aLxzJh1$;i9xrp^*WUjZ&T1i zC~nuxkvXm%`mS_Ddn+$LYJZ3(=YtdT>>J0*pzT!ff_4i{SJhLC4B9rQMW{-oc$&Y; zcna&^yPAj8!$sd^N20xoqU@TjBK`n@UA1>=!k4TG?_ac@Xz!Smoioe;8efd{LoFTp z1||FQTRF6ka%$~(%^3Sm5y+1xGOvZSPJtlPzcb;xZLN+8y^G|2HuY@1a1r`YW-V=V)*g`2?D6J%8gvpFRbOaK(G zT0yY1=X14+t`1^#<_@*y#8EP&&`SdK9+!O;$UjlbOeVMo3?>Mzk`3y{*(`lEXr(XF z#J8-|i6(SDw*65$Y?;A^p?Mz|g45oC!M%WUF9u!Z47aQ6au+@BmkPYb)T)!$OfkVX zM$(B6-^G-u%o?;#B&UCHX^u~E)$Pex4cQEpqv^XeZ@==TBSeGrk_hc>Fj4=vLbCUO zR{3Q!dPm806ZI`Aa@=PXA3>`G)v6i6)p%AP1p`%8@?R?a%J*2ydJfOqMXe7ieOCkR zYMWL_rLsGcjVbnqi`4N|)NYr`s%l3eElDj~od_bGhVsfO7erMbl+|)SIsrarb1?OT zQ1KkbY+wzjpcvTo9nSbWcHP5E6<1l?O0+t4YqBJz!_RuSfg+%_DA9WR7^Zrc--AsX zd>m6Py=$IObltQ-+ccX_Pa)!6Pf6RYhT#Q~+(SZAw6Q5k2=g8NFrn9!6%`=aV0!X-fF+A^A>-FW%PRl1Wiixddry$f!K`*=tHUa~Q6=g_ z%X&&_Zg~)_2%G=W9=7IN#k03BYb{x;l&L|6er_Raz?ZXjtSS9U(MS%jIrJ*(qKkNv zDEmK1shi;Hl44yUuQzXNrlo3vQZ_2e3_n1MP!02-jy`p>YuK#G<}lu}LRn_F{)k~C zM$`;Dpk~;JX!yRPhVQ%guo1(C^_h_@mTK%3S>935=YH_fdy~!Snz{7XD)*=;*U)F? zw3(ACcN|_jtPj$RH@QgzICX*X&uf(7ljF~rga?Jsia56^lI&RIR9Dl@z z9ycmFpdnN0Q)kV;1|6xD{R=to4aG4dM$=!^F~$taMg7M_!v~MO@++hXcF7Pk{!9BteM6y$+YMRO*dgMVP4&$Z`JarmP!oD{7Hdm zK?j$4p@su9HS4T;odUC~2hE^pm;Qgh0e^}q8Fj4q0qUzhvB)^!! zctFf5u^Nf5>Fyd93h^IlVr?~^Vxr!aC$gNUb4(Bjn6E9?cCc$ux3OvOm%0r_)spL^ z6!{)(oPZ@2f95;cVRb?2lV9w8RmDWX@(vi8Vv{4vFMEwF9hE^-ny){uST41nZ=BDp zi;^vRRg~sbS6H#UBaH&vf?3Jlhl#k(YX$lYM@w-b8gd~quA`yQ$zN77SQ#Z|(81I&y}aAN%%fCH8$Sk@lSA;L;IpjRqzZQglt5ER3d5G zTx%Qe5c2(>rc6tV|3N68lM#xQD|=fR2^D3k`%|Hk8Y-u+9CrD$P&KrE)g)6f{qZF$ ztWcWNEdTRHwE4%vp{ZTMgbOYKqaq^!A?WPL)&hIw`HN6w3Nww~`lMZ8v5k{PIkY~; zFiF+O0{*r_+#j14*MC5cY{0!7ac5%N-GQ<4VQ(6o^jSa6wIuoXA_>5kPrNRN@Lrb26< zLU~}OEmXFrt+r&s&C3I(XT9uP>wEiD>Pz3ZJZM{n%hT7jzOBnT&$OSfbyRN%_9vLM zF1Y3j@(2wpmP@>^O@tl=qvzFEB-WhQriFaVk-raNI*gT(FXpc~W@*K87S%#?51B^f zT6#g9@@Fp$UhT7#B#V6G3ojLGm>kRd>T2!HYHsSu>cI3~Ixc%B_>wDzS__K8VPAsJ z(#z4$B-Bw~S0x7G{-2TB7mG}i>>w7cXVJYyYm=gjSzzZ`KA+Er(2+ zRad{-CuhZ@lHh8@h7<#2T1SSd0)>Kn$+tF9G_jnvt4J%;3i;NArZ%gUCjWb-$1z)q zE#Xg7v9RyzgEgLRf1PS}tYT2cMWnx}5B{WaB*{dy5Y&=#0BZz{ic(7SE!dSsI}L(S zISRRn^$k~nT$w^T1@AStsgiYX*jY|leCa_9e;N)4K{slT3FGW^ZbwN9Jn#C)s#a0*sY8o9%vLB* znm6u>yf(HZSS{Fks;mSK{c;2-YbwPKj`a_A#mncfwpyx^_ z;n7$7fK`LPro|1|N)c!_*FsAeuRVS)^n6|rPr+S(p#)k=rpMbbjLfb`!l=DfpR&{E zn%Nh&nu-gv1Ao6rAOF!LI_D;T=}nq~uJ#c^0$xXPp{`X-Nv_J|)Mw%hRw~GNru?M~ zl=LV_3FNXy@jq5g7Hbr9BL4PBFxqPJP;6;$VprE%RrOeA#)TEvQ)h_@Na|$yZ&}GU zU=|2z?WnOiw$+3abmww2*S`HKZ=1fEm*y4HvO^D2sMxEzXC-EIG978L>4E-0S)y*- zy!uIr$ru0H@V=3`PzK~L4_W1h#d@IK$ z!xpo>q&U1*p`DgBE&j)?^2Fpr^eoMw^lLhUJI&;}-PD5qd@HFz0vOl?|{8cya@@KBJJuz;^c||h=-|C^;B&0nF;eU`|HQtH_sbFK)4dCexMqWNhat>%Mf3a>v6SO>J64J*Fy2 zW{y&o)mEE_yC^zq3||pZ3Mz{`ORSusp4mLKYV@o0TW?A1RP9iPtr4-W)MSCIt}Z-y zJy{J7g|8To2jzri(X{Z{P71tvsGAWQGy^V&i5rL1?NzS%u61a-0>axKY?LE0qrIiY zzO|{nW78{}ioL-33t6#g3iCeK)uAbC4b7h2CRs>g$!(Y7mA$Z2g-#gDb2g+`Bs&5C zVHfM7U9J>Zw3I?+W>furCAy|68yU`bi1nT3l}&qQ&`?ugRh9g)H^l!CR<+fu$aPs| z$b69w2m{(c2t(1twbTchhs#iAbU_UZ(!C-)(R*PZJVO3ym#*Ec7ksDC$DZS-YhQL~lIkLOQM3Z87OxdEnvbS_(P1d4x4Le2kaSqyhF2}B1 z&EL9&H;X{*!`iL>R#pvv9HfCpIg5#*)@-yQfCAXX9m|$X*5B7++RAv#xSp6q!P|ah zYcA;$@AM#syd5oQ=i5`wHbUsHMFpc^<*Z5;CF<=R<^nIAh7&KzvXiDlN#!cnqXjmq z+0&YK#f$SQHTyTHUSr+Wb&?%CH5Q>c1glDj=GsSD1Js}B_Vhe(Fiv#wE6X$tEpb@c z9AZh@)i*YUc1t+e?nnZIMKX&}=Oq3+Tir(ywM+VD*O6?JW<^Em7=C7hn4g+<5zkS) z1-hvMA!AyzM^R<^)Jj)#E0(7imSy3$B4z-n9w(5urA}HY9Xx(eOyCrMoP-2vczc9| zr}9T+XjSbjej^mh{$||>b_N-$Cg%$pjroE^xwY6|+d@G|X$bP=1g|8MKKP-GvZBDt z;|K+oF{FFPhW#q9V!M`mDyaq?tEf@F;$2EiE+kDQ<}*QNOr-QEQRJSn`U6s>mt7YxbOmfZ4IpwP-P_QjP0F+Kl>YeLq@K-03QO^^dq|^9`7dGA=#O78_N?MTRy&2}L4Ap>+vm9| zx+@mI)n;g=6v~AXQjT}Ohn?-^>5={fSBw7%E*TBf<7v_51$l)lhv1qNb+@T7xtu25 zOD`d^LW!KKEFwy~*W1ip>xmXGf13~3GQ6cGF`!vWVxCRQ^hduKg|a2Wf57i(xi3ph>cSyk5xIK&0`8`Mq(nb z6t#MMo#V@zya-`mYw&M{#-15o^VQ$&vXM3{m6 zO919?Jw`K&hlVyomPuujKe7ovQ=E#Gl|+_bE-6(m8Y9jE=jxhnaAWjZv>*# zqPMDMG}6Xy>2K~i7scO8jV#2My9YNnC8ZCGlKK=|TInJ?BrLo`naLvPVj*O(iLDet zA|jpbkTC(B!^~W&4{m{R(lyy)%ZlZ;{uvs|RmBFF38BXn zre|#fhCLvixGe0u33VA>yWP9$(ZfKnQofZHnL&Dp%>=}3p`jeg>M#b;RX%LjC|#t~ zYGcW47w!|9#wzbIU9YtjTbQl#u|j4TXke~z+gXVfc0^zGez3&k@b|&usR!-8M<3>$ z{K>FNZ<1+f#)|9d3|cbKIKd$1b0GiXfC(N=(v6miwh0QD0wj-EN#2-fS^Um8}kKe|-e4BO^&|<)v+czDqDM zSL>(#ul*^pFf=go7T7vxS1aoT-QD~lF)V`3*zZC>{c($6n$X`6V+@G9*Ge6$a>bM> z`=e&AXBAco>jP+9x?w(TraXN~T`$;~Q7@jK$Lj%!DQ+LonDGC4JDc4)jw1?lou{C^ zQWjKzEWI)0m{Js3vSBLPUx$|xBZ?A8Nq`n!=283(+3UsfopY*xW-c#9CqMvOoIBIg zGt*sNKc}j?UVZ+JbZE43*EeYw2*6>(uTNLTh2U#$FV7PQDD$+D{os?3pti5!gMe|M z3q@U2HL+$+($Z0m)iVT*|2b1FCl@cfw5-QJg<8+8Yrl7e6}wSN zm))Slg^oj*eAw%&PuP|=B6#O2Z3c}zRx};Oudq+Pw%*^OswhL2-ULn32?hW) zhvmo&cnEXu=cl?Tjt9$RVQJJG{>FuC3>7HydP7*wrCMH#kzgyecU=!ElqO-JVLLdk zs7m0(gPrPT$H4~)M>bAqeK%p#k^zWX)~pyETnFju{*UUlXWC&|wwR)XN$B?0MU)Jd} z*kw-BCKrDAL~%)!clnZ@KcQxrVmJM-76L~^%U+(t7JP_u&)-(`Oe^12&un_(etL<7 zxYwp_Psy{Ayi+fqe?u~XdWmr6yD51Vn@y`y`%IuAz(l-A{gXZvHzn4=?u#lm^N5*F z2{bG|U`|79)IMwI_W?s#7(s=etDM|4)K9Ur*A>tpgN1y)J|gQ@@1m_D^=_=T^R2k2 z{IZ#{r-gLaU)c!+7*)z1pcGqtz^_Sva-i!C zF#w8xJ^V8h!Wu66@~KuHO*0&_^J&Uiu3NH>PKj<#qXCvezsS7;QxSyALlHnE{K$YG z&9TB>!Zak2xlL{aAJkbs=6g8(Cx|^`ysIQ0GuF(7^&ZV~obnV~IXJWB ztCv&usUmT;V#OR<1PX=E@UQD17d~@xj2P%MTq)6wo?-gPgkY37%kc$!S8_QnVj z`eMT>IBg#9j_i|R3mIcU^BMDxXJSZ{Et4+P(5r)CVV9C*2rP}X${Ms^_t_2O9?Aie zmQDFVLxfR^a-VI_5eH(7?NI|L)413OVAbBQRv-w3qsS^KjR=7?W2Q{9Ig4j$QhP3M ztC5x$SjwQB8$AdS5D)1{BMUL6|R$~2bngB~+(T-6SBa_vO zeDl5h894izo~6375I%PA2%}mPbnr+@0w!czAwMC!essm;`h%xX zaWlOx8yM+{jS4I7G)LXQoqKriYqSNnH6T$wMcR(7aOG9oaAK4>wdB=i_6uto^`70( z(J1mi+PuOvpnvx$8*0ne5!f8~UpwV1C<}6|T&P-TiJ*%S*YBvwXm<)sYQ83(!84gu zhGuq!=kX}ND$(}&U+)-a|BFhcfUt$;MLM)tU#)`|NdlaCo%H!e@EXu7RJxWq^&?JW zNsLm}ffV^qDqrZe{ScSC>AZ=WkKn(x=Z}%?vPmw!DCiZ3h1GL)PVFL8R{OQiE=clP zdbPxNmuIMMwDfyr9HjUEvK1x;@oEOwOOw@gw;QF|PPV*nY7PrbyQWM)v{2@_;C5M| zqtV)7mRHc5xO5xP5h4u++>C6mHZg8yr0ni~s(Bsy32xnz|0y_QlyDP%{!=BYn6!x| zVXqWS1*oH6(MnH>BwvfljC|j+kvv?HD6tp9dWjWk%p}WIvzw}I%G$e7M2i1K1CXqG zvBh&@72nZy5?nl2i_n+$EWrH&i?8h3Q;0a`2kO9nPH zh%v>9S{9>)1k&6_st{NP9EP%0@Jv}t4H|I7VqgPZ3q;W1x^ZTWY&Y3iIC#I)OmmXY zqg?<7xv#wbP&@%^PFBAke6)MX6BHGqYr#B1x%)V&F@^QjSX)Kr&7LwH%Duixz_?!D z_Y{Dp!9x7a(qY|VU!+NdL9lBrEkR{ahfI2iCf+s$o=FvEupt;PK&H^@F8q@qAm;l1 z;a>&T4Cdeft*oxCz-B?P}Y{N52AgF}uOG8P5sIbP-d~SFYW;O03`e_xRi~7jM+(JPRu_`D`ChiS)pBE$s^4Cgt@XFU zP*&cAQXiHSvud-ahv&-pWgt+4RxSRLT5kjV(20oJZw}$dawUZz0wm;;$et{Fhj!9F zuDmF$m*_nXZ=wnE7K86(*y!`|#G{~ZK{d&yNi3%rj_7@HhZ{1n%kN=9Ldj0Cp(bW^5%XS^3com-z`*U*<5nU znJCNCtFH`iLPd~8tyr{td|s#_R*cCb2UbHqwKh!BwuyDAWz>mKbMm+{XzTBs^6>36 zF@}U?f+vH25J#KEx+XQFCl4>y+tgXLm4rF?Iq3pY3^|N^)xXK~>1)7zA?FeVF-W~` z>yA~aYusPsH`?X&T2`)}uwriOyNk}&uDLu?TuC}jk_uXsXzv14%rDmbRqQ0xAC5nz zWwGEJ@5d&e=C*=`?_5KDLOvlPJE%6@jmp-SMr%MPqUMP~c0P#sVef{+bL=1RJeUvD zqp8$i$8K?2_7YKkvRAO&pdr&mM|2jqTxT-Xsx^Qt=T6h&X9XLR<|0#9CQnlIL=fw9 z6%w~|9$2j1jTC+2D1?#DiS%QWX95)WUx~=(lZ79nkdP;lUN%*)L2kaDAG+@Fyh1GA zp?OY+>_U}+`XT!@Nh=r}O?f85PT#|{GOn5zH(x*kdnQGh#Z3ObYBt(_tA{b^WjqbMiT6;}bVH&q8SN zb!TFr3j`+I(W^xoCELV6XlRK}RwIkvQ?S*jGOIs`(pl6-1p9B`24h}A5vE)nUXPdQ z>GO+WubxEI1^=BW873w9w8oxp@aM)TVaFQO0m7T4DL*%cu(s|BBUl-Db#`S5mL^j% z0u9GtAqghc6G8PcuY^c@Vg>AbZZak8PnLJ!^S<0Pz0%ukQp&9Buz`&{B6+i^Ibe1V zU)G+QG-T+9wUSdzxKM?Hzp_;4D@<_hCXlnQm{VBDq9;DdSjS}Ji(sVkETMo)hGrSc zE|sY1CHPIa%5p6tfGAF!Tqf4hj}GrW5r$5MCt!CkD8C@vk{Id+kwTT08^vaZ7Q!pV zweW^)O~LjF=5nLv9@8`G^$Cy$f-vS@ z3>Noy*xSqF+06>@E{+{W#PEw1nKubJ!X`uag!4jH*y@?{(L`Y{#EY;$G)5;z(Qs(< z^^yl)M-yohh8lnlfrg@eSrWnrcKm1R8621nU)lo-=m==M`^u~9!3qtdpR~P_TuRRy z^DfVqDm5K|liSl|RX=U$qT`>haHrPk0FrBOvAi zW8TczA)pRhl0sl#h0Kg)OBf-%U~XzoxVOE5lAAMdfDp#)(j`Ry-tzql5h^Cd38EIj zyJ?B1hu7!^jBP21K@_mSuQMj)|Ll22)Baz~B zZMYB>%L*fG=BtWs3!7|aFH6^Z&uDa_JIhKI8b?O=zdvo!r-1byDuf@eG`v&7l|LBC zUy2+K|B;ZaoC=l8lWFdJG8DHLppN|M{P-K31lu=A>@!Ceve1OQu!Uv= z0w>^)YIkT|;iiq1(4ThRtUtpt)~C=K(*s$_=6cnDJY|3mf5Q0@y1ahK zZ7XKM0=eIAL)31P2q7Yer%h+=mG0-ilc}YF^gjK+yxm-#9zmFnH-iBZW^?h2P~P6| z=K6heh8*fU>_PE`9(4bJz?Iur%&bJL1`1EJJRP}(VreiF3Er3|O@FFlebP2`kF-&T zVi3}TL@M2jC4}r{@#g7ea*V5eM0@-A3fg(JhOK~RQLE((s%=_S=$;@~IobW$MVgfiJjqGKqst4FrIaL z>+kH}X7v)T0_kET<$D)=1T70cyl`P$$sfh@T!~LKwebxY7iE=e^KNT=%}QS`X^$nn z#1deebLBZat^PXm+Fog63cdtp#HwPAu_DkHnd#CecaRtSGxBHP+Yk*}bQ?syUF{Yj zDvC~ID(;o2k!VZ;q-8PJ?m2w^uo)GA&X~zngIFU_0q%}>lH6rv0WY!v_GOoYYz%g$ zwd(Wsi^~x0+Z3^|tqKXE79Dh36am^FK}bXyx+5#VdpEvvL};*pWU%q55R?|QVrQ6- z6LMo16iB1@$V)Fc-&gYRqDvWrL}#irAe6%IC9=!$g6^!m0F!q#nG@{vRN1MF&Y#FHD)0T z0FjZN!GRYL?pMzLtz2nsD8+bjv*Rm(YEwZ$E?}X|v)N8NSYPdf-X5zJ&@EQTNL0+J zy@P^v@`pbh-#nUAGk1Tf1|$Hi?uw1d8+<|`0iFM(JE01G$b zMdf8Bxrg8hprk;&s@f2)`BIVk~hyv-r-Q#WpbZqs}YtG6fOHu5=|7*;d#H3cPH!2U}69y z=Ea_C8%5chKO%p$UOSy@3oRbFw!WETIJwt0Ms@@>w|NHzcMicgJOxhL?ECUj%0>8O ze{OuEZP+y$rA8tgWao|g=y0Xp=Aj~e!caqH6QAI*_1!ml0`4*dPDus?BCIG6G-vzr zdo7zmVDl2=cIkfsx?0PzPj?F&q?ve^$SYVG(crb7nE9A4&I#AdF%ci|Q!*B%@tH$~ zrYzR_h|*H*?5M&H#f2C5yq5LC1l&HbB~*ouHcH47n$s4F)+(q_ZygF$VWK4OB;^et z!uGN~R&hN^)>XQaNORR`Z-wmTGVjpSkZ9ayr{bA-oM&+Yv-aOJfnQq3+*qH-b0lbc zC+6%H3*(kZg`ImQpR?8q15O&56bCC-=vc*0>z%Qec`d(URw}$co`08;RO!BnD?zM*gtp1(>$3ESPvB zjDk^FgHqJI$vASOFGtPu`aWqGcC47k?CIRs_GWZhgnhIs1eTo_Upz()VvUPLq>u_( zb$oROM(PX$!)Hv978;Gs&}CXn*~zqZpfD~P7JOsedQ4&OtCD_k(3aEoCSLQr16>?# z6+(2%W(VH*_~5a^g~51V3q9cvHjsc=rN#uF2raU^oRi8x)InN>6d2fetQyuR#oSpY z-SOEOf#|N8pVL zS3D3uru_=0UC-e2RK_(gVzA2fC7pMv+wSPPH=+fB=@|piCZkUe6bQ8l@Yg;E423VS zIqn5nMpiQFkL5VAq)|NJ{_W6y8*gy5DF2ssZP|+ZkNSVPrd3g$=rUpkL<=e3kqu!+ z6d_jx|B0cz=|tZoZ#wrP!4%V($0Fc`pR#N@*cmXGC7~T3Gjm>FD+l(@=?1{~LeQeo z&2)0UKg|rR!KDmhiqZLCC^pmxY$v_@lgk9cRPfR|JP+6n4D78j(;Q6q_KW4s`F5OW zVB9zZH?jMw>r~?sQ*0^Nrf_gxZ=^m`?T+g>G2|qry}y?JQai89Hy1EzI;q20R9Djm z?R8l8HCyG(Ak9<{65K_WFvM4ARi1!6Ph`7|~R)M%?DKOX;&WRM1$CnEr?ea4}sNAADklP@q zI4bqJn|wHu#P?re9-b0=h(I6|VvT%GC}ylnt67RA*=btb7?SY9fK}tgLXa`L0Qoqi zNO2LEgUA6Tkvb)A1Z#pNGpN}{^5AdUzQtKN+_3e}tZd@l6(Z>Yb%10L?4LgWh9X2S zTZc{RgKIlF%xNMH(&1J8(Y_btis?mWmCj`b|F%Ifp)Ycaw3QDp*8?>r2m6~!p>>=g z!VW*2gqeg961&p$x;iA^DCEP%v9S5-CKuFefg_H0m58aQp!9t(nw}d6Xy?zzrq>O1 zZy37987cP*U@O`iIqkmU4x3w`vyhKhT{s;lU+-%SZAO*kfYBE0@2p#Uc; z16(443AkH*LStppKW^AjIVO_*^S`zio4)D8{I&x`DWPyHh_WbKSEkrtM-!MQ3*{eD z-cyU7q5m?xx9I#y7(nT$U%+-2ck)^Ytv&EeoJ<9tc7(OUE*&lAQUF9T!R;5_j?XmL YU0vJ5r7$z4+%9<9@4s`w;1;+42OB$+f&c&j diff --git a/meteor/i18n/nb.po b/meteor/i18n/nb.po index fc71d132ee3..635e55c84e3 100644 --- a/meteor/i18n/nb.po +++ b/meteor/i18n/nb.po @@ -1,2883 +1,3365 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2022-08-02T12:04:21.289Z\n" -"PO-Revision-Date: 2022-08-02 14:14+0200\n" -"Last-Translator: \n" -"Language-Team: \n" -"Language: nb\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" +"mime-version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 3.1.1\n" - -msgid "Account Page" -msgstr "Brukerkontoside" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"POT-Creation-Date: 2026-04-14T14:17:21.170Z\n" +"PO-Revision-Date: 2026-04-14T14:17:21.170Z\n" +"Language: nb\n" -msgid "Name:" -msgstr "Navn:" +msgid " The index of the Atem media/clip banks" +msgstr "" -msgid "Email:" -msgstr "E-post:" +msgid "({{time}} ago)" +msgstr "(for {{time}} siden)" -msgid "Old Password" -msgstr "Gammelt passord" +msgid "({{timecode}})" +msgstr "({{timecode}})" -msgid "New Password" -msgstr "Nytt passord" +msgid "" +"(Comma separated list. Empty - will store snapshots of all Rundown " +"Playlists)" +msgstr "" -msgid "Save Changes" -msgstr "Lagre endringer" +msgid "(Default)" +msgstr "" -msgid "Edit Account" -msgstr "Endre brukerkonto" +msgid "(in: {{time}})" +msgstr "(om: {{time}})" -msgid "Organization" -msgstr "Organisasjon" +msgid "(Optional) A name/identifier of the local network where the Atem is located" +msgstr "" -msgid "User roles in organization" -msgstr "Brukerroller i organisasjon" +msgid "(Optional) A name/identifier of the local network where the share is located" +msgstr "" +"(Valgfri) Et navn/en identifikator for det lokale nettverket hvor den delte " +"mappen er lokalisert" -msgid "Studio" -msgstr "Studio" +msgid "" +"(Optional) A name/identifier of the local network where the share is " +"located, leave empty if globally accessible" +msgstr "" +"(Valgfri) Et navn/en identifikator for det lokale nettverket hvor den delte " +"mappen er lokalisert, la være tom dersom den er globalt tilgjengelig" -msgid "Configurator" -msgstr "Configurator" +msgid "(Optional) This could be the name of the compute" +msgstr "" -msgid "Developer" -msgstr "Developer" +msgid "" +"(Optional) This could be the name of the computer on which the local folder " +"is on" +msgstr "(Valgfri) Dette kan være navnet til datamaskinen som den lokale mappen er på" -msgid "Admin" -msgstr "Admin" +msgid "(Unknown playlist)" +msgstr "(Ukjent kjøreplanliste)" -msgid "Remove Self" -msgstr "Fjern denne brukeren" +msgid "(Unknown rundown)" +msgstr "(Ukjent kjøreplan)" -msgid "Email Address" -msgstr "E-postadresse" +msgid "" +"(what happened and when, what should have happened, what could have " +"triggered the problems, etcetera...)" +msgstr "" -msgid "Password" -msgstr "Passord" +msgid "{{actionName}} failed! More information can be found in the system log." +msgstr "" -msgid "Sign in" -msgstr "Logg inn" +msgid "{{currentRundownName}} - {{rundownPlaylistName}}" +msgstr "{{currentRundownName}} - {{rundownPlaylistName}}" -msgid "Create New Account" -msgstr "Opprett ny brukerkonto" +msgid "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)" +msgstr "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)" -msgid "Lost password?" -msgstr "Glemt passord?" +msgid "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago" +msgstr "for {{days}} dager, {{hours}} t {{minutes}} min {{seconds}} s siden" -msgid "Send reset email" -msgstr "Send e-post for å nullstille" +msgid "{{frames}} black frames detected in the clip" +msgstr "" -msgid "Go back" -msgstr "Tilbake" +msgid "{{frames}} black frames detected within the clip" +msgstr "" -msgid "Password must be atleast 5 characters long" -msgstr "Passord må være minst 5 tegn langt" +msgid "{{frames}} freeze frames detected in the clip" +msgstr "" -msgid "Enter your new password" -msgstr "Skriv inn ditt nye passord" +msgid "{{frames}} freeze frames detected within the clip" +msgstr "" -msgid "Set new password" -msgstr "Lagre nytt passord" +msgid "{{hours}} h {{minutes}} min {{seconds}} s ago" +msgstr "for {{hours}} t {{minutes}} min {{seconds}} s siden" -msgid "Your Account" -msgstr "Din brukerkonto" +msgid "{{indexCount}} indexes was removed." +msgstr "{{indexCount}} indexer ble fjernet." -msgid "About Your Organization" -msgstr "Om din oranisasjon" +msgid "{{minutes}} min {{seconds}} s ago" +msgstr "for {{minutes}} min {{seconds}} s siden" -msgid "We are mainly" -msgstr "Vi er hovedsaklig" +msgid "{{nrcsName}} Connection" +msgstr "{{nrcsName}}-tilkobling" -msgid "Areas" -msgstr "Områder" +msgid "{{prevStatements}} and {{finalStatement}}" +msgstr "" -msgid "Invite User" -msgstr "Inviter brukar" +msgid "{{prevStatements}} or {{finalStatement}}" +msgstr "" -msgid "New User's Email" -msgstr "Ny brukers e-post" +msgid "" +"{{reason}} {{sourceLayer}} exists, but is not yet ready on the playout " +"system" +msgstr "" -msgid "New User's Name" -msgstr "Ny brukers navn" +msgid "{{rundownPlaylistName}} (Looping)" +msgstr "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)" -msgid "Create New User & Send Enrollment Email" -msgstr "Opprett ny bruker og send e-post for innmelding" +msgid "{{seconds}} s ago" +msgstr "for {{seconds}} s siden" -msgid "Users in organization" -msgstr "Brukere i organisasjonen" +msgid "{{showStyleVariant}} – {{showStyleBase}}" +msgstr "{{showStyleVariant}} – {{showStyleBase}}" -msgid "Return to list" -msgstr "Gå tilbake til listen" +msgid "{{sourceLayer}} can't be found on the playout system" +msgstr "" -msgid "There is no rundown active in this studio." -msgstr "Fant ingen aktive kjøreplaner for dette studioet." +msgid "{{sourceLayer}} doesn't have both audio & video" +msgstr "{{sourceLayer}} har ikke lyd og/eller bilde" -msgid "This studio doesn't exist." -msgstr "Dette studioet finnes ikke." +msgid "{{sourceLayer}} has {{audioStreams}} audio streams" +msgstr "{{sourceLayer}} har {{audioStreams}} lydstrømmer" -msgid "There are no active rundowns." -msgstr "Fant ingen aktive kjøreplaner." +msgid "{{sourceLayer}} has the wrong format: {{format}}" +msgstr "{{sourceLayer}}-formatet er ikke støttet: {{format}}" -msgid "Evaluation" -msgstr "Evaluering" +msgid "{{sourceLayer}} has unsupported source: {{containerLabels}}" +msgstr "" -msgid "Please take a minute to fill in this form." -msgstr "Vennligst fyll ut dette skjemaet." +msgid "{{sourceLayer}} is being ingested" +msgstr "{{sourceLayer}} blir prosessert" msgid "" -"Be aware that while filling out the form keyboard and streamdeck commands " -"will not be executed!" -msgstr "OBS! Du kan ikke utføre Sofie-kommandoer mens du skriver evaluering!" +"{{sourceLayer}} is in a placeholder state for an unknown workflow-defined " +"reason" +msgstr "" -msgid "Did you have any problems with the broadcast?" -msgstr "Hadde du noen problemer med sendingen?" +msgid "{{sourceLayer}} is in an unknown state: \"{{status}}\"" +msgstr "" -msgid "" -"Please explain the problems you experienced (what happened and when, what " -"should have happened, what could have triggered the problems, etcetera...)" +msgid "{{sourceLayer}} is missing" msgstr "" -"Vennligst forklar problemene du opplevde (hva skjedde og når skjedde det, " -"hva skulle skjedd, hva kan ha utløst problemene, o.s.v.)" -msgid "Your name" -msgstr "Ditt navn" +msgid "{{sourceLayer}} is missing a file path" +msgstr "{{sourceLayer}} kan ikke spilles av fordi filnavnet mangler" + +msgid "{{sourceLayer}} is not yet ready on the playout system" +msgstr "{{sourceLayer}} er ennå ikke klar til å spilles ut fra avviklingsserver" -msgid "Save message" -msgstr "Lagre melding" +msgid "{{sourceLayer}} is transferring to the playout system" +msgstr "{{sourceLayer}} overføres til avviklingsserver" -msgid "Save message and Deactivate Rundown" -msgstr "Send evalueringen og deaktiver kjøreplanen" +msgid "" +"{{sourceLayer}} is transferring to the playout system but cannot be played " +"yet" +msgstr "" -msgid "No problems" -msgstr "Ingen problemer" +msgid "{{studioName}}: Active Rundown" +msgstr "" -msgid "Something went wrong, but it didn't affect the output" -msgstr "Noe gikk galt, men det påvirket ikke sendingen" +msgid "{{studioName}}: Presenter screen" +msgstr "" -msgid "Something went wrong, and it affected the output" -msgstr "Noe gikk galt, og det påvirket sendingen" +msgid "{{studioName}}: Prompter" +msgstr "" -msgid "Are you sure?" -msgstr "Er du sikker?" +msgid "14 = 7 lines, 20 = 5 lines" +msgstr "" -msgid "" -"Trimming this clip has timed out. It's possible that the story is currently " -"locked for writing in {{nrcsName}} and will eventually be updated. Make sure " -"that the story is not being edited by other users." +msgid "A device must be assigned to the config to edit the settings" msgstr "" -"Endring av inn-/utpunkt for dette klippet tar lang tid. Det er mulig manuset " -"i er låst i {{nrcsName}} og at inn-/utpunkt endres om litt. Forsikre deg om " -"at manuset ikke blir redigert av andre brukere." -msgid "Trimming this clip has failed due to an error: {{error}}." -msgstr "Endring av inn-/utpunkt for dette klippet feilet: {{error}}." +msgid "" +"A Full System Snapshot contains all system settings (studios, showstyles, " +"blueprints, devices, etc.)" +msgstr "" +"Et systemsnapshot inneholder alle systeminnstillinger (studio, showstyles, " +"blueprints, enheter o.s.v.)" -msgid "Trimmed succesfully." -msgstr "Endring av inn-/utpunkt var vellykket." +msgid "a second" +msgstr "" msgid "" -"Trimming this clip is taking longer than expected. It's possible that the " -"story is locked for writing in {{nrcsName}}." +"A snapshot of the current Running Order has been created for " +"troubleshooting." +msgstr "Et snapshot av den gjeldende kjøreplanen har blitt opprettet for feilsøking." + +msgid "A Studio Snapshot contains all system settings related to that studio" +msgstr "Et studiosnapshot inneholder alle systeminnstillinger tilknyttet et studio" + +msgid "AB Channel Display" msgstr "" -"Endring av inn-/utpunkt for dette klippet tek meir tid enn forventa. Det er " -"mogleg manuset er låst for redigering i {{nrcsName}}." -msgid "Trim \"{{name}}\"" -msgstr "Trim \"{{name}}\"" +msgid "AB Playout devices" +msgstr "" -msgid "OK" -msgstr "OK" +msgid "AB Resolver Channel Display" +msgstr "" -msgid "Cancel" +msgid "Abort" msgstr "Avbryt" -msgid "Remove in-trimming" -msgstr "Nullstill innpunkt" +msgid "Aborting all Media Workflows" +msgstr "" -msgid "In" -msgstr "Inn" +msgid "Aborting Media Workflow" +msgstr "" -msgid "Remove all trimming" -msgstr "Nullstill inn- og utpunkt" +msgid "Accessor ID" +msgstr "Aksessor-id" -msgid "Duration" -msgstr "Varighet" +msgid "Accessor Type" +msgstr "Aksessortype" -msgid "Remove out-trimming" -msgstr "Nullstill utpunkt" +msgid "Accessors" +msgstr "Aksessorer" -msgid "Out" -msgstr "Ut" +msgid "Action" +msgstr "Handling" -msgid "Next" -msgstr "Neste" +msgid "Action {{actionName}} done!" +msgstr "" -msgid "Test test" -msgstr "Test test" +msgid "Action {{actionName}} failed: {{error}}" +msgstr "" -msgid "Until next take" -msgstr "Til neste Take" +msgid "Action Buttons" +msgstr "Handlingsknapper" -msgid "Until next segment" -msgstr "Til neste segment" +msgid "Action Triggers" +msgstr "Handlingsutløsere" -msgid "Until end of segment" -msgstr "Til slutten av segment" +msgid "Activate \"On Air\"" +msgstr "" -msgid "Until next rundown" -msgstr "Til neste kjøreplan" +msgid "Activate \"Rehearsal\"" +msgstr "" -msgid "Until end of showstyle" -msgstr "Til slutten av showstyle" +msgid "Activate (On-Air)" +msgstr "Aktiver (gå ON AIR)" -msgid "Script is empty" -msgstr "Manuset er tomt" +msgid "Activate (Rehearsal)" +msgstr "Aktiver (testmodus)" -msgid "Clip:" -msgstr "Klipp:" +msgid "Activate On Air" +msgstr "" -msgid "Home" -msgstr "Hjem" +msgid "Activate Rundown" +msgstr "Aktiver kjøreplan" -msgid "Rundowns" -msgstr "Kjøreplaner" +msgid "Activating Hold" +msgstr "" -msgid "Test Tools" -msgstr "Testverktøy" +msgid "Activating Rundown Playlist" +msgstr "" -msgid "Status" -msgstr "Status" +msgid "Active" +msgstr "Aktiv" -msgid "Settings" -msgstr "Innstillinger" +msgid "Active Rundown" +msgstr "" -msgid "Account" -msgstr "Konto" +msgid "Active Rundown View" +msgstr "" -msgid "Logout" -msgstr "Logg ut" +msgid "Ad-Lib" +msgstr "Adlib" -msgid "My name is {{name}}" -msgstr "Mitt navn er {{name}}" +msgid "Ad-Lib Action" +msgstr "Adlib-handling" -msgid "Operating Mode" -msgstr "Styringsmodus" +msgid "Add" +msgstr "Legg til" -msgid "Switching operating mode to {{mode}}" -msgstr "Bytt til {{mode}}" +msgid "Add {{filtersTitle}}" +msgstr "Legg til {{filtersTitle}}" -msgid "Prompter" -msgstr "Prompter" +msgid "Add a playout device to the studio in order to configure the route sets" +msgstr "" +"For å kunne redigere omkoblingsgrupper, må du legge til en playout-enhet " +"til studio" -msgid "End of script" -msgstr "Slutt på manus" +msgid "Add a playout device to the studio in order to edit the layer mappings" +msgstr "" +"For å kunne redigere lagmappinger, må du legge til en playout-enhet til " +"studio" -msgid "Could not get system status. Please consult system administrator." -msgstr "Kan ikke innhente status for systemet. Kontakt systemadministrator." +msgid "Add Action Trigger" +msgstr "" -msgid "There are no rundowns ingested into Sofie." -msgstr "Det er ikke sendt kjøreplaner til Sofie." +msgid "Add button" +msgstr "Legg til knapp" -msgid "Click on a rundown to control your studio" -msgstr "Klikk på en kjøreplan for å kontrollere studioet ditt" +msgid "Add filter" +msgstr "Legg til filter" -msgid "Rundown" -msgstr "Kjøreplan" +msgid "Add some source layers (e.g. Graphics) for your data to appear in rundowns" +msgstr "Legg til kildelag (for eksempel Grafikk) for å vise dine data i kjøreplaner" -msgid "Problems" -msgstr "Problemer" +msgid "AdLib" +msgstr "Adlib" -msgid "Show Style" -msgstr "Showstyle" +msgid "AdLib Actions are not supported in the current Rundown" +msgstr "" -msgid "On Air Start Time" -msgstr "Sendestart" +msgid "AdLib could not be found!" +msgstr "" -msgid "Expected End Time" -msgstr "Forventet sendeslutt" +msgid "AdLib filter" +msgstr "" -msgid "Last updated" -msgstr "Sist oppdatert" +msgid "Adlib Rank" +msgstr "Adlib-rang" -msgid "View Layout" -msgstr "Vis layout" +msgid "Adlib rundowns are not supported for this ShowStyle!" +msgstr "" -msgid "Today" -msgstr "I dag" +msgid "Adlib Testing" +msgstr "" -msgid "Yesterday" -msgstr "I går" +msgid "AdLib Testing" +msgstr "" -msgid "Tomorrow" -msgstr "I morgen" +msgid "AdLibs can be only placed in a currently playing part!" +msgstr "" -msgid "Last" -msgstr "Forrige" +msgid "AdLibs on this layer can be queued" +msgstr "Adliber på dette laget kan cues" -msgid "Getting Started" -msgstr "Kom i gang" +msgid "All additional source layers must have active pieces" +msgstr "" -msgid "" -"Start with giving this browser configuration permissions by adding this to " -"the URL: " -msgstr "Først må du gå i konfigurasjonsmodus ved å legge dette til url-en: " +msgid "All connections working correctly" +msgstr "Alle tilkoblinger er OK" -msgid "Start Here!" -msgstr "Start her!" +msgid "All devices working correctly" +msgstr "Alle enheter fungerer som de skal" -msgid "Then, run the migrations script:" -msgstr "Kjør så migreringsprosedyren:" +msgid "All is well, go get a" +msgstr "Alt er greit, gå og hent deg en" -msgid "Run Migrations to get set up" -msgstr "Kjør migreringsprosedyrer for å sette opp" +msgid "All Screens in a MultiViewer" +msgstr "" -msgid "Migrations" -msgstr "Migrering" +msgid "All steps" +msgstr "Alle trinn" -msgid "Documentation is available at" -msgstr "Dokumentasjon er tilgjengelig på" +msgid "Allow direct playing pieces" +msgstr "" -msgid "Use {{nrcsName}} order" -msgstr "Bruk rekkefølge fra {{nrcsName}}" +msgid "Allow disabling of Pieces" +msgstr "Tillat deaktivering av elementer" -msgid "Reset Sort Order" -msgstr "Tilbakestill rekkefølge" +msgid "Allow HOLD mode" +msgstr "" -msgid "Enable configuration mode by adding ?configure=1 to the address bar." +msgid "Allow infinites from AdLib testing to persist" msgstr "" -"Aktiver konfigurasjonsmodus ved å legge til ?configure=1 på slutten av " -"nettadressen." -msgid "You need to run migrations to set the system up for operation." -msgstr "Du må kjøre migrering for å klargjøre systemet for bruk." +msgid "Allow Read access" +msgstr "Tillat lesing" -msgid "Drop Rundown here to move it out of its current Playlist" -msgstr "Slipp kjøreplanen her for å flytte den ut av spillelisten" +msgid "Allow Rundowns to be reset while on-air" +msgstr "Tillat tilbakestilling av kjøreplaner som er on-air" -msgid "Sofie Automation" -msgstr "Sofie" +msgid "Allow Write access" +msgstr "Tillat skriving/lagring" -msgid "version" -msgstr "versjon" +msgid "Also Require Source Layers" +msgstr "" -msgid "System Status" -msgstr "Systemstatus" +msgid "Amount of entries exceeds the limt of 10 000 items." +msgstr "" -msgid "System has issues which need to be resolved" -msgstr "Systemet har problemer som må fikses" +msgid "An error while performing the take, playout may be impacted" +msgstr "" -msgid "Status Messages:" -msgstr "Statusmeldinger:" +msgid "An error while setting the next Part, playout may be impacted" +msgstr "" -msgid "{{showStyleVariant}} – {{showStyleBase}}" -msgstr "{{showStyleVariant}} – {{showStyleBase}}" +msgid "An internal error occured!" +msgstr "" -msgid "Drag to reorder or move out of playlist" -msgstr "Dra for å endre rekkefølge eller flytte ut av spillelisten" +msgid "Another Rundown is Already Active!" +msgstr "En annen kjøreplan er allerede aktiv!" -msgid "This rundown is currently active" -msgstr "Denne kjøreplanen er allerede aktiv" +msgid "Answers" +msgstr "Svar" -msgid "Not set" -msgstr "Ikke angitt" +msgid "" +"Any AB Playout devices here will only be active when this or another " +"RouteSet that includes them is active" +msgstr "" -msgid "This rundown will loop indefinitely" -msgstr "Denne kjøreplanen vil gå i en uendelig loop" +msgid "APM Enabled" +msgstr "AMP aktivert" -msgid "({{timecode}})" -msgstr "({{timecode}})" +msgid "APM Transaction Sample Rate" +msgstr "Prøvefrekvens for AMP-transaksjoner" -msgid "Re-sync rundown data with {{nrcsName}}" -msgstr "Ikke synkronisert med MOS/{{nrcsName}}" +msgid "Append" +msgstr "Legg til" -msgid "Delete" -msgstr "Slett" +msgid "Append or Replace" +msgstr "Legg til eller erstatt" -msgid "Standalone Shelf" -msgstr "Frittstående skuff" +msgid "Append rows" +msgstr "" -msgid "Rundown & Shelf" -msgstr "Kjøreplan & skuff" +msgid "Application credentials" +msgstr "Brukernavn/passord (Application Credentials)" -msgid "Default" -msgstr "Standard" +msgid "Application Performance Monitoring" +msgstr "Overvåkning av applikasjonsytelse (AMP)" -msgid "Delete rundown?" -msgstr "Slette kjøreplanen?" +msgid "Apply" +msgstr "" -msgid "Are you sure you want to delete the \"{{name}}\" rundown?" -msgstr "Er du sikker på at du vil slette kjøreplanen \"{{name}}\"?" +msgid "Apply blueprint upgrades" +msgstr "" -msgid "Please note: This action is irreversible!" -msgstr "Merk: Denne handlingen kan ikke angres!" +msgid "Apply Config" +msgstr "" -msgid "Re-Sync rundown?" -msgstr "Synkroniser kjøreplanen med ENPS?" +msgid "Are you sure you want to activate Rehearsal Mode?" +msgstr "Er du sikker på at du vil gå i testmodus?" -msgid "Re-Sync" -msgstr "Synkroniser" +msgid "" +"Are you sure you want to deactivate this Rundown\n" +"(This will clear the outputs)" +msgstr "" +"Er du sikker på at du vil deaktivere denne kjøreplanen?\n" +"(Dette vil nullstille alle utganger.)" -msgid "Are you sure you want to re-sync the \"{{name}}\" rundown?" +msgid "" +"Are you sure you want to deactivate this rundown?\n" +"(This will clear the outputs.)" msgstr "" -"Er du sikker på at du vil synkronisere kjøreplanen \"{{rundownSlug}}\" med " -"ENPS?" -msgid "Start time is close" -msgstr "Oppgitt sendestart er hvert øyeblikk" +msgid "Are you sure you want to delete output layer \"{{outputId}}\"?" +msgstr "" -msgid "Yes" -msgstr "Ja" +msgid "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?" +msgstr "Er du sikker på at du vil slette kildelaget \"{{sourceLayerId}}\"?" -msgid "No" -msgstr "Nei" +msgid "Are you sure you want to delete the \"{{name}}\" rundown?" +msgstr "Er du sikker på at du vil slette kjøreplanen \"{{name}}\"?" -msgid "" -"You are in rehearsal mode, the broadcast starts in less than 1 minute. Do " -"you want to reset the rundown and go into On-Air mode?" -msgstr "" -"Du er i testmodus og sendingen starter om mindre enn ett minutt. Vil du " -"laste inn kjøreplanen på nytt og gjøre klar til sending?" +msgid "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?" +msgstr "Er du sikker på at du vil slette blueprintet \"{{blueprintId}}\"?" -msgid "Hold" -msgstr "Hold" +msgid "Are you sure you want to delete the shelf layout \"{{name}}\"?" +msgstr "Er du sikker på at du vil slette layouten \"{{name}}\"?" -msgid "Could not find a Piece that can be disabled." -msgstr "Kunne ikke finne et element som kan skippes." +msgid "Are you sure you want to delete the show style \"{{showStyleId}}\"?" +msgstr "Er du sikker på at du vil slette showstylen \"{{showStyleId}}\"?" -msgid "Failed to execute take" -msgstr "Kunne ikke gjennomføre Take" +msgid "Are you sure you want to delete the studio \"{{studioId}}\"?" +msgstr "Er du sikker på at du vil slette studioet \"{{studioId}}\"?" -msgid "" -"The rundown you are trying to execute a take on is inactive, would you like " -"to activate this rundown?" -msgstr "" -"Du prøve å gjøre en Take i en inaktiv kjøreplan. Vil du aktivere denne " -"kjøreplanen?" +msgid "Are you sure you want to delete this AdLib?" +msgstr "Er du sikker på at du vil slette denne adliben?" -msgid "Activate (Rehearsal)" -msgstr "Aktiver (testmodus)" +msgid "Are you sure you want to delete this Bucket?" +msgstr "Er du sikker på at du vil slette denne bøtten?" -msgid "Activate (On-Air)" -msgstr "Aktiver (gå ON AIR)" +msgid "Are you sure you want to delete this device: \"{{deviceId}}\"?" +msgstr "Er du sikker på at du vil fjerne enheten \"{{deviceId}}\"?" -msgid "Failed to activate" -msgstr "Kunne ikke aktivere" +msgid "Are you sure you want to empty (remove all adlibs inside) this Bucket?" +msgstr "Er du sikker på at du vil tømme denne bøtten (fjerner alle adliber)?" msgid "" -"Something went wrong, please contact the system administrator if the problem " -"persists." -msgstr "Noe gikk galt, kontakt systemadministrator hvis problemet fortsetter." +"Are you sure you want to force the migration? This will bypass the " +"migration checks, so be sure to verify that the values in the settings are " +"correct!" +msgstr "" +"Er du sikker på at du vil tvinge migreringen? Dette gjør at du hopper over " +"migreringskontrollene, så vær sikker på at verdiene oppgitt i innstillinger " +"er korrekte!" -msgid "Another Rundown is Already Active!" -msgstr "En annen kjøreplan er allerede aktiv!" +msgid "Are you sure you want to import the contents of the file \"{{fileName}}\"?" +msgstr "" -msgid "" -"The rundown \"{{rundownName}}\" will need to be deactivated in order to " -"activate this one.\n" -"\n" -"Are you sure you want to activate this one anyway?" +msgid "Are you sure you want to re-sync the \"{{name}}\" rundown?" msgstr "" -"Kjøreplanen \"{{rundownName}}\" må deaktiveres for å aktivere denne " -"kjøreplanen.\n" -"\n" -"Er du sikker på at du ønsker å aktivere?" +"Er du sikker på at du vil synkronisere kjøreplanen \"{{rundownSlug}}\" med " +"ENPS?" -msgid "Activate Anyway (Rehearsal)" -msgstr "Aktiver uansett (testmodus)" +msgid "" +"Are you sure you want to re-sync the Rundown?\n" +"(If the currently playing Part has been changed, this can affect the output)" +msgstr "" +"Er du sikker på at du vil synkronisere denne kjøreplanen?\n" +"(Dette kan påvirke gjennomføring av en pågående sending)" -msgid "Activate Anyway (On-Air)" -msgstr "Aktiver uansett (gå ON AIR)" +msgid "Are you sure you want to remove {{type}} \"{{deviceId}}\"?" +msgstr "Er du sikker på at du vil fjerne enheten {{type}} \"{{deviceId}}\"?" -msgid "Do you want to activate this Rundown?" -msgstr "Vil du aktivere denne kjøreplanen?" +msgid "Are you sure you want to remove all Variants in the table?" +msgstr "" msgid "" -"The planned end time has passed, are you sure you want to activate this " -"Rundown?" +"Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\n" +"Route Sets assigned to this group will be reset to no group." msgstr "" -"Det planlagte sluttidspunktet er passert, er du sikker på at du vil aktivere " -"denne kjøreplan?" +"Er du sikker på at du vil fjerne eksklusivitetsgruppen \"{{eGroupName}}\"?\n" +"Omkoblinger satt til denne gruppen vil bli resatt til ingen gruppe." -msgid "Are you sure you want to activate Rehearsal Mode?" -msgstr "Er du sikker på at du vil gå i testmodus?" +msgid "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?" +msgstr "Er du sikker på at du fil fjerne mappingen for laget \"{{mappingId}}\"?" + +msgid "Are you sure you want to remove the AB Player \"{{playerId}}\"?" +msgstr "" msgid "" -"Are you sure you want to deactivate this Rundown?\n" -"(This will clear the outputs)" +"Are you sure you want to remove the device \"{{deviceName}}\" and all of " +"it's sub-devices?" msgstr "" -"Er du sikker på at du vil deaktivere denne kjøreplanen?\n" -"(Dette vil nullstille alle utganger.)" +"Er du sikker på at du vil fjerne enheten \"{{deviceName}}\" og alle dens " +"underenheter?" -msgid "The rundown can not be reset while it is active" -msgstr "En aktivert kjøreplan kan ikke tilbakestilles" +msgid "Are you sure you want to remove the Package Container \"{{containerId}}\"?" +msgstr "Er du sikker på at du vil fjerne pakkekontaineren \"{{containerId}}\"?" msgid "" -"A snapshot of the current Running Order has been created for troubleshooting." +"Are you sure you want to remove the Package Container Accessor " +"\"{{accessorId}}\"?" msgstr "" -"Et snapshot av den gjeldende kjøreplanen har blitt opprettet for feilsøking." +"Er du sikker på at du vil fjerne pakkekontainer-aksessoren " +"\"{{accessorId}}\"?" -msgid "Prepare Studio and Activate (Rehearsal)" -msgstr "Forbered studio og aktiver testmodus" +msgid "" +"Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to " +"\"{{newLayerId}}\"?" +msgstr "" +"Er du sikker på at du vil fjerne omkoblingen fra \"{{sourceLayerId}}\" til " +"\"{{newLayerId}}\"?" + +msgid "Are you sure you want to remove the Route Set \"{{routeId}}\"?" +msgstr "Er du sikker på at du vil fjerne omkoblingsgruppen \"{{routeId}}\"?" + +msgid "Are you sure you want to remove the Variant \"{{showStyleVariantId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to replace the blueprints with the file " +"\"{{fileName}}\"?" +msgstr "Er du sikker på at du vil erstatte blueprints fra filen \"{{fileName}}\"?" + +msgid "" +"Are you sure you want to reset all overrides for Packing Container " +"\"{{id}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to reset all overrides for the mapping for layer " +"\"{{mappingId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to reset all overrides for the output layer " +"\"{{outputLayerId}}\"?" +msgstr "" + +msgid "Are you sure you want to reset all overrides for the selected row?" +msgstr "" + +msgid "" +"Are you sure you want to reset all overrides for the source layer " +"\"{{sourceLayerId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to reset the database version?\n" +"Only do this if you plan on running the migration right after." +msgstr "" +"Er du sikker på at du vil nullstille databaseversjonen?\n" +"Bare gjør dette dersom du har tenkt å kjøre en migrering umiddelbart." + +msgid "Are you sure you want to restart this device?" +msgstr "Er du sikker på at du vil starte denne enheten på nytt?" + +msgid "" +"Are you sure you want to restart this Sofie Automation Server Core: " +"{{name}}?" +msgstr "Er du sikker på at du vil starte Sofie Core: {{name}} på nytt?" + +msgid "" +"Are you sure you want to restore the system from the snapshot file " +"\"{{fileName}}\"?" +msgstr "" +"Er du sikker på at du vil gjenopprettet systemet fra snapshotfilen " +"\"{{fileName}}\"?" + +msgid "Are you sure you want to skip the fix up config step for {{name}}" +msgstr "" + +msgid "" +"Are you sure you want to update the blueprints from the file " +"\"{{fileName}}\"?" +msgstr "Er du sikker på at du vil oppdatere blueprints fra filen \"{{fileName}}\"?" + +msgid "" +"Are you sure you want to upload the shelf layout from the file " +"\"{{fileName}}\"?" +msgstr "" +"Er du sikker på at du vil laste opp layout for skuff fra filen " +"\"{{fileName}}\"?" + +msgid "" +"Are you sure, do you really want to REMOVE the Snapshot " +"\"{{snapshotName}}\"?\r\n" +"This cannot be undone!!" +msgstr "" + +msgid "Are you sure?" +msgstr "Er du sikker?" + +msgid "" +"Are you sure? This will cause the whole Sofie system to be unresponsive " +"several seconds!" +msgstr "" + +msgid "Around 10 minutes ago" +msgstr "Cirka 10 minutter siden" + +msgid "Assign" +msgstr "Tilordne" + +msgid "Assigned Show Styles" +msgstr "" + +msgid "Assigned Studios" +msgstr "" + +msgid "Attached Subdevices" +msgstr "Tilkoblede underenheter" + +msgid "Audio Mixing" +msgstr "Lydmiksing" + +msgid "Authorize App Access" +msgstr "" + +msgid "Auto" +msgstr "Auto" + +msgid "AutoNext in QuickLoop behavior" +msgstr "" + +msgid "Available Screens for Studio {{studioId}}" +msgstr "" + +msgid "Bad" +msgstr "Feil" + +msgid "Bank Index" +msgstr "" + +msgid "Base URL" +msgstr "Base-url" + +msgid "Base url to the resource (example: http://myserver/folder)" +msgstr "Base-url for ressursen (eksempel: http://minserver/mappe)" + +msgid "Baseline needs reload, this studio may not work until reloaded" +msgstr "" +"Baseline må lastes på nytt, dette studioet vil kanskje ikke fungere før " +"baseline er lastet på nytt" + +msgid "Behavior" +msgstr "Oppførsel" + +msgid "Blueprint" +msgstr "Blueprint" + +msgid "Blueprint config has changed" +msgstr "" + +msgid "Blueprint config preset" +msgstr "" + +msgid "" +"Blueprint config preset has been changed. From \"{{ oldValue }}\", to \"{{ " +"newValue }}\"" +msgstr "" + +msgid "Blueprint config preset is missing" +msgstr "" + +msgid "Blueprint config preset not set" +msgstr "" + +msgid "Blueprint Configuration" +msgstr "Blueprintkonfigurasjon" + +msgid "Blueprint has a new version" +msgstr "" + +msgid "Blueprint has been changed. From \"{{ oldValue }}\", to \"{{ newValue }}\"" +msgstr "" + +msgid "Blueprint ID" +msgstr "Blueprint-id" + +msgid "Blueprint Name" +msgstr "Blueprintnavn" + +msgid "Blueprint not found!" +msgstr "" + +msgid "Blueprint not set" +msgstr "Blueprint ikke valgt" + +msgid "Blueprint Type" +msgstr "Blueprinttype" + +msgid "Blueprint Version" +msgstr "Blueprintversjon" + +msgid "Blueprints" +msgstr "Blueprints" + +msgid "Blueprints updated successfully." +msgstr "Blueprints ble oppdatert." + +msgid "Bottom" +msgstr "" + +msgid "Box color" +msgstr "" + +msgid "BREAK" +msgstr "PAUSE" + +msgid "Break In" +msgstr "Pause om" + +msgid "Bucket AdLib is not compatible with this Rundown!" +msgstr "" + +msgid "Bucket not found!" +msgstr "" + +msgid "Button" +msgstr "Knapp" + +msgid "Button height scale factor" +msgstr "Høydeskala for knapp" + +msgid "Button mapping" +msgstr "" + +msgid "Button width scale factor" +msgstr "Breddeskala for knapp" + +msgid "By Parts" +msgstr "" + +msgid "By Segments" +msgstr "" + +msgid "Camera" +msgstr "Kamera" + +msgid "Camera Screen" +msgstr "" + +msgid "Can Generate Adlib Testing Rundown" +msgstr "" + +msgid "Can not be used during a hold!" +msgstr "" + +msgid "Cancel" +msgstr "Avbryt" + +msgid "Cancel currently pressed hotkey" +msgstr "Avbryt den trykte tasten" + +msgid "Cannot activate HOLD before a part has been taken!" +msgstr "" + +msgid "Cannot activate HOLD between the current and next parts" +msgstr "" + +msgid "Cannot activate HOLD once an adlib has been used" +msgstr "" + +msgid "Cannot cancel HOLD once it has been taken" +msgstr "" + +msgid "Cannot connect to the {{platformName}}: {{reason}}" +msgstr "" + +msgid "Cannot perform take for {{duration}}ms" +msgstr "" + +msgid "Cannot play this AdLib because it is marked as Floated" +msgstr "Kan ikke spille av adlib fordi den er markert som på vent (float)" + +msgid "Cannot play this AdLib because it is marked as Invalid" +msgstr "Kan ikke spille av adlib fordi den er markert som ugyldig" + +msgid "Cannot play this adlib because source layer is not queueable" +msgstr "Kan ikke spille av adlib fordi den ikke kan settes i kø på kildelaget" + +msgid "Cannot remove the rundown \"{{name}}\" while it is on-air." +msgstr "" + +msgid "Cannot take close to an AUTO" +msgstr "" + +msgid "Cannot take during a transition" +msgstr "" + +msgid "Cannot take unplayable AdLib" +msgstr "" + +msgid "CasparCG on device \"{{deviceName}}\" restarting..." +msgstr "CasparCG på \"{{deviceName}}\" starter på nytt..." + +msgid "Center" +msgstr "" + +msgid "Change to fullscreen mode" +msgstr "Fullskjermmodus" + +msgid "Channel Name" +msgstr "Kanalnavn" + +msgid "" +"Check layer types to select all layers of that type, or check individual " +"layers for more specific filtering." +msgstr "" + +msgid "Check the console for troubleshooting data from device \"{{deviceName}}\"!" +msgstr "Sjekk konsollen for feilsøkingsdata fra enheten \"{{deviceName}}\"!" + +msgid "Cleanup" +msgstr "Opprydding" + +msgid "Cleanup old data" +msgstr "Rydd opp i gamle data" + +msgid "Cleanup old database indexes" +msgstr "Rydd opp i gamle databaseindexer" + +msgid "Clear {{layerName}}" +msgstr "Tøm {{layerName}}" + +msgid "Clear filter" +msgstr "" + +msgid "Clear queued segment" +msgstr "Fjern cuet tittel" + +msgid "Clear QuickLoop" +msgstr "" + +msgid "Clear QuickLoop End" +msgstr "" + +msgid "Clear QuickLoop Start" +msgstr "" + +msgid "Clear Source Layer" +msgstr "Tøm kildelag" + +msgid "Clear value" +msgstr "" + +msgid "Clearing SourceLayer" +msgstr "" + +msgid "Click anywhere for fullscreen" +msgstr "" + +msgid "Click on a rundown to control your studio" +msgstr "Klikk på en kjøreplan for å kontrollere studioet ditt" + +msgid "Click or press Enter for fullscreen" +msgstr "" + +msgid "Click to show available Package Containers" +msgstr "Klikk for å vise tilgjengelige pakkekontainere" + +msgid "Click to show available Show Styles" +msgstr "Klikk for å vise tilgjengelige showstyles" + +msgid "Client IP" +msgstr "Klient-ip" + +msgid "Clip starts with {{frames}} black frames" +msgstr "" + +msgid "Clip starts with {{frames}} freeze frames" +msgstr "" + +msgid "Clips" +msgstr "Klipp" + +msgid "Close" +msgstr "Lukk" + +msgid "Close Properties Panel" +msgstr "" + +msgid "Close Rundown" +msgstr "" + +msgid "Comma-separated list of studio labels to filter by. Leave empty for all." +msgstr "" + +msgid "Comma-separated speeds in px/frame" +msgstr "" + +msgid "Compatible Studios" +msgstr "" + +msgid "Completed with warnings" +msgstr "" + +msgid "Config Fix Up must be run or ignored before the configuration can be edited" +msgstr "" + +msgid "Config for {{name}} fix failed" +msgstr "" + +msgid "Config for {{name}} fixed successfully" +msgstr "" + +msgid "Config for {{name}} upgraded failed" +msgstr "" + +msgid "Config for {{name}} upgraded successfully" +msgstr "" + +msgid "Config has not been applied before" +msgstr "" + +msgid "Config ID: " +msgstr "" + +msgid "Config looks good" +msgstr "" + +msgid "Config preset" +msgstr "" + +msgid "Config preset is missing" +msgstr "" + +msgid "Config preset not set" +msgstr "" + +msgid "Config requires fixing up before it can be validated" +msgstr "" + +msgid "" +"Config value \"{{ name }}\" has changed. From \"{{ oldValue }}\", to \"{{ " +"newValue }}\"" +msgstr "" + +msgid "Configurable Screens" +msgstr "" + +msgid "" +"Configuration for this Gateway has moved to the Studio Peripheral Device " +"settings" +msgstr "" + +msgid "Configure display options" +msgstr "" + +msgid "" +"Configure which pieces should display their assigned AB resolver channel " +"(e.g., \"Server A\") on various screens. This helps operators identify " +"which video server is playing each clip." +msgstr "" + +msgid "Confirm" +msgstr "" + +msgid "Connect some devices to the playout gateway" +msgstr "Koble til en eller flere enheter til playout gatewayen" + +msgid "Connect to {{deviceName}}" +msgstr "" + +msgid "Connected" +msgstr "Tilkoblet" + +msgid "Connected App Containers" +msgstr "Tilkoblede app-kontainere" + +msgid "Connected Expectation Managers" +msgstr "" + +msgid "Connected to the {{platformName}}." +msgstr "" + +msgid "Connected Workers" +msgstr "Tilkoblede arbeidere" + +msgid "Connecting to the {{platformName}}" +msgstr "" + +msgid "Container Status" +msgstr "" + +msgid "Control mode" +msgstr "" + +msgid "Control modes" +msgstr "" + +msgid "" +"Controls for exposed Route Sets will be displayed to the producer within " +"the Rundown View in the Switchboard." +msgstr "" +"Kontroller for eksponerte omkoblingsgrupper vil vises til producer i " +"kjøreplansvisningen i omkoblingspanelet." + +msgid "Core" +msgstr "" + +msgid "Core + Worker processing time" +msgstr "" + +msgid "Core System settings" +msgstr "Systeminstillinger for Core" + +msgid "" +"Could not create a snapshot for the evaluation, because the previous one " +"was created just moments ago. If you want another snapshot, try again in a " +"couple of seconds." +msgstr "" + +msgid "Could not get system status. Please consult system administrator." +msgstr "Kan ikke innhente status for systemet. Kontakt systemadministrator." + +msgid "Could not restart core: {{err}}" +msgstr "" + +msgid "Could not restart Playout Gateway \"{{playoutDeviceName}}\"." +msgstr "Playout-gateway \"{{playoutDeviceName}}\" kunne ikke startes på nytt." + +msgid "Create Adlib Testing Rundown" +msgstr "" + +msgid "Create new Bucket" +msgstr "Opprett ny bøtte" + +msgid "Created" +msgstr "Opprettet" + +msgid "Creating a new Bucket" +msgstr "" + +msgid "Creating Adlib Testing Rundown" +msgstr "" + +msgid "Creating Snapshot for debugging" +msgstr "" + +msgid "Critical problems" +msgstr "" + +msgid "Critical Problems" +msgstr "" + +msgid "Cron jobs" +msgstr "Cron-jobber" + +msgid "Current Part" +msgstr "Nåværende del" + +msgid "Current part can contain next pieces" +msgstr "" + +msgid "Current Segment" +msgstr "Nåværende tittel" + +msgid "Custom Classes" +msgstr "Tilpassede klasser" + +msgid "Custom Hotkey Labels" +msgstr "Egendefinerte etiketter for hurtigtaster" msgid "Deactivate" msgstr "Deaktiver" -msgid "Take" -msgstr "Take" +msgid "Deactivate \"On Air\"" +msgstr "" -msgid "Reset Rundown" -msgstr "Tilbakestill kjøreplanen" +msgid "Deactivate Rundown" +msgstr "Deaktiver kjøreplan" + +msgid "Deactivate Studio" +msgstr "" + +msgid "Deactivating other Rundown Playlist, and activating this one" +msgstr "" + +msgid "Deactivating Rundown Playlist" +msgstr "" + +msgid "Debug" +msgstr "" + +msgid "Debug mode" +msgstr "" + +msgid "Debug State" +msgstr "" + +msgid "Default" +msgstr "Standard" + +msgid "Default (hide)" +msgstr "" + +msgid "Default Layout" +msgstr "Standardlayout" + +msgid "Default shelf height" +msgstr "Standard høyde for skuff" + +msgid "Default State" +msgstr "Standardtilstand" + +msgid "Delete" +msgstr "Slett" + +msgid "Delete Action Trigger" +msgstr "" + +msgid "Delete layout?" +msgstr "Slett layout?" + +msgid "Delete mapping" +msgstr "" + +msgid "Delete output layer" +msgstr "" + +msgid "Delete rundown?" +msgstr "Slette kjøreplanen?" + +msgid "Delete source layer" +msgstr "" + +msgid "Delete this AdLib" +msgstr "Slett denne adliben" + +msgid "Delete this Blueprint?" +msgstr "Slett dette blueprintet?" + +msgid "Delete this Bucket" +msgstr "Slett denne bøtten" -msgid "Reload {{nrcsName}} Data" -msgstr "Last inn {{nrcsName}}-data på nytt" +msgid "Delete this item?" +msgstr "Slett dette elementet?" -msgid "Store Snapshot" -msgstr "Lagre snapshot" +msgid "Delete this output?" +msgstr "Slett denne utgangen?" -msgid "No actions available" -msgstr "Ingen kjøreplanvalg tilgjengelige i påsynsmodus" +msgid "Delete this Show Style?" +msgstr "Slett denne showstylen?" -msgid "Add ?studio=1 to the URL to enter studio mode" -msgstr "Legg til ?admin=1 på slutten av nettadressen for å starte studiomodus" +msgid "Delete this Studio?" +msgstr "Slett dette studioet?" -msgid "Exit" -msgstr "Lukk" +msgid "Device" +msgstr "" -msgid "Error" -msgstr "Feil" +msgid "Device \"{{deviceName}}\" restarting..." +msgstr "\"{{deviceName}}\" starter på nytt..." -msgid "This rundown is now active. Are you sure you want to exit this screen?" -msgstr "Denne kjøreplanen er aktiv. Er du sikker på at du vil avslutte?" +msgid "Device {{deviceName}} is disconnected" +msgstr "{{deviceName}} er koblet fra" -msgid "Invalid AdLib" -msgstr "Ugyldig adlib" +msgid "Device ID" +msgstr "Enhets-id" -msgid "Cannot play this AdLib because it is marked as Invalid" -msgstr "Kan ikke spille av adlib fordi den er markert som ugyldig" +msgid "Device is already attached to another studio." +msgstr "" -#, fuzzy -#| msgid "Floated AdLib" -msgid "Floated Adlib" -msgstr "Adlib satt på vent" +msgid "Device is missing configuration schema" +msgstr "" -msgid "Cannot play this AdLib because it is marked as Floated" -msgstr "Kan ikke spille av adlib fordi den er markert som på vent (float)" +msgid "Device is of unknown type" +msgstr "" -msgid "Not queueable" -msgstr "Kan ikke settes i kø" +msgid "Device Name" +msgstr "Enhetsnavn" -msgid "Cannot play this adlib because source layer is not queueable" -msgstr "Kan ikke spille av adlib fordi den ikke kan settes i kø på kildelaget" +msgid "Device not found" +msgstr "" -msgid "" -"There are no Playout Gateways connected and attached to this studio. Please " -"contact the system administrator to start the Playout Gateway." +msgid "Device Triggers" msgstr "" -"Dette studioet har ingen tilkoblede playout-gatewayer. Kontakt " -"systemadministrator for å starte den." -msgid "Playout Gateway \"{{playoutDeviceName}}\" is now restarting." -msgstr "Playout-gateway \"{{playoutDeviceName}}\" starter på nytt..." +msgid "Device Type" +msgstr "Enhetstype" -msgid "Could not restart Playout Gateway \"{{playoutDeviceName}}\"." -msgstr "Playout-gateway \"{{playoutDeviceName}}\" kunne ikke startes på nytt." +msgid "Devices" +msgstr "Enheter" -msgid "Restart Playout" -msgstr "Start Playout-gateway på nytt" +msgid "Devices with issues" +msgstr "Enheter med problemer" -#, fuzzy -#| msgid "Do you want to restart Quantel Gateway?" -msgid "Do you want to restart the Playout Gateway?" -msgstr "Vil du starte Quantel Gateway på nytt?" +msgid "Did you have any problems with the broadcast?" +msgstr "Hadde du noen problemer med sendingen?" -msgid "Restart CasparCG Server" -msgstr "Restart CasparCG" +msgid "Diff" +msgstr "Forskjell" -msgid "Do you want to restart CasparCG Server \"{{device}}\"?" -msgstr "Er du sikker på at du vil restarte CasparCG Server \"{{device}}\"?" +msgid "Director Screen" +msgstr "" -msgid "CasparCG on device \"{{deviceName}}\" restarting..." -msgstr "CasparCG på \"{{deviceName}}\" starter på nytt..." +msgid "Director's Screen" +msgstr "" -msgid "" -"Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}" -msgstr "Omstart av CasparCG på \"{{deviceName}}\" feilet: {{errorMessage}}" +msgid "Disable" +msgstr "" -msgid "Cancel currently pressed hotkey" -msgstr "Avbryt den trykte tasten" +msgid "Disable Context Menu" +msgstr "Skru av kontekstmeny" -msgid "Change to fullscreen mode" -msgstr "Fullskjermmodus" +msgid "Disable follow take" +msgstr "" -msgid "Show Hotkeys" -msgstr "Vise hurtigtaster" +msgid "Disable hints by adding this to the URL:" +msgstr "Deaktiver hint ved å legge dette til på url-en:" -msgid "Take a Snapshot" -msgstr "Lagre et snapshot" +msgid "Disable next Piece" +msgstr "Skip neste element" -msgid "Restart {{device}}" -msgstr "Start {{device}} på nytt" +msgid "Disable the hover Inspector when hovering over the button" +msgstr "" -msgid "Rundown not found" -msgstr "Kjøreplan ikke funnet" +msgid "Disable the next element" +msgstr "Skip neste super" -msgid "Close" -msgstr "Lukk" +msgid "Disable version check" +msgstr "Deaktiver versjonsjekk" -msgid "Rundown for piece \"{{pieceLabel}}\" could not be found." -msgstr "Finner ikke kjøreplan for \"{{pieceLabel}}\"." +msgid "Disabled" +msgstr "Deaktivert" -msgid "This rundown has been unpublished from Sofie." -msgstr "Denne kjøreplanen er ikke lenger tilgjengelig i Sofie." +msgid "Disabling next Piece" +msgstr "" -msgid "Error: The studio of this Rundown was not found." -msgstr "Feil: Kan ikke finne studioet for denne kjøreplanen." +msgid "Disconnected" +msgstr "Frakoblet" -msgid "This playlist is empty" -msgstr "Denne spillelisten er tom" +msgid "Dismiss" +msgstr "" -msgid "Error: The ShowStyle of this Rundown was not found." -msgstr "Feil: Kan ikke finne showstyle for denne kjøreplanen." +msgid "Dismiss all notifications" +msgstr "" -msgid "Unknown error" -msgstr "Ukjent feil" +msgid "Display AB channel assignments on:" +msgstr "" -msgid "" -"Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data " -"from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced " -"or remove the rundown from Sofie. What do you want to do?" +msgid "Display AdLibs in a column in List View" msgstr "" -"Kjøreplan {{rundownName}} i listen {{playlistName}} mangler i data fra " -"{{nrcsName}}. Du kan enten markere den som usynkronisert og beholde den i " -"Sofie, eller fjerne kjøreplanen fra Sofie. Hva vil du gjøre?" -msgid "(Unknown rundown)" -msgstr "(Ukjent kjøreplan)" +msgid "Display in a column in List View" +msgstr "" -msgid "(Unknown playlist)" -msgstr "(Ukjent kjøreplanliste)" +msgid "Display name of the Package Container" +msgstr "Pakkekontainerens navn som vises i oversikten" -msgid "Leave Unsynced" -msgstr "Behold ikke-synkronisert kjøreplan" +msgid "Display piece duration for source layers" +msgstr "" -msgid "Remove" -msgstr "Fjern" +msgid "Display Rank" +msgstr "Rangering for visning" -msgid "Remove rundown" -msgstr "Fjern kjøreplan" +msgid "Display Style" +msgstr "Visningsstil" + +msgid "Display Take buttons" +msgstr "Vis Take-knapp" msgid "" "Do you really want to remove just the rundown \"{{rundownName}}\" in the " -"playlist {{playlistName}} from Sofie? This cannot be undone!" +"playlist {{playlistName}} from Sofie? \n" +"\n" +"This cannot be undone!" msgstr "" -"Er du sikker på at du vil slette kjøreplanen {{rundownName}} i lista " -"{{playlistName}} fra Sofie? Dette kan ikke angres!" - -msgid "Loop Start" -msgstr "Start for loop" -msgid "Loop End" -msgstr "Slutt for loop" +msgid "Do you really want to restore the snapshot \"{{snapshotName}}\"?" +msgstr "" -msgid "(in: {{time}})" -msgstr "(om: {{time}})" +msgid "Do you want to activate this Rundown?" +msgstr "Vil du aktivere denne kjøreplanen?" -msgid "({{time}} ago)" -msgstr "(for {{time}} siden)" +msgid "" +"Do you want to append these to existing Action Triggers, or do you want to " +"replace them?" +msgstr "" +"Vil du legge disse til de nåværende handlingsutløserne, eller vil du " +"erstatte dem?" -msgid "Planned Start" -msgstr "Planlagt start" +msgid "Do you want to do this?" +msgstr "" -msgid "Planned Duration" -msgstr "Planlagt varighet" +msgid "Do you want to execute {{actionName}}? This may the disrupt the output" +msgstr "" -msgid "Planned End" -msgstr "Planlagt slutt" +msgid "Do you want to restart CasparCG Server \"{{device}}\"?" +msgstr "Er du sikker på at du vil restarte CasparCG Server \"{{device}}\"?" -msgid "Time to planned end" -msgstr "Tid til planlagt slutt" +msgid "Do you want to restart the Playout Gateway?" +msgstr "" -msgid "Time since planned end" -msgstr "Tid siden planlagt slutt" +msgid "Documentation is available at" +msgstr "Dokumentasjon er tilgjengelig på" -msgid "Over/Under" -msgstr "Over/Under" +msgid "Documents to be removed:" +msgstr "Dokumenter som fjernes:" -msgid "" -"The rundown \"{{rundownName}}\" is not published or activated in " -"{{nrcsName}}! No data updates will currently come through." +msgid "Does NOT support HEAD requests" msgstr "" -"Kjøreplanen \"{{rundownName}}\" er ikke synkronisert med MOS/{{nrcsName}}! " -"Kontroller at den er satt MOS Active i ENPS." -msgid "Re-sync" -msgstr "Synkroniser med MOS" +msgid "Don't treat the end of the last rundown in a playlist as a break" +msgstr "Ikke behandle slutten av den siste kjøreplanen i en spilleliste som en pause" -msgid "Re-sync Rundown" -msgstr "Synkroniser kjøreplanen med ENPS på nytt" +msgid "Done" +msgstr "Utført" -msgid "" -"Are you sure you want to re-sync the Rundown?\n" -"(If the currently playing Part has been changed, this can affect the output)" +msgid "Download Action Triggers" +msgstr "Last ned handlingsutløsere" + +msgid "Drag to reorder or move out of playlist" +msgstr "Dra for å endre rekkefølge eller flytte ut av spillelisten" + +msgid "Drop Rundown here to move it out of its current Playlist" +msgstr "Slipp kjøreplanen her for å flytte den ut av spillelisten" + +msgid "Dropzone URL" msgstr "" -"Er du sikker på at du vil synkronisere denne kjøreplanen?\n" -"(Dette kan påvirke gjennomføring av en pågående sending)" -msgid "Restart" -msgstr "Restart" +msgid "Duplicate Action Trigger" +msgstr "" -msgid "" -"Fixing this problem requires a restart to the host device. Are you sure you " -"want to restart {{device}}?\n" -"(This might affect output)" +msgid "Duration" +msgstr "Varighet" + +msgid "DURATION" msgstr "" -"Feilretting krever en omstart av {{device}}. Er du sikker på at du ønsker å " -"starte enheten på nytt?(Dette kan påvirke gjennomføring av en pågående " -"sending)" -msgid "Device \"{{deviceName}}\" restarting..." -msgstr "\"{{deviceName}}\" starter på nytt..." +msgid "e.g., Studio A,Studio B" +msgstr "" -msgid "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}" -msgstr "Kunne ikke starte \"{{deviceName}}\" på nytt: {{errorMessage}}" +msgid "Edit" +msgstr "" -msgid "There is an unknown problem with the part." -msgstr "Det er et ukjent problem med denne delen." +msgid "Edit Action Trigger" +msgstr "" -msgid "Show issue" -msgstr "Vis problem" +msgid "Edit in Nora" +msgstr "Rediger i Nora" -msgid "There is an unspecified problem with the source." -msgstr "Det er et ikke-spesifisert problem med kilden." +msgid "Edit mapping" +msgstr "" -msgid "External message queue has unsent messages." -msgstr "Ekstern meldingskø har meldinger som ikke er sendt." +msgid "Edit Mode" +msgstr "" -msgid "" -"The system configuration has been changed since importing this rundown. It " -"might not run correctly" +msgid "Edit output layer" +msgstr "" + +msgid "Edit Part Properties" msgstr "" -"Systemoppsettet har blitt endret etter at denne kjøreplanen ble importert. " -"Kjøreplanen kan spilles av med feil" -msgid "Unable to check the system configuration for changes" -msgstr "Kan ikke kontrollere endringer i systemoppsettet" +msgid "Edit Piece Properties" +msgstr "" -msgid "The Studio configuration is missing some required fields:" -msgstr "Studiooppsettet mangler obligatoriske felter:" +msgid "Edit Segment Properties" +msgstr "" -msgid "The Show Style configuration \"{{name}}\" could not be validated" -msgstr "Showstyleoppsettet \"{{name}}\" kunne ikke valideres" +msgid "Edit source layer" +msgstr "" -msgid "" -"The ShowStyle \"{{name}}\" configuration is missing some required fields:" -msgstr "Showstyleoppsettet \"{{name}}\" mangler obligatoriske felter:" +msgid "Edit Support Panel" +msgstr "Rediger supportpanel" -msgid "Unable to validate the system configuration" -msgstr "Systemoppsettet kunne ikke valideres" +msgid "Empty" +msgstr "Tom" -msgid "Device {{deviceName}} is disconnected" -msgstr "{{deviceName}} er koblet fra" +msgid "Empty this Bucket" +msgstr "Tøm denne bøtten" -#, fuzzy -#| msgid "Critical Errors" -msgid "Critical Problems" -msgstr "Kritiske feil" +msgid "Emptying Bucket" +msgstr "" -msgid "Warnings" -msgstr "Advarsler" +msgid "Enable" +msgstr "Aktiver" -#, fuzzy -#| msgid "Migrations" -msgid "Notifications" -msgstr "Migrering" +msgid "Enable \"Play from Anywhere\"" +msgstr "Slå på \"Play from Anywhere\"" -#, fuzzy -#| msgid "Rewind Segments to start" -msgid "Rewind all Segments" -msgstr "Sett alle segmenter tilbake til start" +msgid "Enable AdLib Testing, for testing AdLibs before taking the first Part" +msgstr "" -#, fuzzy -#| msgid "Show entire On Air Segment" -msgid "Go to On Air Segment" -msgstr "Vis hele tittelen som er OnAir" +msgid "Enable automatic storage of Rundown Playlist snapshots periodically" +msgstr "" -#, fuzzy -#| msgid "Switchboard" -msgid "Toggle Switchboard Panel" -msgstr "Sentralbord" +msgid "Enable Buckets" +msgstr "" -#, fuzzy -#| msgid "Edit Support Panel" -msgid "Toggle Support Panel" -msgstr "Rediger supportpanel" +msgid "Enable CasparCG restart job" +msgstr "Aktiver CasparCG restartjobber" -msgid "Just now" -msgstr "Nå" +msgid "Enable configuration mode by adding ?configure=1 to the address bar." +msgstr "" +"Aktiver konfigurasjonsmodus ved å legge til ?configure=1 på slutten av " +"nettadressen." -msgid "Less than a minute ago" -msgstr "Under ett minutt siden" +msgid "Enable Evaluation Form" +msgstr "" -msgid "Less than five minutes ago" -msgstr "Under fem minutter siden" +msgid "Enable hints by adding this to the URL:" +msgstr "Aktiver hint ved å legge dette til på url-en:" -msgid "Around 10 minutes ago" -msgstr "Cirka 10 minutter siden" +msgid "Enable QuickLoop" +msgstr "" -msgid "More than 10 minutes ago" -msgstr "Over 10 minutter siden" +msgid "Enable search toolbar" +msgstr "Aktiver søkeverktøy" -msgid "More than 30 minutes ago" -msgstr "Over 30 minutter siden" +msgid "Enable User Editing" +msgstr "" -msgid "More than 2 hours ago" -msgstr "Over 2 timer siden" +msgid "Enabled" +msgstr "Aktivert" -msgid "More than 5 hours ago" -msgstr "Over 5 timer siden" +msgid "Enabled on all Parts, applying QuickLoop Fallback Part Duration if needed" +msgstr "" -msgid "More than a day ago" -msgstr "Over en dag siden" +msgid "Enabled, but skipping parts with undefined or 0 duration" +msgstr "" -msgid "{{nrcsName}} Connection" -msgstr "{{nrcsName}}-tilkobling" +msgid "" +"Enables internal monitoring of blocked main thread. Logs when there is an " +"issue, but (unverified) might cause issues in itself." +msgstr "" -msgid "Last update" -msgstr "Nyeste oppdatering" +msgid "End of script" +msgstr "Slutt på manus" -msgid "Off-line devices" -msgstr "Frakoblede enheter" +msgid "End Words" +msgstr "Stikkord" -msgid "Devices with issues" -msgstr "Enheter med problemer" +msgid "Error" +msgstr "Feil" -msgid "All connections working correctly" -msgstr "Alle tilkoblinger er OK" +msgid "Error when checking for cleaning up" +msgstr "" -msgid "Play-out" -msgstr "Avspilling" +msgid "Error: The ShowStyle of this Rundown was not found." +msgstr "Feil: Kan ikke finne showstyle for denne kjøreplanen." -msgid "All devices working correctly" -msgstr "Alle enheter fungerer som de skal" +msgid "Error: The studio of this Rundown was not found." +msgstr "Feil: Kan ikke finne studioet for denne kjøreplanen." -msgid "Auto" -msgstr "Auto" +msgid "Est. End" +msgstr "" -msgid "Expected End" -msgstr "Forventet slutt" +msgid "Evaluations" +msgstr "Evalueringer" -msgid "Next Loop at" -msgstr "Neste loop starter" +msgid "Exclusivity group" +msgstr "Ekslusivitetsgruppe" -msgid "Diff" -msgstr "Forskjell" +msgid "Exclusivity Group ID" +msgstr "Eksklusivitetsgruppe-id" -msgid "Started" -msgstr "Startet" +msgid "Exclusivity Group Name" +msgstr "Eksklusivitetsgruppenavn" -msgid "Expected Start" -msgstr "Forventet slutt" +msgid "Exclusivity Groups" +msgstr "Eksklusivitetsgrupper" -msgid "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)" -msgstr "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)" +msgid "Execute" +msgstr "Utfør" -msgid "{{currentRundownName}} - {{rundownPlaylistName}}" -msgstr "{{currentRundownName}} - {{rundownPlaylistName}}" +msgid "Execute User Operation" +msgstr "" -msgid "{{rundownPlaylistName}} (Looping)" -msgstr "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)" +msgid "Executed {{actionName}} on device \"{{deviceName}}\": {{response}}" +msgstr "" -msgid "Floated AdLib" -msgstr "Adlib satt på vent" +msgid "Executed {{actionName}} on device \"{{deviceName}}\"..." +msgstr "" -msgid "Switchboard" -msgstr "Sentralbord" +msgid "Executes within the currently open Rundown, requires a Client-side trigger." +msgstr "" +"Utføers innenfor den valgte kjøreplanen, men trenger en utløser fra " +"klienten." -msgid "This is not in it's normal setting" -msgstr "Endret fra standardoppsett" +msgid "Execution times" +msgstr "Kjøretider" -msgid "Off" -msgstr "Av" +msgid "Exit" +msgstr "Lukk" -#, fuzzy -#| msgid "Segment" -msgid "segment" -msgstr "Tittel" +msgid "Expectation Manager" +msgstr "" -#, fuzzy -#| msgid "Critical Errors" -msgid "Critical problems" -msgstr "Kritiske feil" +msgid "Expected End" +msgstr "Forventet slutt" -msgid "On Air At" -msgstr "On Air klokken" +msgid "Expected End text" +msgstr "Tekst for forventet slutt" -msgid "On Air In" -msgstr "On Air om" +msgid "Expected End Time" +msgstr "Forventet sendeslutt" -msgid "Unsynced" -msgstr "Ikke synkronisert med MOS" +msgid "Expected Start" +msgstr "Forventet slutt" -#, fuzzy -#| msgid "Sources Path" -msgid "Sources" -msgstr "Kildesti" +msgid "Export" +msgstr "Eksporter" -msgid "Switch to Timeline mode" +msgid "Export visible" msgstr "" -msgid "On Air" -msgstr "On Air" +msgid "Expose as user selectable layout" +msgstr "Gjør tilgjengelig som brukervalgt layout" -msgid "Loops to top" -msgstr "Looper til toppen" +msgid "Expose layout as a standalone page" +msgstr "Gjør layout tilgjengelig som en selvstendig side" -msgid "Show End" -msgstr "Sendeslutt" +msgid "External message queue has unsent messages." +msgstr "Ekstern meldingskø har meldinger som ikke er sendt." -msgid "BREAK" -msgstr "PAUSE" +msgid "Failed to activate" +msgstr "Kunne ikke aktivere" -msgid "Break In" -msgstr "Pause om" +msgid "Failed to add a new Show Style Variant: {{errorMessage}}" +msgstr "" -msgid "part" -msgstr "punkt" +msgid "Failed to assign AB player for {{pieceNames}}" +msgstr "" -msgid "Set segment as Next" -msgstr "Sett tittel som Neste: Starter på neste Take" +msgid "Failed to assign non-critical AB player for {{pieceNames}}" +msgstr "" -msgid "Queue segment" -msgstr "Cue tittel: Starter når aktiv tittel er ferdig" +msgid "Failed to compare config changes" +msgstr "" -msgid "Clear queued segment" -msgstr "Fjern cuet tittel" +msgid "Failed to copy Show Style Variant: {{errorMessage}}" +msgstr "" -msgid "Set this part as Next" -msgstr "Sett dette punktet som neste: Starter på neste Take" +msgid "Failed to delete Show Style Variant: {{errorMessage}}" +msgstr "" -msgid "Set Next Here" -msgstr "Sett Neste her" +msgid "" +"Failed to execute {{actionName}} on device: \"{{deviceName}}\": " +"{{errorMessage}}" +msgstr "" -msgid "Play from Here" -msgstr "Spill av herfra" +msgid "Failed to execute take" +msgstr "Kunne ikke gjennomføre Take" -msgid "Switch to Storyboard mode" -msgstr "Bytt til storyboard-visning" +msgid "Failed to generate adlib rundown! {{message}}" +msgstr "" -msgid "Zoom Out" -msgstr "Zoom Ut" +msgid "Failed to import new Show Style Variants: {{errorMessage}}" +msgstr "" -msgid "Show All" -msgstr "Vis alle" +msgid "" +"Failed to import Show Style Variant {{name}}. Make sure it is not already " +"imported." +msgstr "" -msgid "Zoom In" -msgstr "Zoom inn" +msgid "Failed to remove all Show Style Variants: {{errorMessage}}" +msgstr "" -msgid "Parts Duration" -msgstr "Varighet for del" +msgid "Failed to reorderShow Style Variant: {{errorMessage}}" +msgstr "" -msgid "Unknown" -msgstr "Ukjent" +msgid "Failed to reset OAuth credentials: {{errorMessage}}" +msgstr "Nullstiling av OAuth credentials feilet: {{errorMessage}}" -msgid "Good" -msgstr "Bra" +msgid "Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}" +msgstr "Omstart av CasparCG på \"{{deviceName}}\" feilet: {{errorMessage}}" -msgid "Minor Warning" -msgstr "Mindre advarsel (avvik)" +msgid "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}" +msgstr "Kunne ikke starte \"{{deviceName}}\" på nytt: {{errorMessage}}" -msgid "Warning" -msgstr "Advarsel" +msgid "Failed to update blueprints: {{errorMessage}}" +msgstr "Oppdatering av blueprints feilet: {{errorMessage}}" -msgid "Bad" -msgstr "Feil" +msgid "Failed to update config: {{errorMessage}}" +msgstr "Oppdatering av konfigurasjon feilet: {{errorMessage}}" -msgid "Fatal" -msgstr "Kritisk" +msgid "Failed to upload OAuth credentials: {{errorMessage}}" +msgstr "Opplasting av OAuth credentials feilet: {{errorMessage}}" -msgid "Connected" -msgstr "Tilkoblet" +msgid "Failed to upload shelf layout: {{errorMessage}}" +msgstr "Opplasting av layout feilet: {{errorMessage}}" -msgid "Disconnected" -msgstr "Frakoblet" +msgid "Failed to validate config" +msgstr "" -msgid "MOS Gateway" -msgstr "MOS-gateway" +msgid "Fatal" +msgstr "Kritisk" -msgid "Spreadsheet Gateway" -msgstr "Spreadsheet-gateway" +msgid "File path to the folder of the local folder" +msgstr "Sti til lokale mappe" -msgid "Play-out Gateway" -msgstr "Playout-gateway" +msgid "Filter by Output Layer" +msgstr "" -msgid "Media Manager" -msgstr "Media Manager" +msgid "Filter by Source Layer" +msgstr "" -msgid "Unknown Device" -msgstr "Ukjent enhet" +msgid "Filter disabled" +msgstr "Filter deaktivert" -msgid "Delete this Studio?" -msgstr "Slett dette studioet?" +msgid "Filter Disabled" +msgstr "Filter deaktivert" -msgid "Are you sure you want to delete the studio \"{{studioId}}\"?" -msgstr "Er du sikker på at du vil slette studioet \"{{studioId}}\"?" +msgid "Filter..." +msgstr "" -msgid "Delete this Show Style?" -msgstr "Slett denne showstylen?" +msgid "Filters" +msgstr "Filtre" -msgid "Are you sure you want to delete the show style \"{{showStyleId}}\"?" -msgstr "Er du sikker på at du vil slette showstylen \"{{showStyleId}}\"?" +msgid "Find Trigger..." +msgstr "Finn utløser..." -msgid "Delete this Blueprint?" -msgstr "Slett dette blueprintet?" +msgid "Fine scroll" +msgstr "" -msgid "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?" -msgstr "Er du sikker på at du vil slette blueprintet \"{{blueprintId}}\"?" +msgid "Fix Up Config" +msgstr "" -msgid "Remove this Device?" -msgstr "Fjern denne enheten?" +msgid "Fixed duration in Segment header" +msgstr "Låst varighet i tittelheader" msgid "" -"Are you sure you want to remove the device \"{{deviceName}}\" and all of " -"it's sub-devices?" +"Fixing this problem requires a restart to the host device. Are you sure you " +"want to restart {{device}}?\n" +"(This might affect output)" msgstr "" -"Er du sikker på at du vil fjerne enheten \"{{deviceName}}\" og alle dens " -"underenheter?" +"Feilretting krever en omstart av {{device}}. Er du sikker på at du ønsker å " +"starte enheten på nytt?(Dette kan påvirke gjennomføring av en pågående " +"sending)" -msgid "Studios" -msgstr "Studio" +msgid "Floated Adlib" +msgstr "" -msgid "Unnamed Studio" -msgstr "Studio uten navn" +msgid "Floated AdLib" +msgstr "Adlib satt på vent" -msgid "Show Styles" -msgstr "Showstyle" +msgid "Folder path" +msgstr "Mappesti" -msgid "Unnamed Show Style" -msgstr "Showstyle uten navn" +msgid "Folder path to shared folder" +msgstr "Sti til delt mappe" -msgid "Source Layers" -msgstr "Kildelag" +msgid "Following" +msgstr "" -msgid "Output Channels" -msgstr "Utgangskanal" +msgid "Font size" +msgstr "" -msgid "Blueprints" -msgstr "Blueprints" +msgid "for {{name}} fix skipped successfully" +msgstr "" -msgid "Unnamed blueprint" -msgstr "Blueprint uten navn" +msgid "Force" +msgstr "Tving" -msgid "Type" -msgstr "Type" +msgid "Force (deactivate others)" +msgstr "Tving (deaktiver andre)" -msgid "Version" -msgstr "Versjon" +msgid "Force Migration" +msgstr "Tving migrering" -msgid "Devices" -msgstr "Enheter" +msgid "Force Migration (unsafe)" +msgstr "Tving migrering (utrygt)" -msgid "Tools" -msgstr "Verktøy" +msgid "Force the Multi-gateway-mode" +msgstr "Tving multigateway-modus" -msgid "Core System settings" -msgstr "Systeminstillinger for Core" +msgid "Forward" +msgstr "" -msgid "Upgrade Database" -msgstr "Oppgrader databasen" +msgid "Forward: {{forward}}" +msgstr "" -msgid "Manage Snapshots" -msgstr "Behandle snapshots" +msgid "Found no future pieces" +msgstr "" -msgid "System Settings" -msgstr "Systeminstillinger" +msgid "Frame Rate" +msgstr "Framerate" -msgid "Update Blueprints?" -msgstr "Oppdater blueprints?" +msgid "From" +msgstr "" -msgid "Update" -msgstr "Oppdater" +msgid "Full System Snapshot" +msgstr "Fullt systemsnapshot" -msgid "" -"Are you sure you want to update the blueprints from the file " -"\"{{fileName}}\"?" +msgid "fullscreen" msgstr "" -"Er du sikker på at du vil oppdatere blueprints fra filen \"{{fileName}}\"?" - -msgid "Blueprints updated successfully." -msgstr "Blueprints ble oppdatert." -msgid "Replace Blueprints?" -msgstr "Erstatte blueprints?" +msgid "Gateway" +msgstr "" -msgid "Replace" -msgstr "Erstatt" +msgid "General" +msgstr "" -msgid "" -"Are you sure you want to replace the blueprints with the file " -"\"{{fileName}}\"?" +msgid "Generated URL" msgstr "" -"Er du sikker på at du vil erstatte blueprints fra filen \"{{fileName}}\"?" -msgid "Failed to update blueprints: {{errorMessage}}" -msgstr "Oppdatering av blueprints feilet: {{errorMessage}}" +msgid "Generating restart token" +msgstr "" -msgid "Assigned Show Styles:" -msgstr "Tilordnede showstyles:" +msgid "Generic Properties" +msgstr "Generelle egenskaper" -msgid "This Blueprint is not being used by any Show Style" -msgstr "Dette blueprintet er ikke i bruk av noen showstyles" +msgid "Generic Script" +msgstr "Generisk manus" -msgid "Assigned Studios:" -msgstr "Tilordnede studio:" +msgid "Getting Started" +msgstr "Kom i gang" -msgid "This Blueprint is not compatible with any Studio" -msgstr "Dette blueprintet er ikke kompatibel med noe studio" +msgid "Global AdLib" +msgstr "Globale adliber" -msgid "Unassign" -msgstr "Fjern tilordning" +msgid "Global AdLibs" +msgstr "Globale adliber" -msgid "Assign" -msgstr "Tilordne" +msgid "Go to Live" +msgstr "" -msgid "Blueprint ID" -msgstr "Blueprint-id" +msgid "Go to On Air line" +msgstr "Gå til OnAir-posisjon" -msgid "Blueprint Name" -msgstr "Blueprintnavn" +msgid "Go to On Air Segment" +msgstr "" -msgid "No name set" -msgstr "Navn ikke definert" +msgid "Good" +msgstr "Bra" -msgid "Blueprint Type" -msgstr "Blueprinttype" +msgid "Graphics" +msgstr "Grafikk" -msgid "Upload a new blueprint" -msgstr "Last opp et nytt blueprint" +msgid "GUI" +msgstr "Brukergrensesnitt" -msgid "Last modified" -msgstr "Sist endret" +msgid "he default state of this Route Set" +msgstr "" -msgid "Blueprint Id" -msgstr "Blueprint-id" +msgid "Heading" +msgstr "" -msgid "Blueprint Version" -msgstr "Blueprintversjon" +msgid "Height" +msgstr "Høyde" -msgid "Disable version check" -msgstr "Deaktiver versjonsjekk" +msgid "Help & Support" +msgstr "Hjelp og brukerstøtte" -msgid "Upload Blueprints" -msgstr "Last opp blueprints" +msgid "Hide" +msgstr "" -msgid "OAuth credentials succesfully uploaded." -msgstr "Opplasting av OAuth credentials var vellykket." +msgid "Hide Countdown" +msgstr "Skjul nedtelling" -msgid "Failed to upload OAuth credentials: {{errorMessage}}" -msgstr "Opplasting av OAuth credentials feilet: {{errorMessage}}" +msgid "Hide default AdLib Start/Execute options" +msgstr "" -msgid "OAuth credentials successfuly reset" -msgstr "OAuth credentials nullstilt" +msgid "Hide Diff" +msgstr "Skjul forskjell" -msgid "Failed to reset OAuth credentials: {{errorMessage}}" -msgstr "Nullstiling av OAuth credentials feilet: {{errorMessage}}" +msgid "Hide Diff Label" +msgstr "Skjul etikett for forskjell" -msgid "Reset Authentication" -msgstr "Nullstill autentisering" +msgid "Hide duplicated AdLibs" +msgstr "Skjul dupliserte adliber" -msgid "Application credentials" -msgstr "Brukernavn/passord (Application Credentials)" +msgid "Hide End Time" +msgstr "Skjul sendeslutt" -msgid "Access token" -msgstr "Tilgangskode (Access Token)" +msgid "Hide Expected End timing when a break is next" +msgstr "Gjem nedtelling til forventet slutt når neste punkt er en pause" -msgid "Click on the link below and accept the permissions request." -msgstr "Klikk på linken under og godta permissions-forespørselen" +msgid "Hide for dynamically inserted parts" +msgstr "Skjul for dynamisk innsatte deler" -msgid "Waiting for gateway to generate URL..." -msgstr "Venter på at gateway genererer URL..." +msgid "Hide Label" +msgstr "Skjul etikett" -msgid "Only Match Global AdLibs" -msgstr "Vis kun globale adliber" +msgid "Hide over/under timer" +msgstr "" -msgid "Name" -msgstr "Navn" +msgid "Hide Panel from view" +msgstr "Ikke vis dette panelet" -msgid "Display Style" -msgstr "Visningsstil" +msgid "Hide Planned End Label" +msgstr "Skjul etikett for planlagt slutt" -msgid "Show thumbnails next to list items" -msgstr "Vis miniatyrbilder ved siden av listeelementer" +msgid "Hide Planned Start" +msgstr "Skjul planlagt start" -msgid "Button width scale factor" -msgstr "Breddeskala for knapp" +msgid "Hide Rundown Divider" +msgstr "Skjul kjøreplanskille" -msgid "Button height scale factor" -msgstr "Høydeskala for knapp" +msgid "Hide rundown divider between rundowns in a playlist" +msgstr "Skjul skille mellom kjøreplaner i en spilleliste" -msgid "Only Display AdLibs from Current Segment" -msgstr "Vis kun adliber fra gjeldende tittel" +msgid "Hide scrollbar" +msgstr "" -msgid "Include Global AdLibs" -msgstr "Inkluder globale adliber" +msgid "Hold" +msgstr "Hold" -msgid "Filter Disabled" -msgstr "Filter deaktivert" +msgid "Hostname or IP address of the Atem" +msgstr "" -msgid "Include Clear Source Layer in Ad-Libs" -msgstr "Ta med \"Tøm kildelag\" i adliber" +msgid "Hotkey" +msgstr "Hurtigtast" -msgid "Source Layer Types" -msgstr "Kildelagstyper" +msgid "How did the show go?" +msgstr "" -msgid "Filter disabled" -msgstr "Filter deaktivert" +msgid "" +"How many of the transactions to monitor. Set to -1 to log nothing (max " +"performance), 0.5 to log 50% of the transactions, 1 to log all transactions" +msgstr "" +"Antall transaksjoner som overvåkes. Sett verdien til -1 for å ikke logge " +"noe (maks ytelse), til 0.5 for å logge halvparten av transaksjonene eller " +"til 1 for å logge alle transaksjonene" -msgid "Label contains" -msgstr "Etikett inneholder" +msgid "" +"How much preparation time to add to global pieces on the timeline before " +"they are played" +msgstr "" -msgid "Tags must contain" -msgstr "Tagger må inneholde" +msgid "HTML that will be shown in the Support Panel" +msgstr "HTML-kode som vil bli vist i supportpanelet" -msgid "Hide Panel from view" -msgstr "Ikke vis dette panelet" +msgid "Human-readable name of the layer" +msgstr "Leservennlig lagnavn" -msgid "Show panel as a timeline" -msgstr "Vis panel som en tidslinje" +msgid "Icon" +msgstr "Ikon" -msgid "Enable search toolbar" -msgstr "Aktiver søkeverktøy" +msgid "Icon color" +msgstr "Ikonfarge" -msgid "Overflow horizontally" -msgstr "Horisontal overflyt" +msgid "Id" +msgstr "Id" -msgid "Display Take buttons" -msgstr "Vis Take-knapp" +msgid "ID" +msgstr "" -msgid "Queue all adlibs" -msgstr "Cue alle adliber" +msgid "" +"ID of the device (corresponds to the device ID in the peripheralDevice " +"settings)" +msgstr "Enhets-id (korresponderer med enhets-id under enhetsinnstillinger)" -msgid "Toggle AdLibs on single mouse click" -msgstr "Veksle mellom adliber med enkelt museklikk" +msgid "ID of the timeline-layer to map to some output" +msgstr "Lag-id for tidslinjelaget som skal mappes til en utgang" -msgid "Current part can contain next pieces" +msgid "Idempotency-Key is already used" msgstr "" -msgid "Indicate only one next piece per source layer" +msgid "Idempotency-Key is missing" msgstr "" -msgid "Hide duplicated AdLibs" -msgstr "Skjul dupliserte adliber" +msgid "If set, only one Route Set will be active per exclusivity group" +msgstr "" +"Bare en omkoblingsgruppe være aktiv per eksklusivitetsgruppe når dette er " +"krysset av for" msgid "" -"Picks the first instance of an adLib per rundown, identified by uniqueness Id" +"If set, Package Manager assumes that the source doesn't support HEAD " +"requests and will use GET instead. If false, HEAD requests will be sent to " +"check availability." msgstr "" -"Velger den første forekomsten av en adlib i hver kjøreplan, identifisert av " -"unik id" -msgid "URL" -msgstr "Adresse (url)" - -msgid "Display Rank" -msgstr "Rangering for visning" +msgid "Ignore and apply" +msgstr "" -msgid "Role" -msgstr "Rolle" +msgid "Ignore QuickLoop" +msgstr "" -msgid "Adlib Rank" -msgstr "Adlib-rang" +msgid "Ignoring take as playing part has changed since TAKE was requested." +msgstr "" -msgid "Place label below panel" -msgstr "Plasser etikett under panel" +msgid "Ignoring TAKES that are too quick after eachother ({{duration}} ms)" +msgstr "" -msgid "Disabled" -msgstr "Deaktivert" +msgid "Import" +msgstr "Importer" -msgid "Show segment name" -msgstr "Vis tittelens navn" +msgid "Import error: {{errorMessage}}" +msgstr "" -msgid "Show part title" -msgstr "Vis delens tittel" +msgid "Import file?" +msgstr "" -msgid "Hide for dynamically inserted parts" -msgstr "Skjul for dynamisk innsatte deler" +msgid "Importing an AdLib to the Bucket" +msgstr "" -msgid "Planned Start Text" -msgstr "Tekst for planlagt start" +msgid "In" +msgstr "Inn" -msgid "Text to show above show start time" -msgstr "Tekst som vises over klokkeslett for sendestart" +msgid "IN" +msgstr "" -msgid "Hide Diff" -msgstr "Skjul forskjell" +msgid "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s" +msgstr "om {{days}} dager, {{hours}} t {{minutes}} min {{seconds}} s" -msgid "Hide Planned Start" -msgstr "Skjul planlagt start" +msgid "in {{hours}} h {{minutes}} min {{seconds}} s" +msgstr "om {{hours}} t {{minutes}} min {{seconds}} s" -msgid "Planned End text" -msgstr "Tekst for planlagt slutt" +msgid "in {{minutes}} min {{seconds}} s" +msgstr "om {{minutes}} min {{seconds}} s" -msgid "Text to show above show end time" -msgstr "Tekst som vises over klokkeslett for sendeslutt" +msgid "in {{seconds}} s" +msgstr "om {{seconds}} s" -msgid "Hide Planned End Label" -msgstr "Skjul etikett for planlagt slutt" +msgid "In rehearsal" +msgstr "" -msgid "Hide Diff Label" -msgstr "Skjul etikett for forskjell" +msgid "Include Clear Source Layer in Ad-Libs" +msgstr "Ta med \"Tøm kildelag\" i adliber" -msgid "Hide Countdown" -msgstr "Skjul nedtelling" +msgid "Include Global AdLibs" +msgstr "Inkluder globale adliber" -msgid "Hide End Time" -msgstr "Skjul sendeslutt" +msgid "Indicate only one next piece per source layer" +msgstr "" -msgid "Hide Label" -msgstr "Skjul etikett" +msgid "Ingest Devices" +msgstr "" -msgid "Script Source Layers" +msgid "Ingest devices are needed to create rundowns" msgstr "" -msgid "Source layers containing script" +msgid "Ingest from Snapshot" msgstr "" -msgid "Require Piece on Source Layer" +msgid "Ingest Rundown Status" msgstr "" -msgid "Text" -msgstr "Tekst" +msgid "Ingest Rundown Statuses" +msgstr "" -msgid "Show Rundown Name" -msgstr "Vis kjøreplannavn" +msgid "Input Devices" +msgstr "" -msgid "Segment" -msgstr "Tittel" +msgid "Input devices allow you to trigger Sofie actions remotely" +msgstr "" -msgid "Part" -msgstr "Del" +msgid "Installation name" +msgstr "Installasjonsnavn" -msgid "Show Piece Icon Color" +msgid "Internal Error generating RundownPlaylist" msgstr "" -msgid "Use color of primary piece as background of panel" -msgstr "" +msgid "Internal ID" +msgstr "Intern-id" -msgid "Box color" -msgstr "" +msgid "Invalid AdLib" +msgstr "Ugyldig adlib" -msgid "Also Require Source Layers" +msgid "Invalid blueprint: \"{{blueprintId}}\"" msgstr "" msgid "" -"Specify additional layers where at least one layer must have an active piece" -msgstr "" - -msgid "Require All Additional Source Layers" +"Invalid config preset for blueprint: \"{{configPresetId}}\" " +"({{blueprintId}})" msgstr "" -msgid "All additional source layers must have active pieces" +msgid "Invert joystick" msgstr "" -msgid "X" -msgstr "X" - -msgid "Y" -msgstr "Y" - -msgid "Width" -msgstr "Bredde" +msgid "Is a Guest Input" +msgstr "Er en gjesteinngang" -msgid "Height" -msgstr "Høyde" +msgid "Is a Live Remote Input" +msgstr "Er en RM" -msgid "Scale" -msgstr "Skala" +msgid "Is collapsed by default" +msgstr "Er minimert som standard" -msgid "Custom Classes" -msgstr "Tilpassede klasser" +msgid "Is flattened" +msgstr "Er slått sammen" -msgid "Device ID" -msgstr "Enhets-id" +msgid "Is hidden" +msgstr "Er skjult" -msgid "Device Type" -msgstr "Enhetstype" +msgid "Is Immutable" +msgstr "" -msgid "Remove this item?" -msgstr "Fjern dette elementet?" +msgid "Is PGM Output" +msgstr "Er programutgang" -msgid "Are you sure you want to remove {{type}} \"{{deviceId}}\"?" -msgstr "Er du sikker på at du vil fjerne enheten {{type}} \"{{deviceId}}\"?" +msgid "ISA URLs" +msgstr "ISA-adresse (url)" -msgid "Attached Subdevices" -msgstr "Tilkoblede underenheter" +msgid "Job Status" +msgstr "" -msgid "Expected End text" -msgstr "Tekst for forventet slutt" +msgid "Just now" +msgstr "Nå" -msgid "Text to show above countdown to end of show" -msgstr "Tekst som vises over nedtelling til forventet slutt" +msgid "Key" +msgstr "Key" -msgid "Hide Expected End timing when a break is next" -msgstr "Gjem nedtelling til forventet slutt når neste punkt er en pause" +msgid "Keyboard" +msgstr "" msgid "" -"While there are still breaks coming up in the show, hide the Expected End " -"timers" +"Keyboard shortcuts and Stream Deck buttons will not work while filling out " +"the form!" msgstr "" -"Gjem nedtelling til forventet slutt mens det fremdeles er pauser igjen i " -"sendingen" -msgid "Show next break timing" -msgstr "Vis tid for neste pause" +msgid "Kill (debug)" +msgstr "Kill (debug)" -msgid "Whether to show countdown to next break" -msgstr "Om nedtelling til neste pause skal vises" +msgid "Label" +msgstr "Etikett" -msgid "Last rundown is not break" -msgstr "Siste kjøreplan er ingen pause" +msgid "Label contains" +msgstr "Etikett inneholder" -msgid "Don't treat the end of the last rundown in a playlist as a break" -msgstr "" -"Ikke behandle slutten av den siste kjøreplanen i en spilleliste som en pause" +msgid "Last" +msgstr "Forrige" -msgid "Next Break text" -msgstr "Tekst for neste pause" +msgid "Last {{layerName}}" +msgstr "Siste {{layerName}}" -msgid "Text to show above countdown to next break" -msgstr "Tekst som vises over nedtelling til neste pause" +msgid "Last modified" +msgstr "Sist endret" -msgid "Expose as user selectable layout" -msgstr "Gjør tilgjengelig som brukervalgt layout" +msgid "Last rundown is not break" +msgstr "Siste kjøreplan er ingen pause" -msgid "Shelf Layout" -msgstr "Layouter for skuffen" +msgid "Last seen" +msgstr "Sist sett" -msgid "Mini Shelf Layout" -msgstr "Layouter for miniskuff" +msgid "Last Seen" +msgstr "" -msgid "Rundown Header Layout" -msgstr "Layout for kjøreplanens topptekst" +msgid "Last update" +msgstr "Nyeste oppdatering" -msgid "Live line countdown requires Source Layer" -msgstr "" +msgid "Last updated" +msgstr "Sist oppdatert" -msgid "" -"One of these source layers must have an active piece for the live line " -"countdown to be show" +msgid "Layer does not allow sticky pieces!" msgstr "" -msgid "Hide Rundown Divider" -msgstr "Skjul kjøreplanskille" +msgid "Layer ID" +msgstr "Lag-id" -msgid "Hide rundown divider between rundowns in a playlist" -msgstr "Skjul skille mellom kjøreplaner i en spilleliste" +msgid "Layer Mappings" +msgstr "Lagmappinger" -msgid "Show Breaks as Segments" -msgstr "Vis pauser som titler" +msgid "Layer Name" +msgstr "Lagnavn" -msgid "Segment countdown requires source layer" -msgstr "Nedtelling for tittel krever kildelag" +msgid "Leave Unsynced" +msgstr "Behold ikke-synkronisert kjøreplan" + +msgid "Less than a minute ago" +msgstr "Under ett minutt siden" + +msgid "Less than five minutes ago" +msgstr "Under fem minutter siden" -msgid "" -"One of these source layers must have a piece for the countdown to segment on-" -"air to be show" +msgid "Lighting" msgstr "" -"Et av disse kildelagene må ha et element for at nedtelling til tittelen er " -"OnAir vises" -msgid "Fixed duration in Segment header" -msgstr "Låst varighet i tittelheader" +msgid "Limit" +msgstr "Grense" -msgid "" -"The segment duration in the segment header always displays the planned " -"duration instead of acting as a counter" +msgid "Live line countdown requires Source Layer" msgstr "" -"Tittelens varighet i tittelheaderen vil alltid vise den planlagte varigheten " -"i stedet for å telle ned" - -msgid "Select visible Source Layers" -msgstr "Velg synlige kildelag" -msgid "Select visible Output Groups" -msgstr "Velg synlig utgangsgruppe" +msgid "Live Speak" +msgstr "STK" -msgid "Display piece duration for source layers" +msgid "Loading" msgstr "" -msgid "Piece on selected source layers will have a duration label shown" +msgid "Loading..." msgstr "" -msgid "Expose layout as a standalone page" -msgstr "Gjør layout tilgjengelig som en selvstendig side" - -msgid "Open shelf by default" -msgstr "Åpne skuff som standard" +msgid "Local" +msgstr "Lokal" -msgid "Default shelf height" -msgstr "Standard høyde for skuff" +msgid "Local Time" +msgstr "Lokal tid" -msgid "Show Buckets" -msgstr "Vis bøtter" +msgid "Logging level" +msgstr "Loggenivå" -msgid "Show Inspector" +msgid "Logo" msgstr "" -msgid "Disable Context Menu" -msgstr "Skru av kontekstmeny" - -msgid "This action has an invalid combination of filters" -msgstr "Denne handlingen har en ugyldig kombinasjon av filtre" - -msgid "Use Trigger Mode" +msgid "Lookahead Maximum Search Distance (Undefined = {{limit}})" msgstr "" -msgid "Trigger Mode" +msgid "Lookahead Mode" +msgstr "Lookahead-modus" + +msgid "Lookahead Target Objects (Undefined = 1)" msgstr "" -msgid "Force" -msgstr "Tving" +msgid "Loop End" +msgstr "Slutt for loop" -msgid "Rehearsal" -msgstr "Testmodus" +msgid "Loop Start" +msgstr "Start for loop" -msgid "Mode: {{triggerMode}}" +msgid "Loops to Start" msgstr "" -msgid "Undo" -msgstr "Angre" +msgid "Lower Third" +msgstr "Super" -msgid "Segments: {{delta}}" -msgstr "Segmenter: {{delta}}" +msgid "Manage Snapshots" +msgstr "Behandle snapshots" -msgid "Parts: {{delta}}" -msgstr "Deler: {{delta}}" +msgid "Mapping cannot be reset as it has no default values" +msgstr "" -msgid "Open" -msgstr "Åpne" +msgid "Mapping Type" +msgstr "" -msgid "Toggle" -msgstr "Veklse" +msgid "Mappings" +msgstr "Lagmappinger" -msgid "On" -msgstr "På" +msgid "Margin (%)" +msgstr "" -msgid "Forward: {{forward}}" +msgid "Maximum register limit" msgstr "" -msgid "Activate Rundown" -msgstr "Aktiver kjøreplan" +msgid "Media" +msgstr "Media" -msgid "Ad-Lib" -msgstr "Adlib" +msgid "Media Preview URL" +msgstr "Forhåndsvisnings-URL" -msgid "Deactivate Rundown" -msgstr "Deaktiver kjøreplan" +msgid "Media Status" +msgstr "" -msgid "Disable next Piece" -msgstr "Skip neste element" +msgid "Media Type" +msgstr "" -msgid "Move Next" -msgstr "Skip neste" +msgid "Memory troubleshooting" +msgstr "" -msgid "Reload NRCS Data" -msgstr "Last inn MOS-data på nytt" +msgid "Menu" +msgstr "" -msgid "Resync with NRCS" -msgstr "Synkroniser med ENPS" +msgid "Message" +msgstr "Melding" -msgid "Shelf" -msgstr "Skuff" +msgid "Message Queue" +msgstr "Meldingskø" -msgid "Rewind Segments to start" -msgstr "Sett alle segmenter tilbake til start" +msgid "Message shown to users in the Evaluations form" +msgstr "" -msgid "Go to On Air line" -msgstr "Gå til OnAir-posisjon" +msgid "Messages" +msgstr "Meldinger" -msgid "Show entire On Air Segment" -msgstr "Vis hele tittelen som er OnAir" +msgid "Method" +msgstr "Metode" -msgid "Queue AdLib from Minishelf" +msgid "Method ${method}" msgstr "" -msgid "Force (deactivate others)" -msgstr "Tving (deaktiver andre)" - -msgid "Move Segments" -msgstr "Skip segmenter" - -msgid "By Segments" +msgid "MIDI Pedal" msgstr "" -msgid "Move Parts" -msgstr "Skip del" +msgid "Migrate database" +msgstr "Migrer database" -msgid "By Parts" -msgstr "" +msgid "Migrations" +msgstr "Migrering" -msgid "State" -msgstr "Tilstand" +msgid "Mini Shelf Layout" +msgstr "Layouter for miniskuff" -msgid "Forward" +msgid "Mini Shelf Layouts" msgstr "" -msgid "Action" -msgstr "Handling" - -msgid "Ad-Lib Action" -msgstr "Adlib-handling" +msgid "Minimum register limit" +msgstr "" -msgid "Clear Source Layer" -msgstr "Tøm kildelag" +msgid "Minimum Take Span" +msgstr "" -msgid "Sticky Piece" -msgstr "Element er sticky" +msgid "Minor Warning" +msgstr "Mindre advarsel (avvik)" -msgid "Global AdLibs" -msgstr "Globale adliber" +msgid "Mirror horizontally" +msgstr "" -msgid "Label" -msgstr "Etikett" +msgid "Mirror vertically" +msgstr "" -msgid "Limit" -msgstr "Grense" +msgid "Mock Piece Content Status" +msgstr "" -msgid "Output Layer" -msgstr "Utgangslag" +msgid "Mode: {{triggerMode}}" +msgstr "" -msgid "Pick" -msgstr "Plukk" +msgid "Modify Shift register" +msgstr "" -msgid "Pick last" -msgstr "Plukk siste" +msgid "Modifying Bucket" +msgstr "" -msgid "Source Layer" -msgstr "Kildelag" +msgid "Modifying Bucket AdLib" +msgstr "" -msgid "Source Layer Type" -msgstr "Kildelagstyper" +msgid "Monitor blocked thread" +msgstr "" -msgid "Tag" -msgstr "Tag" +msgid "More documentation available at:" +msgstr "Mer dokumentasjon er tilgjengelig på:" -msgid "Not Global" -msgstr "Ikke globale" +msgid "More than 10 minutes ago" +msgstr "Over 10 minutter siden" -msgid "Only Global" -msgstr "Bare globale" +msgid "More than 2 hours ago" +msgstr "Over 2 timer siden" -msgid "OnAir" -msgstr "OnAir" +msgid "More than 30 minutes ago" +msgstr "Over 30 minutter siden" -msgid "Now active rundown" -msgstr "Aktiv kjøreplan akkurat nå" +msgid "More than 5 hours ago" +msgstr "Over 5 timer siden" -msgid "View" -msgstr "Visning" +msgid "More than a day ago" +msgstr "Over en dag siden" -msgid "" -"Executes within the currently open Rundown, requires a Client-side trigger." +msgid "Mouse" msgstr "" -"Utføers innenfor den valgte kjøreplanen, men trenger en utløser fra klienten." -msgid "Select Action" -msgstr "Velg handling" - -msgid "" -"No Ad-Lib matches in the current state of Rundown: " -"\"{{rundownPlaylistName}}\"" -msgstr "" -"Ingen treff på adliber i nåværende tilstand for kjøreplanen: " -"\"{{rundownPlaylistName}}\"" +msgid "Move Next" +msgstr "Skip neste" -msgid "No matching Rundowns available to be used for preview" -msgstr "Ingen passende kjøreplaner tilgjengelige for forhåndsvisning" +msgid "Move Next backwards" +msgstr "Unskip neste" -msgid "Multilingual description, editing will overwrite" -msgstr "Endring vil overskrive flerspråklig beskrivelse" +msgid "Move Next forwards" +msgstr "Skip neste" -msgid "Optional description of the action" -msgstr "Valgfri beskrivelse av handlingen" +msgid "Move Next to the following segment" +msgstr "Skip til neste segment" -msgid "Triggered Actions uploaded successfully." -msgstr "Opplasting av handlingsutløsere var vellykket." +msgid "Move Next to the previous segment" +msgstr "Unskip neste segment" -msgid "Triggered Actions failed to upload: {{errorMessage}}" -msgstr "Opplasting av handlingsutløsere feilet: {{errorMessage}}" +msgid "Move Parts" +msgstr "Skip del" -msgid "Append or Replace" -msgstr "Legg til eller erstatt" +msgid "Move Segments" +msgstr "Skip segmenter" -msgid "" -"Do you want to append these to existing Action Triggers, or do you want to " -"replace them?" +msgid "Moving Next" msgstr "" -"Vil du legge disse til de nåværende handlingsutløserne, eller vil du " -"erstatte dem?" -msgid "Append" -msgstr "Legg til" +msgid "Multi-gateway-mode delay time" +msgstr "Delaytid for multigateway-modus" -msgid "Action Triggers" -msgstr "Handlingsutløsere" +msgid "Multilingual description, editing will overwrite" +msgstr "Endring vil overskrive flerspråklig beskrivelse" -msgid "Find Trigger..." -msgstr "Finn utløser..." +msgid "My name is {{name}}" +msgstr "Mitt navn er {{name}}" -msgid "No matching Action Trigger." -msgstr "Fikk ikke treff blant handlingsutløsere." +msgid "Name" +msgstr "Navn" -msgid "No Action Triggers set up." -msgstr "Ingen handlingsutløsere er satt opp." +msgid "Network address" +msgstr "" -msgid "System-wide" -msgstr "Systemvid" +msgid "Network Id" +msgstr "Nettverk-id" -msgid "Upload stored Action Triggers" -msgstr "Last opp lagrede handlingsutløsere" +msgid "New Bucket" +msgstr "Ny bøtte" -msgid "Download Action Triggers" -msgstr "Last ned handlingsutløsere" +msgid "New Filter" +msgstr "Nytt filter" -msgid "On release" -msgstr "På slipp (\"Key up\")" +msgid "New Layer" +msgstr "Nytt lag" -msgid "Empty" -msgstr "Tom" +msgid "New Layout" +msgstr "Ny layout" -msgid "Hotkey" -msgstr "Hurtigtast" +msgid "New Output" +msgstr "Ny utgang" -msgid "Trigger Type" -msgstr "Type utløser" +msgid "New Source" +msgstr "Ny kilde" -msgid "Failed to update config: {{errorMessage}}" -msgstr "Oppdatering av konfigurasjon feilet: {{errorMessage}}" +msgid "Next" +msgstr "Neste" -msgid "Export" -msgstr "Eksporter" +msgid "Next Break text" +msgstr "Tekst for neste pause" -msgid "Import" -msgstr "Importer" +msgid "Next Loop at" +msgstr "Neste loop starter" -msgid "true" -msgstr "true" +msgid "Next Part" +msgstr "Neste del" -msgid "false" -msgstr "false" +msgid "Next scheduled show" +msgstr "Neste planlagte sending" -msgid "{{count}} rows" -msgstr "{{count}} rader" +msgid "Next Segment" +msgstr "Neste tittel" -msgid "Value" -msgstr "Verdi" +msgid "Nintendo Joy-Con" +msgstr "" -msgid "Create" -msgstr "Opprett" +msgid "No" +msgstr "Nei" -msgid "Add config item" -msgstr "Legg til konfigurasjonselement" +msgid "No Action Triggers set up." +msgstr "Ingen handlingsutløsere er satt opp." -msgid "Add" -msgstr "Legg til" +msgid "No actions available" +msgstr "Ingen kjøreplanvalg tilgjengelige i påsynsmodus" + +msgid "" +"No Ad-Lib matches in the current state of Rundown: " +"\"{{rundownPlaylistName}}\"" +msgstr "" +"Ingen treff på adliber i nåværende tilstand for kjøreplanen: " +"\"{{rundownPlaylistName}}\"" -msgid "Item" -msgstr "Element" +msgid "No camera-related source layers found" +msgstr "" -msgid "Delete this item?" -msgstr "Slett dette elementet?" +msgid "No changes" +msgstr "" -msgid "Are you sure you want to delete this config item \"{{configId}}\"?" +msgid "No gateways are configured" msgstr "" -"Er du sikker på at du vil slette konfigurasjonselementet \"{{configId}}\"?" -msgid "Blueprint Configuration" -msgstr "Blueprintkonfigurasjon" +msgid "No matching Action Trigger." +msgstr "Fikk ikke treff blant handlingsutløsere." -msgid "More settings specific to this studio can be found here" -msgstr "Mer spesifikke innstillinger for dette studioet kan du finne her" +msgid "No matching Rundowns available to be used for preview" +msgstr "Ingen passende kjøreplaner tilgjengelige for forhåndsvisning" -msgid "There was an error: {{error}}" -msgstr "Det skjedde en feil: {{error}}" +msgid "No Media matches this filter" +msgstr "" -msgid "Package Manager status" -msgstr "Status fo pakkebehandler" +msgid "No Media required by this system" +msgstr "" -msgid "Reload statuses" -msgstr "Last inn status på nytt" +msgid "No Media required for this Rundown" +msgstr "" -msgid "Updated" -msgstr "Oppdatert" +msgid "No migrations to apply" +msgstr "" -msgid "Package Manager" -msgstr "Pakkebehandler" +msgid "No name set" +msgstr "Navn ikke definert" -msgid "Expectation Manager" +msgid "No Next point found, please set a part as Next before doing a TAKE." msgstr "" -msgid "Statistics" -msgstr "Statistikk" +msgid "No notifications" +msgstr "" -msgid "Times" -msgstr "Tider" +msgid "No output channels set" +msgstr "Ingen utgangskanal definert" -msgid "Connected Workers" -msgstr "Tilkoblede arbeidere" +msgid "No output layers available" +msgstr "" -msgid "Work-in-progress" -msgstr "Pågående jobber" +msgid "No PGM output" +msgstr "Ingen programutgang" -msgid "WorkForce" -msgstr "Arbeiderstyrke" +msgid "No problems" +msgstr "Ingen problemer" -msgid "Kill (debug)" -msgstr "Kill (debug)" +msgid "No schema has been provided for this mapping" +msgstr "" -msgid "Connected Expectation Managers" +msgid "No source layers available" msgstr "" -msgid "Connected App Containers" -msgstr "Tilkoblede app-kontainere" +msgid "No source layers set" +msgstr "Ingen kildelag definert" msgid "No status loaded" msgstr "Ingen status lastet" -msgid "Peripheral Device is outdated" -msgstr "Tilkoblet enhet er utdatert" +msgid "None" +msgstr "Ingen" -msgid "" -"The config UI is now driven by manifests fed by the device. This device " -"needs updating to provide the configManifest to be configurable" +msgid "Normal scrolling" msgstr "" -"Brukergrensesnittet for konfigurasjon drives nå av manifester matet fra " -"enhetene. Denne enheten må oppdateres for å gjøre configManifest " -"konfigurerbart" -msgid "Are you sure you want to restart this device?" -msgstr "Er du sikker på at du vil starte denne enheten på nytt?" +msgid "Not Active" +msgstr "Inaktiv" -msgid "Restart this Device?" -msgstr "Start denne enheten på nytt?" +msgid "Not Connected" +msgstr "Ikke tilkoblet" -msgid "" -"Check the console for troubleshooting data from device \"{{deviceName}}\"!" -msgstr "Sjekk konsollen for feilsøkingsdata fra enheten \"{{deviceName}}\"!" +msgid "Not defined" +msgstr "Ikke definert" -msgid "" -"There was an error when troubleshooting the device: \"{{deviceName}}\": " -"{{errorMessage}}" +msgid "Not Global" +msgstr "Ikke globale" + +msgid "Not in rehearsal" msgstr "" -"Det skjedde en feil under feilsøking av enhenten \"{{deviceName}}\": " -"{{errorMessage}}" -msgid "Generic Properties" -msgstr "Generelle egenskaper" +msgid "Not queueable" +msgstr "Kan ikke settes i kø" -msgid "Device Name" -msgstr "Enhetsnavn" +msgid "Not set" +msgstr "Ikke angitt" -msgid "Restart Device" -msgstr "Start enheten på nytt" +msgid "Note: Core needs to be restarted to apply these settings" +msgstr "Merknad: Core må startes på nytt for å ta i bruk disse innstillingene" -msgid "Troubleshoot" -msgstr "Feilsøk" +msgid "Notes" +msgstr "" -msgid "Reset Database Version" -msgstr "Nullstill databaseversjon" +msgid "Nothing to cleanup!" +msgstr "" -msgid "" -"Are you sure you want to reset the database version?\n" -"Only do this if you plan on running the migration right after." +msgid "Nothing was found on layer!" msgstr "" -"Er du sikker på at du vil nullstille databaseversjonen?\n" -"Bare gjør dette dersom du har tenkt å kjøre en migrering umiddelbart." -msgid "Version for {{name}}: From {{fromVersion}} to {{toVersion}}" -msgstr "Versjon for {{name}}: Fra {{fromVersion}} til {{toVersion}}" +msgid "Now Active Rundown" +msgstr "" -msgid "Re-check" -msgstr "Sjekk på nytt" +msgid "NRCS Name" +msgstr "" -msgid "Reset Version to" -msgstr "Nullstill versjon til" +msgid "OAuth credentials successfully reset" +msgstr "" -msgid "Reset All Versions" -msgstr "Nullstill alle versjoner" +msgid "OAuth credentials successfully uploaded." +msgstr "" -msgid "Migrate database" -msgstr "Migrer database" +msgid "Off" +msgstr "Av" -msgid "All steps" -msgstr "Alle trinn" +msgid "Off-line devices" +msgstr "Frakoblede enheter" -msgid "" -"The migration consists of several phases, you will get more options after " -"you've this migration" -msgstr "" -"Migreringen består av flere faser, du vil få flere valg etter at du har " -"kjørt denne migreringen" +msgid "OK" +msgstr "OK" -msgid "The migration can be completed automatically." -msgstr "Migreringen kan gjennomføres automatisk." +msgid "On" +msgstr "På" -msgid "Run automatic migration procedure" -msgstr "Kjør automatisk migreringsprosedyre" +msgid "On Air" +msgstr "On Air" -msgid "" -"The migration procedure needs some help from you in order to complete, see " -"below:" -msgstr "" -"Migreringsprosedyren trenger litt hjelp fra deg for å kunne fullføre. Se " -"under:" +msgid "On Air At" +msgstr "On Air klokken" -msgid "Double-check Values" -msgstr "Dobbeltsjekk verdier" +msgid "On Air In" +msgstr "On Air om" -msgid "Are you sure the values you have entered are correct?" -msgstr "Er du sikker på at verdiene du har oppgitt er korrekte?" +msgid "On Air Start Time" +msgstr "Sendestart" -msgid "Run Migration Procedure" -msgstr "Kjør migreringsprosedyre" +msgid "On release" +msgstr "På slipp (\"Key up\")" -msgid "Warnings During Migration" -msgstr "Advarsler under migrering" +msgid "OnAir" +msgstr "OnAir" msgid "" -"Please check the database related to the warnings above. If neccessary, you " -"can" +"One of these source layers must have a piece for the countdown to segment " +"on-air to be show" msgstr "" -"Vennligst sjekk databasen tilknyttet advarslene over. Hvis nødvendig kan du" - -msgid "Force Migration" -msgstr "Tving migrering" +"Et av disse kildelagene må ha et element for at nedtelling til tittelen er " +"OnAir vises" msgid "" -"Are you sure you want to force the migration? This will bypass the migration " -"checks, so be sure to verify that the values in the settings are correct!" +"One of these source layers must have an active piece for the live line " +"countdown to be show" msgstr "" -"Er du sikker på at du vil tvinge migreringen? Dette gjør at du hopper over " -"migreringskontrollene, så vær sikker på at verdiene oppgitt i innstillinger " -"er korrekte!" - -msgid "Force Migration (unsafe)" -msgstr "Tving migrering (utrygt)" - -msgid "The migration was completed successfully!" -msgstr "Migreringen var vellykket!" - -msgid "All is well, go get a" -msgstr "Alt er greit, gå og hent deg en" -msgid "New Layout" -msgstr "Ny layout" - -msgid "Button" -msgstr "Knapp" - -msgid "New Filter" -msgstr "Nytt filter" - -msgid "Delete layout?" -msgstr "Slett layout?" +msgid "Only custom trigger modes will be shown" +msgstr "" -msgid "Are you sure you want to delete the shelf layout \"{{name}}\"?" -msgstr "Er du sikker på at du vil slette layouten \"{{name}}\"?" +msgid "Only Display AdLibs from Current Segment" +msgstr "Vis kun adliber fra gjeldende tittel" -msgid "Action Buttons" -msgstr "Handlingsknapper" +msgid "Only Global" +msgstr "Bare globale" -msgid "Toggled Label" +msgid "Only Match Global AdLibs" +msgstr "Vis kun globale adliber" + +msgid "" +"Only one rundown can be active at the same time. Currently active rundowns: " +"{{names}}" msgstr "" -msgid "Icon" -msgstr "Ikon" +msgid "Only Pieces present in rundown are sticky" +msgstr "Kun elementer tilstede i kjøreplanen er sticky" -msgid "Icon color" -msgstr "Ikonfarge" +msgid "Open" +msgstr "Åpne" -msgid "Filters" -msgstr "Filtre" +msgid "Open Camera Screen" +msgstr "" -msgid "There are no filters set up yet" -msgstr "Det er ikke satt opp noen filtre ennå" +msgid "Open Fullscreen" +msgstr "" -msgid "Default Layout" -msgstr "Standardlayout" +msgid "Open Presenter Screen" +msgstr "" -msgid "Add {{filtersTitle}}" -msgstr "Legg til {{filtersTitle}}" +msgid "Open Prompter" +msgstr "" -msgid "Add filter" -msgstr "Legg til filter" +msgid "Open shelf by default" +msgstr "Åpne skuff som standard" -msgid "Add button" -msgstr "Legg til knapp" +msgid "Operating Mode" +msgstr "Styringsmodus" -msgid "Upload Layout?" -msgstr "Last opp layout?" +msgid "Operation" +msgstr "" -msgid "Upload" -msgstr "Last opp" +msgid "Optional description of the action" +msgstr "Valgfri beskrivelse av handlingen" msgid "" -"Are you sure you want to upload the shelf layout from the file " -"\"{{fileName}}\"?" +"Optionally restrict AB channel display to specific output layers (e.g., " +"only PGM). Leave empty to show for all output layers." msgstr "" -"Er du sikker på at du vil laste opp layout for skuff fra filen " -"\"{{fileName}}\"?" -msgid "Shelf layout uploaded successfully." -msgstr "Opplastingen av layout for skuff var vellykket." +msgid "Order 66?" +msgstr "" -msgid "Failed to upload shelf layout: {{errorMessage}}" -msgstr "Opplasting av layout feilet: {{errorMessage}}" +msgid "Original Layer" +msgstr "Opprinnelig lag" -msgid "Show Style Base Name" -msgstr "Showstylenavn" +msgid "Original Layer not found" +msgstr "" -msgid "Blueprint" -msgstr "Blueprint" +msgid "Other" +msgstr "" -msgid "Blueprint not set" -msgstr "Blueprint ikke valgt" +msgid "Out" +msgstr "Ut" -msgid "Compatible Studios:" -msgstr "Kompatible studio:" +msgid "OUT" +msgstr "" -msgid "This Show Style is not compatible with any Studio" -msgstr "Denne showstylen er ikke kompatibelt med noe studio" +msgid "Output channels" +msgstr "Utgangskanal" -msgid "Camera" -msgstr "Kamera" +msgid "Output Channels" +msgstr "Utgangskanal" -msgid "Graphics" -msgstr "Grafikk" +msgid "Output channels are required for your studio to work" +msgstr "Utgangskanaler er nødvendige for at studioet ditt skal fungere" -msgid "Live Speak" -msgstr "STK" +msgid "Output Layer" +msgstr "Utgangslag" -msgid "Lower Third" -msgstr "Super" +msgid "Over" +msgstr "" -msgid "Studio Microphone" -msgstr "Studiomikrofon" +msgid "Over/Under" +msgstr "Over/Under" -msgid "Remote Source" -msgstr "RM" +msgid "Overflow horizontally" +msgstr "Horisontal overflyt" -msgid "Generic Script" -msgstr "Generisk manus" +msgid "Overlay Screen" +msgstr "" -msgid "Split Screen" -msgstr "Splitt" +msgid "Package Container ID" +msgstr "Pakkekontainer-id" -msgid "Clips" -msgstr "Klipp" +msgid "Package Containers" +msgstr "Pakkekontainere" -msgid "Metadata" -msgstr "Metadata" +msgid "Package Containers to use for previews" +msgstr "Pakkekontainere som skal benyttes til forhåndsvisninger" -msgid "Camera Movement" -msgstr "Kamerabevegelse" +msgid "Package Containers to use for thumbnails" +msgstr "Pakkekontainere som skal benyttes til miniatyrbilder" -msgid "Unknown Layer" -msgstr "Ukjent lag" +msgid "Package Manager" +msgstr "Pakkebehandler" -msgid "Audio Mixing" -msgstr "Lydmiksing" +msgid "Package Manager is offline" +msgstr "" -msgid "Transition" -msgstr "Effekt" +msgid "Package Manager status" +msgstr "Status fo pakkebehandler" -msgid "Lights" -msgstr "Lys" +msgid "Package Manager: Restart Package Container" +msgstr "" -msgid "Local" -msgstr "Lokal" +msgid "Package Manager: Restart work" +msgstr "" -msgid "New Source" -msgstr "Ny kilde" +msgid "Package Status" +msgstr "Pakkestatus" -msgid "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?" -msgstr "Er du sikker på at du vil slette kildelaget \"{{sourceLayerId}}\"?" +msgid "Packages" +msgstr "Pakker" -msgid "Source Name" -msgstr "Kildenavn" +msgid "Parameters" +msgstr "Parametre" -msgid "Source Abbreviation" -msgstr "Kildeforkortelse" +msgid "Parent Config ID" +msgstr "" -msgid "Internal ID" -msgstr "Intern-id" +msgid "Parent device is missing" +msgstr "" -msgid "Source Type" -msgstr "Kildetype" +msgid "Parent Devices" +msgstr "" -msgid "Is a Live Remote Input" -msgstr "Er en RM" +msgid "part" +msgstr "punkt" -msgid "Is a Guest Input" -msgstr "Er en gjesteinngang" +msgid "Part" +msgstr "Del" -msgid "Is hidden" -msgstr "Er skjult" +msgid "Part Count Down" +msgstr "Nedtelling for del" +msgid "Part Count Up" +msgstr "Opptelling for del" -msgid "Pieces on this layer can be cleared" -msgstr "Elementer på dette laget kan tømmes" +msgid "Part duration is 0." +msgstr "" -msgid "Pieces on this layer are sticky" -msgstr "Elementer i dette laget er sticky" +msgid "Parts Duration" +msgstr "Varighet for del" -msgid "Only Pieces present in rundown are sticky" -msgstr "Kun elementer tilstede i kjøreplanen er sticky" +msgid "Parts: {{delta}}" +msgstr "Deler: {{delta}}" -msgid "Allow disabling of Pieces" -msgstr "Tillat deaktivering av elementer" +msgid "Password" +msgstr "Passord" -msgid "AdLibs on this layer can be queued" -msgstr "Adliber på dette laget kan cues" +msgid "Password for authentication" +msgstr "Passord for autentisering" -msgid "Exclusivity group" -msgstr "Ekslusivitetsgruppe" +msgid "Peripheral Device is outdated" +msgstr "Tilkoblet enhet er utdatert" -msgid "" -"Add some source layers (e.g. Graphics) for your data to appear in rundowns" +msgid "Peripheral Device not found!" msgstr "" -"Legg til kildelag (for eksempel Grafikk) for å vise dine data i kjøreplaner" -msgid "No source layers set" -msgstr "Ingen kildelag definert" +msgid "Peripheral Devices" +msgstr "" -msgid "Delete this output?" -msgstr "Slett denne utgangen?" +msgid "Pick" +msgstr "Plukk" -msgid "Are you sure you want to delete source layer \"{{outputId}}\"?" -msgstr "Er du sikker på at du vil slette kildelaget \"{{outputId}}\"?" +msgid "Pick last" +msgstr "Plukk siste" -msgid "New Output" -msgstr "Ny utgang" +msgid "" +"Picks the first instance of an adLib per rundown, identified by uniqueness " +"Id" +msgstr "" +"Velger den første forekomsten av en adlib i hver kjøreplan, identifisert av " +"unik id" -msgid "Channel Name" -msgstr "Kanalnavn" +msgid "Piece on selected source layers will have a duration label shown" +msgstr "" -msgid "Is PGM Output" -msgstr "Er programutgang" +msgid "Piece to take is already live!" +msgstr "" -msgid "Is collapsed by default" -msgstr "Er minimert som standard" +msgid "Piece to take is not directly playable!" +msgstr "" -msgid "Is flattened" -msgstr "Er slått sammen" +msgid "Piece to take was not found!" +msgstr "" -msgid "Output channels are required for your studio to work" -msgstr "Utgangskanaler er nødvendige for at studioet ditt skal fungere" +msgid "Pieces on this layer are sticky" +msgstr "Elementer i dette laget er sticky" -msgid "Output channels" -msgstr "Utgangskanal" +msgid "Pieces on this layer can be cleared" +msgstr "Elementer på dette laget kan tømmes" -msgid "No output channels set" -msgstr "Ingen utgangskanal definert" +msgid "Place label below panel" +msgstr "Plasser etikett under panel" -msgid "No PGM output" -msgstr "Ingen programutgang" +msgid "Plan. Dur" +msgstr "" -msgid "Key" -msgstr "Key" +msgid "Plan. End" +msgstr "" -#, fuzzy -#| msgid "Host" -msgid "Host Key" -msgstr "Vert" +msgid "Plan. Start" +msgstr "" -#, fuzzy -#| msgid "Source Layer Type" -msgid "Source Layer type" -msgstr "Kildelagstyper" +msgid "Planned Duration" +msgstr "Planlagt varighet" -#, fuzzy -#| msgid "Icon color" -msgid "Key color" -msgstr "Ikonfarge" +msgid "Planned End" +msgstr "Planlagt slutt" -msgid "Custom Hotkey Labels" -msgstr "Egendefinerte etiketter for hurtigtaster" +msgid "Planned End text" +msgstr "Tekst for planlagt slutt" -msgid "AHK" -msgstr "" +msgid "Planned Start" +msgstr "Planlagt start" -msgid "Remove this Variant?" -msgstr "Fjern denne varianten?" +msgid "Planned Start Text" +msgstr "Tekst for planlagt start" + +msgid "Play-out" +msgstr "Avspilling" -msgid "Are you sure you want to remove the variant \"{{showStyleVariantId}}\"?" +msgid "Playlist" msgstr "" -"Er du sikker på at du vil fjerne denne showstylevarianten " -"\"{{showStyleVariantId}}\"?" -msgid "Unnamed variant" -msgstr "Variant uten navn" +msgid "Playout Devices" +msgstr "" -msgid "Variant Name" -msgstr "Variantnavn" +msgid "Playout devices are needed to control your studio hardware" +msgstr "" -msgid "Variants" -msgstr "Varianter" +msgid "Playout devices which uses this package container" +msgstr "Playout-enheter som benytter denne pakkekontaineren" -msgid "Restore from this Snapshot file?" -msgstr "Gjenopprette fra denne snapshotfilen?" +msgid "Playout Gateway \"{{playoutDeviceName}}\" is now restarting." +msgstr "Playout-gateway \"{{playoutDeviceName}}\" starter på nytt..." msgid "" -"Are you sure you want to restore the system from the snapshot file " -"\"{{fileName}}\"?" -msgstr "" -"Er du sikker på at du vil gjenopprettet systemet fra snapshotfilen " -"\"{{fileName}}\"?" +"Please check the database related to the warnings above. If neccessary, you " +"can" +msgstr "Vennligst sjekk databasen tilknyttet advarslene over. Hvis nødvendig kan du" -msgid "Successfully restored snapshot" -msgstr "Gjenoppretting fra snapshot var vellykket" +msgid "Please explain the problems you experienced" +msgstr "" -msgid "Snapshot restore failed: {{errorMessage}}" -msgstr "Gjenoppretting fra snapshot feilet: {{errorMessage}}" +msgid "Please note: This action is irreversible!" +msgstr "Merk: Denne handlingen kan ikke angres!" -msgid "Full System Snapshot" -msgstr "Fullt systemsnapshot" +msgid "Pool name" +msgstr "" -msgid "" -"A Full System Snapshot contains all system settings (studios, showstyles, " -"blueprints, devices, etc.)" +msgid "Pool PlayerId" msgstr "" -"Et systemsnapshot inneholder alle systeminnstillinger (studio, showstyles, " -"blueprints, enheter o.s.v.)" -msgid "Take a Full System Snapshot" -msgstr "Lagre et fullt systemsnapshot" +msgid "Prepare Studio and Activate (Rehearsal)" +msgstr "Forbered studio og aktiver testmodus" -msgid "Studio Snapshot" -msgstr "Studiosnapshot" +msgid "Preparing for broadcast" +msgstr "" -msgid "A Studio Snapshot contains all system settings related to that studio" +msgid "Preparing, please wait..." msgstr "" -"Et studiosnapshot inneholder alle systeminnstillinger tilknyttet et studio" -msgid "Take a Snapshot for studio \"{{studioName}}\" only" -msgstr "Lagre et studiosnapshot utelukkende for \"{{studioName}}\"" +msgid "Presenter Layout" +msgstr "" -msgid "Restore from Snapshot File" -msgstr "Gjenopprett fra snapshotfil" +msgid "Presenter screen" +msgstr "" -msgid "Upload Snapshot" -msgstr "Last opp snapshot" +msgid "Presenter Screen" +msgstr "" -msgid "Restore from Stored Snapshots" -msgstr "Gjenopprett fra lagrede snapshots" +msgid "Presenter View Layouts" +msgstr "" -msgid "Restore" -msgstr "Gjenopprett" +msgid "Preserve position of segments when unsynced relative to other segments" +msgstr "" -msgid "Show \"Remove snapshots\"-buttons" -msgstr "Vis \"Fjern snapshots\"-knappene" +msgid "Previous" +msgstr "" -msgid "Remove this device?" -msgstr "Fjern denne enheten?" +msgid "Previous work status reasons" +msgstr "Tidligere årsaker for jobbsatus" -msgid "Are you sure you want to remove device \"{{deviceId}}\"?" -msgstr "Er du sikker på at du vil fjerne enheten \"{{deviceId}}\"?" +msgid "Prioritizing Media Workflow" +msgstr "" -msgid "Devices are needed to control your studio hardware" -msgstr "Enheter er nødvendige for å kontrollere utstyret i studioet ditt" +msgid "Priority" +msgstr "Prioritet" -msgid "Attached Devices" -msgstr "Tilkoblede enheter" +msgid "Problems" +msgstr "Problemer" -msgid "No devices connected" -msgstr "Ingen enheter tilkoblet" +msgid "Profile name to be used by FileFlow when exporting the clips" +msgstr "Profilnavn som benyttes av FileFlow når klippene eksporteres" -msgid "Playout gateway not connected" -msgstr "Playout-gateway er ikke tilkoblet" +msgid "Prompter" +msgstr "Prompter" -msgid "Remove this mapping?" -msgstr "Fjern denne mappingen?" +msgid "Prompter Screen" +msgstr "" -msgid "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?" +msgid "Properties" msgstr "" -"Er du sikker på at du fil fjerne mappingen for laget \"{{mappingId}}\"?" -msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" -msgstr "Dette laget blir omkoblet av en aktiv omkoplingsgruppe: {{routeSets}}" +msgid "Quantel FileFlow Profile name" +msgstr "Quantel FileFlow profilnavn" -msgid "Layer ID" -msgstr "Lag-id" +msgid "Quantel FileFlow URL" +msgstr "Quantel FileFlow-adresse (url)" -msgid "ID of the timeline-layer to map to some output" -msgstr "Lag-id for tidslinjelaget som skal mappes til en utgang" +msgid "Quantel gateway URL" +msgstr "Quantel Gateway-adresse (url)" -msgid "Layer Name" -msgstr "Lagnavn" +msgid "Quantel transformer URL" +msgstr "Quantel Transformer-adresse (url)" -msgid "Human-readable name of the layer" -msgstr "Leservennlig lagnavn" +msgid "Quantel Zone ID" +msgstr "" -msgid "The type of device to use for the output" -msgstr "Enhetstype som skal brukes for utgangen" +msgid "Queue AdLib from Minishelf" +msgstr "" -msgid "" -"ID of the device (corresponds to the device ID in the peripheralDevice " -"settings)" -msgstr "Enhets-id (korresponderer med enhets-id under enhetsinnstillinger)" +msgid "Queue all adlibs" +msgstr "Cue alle adliber" -msgid "Lookahead Mode" -msgstr "Lookahead-modus" +msgid "Queue segment" +msgstr "Cue tittel: Starter når aktiv tittel er ferdig" -msgid "Lookahead Target Objects (Default = 1)" -msgstr "Lookahead målobjekter (standard = 1)" +msgid "Queue this AdLib" +msgstr "Cue denne adliben" -msgid "Lookahead Maximum Search Distance (Default = {{limit}})" -msgstr "Lookahead maksimum søkelengde (standard = {{limit}})" +msgid "Queued Messages" +msgstr "Meldinger i kø" -msgid "Layer Mappings" -msgstr "Lagmappinger" +msgid "Queueing next Segment" +msgstr "" -msgid "Add a playout device to the studio in order to edit the layer mappings" +msgid "Quick Links" msgstr "" -"For å kunne redigere lagmappinger, må du legge til en playout-enhet til " -"studio" -msgid "Remove this Exclusivity Group?" -msgstr "Fjern fra denne eksklusivitetsgruppen?" +msgid "QuickLoop Fallback Part Duration" +msgstr "" -msgid "" -"Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\n" -"Route Sets assigned to this group will be reset to no group." +msgid "Range: Forward max" msgstr "" -"Er du sikker på at du vil fjerne eksklusivitetsgruppen \"{{eGroupName}}\"?\n" -"Omkoblinger satt til denne gruppen vil bli resatt til ingen gruppe." -msgid "Remove this Route from this Route Set?" -msgstr "Fjern denne omkoblingen fra omkoblingsgruppen?" +msgid "Range: Neutral max" +msgstr "" -msgid "" -"Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to " -"\"{{newLayerId}}\"?" +msgid "Range: Neutral min" msgstr "" -"Er du sikker på at du vil fjerne omkoblingen fra \"{{sourceLayerId}}\" til " -"\"{{newLayerId}}\"?" -msgid "Remove this Route Set?" -msgstr "Fjern denne omkoblingsgruppen?" +msgid "Range: Reverse min" +msgstr "" -msgid "Are you sure you want to remove the Route Set \"{{routeId}}\"?" -msgstr "Er du sikker på at du vil fjerne omkoblingsgruppen \"{{routeId}}\"?" +msgid "Rate limit exceeded" +msgstr "" -msgid "Routes" -msgstr "Omkoblinger" +msgid "Re-check" +msgstr "Sjekk på nytt" -msgid "There are no routes set up yet" -msgstr "Det er ikke satt opp omkoblinger ennå" +msgid "Re-sync" +msgstr "Synkroniser med MOS" -msgid "Original Layer" -msgstr "Opprinnelig lag" +msgid "Re-Sync" +msgstr "Synkroniser" -msgid "None" -msgstr "Ingen" +msgid "Re-sync Rundown" +msgstr "Synkroniser kjøreplanen med ENPS på nytt" -msgid "New Layer" -msgstr "Nytt lag" +msgid "Re-sync rundown data with {{nrcsName}}" +msgstr "Ikke synkronisert med MOS/{{nrcsName}}" -msgid "Route Type" +msgid "Re-Sync rundown?" +msgstr "Synkroniser kjøreplanen med ENPS?" + +msgid "Re-Syncing Rundown" msgstr "" -msgid "Source Layer not found" -msgstr "Kildelag ikke funnet" +msgid "Re-Syncing Rundown Playlist" +msgstr "" -msgid "There are no exclusivity groups set up." -msgstr "Ingen eksklusivitetsgrupper er satt opp." +msgid "Read marker position" +msgstr "" -msgid "Exclusivity Group ID" -msgstr "Eksklusivitetsgruppe-id" +msgid "Reads the ingest (NRCS) data, and pipes it through the blueprints" +msgstr "" -msgid "Exclusivity Group Name" -msgstr "Eksklusivitetsgruppenavn" +msgid "Ready" +msgstr "Klar" -msgid "Display name of the Exclusivity Group" -msgstr "Eksklusivitetsgruppens navn som vises i oversikten" +msgid "Reconnect now" +msgstr "" -msgid "Active" -msgstr "Aktiv" +msgid "Reconnecting to the {{platformName}}" +msgstr "" -msgid "Not Active" -msgstr "Inaktiv" +msgid "Refreshing debug states" +msgstr "" -msgid "Not defined" -msgstr "Ikke definert" +msgid "Register ID" +msgstr "" -msgid "There are no Route Sets set up." -msgstr "Det er ikke satt opp omkoblinger ennå." +msgid "Rehearsal" +msgstr "Testmodus" -msgid "Route Set ID" -msgstr "Omkoblingsgruppe-id" +msgid "Rehearsal mode is already active" +msgstr "" -msgid "Is this Route Set currently active" -msgstr "Er denne omkoblingsgruppen aktiv nå" +msgid "Rehearsal mode is not allowed" +msgstr "" -msgid "Default State" -msgstr "Standardtilstand" +msgid "Rehearsal State" +msgstr "" -msgid "The default state of this Route Set" -msgstr "Standardtilstand for denne omkoblingsgruppen" +msgid "Reload {{nrcsName}} Data" +msgstr "Last inn {{nrcsName}}-data på nytt" -msgid "Route Set Name" -msgstr "Omkoblingsgruppens navn" +msgid "Reload Baseline" +msgstr "Last inn baseline på nytt" -msgid "Display name of the Route Set" -msgstr "Omkoblingsgruppens navn som vises i oversikten" +msgid "Reload NRCS Data" +msgstr "Last inn MOS-data på nytt" -msgid "If set, only one Route Set will be active per exclusivity group" +msgid "Reload statuses" +msgstr "Last inn status på nytt" + +msgid "Reloading Rundown Playlist Data" msgstr "" -"Bare en omkoblingsgruppe være aktiv per eksklusivitetsgruppe når dette er " -"krysset av for" -msgid "Behavior" -msgstr "Oppførsel" +msgid "Rem. Dur" +msgstr "" -msgid "The way this Route Set should behave towards the user" -msgstr "Måten denne omkoblingsgruppen skal oppføre seg på overfor brukeren" +msgid "Remote" +msgstr "" -msgid "Route Sets" -msgstr "Omkoblingsgrupper" +msgid "Remote Source" +msgstr "RM" -msgid "Add a playout device to the studio in order to configure the route sets" +msgid "Remote Speak" msgstr "" -"For å kunne redigere omkoblingsgrupper, må du legge til en playout-enhet til " -"studio" -msgid "" -"Controls for exposed Route Sets will be displayed to the producer within the " -"Rundown View in the Switchboard." +msgid "Remove" +msgstr "Fjern" + +msgid "Remove all Show Style Variants?" msgstr "" -"Kontroller for eksponerte omkoblingsgrupper vil vises til producer i " -"kjøreplansvisningen i omkoblingspanelet." -msgid "Exclusivity Groups" -msgstr "Eksklusivitetsgrupper" +msgid "Remove all trimming" +msgstr "Nullstill inn- og utpunkt" -msgid "Remove this Package Container?" -msgstr "Fjern denne pakkekontaineren?" +msgid "Remove in-trimming" +msgstr "Nullstill innpunkt" -msgid "" -"Are you sure you want to remove the Package Container \"{{containerId}}\"?" -msgstr "Er du sikker på at du vil fjerne pakkekontaineren \"{{containerId}}\"?" +msgid "Remove indexes" +msgstr "Fjern indexer" -msgid "There are no Package Containers set up." -msgstr "Det er ikke satt opp pakkekontainere ennå." +msgid "Remove old data" +msgstr "Fjern gamle data" -msgid "Package Container ID" -msgstr "Pakkekontainer-id" +msgid "Remove old data from database" +msgstr "Fjern gamle data fra databasen" -msgid "Display name/label of the Package Container" -msgstr "Vis navn/merkelapp for pakkekontaineren" +msgid "Remove out-trimming" +msgstr "Nullstill utpunkt" -msgid "Playout devices which uses this package container" -msgstr "Playout-enheter som benytter denne pakkekontaineren" +msgid "Remove rundown" +msgstr "Fjern kjøreplan" + +msgid "Remove Snapshot" +msgstr "" -msgid "Select playout devices" -msgstr "Velg playout-enhet" +msgid "Remove this AB PLayers from this Route Set?" +msgstr "" -msgid "Select which playout devices are using this package container" -msgstr "Velg hvilke playout-enheter som skal benytte denne pakkekontaineren" +msgid "Remove this device?" +msgstr "Fjern denne enheten?" + +msgid "Remove this Device?" +msgstr "Fjern denne enheten?" + +msgid "Remove this Exclusivity Group?" +msgstr "Fjern fra denne eksklusivitetsgruppen?" -msgid "Accessors" -msgstr "Aksessorer" +msgid "Remove this item?" +msgstr "Fjern dette elementet?" + +msgid "Remove this mapping?" +msgstr "Fjern denne mappingen?" msgid "Remove this Package Container Accessor?" msgstr "Fjern denne pakkekontainer-aksessoren?" -msgid "" -"Are you sure you want to remove the Package Container Accessor " -"\"{{accessorId}}\"?" -msgstr "" -"Er du sikker på at du vil fjerne pakkekontainer-aksessoren " -"\"{{accessorId}}\"?" - -msgid "There are no Accessors set up." -msgstr "Ingen aksessorer er satt opp." - -msgid "Accessor ID" -msgstr "Aksessor-id" +msgid "Remove this Package Container?" +msgstr "Fjern denne pakkekontaineren?" -msgid "Display name of the Package Container" -msgstr "Pakkekontainerens navn som vises i oversikten" +msgid "Remove this Route from this Route Set?" +msgstr "Fjern denne omkoblingen fra omkoblingsgruppen?" -msgid "Accessor Type" -msgstr "Aksessortype" +msgid "Remove this Route Set?" +msgstr "Fjern denne omkoblingsgruppen?" -msgid "Folder path" -msgstr "Mappesti" +msgid "Remove this Show Style Variant?" +msgstr "" -msgid "File path to the folder of the local folder" -msgstr "Sti til lokale mappe" +msgid "Removing Bucket" +msgstr "" -msgid "Resource Id" -msgstr "Ressurs-id" +msgid "Removing Bucket AdLib" +msgstr "" -msgid "" -"(Optional) This could be the name of the computer on which the local folder " -"is on" +msgid "Removing Rundown" msgstr "" -"(Valgfri) Dette kan være navnet til datamaskinen som den lokale mappen er på" -msgid "Base URL" -msgstr "Base-url" +msgid "Removing Rundown Playlist" +msgstr "" -msgid "Base url to the resource (example: http://myserver/folder)" -msgstr "Base-url for ressursen (eksempel: http://minserver/mappe)" +msgid "Rename this AdLib" +msgstr "Gi denne adliben nytt navn" -msgid "Network Id" -msgstr "Nettverk-id" +msgid "Rename this Bucket" +msgstr "Gi bøtten nytt navn" -msgid "" -"(Optional) A name/identifier of the local network where the share is " -"located, leave empty if globally accessible" +msgid "Reording Rundowns in Playlist" msgstr "" -"(Valgfri) Et navn/en identifikator for det lokale nettverket hvor den delte " -"mappen er lokalisert, la være tom dersom den er globalt tilgjengelig" -msgid "Folder path to shared folder" -msgstr "Sti til delt mappe" +msgid "Replace" +msgstr "Erstatt" -msgid "UserName" -msgstr "Brukernavn" +msgid "Replace Blueprints?" +msgstr "Erstatte blueprints?" -msgid "Username for athuentication" -msgstr "Brukernavn for autentisering" +msgid "Replace rows" +msgstr "" -msgid "Password for authentication" -msgstr "Passord for autentisering" +msgid "Require All Additional Source Layers" +msgstr "" -msgid "" -"(Optional) A name/identifier of the local network where the share is located" +msgid "Require Piece on Source Layer" msgstr "" -"(Valgfri) Et navn/en identifikator for det lokale nettverket hvor den delte " -"mappen er lokalisert" -msgid "Quantel gateway URL" -msgstr "Quantel Gateway-adresse (url)" +msgid "Reset" +msgstr "" -msgid "URL to the Quantel Gateway" -msgstr "Start Quantel Gateway på nytt" +msgid "Reset Action" +msgstr "" -msgid "ISA URLs" -msgstr "ISA-adresse (url)" +msgid "Reset All Versions" +msgstr "Nullstill alle versjoner" -msgid "URLs to the ISAs, in order of importance (comma separated)" -msgstr "Adresser (url-er) for ISA-ene (kommaseparert i prioritert rekkefølge)" +msgid "Reset and Activate \"On Air\"" +msgstr "" -msgid "Zone ID" -msgstr "Sone-id" +msgid "Reset App Credentials" +msgstr "" -msgid "Zone ID (default value: \"default\")" -msgstr "Sone-id (standardverdi: \"default\")" +msgid "Reset Database Version" +msgstr "Nullstill databaseversjon" -msgid "Server ID" -msgstr "Server-id" +msgid "Reset mapping to default values" +msgstr "" -msgid "Server ID. For sources, this should generally be omitted (or set to 0) so clip-searches are zone-wide. If set, clip-searches are limited to that server." +msgid "Reset Package Container to default values" msgstr "" -"Server-ID. For kilder skal denne droppes (eller bli satt til 0) siden klippsøk skjer i heile sonen. Hvis denne er satt skjer klippsøk bare på den serveren." -msgid "Quantel transformer URL" -msgstr "Quantel Transformer-adresse (url)" +msgid "Reset row to default values" +msgstr "" -msgid "URL to the Quantel HTTP transformer" -msgstr "Adresse til Quantel HTTP transformer" +msgid "Reset Rundown" +msgstr "Tilbakestill kjøreplanen" -msgid "Quantel FileFlow URL" -msgstr "Quantel FileFlow-adresse (url)" +msgid "Reset Sort Order" +msgstr "Tilbakestill rekkefølge" -msgid "URL to the Quantel FileFlow Manager" -msgstr "Adresse til Quantel FileFlow Manager" +msgid "Reset source layer to default values" +msgstr "" -msgid "Quantel FileFlow Profile name" -msgstr "Quantel FileFlow profilnavn" +msgid "Reset this item?" +msgstr "" -msgid "Profile name to be used by FileFlow when exporting the clips" -msgstr "Profilnavn som benyttes av FileFlow når klippene eksporteres" +msgid "Reset this mapping?" +msgstr "" -msgid "Allow Read access" -msgstr "Tillat lesing" +msgid "Reset this Package Container?" +msgstr "" -msgid "Allow Write access" -msgstr "Tillat skriving/lagring" +msgid "Reset to default" +msgstr "" -msgid "Studio Settings" -msgstr "Studioinnstillinger" +msgid "Reset User Credentials" +msgstr "" -msgid "Package Containers to use for previews" -msgstr "Pakkekontainere som skal benyttes til forhåndsvisninger" +msgid "Resetting and activating Rundown Playlist" +msgstr "" -msgid "Click to show available Package Containers" -msgstr "Klikk for å vise tilgjengelige pakkekontainere" +msgid "Resetting Playlist to default order" +msgstr "" -msgid "Package Containers to use for thumbnails" -msgstr "Pakkekontainere som skal benyttes til miniatyrbilder" +msgid "Resetting Rundown Playlist" +msgstr "" -msgid "Package Containers" -msgstr "Pakkekontainere" +msgid "Resource Id" +msgstr "Ressurs-id" -msgid "Studio Baseline needs update: " -msgstr "Studio baseline må oppdateres: " +msgid "Restart" +msgstr "Restart" -msgid "Baseline needs reload, this studio may not work until reloaded" -msgstr "" -"Baseline må lastes på nytt, dette studioet vil kanskje ikke fungere før " -"baseline er lastet på nytt" +msgid "Restart {{device}}" +msgstr "Start {{device}} på nytt" -msgid "Reload Baseline" -msgstr "Last inn baseline på nytt" +msgid "Restart All Jobs" +msgstr "" -msgid "Studio Name" -msgstr "Studionavn" +msgid "Restart CasparCG Server" +msgstr "Restart CasparCG" -msgid "Select Compatible Show Styles" -msgstr "Velg kompatibel showstyles" +msgid "Restart Container" +msgstr "" -msgid "Show style not set" -msgstr "Showstyle ikke satt" +msgid "Restart Device" +msgstr "Start enheten på nytt" -msgid "Click to show available Show Styles" -msgstr "Klikk for å vise tilgjengelige showstyles" +msgid "Restart Playout" +msgstr "Start Playout-gateway på nytt" -msgid "Frame Rate" -msgstr "Framerate" +msgid "Restart this Device?" +msgstr "Start denne enheten på nytt?" -msgid "Enable \"Play from Anywhere\"" -msgstr "Slå på \"Play from Anywhere\"" +msgid "Restart this system?" +msgstr "Starte dette Sofie-systemet på nytt?" -msgid "Media Preview URL" -msgstr "Forhåndsvisnings-URL" +msgid "Restarting Media Workflow" +msgstr "" -msgid "Sofie Host URL" -msgstr "Sofie vertadresse (url)" +msgid "Restarting Sofie Core" +msgstr "" -msgid "Slack Webhook URLs" -msgstr "Slack Webhook-adresser (url)" +msgid "Restore" +msgstr "Gjenopprett" -msgid "Supported Media Formats" -msgstr "Støttede medieformater" +msgid "Restore Deleted Action" +msgstr "" -msgid "Supported Audio Formats" -msgstr "Støttede lydformater" +msgid "Restore from Snapshot File" +msgstr "Gjenopprett fra snapshotfil" -msgid "Force the Multi-gateway-mode" -msgstr "Tving multigateway-modus" +msgid "Restore from Stored Snapshots" +msgstr "Gjenopprett fra lagrede snapshots" -msgid "Multi-gateway-mode delay time" -msgstr "Delaytid for multigateway-modus" +msgid "Restore from this Snapshot file?" +msgstr "Gjenopprette fra denne snapshotfilen?" -msgid "Preserve contents of playing segment when unsynced" +msgid "Restore Part from NRCS" msgstr "" -msgid "Allow Rundowns to be reset while on-air" -msgstr "Tillat tilbakestilling av kjøreplaner som er on-air" +msgid "Restore Segment from NRCS" +msgstr "" -msgid "" -"Preserve position of segments when unsynced relative to other segments. " -"Note: this has only been tested for the iNews gateway" +msgid "Restore Snapshot" msgstr "" -msgid "Remove indexes" -msgstr "Fjern indexer" +msgid "Resync with NRCS" +msgstr "Synkroniser med ENPS" -msgid "This will remove {{indexCount}} old indexes, do you want to continue?" -msgstr "Dette vil fjerne {{indexCount}} gamle indexer. Vil du fortsette?" +msgid "Retry" +msgstr "Prøv igjen" -msgid "{{indexCount}} indexes was removed." -msgstr "{{indexCount}} indexer ble fjernet." +msgid "Return to list" +msgstr "Gå tilbake til listen" -msgid "Installation name" -msgstr "Installasjonsnavn" +msgid "Reveal in Shelf" +msgstr "Vis i skuff" -msgid "This name will be shown in the title bar of the window" -msgstr "Dette navnet vil vises i tittellinjen for vinduet" +msgid "Reverse speed map" +msgstr "" -msgid "Logging level" -msgstr "Loggenivå" +msgid "Reverse speed map (left trigger)" +msgstr "" -msgid "This affects how much is logged to the console on the server" -msgstr "Dette påvirker hvor mye som blir logget til serverkonsollen" +msgid "Rewind all Segments" +msgstr "" -msgid "System-wide Notification Message" -msgstr "Lokal systemmelding" +msgid "Rewind segments to start" +msgstr "Sett segmentene tilbake til start" -msgid "Message" -msgstr "Melding" +msgid "Rewind Segments to start" +msgstr "Sett alle segmenter tilbake til start" -msgid "Enabled" -msgstr "Aktivert" +msgid "Right hand offset" +msgstr "" -msgid "Edit Support Panel" -msgstr "Rediger supportpanel" +msgid "Role" +msgstr "Rolle" -msgid "HTML that will be shown in the Support Panel" -msgstr "HTML-kode som vil bli vist i supportpanelet" +msgid "Route Set" +msgstr "" -msgid "Application Performance Monitoring" -msgstr "Overvåkning av applikasjonsytelse (AMP)" +msgid "Route Set ID" +msgstr "Omkoblingsgruppe-id" -msgid "APM Enabled" -msgstr "AMP aktivert" +msgid "Route Set Name" +msgstr "Omkoblingsgruppens navn" -msgid "APM Transaction Sample Rate" -msgstr "Prøvefrekvens for AMP-transaksjoner" +msgid "Route Sets" +msgstr "Omkoblingsgrupper" + +msgid "Route Type" +msgstr "" -msgid "" -"How many of the transactions to monitor. Set to -1 to log nothing (max " -"performance), 0.5 to log 50% of the transactions, 1 to log all transactions" +msgid "Routed Mappings" msgstr "" -"Antall transaksjoner som overvåkes. Sett verdien til -1 for å ikke logge noe " -"(maks ytelse), til 0.5 for å logge halvparten av transaksjonene eller til 1 " -"for å logge alle transaksjonene" -msgid "Note: Core needs to be restarted to apply these settings" -msgstr "Merknad: Core må startes på nytt for å ta i bruk disse innstillingene" +msgid "Routes" +msgstr "Omkoblinger" -msgid "Monitor blocked thread" +msgid "Row cannot be reset as it has no default values" msgstr "" -msgid "Enable" -msgstr "Aktiver" +msgid "Run automatic migration procedure" +msgstr "Kjør automatisk migreringsprosedyre" + +msgid "Run Migrations to get set up" +msgstr "Kjør migreringsprosedyrer for å sette opp" + +msgid "Rundown" +msgstr "Kjøreplan" msgid "" -"Enables internal monitoring of blocked main thread. Logs when there is an " -"issue, but (unverified) might cause issues in itself." +"Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data " +"from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced " +"or remove the rundown from Sofie. What do you want to do?" msgstr "" +"Kjøreplan {{rundownName}} i listen {{playlistName}} mangler i data fra " +"{{nrcsName}}. Du kan enten markere den som usynkronisert og beholde den i " +"Sofie, eller fjerne kjøreplanen fra Sofie. Hva vil du gjøre?" -msgid "Cron jobs" -msgstr "Cron-jobber" - -msgid "Enable CasparCG restart job" -msgstr "Aktiver CasparCG restartjobber" +msgid "Rundown & Shelf" +msgstr "Kjøreplan & skuff" -msgid "Cleanup" -msgstr "Opprydding" +msgid "Rundown filter" +msgstr "" -msgid "Cleanup old database indexes" -msgstr "Rydd opp i gamle databaseindexer" +msgid "Rundown for piece \"{{pieceLabel}}\" could not be found." +msgstr "Finner ikke kjøreplan for \"{{pieceLabel}}\"." -msgid "Cleanup old data" -msgstr "Rydd opp i gamle data" +msgid "Rundown Global Piece Prepare Time" +msgstr "" -msgid "Disable CasparCG restart job" -msgstr "Deaktiver CasparCG restartjobber" +msgid "Rundown Header Layout" +msgstr "Layout for kjøreplanens topptekst" -msgid "Enable automatic storage of Rundown Playlist snapshots periodically" +msgid "Rundown Header Layouts" msgstr "" -msgid "Filter: If set, only store snapshots for certain rundowns" +msgid "Rundown is already doing a HOLD!" msgstr "" -msgid "" -"(Comma separated list. Empty - will store snapshots of all Rundown Playlists)" +msgid "Rundown must be active!" msgstr "" -msgid "Error when checking for cleaning up" +msgid "Rundown must be playing or have a next!" msgstr "" -msgid "Remove old data from database" -msgstr "Fjern gamle data fra databasen" - -msgid "" -"There are {{count}} documents that can be removed, do you want to continue?" -msgstr "Det er {{count}} dokumenter som kan fjernes. Vil du fortsette?" +msgid "Rundown must be playing!" +msgstr "" -msgid "Documents to be removed:" -msgstr "Dokumenter som fjernes:" +msgid "Rundown Name" +msgstr "" -msgid "Retry" -msgstr "Prøv igjen" +msgid "Rundown not found" +msgstr "Kjøreplan ikke funnet" -msgid "Remove old data" -msgstr "Fjern gamle data" +msgid "" +"Rundown Playlist is active, please deactivate before preparing it for " +"broadcast" +msgstr "" -msgid "The old data was removed." -msgstr "Gamle data ble fjernet." +msgid "Rundown Playlist is active, please deactivate it before regenerating it." +msgstr "" -msgid "Last {{layerName}}" -msgstr "Siste {{layerName}}" +msgid "Rundown Playlist names to store" +msgstr "" -msgid "Clear {{layerName}}" -msgstr "Tøm {{layerName}}" +msgid "Rundown Playlist not found!" +msgstr "" -msgid "Search..." -msgstr "Søk..." +msgid "Rundown View Layouts" +msgstr "" msgid "" -"Are you sure you want to deactivate this Rundown\n" -"(This will clear the outputs)" +"RundownPlaylist is active but not in rehearsal, please deactivate it or set " +"in in rehearsal to be able to reset it." msgstr "" -"Er du sikker på at du vil deaktivere denne kjøreplanen?\n" -"(Dette vil nullstille alle utganger.)" - -msgid "Successfully stored snapshot" -msgstr "Gjenoppretting fra snapshot var vellykket" - -msgid "End Words" -msgstr "Stikkord" -msgid "Global AdLib" -msgstr "Globale adliber" +msgid "Rundowns" +msgstr "Kjøreplaner" -msgid "AdLib does not provide any options" -msgstr "Adlib har ingen valg" +msgid "Save" +msgstr "" -msgid "Execute" -msgstr "Utfør" +msgid "Save Changes" +msgstr "Lagre endringer" msgid "Save to Bucket" msgstr "Lagre til bøtte" -msgid "Reveal in Shelf" -msgstr "Vis i skuff" - -msgid "Edit in Nora" -msgstr "Rediger i Nora" - -msgid "Current Part" -msgstr "Nåværende del" - -msgid "Next Part" -msgstr "Neste del" - -msgid "Part Count Down" -msgstr "Nedtelling for del" +msgid "Saving AdLib to Bucket" +msgstr "" -msgid "Part Count Up" -msgstr "Opptelling for del" +msgid "Saving Evaluation" +msgstr "" -msgid "Until end of rundown" -msgstr "Til slutten av kjøreplanen" +msgid "Scale" +msgstr "Skala" -msgid "New Bucket" -msgstr "Ny bøtte" +msgid "Script is empty" +msgstr "Manuset er tomt" -msgid "Are you sure you want to delete this AdLib?" -msgstr "Er du sikker på at du vil slette denne adliben?" +msgid "Script Source Layers" +msgstr "" -msgid "Are you sure you want to delete this Bucket?" -msgstr "Er du sikker på at du vil slette denne bøtten?" +msgid "Search..." +msgstr "Søk..." -msgid "Are you sure you want to empty (remove all adlibs inside) this Bucket?" -msgstr "Er du sikker på at du vil tømme denne bøtten (fjerner alle adliber)?" +msgid "Seg. Budg." +msgstr "" -msgid "Current Segment" -msgstr "Nåværende tittel" +msgid "segment" +msgstr "" -msgid "Next Segment" -msgstr "Neste tittel" +msgid "Segment" +msgstr "Tittel" msgid "Segment Count Down" msgstr "Nedtelling for tittel" @@ -2885,1318 +3367,1259 @@ msgstr "Nedtelling for tittel" msgid "Segment Count Up" msgstr "Opptelling for tittel" -msgid "Start this AdLib" -msgstr "Slett denne adliben" - -msgid "Queue this AdLib" -msgstr "Cue denne adliben" - -msgid "Inspect this AdLib" -msgstr "Inspiser denne adliben" - -msgid "Rename this AdLib" -msgstr "Gi denne adliben nytt navn" - -msgid "Delete this AdLib" -msgstr "Slett denne adliben" - -msgid "Empty this Bucket" -msgstr "Tøm denne bøtten" - -msgid "Rename this Bucket" -msgstr "Gi bøtten nytt navn" - -msgid "Delete this Bucket" -msgstr "Slett denne bøtten" - -msgid "Create new Bucket" -msgstr "Opprett ny bøtte" - -msgid "AdLib" -msgstr "Adlib" +msgid "Segment countdown requires source layer" +msgstr "Nedtelling for tittel krever kildelag" -msgid "Shortcuts" -msgstr "Hurtigtaster" +msgid "Segment no longer exists in {{nrcs}}" +msgstr "Segmenet eksisterer ikke lenger i {{nrcs}}" -msgid "Show Style Variant" -msgstr "Showstylevariant" +msgid "Segment was hidden in {{nrcs}}" +msgstr "Tittelen eksisterer ikke lenger i {{nrcs}}" -msgid "Local Time" -msgstr "Lokal tid" +msgid "Segments: {{delta}}" +msgstr "Segmenter: {{delta}}" -msgid "System" -msgstr "System" +msgid "" +"Select a presenter layout. Leave as default to use the first available " +"layout." +msgstr "" -msgid "Media" -msgstr "Media" +msgid "Select Action" +msgstr "Velg handling" -msgid "Packages" -msgstr "Pakker" +msgid "Select Compatible Show Styles" +msgstr "Velg kompatibel showstyles" -msgid "Messages" -msgstr "Meldinger" +msgid "Select image" +msgstr "" -msgid "User Log" -msgstr "Brukerlogg" +msgid "" +"Select one or more control modes. Leave all unchecked for default (mouse + " +"keyboard)." +msgstr "" -msgid "Evaluations" -msgstr "Evalueringer" +msgid "" +"Select source layers to display. Leave all unchecked to show all " +"camera-related layers." +msgstr "" -msgid "Timestamp" -msgstr "Tidsstempel" +msgid "Select visible Output Groups" +msgstr "Velg synlig utgangsgruppe" -msgid "User Name" -msgstr "Brukernavn" +msgid "Select visible Source Layers" +msgstr "Velg synlige kildelag" -msgid "Answers" -msgstr "Svar" +msgid "Select which playout devices are using this package container" +msgstr "Velg hvilke playout-enheter som skal benytte denne pakkekontaineren" -msgid "Message Queue" -msgstr "Meldingskø" +msgid "Send message" +msgstr "" -msgid "Queued Messages" -msgstr "Meldinger i kø" +msgid "Send message and Deactivate Rundown" +msgstr "" msgid "Sent Messages" msgstr "Sendte meldinger" -msgid "File Copy" -msgstr "Kopier fil" - -msgid "File Delete" -msgstr "Slett fil" - -msgid "Check file size" -msgstr "Sjekk filstørrelse" - -msgid "Scan File" -msgstr "Scan fil" - -msgid "Generate Thumbnail" -msgstr "Generer miniatyrbilder" - -msgid "Generate Preview" -msgstr "Generer forhåndsvisning" - -msgid "Unknown action: {{action}}" -msgstr "Ukjent handling: {{action}}" +msgid "Server" +msgstr "" -msgid "Done" -msgstr "Utført" +msgid "Server {{id}}" +msgstr "" -msgid "Failed" -msgstr "Mislykket" +msgid "Server ID" +msgstr "Server-id" -msgid "Working, Media Available" -msgstr "Jobber, media er tilgjengelig" +msgid "" +"Server ID. For sources, this should generally be omitted (or set to 0) so " +"clip-searches are zone-wide. If set, clip-searches are limited to that " +"server." +msgstr "" +"Server-ID. For kilder skal denne droppes (eller bli satt til 0) siden " +"klippsøk skjer i heile sonen. Hvis denne er satt skjer klippsøk bare på den " +"serveren." -msgid "Working" -msgstr "Jobber" +msgid "Set" +msgstr "" -msgid "Pending" -msgstr "Venter" +msgid "Set as QuickLoop End" +msgstr "" -msgid "Blocked" -msgstr "Blokkert" +msgid "Set as QuickLoop Start" +msgstr "" -msgid "Canceled" -msgstr "Avbrutt" +msgid "Set In & Out points" +msgstr "" -msgid "Idle" -msgstr "Inaktiv" +msgid "Set part as Next" +msgstr "" -msgid "Skipped" -msgstr "Hoppet over" +msgid "Set segment as Next" +msgstr "Sett tittel som Neste: Starter på neste Take" -msgid "Step progress: {{progress}}" -msgstr "Fremdrift: {{progress}}" +msgid "Setting as QuickLoop End" +msgstr "" -msgid "Processing" -msgstr "Prosesserer" +msgid "Setting as QuickLoop Start" +msgstr "" -msgid "Unknown: {{status}}" -msgstr "Ukjent: {{status}}" +msgid "Setting Next" +msgstr "" -msgid "Collapse" -msgstr "Minimer" +msgid "Setting Next Segment" +msgstr "" -msgid "Details" -msgstr "Detaljer" +msgid "Settings" +msgstr "Innstillinger" -msgid "Abort" -msgstr "Avbryt" +msgid "Shelf" +msgstr "Skuff" -msgid "Prioritize" -msgstr "Prioriter" +msgid "Shelf Layout" +msgstr "Layouter for skuffen" -msgid "Media Transfer Status" -msgstr "Status for medieoverføringer" +msgid "Shelf layout uploaded successfully." +msgstr "Opplastingen av layout for skuff var vellykket." -msgid "Abort All" -msgstr "Avbryt alle" +msgid "Shelf Layouts" +msgstr "" -msgid "Restart All" -msgstr "Start alle på nytt" +msgid "Shortcuts" +msgstr "Hurtigtaster" -msgid "Unknown Package \"{{packageId}}\"" -msgstr "Ukjent pakke \"{{packageId}}\"" +msgid "Show \"Remove snapshots\"-buttons" +msgstr "Vis \"Fjern snapshots\"-knappene" -msgid "Package Status" -msgstr "Pakkestatus" +msgid "Show All" +msgstr "Vis alle" -msgid "Package container status" -msgstr "Status for pakkekontainer" +msgid "Show Breaks as Segments" +msgstr "Vis pauser som titler" -msgid "Id" -msgstr "Id" +msgid "Show config changes" +msgstr "" -msgid "Work status" -msgstr "Jobbstatus" +msgid "Show End" +msgstr "Sendeslutt" -msgid "Restart All jobs" -msgstr "Start alle jobber på nytt" +msgid "Show entire On Air Segment" +msgstr "Vis hele tittelen som er OnAir" -msgid "Created" -msgstr "Opprettet" +msgid "Show Hotkeys" +msgstr "Vise hurtigtaster" -msgid "Ready" -msgstr "Klar" +msgid "Show Inspector" +msgstr "" -msgid "The progress of steps required for playout" -msgstr "Fremdrift for steg som er nødvendige for avspilling" +msgid "Show issue" +msgstr "Vis problem" -msgid "The progress of all steps" -msgstr "Fremdrift for alle steg" +msgid "Show next break timing" +msgstr "Vis tid for neste pause" -msgid "This step is required for playout" -msgstr "Dette steget er nødvendig for avspilling" +msgid "Show panel as a timeline" +msgstr "Vis panel som en tidslinje" -msgid "Work description" -msgstr "Jobbeskrivlese" +msgid "Show part title" +msgstr "Vis delens tittel" -msgid "Work status reason" -msgstr "Årsak for jobbstatus" +msgid "Show Piece Icon Color" +msgstr "" -msgid "Technical reason: {{reason}}" -msgstr "Teknisk årsak: {{reason}}" +msgid "Show Rundown Name" +msgstr "Vis kjøreplannavn" -msgid "Previous work status reasons" -msgstr "Tidligere årsaker for jobbsatus" +msgid "Show segment name" +msgstr "Vis tittelens navn" -msgid "Priority" -msgstr "Prioritet" +msgid "Show Style" +msgstr "Showstyle" -msgid "Not Connected" -msgstr "Ikke tilkoblet" +msgid "Show Style Base Name" +msgstr "Showstylenavn" -msgid "Do you want to restart CasparCG Server?" -msgstr "Er du sikker på at du vil restarte CasparCG?" +msgid "Show style not set" +msgstr "Showstyle ikke satt" -msgid "Restart Quantel Gateway" -msgstr "Start Quantel Gateway på nytt" +msgid "Show Style Variant" +msgstr "Showstylevariant" -msgid "Do you want to restart Quantel Gateway?" -msgstr "Vil du starte Quantel Gateway på nytt?" +msgid "Show Style Variants" +msgstr "" -msgid "Quantel Gateway restarting..." -msgstr "Quantel Gateway starter på nytt..." +msgid "Show Styles" +msgstr "Showstyle" -msgid "Failed to restart Quantel Gateway: {{errorMessage}}" -msgstr "Klarte ikke å restarte Quantel Gateway: {{errorMessage}}" +msgid "Show thumbnails next to list items" +msgstr "Vis miniatyrbilder ved siden av listeelementer" -msgid "Format HyperDeck disks" -msgstr "Formater HyperDeck-disker" +msgid "ShowStyleBase not found!" +msgstr "" -msgid "" -"Do you want to format the HyperDeck disks? This is a destructive action and " -"cannot be undone." +msgid "Shuttle Keyboard (Contour ShuttleXpress / X-keys)" msgstr "" -"Er du sikker på at du vil formatere HyperDeck-diskene? Dette kan ikke angres." -msgid "Formatting HyperDeck disks on device \"{{deviceName}}\"..." -msgstr "Formaterer HyperDeck-disker på \"{{deviceName}}\"..." +msgid "Shuttle WebHID (Contour ShuttleXpress via browser)" +msgstr "" -msgid "" -"Failed to format HyperDecks on device: \"{{deviceName}}\": {{errorMessage}}" +msgid "Skip Fix Up Step" msgstr "" -"Formatering av HyperDeck-disker på \"{{deviceName}}\" feilet: " -"{{errorMessage}}" -msgid "Last seen" -msgstr "Sist sett" +msgid "Slack Webhook URLs" +msgstr "Slack Webhook-adresser (url)" -msgid "Connect some devices to the playout gateway" -msgstr "Koble til en eller flere enheter til playout gatewayen" +msgid "Smooth scrolling" +msgstr "" -msgid "Format disks" -msgstr "Formater disker" +msgid "Snapshot remove failed: {{errorMessage}}" +msgstr "" -msgid "Are you sure you want to delete this device: \"{{deviceId}}\"?" -msgstr "Er du sikker på at du vil fjerne enheten \"{{deviceId}}\"?" +msgid "Snapshot restore failed: {{errorMessage}}" +msgstr "Gjenoppretting fra snapshot feilet: {{errorMessage}}" -msgid "Sofie Automation Server Core: {{name}}" -msgstr "Sofie:" +msgid "Snapshot restored!" +msgstr "" -msgid "Restart this system?" -msgstr "Starte dette Sofie-systemet på nytt?" +msgid "Sofie" +msgstr "" -msgid "" -"Are you sure you want to restart this Sofie Automation Server Core: {{name}}?" -msgstr "Er du sikker på at du vil starte Sofie Core: {{name}} på nytt?" +msgid "Sofie Automation" +msgstr "Sofie" -msgid "Could not generate restart token!" -msgstr "Kunne ikke generere Restart Token!" +msgid "Sofie Automation Server" +msgstr "" -msgid "Could not generate restart core: {{err}}" -msgstr "Kunne ikke generere Restart Core: {{err}}" +msgid "Sofie Automation Server Core" +msgstr "" msgid "Sofie Automation Server Core will restart in {{time}}s..." msgstr "Sofie Core restartes om {{time}}s..." -msgid "Execution times" -msgstr "Kjøretider" - -msgid "User ID" -msgstr "Bruker-id" - -msgid "Client IP" -msgstr "Klient-ip" - -msgid "Method" -msgstr "Metode" - -msgid "Parameters" -msgstr "Parametre" +msgid "Sofie Automation Server Core: {{name}}" +msgstr "Sofie:" -msgid "Time from platform user event to Action received by Core" +msgid "Sofie logo to be displayed in the header. Requires a page refresh." msgstr "" -msgid "GUI" -msgstr "Brukergrensesnitt" - -msgid "Core + Worker processing time" +msgid "some invalid reason" msgstr "" -msgid "Core" +msgid "some message" msgstr "" -msgid "Worker" +msgid "some reason" msgstr "" -msgid "Gateway" +msgid "something changed" msgstr "" -msgid "TSR" +msgid "" +"Something went wrong when creating the snapshot. Please contact the system " +"administrator if the problem persists." msgstr "" -msgid "User Activity Log" -msgstr "Aktivitetslogg" +msgid "Something went wrong, and it affected the output" +msgstr "Noe gikk galt, og det påvirket sendingen" -msgid "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s" -msgstr "om {{days}} dager, {{hours}} t {{minutes}} min {{seconds}} s" +msgid "Something went wrong, but it didn't affect the output" +msgstr "Noe gikk galt, men det påvirket ikke sendingen" -msgid "in {{hours}} h {{minutes}} min {{seconds}} s" -msgstr "om {{hours}} t {{minutes}} min {{seconds}} s" +msgid "" +"Something went wrong, please contact the system administrator if the " +"problem persists." +msgstr "Noe gikk galt, kontakt systemadministrator hvis problemet fortsetter." -msgid "in {{minutes}} min {{seconds}} s" -msgstr "om {{minutes}} min {{seconds}} s" +msgid "Source Abbreviation" +msgstr "Kildeforkortelse" -msgid "in {{seconds}} s" -msgstr "om {{seconds}} s" +msgid "Source Layer" +msgstr "Kildelag" -msgid "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago" -msgstr "for {{days}} dager, {{hours}} t {{minutes}} min {{seconds}} s siden" +msgid "Source layer cannot be reset as it has no default values" +msgstr "" -msgid "{{hours}} h {{minutes}} min {{seconds}} s ago" -msgstr "for {{hours}} t {{minutes}} min {{seconds}} s siden" +msgid "Source Layer Type" +msgstr "Kildelagstyper" -msgid "{{minutes}} min {{seconds}} s ago" -msgstr "for {{minutes}} min {{seconds}} s siden" +msgid "Source Layer Types" +msgstr "Kildelagstyper" -msgid "{{seconds}} s ago" -msgstr "for {{seconds}} s siden" +msgid "Source Layers" +msgstr "Kildelag" -msgid "Next scheduled show" -msgstr "Neste planlagte sending" +msgid "Source layers containing script" +msgstr "" -msgid "Help & Support" -msgstr "Hjelp og brukerstøtte" +msgid "Source Name" +msgstr "Kildenavn" -msgid "Disable hints by adding this to the URL:" -msgstr "Deaktiver hint ved å legge dette til på url-en:" +msgid "Source Type" +msgstr "Kildetype" -msgid "Enable hints by adding this to the URL:" -msgstr "Aktiver hint ved å legge dette til på url-en:" +msgid "Source/Output Layers" +msgstr "" -msgid "More documentation available at:" -msgstr "Mer dokumentasjon er tilgjengelig på:" +msgid "Sources" +msgstr "" -msgid "Timeline" -msgstr "Tidslinje" +msgid "Space separated list of style class names to use when displaying the action" +msgstr "" -msgid "Mappings" -msgstr "Lagmappinger" +msgid "Specify additional layers where at least one layer must have an active piece" +msgstr "" -msgid "User Log Player" -msgstr "Brukerloggspiller" +msgid "Speed control" +msgstr "" -msgid "Routed Mappings" +msgid "Speed map" msgstr "" -msgid "Play from here" -msgstr "Spill av herfra" +msgid "Speed map (forward, right trigger)" +msgstr "" -msgid "Exectute Single" -msgstr "Utfør enslig handling" +msgid "Split Screen" +msgstr "Splitt" -msgid "Next Action" -msgstr "Neste handling" +msgid "Splits" +msgstr "" -msgid "Run in" -msgstr "Kjør i" +msgid "Standalone Shelf" +msgstr "Frittstående skuff" -msgid "Stop" -msgstr "Stopp" +msgid "Start Here!" +msgstr "Start her!" -msgid "" -"Clip \"{{fileName}}\" can't be played because it doesn't exist on the " -"playout system" +msgid "Start In" msgstr "" -"Klippet \"{{fileName}}\" kan ikke spilles av fordi det ikke finnes på " -"utspillingssystemet" -msgid "{{sourceLayer}} is not yet ready on the playout system" -msgstr "" -"{{sourceLayer}} er ennå ikke klar til å spilles ut fra avviklingsserver" +msgid "Start this AdLib" +msgstr "Slett denne adliben" -msgid "{{sourceLayer}} is transferring to the playout system" -msgstr "{{sourceLayer}} overføres til avviklingsserver" +msgid "Start time is close" +msgstr "Oppgitt sendestart er hvert øyeblikk" msgid "" -"{{sourceLayer}} is transferring to the playout system and cannot be " -"played yet" -msgstr "" -"{{sourceLayer}} overføres til avviklingsserver og kan ikke spilles av ennå" +"Start with giving this browser configuration permissions by adding this to " +"the URL: " +msgstr "Først må du gå i konfigurasjonsmodus ved å legge dette til url-en: " -msgid "{{sourceLayer}} doesn't have both audio & video" -msgstr "{{sourceLayer}} har ikke lyd og/eller bilde" +msgid "Started" +msgstr "Startet" -msgid "{{sourceLayer}} has the wrong format: {{format}}" -msgstr "{{sourceLayer}}-formatet er ikke støttet: {{format}}" +msgid "Starting AdLib" +msgstr "" -msgid "{{sourceLayer}} has {{audioStreams}} audio streams" -msgstr "{{sourceLayer}} har {{audioStreams}} lydstrømmer" +msgid "Starting Bucket AdLib" +msgstr "" -msgid "Clip starts with {{frames}} {{type}} frames" -msgstr "Klippet starter med {{frames}} {{type}} ruter" +msgid "Starting Global AdLib" +msgstr "" -msgid "This clip ends with {{type}} frames after {{count}} seconds" -msgstr "Klippet slutter med {{frames}} {{type}} frame" +msgid "Starting Sticky Piece" +msgstr "" -msgid "{{frames}} {{type}} frames detected within the clip" -msgstr "{{frames}} {{type}} frame oppdaget inne i klippet" +msgid "State" +msgstr "Tilstand" -msgid "{{frames}} {{type}} frames detected in the clip" -msgstr "{{frames}} {{type}} frame oppdaget inne i klippet" +msgid "State \"{{state}}\"" +msgstr "" -msgid "black" -msgstr "svart(e)" +msgid "Statistics" +msgstr "Statistikk" -msgid "freeze" -msgstr "fryst(e)" +msgid "Status" +msgstr "Status" -msgid "{{sourceLayer}} is missing a file path" -msgstr "{{sourceLayer}} kan ikke spilles av fordi filnavnet mangler" +msgid "Status Messages:" +msgstr "Statusmeldinger:" -msgid "Clip doesn't have audio & video" -msgstr "Klippet har ikke lyd og/eller bilde" +msgid "Sticky Piece" +msgstr "Element er sticky" -msgid "Clip starts with {{frames}} {{type}} frame" -msgstr "Klippet starter med {{frames}} {{type}} frame" +msgid "Store Snapshot" +msgstr "Lagre snapshot" -msgid "This clip ends with {{type}} frames after {{count}} second" -msgstr "Klippet slutter med {{frames}} {{type}} frame" +msgid "Studio" +msgstr "Studio" -msgid "{{frames}} {{type}} frame detected within the clip" -msgstr "{{frames}} {{type}} frame oppdaget inne i klippet" +msgid "Studio Baseline needs update: " +msgstr "Studio baseline må oppdateres: " -msgid "{{frames}} {{type}} frame detected in clip" -msgstr "{{frames}} {{type}} frame oppdaget i klippet" +msgid "Studio Labels" +msgstr "" -msgid "{{sourceLayer}} is being ingested" -msgstr "{{sourceLayer}} blir prosessert" +msgid "Studio Name" +msgstr "Studionavn" -msgid "Source is missing" -msgstr "Kilde mangler" +msgid "Studio not found!" +msgstr "" -msgid "Segment no longer exists in {{nrcs}}" -msgstr "Segmenet eksisterer ikke lenger i {{nrcs}}" +msgid "Studio Screen Graphics" +msgstr "" -msgid "Segment was hidden in {{nrcs}}" -msgstr "Tittelen eksisterer ikke lenger i {{nrcs}}" +msgid "Studio Settings" +msgstr "Studioinnstillinger" -msgid "The following parts no longer exist in {{nrcs}}: {{partNames}}" -msgstr "De følgende delene eksisterer ikke lenger i {{nrcs}}: {{partNames}}" +msgid "Studio Snapshot" +msgstr "Studiosnapshot" -msgid "Toggle Shelf" -msgstr "Skuff" +msgid "Studios" +msgstr "Studio" -msgid "Undo Hold" -msgstr "Angre hold" +msgid "Style class names" +msgstr "" -msgid "Disable the next element" -msgstr "Skip neste super" +msgid "Subtract" +msgstr "" -msgid "Undo Disable the next element" -msgstr "Unskip neste super" +msgid "Successfully restored snapshot" +msgstr "Gjenoppretting fra snapshot var vellykket" -msgid "Move Next forwards" -msgstr "Skip neste" +msgid "Successfully stored snapshot" +msgstr "Gjenoppretting fra snapshot var vellykket" -msgid "Move Next to the following segment" -msgstr "Skip til neste segment" +msgid "Support Panel" +msgstr "" -msgid "Move Next backwards" -msgstr "Unskip neste" +msgid "Supported Audio Formats" +msgstr "Støttede lydformater" -msgid "Move Next to the previous segment" -msgstr "Unskip neste segment" +msgid "Supported Media Formats" +msgstr "Støttede medieformater" -msgid "Rewind segments to start" -msgstr "Sett segmentene tilbake til start" +msgid "Switch Route Set" +msgstr "" -msgctxt "°°°°°°plural" -msgid "{{count}} rows°°°°°°" -msgstr "{{count}} rader°°°°°°" +msgid "Switch Segment View Mode" +msgstr "" -msgctxt "°°°°°°plural" -msgid "This layer is now rerouted by an active Route Set: {{routeSets}}°°°°°°" +msgid "Switch to List View" msgstr "" -"Dette laget blir omkoblet av flere aktive omkoblingsgrupper: {{routeSets}}°°°" -"°°°" -msgctxt "°°°°°°plural" -msgid "" -"There are {{count}} documents that can be removed, do you want to continue?°°" -"°°°°" +msgid "Switch to Storyboard View" msgstr "" -"Det er {{count}} dokumenter i {{collections}} som kan fjernes. Vil du " -"fortsette?°°°°°°" -#, fuzzy -#| msgctxt "°°°°°°plural" -#| msgid "Clip starts with {{frames}} {{type}} frame°°°°°°" -msgctxt "°°°°°°plural" -msgid "Clip starts with {{frames}} {{type}} frames°°°°°°" -msgstr "Klippet starter med {{frames}} {{type}} frame°°°°°°" +msgid "Switch to Timeline View" +msgstr "" -#, fuzzy -#| msgctxt "°°°°°°plural" -#| msgid "This clip ends with {{type}} frames after {{count}} second°°°°°°" -msgctxt "°°°°°°plural" -msgid "This clip ends with {{type}} frames after {{count}} seconds°°°°°°" -msgstr "Klippet slutter med {{frames}} {{type}} frame°°°°°°" +msgid "Switchboard" +msgstr "Sentralbord" -#, fuzzy -#| msgctxt "°°°°°°plural" -#| msgid "{{frames}} {{type}} frame detected within the clip°°°°°°" -msgctxt "°°°°°°plural" -msgid "{{frames}} {{type}} frames detected within the clip°°°°°°" -msgstr "{{frames}} {{type}} frames oppdaget inne i klippet°°°°°°" +msgid "Switchboard Panel" +msgstr "" -#, fuzzy -#| msgctxt "°°°°°°plural" -#| msgid "{{frames}} {{type}} frame detected within the clip°°°°°°" -msgctxt "°°°°°°plural" -msgid "{{frames}} {{type}} frames detected in the clip°°°°°°" -msgstr "{{frames}} {{type}} frames oppdaget inne i klippet°°°°°°" +msgid "Switching operating mode to {{mode}}" +msgstr "Bytt til {{mode}}" -msgctxt "°°°°°°plural" -msgid "Clip starts with {{frames}} {{type}} frame°°°°°°" -msgstr "Klippet starter med {{frames}} {{type}} frame°°°°°°" +msgid "Switching routing" +msgstr "" -msgctxt "°°°°°°plural" -msgid "This clip ends with {{type}} frames after {{count}} second°°°°°°" -msgstr "Klippet slutter med {{frames}} {{type}} frame°°°°°°" +msgid "System" +msgstr "System" -msgctxt "°°°°°°plural" -msgid "{{frames}} {{type}} frame detected within the clip°°°°°°" -msgstr "{{frames}} {{type}} frames oppdaget inne i klippet°°°°°°" +msgid "System has issues which need to be resolved" +msgstr "Systemet har problemer som må fikses" -msgctxt "°°°°°°plural" -msgid "{{frames}} {{type}} frame detected in clip°°°°°°" -msgstr "{{frames}} {{type}} frames oppdaget i klippet°°°°°°" +msgid "System must have exactly one studio" +msgstr "" -#~ msgid "Welcome to the Sofie Automation system" -#~ msgstr "Velkommen til Sofie" +msgid "System Status" +msgstr "Systemstatus" -#~ msgid "Sofie Automation version" -#~ msgstr "Sofie-versjon" +msgid "System-wide" +msgstr "Systemvid" -#~ msgid "Sofie status" -#~ msgstr "Sofie-status" +msgid "System-wide Notification Message" +msgstr "Lokal systemmelding" -#~ msgid "Using local Sofie order" -#~ msgstr "Rekkefølge satt i Sofie" +msgid "Table is not allowed to have `properties` defined" +msgstr "" -#~ msgid "Change order in playlist to override" -#~ msgstr "Gjør endringer for å overstyre" +msgid "Table is only allowed the wildcard `patternProperties`" +msgstr "" -#~ msgid "Register Shortcuts for this Panel" -#~ msgstr "Legg til snarveier for dette panelet" +msgid "Tables are not supported here" +msgstr "" -#~ msgid "Cannot play this AdLib becasue it is marked as Invalid" -#~ msgstr "Kan ikke spille av adlib fordi den er markert som ugyldig" +msgid "Tag" +msgstr "Tag" -#~ msgid "Cannot play this AdLib becasue it is marked as Floated" -#~ msgstr "Kan ikke spille av adlib fordi det er markert som på vent (float)" +msgid "Tags must contain" +msgstr "Tagger må inneholde" -#~ msgid "" -#~ "The Rundown was attempted to be moved out of the Playlist when it was on " -#~ "Air. Move it back and try again later." -#~ msgstr "" -#~ "Kjøreplanen ble forsøkt flyttet ut av spillelisten mens den var OnAir. " -#~ "Flytt den tilbake og prøv igjen senere." +msgid "Take" +msgstr "Take" -#~ msgid "Next Break" -#~ msgstr "Neste pause" +msgid "Take a Full System Snapshot" +msgstr "Lagre et fullt systemsnapshot" -#~ msgid "Using {{nrcsName}} order" -#~ msgstr "Rekkefølge fra {{nrcsName}}" +msgid "Take a Snapshot" +msgstr "Lagre et snapshot" -#~ msgid "Log Error" -#~ msgstr "Loggfør feil" +msgid "Take a Snapshot for studio \"{{studioName}}\" only" +msgstr "Lagre et studiosnapshot utelukkende for \"{{studioName}}\"" -#~ msgid "Shortcut List" -#~ msgstr "Liste over huritgtaster" +msgid "Take and Download Memory Heap Snapshot" +msgstr "" -#~ msgid "Clear Shortcut" -#~ msgstr "Fjern hurtigtast" +msgid "Take System Snapshot" +msgstr "" -#~ msgid "Assign Hotkeys to Global AdLibs" -#~ msgstr "Tildel hurtigtaster til globale adliber" +msgid "Take System Snapshot failed: {{errorMessage}}" +msgstr "" -#~ msgid "Activate Sticky Piece Shortcut" -#~ msgstr "Aktiver hurtigtast for sticky element" +msgid "Taking Piece" +msgstr "" -#~ msgid "Escape from filter search" -#~ msgstr "Gå ut av filtersøk" +msgid "Technical reason: {{reason}}" +msgstr "Teknisk årsak: {{reason}}" -#~ msgid "Info" -#~ msgstr "Info" +msgid "test" +msgstr "" -#~ msgid "File name" -#~ msgstr "Filnavn" +msgid "Test ERROR message" +msgstr "" -#~ msgid "Last Updated" -#~ msgstr "Sist oppdatert" +msgid "Test Info message" +msgstr "" -#~ msgid "Invalid" -#~ msgstr "Ugyldig" +msgid "test partInstance" +msgstr "" -#~ msgid "New Tab" -#~ msgstr "Ny fane" +msgid "test pieceInstance" +msgstr "" -#~ msgid "New Panel" -#~ msgstr "Nytt panel" +msgid "test playlist" +msgstr "" -#~ msgid "Expose as a layout for the shelf" -#~ msgstr "Eksponer som en layout for skuffen" +msgid "test rundown" +msgstr "" -#~ msgid "Tabs" -#~ msgstr "Faner" +msgid "Test test" +msgstr "Test test" -#~ msgid "Panels" -#~ msgstr "Paneler" +msgid "Test Tools" +msgstr "Testverktøy" -#~ msgid "Add tab" -#~ msgstr "Legg til fane" +msgid "test2" +msgstr "" -#~ msgid "Add panel" -#~ msgstr "Legg til panel" +msgid "Text" +msgstr "Tekst" -#~ msgid "Shelf Layouts" -#~ msgstr "Layouter for skuffen" +msgid "Text to show above countdown to end of show" +msgstr "Tekst som vises over nedtelling til forventet slutt" -#~ msgid "Show style" -#~ msgstr "Showstyle" +msgid "Text to show above countdown to next break" +msgstr "Tekst som vises over nedtelling til neste pause" -#~ msgid "Clip is being ingested" -#~ msgstr "Klippet blir prosessert" +msgid "Text to show above show end time" +msgstr "Tekst som vises over klokkeslett for sendeslutt" -#~ msgid "Clip can't be played because the filename is missing" -#~ msgstr "Klippet kan ikke spilles av fordi filnavnet mangler" +msgid "Text to show above show start time" +msgstr "Tekst som vises over klokkeslett for sendestart" -#~ msgid "Clip format ({{format}}) is not in one of the accepted formats" -#~ msgstr "Klippformatet ({{format}}) er ikke støttet" +msgid "" +"The config UI is now driven by manifests fed by the device. This device " +"needs updating to provide the configManifest to be configurable" +msgstr "" +"Brukergrensesnittet for konfigurasjon drives nå av manifester matet fra " +"enhetene. Denne enheten må oppdateres for å gjøre configManifest " +"konfigurerbart" -#~ msgid "Activate" -#~ msgstr "Aktiver" +msgid "The following parts no longer exist in {{nrcs}}: {{partNames}}" +msgstr "De følgende delene eksisterer ikke lenger i {{nrcs}}: {{partNames}}" -#, fuzzy -#~| msgid "Mark the rundown as unsynced" -#~ msgid "Leave it in Sofie (mark the rundown as unsynced)" -#~ msgstr "Marker kjøreplan som usynkronisert" +msgid "The migration can be completed automatically." +msgstr "Migreringen kan gjennomføres automatisk." -#, fuzzy -#~| msgid "Remove just the rundown" -#~ msgid "Remove just the rundown from Sofie" -#~ msgstr "Fjern bare kjøreplan" +msgid "" +"The migration consists of several phases, you will get more options after " +"you've this migration" +msgstr "" +"Migreringen består av flere faser, du vil få flere valg etter at du har " +"kjørt denne migreringen" -#~ msgid "CasparCG Channel" -#~ msgstr "CasparCG-kanal" +msgid "The migration was completed successfully!" +msgstr "Migreringen var vellykket!" -#~ msgid "The CasparCG channel to use (1 is the first)" -#~ msgstr "CasparCG-kanalen som skal brukes (1 er den første)" +msgid "The old data was removed." +msgstr "Gamle data ble fjernet." -#~ msgid "CasparCG Layer" -#~ msgstr "CasparCG-lag" +msgid "" +"The planned end time has passed, are you sure you want to activate this " +"Rundown?" +msgstr "" +"Det planlagte sluttidspunktet er passert, er du sikker på at du vil " +"aktivere denne kjøreplan?" -#~ msgid "The layer in a channel to use" -#~ msgstr "Kanallaget som skal brukes" +msgid "The progress of all steps" +msgstr "Fremdrift for alle steg" -#~ msgid "Preview when not on air" -#~ msgstr "Vis preview når ikke OnAir" +msgid "The progress of steps required for playout" +msgstr "Fremdrift for steg som er nødvendige for avspilling" -#~ msgid "Mapping type" -#~ msgstr "Mappingtype" +msgid "" +"The rundown \"{{rundownName}}\" is not published or activated in " +"{{nrcsName}}! No data updates will currently come through." +msgstr "" +"Kjøreplanen \"{{rundownName}}\" er ikke synkronisert med MOS/{{nrcsName}}! " +"Kontroller at den er satt MOS Active i ENPS." -#~ msgid "Index" -#~ msgstr "Index" +msgid "The rundown can not be reset while it is active" +msgstr "En aktivert kjøreplan kan ikke tilbakestilles" -#~ msgid "Identifier" -#~ msgstr "Identifikator" +msgid "" +"The Rundown was attempted to be moved out of the Playlist when it was on " +"Air. Move it back and try again later." +msgstr "" -#~ msgid "Sisyfos Channel" -#~ msgstr "Sisyfos-kanal" +msgid "" +"The rundown you are trying to execute a take on is inactive, would you like " +"to activate this rundown?" +msgstr "" +"Du prøve å gjøre en Take i en inaktiv kjøreplan. Vil du aktivere denne " +"kjøreplanen?" -#~ msgid "Quantel Port ID" -#~ msgstr "Quantel port-id" +msgid "" +"The rundown: \"{{rundownName}}\" will need to be deactivated in order to " +"activate this one.\n" +"\n" +"Are you sure you want to activate this one anyway?" +msgstr "" -#~ msgid "The name you'd like the port to have" -#~ msgstr "Navnet du vil gi porten" +msgid "" +"The segment duration in the segment header always displays the planned " +"duration instead of acting as a counter" +msgstr "" +"Tittelens varighet i tittelheaderen vil alltid vise den planlagte " +"varigheten i stedet for å telle ned" -#~ msgid "Quantel Channel ID" -#~ msgstr "Quantel kanal-id" +msgid "The selected part cannot be played" +msgstr "" -#~ msgid "The channel to use for output (0 is the first one)" -#~ msgstr "Kanalen som skal brukes som utgang (0 er den første)" +msgid "The selected part does not exist" +msgstr "" -#~ msgid "Mode" -#~ msgstr "Modus" +msgid "" +"The system configuration has been changed since importing this rundown. It " +"might not run correctly" +msgstr "" +"Systemoppsettet har blitt endret etter at denne kjøreplanen ble importert. " +"Kjøreplanen kan spilles av med feil" -#~ msgid "Preset" -#~ msgstr "Preset" +msgid "The type of device to use for the output" +msgstr "Enhetstype som skal brukes for utgangen" -#~ msgid "Preset Transition Speed" -#~ msgstr "Preset transtition-hastighet" +msgid "The type of mapping to use" +msgstr "" -#~ msgid "Zoom Speed" -#~ msgstr "Zoom-hastighet" +msgid "The way this Route Set should behave towards the user" +msgstr "Måten denne omkoblingsgruppen skal oppføre seg på overfor brukeren" -#~ msgid "Unknown Mapping" -#~ msgstr "Ukjent lag" +msgid "Then, run the migrations script:" +msgstr "Kjør så migreringsprosedyren:" -#~ msgid "Channel: {{channel}}" -#~ msgstr "Kanal: {{channel}}" +msgid "There are no AB Playout devices set up yet" +msgstr "" -#~ msgid "Port: {{port}}, Channel: {{channel}}" -#~ msgstr "Port: {{port}}, Kanal: {{channel}}" +msgid "There are no Accessors set up." +msgstr "Ingen aksessorer er satt opp." -#~ msgid "Unknown device type: {{device}}" -#~ msgstr "Ukjent enhetstype: {{device}}" +msgid "There are no active rundowns." +msgstr "Fant ingen aktive kjøreplaner." -#~ msgid "Delete this RundownPlaylist?" -#~ msgstr "Slett denne kjøreplanlisten?" +msgid "There are no exclusivity groups set up." +msgstr "Ingen eksklusivitetsgrupper er satt opp." -#~ msgid "Are you sure you want to delete the \"{{name}}\" RundownPlaylist?" -#~ msgstr "Er du sikker på at du vil slette kjøreplanlisten \"{{name}}\"?" +msgid "There are no filters set up yet" +msgstr "Det er ikke satt opp noen filtre ennå" -#~ msgid "Re-Sync this rundownPlaylist?" -#~ msgstr "Synkroniser kjøreplanlisten med ENPS på nytt?" +msgid "" +"There are no Playout Gateways connected and attached to this studio. Please " +"contact the system administrator to start the Playout Gateway." +msgstr "" +"Dette studioet har ingen tilkoblede playout-gatewayer. Kontakt " +"systemadministrator for å starte den." -#~ msgid "" -#~ "Are you sure you want to re-sync all rundowns in playlist \"{{name}}\"?" -#~ msgstr "" -#~ "Er du sikker på at du vil synkronisere alle kjøreplanene i " -#~ "kjøreplanlisten \"{{name}}\" på nytt?" +msgid "There are no Route Sets set up." +msgstr "Det er ikke satt opp omkoblinger ennå." -#~ msgid "Timeline views" -#~ msgstr "Tidslinjevisninger" +msgid "There are no routes set up yet" +msgstr "Det er ikke satt opp omkoblinger ennå" -#~ msgid "Multiple ({{count}})" -#~ msgstr "Flere ({{count}})" +msgid "There are no rundowns ingested into Sofie." +msgstr "Det er ikke sendt kjøreplaner til Sofie." -#~ msgid "Re-sync all rundowns in playlist" -#~ msgstr "Synkroniser alle kjøreplanene i listen på nytt" +msgid "There are no sub-devices for this gateway" +msgstr "" -#~ msgid "" -#~ "Do you really want to remove the rundownPlaylist \"{{rundownName}}\"? " -#~ "This cannot be undone!" -#~ msgstr "" -#~ "Er du sikker på at du bare vil slette kjøreplanen {{rundownName}} i " -#~ "listen {{playlistName}} fra Sofie? Dette kan ikke angres!" +msgid "There is an unknown problem with the part." +msgstr "Det er et ukjent problem med denne delen." -#~ msgid "Cue as Next" -#~ msgstr "Sett som neste" +msgid "There is an unspecified problem with the source." +msgstr "Det er et ikke-spesifisert problem med kilden." -#~ msgid "Edit" -#~ msgstr "Endre" +msgid "There is no rundown active in this studio." +msgstr "Fant ingen aktive kjøreplaner for dette studioet." -#~ msgctxt "°°°°°°plural" -#~ msgid "Multiple ({{count}})°°°°°°" -#~ msgstr "Flere ({{count}})°°°°°°" +msgid "" +"There was an error when troubleshooting the device: \"{{deviceName}}\": " +"{{errorMessage}}" +msgstr "" +"Det skjedde en feil under feilsøking av enhenten \"{{deviceName}}\": " +"{{errorMessage}}" -#~ msgid "Add rundowns by connecting a gateway." -#~ msgstr "Legg til kjøreplaner ved å koble til en gateway." +msgid "There was an error: {{error}}" +msgstr "Det skjedde en feil: {{error}}" -#~ msgid "Check system status messages." -#~ msgstr "Kontroller systemstatusmeldinger." +msgid "This action has an invalid combination of filters" +msgstr "Denne handlingen har en ugyldig kombinasjon av filtre" -#~ msgid "Air Status" -#~ msgstr "Sendingsstatus" +msgid "This affects how much is logged to the console on the server" +msgstr "Dette påvirker hvor mye som blir logget til serverkonsollen" -#~ msgid "Sofie Automation Server" -#~ msgstr "Sofie" +msgid "This blueprint has not provided a valid config schema" +msgstr "" -#~ msgid "Connecting to the {{platformName}}" -#~ msgstr "Kobler til {{platformName}}" +msgid "This Blueprint is not being used by any Show Style" +msgstr "Dette blueprintet er ikke i bruk av noen showstyles" -#~ msgid "Cannot connect to the {{platformName}}: {{reason}}" -#~ msgstr "Kan ikke koble til Sofie-serveren: {{reason}}" +msgid "This Blueprint is not compatible with any Studio" +msgstr "Dette blueprintet er ikke kompatibel med noe studio" -#~ msgid "Reconnecting to the {{platformName}}" -#~ msgstr "Kobler til {{platformName}} på nytt" +msgid "This clip ends with black frames after {{seconds}} seconds" +msgstr "" -#~ msgid "Your machine is offline and cannot connect to the {{platformName}}." -#~ msgstr "Din maskin er frakoblet og kan ikke koble til {{platformName}}." +msgid "This clip ends with freeze frames after {{seconds}} seconds" +msgstr "" -#~ msgid "Connected to the {{platformName}}." -#~ msgstr "Tilkoblet til {{platformName}}." +msgid "This could leave the configuration in a broken state" +msgstr "" -#~ msgid "Reconnect now" -#~ msgstr "Koble til på nytt" +msgid "This enables or disables buckets in the UI - enabled is the default behavior" +msgstr "" -#, fuzzy -#~ msgid "Switching route" -#~ msgstr "Omkoblinger" +msgid "" +"This enables or disables the evaluationform in the UI - enabled is the " +"default behavior" +msgstr "" -#, fuzzy -#~ msgid "Are you sure you want to enable this route: \"{{routeName}}\"?" -#~ msgstr "" -#~ "Er du sikker på at du vil aktivere denne omkoblingen: \"{{routeName}}\"?" +msgid "This feature enables the use of the Properties Panel and the Edit Mode" +msgstr "" -#, fuzzy -#~ msgid "Are you sure you want to disable this route: \"{{routeName}}\"?" -#~ msgstr "" -#~ "Er du sikker på at du vil deaktivere denne omkoblingen: \"{{routeName}}\"?" +msgid "This has only been tested for the iNews gateway" +msgstr "" -#~ msgid "Source Layer ID" -#~ msgstr "Opprinnelig lag-id" +msgid "This is not in it's normal setting" +msgstr "Endret fra standardoppsett" -#~ msgid "Test Tools – Recordings" -#~ msgstr "Testverktøy - Opptak" +msgid "" +"This migration consists of {{stepCount}} steps ({{ignoredStepCount}} steps " +"are ignored)." +msgstr "" -#~ msgid "Path Prefix" -#~ msgstr "Stiprefiks" +msgid "This must be assigned to a device to be able to edit the settings" +msgstr "" -#~ msgid "URL Prefix" -#~ msgstr "Adresseprefiks (url)" +msgid "This name will be shown in the title bar of the window" +msgstr "Dette navnet vil vises i tittellinjen for vinduet" -#~ msgid "Decklink Input Index" -#~ msgstr "Decklink-inngang" +msgid "This playlist is empty" +msgstr "Denne spillelisten er tom" -#~ msgid "Decklink Input Format" -#~ msgstr "Format for Decklink-inngang" +msgid "" +"This requires the blueprints to implement the " +"`generateAdlibTestingIngestRundown` method" +msgstr "" -#~ msgid "Recordings" -#~ msgstr "Opptak" +msgid "This rundown has been unpublished from Sofie." +msgstr "Denne kjøreplanen er ikke lenger tilgjengelig i Sofie." -#~ msgid "A required setting is not configured" -#~ msgstr "En obligatorisk innstilling er ikke konfigurert" +msgid "This rundown is currently active" +msgstr "Denne kjøreplanen er allerede aktiv" -#~ msgid "Are you sure you want to delete recording \"{{name}}\"?" -#~ msgstr "Er du sikker på at du vil slette opptaket \"{{name}}\"?" +msgid "This rundown is now active. Are you sure you want to exit this screen?" +msgstr "Denne kjøreplanen er aktiv. Er du sikker på at du vil avslutte?" -#~ msgid "Stopped" -#~ msgstr "Stoppet" +msgid "This rundown will loop indefinitely" +msgstr "Denne kjøreplanen vil gå i en uendelig loop" -#~ msgid "Your browser does not support video playback" -#~ msgstr "Nettleseren støtter ikke videoavspilling" +msgid "This Show Style is not compatible with any Studio" +msgstr "Denne showstylen er ikke kompatibelt med noe studio" -#~ msgid "Recording still in progress" -#~ msgstr "Opptak pågår fremdeles" +msgid "This step is required for playout" +msgstr "Dette steget er nødvendig for avspilling" -#~ msgid "MOS Connection" -#~ msgstr "MOS-tilkobling" +msgid "This studio doesn't exist." +msgstr "Dette studioet finnes ikke." -#~ msgid "Infinite" -#~ msgstr "Manuell" +msgid "This will remove {{indexCount}} old indexes, do you want to continue?" +msgstr "Dette vil fjerne {{indexCount}} gamle indexer. Vil du fortsette?" -#~ msgid "File Name" -#~ msgstr "Filnavn" +msgid "Time from platform user event to Action received by Core" +msgstr "" -#~ msgid "System Messages" -#~ msgstr "Systemmeldinger" +msgid "Time since planned end" +msgstr "Tid siden planlagt slutt" -#~ msgid "" -#~ "Are you sure you want to delete this runtime argument \"{{property}}: " -#~ "{{value}}\"?" -#~ msgstr "" -#~ "Er du sikker på at du vil slette runtimeargumentet \"{{property}}: " -#~ "{{value}}\"?" +msgid "Time since rehearsal end" +msgstr "" -#~ msgid "Property" -#~ msgstr "Egenskap" +msgid "Time to planned end" +msgstr "Tid til planlagt slutt" -#~ msgid "Runtime Arguments for Blueprints" -#~ msgstr "Runtimeargumenter for blueprints" +msgid "Time to planned start" +msgstr "" -#~ msgid "Source is not set" -#~ msgstr "Kilde er ikke definert" +msgid "Time to rehearsal end" +msgstr "" -#~ msgid "{{displayName}}: {{messages}}" -#~ msgstr "{{displayName}}: {{messages}}" +msgid "Timeline" +msgstr "Tidslinje" -#~ msgid "Params" -#~ msgstr "Parametre" +msgid "Timeline Datastore" +msgstr "" -#~ msgid "Make ready commands" -#~ msgstr "Klargjøringskommandoer" +msgid "Times" +msgstr "Tider" -#~ msgid "Remove this command?" -#~ msgstr "Fjern denne kommandoen?" +msgid "Timestamp" +msgstr "Tidsstempel" -#~ msgid "Are you sure you want to remove this command?" -#~ msgstr "Er du sikker på at du vil fjerne denne kommandoen?" +msgid "To inspect the memory heap snapshot, use Chrome DevTools" +msgstr "" -#~ msgid "Queue" -#~ msgstr "Kø" +msgid "Today" +msgstr "I dag" -#~ msgid "Hosts" -#~ msgstr "Verter" +msgid "Toggle" +msgstr "Veklse" -#~ msgid "iNews Queues" -#~ msgstr "iNews-køer" +msgid "Toggle AdLibs on single mouse click" +msgstr "Veksle mellom adliber med enkelt museklikk" -#~ msgid "User" -#~ msgstr "Bruker" +msgid "Toggle Shelf" +msgstr "Skuff" -#~ msgid "Debug logging" -#~ msgstr "Logging for feilsøking" +msgid "Toggle Support Panel" +msgstr "" -#~ msgid "Storage ID" -#~ msgstr "Lager-id" +msgid "Toggled Label" +msgstr "" -#~ msgid "Base Path" -#~ msgstr "Rotsti" +msgid "Tomorrow" +msgstr "I morgen" -#~ msgid "Media Path" -#~ msgstr "Mediesti" +msgid "Tools" +msgstr "Verktøy" -#~ msgid "Mapped Networked Drive" -#~ msgstr "Stasjonsbokstav" +msgid "Top" +msgstr "" -#~ msgid "Username" -#~ msgstr "Brukernavn" +msgid "Transition" +msgstr "Effekt" -#~ msgid "Don't Scan Entire Storage" -#~ msgstr "Ikke scan hele lagringsområdet" +msgid "Treat as Main content" +msgstr "" -#~ msgid "Base path is a network share" -#~ msgstr "Rotsti er til en nettverksressurs" +msgid "Trigger dead zone" +msgstr "" -#~ msgid "Media Flow ID" -#~ msgstr "Medieflyt-id" +msgid "Trigger Mode" +msgstr "" -#~ msgid "Media Flow Type" -#~ msgstr "Medieflyttype" +msgid "Triggered Actions failed to upload: {{errorMessage}}" +msgstr "Opplasting av handlingsutløsere feilet: {{errorMessage}}" -#~ msgid "Source Storage" -#~ msgstr "Kildens lagringsområde" +msgid "Triggered Actions uploaded successfully." +msgstr "Opplasting av handlingsutløsere var vellykket." -#~ msgid "Target Storage" -#~ msgstr "Målets lagringsområde" +msgid "Trim \"{{name}}\"" +msgstr "Trim \"{{name}}\"" -#~ msgid "Delete this Monitor?" -#~ msgstr "Slett denne monitoren?" +msgid "Trimmed successfully." +msgstr "" -#~ msgid "Are you sure you want to delete the monitor \"{{monitorId}}\"?" -#~ msgstr "Er du sikker på at du vil slette monitoren \"{{monitorId}}\"?" +msgid "Trimming this clip has failed due to an error: {{error}}." +msgstr "Endring av inn-/utpunkt for dette klippet feilet: {{error}}." -#~ msgid "ID already exists" -#~ msgstr "Denne id-en finnes allerede" +msgid "" +"Trimming this clip has timed out. It's possible that the story is currently " +"locked for writing in {{nrcsName}} and will eventually be updated. Make " +"sure that the story is not being edited by other users." +msgstr "" +"Endring av inn-/utpunkt for dette klippet tar lang tid. Det er mulig " +"manuset i er låst i {{nrcsName}} og at inn-/utpunkt endres om litt. " +"Forsikre deg om at manuset ikke blir redigert av andre brukere." -#~ msgid "The ID {{monitorId}} already exists!" -#~ msgstr "Monitor-id-en {{monitorId}} finnes allerede!" +msgid "" +"Trimming this clip is taking longer than expected. It's possible that the " +"story is locked for writing in {{nrcsName}}." +msgstr "" +"Endring av inn-/utpunkt for dette klippet tek meir tid enn forventa. Det er " +"mogleg manuset er låst for redigering i {{nrcsName}}." -#~ msgid "Monitor ID" -#~ msgstr "Monitor-id" +msgid "Troubleshoot" +msgstr "Feilsøk" -#~ msgid "Monitor Type" -#~ msgstr "Monitortype" +msgid "TSR" +msgstr "" -#~ msgid "Media Scanner Host" -#~ msgstr "Vert for mediascanner" +msgid "Type" +msgstr "Type" -#~ msgid "Media Scanner Port" -#~ msgstr "Port for mediascanner" +msgid "Unable to check the system configuration for changes" +msgstr "Kan ikke kontrollere endringer i systemoppsettet" -#~ msgid "Quantel ISA URL" -#~ msgstr "Quantel ISA-adresse (url)" +msgid "Unable to upgrade" +msgstr "" -#~ msgid "Quantel Server ID" -#~ msgstr "Quantel server-id" +msgid "Unassign" +msgstr "Fjern tilordning" -#~ msgid "No. of Available Workers" -#~ msgstr "Antall tilgjengelige Workers" +msgid "Unconfigured" +msgstr "" -#~ msgid "File Linger Time" -#~ msgstr "Maks ventetid for fil" +msgid "Under" +msgstr "" -#~ msgid "Workflow Linger Time" -#~ msgstr "Maks ventetid for arbeidsflyt" +msgid "Undo" +msgstr "Angre" -#~ msgid "Cron-Job Interval Time" -#~ msgstr "Tidsintervall for Cron-Jobs" +msgid "Undo Disable the next element" +msgstr "Unskip neste super" -#~ msgid "Activate Debug Logging" -#~ msgstr "Aktiver logging for feilsøking" +msgid "Undo Hold" +msgstr "Angre hold" -#~ msgid "Remove this storage?" -#~ msgstr "Fjern dette lagringsområdet?" +msgid "Unknown" +msgstr "Ukjent" -#~ msgid "Remove this flow?" -#~ msgstr "Fjern denne flyten?" +msgid "Unknown action" +msgstr "" -#~ msgid "Are you sure you want to remove flow \"{{flowId}}\"?" -#~ msgstr "Er du sikker på at du vil fjerne flyten \"{{flowId}}\"?" +msgid "Unknown error" +msgstr "Ukjent feil" -#~ msgid "Attached Storages" -#~ msgstr "Tilkoblede lagringsområder" +msgid "Unknown Layer" +msgstr "Ukjent lag" -#~ msgid "Media Flows" -#~ msgstr "Medieflyter" +msgid "Unknown Package \"{{packageId}}\"" +msgstr "Ukjent pakke \"{{packageId}}\"" -#~ msgid "Monitors" -#~ msgstr "Monitorer" +msgid "Unnamed blueprint" +msgstr "Blueprint uten navn" -#~ msgid "Primary ID (Newsroom System MOS ID)" -#~ msgstr "Primær id (News Room System MOS ID)" +msgid "Unnamed Show Style" +msgstr "Showstyle uten navn" -#~ msgid "Primary Host (IP or Hostname)" -#~ msgstr "Primær vert (ip-adresse eller vertsnavn)" +msgid "Unnamed Studio" +msgstr "Studio uten navn" -#~ msgid "Primary: dont use MOS query-port" -#~ msgstr "Primær: Ikke bruk MOS Query-port" +msgid "Unnamed variant" +msgstr "Variant uten navn" -#~ msgid "Secondary ID (Newsroom System MOS ID)" -#~ msgstr "Sekundær id (News Room System MOS ID)" +msgid "Unsupported array type \"{{ type }}\"" +msgstr "" -#~ msgid "Secondary Host (IP Address or Hostname)" -#~ msgstr "Sekundær vert (ip-adresse eller vertsnavn)" +msgid "Unsupported field type \"{{ type }}\"" +msgstr "" -#~ msgid "Secondary: dont use MOS query-port" -#~ msgstr "Sekundær: Ikke bruk MOS Query-port" +msgid "Unsyncing Rundown" +msgstr "" -#~ msgid "MOS ID of Gateway (Sofie MOS ID)" -#~ msgstr "MOS-id for gateway (Sofie MOS ID)" +msgid "Until" +msgstr "" -#~ msgid "Device id" -#~ msgstr "Enhets-id" +msgid "Until end of rundown" +msgstr "Til slutten av kjøreplanen" -#~ msgid "Thread Usage" -#~ msgstr "Tråder i bruk" +msgid "Until End of Rundown" +msgstr "" -#~ msgid "Port" -#~ msgstr "Port" +msgid "Until end of segment" +msgstr "Til slutten av segment" -#~ msgid "CasparCG Launcher Host" -#~ msgstr "Vert for CasparCG Launcher" +msgid "Until End of Segment" +msgstr "" -#~ msgid "CasparCG Launcher Port" -#~ msgstr "Port for CasparCG Launcher" +msgid "Until end of showstyle" +msgstr "Til slutten av showstyle" -#~ msgid "Minimum recording time" -#~ msgstr "Minimum opptakstid" +msgid "Until End of Showstyle" +msgstr "" -#~ msgid "Enable SSL" -#~ msgstr "Aktiver SSL" +msgid "Until next rundown" +msgstr "Til neste kjøreplan" -#~ msgid "HTTPMethod" -#~ msgstr "HTTPMethod" +msgid "Until Next Rundown" +msgstr "" -#~ msgid "expectedHttpResponse" -#~ msgstr "expectedHttpResponse" +msgid "Until next segment" +msgstr "Til neste segment" -#~ msgid "Keyword" -#~ msgstr "Nøkkelord" +msgid "Until Next Segment" +msgstr "" -#~ msgid "Interval" -#~ msgstr "Intervall" +msgid "Until next take" +msgstr "Til neste Take" -#~ msgid "(Optional) REST port" -#~ msgstr "(Valgfri) REST-port" +msgid "Until Next Take" +msgstr "" -#~ msgid "(Optional) Websocket port" -#~ msgstr "(Valgfri) Websocket-port" +msgid "Update" +msgstr "Oppdater" -#~ msgid "Show ID" -#~ msgstr "Show-id" +msgid "Update Blueprints?" +msgstr "Oppdater blueprints?" -#~ msgid "Profile" -#~ msgstr "Profil" +msgid "Updated" +msgstr "Oppdatert" -#~ msgid "(Optional) Playlist id" -#~ msgstr "(Valgfri) Spilleliste-id" +msgid "Upgrade config for {{name}}" +msgstr "" -#~ msgid "Preload all elements" -#~ msgstr "Last inn alle elementer på forhånd" +msgid "Upgrade Database" +msgstr "Oppgrader databasen" -#~ msgid "Automatically load internal elements when added" -#~ msgstr "Last interne elementer automatisk når de blir lagt til" +msgid "Upgrade required" +msgstr "" -#~ msgid "Only preload elements in active Rundown" -#~ msgstr "Kun forhåndslast elementer i aktiv kjøreplan" +msgid "Upgrade Status" +msgstr "" -#~ msgid "Activate Multi-Threading" -#~ msgstr "Bruk flere tråder" +msgid "Upload" +msgstr "Last opp" -#~ msgid "Activate Multi-Threaded Timeline Resolving" -#~ msgstr "Bruk flere tråder for Timeline Resolving" +msgid "Upload a new blueprint" +msgstr "Last opp et nytt blueprint" -#~ msgid "(Restart to apply)" -#~ msgstr "(Start på nytt for å ta i bruk endringer)" +msgid "Upload a snapshot file" +msgstr "" -#, fuzzy -#~ msgid "Report command timings on all commangs" -#~ msgstr "Rapporter kommandotider for alle commangs" +msgid "" +"Upload a snapshot file (restores additional info not directly related to a " +"Playlist / Rundown, such as Packages, PackageWorkStatuses etc" +msgstr "" -#~ msgid "Drive folder name" -#~ msgstr "Mappenavn på Drive" +msgid "Upload Blueprints" +msgstr "Last opp blueprints" -#~ msgid "Provide the name of the folder to download rundowns from" -#~ msgstr "Oppgi navnet på mappen kjøreplaner skal lastes ned fra" +msgid "Upload Layout?" +msgstr "Last opp layout?" -#, fuzzy -#~ msgid "" -#~ "Go to the url below and click on the \"Enable the Drive API\" button. " -#~ "Then click on \"Download Client configuration\", save the credentials." -#~ "json file and upload it here." -#~ msgstr "" -#~ "Gå til linken under og klikk på \"Enable the Drive API\"-knappen. Klikk " -#~ "deretter på \"Download Client configuration\", lagre credentials.json-" -#~ "filen og last den opp her." +msgid "Upload Snapshot" +msgstr "Last opp snapshot" -#~ msgid "Row" -#~ msgstr "Rad" +msgid "Upload Snapshot (for debugging)" +msgstr "" -#~ msgid "Cannot connect to the" -#~ msgstr "Kan ikke koble til" +msgid "Upload stored Action Triggers" +msgstr "Last opp lagrede handlingsutløsere" -#~ msgid "Sofie Automation Server:" -#~ msgstr "Sofie:" +msgid "URL" +msgstr "Adresse (url)" -#~ msgid "rows" -#~ msgstr "rader" +msgid "URL to the Quantel FileFlow Manager" +msgstr "Adresse til Quantel FileFlow Manager" -#~ msgid "Delete this Item?" -#~ msgstr "Slett dette elementet?" +msgid "URL to the Quantel Gateway" +msgstr "Start Quantel Gateway på nytt" -#~ msgid "Set Database Version" -#~ msgstr "Sett databaseversjon" +msgid "URL to the Quantel HTTP transformer" +msgstr "Adresse til Quantel HTTP transformer" -#~ msgid "Are you sure you want to set the database version to" -#~ msgstr "Er du sikker på at du vil sette databaseversjon til" +msgid "URLs to the ISAs, in order of importance (comma separated)" +msgstr "Adresser (url-er) for ISA-ene (kommaseparert i prioritert rekkefølge)" -#~ msgid "Restart this device?" -#~ msgstr "Start denne enheten på nytt?" +msgid "Use {{nrcsName}} order" +msgstr "Bruk rekkefølge fra {{nrcsName}}" -#~ msgid "Is unlimited" -#~ msgstr "Er ubegrenset" +msgid "Use as default" +msgstr "" -#~ msgid "Is on clean PGM" -#~ msgstr "Ligger på cleanfeed" +msgid "Use color of primary piece as background of panel" +msgstr "" -#~ msgid "Reload Rundown" -#~ msgstr "Last inn kjøreplanen på nytt" +msgid "Use Trigger Mode" +msgstr "" -#~ msgid "ID" -#~ msgstr "ID" +msgid "User Activity Log" +msgstr "Aktivitetslogg" -#~ msgid "status" -#~ msgstr "status" +msgid "User ID" +msgstr "Bruker-id" -#~ msgid "" -#~ "System was unable to deactivate existing rundowns. Please contact the " -#~ "system administrator." -#~ msgstr "" -#~ "System klarte ikke å deaktivere eksisterende kjøreplaner. Kontakt " -#~ "systemadministrator." +msgid "User Log" +msgstr "Brukerlogg" -#, fuzzy -#~ msgid "Media-Scanner host" -#~ msgstr "Media Scanner-vert" +msgid "User Name" +msgstr "Brukernavn" -#, fuzzy -#~ msgid "Media-Scanner port" -#~ msgstr "Media Scanner-port" +msgid "Username for authentication" +msgstr "" -#~ msgid "Nyman's Playground" -#~ msgstr "Tester" +msgid "Validate and Apply Config" +msgstr "" -#~ msgid "channel" -#~ msgstr "kanal" +msgid "Validation failed!" +msgstr "" -#~ msgid "layer" -#~ msgstr "lag" +msgid "Value" +msgstr "Verdi" -#~ msgctxt "item" -#~ msgid "new_config" -#~ msgstr "new_config" +msgid "Value between 0 and 1" +msgstr "" -#~ msgid "No studios are compatible with this blueprint" -#~ msgstr "Ingen studio er kompatible med denne showstylen" +msgid "Variants" +msgstr "Varianter" -#~ msgid "No studios are compatible with this Show Style" -#~ msgstr "Ingen studio er kompatible med denne showstylen" +msgid "version" +msgstr "versjon" -#~ msgid "Refresh" -#~ msgstr "Oppdater" +msgid "Version" +msgstr "Versjon" -#~ msgid "Are you sure you want to remove device \"{{devideId}}\"?" -#~ msgstr "Er du sikker på at du vil fjerne enheten \"{{devideId}}\"?" +msgid "Version for {{name}}: From {{fromVersion}} to {{toVersion}}" +msgstr "Versjon for {{name}}: Fra {{fromVersion}} til {{toVersion}}" -#~ msgid "" -#~ "Only a single Rundown can be active in a studio at the same time. Please " -#~ "deactivate the other Rundown and try again." -#~ msgstr "" -#~ "Bare én kjøreplan kan være aktiv i et studio. Deaktiver den andre " -#~ "kjøreplanen, og prøv på nytt." +msgid "View" +msgstr "Visning" -#~ msgid "Are you sure you want to reset this Rundown?" -#~ msgstr "Er du sikker på at du vil tilbakestille denne kjøreplan?" +msgid "View Layout" +msgstr "Vis layout" -#~ msgid "Line Templates" -#~ msgstr "Line Templates" +msgid "Waiting for action: {{actionName}}..." +msgstr "" -#~ msgid "Restore Backup" -#~ msgstr "Restore Backup" +msgid "Waiting for gateway to generate URL..." +msgstr "Venter på at gateway genererer URL..." -#~ msgid "Blueprint logic ID" -#~ msgstr "Blueprint logic ID" +msgid "Warning" +msgstr "Advarsel" -#~ msgid "Is Helper" -#~ msgstr "Is Helper" +msgid "Warnings" +msgstr "Advarsler" -#~ msgid "Select this" -#~ msgstr "Select this" +msgid "Warnings During Migration" +msgstr "Advarsler under migrering" -#~ msgid "System version" -#~ msgstr "System version" +msgid "What type of bank" +msgstr "" -#~ msgid "Take debug snapshot" -#~ msgstr "Take debug snapshot" +msgid "When" +msgstr "" -#~ msgid "" -#~ "A Debug Snapshot contains info about the system and the active running " -#~ "order(s)" -#~ msgstr "" -#~ "A Debug Snapshot contains info about the system and the active running " -#~ "order(s)" +msgid "When disabled, any HOLD operations will be silently ignored" +msgstr "" -#~ msgid "Baseline logic ID" -#~ msgstr "Baseline logic ID" +msgid "" +"When enabled, double clicking on certain pieces in the GUI will play them " +"as adlibs" +msgstr "" -#~ msgid "Post Process logic ID" -#~ msgstr "Post Process logic ID" +msgid "" +"When enabled, this will override the piece content statuses to have no " +"errors or warnings and display a mock preview. This should only be used for " +"development!" +msgstr "" -#~ msgid "External Message logic ID" -#~ msgstr "External Message logic ID" +msgid "When set, resources are considered immutable, ie they will not change" +msgstr "" -#~ msgid "Download Full Backup" -#~ msgstr "Download Full Backup" +msgid "Whether to show countdown to next break" +msgstr "Om nedtelling til neste pause skal vises" -#~ msgid "Download Current State" -#~ msgstr "Download Current State" +msgid "" +"While there are still breaks coming up in the show, hide the Expected End " +"timers" +msgstr "" +"Gjem nedtelling til forventet slutt mens det fremdeles er pauser igjen i " +"sendingen" -#~ msgid "Blueprints' logic" -#~ msgstr "Blueprints' logic" +msgid "Width" +msgstr "Bredde" -#~ msgid "Are you sure you want to delete blueprint logic \"{{itemId}}\"?" -#~ msgstr "Are you sure you want to delete blueprint logic \"{{itemId}}\"?" +msgid "Work description" +msgstr "Jobbeskrivlese" -#~ msgid "Helper" -#~ msgstr "Helper" +msgid "Work status" +msgstr "Jobbstatus" -#~ msgid "Are you sure you want to delete output channel \"{{channelId}}\"?" -#~ msgstr "Are you sure you want to delete output channel \"{{channelId}}\"?" +msgid "Work status reason" +msgstr "Årsak for jobbstatus" -#~ msgid "Snapshot" -#~ msgstr "Snapshot" +msgid "Work-in-progress" +msgstr "Pågående jobber" -#~ msgid "Download Debug-Snapshot (all studios)" -#~ msgstr "Download Debug-Snapshot (all studios)" +msgid "Worker" +msgstr "" -#~ msgid "Download System Snapshot for the studio" -#~ msgstr "Download System Snapshot for the studio" +msgid "WorkForce" +msgstr "Arbeiderstyrke" -#~ msgid "Download Debug-Snapshot for the studio" -#~ msgstr "Download Debug-Snapshot for the studio" +msgid "X" +msgstr "X" -#~ msgid "Telemetry" -#~ msgstr "Telemetry" +msgid "Xbox Controller" +msgstr "" -#~ msgid "MOS Device" -#~ msgstr "MOS Device" +msgid "Y" +msgstr "Y" -#~ msgid "Sub-Device" -#~ msgstr "Sub-Device" +msgid "Yes" +msgstr "Ja" -#~ msgid "Kill this device process?" -#~ msgstr "Kill this device process?" +msgid "Yes, Take and Download Memory Heap Snapshot" +msgstr "" -#~ msgid "Kill" -#~ msgstr "Kill" +msgid "Yesterday" +msgstr "I går" -#~ msgid "Are you sure you want to kill the process of this device?" -#~ msgstr "Are you sure you want to kill the process of this device?" +msgid "" +"You are in rehearsal mode, the broadcast starts in less than 1 minute. Do " +"you want to go into On-Air mode?" +msgstr "" -#~| msgid "Restore this backup?" -#~ msgid "Restore from this Backup?" -#~ msgstr "Restore from this Backup?" +msgid "You need to run migrations to set the system up for operation." +msgstr "Du må kjøre migrering for å klargjøre systemet for bruk." -#~ msgid "Url" -#~ msgstr "Url" +msgid "Your machine is offline and cannot connect to the {{platformName}}." +msgstr "" -#~ msgid "Generic properties" -#~ msgstr "Generic Properties" +msgid "Your name" +msgstr "Ditt navn" -#~ msgid "ON AIR" -#~ msgstr "ON AIR" +msgid "Zone ID" +msgstr "Sone-id" -#~ msgid "Click Anywhere to go Fullscreen..." -#~ msgstr "Klikk hvor som helst for gå til fullskjerm..." +msgid "Zoom In" +msgstr "Zoom inn" -#~ msgid "Please enter your name" -#~ msgstr "Vennligst legg til ditt navn" +msgid "Zoom Out" +msgstr "Zoom Ut" -#~ msgid "Template ID" -#~ msgstr "Template ID" +msgctxt "one" +msgid "{{count}} items" +msgstr "" -#~ msgid "Templates" -#~ msgstr "Templates" +msgctxt "one" +msgid "There are {{count}} documents that can be removed, do you want to continue?" +msgstr "" -#, fuzzy -#~| msgid "Devices" -#~ msgid "Mos-devices" -#~ msgstr "Devices" +msgctxt "one" +msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" +msgstr "" -#~ msgid "Connecting to Sofie Automation Server..." -#~ msgstr "Kobler til Sofie-serveren..." +msgctxt "other" +msgid "{{count}} items" +msgstr "" -#~ msgid "Connected to Sofie Automation Server." -#~ msgstr "Koblet til Sofie-serveren." +msgctxt "other" +msgid "There are {{count}} documents that can be removed, do you want to continue?" +msgstr "" -#~ msgid "MOS Devices" -#~ msgstr "MOS Devices" +msgctxt "other" +msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" +msgstr "" diff --git a/meteor/i18n/nn.mo b/meteor/i18n/nn.mo deleted file mode 100644 index d3f1de2ac9043409b2ae8f4153019f42cafc4532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75883 zcmcG%2Vfk<+5f)=(@gI;w6(zvEV%#%Ofhmdu2{BdA>pJu$%jsN;_hTym<|Dw&_Y52 zyz~}&2qp9q3?u;(T1Wws5Fl@;fh3er5`LfW^UU6zWVw+4`#)HJc4lX0XQw>#%rmpA zKW^6d{D|KJYevy_u%a%0!aWYxhR;Lg=S8?Nd=qX8KY?4o^&6vT8yG{~e{Z-6oDUVhC9oaper2fqoeT%U zGXigf6kYTySO=ejQ(!bNiY9}pupc}JDjydG_YLqs+;_q)U^L&uZ3%Us7!H7g;r4JU z+!D4xJ@>It&vP2w3|;{H!yBRU_ZU>UyaDw*??Z+EI9y+EftT-axCj0d;V5_vq^hHv z;J)y4*Z}uFIEwa!OW^_VcBtzgKwTeoNED5QCAcfR1rCQV!UNzshkE#_Q04Ls_?5j~klZy_Ay8lg(EJSZYZWT2g;qB}qsB~X} zGhzR)kvG^1<$ejOzJ`50if9ti!BFk|2B`M=G29SNKGN%B9^4Z5VNm7S5!{QR%JnR$ z_H!BB9Nr4m9v_8tr|4Ozdi@PldwChI3txdMw>RPbaOYGMaqFlV>iROMdVC71-hU0Z zhJS=g|D(V)o4mbm61XE&zUl)HfNFq6g@1;6u3ZogD!0*aJ2(NVJs%0xFG^75 zc?KK?FNV9o$Kg)!?@;BkWyb4qN2vT2p~9U9)gB*!Yr!`I-wXT@>N!4xx=-I0&+j%+ z@diPqe-PXT9u8HmW!Mf+gUaUza5!AAHHwD9F>nCN(e57)3*1 zf2edD;5x7o>b{3Vm0t#`{EAR=?PRF?eg~>vuZ2qQ$58Pfg$nl+R6BkND*T&J_y140 zzE;-L-2&?VJHxHu7^w7SLbbOep~`0oRJb#P`%X#?MzVJtID7*tIKYxI#hxeiS<9cmge>*~z)6l>@pq_g?)N{{) z%2y`v+fZ`#$8cBpbZ~zLPr=QbV5D%F>q6OdfUlnd`@&%*Z)YuV81C=E5%58%=l>g2`FsHN zy#Il^U%#^Fe@x(HsPbG8++Tw+?mRpho�Bn{|3RF_fGe4t1YtP;zoMRJmlK+D8{u zxbvW%=SNWG^f=rLJ`YtN8!YmA*cxtvdpO(-?iV-*svMi4(l0`#b23!E&VkDRC2&)C z4OG3{1=TL^hr{7xQ1$Q+sPsMwTz9dT)7DVw4}eN{6s&{epz_%cw}4CFR`4vSd|n;= z?+X4;LCN*k;BIi8C7z#s0uO-7$0T?joB?C_CpZU2U9??r5LEt8gnFKDLgnLZxCXp1 z_+Jdi;l2VY-9LxxZ@@vg--cVlO;Jvi-yu-tG#08|%z%2{LxQ^{xQkHraU#_7eg|$1 zuY)T8hoRcnFQMA=Z=v$}DOA3{fNR2amwNaOpxR4+sPf+tDttZE{da@?VFOgUhePEr z19iWnq4Iw`RQlh6O6MY|a=AXZ?|{nhGWZqvB2>A22zCF@p~A0mth+aaa&Hcmj~$@G z4~Kig-JtH%1_#4qpzePKRJdxxJRJ!{cPYLpvv)0sCb`3{Ix=*n}^EB2~g>L3+g#8hB3SW>N%c-+rTxC_xo%IiJqY0R95%zHdX_?<2T9?0bUC+ksH&HA2;ME7Wu6pyCw+ zj|)5l>b~C%?n?r%gDT(KgZp8q=YA5ZzrGBW{yR|R{`cVjZ>V@1p6Ka*1Tq1xNz;64H>UKT2!$3x}s9H{61 z9#s051^3NR`Mw*f-XDc~!Ka|geXWx{pW8t7r){Cq-4W_}MuzMALA8qmpvrMd;6YIB z;&7<@w?oy#5_mW~9u9>sLgiykln1qojo~mj6e|C7q1xxcQ0=@O4ur=-<>Rv8e-m62 z_kB?H^AObar-S=txI6B5pwb&~s+ZTEP|rI9s-JxwDqp8TrSl`Ga()Qv{=b3B=U<@u z$A6%nZ=G*=em8@Pw>4CG4}pre2UPh@gx0U1>h)`I7kCU*JzN9To^OVI;geA1`V5>2 zUxYitp{KdLp9&=(b5QA@3RRxx2lo|F`M3@0IUk0~&+~z=LgnZ0Q1L&7qu~0ddp`Dq z``|tt&W1mL--CaJs;6Vm@cds3hv0q|ZU;YsDwnOk>FEy$+y%;iH@Gj{Klm?&dhS!9 z+VPdaeHT=}c>(G<--OC%pKtkdZ3g9zp~`&}Tn9D;|7mbt+=m7KW~gv&a4=j7*Me8V z_26~k`fX6@J`&u|L)H6hQ04hKTpRZLwwL#&Q2E>*s+>nbmGeZX`!zy6Uo%v=lfw1$ zgZpZ@1O9hImE&)N`wOV{x5Js9-#wtp=c`cZ91Ydaz7hO?5ZpIG<>Nu9@^}_1{O^PR zyHM#yXZiDN4fVW(;QDY3RJ`%QePFo00ID8Zp`PyqsOLL7_+JW@zn=s?36+mmpwjy% zl-&Casywzj+sBLDq3%B)sygP?U z{Jsy>F4sNB$B{u$*QY?`YZlxXE`X8`EpQAx7OH%2htI;tpxV<9&h`6V9(Xg{3IBWH zM(~eN&+#r)yZR^G82&rBH~fy@cWbEf*%596N5dg-0^Aw4LA8r>q3Z8Pa6`BZs{EdX zs>k=Bo^S2*oIAn&a327b?(tCZ&VkC$C2$J-AyoSBL517syMEstq58)}sON2idY&>= zc^?Dyyr)B@cO#Tsx)Un=3sB|$$Kd`5D&7}xQ@8;_MD?@-RC^f)H-q~_wZDU)+Q|`6 z;g&!>=P6L_;3BB<*P~1601|L&a+e?qaxp3Y>@k51`_|163~{K$X*{Q0;oH3p||-pvIZ)q3*LcRKJ-F z^_v(4Ha+83%wl;fJ%QdR5~-E z%H;^SD{O-b{{tAqtKgdOQK)?U94fyrLOsWypwjsas(#kL$lLqAP|trTRC`QA#mht8 zrv!I_m%$i50o6|4hWo>f{>S^nOt?4h^Wo|6X{dI7@Wmc(DXhbNFZ?=u10DpYUgGWQ zBG@1Iqi_!VV{nhS)bBqHcHv)!XTrb15pd~c%sJqVFb_AqoH;l=5e|i~LAApTuJHVi zhdbbIhC|@V@Z0bPsD3!+N|z4{pstsp{4a)zcRy5lyacDiwXbsbT&QwAF1T-j3vmA) zZVyLZ?RKYGP~lF3qu_0Uufu(DZ*`5^XB*)@xX*?I;S=xx_%0j+>#z0xl7TAMV_^($ zfRo{qa3{Fg4_$7LfnULW2%H2krj_c7(dV3)~&<35UWQRJ%JLZUL`>YA5%?C*cQh5q$W1kH6;)-u@=SHSwPYb^n=A z`9C_i&xB)f-wJiVx1ri=e52=Y2JDM_K2-T12K9VP;5zVRsDAt{sD5=JR6YL?>OMaU z{;xvGm-nIMRNtFC-quk4Y%tXG?FH9`Q=y*opuiTm74D@_@y`pq1}fg&Q04sd;QusK z{$7Qfz_-Hn&!FzN#?5~JEuiA>1eN}7P|tZ_a4&=ke*#o`-+{{Cg;4RXges5Qpq}^s zaQ(M%Tima}E#c=-^}N|FUO(GFr85XB-Fm2YItHq|i*S9o7!HP~K!v*jD&B2S{ro<- zA$$Q2fv-XJ`}Ke1_xmc;^EW}ISAr_P6N3MFQ2DhBAva#;JvZoe82W84L} z2|O37e69$*7V17X!j0hVa3p*Ls$KsRN*>hT=JKTtj>i3cI0F6x?gl@Bb+GPs?-xL<<1z;~h2+2#(v?^LMtyP)FV1XX{(f|55MLiO9>clx?V3!H}g z9(WX7^CyfUFb}^De-4$ep?7(>1@Iu;m&2{8pnpJx>vxaK-=pAk+#f;J19;Lh+GsQP&VsvSKGH-mqG+rsyQ{|5Jm_6_^t zKM1OxN5YNa7^wQ63{{`Aq4IHLU?%Kv2e zb$AQh4{r2RUuT^G_riS+JOKV2j)Chv;&##l;8@%zLiLaPpxW&_a5mg-nYYUvoQ(Tg zcny3PPJ!Qjl)S^2;NI}a$EYKC5u5}EJ??CSGjabIHo!07;jrOn-fk~}^Kich$HR#~ z_j)-U9*_GOxIdiz3%}3V@KD@8htuG|Cp^D7sPerZt^=QdlC#gjb>SOO?dtDP?QHEQ zz5Q$rRnEJ>ey{=dg)`t<@Sxy67%H8wLX~3%Hp7eIWEef=@uxtAn+w;2heM^Gfg8YM z1HTEC-|q)r4VACkpz^s4D*o@_SKzBq>HZrkUu*x;pKk-G^asMV;ofjVctCK^g6jA4 zp~@u>RnCi`>g^sl3jPzSoe%nz-**ovxi$_e{6SFV^F63?y$r_iPPh^L4OIBQK=sRy zq0;a7G_n>>guB2J)N@@54};giL2%7yT&@iZJQ(VEz5(^TXF}D>c~H;s1E~941~-7W zLEY~msB-!Qs-La#toQ$~z#{H?xE;I#DqjymN!3O zT;~Osx0^%78v)0`z2Fvb2^#2A@C1y2<}aO>-}OccslMzsQP;eE`VRaNpQjMT%KGEmCj$F+Q~M* z_wjfxR5_R7On5fbbG!`oe6Pdp;U{n(xb=(PE*qih|1hX4xC{093zw0V+Q)L$!y|fAIV459OW!Rj;$5@_A(N zUj$XJC&QueOsI0d6Yc@ufO?)C{^;d41a6OeUpN~c1`mKYziM>WD)(W5`#?S4%;25}x5eE8Z-Xa8#oOc0p3iAeh7Z8Ip~`pBUwmBo7Tg>6 zFQMvbo&WXt{h`Wh2e<|t3iaH(!b9P1aBX-NToZl|Djyd>Q?BxKD%Y!0Vy%_hYDh+yT|!?uP5Y$Dq>x zC0rZ+5vn}?0(XG#L)~xVH@uzohpPYKQ024_RJ_?x@fN^!U<&RGvrzSPHq`T83zd%t zpyK^BxPK1SexHK6-*Zsuy$qG!U!nTvdr;3i=uID&_khaJ;ekg%m1h^!bA1~s-HV~* z!j(|{<_1Wib&(Bs+?qR{bf8ar|AO2~mdMgC?u~6xs4i)cGsQlax74Ckh z=lBI20pEn`zy06w_B#TqTy}@+!?94$H95HFK&AK9;GYRBzzy*43hvXO?td;+d$;!BFM23p^Okg-Z7ZsONYPsy+P;?gH2T zo9o}Bpu%-P$;(sWhVXKz^1lV@{y&4t--}S~>J_*Vd;=={$HBeUd+y!>PQia5R60jN z^`}z;PlI~?Z^Dh?rBKgt3sk$f4JzGxpq~3@Q1$sNRJ@m=^7D5nx$!a7^B?rS$LoT6 z-qYa-cp2;qpN7iMuc6X^7plE}234;8|L*x243*z8P|vwH)cvOhcO#TsZGtMl%i*T* z4!ALV9PSB!19yWP{ln844<(-uf@(i)!MzyjxxWK-p9`SM<0`lr{9)j|Q1|&I+ywp! zsy^O_O7~xIC%D-^J-snd^|~Kac^wLM->*ZplQt;%x(q7bU!cOT_krJc5Y+SU1yycS zg8RteE5#C;7M1_yuS@@WoKeip$P zUIf(+?hAYj>bakWdY<3H2KX9O{~PqNKSwBx6UUne+EK5-`-H| z>EKR!M)*UE`N50U%`JY)bq}VD)%f@_@kko?=;vSUKV%{l>B-gsy)03 z74HM6{C*ApALZvKMrmO=LZ&{@_h~*2ycd4!)KuC;f-+pL#X=Q{NMiEdqCyy zaH#t0fJ*<^;64wk{4a$n$48*b|5d2xd;{vaKZnZi*8lNzhCw~&9&lGU8*02>3iW)K zLp|?xQ1x~@RQr1zs(rl!mH+L&ppC%Yp`K%lsE_rR0Z{ck3aVX=gR1YNpz^mAs(qgX z)xW+4b>EAiKLox3C7=HZ)ehEJ$M4$@svU0{I0!1<-oZT) zs$Cue4}`}<<@Xm*&-n~g_}>Tr*P-(HHq`Tc4%IKVSl6F>FjPKx9bJiNXJSP~oqFyTBWu(tQCc++TzHGpPQ(;d&l^1XR6`fod<);1IX~svM7pihl-N z6J88ezn4SF=R4sJ@Ls5Neh>GC??APO!Tmhl2B`FALDlaea5wn1!1Dqh5BwDF&h`5B zJ-ueA_HzmRKKw;+AGtvvvoD?r)n1;5hrxftx$uw;`)QaPPlSAG60S zfJ^Z|1D*i;ZtVW2!b5TY1UA7fKrki>_&4uc3i=guRJ=hE%hAP*c zH}n2811f$F4u!6o(<9+doh5;nm1q59!y7FCr06pY~+ z@Bnx{RDHh%Ro@>#$%{{ed+lw!{cQl%UN?sdw?9K8}Au`mZm!XHBQ z^WQ=B!#ChQaHIZxqF=%T;mL5DuXwuu1J!P?hLUgBLp|4BP;%(y;Qj!foN>Q1a|msPX?3sD8QG_8xu+RC(j>9R4eqm` z?tcZ;eQ$%h-=k1+=#{{}J9zp-q0-wE>iHX>;?D@~BcS3h4DM5*%K03qaq=dpa(o=B zJf4C|?{`pg_*JNO`&Mud+0pMa0;)d7LWNI3#czYU-(slze-{pe*T5O@X{dC!i@kn! zhRXM7sBv*WsCbP~<(GjfzcN%mI}Y}Nmjqr0*T8)RRJ~sf)!uG|`@s4EUf)MS$AR}i-S4MR3y%cXj#^wUt~cs|tg+zF?_e}wD%4(?-i=61Lz{`bMd;Xk4BF?&cKvtOSARnIR% zh5HC9z51bEKSxqYpW^;4d=37TNr?RiI=qZ< zpK|QU@l_5)ZPdV_pUPUlAL9QB#|9i{6K^obt%ON)d>y|pIGPC8%<&P&bgrxXPv=N} zd*G+vuJE9cKH@+627YG{|7Fg9&T&k*))&4J&i5kDO@w)#uovL>5a%iQ0Pf#$)ZxAo zzRR&b=YQqeGaL_cuHPKue-C%^`$srG5dXb6f1P6&&TD_)!o4BKPis6sBfVE~zY_fZ zNW9xPhK0EQ4E}UK`yCe;yeoK%bOzwMf_VR> zxz+~H#s3!}+@ZMXiniD18-)EF|5V7+DY)*^kmo-Ue?8nEa_IL*j;rxoLb$6qf0Xk(Ic^H!6-ILDHT?9u5C8cbyK|i2 zUePbOc6m4#PQX1GK1G_S4bi_i_T)GK_h(SQjfp>=vszxR&K!eK@3_-3@Dh{~N;If`1{Tb1ml^bDjmEAG2P zyiIU#&9MN#{o#2v=_$&YxUb~6G{irOaJO*Y#qk^bQM>JT7R-k8rS277g!@s#AAuV& zUHPqv-{rW!R`J2UhToI8$KZcAJeuQR^7B6&`kjdX-W<1bo+a$D9Pe=Gw`JI8nMdxB$Mj&=^z-pcPF&W@|NWDB5g;XX5YHqx&B{Sx;8!fX;e535N_UMGfl z*AY&0quaUuZTy%b*&Jjf=cBoHKnT|y*b2AgI6Sxq6Su(m9pV04hWqS|-!)ttjQ{_@ zkMNf+TI7&Ua~sFrA&$oWnOr{>>US>Z)8Rk4elO?yaJ<8DLI{5Aj`FpjG^-xj}j!}Vw3 zpl}Z^neO>(t|z}!I2lWrS2+I4@tbg=05_@$C-3ibeN;&6EY3%BeFxlgISvl-670lPA93weDfa6a5&*I47cMZp>`0dN_tt2x11^)WAllE>LM{)cc_dJeq9Gl_( zEn)f)Zy@Jq;x~{(zr%1}!=c|Wj#-4ef@3@UV%+<1=(jWOal}80^AyKMA&#ys;(RG_ zE`&D{=6tv#?unf1cRa^OxZgLI|8od#{qBR`AzYsGmpJ~;xpdhwd>iUFk1!{Z<|!O& zb7VM@-|k$?<6eW~89cATy;jZjXYuJO293K+M!Fs}9AL6VV!mLgBpWt^V zoCn8o{D{gqacIZ-#qvIDdlhW!xE#9DaKcPQPV=?pprezvFc>alb>jhv8)SbBiHAb_BQl8u5FIuwz4-Z{W8L=ZC{bIPT*(m16|qZV&N>;65~*4~P44?Yr;- zjw`sPdFOGQKMM6*$6@olzYz8k((iyhl0UKg&*{bSCL;o8j{UkPEZ!~c5Dzg3g&)NoC476-Q| zn4jQ(Dd$^+``jM<758nf{TufU;o7p0#-F*i7UySk?8dPPVLrfb5a%1h32*}3Jj8pE z^Y0O61pfM675oR{-VXoo;69l1F`Tc%`Idw^2!4)xcg|mj&vWRvS6~O>+d?`&<9yfP ze?I4!wxZ_<+X?lX5W<}o_)Gi?9A^_}oe;ePTS#Bm|V!-PMW<1d^)0Z)P(5`GfL2OQHmcHkHi;x6S} z_Ldzu?%|k*dn(6GoX_G&en*CU9LTl1g69~x5&nPDeKZxZg}koG6|JcxT@2=GZrFCVVo z*&}{k2zM>~2gg=~YYTotk@#oBUj@If!)*yOANJ)alism}c>}*)I9fRWTDZs1ntPSG zrr%Dm5&u)+haqnbA>1{%zgiRLc+QU|+>b)s)42Wvj-xpG;P*P|eU0-?;a54DInQwn z zp{ki@vj>qt;hsSgD=h&WO8Gio@?;zaW@FkA@LL#&A8;Uy`I=iEs zE#yGAv&4qNW^SI;URBK^a)DTbZ%;n<7u2MPE9yjJw9i@eZ zGFi=+Q`vke=9%J>Uo2(HhB-6iN%<6hX*-`+Oyx_dX0^AtG1cCY z%fxeu9W^u+ie>*dZph_y)>)1ZEp!&+*~QjWK6^|`aiRvRcrx9VNgUjE+Qe{y^Lbqz zBxk`2#gfhnoq3XW#&c4wx*QU-e2nibmkW6XHRrrywzV~5QOnsyWG)^$J73q3EmFjU zaG$xEg_%^bM4zd8Hsre&r@DFxom}WG5~ZpJ<*AUfblr?>Q$U>LGwpu-8>b7IQkt5s5VTMs!yi6sHo;tUftp7&P=B&A#F!I&g*->QQiPD@%*_n zbV^MV$1le1G-t(#Q`Q$$CEnmtDbCs%&v4mm40p7UI@SIZjy|WJU+M$mI%)g=mdC z_#9m1(Yt%4vn4v5ZcY!Llg%I_q6V4_LR+0!%}tW7AxFnecUdD_lo>#f(qe1&6hWRw zVxV5RDT1(>hS1TG%Qjm-jOS#ElwEr&Pub2ar#Vp0aVcpR zi&6-(lKCw(5wB*4G}34iifml>Sc<+lg&a~XCH=6{qsT-$vyvF;OeKzWjZhBb`VF-~ zV{um1$7q8HW3;2uGE|i4+qLN_i5Wh7Aw6~~vjacN;D*P@Oku~5y$>fK#q8d8QqO;P1)l;g|X z-UmCywzJias-`bFxjGpo?D)>+woG|71J_6+J(oMbOwL?Dj8$YmT~%b)jfNI8?FBR? zDH17(rV=8o#PBf8M}k#81f7|ltaj0!Z7o_K9~W1OB#QqcI~ zUAqu3$`rFLT?9p^PTxIjQD0RIPY|>$nU(Btw6Y!nma6GRQs^l8*lQ{XgT zv0PMyT8whP5Y4Iubz`M{p;VSguyh*>%!K2H&T^sMm7m5;ksQ#piy6%UT`H}#I%-n$ zB3wmiQnq}DNujkj6Q9Z4?8_BlX9q>+Dc00SCDkvRSgwE;YT2n(0aiabd-#;JLZ)Wv zc+^M+ojm;zan@Rh8cd`GAlVwFDwuXq8e8M*vtxY9l2EGfQl$IyPQdE_aL_F`~Vz zWKCd%+YN?UR5b1k7LrW5L`)S?7(bG^6jMXZ+suH%j5M1I!I+edXXIY25z$6Xj1%+^ zD#dkpv@7LC>UP|g>1ryZipT`YtyD&VZ%;D_qcXRni*sia{JIUoKeHs$OsS>^kUmOw zwor`5W5;RJ4HGj$^-Pl5CT>2_u7uF!y2eoxH&)-Rxn?u(YS0DVNoxWqU-adwPpRfK z_D0HMb=v%<1&c{neKekDY>TJmBcG)Q#0xXlk4spkC>nHoJ)F%7DS|X|rrVVXWVD!y z9847!W!jnN;%54i3y5p90&2cQi^4`jfrsmq8sYT_QW~fdB#!Bk6m@f|lQzVB6~FdW zkrHNLjwjOyWD@(5(Wd25nX-yj>PQtQOpz?}c3hRf$?!l%C8ucB_4W191k+p8d{uZQ zCSaInY!0t0`Ap7hY=ZPQwJ+4M>@gXl$*nfV_*^QeK$-RRFy;6R+4}ejGBY5WzyvDa z*^w~DsAkcY^DrTv&8KOhCA(OR8@;R9xEZ|MVnK^(_1d!O+0vz^lz0IUFiX}cpj0yX z(bG%KC>C-)v-j>7LUPe&9;rpCY%YaWYI!|H8B(Il2dxoA3L~4?v^hF69Zlt#R0}HO z5bJ_!Dl~?g79#tNE@qZsSaeHGrea)iaqmec0d0DmGWj()mc^&w*RA5ipo)2-NE0tH zNLNN{^Z61%?YTR~S`3{RO0Rd$D)L=EyTE7JLLA23kPFIkySz$sne3R&CHSxvX_o4s$39!2tAA+ zq0p&7wQ1~R>CR?`YpEY$7Lr&&7GyJvW$RJ6My@w6H0erD(f}OHwMZvLrwY zZI>ozwiO~X)1k&Ca^3WjNh^gUaBBvISHo}Utg>&I-jFF4u_Ub|pjkQTRy9dc!of~l zPL(@L_3<2f1+u4^p4*wLTusR)MFZo$1@xS13P~R3c%3|Q`4q1diIz}L6Kxy~O_{g| zU<|a5coqc~OsTbA^BE80!Awagmq~f*DW@(QPim)|CE?JD!(=-6Q2#}`?oov%sy7&! zH4&>g8k3&iSwi=UC*+vzNMN{gs!8W~Mye@;g=u2Nz*BRYn7i<5Nl`oriKG@;aW+zz znTpE|8$QC|DO*iIuy%|~oJF^$RovA+UC$XLlbS z3A2^_=tov2i%>*5Fo$}bu4?=Up&0PPnHoWIZlinfPbLE1NHprv@M(e)0d#8*r8=<% z3Nz|SF`~!zE}fZr7t>OJDp72%kS&!k5=IlV>8hbn=5D$)lBm6AM$uG6-)c^k^zhl1 z7XFb5G!xU|nIt!{V3bb#T!6d=!GOv)wh}O(SyJ}?T=9}nFmGCb4M0hvVfNwnk0dxx z-K_{*47r7vE9zUW#44P*=B3)O!iBYm#4}HcCks}SYD@uU0H{Q*4K=6dM7!5e6-n9c z>IlX_w1TkEv*MKKrE`!Rwl=^dw9zz+Xkx*t1^J*Qxx_Xb=AK8?_!!kg`FyRmaO?v<`VlZr;0*cezk*+NOe$oU{fTi`6_D zx~~8Ym-U<3rDm1M9ct#b>V4fWnn~Ordn$A0;zTJ{>1?U36dr)qmtuHCFn7v?EyKO7 zIZ2)G`Ed0;Q%_`fMKKKX(8|2rtj;~Mw1P;>WjzUZP$$*`+C&%^6t1~b&EKcKdVq@V zltO}rtJbR+0KN9pbgY839L-R?JTx+dc7_-)SJX8|Ute6ZCVP$YX!fi-6Fc3&8BNj>Q!<*Fqp`E2 zgM~=6LbPSSP6lTcm^|26hQ^YP>bgS zlWUdAOe%okFu8_AtKYzWPPvPlQVjax=BpHeAzX>Y53Pw83OS;O z*lC!K(ZSL@vezKfifl(2m(~gC8AXKT;#lTkX10Vc7EuT7-?12w(V`NolUF}wlxSaz zscqAyWCb9S^xYisJOXw1DKmgoeNbr7qlm9nbpb&~X){Y@ZI|(^8ABSwBh6B3Pzc~Ig-c;u zKqRGd^g*hY74u0;sBX51#AL1{yRJ>n%w*n~*T9fmDAh-kQ9jTD9O{sXRIc%2=BF}@ z%M5}w-a&v?gr}`#r%jEhSrZ8Zh-`1@(1qem&lAdY^`1$0s`M|4_Wx5tJp|)w-Tc40 zVKCM;B_^8EN^ccHyD(TqNRyhi%XYnnFQ;YWnR(hs^wXJ4YO-}< zVqIsZ9#X+f9ap-L>mN!?-lmKMhi0`s!>z>FWVItRgYVesZAViyC7J7@E-qqnUGuj!zL$u^@=YHCu>BJC0{6ibaQQ+&pazKj&3?M$(4U!k~b9mHhq5d~PJyl3ZQ z_UXj3#NtyRX4vV$A6cEr_7=_5lch1)f!GHTCjZ^W{i$YLjHYIC9h%howP>mYHZ$pb z**5H$(Otfv`fd zeaE6E%6!_>!{kw3EF)v^*Qzs*RME7i((t3Hn1uMpOsP{bGY_Lg}BbPC*=yQQWVf+TH~SZsUawn3%TQINr%8>=IRON0$AiY#V3 z(DYKdi8g#yR=$VTB&e}ojkV71q_kLQ70it0;wGBb*r4T!QZx-AZ~tKJ$)RLTOY=Ya z2&>xIsF^1^3lp~RsTVRf+wVRrNjeT04#b>p0Gqu*5Wc66SH;sIR zP~Cgtw0ue0imSoat_X-&#e!&492G1+L5)Bq3wG|%t#N51lMG4Gv{F20%FKASnKR@{ z#Zxc@U~bK0-*Ar^nmx^B*8`C%-js{kE3vq;EEsG)#BU)FWkeGNR>CHbW?z!dOk=MW zR#kWN;)Fe4;n|4>y zbF!}U`W#1rsXKCGXFitKo`=ZYjovfVETz=?$xQaF(KB>w`Aa#9X4t;j`FU-gP!2RH zK>De3`cj*%v{k)ZRGPJma5FUSktCvu$I;bBHpaC@Gc?Bgzb@Wp6k1!=FLN1eO>YSGlZ-5B<9BaBblRh+!0-yEnr5hj9d;E!{FHHc(QN0IX=4MCx?$zTwvGOoyYqzAwPro=RQP$U{G za7mieeX52ldHR5@*98)*HJ)~N)mK9|L!f1_axG+`COvf^Y+uZ-rRx{5PrLF#mz^>MAwUEPmg9} zYUikv4wIBZIk6bAah#Shu8Tv-h*O8Ri+)P`>hs_gO(&8ezJpU%@n6l%G z#G!cFH8T71XxyP~&c1G~W|L^?rb~I}Ml-5QF*)!jmiABDJSanw^wppWyQ-b6MkOuU z0)}F>^OWVnv&qk{s#9;?ODREZ<=wi|&lZ%PlR&Ijn z%A2+s{K7avNoq4oG#k0rqTT5W*#v%!7IB$AM9ZxvNMl*fv($^1s))W++m%)oteng) zs{xa7I=N+Vn00;v4s(^_ily1c z6j@ssHOeUrRh15soY>Q*I_Y+74f0L)l|ZqzbRe?x7kh(f}kSFw(U~b251? zaKU8S5r=sPV+?{v`@hoB9Gai~gXIr3RZi)J6m_;pvgog_ogrwYD{7at%^5-)9v(A^ z^cTZS!|u#yF$3nMTryOJreDyEnwwfDwFVSgeXrMpu3=m6l|j8|Kvbq*%c{wtez5{*#;RJE;c zq`E~vl6J^=)r1C8A2VHKomTJ9rHWm{ZE=fQNYcwJ>BupWv6#uoWMQ~K+sbG@5f5D~ zOUgn<3QekPGn%X!!?9`Lk8IW62b0W6m~aWfoLmW}Z{?FrCbzPe+0@HYn45+rnPT$K zu!Y9XL-@$dS(X=TY$+D|;(#Qi?8((h4kgf{8Jj&1QDiirTn?tRxt8SItxDN;rqHd4Up8My8}H%eTi!wnSjT*STo1P^ zAxVEu(nE|amtZKEB#2dl%YBuua!YHX;V-xR)%|iZ`k_aL(J$F!zlw0(FIP+{y?KZ2 z!DMCy!NRWe?r~kxcK7p*FSU=??h|zd^M0mh_QqK26DCs0`)q-6WqS@=>gS@jp;{v| z7BIHjtlPcRZfx6P@DCy9`VOGr3fGXl@^EF9k&$y-j|)`tiJ#rdc0eT;D+^YQw#B#- z(5toL3`0l7opk8X-7Ee*nuKM<+k|fxWq0q1bGrd z9f}=Wh}deSC)aG>sohlX_1HEa^-E%ui|WJW&LV>k)?8+EI2W}59=Ms%b062} z&AyYAtX&8qsrCZfS))pWlL}gONP1MAWSSl(VbGb6D~m9(iE@^s2wCfIw~$Fn&4n$m zx`_5>ZwNG3-=WgD#(#4@Q3HpN6vv3tY|C$u+3td9?tO3`BzPP#w~ z^01|-QTNjHrj^PwM_8lo=CEPDx=An&(LD_9Ey#vOZ77fUH}B2dSEQtW*^73O+cC8i zmdp(UYo`L;y_gLo9@TkiWj{del(rHsuhk5mN7j1!ftf?Rz?nKEiD~eu0ny@Eq0Ppv!rz zW*v!nO&+CSVj%HeqgpJJ#2uP7t}K|FTU;jCzlkVtN2nE6@XQ+2<7El<+7!#|tDw+T zqXWYjSqg6rl?K)&`wi{7#vwZ~10O}f*1jNG-JcnipZM}!NH!5lCO<8{H60IWr?I_L z)8jh(x9nB4ns6aO%_*sc#2VyT@L;wD)KBv=OZsZ5ub{fOO%RoJ-ro<)X~_kOx~%v0 z-JMjDG=$(|?;lG@BE{XSYDlp4*kmL^Y_Z=oYUEu@I$PA(mSyTg*)Uygix18;@x6;S zU!<#ybF^6>0&O=;FFP%IzE&3X^q?{-@0qy;S!t>PS%25l9(P>HWYKE9p%!j1RjVP= z>l;~^rBTxN7OPJ!R+FcLAgLv!?TyzkJ7Zv!Ew%RuG$}<$XVcp8x7?euD~QFHKT_Cu zY^EIEtPMNcyX{@qOMNx-Ls=tAl8DG(SFakHXcCLEw#JzF%`oFvtrYUFf-L7lje05; zqq-Tbn|CFUw`Y%FyP-qm>M$AN5rY->aZ4$3L2P2XTZ`ueAi2 z%rHZ;Tc&+6bgc3eUfyeF!^lEezk`q0hhIes2gw=S-S0upYu+j|JBn$niDcxmZTzIY z`>TSJZM%%3CH}s;SMjgDEfV>gBdP&yXmRVUw&F;5@ai;YDhzvVBY>WrWH)>g$jUXj zs!!|q-uLjKB9(|zV^E`<3%NzAB8zFGR_%E_i!bl6sAnA_si{oZEnr-a=4F}}=Cz+B*i9q`-GV}q%tM;R z^Vn}%@>7)WGH2N5^pBpmq*2VwR#J0)Wm z6vOX;&D|0UZMTwBEOzz;VT*>@*1|Fm`LE2-EKGGBSGtpSPju6bYjt0>E(KB(Q|pIvRl<$F<%6O?Tij9r>@^x&|;Vj(n-Q;B3fu8QrbHB)L52b z6@h}pNhU)cd;k*BtZd;0pNdRlh)IlYe#^WVN*@ zYunlEnWaI6Y_sk`U$x!5Dn)$hEXk+2bPIjtLRkpnjmJ;cAeHH$0V|-s@#all_96}C z$%;plUb9!p*_IT0Skka3U~NRVR8o~5%L2dc6tRv>O{un!Zi(sD*8gpL1b3}oUd~p# zqHx}oDr-wMTOCZtXGdI0do)zh_BcL#*S=9o0I8vs;_t4R6EBR5gqY)ZOY~doN7X!6 zB>D>S>P;}IDwO+fPrTfY#TBH&I*TocGMS^;;x6WN-O{0l>l+g*yc`TlbO|g;8X$Oi zun^my-49JQ9a-M zS8lvltkydi{&cm;RwlnTCi5^Ai@WO|5^CzNbQV)Nv_ZJ4W0B9^qG+f?^iT?32o}jS zB9Bl_Ai2q^VyG1&6eU(DkgR`(K|^X2LeefF9zrAYd-!A*GaPDadnY?W$SB*8`F51| zD$|4Piwd?XF7c=ty+@WAN}6nVjkWuW`(#0%s6nAg8e1tt1Cj#CJ^rhx8i0w*{}O;S z2K%58`v==mZRiw|ydH$U%&je;)|{>1go;VpQl&?mMz4;A9CV#a@le2AL!v)O#a3~e zBr$u8-cOULRl}0qURu1erZK`S(VV-v)x48tT#@&kYy(b)eltc_`X~{T(41Dsco#5! zmzn@<>kSXIl9xj;!G_5=;+nm>>c2JjF_j>vou@pBwp(E;(#%I;q~WL>SD@oq2%Fp{ zAA2x!uR1Z8Ea6}rU%FI@FPRVok5oDHkT$wvF~fYt)<5TQLhb9{G9R*u<`vjx$-k|7 zo6V7If1;hr8cU~YXJe$TL$hThgmFv7yaFmORt9`+>ZUFHEQDiN4zKaJu_ZX@3Jun; zm{6D{Og+QRmld%P%n6bX{ytnyq!m~?320y7QcKgOKe^k9=4x;Fc8qySu#{Dv)@js+ zLO#;0VUw3c_EmCo+;LPuSh(#GQnG$P?bXdhBVT40Df`msIc@?;*R$ssg)DqKlYyq&ja(;Gs@iNy_L#Hz zw?3tNXe?d+S;!wMpyGsO2HV1`9Ugjrf7K7S>POOTtDgdn%v>g$BS#_{lNHO#o`z({ zRxe=^vso{frp}u;$KU9{&d-(w-NIJqR7$)%K@Xw0T{B1KxOV7!&=KwJyy~dkAex*H zi{{&xij_gzm*55M7Mkv=r8TO=J15WYZ%JG_lBA3Z3S*l>b0=4y>+m__vPWE?5U zVjsM%u)0S#ukMk}OY@y*zI_BOpsIqf=>qD0zf|60rRv z6+2mQr>M}y)eo&E!E9Y15sNY1(?2&oA934FDp;G1@GkmZUwsmi7cAUo+C*o?gA>!j z)dLh|78T}ae2xXR*E&IK^-J0cR0*S+G6|Jp0Yu9&VF#iDEgyA(Uc`^Wa*p39I7(5n zJtEl(p`Mah#m2^y_2EQ)t*Fo*0-#3I-B5lC6^}@t<*|Q~wXcIyE;p4}D_zy5VERUK z__7XNUdtc{hxZ@?ONobV#FVs~nHYnjgMGTiilWSv2QOqHgfb&1(pJ#f79AZe%Cv!TMHN#QXJpCZhV@ zfO~l7t33}!RP(B1dr@L*H;>8ocBzsg7v=+p$8NT6NWR$+9m3yaJ`}0Nfqp4H;Lu`sbDA`V?2)o{ZIz z%}_a-zDx7=D_=T7JXEiQ(B6g-^}j15d+TR~-!Y?ilub8L-;yH7eOBNxH_)!MX@yj(dm-6|V(+s^9bZB1cB`zKb`;W* z)UwryAkt|lub%kbxaRY*THHq`z{hM3rhX7AnZuY3tO*qq1G~PunS8abXLzaNDr;MJ zR;O-Hm8Ep}SuZzG1hf_<+GwA?RPXY8utkGUVXCEf&l8HSn-*xBX4B~{M6$;zWqZ>w zydaW$Nl1z|HYEvRzM3B<^qR7w0t5qPZ_#`=Evgk+NK{`v_WxB(n>=88@_B$Ii=rmu z*CV%9|Le$MsLl1AAe)0(?J8D>6?CI&)JJaZEv0#tL9i-p{!4q1dgRD{Gg75; zoxKpt8wvWj4?cQJvNcmTkN#TY9vkQL{bo*^IjMTbQS~GHAm%jg9we%9i`hvF2Zgj?~UBg}nEM(te}& zp}(kO>^C$Y57;jrHEf@Lht!3SSSY`93Yt{MyVZ}XAJs1!isIW+%npku;fIouAA#Aj z@=i>f9?_H&MiVC1ZTd7Ve=(`dsLWpwpc47M0*kELl!tkREz2pat{yOpo?ZU`JAM9~ zQc9oBmf547-Eo#1a?om@*Fagz=30-+ASiR0)@ok(m>}70!%SjP_1MzLn8Bq}Sd&sE z^i7_M^-D`HJa^^sI+k6-Ur%Zy-E`cx?Cvi6ep+6-1|Kq6{sMjl+6D6gOf9|rVI?Vl z$yH2M=@fs9N!1~fPa=ksT^%S3c{Ey#Wmr@&px8_vKU3Rj99SX4Us+;VHJM$a*;VGS zoKwRdvQS^DZ(@I-ZstoRU*a}x`L41T8are|bWbVypm%DK6(1Fl9mPw8#i(IX zQ?ZLbg=DZYiDpx4v|&DQ#^*v)3WuFA*UCJuA-?t6voKb%1Nn>M@Mz7dUTd^G!WiOQda%J z;-Rg}u8C*QiW{cQ)!prBL_?hlffY25I&a#RA5VP+3G+PR1&h>wk!JGvvDJ5_G-8^DZ%|XC z<3%0+#%3k7ud~zySI6MRHogF3vS-=dShMw)oLaJNZT^Rxcv!|H)~f7nWQ0?kt?5NH zs!HN0nZ8!o?aw*Y(FynyM#-OZvYHA-O829RdE>c3g2W3BkCslF0V+d~e%ZK$&?gL^ zcSfuh>Fa!!MJ*WH3@ZbY{g`Rld1c8U0$Fguf`Cbv8MQh`aFyEFkAs`h7l->+&auH zy3Am$wn%SfV}QvB2~NZd-@Uh9ZbPB=i(s+hLd~Cv;;X^Q-+)4LGXAlTg!sOwY6Qcyo#h8F!ydP<46=xn>ISowmQ2WbW!&?eSDfAM+`ay~-`gicU;4h4vDY$OnSieL zZC!eKru}@iqk2QIKbfSJy;awbM`&7hH-dWg!dsi@P1t#cePLnM8E!d>4=^UOCsqBF z^QvQ(k(HQtYjYkC_2gVc_@9i*6o;+!fy$CdzI}xkf$0w8c*R@~$Dl?!D7bWC`mcI1 z-P+7}77$IszTM6)GqxF(XtcWAy&YBLKq3ILQr>D5HDV+JVL+&+-uggSzKw0{Qm4Cj z@0y;#mn!*m+N?Q^D}7pN!a>)P=2%Hql9zWvErFoyuTYej(ws&4*Rxn}rN<(%Ouys% z5SpN@RHFR%b)Lj*E476`C&lu-=>^twdi+7D*@@~v-IW*vT)j6L8RAGPvgD4oQMs-r zz#u18LmzKlUbfR77-r*;l|+TGLUR=f=@fid-H1af9Vtc+x{SZ3>XN47{Bo(NRWg-i zvWS9gTCFFS3V%CvwNvY%cv%74o{2A?0$N{@{z1X?6#P~t66f9jw$uvBUDcH|0h_;6 z<%OGg*`JLDp#ct)uyh0qm0$T_8&Y&ZQW4td6#5HOUU(JZ9ju3|T?8Qqn%gm-z_7=& z`lh2*ClNH4n!jnt3`pfj^RwE3$=~K!X{WCl_cgg~!qACel7cD)VLV2>Anjby*Ob@Q z7M{MXY&yA(zUX*W@n`ae$7AtB-)&e)jRjTT-viP|dNdJ6xl(a(FFNK*b04C6^(5sf z9YK@NmBvKXX<~^qs60MTW^}SL47DBRsb9845uuc%q;W-?_%G`whb_sbzxuL>Os>*N zOKqJk?8#cKx*o@5mw?PgCm@qkl|MVhd|YM(>E7tCd3MD32|EKSHUYM1Ry=JYW*(Xx zV8cNWN*1!ME1Pq#+CGKC-Nlrm&GdWy5wU1a!~Dic(c}zMLYdO*_>)tW6MHJXOT)L+ z=utz{n&(vi?i62Wu^nV1BsVdjD89WjqnVyci2p-VVPlYu&K2cIGJz4Pgnk;MgY$2g zzQird?k=(?DA8G3-hq9lh??EQ21Do425n|JY{;Q~Gc&|E!`DuTg=NudiA|rHhb(d}O${WSl`^L} zlKrtM-W8MeO+8)PhtTcVjldr*|ME$6wsz&PCRG3Ql+vsS;$-A7IYVtHqD4#ZdVNSO ztJv-)3htfCw=StjFx*hi8a^x}fpsFMpDatj*R&DmBL$oDY%_@Ue1(Slx zK+cLNXUGJbbJmP^HQiPXN%DcFlr$ptfx09>LDeNyDX$L`P!*931eK&UL-z=|E(*PM zgj)`p7+%$6Lk5!eAw7GA%e1QD7I}UgrD>t7NuC zi{m=4`QUZb`=7tH`8q$Mbo1U-o5*_8%0 zd+~r85kv#ir_Izc>=jaFFlk@)u}TdaRq*Ndo&hr02mItS=MqSvvV0?4BA47A>W}Je zkWZhP+ElO6M`rVcs6=dRsaU9&_m+CB8CsmFW0R-X4UfQvT20!zg_kKnU+Qzay!}n9 zI=<+lKcX>96JeX}VY;kwB_bx9lhrI0x6v;d*jW*v%xx2~wTeuc7jdvNcvD)~$>*V3 zHB68)U$SsZVD+p~ z=dSJ(>);uz3&D^QqPYT6QT+5*v^_o7Dcy+D&8s5Vu*D=nWnxIAX?NeyihHK();gJU zuur6NM_F+te+jGi(Tv{eqnXCCNhTJgWcV5DJg692f(#NPN~R{>@-z@j1vT9-#NnPE znabmGo^1;1<6gi$h6)63xeLKsc}gtU6n!PW5&I8mt5w zrPP*X^wnSD3eCj+5*F$~S_6>Q17r_U$KAi$nLICaBMDfBkglCrmutLAovbU= zq#6{glE(O|cR89|DUP|?Bx`#g)U<*iqaA93>K^J(3s`=i#|)&-6{<ddPIjqe0Sg)9 znz@TdDn%a}SU@e%mO>jzOoUa%MtvxRN!ko@%GyjCJvm;+)ipfX`h2w2#Dlwp zS-SrIRhCVEHW4wa5oJUHw)-^E{%(c-rfr@6R7I+o7#Wp)&%4Zxs`^6vswdO@wVmXz zs5bh?G%EdHFKH(l>`EuclDG7RNCn1 zHj~qz@oM6o;g9L7yRR&iD#BKdG&|5Y((}|QqnLN({bi#rqg#D zTN-?j)HtS|$kFTH;tIj4bxfC4t%qSexr}J3S+cZ=PIr+isn+f9a_NpWRaCRQ%lf_N zZM7wH5?<3JnWfqgC|yY^JR!;g7IKDkH3@rG-sRUfJG7W+7HADCXg->5*jk<2AY_Ob z8&9wFFD+EkuoBycRk9aqtzclv*LyrOkH~mL+vqKt{i!Y+5u)m;hRa%+?`CBif|vKU z;h5kR!?uUEBzpLT2Ci|tgaoU!|K1RibLn~3GNO6R5xlrGd+!lUb*ZA!w?Rp5 zik4cc(t;<_%-?$u8E$1E*^FZz(9f3-FM)6r9d(f`4$Svu0HY_74NZcBj$5L(ZAmt` zQDk&4m%-#h+A))lnOUBqtqn6r%|s*RO}qQ-LlQ1uXqOQvV}pISD7}mTzQ9RIqam!; zo2Rqet=CG}RHU`_`R@AKK(jWa!c3<2&xh4MgC&)Udzay>7q>Fj__&SD-DV@bL}vD2 zW>TJ1xv!|q*h=YTy=ApWjBcW7Wszh72zLq1V?}qEu9wwHZJI!5sc&owK_$1dGOO-L zJKOGMo7wowR^d;4B|0Z+V6Q&RN+;9Rs{Kbw_l#xN&Ve zw1uc?zeG!^o+Jgb+8RK(?u&Z1U&zTZgH zfdK@ux+*g(DkI>uAI|Lf_5;F2IutFGFAS)MwJ4&;^x~?5Y{i>K(F7;!K!g|n#_8jO zK!c#gYk0Fzfur;HA8{vH9fK&o*9^xE;1;WA@hjDQbIj=z(b(h?&)_MSLreaEfnn@j zeg0FaI3@pm_$X@>Bi-b@$%|5kYGAl~~xnPE)M6@Gzbiow#_Ju*}%LaKe@{Fy{F3ILq zAj#s5PVCW4vo)I!q^DDA3m#5T!Y6x2h${R%21WIdl5tF3qh4@2hx1F@zE zy~p;2a0)VSDM17)0JhsTGE<@f@moVnSB2-|;DJnksuFrV^2wUpEL{65YMsl1VS>ca z_ya@C5rtD4vi%ggp*J@dK4J1&PqYf!TVulwgqf5%wcJb|sY0v@@e}T3TMf|%z@m0@p21`X}kRW42olJ81JC=e>`#f_~Y!MsyW$O`Az{ZfX z2~yzW<=x%c`QLau`rGktI0fPf3n1~C%E2(!9E}J{VTO}bG12v)H-$5&w=T{dh!L;( z;0kmS7#f*5} zh08H3f)hYOMIp*jMS0Aw{GaofIrv5N^A!e{kcnO?6apPW`mC=nIGmElskw5MMAQ!p zs^E6ec(-J-%h>U1<3E#|*(>spLCE6&uK0ItJ7p!~U@q!S7|MaVX5ywnjH|TS#E%q# zBpyOEwQ^sr&fp3VllH0s3x!}Y`EsmdA3w=*ge>B_A_*lWBRE$_RGDeh3hxBbzm?tz zLNCdCYt-(eYIdC@X%5MA30# zq_J6ULWA{rnmB0P$VsBT;nmvZO&dTS)l0p0@=aw5%P)&{fshgzu>h)Ip#7_LAXffr z;s6|eQj9SbF_RX&jI#V6Ld$a_tL=m1XG#t!GWW(@7P06(+9=e;T4E+vlPeCbqhZ2Q z5vnxp`cR3Y3Q3U6K(_>>55gI98(7}J?AYpB%hyV^_Vlh9Rj{>7y6^-oo-H}U1U0hu z$uF?7*G_|+e=siMZL_Rjz@$gkDlEHG8i51b@807_$P64_KsbCPUOT`{jL;PwZR3f| zMHQ6e(Tc3E!~eIdIXa44%M6iT4jwFiw<#5B$q^AK9;jy$1Vd2^{EV^8(5KvL>2DWG zoNHm>-H_$LY|2vQw=(qfnQ`uGKDvMy9Ld)h>Cb!tJ2P zyH1)c7>J`TP<&7?`V$vq0@P;1Z#PQ&CVEXskWaR$IyeXH!*sz-0NXDj^JprfjKdel=>h0SjN>c#@_Hu;q zRDw3zM3=4sR`C%#=!`d+CyUykLN8~Mw5^;U9O0E@H4sU}a@od>r+2~x%tUBZl4KK$ z>vz#juXitX4>!cXOR+67S(B$t6!R;FIDZUV{tz{$UMax}h>1u~l(&o^dc75UIhoi! zPCjcAg^fWtSajK?T?W`Y`^nyG#n^hxK{so`t8iCZJthfPz4v=7*F4;tY*pa5qLxq(`f!l0*=ArB|Zgoed zHb%;A;|f_Po;?F1DAajRJ8j1YdF9 zarlbHPMK6wNwnO_y$OGxH3d2m^4kPP$X32ZSLw;)UI=k~gRmbg|7Y<&7PY<$gAHDq$_Y_nG~)q29Sm zOjE2e>ruGB4fhT*05|`RArs#ZGh?(E`XiK*rQ4TBO=CWj?9rk+CL3?4lVv42FL3iz z*nyuknk-ll>&Xk58io!>kd~DUM zwMT0RqQA-ypXeBA`ci(n_r>fUUs#yWMsaYmYJgSwLWpgTfdW2)+a7cJ$Id6oyjf*{ z%Xkohlw?_iZ|BvM$*)YH8x5YN*99`z)o{t_Q!!3I_n@Ip%-})7kc6(@Lb34EpLxMF z+}VPKM7%Ng-jne~fsLS)0n!Zjc}2r(fW{QcwbOkM#Q#)c8)QTL#mi>o9UQV3VK@

k2&^@4RHau#bsD2GvkRC`dNju-X`C{pKO^lkUm<09nEhb%}e;U~&1P4|& zJY<5)Mi%WesVbO`#FI8zgUuW$B3{zolxefh1tpCx^5PLCu3&NDDRpxtuu_ifgDD2n zuJ#^l&mEK62?Q&&^5`c=jTjpTze3`c3C0SE#vJ`!>Iv+R0~t)*de%K=sb}mHdLN5Q zU(ql!s-(E<&#cN0GkFIxB9W2Xv8+eQ_+um{$Y!T3$1LfIaLTvAMj{D1bZ&ZkdB^;OJcs+z9L zZsCEA9%tWyAEK(9YSN_bM*uo%RRlf=M6{EjWanh9&$+h+)VqNxjKx4J#mKR3(e|ZH z0WeI52XUEK z&fht;#hx1_j0#??ap?fkxRisD_;I$%oFX1cqQ#>>^Z7M==@4f*!Z_+|hY-yyi$a6G zX=157YfEKjYx}4hjWE3I#=+ z^lXpG-k04dI;__9Zmp| z)P4wTL2PR!)2t5b%s*YD#()GjraWSf5!IIEk2q=I65L9nW)KlK!ph=J26IK9d~g1q z`?u9dD`~(dxa<+f$igD_pxpr^I9uhtCeeZuafAu30|zoh>!SxV0p#U^od^;=}p@j|!uVo)JSaTho#Ym)4iM(o18F}ZdU9W_z%C zkoItnfm6k780>~A!>6JH=@>9PZUfnC1nyQ35GPhd8_ zI+DIDwxe0!SJtsE)tCv(8(9@}Kx4&1W^FvmBillR#1bnH@ueoYxaQVMr4~RM<<%*3 z59;6v<_hKC)DG*_?koEPhR4+nTNrN`Os)C76JmlOM&2@}fb2m)pR^YF``93tj6k%s~ch@wei9whFxU3-dL;+)3; z3vmY72_KpdWL?*!xATywV_E4Kk}Tri6j;1)s_CZ7qMMsCip%W1gGVVDAdTzPxXFN` z%O@23Wbf!bC2Wep&83<|UmGODZ)7D#VzyRk{$iDnGbDdxc}io3gCeNg9)k!~axI^M zP!2?nU4UyocY0`3et!b<;yU`oE*l!ID-Pl>2%tje-ZjqR`;bgbAtC>jX(I^D(l#A) z{ici~c(A9^2_gxQIvD$Eq5yA$e7|wHF2WXxA#@}N--<<3=H`I54Ml!;HO^o2~U&NFMPsBZ#b*(@zr#^fqksy#g8B1DmoLi zZ$ozKp7;V^rAv}p*>kYZ*OR>r_Ny0fozbS~t^qQ(`Rg-8LJOE{3GGmHuZ&t}>|588 zD+K#pgP&_#KGG}E{bYKbs%h&LrFY!a8*Rk5K3-oFMq~g+a-G*DCZef+Dfq?Z39iqNs z8d!LZCUinToS%J(8Z18ZAkx02W-j=2EMl`x;$MMeuCBeuwYSYEQr=)Y@c}Fu`}Qc zO&g?0qpBC0zJ0l0vtr2OlWtl52q>&d_Jh64`Os^op=s>EwwYKvx0VDPkFIHxME3C< ze7D7Q8~u$1Wv#W1sET@)_gS#=UKBMhdC|3K2Y8P;xPNRtYoQj@Qizv??N>S3y(ZI(147pl$PV2VhfGmri^DX40m+;sJMvyHe>{5OVkrhJqU-FMjDCRX3q_4 z6+q6s;AN7zVt}rIi>ejN`W5P>NQ3|+iG(_q{tFL_`GZmu7)__tS%BZX-}x5Qq;2v9 zW}$6Jbr2fCq6vFzz0U;{Rg_TDPs!a?NW5}qaGCkto#iLc@hz`Wfl%JPl%(>jXZG`s zk&y@#yP*UiZ`m&YR51zsDmasRy_=awy(tm>S=PX?1E+0x8+pQ?A|gV-2;ffoe=TK} z30ORrw=%%-xpLz02Zf1c>d>43G-B0q4xWSB!0Ewlg)A5fdCr-cH^gXBGxflOlM`Ix z<5Z0nNIQc&YY$F5X?KMyzMdeZTdR&TF;;_F3PGv%N$=Pp``@Mod?HD6;=qnlk`Rtf z+l*H;LDP;ZtKh@y-mcE=M+WO59Un8Vb$WSCUPewIO)Cge9B~59xiS{36+cDA;gGRf8Ezy$^)OT5ntBA zwm!e>)7pn=RI?A7n(22&2)fvmmk}p-3!N%>vwlgUFNc}Ka^*o6g_;RRlC-$P(!8yO zjckKiD3INKLae-N6)Z)xqvEvWI;RYSqMNu{-<+YpBcCXbOg}GSJgg12Kn7mZBok$E zURgZ?y=g-6hL8+}7jcit>)1k=02l=wZb(~;r#ky%5@w*7Mh3$VIKuaWXeUc2`6t0(Gw%zpcTxYA+yV`W8-%9(+>h$>dg!{$NEvsXdDE}c&CV*FS-;& z4#K+WBdYKvP?VGOb$bN@)d?0ez#11r-S#jeD8BDE3cl-iBv0BF)+Dh0jETjEVH@R) zIE`_^ZW^bWyNm5&UuD znHz0*yNvabVPHJRXO*+i@5|*wCwS%n;QZ@$3$s(sLR5K4W0RbPEGLXDlca?az91u0 z7*lB%uWT?BK>@cdaUsf&49PqInV0$c^B)=3)UW7NyQ_kT3()^UcQ8(g_P@Kn<5a1o zqH^%Pm;Ip3O{h2GDQEHi>O#(byjGDJsaV6SoXxs#EaMw6-wA-BAHw*Cr{`^b2Cz_fo$5# zRvac@g6#LvSZEN2K4l&O)`2QS)gb2@<%9+h;LtLD^JWnbxOOBA1$-LV5vK^+a2|hq zBq4nI_-uDNLn0d2f<4%2tG+YjF8J zWR{CJeX+n;BfJ?~QbSzb7G0Y$b=2?wM^-~cO>2sF57igGKHM1SuC|j0sGV``JAs>{ z8R9uBR|gcI2b49hzqf)k!QS>?D<1>!!0B!uY*nLjmZ#nk_*~)7(e6DfqqWsNcK6wD z>8Zv*Z4HHs6o-B0hLzPC6fAJ0bi&C8Shx?%b9nzDsFRc1fB$`-FT{8f0({73aW;g{ zbcu(^K(kuL95zxNVyh(2;qoJXUmF#m0)~acrClp%I6$6H6Ec;r7vn{U5Om?AgaI(b z59uPBh3(z*%Ft9OC8=92UNxErIWrfhF&)Q^?BJ+eyATG#vC$2=MhF;H2Shgias`p% zt>2~&2d>iS{=KNmBmrJ3T%F=HlusJSeB0?Fpg|;GJ=3%Ce8NmGew;}doa2B+IkaGG^zYQ3h;GU diff --git a/meteor/i18n/nn.po b/meteor/i18n/nn.po index cc2d2cc3f3f..cf147f35b6e 100644 --- a/meteor/i18n/nn.po +++ b/meteor/i18n/nn.po @@ -1,2888 +1,3366 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2022-08-02T12:04:21.289Z\n" -"PO-Revision-Date: 2022-08-02 14:14+0200\n" -"Last-Translator: \n" -"Language-Team: \n" -"Language: nn\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" +"mime-version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 3.1.1\n" - -msgid "Account Page" -msgstr "Brukarkontoside" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"POT-Creation-Date: 2026-04-14T14:17:21.139Z\n" +"PO-Revision-Date: 2026-04-14T14:17:21.139Z\n" +"Language: nn\n" -msgid "Name:" -msgstr "Namn:" +msgid " The index of the Atem media/clip banks" +msgstr "" -msgid "Email:" -msgstr "E-post:" +msgid "({{time}} ago)" +msgstr "(for {{time}} sidan)" -msgid "Old Password" -msgstr "Gammalt passord" +msgid "({{timecode}})" +msgstr "({{timecode}})" -msgid "New Password" -msgstr "Nytt passord" +msgid "" +"(Comma separated list. Empty - will store snapshots of all Rundown " +"Playlists)" +msgstr "" -msgid "Save Changes" -msgstr "Lagre endringer" +msgid "(Default)" +msgstr "" -msgid "Edit Account" -msgstr "Endre brukarkonto" +msgid "(in: {{time}})" +msgstr "(om: {{time}})" -msgid "Organization" -msgstr "Organisasjon" +msgid "(Optional) A name/identifier of the local network where the Atem is located" +msgstr "" -msgid "User roles in organization" -msgstr "Brukarroller i organisasjon" +msgid "(Optional) A name/identifier of the local network where the share is located" +msgstr "" +"(Valfri) Eit namn/ein identifikator for det lokale nettverket der den delte " +"mappa er lokalisert" -msgid "Studio" -msgstr "Studio" +msgid "" +"(Optional) A name/identifier of the local network where the share is " +"located, leave empty if globally accessible" +msgstr "" +"(Valfri) Eit namn/ein identifikator for det lokale nettverket der den delte " +"mappa er lokalisert, la vere tom om den er globalt tilgjengeleg" -msgid "Configurator" -msgstr "Configurator" +msgid "(Optional) This could be the name of the compute" +msgstr "" -msgid "Developer" -msgstr "Developer" +msgid "" +"(Optional) This could be the name of the computer on which the local folder " +"is on" +msgstr "(Valfri) Dette kan vere namnet til datamaskinen som den lokale mappa er på" -msgid "Admin" -msgstr "Admin" +msgid "(Unknown playlist)" +msgstr "(Ukjend køyreplanliste)" -msgid "Remove Self" -msgstr "Fjern denne brukarkontoen" +msgid "(Unknown rundown)" +msgstr "(Ukjend køyreplan)" -msgid "Email Address" -msgstr "E-postadresse" +msgid "" +"(what happened and when, what should have happened, what could have " +"triggered the problems, etcetera...)" +msgstr "" -msgid "Password" -msgstr "Passord" +msgid "{{actionName}} failed! More information can be found in the system log." +msgstr "" -msgid "Sign in" -msgstr "Logg inn" +msgid "{{currentRundownName}} - {{rundownPlaylistName}}" +msgstr "{{currentRundownName}} - {{rundownPlaylistName}}" -msgid "Create New Account" -msgstr "Opprett ny brukarkonto" +msgid "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)" +msgstr "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)" -msgid "Lost password?" -msgstr "Gløymd passord?" +msgid "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago" +msgstr "for {{days}} dagar, {{hours}} t {{minutes}} min {{seconds}} s sidan" -msgid "Send reset email" -msgstr "Send e-post for å nullstille" +msgid "{{frames}} black frames detected in the clip" +msgstr "" -msgid "Go back" -msgstr "Tilbake" +msgid "{{frames}} black frames detected within the clip" +msgstr "" -msgid "Password must be atleast 5 characters long" -msgstr "Passord må vere minst 5 tegn langt" +msgid "{{frames}} freeze frames detected in the clip" +msgstr "" -msgid "Enter your new password" -msgstr "Skriv inn ditt nye passord" +msgid "{{frames}} freeze frames detected within the clip" +msgstr "" -msgid "Set new password" -msgstr "Lagre nytt passord" +msgid "{{hours}} h {{minutes}} min {{seconds}} s ago" +msgstr "for {{hours}} t {{minutes}} min {{seconds}} s sidan" -msgid "Your Account" -msgstr "Din brukarkonto" +msgid "{{indexCount}} indexes was removed." +msgstr "{{indexCount}} indexer vart fjerna." -msgid "About Your Organization" -msgstr "Om din oranisasjon" +msgid "{{minutes}} min {{seconds}} s ago" +msgstr "for {{minutes}} min {{seconds}} s sidan" -msgid "We are mainly" -msgstr "Vi er hovudsakleg" +msgid "{{nrcsName}} Connection" +msgstr "{{nrcsName}}-tilkopling" -msgid "Areas" -msgstr "Område" +msgid "{{prevStatements}} and {{finalStatement}}" +msgstr "" -msgid "Invite User" -msgstr "Inviter brukar" +msgid "{{prevStatements}} or {{finalStatement}}" +msgstr "" -msgid "New User's Email" -msgstr "Ny brukar sin e-post" +msgid "" +"{{reason}} {{sourceLayer}} exists, but is not yet ready on the playout " +"system" +msgstr "" -msgid "New User's Name" -msgstr "Ny brukar sitt namn" +msgid "{{rundownPlaylistName}} (Looping)" +msgstr "{{rundownPlaylistName}} (Looper)" -msgid "Create New User & Send Enrollment Email" -msgstr "Opprett ny brukar og send e-post for innmelding" +msgid "{{seconds}} s ago" +msgstr "for {{seconds}} s sidan" -msgid "Users in organization" -msgstr "Brukarar i organisasjonen" +msgid "{{showStyleVariant}} – {{showStyleBase}}" +msgstr "{{showStyleVariant}} – {{showStyleBase}}" -msgid "Return to list" -msgstr "Gå tilbake til lista" +msgid "{{sourceLayer}} can't be found on the playout system" +msgstr "" -msgid "There is no rundown active in this studio." -msgstr "Fann ingen aktive køyreplanar for dette studioet." +msgid "{{sourceLayer}} doesn't have both audio & video" +msgstr "{{sourceLayer}} har ikkje lyd og/eller bilete" -msgid "This studio doesn't exist." -msgstr "Dette studioet eksisterer ikkje." +msgid "{{sourceLayer}} has {{audioStreams}} audio streams" +msgstr "{{sourceLayer}} har {{audioStreams}} lydstraumar" -msgid "There are no active rundowns." -msgstr "Fann ingen aktive køyreplanar." +msgid "{{sourceLayer}} has the wrong format: {{format}}" +msgstr "{{sourceLayer}}-formatet er ikkje støtta: {{format}}" -msgid "Evaluation" -msgstr "Evaluering" +msgid "{{sourceLayer}} has unsupported source: {{containerLabels}}" +msgstr "" -msgid "Please take a minute to fill in this form." -msgstr "Ver venleg og fyll ut dette skjemaet." +msgid "{{sourceLayer}} is being ingested" +msgstr "{{sourceLayer}} vert prosessert" msgid "" -"Be aware that while filling out the form keyboard and streamdeck commands " -"will not be executed!" -msgstr "OBS! Du kan ikkje utføra Sofie-kommandoar medan du skriv evalueringa!" +"{{sourceLayer}} is in a placeholder state for an unknown workflow-defined " +"reason" +msgstr "" -msgid "Did you have any problems with the broadcast?" -msgstr "Hadde du nokre problem under sendinga?" +msgid "{{sourceLayer}} is in an unknown state: \"{{status}}\"" +msgstr "" -msgid "" -"Please explain the problems you experienced (what happened and when, what " -"should have happened, what could have triggered the problems, etcetera...)" +msgid "{{sourceLayer}} is missing" msgstr "" -"Ver venleg og forklar kva problem du hadde (kva hende og når hende det, kva " -"skulle skjedd, kva kan ha utløyst problema o.s.b.)" -msgid "Your name" -msgstr "Namnet ditt" +msgid "{{sourceLayer}} is missing a file path" +msgstr "{{sourceLayer}} kan ikkje spelast av fordi filnamnet manglar" + +msgid "{{sourceLayer}} is not yet ready on the playout system" +msgstr "{{sourceLayer}} er enno ikkje klar til å spelast ut fra avviklingsserver" -msgid "Save message" -msgstr "Lagre melding" +msgid "{{sourceLayer}} is transferring to the playout system" +msgstr "{{sourceLayer}} overførast til avviklingsserver" -msgid "Save message and Deactivate Rundown" -msgstr "Send evalueringa og deaktiver køyreplanen" +msgid "" +"{{sourceLayer}} is transferring to the playout system but cannot be played " +"yet" +msgstr "" -msgid "No problems" -msgstr "Ingen problem" +msgid "{{studioName}}: Active Rundown" +msgstr "" -msgid "Something went wrong, but it didn't affect the output" -msgstr "Noko gjekk gale, men det virka ikkje inn på sendinga" +msgid "{{studioName}}: Presenter screen" +msgstr "" -msgid "Something went wrong, and it affected the output" -msgstr "Noko gjekk gale, og det virka inn på sendinga" +msgid "{{studioName}}: Prompter" +msgstr "" -msgid "Are you sure?" -msgstr "Er du sikker?" +msgid "14 = 7 lines, 20 = 5 lines" +msgstr "" -msgid "" -"Trimming this clip has timed out. It's possible that the story is currently " -"locked for writing in {{nrcsName}} and will eventually be updated. Make sure " -"that the story is not being edited by other users." +msgid "A device must be assigned to the config to edit the settings" msgstr "" -"Endring av inn-/utpunkt for dette klippet tek lang tid. Det er mogleg " -"manuset i er låst i {{nrcsName}} og at inn-/utpunkt endrast om litt. " -"Forsikre deg om at manuset ikkje vert redigert av andre brukarar." -msgid "Trimming this clip has failed due to an error: {{error}}." -msgstr "Endring av inn-/utpunkt for dette klippet feila: {{error}}." +msgid "" +"A Full System Snapshot contains all system settings (studios, showstyles, " +"blueprints, devices, etc.)" +msgstr "" +"Eit fullt systemsnapshot inneheld alle systeminnstillingar (studio, " +"showstyles, blueprints, einingar o.s.b.)" -msgid "Trimmed succesfully." -msgstr "Endring av inn-/utpunkt var vellukka." +msgid "a second" +msgstr "" msgid "" -"Trimming this clip is taking longer than expected. It's possible that the " -"story is locked for writing in {{nrcsName}}." +"A snapshot of the current Running Order has been created for " +"troubleshooting." +msgstr "Eit snapshot av den gjeldande køyreplanen har verte oppretta." + +msgid "A Studio Snapshot contains all system settings related to that studio" +msgstr "Eit studiosnapshot inneheld alle systeminnstillingar knytt til eit studio" + +msgid "AB Channel Display" msgstr "" -"Endring av inn-/utpunkt for dette klippet tek meir tid enn forventa. Det er " -"mogleg manuset er låst for redigering i {{nrcsName}}." -msgid "Trim \"{{name}}\"" -msgstr "Trim \"{{name}}\"" +msgid "AB Playout devices" +msgstr "" -msgid "OK" -msgstr "OK" +msgid "AB Resolver Channel Display" +msgstr "" -msgid "Cancel" +msgid "Abort" msgstr "Avbryt" -msgid "Remove in-trimming" -msgstr "Nullstill innpunkt" +msgid "Aborting all Media Workflows" +msgstr "" -msgid "In" -msgstr "Inn" +msgid "Aborting Media Workflow" +msgstr "" -msgid "Remove all trimming" -msgstr "Nullstill inn- og utpunkt" +msgid "Accessor ID" +msgstr "Aksessor-id" -msgid "Duration" -msgstr "Lengde" +msgid "Accessor Type" +msgstr "Aksessortype" -msgid "Remove out-trimming" -msgstr "Nullstill utpunkt" +msgid "Accessors" +msgstr "Aksessorer" -msgid "Out" -msgstr "Ut" +msgid "Action" +msgstr "Handling" -msgid "Next" -msgstr "Neste" +msgid "Action {{actionName}} done!" +msgstr "" -msgid "Test test" -msgstr "Test test" +msgid "Action {{actionName}} failed: {{error}}" +msgstr "" -msgid "Until next take" -msgstr "Til neste Take" +msgid "Action Buttons" +msgstr "Handlingsknappar" -msgid "Until next segment" -msgstr "Til neste segment" +msgid "Action Triggers" +msgstr "Handlingsutløysarar" -msgid "Until end of segment" -msgstr "Til slutten av segment" +msgid "Activate \"On Air\"" +msgstr "" -msgid "Until next rundown" -msgstr "Til neste køyreplan" +msgid "Activate \"Rehearsal\"" +msgstr "" -msgid "Until end of showstyle" -msgstr "Til slutten av showstyle" +msgid "Activate (On-Air)" +msgstr "Aktiver (gå ON AIR)" -msgid "Script is empty" -msgstr "Manuset er tomt" +msgid "Activate (Rehearsal)" +msgstr "Aktiver (testmodus)" -msgid "Clip:" -msgstr "Klipp:" +msgid "Activate On Air" +msgstr "" -msgid "Home" -msgstr "Heim" +msgid "Activate Rundown" +msgstr "Aktiver køyreplan" -msgid "Rundowns" -msgstr "Køyreplanar" +msgid "Activating Hold" +msgstr "" -msgid "Test Tools" -msgstr "Testverktøy" +msgid "Activating Rundown Playlist" +msgstr "" -msgid "Status" -msgstr "Status" +msgid "Active" +msgstr "Aktiv" -msgid "Settings" -msgstr "Innstillingar" +msgid "Active Rundown" +msgstr "" -msgid "Account" -msgstr "Konto" +msgid "Active Rundown View" +msgstr "" -msgid "Logout" -msgstr "Logg ut" +msgid "Ad-Lib" +msgstr "Adlib" -msgid "My name is {{name}}" -msgstr "Mitt namn er {{name}}" +msgid "Ad-Lib Action" +msgstr "Adlib-handling" -msgid "Operating Mode" -msgstr "Styringsmodus" +msgid "Add" +msgstr "Legg til" -msgid "Switching operating mode to {{mode}}" -msgstr "Endrer styringsmodus til {{mode}}" +msgid "Add {{filtersTitle}}" +msgstr "Legg til {{filtersTitle}}" -msgid "Prompter" -msgstr "Prompter" +msgid "Add a playout device to the studio in order to configure the route sets" +msgstr "" +"For å kunne redigere omkoplingsgrupper, må du leggje til ein playout-eining " +"til studio" -msgid "End of script" -msgstr "Slutt på manus" +msgid "Add a playout device to the studio in order to edit the layer mappings" +msgstr "" +"For å kunne redigere lagmappingar, må du leggje til ein playout-eining til " +"studio" -msgid "Could not get system status. Please consult system administrator." -msgstr "Kan ikkje innhente status for systemet. Kontakt systemadministrator." +msgid "Add Action Trigger" +msgstr "" -msgid "There are no rundowns ingested into Sofie." -msgstr "Det er ikkje send køyreplanar til Sofie." +msgid "Add button" +msgstr "Legg til knapp" -msgid "Click on a rundown to control your studio" -msgstr "Klikk på ein køyreplan for å kontrollere studioet ditt" +msgid "Add filter" +msgstr "Legg til filter" -msgid "Rundown" -msgstr "Køyreplan" +msgid "Add some source layers (e.g. Graphics) for your data to appear in rundowns" +msgstr "Legg til kjeldelag (til dømes Grafikk) for å vise dine data i køyreplanar" -msgid "Problems" -msgstr "Problem" +msgid "AdLib" +msgstr "Adlib" -msgid "Show Style" -msgstr "Showstyle" +msgid "AdLib Actions are not supported in the current Rundown" +msgstr "" -msgid "On Air Start Time" -msgstr "Sendestart" +msgid "AdLib could not be found!" +msgstr "" -msgid "Expected End Time" -msgstr "Venta sendeslutt" +msgid "AdLib filter" +msgstr "" -msgid "Last updated" -msgstr "Sist oppdatert" +msgid "Adlib Rank" +msgstr "Adlib-rang" -msgid "View Layout" -msgstr "Vis layout" +msgid "Adlib rundowns are not supported for this ShowStyle!" +msgstr "" -msgid "Today" -msgstr "I dag" +msgid "Adlib Testing" +msgstr "" -msgid "Yesterday" -msgstr "I går" +msgid "AdLib Testing" +msgstr "" -msgid "Tomorrow" -msgstr "I morgon" +msgid "AdLibs can be only placed in a currently playing part!" +msgstr "" -msgid "Last" -msgstr "Førre" +msgid "AdLibs on this layer can be queued" +msgstr "Adliber på dette laget kan cues" -msgid "Getting Started" -msgstr "Kom i gong" +msgid "All additional source layers must have active pieces" +msgstr "" -msgid "" -"Start with giving this browser configuration permissions by adding this to " -"the URL: " -msgstr "Først må du gå i konfigurasjonsmodus ved å leggje dette til i url-en: " +msgid "All connections working correctly" +msgstr "Alle tilkoplingar er OK" -msgid "Start Here!" -msgstr "Start her!" +msgid "All devices working correctly" +msgstr "Alle eininger fungerer som dei skal" -msgid "Then, run the migrations script:" -msgstr "Køyr deretter migreringsprosedyra:" +msgid "All is well, go get a" +msgstr "Alt er greitt, gå og finn deg ein" -msgid "Run Migrations to get set up" -msgstr "Køyr migreringsprosedyrar for å setje opp" +msgid "All Screens in a MultiViewer" +msgstr "" -msgid "Migrations" -msgstr "Migrering" +msgid "All steps" +msgstr "Alle steg" -msgid "Documentation is available at" -msgstr "Dokumentasjon er tilgjengelig på" +msgid "Allow direct playing pieces" +msgstr "" -msgid "Use {{nrcsName}} order" -msgstr "Nytt rekkefølgje frå {{nrcsName}}" +msgid "Allow disabling of Pieces" +msgstr "Tillat deaktivering av element" -msgid "Reset Sort Order" -msgstr "Tilbakestill rekkefølgje" +msgid "Allow HOLD mode" +msgstr "" -msgid "Enable configuration mode by adding ?configure=1 to the address bar." +msgid "Allow infinites from AdLib testing to persist" msgstr "" -"Aktiver konfigurasjonsmodus ved å legge til ?configure=1 på slutten av " -"nettadressa." -msgid "You need to run migrations to set the system up for operation." -msgstr "Du må køyre migrering for å klargjere systemet for bruk." +msgid "Allow Read access" +msgstr "Tillat lesing" -#, fuzzy -#| msgid "Drop rundown here to move it out of its current playlist" -msgid "Drop Rundown here to move it out of its current Playlist" -msgstr "Slepp køyreplanen her for å flytta den ut av nåværande speleliste" +msgid "Allow Rundowns to be reset while on-air" +msgstr "" -msgid "Sofie Automation" -msgstr "Sofie" +msgid "Allow Write access" +msgstr "Tillat skriving/lagring" -msgid "version" -msgstr "versjon" +msgid "Also Require Source Layers" +msgstr "" -msgid "System Status" -msgstr "Systemstatus" +msgid "Amount of entries exceeds the limt of 10 000 items." +msgstr "" -msgid "System has issues which need to be resolved" -msgstr "Systemet har problemer som må løysast" +msgid "An error while performing the take, playout may be impacted" +msgstr "" -msgid "Status Messages:" -msgstr "Statusmeldingar:" +msgid "An error while setting the next Part, playout may be impacted" +msgstr "" -msgid "{{showStyleVariant}} – {{showStyleBase}}" -msgstr "{{showStyleVariant}} – {{showStyleBase}}" +msgid "An internal error occured!" +msgstr "" -msgid "Drag to reorder or move out of playlist" -msgstr "Dra for å endre rekkefølgje eller flytta ut av speleliste" +msgid "Another Rundown is Already Active!" +msgstr "Ein annan køyreplan er allereie aktiv!" -msgid "This rundown is currently active" -msgstr "Denne køyreplanen er allereie aktiv" +msgid "Answers" +msgstr "Svar" -msgid "Not set" -msgstr "Ikkje angjeve" +msgid "" +"Any AB Playout devices here will only be active when this or another " +"RouteSet that includes them is active" +msgstr "" -msgid "This rundown will loop indefinitely" -msgstr "Denne køyreplanen vil gå i ein uendeleg loop" +msgid "APM Enabled" +msgstr "AMP aktivert" -msgid "({{timecode}})" -msgstr "({{timecode}})" +msgid "APM Transaction Sample Rate" +msgstr "Prøvefrekvens for AMP-transaksjonar" -msgid "Re-sync rundown data with {{nrcsName}}" -msgstr "Ikkje synkronisert med MOS/{{nrcsName}}" +msgid "Append" +msgstr "Legg til" -msgid "Delete" -msgstr "Slett" +msgid "Append or Replace" +msgstr "Legg til eller erstatt" -msgid "Standalone Shelf" -msgstr "Frittståande skuff" +msgid "Append rows" +msgstr "" -msgid "Rundown & Shelf" -msgstr "Køyreplan & skuff" +msgid "Application credentials" +msgstr "Brukarnamn/passord (Application Credentials)" -msgid "Default" -msgstr "Standard" +msgid "Application Performance Monitoring" +msgstr "Overvaking av yting for applikasjonar (AMP)" -msgid "Delete rundown?" -msgstr "Slette køyreplanen?" +msgid "Apply" +msgstr "" -msgid "Are you sure you want to delete the \"{{name}}\" rundown?" -msgstr "Er du viss på at du vil slette køyreplanen \"{{name}}\"?" +msgid "Apply blueprint upgrades" +msgstr "" -msgid "Please note: This action is irreversible!" -msgstr "Merk: Denne handlinga kan du ikkje angre!" +msgid "Apply Config" +msgstr "" -msgid "Re-Sync rundown?" -msgstr "Synkroniser køyreplanen med ENPS på ny?" +msgid "Are you sure you want to activate Rehearsal Mode?" +msgstr "Er du sikker på at du vil gå i testmodus?" -msgid "Re-Sync" -msgstr "Synkroniser" +msgid "" +"Are you sure you want to deactivate this Rundown\n" +"(This will clear the outputs)" +msgstr "" +"Er du sikker på at du vil deaktivere denne køyreplanen?\n" +"(Dette vil nullstille alle utgangar.)" -msgid "Are you sure you want to re-sync the \"{{name}}\" rundown?" +msgid "" +"Are you sure you want to deactivate this rundown?\n" +"(This will clear the outputs.)" msgstr "" -"Er du viss på at du vil synkronisere køyreplanen \"{{name}}\" med ENPS?" -msgid "Start time is close" -msgstr "Oppgitt sendestart er kvart augeblink" +msgid "Are you sure you want to delete output layer \"{{outputId}}\"?" +msgstr "" -msgid "Yes" -msgstr "Ja" +msgid "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?" +msgstr "Er du sikker på at du vil slette kjeldelaget \"{{sourceLayerId}}\"?" -msgid "No" -msgstr "Nei" +msgid "Are you sure you want to delete the \"{{name}}\" rundown?" +msgstr "Er du viss på at du vil slette køyreplanen \"{{name}}\"?" -msgid "" -"You are in rehearsal mode, the broadcast starts in less than 1 minute. Do " -"you want to reset the rundown and go into On-Air mode?" -msgstr "" -"Du er i testmodus og sendinga startar om mindre enn eitt minutt. Vil du " -"laste inn køyreplanen på nytt og gjere klar til sending?" +msgid "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?" +msgstr "Er du sikker på at du vil slette blueprintet \"{{blueprintId}}\"?" -msgid "Hold" -msgstr "Hold" +msgid "Are you sure you want to delete the shelf layout \"{{name}}\"?" +msgstr "Er du sikker på at du vil slette layouten \"{{name}}\"?" -msgid "Could not find a Piece that can be disabled." -msgstr "Kunne ikkje finne eit element som kan skippes." +msgid "Are you sure you want to delete the show style \"{{showStyleId}}\"?" +msgstr "Er du sikker på at du vil slette showstylen \"{{showStyleId}}\"?" -msgid "Failed to execute take" -msgstr "Kunne ikkje gjennomføre Take" +msgid "Are you sure you want to delete the studio \"{{studioId}}\"?" +msgstr "Er du sikker på at du vil slette studioet \"{{studioId}}\"?" -msgid "" -"The rundown you are trying to execute a take on is inactive, would you like " -"to activate this rundown?" -msgstr "" -"Du prøve å gjere ein Take i ein inaktiv køyreplan. Vil du aktivere denne " -"køyreplanen?" +msgid "Are you sure you want to delete this AdLib?" +msgstr "Er du sikker på at du vil slette denne adliben?" -msgid "Activate (Rehearsal)" -msgstr "Aktiver (testmodus)" +msgid "Are you sure you want to delete this Bucket?" +msgstr "Er du sikker på at du vil slette denne bøtta?" -msgid "Activate (On-Air)" -msgstr "Aktiver (gå ON AIR)" +msgid "Are you sure you want to delete this device: \"{{deviceId}}\"?" +msgstr "Er du sikker på at du vil fjerne eininga \"{{deviceId}}\"?" -msgid "Failed to activate" -msgstr "Kunne ikkje aktivere" +msgid "Are you sure you want to empty (remove all adlibs inside) this Bucket?" +msgstr "Er du sikker på at du vil tømme denne bøtta (fjerner alle adliber)?" msgid "" -"Something went wrong, please contact the system administrator if the problem " -"persists." -msgstr "Noko gikk gale, kontakt systemadministrator om problemet held fram." +"Are you sure you want to force the migration? This will bypass the " +"migration checks, so be sure to verify that the values in the settings are " +"correct!" +msgstr "" +"Er du sikker på at du vil tvinge migreringa? Dette gjer at du hoppar over " +"migreringskontrollane, så ver sikker på at verdiane oppgitt i innstillingar " +"er korrekte!" -msgid "Another Rundown is Already Active!" -msgstr "Ein annan køyreplan er allereie aktiv!" +msgid "Are you sure you want to import the contents of the file \"{{fileName}}\"?" +msgstr "" + +msgid "Are you sure you want to re-sync the \"{{name}}\" rundown?" +msgstr "Er du viss på at du vil synkronisere køyreplanen \"{{name}}\" med ENPS?" msgid "" -"The rundown \"{{rundownName}}\" will need to be deactivated in order to " -"activate this one.\n" -"\n" -"Are you sure you want to activate this one anyway?" +"Are you sure you want to re-sync the Rundown?\n" +"(If the currently playing Part has been changed, this can affect the output)" msgstr "" -"Køyreplanen \"{{rundownName}}\" må deaktiveres for å aktivere denne " -"køyreplanen.\n" -"\n" -"Er du sikker på at du ønsker å aktivere?" - -msgid "Activate Anyway (Rehearsal)" -msgstr "Aktiver uansett (testmodus)" +"Er du sikker på at du vil gjenopprette synkronisering mot ENPS for denne " +"køyreplanen?\n" +"(Dette kan virke inn på pågåande sending)" -msgid "Activate Anyway (On-Air)" -msgstr "Aktiver uansett (gå ON AIR)" +msgid "Are you sure you want to remove {{type}} \"{{deviceId}}\"?" +msgstr "Er du sikker på at du vil fjerne eininga {{type}} \"{{deviceId}}\"?" -msgid "Do you want to activate this Rundown?" -msgstr "Vil du aktivere denne køyreplanen?" +msgid "Are you sure you want to remove all Variants in the table?" +msgstr "" msgid "" -"The planned end time has passed, are you sure you want to activate this " -"Rundown?" +"Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\n" +"Route Sets assigned to this group will be reset to no group." msgstr "" -"Det planlagte sluttidspunktet er passert, er du sikker på at du vil aktivere " -"denne køyreplanen?" +"Er du sikker på at du vil fjerne eksklusivitetsgruppa \"{{eGroupName}}\"?\n" +"Omkoplingar satt til denne gruppa vil bli resatt til inga gruppe." -msgid "Are you sure you want to activate Rehearsal Mode?" -msgstr "Er du sikker på at du vil gå i testmodus?" +msgid "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?" +msgstr "Er du sikker på at du fil fjerne mappinga for laget \"{{mappingId}}\"?" + +msgid "Are you sure you want to remove the AB Player \"{{playerId}}\"?" +msgstr "" msgid "" -"Are you sure you want to deactivate this Rundown?\n" -"(This will clear the outputs)" +"Are you sure you want to remove the device \"{{deviceName}}\" and all of " +"it's sub-devices?" msgstr "" -"Er du sikker på at du vil deaktivere denne køyreplanen?\n" -"(Dette vil nullstille alle utgangar.)" +"Er du sikker på at du vil fjerne eninga \"{{deviceName}}\" og alle " +"undereiningane?" -msgid "The rundown can not be reset while it is active" -msgstr "Ein aktivert køyreplan kan ikkje tilbakestillast" +msgid "Are you sure you want to remove the Package Container \"{{containerId}}\"?" +msgstr "Er du sikker på at du vil fjerne pakkecontaineren \"{{containerId}}\"?" msgid "" -"A snapshot of the current Running Order has been created for troubleshooting." -msgstr "Eit snapshot av den gjeldande køyreplanen har verte oppretta." +"Are you sure you want to remove the Package Container Accessor " +"\"{{accessorId}}\"?" +msgstr "" +"Er du sikker på at du vil fjerne pakkekontainer-aksessoren " +"\"{{accessorId}}\"?" -msgid "Prepare Studio and Activate (Rehearsal)" -msgstr "Førebu studio og aktiver testmodus" +msgid "" +"Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to " +"\"{{newLayerId}}\"?" +msgstr "" +"Er du sikker på at du vil fjerne omkoplinga frå \"{{sourceLayerId}}\" til " +"\"{{newLayerId}}\"?" + +msgid "Are you sure you want to remove the Route Set \"{{routeId}}\"?" +msgstr "Er du sikker på at du vil fjerne omkoplingsgruppa \"{{routeId}}\"?" + +msgid "Are you sure you want to remove the Variant \"{{showStyleVariantId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to replace the blueprints with the file " +"\"{{fileName}}\"?" +msgstr "Er du sikker på at du vil erstatte blueprints frå fila \"{{fileName}}\"?" + +msgid "" +"Are you sure you want to reset all overrides for Packing Container " +"\"{{id}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to reset all overrides for the mapping for layer " +"\"{{mappingId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to reset all overrides for the output layer " +"\"{{outputLayerId}}\"?" +msgstr "" + +msgid "Are you sure you want to reset all overrides for the selected row?" +msgstr "" + +msgid "" +"Are you sure you want to reset all overrides for the source layer " +"\"{{sourceLayerId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to reset the database version?\n" +"Only do this if you plan on running the migration right after." +msgstr "" +"Er du sikker på at du vil nullstille databaseversjonen?\n" +"Berre gjer dette om du har tenkt å køyre ei migrering med ein gong." + +msgid "Are you sure you want to restart this device?" +msgstr "Er du sikker på at du vil starte denne eininga på nytt?" + +msgid "" +"Are you sure you want to restart this Sofie Automation Server Core: " +"{{name}}?" +msgstr "Er du sikker på at du vil starte Sofie Core: {{name}} om att?" + +msgid "" +"Are you sure you want to restore the system from the snapshot file " +"\"{{fileName}}\"?" +msgstr "" +"Er du sikker på at du vil tilbakestille systemet frå denne snapshotfila " +"\"{{fileName}}\"?" + +msgid "Are you sure you want to skip the fix up config step for {{name}}" +msgstr "" + +msgid "" +"Are you sure you want to update the blueprints from the file " +"\"{{fileName}}\"?" +msgstr "Er du sikker på at du vil oppdatere blueprints frå fila \"{{fileName}}\"?" + +msgid "" +"Are you sure you want to upload the shelf layout from the file " +"\"{{fileName}}\"?" +msgstr "" +"Er du sikker på at du vil laste opp layout for skuff frå fila " +"\"{{fileName}}\"?" + +msgid "" +"Are you sure, do you really want to REMOVE the Snapshot " +"\"{{snapshotName}}\"?\r\n" +"This cannot be undone!!" +msgstr "" + +msgid "Are you sure?" +msgstr "Er du sikker?" + +msgid "" +"Are you sure? This will cause the whole Sofie system to be unresponsive " +"several seconds!" +msgstr "" + +msgid "Around 10 minutes ago" +msgstr "Cirka 10 minutt sidan" + +msgid "Assign" +msgstr "Tilordne" + +msgid "Assigned Show Styles" +msgstr "" + +msgid "Assigned Studios" +msgstr "" + +msgid "Attached Subdevices" +msgstr "Tilkopla undereiningar" + +msgid "Audio Mixing" +msgstr "Lydmiksing" + +msgid "Authorize App Access" +msgstr "" + +msgid "Auto" +msgstr "Auto" + +msgid "AutoNext in QuickLoop behavior" +msgstr "" + +msgid "Available Screens for Studio {{studioId}}" +msgstr "" + +msgid "Bad" +msgstr "Feil" + +msgid "Bank Index" +msgstr "" + +msgid "Base URL" +msgstr "Base-url" + +msgid "Base url to the resource (example: http://myserver/folder)" +msgstr "Base-url for ressursen (døme: http://minserver/mappe)" + +msgid "Baseline needs reload, this studio may not work until reloaded" +msgstr "" +"Baseline må lastast om att, dette studioet vil kanskje ikkje fungere før " +"baseline er lasta om att" + +msgid "Behavior" +msgstr "Oppførsel" + +msgid "Blueprint" +msgstr "Blueprint" + +msgid "Blueprint config has changed" +msgstr "" + +msgid "Blueprint config preset" +msgstr "" + +msgid "" +"Blueprint config preset has been changed. From \"{{ oldValue }}\", to \"{{ " +"newValue }}\"" +msgstr "" + +msgid "Blueprint config preset is missing" +msgstr "" + +msgid "Blueprint config preset not set" +msgstr "" + +msgid "Blueprint Configuration" +msgstr "Blueprintkonfigurasjon" + +msgid "Blueprint has a new version" +msgstr "" + +msgid "Blueprint has been changed. From \"{{ oldValue }}\", to \"{{ newValue }}\"" +msgstr "" + +msgid "Blueprint ID" +msgstr "Blueprint-id" + +msgid "Blueprint Name" +msgstr "Blueprintnamn" + +msgid "Blueprint not found!" +msgstr "" + +msgid "Blueprint not set" +msgstr "Blueprint ikkje valt" + +msgid "Blueprint Type" +msgstr "Blueprinttype" + +msgid "Blueprint Version" +msgstr "Blueprintversjon" + +msgid "Blueprints" +msgstr "Blueprint" + +msgid "Blueprints updated successfully." +msgstr "Blueprints blei oppdatert." + +msgid "Bottom" +msgstr "" + +msgid "Box color" +msgstr "" + +msgid "BREAK" +msgstr "PAUSE" + +msgid "Break In" +msgstr "Pause om" + +msgid "Bucket AdLib is not compatible with this Rundown!" +msgstr "" + +msgid "Bucket not found!" +msgstr "" + +msgid "Button" +msgstr "Knapp" + +msgid "Button height scale factor" +msgstr "Høgdeskala for knapp" + +msgid "Button mapping" +msgstr "" + +msgid "Button width scale factor" +msgstr "Breiddeskala for knapp" + +msgid "By Parts" +msgstr "" + +msgid "By Segments" +msgstr "" + +msgid "Camera" +msgstr "Kamera" + +msgid "Camera Screen" +msgstr "" + +msgid "Can Generate Adlib Testing Rundown" +msgstr "" + +msgid "Can not be used during a hold!" +msgstr "" + +msgid "Cancel" +msgstr "Avbryt" + +msgid "Cancel currently pressed hotkey" +msgstr "Avbryt den trykte tasten" + +msgid "Cannot activate HOLD before a part has been taken!" +msgstr "" + +msgid "Cannot activate HOLD between the current and next parts" +msgstr "" + +msgid "Cannot activate HOLD once an adlib has been used" +msgstr "" + +msgid "Cannot cancel HOLD once it has been taken" +msgstr "" + +msgid "Cannot connect to the {{platformName}}: {{reason}}" +msgstr "" + +msgid "Cannot perform take for {{duration}}ms" +msgstr "" + +msgid "Cannot play this AdLib because it is marked as Floated" +msgstr "Kan ikkje spele av adlib fordi den er markert som på vent (float)" + +msgid "Cannot play this AdLib because it is marked as Invalid" +msgstr "Kan ikkje spele av adlib fordi den er markert som ugyldig" + +msgid "Cannot play this adlib because source layer is not queueable" +msgstr "Kan ikkje spele av adlib fordi den ikkje kan setjast i kø på kjeldelaget" + +msgid "Cannot remove the rundown \"{{name}}\" while it is on-air." +msgstr "" + +msgid "Cannot take close to an AUTO" +msgstr "" + +msgid "Cannot take during a transition" +msgstr "" + +msgid "Cannot take unplayable AdLib" +msgstr "" + +msgid "CasparCG on device \"{{deviceName}}\" restarting..." +msgstr "CasparCG på \"{{deviceName}}\" startar på nytt..." + +msgid "Center" +msgstr "" + +msgid "Change to fullscreen mode" +msgstr "Fullskjermmodus" + +msgid "Channel Name" +msgstr "Kanalnavn" + +msgid "" +"Check layer types to select all layers of that type, or check individual " +"layers for more specific filtering." +msgstr "" + +msgid "Check the console for troubleshooting data from device \"{{deviceName}}\"!" +msgstr "Sjekk konsollen for feilsøkingsdata frå eninga \"{{deviceName}}\"!" + +msgid "Cleanup" +msgstr "Opprydding" + +msgid "Cleanup old data" +msgstr "Rydd opp i gamle data" + +msgid "Cleanup old database indexes" +msgstr "Rydd opp i gamle databaseindexer" + +msgid "Clear {{layerName}}" +msgstr "Tøm {{layerName}}" + +msgid "Clear filter" +msgstr "" + +msgid "Clear queued segment" +msgstr "Fjern cuet tittel" + +msgid "Clear QuickLoop" +msgstr "" + +msgid "Clear QuickLoop End" +msgstr "" + +msgid "Clear QuickLoop Start" +msgstr "" + +msgid "Clear Source Layer" +msgstr "Tøm kjeldelag" + +msgid "Clear value" +msgstr "" + +msgid "Clearing SourceLayer" +msgstr "" + +msgid "Click anywhere for fullscreen" +msgstr "" + +msgid "Click on a rundown to control your studio" +msgstr "Klikk på ein køyreplan for å kontrollere studioet ditt" + +msgid "Click or press Enter for fullscreen" +msgstr "" + +msgid "Click to show available Package Containers" +msgstr "Klikk for å vise tilgjengelege pakkekntainere" + +msgid "Click to show available Show Styles" +msgstr "Klikk for å vise tilgjengelege showstyles" + +msgid "Client IP" +msgstr "Klient-ip" + +msgid "Clip starts with {{frames}} black frames" +msgstr "" + +msgid "Clip starts with {{frames}} freeze frames" +msgstr "" + +msgid "Clips" +msgstr "Klipp" + +msgid "Close" +msgstr "Lukk" + +msgid "Close Properties Panel" +msgstr "" + +msgid "Close Rundown" +msgstr "" + +msgid "Comma-separated list of studio labels to filter by. Leave empty for all." +msgstr "" + +msgid "Comma-separated speeds in px/frame" +msgstr "" + +msgid "Compatible Studios" +msgstr "" + +msgid "Completed with warnings" +msgstr "" + +msgid "Config Fix Up must be run or ignored before the configuration can be edited" +msgstr "" + +msgid "Config for {{name}} fix failed" +msgstr "" + +msgid "Config for {{name}} fixed successfully" +msgstr "" + +msgid "Config for {{name}} upgraded failed" +msgstr "" + +msgid "Config for {{name}} upgraded successfully" +msgstr "" + +msgid "Config has not been applied before" +msgstr "" + +msgid "Config ID: " +msgstr "" + +msgid "Config looks good" +msgstr "" + +msgid "Config preset" +msgstr "" + +msgid "Config preset is missing" +msgstr "" + +msgid "Config preset not set" +msgstr "" + +msgid "Config requires fixing up before it can be validated" +msgstr "" + +msgid "" +"Config value \"{{ name }}\" has changed. From \"{{ oldValue }}\", to \"{{ " +"newValue }}\"" +msgstr "" + +msgid "Configurable Screens" +msgstr "" + +msgid "" +"Configuration for this Gateway has moved to the Studio Peripheral Device " +"settings" +msgstr "" + +msgid "Configure display options" +msgstr "" + +msgid "" +"Configure which pieces should display their assigned AB resolver channel " +"(e.g., \"Server A\") on various screens. This helps operators identify " +"which video server is playing each clip." +msgstr "" + +msgid "Confirm" +msgstr "" + +msgid "Connect some devices to the playout gateway" +msgstr "Kople til ein eller fleire einingar til playout-gatewayen" + +msgid "Connect to {{deviceName}}" +msgstr "" + +msgid "Connected" +msgstr "Tilkopla" + +msgid "Connected App Containers" +msgstr "Tilkopla app-kontainere" + +msgid "Connected Expectation Managers" +msgstr "" + +msgid "Connected to the {{platformName}}." +msgstr "" + +msgid "Connected Workers" +msgstr "Tilkopla arbeidarar" + +msgid "Connecting to the {{platformName}}" +msgstr "" + +msgid "Container Status" +msgstr "" + +msgid "Control mode" +msgstr "" + +msgid "Control modes" +msgstr "" + +msgid "" +"Controls for exposed Route Sets will be displayed to the producer within " +"the Rundown View in the Switchboard." +msgstr "" +"Kontroller for eksponerte omkoplingsgrupper vil verte synt for producer i " +"køyreplansvisninga i omkoplingspanelet." + +msgid "Core" +msgstr "" + +msgid "Core + Worker processing time" +msgstr "" + +msgid "Core System settings" +msgstr "Systeminnstillingar for Core" + +msgid "" +"Could not create a snapshot for the evaluation, because the previous one " +"was created just moments ago. If you want another snapshot, try again in a " +"couple of seconds." +msgstr "" + +msgid "Could not get system status. Please consult system administrator." +msgstr "Kan ikkje innhente status for systemet. Kontakt systemadministrator." + +msgid "Could not restart core: {{err}}" +msgstr "" + +msgid "Could not restart Playout Gateway \"{{playoutDeviceName}}\"." +msgstr "Playout-gateway \"{{playoutDeviceName}}\" kunne ikkje startas om att." + +msgid "Create Adlib Testing Rundown" +msgstr "" + +msgid "Create new Bucket" +msgstr "Opprett ny bøtte" + +msgid "Created" +msgstr "Oppretta" + +msgid "Creating a new Bucket" +msgstr "" + +msgid "Creating Adlib Testing Rundown" +msgstr "" + +msgid "Creating Snapshot for debugging" +msgstr "" + +msgid "Critical problems" +msgstr "" + +msgid "Critical Problems" +msgstr "" + +msgid "Cron jobs" +msgstr "Cron-jobbar" + +msgid "Current Part" +msgstr "Noverande del" + +msgid "Current part can contain next pieces" +msgstr "" + +msgid "Current Segment" +msgstr "Noverande tittel" + +msgid "Custom Classes" +msgstr "Tilpassa klasser" + +msgid "Custom Hotkey Labels" +msgstr "Eigendefinerte etikettar for hurtigtastar" + +msgid "Deactivate" +msgstr "Deaktiver" + +msgid "Deactivate \"On Air\"" +msgstr "" + +msgid "Deactivate Rundown" +msgstr "Deaktiver køyreplan" + +msgid "Deactivate Studio" +msgstr "" + +msgid "Deactivating other Rundown Playlist, and activating this one" +msgstr "" + +msgid "Deactivating Rundown Playlist" +msgstr "" + +msgid "Debug" +msgstr "" + +msgid "Debug mode" +msgstr "" + +msgid "Debug State" +msgstr "" + +msgid "Default" +msgstr "Standard" + +msgid "Default (hide)" +msgstr "" + +msgid "Default Layout" +msgstr "Standardlayout" + +msgid "Default shelf height" +msgstr "Standard høyde for skuff" + +msgid "Default State" +msgstr "Standardtilstand" + +msgid "Delete" +msgstr "Slett" + +msgid "Delete Action Trigger" +msgstr "" + +msgid "Delete layout?" +msgstr "Slett layout?" + +msgid "Delete mapping" +msgstr "" + +msgid "Delete output layer" +msgstr "" + +msgid "Delete rundown?" +msgstr "Slette køyreplanen?" + +msgid "Delete source layer" +msgstr "" + +msgid "Delete this AdLib" +msgstr "Slett denne adliben" + +msgid "Delete this Blueprint?" +msgstr "Slett dette blueprintet?" + +msgid "Delete this Bucket" +msgstr "Slett denne bøtta" + +msgid "Delete this item?" +msgstr "Slett dette elementet?" + +msgid "Delete this output?" +msgstr "Slett denne utgangen?" + +msgid "Delete this Show Style?" +msgstr "Slett denne showstylen?" + +msgid "Delete this Studio?" +msgstr "Slett dette studioet?" -msgid "Deactivate" -msgstr "Deaktiver" +msgid "Device" +msgstr "" -msgid "Take" -msgstr "Take" +msgid "Device \"{{deviceName}}\" restarting..." +msgstr "\"{{deviceName}}\" starter på ny..." -msgid "Reset Rundown" -msgstr "Tilbakestill køyreplanen" +msgid "Device {{deviceName}} is disconnected" +msgstr "{{deviceName}} er fråkopla" -msgid "Reload {{nrcsName}} Data" -msgstr "Last inn {{nrcsName}}-data på nytt" +msgid "Device ID" +msgstr "Eining-id" -msgid "Store Snapshot" -msgstr "Lagre snapshot" +msgid "Device is already attached to another studio." +msgstr "" -msgid "No actions available" -msgstr "Ingen køyreplanval tilgjengelege i påsynmodus" +msgid "Device is missing configuration schema" +msgstr "" -msgid "Add ?studio=1 to the URL to enter studio mode" -msgstr "Leggje til ?admin=1 på slutten av nettadressa for å starte studiomodus" +msgid "Device is of unknown type" +msgstr "" -msgid "Exit" -msgstr "Lukk" +msgid "Device Name" +msgstr "Einingsnamn" -msgid "Error" -msgstr "Feil" +msgid "Device not found" +msgstr "" -msgid "This rundown is now active. Are you sure you want to exit this screen?" -msgstr "Denne køyreplanen er aktiv. Er du sikker på at du vil avslutte?" +msgid "Device Triggers" +msgstr "" -msgid "Invalid AdLib" -msgstr "Ugyldig adlib" +msgid "Device Type" +msgstr "Type eining" -msgid "Cannot play this AdLib because it is marked as Invalid" -msgstr "Kan ikkje spele av adlib fordi den er markert som ugyldig" +msgid "Devices" +msgstr "Einingar" -#, fuzzy -#| msgid "Floated AdLib" -msgid "Floated Adlib" -msgstr "Adlib satt på vent" +msgid "Devices with issues" +msgstr "Einingar med problem" -msgid "Cannot play this AdLib because it is marked as Floated" -msgstr "Kan ikkje spele av adlib fordi den er markert som på vent (float)" +msgid "Did you have any problems with the broadcast?" +msgstr "Hadde du nokre problem under sendinga?" -msgid "Not queueable" -msgstr "Kan ikkje setjast i kø" +msgid "Diff" +msgstr "Skilnad" -msgid "Cannot play this adlib because source layer is not queueable" -msgstr "Kan ikkje spele av adlib fordi den ikkje kan setjast i kø på kjeldelaget" +msgid "Director Screen" +msgstr "" -msgid "" -"There are no Playout Gateways connected and attached to this studio. Please " -"contact the system administrator to start the Playout Gateway." +msgid "Director's Screen" msgstr "" -"Dette studioet har ingen tilkopla playout-gatewayar. Kontakt " -"systemadministrator for å starte den." -msgid "Playout Gateway \"{{playoutDeviceName}}\" is now restarting." -msgstr "Playout-gateway \"{{playoutDeviceName}}\" startar om att." +msgid "Disable" +msgstr "" -msgid "Could not restart Playout Gateway \"{{playoutDeviceName}}\"." -msgstr "Playout-gateway \"{{playoutDeviceName}}\" kunne ikkje startas om att." +msgid "Disable Context Menu" +msgstr "Skruv av kontekstmeny" -msgid "Restart Playout" -msgstr "Start Playout-gateway på ny" +msgid "Disable follow take" +msgstr "" -#, fuzzy -#| msgid "Do you want to restart Quantel Gateway?" -msgid "Do you want to restart the Playout Gateway?" -msgstr "Er du sikker på at du vil starte Quantel-gateway om att?" +msgid "Disable hints by adding this to the URL:" +msgstr "Deaktiver hint ved å legge dette til på url-en:" -msgid "Restart CasparCG Server" -msgstr "Start CasparCG på nytt" +msgid "Disable next Piece" +msgstr "Skip neste element" -msgid "Do you want to restart CasparCG Server \"{{device}}\"?" +msgid "Disable the hover Inspector when hovering over the button" msgstr "" -"Er du sikker på at du vil starta CasparCG Server \"{{device}}\" på nytt?" -msgid "CasparCG on device \"{{deviceName}}\" restarting..." -msgstr "CasparCG på \"{{deviceName}}\" startar på nytt..." +msgid "Disable the next element" +msgstr "Skip neste super" -msgid "" -"Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}" -msgstr "Omstart av CasparCG på \"{{deviceName}}\" feila: {{errorMessage}}" +msgid "Disable version check" +msgstr "Deaktiver versjonsjekk" -msgid "Cancel currently pressed hotkey" -msgstr "Avbryt den trykte tasten" +msgid "Disabled" +msgstr "Deaktivert" -msgid "Change to fullscreen mode" -msgstr "Fullskjermmodus" +msgid "Disabling next Piece" +msgstr "" -msgid "Show Hotkeys" -msgstr "Vis hurtigtastar" +msgid "Disconnected" +msgstr "Fråkopla" -msgid "Take a Snapshot" -msgstr "Lagre eit snapshot" +msgid "Dismiss" +msgstr "" -msgid "Restart {{device}}" -msgstr "Start {{device}} på ny" +msgid "Dismiss all notifications" +msgstr "" -msgid "Rundown not found" -msgstr "Køyreplan ikkje funnen" +msgid "Display AB channel assignments on:" +msgstr "" -msgid "Close" -msgstr "Lukk" +msgid "Display AdLibs in a column in List View" +msgstr "" -msgid "Rundown for piece \"{{pieceLabel}}\" could not be found." -msgstr "Kan ikkje finne øyreplan for \"{{pieceLabel}}\"." +msgid "Display in a column in List View" +msgstr "" -msgid "This rundown has been unpublished from Sofie." -msgstr "Denne køyreplanen er ikkje lenger tilgjengeleg i Sofie." +msgid "Display name of the Package Container" +msgstr "Pakkekontaineren sitt namn som visast i oversikten" -msgid "Error: The studio of this Rundown was not found." -msgstr "Feil: Kan ikkje finne studioet for denne køyreplanen." +msgid "Display piece duration for source layers" +msgstr "" -msgid "This playlist is empty" -msgstr "Denne spelelista er tom" +msgid "Display Rank" +msgstr "Rangering for visning" -msgid "Error: The ShowStyle of this Rundown was not found." -msgstr "Feil: Kan ikkje finne showstyle for denne køyreplanen." +msgid "Display Style" +msgstr "Stil for vising" -msgid "Unknown error" -msgstr "Ukjend feil" +msgid "Display Take buttons" +msgstr "Vis Take-knapp" msgid "" -"Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data " -"from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced " -"or remove the rundown from Sofie. What do you want to do?" +"Do you really want to remove just the rundown \"{{rundownName}}\" in the " +"playlist {{playlistName}} from Sofie? \n" +"\n" +"This cannot be undone!" msgstr "" -"Køyreplanen {{rundownName}} i lista {{playlistName}} manglar i data frå " -"{{nrcsName}}. Du kan anten markere den som ikkje synkronisert og behalde den " -"i Sofie, eller du kan fjerne køyreplanen ifrå Sofie. Kva vil du gjere?" - -msgid "(Unknown rundown)" -msgstr "(Ukjend køyreplan)" -msgid "(Unknown playlist)" -msgstr "(Ukjend køyreplanliste)" +msgid "Do you really want to restore the snapshot \"{{snapshotName}}\"?" +msgstr "" -msgid "Leave Unsynced" -msgstr "Behald ikkje-synkronisert køyreplan" +msgid "Do you want to activate this Rundown?" +msgstr "Vil du aktivere denne køyreplanen?" -msgid "Remove" -msgstr "Fjern" +msgid "" +"Do you want to append these to existing Action Triggers, or do you want to " +"replace them?" +msgstr "" +"Vil du legge desse til dei noverande handlingsutløysarane, eller vil du " +"erstatta dei?" -msgid "Remove rundown" -msgstr "Fjern køyreplan" +msgid "Do you want to do this?" +msgstr "" -msgid "" -"Do you really want to remove just the rundown \"{{rundownName}}\" in the " -"playlist {{playlistName}} from Sofie? This cannot be undone!" +msgid "Do you want to execute {{actionName}}? This may the disrupt the output" msgstr "" -"Er du sikker på at du vil slette køyreplanen {{rundownName}} i lista " -"{{playlistName}} frå Sofie? Denne handlinga kan du ikkje angre!" -msgid "Loop Start" -msgstr "Start for loop" +msgid "Do you want to restart CasparCG Server \"{{device}}\"?" +msgstr "Er du sikker på at du vil starta CasparCG Server \"{{device}}\" på nytt?" -msgid "Loop End" -msgstr "Slutt for loop" +msgid "Do you want to restart the Playout Gateway?" +msgstr "" -msgid "(in: {{time}})" -msgstr "(om: {{time}})" +msgid "Documentation is available at" +msgstr "Dokumentasjon er tilgjengelig på" -msgid "({{time}} ago)" -msgstr "(for {{time}} sidan)" +msgid "Documents to be removed:" +msgstr "Dokument som vert fjerna:" -msgid "Planned Start" -msgstr "Planlagt start" +msgid "Does NOT support HEAD requests" +msgstr "" -msgid "Planned Duration" -msgstr "Planlagt varigheit" +msgid "Don't treat the end of the last rundown in a playlist as a break" +msgstr "Ikkje behandle slutten av den siste køyreplanen i ei speleliste som ei pause" -msgid "Planned End" -msgstr "Planlagt slutt" +msgid "Done" +msgstr "Utført" -msgid "Time to planned end" -msgstr "Tid til planlagt slutt" +msgid "Download Action Triggers" +msgstr "Last ned handlingsutløysarar" -msgid "Time since planned end" -msgstr "Tid sidan planlagt slutt" +msgid "Drag to reorder or move out of playlist" +msgstr "Dra for å endre rekkefølgje eller flytta ut av speleliste" -msgid "Over/Under" -msgstr "Over/Under" +msgid "Drop Rundown here to move it out of its current Playlist" +msgstr "" -msgid "" -"The rundown \"{{rundownName}}\" is not published or activated in " -"{{nrcsName}}! No data updates will currently come through." +msgid "Dropzone URL" msgstr "" -"Køyreplanen \"{{rundownName}}\" er ikkje synkronisert med MOS/{{nrcsName}}! " -"Kontroller at den er satt til MOS Active i ENPS." -msgid "Re-sync" -msgstr "Synkroniser med MOS" +msgid "Duplicate Action Trigger" +msgstr "" -msgid "Re-sync Rundown" -msgstr "Synkroniser køyreplanen med ENPS på nytt" +msgid "Duration" +msgstr "Lengde" -msgid "" -"Are you sure you want to re-sync the Rundown?\n" -"(If the currently playing Part has been changed, this can affect the output)" +msgid "DURATION" msgstr "" -"Er du sikker på at du vil gjenopprette synkronisering mot ENPS for denne " -"køyreplanen?\n" -"(Dette kan virke inn på pågåande sending)" - -msgid "Restart" -msgstr "Restart" -msgid "" -"Fixing this problem requires a restart to the host device. Are you sure you " -"want to restart {{device}}?\n" -"(This might affect output)" +msgid "e.g., Studio A,Studio B" msgstr "" -"Feilretting krever ein omstart av {{device}}. Er du sikker på at du ønsker å " -"starta einingen på nytt?(Dette kan ha innverknad på gjennomføringa av ein " -"igangverande sending)" -msgid "Device \"{{deviceName}}\" restarting..." -msgstr "\"{{deviceName}}\" starter på ny..." +msgid "Edit" +msgstr "" -msgid "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}" -msgstr "Kunne ikkje starta \"{{deviceName}}\" om att: {{errorMessage}}" +msgid "Edit Action Trigger" +msgstr "" -msgid "There is an unknown problem with the part." -msgstr "Det er eit ukjend problem med denne delen." +msgid "Edit in Nora" +msgstr "Rediger i Nora" -msgid "Show issue" -msgstr "Vis problem" +msgid "Edit mapping" +msgstr "" -msgid "There is an unspecified problem with the source." -msgstr "Det er eit ikkje-spesifisert problem med kjelden." +msgid "Edit Mode" +msgstr "" -msgid "External message queue has unsent messages." -msgstr "Ekstern meldingskø har meldingar som ikkje er sendt." +msgid "Edit output layer" +msgstr "" -msgid "" -"The system configuration has been changed since importing this rundown. It " -"might not run correctly" +msgid "Edit Part Properties" msgstr "" -"Systemoppsettet har verte endra etter at denne køyreplanen vart importert. " -"Køyreplanen kan verte spelt av med feil" -msgid "Unable to check the system configuration for changes" -msgstr "Kan ikkje kontrollere endringar i systemoppsettet" +msgid "Edit Piece Properties" +msgstr "" -msgid "The Studio configuration is missing some required fields:" -msgstr "Studiooppsettet manglar obligatoriske felt:" +msgid "Edit Segment Properties" +msgstr "" -msgid "The Show Style configuration \"{{name}}\" could not be validated" -msgstr "Showstyleoppsettet \"{{name}}\" kunne ikkje validerast" +msgid "Edit source layer" +msgstr "" -msgid "" -"The ShowStyle \"{{name}}\" configuration is missing some required fields:" -msgstr "Showstyleoppsettet \"{{name}}\" manglar obligatoriske felt:" +msgid "Edit Support Panel" +msgstr "Rediger supportpanel" -msgid "Unable to validate the system configuration" -msgstr "Systemoppsettet kunne ikkje validerast" +msgid "Empty" +msgstr "Tom" -msgid "Device {{deviceName}} is disconnected" -msgstr "{{deviceName}} er fråkopla" +msgid "Empty this Bucket" +msgstr "Tøm denne bøtta" -#, fuzzy -#| msgid "Critical Errors" -msgid "Critical Problems" -msgstr "Kritiske feil" +msgid "Emptying Bucket" +msgstr "" -msgid "Warnings" -msgstr "Åtvaringar" +msgid "Enable" +msgstr "Aktivert" -#, fuzzy -#| msgid "Migrations" -msgid "Notifications" -msgstr "Migrering" +msgid "Enable \"Play from Anywhere\"" +msgstr "Slå på \"Play from Anywhere\"" -#, fuzzy -#| msgid "Rewind Segments to start" -msgid "Rewind all Segments" -msgstr "Sett alle segment attende til start" +msgid "Enable AdLib Testing, for testing AdLibs before taking the first Part" +msgstr "" -#, fuzzy -#| msgid "Show entire On Air Segment" -msgid "Go to On Air Segment" -msgstr "Vis heile tittelen som er OnAir" +msgid "Enable automatic storage of Rundown Playlist snapshots periodically" +msgstr "" -#, fuzzy -#| msgid "Switchboard" -msgid "Toggle Switchboard Panel" -msgstr "Omkoplingssentral" +msgid "Enable Buckets" +msgstr "" -#, fuzzy -#| msgid "Edit Support Panel" -msgid "Toggle Support Panel" -msgstr "Rediger supportpanel" +msgid "Enable CasparCG restart job" +msgstr "Aktiver CasparCG restartjobbar" -msgid "Just now" -msgstr "No" +msgid "Enable configuration mode by adding ?configure=1 to the address bar." +msgstr "" +"Aktiver konfigurasjonsmodus ved å legge til ?configure=1 på slutten av " +"nettadressa." -msgid "Less than a minute ago" -msgstr "Under eitt minutt sidan" +msgid "Enable Evaluation Form" +msgstr "" -msgid "Less than five minutes ago" -msgstr "Under fem minutt sidan" +msgid "Enable hints by adding this to the URL:" +msgstr "Aktiver hint ved å legge dette til på url-en:" -msgid "Around 10 minutes ago" -msgstr "Cirka 10 minutt sidan" +msgid "Enable QuickLoop" +msgstr "" -msgid "More than 10 minutes ago" -msgstr "Over 10 minutt sidan" +msgid "Enable search toolbar" +msgstr "Aktiver søkeverktøy" -msgid "More than 30 minutes ago" -msgstr "Over 30 minutt sidan" +msgid "Enable User Editing" +msgstr "" -msgid "More than 2 hours ago" -msgstr "Over 2 timer sidan" +msgid "Enabled" +msgstr "Aktivert" -msgid "More than 5 hours ago" -msgstr "Over 5 timar sidan" +msgid "Enabled on all Parts, applying QuickLoop Fallback Part Duration if needed" +msgstr "" -msgid "More than a day ago" -msgstr "Over ein dag sidan" +msgid "Enabled, but skipping parts with undefined or 0 duration" +msgstr "" -msgid "{{nrcsName}} Connection" -msgstr "{{nrcsName}}-tilkopling" +msgid "" +"Enables internal monitoring of blocked main thread. Logs when there is an " +"issue, but (unverified) might cause issues in itself." +msgstr "" -msgid "Last update" -msgstr "Nyeste oppdatering" +msgid "End of script" +msgstr "Slutt på manus" -msgid "Off-line devices" -msgstr "Fråkoplete einingar" +msgid "End Words" +msgstr "Stikkord" -msgid "Devices with issues" -msgstr "Einingar med problem" +msgid "Error" +msgstr "Feil" -msgid "All connections working correctly" -msgstr "Alle tilkoplingar er OK" +msgid "Error when checking for cleaning up" +msgstr "" -msgid "Play-out" -msgstr "Avspelning" +msgid "Error: The ShowStyle of this Rundown was not found." +msgstr "Feil: Kan ikkje finne showstyle for denne køyreplanen." -msgid "All devices working correctly" -msgstr "Alle eininger fungerer som dei skal" +msgid "Error: The studio of this Rundown was not found." +msgstr "Feil: Kan ikkje finne studioet for denne køyreplanen." -msgid "Auto" -msgstr "Auto" +msgid "Est. End" +msgstr "" -msgid "Expected End" -msgstr "Venta slutt" +msgid "Evaluations" +msgstr "Evalueringar" -msgid "Next Loop at" -msgstr "Neste loop starter" +msgid "Exclusivity group" +msgstr "Ekslusivitetgruppe" -msgid "Diff" -msgstr "Skilnad" +msgid "Exclusivity Group ID" +msgstr "Eksklusivitetgruppe-id" -msgid "Started" -msgstr "Starta" +msgid "Exclusivity Group Name" +msgstr "Eksklusivitetgruppenamn" -msgid "Expected Start" -msgstr "Venta slutt" +msgid "Exclusivity Groups" +msgstr "Ekslusivitetgrupper" -msgid "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)" -msgstr "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)" +msgid "Execute" +msgstr "Utfør" -msgid "{{currentRundownName}} - {{rundownPlaylistName}}" -msgstr "{{currentRundownName}} - {{rundownPlaylistName}}" +msgid "Execute User Operation" +msgstr "" -msgid "{{rundownPlaylistName}} (Looping)" -msgstr "{{rundownPlaylistName}} (Looper)" +msgid "Executed {{actionName}} on device \"{{deviceName}}\": {{response}}" +msgstr "" -msgid "Floated AdLib" -msgstr "Adlib satt på vent" +msgid "Executed {{actionName}} on device \"{{deviceName}}\"..." +msgstr "" -msgid "Switchboard" -msgstr "Omkoplingssentral" +msgid "Executes within the currently open Rundown, requires a Client-side trigger." +msgstr "" +"Blir utførte innanfor den valde køyreplanen, men treng ein utløysar frå " +"klienten." -msgid "This is not in it's normal setting" -msgstr "Denne innstillinga er vorte endra frå standardverdien" +msgid "Execution times" +msgstr "Køyretider" -msgid "Off" -msgstr "Av" +msgid "Exit" +msgstr "Lukk" -#, fuzzy -#| msgid "Segment" -msgid "segment" -msgstr "Tittel" +msgid "Expectation Manager" +msgstr "" -#, fuzzy -#| msgid "Critical Errors" -msgid "Critical problems" -msgstr "Kritiske feil" +msgid "Expected End" +msgstr "Venta slutt" -msgid "On Air At" -msgstr "On Air klokka" +msgid "Expected End text" +msgstr "Tekst for venta slutt" -msgid "On Air In" -msgstr "On Air om" +msgid "Expected End Time" +msgstr "Venta sendeslutt" -msgid "Unsynced" -msgstr "Ikkje synkronisert med MOS" +msgid "Expected Start" +msgstr "Venta slutt" -msgid "Sources" -msgstr "Kjelder" +msgid "Export" +msgstr "Eksporter" -msgid "Switch to Timeline mode" +msgid "Export visible" msgstr "" -msgid "On Air" -msgstr "On Air" - -msgid "Loops to top" -msgstr "Looper til toppen" +msgid "Expose as user selectable layout" +msgstr "Gjer tilgjengeleg som brukarvalgt layout" -msgid "Show End" -msgstr "Sendeslutt" +msgid "Expose layout as a standalone page" +msgstr "Gjer layout tilgjengeleg som ei sjølvstendig side" -msgid "BREAK" -msgstr "PAUSE" +msgid "External message queue has unsent messages." +msgstr "Ekstern meldingskø har meldingar som ikkje er sendt." -msgid "Break In" -msgstr "Pause om" +msgid "Failed to activate" +msgstr "Kunne ikkje aktivere" -msgid "part" -msgstr "punkt" +msgid "Failed to add a new Show Style Variant: {{errorMessage}}" +msgstr "" -msgid "Set segment as Next" -msgstr "Set tittel som neste: Startar på neste Take" +msgid "Failed to assign AB player for {{pieceNames}}" +msgstr "" -msgid "Queue segment" -msgstr "Cue tittel: Startar når aktiv tittel er ferdig" +msgid "Failed to assign non-critical AB player for {{pieceNames}}" +msgstr "" -msgid "Clear queued segment" -msgstr "Fjern cuet tittel" +msgid "Failed to compare config changes" +msgstr "" -msgid "Set this part as Next" -msgstr "Set dette punktet som neste: Startar på neste Take" +msgid "Failed to copy Show Style Variant: {{errorMessage}}" +msgstr "" -msgid "Set Next Here" -msgstr "Sett Neste her" +msgid "Failed to delete Show Style Variant: {{errorMessage}}" +msgstr "" -msgid "Play from Here" -msgstr "Spel av frå her" +msgid "" +"Failed to execute {{actionName}} on device: \"{{deviceName}}\": " +"{{errorMessage}}" +msgstr "" -#, fuzzy -#| msgid "Switchboard" -msgid "Switch to Storyboard mode" -msgstr "Byt til Storyboard-visning" +msgid "Failed to execute take" +msgstr "Kunne ikkje gjennomføre Take" -msgid "Zoom Out" -msgstr "Zoom ut" +msgid "Failed to generate adlib rundown! {{message}}" +msgstr "" -msgid "Show All" -msgstr "Vis Alle" +msgid "Failed to import new Show Style Variants: {{errorMessage}}" +msgstr "" -msgid "Zoom In" -msgstr "Zoom In" +msgid "" +"Failed to import Show Style Variant {{name}}. Make sure it is not already " +"imported." +msgstr "" -msgid "Parts Duration" -msgstr "Varigheit for del" +msgid "Failed to remove all Show Style Variants: {{errorMessage}}" +msgstr "" -msgid "Unknown" -msgstr "Ukjend" +msgid "Failed to reorderShow Style Variant: {{errorMessage}}" +msgstr "" -msgid "Good" -msgstr "Bra" +msgid "Failed to reset OAuth credentials: {{errorMessage}}" +msgstr "Nullstilling av OAuth credentials feila: {{errorMessage}}" -msgid "Minor Warning" -msgstr "Mindre åtvaring (avvik)" +msgid "Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}" +msgstr "Omstart av CasparCG på \"{{deviceName}}\" feila: {{errorMessage}}" -msgid "Warning" -msgstr "Åtvaring" +msgid "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}" +msgstr "Kunne ikkje starta \"{{deviceName}}\" om att: {{errorMessage}}" -msgid "Bad" -msgstr "Feil" +msgid "Failed to update blueprints: {{errorMessage}}" +msgstr "Oppdatering av blueprints feila: {{errorMessage}}" -msgid "Fatal" -msgstr "Kritisk" +msgid "Failed to update config: {{errorMessage}}" +msgstr "Oppdatering av konfigurasjon feila: {{errorMessage}}" -msgid "Connected" -msgstr "Tilkopla" +msgid "Failed to upload OAuth credentials: {{errorMessage}}" +msgstr "Opplasting av OAuth credentials feila: {{errorMessage}}" -msgid "Disconnected" -msgstr "Fråkopla" +msgid "Failed to upload shelf layout: {{errorMessage}}" +msgstr "Opplasting av layout feila: {{errorMessage}}" -msgid "MOS Gateway" -msgstr "MOS-gateway" +msgid "Failed to validate config" +msgstr "" -msgid "Spreadsheet Gateway" -msgstr "Spreadsheet-gateway" +msgid "Fatal" +msgstr "Kritisk" -msgid "Play-out Gateway" -msgstr "Playout-gateway" +msgid "File path to the folder of the local folder" +msgstr "Sti til lokal mappe" -msgid "Media Manager" -msgstr "Media Manager" +msgid "Filter by Output Layer" +msgstr "" -msgid "Unknown Device" -msgstr "Ukjend eining" +msgid "Filter by Source Layer" +msgstr "" -msgid "Delete this Studio?" -msgstr "Slett dette studioet?" +msgid "Filter disabled" +msgstr "Filter deaktivert" -msgid "Are you sure you want to delete the studio \"{{studioId}}\"?" -msgstr "Er du sikker på at du vil slette studioet \"{{studioId}}\"?" +msgid "Filter Disabled" +msgstr "Filter deaktivert" -msgid "Delete this Show Style?" -msgstr "Slett denne showstylen?" +msgid "Filter..." +msgstr "" -msgid "Are you sure you want to delete the show style \"{{showStyleId}}\"?" -msgstr "Er du sikker på at du vil slette showstylen \"{{showStyleId}}\"?" +msgid "Filters" +msgstr "Filtre" -msgid "Delete this Blueprint?" -msgstr "Slett dette blueprintet?" +msgid "Find Trigger..." +msgstr "Finn utløysar..." -msgid "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?" -msgstr "Er du sikker på at du vil slette blueprintet \"{{blueprintId}}\"?" +msgid "Fine scroll" +msgstr "" -msgid "Remove this Device?" -msgstr "Fjern denne eininga?" +msgid "Fix Up Config" +msgstr "" + +msgid "Fixed duration in Segment header" +msgstr "Låst lengde i tittelheader" msgid "" -"Are you sure you want to remove the device \"{{deviceName}}\" and all of " -"it's sub-devices?" +"Fixing this problem requires a restart to the host device. Are you sure you " +"want to restart {{device}}?\n" +"(This might affect output)" msgstr "" -"Er du sikker på at du vil fjerne eninga \"{{deviceName}}\" og alle " -"undereiningane?" +"Feilretting krever ein omstart av {{device}}. Er du sikker på at du ønsker " +"å starta einingen på nytt?(Dette kan ha innverknad på gjennomføringa av ein " +"igangverande sending)" -msgid "Studios" -msgstr "Studio" +msgid "Floated Adlib" +msgstr "" -msgid "Unnamed Studio" -msgstr "Studio utan namn" +msgid "Floated AdLib" +msgstr "Adlib satt på vent" -msgid "Show Styles" -msgstr "Showstyle" +msgid "Folder path" +msgstr "Mappesti" -msgid "Unnamed Show Style" -msgstr "Showstyle utan namn" +msgid "Folder path to shared folder" +msgstr "Sti til delt mappe" -msgid "Source Layers" -msgstr "Kjeldelag" +msgid "Following" +msgstr "" -msgid "Output Channels" -msgstr "Utgangskanalar" +msgid "Font size" +msgstr "" -msgid "Blueprints" -msgstr "Blueprint" +msgid "for {{name}} fix skipped successfully" +msgstr "" -msgid "Unnamed blueprint" -msgstr "Blueprint utan namn" +msgid "Force" +msgstr "Tving" -msgid "Type" -msgstr "Type" +msgid "Force (deactivate others)" +msgstr "Tving (deaktiver andre)" -msgid "Version" -msgstr "Versjon" +msgid "Force Migration" +msgstr "Tving migrering" -msgid "Devices" -msgstr "Einingar" +msgid "Force Migration (unsafe)" +msgstr "Tving migrering (utrygt)" -msgid "Tools" -msgstr "Verktøy" +msgid "Force the Multi-gateway-mode" +msgstr "Tving multigateway-modus" -msgid "Core System settings" -msgstr "Systeminnstillingar for Core" +msgid "Forward" +msgstr "" -msgid "Upgrade Database" -msgstr "Oppgrader databasen" +msgid "Forward: {{forward}}" +msgstr "" -msgid "Manage Snapshots" -msgstr "Behandle snapshots" +msgid "Found no future pieces" +msgstr "" -msgid "System Settings" -msgstr "Systeminstillinger" +msgid "Frame Rate" +msgstr "Framerate" -msgid "Update Blueprints?" -msgstr "Oppdater blueprints?" +msgid "From" +msgstr "" -msgid "Update" -msgstr "Oppdater" +msgid "Full System Snapshot" +msgstr "Fullt systemsnapshot" -msgid "" -"Are you sure you want to update the blueprints from the file " -"\"{{fileName}}\"?" +msgid "fullscreen" msgstr "" -"Er du sikker på at du vil oppdatere blueprints frå fila \"{{fileName}}\"?" -msgid "Blueprints updated successfully." -msgstr "Blueprints blei oppdatert." - -msgid "Replace Blueprints?" -msgstr "Erstatte blueprints?" +msgid "Gateway" +msgstr "" -msgid "Replace" -msgstr "Erstatt" +msgid "General" +msgstr "" -msgid "" -"Are you sure you want to replace the blueprints with the file " -"\"{{fileName}}\"?" +msgid "Generated URL" msgstr "" -"Er du sikker på at du vil erstatte blueprints frå fila \"{{fileName}}\"?" -msgid "Failed to update blueprints: {{errorMessage}}" -msgstr "Oppdatering av blueprints feila: {{errorMessage}}" +msgid "Generating restart token" +msgstr "" -msgid "Assigned Show Styles:" -msgstr "Tilordna showstyles:" +msgid "Generic Properties" +msgstr "Generelle eigenskapar" -msgid "This Blueprint is not being used by any Show Style" -msgstr "Dette blueprintet er ikkje i bruk av nokon showstyles" +msgid "Generic Script" +msgstr "Generisk manus" -msgid "Assigned Studios:" -msgstr "Tilordna studio:" +msgid "Getting Started" +msgstr "Kom i gong" -msgid "This Blueprint is not compatible with any Studio" -msgstr "Dette blueprintet er ikkje kompatibel med noko studio" +msgid "Global AdLib" +msgstr "Globale adliber" -msgid "Unassign" -msgstr "Fjern tilordning" +msgid "Global AdLibs" +msgstr "Globale adliber" -msgid "Assign" -msgstr "Tilordne" +msgid "Go to Live" +msgstr "" -msgid "Blueprint ID" -msgstr "Blueprint-id" +msgid "Go to On Air line" +msgstr "Gå til OnAir-posisjon" -msgid "Blueprint Name" -msgstr "Blueprintnamn" +msgid "Go to On Air Segment" +msgstr "" -msgid "No name set" -msgstr "Namn ikkje definert" +msgid "Good" +msgstr "Bra" -msgid "Blueprint Type" -msgstr "Blueprinttype" +msgid "Graphics" +msgstr "Grafikk" -msgid "Upload a new blueprint" -msgstr "Last opp eit nytt blueprint" +msgid "GUI" +msgstr "Brukergrensesnitt" -msgid "Last modified" -msgstr "Sist endra" +msgid "he default state of this Route Set" +msgstr "" -msgid "Blueprint Id" -msgstr "Blueprint-id" +msgid "Heading" +msgstr "" -msgid "Blueprint Version" -msgstr "Blueprintversjon" +msgid "Height" +msgstr "Høgde" -msgid "Disable version check" -msgstr "Deaktiver versjonsjekk" +msgid "Help & Support" +msgstr "Hjelp og brukarstøtte" -msgid "Upload Blueprints" -msgstr "Last opp blueprints" +msgid "Hide" +msgstr "" -msgid "OAuth credentials succesfully uploaded." -msgstr "Opplasting av OAuth credentials var vellukka." +msgid "Hide Countdown" +msgstr "Skjul nedteljing" -msgid "Failed to upload OAuth credentials: {{errorMessage}}" -msgstr "Opplasting av OAuth credentials feila: {{errorMessage}}" +msgid "Hide default AdLib Start/Execute options" +msgstr "" -msgid "OAuth credentials successfuly reset" -msgstr "OAuth credentials nullstilt." +msgid "Hide Diff" +msgstr "Skjul skilnad" -msgid "Failed to reset OAuth credentials: {{errorMessage}}" -msgstr "Nullstilling av OAuth credentials feila: {{errorMessage}}" +msgid "Hide Diff Label" +msgstr "Skjul etikett for skilnad" -msgid "Reset Authentication" -msgstr "Passord for autentisering" +msgid "Hide duplicated AdLibs" +msgstr "Skjul dupliserte adliber" -msgid "Application credentials" -msgstr "Brukarnamn/passord (Application Credentials)" +msgid "Hide End Time" +msgstr "Skjul sendeslutt" -msgid "Access token" -msgstr "Tilgongskode (Access Token)" +msgid "Hide Expected End timing when a break is next" +msgstr "Gøym nedteljing til venta slutt når neste punkt er ei pause" -msgid "Click on the link below and accept the permissions request." -msgstr "Klikk på lenka under og godta permissions-førespurnaden" +msgid "Hide for dynamically inserted parts" +msgstr "Skjul for dynamisk innsatte delar" -msgid "Waiting for gateway to generate URL..." -msgstr "Ventar på at gateway genererar URL..." +msgid "Hide Label" +msgstr "Skjul etikett" -msgid "Only Match Global AdLibs" -msgstr "Vis kun globale adliber" +msgid "Hide over/under timer" +msgstr "" -msgid "Name" -msgstr "Namn" +msgid "Hide Panel from view" +msgstr "Ikkje vis dette panelet" -msgid "Display Style" -msgstr "Stil for vising" +msgid "Hide Planned End Label" +msgstr "Skjul etikett for planlagt slutt" -msgid "Show thumbnails next to list items" -msgstr "Vis miniatyrbilete ved sida av listeelement" +msgid "Hide Planned Start" +msgstr "Skjul planlagt start" -msgid "Button width scale factor" -msgstr "Breiddeskala for knapp" +msgid "Hide Rundown Divider" +msgstr "Skjul køyreplanskilje" -msgid "Button height scale factor" -msgstr "Høgdeskala for knapp" +msgid "Hide rundown divider between rundowns in a playlist" +msgstr "Skjul skilje mellom køyreplanar i ei speleliste" -msgid "Only Display AdLibs from Current Segment" -msgstr "Vis berre adliber frå gjeldande tittel" +msgid "Hide scrollbar" +msgstr "" -msgid "Include Global AdLibs" -msgstr "Inkluder globale adliber" +msgid "Hold" +msgstr "Hold" -msgid "Filter Disabled" -msgstr "Filter deaktivert" +msgid "Hostname or IP address of the Atem" +msgstr "" -msgid "Include Clear Source Layer in Ad-Libs" -msgstr "Ta med \"Tøm kjeldelag\" i adliber" +msgid "Hotkey" +msgstr "Hurtigtast" -msgid "Source Layer Types" -msgstr "Kjeldelagstypar" +msgid "How did the show go?" +msgstr "" -msgid "Filter disabled" -msgstr "Filter deaktivert" +msgid "" +"How many of the transactions to monitor. Set to -1 to log nothing (max " +"performance), 0.5 to log 50% of the transactions, 1 to log all transactions" +msgstr "" +"Tal på transaksjonar som overvakast. Set verdien til -1 for å ikkje logge " +"noko (maks yting), til 0.5 for å logge halvparten av transaksjonane eller " +"til 1 for å logge alle transaksjonane" -msgid "Label contains" -msgstr "Etikett inneheld" +msgid "" +"How much preparation time to add to global pieces on the timeline before " +"they are played" +msgstr "" -msgid "Tags must contain" -msgstr "Tagger må innehalde" +msgid "HTML that will be shown in the Support Panel" +msgstr "HTML-kode som vert vist i supportpanelet" -msgid "Hide Panel from view" -msgstr "Ikkje vis dette panelet" +msgid "Human-readable name of the layer" +msgstr "Lesarvenleg lagnamn" -msgid "Show panel as a timeline" -msgstr "Vis panel som ei tidslinje" +msgid "Icon" +msgstr "Ikon" -msgid "Enable search toolbar" -msgstr "Aktiver søkeverktøy" +msgid "Icon color" +msgstr "Ikonfarge" -msgid "Overflow horizontally" -msgstr "Horisontal overflyt" +msgid "Id" +msgstr "Id" -msgid "Display Take buttons" -msgstr "Vis Take-knapp" +msgid "ID" +msgstr "" -msgid "Queue all adlibs" -msgstr "Cue alle adliber" +msgid "" +"ID of the device (corresponds to the device ID in the peripheralDevice " +"settings)" +msgstr "Eining-id (korresponderer med enhets-id under enhetsinnstillinger)" -msgid "Toggle AdLibs on single mouse click" -msgstr "Veksle mellom adliber med enkelt museklikk" +msgid "ID of the timeline-layer to map to some output" +msgstr "Lag-id for tidslinjelaget som skal mappast til ein utgang" -msgid "Current part can contain next pieces" +msgid "Idempotency-Key is already used" msgstr "" -msgid "Indicate only one next piece per source layer" +msgid "Idempotency-Key is missing" msgstr "" -msgid "Hide duplicated AdLibs" -msgstr "Skjul dupliserte adliber" +msgid "If set, only one Route Set will be active per exclusivity group" +msgstr "" +"Berre ei omkoplingsgruppe vere aktiv per eksklusivitetsgruppe når dette er " +"kryssa av for" msgid "" -"Picks the first instance of an adLib per rundown, identified by uniqueness Id" +"If set, Package Manager assumes that the source doesn't support HEAD " +"requests and will use GET instead. If false, HEAD requests will be sent to " +"check availability." msgstr "" -"Velger den første førekomsten av ein adlib i kvar køyreplan, identifisert av " -"unik id" - -msgid "URL" -msgstr "Adresse (url)" -msgid "Display Rank" -msgstr "Rangering for visning" +msgid "Ignore and apply" +msgstr "" -msgid "Role" -msgstr "Rolle" +msgid "Ignore QuickLoop" +msgstr "" -msgid "Adlib Rank" -msgstr "Adlib-rang" +msgid "Ignoring take as playing part has changed since TAKE was requested." +msgstr "" -msgid "Place label below panel" -msgstr "Plasser etikett under panel" +msgid "Ignoring TAKES that are too quick after eachother ({{duration}} ms)" +msgstr "" -msgid "Disabled" -msgstr "Deaktivert" +msgid "Import" +msgstr "Import" -msgid "Show segment name" -msgstr "Vis tittelen sitt namn" +msgid "Import error: {{errorMessage}}" +msgstr "" -msgid "Show part title" -msgstr "Vis delen sin tittel" +msgid "Import file?" +msgstr "" -msgid "Hide for dynamically inserted parts" -msgstr "Skjul for dynamisk innsatte delar" +msgid "Importing an AdLib to the Bucket" +msgstr "" -msgid "Planned Start Text" -msgstr "Tekst for planlagt start" +msgid "In" +msgstr "Inn" -msgid "Text to show above show start time" -msgstr "Tekst som blir vist over klokkeslett for sendestart" +msgid "IN" +msgstr "" -msgid "Hide Diff" -msgstr "Skjul skilnad" +msgid "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s" +msgstr "om {{days}} dagar, {{hours}} h {{minutes}} min {{seconds}} s" -msgid "Hide Planned Start" -msgstr "Skjul planlagt start" +msgid "in {{hours}} h {{minutes}} min {{seconds}} s" +msgstr "om {{hours}} t {{minutes}} min {{seconds}} s" -msgid "Planned End text" -msgstr "Tekst for planlagt slutt" +msgid "in {{minutes}} min {{seconds}} s" +msgstr "om {{minutes}} min {{seconds}} s" -msgid "Text to show above show end time" -msgstr "Tekst som blir vist over klokkeslett for sendeslutt" +msgid "in {{seconds}} s" +msgstr "om {{seconds}} s" -msgid "Hide Planned End Label" -msgstr "Skjul etikett for planlagt slutt" +msgid "In rehearsal" +msgstr "" -msgid "Hide Diff Label" -msgstr "Skjul etikett for skilnad" +msgid "Include Clear Source Layer in Ad-Libs" +msgstr "Ta med \"Tøm kjeldelag\" i adliber" -msgid "Hide Countdown" -msgstr "Skjul nedteljing" +msgid "Include Global AdLibs" +msgstr "Inkluder globale adliber" -msgid "Hide End Time" -msgstr "Skjul sendeslutt" +msgid "Indicate only one next piece per source layer" +msgstr "" -msgid "Hide Label" -msgstr "Skjul etikett" +msgid "Ingest Devices" +msgstr "" -msgid "Script Source Layers" +msgid "Ingest devices are needed to create rundowns" msgstr "" -msgid "Source layers containing script" +msgid "Ingest from Snapshot" msgstr "" -msgid "Require Piece on Source Layer" +msgid "Ingest Rundown Status" msgstr "" -msgid "Text" -msgstr "Tekst" +msgid "Ingest Rundown Statuses" +msgstr "" -msgid "Show Rundown Name" -msgstr "Vis køyreplannamn" +msgid "Input Devices" +msgstr "" -msgid "Segment" -msgstr "Tittel" +msgid "Input devices allow you to trigger Sofie actions remotely" +msgstr "" -msgid "Part" -msgstr "Del" +msgid "Installation name" +msgstr "Installasjonsnamn" -msgid "Show Piece Icon Color" +msgid "Internal Error generating RundownPlaylist" msgstr "" -msgid "Use color of primary piece as background of panel" -msgstr "" +msgid "Internal ID" +msgstr "Intern-id" -msgid "Box color" -msgstr "" +msgid "Invalid AdLib" +msgstr "Ugyldig adlib" -msgid "Also Require Source Layers" +msgid "Invalid blueprint: \"{{blueprintId}}\"" msgstr "" msgid "" -"Specify additional layers where at least one layer must have an active piece" +"Invalid config preset for blueprint: \"{{configPresetId}}\" " +"({{blueprintId}})" msgstr "" -msgid "Require All Additional Source Layers" +msgid "Invert joystick" msgstr "" -msgid "All additional source layers must have active pieces" -msgstr "" +msgid "Is a Guest Input" +msgstr "Er ein gjesteinngang" -msgid "X" -msgstr "X" +msgid "Is a Live Remote Input" +msgstr "Er ein RM" -msgid "Y" -msgstr "Y" +msgid "Is collapsed by default" +msgstr "Er minimert som standard" -msgid "Width" -msgstr "Breidde" +msgid "Is flattened" +msgstr "Er slått saman" -msgid "Height" -msgstr "Høgde" +msgid "Is hidden" +msgstr "Er skjult" -msgid "Scale" -msgstr "Skala" +msgid "Is Immutable" +msgstr "" -msgid "Custom Classes" -msgstr "Tilpassa klasser" +msgid "Is PGM Output" +msgstr "Er programutgang" -msgid "Device ID" -msgstr "Eining-id" +msgid "ISA URLs" +msgstr "ISA-adresse (url)" -msgid "Device Type" -msgstr "Type eining" +msgid "Job Status" +msgstr "" -msgid "Remove this item?" -msgstr "Fjern dette elementet?" +msgid "Just now" +msgstr "No" -msgid "Are you sure you want to remove {{type}} \"{{deviceId}}\"?" -msgstr "Er du sikker på at du vil fjerne eininga {{type}} \"{{deviceId}}\"?" +msgid "Key" +msgstr "Key" -msgid "Attached Subdevices" -msgstr "Tilkopla undereiningar" +msgid "Keyboard" +msgstr "" -msgid "Expected End text" -msgstr "Tekst for venta slutt" +msgid "" +"Keyboard shortcuts and Stream Deck buttons will not work while filling out " +"the form!" +msgstr "" -msgid "Text to show above countdown to end of show" -msgstr "Tekst som blir vist over nedteljing til venta slutt" +msgid "Kill (debug)" +msgstr "Kill (debug)" -msgid "Hide Expected End timing when a break is next" -msgstr "Gøym nedteljing til venta slutt når neste punkt er ei pause" +msgid "Label" +msgstr "Etikett" -msgid "" -"While there are still breaks coming up in the show, hide the Expected End " -"timers" -msgstr "" -"Gøym nedteljing til venta slutt medan det framleis er pauser att i sendinga" +msgid "Label contains" +msgstr "Etikett inneheld" -msgid "Show next break timing" -msgstr "Vis tid for neste pause" +msgid "Last" +msgstr "Førre" -msgid "Whether to show countdown to next break" -msgstr "Om nedteljing til neste pause skal visast" +msgid "Last {{layerName}}" +msgstr "Siste {{layerName}}" + +msgid "Last modified" +msgstr "Sist endra" msgid "Last rundown is not break" msgstr "Siste køyreplan er inga pause" -msgid "Don't treat the end of the last rundown in a playlist as a break" +msgid "Last seen" +msgstr "Sist sett" + +msgid "Last Seen" msgstr "" -"Ikkje behandle slutten av den siste køyreplanen i ei speleliste som ei pause" -msgid "Next Break text" -msgstr "Tekst for neste pause" +msgid "Last update" +msgstr "Nyeste oppdatering" -msgid "Text to show above countdown to next break" -msgstr "Tekst som blir vist over nedteljing til neste pause" +msgid "Last updated" +msgstr "Sist oppdatert" -msgid "Expose as user selectable layout" -msgstr "Gjer tilgjengeleg som brukarvalgt layout" +msgid "Layer does not allow sticky pieces!" +msgstr "" -msgid "Shelf Layout" -msgstr "Layouter for skuffen" +msgid "Layer ID" +msgstr "Lag-id" -msgid "Mini Shelf Layout" -msgstr "Layouter for miniskuff" +msgid "Layer Mappings" +msgstr "Lagmapping" -msgid "Rundown Header Layout" -msgstr "Layout for køyreplanen sin topptekst" +msgid "Layer Name" +msgstr "Lagnamn" -msgid "Live line countdown requires Source Layer" -msgstr "" +msgid "Leave Unsynced" +msgstr "Behald ikkje-synkronisert køyreplan" -msgid "" -"One of these source layers must have an active piece for the live line " -"countdown to be show" -msgstr "" +msgid "Less than a minute ago" +msgstr "Under eitt minutt sidan" -msgid "Hide Rundown Divider" -msgstr "Skjul køyreplanskilje" +msgid "Less than five minutes ago" +msgstr "Under fem minutt sidan" -msgid "Hide rundown divider between rundowns in a playlist" -msgstr "Skjul skilje mellom køyreplanar i ei speleliste" +msgid "Lighting" +msgstr "" -msgid "Show Breaks as Segments" -msgstr "Vis pauser som titlar" +msgid "Limit" +msgstr "Grense" -msgid "Segment countdown requires source layer" -msgstr "Nedteljing for tittel krev kjeldelag" +msgid "Live line countdown requires Source Layer" +msgstr "" + +msgid "Live Speak" +msgstr "STK" -msgid "" -"One of these source layers must have a piece for the countdown to segment on-" -"air to be show" +msgid "Loading" msgstr "" -"Eit av desse kjeldelaga må ha eit element for at nedteljinga til tittelen er " -"OnAir visas" - -msgid "Fixed duration in Segment header" -msgstr "Låst lengde i tittelheader" -msgid "" -"The segment duration in the segment header always displays the planned " -"duration instead of acting as a counter" +msgid "Loading..." msgstr "" -"Tittelen si lengde i tittelheaderen vil alltid vise den planlagde lengda i " -"staden for å telje ned" -msgid "Select visible Source Layers" -msgstr "Vel synlege kjeldelag" +msgid "Local" +msgstr "Lokal" -msgid "Select visible Output Groups" -msgstr "Vel synleg gruppe for utgang" +msgid "Local Time" +msgstr "Lokal tid" -msgid "Display piece duration for source layers" +msgid "Logging level" +msgstr "Loggenivå" + +msgid "Logo" msgstr "" -msgid "Piece on selected source layers will have a duration label shown" +msgid "Lookahead Maximum Search Distance (Undefined = {{limit}})" msgstr "" -msgid "Expose layout as a standalone page" -msgstr "Gjer layout tilgjengeleg som ei sjølvstendig side" +msgid "Lookahead Mode" +msgstr "Lookahead-modus" -msgid "Open shelf by default" -msgstr "Åpne skuff som standard" +msgid "Lookahead Target Objects (Undefined = 1)" +msgstr "" -msgid "Default shelf height" -msgstr "Standard høyde for skuff" +msgid "Loop End" +msgstr "Slutt for loop" -msgid "Show Buckets" -msgstr "Vis bøtter" +msgid "Loop Start" +msgstr "Start for loop" -msgid "Show Inspector" +msgid "Loops to Start" msgstr "" -msgid "Disable Context Menu" -msgstr "Skruv av kontekstmeny" - -msgid "This action has an invalid combination of filters" -msgstr "Denne handlinga har ein ugydlig kombinasjon av filtre" +msgid "Lower Third" +msgstr "Super" -msgid "Use Trigger Mode" -msgstr "Type utløysar" +msgid "Manage Snapshots" +msgstr "Behandle snapshots" -msgid "Trigger Mode" +msgid "Mapping cannot be reset as it has no default values" msgstr "" -msgid "Force" -msgstr "Tving" +msgid "Mapping Type" +msgstr "" -msgid "Rehearsal" -msgstr "Testmodus" +msgid "Mappings" +msgstr "Lagmapping" -msgid "Mode: {{triggerMode}}" +msgid "Margin (%)" msgstr "" -msgid "Undo" -msgstr "Angre" - -msgid "Segments: {{delta}}" -msgstr "Segment: {{delta}}" +msgid "Maximum register limit" +msgstr "" -msgid "Parts: {{delta}}" -msgstr "Delar: {{delta}}" +msgid "Media" +msgstr "Media" -msgid "Open" -msgstr "Opne" +msgid "Media Preview URL" +msgstr "Førehandsvisningsadresse (url)" -msgid "Toggle" -msgstr "Veklse" +msgid "Media Status" +msgstr "" -msgid "On" -msgstr "På" +msgid "Media Type" +msgstr "" -msgid "Forward: {{forward}}" +msgid "Memory troubleshooting" msgstr "" -msgid "Activate Rundown" -msgstr "Aktiver køyreplan" +msgid "Menu" +msgstr "" -msgid "Ad-Lib" -msgstr "Adlib" +msgid "Message" +msgstr "Melding" -msgid "Deactivate Rundown" -msgstr "Deaktiver køyreplan" +msgid "Message Queue" +msgstr "Kø for meldingar" -msgid "Disable next Piece" -msgstr "Skip neste element" +msgid "Message shown to users in the Evaluations form" +msgstr "" -msgid "Move Next" -msgstr "Skip neste" +msgid "Messages" +msgstr "Meldingar" -msgid "Reload NRCS Data" -msgstr "Last inn MOS-data på nytt" +msgid "Method" +msgstr "Metode" -msgid "Resync with NRCS" -msgstr "Synkroniser med ENPS" +msgid "Method ${method}" +msgstr "" -msgid "Shelf" -msgstr "Skuff" +msgid "MIDI Pedal" +msgstr "" -msgid "Rewind Segments to start" -msgstr "Sett alle segment attende til start" +msgid "Migrate database" +msgstr "Migrer database" -msgid "Go to On Air line" -msgstr "Gå til OnAir-posisjon" +msgid "Migrations" +msgstr "Migrering" -msgid "Show entire On Air Segment" -msgstr "Vis heile tittelen som er OnAir" +msgid "Mini Shelf Layout" +msgstr "Layouter for miniskuff" -msgid "Queue AdLib from Minishelf" +msgid "Mini Shelf Layouts" msgstr "" -msgid "Force (deactivate others)" -msgstr "Tving (deaktiver andre)" - -msgid "Move Segments" -msgstr "Skip segment" +msgid "Minimum register limit" +msgstr "" -msgid "By Segments" +msgid "Minimum Take Span" msgstr "" -msgid "Move Parts" -msgstr "Skip del" +msgid "Minor Warning" +msgstr "Mindre åtvaring (avvik)" -msgid "By Parts" +msgid "Mirror horizontally" msgstr "" -msgid "State" -msgstr "Tilstand" - -msgid "Forward" +msgid "Mirror vertically" msgstr "" -msgid "Action" -msgstr "Handling" +msgid "Mock Piece Content Status" +msgstr "" -msgid "Ad-Lib Action" -msgstr "Adlib-handling" +msgid "Mode: {{triggerMode}}" +msgstr "" -msgid "Clear Source Layer" -msgstr "Tøm kjeldelag" +msgid "Modify Shift register" +msgstr "" -msgid "Sticky Piece" -msgstr "Element er sticky" +msgid "Modifying Bucket" +msgstr "" -msgid "Global AdLibs" -msgstr "Globale adliber" +msgid "Modifying Bucket AdLib" +msgstr "" -msgid "Label" -msgstr "Etikett" +msgid "Monitor blocked thread" +msgstr "" -msgid "Limit" -msgstr "Grense" +msgid "More documentation available at:" +msgstr "Meir dokumentasjon er tilgjengeleg på:" -msgid "Output Layer" -msgstr "Utgangslag" +msgid "More than 10 minutes ago" +msgstr "Over 10 minutt sidan" -msgid "Pick" -msgstr "Plukk" +msgid "More than 2 hours ago" +msgstr "Over 2 timer sidan" -msgid "Pick last" -msgstr "Plukk siste" +msgid "More than 30 minutes ago" +msgstr "Over 30 minutt sidan" -msgid "Source Layer" -msgstr "Kjeldelag" +msgid "More than 5 hours ago" +msgstr "Over 5 timar sidan" -msgid "Source Layer Type" -msgstr "Kjeldelagstypar" +msgid "More than a day ago" +msgstr "Over ein dag sidan" -msgid "Tag" -msgstr "Tag" +msgid "Mouse" +msgstr "" -msgid "Not Global" -msgstr "Ikkje globale" +msgid "Move Next" +msgstr "Skip neste" -msgid "Only Global" -msgstr "Berre globale" +msgid "Move Next backwards" +msgstr "Unskip neste" -msgid "OnAir" -msgstr "OnAir" +msgid "Move Next forwards" +msgstr "Skip neste" -msgid "Now active rundown" -msgstr "Aktiv køyreplan nett no" +msgid "Move Next to the following segment" +msgstr "Skip til neste segment" -msgid "View" -msgstr "Visning" +msgid "Move Next to the previous segment" +msgstr "Unskip neste segment" -msgid "" -"Executes within the currently open Rundown, requires a Client-side trigger." -msgstr "" -"Blir utførte innanfor den valde køyreplanen, men treng ein utløysar frå " -"klienten." +msgid "Move Parts" +msgstr "Skip del" -msgid "Select Action" -msgstr "Vel handling" +msgid "Move Segments" +msgstr "Skip segment" -msgid "" -"No Ad-Lib matches in the current state of Rundown: " -"\"{{rundownPlaylistName}}\"" +msgid "Moving Next" msgstr "" -"Ingen treff på adliber i noverande tilstand for køyeplanen: " -"\"{{rundownPlaylistName}}\"" -msgid "No matching Rundowns available to be used for preview" -msgstr "Ingen passande køyreplanar tilgjengelege for førehandvisning" +msgid "Multi-gateway-mode delay time" +msgstr "Delaytid for multigateway-modus" msgid "Multilingual description, editing will overwrite" msgstr "Endring vil overskrive fleirspråkleg skildring" -msgid "Optional description of the action" -msgstr "Valfri skildring av handlinga" - -msgid "Triggered Actions uploaded successfully." -msgstr "Opplasting av handlingsutløysarar var vellukka." - -msgid "Triggered Actions failed to upload: {{errorMessage}}" -msgstr "Opplasting av handlingsutløysarar feila: {{errorMessage}}" +msgid "My name is {{name}}" +msgstr "Mitt namn er {{name}}" -msgid "Append or Replace" -msgstr "Legg til eller erstatt" +msgid "Name" +msgstr "Namn" -msgid "" -"Do you want to append these to existing Action Triggers, or do you want to " -"replace them?" +msgid "Network address" msgstr "" -"Vil du legge desse til dei noverande handlingsutløysarane, eller vil du " -"erstatta dei?" - -msgid "Append" -msgstr "Legg til" -msgid "Action Triggers" -msgstr "Handlingsutløysarar" - -msgid "Find Trigger..." -msgstr "Finn utløysar..." - -msgid "No matching Action Trigger." -msgstr "Fekk ikkje treff blant handlingsutløysar." - -msgid "No Action Triggers set up." -msgstr "Ingen handlingsutløysarar er satt opp." - -msgid "System-wide" -msgstr "Systemvid" +msgid "Network Id" +msgstr "Nettverk-id" -msgid "Upload stored Action Triggers" -msgstr "Last opp lagra handlingsutløysarar" +msgid "New Bucket" +msgstr "Ny bøtte" -msgid "Download Action Triggers" -msgstr "Last ned handlingsutløysarar" +msgid "New Filter" +msgstr "Nytt filter" -msgid "On release" -msgstr "På slipp (\"Key up\")" +msgid "New Layer" +msgstr "Nytt lag" -msgid "Empty" -msgstr "Tom" +msgid "New Layout" +msgstr "Ny layout" -msgid "Hotkey" -msgstr "Hurtigtast" +msgid "New Output" +msgstr "Ny utgang" -msgid "Trigger Type" -msgstr "Type utløysar" +msgid "New Source" +msgstr "Ny kjelde" -msgid "Failed to update config: {{errorMessage}}" -msgstr "Oppdatering av konfigurasjon feila: {{errorMessage}}" +msgid "Next" +msgstr "Neste" -msgid "Export" -msgstr "Eksporter" +msgid "Next Break text" +msgstr "Tekst for neste pause" -msgid "Import" -msgstr "Import" +msgid "Next Loop at" +msgstr "Neste loop starter" -msgid "true" -msgstr "true" +msgid "Next Part" +msgstr "Neste del" -msgid "false" -msgstr "false" +msgid "Next scheduled show" +msgstr "Neste planlagde sending" -msgid "{{count}} rows" -msgstr "{{count}} rader" +msgid "Next Segment" +msgstr "Neste tittel" -msgid "Value" -msgstr "Verdi" +msgid "Nintendo Joy-Con" +msgstr "" -msgid "Create" -msgstr "Opprett" +msgid "No" +msgstr "Nei" -msgid "Add config item" -msgstr "Legg til konfigurasjonselement" +msgid "No Action Triggers set up." +msgstr "Ingen handlingsutløysarar er satt opp." -msgid "Add" -msgstr "Legg til" +msgid "No actions available" +msgstr "Ingen køyreplanval tilgjengelege i påsynmodus" -msgid "Item" -msgstr "Element" +msgid "" +"No Ad-Lib matches in the current state of Rundown: " +"\"{{rundownPlaylistName}}\"" +msgstr "" +"Ingen treff på adliber i noverande tilstand for køyeplanen: " +"\"{{rundownPlaylistName}}\"" -msgid "Delete this item?" -msgstr "Slett dette elementet?" +msgid "No camera-related source layers found" +msgstr "" -msgid "Are you sure you want to delete this config item \"{{configId}}\"?" +msgid "No changes" msgstr "" -"Er du sikker på at du vil slette dette konfigurasjonselementet " -"\"{{configId}}\"?" -msgid "Blueprint Configuration" -msgstr "Blueprintkonfigurasjon" +msgid "No gateways are configured" +msgstr "" -msgid "More settings specific to this studio can be found here" -msgstr "Meir spesifikke innstillingar for dette studioet finn du her" +msgid "No matching Action Trigger." +msgstr "Fekk ikkje treff blant handlingsutløysar." -msgid "There was an error: {{error}}" -msgstr "Det skjedde ein feil: {{error}}" +msgid "No matching Rundowns available to be used for preview" +msgstr "Ingen passande køyreplanar tilgjengelege for førehandvisning" -msgid "Package Manager status" -msgstr "Status for pakkebehandlar" +msgid "No Media matches this filter" +msgstr "" -msgid "Reload statuses" -msgstr "Last inn status om att" +msgid "No Media required by this system" +msgstr "" -msgid "Updated" -msgstr "Oppdatert" +msgid "No Media required for this Rundown" +msgstr "" -msgid "Package Manager" -msgstr "Pakkebehandlar" +msgid "No migrations to apply" +msgstr "" -msgid "Expectation Manager" +msgid "No name set" +msgstr "Namn ikkje definert" + +msgid "No Next point found, please set a part as Next before doing a TAKE." msgstr "" -msgid "Statistics" -msgstr "Statistikk" +msgid "No notifications" +msgstr "" -msgid "Times" -msgstr "Tider" +msgid "No output channels set" +msgstr "Ingen utgangskanal definert" -msgid "Connected Workers" -msgstr "Tilkopla arbeidarar" +msgid "No output layers available" +msgstr "" -msgid "Work-in-progress" -msgstr "Pågåande jobbar" +msgid "No PGM output" +msgstr "Ingen programutgang" -msgid "WorkForce" -msgstr "Arbeidarstyrke" +msgid "No problems" +msgstr "Ingen problem" -msgid "Kill (debug)" -msgstr "Kill (debug)" +msgid "No schema has been provided for this mapping" +msgstr "" -msgid "Connected Expectation Managers" +msgid "No source layers available" msgstr "" -msgid "Connected App Containers" -msgstr "Tilkopla app-kontainere" +msgid "No source layers set" +msgstr "Ingen kjeldelag definert" msgid "No status loaded" msgstr "Ingen status lasta" -msgid "Peripheral Device is outdated" -msgstr "Tilkopla eining er utdatert" +msgid "None" +msgstr "Ingen" -msgid "" -"The config UI is now driven by manifests fed by the device. This device " -"needs updating to provide the configManifest to be configurable" +msgid "Normal scrolling" msgstr "" -"Brukergrensesnitt for konfigurasjon drivast no av manifest mata frå " -"einingane. Denne einga må oppdaterast for å gjere configManifest " -"konfigurerbart" -msgid "Are you sure you want to restart this device?" -msgstr "Er du sikker på at du vil starte denne eininga på nytt?" +msgid "Not Active" +msgstr "Inaktiv" -msgid "Restart this Device?" -msgstr "Start denne eininga på nytt?" +msgid "Not Connected" +msgstr "Ikkje tilkopla" -msgid "" -"Check the console for troubleshooting data from device \"{{deviceName}}\"!" -msgstr "Sjekk konsollen for feilsøkingsdata frå eninga \"{{deviceName}}\"!" +msgid "Not defined" +msgstr "Ikkje definert" -msgid "" -"There was an error when troubleshooting the device: \"{{deviceName}}\": " -"{{errorMessage}}" +msgid "Not Global" +msgstr "Ikkje globale" + +msgid "Not in rehearsal" msgstr "" -"Det hende ein feil under feilsøking av eininga \"{{deviceName}}\": " -"{{errorMessage}}" -msgid "Generic Properties" -msgstr "Generelle eigenskapar" +msgid "Not queueable" +msgstr "Kan ikkje setjast i kø" -msgid "Device Name" -msgstr "Einingsnamn" +msgid "Not set" +msgstr "Ikkje angjeve" -msgid "Restart Device" -msgstr "Start eining på nytt" +msgid "Note: Core needs to be restarted to apply these settings" +msgstr "Merknad: Core må startast om att for å ta i bruk desse innstillingane" -msgid "Troubleshoot" -msgstr "Feilsøk" +msgid "Notes" +msgstr "" -msgid "Reset Database Version" -msgstr "Nullstill databaseversjon" +msgid "Nothing to cleanup!" +msgstr "" -msgid "" -"Are you sure you want to reset the database version?\n" -"Only do this if you plan on running the migration right after." +msgid "Nothing was found on layer!" msgstr "" -"Er du sikker på at du vil nullstille databaseversjonen?\n" -"Berre gjer dette om du har tenkt å køyre ei migrering med ein gong." -msgid "Version for {{name}}: From {{fromVersion}} to {{toVersion}}" -msgstr "Versjon for {{name}}: Frå {{fromVersion}} til {{toVersion}}" +msgid "Now Active Rundown" +msgstr "" -msgid "Re-check" -msgstr "Sjekk om att" +msgid "NRCS Name" +msgstr "" -msgid "Reset Version to" -msgstr "Nullstill versjon til" +msgid "OAuth credentials successfully reset" +msgstr "" -msgid "Reset All Versions" -msgstr "Nullstill alle versjonar" +msgid "OAuth credentials successfully uploaded." +msgstr "" -msgid "Migrate database" -msgstr "Migrer database" +msgid "Off" +msgstr "Av" -msgid "All steps" -msgstr "Alle steg" +msgid "Off-line devices" +msgstr "Fråkoplete einingar" -msgid "" -"The migration consists of several phases, you will get more options after " -"you've this migration" -msgstr "" -"Migreringa har fleire fasar, du vil få fleire val etter at du har køyrd " -"denne migreringa" +msgid "OK" +msgstr "OK" -msgid "The migration can be completed automatically." -msgstr "Migreringa kan gjerast ferdig automatisk." +msgid "On" +msgstr "På" -msgid "Run automatic migration procedure" -msgstr "Køyr automatisk migreringsprosedyre" +msgid "On Air" +msgstr "On Air" -msgid "" -"The migration procedure needs some help from you in order to complete, see " -"below:" -msgstr "" -"Migreringsprosedyra treng litt hjelp frå deg for å gjere seg ferdig. Sjå " -"under:" +msgid "On Air At" +msgstr "On Air klokka" -msgid "Double-check Values" -msgstr "Dobbeltsjekk verdiar" +msgid "On Air In" +msgstr "On Air om" -msgid "Are you sure the values you have entered are correct?" -msgstr "Er du sikker på at verdiane du har oppgitt er korrekte?" +msgid "On Air Start Time" +msgstr "Sendestart" -msgid "Run Migration Procedure" -msgstr "Køyr migreringsprosedyre" +msgid "On release" +msgstr "På slipp (\"Key up\")" -msgid "Warnings During Migration" -msgstr "Åtvaringar under migrering" +msgid "OnAir" +msgstr "OnAir" msgid "" -"Please check the database related to the warnings above. If neccessary, you " -"can" +"One of these source layers must have a piece for the countdown to segment " +"on-air to be show" msgstr "" -"Ver vennleg og sjekk databasen tilknytta åtvaringane over. Om det er " -"naudsynt kan du" - -msgid "Force Migration" -msgstr "Tving migrering" +"Eit av desse kjeldelaga må ha eit element for at nedteljinga til tittelen " +"er OnAir visas" msgid "" -"Are you sure you want to force the migration? This will bypass the migration " -"checks, so be sure to verify that the values in the settings are correct!" +"One of these source layers must have an active piece for the live line " +"countdown to be show" msgstr "" -"Er du sikker på at du vil tvinge migreringa? Dette gjer at du hoppar over " -"migreringskontrollane, så ver sikker på at verdiane oppgitt i innstillingar " -"er korrekte!" - -msgid "Force Migration (unsafe)" -msgstr "Tving migrering (utrygt)" - -msgid "The migration was completed successfully!" -msgstr "Migreringa var vellukka!" - -msgid "All is well, go get a" -msgstr "Alt er greitt, gå og finn deg ein" - -msgid "New Layout" -msgstr "Ny layout" - -msgid "Button" -msgstr "Knapp" -msgid "New Filter" -msgstr "Nytt filter" +msgid "Only custom trigger modes will be shown" +msgstr "" -msgid "Delete layout?" -msgstr "Slett layout?" +msgid "Only Display AdLibs from Current Segment" +msgstr "Vis berre adliber frå gjeldande tittel" -msgid "Are you sure you want to delete the shelf layout \"{{name}}\"?" -msgstr "Er du sikker på at du vil slette layouten \"{{name}}\"?" +msgid "Only Global" +msgstr "Berre globale" -msgid "Action Buttons" -msgstr "Handlingsknappar" +msgid "Only Match Global AdLibs" +msgstr "Vis kun globale adliber" -msgid "Toggled Label" +msgid "" +"Only one rundown can be active at the same time. Currently active rundowns: " +"{{names}}" msgstr "" -msgid "Icon" -msgstr "Ikon" +msgid "Only Pieces present in rundown are sticky" +msgstr "Kun element til stades i køyreplanen er sticky" -msgid "Icon color" -msgstr "Ikonfarge" +msgid "Open" +msgstr "Opne" -msgid "Filters" -msgstr "Filtre" +msgid "Open Camera Screen" +msgstr "" -msgid "There are no filters set up yet" -msgstr "Det er ikkje satt opp noko filter enno" +msgid "Open Fullscreen" +msgstr "" -msgid "Default Layout" -msgstr "Standardlayout" +msgid "Open Presenter Screen" +msgstr "" -msgid "Add {{filtersTitle}}" -msgstr "Legg til {{filtersTitle}}" +msgid "Open Prompter" +msgstr "" -msgid "Add filter" -msgstr "Legg til filter" +msgid "Open shelf by default" +msgstr "Åpne skuff som standard" -msgid "Add button" -msgstr "Legg til knapp" +msgid "Operating Mode" +msgstr "Styringsmodus" -msgid "Upload Layout?" -msgstr "Last opp layout?" +msgid "Operation" +msgstr "" -msgid "Upload" -msgstr "Last opp" +msgid "Optional description of the action" +msgstr "Valfri skildring av handlinga" msgid "" -"Are you sure you want to upload the shelf layout from the file " -"\"{{fileName}}\"?" +"Optionally restrict AB channel display to specific output layers (e.g., " +"only PGM). Leave empty to show for all output layers." msgstr "" -"Er du sikker på at du vil laste opp layout for skuff frå fila " -"\"{{fileName}}\"?" -msgid "Shelf layout uploaded successfully." -msgstr "Opplasting av layout for skuff var vellukka." +msgid "Order 66?" +msgstr "" -msgid "Failed to upload shelf layout: {{errorMessage}}" -msgstr "Opplasting av layout feila: {{errorMessage}}" +msgid "Original Layer" +msgstr "Opprinneleg lag" -msgid "Show Style Base Name" -msgstr "Showstylenamn" +msgid "Original Layer not found" +msgstr "" -msgid "Blueprint" -msgstr "Blueprint" +msgid "Other" +msgstr "" -msgid "Blueprint not set" -msgstr "Blueprint ikkje valt" +msgid "Out" +msgstr "Ut" -msgid "Compatible Studios:" -msgstr "Kompatible studio:" +msgid "OUT" +msgstr "" -msgid "This Show Style is not compatible with any Studio" -msgstr "Denne showstylen er ikkje kompatibelt med noko studio" +msgid "Output channels" +msgstr "Utgangskanalar" -msgid "Camera" -msgstr "Kamera" +msgid "Output Channels" +msgstr "Utgangskanalar" -msgid "Graphics" -msgstr "Grafikk" +msgid "Output channels are required for your studio to work" +msgstr "Utgangskanalar er naudsynte for at studioet ditt skal fungere" -msgid "Live Speak" -msgstr "STK" +msgid "Output Layer" +msgstr "Utgangslag" -msgid "Lower Third" -msgstr "Super" +msgid "Over" +msgstr "" -msgid "Studio Microphone" -msgstr "Studiomikrofon" +msgid "Over/Under" +msgstr "Over/Under" -msgid "Remote Source" -msgstr "RM" +msgid "Overflow horizontally" +msgstr "Horisontal overflyt" -msgid "Generic Script" -msgstr "Generisk manus" +msgid "Overlay Screen" +msgstr "" -msgid "Split Screen" -msgstr "Splitt" +msgid "Package Container ID" +msgstr "Pakkekontainer-id" -msgid "Clips" -msgstr "Klipp" +msgid "Package Containers" +msgstr "Pakkekontainere" -msgid "Metadata" -msgstr "Metadata" +msgid "Package Containers to use for previews" +msgstr "Pakkekontainere som skal nyttast til førehandsvisingar" -msgid "Camera Movement" -msgstr "Kamerarørsle" +msgid "Package Containers to use for thumbnails" +msgstr "Pakkekontainere som skal nyttast til miniatyrbilete" -msgid "Unknown Layer" -msgstr "Ukjend lag" +msgid "Package Manager" +msgstr "Pakkebehandlar" -msgid "Audio Mixing" -msgstr "Lydmiksing" +msgid "Package Manager is offline" +msgstr "" -msgid "Transition" -msgstr "Effekt" +msgid "Package Manager status" +msgstr "Status for pakkebehandlar" -msgid "Lights" -msgstr "Lys" +msgid "Package Manager: Restart Package Container" +msgstr "" -msgid "Local" -msgstr "Lokal" +msgid "Package Manager: Restart work" +msgstr "" -msgid "New Source" -msgstr "Ny kjelde" +msgid "Package Status" +msgstr "Pakkestatus" -msgid "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?" -msgstr "Er du sikker på at du vil slette kjeldelaget \"{{sourceLayerId}}\"?" +msgid "Packages" +msgstr "Pakker" -msgid "Source Name" -msgstr "Kjeldenamn" +msgid "Parameters" +msgstr "Parametrar" -msgid "Source Abbreviation" -msgstr "Kjeldeforkorting" +msgid "Parent Config ID" +msgstr "" -msgid "Internal ID" -msgstr "Intern-id" +msgid "Parent device is missing" +msgstr "" -msgid "Source Type" -msgstr "Kjeldetype" +msgid "Parent Devices" +msgstr "" -msgid "Is a Live Remote Input" -msgstr "Er ein RM" +msgid "part" +msgstr "punkt" -msgid "Is a Guest Input" -msgstr "Er ein gjesteinngang" +msgid "Part" +msgstr "Del" -msgid "Is hidden" -msgstr "Er skjult" +msgid "Part Count Down" +msgstr "Nedteljing for del" -msgid "Pieces on this layer can be cleared" -msgstr "Element på dette laget kan tømmas" +msgid "Part Count Up" +msgstr "Opptelling for del" -msgid "Pieces on this layer are sticky" -msgstr "Element på dette laget er sticky" +msgid "Part duration is 0." +msgstr "" -msgid "Only Pieces present in rundown are sticky" -msgstr "Kun element til stades i køyreplanen er sticky" +msgid "Parts Duration" +msgstr "Varigheit for del" -msgid "Allow disabling of Pieces" -msgstr "Tillat deaktivering av element" +msgid "Parts: {{delta}}" +msgstr "Delar: {{delta}}" -msgid "AdLibs on this layer can be queued" -msgstr "Adliber på dette laget kan cues" +msgid "Password" +msgstr "Passord" -msgid "Exclusivity group" -msgstr "Ekslusivitetgruppe" +msgid "Password for authentication" +msgstr "Passord for autentisering" -msgid "" -"Add some source layers (e.g. Graphics) for your data to appear in rundowns" +msgid "Peripheral Device is outdated" +msgstr "Tilkopla eining er utdatert" + +msgid "Peripheral Device not found!" msgstr "" -"Legg til kjeldelag (til dømes Grafikk) for å vise dine data i køyreplanar" -msgid "No source layers set" -msgstr "Ingen kjeldelag definert" +msgid "Peripheral Devices" +msgstr "" -msgid "Delete this output?" -msgstr "Slett denne utgangen?" +msgid "Pick" +msgstr "Plukk" -msgid "Are you sure you want to delete source layer \"{{outputId}}\"?" -msgstr "Er du sikker på at du vil slette kjeldelaget \"{{outputId}}\"?" +msgid "Pick last" +msgstr "Plukk siste" -msgid "New Output" -msgstr "Ny utgang" +msgid "" +"Picks the first instance of an adLib per rundown, identified by uniqueness " +"Id" +msgstr "" +"Velger den første førekomsten av ein adlib i kvar køyreplan, identifisert " +"av unik id" -msgid "Channel Name" -msgstr "Kanalnavn" +msgid "Piece on selected source layers will have a duration label shown" +msgstr "" -msgid "Is PGM Output" -msgstr "Er programutgang" +msgid "Piece to take is already live!" +msgstr "" -msgid "Is collapsed by default" -msgstr "Er minimert som standard" +msgid "Piece to take is not directly playable!" +msgstr "" -msgid "Is flattened" -msgstr "Er slått saman" +msgid "Piece to take was not found!" +msgstr "" -msgid "Output channels are required for your studio to work" -msgstr "Utgangskanalar er naudsynte for at studioet ditt skal fungere" +msgid "Pieces on this layer are sticky" +msgstr "Element på dette laget er sticky" -msgid "Output channels" -msgstr "Utgangskanalar" +msgid "Pieces on this layer can be cleared" +msgstr "Element på dette laget kan tømmas" -msgid "No output channels set" -msgstr "Ingen utgangskanal definert" +msgid "Place label below panel" +msgstr "Plasser etikett under panel" -msgid "No PGM output" -msgstr "Ingen programutgang" +msgid "Plan. Dur" +msgstr "" -msgid "Key" -msgstr "Key" +msgid "Plan. End" +msgstr "" -#, fuzzy -#| msgid "Host" -msgid "Host Key" -msgstr "Vert" +msgid "Plan. Start" +msgstr "" -#, fuzzy -#| msgid "Source Layer Type" -msgid "Source Layer type" -msgstr "Kjeldelagstypar" +msgid "Planned Duration" +msgstr "Planlagt varigheit" -#, fuzzy -#| msgid "Icon color" -msgid "Key color" -msgstr "Ikonfarge" +msgid "Planned End" +msgstr "Planlagt slutt" -msgid "Custom Hotkey Labels" -msgstr "Eigendefinerte etikettar for hurtigtastar" +msgid "Planned End text" +msgstr "Tekst for planlagt slutt" -msgid "AHK" -msgstr "" +msgid "Planned Start" +msgstr "Planlagt start" + +msgid "Planned Start Text" +msgstr "Tekst for planlagt start" -msgid "Remove this Variant?" -msgstr "Fjern denne varianten?" +msgid "Play-out" +msgstr "Avspelning" -msgid "Are you sure you want to remove the variant \"{{showStyleVariantId}}\"?" +msgid "Playlist" msgstr "" -"Er du sikker på at du vil fjerne denne showstylevarianten " -"\"{{showStyleVariantId}}\"?" -msgid "Unnamed variant" -msgstr "Variant utan namn" +msgid "Playout Devices" +msgstr "" -msgid "Variant Name" -msgstr "Variantnamn" +msgid "Playout devices are needed to control your studio hardware" +msgstr "" -msgid "Variants" -msgstr "Variantar" +msgid "Playout devices which uses this package container" +msgstr "Playout-einingar som nyttar denne pakkekontaineren" -msgid "Restore from this Snapshot file?" -msgstr "Tilbakestill frå denne snapshotfila?" +msgid "Playout Gateway \"{{playoutDeviceName}}\" is now restarting." +msgstr "Playout-gateway \"{{playoutDeviceName}}\" startar om att." msgid "" -"Are you sure you want to restore the system from the snapshot file " -"\"{{fileName}}\"?" +"Please check the database related to the warnings above. If neccessary, you " +"can" +msgstr "" +"Ver vennleg og sjekk databasen tilknytta åtvaringane over. Om det er " +"naudsynt kan du" + +msgid "Please explain the problems you experienced" msgstr "" -"Er du sikker på at du vil tilbakestille systemet frå denne snapshotfila " -"\"{{fileName}}\"?" -msgid "Successfully restored snapshot" -msgstr "Tilbakestilling frå snapshot var vellukka" +msgid "Please note: This action is irreversible!" +msgstr "Merk: Denne handlinga kan du ikkje angre!" -msgid "Snapshot restore failed: {{errorMessage}}" -msgstr "Tilbakestilling frå snapshot feila: {{errorMessage}}" +msgid "Pool name" +msgstr "" -msgid "Full System Snapshot" -msgstr "Fullt systemsnapshot" +msgid "Pool PlayerId" +msgstr "" -msgid "" -"A Full System Snapshot contains all system settings (studios, showstyles, " -"blueprints, devices, etc.)" +msgid "Prepare Studio and Activate (Rehearsal)" +msgstr "Førebu studio og aktiver testmodus" + +msgid "Preparing for broadcast" msgstr "" -"Eit fullt systemsnapshot inneheld alle systeminnstillingar (studio, " -"showstyles, blueprints, einingar o.s.b.)" -msgid "Take a Full System Snapshot" -msgstr "Lagre eit fullt systemsnapshot" +msgid "Preparing, please wait..." +msgstr "" -msgid "Studio Snapshot" -msgstr "Studiosnapshot" +msgid "Presenter Layout" +msgstr "" -msgid "A Studio Snapshot contains all system settings related to that studio" +msgid "Presenter screen" msgstr "" -"Eit studiosnapshot inneheld alle systeminnstillingar knytt til eit studio" -msgid "Take a Snapshot for studio \"{{studioName}}\" only" -msgstr "Lagre eit studiosnapshot utelukkande for \"{{studioName}}\"" +msgid "Presenter Screen" +msgstr "" -msgid "Restore from Snapshot File" -msgstr "Tilbakestill frå snapshotfil" +msgid "Presenter View Layouts" +msgstr "" -msgid "Upload Snapshot" -msgstr "Last opp snapshot" +msgid "Preserve position of segments when unsynced relative to other segments" +msgstr "" -msgid "Restore from Stored Snapshots" -msgstr "Tilbakestill frå lagra snapshots" +msgid "Previous" +msgstr "" -msgid "Restore" -msgstr "Tilbakestill" +msgid "Previous work status reasons" +msgstr "Tidlegare årsakar for jobbsatus" -msgid "Show \"Remove snapshots\"-buttons" -msgstr "Vis \"Fjern snapshots\"-knappar" +msgid "Prioritizing Media Workflow" +msgstr "" -msgid "Remove this device?" -msgstr "Fjern denne eininga?" +msgid "Priority" +msgstr "Prioritet" -msgid "Are you sure you want to remove device \"{{deviceId}}\"?" -msgstr "Er du sikker på at du vil fjerne eininga \"{{deviceId}}\"?" +msgid "Problems" +msgstr "Problem" -msgid "Devices are needed to control your studio hardware" -msgstr "Einingar er naudsynte for å kontrollere utstyr i studioet ditt" +msgid "Profile name to be used by FileFlow when exporting the clips" +msgstr "Profilnamn som blir nytta av FileFlow når klippa vert eksportert" -msgid "Attached Devices" -msgstr "Tilkopla eingingar" +msgid "Prompter" +msgstr "Prompter" -msgid "No devices connected" -msgstr "Ingen einingar tilkopla" +msgid "Prompter Screen" +msgstr "" -msgid "Playout gateway not connected" -msgstr "Playout-gateway ikkje tilkopla" +msgid "Properties" +msgstr "" -msgid "Remove this mapping?" -msgstr "Fjern denne mappinga?" +msgid "Quantel FileFlow Profile name" +msgstr "Quantel FileFlow profilnavn" -msgid "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?" -msgstr "Er du sikker på at du fil fjerne mappinga for laget \"{{mappingId}}\"?" +msgid "Quantel FileFlow URL" +msgstr "Quantel GatewayFileFlow-adresse (url)" -msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" -msgstr "Dette laget vert omkopla av ei aktiv omkoplingsgruppe: {{routeSets}}" +msgid "Quantel gateway URL" +msgstr "Quantel Gateway-adresse (url)" -msgid "Layer ID" -msgstr "Lag-id" +msgid "Quantel transformer URL" +msgstr "Quantel Transformer-adresse (url)" -msgid "ID of the timeline-layer to map to some output" -msgstr "Lag-id for tidslinjelaget som skal mappast til ein utgang" +msgid "Quantel Zone ID" +msgstr "" -msgid "Layer Name" -msgstr "Lagnamn" +msgid "Queue AdLib from Minishelf" +msgstr "" -msgid "Human-readable name of the layer" -msgstr "Lesarvenleg lagnamn" +msgid "Queue all adlibs" +msgstr "Cue alle adliber" -msgid "The type of device to use for the output" -msgstr "Einingtype som skal nyttast for utgangen" +msgid "Queue segment" +msgstr "Cue tittel: Startar når aktiv tittel er ferdig" -msgid "" -"ID of the device (corresponds to the device ID in the peripheralDevice " -"settings)" -msgstr "Eining-id (korresponderer med enhets-id under enhetsinnstillinger)" +msgid "Queue this AdLib" +msgstr "Cue denne adliben" -msgid "Lookahead Mode" -msgstr "Lookahead-modus" +msgid "Queued Messages" +msgstr "Meldingar i kø" -msgid "Lookahead Target Objects (Default = 1)" -msgstr "Lookahead målobjekter (standard = 1)" +msgid "Queueing next Segment" +msgstr "" -msgid "Lookahead Maximum Search Distance (Default = {{limit}})" -msgstr "Lookahead maksimum søkelengde (standard = {{limit}})" +msgid "Quick Links" +msgstr "" -msgid "Layer Mappings" -msgstr "Lagmapping" +msgid "QuickLoop Fallback Part Duration" +msgstr "" -msgid "Add a playout device to the studio in order to edit the layer mappings" +msgid "Range: Forward max" msgstr "" -"For å kunne redigere lagmappingar, må du leggje til ein playout-eining til " -"studio" -msgid "Remove this Exclusivity Group?" -msgstr "Fjern frå denne eksklusivitetgruppa?" +msgid "Range: Neutral max" +msgstr "" -msgid "" -"Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\n" -"Route Sets assigned to this group will be reset to no group." +msgid "Range: Neutral min" msgstr "" -"Er du sikker på at du vil fjerne eksklusivitetsgruppa \"{{eGroupName}}\"?\n" -"Omkoplingar satt til denne gruppa vil bli resatt til inga gruppe." -msgid "Remove this Route from this Route Set?" -msgstr "Fjern denne omkoplinga frå denne omkoplingsgruppa?" +msgid "Range: Reverse min" +msgstr "" -msgid "" -"Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to " -"\"{{newLayerId}}\"?" +msgid "Rate limit exceeded" msgstr "" -"Er du sikker på at du vil fjerne omkoplinga frå \"{{sourceLayerId}}\" til " -"\"{{newLayerId}}\"?" -msgid "Remove this Route Set?" -msgstr "Fjern denne omkoplingsgruppa?" +msgid "Re-check" +msgstr "Sjekk om att" -msgid "Are you sure you want to remove the Route Set \"{{routeId}}\"?" -msgstr "Er du sikker på at du vil fjerne omkoplingsgruppa \"{{routeId}}\"?" +msgid "Re-sync" +msgstr "Synkroniser med MOS" -msgid "Routes" -msgstr "Omkoplingar" +msgid "Re-Sync" +msgstr "Synkroniser" -msgid "There are no routes set up yet" -msgstr "Det er ikkje satt opp omkoplingar enno" +msgid "Re-sync Rundown" +msgstr "Synkroniser køyreplanen med ENPS på nytt" -msgid "Original Layer" -msgstr "Opprinneleg lag" +msgid "Re-sync rundown data with {{nrcsName}}" +msgstr "Ikkje synkronisert med MOS/{{nrcsName}}" -msgid "None" -msgstr "Ingen" +msgid "Re-Sync rundown?" +msgstr "Synkroniser køyreplanen med ENPS på ny?" -msgid "New Layer" -msgstr "Nytt lag" +msgid "Re-Syncing Rundown" +msgstr "" -msgid "Route Type" +msgid "Re-Syncing Rundown Playlist" msgstr "" -msgid "Source Layer not found" -msgstr "Kjeldelag ikkje funnen" +msgid "Read marker position" +msgstr "" -msgid "There are no exclusivity groups set up." -msgstr "Ingen eksklusivitetsgrupper er satt opp." +msgid "Reads the ingest (NRCS) data, and pipes it through the blueprints" +msgstr "" -msgid "Exclusivity Group ID" -msgstr "Eksklusivitetgruppe-id" +msgid "Ready" +msgstr "Klar" -msgid "Exclusivity Group Name" -msgstr "Eksklusivitetgruppenamn" +msgid "Reconnect now" +msgstr "" -msgid "Display name of the Exclusivity Group" -msgstr "Eksklusivitetsgruppa sitt namn som visast i oversikten" +msgid "Reconnecting to the {{platformName}}" +msgstr "" -msgid "Active" -msgstr "Aktiv" +msgid "Refreshing debug states" +msgstr "" -msgid "Not Active" -msgstr "Inaktiv" +msgid "Register ID" +msgstr "" -msgid "Not defined" -msgstr "Ikkje definert" +msgid "Rehearsal" +msgstr "Testmodus" -msgid "There are no Route Sets set up." -msgstr "Det er ikkje satt opp omkoplingar enno." +msgid "Rehearsal mode is already active" +msgstr "" -msgid "Route Set ID" -msgstr "Omkoplingsgruppe-id" +msgid "Rehearsal mode is not allowed" +msgstr "" -msgid "Is this Route Set currently active" -msgstr "Er denne omkoplingsgruppa aktiv no" +msgid "Rehearsal State" +msgstr "" -msgid "Default State" -msgstr "Standardtilstand" +msgid "Reload {{nrcsName}} Data" +msgstr "Last inn {{nrcsName}}-data på nytt" -msgid "The default state of this Route Set" -msgstr "Standardtilstand for denne omkoplingsgruppa" +msgid "Reload Baseline" +msgstr "Last inn baseline om att" -msgid "Route Set Name" -msgstr "Omkoplingsgruppa sitt namn" +msgid "Reload NRCS Data" +msgstr "Last inn MOS-data på nytt" -msgid "Display name of the Route Set" -msgstr "Omkoplingsgruppa sitt namn som visast i oversikten" +msgid "Reload statuses" +msgstr "Last inn status om att" -msgid "If set, only one Route Set will be active per exclusivity group" +msgid "Reloading Rundown Playlist Data" msgstr "" -"Berre ei omkoplingsgruppe vere aktiv per eksklusivitetsgruppe når dette er " -"kryssa av for" -msgid "Behavior" -msgstr "Oppførsel" +msgid "Rem. Dur" +msgstr "" -msgid "The way this Route Set should behave towards the user" -msgstr "Måten denne omkoplingsgruppa skal oppføre seg overfor brukaren" +msgid "Remote" +msgstr "" -msgid "Route Sets" -msgstr "Omkoplingsgrupper" +msgid "Remote Source" +msgstr "RM" -msgid "Add a playout device to the studio in order to configure the route sets" +msgid "Remote Speak" msgstr "" -"For å kunne redigere omkoplingsgrupper, må du leggje til ein playout-eining " -"til studio" -msgid "" -"Controls for exposed Route Sets will be displayed to the producer within the " -"Rundown View in the Switchboard." +msgid "Remove" +msgstr "Fjern" + +msgid "Remove all Show Style Variants?" msgstr "" -"Kontroller for eksponerte omkoplingsgrupper vil verte synt for producer i " -"køyreplansvisninga i omkoplingspanelet." -msgid "Exclusivity Groups" -msgstr "Ekslusivitetgrupper" +msgid "Remove all trimming" +msgstr "Nullstill inn- og utpunkt" -msgid "Remove this Package Container?" -msgstr "Fjern denne pakkecontaineren?" +msgid "Remove in-trimming" +msgstr "Nullstill innpunkt" -msgid "" -"Are you sure you want to remove the Package Container \"{{containerId}}\"?" -msgstr "Er du sikker på at du vil fjerne pakkecontaineren \"{{containerId}}\"?" +msgid "Remove indexes" +msgstr "Fjern indexer" -msgid "There are no Package Containers set up." -msgstr "Det er ikkje satt opp pakkekontainere enno." +msgid "Remove old data" +msgstr "Fjern gamle data" -msgid "Package Container ID" -msgstr "Pakkekontainer-id" +msgid "Remove old data from database" +msgstr "Fjern gamle data frå databasen" -msgid "Display name/label of the Package Container" -msgstr "Vis namn/merkelapp for pakkekontaineren" +msgid "Remove out-trimming" +msgstr "Nullstill utpunkt" -msgid "Playout devices which uses this package container" -msgstr "Playout-einingar som nyttar denne pakkekontaineren" +msgid "Remove rundown" +msgstr "Fjern køyreplan" -msgid "Select playout devices" -msgstr "Vel playout-eining" +msgid "Remove Snapshot" +msgstr "" -msgid "Select which playout devices are using this package container" +msgid "Remove this AB PLayers from this Route Set?" msgstr "" -"Vel kva for nokre playout-einingar som skal nytte denne pakkekontaineren" -msgid "Accessors" -msgstr "Aksessorer" +msgid "Remove this device?" +msgstr "Fjern denne eininga?" + +msgid "Remove this Device?" +msgstr "Fjern denne eininga?" + +msgid "Remove this Exclusivity Group?" +msgstr "Fjern frå denne eksklusivitetgruppa?" + +msgid "Remove this item?" +msgstr "Fjern dette elementet?" + +msgid "Remove this mapping?" +msgstr "Fjern denne mappinga?" msgid "Remove this Package Container Accessor?" msgstr "Fjern denne pakkekontainer-aksessoren?" -msgid "" -"Are you sure you want to remove the Package Container Accessor " -"\"{{accessorId}}\"?" -msgstr "" -"Er du sikker på at du vil fjerne pakkekontainer-aksessoren " -"\"{{accessorId}}\"?" - -msgid "There are no Accessors set up." -msgstr "Ingen aksessorer er satt opp." - -msgid "Accessor ID" -msgstr "Aksessor-id" +msgid "Remove this Package Container?" +msgstr "Fjern denne pakkecontaineren?" -msgid "Display name of the Package Container" -msgstr "Pakkekontaineren sitt namn som visast i oversikten" +msgid "Remove this Route from this Route Set?" +msgstr "Fjern denne omkoplinga frå denne omkoplingsgruppa?" -msgid "Accessor Type" -msgstr "Aksessortype" +msgid "Remove this Route Set?" +msgstr "Fjern denne omkoplingsgruppa?" -msgid "Folder path" -msgstr "Mappesti" +msgid "Remove this Show Style Variant?" +msgstr "" -msgid "File path to the folder of the local folder" -msgstr "Sti til lokal mappe" +msgid "Removing Bucket" +msgstr "" -msgid "Resource Id" -msgstr "Ressurs-id" +msgid "Removing Bucket AdLib" +msgstr "" -msgid "" -"(Optional) This could be the name of the computer on which the local folder " -"is on" +msgid "Removing Rundown" msgstr "" -"(Valfri) Dette kan vere namnet til datamaskinen som den lokale mappa er på" -msgid "Base URL" -msgstr "Base-url" +msgid "Removing Rundown Playlist" +msgstr "" -msgid "Base url to the resource (example: http://myserver/folder)" -msgstr "Base-url for ressursen (døme: http://minserver/mappe)" +msgid "Rename this AdLib" +msgstr "Gi denne adliben nytt namn" -msgid "Network Id" -msgstr "Nettverk-id" +msgid "Rename this Bucket" +msgstr "Gi bøtta nytt namn" -msgid "" -"(Optional) A name/identifier of the local network where the share is " -"located, leave empty if globally accessible" +msgid "Reording Rundowns in Playlist" msgstr "" -"(Valfri) Eit namn/ein identifikator for det lokale nettverket der den delte " -"mappa er lokalisert, la vere tom om den er globalt tilgjengeleg" -msgid "Folder path to shared folder" -msgstr "Sti til delt mappe" +msgid "Replace" +msgstr "Erstatt" -msgid "UserName" -msgstr "Brukernamn" +msgid "Replace Blueprints?" +msgstr "Erstatte blueprints?" -msgid "Username for athuentication" -msgstr "Brukarnamn for autentisering" +msgid "Replace rows" +msgstr "" -msgid "Password for authentication" -msgstr "Passord for autentisering" +msgid "Require All Additional Source Layers" +msgstr "" -msgid "" -"(Optional) A name/identifier of the local network where the share is located" +msgid "Require Piece on Source Layer" msgstr "" -"(Valfri) Eit namn/ein identifikator for det lokale nettverket der den delte " -"mappa er lokalisert" -msgid "Quantel gateway URL" -msgstr "Quantel Gateway-adresse (url)" +msgid "Reset" +msgstr "" -msgid "URL to the Quantel Gateway" -msgstr "Start Quantel-gateway om att" +msgid "Reset Action" +msgstr "" -msgid "ISA URLs" -msgstr "ISA-adresse (url)" +msgid "Reset All Versions" +msgstr "Nullstill alle versjonar" -msgid "URLs to the ISAs, in order of importance (comma separated)" -msgstr "Adresser (url-er) for ISA-ene (kommaseparert i prioritert rekkefølgje)" +msgid "Reset and Activate \"On Air\"" +msgstr "" -msgid "Zone ID" -msgstr "Sone-id" +msgid "Reset App Credentials" +msgstr "" -msgid "Zone ID (default value: \"default\")" -msgstr "Sone-id (standardverdi: \"default\")" +msgid "Reset Database Version" +msgstr "Nullstill databaseversjon" -msgid "Server ID" -msgstr "Server-id" +msgid "Reset mapping to default values" +msgstr "" -msgid "Server ID. For sources, this should generally be omitted (or set to 0) so clip-searches are zone-wide. If set, clip-searches are limited to that server." +msgid "Reset Package Container to default values" msgstr "" -"Server-id (Må droppast for kjelder, sidan klippsøk skjer i heile sona.)" -msgid "Quantel transformer URL" -msgstr "Quantel Transformer-adresse (url)" +msgid "Reset row to default values" +msgstr "" -msgid "URL to the Quantel HTTP transformer" -msgstr "Adresse til Quantel HTTP transformer" +msgid "Reset Rundown" +msgstr "Tilbakestill køyreplanen" -msgid "Quantel FileFlow URL" -msgstr "Quantel GatewayFileFlow-adresse (url)" +msgid "Reset Sort Order" +msgstr "Tilbakestill rekkefølgje" -msgid "URL to the Quantel FileFlow Manager" -msgstr "Adresse til Quantel FileFlow Manager" +msgid "Reset source layer to default values" +msgstr "" -msgid "Quantel FileFlow Profile name" -msgstr "Quantel FileFlow profilnavn" +msgid "Reset this item?" +msgstr "" -msgid "Profile name to be used by FileFlow when exporting the clips" -msgstr "Profilnamn som blir nytta av FileFlow når klippa vert eksportert" +msgid "Reset this mapping?" +msgstr "" -msgid "Allow Read access" -msgstr "Tillat lesing" +msgid "Reset this Package Container?" +msgstr "" -msgid "Allow Write access" -msgstr "Tillat skriving/lagring" +msgid "Reset to default" +msgstr "" -msgid "Studio Settings" -msgstr "Studioinnstillingar" +msgid "Reset User Credentials" +msgstr "" -msgid "Package Containers to use for previews" -msgstr "Pakkekontainere som skal nyttast til førehandsvisingar" +msgid "Resetting and activating Rundown Playlist" +msgstr "" -msgid "Click to show available Package Containers" -msgstr "Klikk for å vise tilgjengelege pakkekntainere" +msgid "Resetting Playlist to default order" +msgstr "" -msgid "Package Containers to use for thumbnails" -msgstr "Pakkekontainere som skal nyttast til miniatyrbilete" +msgid "Resetting Rundown Playlist" +msgstr "" -msgid "Package Containers" -msgstr "Pakkekontainere" +msgid "Resource Id" +msgstr "Ressurs-id" -msgid "Studio Baseline needs update: " -msgstr "Studio baseline treng oppdatering: " +msgid "Restart" +msgstr "Restart" -msgid "Baseline needs reload, this studio may not work until reloaded" -msgstr "" -"Baseline må lastast om att, dette studioet vil kanskje ikkje fungere før " -"baseline er lasta om att" +msgid "Restart {{device}}" +msgstr "Start {{device}} på ny" -msgid "Reload Baseline" -msgstr "Last inn baseline om att" +msgid "Restart All Jobs" +msgstr "" -msgid "Studio Name" -msgstr "Studionamn" +msgid "Restart CasparCG Server" +msgstr "Start CasparCG på nytt" -msgid "Select Compatible Show Styles" -msgstr "Vel kompatible showstyles" +msgid "Restart Container" +msgstr "" -msgid "Show style not set" -msgstr "Showstyle ikkje satt" +msgid "Restart Device" +msgstr "Start eining på nytt" -msgid "Click to show available Show Styles" -msgstr "Klikk for å vise tilgjengelege showstyles" +msgid "Restart Playout" +msgstr "Start Playout-gateway på ny" -msgid "Frame Rate" -msgstr "Framerate" +msgid "Restart this Device?" +msgstr "Start denne eininga på nytt?" -msgid "Enable \"Play from Anywhere\"" -msgstr "Slå på \"Play from Anywhere\"" +msgid "Restart this system?" +msgstr "Start dette Sofie-systemet om att?" -msgid "Media Preview URL" -msgstr "Førehandsvisningsadresse (url)" +msgid "Restarting Media Workflow" +msgstr "" -msgid "Sofie Host URL" -msgstr "Sofie vertadresse (url)" +msgid "Restarting Sofie Core" +msgstr "" -msgid "Slack Webhook URLs" -msgstr "Slack Webhook-adresser (url)" +msgid "Restore" +msgstr "Tilbakestill" -msgid "Supported Media Formats" -msgstr "Støtta medieformat" +msgid "Restore Deleted Action" +msgstr "" -msgid "Supported Audio Formats" -msgstr "Støtta lydformat" +msgid "Restore from Snapshot File" +msgstr "Tilbakestill frå snapshotfil" -msgid "Force the Multi-gateway-mode" -msgstr "Tving multigateway-modus" +msgid "Restore from Stored Snapshots" +msgstr "Tilbakestill frå lagra snapshots" -msgid "Multi-gateway-mode delay time" -msgstr "Delaytid for multigateway-modus" +msgid "Restore from this Snapshot file?" +msgstr "Tilbakestill frå denne snapshotfila?" -msgid "Preserve contents of playing segment when unsynced" +msgid "Restore Part from NRCS" msgstr "" -#, fuzzy -#| msgid "The rundown can not be reset while it is active" -msgid "Allow Rundowns to be reset while on-air" -msgstr "Ein aktivert køyreplan kan ikkje tilbakestillast" - -msgid "" -"Preserve position of segments when unsynced relative to other segments. " -"Note: this has only been tested for the iNews gateway" +msgid "Restore Segment from NRCS" msgstr "" -msgid "Remove indexes" -msgstr "Fjern indexer" +msgid "Restore Snapshot" +msgstr "" -msgid "This will remove {{indexCount}} old indexes, do you want to continue?" -msgstr "Dette vil fjerne {{indexCount}} gamle indexer. Vil du fortsette?" +msgid "Resync with NRCS" +msgstr "Synkroniser med ENPS" -msgid "{{indexCount}} indexes was removed." -msgstr "{{indexCount}} indexer vart fjerna." +msgid "Retry" +msgstr "Prøv igjen" -msgid "Installation name" -msgstr "Installasjonsnamn" +msgid "Return to list" +msgstr "Gå tilbake til lista" -msgid "This name will be shown in the title bar of the window" -msgstr "Dette namnet vert vist i tittellinja for vindauget" +msgid "Reveal in Shelf" +msgstr "Vis i skuff" -msgid "Logging level" -msgstr "Loggenivå" +msgid "Reverse speed map" +msgstr "" -msgid "This affects how much is logged to the console on the server" -msgstr "Dette påverkar kor mykje som blir logga til serverkonsollen" +msgid "Reverse speed map (left trigger)" +msgstr "" -msgid "System-wide Notification Message" -msgstr "Lokal systemmelding" +msgid "Rewind all Segments" +msgstr "" -msgid "Message" -msgstr "Melding" +msgid "Rewind segments to start" +msgstr "Sett segmenta tilbake til start" -msgid "Enabled" -msgstr "Aktivert" +msgid "Rewind Segments to start" +msgstr "Sett alle segment attende til start" -msgid "Edit Support Panel" -msgstr "Rediger supportpanel" +msgid "Right hand offset" +msgstr "" -msgid "HTML that will be shown in the Support Panel" -msgstr "HTML-kode som vert vist i supportpanelet" +msgid "Role" +msgstr "Rolle" -msgid "Application Performance Monitoring" -msgstr "Overvaking av yting for applikasjonar (AMP)" +msgid "Route Set" +msgstr "" -msgid "APM Enabled" -msgstr "AMP aktivert" +msgid "Route Set ID" +msgstr "Omkoplingsgruppe-id" -msgid "APM Transaction Sample Rate" -msgstr "Prøvefrekvens for AMP-transaksjonar" +msgid "Route Set Name" +msgstr "Omkoplingsgruppa sitt namn" -msgid "" -"How many of the transactions to monitor. Set to -1 to log nothing (max " -"performance), 0.5 to log 50% of the transactions, 1 to log all transactions" -msgstr "" -"Tal på transaksjonar som overvakast. Set verdien til -1 for å ikkje logge " -"noko (maks yting), til 0.5 for å logge halvparten av transaksjonane eller " -"til 1 for å logge alle transaksjonane" +msgid "Route Sets" +msgstr "Omkoplingsgrupper" -msgid "Note: Core needs to be restarted to apply these settings" -msgstr "Merknad: Core må startast om att for å ta i bruk desse innstillingane" +msgid "Route Type" +msgstr "" -msgid "Monitor blocked thread" +msgid "Routed Mappings" msgstr "" -msgid "Enable" -msgstr "Aktivert" +msgid "Routes" +msgstr "Omkoplingar" -msgid "" -"Enables internal monitoring of blocked main thread. Logs when there is an " -"issue, but (unverified) might cause issues in itself." +msgid "Row cannot be reset as it has no default values" msgstr "" -msgid "Cron jobs" -msgstr "Cron-jobbar" +msgid "Run automatic migration procedure" +msgstr "Køyr automatisk migreringsprosedyre" -msgid "Enable CasparCG restart job" -msgstr "Aktiver CasparCG restartjobbar" +msgid "Run Migrations to get set up" +msgstr "Køyr migreringsprosedyrar for å setje opp" -msgid "Cleanup" -msgstr "Opprydding" +msgid "Rundown" +msgstr "Køyreplan" -msgid "Cleanup old database indexes" -msgstr "Rydd opp i gamle databaseindexer" +msgid "" +"Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data " +"from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced " +"or remove the rundown from Sofie. What do you want to do?" +msgstr "" +"Køyreplanen {{rundownName}} i lista {{playlistName}} manglar i data frå " +"{{nrcsName}}. Du kan anten markere den som ikkje synkronisert og behalde " +"den i Sofie, eller du kan fjerne køyreplanen ifrå Sofie. Kva vil du gjere?" -msgid "Cleanup old data" -msgstr "Rydd opp i gamle data" +msgid "Rundown & Shelf" +msgstr "Køyreplan & skuff" -msgid "Disable CasparCG restart job" -msgstr "Deaktiver CasparCG restartjobbar" +msgid "Rundown filter" +msgstr "" -msgid "Enable automatic storage of Rundown Playlist snapshots periodically" +msgid "Rundown for piece \"{{pieceLabel}}\" could not be found." +msgstr "Kan ikkje finne øyreplan for \"{{pieceLabel}}\"." + +msgid "Rundown Global Piece Prepare Time" msgstr "" -msgid "Filter: If set, only store snapshots for certain rundowns" +msgid "Rundown Header Layout" +msgstr "Layout for køyreplanen sin topptekst" + +msgid "Rundown Header Layouts" msgstr "" -msgid "" -"(Comma separated list. Empty - will store snapshots of all Rundown Playlists)" +msgid "Rundown is already doing a HOLD!" msgstr "" -msgid "Error when checking for cleaning up" +msgid "Rundown must be active!" msgstr "" -msgid "Remove old data from database" -msgstr "Fjern gamle data frå databasen" +msgid "Rundown must be playing or have a next!" +msgstr "" -msgid "" -"There are {{count}} documents that can be removed, do you want to continue?" -msgstr "Det er {{count}} dokument som kan fjernast. Vil du fortsette?" +msgid "Rundown must be playing!" +msgstr "" -msgid "Documents to be removed:" -msgstr "Dokument som vert fjerna:" +msgid "Rundown Name" +msgstr "" -msgid "Retry" -msgstr "Prøv igjen" +msgid "Rundown not found" +msgstr "Køyreplan ikkje funnen" -msgid "Remove old data" -msgstr "Fjern gamle data" +msgid "" +"Rundown Playlist is active, please deactivate before preparing it for " +"broadcast" +msgstr "" -msgid "The old data was removed." -msgstr "Gamle data vart fjerna." +msgid "Rundown Playlist is active, please deactivate it before regenerating it." +msgstr "" -msgid "Last {{layerName}}" -msgstr "Siste {{layerName}}" +msgid "Rundown Playlist names to store" +msgstr "" -msgid "Clear {{layerName}}" -msgstr "Tøm {{layerName}}" +msgid "Rundown Playlist not found!" +msgstr "" -msgid "Search..." -msgstr "Søk..." +msgid "Rundown View Layouts" +msgstr "" msgid "" -"Are you sure you want to deactivate this Rundown\n" -"(This will clear the outputs)" +"RundownPlaylist is active but not in rehearsal, please deactivate it or set " +"in in rehearsal to be able to reset it." msgstr "" -"Er du sikker på at du vil deaktivere denne køyreplanen?\n" -"(Dette vil nullstille alle utgangar.)" - -msgid "Successfully stored snapshot" -msgstr "Tilbakestilling frå snapshot var vellukka" - -msgid "End Words" -msgstr "Stikkord" -msgid "Global AdLib" -msgstr "Globale adliber" +msgid "Rundowns" +msgstr "Køyreplanar" -msgid "AdLib does not provide any options" -msgstr "Adlib har ingen val" +msgid "Save" +msgstr "" -msgid "Execute" -msgstr "Utfør" +msgid "Save Changes" +msgstr "Lagre endringer" msgid "Save to Bucket" msgstr "Lagre til bøtte" -msgid "Reveal in Shelf" -msgstr "Vis i skuff" - -msgid "Edit in Nora" -msgstr "Rediger i Nora" - -msgid "Current Part" -msgstr "Noverande del" - -msgid "Next Part" -msgstr "Neste del" - -msgid "Part Count Down" -msgstr "Nedteljing for del" +msgid "Saving AdLib to Bucket" +msgstr "" -msgid "Part Count Up" -msgstr "Opptelling for del" +msgid "Saving Evaluation" +msgstr "" -msgid "Until end of rundown" -msgstr "Til slutten av køyreplanen" +msgid "Scale" +msgstr "Skala" -msgid "New Bucket" -msgstr "Ny bøtte" +msgid "Script is empty" +msgstr "Manuset er tomt" -msgid "Are you sure you want to delete this AdLib?" -msgstr "Er du sikker på at du vil slette denne adliben?" +msgid "Script Source Layers" +msgstr "" -msgid "Are you sure you want to delete this Bucket?" -msgstr "Er du sikker på at du vil slette denne bøtta?" +msgid "Search..." +msgstr "Søk..." -msgid "Are you sure you want to empty (remove all adlibs inside) this Bucket?" -msgstr "Er du sikker på at du vil tømme denne bøtta (fjerner alle adliber)?" +msgid "Seg. Budg." +msgstr "" -msgid "Current Segment" -msgstr "Noverande tittel" +msgid "segment" +msgstr "" -msgid "Next Segment" -msgstr "Neste tittel" +msgid "Segment" +msgstr "Tittel" msgid "Segment Count Down" msgstr "Nedteljing for tittel" @@ -2890,1296 +3368,1254 @@ msgstr "Nedteljing for tittel" msgid "Segment Count Up" msgstr "Oppteljing for tittel" -msgid "Start this AdLib" -msgstr "Start denne adliben" - -msgid "Queue this AdLib" -msgstr "Cue denne adliben" - -msgid "Inspect this AdLib" -msgstr "Inspiser denne adliben" - -msgid "Rename this AdLib" -msgstr "Gi denne adliben nytt namn" - -msgid "Delete this AdLib" -msgstr "Slett denne adliben" - -msgid "Empty this Bucket" -msgstr "Tøm denne bøtta" - -msgid "Rename this Bucket" -msgstr "Gi bøtta nytt namn" - -msgid "Delete this Bucket" -msgstr "Slett denne bøtta" - -msgid "Create new Bucket" -msgstr "Opprett ny bøtte" - -msgid "AdLib" -msgstr "Adlib" +msgid "Segment countdown requires source layer" +msgstr "Nedteljing for tittel krev kjeldelag" -msgid "Shortcuts" -msgstr "Hurtigtastar" +msgid "Segment no longer exists in {{nrcs}}" +msgstr "Segmentet eksisterer ikkje lenger i {{nrcs}}" -msgid "Show Style Variant" -msgstr "Showstylevariant" +msgid "Segment was hidden in {{nrcs}}" +msgstr "Tittelen eksisterer ikkje lenger i {{nrcs}}" -msgid "Local Time" -msgstr "Lokal tid" +msgid "Segments: {{delta}}" +msgstr "Segment: {{delta}}" -msgid "System" -msgstr "System" +msgid "" +"Select a presenter layout. Leave as default to use the first available " +"layout." +msgstr "" -msgid "Media" -msgstr "Media" +msgid "Select Action" +msgstr "Vel handling" -msgid "Packages" -msgstr "Pakker" +msgid "Select Compatible Show Styles" +msgstr "Vel kompatible showstyles" -msgid "Messages" -msgstr "Meldingar" +msgid "Select image" +msgstr "" -msgid "User Log" -msgstr "Brukarlogg" +msgid "" +"Select one or more control modes. Leave all unchecked for default (mouse + " +"keyboard)." +msgstr "" -msgid "Evaluations" -msgstr "Evalueringar" +msgid "" +"Select source layers to display. Leave all unchecked to show all " +"camera-related layers." +msgstr "" -msgid "Timestamp" -msgstr "Tidsstempel" +msgid "Select visible Output Groups" +msgstr "Vel synleg gruppe for utgang" -msgid "User Name" -msgstr "Brukernamn" +msgid "Select visible Source Layers" +msgstr "Vel synlege kjeldelag" -msgid "Answers" -msgstr "Svar" +msgid "Select which playout devices are using this package container" +msgstr "Vel kva for nokre playout-einingar som skal nytte denne pakkekontaineren" -msgid "Message Queue" -msgstr "Kø for meldingar" +msgid "Send message" +msgstr "" -msgid "Queued Messages" -msgstr "Meldingar i kø" +msgid "Send message and Deactivate Rundown" +msgstr "" msgid "Sent Messages" msgstr "Sendte meldingar" -msgid "File Copy" -msgstr "Kopier fil" - -msgid "File Delete" -msgstr "Slett fil" - -msgid "Check file size" -msgstr "Sjekk filstorleik" - -msgid "Scan File" -msgstr "Scan fil" - -msgid "Generate Thumbnail" -msgstr "Generer miniatyrbilete" - -msgid "Generate Preview" -msgstr "Generer førehandsvisning" - -msgid "Unknown action: {{action}}" -msgstr "Ukjent handling" +msgid "Server" +msgstr "" -msgid "Done" -msgstr "Utført" +msgid "Server {{id}}" +msgstr "" -msgid "Failed" -msgstr "Mislukka" +msgid "Server ID" +msgstr "Server-id" -msgid "Working, Media Available" -msgstr "Arbeider, media er tilgjengeleg" +msgid "" +"Server ID. For sources, this should generally be omitted (or set to 0) so " +"clip-searches are zone-wide. If set, clip-searches are limited to that " +"server." +msgstr "Server-id (Må droppast for kjelder, sidan klippsøk skjer i heile sona.)" -msgid "Working" -msgstr "Arbeider" +msgid "Set" +msgstr "" -msgid "Pending" -msgstr "Venter" +msgid "Set as QuickLoop End" +msgstr "" -msgid "Blocked" -msgstr "Blokkert" +msgid "Set as QuickLoop Start" +msgstr "" -msgid "Canceled" -msgstr "Avbrote" +msgid "Set In & Out points" +msgstr "" -msgid "Idle" -msgstr "Inaktiv" +msgid "Set part as Next" +msgstr "" -msgid "Skipped" -msgstr "Hoppa over" +msgid "Set segment as Next" +msgstr "Set tittel som neste: Startar på neste Take" -msgid "Step progress: {{progress}}" -msgstr "Framdrift: {{progress}}" +msgid "Setting as QuickLoop End" +msgstr "" -msgid "Processing" -msgstr "Prosesserer" +msgid "Setting as QuickLoop Start" +msgstr "" -msgid "Unknown: {{status}}" -msgstr "Ukjend: {{status}}" +msgid "Setting Next" +msgstr "" -msgid "Collapse" -msgstr "Minimer" +msgid "Setting Next Segment" +msgstr "" -msgid "Details" -msgstr "Detaljar" +msgid "Settings" +msgstr "Innstillingar" -msgid "Abort" -msgstr "Avbryt" +msgid "Shelf" +msgstr "Skuff" -msgid "Prioritize" -msgstr "Prioriter" +msgid "Shelf Layout" +msgstr "Layouter for skuffen" -msgid "Media Transfer Status" -msgstr "Status for medieoverføringar" +msgid "Shelf layout uploaded successfully." +msgstr "Opplasting av layout for skuff var vellukka." -msgid "Abort All" -msgstr "Avbryt alle" +msgid "Shelf Layouts" +msgstr "" -msgid "Restart All" -msgstr "Start alle på ny" +msgid "Shortcuts" +msgstr "Hurtigtastar" -msgid "Unknown Package \"{{packageId}}\"" -msgstr "Ukjend pakke \"{{packageId}}\"" +msgid "Show \"Remove snapshots\"-buttons" +msgstr "Vis \"Fjern snapshots\"-knappar" -msgid "Package Status" -msgstr "Pakkestatus" +msgid "Show All" +msgstr "Vis Alle" -msgid "Package container status" -msgstr "Status for pakkekontainer" +msgid "Show Breaks as Segments" +msgstr "Vis pauser som titlar" -msgid "Id" -msgstr "Id" +msgid "Show config changes" +msgstr "" -msgid "Work status" -msgstr "Jobbstatus" +msgid "Show End" +msgstr "Sendeslutt" -msgid "Restart All jobs" -msgstr "Start alle jobbar om att" +msgid "Show entire On Air Segment" +msgstr "Vis heile tittelen som er OnAir" -msgid "Created" -msgstr "Oppretta" +msgid "Show Hotkeys" +msgstr "Vis hurtigtastar" -msgid "Ready" -msgstr "Klar" +msgid "Show Inspector" +msgstr "" -msgid "The progress of steps required for playout" -msgstr "Framdrift for steg som er naudsynte for avspeling" +msgid "Show issue" +msgstr "Vis problem" -msgid "The progress of all steps" -msgstr "Framdrift for alle steg" +msgid "Show next break timing" +msgstr "Vis tid for neste pause" -msgid "This step is required for playout" -msgstr "Dette steget er naudsynt for avspeling" +msgid "Show panel as a timeline" +msgstr "Vis panel som ei tidslinje" -msgid "Work description" -msgstr "Jobbskildring" +msgid "Show part title" +msgstr "Vis delen sin tittel" -msgid "Work status reason" -msgstr "Årsak for jobbstatus" +msgid "Show Piece Icon Color" +msgstr "" -msgid "Technical reason: {{reason}}" -msgstr "Teknisk årsak: {{reason}}" +msgid "Show Rundown Name" +msgstr "Vis køyreplannamn" -msgid "Previous work status reasons" -msgstr "Tidlegare årsakar for jobbsatus" +msgid "Show segment name" +msgstr "Vis tittelen sitt namn" -msgid "Priority" -msgstr "Prioritet" +msgid "Show Style" +msgstr "Showstyle" -msgid "Not Connected" -msgstr "Ikkje tilkopla" +msgid "Show Style Base Name" +msgstr "Showstylenamn" -msgid "Do you want to restart CasparCG Server?" -msgstr "Er du sikker på at du vil starte CasparCG om att?" +msgid "Show style not set" +msgstr "Showstyle ikkje satt" -msgid "Restart Quantel Gateway" -msgstr "Start Quantel-gateway om att" +msgid "Show Style Variant" +msgstr "Showstylevariant" -msgid "Do you want to restart Quantel Gateway?" -msgstr "Er du sikker på at du vil starte Quantel-gateway om att?" +msgid "Show Style Variants" +msgstr "" -msgid "Quantel Gateway restarting..." -msgstr "Quantel-gateway startar om att..." +msgid "Show Styles" +msgstr "Showstyle" -msgid "Failed to restart Quantel Gateway: {{errorMessage}}" -msgstr "Kunne ikkje starta Quantel-gateway om att: {{errorMessage}}" +msgid "Show thumbnails next to list items" +msgstr "Vis miniatyrbilete ved sida av listeelement" -msgid "Format HyperDeck disks" -msgstr "Formater Hyperdeck-diskar" +msgid "ShowStyleBase not found!" +msgstr "" -msgid "" -"Do you want to format the HyperDeck disks? This is a destructive action and " -"cannot be undone." -msgstr "Ynskjer du å formatere Hyperdeck-diskar? Dette kan ikkje gjerast om." +msgid "Shuttle Keyboard (Contour ShuttleXpress / X-keys)" +msgstr "" -msgid "Formatting HyperDeck disks on device \"{{deviceName}}\"..." -msgstr "Formaterer Hyperdeck-diskar på eining \"{{deviceName}}\"..." +msgid "Shuttle WebHID (Contour ShuttleXpress via browser)" +msgstr "" -msgid "" -"Failed to format HyperDecks on device: \"{{deviceName}}\": {{errorMessage}}" +msgid "Skip Fix Up Step" msgstr "" -"Kunne ikkje formatere Hyperdecks på eining: \"{{deviceName}}\": " -"{{errorMessage}}" -msgid "Last seen" -msgstr "Sist sett" +msgid "Slack Webhook URLs" +msgstr "Slack Webhook-adresser (url)" -msgid "Connect some devices to the playout gateway" -msgstr "Kople til ein eller fleire einingar til playout-gatewayen" +msgid "Smooth scrolling" +msgstr "" -msgid "Format disks" -msgstr "Formater diskar" +msgid "Snapshot remove failed: {{errorMessage}}" +msgstr "" -msgid "Are you sure you want to delete this device: \"{{deviceId}}\"?" -msgstr "Er du sikker på at du vil fjerne eininga \"{{deviceId}}\"?" +msgid "Snapshot restore failed: {{errorMessage}}" +msgstr "Tilbakestilling frå snapshot feila: {{errorMessage}}" -msgid "Sofie Automation Server Core: {{name}}" -msgstr "Sofie:" +msgid "Snapshot restored!" +msgstr "" -msgid "Restart this system?" -msgstr "Start dette Sofie-systemet om att?" +msgid "Sofie" +msgstr "" -msgid "" -"Are you sure you want to restart this Sofie Automation Server Core: {{name}}?" -msgstr "Er du sikker på at du vil starte Sofie Core: {{name}} om att?" +msgid "Sofie Automation" +msgstr "Sofie" -msgid "Could not generate restart token!" -msgstr "Kunne ikkje generere Restart Token!" +msgid "Sofie Automation Server" +msgstr "" -msgid "Could not generate restart core: {{err}}" -msgstr "Kunne ikkje generere Restart Core: {{err}}" +msgid "Sofie Automation Server Core" +msgstr "" msgid "Sofie Automation Server Core will restart in {{time}}s..." msgstr "Sofie Core starter om att om {{time}}s..." -msgid "Execution times" -msgstr "Køyretider" - -msgid "User ID" -msgstr "Brukar-id" - -msgid "Client IP" -msgstr "Klient-ip" - -msgid "Method" -msgstr "Metode" - -msgid "Parameters" -msgstr "Parametrar" +msgid "Sofie Automation Server Core: {{name}}" +msgstr "Sofie:" -msgid "Time from platform user event to Action received by Core" +msgid "Sofie logo to be displayed in the header. Requires a page refresh." msgstr "" -msgid "GUI" -msgstr "Brukergrensesnitt" - -msgid "Core + Worker processing time" +msgid "some invalid reason" msgstr "" -msgid "Core" +msgid "some message" msgstr "" -msgid "Worker" +msgid "some reason" msgstr "" -msgid "Gateway" +msgid "something changed" msgstr "" -msgid "TSR" +msgid "" +"Something went wrong when creating the snapshot. Please contact the system " +"administrator if the problem persists." msgstr "" -msgid "User Activity Log" -msgstr "Aktivitetslogg" +msgid "Something went wrong, and it affected the output" +msgstr "Noko gjekk gale, og det virka inn på sendinga" -msgid "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s" -msgstr "om {{days}} dagar, {{hours}} h {{minutes}} min {{seconds}} s" +msgid "Something went wrong, but it didn't affect the output" +msgstr "Noko gjekk gale, men det virka ikkje inn på sendinga" -msgid "in {{hours}} h {{minutes}} min {{seconds}} s" -msgstr "om {{hours}} t {{minutes}} min {{seconds}} s" +msgid "" +"Something went wrong, please contact the system administrator if the " +"problem persists." +msgstr "Noko gikk gale, kontakt systemadministrator om problemet held fram." -msgid "in {{minutes}} min {{seconds}} s" -msgstr "om {{minutes}} min {{seconds}} s" +msgid "Source Abbreviation" +msgstr "Kjeldeforkorting" -msgid "in {{seconds}} s" -msgstr "om {{seconds}} s" +msgid "Source Layer" +msgstr "Kjeldelag" -msgid "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago" -msgstr "for {{days}} dagar, {{hours}} t {{minutes}} min {{seconds}} s sidan" +msgid "Source layer cannot be reset as it has no default values" +msgstr "" -msgid "{{hours}} h {{minutes}} min {{seconds}} s ago" -msgstr "for {{hours}} t {{minutes}} min {{seconds}} s sidan" +msgid "Source Layer Type" +msgstr "Kjeldelagstypar" -msgid "{{minutes}} min {{seconds}} s ago" -msgstr "for {{minutes}} min {{seconds}} s sidan" +msgid "Source Layer Types" +msgstr "Kjeldelagstypar" -msgid "{{seconds}} s ago" -msgstr "for {{seconds}} s sidan" +msgid "Source Layers" +msgstr "Kjeldelag" -msgid "Next scheduled show" -msgstr "Neste planlagde sending" +msgid "Source layers containing script" +msgstr "" -msgid "Help & Support" -msgstr "Hjelp og brukarstøtte" +msgid "Source Name" +msgstr "Kjeldenamn" -msgid "Disable hints by adding this to the URL:" -msgstr "Deaktiver hint ved å legge dette til på url-en:" +msgid "Source Type" +msgstr "Kjeldetype" -msgid "Enable hints by adding this to the URL:" -msgstr "Aktiver hint ved å legge dette til på url-en:" +msgid "Source/Output Layers" +msgstr "" -msgid "More documentation available at:" -msgstr "Meir dokumentasjon er tilgjengeleg på:" +msgid "Sources" +msgstr "Kjelder" -msgid "Timeline" -msgstr "Tidslinje" +msgid "Space separated list of style class names to use when displaying the action" +msgstr "" -msgid "Mappings" -msgstr "Lagmapping" +msgid "Specify additional layers where at least one layer must have an active piece" +msgstr "" + +msgid "Speed control" +msgstr "" -msgid "User Log Player" -msgstr "Brukarloggspelar" +msgid "Speed map" +msgstr "" -msgid "Routed Mappings" +msgid "Speed map (forward, right trigger)" +msgstr "" + +msgid "Split Screen" +msgstr "Splitt" + +msgid "Splits" msgstr "" -msgid "Play from here" -msgstr "Spel av frå her" +msgid "Standalone Shelf" +msgstr "Frittståande skuff" -msgid "Exectute Single" -msgstr "Utfør einsleg handling" +msgid "Start Here!" +msgstr "Start her!" -msgid "Next Action" -msgstr "Neste handling" +msgid "Start In" +msgstr "" -msgid "Run in" -msgstr "Køyr i" +msgid "Start this AdLib" +msgstr "Start denne adliben" -msgid "Stop" -msgstr "Stopp" +msgid "Start time is close" +msgstr "Oppgitt sendestart er kvart augeblink" msgid "" -"Clip \"{{fileName}}\" can't be played because it doesn't exist on the " -"playout system" -msgstr "" -"Klippet \"{{fileName}}\" kan ikkje spelast av fordi det ikkje finnast på " -"utspelingssystemet" +"Start with giving this browser configuration permissions by adding this to " +"the URL: " +msgstr "Først må du gå i konfigurasjonsmodus ved å leggje dette til i url-en: " -msgid "{{sourceLayer}} is not yet ready on the playout system" +msgid "Started" +msgstr "Starta" + +msgid "Starting AdLib" msgstr "" -"{{sourceLayer}} er enno ikkje klar til å spelast ut fra avviklingsserver" -msgid "{{sourceLayer}} is transferring to the playout system" -msgstr "{{sourceLayer}} overførast til avviklingsserver" +msgid "Starting Bucket AdLib" +msgstr "" -msgid "" -"{{sourceLayer}} is transferring to the playout system and cannot be " -"played yet" +msgid "Starting Global AdLib" msgstr "" -"{{sourceLayer}} overførast til avviklingsserver og kan ikkje spelast av enno" -msgid "{{sourceLayer}} doesn't have both audio & video" -msgstr "{{sourceLayer}} har ikkje lyd og/eller bilete" +msgid "Starting Sticky Piece" +msgstr "" -msgid "{{sourceLayer}} has the wrong format: {{format}}" -msgstr "{{sourceLayer}}-formatet er ikkje støtta: {{format}}" +msgid "State" +msgstr "Tilstand" -msgid "{{sourceLayer}} has {{audioStreams}} audio streams" -msgstr "{{sourceLayer}} har {{audioStreams}} lydstraumar" +msgid "State \"{{state}}\"" +msgstr "" -msgid "Clip starts with {{frames}} {{type}} frames" -msgstr "Klippet startar med {{frames}} {{type}} ruter" +msgid "Statistics" +msgstr "Statistikk" -msgid "This clip ends with {{type}} frames after {{count}} seconds" -msgstr "Klippet sluttar med {{type}} ruter etter {{count}} sekund" +msgid "Status" +msgstr "Status" -msgid "{{frames}} {{type}} frames detected within the clip" -msgstr "{{frames}} {{type}} rute oppdaga inne i klippet" +msgid "Status Messages:" +msgstr "Statusmeldingar:" -msgid "{{frames}} {{type}} frames detected in the clip" -msgstr "{{frames}} {{type}} rute oppdaga inne i klippet" +msgid "Sticky Piece" +msgstr "Element er sticky" -msgid "black" -msgstr "svart(e)" +msgid "Store Snapshot" +msgstr "Lagre snapshot" -msgid "freeze" -msgstr "fryst(e)" +msgid "Studio" +msgstr "Studio" -msgid "{{sourceLayer}} is missing a file path" -msgstr "{{sourceLayer}} kan ikkje spelast av fordi filnamnet manglar" +msgid "Studio Baseline needs update: " +msgstr "Studio baseline treng oppdatering: " -msgid "Clip doesn't have audio & video" -msgstr "Klippet har ikkje lyd og/eller bilete" +msgid "Studio Labels" +msgstr "" -msgid "Clip starts with {{frames}} {{type}} frame" -msgstr "Klippet startar med {{frames}} {{type}} frame" +msgid "Studio Name" +msgstr "Studionamn" -msgid "This clip ends with {{type}} frames after {{count}} second" -msgstr "Klippet sluttar med {{frames}} {{type}} frame" +msgid "Studio not found!" +msgstr "" -msgid "{{frames}} {{type}} frame detected within the clip" -msgstr "{{frames}} {{type}} frame oppdaga inne i klippet" +msgid "Studio Screen Graphics" +msgstr "" -msgid "{{frames}} {{type}} frame detected in clip" -msgstr "{{frames}} {{type}} frame oppdaga i klippet" +msgid "Studio Settings" +msgstr "Studioinnstillingar" -msgid "{{sourceLayer}} is being ingested" -msgstr "{{sourceLayer}} vert prosessert" +msgid "Studio Snapshot" +msgstr "Studiosnapshot" -msgid "Source is missing" -msgstr "Kjelde manglar" +msgid "Studios" +msgstr "Studio" -msgid "Segment no longer exists in {{nrcs}}" -msgstr "Segmentet eksisterer ikkje lenger i {{nrcs}}" +msgid "Style class names" +msgstr "" -msgid "Segment was hidden in {{nrcs}}" -msgstr "Tittelen eksisterer ikkje lenger i {{nrcs}}" +msgid "Subtract" +msgstr "" -msgid "The following parts no longer exist in {{nrcs}}: {{partNames}}" -msgstr "Dei følgande delane eksisterer ikkje lenger i {{nrcs}}: {{partNames}}" +msgid "Successfully restored snapshot" +msgstr "Tilbakestilling frå snapshot var vellukka" -msgid "Toggle Shelf" -msgstr "Skuff" +msgid "Successfully stored snapshot" +msgstr "Tilbakestilling frå snapshot var vellukka" -msgid "Undo Hold" -msgstr "Angre hold" +msgid "Support Panel" +msgstr "" -msgid "Disable the next element" -msgstr "Skip neste super" +msgid "Supported Audio Formats" +msgstr "Støtta lydformat" -msgid "Undo Disable the next element" -msgstr "Unskip neste super" +msgid "Supported Media Formats" +msgstr "Støtta medieformat" -msgid "Move Next forwards" -msgstr "Skip neste" +msgid "Switch Route Set" +msgstr "" -msgid "Move Next to the following segment" -msgstr "Skip til neste segment" +msgid "Switch Segment View Mode" +msgstr "" -msgid "Move Next backwards" -msgstr "Unskip neste" +msgid "Switch to List View" +msgstr "" -msgid "Move Next to the previous segment" -msgstr "Unskip neste segment" +msgid "Switch to Storyboard View" +msgstr "" -msgid "Rewind segments to start" -msgstr "Sett segmenta tilbake til start" +msgid "Switch to Timeline View" +msgstr "" -msgctxt "°°°°°°plural" -msgid "{{count}} rows°°°°°°" -msgstr "{{count}} rader°°°°°°" +msgid "Switchboard" +msgstr "Omkoplingssentral" -msgctxt "°°°°°°plural" -msgid "This layer is now rerouted by an active Route Set: {{routeSets}}°°°°°°" +msgid "Switchboard Panel" msgstr "" -"Dette laget vert omkopla av fleire aktive omkoplingsgrupper: {{routeSets}}°°°" -"°°°" -msgctxt "°°°°°°plural" -msgid "" -"There are {{count}} documents that can be removed, do you want to continue?°°" -"°°°°" +msgid "Switching operating mode to {{mode}}" +msgstr "Endrer styringsmodus til {{mode}}" + +msgid "Switching routing" msgstr "" -"Det er {{count}} dokument i {{collections}} som kan fjernast. Vil du " -"fortsette?°°°°°°" -#, fuzzy -#| msgctxt "°°°°°°plural" -#| msgid "Clip starts with {{frames}} {{type}} frame°°°°°°" -msgctxt "°°°°°°plural" -msgid "Clip starts with {{frames}} {{type}} frames°°°°°°" -msgstr "Klipp startar med {{frames}} {{type}} frame°°°°°°" +msgid "System" +msgstr "System" -#, fuzzy -#| msgctxt "°°°°°°plural" -#| msgid "This clip ends with {{type}} frames after {{count}} second°°°°°°" -msgctxt "°°°°°°plural" -msgid "This clip ends with {{type}} frames after {{count}} seconds°°°°°°" -msgstr "Klipp sluttar {{frames}} {{type}} frame°°°°°°" +msgid "System has issues which need to be resolved" +msgstr "Systemet har problemer som må løysast" -#, fuzzy -#| msgctxt "°°°°°°plural" -#| msgid "{{frames}} {{type}} frame detected within the clip°°°°°°" -msgctxt "°°°°°°plural" -msgid "{{frames}} {{type}} frames detected within the clip°°°°°°" -msgstr "{{frames}} {{type}} rute oppdaga inne i klippet°°°°°°" +msgid "System must have exactly one studio" +msgstr "" -#, fuzzy -#| msgctxt "°°°°°°plural" -#| msgid "{{frames}} {{type}} frame detected within the clip°°°°°°" -msgctxt "°°°°°°plural" -msgid "{{frames}} {{type}} frames detected in the clip°°°°°°" -msgstr "{{frames}} {{type}} rute oppdaga inne i klippet°°°°°°" +msgid "System Status" +msgstr "Systemstatus" -msgctxt "°°°°°°plural" -msgid "Clip starts with {{frames}} {{type}} frame°°°°°°" -msgstr "Klipp startar med {{frames}} {{type}} frame°°°°°°" +msgid "System-wide" +msgstr "Systemvid" -msgctxt "°°°°°°plural" -msgid "This clip ends with {{type}} frames after {{count}} second°°°°°°" -msgstr "Klipp sluttar {{frames}} {{type}} frame°°°°°°" +msgid "System-wide Notification Message" +msgstr "Lokal systemmelding" -msgctxt "°°°°°°plural" -msgid "{{frames}} {{type}} frame detected within the clip°°°°°°" -msgstr "{{frames}} {{type}} frame oppdaga inne i klippet°°°°°°" +msgid "Table is not allowed to have `properties` defined" +msgstr "" -msgctxt "°°°°°°plural" -msgid "{{frames}} {{type}} frame detected in clip°°°°°°" -msgstr "{{frames}} {{type}} frame oppdaga i klippet°°°°°°" +msgid "Table is only allowed the wildcard `patternProperties`" +msgstr "" -#~ msgid "Welcome to the Sofie Automation system" -#~ msgstr "Velkomen til Sofie" +msgid "Tables are not supported here" +msgstr "" -#~ msgid "Sofie Automation version" -#~ msgstr "Sofie-versjon" +msgid "Tag" +msgstr "Tag" -#~ msgid "Sofie status" -#~ msgstr "Sofie-status" +msgid "Tags must contain" +msgstr "Tagger må innehalde" -#~ msgid "Using local Sofie order" -#~ msgstr "Rekkefølgje satt i Sofie" +msgid "Take" +msgstr "Take" -#~ msgid "Change order in playlist to override" -#~ msgstr "Gjør ei endring for å overstyre" +msgid "Take a Full System Snapshot" +msgstr "Lagre eit fullt systemsnapshot" -#~ msgid "Register Shortcuts for this Panel" -#~ msgstr "Legg til snarvegar for dette panelet" +msgid "Take a Snapshot" +msgstr "Lagre eit snapshot" -#~ msgid "Cannot play this AdLib becasue it is marked as Invalid" -#~ msgstr "Kan ikkje spele av adlib fordi den er markert som ugyldig" +msgid "Take a Snapshot for studio \"{{studioName}}\" only" +msgstr "Lagre eit studiosnapshot utelukkande for \"{{studioName}}\"" -#~ msgid "Cannot play this AdLib becasue it is marked as Floated" -#~ msgstr "Kan ikkje spele av adlib fordi det er markert som på vent (float)" +msgid "Take and Download Memory Heap Snapshot" +msgstr "" -#~ msgid "" -#~ "The Rundown was attempted to be moved out of the Playlist when it was on " -#~ "Air. Move it back and try again later." -#~ msgstr "" -#~ "Køyreplanen vart forsøkt flytta ut av spillelista mens den var OnAir. " -#~ "Flytt den attende og prøv igjen seinare." +msgid "Take System Snapshot" +msgstr "" -#~ msgid "Next Break" -#~ msgstr "Neste pause" +msgid "Take System Snapshot failed: {{errorMessage}}" +msgstr "" -#~ msgid "Using {{nrcsName}} order" -#~ msgstr "Nytter rekkefølgje frå {{nrcsName}}" +msgid "Taking Piece" +msgstr "" -#~ msgid "Log Error" -#~ msgstr "Loggfør feil" +msgid "Technical reason: {{reason}}" +msgstr "Teknisk årsak: {{reason}}" -#~ msgid "Shortcut List" -#~ msgstr "Liste over hurtigtastar" +msgid "test" +msgstr "" -#~ msgid "Clear Shortcut" -#~ msgstr "Fjern hurtigtast" +msgid "Test ERROR message" +msgstr "" -#~ msgid "Assign Hotkeys to Global AdLibs" -#~ msgstr "Tildel hurtigtastar til globale adliber" +msgid "Test Info message" +msgstr "" -#~ msgid "Activate Sticky Piece Shortcut" -#~ msgstr "Aktiver hurtigtast for sticky element" +msgid "test partInstance" +msgstr "" -#~ msgid "Escape from filter search" -#~ msgstr "Gå ut av filtersøk" +msgid "test pieceInstance" +msgstr "" -#~ msgid "Info" -#~ msgstr "Info" +msgid "test playlist" +msgstr "" -#~ msgid "File name" -#~ msgstr "Filnamn" +msgid "test rundown" +msgstr "" -#~ msgid "Last Updated" -#~ msgstr "Sist oppdatert" +msgid "Test test" +msgstr "Test test" -#~ msgid "Invalid" -#~ msgstr "Ugyldig" +msgid "Test Tools" +msgstr "Testverktøy" -#~ msgid "New Tab" -#~ msgstr "Ny fane" +msgid "test2" +msgstr "" -#~ msgid "New Panel" -#~ msgstr "Nytt panel" +msgid "Text" +msgstr "Tekst" -#~ msgid "Expose as a layout for the shelf" -#~ msgstr "Eksponer som ein layout for skuffen" +msgid "Text to show above countdown to end of show" +msgstr "Tekst som blir vist over nedteljing til venta slutt" -#~ msgid "Tabs" -#~ msgstr "Faner" +msgid "Text to show above countdown to next break" +msgstr "Tekst som blir vist over nedteljing til neste pause" -#~ msgid "Panels" -#~ msgstr "Paneler" +msgid "Text to show above show end time" +msgstr "Tekst som blir vist over klokkeslett for sendeslutt" -#~ msgid "Add tab" -#~ msgstr "Legg til fane" +msgid "Text to show above show start time" +msgstr "Tekst som blir vist over klokkeslett for sendestart" -#~ msgid "Add panel" -#~ msgstr "Legg til panel" +msgid "" +"The config UI is now driven by manifests fed by the device. This device " +"needs updating to provide the configManifest to be configurable" +msgstr "" +"Brukergrensesnitt for konfigurasjon drivast no av manifest mata frå " +"einingane. Denne einga må oppdaterast for å gjere configManifest " +"konfigurerbart" -#~ msgid "Shelf Layouts" -#~ msgstr "Layouter for skuffen" +msgid "The following parts no longer exist in {{nrcs}}: {{partNames}}" +msgstr "Dei følgande delane eksisterer ikkje lenger i {{nrcs}}: {{partNames}}" -#~ msgid "Show style" -#~ msgstr "Showstyle" +msgid "The migration can be completed automatically." +msgstr "Migreringa kan gjerast ferdig automatisk." -#~ msgid "Clip can't be played because the filename is missing" -#~ msgstr "Klippet kan ikkje spelast av fordi filnamnet mangler" +msgid "" +"The migration consists of several phases, you will get more options after " +"you've this migration" +msgstr "" +"Migreringa har fleire fasar, du vil få fleire val etter at du har køyrd " +"denne migreringa" -#~ msgid "Clip format ({{format}}) is not in one of the accepted formats" -#~ msgstr "Klippformatet ({{format}}) er ikkje støtta" +msgid "The migration was completed successfully!" +msgstr "Migreringa var vellukka!" -#~ msgid "Activate" -#~ msgstr "Aktiver" +msgid "The old data was removed." +msgstr "Gamle data vart fjerna." -#, fuzzy -#~| msgid "Mark the rundown as unsynced" -#~ msgid "Leave it in Sofie (mark the rundown as unsynced)" -#~ msgstr "Marker køyreplan som ikkje-synkronisert" +msgid "" +"The planned end time has passed, are you sure you want to activate this " +"Rundown?" +msgstr "" +"Det planlagte sluttidspunktet er passert, er du sikker på at du vil " +"aktivere denne køyreplanen?" -#, fuzzy -#~| msgid "Remove just the rundown" -#~ msgid "Remove just the rundown from Sofie" -#~ msgstr "Fjern berre køyreplan" +msgid "The progress of all steps" +msgstr "Framdrift for alle steg" -#~ msgid "CasparCG Channel" -#~ msgstr "CasparCG-kanal" +msgid "The progress of steps required for playout" +msgstr "Framdrift for steg som er naudsynte for avspeling" -#~ msgid "The CasparCG channel to use (1 is the first)" -#~ msgstr "CasparCG-kanalen som skal nyttast (1 er den første)" +msgid "" +"The rundown \"{{rundownName}}\" is not published or activated in " +"{{nrcsName}}! No data updates will currently come through." +msgstr "" +"Køyreplanen \"{{rundownName}}\" er ikkje synkronisert med MOS/{{nrcsName}}! " +"Kontroller at den er satt til MOS Active i ENPS." -#~ msgid "CasparCG Layer" -#~ msgstr "CasparCG-lag" +msgid "The rundown can not be reset while it is active" +msgstr "Ein aktivert køyreplan kan ikkje tilbakestillast" -#~ msgid "The layer in a channel to use" -#~ msgstr "Kanallaget som skal nyttast" +msgid "" +"The Rundown was attempted to be moved out of the Playlist when it was on " +"Air. Move it back and try again later." +msgstr "" -#~ msgid "Preview when not on air" -#~ msgstr "Vis preview når ikkje OnAir" +msgid "" +"The rundown you are trying to execute a take on is inactive, would you like " +"to activate this rundown?" +msgstr "" +"Du prøve å gjere ein Take i ein inaktiv køyreplan. Vil du aktivere denne " +"køyreplanen?" -#~ msgid "Mapping type" -#~ msgstr "Mappingtype" +msgid "" +"The rundown: \"{{rundownName}}\" will need to be deactivated in order to " +"activate this one.\n" +"\n" +"Are you sure you want to activate this one anyway?" +msgstr "" -#~ msgid "Index" -#~ msgstr "Index" +msgid "" +"The segment duration in the segment header always displays the planned " +"duration instead of acting as a counter" +msgstr "" +"Tittelen si lengde i tittelheaderen vil alltid vise den planlagde lengda i " +"staden for å telje ned" -#~ msgid "Identifier" -#~ msgstr "Identifikator" +msgid "The selected part cannot be played" +msgstr "" -#~ msgid "Sisyfos Channel" -#~ msgstr "Sisyfos-kanal" +msgid "The selected part does not exist" +msgstr "" -#~ msgid "Quantel Port ID" -#~ msgstr "Quantel port-id" +msgid "" +"The system configuration has been changed since importing this rundown. It " +"might not run correctly" +msgstr "" +"Systemoppsettet har verte endra etter at denne køyreplanen vart importert. " +"Køyreplanen kan verte spelt av med feil" -#~ msgid "The name you'd like the port to have" -#~ msgstr "Namnet du vil gje porten" +msgid "The type of device to use for the output" +msgstr "Einingtype som skal nyttast for utgangen" -#~ msgid "Quantel Channel ID" -#~ msgstr "Quantel kanal-id" +msgid "The type of mapping to use" +msgstr "" -#~ msgid "The channel to use for output (0 is the first one)" -#~ msgstr "Kanalen som skal nyttast som utgang (0 er den første)" +msgid "The way this Route Set should behave towards the user" +msgstr "Måten denne omkoplingsgruppa skal oppføre seg overfor brukaren" -#~ msgid "Mode" -#~ msgstr "Modus" +msgid "Then, run the migrations script:" +msgstr "Køyr deretter migreringsprosedyra:" -#~ msgid "Preset" -#~ msgstr "Preset" +msgid "There are no AB Playout devices set up yet" +msgstr "" -#~ msgid "Preset Transition Speed" -#~ msgstr "Preset transition-hastigheit" +msgid "There are no Accessors set up." +msgstr "Ingen aksessorer er satt opp." -#~ msgid "Zoom Speed" -#~ msgstr "Zoom-hastigheit" +msgid "There are no active rundowns." +msgstr "Fann ingen aktive køyreplanar." -#~ msgid "Unknown Mapping" -#~ msgstr "Ukjend lag" +msgid "There are no exclusivity groups set up." +msgstr "Ingen eksklusivitetsgrupper er satt opp." -#~ msgid "Channel: {{channel}}" -#~ msgstr "Kanal: {{channel}}" +msgid "There are no filters set up yet" +msgstr "Det er ikkje satt opp noko filter enno" -#~ msgid "Port: {{port}}, Channel: {{channel}}" -#~ msgstr "Port: {{port}}, Kanal: {{channel}}" +msgid "" +"There are no Playout Gateways connected and attached to this studio. Please " +"contact the system administrator to start the Playout Gateway." +msgstr "" +"Dette studioet har ingen tilkopla playout-gatewayar. Kontakt " +"systemadministrator for å starte den." -#~ msgid "Unknown device type: {{device}}" -#~ msgstr "Ukjend einingtype: {{device}}" +msgid "There are no Route Sets set up." +msgstr "Det er ikkje satt opp omkoplingar enno." -#~ msgid "Delete this RundownPlaylist?" -#~ msgstr "Slett denne køyreplanlista?" +msgid "There are no routes set up yet" +msgstr "Det er ikkje satt opp omkoplingar enno" -#~ msgid "Are you sure you want to delete the \"{{name}}\" RundownPlaylist?" -#~ msgstr "Er du sikker på at du vil slette køyreplanlista \"{{name}}\"?" +msgid "There are no rundowns ingested into Sofie." +msgstr "Det er ikkje send køyreplanar til Sofie." -#~ msgid "Re-Sync this rundownPlaylist?" -#~ msgstr "Synkroniser køyreplanlista på nytt?" +msgid "There are no sub-devices for this gateway" +msgstr "" -#~ msgid "" -#~ "Are you sure you want to re-sync all rundowns in playlist \"{{name}}\"?" -#~ msgstr "" -#~ "Er du sikker på at du vil synkronisere alle køyreplanane i køyreplanlista " -#~ "\"{{name}}\" om att?" +msgid "There is an unknown problem with the part." +msgstr "Det er eit ukjend problem med denne delen." -#~ msgid "Timeline views" -#~ msgstr "Tidslinjevisningar" +msgid "There is an unspecified problem with the source." +msgstr "Det er eit ikkje-spesifisert problem med kjelden." -#~ msgid "Multiple ({{count}})" -#~ msgstr "Fleire ({{count}})" +msgid "There is no rundown active in this studio." +msgstr "Fann ingen aktive køyreplanar for dette studioet." -#~ msgid "Re-sync all rundowns in playlist" -#~ msgstr "Synkroniser alle køyreplanane i lista på nytt" +msgid "" +"There was an error when troubleshooting the device: \"{{deviceName}}\": " +"{{errorMessage}}" +msgstr "" +"Det hende ein feil under feilsøking av eininga \"{{deviceName}}\": " +"{{errorMessage}}" -#~ msgid "" -#~ "Do you really want to remove the rundownPlaylist \"{{rundownName}}\"? " -#~ "This cannot be undone!" -#~ msgstr "" -#~ "Er du sikker på at du vil slette køyreplanen {{rundownName}} i lista " -#~ "{{playlistName}} frå Sofie? Denne handlinga kan du ikkje angre!" +msgid "There was an error: {{error}}" +msgstr "Det skjedde ein feil: {{error}}" -#~ msgid "Cue as Next" -#~ msgstr "Sett som neste" +msgid "This action has an invalid combination of filters" +msgstr "Denne handlinga har ein ugydlig kombinasjon av filtre" -#~ msgid "Edit" -#~ msgstr "Endre" +msgid "This affects how much is logged to the console on the server" +msgstr "Dette påverkar kor mykje som blir logga til serverkonsollen" -#~ msgctxt "°°°°°°plural" -#~ msgid "Multiple ({{count}})°°°°°°" -#~ msgstr "Fleire ({{count}})°°°°°°" +msgid "This blueprint has not provided a valid config schema" +msgstr "" -#~ msgid "Add rundowns by connecting a gateway." -#~ msgstr "Legg til køyreplanar ved å kople til ein gateway." +msgid "This Blueprint is not being used by any Show Style" +msgstr "Dette blueprintet er ikkje i bruk av nokon showstyles" -#~ msgid "Check system status messages." -#~ msgstr "Kontroller systemstatusmeldingar." +msgid "This Blueprint is not compatible with any Studio" +msgstr "Dette blueprintet er ikkje kompatibel med noko studio" -#~ msgid "Air Status" -#~ msgstr "Status for sendinga" +msgid "This clip ends with black frames after {{seconds}} seconds" +msgstr "" -#~ msgid "Sofie Automation Server" -#~ msgstr "Sofie" +msgid "This clip ends with freeze frames after {{seconds}} seconds" +msgstr "" -#~ msgid "Connecting to the {{platformName}}" -#~ msgstr "Koplar til {{platformName}}" +msgid "This could leave the configuration in a broken state" +msgstr "" -#~ msgid "Cannot connect to the {{platformName}}: {{reason}}" -#~ msgstr "Kan ikkje kople til Sofie-serveren: {{reason}}" +msgid "This enables or disables buckets in the UI - enabled is the default behavior" +msgstr "" -#~ msgid "Reconnecting to the {{platformName}}" -#~ msgstr "Koplar til {{platformName}} på nytt" +msgid "" +"This enables or disables the evaluationform in the UI - enabled is the " +"default behavior" +msgstr "" -#~ msgid "Your machine is offline and cannot connect to the {{platformName}}." -#~ msgstr "Maskina di er fråkopla og kan ikkje kople til {{platformName}}." +msgid "This feature enables the use of the Properties Panel and the Edit Mode" +msgstr "" -#~ msgid "Connected to the {{platformName}}." -#~ msgstr "Kopla til {{platfromName}}." +msgid "This has only been tested for the iNews gateway" +msgstr "" -#~ msgid "Reconnect now" -#~ msgstr "Kople til på nytt" +msgid "This is not in it's normal setting" +msgstr "Denne innstillinga er vorte endra frå standardverdien" -#, fuzzy -#~ msgid "Switching route" -#~ msgstr "Omkoplinger" +msgid "" +"This migration consists of {{stepCount}} steps ({{ignoredStepCount}} steps " +"are ignored)." +msgstr "" -#, fuzzy -#~ msgid "Are you sure you want to enable this route: \"{{routeName}}\"?" -#~ msgstr "" -#~ "Er du sikker på at du vil aktivere denne omkoplinga: \"{{routeName}}\"?" +msgid "This must be assigned to a device to be able to edit the settings" +msgstr "" -#, fuzzy -#~ msgid "Are you sure you want to disable this route: \"{{routeName}}\"?" -#~ msgstr "" -#~ "Er du sikker på at du vil deaktivere denne omkoplinga: \"{{routeName}}\"?" +msgid "This name will be shown in the title bar of the window" +msgstr "Dette namnet vert vist i tittellinja for vindauget" -#~ msgid "Source Layer ID" -#~ msgstr "Opprinneleg lag-id" +msgid "This playlist is empty" +msgstr "Denne spelelista er tom" -#~ msgid "Test Tools – Recordings" -#~ msgstr "Testverktøy - Opptak" +msgid "" +"This requires the blueprints to implement the " +"`generateAdlibTestingIngestRundown` method" +msgstr "" -#~ msgid "Path Prefix" -#~ msgstr "Stiprefiks" +msgid "This rundown has been unpublished from Sofie." +msgstr "Denne køyreplanen er ikkje lenger tilgjengeleg i Sofie." -#~ msgid "URL Prefix" -#~ msgstr "Adresseprefix (url)" +msgid "This rundown is currently active" +msgstr "Denne køyreplanen er allereie aktiv" -#~ msgid "Decklink Input Index" -#~ msgstr "Decklink-inngang" +msgid "This rundown is now active. Are you sure you want to exit this screen?" +msgstr "Denne køyreplanen er aktiv. Er du sikker på at du vil avslutte?" -#~ msgid "Decklink Input Format" -#~ msgstr "Format for Decklink-inngang" +msgid "This rundown will loop indefinitely" +msgstr "Denne køyreplanen vil gå i ein uendeleg loop" -#~ msgid "Recordings" -#~ msgstr "Opptak" +msgid "This Show Style is not compatible with any Studio" +msgstr "Denne showstylen er ikkje kompatibelt med noko studio" -#~ msgid "A required setting is not configured" -#~ msgstr "Ei obligatorisk innstilling er ikkje konfigurert" +msgid "This step is required for playout" +msgstr "Dette steget er naudsynt for avspeling" -#~ msgid "Are you sure you want to delete recording \"{{name}}\"?" -#~ msgstr "Er du sikker på at du vil slette opptaket \"{{name}}\"?" +msgid "This studio doesn't exist." +msgstr "Dette studioet eksisterer ikkje." -#~ msgid "Stopped" -#~ msgstr "Stoppa" +msgid "This will remove {{indexCount}} old indexes, do you want to continue?" +msgstr "Dette vil fjerne {{indexCount}} gamle indexer. Vil du fortsette?" -#~ msgid "Your browser does not support video playback" -#~ msgstr "Nettlesaren støtter ikkje videoavspeling" +msgid "Time from platform user event to Action received by Core" +msgstr "" -#~ msgid "Recording still in progress" -#~ msgstr "Opptak held framleis på" +msgid "Time since planned end" +msgstr "Tid sidan planlagt slutt" -#~ msgid "MOS Connection" -#~ msgstr "MOS-tilkopling" +msgid "Time since rehearsal end" +msgstr "" -#~ msgid "Infinite" -#~ msgstr "Manuell" +msgid "Time to planned end" +msgstr "Tid til planlagt slutt" -#~ msgid "File Name" -#~ msgstr "Filnamn" +msgid "Time to planned start" +msgstr "" -#~ msgid "System Messages" -#~ msgstr "Systemmeldingar" +msgid "Time to rehearsal end" +msgstr "" -#~ msgid "" -#~ "Are you sure you want to delete this runtime argument \"{{property}}: " -#~ "{{value}}\"?" -#~ msgstr "" -#~ "Er du sikker på at du vil slette runtimeargumentet \"{{property}}: " -#~ "{{value}}\"?" +msgid "Timeline" +msgstr "Tidslinje" -#~ msgid "Property" -#~ msgstr "Eigenskap" +msgid "Timeline Datastore" +msgstr "" -#~ msgid "Runtime Arguments for Blueprints" -#~ msgstr "Runtimeargument for blueprints" +msgid "Times" +msgstr "Tider" -#~ msgid "Source is not set" -#~ msgstr "Kjelde er ikkje satt" +msgid "Timestamp" +msgstr "Tidsstempel" -#~ msgid "{{displayName}}: {{messages}}" -#~ msgstr "{{displayName}}: {{messages}}" +msgid "To inspect the memory heap snapshot, use Chrome DevTools" +msgstr "" -#~ msgid "Params" -#~ msgstr "Parametrar" +msgid "Today" +msgstr "I dag" -#~ msgid "Make ready commands" -#~ msgstr "Klargjeringskommandoar" +msgid "Toggle" +msgstr "Veklse" -#~ msgid "Remove this command?" -#~ msgstr "Fjern denne kommandoen?" +msgid "Toggle AdLibs on single mouse click" +msgstr "Veksle mellom adliber med enkelt museklikk" -#~ msgid "Are you sure you want to remove this command?" -#~ msgstr "Er du sikker på at du vil fjerne denne kommandoen?" +msgid "Toggle Shelf" +msgstr "Skuff" -#~ msgid "Queue" -#~ msgstr "Kø" +msgid "Toggle Support Panel" +msgstr "" -#~ msgid "Hosts" -#~ msgstr "Vertar" +msgid "Toggled Label" +msgstr "" -#~ msgid "iNews Queues" -#~ msgstr "iNews-køar" +msgid "Tomorrow" +msgstr "I morgon" -#~ msgid "User" -#~ msgstr "Brukar" +msgid "Tools" +msgstr "Verktøy" -#~ msgid "Debug logging" -#~ msgstr "Logging for feilsøking" +msgid "Top" +msgstr "" -#~ msgid "Storage ID" -#~ msgstr "Lager-id" +msgid "Transition" +msgstr "Effekt" -#~ msgid "Base Path" -#~ msgstr "Rotsti" +msgid "Treat as Main content" +msgstr "" -#~ msgid "Media Path" -#~ msgstr "Mediesti" +msgid "Trigger dead zone" +msgstr "" -#~ msgid "Mapped Networked Drive" -#~ msgstr "Stasjonsbokstav" +msgid "Trigger Mode" +msgstr "" -#~ msgid "Username" -#~ msgstr "Brukarnamn" +msgid "Triggered Actions failed to upload: {{errorMessage}}" +msgstr "Opplasting av handlingsutløysarar feila: {{errorMessage}}" -#~ msgid "Don't Scan Entire Storage" -#~ msgstr "Ikkje scan heile lagringsområdet" +msgid "Triggered Actions uploaded successfully." +msgstr "Opplasting av handlingsutløysarar var vellukka." -#~ msgid "Base path is a network share" -#~ msgstr "Rotsti er ein nettverksressurs" +msgid "Trim \"{{name}}\"" +msgstr "Trim \"{{name}}\"" -#~ msgid "Media Flow ID" -#~ msgstr "Medieflyt-id" +msgid "Trimmed successfully." +msgstr "" -#~ msgid "Media Flow Type" -#~ msgstr "Medieflyttype" +msgid "Trimming this clip has failed due to an error: {{error}}." +msgstr "Endring av inn-/utpunkt for dette klippet feila: {{error}}." -#~ msgid "Source Storage" -#~ msgstr "Kjelden sitt lagringsområde" +msgid "" +"Trimming this clip has timed out. It's possible that the story is currently " +"locked for writing in {{nrcsName}} and will eventually be updated. Make " +"sure that the story is not being edited by other users." +msgstr "" +"Endring av inn-/utpunkt for dette klippet tek lang tid. Det er mogleg " +"manuset i er låst i {{nrcsName}} og at inn-/utpunkt endrast om litt. " +"Forsikre deg om at manuset ikkje vert redigert av andre brukarar." -#~ msgid "Target Storage" -#~ msgstr "Målet sitt lagringsområde" +msgid "" +"Trimming this clip is taking longer than expected. It's possible that the " +"story is locked for writing in {{nrcsName}}." +msgstr "" +"Endring av inn-/utpunkt for dette klippet tek meir tid enn forventa. Det er " +"mogleg manuset er låst for redigering i {{nrcsName}}." -#~ msgid "Delete this Monitor?" -#~ msgstr "Slett denne monitoren?" +msgid "Troubleshoot" +msgstr "Feilsøk" -#~ msgid "Are you sure you want to delete the monitor \"{{monitorId}}\"?" -#~ msgstr "Er du sikker på at du vil slette monitoren \"{{monitorId}}\"?" +msgid "TSR" +msgstr "" -#~ msgid "ID already exists" -#~ msgstr "Denne id-en eksisterar allereie" +msgid "Type" +msgstr "Type" -#~ msgid "The ID {{monitorId}} already exists!" -#~ msgstr "Monitor-id-en {{monitorId}} eksisterar allereie!" +msgid "Unable to check the system configuration for changes" +msgstr "Kan ikkje kontrollere endringar i systemoppsettet" -#~ msgid "Monitor ID" -#~ msgstr "Monitor-id" +msgid "Unable to upgrade" +msgstr "" -#~ msgid "Monitor Type" -#~ msgstr "Monitortype" +msgid "Unassign" +msgstr "Fjern tilordning" -#~ msgid "Media Scanner Host" -#~ msgstr "Vert for mediascanner" +msgid "Unconfigured" +msgstr "" -#~ msgid "Media Scanner Port" -#~ msgstr "Port for mediascanner" +msgid "Under" +msgstr "" -#~ msgid "Quantel ISA URL" -#~ msgstr "Quantel ISA-adresse (url)" +msgid "Undo" +msgstr "Angre" -#~ msgid "Quantel Server ID" -#~ msgstr "Quantel server-id" +msgid "Undo Disable the next element" +msgstr "Unskip neste super" -#~ msgid "No. of Available Workers" -#~ msgstr "Tal på tilgjengelege Workers" +msgid "Undo Hold" +msgstr "Angre hold" -#~ msgid "File Linger Time" -#~ msgstr "Maks ventetid for fil" +msgid "Unknown" +msgstr "Ukjend" -#~ msgid "Workflow Linger Time" -#~ msgstr "Maks ventetid for arbeidsflyt" +msgid "Unknown action" +msgstr "" -#~ msgid "Cron-Job Interval Time" -#~ msgstr "Tidsintervall for Cron-Jobs" +msgid "Unknown error" +msgstr "Ukjend feil" -#~ msgid "Activate Debug Logging" -#~ msgstr "Aktiver logging for feilsøking" +msgid "Unknown Layer" +msgstr "Ukjend lag" -#~ msgid "Remove this storage?" -#~ msgstr "Fjern dette lagringsområdet?" +msgid "Unknown Package \"{{packageId}}\"" +msgstr "Ukjend pakke \"{{packageId}}\"" -#~ msgid "Remove this flow?" -#~ msgstr "Fjern denne flyten?" +msgid "Unnamed blueprint" +msgstr "Blueprint utan namn" -#~ msgid "Are you sure you want to remove flow \"{{flowId}}\"?" -#~ msgstr "Er du sikker på at du vil fjerne flyten \"{{flowId}}\"?" +msgid "Unnamed Show Style" +msgstr "Showstyle utan namn" -#~ msgid "Attached Storages" -#~ msgstr "Tilkopla lagringsområder" +msgid "Unnamed Studio" +msgstr "Studio utan namn" -#~ msgid "Media Flows" -#~ msgstr "Medieflytar" +msgid "Unnamed variant" +msgstr "Variant utan namn" -#~ msgid "Monitors" -#~ msgstr "Monitorar" +msgid "Unsupported array type \"{{ type }}\"" +msgstr "" -#~ msgid "Primary ID (Newsroom System MOS ID)" -#~ msgstr "Primær id (News Room System MOS ID)" +msgid "Unsupported field type \"{{ type }}\"" +msgstr "" -#~ msgid "Primary Host (IP or Hostname)" -#~ msgstr "Primær vert (ip-adresse eller vertsnamn)" +msgid "Unsyncing Rundown" +msgstr "" -#~ msgid "Primary: dont use MOS query-port" -#~ msgstr "Primær: Ikkje bruk MOS Query-port" +msgid "Until" +msgstr "" -#~ msgid "Secondary ID (Newsroom System MOS ID)" -#~ msgstr "Sekundær id (News Room System MOS ID)" +msgid "Until end of rundown" +msgstr "Til slutten av køyreplanen" -#~ msgid "Secondary Host (IP Address or Hostname)" -#~ msgstr "Sekundær vert (ip-adresse eller vertsnamn)" +msgid "Until End of Rundown" +msgstr "" -#~ msgid "Secondary: dont use MOS query-port" -#~ msgstr "Sekundær: Ikkje bruk MOS Query-port" +msgid "Until end of segment" +msgstr "Til slutten av segment" -#~ msgid "MOS ID of Gateway (Sofie MOS ID)" -#~ msgstr "MOS-id for gateway (Sofie MOS ID)" +msgid "Until End of Segment" +msgstr "" -#~ msgid "Device id" -#~ msgstr "Einings-id" +msgid "Until end of showstyle" +msgstr "Til slutten av showstyle" -#~ msgid "Thread Usage" -#~ msgstr "Trådar i bruk" +msgid "Until End of Showstyle" +msgstr "" -#~ msgid "Port" -#~ msgstr "Port" +msgid "Until next rundown" +msgstr "Til neste køyreplan" -#~ msgid "CasparCG Launcher Host" -#~ msgstr "Vert for CasparCG Launcher" +msgid "Until Next Rundown" +msgstr "" -#~ msgid "CasparCG Launcher Port" -#~ msgstr "Port for CasparCG Launcher" +msgid "Until next segment" +msgstr "Til neste segment" -#~ msgid "Minimum recording time" -#~ msgstr "Minimum lengde for opptak" +msgid "Until Next Segment" +msgstr "" -#~ msgid "Enable SSL" -#~ msgstr "Aktiver SSL" +msgid "Until next take" +msgstr "Til neste Take" -#~ msgid "HTTPMethod" -#~ msgstr "HTTPMethod" +msgid "Until Next Take" +msgstr "" -#~ msgid "expectedHttpResponse" -#~ msgstr "expectedHttpResponse" +msgid "Update" +msgstr "Oppdater" -#~ msgid "Keyword" -#~ msgstr "Nøkkelord" +msgid "Update Blueprints?" +msgstr "Oppdater blueprints?" -#~ msgid "Interval" -#~ msgstr "Intervall" +msgid "Updated" +msgstr "Oppdatert" -#~ msgid "(Optional) REST port" -#~ msgstr "(Valfri) REST-port" +msgid "Upgrade config for {{name}}" +msgstr "" -#~ msgid "(Optional) Websocket port" -#~ msgstr "(Valfri) Websocket-port" +msgid "Upgrade Database" +msgstr "Oppgrader databasen" -#~ msgid "Show ID" -#~ msgstr "Show-id" +msgid "Upgrade required" +msgstr "" -#~ msgid "Profile" -#~ msgstr "Profil" +msgid "Upgrade Status" +msgstr "" -#~ msgid "(Optional) Playlist id" -#~ msgstr "(Valfri) Spilleliste-id" +msgid "Upload" +msgstr "Last opp" -#~ msgid "Preload all elements" -#~ msgstr "Last inn alle element på førehand" +msgid "Upload a new blueprint" +msgstr "Last opp eit nytt blueprint" -#~ msgid "Automatically load internal elements when added" -#~ msgstr "Last interne element automatisk når dei vert lagd til" +msgid "Upload a snapshot file" +msgstr "" -#~ msgid "Only preload elements in active Rundown" -#~ msgstr "Berre last inn element i aktiv kjøreplan på førehand" +msgid "" +"Upload a snapshot file (restores additional info not directly related to a " +"Playlist / Rundown, such as Packages, PackageWorkStatuses etc" +msgstr "" -#~ msgid "Activate Multi-Threading" -#~ msgstr "Bruk fleire trådar" +msgid "Upload Blueprints" +msgstr "Last opp blueprints" -#~ msgid "Activate Multi-Threaded Timeline Resolving" -#~ msgstr "Bruk fleire trådar for Timeline Resolving" +msgid "Upload Layout?" +msgstr "Last opp layout?" -#~ msgid "(Restart to apply)" -#~ msgstr "(Start om att for å ta i bruk endringar)" +msgid "Upload Snapshot" +msgstr "Last opp snapshot" -#, fuzzy -#~ msgid "Report command timings on all commangs" -#~ msgstr "Rapporter kommandotider for alle commangs" +msgid "Upload Snapshot (for debugging)" +msgstr "" -#~ msgid "Drive folder name" -#~ msgstr "Mappenamn på Drive" +msgid "Upload stored Action Triggers" +msgstr "Last opp lagra handlingsutløysarar" -#~ msgid "Provide the name of the folder to download rundowns from" -#~ msgstr "Oppgje namnet på mappa køyreplanar skal lastas ned frå" +msgid "URL" +msgstr "Adresse (url)" -#, fuzzy -#~ msgid "" -#~ "Go to the url below and click on the \"Enable the Drive API\" button. " -#~ "Then click on \"Download Client configuration\", save the credentials." -#~ "json file and upload it here." -#~ msgstr "" -#~ "Gå til lenka under og klikk på \"Enable the Drive API\"-knappen. Klikk " -#~ "deretter på \"Download Client configuration\", lagre credentials.json-" -#~ "fila og last den opp her." +msgid "URL to the Quantel FileFlow Manager" +msgstr "Adresse til Quantel FileFlow Manager" -#~ msgid "Row" -#~ msgstr "Rad" +msgid "URL to the Quantel Gateway" +msgstr "Start Quantel-gateway om att" -#~ msgid "Cannot connect to the" -#~ msgstr "Kan ikkje kople til" +msgid "URL to the Quantel HTTP transformer" +msgstr "Adresse til Quantel HTTP transformer" -#~ msgid "Sofie Automation Server:" -#~ msgstr "Sofie:" +msgid "URLs to the ISAs, in order of importance (comma separated)" +msgstr "Adresser (url-er) for ISA-ene (kommaseparert i prioritert rekkefølgje)" -#~ msgid "rows" -#~ msgstr "rader" +msgid "Use {{nrcsName}} order" +msgstr "Nytt rekkefølgje frå {{nrcsName}}" -#~ msgid "Delete this Item?" -#~ msgstr "Slett dette elementet?" +msgid "Use as default" +msgstr "" -#~ msgid "Set Database Version" -#~ msgstr "Sett databaseversjon" +msgid "Use color of primary piece as background of panel" +msgstr "" -#~ msgid "Are you sure you want to set the database version to" -#~ msgstr "Er du sikker på at du vil sette databaseversjon til" +msgid "Use Trigger Mode" +msgstr "Type utløysar" -#~ msgid "Restart this device?" -#~ msgstr "Start denne eininga på nytt?" +msgid "User Activity Log" +msgstr "Aktivitetslogg" -#~ msgid "Is unlimited" -#~ msgstr "Er utan grenser" +msgid "User ID" +msgstr "Brukar-id" -#~ msgid "Is on clean PGM" -#~ msgstr "Ligg på cleanfeed" +msgid "User Log" +msgstr "Brukarlogg" -#~ msgid "Reload Rundown" -#~ msgstr "Last inn køyreplanen på nytt" +msgid "User Name" +msgstr "Brukernamn" -#~ msgid "ID" -#~ msgstr "ID" +msgid "Username for authentication" +msgstr "" -#~ msgid "status" -#~ msgstr "status" +msgid "Validate and Apply Config" +msgstr "" -#~ msgid "Nyman's Playground" -#~ msgstr "Testar" +msgid "Validation failed!" +msgstr "" -#~ msgid "channel" -#~ msgstr "kanal" +msgid "Value" +msgstr "Verdi" -#~ msgid "layer" -#~ msgstr "lag" +msgid "Value between 0 and 1" +msgstr "" -#~ msgctxt "item" -#~ msgid "new_config" -#~ msgstr "new_config" +msgid "Variants" +msgstr "Variantar" -#~ msgid "No studios are compatible with this blueprint" -#~ msgstr "Inga studio er kompatible med denne showstylen" +msgid "version" +msgstr "versjon" -#~ msgid "No studios are compatible with this Show Style" -#~ msgstr "Inga studio er kompatible med denne showstylen" +msgid "Version" +msgstr "Versjon" -#~ msgid "Refresh" -#~ msgstr "Oppdater" +msgid "Version for {{name}}: From {{fromVersion}} to {{toVersion}}" +msgstr "Versjon for {{name}}: Frå {{fromVersion}} til {{toVersion}}" -#~ msgid "Are you sure you want to remove device \"{{devideId}}\"?" -#~ msgstr "Er du sikker på at du vil fjerne eininga \"{{devideId}}\"?" +msgid "View" +msgstr "Visning" -#~ msgid "" -#~ "Only a single Rundown can be active in a studio at the same time. Please " -#~ "deactivate the other Rundown and try again." -#~ msgstr "" -#~ "Bare éin køyreplan kan være aktiv i eit studio. Deaktiver den andre " -#~ "køyreplanen, og prøv på nytt." +msgid "View Layout" +msgstr "Vis layout" -#~ msgid "Are you sure you want to reset this Rundown?" -#~ msgstr "Er du sikker på at du vil tilbakestille denne kjøreplan?" +msgid "Waiting for action: {{actionName}}..." +msgstr "" -#~ msgid "Line Templates" -#~ msgstr "Line Templates" +msgid "Waiting for gateway to generate URL..." +msgstr "Ventar på at gateway genererar URL..." -#~ msgid "Restore Backup" -#~ msgstr "Restore Backup" +msgid "Warning" +msgstr "Åtvaring" -#~ msgid "Blueprint logic ID" -#~ msgstr "Blueprint logic ID" +msgid "Warnings" +msgstr "Åtvaringar" -#~ msgid "Is Helper" -#~ msgstr "Is Helper" +msgid "Warnings During Migration" +msgstr "Åtvaringar under migrering" -#~ msgid "Select this" -#~ msgstr "Select this" +msgid "What type of bank" +msgstr "" -#~ msgid "System version" -#~ msgstr "System version" +msgid "When" +msgstr "" -#~ msgid "Take debug snapshot" -#~ msgstr "Take debug snapshot" +msgid "When disabled, any HOLD operations will be silently ignored" +msgstr "" -#~ msgid "" -#~ "A Debug Snapshot contains info about the system and the active running " -#~ "order(s)" -#~ msgstr "" -#~ "A Debug Snapshot contains info about the system and the active running " -#~ "order(s)" +msgid "" +"When enabled, double clicking on certain pieces in the GUI will play them " +"as adlibs" +msgstr "" -#~ msgid "Baseline logic ID" -#~ msgstr "Baseline logic ID" +msgid "" +"When enabled, this will override the piece content statuses to have no " +"errors or warnings and display a mock preview. This should only be used for " +"development!" +msgstr "" -#~ msgid "Post Process logic ID" -#~ msgstr "Post Process logic ID" +msgid "When set, resources are considered immutable, ie they will not change" +msgstr "" -#~ msgid "External Message logic ID" -#~ msgstr "External Message logic ID" +msgid "Whether to show countdown to next break" +msgstr "Om nedteljing til neste pause skal visast" -#~ msgid "Download Full Backup" -#~ msgstr "Download Full Backup" +msgid "" +"While there are still breaks coming up in the show, hide the Expected End " +"timers" +msgstr "Gøym nedteljing til venta slutt medan det framleis er pauser att i sendinga" -#~ msgid "Download Current State" -#~ msgstr "Download Current State" +msgid "Width" +msgstr "Breidde" -#~ msgid "Blueprints' logic" -#~ msgstr "Blueprints' logic" +msgid "Work description" +msgstr "Jobbskildring" -#~ msgid "Helper" -#~ msgstr "Helper" +msgid "Work status" +msgstr "Jobbstatus" -#~ msgid "Are you sure you want to delete output channel \"{{channelId}}\"?" -#~ msgstr "Are you sure you want to delete output channel \"{{channelId}}\"?" +msgid "Work status reason" +msgstr "Årsak for jobbstatus" -#~ msgid "Snapshot" -#~ msgstr "Snapshot" +msgid "Work-in-progress" +msgstr "Pågåande jobbar" -#~ msgid "Download Debug-Snapshot (all studios)" -#~ msgstr "Download Debug-Snapshot (all studios)" +msgid "Worker" +msgstr "" -#~ msgid "Download System Snapshot for the studio" -#~ msgstr "Download System Snapshot for the studio" +msgid "WorkForce" +msgstr "Arbeidarstyrke" -#~ msgid "Download Debug-Snapshot for the studio" -#~ msgstr "Download Debug-Snapshot for the studio" +msgid "X" +msgstr "X" -#~ msgid "Telemetry" -#~ msgstr "Telemetry" +msgid "Xbox Controller" +msgstr "" -#~ msgid "MOS Device" -#~ msgstr "MOS Device" +msgid "Y" +msgstr "Y" -#~ msgid "Sub-Device" -#~ msgstr "Sub-Device" +msgid "Yes" +msgstr "Ja" -#~ msgid "Kill this device process?" -#~ msgstr "Kill this device process?" +msgid "Yes, Take and Download Memory Heap Snapshot" +msgstr "" -#~ msgid "Kill" -#~ msgstr "Kill" +msgid "Yesterday" +msgstr "I går" -#~ msgid "Are you sure you want to kill the process of this device?" -#~ msgstr "Are you sure you want to kill the process of this device?" +msgid "" +"You are in rehearsal mode, the broadcast starts in less than 1 minute. Do " +"you want to go into On-Air mode?" +msgstr "" -#~| msgid "Restore this backup?" -#~ msgid "Restore from this Backup?" -#~ msgstr "Restore from this Backup?" +msgid "You need to run migrations to set the system up for operation." +msgstr "Du må køyre migrering for å klargjere systemet for bruk." -#~ msgid "Url" -#~ msgstr "Url" +msgid "Your machine is offline and cannot connect to the {{platformName}}." +msgstr "" -#~ msgid "Generic properties" -#~ msgstr "Generic Properties" +msgid "Your name" +msgstr "Namnet ditt" -#~ msgid "ON AIR" -#~ msgstr "ON AIR" +msgid "Zone ID" +msgstr "Sone-id" -#~ msgid "Click Anywhere to go Fullscreen..." -#~ msgstr "Klikk hvor som helst for gå til fullskjerm..." +msgid "Zoom In" +msgstr "Zoom In" -#~ msgid "Please enter your name" -#~ msgstr "Vennligst legg til ditt navn" +msgid "Zoom Out" +msgstr "Zoom ut" -#~ msgid "Template ID" -#~ msgstr "Template ID" +msgctxt "one" +msgid "{{count}} items" +msgstr "" -#~ msgid "Templates" -#~ msgstr "Templates" +msgctxt "one" +msgid "There are {{count}} documents that can be removed, do you want to continue?" +msgstr "" -#, fuzzy -#~| msgid "Devices" -#~ msgid "Mos-devices" -#~ msgstr "Devices" +msgctxt "one" +msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" +msgstr "" -#~ msgid "Connecting to Sofie Automation Server..." -#~ msgstr "Kobler til Sofie-serveren..." +msgctxt "other" +msgid "{{count}} items" +msgstr "" -#~ msgid "Connected to Sofie Automation Server." -#~ msgstr "Koblet til Sofie-serveren." +msgctxt "other" +msgid "There are {{count}} documents that can be removed, do you want to continue?" +msgstr "" -#~ msgid "MOS Devices" -#~ msgstr "MOS Devices" +msgctxt "other" +msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" +msgstr "" diff --git a/meteor/i18n/sv.po b/meteor/i18n/sv.po new file mode 100644 index 00000000000..26a0634fa8e --- /dev/null +++ b/meteor/i18n/sv.po @@ -0,0 +1,4536 @@ +msgid "" +msgstr "" +"Project-Id-Version: i18next-conv\n" +"mime-version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"POT-Creation-Date: 2026-04-14T14:17:21.154Z\n" +"PO-Revision-Date: 2026-04-14T14:17:21.154Z\n" +"Language: sv\n" + +msgid " The index of the Atem media/clip banks" +msgstr "" + +msgid "({{time}} ago)" +msgstr "" + +msgid "({{timecode}})" +msgstr "" + +msgid "" +"(Comma separated list. Empty - will store snapshots of all Rundown " +"Playlists)" +msgstr "" + +msgid "(Default)" +msgstr "" + +msgid "(in: {{time}})" +msgstr "" + +msgid "(Optional) A name/identifier of the local network where the Atem is located" +msgstr "" + +msgid "(Optional) A name/identifier of the local network where the share is located" +msgstr "" + +msgid "" +"(Optional) A name/identifier of the local network where the share is " +"located, leave empty if globally accessible" +msgstr "" + +msgid "(Optional) This could be the name of the compute" +msgstr "" + +msgid "" +"(Optional) This could be the name of the computer on which the local folder " +"is on" +msgstr "" + +msgid "(Unknown playlist)" +msgstr "" + +msgid "(Unknown rundown)" +msgstr "" + +msgid "" +"(what happened and when, what should have happened, what could have " +"triggered the problems, etcetera...)" +msgstr "" + +msgid "{{actionName}} failed! More information can be found in the system log." +msgstr "" + +msgid "{{currentRundownName}} - {{rundownPlaylistName}}" +msgstr "" + +msgid "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)" +msgstr "" + +msgid "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago" +msgstr "" + +msgid "{{frames}} black frames detected in the clip" +msgstr "" + +msgid "{{frames}} black frames detected within the clip" +msgstr "" + +msgid "{{frames}} freeze frames detected in the clip" +msgstr "" + +msgid "{{frames}} freeze frames detected within the clip" +msgstr "" + +msgid "{{hours}} h {{minutes}} min {{seconds}} s ago" +msgstr "" + +msgid "{{indexCount}} indexes was removed." +msgstr "" + +msgid "{{minutes}} min {{seconds}} s ago" +msgstr "" + +msgid "{{nrcsName}} Connection" +msgstr "" + +msgid "{{prevStatements}} and {{finalStatement}}" +msgstr "" + +msgid "{{prevStatements}} or {{finalStatement}}" +msgstr "" + +msgid "" +"{{reason}} {{sourceLayer}} exists, but is not yet ready on the playout " +"system" +msgstr "" + +msgid "{{rundownPlaylistName}} (Looping)" +msgstr "" + +msgid "{{seconds}} s ago" +msgstr "" + +msgid "{{showStyleVariant}} – {{showStyleBase}}" +msgstr "" + +msgid "{{sourceLayer}} can't be found on the playout system" +msgstr "" + +msgid "{{sourceLayer}} doesn't have both audio & video" +msgstr "" + +msgid "{{sourceLayer}} has {{audioStreams}} audio streams" +msgstr "" + +msgid "{{sourceLayer}} has the wrong format: {{format}}" +msgstr "" + +msgid "{{sourceLayer}} has unsupported source: {{containerLabels}}" +msgstr "" + +msgid "{{sourceLayer}} is being ingested" +msgstr "" + +msgid "" +"{{sourceLayer}} is in a placeholder state for an unknown workflow-defined " +"reason" +msgstr "" + +msgid "{{sourceLayer}} is in an unknown state: \"{{status}}\"" +msgstr "" + +msgid "{{sourceLayer}} is missing" +msgstr "" + +msgid "{{sourceLayer}} is missing a file path" +msgstr "" + +msgid "{{sourceLayer}} is not yet ready on the playout system" +msgstr "" + +msgid "{{sourceLayer}} is transferring to the playout system" +msgstr "" + +msgid "" +"{{sourceLayer}} is transferring to the playout system but cannot be played " +"yet" +msgstr "" + +msgid "{{studioName}}: Active Rundown" +msgstr "" + +msgid "{{studioName}}: Presenter screen" +msgstr "" + +msgid "{{studioName}}: Prompter" +msgstr "" + +msgid "14 = 7 lines, 20 = 5 lines" +msgstr "" + +msgid "A device must be assigned to the config to edit the settings" +msgstr "" + +msgid "" +"A Full System Snapshot contains all system settings (studios, showstyles, " +"blueprints, devices, etc.)" +msgstr "" + +msgid "a second" +msgstr "" + +msgid "" +"A snapshot of the current Running Order has been created for " +"troubleshooting." +msgstr "" + +msgid "A Studio Snapshot contains all system settings related to that studio" +msgstr "" + +msgid "AB Channel Display" +msgstr "" + +msgid "AB Playout devices" +msgstr "" + +msgid "AB Resolver Channel Display" +msgstr "" + +msgid "Abort" +msgstr "" + +msgid "Aborting all Media Workflows" +msgstr "" + +msgid "Aborting Media Workflow" +msgstr "" + +msgid "Accessor ID" +msgstr "" + +msgid "Accessor Type" +msgstr "" + +msgid "Accessors" +msgstr "" + +msgid "Action" +msgstr "" + +msgid "Action {{actionName}} done!" +msgstr "" + +msgid "Action {{actionName}} failed: {{error}}" +msgstr "" + +msgid "Action Buttons" +msgstr "" + +msgid "Action Triggers" +msgstr "" + +msgid "Activate \"On Air\"" +msgstr "" + +msgid "Activate \"Rehearsal\"" +msgstr "" + +msgid "Activate (On-Air)" +msgstr "" + +msgid "Activate (Rehearsal)" +msgstr "" + +msgid "Activate On Air" +msgstr "" + +msgid "Activate Rundown" +msgstr "" + +msgid "Activating Hold" +msgstr "" + +msgid "Activating Rundown Playlist" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "Active Rundown" +msgstr "" + +msgid "Active Rundown View" +msgstr "" + +msgid "Ad-Lib" +msgstr "" + +msgid "Ad-Lib Action" +msgstr "" + +msgid "Add" +msgstr "" + +msgid "Add {{filtersTitle}}" +msgstr "" + +msgid "Add a playout device to the studio in order to configure the route sets" +msgstr "" + +msgid "Add a playout device to the studio in order to edit the layer mappings" +msgstr "" + +msgid "Add Action Trigger" +msgstr "" + +msgid "Add button" +msgstr "" + +msgid "Add filter" +msgstr "" + +msgid "Add some source layers (e.g. Graphics) for your data to appear in rundowns" +msgstr "" + +msgid "AdLib" +msgstr "" + +msgid "AdLib Actions are not supported in the current Rundown" +msgstr "" + +msgid "AdLib could not be found!" +msgstr "" + +msgid "AdLib filter" +msgstr "" + +msgid "Adlib Rank" +msgstr "" + +msgid "Adlib rundowns are not supported for this ShowStyle!" +msgstr "" + +msgid "Adlib Testing" +msgstr "" + +msgid "AdLib Testing" +msgstr "" + +msgid "AdLibs can be only placed in a currently playing part!" +msgstr "" + +msgid "AdLibs on this layer can be queued" +msgstr "" + +msgid "All additional source layers must have active pieces" +msgstr "" + +msgid "All connections working correctly" +msgstr "" + +msgid "All devices working correctly" +msgstr "" + +msgid "All is well, go get a" +msgstr "" + +msgid "All Screens in a MultiViewer" +msgstr "" + +msgid "All steps" +msgstr "" + +msgid "Allow direct playing pieces" +msgstr "" + +msgid "Allow disabling of Pieces" +msgstr "" + +msgid "Allow HOLD mode" +msgstr "" + +msgid "Allow infinites from AdLib testing to persist" +msgstr "" + +msgid "Allow Read access" +msgstr "" + +msgid "Allow Rundowns to be reset while on-air" +msgstr "" + +msgid "Allow Write access" +msgstr "" + +msgid "Also Require Source Layers" +msgstr "" + +msgid "Amount of entries exceeds the limt of 10 000 items." +msgstr "" + +msgid "An error while performing the take, playout may be impacted" +msgstr "" + +msgid "An error while setting the next Part, playout may be impacted" +msgstr "" + +msgid "An internal error occured!" +msgstr "" + +msgid "Another Rundown is Already Active!" +msgstr "" + +msgid "Answers" +msgstr "" + +msgid "" +"Any AB Playout devices here will only be active when this or another " +"RouteSet that includes them is active" +msgstr "" + +msgid "APM Enabled" +msgstr "" + +msgid "APM Transaction Sample Rate" +msgstr "" + +msgid "Append" +msgstr "" + +msgid "Append or Replace" +msgstr "" + +msgid "Append rows" +msgstr "" + +msgid "Application credentials" +msgstr "" + +msgid "Application Performance Monitoring" +msgstr "" + +msgid "Apply" +msgstr "" + +msgid "Apply blueprint upgrades" +msgstr "" + +msgid "Apply Config" +msgstr "" + +msgid "Are you sure you want to activate Rehearsal Mode?" +msgstr "" + +msgid "" +"Are you sure you want to deactivate this Rundown\n" +"(This will clear the outputs)" +msgstr "" + +msgid "" +"Are you sure you want to deactivate this rundown?\n" +"(This will clear the outputs.)" +msgstr "" + +msgid "Are you sure you want to delete output layer \"{{outputId}}\"?" +msgstr "" + +msgid "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?" +msgstr "" + +msgid "Are you sure you want to delete the \"{{name}}\" rundown?" +msgstr "" + +msgid "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?" +msgstr "" + +msgid "Are you sure you want to delete the shelf layout \"{{name}}\"?" +msgstr "" + +msgid "Are you sure you want to delete the show style \"{{showStyleId}}\"?" +msgstr "" + +msgid "Are you sure you want to delete the studio \"{{studioId}}\"?" +msgstr "" + +msgid "Are you sure you want to delete this AdLib?" +msgstr "" + +msgid "Are you sure you want to delete this Bucket?" +msgstr "" + +msgid "Are you sure you want to delete this device: \"{{deviceId}}\"?" +msgstr "" + +msgid "Are you sure you want to empty (remove all adlibs inside) this Bucket?" +msgstr "" + +msgid "" +"Are you sure you want to force the migration? This will bypass the " +"migration checks, so be sure to verify that the values in the settings are " +"correct!" +msgstr "" + +msgid "Are you sure you want to import the contents of the file \"{{fileName}}\"?" +msgstr "" + +msgid "Are you sure you want to re-sync the \"{{name}}\" rundown?" +msgstr "" + +msgid "" +"Are you sure you want to re-sync the Rundown?\n" +"(If the currently playing Part has been changed, this can affect the output)" +msgstr "" + +msgid "Are you sure you want to remove {{type}} \"{{deviceId}}\"?" +msgstr "" + +msgid "Are you sure you want to remove all Variants in the table?" +msgstr "" + +msgid "" +"Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\n" +"Route Sets assigned to this group will be reset to no group." +msgstr "" + +msgid "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?" +msgstr "" + +msgid "Are you sure you want to remove the AB Player \"{{playerId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to remove the device \"{{deviceName}}\" and all of " +"it's sub-devices?" +msgstr "" + +msgid "Are you sure you want to remove the Package Container \"{{containerId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to remove the Package Container Accessor " +"\"{{accessorId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to " +"\"{{newLayerId}}\"?" +msgstr "" + +msgid "Are you sure you want to remove the Route Set \"{{routeId}}\"?" +msgstr "" + +msgid "Are you sure you want to remove the Variant \"{{showStyleVariantId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to replace the blueprints with the file " +"\"{{fileName}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to reset all overrides for Packing Container " +"\"{{id}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to reset all overrides for the mapping for layer " +"\"{{mappingId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to reset all overrides for the output layer " +"\"{{outputLayerId}}\"?" +msgstr "" + +msgid "Are you sure you want to reset all overrides for the selected row?" +msgstr "" + +msgid "" +"Are you sure you want to reset all overrides for the source layer " +"\"{{sourceLayerId}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to reset the database version?\n" +"Only do this if you plan on running the migration right after." +msgstr "" + +msgid "Are you sure you want to restart this device?" +msgstr "" + +msgid "" +"Are you sure you want to restart this Sofie Automation Server Core: " +"{{name}}?" +msgstr "" + +msgid "" +"Are you sure you want to restore the system from the snapshot file " +"\"{{fileName}}\"?" +msgstr "" + +msgid "Are you sure you want to skip the fix up config step for {{name}}" +msgstr "" + +msgid "" +"Are you sure you want to update the blueprints from the file " +"\"{{fileName}}\"?" +msgstr "" + +msgid "" +"Are you sure you want to upload the shelf layout from the file " +"\"{{fileName}}\"?" +msgstr "" + +msgid "" +"Are you sure, do you really want to REMOVE the Snapshot " +"\"{{snapshotName}}\"?\r\n" +"This cannot be undone!!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "" +"Are you sure? This will cause the whole Sofie system to be unresponsive " +"several seconds!" +msgstr "" + +msgid "Around 10 minutes ago" +msgstr "" + +msgid "Assign" +msgstr "" + +msgid "Assigned Show Styles" +msgstr "" + +msgid "Assigned Studios" +msgstr "" + +msgid "Attached Subdevices" +msgstr "" + +msgid "Audio Mixing" +msgstr "" + +msgid "Authorize App Access" +msgstr "" + +msgid "Auto" +msgstr "" + +msgid "AutoNext in QuickLoop behavior" +msgstr "" + +msgid "Available Screens for Studio {{studioId}}" +msgstr "" + +msgid "Bad" +msgstr "" + +msgid "Bank Index" +msgstr "" + +msgid "Base URL" +msgstr "" + +msgid "Base url to the resource (example: http://myserver/folder)" +msgstr "" + +msgid "Baseline needs reload, this studio may not work until reloaded" +msgstr "" + +msgid "Behavior" +msgstr "" + +msgid "Blueprint" +msgstr "" + +msgid "Blueprint config has changed" +msgstr "" + +msgid "Blueprint config preset" +msgstr "" + +msgid "" +"Blueprint config preset has been changed. From \"{{ oldValue }}\", to \"{{ " +"newValue }}\"" +msgstr "" + +msgid "Blueprint config preset is missing" +msgstr "" + +msgid "Blueprint config preset not set" +msgstr "" + +msgid "Blueprint Configuration" +msgstr "" + +msgid "Blueprint has a new version" +msgstr "" + +msgid "Blueprint has been changed. From \"{{ oldValue }}\", to \"{{ newValue }}\"" +msgstr "" + +msgid "Blueprint ID" +msgstr "" + +msgid "Blueprint Name" +msgstr "" + +msgid "Blueprint not found!" +msgstr "" + +msgid "Blueprint not set" +msgstr "" + +msgid "Blueprint Type" +msgstr "" + +msgid "Blueprint Version" +msgstr "" + +msgid "Blueprints" +msgstr "" + +msgid "Blueprints updated successfully." +msgstr "" + +msgid "Bottom" +msgstr "" + +msgid "Box color" +msgstr "" + +msgid "BREAK" +msgstr "" + +msgid "Break In" +msgstr "" + +msgid "Bucket AdLib is not compatible with this Rundown!" +msgstr "" + +msgid "Bucket not found!" +msgstr "" + +msgid "Button" +msgstr "" + +msgid "Button height scale factor" +msgstr "" + +msgid "Button mapping" +msgstr "" + +msgid "Button width scale factor" +msgstr "" + +msgid "By Parts" +msgstr "" + +msgid "By Segments" +msgstr "" + +msgid "Camera" +msgstr "" + +msgid "Camera Screen" +msgstr "" + +msgid "Can Generate Adlib Testing Rundown" +msgstr "" + +msgid "Can not be used during a hold!" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Cancel currently pressed hotkey" +msgstr "" + +msgid "Cannot activate HOLD before a part has been taken!" +msgstr "" + +msgid "Cannot activate HOLD between the current and next parts" +msgstr "" + +msgid "Cannot activate HOLD once an adlib has been used" +msgstr "" + +msgid "Cannot cancel HOLD once it has been taken" +msgstr "" + +msgid "Cannot connect to the {{platformName}}: {{reason}}" +msgstr "" + +msgid "Cannot perform take for {{duration}}ms" +msgstr "" + +msgid "Cannot play this AdLib because it is marked as Floated" +msgstr "" + +msgid "Cannot play this AdLib because it is marked as Invalid" +msgstr "" + +msgid "Cannot play this adlib because source layer is not queueable" +msgstr "" + +msgid "Cannot remove the rundown \"{{name}}\" while it is on-air." +msgstr "" + +msgid "Cannot take close to an AUTO" +msgstr "" + +msgid "Cannot take during a transition" +msgstr "" + +msgid "Cannot take unplayable AdLib" +msgstr "" + +msgid "CasparCG on device \"{{deviceName}}\" restarting..." +msgstr "" + +msgid "Center" +msgstr "" + +msgid "Change to fullscreen mode" +msgstr "" + +msgid "Channel Name" +msgstr "" + +msgid "" +"Check layer types to select all layers of that type, or check individual " +"layers for more specific filtering." +msgstr "" + +msgid "Check the console for troubleshooting data from device \"{{deviceName}}\"!" +msgstr "" + +msgid "Cleanup" +msgstr "" + +msgid "Cleanup old data" +msgstr "" + +msgid "Cleanup old database indexes" +msgstr "" + +msgid "Clear {{layerName}}" +msgstr "" + +msgid "Clear filter" +msgstr "" + +msgid "Clear queued segment" +msgstr "" + +msgid "Clear QuickLoop" +msgstr "" + +msgid "Clear QuickLoop End" +msgstr "" + +msgid "Clear QuickLoop Start" +msgstr "" + +msgid "Clear Source Layer" +msgstr "" + +msgid "Clear value" +msgstr "" + +msgid "Clearing SourceLayer" +msgstr "" + +msgid "Click anywhere for fullscreen" +msgstr "" + +msgid "Click on a rundown to control your studio" +msgstr "" + +msgid "Click or press Enter for fullscreen" +msgstr "" + +msgid "Click to show available Package Containers" +msgstr "" + +msgid "Click to show available Show Styles" +msgstr "" + +msgid "Client IP" +msgstr "" + +msgid "Clip starts with {{frames}} black frames" +msgstr "" + +msgid "Clip starts with {{frames}} freeze frames" +msgstr "" + +msgid "Clips" +msgstr "" + +msgid "Close" +msgstr "" + +msgid "Close Properties Panel" +msgstr "" + +msgid "Close Rundown" +msgstr "" + +msgid "Comma-separated list of studio labels to filter by. Leave empty for all." +msgstr "" + +msgid "Comma-separated speeds in px/frame" +msgstr "" + +msgid "Compatible Studios" +msgstr "" + +msgid "Completed with warnings" +msgstr "" + +msgid "Config Fix Up must be run or ignored before the configuration can be edited" +msgstr "" + +msgid "Config for {{name}} fix failed" +msgstr "" + +msgid "Config for {{name}} fixed successfully" +msgstr "" + +msgid "Config for {{name}} upgraded failed" +msgstr "" + +msgid "Config for {{name}} upgraded successfully" +msgstr "" + +msgid "Config has not been applied before" +msgstr "" + +msgid "Config ID: " +msgstr "" + +msgid "Config looks good" +msgstr "" + +msgid "Config preset" +msgstr "" + +msgid "Config preset is missing" +msgstr "" + +msgid "Config preset not set" +msgstr "" + +msgid "Config requires fixing up before it can be validated" +msgstr "" + +msgid "" +"Config value \"{{ name }}\" has changed. From \"{{ oldValue }}\", to \"{{ " +"newValue }}\"" +msgstr "" + +msgid "Configurable Screens" +msgstr "" + +msgid "" +"Configuration for this Gateway has moved to the Studio Peripheral Device " +"settings" +msgstr "" + +msgid "Configure display options" +msgstr "" + +msgid "" +"Configure which pieces should display their assigned AB resolver channel " +"(e.g., \"Server A\") on various screens. This helps operators identify " +"which video server is playing each clip." +msgstr "" + +msgid "Confirm" +msgstr "" + +msgid "Connect some devices to the playout gateway" +msgstr "" + +msgid "Connect to {{deviceName}}" +msgstr "" + +msgid "Connected" +msgstr "" + +msgid "Connected App Containers" +msgstr "" + +msgid "Connected Expectation Managers" +msgstr "" + +msgid "Connected to the {{platformName}}." +msgstr "" + +msgid "Connected Workers" +msgstr "" + +msgid "Connecting to the {{platformName}}" +msgstr "" + +msgid "Container Status" +msgstr "" + +msgid "Control mode" +msgstr "" + +msgid "Control modes" +msgstr "" + +msgid "" +"Controls for exposed Route Sets will be displayed to the producer within " +"the Rundown View in the Switchboard." +msgstr "" + +msgid "Core" +msgstr "" + +msgid "Core + Worker processing time" +msgstr "" + +msgid "Core System settings" +msgstr "" + +msgid "" +"Could not create a snapshot for the evaluation, because the previous one " +"was created just moments ago. If you want another snapshot, try again in a " +"couple of seconds." +msgstr "" + +msgid "Could not get system status. Please consult system administrator." +msgstr "" + +msgid "Could not restart core: {{err}}" +msgstr "" + +msgid "Could not restart Playout Gateway \"{{playoutDeviceName}}\"." +msgstr "" + +msgid "Create Adlib Testing Rundown" +msgstr "" + +msgid "Create new Bucket" +msgstr "" + +msgid "Created" +msgstr "" + +msgid "Creating a new Bucket" +msgstr "" + +msgid "Creating Adlib Testing Rundown" +msgstr "" + +msgid "Creating Snapshot for debugging" +msgstr "" + +msgid "Critical problems" +msgstr "" + +msgid "Critical Problems" +msgstr "" + +msgid "Cron jobs" +msgstr "" + +msgid "Current Part" +msgstr "" + +msgid "Current part can contain next pieces" +msgstr "" + +msgid "Current Segment" +msgstr "" + +msgid "Custom Classes" +msgstr "" + +msgid "Custom Hotkey Labels" +msgstr "" + +msgid "Deactivate" +msgstr "" + +msgid "Deactivate \"On Air\"" +msgstr "" + +msgid "Deactivate Rundown" +msgstr "" + +msgid "Deactivate Studio" +msgstr "" + +msgid "Deactivating other Rundown Playlist, and activating this one" +msgstr "" + +msgid "Deactivating Rundown Playlist" +msgstr "" + +msgid "Debug" +msgstr "" + +msgid "Debug mode" +msgstr "" + +msgid "Debug State" +msgstr "" + +msgid "Default" +msgstr "" + +msgid "Default (hide)" +msgstr "" + +msgid "Default Layout" +msgstr "" + +msgid "Default shelf height" +msgstr "" + +msgid "Default State" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Delete Action Trigger" +msgstr "" + +msgid "Delete layout?" +msgstr "" + +msgid "Delete mapping" +msgstr "" + +msgid "Delete output layer" +msgstr "" + +msgid "Delete rundown?" +msgstr "" + +msgid "Delete source layer" +msgstr "" + +msgid "Delete this AdLib" +msgstr "" + +msgid "Delete this Blueprint?" +msgstr "" + +msgid "Delete this Bucket" +msgstr "" + +msgid "Delete this item?" +msgstr "" + +msgid "Delete this output?" +msgstr "" + +msgid "Delete this Show Style?" +msgstr "" + +msgid "Delete this Studio?" +msgstr "" + +msgid "Device" +msgstr "" + +msgid "Device \"{{deviceName}}\" restarting..." +msgstr "" + +msgid "Device {{deviceName}} is disconnected" +msgstr "" + +msgid "Device ID" +msgstr "" + +msgid "Device is already attached to another studio." +msgstr "" + +msgid "Device is missing configuration schema" +msgstr "" + +msgid "Device is of unknown type" +msgstr "" + +msgid "Device Name" +msgstr "" + +msgid "Device not found" +msgstr "" + +msgid "Device Triggers" +msgstr "" + +msgid "Device Type" +msgstr "" + +msgid "Devices" +msgstr "" + +msgid "Devices with issues" +msgstr "" + +msgid "Did you have any problems with the broadcast?" +msgstr "" + +msgid "Diff" +msgstr "" + +msgid "Director Screen" +msgstr "" + +msgid "Director's Screen" +msgstr "" + +msgid "Disable" +msgstr "" + +msgid "Disable Context Menu" +msgstr "" + +msgid "Disable follow take" +msgstr "" + +msgid "Disable hints by adding this to the URL:" +msgstr "" + +msgid "Disable next Piece" +msgstr "" + +msgid "Disable the hover Inspector when hovering over the button" +msgstr "" + +msgid "Disable the next element" +msgstr "" + +msgid "Disable version check" +msgstr "" + +msgid "Disabled" +msgstr "" + +msgid "Disabling next Piece" +msgstr "" + +msgid "Disconnected" +msgstr "" + +msgid "Dismiss" +msgstr "" + +msgid "Dismiss all notifications" +msgstr "" + +msgid "Display AB channel assignments on:" +msgstr "" + +msgid "Display AdLibs in a column in List View" +msgstr "" + +msgid "Display in a column in List View" +msgstr "" + +msgid "Display name of the Package Container" +msgstr "" + +msgid "Display piece duration for source layers" +msgstr "" + +msgid "Display Rank" +msgstr "" + +msgid "Display Style" +msgstr "" + +msgid "Display Take buttons" +msgstr "" + +msgid "" +"Do you really want to remove just the rundown \"{{rundownName}}\" in the " +"playlist {{playlistName}} from Sofie? \n" +"\n" +"This cannot be undone!" +msgstr "" + +msgid "Do you really want to restore the snapshot \"{{snapshotName}}\"?" +msgstr "" + +msgid "Do you want to activate this Rundown?" +msgstr "" + +msgid "" +"Do you want to append these to existing Action Triggers, or do you want to " +"replace them?" +msgstr "" + +msgid "Do you want to do this?" +msgstr "" + +msgid "Do you want to execute {{actionName}}? This may the disrupt the output" +msgstr "" + +msgid "Do you want to restart CasparCG Server \"{{device}}\"?" +msgstr "" + +msgid "Do you want to restart the Playout Gateway?" +msgstr "" + +msgid "Documentation is available at" +msgstr "" + +msgid "Documents to be removed:" +msgstr "" + +msgid "Does NOT support HEAD requests" +msgstr "" + +msgid "Don't treat the end of the last rundown in a playlist as a break" +msgstr "" + +msgid "Done" +msgstr "" + +msgid "Download Action Triggers" +msgstr "" + +msgid "Drag to reorder or move out of playlist" +msgstr "" + +msgid "Drop Rundown here to move it out of its current Playlist" +msgstr "" + +msgid "Dropzone URL" +msgstr "" + +msgid "Duplicate Action Trigger" +msgstr "" + +msgid "Duration" +msgstr "" + +msgid "DURATION" +msgstr "" + +msgid "e.g., Studio A,Studio B" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Edit Action Trigger" +msgstr "" + +msgid "Edit in Nora" +msgstr "" + +msgid "Edit mapping" +msgstr "" + +msgid "Edit Mode" +msgstr "" + +msgid "Edit output layer" +msgstr "" + +msgid "Edit Part Properties" +msgstr "" + +msgid "Edit Piece Properties" +msgstr "" + +msgid "Edit Segment Properties" +msgstr "" + +msgid "Edit source layer" +msgstr "" + +msgid "Edit Support Panel" +msgstr "" + +msgid "Empty" +msgstr "" + +msgid "Empty this Bucket" +msgstr "" + +msgid "Emptying Bucket" +msgstr "" + +msgid "Enable" +msgstr "" + +msgid "Enable \"Play from Anywhere\"" +msgstr "" + +msgid "Enable AdLib Testing, for testing AdLibs before taking the first Part" +msgstr "" + +msgid "Enable automatic storage of Rundown Playlist snapshots periodically" +msgstr "" + +msgid "Enable Buckets" +msgstr "" + +msgid "Enable CasparCG restart job" +msgstr "" + +msgid "Enable configuration mode by adding ?configure=1 to the address bar." +msgstr "" + +msgid "Enable Evaluation Form" +msgstr "" + +msgid "Enable hints by adding this to the URL:" +msgstr "" + +msgid "Enable QuickLoop" +msgstr "" + +msgid "Enable search toolbar" +msgstr "" + +msgid "Enable User Editing" +msgstr "" + +msgid "Enabled" +msgstr "" + +msgid "Enabled on all Parts, applying QuickLoop Fallback Part Duration if needed" +msgstr "" + +msgid "Enabled, but skipping parts with undefined or 0 duration" +msgstr "" + +msgid "" +"Enables internal monitoring of blocked main thread. Logs when there is an " +"issue, but (unverified) might cause issues in itself." +msgstr "" + +msgid "End of script" +msgstr "" + +msgid "End Words" +msgstr "" + +msgid "Error" +msgstr "" + +msgid "Error when checking for cleaning up" +msgstr "" + +msgid "Error: The ShowStyle of this Rundown was not found." +msgstr "" + +msgid "Error: The studio of this Rundown was not found." +msgstr "" + +msgid "Est. End" +msgstr "" + +msgid "Evaluations" +msgstr "" + +msgid "Exclusivity group" +msgstr "" + +msgid "Exclusivity Group ID" +msgstr "" + +msgid "Exclusivity Group Name" +msgstr "" + +msgid "Exclusivity Groups" +msgstr "" + +msgid "Execute" +msgstr "" + +msgid "Execute User Operation" +msgstr "" + +msgid "Executed {{actionName}} on device \"{{deviceName}}\": {{response}}" +msgstr "" + +msgid "Executed {{actionName}} on device \"{{deviceName}}\"..." +msgstr "" + +msgid "Executes within the currently open Rundown, requires a Client-side trigger." +msgstr "" + +msgid "Execution times" +msgstr "" + +msgid "Exit" +msgstr "" + +msgid "Expectation Manager" +msgstr "" + +msgid "Expected End" +msgstr "" + +msgid "Expected End text" +msgstr "" + +msgid "Expected End Time" +msgstr "" + +msgid "Expected Start" +msgstr "" + +msgid "Export" +msgstr "" + +msgid "Export visible" +msgstr "" + +msgid "Expose as user selectable layout" +msgstr "" + +msgid "Expose layout as a standalone page" +msgstr "" + +msgid "External message queue has unsent messages." +msgstr "" + +msgid "Failed to activate" +msgstr "" + +msgid "Failed to add a new Show Style Variant: {{errorMessage}}" +msgstr "" + +msgid "Failed to assign AB player for {{pieceNames}}" +msgstr "" + +msgid "Failed to assign non-critical AB player for {{pieceNames}}" +msgstr "" + +msgid "Failed to compare config changes" +msgstr "" + +msgid "Failed to copy Show Style Variant: {{errorMessage}}" +msgstr "" + +msgid "Failed to delete Show Style Variant: {{errorMessage}}" +msgstr "" + +msgid "" +"Failed to execute {{actionName}} on device: \"{{deviceName}}\": " +"{{errorMessage}}" +msgstr "" + +msgid "Failed to execute take" +msgstr "" + +msgid "Failed to generate adlib rundown! {{message}}" +msgstr "" + +msgid "Failed to import new Show Style Variants: {{errorMessage}}" +msgstr "" + +msgid "" +"Failed to import Show Style Variant {{name}}. Make sure it is not already " +"imported." +msgstr "" + +msgid "Failed to remove all Show Style Variants: {{errorMessage}}" +msgstr "" + +msgid "Failed to reorderShow Style Variant: {{errorMessage}}" +msgstr "" + +msgid "Failed to reset OAuth credentials: {{errorMessage}}" +msgstr "" + +msgid "Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}" +msgstr "" + +msgid "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}" +msgstr "" + +msgid "Failed to update blueprints: {{errorMessage}}" +msgstr "" + +msgid "Failed to update config: {{errorMessage}}" +msgstr "" + +msgid "Failed to upload OAuth credentials: {{errorMessage}}" +msgstr "" + +msgid "Failed to upload shelf layout: {{errorMessage}}" +msgstr "" + +msgid "Failed to validate config" +msgstr "" + +msgid "Fatal" +msgstr "" + +msgid "File path to the folder of the local folder" +msgstr "" + +msgid "Filter by Output Layer" +msgstr "" + +msgid "Filter by Source Layer" +msgstr "" + +msgid "Filter disabled" +msgstr "" + +msgid "Filter Disabled" +msgstr "" + +msgid "Filter..." +msgstr "" + +msgid "Filters" +msgstr "" + +msgid "Find Trigger..." +msgstr "" + +msgid "Fine scroll" +msgstr "" + +msgid "Fix Up Config" +msgstr "" + +msgid "Fixed duration in Segment header" +msgstr "" + +msgid "" +"Fixing this problem requires a restart to the host device. Are you sure you " +"want to restart {{device}}?\n" +"(This might affect output)" +msgstr "" + +msgid "Floated Adlib" +msgstr "" + +msgid "Floated AdLib" +msgstr "" + +msgid "Folder path" +msgstr "" + +msgid "Folder path to shared folder" +msgstr "" + +msgid "Following" +msgstr "" + +msgid "Font size" +msgstr "" + +msgid "for {{name}} fix skipped successfully" +msgstr "" + +msgid "Force" +msgstr "" + +msgid "Force (deactivate others)" +msgstr "" + +msgid "Force Migration" +msgstr "" + +msgid "Force Migration (unsafe)" +msgstr "" + +msgid "Force the Multi-gateway-mode" +msgstr "" + +msgid "Forward" +msgstr "" + +msgid "Forward: {{forward}}" +msgstr "" + +msgid "Found no future pieces" +msgstr "" + +msgid "Frame Rate" +msgstr "" + +msgid "From" +msgstr "" + +msgid "Full System Snapshot" +msgstr "" + +msgid "fullscreen" +msgstr "" + +msgid "Gateway" +msgstr "" + +msgid "General" +msgstr "" + +msgid "Generated URL" +msgstr "" + +msgid "Generating restart token" +msgstr "" + +msgid "Generic Properties" +msgstr "" + +msgid "Generic Script" +msgstr "" + +msgid "Getting Started" +msgstr "" + +msgid "Global AdLib" +msgstr "" + +msgid "Global AdLibs" +msgstr "" + +msgid "Go to Live" +msgstr "" + +msgid "Go to On Air line" +msgstr "" + +msgid "Go to On Air Segment" +msgstr "" + +msgid "Good" +msgstr "" + +msgid "Graphics" +msgstr "" + +msgid "GUI" +msgstr "" + +msgid "he default state of this Route Set" +msgstr "" + +msgid "Heading" +msgstr "" + +msgid "Height" +msgstr "" + +msgid "Help & Support" +msgstr "" + +msgid "Hide" +msgstr "" + +msgid "Hide Countdown" +msgstr "" + +msgid "Hide default AdLib Start/Execute options" +msgstr "" + +msgid "Hide Diff" +msgstr "" + +msgid "Hide Diff Label" +msgstr "" + +msgid "Hide duplicated AdLibs" +msgstr "" + +msgid "Hide End Time" +msgstr "" + +msgid "Hide Expected End timing when a break is next" +msgstr "" + +msgid "Hide for dynamically inserted parts" +msgstr "" + +msgid "Hide Label" +msgstr "" + +msgid "Hide over/under timer" +msgstr "" + +msgid "Hide Panel from view" +msgstr "" + +msgid "Hide Planned End Label" +msgstr "" + +msgid "Hide Planned Start" +msgstr "" + +msgid "Hide Rundown Divider" +msgstr "" + +msgid "Hide rundown divider between rundowns in a playlist" +msgstr "" + +msgid "Hide scrollbar" +msgstr "" + +msgid "Hold" +msgstr "" + +msgid "Hostname or IP address of the Atem" +msgstr "" + +msgid "Hotkey" +msgstr "" + +msgid "How did the show go?" +msgstr "" + +msgid "" +"How many of the transactions to monitor. Set to -1 to log nothing (max " +"performance), 0.5 to log 50% of the transactions, 1 to log all transactions" +msgstr "" + +msgid "" +"How much preparation time to add to global pieces on the timeline before " +"they are played" +msgstr "" + +msgid "HTML that will be shown in the Support Panel" +msgstr "" + +msgid "Human-readable name of the layer" +msgstr "" + +msgid "Icon" +msgstr "" + +msgid "Icon color" +msgstr "" + +msgid "Id" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "" +"ID of the device (corresponds to the device ID in the peripheralDevice " +"settings)" +msgstr "" + +msgid "ID of the timeline-layer to map to some output" +msgstr "" + +msgid "Idempotency-Key is already used" +msgstr "" + +msgid "Idempotency-Key is missing" +msgstr "" + +msgid "If set, only one Route Set will be active per exclusivity group" +msgstr "" + +msgid "" +"If set, Package Manager assumes that the source doesn't support HEAD " +"requests and will use GET instead. If false, HEAD requests will be sent to " +"check availability." +msgstr "" + +msgid "Ignore and apply" +msgstr "" + +msgid "Ignore QuickLoop" +msgstr "" + +msgid "Ignoring take as playing part has changed since TAKE was requested." +msgstr "" + +msgid "Ignoring TAKES that are too quick after eachother ({{duration}} ms)" +msgstr "" + +msgid "Import" +msgstr "" + +msgid "Import error: {{errorMessage}}" +msgstr "" + +msgid "Import file?" +msgstr "" + +msgid "Importing an AdLib to the Bucket" +msgstr "" + +msgid "In" +msgstr "" + +msgid "IN" +msgstr "" + +msgid "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s" +msgstr "" + +msgid "in {{hours}} h {{minutes}} min {{seconds}} s" +msgstr "" + +msgid "in {{minutes}} min {{seconds}} s" +msgstr "" + +msgid "in {{seconds}} s" +msgstr "" + +msgid "In rehearsal" +msgstr "" + +msgid "Include Clear Source Layer in Ad-Libs" +msgstr "" + +msgid "Include Global AdLibs" +msgstr "" + +msgid "Indicate only one next piece per source layer" +msgstr "" + +msgid "Ingest Devices" +msgstr "" + +msgid "Ingest devices are needed to create rundowns" +msgstr "" + +msgid "Ingest from Snapshot" +msgstr "" + +msgid "Ingest Rundown Status" +msgstr "" + +msgid "Ingest Rundown Statuses" +msgstr "" + +msgid "Input Devices" +msgstr "" + +msgid "Input devices allow you to trigger Sofie actions remotely" +msgstr "" + +msgid "Installation name" +msgstr "" + +msgid "Internal Error generating RundownPlaylist" +msgstr "" + +msgid "Internal ID" +msgstr "" + +msgid "Invalid AdLib" +msgstr "" + +msgid "Invalid blueprint: \"{{blueprintId}}\"" +msgstr "" + +msgid "" +"Invalid config preset for blueprint: \"{{configPresetId}}\" " +"({{blueprintId}})" +msgstr "" + +msgid "Invert joystick" +msgstr "" + +msgid "Is a Guest Input" +msgstr "" + +msgid "Is a Live Remote Input" +msgstr "" + +msgid "Is collapsed by default" +msgstr "" + +msgid "Is flattened" +msgstr "" + +msgid "Is hidden" +msgstr "" + +msgid "Is Immutable" +msgstr "" + +msgid "Is PGM Output" +msgstr "" + +msgid "ISA URLs" +msgstr "" + +msgid "Job Status" +msgstr "" + +msgid "Just now" +msgstr "" + +msgid "Key" +msgstr "" + +msgid "Keyboard" +msgstr "" + +msgid "" +"Keyboard shortcuts and Stream Deck buttons will not work while filling out " +"the form!" +msgstr "" + +msgid "Kill (debug)" +msgstr "" + +msgid "Label" +msgstr "" + +msgid "Label contains" +msgstr "" + +msgid "Last" +msgstr "" + +msgid "Last {{layerName}}" +msgstr "" + +msgid "Last modified" +msgstr "" + +msgid "Last rundown is not break" +msgstr "" + +msgid "Last seen" +msgstr "" + +msgid "Last Seen" +msgstr "" + +msgid "Last update" +msgstr "" + +msgid "Last updated" +msgstr "" + +msgid "Layer does not allow sticky pieces!" +msgstr "" + +msgid "Layer ID" +msgstr "" + +msgid "Layer Mappings" +msgstr "" + +msgid "Layer Name" +msgstr "" + +msgid "Leave Unsynced" +msgstr "" + +msgid "Less than a minute ago" +msgstr "" + +msgid "Less than five minutes ago" +msgstr "" + +msgid "Lighting" +msgstr "" + +msgid "Limit" +msgstr "" + +msgid "Live line countdown requires Source Layer" +msgstr "" + +msgid "Live Speak" +msgstr "" + +msgid "Loading" +msgstr "" + +msgid "Loading..." +msgstr "" + +msgid "Local" +msgstr "" + +msgid "Local Time" +msgstr "" + +msgid "Logging level" +msgstr "" + +msgid "Logo" +msgstr "" + +msgid "Lookahead Maximum Search Distance (Undefined = {{limit}})" +msgstr "" + +msgid "Lookahead Mode" +msgstr "" + +msgid "Lookahead Target Objects (Undefined = 1)" +msgstr "" + +msgid "Loop End" +msgstr "" + +msgid "Loop Start" +msgstr "" + +msgid "Loops to Start" +msgstr "" + +msgid "Lower Third" +msgstr "" + +msgid "Manage Snapshots" +msgstr "" + +msgid "Mapping cannot be reset as it has no default values" +msgstr "" + +msgid "Mapping Type" +msgstr "" + +msgid "Mappings" +msgstr "" + +msgid "Margin (%)" +msgstr "" + +msgid "Maximum register limit" +msgstr "" + +msgid "Media" +msgstr "" + +msgid "Media Preview URL" +msgstr "" + +msgid "Media Status" +msgstr "" + +msgid "Media Type" +msgstr "" + +msgid "Memory troubleshooting" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Message" +msgstr "" + +msgid "Message Queue" +msgstr "" + +msgid "Message shown to users in the Evaluations form" +msgstr "" + +msgid "Messages" +msgstr "" + +msgid "Method" +msgstr "" + +msgid "Method ${method}" +msgstr "" + +msgid "MIDI Pedal" +msgstr "" + +msgid "Migrate database" +msgstr "" + +msgid "Migrations" +msgstr "" + +msgid "Mini Shelf Layout" +msgstr "" + +msgid "Mini Shelf Layouts" +msgstr "" + +msgid "Minimum register limit" +msgstr "" + +msgid "Minimum Take Span" +msgstr "" + +msgid "Minor Warning" +msgstr "" + +msgid "Mirror horizontally" +msgstr "" + +msgid "Mirror vertically" +msgstr "" + +msgid "Mock Piece Content Status" +msgstr "" + +msgid "Mode: {{triggerMode}}" +msgstr "" + +msgid "Modify Shift register" +msgstr "" + +msgid "Modifying Bucket" +msgstr "" + +msgid "Modifying Bucket AdLib" +msgstr "" + +msgid "Monitor blocked thread" +msgstr "" + +msgid "More documentation available at:" +msgstr "" + +msgid "More than 10 minutes ago" +msgstr "" + +msgid "More than 2 hours ago" +msgstr "" + +msgid "More than 30 minutes ago" +msgstr "" + +msgid "More than 5 hours ago" +msgstr "" + +msgid "More than a day ago" +msgstr "" + +msgid "Mouse" +msgstr "" + +msgid "Move Next" +msgstr "" + +msgid "Move Next backwards" +msgstr "" + +msgid "Move Next forwards" +msgstr "" + +msgid "Move Next to the following segment" +msgstr "" + +msgid "Move Next to the previous segment" +msgstr "" + +msgid "Move Parts" +msgstr "" + +msgid "Move Segments" +msgstr "" + +msgid "Moving Next" +msgstr "" + +msgid "Multi-gateway-mode delay time" +msgstr "" + +msgid "Multilingual description, editing will overwrite" +msgstr "" + +msgid "My name is {{name}}" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Network address" +msgstr "" + +msgid "Network Id" +msgstr "" + +msgid "New Bucket" +msgstr "" + +msgid "New Filter" +msgstr "" + +msgid "New Layer" +msgstr "" + +msgid "New Layout" +msgstr "" + +msgid "New Output" +msgstr "" + +msgid "New Source" +msgstr "" + +msgid "Next" +msgstr "" + +msgid "Next Break text" +msgstr "" + +msgid "Next Loop at" +msgstr "" + +msgid "Next Part" +msgstr "" + +msgid "Next scheduled show" +msgstr "" + +msgid "Next Segment" +msgstr "" + +msgid "Nintendo Joy-Con" +msgstr "" + +msgid "No" +msgstr "" + +msgid "No Action Triggers set up." +msgstr "" + +msgid "No actions available" +msgstr "" + +msgid "" +"No Ad-Lib matches in the current state of Rundown: " +"\"{{rundownPlaylistName}}\"" +msgstr "" + +msgid "No camera-related source layers found" +msgstr "" + +msgid "No changes" +msgstr "" + +msgid "No gateways are configured" +msgstr "" + +msgid "No matching Action Trigger." +msgstr "" + +msgid "No matching Rundowns available to be used for preview" +msgstr "" + +msgid "No Media matches this filter" +msgstr "" + +msgid "No Media required by this system" +msgstr "" + +msgid "No Media required for this Rundown" +msgstr "" + +msgid "No migrations to apply" +msgstr "" + +msgid "No name set" +msgstr "" + +msgid "No Next point found, please set a part as Next before doing a TAKE." +msgstr "" + +msgid "No notifications" +msgstr "" + +msgid "No output channels set" +msgstr "" + +msgid "No output layers available" +msgstr "" + +msgid "No PGM output" +msgstr "" + +msgid "No problems" +msgstr "" + +msgid "No schema has been provided for this mapping" +msgstr "" + +msgid "No source layers available" +msgstr "" + +msgid "No source layers set" +msgstr "" + +msgid "No status loaded" +msgstr "" + +msgid "None" +msgstr "" + +msgid "Normal scrolling" +msgstr "" + +msgid "Not Active" +msgstr "" + +msgid "Not Connected" +msgstr "" + +msgid "Not defined" +msgstr "" + +msgid "Not Global" +msgstr "" + +msgid "Not in rehearsal" +msgstr "" + +msgid "Not queueable" +msgstr "" + +msgid "Not set" +msgstr "" + +msgid "Note: Core needs to be restarted to apply these settings" +msgstr "" + +msgid "Notes" +msgstr "" + +msgid "Nothing to cleanup!" +msgstr "" + +msgid "Nothing was found on layer!" +msgstr "" + +msgid "Now Active Rundown" +msgstr "" + +msgid "NRCS Name" +msgstr "" + +msgid "OAuth credentials successfully reset" +msgstr "" + +msgid "OAuth credentials successfully uploaded." +msgstr "" + +msgid "Off" +msgstr "" + +msgid "Off-line devices" +msgstr "" + +msgid "OK" +msgstr "" + +msgid "On" +msgstr "" + +msgid "On Air" +msgstr "" + +msgid "On Air At" +msgstr "" + +msgid "On Air In" +msgstr "" + +msgid "On Air Start Time" +msgstr "" + +msgid "On release" +msgstr "" + +msgid "OnAir" +msgstr "" + +msgid "" +"One of these source layers must have a piece for the countdown to segment " +"on-air to be show" +msgstr "" + +msgid "" +"One of these source layers must have an active piece for the live line " +"countdown to be show" +msgstr "" + +msgid "Only custom trigger modes will be shown" +msgstr "" + +msgid "Only Display AdLibs from Current Segment" +msgstr "" + +msgid "Only Global" +msgstr "" + +msgid "Only Match Global AdLibs" +msgstr "" + +msgid "" +"Only one rundown can be active at the same time. Currently active rundowns: " +"{{names}}" +msgstr "" + +msgid "Only Pieces present in rundown are sticky" +msgstr "" + +msgid "Open" +msgstr "" + +msgid "Open Camera Screen" +msgstr "" + +msgid "Open Fullscreen" +msgstr "" + +msgid "Open Presenter Screen" +msgstr "" + +msgid "Open Prompter" +msgstr "" + +msgid "Open shelf by default" +msgstr "" + +msgid "Operating Mode" +msgstr "" + +msgid "Operation" +msgstr "" + +msgid "Optional description of the action" +msgstr "" + +msgid "" +"Optionally restrict AB channel display to specific output layers (e.g., " +"only PGM). Leave empty to show for all output layers." +msgstr "" + +msgid "Order 66?" +msgstr "" + +msgid "Original Layer" +msgstr "" + +msgid "Original Layer not found" +msgstr "" + +msgid "Other" +msgstr "" + +msgid "Out" +msgstr "" + +msgid "OUT" +msgstr "" + +msgid "Output channels" +msgstr "" + +msgid "Output Channels" +msgstr "" + +msgid "Output channels are required for your studio to work" +msgstr "" + +msgid "Output Layer" +msgstr "" + +msgid "Over" +msgstr "" + +msgid "Over/Under" +msgstr "" + +msgid "Overflow horizontally" +msgstr "" + +msgid "Overlay Screen" +msgstr "" + +msgid "Package Container ID" +msgstr "" + +msgid "Package Containers" +msgstr "" + +msgid "Package Containers to use for previews" +msgstr "" + +msgid "Package Containers to use for thumbnails" +msgstr "" + +msgid "Package Manager" +msgstr "" + +msgid "Package Manager is offline" +msgstr "" + +msgid "Package Manager status" +msgstr "" + +msgid "Package Manager: Restart Package Container" +msgstr "" + +msgid "Package Manager: Restart work" +msgstr "" + +msgid "Package Status" +msgstr "" + +msgid "Packages" +msgstr "" + +msgid "Parameters" +msgstr "" + +msgid "Parent Config ID" +msgstr "" + +msgid "Parent device is missing" +msgstr "" + +msgid "Parent Devices" +msgstr "" + +msgid "part" +msgstr "" + +msgid "Part" +msgstr "" + +msgid "Part Count Down" +msgstr "" + +msgid "Part Count Up" +msgstr "" + +msgid "Part duration is 0." +msgstr "" + +msgid "Parts Duration" +msgstr "" + +msgid "Parts: {{delta}}" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password for authentication" +msgstr "" + +msgid "Peripheral Device is outdated" +msgstr "" + +msgid "Peripheral Device not found!" +msgstr "" + +msgid "Peripheral Devices" +msgstr "" + +msgid "Pick" +msgstr "" + +msgid "Pick last" +msgstr "" + +msgid "" +"Picks the first instance of an adLib per rundown, identified by uniqueness " +"Id" +msgstr "" + +msgid "Piece on selected source layers will have a duration label shown" +msgstr "" + +msgid "Piece to take is already live!" +msgstr "" + +msgid "Piece to take is not directly playable!" +msgstr "" + +msgid "Piece to take was not found!" +msgstr "" + +msgid "Pieces on this layer are sticky" +msgstr "" + +msgid "Pieces on this layer can be cleared" +msgstr "" + +msgid "Place label below panel" +msgstr "" + +msgid "Plan. Dur" +msgstr "" + +msgid "Plan. End" +msgstr "" + +msgid "Plan. Start" +msgstr "" + +msgid "Planned Duration" +msgstr "" + +msgid "Planned End" +msgstr "" + +msgid "Planned End text" +msgstr "" + +msgid "Planned Start" +msgstr "" + +msgid "Planned Start Text" +msgstr "" + +msgid "Play-out" +msgstr "" + +msgid "Playlist" +msgstr "" + +msgid "Playout Devices" +msgstr "" + +msgid "Playout devices are needed to control your studio hardware" +msgstr "" + +msgid "Playout devices which uses this package container" +msgstr "" + +msgid "Playout Gateway \"{{playoutDeviceName}}\" is now restarting." +msgstr "" + +msgid "" +"Please check the database related to the warnings above. If neccessary, you " +"can" +msgstr "" + +msgid "Please explain the problems you experienced" +msgstr "" + +msgid "Please note: This action is irreversible!" +msgstr "" + +msgid "Pool name" +msgstr "" + +msgid "Pool PlayerId" +msgstr "" + +msgid "Prepare Studio and Activate (Rehearsal)" +msgstr "" + +msgid "Preparing for broadcast" +msgstr "" + +msgid "Preparing, please wait..." +msgstr "" + +msgid "Presenter Layout" +msgstr "" + +msgid "Presenter screen" +msgstr "" + +msgid "Presenter Screen" +msgstr "" + +msgid "Presenter View Layouts" +msgstr "" + +msgid "Preserve position of segments when unsynced relative to other segments" +msgstr "" + +msgid "Previous" +msgstr "" + +msgid "Previous work status reasons" +msgstr "" + +msgid "Prioritizing Media Workflow" +msgstr "" + +msgid "Priority" +msgstr "" + +msgid "Problems" +msgstr "" + +msgid "Profile name to be used by FileFlow when exporting the clips" +msgstr "" + +msgid "Prompter" +msgstr "" + +msgid "Prompter Screen" +msgstr "" + +msgid "Properties" +msgstr "" + +msgid "Quantel FileFlow Profile name" +msgstr "" + +msgid "Quantel FileFlow URL" +msgstr "" + +msgid "Quantel gateway URL" +msgstr "" + +msgid "Quantel transformer URL" +msgstr "" + +msgid "Quantel Zone ID" +msgstr "" + +msgid "Queue AdLib from Minishelf" +msgstr "" + +msgid "Queue all adlibs" +msgstr "" + +msgid "Queue segment" +msgstr "" + +msgid "Queue this AdLib" +msgstr "" + +msgid "Queued Messages" +msgstr "" + +msgid "Queueing next Segment" +msgstr "" + +msgid "Quick Links" +msgstr "" + +msgid "QuickLoop Fallback Part Duration" +msgstr "" + +msgid "Range: Forward max" +msgstr "" + +msgid "Range: Neutral max" +msgstr "" + +msgid "Range: Neutral min" +msgstr "" + +msgid "Range: Reverse min" +msgstr "" + +msgid "Rate limit exceeded" +msgstr "" + +msgid "Re-check" +msgstr "" + +msgid "Re-sync" +msgstr "" + +msgid "Re-Sync" +msgstr "" + +msgid "Re-sync Rundown" +msgstr "" + +msgid "Re-sync rundown data with {{nrcsName}}" +msgstr "" + +msgid "Re-Sync rundown?" +msgstr "" + +msgid "Re-Syncing Rundown" +msgstr "" + +msgid "Re-Syncing Rundown Playlist" +msgstr "" + +msgid "Read marker position" +msgstr "" + +msgid "Reads the ingest (NRCS) data, and pipes it through the blueprints" +msgstr "" + +msgid "Ready" +msgstr "" + +msgid "Reconnect now" +msgstr "" + +msgid "Reconnecting to the {{platformName}}" +msgstr "" + +msgid "Refreshing debug states" +msgstr "" + +msgid "Register ID" +msgstr "" + +msgid "Rehearsal" +msgstr "" + +msgid "Rehearsal mode is already active" +msgstr "" + +msgid "Rehearsal mode is not allowed" +msgstr "" + +msgid "Rehearsal State" +msgstr "" + +msgid "Reload {{nrcsName}} Data" +msgstr "" + +msgid "Reload Baseline" +msgstr "" + +msgid "Reload NRCS Data" +msgstr "" + +msgid "Reload statuses" +msgstr "" + +msgid "Reloading Rundown Playlist Data" +msgstr "" + +msgid "Rem. Dur" +msgstr "" + +msgid "Remote" +msgstr "" + +msgid "Remote Source" +msgstr "" + +msgid "Remote Speak" +msgstr "" + +msgid "Remove" +msgstr "" + +msgid "Remove all Show Style Variants?" +msgstr "" + +msgid "Remove all trimming" +msgstr "" + +msgid "Remove in-trimming" +msgstr "" + +msgid "Remove indexes" +msgstr "" + +msgid "Remove old data" +msgstr "" + +msgid "Remove old data from database" +msgstr "" + +msgid "Remove out-trimming" +msgstr "" + +msgid "Remove rundown" +msgstr "" + +msgid "Remove Snapshot" +msgstr "" + +msgid "Remove this AB PLayers from this Route Set?" +msgstr "" + +msgid "Remove this device?" +msgstr "" + +msgid "Remove this Device?" +msgstr "" + +msgid "Remove this Exclusivity Group?" +msgstr "" + +msgid "Remove this item?" +msgstr "" + +msgid "Remove this mapping?" +msgstr "" + +msgid "Remove this Package Container Accessor?" +msgstr "" + +msgid "Remove this Package Container?" +msgstr "" + +msgid "Remove this Route from this Route Set?" +msgstr "" + +msgid "Remove this Route Set?" +msgstr "" + +msgid "Remove this Show Style Variant?" +msgstr "" + +msgid "Removing Bucket" +msgstr "" + +msgid "Removing Bucket AdLib" +msgstr "" + +msgid "Removing Rundown" +msgstr "" + +msgid "Removing Rundown Playlist" +msgstr "" + +msgid "Rename this AdLib" +msgstr "" + +msgid "Rename this Bucket" +msgstr "" + +msgid "Reording Rundowns in Playlist" +msgstr "" + +msgid "Replace" +msgstr "" + +msgid "Replace Blueprints?" +msgstr "" + +msgid "Replace rows" +msgstr "" + +msgid "Require All Additional Source Layers" +msgstr "" + +msgid "Require Piece on Source Layer" +msgstr "" + +msgid "Reset" +msgstr "" + +msgid "Reset Action" +msgstr "" + +msgid "Reset All Versions" +msgstr "" + +msgid "Reset and Activate \"On Air\"" +msgstr "" + +msgid "Reset App Credentials" +msgstr "" + +msgid "Reset Database Version" +msgstr "" + +msgid "Reset mapping to default values" +msgstr "" + +msgid "Reset Package Container to default values" +msgstr "" + +msgid "Reset row to default values" +msgstr "" + +msgid "Reset Rundown" +msgstr "" + +msgid "Reset Sort Order" +msgstr "" + +msgid "Reset source layer to default values" +msgstr "" + +msgid "Reset this item?" +msgstr "" + +msgid "Reset this mapping?" +msgstr "" + +msgid "Reset this Package Container?" +msgstr "" + +msgid "Reset to default" +msgstr "" + +msgid "Reset User Credentials" +msgstr "" + +msgid "Resetting and activating Rundown Playlist" +msgstr "" + +msgid "Resetting Playlist to default order" +msgstr "" + +msgid "Resetting Rundown Playlist" +msgstr "" + +msgid "Resource Id" +msgstr "" + +msgid "Restart" +msgstr "" + +msgid "Restart {{device}}" +msgstr "" + +msgid "Restart All Jobs" +msgstr "" + +msgid "Restart CasparCG Server" +msgstr "" + +msgid "Restart Container" +msgstr "" + +msgid "Restart Device" +msgstr "" + +msgid "Restart Playout" +msgstr "" + +msgid "Restart this Device?" +msgstr "" + +msgid "Restart this system?" +msgstr "" + +msgid "Restarting Media Workflow" +msgstr "" + +msgid "Restarting Sofie Core" +msgstr "" + +msgid "Restore" +msgstr "" + +msgid "Restore Deleted Action" +msgstr "" + +msgid "Restore from Snapshot File" +msgstr "" + +msgid "Restore from Stored Snapshots" +msgstr "" + +msgid "Restore from this Snapshot file?" +msgstr "" + +msgid "Restore Part from NRCS" +msgstr "" + +msgid "Restore Segment from NRCS" +msgstr "" + +msgid "Restore Snapshot" +msgstr "" + +msgid "Resync with NRCS" +msgstr "" + +msgid "Retry" +msgstr "" + +msgid "Return to list" +msgstr "" + +msgid "Reveal in Shelf" +msgstr "" + +msgid "Reverse speed map" +msgstr "" + +msgid "Reverse speed map (left trigger)" +msgstr "" + +msgid "Rewind all Segments" +msgstr "" + +msgid "Rewind segments to start" +msgstr "" + +msgid "Rewind Segments to start" +msgstr "" + +msgid "Right hand offset" +msgstr "" + +msgid "Role" +msgstr "" + +msgid "Route Set" +msgstr "" + +msgid "Route Set ID" +msgstr "" + +msgid "Route Set Name" +msgstr "" + +msgid "Route Sets" +msgstr "" + +msgid "Route Type" +msgstr "" + +msgid "Routed Mappings" +msgstr "" + +msgid "Routes" +msgstr "" + +msgid "Row cannot be reset as it has no default values" +msgstr "" + +msgid "Run automatic migration procedure" +msgstr "" + +msgid "Run Migrations to get set up" +msgstr "" + +msgid "Rundown" +msgstr "" + +msgid "" +"Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data " +"from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced " +"or remove the rundown from Sofie. What do you want to do?" +msgstr "" + +msgid "Rundown & Shelf" +msgstr "" + +msgid "Rundown filter" +msgstr "" + +msgid "Rundown for piece \"{{pieceLabel}}\" could not be found." +msgstr "" + +msgid "Rundown Global Piece Prepare Time" +msgstr "" + +msgid "Rundown Header Layout" +msgstr "" + +msgid "Rundown Header Layouts" +msgstr "" + +msgid "Rundown is already doing a HOLD!" +msgstr "" + +msgid "Rundown must be active!" +msgstr "" + +msgid "Rundown must be playing or have a next!" +msgstr "" + +msgid "Rundown must be playing!" +msgstr "" + +msgid "Rundown Name" +msgstr "" + +msgid "Rundown not found" +msgstr "" + +msgid "" +"Rundown Playlist is active, please deactivate before preparing it for " +"broadcast" +msgstr "" + +msgid "Rundown Playlist is active, please deactivate it before regenerating it." +msgstr "" + +msgid "Rundown Playlist names to store" +msgstr "" + +msgid "Rundown Playlist not found!" +msgstr "" + +msgid "Rundown View Layouts" +msgstr "" + +msgid "" +"RundownPlaylist is active but not in rehearsal, please deactivate it or set " +"in in rehearsal to be able to reset it." +msgstr "" + +msgid "Rundowns" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Save Changes" +msgstr "" + +msgid "Save to Bucket" +msgstr "" + +msgid "Saving AdLib to Bucket" +msgstr "" + +msgid "Saving Evaluation" +msgstr "" + +msgid "Scale" +msgstr "" + +msgid "Script is empty" +msgstr "" + +msgid "Script Source Layers" +msgstr "" + +msgid "Search..." +msgstr "" + +msgid "Seg. Budg." +msgstr "" + +msgid "segment" +msgstr "" + +msgid "Segment" +msgstr "" + +msgid "Segment Count Down" +msgstr "" + +msgid "Segment Count Up" +msgstr "" + +msgid "Segment countdown requires source layer" +msgstr "" + +msgid "Segment no longer exists in {{nrcs}}" +msgstr "" + +msgid "Segment was hidden in {{nrcs}}" +msgstr "" + +msgid "Segments: {{delta}}" +msgstr "" + +msgid "" +"Select a presenter layout. Leave as default to use the first available " +"layout." +msgstr "" + +msgid "Select Action" +msgstr "" + +msgid "Select Compatible Show Styles" +msgstr "" + +msgid "Select image" +msgstr "" + +msgid "" +"Select one or more control modes. Leave all unchecked for default (mouse + " +"keyboard)." +msgstr "" + +msgid "" +"Select source layers to display. Leave all unchecked to show all " +"camera-related layers." +msgstr "" + +msgid "Select visible Output Groups" +msgstr "" + +msgid "Select visible Source Layers" +msgstr "" + +msgid "Select which playout devices are using this package container" +msgstr "" + +msgid "Send message" +msgstr "" + +msgid "Send message and Deactivate Rundown" +msgstr "" + +msgid "Sent Messages" +msgstr "" + +msgid "Server" +msgstr "" + +msgid "Server {{id}}" +msgstr "" + +msgid "Server ID" +msgstr "" + +msgid "" +"Server ID. For sources, this should generally be omitted (or set to 0) so " +"clip-searches are zone-wide. If set, clip-searches are limited to that " +"server." +msgstr "" + +msgid "Set" +msgstr "" + +msgid "Set as QuickLoop End" +msgstr "" + +msgid "Set as QuickLoop Start" +msgstr "" + +msgid "Set In & Out points" +msgstr "" + +msgid "Set part as Next" +msgstr "" + +msgid "Set segment as Next" +msgstr "" + +msgid "Setting as QuickLoop End" +msgstr "" + +msgid "Setting as QuickLoop Start" +msgstr "" + +msgid "Setting Next" +msgstr "" + +msgid "Setting Next Segment" +msgstr "" + +msgid "Settings" +msgstr "" + +msgid "Shelf" +msgstr "" + +msgid "Shelf Layout" +msgstr "" + +msgid "Shelf layout uploaded successfully." +msgstr "" + +msgid "Shelf Layouts" +msgstr "" + +msgid "Shortcuts" +msgstr "" + +msgid "Show \"Remove snapshots\"-buttons" +msgstr "" + +msgid "Show All" +msgstr "" + +msgid "Show Breaks as Segments" +msgstr "" + +msgid "Show config changes" +msgstr "" + +msgid "Show End" +msgstr "" + +msgid "Show entire On Air Segment" +msgstr "" + +msgid "Show Hotkeys" +msgstr "" + +msgid "Show Inspector" +msgstr "" + +msgid "Show issue" +msgstr "" + +msgid "Show next break timing" +msgstr "" + +msgid "Show panel as a timeline" +msgstr "" + +msgid "Show part title" +msgstr "" + +msgid "Show Piece Icon Color" +msgstr "" + +msgid "Show Rundown Name" +msgstr "" + +msgid "Show segment name" +msgstr "" + +msgid "Show Style" +msgstr "" + +msgid "Show Style Base Name" +msgstr "" + +msgid "Show style not set" +msgstr "" + +msgid "Show Style Variant" +msgstr "" + +msgid "Show Style Variants" +msgstr "" + +msgid "Show Styles" +msgstr "" + +msgid "Show thumbnails next to list items" +msgstr "" + +msgid "ShowStyleBase not found!" +msgstr "" + +msgid "Shuttle Keyboard (Contour ShuttleXpress / X-keys)" +msgstr "" + +msgid "Shuttle WebHID (Contour ShuttleXpress via browser)" +msgstr "" + +msgid "Skip Fix Up Step" +msgstr "" + +msgid "Slack Webhook URLs" +msgstr "" + +msgid "Smooth scrolling" +msgstr "" + +msgid "Snapshot remove failed: {{errorMessage}}" +msgstr "" + +msgid "Snapshot restore failed: {{errorMessage}}" +msgstr "" + +msgid "Snapshot restored!" +msgstr "" + +msgid "Sofie" +msgstr "" + +msgid "Sofie Automation" +msgstr "" + +msgid "Sofie Automation Server" +msgstr "" + +msgid "Sofie Automation Server Core" +msgstr "" + +msgid "Sofie Automation Server Core will restart in {{time}}s..." +msgstr "" + +msgid "Sofie Automation Server Core: {{name}}" +msgstr "" + +msgid "Sofie logo to be displayed in the header. Requires a page refresh." +msgstr "" + +msgid "some invalid reason" +msgstr "" + +msgid "some message" +msgstr "" + +msgid "some reason" +msgstr "" + +msgid "something changed" +msgstr "" + +msgid "" +"Something went wrong when creating the snapshot. Please contact the system " +"administrator if the problem persists." +msgstr "" + +msgid "Something went wrong, and it affected the output" +msgstr "" + +msgid "Something went wrong, but it didn't affect the output" +msgstr "" + +msgid "" +"Something went wrong, please contact the system administrator if the " +"problem persists." +msgstr "" + +msgid "Source Abbreviation" +msgstr "" + +msgid "Source Layer" +msgstr "" + +msgid "Source layer cannot be reset as it has no default values" +msgstr "" + +msgid "Source Layer Type" +msgstr "" + +msgid "Source Layer Types" +msgstr "" + +msgid "Source Layers" +msgstr "" + +msgid "Source layers containing script" +msgstr "" + +msgid "Source Name" +msgstr "" + +msgid "Source Type" +msgstr "" + +msgid "Source/Output Layers" +msgstr "" + +msgid "Sources" +msgstr "" + +msgid "Space separated list of style class names to use when displaying the action" +msgstr "" + +msgid "Specify additional layers where at least one layer must have an active piece" +msgstr "" + +msgid "Speed control" +msgstr "" + +msgid "Speed map" +msgstr "" + +msgid "Speed map (forward, right trigger)" +msgstr "" + +msgid "Split Screen" +msgstr "" + +msgid "Splits" +msgstr "" + +msgid "Standalone Shelf" +msgstr "" + +msgid "Start Here!" +msgstr "" + +msgid "Start In" +msgstr "" + +msgid "Start this AdLib" +msgstr "" + +msgid "Start time is close" +msgstr "" + +msgid "" +"Start with giving this browser configuration permissions by adding this to " +"the URL: " +msgstr "" + +msgid "Started" +msgstr "" + +msgid "Starting AdLib" +msgstr "" + +msgid "Starting Bucket AdLib" +msgstr "" + +msgid "Starting Global AdLib" +msgstr "" + +msgid "Starting Sticky Piece" +msgstr "" + +msgid "State" +msgstr "" + +msgid "State \"{{state}}\"" +msgstr "" + +msgid "Statistics" +msgstr "" + +msgid "Status" +msgstr "" + +msgid "Status Messages:" +msgstr "" + +msgid "Sticky Piece" +msgstr "" + +msgid "Store Snapshot" +msgstr "" + +msgid "Studio" +msgstr "" + +msgid "Studio Baseline needs update: " +msgstr "" + +msgid "Studio Labels" +msgstr "" + +msgid "Studio Name" +msgstr "" + +msgid "Studio not found!" +msgstr "" + +msgid "Studio Screen Graphics" +msgstr "" + +msgid "Studio Settings" +msgstr "" + +msgid "Studio Snapshot" +msgstr "" + +msgid "Studios" +msgstr "" + +msgid "Style class names" +msgstr "" + +msgid "Subtract" +msgstr "" + +msgid "Successfully restored snapshot" +msgstr "" + +msgid "Successfully stored snapshot" +msgstr "" + +msgid "Support Panel" +msgstr "" + +msgid "Supported Audio Formats" +msgstr "" + +msgid "Supported Media Formats" +msgstr "" + +msgid "Switch Route Set" +msgstr "" + +msgid "Switch Segment View Mode" +msgstr "" + +msgid "Switch to List View" +msgstr "" + +msgid "Switch to Storyboard View" +msgstr "" + +msgid "Switch to Timeline View" +msgstr "" + +msgid "Switchboard" +msgstr "" + +msgid "Switchboard Panel" +msgstr "" + +msgid "Switching operating mode to {{mode}}" +msgstr "" + +msgid "Switching routing" +msgstr "" + +msgid "System" +msgstr "" + +msgid "System has issues which need to be resolved" +msgstr "" + +msgid "System must have exactly one studio" +msgstr "" + +msgid "System Status" +msgstr "" + +msgid "System-wide" +msgstr "" + +msgid "System-wide Notification Message" +msgstr "" + +msgid "Table is not allowed to have `properties` defined" +msgstr "" + +msgid "Table is only allowed the wildcard `patternProperties`" +msgstr "" + +msgid "Tables are not supported here" +msgstr "" + +msgid "Tag" +msgstr "" + +msgid "Tags must contain" +msgstr "" + +msgid "Take" +msgstr "" + +msgid "Take a Full System Snapshot" +msgstr "" + +msgid "Take a Snapshot" +msgstr "" + +msgid "Take a Snapshot for studio \"{{studioName}}\" only" +msgstr "" + +msgid "Take and Download Memory Heap Snapshot" +msgstr "" + +msgid "Take System Snapshot" +msgstr "" + +msgid "Take System Snapshot failed: {{errorMessage}}" +msgstr "" + +msgid "Taking Piece" +msgstr "" + +msgid "Technical reason: {{reason}}" +msgstr "" + +msgid "test" +msgstr "" + +msgid "Test ERROR message" +msgstr "" + +msgid "Test Info message" +msgstr "" + +msgid "test partInstance" +msgstr "" + +msgid "test pieceInstance" +msgstr "" + +msgid "test playlist" +msgstr "" + +msgid "test rundown" +msgstr "" + +msgid "Test test" +msgstr "" + +msgid "Test Tools" +msgstr "" + +msgid "test2" +msgstr "" + +msgid "Text" +msgstr "" + +msgid "Text to show above countdown to end of show" +msgstr "" + +msgid "Text to show above countdown to next break" +msgstr "" + +msgid "Text to show above show end time" +msgstr "" + +msgid "Text to show above show start time" +msgstr "" + +msgid "" +"The config UI is now driven by manifests fed by the device. This device " +"needs updating to provide the configManifest to be configurable" +msgstr "" + +msgid "The following parts no longer exist in {{nrcs}}: {{partNames}}" +msgstr "" + +msgid "The migration can be completed automatically." +msgstr "" + +msgid "" +"The migration consists of several phases, you will get more options after " +"you've this migration" +msgstr "" + +msgid "The migration was completed successfully!" +msgstr "" + +msgid "The old data was removed." +msgstr "" + +msgid "" +"The planned end time has passed, are you sure you want to activate this " +"Rundown?" +msgstr "" + +msgid "The progress of all steps" +msgstr "" + +msgid "The progress of steps required for playout" +msgstr "" + +msgid "" +"The rundown \"{{rundownName}}\" is not published or activated in " +"{{nrcsName}}! No data updates will currently come through." +msgstr "" + +msgid "The rundown can not be reset while it is active" +msgstr "" + +msgid "" +"The Rundown was attempted to be moved out of the Playlist when it was on " +"Air. Move it back and try again later." +msgstr "" + +msgid "" +"The rundown you are trying to execute a take on is inactive, would you like " +"to activate this rundown?" +msgstr "" + +msgid "" +"The rundown: \"{{rundownName}}\" will need to be deactivated in order to " +"activate this one.\n" +"\n" +"Are you sure you want to activate this one anyway?" +msgstr "" + +msgid "" +"The segment duration in the segment header always displays the planned " +"duration instead of acting as a counter" +msgstr "" + +msgid "The selected part cannot be played" +msgstr "" + +msgid "The selected part does not exist" +msgstr "" + +msgid "" +"The system configuration has been changed since importing this rundown. It " +"might not run correctly" +msgstr "" + +msgid "The type of device to use for the output" +msgstr "" + +msgid "The type of mapping to use" +msgstr "" + +msgid "The way this Route Set should behave towards the user" +msgstr "" + +msgid "Then, run the migrations script:" +msgstr "" + +msgid "There are no AB Playout devices set up yet" +msgstr "" + +msgid "There are no Accessors set up." +msgstr "" + +msgid "There are no active rundowns." +msgstr "" + +msgid "There are no exclusivity groups set up." +msgstr "" + +msgid "There are no filters set up yet" +msgstr "" + +msgid "" +"There are no Playout Gateways connected and attached to this studio. Please " +"contact the system administrator to start the Playout Gateway." +msgstr "" + +msgid "There are no Route Sets set up." +msgstr "" + +msgid "There are no routes set up yet" +msgstr "" + +msgid "There are no rundowns ingested into Sofie." +msgstr "" + +msgid "There are no sub-devices for this gateway" +msgstr "" + +msgid "There is an unknown problem with the part." +msgstr "" + +msgid "There is an unspecified problem with the source." +msgstr "" + +msgid "There is no rundown active in this studio." +msgstr "" + +msgid "" +"There was an error when troubleshooting the device: \"{{deviceName}}\": " +"{{errorMessage}}" +msgstr "" + +msgid "There was an error: {{error}}" +msgstr "" + +msgid "This action has an invalid combination of filters" +msgstr "" + +msgid "This affects how much is logged to the console on the server" +msgstr "" + +msgid "This blueprint has not provided a valid config schema" +msgstr "" + +msgid "This Blueprint is not being used by any Show Style" +msgstr "" + +msgid "This Blueprint is not compatible with any Studio" +msgstr "" + +msgid "This clip ends with black frames after {{seconds}} seconds" +msgstr "" + +msgid "This clip ends with freeze frames after {{seconds}} seconds" +msgstr "" + +msgid "This could leave the configuration in a broken state" +msgstr "" + +msgid "This enables or disables buckets in the UI - enabled is the default behavior" +msgstr "" + +msgid "" +"This enables or disables the evaluationform in the UI - enabled is the " +"default behavior" +msgstr "" + +msgid "This feature enables the use of the Properties Panel and the Edit Mode" +msgstr "" + +msgid "This has only been tested for the iNews gateway" +msgstr "" + +msgid "This is not in it's normal setting" +msgstr "" + +msgid "" +"This migration consists of {{stepCount}} steps ({{ignoredStepCount}} steps " +"are ignored)." +msgstr "" + +msgid "This must be assigned to a device to be able to edit the settings" +msgstr "" + +msgid "This name will be shown in the title bar of the window" +msgstr "" + +msgid "This playlist is empty" +msgstr "" + +msgid "" +"This requires the blueprints to implement the " +"`generateAdlibTestingIngestRundown` method" +msgstr "" + +msgid "This rundown has been unpublished from Sofie." +msgstr "" + +msgid "This rundown is currently active" +msgstr "" + +msgid "This rundown is now active. Are you sure you want to exit this screen?" +msgstr "" + +msgid "This rundown will loop indefinitely" +msgstr "" + +msgid "This Show Style is not compatible with any Studio" +msgstr "" + +msgid "This step is required for playout" +msgstr "" + +msgid "This studio doesn't exist." +msgstr "" + +msgid "This will remove {{indexCount}} old indexes, do you want to continue?" +msgstr "" + +msgid "Time from platform user event to Action received by Core" +msgstr "" + +msgid "Time since planned end" +msgstr "" + +msgid "Time since rehearsal end" +msgstr "" + +msgid "Time to planned end" +msgstr "" + +msgid "Time to planned start" +msgstr "" + +msgid "Time to rehearsal end" +msgstr "" + +msgid "Timeline" +msgstr "" + +msgid "Timeline Datastore" +msgstr "" + +msgid "Times" +msgstr "" + +msgid "Timestamp" +msgstr "" + +msgid "To inspect the memory heap snapshot, use Chrome DevTools" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "Toggle" +msgstr "" + +msgid "Toggle AdLibs on single mouse click" +msgstr "" + +msgid "Toggle Shelf" +msgstr "" + +msgid "Toggle Support Panel" +msgstr "" + +msgid "Toggled Label" +msgstr "" + +msgid "Tomorrow" +msgstr "" + +msgid "Tools" +msgstr "" + +msgid "Top" +msgstr "" + +msgid "Transition" +msgstr "" + +msgid "Treat as Main content" +msgstr "" + +msgid "Trigger dead zone" +msgstr "" + +msgid "Trigger Mode" +msgstr "" + +msgid "Triggered Actions failed to upload: {{errorMessage}}" +msgstr "" + +msgid "Triggered Actions uploaded successfully." +msgstr "" + +msgid "Trim \"{{name}}\"" +msgstr "" + +msgid "Trimmed successfully." +msgstr "" + +msgid "Trimming this clip has failed due to an error: {{error}}." +msgstr "" + +msgid "" +"Trimming this clip has timed out. It's possible that the story is currently " +"locked for writing in {{nrcsName}} and will eventually be updated. Make " +"sure that the story is not being edited by other users." +msgstr "" + +msgid "" +"Trimming this clip is taking longer than expected. It's possible that the " +"story is locked for writing in {{nrcsName}}." +msgstr "" + +msgid "Troubleshoot" +msgstr "" + +msgid "TSR" +msgstr "" + +msgid "Type" +msgstr "" + +msgid "Unable to check the system configuration for changes" +msgstr "" + +msgid "Unable to upgrade" +msgstr "" + +msgid "Unassign" +msgstr "" + +msgid "Unconfigured" +msgstr "" + +msgid "Under" +msgstr "" + +msgid "Undo" +msgstr "" + +msgid "Undo Disable the next element" +msgstr "" + +msgid "Undo Hold" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Unknown action" +msgstr "" + +msgid "Unknown error" +msgstr "" + +msgid "Unknown Layer" +msgstr "" + +msgid "Unknown Package \"{{packageId}}\"" +msgstr "" + +msgid "Unnamed blueprint" +msgstr "" + +msgid "Unnamed Show Style" +msgstr "" + +msgid "Unnamed Studio" +msgstr "" + +msgid "Unnamed variant" +msgstr "" + +msgid "Unsupported array type \"{{ type }}\"" +msgstr "" + +msgid "Unsupported field type \"{{ type }}\"" +msgstr "" + +msgid "Unsyncing Rundown" +msgstr "" + +msgid "Until" +msgstr "" + +msgid "Until end of rundown" +msgstr "" + +msgid "Until End of Rundown" +msgstr "" + +msgid "Until end of segment" +msgstr "" + +msgid "Until End of Segment" +msgstr "" + +msgid "Until end of showstyle" +msgstr "" + +msgid "Until End of Showstyle" +msgstr "" + +msgid "Until next rundown" +msgstr "" + +msgid "Until Next Rundown" +msgstr "" + +msgid "Until next segment" +msgstr "" + +msgid "Until Next Segment" +msgstr "" + +msgid "Until next take" +msgstr "" + +msgid "Until Next Take" +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update Blueprints?" +msgstr "" + +msgid "Updated" +msgstr "" + +msgid "Upgrade config for {{name}}" +msgstr "" + +msgid "Upgrade Database" +msgstr "" + +msgid "Upgrade required" +msgstr "" + +msgid "Upgrade Status" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "Upload a new blueprint" +msgstr "" + +msgid "Upload a snapshot file" +msgstr "" + +msgid "" +"Upload a snapshot file (restores additional info not directly related to a " +"Playlist / Rundown, such as Packages, PackageWorkStatuses etc" +msgstr "" + +msgid "Upload Blueprints" +msgstr "" + +msgid "Upload Layout?" +msgstr "" + +msgid "Upload Snapshot" +msgstr "" + +msgid "Upload Snapshot (for debugging)" +msgstr "" + +msgid "Upload stored Action Triggers" +msgstr "" + +msgid "URL" +msgstr "" + +msgid "URL to the Quantel FileFlow Manager" +msgstr "" + +msgid "URL to the Quantel Gateway" +msgstr "" + +msgid "URL to the Quantel HTTP transformer" +msgstr "" + +msgid "URLs to the ISAs, in order of importance (comma separated)" +msgstr "" + +msgid "Use {{nrcsName}} order" +msgstr "" + +msgid "Use as default" +msgstr "" + +msgid "Use color of primary piece as background of panel" +msgstr "" + +msgid "Use Trigger Mode" +msgstr "" + +msgid "User Activity Log" +msgstr "" + +msgid "User ID" +msgstr "" + +msgid "User Log" +msgstr "" + +msgid "User Name" +msgstr "" + +msgid "Username for authentication" +msgstr "" + +msgid "Validate and Apply Config" +msgstr "" + +msgid "Validation failed!" +msgstr "" + +msgid "Value" +msgstr "" + +msgid "Value between 0 and 1" +msgstr "" + +msgid "Variants" +msgstr "" + +msgid "version" +msgstr "" + +msgid "Version" +msgstr "" + +msgid "Version for {{name}}: From {{fromVersion}} to {{toVersion}}" +msgstr "" + +msgid "View" +msgstr "" + +msgid "View Layout" +msgstr "" + +msgid "Waiting for action: {{actionName}}..." +msgstr "" + +msgid "Waiting for gateway to generate URL..." +msgstr "" + +msgid "Warning" +msgstr "" + +msgid "Warnings" +msgstr "" + +msgid "Warnings During Migration" +msgstr "" + +msgid "What type of bank" +msgstr "" + +msgid "When" +msgstr "" + +msgid "When disabled, any HOLD operations will be silently ignored" +msgstr "" + +msgid "" +"When enabled, double clicking on certain pieces in the GUI will play them " +"as adlibs" +msgstr "" + +msgid "" +"When enabled, this will override the piece content statuses to have no " +"errors or warnings and display a mock preview. This should only be used for " +"development!" +msgstr "" + +msgid "When set, resources are considered immutable, ie they will not change" +msgstr "" + +msgid "Whether to show countdown to next break" +msgstr "" + +msgid "" +"While there are still breaks coming up in the show, hide the Expected End " +"timers" +msgstr "" + +msgid "Width" +msgstr "" + +msgid "Work description" +msgstr "" + +msgid "Work status" +msgstr "" + +msgid "Work status reason" +msgstr "" + +msgid "Work-in-progress" +msgstr "" + +msgid "Worker" +msgstr "" + +msgid "WorkForce" +msgstr "" + +msgid "X" +msgstr "" + +msgid "Xbox Controller" +msgstr "" + +msgid "Y" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "Yes, Take and Download Memory Heap Snapshot" +msgstr "" + +msgid "Yesterday" +msgstr "" + +msgid "" +"You are in rehearsal mode, the broadcast starts in less than 1 minute. Do " +"you want to go into On-Air mode?" +msgstr "" + +msgid "You need to run migrations to set the system up for operation." +msgstr "" + +msgid "Your machine is offline and cannot connect to the {{platformName}}." +msgstr "" + +msgid "Your name" +msgstr "" + +msgid "Zone ID" +msgstr "" + +msgid "Zoom In" +msgstr "" + +msgid "Zoom Out" +msgstr "" + +msgctxt "one" +msgid "{{count}} items" +msgstr "" + +msgctxt "one" +msgid "There are {{count}} documents that can be removed, do you want to continue?" +msgstr "" + +msgctxt "one" +msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" +msgstr "" + +msgctxt "other" +msgid "{{count}} items" +msgstr "" + +msgctxt "other" +msgid "There are {{count}} documents that can be removed, do you want to continue?" +msgstr "" + +msgctxt "other" +msgid "This layer is now rerouted by an active Route Set: {{routeSets}}" +msgstr "" diff --git a/meteor/jest.config.js b/meteor/jest.config.js index 34c1398c620..35279a2c967 100644 --- a/meteor/jest.config.js +++ b/meteor/jest.config.js @@ -3,6 +3,8 @@ const path = require('path') const commonConfig = { modulePaths: ['/node_modules/'], moduleNameMapper: { + '^@sofie-automation/shared-lib/dist/(.+)\\.js$': '/../packages/shared-lib/src/$1', + '^@sofie-automation/shared-lib/dist/(.+)$': '/../packages/shared-lib/src/$1', // Ensure libraries that would match the extension rule are still resolved 'bignumber.js': 'bignumber.js', // Drop file extensions in imports diff --git a/meteor/package.json b/meteor/package.json index 32c69ae4ee4..7288e14aff2 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -25,7 +25,7 @@ "lint:raw": "eslint", "lintfix": "run lint --fix", "quickformat": "prettier \"__mocks__/**\" --write ; prettier \"lib/**\" --write ; prettier \"server/**\" --write ; prettier \"client/**\" --write ; prettier \"*.json\" --write ; prettier \"*.js\" --write ; prettier \"*.md\" --write", - "i18n-extract-pot": "node ./scripts/extract-i18next-pot.mjs -f \"{./lib/**/*.+(ts|tsx),./server/**/*.+(ts|tsx),../packages/job-worker/src/**/*.+(ts|tsx),../packages/corelib/src/**/*.+(ts|tsx),../packages/webui/src/**/*.+(ts|tsx)}\" -o i18n/template.pot", + "i18n-extract-po": "node ./scripts/extract-i18next-po.mjs", "i18n-compile-json": "node ./scripts/i18n-compile-json.mjs", "visualize": "meteor --production --extra-packages bundle-visualizer", "release": "commit-and-tag-version --commit-all", @@ -35,72 +35,75 @@ "validate:dev-dependencies": "yarn npm audit --environment development --severity moderate" }, "dependencies": { - "@babel/runtime": "^7.28.6", + "@babel/runtime": "^7.29.2", "@koa/cors": "^5.0.0", - "@koa/router": "^15.3.0", + "@koa/router": "^15.4.0", "@mos-connection/helper": "^5.0.0-alpha.0", - "@slack/webhook": "^7.0.6", + "@slack/webhook": "^7.0.9", "@sofie-automation/blueprints-integration": "portal:../packages/blueprints-integration", "@sofie-automation/corelib": "portal:../packages/corelib", "@sofie-automation/job-worker": "portal:../packages/job-worker", "@sofie-automation/meteor-lib": "portal:../packages/meteor-lib", "@sofie-automation/shared-lib": "portal:../packages/shared-lib", + "@swc/core": "^1.15.41", + "@swc/helpers": "0.5.17", "bcrypt": "^6.0.0", - "body-parser": "^1.20.4", "deep-extend": "0.6.0", "deepmerge": "^4.3.1", "elastic-apm-node": "^4.15.0", - "i18next": "^21.10.0", + "i18next": "^26.0.8", "indexof": "0.0.1", - "koa": "^3.1.1", + "koa": "^3.2.0", "koa-bodyparser": "^4.4.1", "koa-mount": "^4.2.0", "koa-static": "^5.0.0", - "meteor-node-stubs": "^1.2.25", + "meteor-node-stubs": "^1.2.27", "moment": "^2.30.1", "nanoid": "^3.3.11", "ntp-client": "^0.5.3", "object-path": "^0.11.8", "p-lazy": "^3.1.0", - "semver": "^7.7.3", + "semver": "^7.7.4", "superfly-timeline": "9.2.0", - "threadedclass": "^1.3.0", - "timecode": "0.0.4", + "threadedclass": "^1.4.0", "type-fest": "^4.41.0", - "underscore": "^1.13.7", + "underscore": "^1.13.8", "winston": "^3.19.0" }, "devDependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@meteorjs/rspack": "2.0.1", + "@rsdoctor/rspack-plugin": "1.5.7", + "@rspack/cli": "1.7.1", + "@rspack/core": "1.7.1", "@shopify/jest-koa-mocks": "^5.3.1", - "@sofie-automation/code-standard-preset": "^3.0.0", - "@types/body-parser": "^1.19.6", + "@sofie-automation/code-standard-preset": "^3.2.2", "@types/deep-extend": "^0.6.2", "@types/jest": "^30.0.0", - "@types/koa": "^3.0.1", + "@types/koa": "^3.0.2", "@types/koa-bodyparser": "^4.3.13", "@types/koa-mount": "^4.0.5", "@types/koa-static": "^4.0.4", "@types/koa__cors": "^5.0.1", - "@types/node": "^22.19.8", + "@types/node": "^22.19.17", "@types/semver": "^7.7.1", "@types/underscore": "^1.13.0", - "babel-jest": "^30.2.0", - "commit-and-tag-version": "^12.6.1", + "babel-jest": "^30.3.0", + "commit-and-tag-version": "^12.7.1", "ejson": "^2.2.3", - "eslint": "^9.39.2", + "eslint": "^9.39.4", "fast-clone": "^1.5.13", - "glob": "^13.0.1", - "i18next-conv": "^10.2.0", - "i18next-scanner": "^4.6.0", - "jest": "^30.2.0", - "jest-util": "^30.2.0", + "glob": "^13.0.6", + "i18next-cli": "^1.56.7", + "i18next-conv": "^16.0.0", + "jest": "^30.3.0", + "jest-util": "^30.3.0", "legally": "^3.5.10", - "open-cli": "^8.0.0", - "prettier": "^3.8.1", - "ts-jest": "^29.4.6", - "typescript": "~5.7.3", + "open-cli": "^9.0.0", + "prettier": "^3.8.3", + "ts-jest": "^29.4.9", + "typescript": "~5.9.3", "yargs": "^17.7.2" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", @@ -127,7 +130,8 @@ "@sofie-automation/blueprints-integration": "portal:../packages/blueprints-integration", "@sofie-automation/corelib": "portal:../packages/corelib", "@sofie-automation/job-worker": "portal:../packages/job-worker", - "@sofie-automation/shared-lib": "portal:../packages/shared-lib" + "@sofie-automation/shared-lib": "portal:../packages/shared-lib", + "@meteorjs/rspack": "patch:@meteorjs/rspack@npm%3A2.0.1#~/.yarn/patches/@meteorjs-rspack-npm-2.0.1-d001eb481c.patch" }, - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/meteor/rspack.config.cjs b/meteor/rspack.config.cjs new file mode 100644 index 00000000000..3c64a7a4933 --- /dev/null +++ b/meteor/rspack.config.cjs @@ -0,0 +1,6 @@ +const { defineConfig } = require('@meteorjs/rspack') + +module.exports = defineConfig((Meteor) => ({ + // Exclude native modules from the bundle (use Meteor runtime) + ...(Meteor.isServer ? Meteor.compileWithMeteor(['threadedclass']) : {}), +})) diff --git a/meteor/scripts/extract-i18next-po.mjs b/meteor/scripts/extract-i18next-po.mjs new file mode 100644 index 00000000000..06ad2e40f19 --- /dev/null +++ b/meteor/scripts/extract-i18next-po.mjs @@ -0,0 +1,4 @@ +#!/usr/bin/env node +import { extractTranslations } from './translation/extract.mjs' + +extractTranslations().catch(console.error) diff --git a/meteor/scripts/extract-i18next-pot.mjs b/meteor/scripts/extract-i18next-pot.mjs deleted file mode 100644 index a9d5f795e17..00000000000 --- a/meteor/scripts/extract-i18next-pot.mjs +++ /dev/null @@ -1,119 +0,0 @@ -/** - * The following code is based on i18next-extract-gettext, taken under license - * from an abandoned repository: - * https://github.com/queicherius/i18next-extract-gettext/ - * - * - * The MIT License (MIT) - * - * Copyright (c) 2016 David Reeß - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import fs from 'fs' -import yargs from 'yargs' -import { Parser } from 'i18next-scanner' -import converter from 'i18next-conv' -import { glob } from 'glob' - -const args = yargs(process.argv) - .option('files', { - description: 'files to process, specified as a glob pattern', - alias: 'f', - type: 'string', - required: true, - }) - .option('output', { - description: 'the output template file name', - alias: 'o', - type: 'string', - required: true, - }) - .option('key-separator', { - description: 'key separator to use within the POT file', - type: 'string', - }) - .option('ns-separator', { - description: 'name space separator to use within the POT file', - type: 'string', - }) - .option('plural-separator', { - description: 'plural separator to use within the POT file', - type: 'string', - }) - .option('context-separator', { - description: 'context separator to use within the POT file', - type: 'string', - }) - .help() - .alias('help', 'h').argv - -const parserOptions = { - // Include react helpers into parsing - attr: { - list: ['data-i18n', 'i18nKey'], - }, - func: { - list: ['i18next.t', 'i18n.t', 't', 'generateTranslation'], - }, - // Make sure common separators don't break the string - keySeparator: args.keySeparator || '°°°°°°.°°°°°°', - nsSeparator: args.nsSeparator || '°°°°°°:°°°°°°', - pluralSeparator: args.pluralSeparator || '°°°°°°_°°°°°°', - contextSeparator: args.contextSeparator || '°°°°°°_°°°°°°', - // Interpolate correctly - interpolation: { - prefix: '{{', - suffix: '}}', - }, - acorn: { - ecmaVersion: 11, - sourceType: 'module', - }, -} - -const parser = new Parser(parserOptions) - -const fileGlob = args.files -const outputFile = args.output - -console.log('Extracting translatable strings...') -console.log('This process may print out some error messages, but the translation template should work fine.') -console.log('──────\n') - -const files = await glob(fileGlob) - -// console.debug('Loading content of ' + files.length + ' files') - -// console.debug('Parsing translation keys out of content') -files.map(function (file) { - const content = fs.readFileSync(file, 'utf-8') - parser.parseFuncFromString(content, parserOptions) - parser.parseAttrFromString(content, parserOptions) -}) - -const json = parser.get().en.translation - -// console.debug('Converting ' + Object.keys(json).length + ' translation keys into gettext') -const data = await converter.i18nextToPot('en', JSON.stringify(json), { quiet: true }) -// console.debug('Writing into output file') -fs.writeFileSync(outputFile, data, 'utf-8') -console.log('\n──────') -console.log(`✅ Successfully written ${Object.keys(json).length} strings to template "${outputFile}".`) diff --git a/meteor/scripts/i18n-compile-json.mjs b/meteor/scripts/i18n-compile-json.mjs index 464922ede8b..4b884313147 100644 --- a/meteor/scripts/i18n-compile-json.mjs +++ b/meteor/scripts/i18n-compile-json.mjs @@ -1,5 +1,6 @@ -import { glob } from 'glob' -import { spawn } from 'child_process' +import { writeFile, mkdir } from 'node:fs/promises' +import { join } from 'node:path' +import { getTranslations } from './translation/bundle.mjs' /************************************************* @@ -8,29 +9,19 @@ and compiles the json-files (used in production). **************************************************/ -const errors = [] -const failedLanguages = [] -// List all po-files: -const poFiles = await glob('./i18n/*.po') - -const languages = [] -for (const poFile of poFiles) { - const mLanguage = poFile.match(/\/(\w+)\.po/) - if (mLanguage) languages.push(mLanguage[1]) -} - -console.log(`🔍 Found languages: ${languages.join(', ')}`) +const translations = await getTranslations('i18n', 'translations') -for (const lng of languages) { +const errors = [] +for (const { language, data } of translations) { try { - console.log('\n') - await runCmd( - `i18next-conv -l ${lng} -s i18n/${lng}.po -t ../packages/webui/public/locales/${lng}/translations.json --skipUntranslated` - ) + const outDir = join('..', 'packages', 'webui', 'public', 'locales', language) + await mkdir(outDir, { recursive: true }) + const outPath = join(outDir, 'translations.json') + await writeFile(outPath, JSON.stringify(data, null, '\t') + '\n', 'utf-8') + console.log(`✅ Written ${outPath}`) } catch (e) { - console.error(`💣 Failed: ${lng}`) - errors.push(`${lng}: ${e}`) - failedLanguages.push(lng) + console.error(`💣 Failed: ${language}: ${e}`) + errors.push(`${language}: ${e}`) } } @@ -38,28 +29,8 @@ if (errors.length) { for (const error of errors) { console.error(error) } - console.log(`\n\n😓 Failed to compile: ${failedLanguages.join(', ')}`) + console.log(`\n\n😓 Failed to compile: ${errors.map((e) => e.split(':')[0]).join(', ')}`) process.exit(1) } -console.log(`\n\n🥳 Succesfully compiled all translations: ${languages.join(', ')}`) - -function runCmd(cmd) { - console.log(cmd) - return new Promise((resolve, reject) => { - const child = spawn(cmd, { - shell: true, - windowsHide: true, - }) - child.stdout.on('data', (data) => { - console.log(`${data}`.trim()) - }) - child.stderr.on('data', (data) => { - console.error(`${data}`.trim()) - }) - child.on('close', (code) => { - if (code === 0) resolve() - else reject(new Error(`child process exited with code ${code}`)) - }) - }) -} +console.log(`\n\n🥳 successfully compiled all translations: ${translations.map((t) => t.language).join(', ')}`) diff --git a/meteor/scripts/translation/bundle.mjs b/meteor/scripts/translation/bundle.mjs new file mode 100644 index 00000000000..fa8948065b7 --- /dev/null +++ b/meteor/scripts/translation/bundle.mjs @@ -0,0 +1,59 @@ +/** + * This script will read and bundle the translations in the project's .po files. + */ +/* eslint-disable */ +import { readdir, readFile } from 'node:fs/promises' +import { join, basename } from 'node:path' +import { gettextToI18next } from 'i18next-conv' + +import { conversionOptions } from './config.mjs' + +const reverseHack = !!process.env.GENERATE_REVERSE_ENGLISH + +async function processPoFile(filePath, namespace) { + const start = Date.now() + // filePath is like i18n/nb.po — language is the filename without extension + const language = basename(filePath, '.po') + + const poFile = await readFile(filePath, 'utf-8') + + const converted = await gettextToI18next(language, poFile, { + ...conversionOptions, + language, + skipUntranslated: !reverseHack || language !== 'en', + ns: namespace, + }) + + const data = JSON.parse(converted) + + console.info( + `Processed ${namespace} ${language} (${Object.keys(data).length} translated keys) (${Date.now() - start} ms)` + ) + + if (reverseHack && language === 'en') { + for (const key of Object.keys(data)) { + data[key] = key.split('').reverse().join('') + } + } + + return { language, data } +} + +export async function getTranslations(i18nDir, namespace) { + console.info('Bundling translations...') + + let entries + try { + entries = await readdir(i18nDir) + } catch { + throw new Error(`Failed to read directory: ${i18nDir}`) + } + + const poFiles = entries.filter((f) => f.endsWith('.po')).map((f) => join(i18nDir, f)) + + const translations = await Promise.all(poFiles.map((f) => processPoFile(f, namespace))) + + console.info('Translations bundling complete.') + + return translations +} diff --git a/meteor/scripts/translation/config.mjs b/meteor/scripts/translation/config.mjs new file mode 100644 index 00000000000..cdc8c7258b5 --- /dev/null +++ b/meteor/scripts/translation/config.mjs @@ -0,0 +1,9 @@ +export const conversionOptions = { + gettextDefaultCharset: 'UTF-8', + splitNewLine: true, + ignorePlurals: false, + keyseparator: '→', + nsseparator: '⇒', + pluralSeparator: '⥤', + contextseparator: '⥤', +} diff --git a/meteor/scripts/translation/extract.mjs b/meteor/scripts/translation/extract.mjs new file mode 100644 index 00000000000..fdbfea0954f --- /dev/null +++ b/meteor/scripts/translation/extract.mjs @@ -0,0 +1,121 @@ +// @ts-check +/** + * This script will extract keys from the source code (provided they are wrapped + * in a call to the (mock) i18next translation function t()). + * The extracted keys are written to .po files, one for each specified locale. + * + * Translations in already existing .po files will be preserved. + */ + +import { writeFile, readFile } from 'node:fs/promises' +import { runExtractor } from 'i18next-cli' +import { i18nextToPo, gettextToI18next } from 'i18next-conv' + +import { conversionOptions } from './config.mjs' + +export async function extractTranslations() { + const start = Date.now() + console.info(`\nExtracting keys...`) + // const entryPointRoot = parse(sourcePath).dir + const locales = ['en', 'nb', 'nn', 'sv'] + const outputPattern = `i18n/{{language}}.json` + + const extractionStats = { keysExtracted: 0, locales: [] } + + await runExtractor({ + locales, + extract: { + input: [ + // `${entryPointRoot}/**/*.ts`, + './lib/**/*.+(ts|tsx)', + './server/**/*.+(ts|tsx)', + '../packages/job-worker/src/**/*.+(ts|tsx)', + '../packages/corelib/src/**/*.+(ts|tsx)', + '../packages/webui/src/**/*.+(ts|tsx)', + ], + output: outputPattern, + sort: true, + nsSeparator: false, + keySeparator: false, + defaultValue: '', + removeUnusedKeys: true, + mergeNamespaces: true, + functions: ['t', 'generateTranslation', 'i18n.t'], + }, + plugins: [jsonToPoPlugin('translation', extractionStats)], + }) + + const taskDuration = Date.now() - start + const { keysExtracted } = extractionStats + if (keysExtracted) { + console.info(`=> OK, ${keysExtracted} keys extracted in ${taskDuration} ms`) + for (const { language, keysMerged, keysRemoved } of extractionStats.locales) { + console.info( + `\t${language}: added ${keysExtracted - keysMerged} new keys, merged ${keysMerged} existing translations, removed ${keysRemoved} obsolete keys` + ) + } + } else { + console.info(`=> No keys found in ${taskDuration}ms`) + } +} + +function jsonToPoPlugin(_translationNamespace, extractionStats) { + return { + name: 'json-to-po', + async afterSync(results) { + await Promise.all( + results.map(async (result) => { + const language = result.path + .split(/[/\\]/) + .at(-1) + .replace(/\.json$/, '') + const poPath = result.path.replace(/\.json$/, '.po') + + console.log('lang', language) + + // Load existing translations from the .po file if it exists + let existingTranslations = {} + try { + const existingPo = await readFile(poPath, 'utf-8') + const fixedPo = existingPo.replaceAll('#~|', '#~') // Remove empty header to avoid parse issues + const converted = await gettextToI18next(language, fixedPo, { + ...conversionOptions, + language, + skipUntranslated: true, + }) + existingTranslations = JSON.parse(converted) + } catch { + // No existing .po file or parse error - start fresh + console.log(`No existing/valid .po file found for ${language}, starting with empty translations.`) + } + + // Merge: use existing translations for known keys, empty string for new keys + const newTranslations = result.newTranslations + const newKeys = Object.keys(newTranslations) + const merged = {} + let keysMerged = 0 + for (const key of newKeys) { + const existing = existingTranslations[key] + if (existing) { + merged[key] = existing + keysMerged++ + } else { + merged[key] = '' + } + } + const keysRemoved = Object.keys(existingTranslations).length - keysMerged + + extractionStats.keysExtracted = newKeys.length + extractionStats.locales.push({ language, keysMerged, keysRemoved }) + + const poContent = await i18nextToPo(language, JSON.stringify(merged), { + ...conversionOptions, + language, + skipUntranslated: false, + }) + await writeFile(poPath, poContent) + }) + ) + }, + } +} diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index 131960500fd..93de8155db4 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -12,6 +12,7 @@ import { StatusCode, TSR, } from '@sofie-automation/blueprints-integration' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { PeripheralDeviceType, PeripheralDeviceCategory, @@ -509,6 +510,7 @@ describe('cronjobs', () => { name: props.deviceName, status: { statusCode: StatusCode.GOOD, + statusDetails: [], }, token: '', ...props, @@ -590,6 +592,7 @@ describe('cronjobs', () => { frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: 1000, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, }), routeSetsWithOverrides: newObjectWithOverrides({}), routeSetExclusivityGroupsWithOverrides: newObjectWithOverrides({}), diff --git a/meteor/server/api/__tests__/peripheralDevice.resolveActionResult.test.ts b/meteor/server/api/__tests__/peripheralDevice.resolveActionResult.test.ts new file mode 100644 index 00000000000..acc0fb8db7e --- /dev/null +++ b/meteor/server/api/__tests__/peripheralDevice.resolveActionResult.test.ts @@ -0,0 +1,220 @@ +import { DeviceStatusContext, TSR } from '@sofie-automation/blueprints-integration' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { Blueprints, PeripheralDevices, Studios } from '../../collections' +import { evalBlueprint } from '../blueprints/cache' +import { resolveActionResult } from '../peripheralDevice' + +jest.mock('../deviceTriggers/observer') +jest.mock('../blueprints/cache') + +const mockEvalBlueprint = evalBlueprint as jest.MockedFunction + +const ACTION_ERROR_CODE = 'ACTION_HTTP_REQUEST_FAILED' +const deviceId = protectString('device0') +const studioId = protectString('studio0') + +function makeErrorResult(overrides: Partial = {}): TSR.ActionExecutionResult { + return { + result: TSR.ActionExecutionResultCode.Error, + response: { key: 'HTTP request to {{url}} failed: {{errorMessage}}' }, + code: ACTION_ERROR_CODE, + context: { + url: 'http://graphics/api', + errorMessage: 'connection refused', + }, + ...overrides, + } +} + +describe('resolveActionResult', () => { + beforeEach(() => { + jest.spyOn(PeripheralDevices, 'findOneAsync').mockReset() + jest.spyOn(Studios, 'findOneAsync').mockReset() + jest.spyOn(Blueprints, 'findOneAsync').mockReset() + mockEvalBlueprint.mockReset() + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('returns Ok results unchanged', async () => { + const result: TSR.ActionExecutionResult = { + result: TSR.ActionExecutionResultCode.Ok, + response: { key: 'Action completed' }, + } + + const resolved = await resolveActionResult(deviceId, result) + + expect(resolved).toBe(result) + expect(PeripheralDevices.findOneAsync).not.toHaveBeenCalled() + }) + + it('returns non-Ok results without a code unchanged', async () => { + const result = makeErrorResult({ code: undefined }) + + const resolved = await resolveActionResult(deviceId, result) + + expect(resolved).toBe(result) + expect(PeripheralDevices.findOneAsync).not.toHaveBeenCalled() + }) + + it('interpolates a matching string template from deviceActionMessages', async () => { + jest.spyOn(PeripheralDevices, 'findOneAsync').mockResolvedValue({ + name: 'Playout Gateway', + studioAndConfigId: { studioId, configId: 'config0' }, + } as any) + jest.spyOn(Studios, 'findOneAsync').mockResolvedValue({ blueprintId: 'blueprint0' } as any) + jest.spyOn(Blueprints, 'findOneAsync').mockResolvedValue({ + _id: 'blueprint0', + name: 'test', + code: '', + } as any) + mockEvalBlueprint.mockReturnValue({ + deviceActionMessages: { + [ACTION_ERROR_CODE]: 'Failed to trigger graphics at {{url}}: {{errorMessage}}', + }, + } as any) + + const resolved = await resolveActionResult(deviceId, makeErrorResult()) + + expect(resolved.response).toEqual({ + key: 'Failed to trigger graphics at http://graphics/api: connection refused', + }) + }) + + it('uses a DeviceStatusMessageFunction from deviceActionMessages', async () => { + jest.spyOn(PeripheralDevices, 'findOneAsync').mockResolvedValue({ + name: 'Playout Gateway', + studioAndConfigId: { studioId, configId: 'config0' }, + } as any) + jest.spyOn(Studios, 'findOneAsync').mockResolvedValue({ blueprintId: 'blueprint0' } as any) + jest.spyOn(Blueprints, 'findOneAsync').mockResolvedValue({ + _id: 'blueprint0', + name: 'test', + code: '', + } as any) + mockEvalBlueprint.mockReturnValue({ + deviceActionMessages: { + [ACTION_ERROR_CODE]: (context: DeviceStatusContext) => + `${context.deviceName} could not reach ${context.url as string}`, + }, + } as any) + + const resolved = await resolveActionResult(deviceId, makeErrorResult()) + + expect(resolved.response).toEqual({ + key: 'Playout Gateway could not reach http://graphics/api', + }) + }) + + it('clears the response when the blueprint suppresses the message', async () => { + jest.spyOn(PeripheralDevices, 'findOneAsync').mockResolvedValue({ + name: 'Playout Gateway', + studioAndConfigId: { studioId, configId: 'config0' }, + } as any) + jest.spyOn(Studios, 'findOneAsync').mockResolvedValue({ blueprintId: 'blueprint0' } as any) + jest.spyOn(Blueprints, 'findOneAsync').mockResolvedValue({ + _id: 'blueprint0', + name: 'test', + code: '', + } as any) + mockEvalBlueprint.mockReturnValue({ + deviceActionMessages: { + [ACTION_ERROR_CODE]: '', + }, + } as any) + + const original = makeErrorResult() + const resolved = await resolveActionResult(deviceId, original) + + expect(resolved).toMatchObject({ + result: TSR.ActionExecutionResultCode.Error, + code: ACTION_ERROR_CODE, + context: original.context, + response: { key: '' }, + }) + }) + + it('returns the original result when there is no matching deviceActionMessages entry', async () => { + jest.spyOn(PeripheralDevices, 'findOneAsync').mockResolvedValue({ + name: 'Playout Gateway', + studioAndConfigId: { studioId, configId: 'config0' }, + } as any) + jest.spyOn(Studios, 'findOneAsync').mockResolvedValue({ blueprintId: 'blueprint0' } as any) + jest.spyOn(Blueprints, 'findOneAsync').mockResolvedValue({ + _id: 'blueprint0', + name: 'test', + code: '', + } as any) + mockEvalBlueprint.mockReturnValue({ + deviceActionMessages: {}, + } as any) + + const result = makeErrorResult() + const resolved = await resolveActionResult(deviceId, result) + + expect(resolved).toBe(result) + }) + + it('resolves messages for child devices via the parent studio', async () => { + const parentDeviceId = protectString('parent0') + jest.spyOn(PeripheralDevices, 'findOneAsync').mockImplementation(async (id) => { + if (id === deviceId) { + return { + name: 'casparcg0', + parentDeviceId, + } as any + } + if (id === parentDeviceId) { + return { + studioAndConfigId: { studioId, configId: 'config0' }, + } as any + } + return undefined + }) + jest.spyOn(Studios, 'findOneAsync').mockResolvedValue({ blueprintId: 'blueprint0' } as any) + jest.spyOn(Blueprints, 'findOneAsync').mockResolvedValue({ + _id: 'blueprint0', + name: 'test', + code: '', + } as any) + mockEvalBlueprint.mockReturnValue({ + deviceActionMessages: { + [ACTION_ERROR_CODE]: 'Failed to trigger graphics at {{url}}: {{errorMessage}}', + }, + } as any) + + const resolved = await resolveActionResult(deviceId, makeErrorResult()) + + expect(resolved.response).toEqual({ + key: 'Failed to trigger graphics at http://graphics/api: connection refused', + }) + }) + + it('returns the original result when the device has no studio', async () => { + jest.spyOn(PeripheralDevices, 'findOneAsync').mockResolvedValue({ + name: 'Playout Gateway', + } as any) + + const result = makeErrorResult() + const resolved = await resolveActionResult(deviceId, result) + + expect(resolved).toBe(result) + expect(Studios.findOneAsync).not.toHaveBeenCalled() + }) + + it('returns the original result when blueprint lookup fails', async () => { + jest.spyOn(PeripheralDevices, 'findOneAsync').mockResolvedValue({ + name: 'Playout Gateway', + studioAndConfigId: { studioId, configId: 'config0' }, + } as any) + jest.spyOn(Studios, 'findOneAsync').mockResolvedValue(undefined) + + const result = makeErrorResult() + const resolved = await resolveActionResult(deviceId, result) + + expect(resolved).toBe(result) + }) +}) diff --git a/meteor/server/api/__tests__/peripheralDevice.test.ts b/meteor/server/api/__tests__/peripheralDevice.test.ts index 594c44049ca..5a37e3edb70 100644 --- a/meteor/server/api/__tests__/peripheralDevice.test.ts +++ b/meteor/server/api/__tests__/peripheralDevice.test.ts @@ -203,7 +203,7 @@ describe('test peripheralDevice general API methods', () => { }) await MeteorCall.peripheralDevice.setStatus(device._id, device.token, { statusCode: StatusCode.WARNING_MINOR, - messages: ["Something's not right"], + statusDetails: [{ message: "Something's not right" }], }) expect(((await PeripheralDevices.findOneAsync(device._id)) as PeripheralDevice).status).toMatchObject({ statusCode: StatusCode.WARNING_MINOR, @@ -650,6 +650,7 @@ describe('test peripheralDevice general API methods', () => { lastSeen: 0, status: { statusCode: StatusCode.GOOD, + statusDetails: [], }, subType: '_process', token: 'MockToken', diff --git a/meteor/server/api/blueprints/__tests__/api.test.ts b/meteor/server/api/blueprints/__tests__/api.test.ts index 1bd945c2fd2..2c92ed6f4f8 100644 --- a/meteor/server/api/blueprints/__tests__/api.test.ts +++ b/meteor/server/api/blueprints/__tests__/api.test.ts @@ -1,17 +1,21 @@ import _ from 'underscore' +import path from 'path' +import os from 'os' +import { promises as fsp } from 'fs' import { setupDefaultStudioEnvironment, packageBlueprint } from '../../../../__mocks__/helpers/database' import { literal, getRandomId } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' import { SYSTEM_ID, ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' -import { insertBlueprint, uploadBlueprint } from '../api' +import { insertBlueprint, uploadBlueprint, uploadBlueprintAsset } from '../api' import { MeteorCall } from '../../methods' import '../../../../__mocks__/_extendJest' import { Blueprints, CoreSystem } from '../../../collections' import { SupressLogMessages } from '../../../../__mocks__/suppressLogging' import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { Meteor } from 'meteor/meteor' +import * as CoreSystemAPI from '../../../coreSystem' // we don't want the deviceTriggers observer to start up at this time jest.mock('../../deviceTriggers/observer') @@ -549,4 +553,34 @@ describe('Test blueprint management api', () => { ) }) }) + describe('uploadBlueprintAsset', () => { + let storePath: string + + beforeEach(async () => { + storePath = await fsp.mkdtemp(path.join(os.tmpdir(), 'sofie-blueprint-assets-')) + jest.spyOn(CoreSystemAPI, 'getSystemStorePath').mockReturnValue(storePath) + }) + + afterEach(async () => { + jest.restoreAllMocks() + await fsp.rm(storePath, { recursive: true, force: true }) + }) + + test('writes decoded base64 payload to file', async () => { + const payload = Buffer.from('some fake binary data \u0000\u0001', 'utf8') + const fileId = 'myBlueprint/logo.bin' + + await uploadBlueprintAsset(DEFAULT_CONNECTION, fileId, payload.toString('base64')) + + const expectedFilePath = path.join(storePath, 'assets', fileId) + const writtenBuffer = await fsp.readFile(expectedFilePath) + expect(writtenBuffer.equals(payload)).toBeTruthy() + }) + + test('rejects path traversal attempts', async () => { + await expect( + uploadBlueprintAsset(DEFAULT_CONNECTION, '../outside.bin', Buffer.from('x').toString('base64')) + ).rejects.toThrow('Asset name outside of asset storage path') + }) + }) }) diff --git a/meteor/server/api/blueprints/__tests__/http.test.ts b/meteor/server/api/blueprints/__tests__/http.test.ts index 6673f4eb6a9..6755071de6a 100644 --- a/meteor/server/api/blueprints/__tests__/http.test.ts +++ b/meteor/server/api/blueprints/__tests__/http.test.ts @@ -1,5 +1,6 @@ import _ from 'underscore' import { Meteor } from 'meteor/meteor' +import { PassThrough } from 'stream' import { SupressLogMessages } from '../../../../__mocks__/suppressLogging' import { callKoaRoute } from '../../../../__mocks__/koa-util' import { blueprintsRouter } from '../http' @@ -357,4 +358,247 @@ describe('Test blueprint http api', () => { } }) }) + + describe('router upload assets', () => { + describe('POST /assets', () => { + async function callRoute(body: any) { + const ctx = await callKoaRoute(blueprintsRouter, { + method: 'POST', + url: '/assets', + + requestBody: body, + }) + + expect(ctx.response.type).toBe('text/plain') + return ctx + } + + function resetUploadAssetMock() { + const uploadBlueprintAsset = api.uploadBlueprintAsset as any as jest.MockInstance + uploadBlueprintAsset.mockClear() + return uploadBlueprintAsset + } + + beforeEach(() => { + resetUploadAssetMock() + }) + + test('missing body', async () => { + SupressLogMessages.suppressLogMessage(/Invalid request body/i) + const res = await callRoute(undefined) + expect(res.response.status).toEqual(500) + + expect(api.uploadBlueprintAsset).toHaveBeenCalledTimes(0) + }) + + test('empty body', async () => { + SupressLogMessages.suppressLogMessage(/Missing request body/i) + const res = await callRoute('') + expect(res.response.status).toEqual(500) + + expect(api.uploadBlueprintAsset).toHaveBeenCalledTimes(0) + }) + + test('non-object body', async () => { + SupressLogMessages.suppressLogMessage(/Invalid request body/i) + const res = await callRoute(99) + expect(res.response.status).toEqual(500) + + expect(api.uploadBlueprintAsset).toHaveBeenCalledTimes(0) + }) + + test('empty object body', async () => { + SupressLogMessages.suppressLogMessage(/Invalid request body/i) + const res = await callRoute({}) + expect(res.response.status).toEqual(500) + + expect(api.uploadBlueprintAsset).toHaveBeenCalledTimes(0) + }) + + test('with json body', async () => { + const fileId = 'folder/asset.png' + const payload = { + [fileId]: 'Ym9keQ==', + } + + const res = await callRoute(payload) + expect(res.response.status).toEqual(200) + expect(res.body).toEqual('') + + expect(api.uploadBlueprintAsset).toHaveBeenCalledTimes(1) + expect(api.uploadBlueprintAsset).toHaveBeenCalledWith(DEFAULT_CONTEXT, fileId, payload[fileId]) + }) + + test('with json body - multiple', async () => { + const count = 10 + const payload: Record = {} + for (let i = 0; i < count; i++) { + payload[`id${i}.png`] = `body${i}` + } + + const res = await callRoute(payload) + expect(res.response.status).toEqual(200) + expect(res.body).toEqual('') + + expect(api.uploadBlueprintAsset).toHaveBeenCalledTimes(count) + for (let i = 0; i < count; i++) { + expect(api.uploadBlueprintAsset).toHaveBeenCalledWith(DEFAULT_CONTEXT, `id${i}.png`, `body${i}`) + } + }) + + test('with errors', async () => { + const count = 10 + const payload: Record = {} + for (let i = 0; i < count; i++) { + payload[`id${i}.png`] = `body${i}` + } + + const uploadBlueprintAsset = resetUploadAssetMock() + let called = 0 + uploadBlueprintAsset.mockImplementation(() => { + called++ + if (called === 3 || called === 7) { + throw new Meteor.Error(505, 'Some thrown error') + } + }) + + try { + SupressLogMessages.suppressLogMessage(/Some thrown error/i) + SupressLogMessages.suppressLogMessage(/Some thrown error/i) + const res = await callRoute(payload) + expect(res.response.status).toEqual(500) + expect(res.body).toEqual( + 'Errors were encountered: \n[505] Some thrown error\n[505] Some thrown error\n' + ) + + expect(api.uploadBlueprintAsset).toHaveBeenCalledTimes(count) + for (let i = 0; i < count; i++) { + expect(api.uploadBlueprintAsset).toHaveBeenCalledWith(DEFAULT_CONTEXT, `id${i}.png`, `body${i}`) + } + } finally { + uploadBlueprintAsset.mockRestore() + } + }) + }) + + describe('GET /assets/*fileId', () => { + function createDataStream() { + const stream = new PassThrough() + stream.end('asset') + return stream + } + + async function callRoute(fileId: string) { + const ctx = await callKoaRoute(blueprintsRouter, { + method: 'GET', + url: `/assets/${fileId}`, + }) + + return ctx + } + + function resetRetrieveAssetMock() { + const retrieveBlueprintAsset = api.retrieveBlueprintAsset as any as jest.MockInstance + retrieveBlueprintAsset.mockClear() + return retrieveBlueprintAsset + } + + beforeEach(() => { + resetRetrieveAssetMock() + }) + + test('png asset', async () => { + const fileId = 'folder/file.png' + const dataStream = createDataStream() + + const retrieveBlueprintAsset = resetRetrieveAssetMock() + retrieveBlueprintAsset.mockReturnValue(dataStream) + + const res = await callRoute(fileId) + + expect(res.statusCode).toEqual(200) + expect(res.response.type).toEqual('image/png') + expect(res.body).toBe(dataStream) + expect(res.response.get('Cache-Control')).toEqual('public, max-age=1296000, immutable') + + expect(api.retrieveBlueprintAsset).toHaveBeenCalledTimes(1) + expect(api.retrieveBlueprintAsset).toHaveBeenCalledWith(DEFAULT_CONTEXT, fileId) + }) + + test('svg asset', async () => { + const fileId = 'folder/file.svg' + const dataStream = createDataStream() + + const retrieveBlueprintAsset = resetRetrieveAssetMock() + retrieveBlueprintAsset.mockReturnValue(dataStream) + + const res = await callRoute(fileId) + + expect(res.statusCode).toEqual(200) + expect(res.response.type).toEqual('image/svg+xml') + expect(res.body).toBe(dataStream) + + expect(api.retrieveBlueprintAsset).toHaveBeenCalledTimes(1) + expect(api.retrieveBlueprintAsset).toHaveBeenCalledWith(DEFAULT_CONTEXT, fileId) + }) + + test('gif asset', async () => { + const fileId = 'folder/file.gif' + const dataStream = createDataStream() + + const retrieveBlueprintAsset = resetRetrieveAssetMock() + retrieveBlueprintAsset.mockReturnValue(dataStream) + + const res = await callRoute(fileId) + + expect(res.statusCode).toEqual(200) + expect(res.response.type).toEqual('image/gif') + expect(res.body).toBe(dataStream) + + expect(api.retrieveBlueprintAsset).toHaveBeenCalledTimes(1) + expect(api.retrieveBlueprintAsset).toHaveBeenCalledWith(DEFAULT_CONTEXT, fileId) + }) + + test('not found', async () => { + const fileId = 'folder/missing.png' + + const retrieveBlueprintAsset = resetRetrieveAssetMock() + retrieveBlueprintAsset.mockImplementation(() => { + const err = new Error('No such file') as Error & { code?: string } + err.code = 'ENOENT' + throw err + }) + + SupressLogMessages.suppressLogMessage(/Blueprint asset not found/i) + const res = await callRoute(fileId) + expect(res.statusCode).toEqual(404) + }) + + test('path traversal attempt', async () => { + const fileId = 'folder/../escape.png' + + const retrieveBlueprintAsset = resetRetrieveAssetMock() + retrieveBlueprintAsset.mockImplementation(() => { + throw new Error('Requested asset outside of asset storage path') + }) + + SupressLogMessages.suppressLogMessage(/Blueprint asset path traversal attempt/i) + const res = await callRoute(fileId) + expect(res.statusCode).toEqual(400) + }) + + test('internal error', async () => { + const fileId = 'folder/file.png' + + const retrieveBlueprintAsset = resetRetrieveAssetMock() + retrieveBlueprintAsset.mockImplementation(() => { + throw new Error('Some thrown error') + }) + + SupressLogMessages.suppressLogMessage(/Blueprint asset retrieval failed/i) + const res = await callRoute(fileId) + expect(res.statusCode).toEqual(500) + }) + }) + }) }) diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index 4a152ef66e9..a02c027868b 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -112,23 +112,36 @@ export async function uploadBlueprintAsset(cred: RequestCredentials, fileId: str assertConnectionHasOneOfPermissions(cred, ...PERMISSIONS_FOR_MANAGE_BLUEPRINTS) const storePath = getSystemStorePath() + const assetsDir = path.resolve(storePath, 'assets') + path.sep + const assetPath = path.resolve(path.join(assetsDir, fileId)) + if (!assetPath.startsWith(assetsDir)) { + throw new Error('Asset name outside of asset storage path') + } // TODO: add access control here const data = Buffer.from(body, 'base64') - const parsedPath = path.parse(fileId) - logger.info( - `Write ${data.length} bytes to ${path.join(storePath, fileId)} (storePath: ${storePath}, fileId: ${fileId})` - ) + logger.info(`Write ${data.length} bytes to ${assetPath} (storePath: ${storePath}, fileId: ${fileId})`) + + const assetDirPath = path.dirname(assetPath) - await fsp.mkdir(path.join(storePath, parsedPath.dir), { recursive: true }) - await fsp.writeFile(path.join(storePath, fileId), data) + await fsp.mkdir(assetDirPath, { recursive: true }) + await fsp.writeFile(assetPath, data) } -export function retrieveBlueprintAsset(_cred: RequestCredentials, fileId: string): ReadStream { +export async function retrieveBlueprintAsset(_cred: RequestCredentials, fileId: string): Promise { check(fileId, String) const storePath = getSystemStorePath() + const assetsDir = path.resolve(storePath, 'assets') + path.sep + const assetPath = path.resolve(path.join(assetsDir, fileId)) + if (!assetPath.startsWith(assetsDir)) { + throw new Error('Requested asset outside of asset storage path') + } - return createReadStream(path.join(storePath, fileId)) + const stream = createReadStream(assetPath) + return new Promise((resolve, reject) => { + stream.on('open', () => resolve(stream)) + stream.on('error', (err) => reject(err)) + }) } /** Only to be called from internal functions */ export async function internalUploadBlueprint( diff --git a/meteor/server/api/blueprints/http.ts b/meteor/server/api/blueprints/http.ts index 29b1a6bc719..bf9666dd303 100644 --- a/meteor/server/api/blueprints/http.ts +++ b/meteor/server/api/blueprints/http.ts @@ -179,15 +179,15 @@ blueprintsRouter.post( } ) -blueprintsRouter.get('/assets/*splat', async (ctx) => { +blueprintsRouter.get('/assets/*fileId', async (ctx) => { logger.debug(`Blueprint Asset: ${ctx.socket.remoteAddress} GET "${ctx.url}"`) // TODO - some sort of user verification // for now just check it's a png to prevent snapshots being downloaded - const filePath = ctx.params[0] + const filePath = ctx.params.fileId if (filePath.match(/\.(png|svg|gif)?$/)) { try { - const dataStream = retrieveBlueprintAsset(ctx, filePath) + const dataStream = await retrieveBlueprintAsset(ctx, filePath) const extension = path.extname(filePath) if (extension === '.svg') { ctx.response.type = 'image/svg+xml' @@ -200,8 +200,17 @@ blueprintsRouter.get('/assets/*splat', async (ctx) => { ctx.set('Cache-Control', `public, max-age=${BLUEPRINT_ASSET_MAX_AGE}, immutable`) ctx.statusCode = 200 ctx.body = dataStream - } catch { - ctx.statusCode = 404 // Probably + } catch (e) { + if (e instanceof Error && 'code' in e && e.code === 'ENOENT') { + logger.warn('Blueprint asset not found: ' + e) + ctx.statusCode = 404 + } else if (e instanceof Error && e.message.includes('outside of asset storage path')) { + logger.warn('Blueprint asset path traversal attempt: ' + e) + ctx.statusCode = 400 + } else { + logger.warn('Blueprint asset retrieval failed: ' + e) + ctx.statusCode = 500 + } } } else { ctx.statusCode = 403 diff --git a/meteor/server/api/buckets.ts b/meteor/server/api/buckets.ts index 39516601d03..e82e65c9f58 100644 --- a/meteor/server/api/buckets.ts +++ b/meteor/server/api/buckets.ts @@ -230,7 +230,7 @@ export namespace BucketsAPI { await Promise.all([ Buckets.removeAsync(bucket._id), - await runIngestOperation(bucket.studioId, IngestJobs.BucketEmpty, { + runIngestOperation(bucket.studioId, IngestJobs.BucketEmpty, { bucketId: bucket._id, }), ]) diff --git a/meteor/server/api/cleanup.ts b/meteor/server/api/cleanup.ts index b56ef583b40..a326e45ad01 100644 --- a/meteor/server/api/cleanup.ts +++ b/meteor/server/api/cleanup.ts @@ -2,7 +2,7 @@ import { ProtectedString } from '@sofie-automation/shared-lib/dist/lib/protected import { getCurrentTime } from '../lib/lib' import { CollectionCleanupResult } from '@sofie-automation/meteor-lib/dist/api/system' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { getActiveRundownPlaylistsInStudioFromDb, getExpiredRemovedPackageInfos, diff --git a/meteor/server/api/client.ts b/meteor/server/api/client.ts index ba62dfb7bef..10dd6dc7cac 100644 --- a/meteor/server/api/client.ts +++ b/meteor/server/api/client.ts @@ -30,6 +30,7 @@ import { } from '../security/check' import { UserActionsLog } from '../collections' import { executePeripheralDeviceFunctionWithCustomTimeout } from './peripheralDevice/executeFunction' +import { resolveActionResult } from './peripheralDevice' import { LeveledLogMethodFixed } from '@sofie-automation/corelib/dist/logging' import { assertConnectionHasOneOfPermissions } from '../security/auth' @@ -458,7 +459,7 @@ class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { actionId: string, payload?: Record ) { - return ServerClientAPI.callPeripheralDeviceFunctionOrAction( + const result = await ServerClientAPI.callPeripheralDeviceFunctionOrAction( this, context, deviceId, @@ -470,6 +471,7 @@ class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { actionId, payload ) + return resolveActionResult(deviceId, result) } async callBackgroundPeripheralDeviceFunction( deviceId: PeripheralDeviceId, diff --git a/meteor/server/api/deviceTriggers/RundownContentObserver.ts b/meteor/server/api/deviceTriggers/RundownContentObserver.ts index e65674ce855..e6dcf9f782d 100644 --- a/meteor/server/api/deviceTriggers/RundownContentObserver.ts +++ b/meteor/server/api/deviceTriggers/RundownContentObserver.ts @@ -33,9 +33,7 @@ export class RundownContentObserver { #observers: Meteor.LiveQueryHandle[] = [] #cache: ContentCache #cancelCache: () => void - #cleanup: (() => void) | undefined = () => { - throw new Error('RundownContentObserver.#cleanup has not been set!') - } + #cleanup: (() => void) | undefined #disposed = false private constructor(onChanged: ChangedHandler) { diff --git a/meteor/server/api/deviceTriggers/StudioObserver.ts b/meteor/server/api/deviceTriggers/StudioObserver.ts index c98a154b468..5f18675eda1 100644 --- a/meteor/server/api/deviceTriggers/StudioObserver.ts +++ b/meteor/server/api/deviceTriggers/StudioObserver.ts @@ -10,7 +10,7 @@ import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mo import EventEmitter from 'events' import { Meteor } from 'meteor/meteor' import _ from 'underscore' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { logger } from '../../logging' @@ -212,6 +212,8 @@ export class StudioObserver extends EventEmitter { this.nextProps = undefined const { activePlaylistId, activationId } = this.currentProps + const rundownContentChanged = this.#rundownContentChanged + const pieceInstancesChanged = this.#pieceInstancesChanged this.showStyleBaseId = showStyleBaseId @@ -219,7 +221,7 @@ export class StudioObserver extends EventEmitter { logger.silly(`Creating new RundownContentObserver`) const obs1 = await RundownContentObserver.create(activePlaylistId, showStyleBaseId, rundownIds, (cache) => { - return this.#rundownContentChanged(showStyleBaseId, cache) + return rundownContentChanged(showStyleBaseId, cache) }) return () => { @@ -228,11 +230,9 @@ export class StudioObserver extends EventEmitter { }) this.#pieceInstancesLiveQuery = await PieceInstancesObserver.create(activationId, showStyleBaseId, (cache) => { - const cleanupChanges = this.#pieceInstancesChanged(showStyleBaseId, cache) + const cleanupChanges = pieceInstancesChanged(showStyleBaseId, cache) - return () => { - cleanupChanges?.() - } + return () => cleanupChanges?.() }) if (this.#disposed) { @@ -249,5 +249,6 @@ export class StudioObserver extends EventEmitter { this.#playlistInStudioLiveQuery.stop() this.updatePlaylistInStudio.cancel() this.#rundownsLiveQuery?.stop() + this.#pieceInstancesLiveQuery?.stop() } } diff --git a/meteor/server/api/deviceTriggers/__tests__/StudioObserver.test.ts b/meteor/server/api/deviceTriggers/__tests__/StudioObserver.test.ts new file mode 100644 index 00000000000..b2ebaeecc05 --- /dev/null +++ b/meteor/server/api/deviceTriggers/__tests__/StudioObserver.test.ts @@ -0,0 +1,156 @@ +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { + RundownId, + RundownPlaylistActivationId, + RundownPlaylistId, + ShowStyleBaseId, + StudioId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' + +import type { ContentCache as RundownContentCache } from '../reactiveContentCache' +import type { ContentCache as PieceInstancesContentCache } from '../reactiveContentCacheForPieceInstances' +import { runAllTimers } from '../../../../__mocks__/helpers/jest' + +type OnChangedRundown = (cache: RundownContentCache) => () => void +type OnChangedPieceInstances = (cache: PieceInstancesContentCache) => () => void + +let capturedRundownContentOnChanged: OnChangedRundown | undefined +let capturedPieceInstancesOnChanged: OnChangedPieceInstances | undefined + +jest.mock('../../../publications/lib/observerChain', () => { + const fakeHandle = { stop: jest.fn() } + const chain: any = { + next: jest.fn(() => chain), + end: jest.fn(() => fakeHandle), + } + return { + observerChain: jest.fn(() => chain), + } +}) + +jest.mock('../RundownsObserver', () => { + return { + RundownsObserver: { + create: jest.fn( + async (_playlistId: RundownPlaylistId, onChanged: (ids: RundownId[]) => Promise<() => void>) => { + // Immediately drive the callback once, to emulate initial observer execution + await onChanged([protectString('r0')]) + return { stop: jest.fn() } + } + ), + }, + } +}) + +jest.mock('../RundownContentObserver', () => { + return { + RundownContentObserver: { + create: jest.fn( + async ( + _playlistId: RundownPlaylistId, + _showStyleBaseId: ShowStyleBaseId, + _rundownIds: RundownId[], + onChanged: OnChangedRundown + ) => { + capturedRundownContentOnChanged = onChanged + return { stop: jest.fn() } + } + ), + }, + } +}) + +jest.mock('../PieceInstancesObserver', () => { + return { + PieceInstancesObserver: { + create: jest.fn( + async ( + _activationId: RundownPlaylistActivationId, + _showStyleBaseId: ShowStyleBaseId, + onChanged: OnChangedPieceInstances + ) => { + capturedPieceInstancesOnChanged = onChanged + return { stop: jest.fn() } + } + ), + }, + } +}) + +describe('StudioObserver', () => { + beforeEach(() => { + jest.useFakeTimers() + capturedRundownContentOnChanged = undefined + capturedPieceInstancesOnChanged = undefined + }) + + test('rundown deactivation regression: observer callbacks must not depend on `this` (private fields)', async () => { + // Import after mocks are in place + const { StudioObserver } = await import('../StudioObserver') + + const studioId = protectString('studio0') + const playlistId = protectString('playlist0') + const activationId = protectString('activation0') + const rundownId = protectString('rundown0') + const showStyleBaseId = protectString('showStyleBase0') + + const rundownCleanup = jest.fn() + const pieceCleanup = jest.fn() + + const onRundownContentChanged = jest.fn( + (_ssbId: ShowStyleBaseId, _cache: RundownContentCache) => rundownCleanup + ) + const onPieceInstancesChanged = jest.fn( + (_ssbId: ShowStyleBaseId, _cache: PieceInstancesContentCache) => pieceCleanup + ) + + const observer = new StudioObserver(studioId, onRundownContentChanged, onPieceInstancesChanged) + + // Prime state so updateShowStyle goes down the creation path + ;(observer as any).nextProps = { + activePlaylistId: playlistId, + activationId, + currentRundownId: rundownId, + } + + const state = { + currentRundown: { _id: rundownId, showStyleBaseId }, + showStyleBase: { _id: showStyleBaseId }, + } + + // Trigger the debounced execution + const ps: Promise = (observer as any).updateShowStyle.call(state) + + // Flush debounce timers and any queued promises + await jest.advanceTimersByTimeAsync(25) + await runAllTimers() + await ps + + // Ensure we captured callbacks from the two observers + expect(capturedRundownContentOnChanged).toBeTruthy() + expect(capturedPieceInstancesOnChanged).toBeTruthy() + + const mockRundownCache = {} as any as RundownContentCache + const mockPieceInstancesCache = {} as any as PieceInstancesContentCache + + // Regression: invoke callbacks without a bound `this` (simulates lost context) + expect(() => capturedRundownContentOnChanged!(mockRundownCache)).not.toThrow() + expect(() => capturedPieceInstancesOnChanged!(mockPieceInstancesCache)).not.toThrow() + + // They should return cleanup fns + const cleanup1 = capturedRundownContentOnChanged!(mockRundownCache) + const cleanup2 = capturedPieceInstancesOnChanged!(mockPieceInstancesCache) + expect(typeof cleanup1).toBe('function') + expect(typeof cleanup2).toBe('function') + + // Ensure our handlers were called with expected args + expect(onRundownContentChanged).toHaveBeenCalledWith(showStyleBaseId, mockRundownCache) + expect(onPieceInstancesChanged).toHaveBeenCalledWith(showStyleBaseId, mockPieceInstancesCache) + + // Ensure returned cleanup fns are callable + expect(() => cleanup1()).not.toThrow() + expect(() => cleanup2()).not.toThrow() + + observer.stop() + }) +}) diff --git a/meteor/server/api/deviceTriggers/__tests__/TagsService.test.ts b/meteor/server/api/deviceTriggers/__tests__/TagsService.test.ts index 1248e391583..bb42dce479e 100644 --- a/meteor/server/api/deviceTriggers/__tests__/TagsService.test.ts +++ b/meteor/server/api/deviceTriggers/__tests__/TagsService.test.ts @@ -9,7 +9,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ContentCache } from '../reactiveContentCacheForPieceInstances' import { ReactiveCacheCollection } from '../../../publications/lib/ReactiveCacheCollection' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { literal, normalizeArray } from '@sofie-automation/corelib/dist/lib' diff --git a/meteor/server/api/deviceTriggers/observer.ts b/meteor/server/api/deviceTriggers/observer.ts index cbabcc5356a..704c43b78cc 100644 --- a/meteor/server/api/deviceTriggers/observer.ts +++ b/meteor/server/api/deviceTriggers/observer.ts @@ -1,5 +1,6 @@ import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { TFunction } from 'i18next' import { check } from 'meteor/check' import { Meteor } from 'meteor/meteor' import _ from 'underscore' @@ -145,6 +146,10 @@ export async function receiveInputDeviceTrigger( const context = actionManager.getContext() if (!context) throw new Meteor.Error(500, `Undefined Device Trigger context for studio "${studioId}"`) - await executableAction.execute((t: ITranslatableMessage) => t.key ?? t, `${deviceId}: ${triggerId}`, context) + await executableAction.execute( + ((t: ITranslatableMessage) => t.key ?? t) as unknown as TFunction, // TFunction has some odd generic constraints on the return type now + `${deviceId}: ${triggerId}`, + context + ) } } diff --git a/meteor/server/api/deviceTriggers/reactiveContentCache.ts b/meteor/server/api/deviceTriggers/reactiveContentCache.ts index 5c01abb7ec4..b8e170c1047 100644 --- a/meteor/server/api/deviceTriggers/reactiveContentCache.ts +++ b/meteor/server/api/deviceTriggers/reactiveContentCache.ts @@ -6,7 +6,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' diff --git a/meteor/server/api/deviceTriggers/reactiveContentCacheForPieceInstances.ts b/meteor/server/api/deviceTriggers/reactiveContentCacheForPieceInstances.ts index fafba70f46d..2035e70c21e 100644 --- a/meteor/server/api/deviceTriggers/reactiveContentCacheForPieceInstances.ts +++ b/meteor/server/api/deviceTriggers/reactiveContentCacheForPieceInstances.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import _ from 'underscore' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { ReactiveCacheCollection } from '../../publications/lib/ReactiveCacheCollection' import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' diff --git a/meteor/server/api/deviceTriggers/triggersContext.ts b/meteor/server/api/deviceTriggers/triggersContext.ts index a93d5da362a..435ee512128 100644 --- a/meteor/server/api/deviceTriggers/triggersContext.ts +++ b/meteor/server/api/deviceTriggers/triggersContext.ts @@ -18,7 +18,10 @@ import { PartId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DummyReactiveVar } from '@sofie-automation/meteor-lib/dist/triggers/reactive-var' import { ReactivePlaylistActionContext } from '@sofie-automation/meteor-lib/dist/triggers/actionFactory' import { FindOneOptions, FindOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { DBRundownPlaylist, SelectedPartInstance } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + DBRundownPlaylist, + SelectedPartInstance, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { AdLibActions, AdLibPieces, @@ -156,10 +159,9 @@ async function fetchInfoForSelectedPart(partInfo: SelectedPartInstance | null): const partInstance = (await PartInstances.findOneAsync(partInfo.partInstanceId, { projection: { - // @ts-expect-error deep property 'part._id': 1, segmentId: 1, - }, + } as any, })) as (Pick & { part: Pick }) | null if (!partInstance) return null diff --git a/meteor/server/api/evaluations.ts b/meteor/server/api/evaluations.ts index 22ac4689f62..5b7385e50c2 100644 --- a/meteor/server/api/evaluations.ts +++ b/meteor/server/api/evaluations.ts @@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor' import _ from 'underscore' import { fetchStudioLight } from '../optimizations' import { sendSlackMessageToWebhook } from './integration/slack' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { Evaluations, RundownPlaylists } from '../collections' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { VerifiedRundownPlaylistForUserAction } from '../security/check' diff --git a/meteor/server/api/peripheralDevice.ts b/meteor/server/api/peripheralDevice.ts index be6e118a0af..ca864f3438d 100644 --- a/meteor/server/api/peripheralDevice.ts +++ b/meteor/server/api/peripheralDevice.ts @@ -2,7 +2,14 @@ import { Meteor } from 'meteor/meteor' import { check, Match } from '../lib/check' import _ from 'underscore' import { PeripheralDeviceType, PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { PeripheralDeviceCommands, PeripheralDevices, Rundowns, Studios, UserActionsLog } from '../collections' +import { + PeripheralDeviceCommands, + PeripheralDevices, + Rundowns, + Studios, + UserActionsLog, + Blueprints, +} from '../collections' import { stringifyObjects, literal } from '@sofie-automation/corelib/dist/lib' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { getCurrentTime } from '../lib/lib' @@ -30,14 +37,16 @@ import { checkAccessAndGetPeripheralDevice } from '../security/check' import { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog' import { PackageManagerIntegration } from './integration/expectedPackages' import { profiler } from './profiler' -import { QueueStudioJob } from '../worker/worker' +import { QueueStudioJob, QueueOrUpdateStudioJob } from '../worker/worker' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' import { PlayoutChangedResults, PeripheralDeviceInitOptions, PeripheralDeviceStatusObject, TimelineTriggerTimeResult, + DeviceStatusDetail, } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' +import type { PeripheralDeviceExternalEvent } from '@sofie-automation/shared-lib/dist/peripheralDevice/externalEvents' import { checkStudioExists } from '../optimizations' import { ExpectedPackageId, @@ -65,8 +74,215 @@ import bodyParser from 'koa-bodyparser' import { assertConnectionHasOneOfPermissions } from '../security/auth' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { getRootSubpath } from '../lib' +import { evalBlueprint } from './blueprints/cache' +import { StudioBlueprintManifest, TSR } from '@sofie-automation/blueprints-integration' +import { StatusMessageResolver } from '@sofie-automation/corelib' +import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' +import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' const apmNamespace = 'peripheralDevice' + +type StudioBlueprintLookup = { + blueprint: Pick + manifest: StudioBlueprintManifest +} + +/** + * Load and evaluate the Studio blueprint manifest for a studio. + * Returns undefined when the studio has no blueprint or lookup fails. + */ +async function getStudioBlueprintManifest(studioId: StudioId): Promise { + const studio = (await Studios.findOneAsync(studioId, { + projection: { blueprintId: 1 }, + })) as Pick | undefined + + if (!studio?.blueprintId) return undefined + + const blueprint = (await Blueprints.findOneAsync(studio.blueprintId, { + projection: { _id: 1, name: 1, code: 1 }, + })) as Pick | undefined + + if (!blueprint) return undefined + + const manifest = evalBlueprint(blueprint) as StudioBlueprintManifest + return { blueprint, manifest } +} + +/** + * Resolve device status details using the Studio blueprint's deviceStatusMessages. + * This allows blueprints to customize status messages shown to operators. + * + * @param studioId - The studio ID to look up the blueprint + * @param deviceName - The peripheral device name (shorter than TSR's internal name) + * @param deviceId - The peripheral device ID + * @param statusDetails - Structured status details from TSR + * @param defaultMessages - The original messages from TSR (used as fallback) + */ +async function resolveDeviceStatusDetails( + studioId: StudioId, + deviceName: string, + deviceId: PeripheralDeviceId, + statusDetails: DeviceStatusDetail[], + defaultMessages: string[] +): Promise { + try { + const studioBlueprint = await getStudioBlueprintManifest(studioId) + if (!studioBlueprint) { + // No blueprint, return empty (caller will use original messages) + return [] + } + + const { blueprint, manifest: blueprintManifest } = studioBlueprint + + logger.debug( + `Blueprint ${blueprint._id} deviceStatusMessages keys: ${Object.keys(blueprintManifest.deviceStatusMessages ?? {}).join(', ')}` + ) + + if (!blueprintManifest.deviceStatusMessages) { + // Blueprint doesn't define any custom status messages + logger.debug(`Blueprint ${blueprint._id} has no deviceStatusMessages`) + return [] + } + + // Create resolver with the blueprint's status messages + const resolver = new StatusMessageResolver( + blueprint._id, + blueprintManifest.deviceStatusMessages, + undefined // No system error messages + ) + + // Resolve each status detail + const resolvedMessages: string[] = [] + for (let i = 0; i < statusDetails.length; i++) { + const statusDetail = statusDetails[i] + // statusDetail.message is always pre-rendered by TSR; use it as fallback if no defaultMessages entry + const defaultMessage = defaultMessages[i] ?? statusDetail.message + + if (!statusDetail.code) { + // No structured code - use the pre-rendered TSR message directly + resolvedMessages.push(defaultMessage) + continue + } + + logger.debug( + `Resolving status code: ${statusDetail.code}, context: ${JSON.stringify(statusDetail.context)}` + ) + const message = resolver.getDeviceStatusMessage( + statusDetail.code, + { + ...statusDetail.context, + // Override with peripheral device info (TSR might have longer names) + deviceName, + deviceId: unprotectString(deviceId), + }, + defaultMessage + ) + + if (message) { + // Interpolate the message template with context values + const interpolated = interpollateTranslation(message.key, message.args) + logger.debug(`Resolved message for ${statusDetail.code}: ${interpolated}`) + resolvedMessages.push(interpolated) + // Also mutate statusDetail.message so the UI can read from statusDetails[].message directly + statusDetail.message = interpolated + } else { + // Message suppressed by blueprint - clear the message so the UI doesn't show the raw TSR message + statusDetail.message = '' + logger.debug(`Message suppressed for ${statusDetail.code}`) + } + } + + return resolvedMessages + } catch (e) { + // Log error but don't fail - fall back to original messages + logger.error(`Error resolving device status messages: ${e}`) + return [] + } +} + +/** + * Resolve a TSR ActionExecutionResult using the Studio blueprint's deviceActionMessages. + * If the result has a structured `code` and `context`, and the blueprint defines a custom + * message template for that code, the `response` field is replaced with the resolved message. + * When the blueprint suppresses the message (empty string template), `response` is cleared. + * + * @param deviceId - The peripheral device ID (used to look up the studio and blueprint) + * @param result - The action execution result from TSR + * @returns The result with `response` resolved, cleared on suppression, or unchanged when no custom message applies + */ +export async function resolveActionResult( + deviceId: PeripheralDeviceId, + result: TSR.ActionExecutionResult +): Promise { + if (result.result === TSR.ActionExecutionResultCode.Ok) return result + if (!result.code) return result + + try { + const device = (await PeripheralDevices.findOneAsync(deviceId, { + projection: { name: 1, studioAndConfigId: 1, parentDeviceId: 1 }, + })) as Pick | undefined + + if (!device) return result + + // Child devices (like casparcg0) don't have studioAndConfigId directly - get it from parent + let studioId = device.studioAndConfigId?.studioId + if (!studioId && device.parentDeviceId) { + const parentDevice = await PeripheralDevices.findOneAsync(device.parentDeviceId, { + projection: { studioAndConfigId: 1 }, + }) + studioId = parentDevice?.studioAndConfigId?.studioId + } + + if (!studioId) return result + + const studioBlueprint = await getStudioBlueprintManifest(studioId) + if (!studioBlueprint) return result + + const { blueprint, manifest: blueprintManifest } = studioBlueprint + + if (!blueprintManifest.deviceActionMessages) return result + + const resolver = new StatusMessageResolver(blueprint._id, blueprintManifest.deviceActionMessages, undefined) + + // Use the existing TSR response as the fallback default message + const defaultMessage = result.response?.key ?? '' + + const resolved = resolver.getDeviceStatusMessage( + result.code, + { + ...(result.context ?? {}), + deviceName: device.name, + deviceId: unprotectString(deviceId), + }, + defaultMessage + ) + + if (resolved === null) { + // Message suppressed by blueprint - clear response so the UI doesn't show the raw TSR message + return { + ...result, + response: { key: '' }, + } + } + + // resolved.key is either the custom blueprint message or the defaultMessage + if (resolved.key === defaultMessage) { + // No custom message found - keep original response unchanged + return result + } + + const interpolated = interpollateTranslation(resolved.key, resolved.args) + return { + ...result, + response: { key: interpolated }, + } + } catch (e) { + logger.error(`Error resolving device action messages: ${e}`) + return result + } +} + export namespace ServerPeripheralDeviceAPI { export async function initialize( context: MethodContext, @@ -141,6 +357,7 @@ export namespace ServerPeripheralDeviceAPI { created: getCurrentTime(), status: { statusCode: StatusCode.UNKNOWN, + statusDetails: [], }, connected: true, connectionId: options.connectionId, @@ -203,6 +420,37 @@ export namespace ServerPeripheralDeviceAPI { throw new Meteor.Error(400, 'device status code is not known') } + // Resolve status messages using Studio blueprint if structured status details are present + // Child devices (like casparcg0) don't have studioAndConfigId directly - get it from parent + let studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId && peripheralDevice.parentDeviceId) { + const parentDevice = await PeripheralDevices.findOneAsync(peripheralDevice.parentDeviceId, { + projection: { studioAndConfigId: 1 }, + }) + studioId = parentDevice?.studioAndConfigId?.studioId + } + + logger.info( + `Device ${deviceId} setStatus: statusDetails=${status.statusDetails?.length ?? 'undefined'}, messages=${status.messages?.length ?? 'undefined'}, studioId=${studioId ?? 'none'}` + ) + if (status.statusDetails && status.statusDetails.length > 0) { + if (studioId) { + const resolvedMessages = await resolveDeviceStatusDetails( + studioId, + peripheralDevice.name, + peripheralDevice._id, + status.statusDetails, + status.messages ?? [] + ) + // Use blueprint-resolved messages if available, otherwise fall back to statusDetails messages + status.messages = + resolvedMessages.length > 0 ? resolvedMessages : status.statusDetails.map((d) => d.message) + } else { + // No studio context, derive messages directly from statusDetails + status.messages = status.statusDetails.map((d) => d.message) + } + } + // check if we have to update something: if (!_.isEqual(status, peripheralDevice.status)) { logger.info( @@ -312,6 +560,32 @@ export namespace ServerPeripheralDeviceAPI { transaction?.end() } + export async function reportExternalEvents( + context: MethodContext, + deviceId: PeripheralDeviceId, + token: string, + events: PeripheralDeviceExternalEvent[] + ): Promise { + check(events, Array) + + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) + + if (!peripheralDevice.studioAndConfigId) + throw new Error(`PeripheralDevice "${peripheralDevice._id}" sent reportExternalEvents, but has no studioId`) + + if (!events.length) return + + const studioId = peripheralDevice.studioAndConfigId.studioId + // An arbitrary cap, to avoid unbound memory growth + const MAX_PENDING_EXTERNAL_EVENTS = 1000 + + // Merge events into the last pending OnExternalEvents job in the queue, or enqueue a new one. + // This prevents queue flooding when many events arrive in a burst, or when multiple gateways + // report events for the same studio simultaneously. + QueueOrUpdateStudioJob(StudioJobs.OnExternalEvents, studioId, (existing) => ({ + events: [...(existing?.events ?? []), ...events].slice(-MAX_PENDING_EXTERNAL_EVENTS), + })) + } export async function pingWithCommand( context: MethodContext, deviceId: PeripheralDeviceId, @@ -883,6 +1157,13 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri ) { return ServerPeripheralDeviceAPI.playoutPlaybackChanged(this, deviceId, deviceToken, changedResults) } + async reportExternalEvents( + deviceId: PeripheralDeviceId, + deviceToken: string, + events: PeripheralDeviceExternalEvent[] + ) { + return ServerPeripheralDeviceAPI.reportExternalEvents(this, deviceId, deviceToken, events) + } async reportResolveDone( deviceId: PeripheralDeviceId, deviceToken: string, diff --git a/meteor/server/api/rest/v1/__tests__/playlists.spec.ts b/meteor/server/api/rest/v1/__tests__/playlists.spec.ts new file mode 100644 index 00000000000..33a1262dce0 --- /dev/null +++ b/meteor/server/api/rest/v1/__tests__/playlists.spec.ts @@ -0,0 +1,228 @@ +import { registerRoutes } from '../playlists' +import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { PlaylistsRestAPI } from '../../../../lib/rest/v1' + +describe('Playlists REST API Routes', () => { + let mockRegisterRoute: jest.Mock + let mockServerAPI: jest.Mocked + + beforeEach(() => { + mockRegisterRoute = jest.fn() + mockServerAPI = { + tTimerStartCountdown: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerStartFreeRun: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerPause: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerResume: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerRestart: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerClearProjected: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerSetProjectedAnchorPart: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerSetProjectedTime: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerSetProjectedDuration: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + } as any + + registerRoutes(mockRegisterRoute) + }) + + test('should register T-timer countdown route', () => { + const countdownRoute = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/countdown' + ) + expect(countdownRoute).toBeDefined() + expect(countdownRoute[0]).toBe('post') + }) + + test('T-timer countdown handler should call serverAPI.tTimerStartCountdown', async () => { + const countdownRoute = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/countdown' + ) + const handler = countdownRoute[4] + + const params = { playlistId: 'playlist0', timerIndex: '1' } + const body = { duration: 60, stopAtZero: true, startPaused: false } + const connection = {} as any + const event = 'test-event' + + await handler(mockServerAPI, connection, event, params, body) + + expect(mockServerAPI.tTimerStartCountdown).toHaveBeenCalledWith( + connection, + event, + protectString('playlist0'), + 1, + 60, + true, + false + ) + }) + + test('T-timer countdown handler should reject malformed timerIndex', async () => { + const countdownRoute = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/countdown' + ) + const handler = countdownRoute[4] + + const params = { playlistId: 'playlist0', timerIndex: '1foo' } + const body = { duration: 60, stopAtZero: true, startPaused: false } + const connection = {} as any + const event = 'test-event' + + await expect(handler(mockServerAPI, connection, event, params, body)).rejects.toMatchObject({ error: 400 }) + expect(mockServerAPI.tTimerStartCountdown).not.toHaveBeenCalled() + }) + + test('T-timer countdown handler should reject out-of-range timerIndex', async () => { + const countdownRoute = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/countdown' + ) + const handler = countdownRoute[4] + + const params = { playlistId: 'playlist0', timerIndex: '4' } + const body = { duration: 60, stopAtZero: true, startPaused: false } + const connection = {} as any + const event = 'test-event' + + await expect(handler(mockServerAPI, connection, event, params, body)).rejects.toMatchObject({ error: 400 }) + expect(mockServerAPI.tTimerStartCountdown).not.toHaveBeenCalled() + }) + + test('should register T-timer pause route', () => { + const pauseRoute = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/pause' + ) + expect(pauseRoute).toBeDefined() + expect(pauseRoute[0]).toBe('post') + }) + + test('T-timer pause handler should call serverAPI.tTimerPause', async () => { + const pauseRoute = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/pause' + ) + const handler = pauseRoute[4] + + const params = { playlistId: 'playlist0', timerIndex: '2' } + const connection = {} as any + const event = 'test-event' + + await handler(mockServerAPI, connection, event, params, {}) + + expect(mockServerAPI.tTimerPause).toHaveBeenCalledWith(connection, event, protectString('playlist0'), 2) + }) + + test('T-timer projected clear handler should accept playlist externalId', async () => { + const route = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/projected/clear' + ) + expect(route).toBeDefined() + const handler = route[4] + + const params = { playlistId: 'playlistExternalId', timerIndex: '1' } + const connection = {} as any + const event = 'test-event' + + await handler(mockServerAPI, connection, event, params, {}) + + expect(mockServerAPI.tTimerClearProjected).toHaveBeenCalledWith( + connection, + event, + protectString('playlistExternalId'), + 1 + ) + }) + + test('T-timer projected anchor-part handler should accept playlist externalId', async () => { + const route = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/projected/anchor-part' + ) + expect(route).toBeDefined() + const handler = route[4] + + const params = { playlistId: 'playlistExternalId', timerIndex: '2' } + const body = { externalId: 'partExternalId' } + const connection = {} as any + const event = 'test-event' + + await handler(mockServerAPI, connection, event, params, body) + + expect(mockServerAPI.tTimerSetProjectedAnchorPart).toHaveBeenCalledWith( + connection, + event, + protectString('playlistExternalId'), + 2, + undefined, + 'partExternalId' + ) + }) + + test('T-timer projected anchor-part handler should accept partId', async () => { + const route = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/projected/anchor-part' + ) + expect(route).toBeDefined() + const handler = route[4] + + const params = { playlistId: 'playlistExternalId', timerIndex: '2' } + const body = { partId: 'partInternalId' } + const connection = {} as any + const event = 'test-event' + + await handler(mockServerAPI, connection, event, params, body) + + expect(mockServerAPI.tTimerSetProjectedAnchorPart).toHaveBeenCalledWith( + connection, + event, + protectString('playlistExternalId'), + 2, + protectString('partInternalId'), + undefined + ) + }) + + test('T-timer projected time handler should accept playlist externalId', async () => { + const route = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/projected/time' + ) + expect(route).toBeDefined() + const handler = route[4] + + const params = { playlistId: 'playlistExternalId', timerIndex: '3' } + const body = { time: 1707024000000, paused: false } + const connection = {} as any + const event = 'test-event' + + await handler(mockServerAPI, connection, event, params, body) + + expect(mockServerAPI.tTimerSetProjectedTime).toHaveBeenCalledWith( + connection, + event, + protectString('playlistExternalId'), + 3, + 1707024000000, + false + ) + }) + + test('T-timer projected duration handler should accept playlist externalId', async () => { + const route = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/projected/duration' + ) + expect(route).toBeDefined() + const handler = route[4] + + const params = { playlistId: 'playlistExternalId', timerIndex: '1' } + const body = { duration: 60000, paused: true } + const connection = {} as any + const event = 'test-event' + + await handler(mockServerAPI, connection, event, params, body) + + expect(mockServerAPI.tTimerSetProjectedDuration).toHaveBeenCalledWith( + connection, + event, + protectString('playlistExternalId'), + 1, + 60000, + true + ) + }) +}) diff --git a/meteor/server/api/rest/v1/ingest.ts b/meteor/server/api/rest/v1/ingest.ts index da2bd206e0f..32fc262a36a 100644 --- a/meteor/server/api/rest/v1/ingest.ts +++ b/meteor/server/api/rest/v1/ingest.ts @@ -9,7 +9,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { getRundownNrcsName, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index cdd3e69174b..50b68accb8e 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -14,6 +14,7 @@ import { RundownPlaylistId, SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import { Match, check } from '../../../lib/check' import { PlaylistsRestAPI } from '../../../lib/rest/v1' import { Meteor } from 'meteor/meteor' @@ -30,7 +31,7 @@ import { RundownPlaylists, Segments, } from '../../../collections' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ServerClientAPI } from '../../client' import { QueueNextSegmentResult, StudioJobs, TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio' import { getCurrentTime } from '../../../lib/lib' @@ -38,6 +39,15 @@ import { TriggerReloadDataResponse } from '@sofie-automation/meteor-lib/dist/api import { ServerRundownAPI } from '../../rundown' import { triggerWriteAccess } from '../../../security/securityVerify' +function parseTimerIndex(rawTimerIndex: string): RundownTTimerIndex { + const timerIndex = Number(rawTimerIndex) + if (!Number.isInteger(timerIndex) || timerIndex < 1 || timerIndex > 3) { + throw new Meteor.Error(400, `Invalid timerIndex`) + } + + return timerIndex as RundownTTimerIndex +} + class PlaylistsServerAPI implements PlaylistsRestAPI { constructor(private context: ServerAPIContext) {} @@ -647,6 +657,261 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { } ) } + + async tTimerStartCountdown( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + duration: number, + stopAtZero?: boolean, + startPaused?: boolean + ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + playlist._id, + () => { + check(playlist._id, String) + check(timerIndex, Number) + check(duration, Number) + check(stopAtZero, Match.Optional(Boolean)) + check(startPaused, Match.Optional(Boolean)) + }, + StudioJobs.TTimerStartCountdown, + { + playlistId: playlist._id, + timerIndex, + duration, + stopAtZero: !!stopAtZero, + startPaused: !!startPaused, + } + ) + } + + async tTimerStartFreeRun( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + startPaused?: boolean + ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + playlist._id, + () => { + check(playlist._id, String) + check(timerIndex, Number) + check(startPaused, Match.Optional(Boolean)) + }, + StudioJobs.TTimerStartFreeRun, + { + playlistId: playlist._id, + timerIndex, + startPaused: !!startPaused, + } + ) + } + + async tTimerPause( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + playlist._id, + () => { + check(playlist._id, String) + check(timerIndex, Number) + }, + StudioJobs.TTimerPause, + { + playlistId: playlist._id, + timerIndex, + } + ) + } + + async tTimerResume( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + playlist._id, + () => { + check(playlist._id, String) + check(timerIndex, Number) + }, + StudioJobs.TTimerResume, + { + playlistId: playlist._id, + timerIndex, + } + ) + } + + async tTimerRestart( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + playlist._id, + () => { + check(playlist._id, String) + check(timerIndex, Number) + }, + StudioJobs.TTimerRestart, + { + playlistId: playlist._id, + timerIndex, + } + ) + } + + async tTimerClearProjected( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + playlist._id, + () => { + check(playlist._id, String) + check(timerIndex, Number) + }, + StudioJobs.TTimerClearProjected, + { + playlistId: playlist._id, + timerIndex, + } + ) + } + + async tTimerSetProjectedAnchorPart( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + partId?: PartId, + externalId?: string + ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + playlist._id, + () => { + check(playlist._id, String) + check(timerIndex, Number) + check(partId, Match.Optional(String)) + check(externalId, Match.Optional(String)) + }, + StudioJobs.TTimerSetProjectedAnchorPart, + { + playlistId: playlist._id, + timerIndex, + partId, + externalId, + } + ) + } + + async tTimerSetProjectedTime( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + time: number, + paused?: boolean + ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + playlist._id, + () => { + check(playlist._id, String) + check(timerIndex, Number) + check(time, Number) + check(paused, Match.Optional(Boolean)) + }, + StudioJobs.TTimerSetProjectedTime, + { + playlistId: playlist._id, + timerIndex, + time, + paused: !!paused, + } + ) + } + + async tTimerSetProjectedDuration( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + duration: number, + paused?: boolean + ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + playlist._id, + () => { + check(playlist._id, String) + check(timerIndex, Number) + check(duration, Number) + check(paused, Match.Optional(Boolean)) + }, + StudioJobs.TTimerSetProjectedDuration, + { + playlistId: playlist._id, + timerIndex, + duration, + paused: !!paused, + } + ) + } } class PlaylistsAPIFactory implements APIFactory { @@ -724,7 +989,9 @@ export function registerRoutes(registerRoute: APIRegisterHook) 'post', '/playlists/:playlistId/execute-adlib', new Map([ + [400, []], [404, [UserErrorMessage.RundownPlaylistNotFound]], + [409, [UserErrorMessage.ValidationFailed]], [412, [UserErrorMessage.InactiveRundown, UserErrorMessage.NoCurrentPart, UserErrorMessage.AdlibNotFound]], ]), playlistsAPIFactory, @@ -760,7 +1027,9 @@ export function registerRoutes(registerRoute: APIRegisterHook) 'post', '/playlists/:playlistId/execute-bucket-adlib', new Map([ + [400, []], [404, [UserErrorMessage.RundownPlaylistNotFound]], + [409, [UserErrorMessage.ValidationFailed]], [ 412, [ @@ -999,4 +1268,203 @@ export function registerRoutes(registerRoute: APIRegisterHook) return await serverAPI.recallStickyPiece(connection, event, playlistId, sourceLayerId) } ) + + registerRoute< + { playlistId: string; timerIndex: string }, + { duration: number; stopAtZero?: boolean; startPaused?: boolean }, + void + >( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/countdown', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, body) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = parseTimerIndex(params.timerIndex) + logger.info(`API POST: t-timer countdown ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerStartCountdown( + connection, + event, + rundownPlaylistId, + timerIndex, + body.duration, + body.stopAtZero, + body.startPaused + ) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, { startPaused?: boolean }, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/free-run', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, body) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = parseTimerIndex(params.timerIndex) + logger.info(`API POST: t-timer free-run ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerStartFreeRun( + connection, + event, + rundownPlaylistId, + timerIndex, + body.startPaused + ) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, never, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/pause', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, _) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = parseTimerIndex(params.timerIndex) + logger.info(`API POST: t-timer pause ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerPause(connection, event, rundownPlaylistId, timerIndex) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, never, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/resume', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, _) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = parseTimerIndex(params.timerIndex) + logger.info(`API POST: t-timer resume ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerResume(connection, event, rundownPlaylistId, timerIndex) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, never, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/restart', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, _) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = parseTimerIndex(params.timerIndex) + logger.info(`API POST: t-timer restart ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerRestart(connection, event, rundownPlaylistId, timerIndex) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, never, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/projected/clear', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, _) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = parseTimerIndex(params.timerIndex) + logger.info(`API POST: t-timer projected clear ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerClearProjected(connection, event, rundownPlaylistId, timerIndex) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, { partId?: string; externalId?: string }, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/projected/anchor-part', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, body) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = parseTimerIndex(params.timerIndex) + logger.info(`API POST: t-timer projected anchor-part ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + check(body.partId, Match.Optional(String)) + check(body.externalId, Match.Optional(String)) + + if (!body.partId && !body.externalId) { + throw new Meteor.Error(400, `Must provide either 'partId' or 'externalId'`) + } + + const partId = body.partId ? protectString(body.partId) : undefined + const externalId = body.externalId + + return await serverAPI.tTimerSetProjectedAnchorPart( + connection, + event, + rundownPlaylistId, + timerIndex, + partId, + externalId + ) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, { time: number; paused?: boolean }, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/projected/time', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, body) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = parseTimerIndex(params.timerIndex) + logger.info(`API POST: t-timer projected time ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + check(body.time, Number) + check(body.paused, Match.Optional(Boolean)) + + return await serverAPI.tTimerSetProjectedTime( + connection, + event, + rundownPlaylistId, + timerIndex, + body.time, + body.paused + ) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, { duration: number; paused?: boolean }, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/projected/duration', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, body) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = parseTimerIndex(params.timerIndex) + logger.info(`API POST: t-timer projected duration ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + check(body.duration, Number) + check(body.paused, Match.Optional(Boolean)) + + return await serverAPI.tTimerSetProjectedDuration( + connection, + event, + rundownPlaylistId, + timerIndex, + body.duration, + body.paused + ) + } + ) } diff --git a/meteor/server/api/rest/v1/showstyles.ts b/meteor/server/api/rest/v1/showstyles.ts index 9c434b6ac05..08a98690230 100644 --- a/meteor/server/api/rest/v1/showstyles.ts +++ b/meteor/server/api/rest/v1/showstyles.ts @@ -23,7 +23,7 @@ import { validateAPIBlueprintConfigForShowStyle, } from './typeConversion' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { runUpgradeForShowStyleBase, validateConfigForShowStyleBase } from '../../../migration/upgrades' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { assertNever } from '@sofie-automation/corelib/dist/lib' diff --git a/meteor/server/api/rest/v1/studios.ts b/meteor/server/api/rest/v1/studios.ts index be8fd376423..c4325e13e73 100644 --- a/meteor/server/api/rest/v1/studios.ts +++ b/meteor/server/api/rest/v1/studios.ts @@ -10,7 +10,7 @@ import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { PeripheralDevices, RundownPlaylists, Studios } from '../../../collections' import { APIStudioFrom, studioFrom, validateAPIBlueprintConfigForStudio } from './typeConversion' import { runUpgradeForStudio, updateStudioBaseline, validateConfigForStudio } from '../../../migration/upgrades' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ServerClientAPI } from '../../client' import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' import { getCurrentTime } from '../../../lib/lib' diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index 6d7f3b61888..e09d044036c 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -55,7 +55,7 @@ import { DEFAULT_FALLBACK_PART_DURATION, } from '@sofie-automation/shared-lib/dist/core/constants' import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' -import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' +import { ForceQuickLoopAutoNext, ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { PlaylistSnapshotOptions, SystemSnapshotOptions } from '@sofie-automation/meteor-lib/dist/api/shapshot' /* @@ -424,6 +424,7 @@ export function studioSettingsFrom(apiStudioSettings: APIStudioSettings): Comple allowPieceDirectPlay: apiStudioSettings.allowPieceDirectPlay ?? true, // Backwards compatible enableBuckets: apiStudioSettings.enableBuckets ?? true, // Backwards compatible enableEvaluationForm: apiStudioSettings.enableEvaluationForm ?? true, // Backwards compatible + shelfAdlibButtonSize: apiStudioSettings.shelfAdlibButtonSize ?? ShelfButtonSize.LARGE, mockPieceContentStatus: apiStudioSettings.mockPieceContentStatus, rundownGlobalPiecesPrepareTime: apiStudioSettings.rundownGlobalPiecesPrepareTime, } @@ -452,6 +453,7 @@ export function APIStudioSettingsFrom(settings: IStudioSettings): Complete { /** Max count of one type of items to include in the snapshot */ const LIMIT_COUNT = 500 @@ -343,6 +344,8 @@ async function createRundownPlaylistSnapshot( playlistId: playlist._id, full: !!options.withArchivedDocuments, withTimeline: !!options.withTimeline, + snapshotId, + reason, }) const coreResult = await queuedJob.complete const coreSnapshot: CoreRundownPlaylistSnapshot = JSONBlobParse(coreResult.snapshotJson) @@ -693,19 +696,63 @@ export async function storeSystemSnapshot( return internalStoreSystemSnapshot(options, reason) } +/** + * Runs {@link StudioJobs.OnSystemSnapshotCreated} for each studio after a snapshot is stored. + * + * Studio-scoped system snapshots run one job; full-system snapshots run one job per studio in the snapshot. + * Waits for each blueprint hook to finish; hook failures are logged and do not fail snapshot storage. + */ +async function queueOnSystemSnapshotCreatedJobs( + storedId: SnapshotId, + reason: string, + type: BlueprintSnapshotType, + options: SystemSnapshotOptions, + studioIds: StudioId[] +): Promise { + const fullSystem = !options.studioId + + for (const studioId of studioIds) { + try { + const job = await QueueStudioJob(StudioJobs.OnSystemSnapshotCreated, studioId, { + snapshotId: storedId, + reason, + type, + options: { + studioId, + withDeviceSnapshots: options.withDeviceSnapshots, + fullSystem, + }, + }) + await job.complete + } catch (err) { + logger.error( + `OnSystemSnapshotCreated failed for snapshot ${storedId} (studio ${studioId}, withDeviceSnapshots=${options.withDeviceSnapshots}): ${stringifyError(err)}` + ) + } + } +} + /** Take and store a system snapshot. For internal use only, performs no access control. */ export async function internalStoreSystemSnapshot(options: SystemSnapshotOptions, reason: string): Promise { check(options.studioId, Match.Optional(String)) const s = await createSystemSnapshot(options) - return storeSnaphot(s, reason) + const storedId = await storeSnaphot(s, reason) + + // Full-system snapshots: one blueprint hook job per studio in the snapshot + const studioIds = options.studioId ? [options.studioId] : s.studios.map((studio) => studio._id) + if (studioIds.length > 0) { + await queueOnSystemSnapshotCreatedJobs(storedId, reason, 'system', options, studioIds) + } + + return storedId } export async function storeRundownPlaylistSnapshot( playlist: VerifiedRundownPlaylistForUserAction, options: PlaylistSnapshotOptions, reason: string ): Promise { - const s = await createRundownPlaylistSnapshot(playlist, options) + const s = await createRundownPlaylistSnapshot(playlist, options, reason) return storeSnaphot(s, reason) } export async function internalStoreRundownPlaylistSnapshot( @@ -713,7 +760,7 @@ export async function internalStoreRundownPlaylistSnapshot( options: PlaylistSnapshotOptions, reason: string ): Promise { - const s = await createRundownPlaylistSnapshot(playlist, options) + const s = await createRundownPlaylistSnapshot(playlist, options, reason) return storeSnaphot(s, reason) } export async function storeDebugSnapshot( @@ -731,7 +778,13 @@ export async function storeDebugSnapshot( assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_SNAPSHOT_MANAGEMENT) const s = await createDebugSnapshot(studioId) - return storeSnaphot(s, reason) + const storedId = await storeSnaphot(s, reason) + + await queueOnSystemSnapshotCreatedJobs(storedId, reason, 'debug', { studioId, withDeviceSnapshots: true }, [ + studioId, + ]) + + return storedId } export async function restoreSnapshot( context: MethodContext, diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index 5e9a3d94c30..0e245cab794 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -28,6 +28,7 @@ import { logger } from '../../logging' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' import { assertConnectionHasOneOfPermissions } from '../../security/auth' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' const PERMISSIONS_FOR_MANAGE_STUDIOS: Array = ['configure'] @@ -64,6 +65,7 @@ export async function insertStudioInner(newId?: StudioId): Promise { allowPieceDirectPlay: false, enableBuckets: true, enableEvaluationForm: true, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, }), _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), diff --git a/meteor/server/api/studio/lib.ts b/meteor/server/api/studio/lib.ts index dc0c161793d..b8e317c2885 100644 --- a/meteor/server/api/studio/lib.ts +++ b/meteor/server/api/studio/lib.ts @@ -1,7 +1,7 @@ import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { getCurrentTime } from '../../lib/lib' import { ExpectedPackages, PackageInfos, PeripheralDevices, RundownPlaylists } from '../../collections' diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index ef467b48c60..545ddfb74e2 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -43,7 +43,7 @@ import { import { NrcsIngestDataCache, Parts, Pieces, Rundowns } from '../collections' import { NrcsIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/NrcsIngestDataCache' import { verifyHashedToken } from './singleUseTokens' -import { QuickLoopMarker } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { QuickLoopMarker } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { runIngestOperation } from './ingest/lib' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' diff --git a/meteor/server/collections/rundown.ts b/meteor/server/collections/rundown.ts index c084b62c41e..c3fb4e1d174 100644 --- a/meteor/server/collections/rundown.ts +++ b/meteor/server/collections/rundown.ts @@ -9,12 +9,12 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineObj' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { PartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { createAsyncOnlyReadOnlyMongoCollection } from './collection' import { registerIndex } from './indices' -import { PartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' export const AdLibActions = createAsyncOnlyReadOnlyMongoCollection(CollectionName.AdLibActions) registerIndex(AdLibActions, { @@ -68,11 +68,10 @@ registerIndex(PartInstances, { }) registerIndex(PartInstances, { rundownId: 1, - // @ts-expect-error deep property 'part._id': 1, takeCount: 1, reset: 1, -}) +} as any) export const Parts = createAsyncOnlyReadOnlyMongoCollection(CollectionName.Parts) registerIndex(Parts, { diff --git a/meteor/server/coreSystem/index.ts b/meteor/server/coreSystem/index.ts index f5dadbb8d24..2348e408c38 100644 --- a/meteor/server/coreSystem/index.ts +++ b/meteor/server/coreSystem/index.ts @@ -18,6 +18,8 @@ import { checkDatabaseVersions } from './checkDatabaseVersions' import PLazy from 'p-lazy' import { getCoreSystemAsync } from './collection' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +const mosPkgJson = require('@mos-connection/helper/package.json') +const superTimelinePkgJson = require('superfly-timeline/package.json') export { PackageInfo } @@ -125,30 +127,10 @@ function onCoreSystemChanged() { export const RelevantSystemVersions = PLazy.from(async () => { const versions: { [name: string]: string } = {} - const dependencies: any = PackageInfo.dependencies - if (dependencies) { - const libNames: string[] = ['@mos-connection/helper', 'superfly-timeline'] - - const getRealVersion = async (name: string, fallback: string): Promise => { - try { - const pkgInfo = require(name + '/package.json') - return pkgInfo.version - } catch (e) { - logger.warn(`Failed to read version of package "${name}": ${stringifyError(e)}`) - return parseVersion(fallback) - } - } - - await Promise.all([ - ...libNames.map(async (name) => { - versions[name] = await getRealVersion(name, dependencies[name]) - }), - ]) - versions['core'] = PackageInfo.versionExtended || PackageInfo.version // package version - versions['timeline-state-resolver-types'] = TMP_TSR_VERSION - } else { - logger.error(`Core package dependencies missing`) - } + versions['@mos-connection/helper'] = mosPkgJson.version + versions['superfly-timeline'] = superTimelinePkgJson.version + versions['core'] = PackageInfo.versionExtended || PackageInfo.version // package version + versions['timeline-state-resolver-types'] = TMP_TSR_VERSION return versions }) diff --git a/meteor/server/cronjobs.ts b/meteor/server/cronjobs.ts index eba5011a248..e45704cbaef 100644 --- a/meteor/server/cronjobs.ts +++ b/meteor/server/cronjobs.ts @@ -26,7 +26,7 @@ import { translateMessage, } from '@sofie-automation/corelib/dist/TranslatableMessage' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' const lowPrioFcn = (fcn: () => any) => { diff --git a/meteor/server/lib/rest/v1/playlists.ts b/meteor/server/lib/rest/v1/playlists.ts index 778ff2b04d2..c50c53a82dc 100644 --- a/meteor/server/lib/rest/v1/playlists.ts +++ b/meteor/server/lib/rest/v1/playlists.ts @@ -12,6 +12,7 @@ import { SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { QueueNextSegmentResult, TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio' +import { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import { Meteor } from 'meteor/meteor' /* ************************************************************************* @@ -271,4 +272,146 @@ export interface PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, sourceLayerId: string ): Promise> + /** + * Configure a T-timer as a countdown. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + * @param duration Duration in seconds. + * @param stopAtZero Whether to stop at zero. + * @param startPaused Whether to start paused. + */ + tTimerStartCountdown( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + duration: number, + stopAtZero?: boolean, + startPaused?: boolean + ): Promise> + + /** + * Configure a T-timer as a free-running timer. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + * @param startPaused Whether to start paused. + */ + tTimerStartFreeRun( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + startPaused?: boolean + ): Promise> + /** + * Pause a T-timer. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + */ + tTimerPause( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> + /** + * Resume a T-timer. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + */ + tTimerResume( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> + /** + * Restart a T-timer. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + */ + tTimerRestart( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> + + /** + * Clear any projection state (manual or anchor-based) for a T-timer. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + */ + tTimerClearProjected( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> + + /** + * Set the anchor part for automatic projection calculation. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + * @param partId Part id (internal) or part externalId (will be resolved by the worker) + * @param externalId Part externalId (will be resolved by the worker) + */ + tTimerSetProjectedAnchorPart( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + partId?: PartId, + externalId?: string + ): Promise> + + /** + * Set the projection as an absolute timestamp. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + * @param time Unix timestamp (ms) + * @param paused If true, the projection is treated as paused (doesn't count down with time) + */ + tTimerSetProjectedTime( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + time: number, + paused?: boolean + ): Promise> + + /** + * Set the projection as a duration from now. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + * @param duration Duration in milliseconds. + * @param paused If true, the projection is treated as paused (doesn't count down with time) + */ + tTimerSetProjectedDuration( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + duration: number, + paused?: boolean + ): Promise> } diff --git a/meteor/server/lib/rest/v1/studios.ts b/meteor/server/lib/rest/v1/studios.ts index c94e9429624..898f5bda180 100644 --- a/meteor/server/lib/rest/v1/studios.ts +++ b/meteor/server/lib/rest/v1/studios.ts @@ -1,6 +1,7 @@ import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Meteor } from 'meteor/meteor' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' /* ************************************************************************* This file contains types and interfaces that are used by the REST API. @@ -223,6 +224,8 @@ export interface APIStudioSettings { allowPieceDirectPlay?: boolean enableBuckets?: boolean enableEvaluationForm?: boolean + /** Default size of AdLib buttons in the mini shelf */ + shelfAdlibButtonSize?: Exclude mockPieceContentStatus?: boolean rundownGlobalPiecesPrepareTime?: number } diff --git a/meteor/server/migration/0_1_0.ts b/meteor/server/migration/0_1_0.ts index 70449b205aa..6579ea032f4 100644 --- a/meteor/server/migration/0_1_0.ts +++ b/meteor/server/migration/0_1_0.ts @@ -6,6 +6,7 @@ import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objec import { ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ShowStyleBases, ShowStyleVariants, Studios } from '../collections' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' /** * This file contains system specific migration steps. @@ -37,6 +38,7 @@ export const addSteps = addMigrationSteps('0.1.0', [ allowPieceDirectPlay: false, enableBuckets: true, enableEvaluationForm: true, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, }), mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 238ea09fadd..f67dc3b7054 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -1,7 +1,8 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' -import { RundownPlaylists } from '../collections' +import { RundownPlaylists, Segments, Studios } from '../collections' import { ContainerIdsToObjectWithOverridesMigrationStep } from './steps/X_X_X/ContainerIdsToObjectWithOverridesMigrationStep' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' /* * ************************************************************************************** @@ -83,5 +84,69 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ ) }, }, + { + id: `studios settings create default shelfAdlibButtonSize=large`, + canBeRunAutomatically: true, + validate: async () => { + const studios = await Studios.findFetchAsync({ + 'settingsWithOverrides.defaults.shelfAdlibButtonSize': { $exists: false }, + }) + + if (studios.length > 0) return `Some studios are missing settings default shelfAdlibButtonSize` + return false + }, + migrate: async () => { + const studios = await Studios.findFetchAsync({ + 'settingsWithOverrides.defaults.shelfAdlibButtonSize': { $exists: false }, + }) + + for (const studio of studios) { + await Studios.updateAsync(studio._id, { + $set: { + 'settingsWithOverrides.defaults.shelfAdlibButtonSize': ShelfButtonSize.LARGE, + }, + }) + } + }, + }, + { + id: `segments migrate showShelf to displayMinishelf`, + canBeRunAutomatically: true, + validate: async () => { + const count = await Segments.countDocuments({ + showShelf: { $exists: true }, + }) + if (count > 0) return `There are ${count} Segments with legacy showShelf` + return false + }, + migrate: async () => { + // showShelf: true => displayMinishelf: inherit (if missing) + await Segments.mutableCollection.updateAsync( + { + showShelf: true, + displayMinishelf: { $exists: false }, + }, + { + $set: { + displayMinishelf: ShelfButtonSize.INHERIT, + }, + }, + { multi: true } + ) + + // Always remove legacy field + await Segments.mutableCollection.updateAsync( + { + showShelf: { $exists: true }, + }, + { + $unset: { + showShelf: 1, + }, + }, + { multi: true } + ) + }, + }, // Add your migration here ]) diff --git a/meteor/server/publications/_publications.ts b/meteor/server/publications/_publications.ts index 64a027a2791..501d8fa4d15 100644 --- a/meteor/server/publications/_publications.ts +++ b/meteor/server/publications/_publications.ts @@ -4,6 +4,7 @@ import './lib/lib' import './buckets' import './blueprintUpgradeStatus/publication' import './ingestStatus/publication' +import './externalEventSubscriptions' import './packageManager/expectedPackages/publication' import './packageManager/packageContainers' import './packageManager/playoutContext' diff --git a/meteor/server/publications/externalEventSubscriptions.ts b/meteor/server/publications/externalEventSubscriptions.ts new file mode 100644 index 00000000000..acf8b14d559 --- /dev/null +++ b/meteor/server/publications/externalEventSubscriptions.ts @@ -0,0 +1,179 @@ +import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { assertNever, getHash, literal } from '@sofie-automation/corelib/dist/lib' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { ReadonlyDeep } from 'type-fest' +import { + CustomPublish, + CustomPublishCollection, + meteorCustomPublish, + setUpCollectionOptimizedObserver, + SetupObserversResult, + TriggerUpdate, +} from '../lib/customPublication' +import { logger } from '../logging' +import { RundownPlaylists, Rundowns } from '../collections' +import { + PeripheralDevicePubSub, + PeripheralDevicePubSubCollectionsNames, + ExternalEventSubscriptionDocument, + ExternalEventSubscriptionId, +} from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' +import type { PeripheralDeviceExternalEvent } from '@sofie-automation/shared-lib/dist/peripheralDevice/externalEvents' +import { checkAccessAndGetPeripheralDevice } from '../security/check' +import { check } from '../lib/check' + +type RundownPlaylistFields = '_id' | 'activationId' +const rundownPlaylistFieldSpecifier = literal< + MongoFieldSpecifierOnesStrict> +>({ + _id: 1, + activationId: 1, +}) + +type RundownFields = '_id' | 'playlistId' | 'externalEventSubscriptions' +const rundownFieldSpecifier = literal>>({ + _id: 1, + playlistId: 1, + externalEventSubscriptions: 1, +}) + +interface ExternalEventSubscriptionsArgs { + readonly studioId: StudioId + readonly type: PeripheralDeviceExternalEvent['type'] +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface ExternalEventSubscriptionsState {} + +interface ExternalEventSubscriptionsUpdateProps { + invalidateAll: true +} + +async function setupExternalEventSubscriptionsObservers( + args: ReadonlyDeep, + triggerUpdate: TriggerUpdate +): Promise { + const trigger = () => triggerUpdate({ invalidateAll: true }) + + return [ + // Observe active playlists in the studio — activation/deactivation changes which rundowns are in scope + RundownPlaylists.observeChanges( + { studioId: args.studioId }, + { added: trigger, changed: trigger, removed: trigger }, + { projection: rundownPlaylistFieldSpecifier } + ), + // Observe rundowns in the studio — react only when externalEventSubscriptions or playlistId changes + Rundowns.observeChanges( + { studioId: args.studioId }, + { added: trigger, changed: trigger, removed: trigger }, + { projection: rundownFieldSpecifier } + ), + ] +} + +async function manipulateExternalEventSubscriptionsData( + args: ReadonlyDeep, + _state: Partial, + collection: CustomPublishCollection, + _updateProps: Partial> | undefined +): Promise { + // Find all active playlists in the studio + const activePlaylists = (await RundownPlaylists.findFetchAsync( + { studioId: args.studioId, activationId: { $exists: true } }, + { projection: rundownPlaylistFieldSpecifier } + )) as Pick[] + const activePlaylistIds = activePlaylists.map((p) => p._id) + + // Find rundowns belonging to active playlists + const activeRundowns = (await Rundowns.findFetchAsync( + { studioId: args.studioId, playlistId: { $in: activePlaylistIds } }, + { projection: rundownFieldSpecifier } + )) as Pick[] + + // Build the set of valid IDs and the docs to upsert (filtered by type) + const validIds = new Set() + const subsToUpsert: ExternalEventSubscriptionDocument[] = [] + + for (const rundown of activeRundowns) { + for (const sub of rundown.externalEventSubscriptions ?? []) { + if (sub.type !== args.type) continue + + switch (sub.type) { + case 'tsr': { + const id = protectString( + getHash(`tsr_${sub.deviceId}_${sub.deviceType}_${String(sub.event)}`) + ) + validIds.add(id) + subsToUpsert.push({ + _id: id, + type: 'tsr', + deviceId: sub.deviceId, + deviceType: sub.deviceType, + event: sub.event as string, + }) + break + } + default: + assertNever(sub.type) + break + } + } + } + + // Remove docs for subscriptions that are no longer active + collection.remove((doc) => !validIds.has(doc._id)) + + // Upsert each individual subscription doc + for (const sub of subsToUpsert) { + collection.replace(sub) + } +} + +async function startOrJoinExternalEventSubscriptionsPublication( + pub: CustomPublish, + studioId: StudioId, + type: PeripheralDeviceExternalEvent['type'] +) { + await setUpCollectionOptimizedObserver< + ExternalEventSubscriptionDocument, + ExternalEventSubscriptionsArgs, + ExternalEventSubscriptionsState, + ExternalEventSubscriptionsUpdateProps + >( + `pub_${PeripheralDevicePubSub.externalEventSubscriptionsForDevice}_${studioId}_${type}`, + { studioId, type }, + setupExternalEventSubscriptionsObservers, + manipulateExternalEventSubscriptionsData, + pub, + 100 + ) +} + +meteorCustomPublish( + PeripheralDevicePubSub.externalEventSubscriptionsForDevice, + PeripheralDevicePubSubCollectionsNames.externalEventSubscriptions, + async function ( + pub: CustomPublish, + type: PeripheralDeviceExternalEvent['type'], + deviceId: PeripheralDeviceId, + token: string | undefined + ) { + check(deviceId, String) + check(type, String) + + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) + + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) { + logger.warn( + `Publication ${PeripheralDevicePubSub.externalEventSubscriptionsForDevice}: device ${deviceId} has no studio` + ) + return + } + + await startOrJoinExternalEventSubscriptionsPublication(pub, studioId, type) + } +) diff --git a/meteor/server/publications/ingestStatus/reactiveContentCache.ts b/meteor/server/publications/ingestStatus/reactiveContentCache.ts index 87f50a691ac..d9f6b93dd8e 100644 --- a/meteor/server/publications/ingestStatus/reactiveContentCache.ts +++ b/meteor/server/publications/ingestStatus/reactiveContentCache.ts @@ -5,8 +5,8 @@ import type { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/di import type { NrcsIngestDataCacheObj } from '@sofie-automation/corelib/dist/dataModel/NrcsIngestDataCache' import type { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import type { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' export type PlaylistCompact = Pick< DBRundownPlaylist, diff --git a/meteor/server/publications/lib/lib.ts b/meteor/server/publications/lib/lib.ts index 69a06ae2d71..62bb2aa2e42 100644 --- a/meteor/server/publications/lib/lib.ts +++ b/meteor/server/publications/lib/lib.ts @@ -72,7 +72,7 @@ export async function waitForAllObserversReady( ): Promise { // Wait for all the promises to complete // Future: could this fail faster by aborting the rest once the first fails? - const results = await Promise.allSettled(observers) + const results = await Promise.allSettled(observers as Array>) const allSuccessfull = results.filter( (r): r is PromiseFulfilledResult => r.status === 'fulfilled' ) diff --git a/meteor/server/publications/lib/quickLoop.ts b/meteor/server/publications/lib/quickLoop.ts index 03e4aae1165..c04b589ccaf 100644 --- a/meteor/server/publications/lib/quickLoop.ts +++ b/meteor/server/publications/lib/quickLoop.ts @@ -3,7 +3,7 @@ import { DBRundownPlaylist, QuickLoopMarker, QuickLoopMarkerType, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { MarkerPosition, compareMarkerPositions } from '@sofie-automation/corelib/dist/playout/playlist' import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' diff --git a/meteor/server/publications/packageManager/expectedPackages/contentCache.ts b/meteor/server/publications/packageManager/expectedPackages/contentCache.ts index 4d5a6533ed9..a60a66294a3 100644 --- a/meteor/server/publications/packageManager/expectedPackages/contentCache.ts +++ b/meteor/server/publications/packageManager/expectedPackages/contentCache.ts @@ -2,7 +2,7 @@ import { ReactiveCacheCollection } from '../../lib/ReactiveCacheCollection' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' import { ExpectedPackageDB } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' export type RundownPlaylistCompact = Pick< @@ -28,10 +28,11 @@ export const pieceInstanceFieldsSpecifier = literal +export type ExpectedPackageDBCompact = Pick export const expectedPackageDBFieldsSpecifier = literal>({ _id: 1, + rundownId: 1, package: 1, }) diff --git a/meteor/server/publications/packageManager/expectedPackages/generate.ts b/meteor/server/publications/packageManager/expectedPackages/generate.ts index 203d4239ea9..326e15ce217 100644 --- a/meteor/server/publications/packageManager/expectedPackages/generate.ts +++ b/meteor/server/publications/packageManager/expectedPackages/generate.ts @@ -127,6 +127,7 @@ function generateExpectedPackageForDevice( expectedPackage: { ...expectedPackage.package, _id: expectedPackage._id, + rundownId: expectedPackage.rundownId ?? undefined, sideEffect: packageSideEffect, }, sources: combinedSources, diff --git a/meteor/server/publications/packageManager/playoutContext.ts b/meteor/server/publications/packageManager/playoutContext.ts index 99e22143fe5..bb3f5390a11 100644 --- a/meteor/server/publications/packageManager/playoutContext.ts +++ b/meteor/server/publications/packageManager/playoutContext.ts @@ -1,6 +1,6 @@ import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' import { PackageManagerPlayoutContext } from '@sofie-automation/shared-lib/dist/package-manager/publications' diff --git a/meteor/server/publications/partInstancesUI/publication.ts b/meteor/server/publications/partInstancesUI/publication.ts index 5c30cea8a2c..464cc6cb51d 100644 --- a/meteor/server/publications/partInstancesUI/publication.ts +++ b/meteor/server/publications/partInstancesUI/publication.ts @@ -13,7 +13,7 @@ import { ContentCache, PartInstanceOmitedFields, createReactiveContentCache } fr import { ReadonlyDeep } from 'type-fest' import { RundownPlaylists } from '../../collections' import { literal } from '@sofie-automation/corelib/dist/lib' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' import { RundownsObserver } from '../lib/rundownsObserver' import { RundownContentObserver } from './rundownContentObserver' diff --git a/meteor/server/publications/partInstancesUI/reactiveContentCache.ts b/meteor/server/publications/partInstancesUI/reactiveContentCache.ts index 66e1e0658ed..34ce3e25427 100644 --- a/meteor/server/publications/partInstancesUI/reactiveContentCache.ts +++ b/meteor/server/publications/partInstancesUI/reactiveContentCache.ts @@ -2,7 +2,7 @@ import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { ReactiveCacheCollection } from '../lib/ReactiveCacheCollection' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict, MongoFieldSpecifierZeroes } from '@sofie-automation/corelib/dist/mongo' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/meteor/server/publications/partsUI/publication.ts b/meteor/server/publications/partsUI/publication.ts index 868956828aa..8474450ecc5 100644 --- a/meteor/server/publications/partsUI/publication.ts +++ b/meteor/server/publications/partsUI/publication.ts @@ -14,7 +14,7 @@ import { ContentCache, PartOmitedFields, createReactiveContentCache } from './re import { ReadonlyDeep } from 'type-fest' import { RundownPlaylists } from '../../collections' import { literal } from '@sofie-automation/corelib/dist/lib' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' import { RundownsObserver } from '../lib/rundownsObserver' import { RundownContentObserver } from './rundownContentObserver' diff --git a/meteor/server/publications/partsUI/reactiveContentCache.ts b/meteor/server/publications/partsUI/reactiveContentCache.ts index 12d9423e8e4..74b34843abb 100644 --- a/meteor/server/publications/partsUI/reactiveContentCache.ts +++ b/meteor/server/publications/partsUI/reactiveContentCache.ts @@ -3,7 +3,7 @@ import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { ReactiveCacheCollection } from '../lib/ReactiveCacheCollection' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict, MongoFieldSpecifierZeroes } from '@sofie-automation/corelib/dist/mongo' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts index a7573349389..8e43d6865f9 100644 --- a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts +++ b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts @@ -14,8 +14,10 @@ import { SourceLayerType, IBlueprintPieceGeneric, PieceLifespan, + SplitsContent, VTContent, } from '@sofie-automation/blueprints-integration' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { Complete, literal } from '@sofie-automation/corelib/dist/lib' import { MongoMock } from '../../../../__mocks__/mongo' import { @@ -42,6 +44,10 @@ import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' const mockMediaObjectsCollection = MongoMock.getInnerMockCollection(MediaObjects) describe('lib/mediaObjects', () => { + beforeEach(async () => { + await mockMediaObjectsCollection.removeAsync({}) + }) + describe('buildFormatString', () => { it('deepscan tff, stream unknown', () => { const format1 = buildFormatString( @@ -244,6 +250,305 @@ describe('lib/mediaObjects', () => { expect(mediaId3).toEqual(undefined) }) + test('packageName: media object status uses mediaId or falls back to fileName', async () => { + const mockStudioSettings: IStudioSettings = { + supportedMediaFormats: '1920x1080i5000, 1280x720, i5000, i5000tff', + mediaPreviewsUrl: '', + supportedAudioStreams: '4', + frameRate: 25, + minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, + allowHold: false, + allowPieceDirectPlay: false, + enableBuckets: false, + enableEvaluationForm: false, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, + } + + const mockDefaultStudio = defaultStudio(protectString('studio0')) + const mockStudio: Complete = { + _id: mockDefaultStudio._id, + settings: mockStudioSettings, + packageContainerSettings: { + previewContainerIds: ['previews0'], + thumbnailContainerIds: ['thumbnails0'], + }, + routeSets: applyAndValidateOverrides(mockDefaultStudio.routeSetsWithOverrides).obj, + mappings: applyAndValidateOverrides(mockDefaultStudio.mappingsWithOverrides).obj, + packageContainers: applyAndValidateOverrides(mockDefaultStudio.packageContainersWithOverrides).obj, + } + + await mockMediaObjectsCollection.insertAsync( + literal({ + _id: protectString(''), + _attachments: {}, + _rev: '', + cinf: '', + collectionId: 'studio0', + mediaId: 'TEST_FILE', + mediaPath: '', + mediaSize: 0, + mediaTime: 0, + mediainfo: literal({ + name: 'test_file', + field_order: PackageInfo.FieldOrder.TFF, + streams: [ + literal({ + width: 1920, + height: 1080, + codec: { + type: MediaStreamType.Video, + time_base: '1/50', + }, + }), + literal({ + channels: 1, + codec: { + type: MediaStreamType.Audio, + time_base: '1/25', + }, + }), + literal({ + channels: 1, + codec: { + type: MediaStreamType.Audio, + time_base: '1/25', + }, + }), + literal({ + channels: 1, + codec: { + type: MediaStreamType.Audio, + time_base: '1/25', + }, + }), + literal({ + channels: 1, + codec: { + type: MediaStreamType.Audio, + time_base: '1/25', + }, + }), + ], + }), + objId: '', + previewPath: '', + previewSize: 0, + previewTime: 0, + studioId: protectString('studio0'), + thumbSize: 0, + thumbTime: 0, + tinf: '', + }) + ) + + await mockMediaObjectsCollection.insertAsync( + literal({ + _id: protectString(''), + _attachments: {}, + _rev: '', + cinf: '', + collectionId: 'studio0', + mediaId: 'TEST_FILE_2', + mediaPath: '', + mediaSize: 0, + mediaTime: 0, + mediainfo: literal({ + name: 'test_file_2', + field_order: PackageInfo.FieldOrder.Progressive, + streams: [ + literal({ + width: 1920, + height: 1080, + codec: { + type: MediaStreamType.Video, + time_base: '1/50', + }, + }), + literal({ + channels: 1, + codec: { + type: MediaStreamType.Audio, + time_base: '1/25', + }, + }), + literal({ + channels: 1, + codec: { + type: MediaStreamType.Audio, + time_base: '1/25', + }, + }), + literal({ + channels: 1, + codec: { + type: MediaStreamType.Audio, + time_base: '1/25', + }, + }), + literal({ + channels: 1, + codec: { + type: MediaStreamType.Audio, + time_base: '1/25', + }, + }), + ], + }), + objId: '', + previewPath: '', + previewSize: 0, + previewTime: 0, + studioId: protectString('studio0'), + thumbSize: 0, + thumbTime: 0, + tinf: '', + }) + ) + + const sourcelayer = literal({ + _id: '', + _rank: 0, + name: '', + type: SourceLayerType.LIVE_SPEAK, + }) + + const messageFactory = new PieceContentStatusMessageFactory(undefined) + const mockOwnerId = protectString('rundown0') + + const [status1] = await checkPieceContentStatusAndDependencies( + mockStudio, + mockOwnerId, + messageFactory, + literal({ + _id: protectString('piece1'), + name: 'Test_file', + prerollDuration: 0, + externalId: '', + lifespan: PieceLifespan.WithinPart, + privateData: {}, + outputLayerId: '', + sourceLayerId: '', + content: literal({ + fileName: 'test_file', + path: '', + }), + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + }), + sourcelayer + ) + expect(status1.packageName).toEqual('TEST_FILE') + + const [status2] = await checkPieceContentStatusAndDependencies( + mockStudio, + mockOwnerId, + messageFactory, + literal({ + _id: protectString('piece2'), + name: 'Test_file_2', + prerollDuration: 0, + externalId: '', + lifespan: PieceLifespan.WithinPart, + privateData: {}, + outputLayerId: '', + sourceLayerId: '', + content: literal({ + fileName: 'test_file_2', + path: '', + }), + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + }), + sourcelayer + ) + expect(status2.packageName).toEqual('TEST_FILE_2') + + const [status3] = await checkPieceContentStatusAndDependencies( + mockStudio, + mockOwnerId, + messageFactory, + literal({ + _id: protectString('piece3'), + name: 'Test_file_3', + prerollDuration: 0, + externalId: '', + lifespan: PieceLifespan.WithinPart, + privateData: {}, + outputLayerId: '', + sourceLayerId: '', + content: literal({ + fileName: 'test_file_3', + path: '', + }), + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + }), + sourcelayer + ) + expect(status3.packageName).toEqual('TEST_FILE_3') + }) + + test('packageName: exposed even when ignoreMediaObjectStatus is set', async () => { + const mockStudioSettings: IStudioSettings = { + supportedMediaFormats: '1920x1080i5000, 1280x720, i5000, i5000tff', + mediaPreviewsUrl: '', + supportedAudioStreams: '4', + frameRate: 25, + minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, + allowHold: false, + allowPieceDirectPlay: false, + enableBuckets: false, + enableEvaluationForm: false, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, + } + + const mockDefaultStudio = defaultStudio(protectString('studio0')) + const mockStudio: Complete = { + _id: mockDefaultStudio._id, + settings: mockStudioSettings, + packageContainerSettings: { + previewContainerIds: ['previews0'], + thumbnailContainerIds: ['thumbnails0'], + }, + routeSets: applyAndValidateOverrides(mockDefaultStudio.routeSetsWithOverrides).obj, + mappings: applyAndValidateOverrides(mockDefaultStudio.mappingsWithOverrides).obj, + packageContainers: applyAndValidateOverrides(mockDefaultStudio.packageContainersWithOverrides).obj, + } + + const ignorePiece = literal({ + _id: protectString('pieceIgnore'), + name: 'Ignore media status', + prerollDuration: 0, + externalId: '', + lifespan: PieceLifespan.WithinPart, + privateData: {}, + outputLayerId: '', + sourceLayerId: '', + content: { + fileName: 'ignored_file', + path: '', + ignoreMediaObjectStatus: true, + } as any, + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + }) + + const messageFactory = new PieceContentStatusMessageFactory(undefined) + const mockOwnerId = protectString('rundown0') + + const [ignoredStatus] = await checkPieceContentStatusAndDependencies( + mockStudio, + mockOwnerId, + messageFactory, + ignorePiece, + literal({ + _id: '', + _rank: 0, + name: '', + type: SourceLayerType.VT, + }) + ) + + expect(ignoredStatus.status).toEqual(PieceStatusCode.UNKNOWN) + expect(ignoredStatus.packageName).toEqual('IGNORED_FILE') + }) + test('checkPieceContentStatus', async () => { const mockStudioSettings: IStudioSettings = { supportedMediaFormats: '1920x1080i5000, 1280x720, i5000, i5000tff', @@ -255,6 +560,7 @@ describe('lib/mediaObjects', () => { allowPieceDirectPlay: false, enableBuckets: false, enableEvaluationForm: false, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, } const mockDefaultStudio = defaultStudio(protectString('studio0')) @@ -514,4 +820,168 @@ describe('lib/mediaObjects', () => { }) ) }) + + describe('SPLITS boxPreviews', () => { + const mockStudioSettings: IStudioSettings = { + supportedMediaFormats: '1920x1080i5000', + mediaPreviewsUrl: 'http://preview/', + supportedAudioStreams: '4', + frameRate: 25, + minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, + allowHold: false, + allowPieceDirectPlay: false, + enableBuckets: false, + enableEvaluationForm: false, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, + } + + const mockDefaultStudio = defaultStudio(protectString('studio0')) + const mockStudio: Complete = { + _id: mockDefaultStudio._id, + settings: mockStudioSettings, + packageContainerSettings: { + previewContainerIds: ['previews0'], + thumbnailContainerIds: ['thumbnails0'], + }, + routeSets: applyAndValidateOverrides(mockDefaultStudio.routeSetsWithOverrides).obj, + mappings: applyAndValidateOverrides(mockDefaultStudio.mappingsWithOverrides).obj, + packageContainers: applyAndValidateOverrides(mockDefaultStudio.packageContainersWithOverrides).obj, + } + + const splitsLayer = literal({ + _id: '', + _rank: 0, + name: 'Splits', + type: SourceLayerType.SPLITS, + }) + + const messageFactory = new PieceContentStatusMessageFactory(undefined) + const mockOwnerId = protectString('rundown0') + + async function insertMediaObject(mediaId: string): Promise { + await mockMediaObjectsCollection.insertAsync( + literal({ + _id: protectString(''), + _attachments: {}, + _rev: '', + cinf: '', + collectionId: 'studio0', + mediaId, + mediaPath: '', + mediaSize: 0, + mediaTime: 0, + mediainfo: literal({ + name: mediaId, + field_order: PackageInfo.FieldOrder.Progressive, + streams: [ + literal({ + width: 1920, + height: 1080, + codec: { + type: MediaStreamType.Video, + time_base: '1/50', + }, + }), + ], + }), + objId: '', + previewPath: 'previews', + previewSize: 0, + previewTime: 0, + studioId: protectString('studio0'), + thumbSize: 0, + thumbTime: 0, + tinf: '', + }) + ) + } + + test('MediaObject fallback: index-aligned boxPreviews with dots in file names', async () => { + await insertMediaObject('CLIPS/HEAD3_SNOW.MP4') + await insertMediaObject('CLIPS/OTHER.MP4') + + const [status] = await checkPieceContentStatusAndDependencies( + mockStudio, + mockOwnerId, + messageFactory, + literal({ + _id: protectString('split1'), + name: 'Split', + prerollDuration: 0, + externalId: '', + lifespan: PieceLifespan.WithinPart, + privateData: {}, + outputLayerId: '', + sourceLayerId: '', + content: literal({ + boxSourceConfiguration: [ + { + type: SourceLayerType.CAMERA, + studioLabel: 'Cam 1', + switcherInput: 1, + }, + { + type: SourceLayerType.VT, + studioLabel: 'Snow', + switcherInput: '', + fileName: 'clips/head3_Snow.mp4', + path: 'clips/head3_Snow.mp4', + }, + { + type: SourceLayerType.VT, + studioLabel: 'Other', + switcherInput: '', + fileName: 'clips/other.mp4', + path: 'clips/other.mp4', + }, + ], + }), + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + }), + splitsLayer + ) + + expect(status.thumbnailUrl).toBeUndefined() + expect(status.previewUrl).toBeUndefined() + expect(status.boxPreviews).toHaveLength(3) + expect(status.boxPreviews?.[0]).toEqual({}) + expect(status.boxPreviews?.[1]?.thumbnailUrl).toContain('CLIPS%2FHEAD3_SNOW.MP4') + expect(status.boxPreviews?.[2]?.thumbnailUrl).toContain('CLIPS%2FOTHER.MP4') + }) + + test('case-insensitive fileName matching', async () => { + await insertMediaObject('CLIPS/FOO.MP4') + + const [status] = await checkPieceContentStatusAndDependencies( + mockStudio, + mockOwnerId, + messageFactory, + literal({ + _id: protectString('split2'), + name: 'Split', + prerollDuration: 0, + externalId: '', + lifespan: PieceLifespan.WithinPart, + privateData: {}, + outputLayerId: '', + sourceLayerId: '', + content: literal({ + boxSourceConfiguration: [ + { + type: SourceLayerType.VT, + studioLabel: 'Foo', + switcherInput: '', + fileName: 'clips/foo.mp4', + path: 'clips/foo.mp4', + }, + ], + }), + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + }), + splitsLayer + ) + + expect(status.boxPreviews?.[0]?.thumbnailUrl).toBeDefined() + }) + }) }) diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index ba2435e8cd3..276e5ad7b28 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -8,6 +8,7 @@ import { LiveSpeakContent, PackageInfo, SourceLayerType, + SplitsContent, VTContent, } from '@sofie-automation/blueprints-integration' import { getExpectedPackageId } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' @@ -52,11 +53,17 @@ import { PackageInfoLight, PieceDependencies, } from './common' -import { PieceContentStatusObj } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' +import { PieceContentStatusObj, SplitBoxPreviewUrls } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' import { PieceContentStatusMessageFactory, PieceContentStatusMessageRequiredArgs } from './messageFactory' import { PackageStatusMessage } from '@sofie-automation/shared-lib/dist/packageStatusMessages' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { StudioPackageContainerSettings } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' +import { + buildPublishedBoxPreviews, + getMediaIdFromSplitBox, + getMediaIdsFromSplitsContent, + normalizeSplitBoxMediaId, +} from '@sofie-automation/shared-lib/dist/package-manager/splitBoxMedia' const DEFAULT_MESSAGE_FACTORY = new PieceContentStatusMessageFactory(undefined) @@ -241,24 +248,45 @@ export async function checkPieceContentStatusAndDependencies( } if (studio.settings.mockPieceContentStatus) { - return [ - { - status: PieceStatusCode.OK, - messages: [], - progress: undefined, + const mockStatus: PieceContentStatusObj = { + status: PieceStatusCode.OK, + messages: [], + progress: undefined, - freezes: [], - blacks: [], - scenes: [], + freezes: [], + blacks: [], + scenes: [], - thumbnailUrl: '/dev/fakeThumbnail.png', - previewUrl: '/dev/fakePreview.mp4', + thumbnailUrl: '/dev/fakeThumbnail.png', + previewUrl: '/dev/fakePreview.mp4', - packageName: null, - contentDuration: 30 * 1000, - }, - pieceDependencies, - ] + packageName: '/dev/fakePreview.mp4', + contentDuration: 30 * 1000, + } + + if (sourceLayer.type === SourceLayerType.SPLITS) { + const splitsContent = piece.content as SplitsContent | undefined + if (splitsContent?.boxSourceConfiguration) { + return [ + { + ...mockStatus, + thumbnailUrl: undefined, + previewUrl: undefined, + boxPreviews: splitsContent.boxSourceConfiguration.map((box) => + getMediaIdFromSplitBox(box) + ? { + thumbnailUrl: '/dev/fakeThumbnail.png', + previewUrl: '/dev/fakePreview.mp4', + } + : {} + ), + }, + pieceDependencies, + ] + } + } + + return [mockStatus, pieceDependencies] } const ignoreMediaStatus = piece.content && piece.content.ignoreMediaObjectStatus @@ -295,6 +323,17 @@ export async function checkPieceContentStatusAndDependencies( ) as Promise } + const getMediaObject = async (mediaId: string) => { + pieceDependencies.mediaObjects.push(mediaId) + return MediaObjects.findOneAsync( + { + studioId: studio._id, + mediaId, + }, + { projection: mediaObjectFieldSpecifier } + ) as Promise + } + // Using Expected Packages: const status = await checkPieceContentExpectedPackageStatus( piece, @@ -305,6 +344,9 @@ export async function checkPieceContentStatusAndDependencies( getPackageContainerPackageStatus, messageFactory || DEFAULT_MESSAGE_FACTORY ) + if (sourceLayer.type === SourceLayerType.SPLITS && status.boxPreviews) { + await fillSplitsBoxPreviewsFromMediaObjects(studio, piece, status, getMediaObject) + } return [status, pieceDependencies] } else { // Fallback to MediaObject statuses: @@ -319,6 +361,11 @@ export async function checkPieceContentStatusAndDependencies( ) as Promise } + if (sourceLayer.type === SourceLayerType.SPLITS) { + const status = await checkPieceContentSplitsMediaObjectStatus(piece, studio, getMediaObject) + return [status, pieceDependencies] + } + const status = await checkPieceContentMediaObjectStatus( piece, sourceLayer, @@ -343,7 +390,7 @@ export async function checkPieceContentStatusAndDependencies( thumbnailUrl: undefined, previewUrl: undefined, - packageName: null, + packageName: getMediaObjectMediaId(piece, sourceLayer) || null, contentDuration: undefined, }, pieceDependencies, @@ -355,6 +402,89 @@ interface MediaObjectMessage { message: ITranslatableMessage | null } +async function fillSplitsBoxPreviewsFromMediaObjects( + studio: PieceContentStatusStudio, + piece: PieceContentStatusPiece, + status: PieceContentStatusObj, + getMediaObject: (mediaId: string) => Promise +): Promise { + const splitsContent = piece.content as SplitsContent | undefined + const boxes = splitsContent?.boxSourceConfiguration + if (!boxes?.length || !status.boxPreviews) return + + const newPreviews = [...status.boxPreviews] + for (let i = 0; i < boxes.length; i++) { + const mediaId = getMediaIdFromSplitBox(boxes[i]) + if (!mediaId) continue + + const preview = newPreviews[i] ?? {} + if (preview.thumbnailUrl || preview.previewUrl) continue + + const mediaObject = await getMediaObject(mediaId) + if (!mediaObject) continue + + newPreviews[i] = { + thumbnailUrl: getAssetUrlFromContentMetaData(mediaObject, 'thumbnail', studio.settings.mediaPreviewsUrl), + previewUrl: getAssetUrlFromContentMetaData(mediaObject, 'preview', studio.settings.mediaPreviewsUrl), + } + } + status.boxPreviews = newPreviews +} + +async function checkPieceContentSplitsMediaObjectStatus( + piece: PieceContentStatusPiece, + studio: PieceContentStatusStudio, + getMediaObject: (mediaId: string) => Promise +): Promise { + const splitsContent = piece.content as SplitsContent | undefined + const boxes = splitsContent?.boxSourceConfiguration ?? [] + const previewByMediaId = new Map() + let contentDuration: number | undefined + + for (const mediaId of getMediaIdsFromSplitsContent({ boxSourceConfiguration: boxes })) { + const mediaObject = await getMediaObject(mediaId) + if (!mediaObject) continue + + previewByMediaId.set(mediaId, { + thumbnailUrl: getAssetUrlFromContentMetaData(mediaObject, 'thumbnail', studio.settings.mediaPreviewsUrl), + previewUrl: getAssetUrlFromContentMetaData(mediaObject, 'preview', studio.settings.mediaPreviewsUrl), + }) + + if (mediaObject.mediainfo?.streams?.length) { + const maximumStreamDuration = mediaObject.mediainfo.streams.reduce( + (prev, current) => + current.duration !== undefined ? Math.max(prev, Number.parseFloat(current.duration)) : prev, + Number.NaN + ) + if (Number.isFinite(maximumStreamDuration)) { + contentDuration = Math.max(contentDuration ?? 0, maximumStreamDuration) + } + } + } + + let pieceStatus = PieceStatusCode.UNKNOWN + if (previewByMediaId.size > 0) { + pieceStatus = PieceStatusCode.OK + } + + return { + status: pieceStatus, + messages: [], + progress: 0, + + freezes: [], + blacks: [], + scenes: [], + + thumbnailUrl: undefined, + previewUrl: undefined, + + packageName: null, + contentDuration, + boxPreviews: buildPublishedBoxPreviews(boxes, previewByMediaId), + } +} + async function checkPieceContentMediaObjectStatus( piece: PieceContentStatusPiece, sourceLayer: ISourceLayer, @@ -566,7 +696,7 @@ async function checkPieceContentMediaObjectStatus( ? getAssetUrlFromContentMetaData(metadata, 'preview', studio.settings.mediaPreviewsUrl) : undefined, - packageName: metadata?.mediaId || null, + packageName: metadata?.mediaId || fileName || null, contentDuration, } @@ -609,6 +739,8 @@ async function checkPieceContentExpectedPackageStatus( messageFactory: PieceContentStatusMessageFactory ): Promise { const settings: IStudioSettings | undefined = studio?.settings + const isSplitsLayer = sourceLayer.type === SourceLayerType.SPLITS + const previewByMediaId = new Map() const ignoreMediaAudioStatus = piece.content && piece.content.ignoreAudioFormat @@ -708,28 +840,31 @@ async function checkPieceContentExpectedPackageStatus( continue } - if (!thumbnailUrl) { - const sideEffect = getSideEffect(expectedPackage, studio.packageContainerSettings) - - thumbnailUrl = await getAssetUrlFromPackageContainerStatus( - studio.packageContainers, - getPackageContainerPackageStatus, - candidatePackageId, - sideEffect.thumbnailContainerId, - sideEffect.thumbnailPackageSettings?.path - ) - } + const resolvedUrls = await resolveExpectedPackageAssetUrls( + studio, + expectedPackage, + candidatePackageId, + getPackageContainerPackageStatus + ) - if (!previewUrl) { - const sideEffect = getSideEffect(expectedPackage, studio.packageContainerSettings) + if (isSplitsLayer) { + const pkgMediaPath = getExpectedPackageFileName(expectedPackage) + if (pkgMediaPath) { + const mediaId = normalizeSplitBoxMediaId(pkgMediaPath) + const existing = previewByMediaId.get(mediaId) ?? {} + previewByMediaId.set(mediaId, { + thumbnailUrl: existing.thumbnailUrl ?? resolvedUrls.thumbnailUrl, + previewUrl: existing.previewUrl ?? resolvedUrls.previewUrl, + }) + } + } else { + if (!thumbnailUrl) { + thumbnailUrl = resolvedUrls.thumbnailUrl + } - previewUrl = await getAssetUrlFromPackageContainerStatus( - studio.packageContainers, - getPackageContainerPackageStatus, - candidatePackageId, - sideEffect.previewContainerId, - sideEffect.previewPackageSettings?.path - ) + if (!previewUrl) { + previewUrl = resolvedUrls.previewUrl + } } progress = getPackageProgress(packageOnPackageContainer.status) ?? undefined @@ -868,6 +1003,16 @@ async function checkPieceContentExpectedPackageStatus( : messageFactory.getTranslation(msg.message, messageArgs) }) + let boxPreviews: SplitBoxPreviewUrls[] | undefined + if (isSplitsLayer) { + const splitsContent = piece.content as SplitsContent | undefined + if (splitsContent?.boxSourceConfiguration) { + boxPreviews = buildPublishedBoxPreviews(splitsContent.boxSourceConfiguration, previewByMediaId) + } + thumbnailUrl = undefined + previewUrl = undefined + } + return { status: pieceStatus, messages: _.compact(translatedMessages), @@ -883,9 +1028,40 @@ async function checkPieceContentExpectedPackageStatus( packageName, contentDuration, + boxPreviews, } } +async function resolveExpectedPackageAssetUrls( + studio: PieceContentStatusStudio, + expectedPackage: ExpectedPackage.Any, + candidatePackageId: ExpectedPackageId, + getPackageContainerPackageStatus: ( + packageContainerId: string, + expectedPackageId: ExpectedPackageId + ) => Promise +): Promise { + const sideEffect = getSideEffect(expectedPackage, studio.packageContainerSettings) + + const thumbnailUrl = await getAssetUrlFromPackageContainerStatus( + studio.packageContainers, + getPackageContainerPackageStatus, + candidatePackageId, + sideEffect.thumbnailContainerId, + sideEffect.thumbnailPackageSettings?.path + ) + + const previewUrl = await getAssetUrlFromPackageContainerStatus( + studio.packageContainers, + getPackageContainerPackageStatus, + candidatePackageId, + sideEffect.previewContainerId, + sideEffect.previewPackageSettings?.path + ) + + return { thumbnailUrl, previewUrl } +} + async function getAssetUrlFromPackageContainerStatus( packageContainers: Record, getPackageContainerPackageStatus: ( diff --git a/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts b/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts index a9b95c71407..f2710d50b3a 100644 --- a/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts +++ b/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts @@ -15,7 +15,7 @@ import { import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' import { ReadonlyDeep } from 'type-fest' import { UIPieceContentStatus } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { MediaObjects, PackageContainerPackageStatuses, diff --git a/meteor/server/publications/rundown.ts b/meteor/server/publications/rundown.ts index 6e4e5788858..b83748913c9 100644 --- a/meteor/server/publications/rundown.ts +++ b/meteor/server/publications/rundown.ts @@ -67,6 +67,7 @@ meteorPublish( { projection: { privateData: 0, + externalEventSubscriptions: 0, }, } ) @@ -90,6 +91,7 @@ meteorPublish( const modifier: FindOptions = { projection: { privateData: 0, + externalEventSubscriptions: 0, }, } @@ -112,6 +114,7 @@ meteorPublish( const modifier: FindOptions = { projection: { privateData: 0, + externalEventSubscriptions: 0, }, } diff --git a/meteor/server/publications/rundownPlaylist.ts b/meteor/server/publications/rundownPlaylist.ts index c52efc85d36..582c104934e 100644 --- a/meteor/server/publications/rundownPlaylist.ts +++ b/meteor/server/publications/rundownPlaylist.ts @@ -1,5 +1,5 @@ import { meteorPublish } from './lib/lib' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { RundownPlaylists } from '../collections' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' diff --git a/meteor/server/publications/segmentPartNotesUI/__tests__/generateNotesForSegment.test.ts b/meteor/server/publications/segmentPartNotesUI/__tests__/generateNotesForSegment.test.ts index 3debf641199..fea13201aa3 100644 --- a/meteor/server/publications/segmentPartNotesUI/__tests__/generateNotesForSegment.test.ts +++ b/meteor/server/publications/segmentPartNotesUI/__tests__/generateNotesForSegment.test.ts @@ -486,4 +486,121 @@ describe('generateNotesForSegment', () => { ]) ) }) + + test('partInstance with runtime invalidReason', async () => { + const playlistId = protectString('playlist0') + const nrcsName = 'some nrcs' + + const segment: Pick = { + _id: protectString('segment0'), + _rank: 1, + rundownId: protectString('rundown0'), + name: 'A segment', + notes: [], + orphaned: undefined, + } + + const partInstance0: Pick = { + _id: protectString('instance0'), + segmentId: segment._id, + rundownId: segment.rundownId, + orphaned: undefined, + reset: false, + invalidReason: { + message: generateTranslation('Runtime error occurred'), + severity: NoteSeverity.ERROR, + }, + part: { + _id: protectString('part0'), + title: 'Test Part', + } as any, + } + + const notes = generateNotesForSegment(playlistId, segment, nrcsName, [], [partInstance0]) + expect(notes).toEqual( + literal([ + { + _id: protectString('segment0_partinstance_instance0_invalid_runtime'), + note: { + type: NoteSeverity.ERROR, + message: partInstance0.invalidReason!.message, + rank: segment._rank, + origin: { + segmentId: segment._id, + rundownId: segment.rundownId, + name: partInstance0.part.title, + partId: partInstance0.part._id, + segmentName: segment.name, + }, + }, + playlistId: playlistId, + rundownId: segment.rundownId, + segmentId: segment._id, + }, + ]) + ) + }) + + test('partInstance with runtime invalidReason but reset - no note', async () => { + const playlistId = protectString('playlist0') + const nrcsName = 'some nrcs' + + const segment: Pick = { + _id: protectString('segment0'), + _rank: 1, + rundownId: protectString('rundown0'), + name: 'A segment', + notes: [], + orphaned: undefined, + } + + const partInstance0: Pick = { + _id: protectString('instance0'), + segmentId: segment._id, + rundownId: segment.rundownId, + orphaned: undefined, + reset: true, + invalidReason: { + message: generateTranslation('Runtime error occurred'), + severity: NoteSeverity.ERROR, + }, + part: { + _id: protectString('part0'), + title: 'Test Part', + } as any, + } + + const notes = generateNotesForSegment(playlistId, segment, nrcsName, [], [partInstance0]) + expect(notes).toHaveLength(0) + }) + + test('partInstance without invalidReason - no note', async () => { + const playlistId = protectString('playlist0') + const nrcsName = 'some nrcs' + + const segment: Pick = { + _id: protectString('segment0'), + _rank: 1, + rundownId: protectString('rundown0'), + name: 'A segment', + notes: [], + orphaned: undefined, + } + + const partInstance0: Pick = { + _id: protectString('instance0'), + segmentId: segment._id, + rundownId: segment.rundownId, + orphaned: undefined, + reset: false, + invalidReason: undefined, + part: { + _id: protectString('part0'), + title: 'Test Part', + } as any, + } + + const notes = generateNotesForSegment(playlistId, segment, nrcsName, [], [partInstance0]) + expect(notes).toHaveLength(0) + }) }) diff --git a/meteor/server/publications/segmentPartNotesUI/__tests__/publication.test.ts b/meteor/server/publications/segmentPartNotesUI/__tests__/publication.test.ts index f232e383710..42e34002970 100644 --- a/meteor/server/publications/segmentPartNotesUI/__tests__/publication.test.ts +++ b/meteor/server/publications/segmentPartNotesUI/__tests__/publication.test.ts @@ -69,7 +69,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => { Rundowns: new ReactiveCacheCollection('Rundowns'), Segments: new ReactiveCacheCollection('Segments'), Parts: new ReactiveCacheCollection('Parts'), - DeletedPartInstances: new ReactiveCacheCollection('DeletedPartInstances'), + PartInstances: new ReactiveCacheCollection('PartInstances'), } newCache.Rundowns.insert({ @@ -356,11 +356,11 @@ describe('manipulateUISegmentPartNotesPublicationData', () => { invalid: false, invalidReason: undefined, }) - newCache.DeletedPartInstances.insert({ + newCache.PartInstances.insert({ _id: 'instance0', segmentId: segmentId0, rundownId: rundownId, - orphaned: undefined, + orphaned: 'deleted', reset: false, part: 'part' as any, }) @@ -421,6 +421,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => { [ { _id: 'instance0', + orphaned: 'deleted', part: 'part', reset: false, rundownId: 'rundown0', diff --git a/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts b/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts index 89dd9545a2f..da4205a90d6 100644 --- a/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts +++ b/meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts @@ -155,5 +155,31 @@ export function generateNotesForSegment( } } + // Generate notes for runtime invalidReason on PartInstances + // This is distinct from planned invalidReason on Parts - these are runtime validation issues + for (const partInstance of partInstances) { + // Skip if the PartInstance has been reset (no longer relevant) or has no runtime invalidReason + if (partInstance.reset || !partInstance.invalidReason) continue + + notes.push({ + _id: protectString(`${segment._id}_partinstance_${partInstance._id}_invalid_runtime`), + playlistId, + rundownId: partInstance.rundownId, + segmentId: segment._id, + note: { + type: partInstance.invalidReason.severity ?? NoteSeverity.ERROR, + message: partInstance.invalidReason.message, + rank: segment._rank, + origin: { + segmentId: partInstance.segmentId, + partId: partInstance.part._id, + rundownId: partInstance.rundownId, + segmentName: segment.name, + name: partInstance.part.title, + }, + }, + }) + } + return notes } diff --git a/meteor/server/publications/segmentPartNotesUI/publication.ts b/meteor/server/publications/segmentPartNotesUI/publication.ts index 4de954886c1..131440a6f1a 100644 --- a/meteor/server/publications/segmentPartNotesUI/publication.ts +++ b/meteor/server/publications/segmentPartNotesUI/publication.ts @@ -27,7 +27,7 @@ import { } from './reactiveContentCache' import { RundownsObserver } from '../lib/rundownsObserver' import { RundownContentObserver } from './rundownContentObserver' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { generateNotesForSegment } from './generateNotesForSegment' import { RundownPlaylists } from '../../collections' import { check, Match } from 'meteor/check' @@ -91,7 +91,7 @@ async function setupUISegmentPartNotesPublicationObservers( triggerUpdate({ invalidateSegmentIds: [doc.segmentId, oldDoc.segmentId] }), removed: (doc) => triggerUpdate({ invalidateSegmentIds: [doc.segmentId] }), }), - cache.DeletedPartInstances.find({}).observe({ + cache.PartInstances.find({}).observe({ added: (doc) => triggerUpdate({ invalidateSegmentIds: [doc.segmentId] }), changed: (doc, oldDoc) => triggerUpdate({ invalidateSegmentIds: [doc.segmentId, oldDoc.segmentId] }), @@ -184,13 +184,13 @@ export async function manipulateUISegmentPartNotesPublicationData( interface UpdateNotesData { rundownsCache: Map> parts: Map[]> - deletedPartInstances: Map[]> + partInstances: Map[]> } function compileUpdateNotesData(cache: ReadonlyDeep): UpdateNotesData { return { rundownsCache: normalizeArrayToMap(cache.Rundowns.find({}).fetch(), '_id'), parts: groupByToMap(cache.Parts.find({}).fetch(), 'segmentId'), - deletedPartInstances: groupByToMap(cache.DeletedPartInstances.find({}).fetch(), 'segmentId'), + partInstances: groupByToMap(cache.PartInstances.find({}).fetch(), 'segmentId'), } } @@ -205,7 +205,7 @@ function updateNotesForSegment( segment, getRundownNrcsName(state.rundownsCache.get(segment.rundownId)), state.parts.get(segment._id) ?? [], - state.deletedPartInstances.get(segment._id) ?? [] + state.partInstances.get(segment._id) ?? [] ) // Insert generated notes diff --git a/meteor/server/publications/segmentPartNotesUI/reactiveContentCache.ts b/meteor/server/publications/segmentPartNotesUI/reactiveContentCache.ts index 22b82c88159..788a681418d 100644 --- a/meteor/server/publications/segmentPartNotesUI/reactiveContentCache.ts +++ b/meteor/server/publications/segmentPartNotesUI/reactiveContentCache.ts @@ -35,7 +35,7 @@ export const partFieldSpecifier = literal> >({ @@ -44,7 +44,9 @@ export const partInstanceFieldSpecifier = literal< rundownId: 1, orphaned: 1, reset: 1, + invalidReason: 1, // @ts-expect-error Deep not supported + 'part._id': 1, 'part.title': 1, }) @@ -52,7 +54,7 @@ export interface ContentCache { Rundowns: ReactiveCacheCollection> Segments: ReactiveCacheCollection> Parts: ReactiveCacheCollection> - DeletedPartInstances: ReactiveCacheCollection> + PartInstances: ReactiveCacheCollection> } export function createReactiveContentCache(): ContentCache { @@ -60,9 +62,7 @@ export function createReactiveContentCache(): ContentCache { Rundowns: new ReactiveCacheCollection>('rundowns'), Segments: new ReactiveCacheCollection>('segments'), Parts: new ReactiveCacheCollection>('parts'), - DeletedPartInstances: new ReactiveCacheCollection>( - 'deletedPartInstances' - ), + PartInstances: new ReactiveCacheCollection>('partInstances'), } return cache diff --git a/meteor/server/publications/segmentPartNotesUI/rundownContentObserver.ts b/meteor/server/publications/segmentPartNotesUI/rundownContentObserver.ts index 8beb78d1feb..82bb509065b 100644 --- a/meteor/server/publications/segmentPartNotesUI/rundownContentObserver.ts +++ b/meteor/server/publications/segmentPartNotesUI/rundownContentObserver.ts @@ -58,8 +58,12 @@ export class RundownContentObserver { } ), PartInstances.observeChanges( - { rundownId: { $in: rundownIds }, reset: { $ne: true }, orphaned: 'deleted' }, - cache.DeletedPartInstances.link(), + { + rundownId: { $in: rundownIds }, + reset: { $ne: true }, + $or: [{ invalidReason: { $exists: true } }, { orphaned: 'deleted' }], + }, + cache.PartInstances.link(), { projection: partInstanceFieldSpecifier } ), ]) diff --git a/meteor/server/security/check.ts b/meteor/server/security/check.ts index 2b93e231883..df19d425037 100644 --- a/meteor/server/security/check.ts +++ b/meteor/server/security/check.ts @@ -1,6 +1,6 @@ import { PeripheralDeviceId, RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Meteor } from 'meteor/meteor' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { assertConnectionHasOneOfPermissions, RequestCredentials } from './auth' import { PeripheralDevices, RundownPlaylists, Rundowns } from '../collections' diff --git a/meteor/server/systemStatus/__tests__/systemStatus.test.ts b/meteor/server/systemStatus/__tests__/systemStatus.test.ts index b482a82fe31..09571d5b698 100644 --- a/meteor/server/systemStatus/__tests__/systemStatus.test.ts +++ b/meteor/server/systemStatus/__tests__/systemStatus.test.ts @@ -90,6 +90,7 @@ describe('systemStatus', () => { $set: { status: literal({ statusCode: StatusCode.WARNING_MAJOR, + statusDetails: [], messages: [], }), }, @@ -116,6 +117,7 @@ describe('systemStatus', () => { $set: { status: literal({ statusCode: StatusCode.GOOD, + statusDetails: [], messages: [], }), }, diff --git a/meteor/server/webmanifest.ts b/meteor/server/webmanifest.ts index 3795941f299..909c5d7a642 100644 --- a/meteor/server/webmanifest.ts +++ b/meteor/server/webmanifest.ts @@ -12,7 +12,7 @@ import { getLocale, getRootSubpath, Translations } from './lib' import { generateTranslation } from '@sofie-automation/corelib/dist/lib' import { ITranslatableMessage } from '@sofie-automation/blueprints-integration' import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { getCoreSystemAsync } from './coreSystem/collection' import Koa from 'koa' import KoaRouter from '@koa/router' diff --git a/meteor/server/worker/__tests__/jobQueue.test.ts b/meteor/server/worker/__tests__/jobQueue.test.ts index 93db5024fda..2977379faa3 100644 --- a/meteor/server/worker/__tests__/jobQueue.test.ts +++ b/meteor/server/worker/__tests__/jobQueue.test.ts @@ -651,6 +651,175 @@ describe('WorkerJobQueueManager', () => { }) }) + describe('mergeOrQueueJob', () => { + it('should enqueue a new job when the queue is empty', async () => { + const queueName = 'testQueue' + const jobName = 'mergeJob' + const generateData = jest.fn((_existing: unknown | null) => ({ value: 1 })) + + manager.mergeOrQueueJob(queueName, jobName, generateData) + + expect(generateData).toHaveBeenCalledTimes(1) + expect(generateData).toHaveBeenCalledWith(null) + + const job = await manager.getNextJob(queueName) + expect(job).not.toBeNull() + expect(job?.name).toBe(jobName) + expect(job?.data).toEqual({ value: 1 }) + }) + + it('should enqueue a new job when the tail has a different job name', async () => { + const queueName = 'testQueue' + const generateData = jest.fn((_existing: unknown | null) => ({ value: 42 })) + + await manager.queueJobWithoutResult(queueName, 'otherJob', { existing: true }, undefined) + + manager.mergeOrQueueJob(queueName, 'mergeJob', generateData) + + expect(generateData).toHaveBeenCalledWith(null) + + // Two jobs should be in the queue + const firstJob = await manager.getNextJob(queueName) + expect(firstJob?.name).toBe('otherJob') + + const secondJob = await manager.getNextJob(queueName) + expect(secondJob?.name).toBe('mergeJob') + expect(secondJob?.data).toEqual({ value: 42 }) + }) + + it('should merge data into the tail job when it has the same name', async () => { + const queueName = 'testQueue' + const jobName = 'mergeJob' + const initialData = { count: 1 } + const generateData = jest.fn((existing: unknown | null) => ({ + ...(existing as object), + count: ((existing as { count: number } | null)?.count ?? 0) + 1, + })) + + await manager.queueJobWithoutResult(queueName, jobName, initialData, undefined) + + manager.mergeOrQueueJob(queueName, jobName, generateData) + + expect(generateData).toHaveBeenCalledWith(initialData) + + // Should still be only one job in the queue + const firstJob = await manager.getNextJob(queueName) + expect(firstJob?.name).toBe(jobName) + expect(firstJob?.data).toEqual({ count: 2 }) + + const noMoreJobs = await manager.getNextJob(queueName) + expect(noMoreJobs).toBeNull() + }) + + it('should only merge with the tail job, not jobs earlier in the queue', async () => { + const queueName = 'testQueue' + const jobName = 'mergeJob' + const generateData = jest.fn((_existing: unknown | null) => ({ merged: true })) + + // Queue: [mergeJob (index 0), otherJob (index 1/tail)] + await manager.queueJobWithoutResult(queueName, jobName, { original: true }, undefined) + await manager.queueJobWithoutResult(queueName, 'otherJob', {}, undefined) + + // Tail is 'otherJob', not 'mergeJob' — should enqueue a new job + manager.mergeOrQueueJob(queueName, jobName, generateData) + + expect(generateData).toHaveBeenCalledWith(null) + + // Three jobs should be in the queue + const firstJob = await manager.getNextJob(queueName) + expect(firstJob?.name).toBe(jobName) + expect(firstJob?.data).toEqual({ original: true }) + + const secondJob = await manager.getNextJob(queueName) + expect(secondJob?.name).toBe('otherJob') + + const thirdJob = await manager.getNextJob(queueName) + expect(thirdJob?.name).toBe(jobName) + expect(thirdJob?.data).toEqual({ merged: true }) + }) + + it('should notify a waiting worker when a new job is enqueued', async () => { + const queueName = 'testQueue' + + // Worker starts waiting (queue is empty) + const waitPromise = manager.waitForNextJob(queueName) + + manager.mergeOrQueueJob(queueName, 'mergeJob', () => ({ value: 1 })) + + // Wait for deferred notification + await waitTime(10) + + await expect(waitPromise).resolves.toBeUndefined() + }) + + it('should not add a new job when merging into the tail', async () => { + const queueName = 'testQueue' + const jobName = 'mergeJob' + + await manager.queueJobWithoutResult(queueName, jobName, { value: 0 }, undefined) + + // Merge — should not add a second job + manager.mergeOrQueueJob(queueName, jobName, (existing) => ({ + ...(existing as object), + value: ((existing as { value: number } | null)?.value ?? 0) + 1, + })) + + const firstJob = await manager.getNextJob(queueName) + expect(firstJob?.name).toBe(jobName) + expect(firstJob?.data).toEqual({ value: 1 }) + + // Queue should now be empty + const noMoreJobs = await manager.getNextJob(queueName) + expect(noMoreJobs).toBeNull() + }) + + it('should enqueue a new job after the previous tail job has been consumed', async () => { + const queueName = 'testQueue' + const jobName = 'mergeJob' + + // First call: new job + manager.mergeOrQueueJob(queueName, jobName, () => ({ round: 1 })) + + // Consume the job + const firstJob = await manager.getNextJob(queueName) + expect(firstJob?.name).toBe(jobName) + expect(firstJob?.data).toEqual({ round: 1 }) + + // Second call: queue is empty again — should create a fresh job with null as existing + const generateData = jest.fn(() => ({ round: 2 })) + manager.mergeOrQueueJob(queueName, jobName, generateData) + + expect(generateData).toHaveBeenCalledWith(null) + + const secondJob = await manager.getNextJob(queueName) + expect(secondJob?.name).toBe(jobName) + expect(secondJob?.data).toEqual({ round: 2 }) + }) + + it('should accumulate multiple merges into the same tail job', async () => { + const queueName = 'testQueue' + const jobName = 'mergeJob' + + // First call creates a new job + manager.mergeOrQueueJob(queueName, jobName, () => ({ total: 1 })) + + // Subsequent calls should keep merging into the tail + manager.mergeOrQueueJob(queueName, jobName, (existing) => ({ + total: ((existing as { total: number } | null)?.total ?? 0) + 1, + })) + manager.mergeOrQueueJob(queueName, jobName, (existing) => ({ + total: ((existing as { total: number } | null)?.total ?? 0) + 1, + })) + + // Should be exactly one job with accumulated data + const job = await manager.getNextJob(queueName) + expect(job?.name).toBe(jobName) + expect(job?.data).toEqual({ total: 3 }) + + expect(await manager.getNextJob(queueName)).toBeNull() + }) + }) + describe('multiple queues', () => { it('should maintain separate queues for different queue names', async () => { const queue1 = 'queue1' diff --git a/meteor/server/worker/jobQueue.ts b/meteor/server/worker/jobQueue.ts index 459116cf103..24e024c9fb8 100644 --- a/meteor/server/worker/jobQueue.ts +++ b/meteor/server/worker/jobQueue.ts @@ -360,6 +360,46 @@ export class WorkerJobQueueManager { } this.#runningJobs.clear() } + + /** + * Merge new data into the last pending job in the queue if it has the given name, + * otherwise enqueue a new fire-and-forget job. + * + * This prevents queue flooding when high-frequency events arrive: as long as the tail job + * has not yet been picked up by the worker, incoming data is merged into it in-place. + * Once the worker starts executing it or another jobName gets queued, the next call will enqueue a fresh job. + * + * @param queueName - The target queue + * @param jobName - The job type name to match against + * @param generateData - Called to produce job data. Receives the existing job's data when + * merging into a tail job, or `null` when creating a new job. + */ + mergeOrQueueJob(queueName: string, jobName: string, generateData: (existing: unknown | null) => unknown): void { + const queue = this.#getOrCreateQueue(queueName) + + // Only inspect the very last entry. Scanning further back would risk updating a job in the + // middle of the queue, breaking the ordering guarantee relative to other job types. + const tail = queue.jobsHighPriority[queue.jobsHighPriority.length - 1] + if (tail && tail.spec.name === jobName) { + // Merge into the existing tail job in-place + tail.spec.data = generateData(tail.spec.data) + logger.debug(`Merged events into existing "${jobName}" job in queue "${queueName}"`) + } else { + // Tail is absent, already picked up, or a different job type — push a new job + const entry: JobEntry = { + spec: { + id: getRandomString(), + name: jobName, + data: generateData(null), + }, + completionHandler: null, + } + queue.jobsHighPriority.push(entry) + queue.metricsTotal.inc() + logger.debug(`Enqueued new "${jobName}" job in queue "${queueName}"`) + this.#notifyWorker(queue) + } + } } function generateCompletionHandler( diff --git a/meteor/server/worker/worker.ts b/meteor/server/worker/worker.ts index 6a813e0a3aa..d5b496ca45e 100644 --- a/meteor/server/worker/worker.ts +++ b/meteor/server/worker/worker.ts @@ -239,6 +239,33 @@ export async function QueueStudioJob( return queueManager.queueJobAndWrapResult(getStudioQueueName(studioId), jobName, jobParameters, now, options) } +/** + * Merge new job data into the last pending job in the studio's queue if it has the same name, + * otherwise enqueue a new fire-and-forget job. + * + * Use this instead of {@link QueueStudioJob} when events can be safely batched together and you + * do not need to await the result (e.g. device feedback events from gateways). + * + * @param jobName - The job type + * @param studioId - Id of the studio + * @param generateData - Called to produce the job data. Receives the existing pending job's data + * when merging, or `null` when creating a new job. Both cases are handled by the same function. + */ +export function QueueOrUpdateStudioJob( + jobName: T, + studioId: StudioId, + generateData: (existing: Parameters[0] | null) => Parameters[0] +): void { + if (isInTestWrite()) throw new Meteor.Error(404, 'Should not be reachable during startup tests') + if (!studioId) throw new Meteor.Error(500, 'Missing studioId') + + queueManager.mergeOrQueueJob( + getStudioQueueName(studioId), + jobName, + generateData as (existing: unknown | null) => unknown + ) +} + /** * Queue a job for ingest * @param jobName Job name diff --git a/meteor/tsconfig-meteor.json b/meteor/tsconfig-meteor.json index ab1b76a6c28..7d27cd7c2a1 100644 --- a/meteor/tsconfig-meteor.json +++ b/meteor/tsconfig-meteor.json @@ -4,7 +4,7 @@ { "compilerOptions": { /* Basic Options */ - "target": "es2018", + "target": "es2024", "module": "es2015", "lib": ["esnext"], "allowJs": true, diff --git a/meteor/yarn.lock b/meteor/yarn.lock index b5f457f7f10..82aed834502 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -2,7 +2,7 @@ # Manual changes might be lost - proceed with caution! __metadata: - version: 8 + version: 9 cacheKey: 10 "@aashutoshrathi/word-wrap@npm:^1.2.3": @@ -23,6 +23,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:7.26.2": + version: 7.26.2 + resolution: "@babel/code-frame@npm:7.26.2" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.25.9" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.0.0" + checksum: 10/db2c2122af79d31ca916755331bb4bac96feb2b334cdaca5097a6b467fdd41963b89b14b6836a14f083de7ff887fc78fa1b3c10b14e743d33e12dbfe5ee3d223 + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.27.1, @babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": version: 7.29.0 resolution: "@babel/code-frame@npm:7.29.0" @@ -134,7 +145,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.28.5": +"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.28.5": version: 7.28.5 resolution: "@babel/helper-validator-identifier@npm:7.28.5" checksum: 10/8e5d9b0133702cfacc7f368bf792f0f8ac0483794877c6dca5fcb73810ee138e27527701826fb58a40a004f3a5ec0a2f3c3dd5e326d262530b119918f3132ba7 @@ -368,10 +379,10 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/runtime@npm:7.28.6" - checksum: 10/fbcd439cb74d4a681958eb064c509829e3f46d8a4bfaaf441baa81bb6733d1e680bccc676c813883d7741bcaada1d0d04b15aa320ef280b5734e2192b50decf9 +"@babel/runtime@npm:^7.28.6, @babel/runtime@npm:^7.29.2": + version: 7.29.2 + resolution: "@babel/runtime@npm:7.29.2" + checksum: 10/f55ba4052aa0255055b34371a145fbe69c29b37b49eaea14805b095bfb4153701486416e89392fd27ec8abafa53868be86e960b9f8f959fff91f2c8ac2a14b02 languageName: node linkType: hard @@ -418,6 +429,13 @@ __metadata: languageName: node linkType: hard +"@borewit/text-codec@npm:^0.2.1": + version: 0.2.2 + resolution: "@borewit/text-codec@npm:0.2.2" + checksum: 10/c971790a72d9e766286db71f68613d1bac3b8bd9eaba52fbf18a8b17903c095968ed5369efdba378751926440aab93f3dd17c89242ef20525808ddced22d49b8 + languageName: node + linkType: hard + "@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0": version: 1.6.0 resolution: "@colors/colors@npm:1.6.0" @@ -425,6 +443,22 @@ __metadata: languageName: node linkType: hard +"@croct/json5-parser@npm:^0.2.2": + version: 0.2.2 + resolution: "@croct/json5-parser@npm:0.2.2" + dependencies: + "@croct/json": "npm:^2.1.0" + checksum: 10/27883b0c32503962a3d353829280b9e92f211cc90a485436a9625ff08ad907f936ca1662dc6056a3cea0c256c9d942395f3a9d4d4ae994817e085ae2ca352321 + languageName: node + linkType: hard + +"@croct/json@npm:^2.1.0": + version: 2.1.0 + resolution: "@croct/json@npm:2.1.0" + checksum: 10/8ccd333d533bb8d9cfee52a87473f37a33ad76794cebed47281317095adb0bdcc01f795f7adb56aa55999eb246155c2abfba381aaee4aab1e05ace34db0e5ecf + languageName: node + linkType: hard + "@dabh/diagnostics@npm:^2.0.8": version: 2.0.8 resolution: "@dabh/diagnostics@npm:2.0.8" @@ -436,6 +470,13 @@ __metadata: languageName: node linkType: hard +"@discoveryjs/json-ext@npm:0.5.7, @discoveryjs/json-ext@npm:^0.5.7": + version: 0.5.7 + resolution: "@discoveryjs/json-ext@npm:0.5.7" + checksum: 10/b95682a852448e8ef50d6f8e3b7ba288aab3fd98a2bafbe46881a3db0c6e7248a2debe9e1ee0d4137c521e4743ca5bbcb1c0765c9d7b3e0ef53231506fec42b4 + languageName: node + linkType: hard + "@elastic/ecs-helpers@npm:^2.1.1": version: 2.1.1 resolution: "@elastic/ecs-helpers@npm:2.1.1" @@ -452,35 +493,35 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:^1.4.3": - version: 1.8.1 - resolution: "@emnapi/core@npm:1.8.1" +"@emnapi/core@npm:^1.4.3, @emnapi/core@npm:^1.5.0": + version: 1.10.0 + resolution: "@emnapi/core@npm:1.10.0" dependencies: - "@emnapi/wasi-threads": "npm:1.1.0" + "@emnapi/wasi-threads": "npm:1.2.1" tslib: "npm:^2.4.0" - checksum: 10/904ea60c91fc7d8aeb4a8f2c433b8cfb47c50618f2b6f37429fc5093c857c6381c60628a5cfbc3a7b0d75b0a288f21d4ed2d4533e82f92c043801ef255fd6a5c + checksum: 10/d32f386084e64deaf2609aabb8295d1ad5af6144d0f46d2060b76cc53f1f3b486df54bec9b0f33c37d85a3822e1193ebcd4e3deb4a5f0e4cd650aa2ffc631715 languageName: node linkType: hard -"@emnapi/runtime@npm:^1.4.3": - version: 1.8.1 - resolution: "@emnapi/runtime@npm:1.8.1" +"@emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.5.0": + version: 1.10.0 + resolution: "@emnapi/runtime@npm:1.10.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10/26725e202d4baefdc4a6ba770f703dfc80825a27c27a08c22bac1e1ce6f8f75c47b4fe9424d9b63239463c33ef20b650f08d710da18dfa1164a95e5acb865dba + checksum: 10/d21083d07fa0c2da171c142e78ef986b66b07d45b06accc0bcaf49fcc61bb4dbc10e1c1760813070165b9f49b054376a931045347f21c0f42ff1eb2d2040faac languageName: node linkType: hard -"@emnapi/wasi-threads@npm:1.1.0": - version: 1.1.0 - resolution: "@emnapi/wasi-threads@npm:1.1.0" +"@emnapi/wasi-threads@npm:1.2.1": + version: 1.2.1 + resolution: "@emnapi/wasi-threads@npm:1.2.1" dependencies: tslib: "npm:^2.4.0" - checksum: 10/0d557e75262d2f4c95cb2a456ba0785ef61f919ce488c1d76e5e3acfd26e00c753ef928cd80068363e0c166ba8cc0141305daf0f81aad5afcd421f38f11e0f4e + checksum: 10/57cd4292be81c05d26aa886d68a9e4c449ff666e8503fed6463dfc6b64a4e4213f03c152d53296b7cda32840271e38cd33347332070658f01befeb9bf4e59f36 languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.4.0, @eslint-community/eslint-utils@npm:^4.4.1, @eslint-community/eslint-utils@npm:^4.8.0": +"@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.5.0, @eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": version: 4.9.1 resolution: "@eslint-community/eslint-utils@npm:4.9.1" dependencies: @@ -491,21 +532,21 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.11.0, @eslint-community/regexpp@npm:^4.12.1": - version: 4.12.1 - resolution: "@eslint-community/regexpp@npm:4.12.1" - checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc +"@eslint-community/regexpp@npm:^4.11.0, @eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 10/049b280fddf71dd325514e0a520024969431dc3a8b02fa77476e6820e9122f28ab4c9168c11821f91a27982d2453bcd7a66193356ea84e84fb7c8d793be1ba0c languageName: node linkType: hard -"@eslint/config-array@npm:^0.21.1": - version: 0.21.1 - resolution: "@eslint/config-array@npm:0.21.1" +"@eslint/config-array@npm:^0.21.2": + version: 0.21.2 + resolution: "@eslint/config-array@npm:0.21.2" dependencies: "@eslint/object-schema": "npm:^2.1.7" debug: "npm:^4.3.1" - minimatch: "npm:^3.1.2" - checksum: 10/6eaa0435972f735ce52d581f355a0b616e50a9b8a73304a7015398096e252798b9b3b968a67b524eefb0fdeacc57c4d960f0ec6432abe1c1e24be815b88c5d18 + minimatch: "npm:^3.1.5" + checksum: 10/148477ba995cf57fc725601916d5a7914aa249112d8bec2c3ac9122e2b2f540e6ef013ff4f6785346a4b565f09b20db127fa6f7322f5ffbdb3f1f8d2078a531c languageName: node linkType: hard @@ -527,27 +568,27 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^3.3.1": - version: 3.3.3 - resolution: "@eslint/eslintrc@npm:3.3.3" +"@eslint/eslintrc@npm:^3.3.5": + version: 3.3.5 + resolution: "@eslint/eslintrc@npm:3.3.5" dependencies: - ajv: "npm:^6.12.4" + ajv: "npm:^6.14.0" debug: "npm:^4.3.2" espree: "npm:^10.0.1" globals: "npm:^14.0.0" ignore: "npm:^5.2.0" import-fresh: "npm:^3.2.1" js-yaml: "npm:^4.1.1" - minimatch: "npm:^3.1.2" + minimatch: "npm:^3.1.5" strip-json-comments: "npm:^3.1.1" - checksum: 10/b586a364ff15ce1b68993aefc051ca330b1fece15fb5baf4a708d00113f9a14895cffd84a5f24c5a97bd4b4321130ab2314f90aa462a250f6b859c2da2cba1f3 + checksum: 10/edabb65693d82a88cac3b2cf932a0f825e986b5e0a21ef08782d07e3a61ad87d39db67cfd5aeb146fd5053e5e24e389dbe5649ab22936a71d633c7b32a7e6d86 languageName: node linkType: hard -"@eslint/js@npm:9.39.2": - version: 9.39.2 - resolution: "@eslint/js@npm:9.39.2" - checksum: 10/6b7f676746f3111b5d1b23715319212ab9297868a0fa9980d483c3da8965d5841673aada2d5653e85a3f7156edee0893a7ae7035211b4efdcb2848154bb947f2 +"@eslint/js@npm:9.39.4": + version: 9.39.4 + resolution: "@eslint/js@npm:9.39.4" + checksum: 10/0a7ab4c4108cf2cadf66849ebd20f5957cc53052b88d8807d0b54e489dbf6ffcaf741e144e7f9b187c395499ce2e6ddc565dbfa4f60c6df455cf2b30bcbdc5a3 languageName: node linkType: hard @@ -568,15 +609,6 @@ __metadata: languageName: node linkType: hard -"@gulpjs/to-absolute-glob@npm:^4.0.0": - version: 4.0.0 - resolution: "@gulpjs/to-absolute-glob@npm:4.0.0" - dependencies: - is-negated-glob: "npm:^1.0.0" - checksum: 10/30ec7825064422b6f02c1975ab6c779ff73409411c37bec2e984262459935afd196c1dbe960075e914967a047743ccf726fce3d3ebb4417ca2e3c34538fbceb8 - languageName: node - linkType: hard - "@humanfs/core@npm:^0.19.1": version: 0.19.1 resolution: "@humanfs/core@npm:0.19.1" @@ -622,6 +654,247 @@ __metadata: languageName: node linkType: hard +"@inquirer/ansi@npm:^2.0.5": + version: 2.0.5 + resolution: "@inquirer/ansi@npm:2.0.5" + checksum: 10/482f8a606885ee0377a60eb5e9b303ae75fcfb2c6250819be348047c89e4e01a25feef369d3646dec7ba17e38cd5cc08271db6db21c401be315b3ada749e6b53 + languageName: node + linkType: hard + +"@inquirer/checkbox@npm:^5.1.4": + version: 5.1.4 + resolution: "@inquirer/checkbox@npm:5.1.4" + dependencies: + "@inquirer/ansi": "npm:^2.0.5" + "@inquirer/core": "npm:^11.1.9" + "@inquirer/figures": "npm:^2.0.5" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/1031846424338c293e6b77526c67b5c5d3dfbe4a12c0c7e8f993f376f99a7cbb6e9b5c993706177aa05130dbebb2e84b0408bc2dbc3026ade8250f2e36a9ea90 + languageName: node + linkType: hard + +"@inquirer/confirm@npm:^6.0.12": + version: 6.0.12 + resolution: "@inquirer/confirm@npm:6.0.12" + dependencies: + "@inquirer/core": "npm:^11.1.9" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/7195a02074b29c7562fd574b80ca1caa9a177fedb830f8d13831cb4498df7c8252862b9f0d964118c2bf139faffd78c9e0ecaad0972d6c02323f5f6efc7d408b + languageName: node + linkType: hard + +"@inquirer/core@npm:^11.1.9": + version: 11.1.9 + resolution: "@inquirer/core@npm:11.1.9" + dependencies: + "@inquirer/ansi": "npm:^2.0.5" + "@inquirer/figures": "npm:^2.0.5" + "@inquirer/type": "npm:^4.0.5" + cli-width: "npm:^4.1.0" + fast-wrap-ansi: "npm:^0.2.0" + mute-stream: "npm:^3.0.0" + signal-exit: "npm:^4.1.0" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/999949afced2c4c5145ad3c41293788cb23ad59209da826625212c0911e965ad3b504f4ae2b80887475e1029df968775a025ec3774434f9e371f58472447000e + languageName: node + linkType: hard + +"@inquirer/editor@npm:^5.1.1": + version: 5.1.1 + resolution: "@inquirer/editor@npm:5.1.1" + dependencies: + "@inquirer/core": "npm:^11.1.9" + "@inquirer/external-editor": "npm:^3.0.0" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/3f95eb0635bd7ea9687979cc34ce9438152faf876f3a541b4e1be79c318725ed1ed33185ee466653363c81b30929ccaa8c74e00707d6dc974b76524aeb2b5a27 + languageName: node + linkType: hard + +"@inquirer/expand@npm:^5.0.13": + version: 5.0.13 + resolution: "@inquirer/expand@npm:5.0.13" + dependencies: + "@inquirer/core": "npm:^11.1.9" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/6a7335a5689c5eb349a84e9752cfb92eafb9ac1c525a7ce5d79dbadefed39e20cc2f68251a0c080091dc76fda4d5fb5788ee5777ebdcdeb980ad263817ee796d + languageName: node + linkType: hard + +"@inquirer/external-editor@npm:^3.0.0": + version: 3.0.0 + resolution: "@inquirer/external-editor@npm:3.0.0" + dependencies: + chardet: "npm:^2.1.1" + iconv-lite: "npm:^0.7.2" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/a2b0a255601f563317c21547778fb081d0356de478ffa70eb29a9e2247761a76b97fb7f50dcc5e1e3cafb2f888f3ac684374c35f929d1f8b280361c6c66c97d0 + languageName: node + linkType: hard + +"@inquirer/figures@npm:^2.0.5": + version: 2.0.5 + resolution: "@inquirer/figures@npm:2.0.5" + checksum: 10/e4d09c11a75206578abcfd8fc69b0f54cff7a853826696df5b3a45ed24ebc5c82e8998f1e9fa42119de848e6a0a526a6ac476053800413637bf6d21c2116cc60 + languageName: node + linkType: hard + +"@inquirer/input@npm:^5.0.12": + version: 5.0.12 + resolution: "@inquirer/input@npm:5.0.12" + dependencies: + "@inquirer/core": "npm:^11.1.9" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/2a0c987442b0214eca2c80d3a9c99721bd9229ddf814710a5d719f76d2fb846afb4fee551c1affdfa8ef6e40b3453f984090732377715c7cc9dd7fa7c416a1a0 + languageName: node + linkType: hard + +"@inquirer/number@npm:^4.0.12": + version: 4.0.12 + resolution: "@inquirer/number@npm:4.0.12" + dependencies: + "@inquirer/core": "npm:^11.1.9" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/2315c7ba170a116510eb9f0325ba4be84c4a01375cfeaa2d313264e47ece8004a7626ec6cf601c04bc9942931e4f7ed6a6879366ec4fdf8d73d86ef81b43a5bb + languageName: node + linkType: hard + +"@inquirer/password@npm:^5.0.12": + version: 5.0.12 + resolution: "@inquirer/password@npm:5.0.12" + dependencies: + "@inquirer/ansi": "npm:^2.0.5" + "@inquirer/core": "npm:^11.1.9" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/feca5d674530c7cb05a7056de066edcfd966fc0dffee90c9fb60b82b121523550d430af00462fe332d150fabfc7431e1ee2bce006bc31ba249f94e8daa98c71e + languageName: node + linkType: hard + +"@inquirer/prompts@npm:^8.4.2": + version: 8.4.2 + resolution: "@inquirer/prompts@npm:8.4.2" + dependencies: + "@inquirer/checkbox": "npm:^5.1.4" + "@inquirer/confirm": "npm:^6.0.12" + "@inquirer/editor": "npm:^5.1.1" + "@inquirer/expand": "npm:^5.0.13" + "@inquirer/input": "npm:^5.0.12" + "@inquirer/number": "npm:^4.0.12" + "@inquirer/password": "npm:^5.0.12" + "@inquirer/rawlist": "npm:^5.2.8" + "@inquirer/search": "npm:^4.1.8" + "@inquirer/select": "npm:^5.1.4" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/96d7b01ca9961554e1739c60c82a2162233f246c2b401cff1b1376861e7e732814475aa20bcbe74014a4a8bda7872a907f9c8d9beb034f68d900e04606b32f48 + languageName: node + linkType: hard + +"@inquirer/rawlist@npm:^5.2.8": + version: 5.2.8 + resolution: "@inquirer/rawlist@npm:5.2.8" + dependencies: + "@inquirer/core": "npm:^11.1.9" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/ac975f95aecbf66c96f1814ab9d4601cb059ebc04b9371a3335dc71dc5c05141cf7c99502219785b187f3e53d86e65f823c5973e5c1b5d3b871d8e019d96ec3d + languageName: node + linkType: hard + +"@inquirer/search@npm:^4.1.8": + version: 4.1.8 + resolution: "@inquirer/search@npm:4.1.8" + dependencies: + "@inquirer/core": "npm:^11.1.9" + "@inquirer/figures": "npm:^2.0.5" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/a4ed90e0d71618b519c97e5180b93106c3d64ac9bfa3939d6a1a06f159d572fdd5a5f9be45225f8cd78913890965018d0108d8ad95c241ac5f38ccea2b1ba499 + languageName: node + linkType: hard + +"@inquirer/select@npm:^5.1.4": + version: 5.1.4 + resolution: "@inquirer/select@npm:5.1.4" + dependencies: + "@inquirer/ansi": "npm:^2.0.5" + "@inquirer/core": "npm:^11.1.9" + "@inquirer/figures": "npm:^2.0.5" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/e11893e979fccdd1b8d2d51858dd59f443c3062337f659961fbd11062854e542d6fdf6cdc71ff24846790a0251a16abc4bd38767eb44c26671e9a23a4e1da3ff + languageName: node + linkType: hard + +"@inquirer/type@npm:^4.0.5": + version: 4.0.5 + resolution: "@inquirer/type@npm:4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/83d15e11cc0586373070e8c262f69b1d1e4a6c72f58b3afb3d163479309f5a9bb584320eec2d85474506fb845a114e2c50010758fcf3af56c93293d579f76333 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -665,110 +938,109 @@ __metadata: languageName: node linkType: hard -"@jest/console@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/console@npm:30.2.0" +"@jest/console@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/console@npm:30.3.0" dependencies: - "@jest/types": "npm:30.2.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" chalk: "npm:^4.1.2" - jest-message-util: "npm:30.2.0" - jest-util: "npm:30.2.0" + jest-message-util: "npm:30.3.0" + jest-util: "npm:30.3.0" slash: "npm:^3.0.0" - checksum: 10/7cda9793962afa5c7fcfdde0ff5012694683b17941ee3c6a55ea9fd9a02f1c51ec4b4c767b867e1226f85a26af1d0f0d72c6a344e34c5bc4300312ebffd6e50b + checksum: 10/aa23c9d77975b7c547190394272454e3563fbf0f99e7170f8b3f8128d83aaa62ad2d07291633e0ec1d4aee7e256dcf0b254bd391cdcd039d0ce6eac6ca835b24 languageName: node linkType: hard -"@jest/core@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/core@npm:30.2.0" +"@jest/core@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/core@npm:30.3.0" dependencies: - "@jest/console": "npm:30.2.0" + "@jest/console": "npm:30.3.0" "@jest/pattern": "npm:30.0.1" - "@jest/reporters": "npm:30.2.0" - "@jest/test-result": "npm:30.2.0" - "@jest/transform": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/reporters": "npm:30.3.0" + "@jest/test-result": "npm:30.3.0" + "@jest/transform": "npm:30.3.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" ansi-escapes: "npm:^4.3.2" chalk: "npm:^4.1.2" ci-info: "npm:^4.2.0" exit-x: "npm:^0.2.2" graceful-fs: "npm:^4.2.11" - jest-changed-files: "npm:30.2.0" - jest-config: "npm:30.2.0" - jest-haste-map: "npm:30.2.0" - jest-message-util: "npm:30.2.0" + jest-changed-files: "npm:30.3.0" + jest-config: "npm:30.3.0" + jest-haste-map: "npm:30.3.0" + jest-message-util: "npm:30.3.0" jest-regex-util: "npm:30.0.1" - jest-resolve: "npm:30.2.0" - jest-resolve-dependencies: "npm:30.2.0" - jest-runner: "npm:30.2.0" - jest-runtime: "npm:30.2.0" - jest-snapshot: "npm:30.2.0" - jest-util: "npm:30.2.0" - jest-validate: "npm:30.2.0" - jest-watcher: "npm:30.2.0" - micromatch: "npm:^4.0.8" - pretty-format: "npm:30.2.0" + jest-resolve: "npm:30.3.0" + jest-resolve-dependencies: "npm:30.3.0" + jest-runner: "npm:30.3.0" + jest-runtime: "npm:30.3.0" + jest-snapshot: "npm:30.3.0" + jest-util: "npm:30.3.0" + jest-validate: "npm:30.3.0" + jest-watcher: "npm:30.3.0" + pretty-format: "npm:30.3.0" slash: "npm:^3.0.0" peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true - checksum: 10/6763bb1efd937778f009821cd94c3705d3c31a156258a224b8745c1e0887976683f5413745ffb361b526f0fa2692e36aaa963aa197cc77ba932cff9d6d28af9d + checksum: 10/76f8561686e3bbaf2fcdc9c2391d47fef403e5fe0a936a48762ca60bcaf18692b5d2f8e5e26610cc43e965a6b120458dc9a7484e7e8ffb459118b61a90c2063d languageName: node linkType: hard -"@jest/diff-sequences@npm:30.0.1": - version: 30.0.1 - resolution: "@jest/diff-sequences@npm:30.0.1" - checksum: 10/0ddb7c7ba92d6057a2ee51a9cfc2155b77cca707fe959167466ea02dcb0687018cc3c22b9622f25f3a417d6ad370e2d4dcfedf9f1410dc9c02954a7484423cc7 +"@jest/diff-sequences@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/diff-sequences@npm:30.3.0" + checksum: 10/0d5b6e1599c5e0bb702f0804e7f93bbe4911b5929c40fd6a77c06105711eae24d709c8964e8d623cc70c34b7dc7262d76a115a6eb05f1576336cdb6c46593e7c languageName: node linkType: hard -"@jest/environment@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/environment@npm:30.2.0" +"@jest/environment@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/environment@npm:30.3.0" dependencies: - "@jest/fake-timers": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/fake-timers": "npm:30.3.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" - jest-mock: "npm:30.2.0" - checksum: 10/e168a4ff328980eb9fde5e43aea80807fd0b2dbd4579ae8f68a03415a1e58adf5661db298054fa2351c7cb2b5a74bf67b8ab996656cf5927d0b0d0b6e2c2966b + jest-mock: "npm:30.3.0" + checksum: 10/9b64add2e5430411ca997aed23cd34786d0e87562f5930ad0d4160df51435ae061809fcaa6bbc6c0ff9f0ba5f1241a5ce9a32ec772fa1d7c6b022f0169b622a4 languageName: node linkType: hard -"@jest/expect-utils@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/expect-utils@npm:30.2.0" +"@jest/expect-utils@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/expect-utils@npm:30.3.0" dependencies: "@jest/get-type": "npm:30.1.0" - checksum: 10/f2442f1bceb3411240d0f16fd0074377211b4373d3b8b2dc28929e861b6527a6deb403a362c25afa511d933cda4dfbdc98d4a08eeb51ee4968f7cb0299562349 + checksum: 10/766fd24f527a13004c542c2642b68b9142270801ab20bd448a559d9c2f40af079d0eb9ec9520a47f97b4d6c7d0837ba46e86284f53c939f11d9fcbda73a11e19 languageName: node linkType: hard -"@jest/expect@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/expect@npm:30.2.0" +"@jest/expect@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/expect@npm:30.3.0" dependencies: - expect: "npm:30.2.0" - jest-snapshot: "npm:30.2.0" - checksum: 10/d950d95a64d5c6a39d56171dabb8dbe59423096231bb4f21d8ee0019878e6626701ac9d782803dc2589e2799ed39704031f818533f8a3e571b57032eafa85d12 + expect: "npm:30.3.0" + jest-snapshot: "npm:30.3.0" + checksum: 10/74832945a2b18c7b962b27e0ca4d25d19a29d1c3ca6fe4a9c23946025b4146799e62a81d50060ac7bcaf7036fb477aa350ddf300e215333b42d013a3d9f8ba2b languageName: node linkType: hard -"@jest/fake-timers@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/fake-timers@npm:30.2.0" +"@jest/fake-timers@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/fake-timers@npm:30.3.0" dependencies: - "@jest/types": "npm:30.2.0" - "@sinonjs/fake-timers": "npm:^13.0.0" + "@jest/types": "npm:30.3.0" + "@sinonjs/fake-timers": "npm:^15.0.0" "@types/node": "npm:*" - jest-message-util: "npm:30.2.0" - jest-mock: "npm:30.2.0" - jest-util: "npm:30.2.0" - checksum: 10/c2df66576ba8049b07d5f239777243e21fcdaa09a446be1e55fac709d6273e2a926c1562e0372c3013142557ed9d386381624023549267a667b6e1b656e37fe6 + jest-message-util: "npm:30.3.0" + jest-mock: "npm:30.3.0" + jest-util: "npm:30.3.0" + checksum: 10/e39d30b61ae85485bfa0b1d86d62d866d33964bf0b95b8b4f45d2f1f1baa94fd7e134c7729370a58cb67b58d2b860fb396290b5c271782ed4d3728341027549b languageName: node linkType: hard @@ -779,15 +1051,15 @@ __metadata: languageName: node linkType: hard -"@jest/globals@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/globals@npm:30.2.0" +"@jest/globals@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/globals@npm:30.3.0" dependencies: - "@jest/environment": "npm:30.2.0" - "@jest/expect": "npm:30.2.0" - "@jest/types": "npm:30.2.0" - jest-mock: "npm:30.2.0" - checksum: 10/d4a331d3847cebb3acefe120350d8a6bb5517c1403de7cd2b4dc67be425f37ba0511beee77d6837b4da2d93a25a06d6f829ad7837da365fae45e1da57523525c + "@jest/environment": "npm:30.3.0" + "@jest/expect": "npm:30.3.0" + "@jest/types": "npm:30.3.0" + jest-mock: "npm:30.3.0" + checksum: 10/485bdc0f35faf3e76cb451b75e16892d87f7ab5757e290b1a9e849a3af0ef81c47abddb188fbc0442a4689514cf0551e34d13970c9cf03610a269c39f800ff46 languageName: node linkType: hard @@ -801,30 +1073,30 @@ __metadata: languageName: node linkType: hard -"@jest/reporters@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/reporters@npm:30.2.0" +"@jest/reporters@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/reporters@npm:30.3.0" dependencies: "@bcoe/v8-coverage": "npm:^0.2.3" - "@jest/console": "npm:30.2.0" - "@jest/test-result": "npm:30.2.0" - "@jest/transform": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/console": "npm:30.3.0" + "@jest/test-result": "npm:30.3.0" + "@jest/transform": "npm:30.3.0" + "@jest/types": "npm:30.3.0" "@jridgewell/trace-mapping": "npm:^0.3.25" "@types/node": "npm:*" chalk: "npm:^4.1.2" collect-v8-coverage: "npm:^1.0.2" exit-x: "npm:^0.2.2" - glob: "npm:^10.3.10" + glob: "npm:^10.5.0" graceful-fs: "npm:^4.2.11" istanbul-lib-coverage: "npm:^3.0.0" istanbul-lib-instrument: "npm:^6.0.0" istanbul-lib-report: "npm:^3.0.0" istanbul-lib-source-maps: "npm:^5.0.0" istanbul-reports: "npm:^3.1.3" - jest-message-util: "npm:30.2.0" - jest-util: "npm:30.2.0" - jest-worker: "npm:30.2.0" + jest-message-util: "npm:30.3.0" + jest-util: "npm:30.3.0" + jest-worker: "npm:30.3.0" slash: "npm:^3.0.0" string-length: "npm:^4.0.2" v8-to-istanbul: "npm:^9.0.1" @@ -833,7 +1105,7 @@ __metadata: peerDependenciesMeta: node-notifier: optional: true - checksum: 10/3848b59bf740c10c4e5c234dcc41c54adbd74932bf05d1d1582d09d86e9baa86ddaf3c43903505fd042ba1203c2889a732137d08058ce9dc0069ba33b5d5373d + checksum: 10/50cc20d9e908239352c5c6bc594c2880e30e16db6f8c0657513d1a46e3a761ed20464afa604af35bc72cbca0eac6cd34829c075513ecf725af03161a7662097e languageName: node linkType: hard @@ -846,15 +1118,15 @@ __metadata: languageName: node linkType: hard -"@jest/snapshot-utils@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/snapshot-utils@npm:30.2.0" +"@jest/snapshot-utils@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/snapshot-utils@npm:30.3.0" dependencies: - "@jest/types": "npm:30.2.0" + "@jest/types": "npm:30.3.0" chalk: "npm:^4.1.2" graceful-fs: "npm:^4.2.11" natural-compare: "npm:^1.4.0" - checksum: 10/6b30ab2b0682117e3ce775e70b5be1eb01e1ea53a74f12ac7090cd1a5f37e9b795cd8de83853afa7b4b799c96b1c482499aa993ca2034ea0679525d32b7f9625 + checksum: 10/2214d4f0f33d2363a0785c0ba75066bf4ed4beefd5b2d2a5c3124d66ab92f91163f03696be625223bdb0527f1e6360c4b306ba9ae421aeb966d4a57d6d972099 languageName: node linkType: hard @@ -869,56 +1141,55 @@ __metadata: languageName: node linkType: hard -"@jest/test-result@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/test-result@npm:30.2.0" +"@jest/test-result@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/test-result@npm:30.3.0" dependencies: - "@jest/console": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/console": "npm:30.3.0" + "@jest/types": "npm:30.3.0" "@types/istanbul-lib-coverage": "npm:^2.0.6" collect-v8-coverage: "npm:^1.0.2" - checksum: 10/f58f79c3c3ba6dd15325e05b0b5a300777cd8cc38327f622608b6fe849b1073ee9633e33d1e5d7ef5b97a1ce71543d0ad92674b7a279f53033143e8dd7c22959 + checksum: 10/89bed2adc8077e592deb74e4a9bd6c1d937c1ae18805b3b4e799d00276ab91a4974b7dc1f38dc12a5da7712ef0ba2e63c69245696e63f4a7b292fc79bb3981b7 languageName: node linkType: hard -"@jest/test-sequencer@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/test-sequencer@npm:30.2.0" +"@jest/test-sequencer@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/test-sequencer@npm:30.3.0" dependencies: - "@jest/test-result": "npm:30.2.0" + "@jest/test-result": "npm:30.3.0" graceful-fs: "npm:^4.2.11" - jest-haste-map: "npm:30.2.0" + jest-haste-map: "npm:30.3.0" slash: "npm:^3.0.0" - checksum: 10/7923964b27048b2233858b32aa1b34d4dd9e404311626d944a706bcdcaa0b1585f43f2ffa3fa893ecbf133566f31ba2b79ab5eaaaf674b8558c6c7029ecbea5e + checksum: 10/d2a593733b029bae5e1a60249fb8ced2fa701e2b336b69de4cd0a1e0008f4373ab1329422f819e209d1d95a29959bd0cc131c7f94c9ad8f3831833f79a08f997 languageName: node linkType: hard -"@jest/transform@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/transform@npm:30.2.0" +"@jest/transform@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/transform@npm:30.3.0" dependencies: "@babel/core": "npm:^7.27.4" - "@jest/types": "npm:30.2.0" + "@jest/types": "npm:30.3.0" "@jridgewell/trace-mapping": "npm:^0.3.25" babel-plugin-istanbul: "npm:^7.0.1" chalk: "npm:^4.1.2" convert-source-map: "npm:^2.0.0" fast-json-stable-stringify: "npm:^2.1.0" graceful-fs: "npm:^4.2.11" - jest-haste-map: "npm:30.2.0" + jest-haste-map: "npm:30.3.0" jest-regex-util: "npm:30.0.1" - jest-util: "npm:30.2.0" - micromatch: "npm:^4.0.8" + jest-util: "npm:30.3.0" pirates: "npm:^4.0.7" slash: "npm:^3.0.0" write-file-atomic: "npm:^5.0.1" - checksum: 10/c75d72d524c2a50ea6c05778a9b76a6e48bc228a3390896a6edd4416f7b4954ee0a07e229ed7b4949ce8889324b70034c784751e3fc455a25648bd8dcad17d0d + checksum: 10/279b6b73f59c274d7011febcbc0a1fa8939e8f677801a0a9bd95b9cf49244957267f3769c8cd541ae8026d8176089cd5e55f0f8d5361ec7788970978f4f394b4 languageName: node linkType: hard -"@jest/types@npm:30.2.0": - version: 30.2.0 - resolution: "@jest/types@npm:30.2.0" +"@jest/types@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/types@npm:30.3.0" dependencies: "@jest/pattern": "npm:30.0.1" "@jest/schemas": "npm:30.0.5" @@ -927,7 +1198,7 @@ __metadata: "@types/node": "npm:*" "@types/yargs": "npm:^17.0.33" chalk: "npm:^4.1.2" - checksum: 10/f50fcaea56f873a51d19254ab16762f2ea8ca88e3e08da2e496af5da2b67c322915a4fcd0153803cc05063ffe87ebef2ab4330e0a1b06ab984a26c916cbfc26b + checksum: 10/d6943cc270f07c7bc1ee6f3bb9ad1263ce7897d1a282221bf1d27499d77f2a68cfa6625ca73c193d3f81fe22a8e00635cd7acb5e73a546965c172219c81ec12c languageName: node linkType: hard @@ -958,7 +1229,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": +"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": version: 1.5.5 resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 @@ -975,246 +1246,1001 @@ __metadata: languageName: node linkType: hard -"@koa/cors@npm:^5.0.0": - version: 5.0.0 - resolution: "@koa/cors@npm:5.0.0" - dependencies: - vary: "npm:^1.1.2" - checksum: 10/3a0e32fbc422a5f9a41540ce3b7499d46073ddb0e4e851394a74bac5ecd0eaa1f24a8f189b7bd6a50c5863788ae6945c52d990edf99fdd2151a4404f266fe2e7 +"@jsonjoy.com/base64@npm:17.67.0": + version: 17.67.0 + resolution: "@jsonjoy.com/base64@npm:17.67.0" + peerDependencies: + tslib: 2 + checksum: 10/ae44b0c4c83ecc5c0ee1911706a665e18e89d64a2b705cc458d7f6fc3c3c7db0e621261e978d02b74ded6a9fe1aafc8e708eb8a133e794a92bb033c50a0c4ccd languageName: node linkType: hard -"@koa/router@npm:^15.3.0": - version: 15.3.0 - resolution: "@koa/router@npm:15.3.0" - dependencies: - debug: "npm:^4.4.3" - http-errors: "npm:^2.0.1" - koa-compose: "npm:^4.1.0" - path-to-regexp: "npm:^8.3.0" +"@jsonjoy.com/base64@npm:^1.1.2": + version: 1.1.2 + resolution: "@jsonjoy.com/base64@npm:1.1.2" peerDependencies: - koa: ^2.0.0 || ^3.0.0 - peerDependenciesMeta: - koa: - optional: false - checksum: 10/5f2679916514c28a1694ec3c0eac1b869c21d5527a4fdc4b248b3f4c595010389973401eadf6ff3f5c26d24ee84a391325880de51320f1965f917a21120d2899 + tslib: 2 + checksum: 10/d76bb58eff841c090d9bf69a073611ffa73c40a664ccbcea689f65961f57d7b24051269d06b437e4f6204285d6ba92f50f587c5e95c5f9e4f10b36a2ed4cd0c8 languageName: node linkType: hard -"@meteorjs/browserify-sign@npm:^4.2.3": - version: 4.2.6 - resolution: "@meteorjs/browserify-sign@npm:4.2.6" - dependencies: - bn.js: "npm:^5.2.1" - brorand: "npm:^1.1.0" - browserify-rsa: "npm:^4.1.0" - create-hash: "npm:^1.2.0" - create-hmac: "npm:^1.1.7" - hash-base: "npm:~3.0" - hash.js: "npm:^1.0.0" - hmac-drbg: "npm:^1.0.1" - inherits: "npm:^2.0.4" - minimalistic-assert: "npm:^1.0.1" - minimalistic-crypto-utils: "npm:^1.0.1" - parse-asn1: "npm:^5.1.7" - readable-stream: "npm:^2.3.8" - safe-buffer: "npm:^5.2.1" - checksum: 10/a4e5dc58d348f373a28ba3e55b27967780e8b674a180f6408db944de888e647f6d10c1b2f7a544f8fafcfcd50e0e990a4a5cc974f38346655c3a4f029001c640 +"@jsonjoy.com/buffers@npm:17.67.0, @jsonjoy.com/buffers@npm:^17.65.0": + version: 17.67.0 + resolution: "@jsonjoy.com/buffers@npm:17.67.0" + peerDependencies: + tslib: 2 + checksum: 10/6c8f6c4c73ec4ddab538a88be0bf72d8a934752755d43b0289fbe19ce9fa6123f082d1cd5ae179495e121a2f50267d26d36641f6dadedd8d5d2a2f980426e8ff languageName: node linkType: hard -"@meteorjs/create-ecdh@npm:^4.0.4": - version: 4.0.5 - resolution: "@meteorjs/create-ecdh@npm:4.0.5" - dependencies: - bn.js: "npm:^4.11.9" - brorand: "npm:^1.1.0" - hash.js: "npm:^1.0.0" - hmac-drbg: "npm:^1.0.1" - inherits: "npm:^2.0.4" - minimalistic-assert: "npm:^1.0.1" - minimalistic-crypto-utils: "npm:^1.0.1" - checksum: 10/e2173d7594eb0be2dd5cdd8840a9c49f0c653948ab73a6b5370bbfc22519255cc44cbd7dcd093cf9900064bf275adba3feb3de710e1fe98428e6a2f228f8c2ec +"@jsonjoy.com/buffers@npm:^1.0.0, @jsonjoy.com/buffers@npm:^1.2.0": + version: 1.2.1 + resolution: "@jsonjoy.com/buffers@npm:1.2.1" + peerDependencies: + tslib: 2 + checksum: 10/8ef4784d05c0fb4d0f27a1f78f5b0ae1f3b537d237f978d10be0b88f59a534ae44db2a4bde28eee0eb461ede31dc194aab5927ac001ed2b764629fa43ae9b60b languageName: node linkType: hard -"@meteorjs/crypto-browserify@npm:^3.12.1": - version: 3.12.4 - resolution: "@meteorjs/crypto-browserify@npm:3.12.4" - dependencies: - "@meteorjs/browserify-sign": "npm:^4.2.3" - "@meteorjs/create-ecdh": "npm:^4.0.4" - browserify-cipher: "npm:^1.0.1" - create-hash: "npm:^1.2.0" - create-hmac: "npm:^1.1.7" - diffie-hellman: "npm:^5.0.3" - hash-base: "npm:~3.0.4" - inherits: "npm:^2.0.4" - pbkdf2: "npm:^3.1.2" - public-encrypt: "npm:^4.0.3" - randombytes: "npm:^2.1.0" - randomfill: "npm:^1.0.4" - checksum: 10/c6c033ee5efda6e2340dc8719eb17e838c256f0c52c7cfedb711f0e002c78d97f1466c3ececa7ba4cd042dda804987fa093113f81e65028a132728c7c5c75cad +"@jsonjoy.com/codegen@npm:17.67.0": + version: 17.67.0 + resolution: "@jsonjoy.com/codegen@npm:17.67.0" + peerDependencies: + tslib: 2 + checksum: 10/e2462836c708999d045c4a15099f12e721089a3731f0ad33da210559a52ed763b8bddbec3c181857341984ef12ea355290609f37f0dc6f8de1545c028090adf5 languageName: node linkType: hard -"@mongodb-js/saslprep@npm:^1.3.0": - version: 1.4.5 - resolution: "@mongodb-js/saslprep@npm:1.4.5" - dependencies: - sparse-bitfield: "npm:^3.0.3" - checksum: 10/40cde05e68d5ab243b1db7196b86b91c1de099a451c73fe2faa4ba3f220009f0e829a150a716de991a764068fd12f5d9303ae7d05ab3c9973d39c5588a67ebf7 +"@jsonjoy.com/codegen@npm:^1.0.0": + version: 1.0.0 + resolution: "@jsonjoy.com/codegen@npm:1.0.0" + peerDependencies: + tslib: 2 + checksum: 10/a0afb03d2af4fbc1377c547e507f5db99a25f515d8c4b6b2cef1ff28145ac59fff12b6e1f41f9734cb62ea5619e7f9be1acd0908305d6f4176898ee534ee9a64 languageName: node linkType: hard -"@mos-connection/helper@npm:^5.0.0-alpha.0": - version: 5.0.0-alpha.0 - resolution: "@mos-connection/helper@npm:5.0.0-alpha.0" +"@jsonjoy.com/fs-core@npm:4.57.2": + version: 4.57.2 + resolution: "@jsonjoy.com/fs-core@npm:4.57.2" dependencies: - "@mos-connection/model": "npm:5.0.0-alpha.0" - iconv-lite: "npm:^0.7.0" - tslib: "npm:^2.8.1" - xml-js: "npm:^1.6.11" - xmlbuilder: "npm:^15.1.1" - checksum: 10/bcfcd90839c508bcf38294a27824357c61c3ac76317beb127ad1b984a75942eaad3e04353ada962e5ee93ddbbb9e1dc70503230e74f98738519b0d64e0786a16 + "@jsonjoy.com/fs-node-builtins": "npm:4.57.2" + "@jsonjoy.com/fs-node-utils": "npm:4.57.2" + thingies: "npm:^2.5.0" + peerDependencies: + tslib: 2 + checksum: 10/6db8b3a7fb874229c7991bbdc094d752adbde7d774e5ef70df5a787130c7c8ed4ac2d34eaac079383c527269feaa91d1cb4f5c1504af995cca95070af769a0bd languageName: node linkType: hard -"@mos-connection/model@npm:5.0.0-alpha.0, @mos-connection/model@npm:^5.0.0-alpha.0": - version: 5.0.0-alpha.0 +"@jsonjoy.com/fs-fsa@npm:4.57.2": + version: 4.57.2 + resolution: "@jsonjoy.com/fs-fsa@npm:4.57.2" + dependencies: + "@jsonjoy.com/fs-core": "npm:4.57.2" + "@jsonjoy.com/fs-node-builtins": "npm:4.57.2" + "@jsonjoy.com/fs-node-utils": "npm:4.57.2" + thingies: "npm:^2.5.0" + peerDependencies: + tslib: 2 + checksum: 10/0edf3b73d06a27e81f8a8e3b042022b9440c4794bb21d9957c15cd5c87f629e7e2f6695d464f82bb52d16e08fb3e682090ced5e712cc5bb05b41cbe99ce6e393 + languageName: node + linkType: hard + +"@jsonjoy.com/fs-node-builtins@npm:4.57.2": + version: 4.57.2 + resolution: "@jsonjoy.com/fs-node-builtins@npm:4.57.2" + peerDependencies: + tslib: 2 + checksum: 10/3284f0f0a989ad2bc0abc485748b2f3581648401c7d86be9b4541374f65050d384b61b5e44eff9b463d43fd1764bead1251783681105962ba5954b5e64b42480 + languageName: node + linkType: hard + +"@jsonjoy.com/fs-node-to-fsa@npm:4.57.2": + version: 4.57.2 + resolution: "@jsonjoy.com/fs-node-to-fsa@npm:4.57.2" + dependencies: + "@jsonjoy.com/fs-fsa": "npm:4.57.2" + "@jsonjoy.com/fs-node-builtins": "npm:4.57.2" + "@jsonjoy.com/fs-node-utils": "npm:4.57.2" + peerDependencies: + tslib: 2 + checksum: 10/8d6e7447c640c02eb89c03e6a565af13b30607402a83ab462f0e16cced95d1cf0a09cc43fe297c379e51e905e0a6f7e14e65a65d19ece0756c3ae888e618e88c + languageName: node + linkType: hard + +"@jsonjoy.com/fs-node-utils@npm:4.57.2": + version: 4.57.2 + resolution: "@jsonjoy.com/fs-node-utils@npm:4.57.2" + dependencies: + "@jsonjoy.com/fs-node-builtins": "npm:4.57.2" + peerDependencies: + tslib: 2 + checksum: 10/f63c7c8fd5a63a163a01bc70dac262419bcc1ae182186f249e038703b04a401e49eab9043514384555177e9385929b58a76cab945e8c7cdc6809efc2ea50bf31 + languageName: node + linkType: hard + +"@jsonjoy.com/fs-node@npm:4.57.2": + version: 4.57.2 + resolution: "@jsonjoy.com/fs-node@npm:4.57.2" + dependencies: + "@jsonjoy.com/fs-core": "npm:4.57.2" + "@jsonjoy.com/fs-node-builtins": "npm:4.57.2" + "@jsonjoy.com/fs-node-utils": "npm:4.57.2" + "@jsonjoy.com/fs-print": "npm:4.57.2" + "@jsonjoy.com/fs-snapshot": "npm:4.57.2" + glob-to-regex.js: "npm:^1.0.0" + thingies: "npm:^2.5.0" + peerDependencies: + tslib: 2 + checksum: 10/2e7777874624035b5503a6d7cbefa82c06adcf3ad63140cd8ce83082b46dace5f8981f98121cb27c79f5755b3790623fbc9facf2b27b00aca28498d4d33df611 + languageName: node + linkType: hard + +"@jsonjoy.com/fs-print@npm:4.57.2": + version: 4.57.2 + resolution: "@jsonjoy.com/fs-print@npm:4.57.2" + dependencies: + "@jsonjoy.com/fs-node-utils": "npm:4.57.2" + tree-dump: "npm:^1.1.0" + peerDependencies: + tslib: 2 + checksum: 10/4931c8de684a655ab11ba2285016c35f3558f1f8f3444ac42fe7f5ea417556661b6f88d238c107f30c30b2d131eb2e0ecb1bb8626a7f6e0440996f42ea31dd7b + languageName: node + linkType: hard + +"@jsonjoy.com/fs-snapshot@npm:4.57.2": + version: 4.57.2 + resolution: "@jsonjoy.com/fs-snapshot@npm:4.57.2" + dependencies: + "@jsonjoy.com/buffers": "npm:^17.65.0" + "@jsonjoy.com/fs-node-utils": "npm:4.57.2" + "@jsonjoy.com/json-pack": "npm:^17.65.0" + "@jsonjoy.com/util": "npm:^17.65.0" + peerDependencies: + tslib: 2 + checksum: 10/191b9d9a63f0ad30342da7ab37a45bbe84c935ae204e3780d69acf2fb12fdf07f7da8c3ade38255207de72fdb3b64a30e8dcc2ad93cfe424e77697d3cd419166 + languageName: node + linkType: hard + +"@jsonjoy.com/json-pack@npm:^1.11.0": + version: 1.21.0 + resolution: "@jsonjoy.com/json-pack@npm:1.21.0" + dependencies: + "@jsonjoy.com/base64": "npm:^1.1.2" + "@jsonjoy.com/buffers": "npm:^1.2.0" + "@jsonjoy.com/codegen": "npm:^1.0.0" + "@jsonjoy.com/json-pointer": "npm:^1.0.2" + "@jsonjoy.com/util": "npm:^1.9.0" + hyperdyperid: "npm:^1.2.0" + thingies: "npm:^2.5.0" + tree-dump: "npm:^1.1.0" + peerDependencies: + tslib: 2 + checksum: 10/138b7eb8c96e6e435b0218c8f2eb5554e4eb49198a8718673a65e81da53b4617553ffa7124b51d6ea00fdfb868d6ff8b5ad6365e8336380ca7025f04d0412ee7 + languageName: node + linkType: hard + +"@jsonjoy.com/json-pack@npm:^17.65.0": + version: 17.67.0 + resolution: "@jsonjoy.com/json-pack@npm:17.67.0" + dependencies: + "@jsonjoy.com/base64": "npm:17.67.0" + "@jsonjoy.com/buffers": "npm:17.67.0" + "@jsonjoy.com/codegen": "npm:17.67.0" + "@jsonjoy.com/json-pointer": "npm:17.67.0" + "@jsonjoy.com/util": "npm:17.67.0" + hyperdyperid: "npm:^1.2.0" + thingies: "npm:^2.5.0" + tree-dump: "npm:^1.1.0" + peerDependencies: + tslib: 2 + checksum: 10/9ff4403862e49433fe607175e90af749d64902640d63919ba559e5748d1a3db60d7366cc3b84dcc4a57ad478540e5eecb22fed80766e293482a0ab8e583b1b0b + languageName: node + linkType: hard + +"@jsonjoy.com/json-pointer@npm:17.67.0": + version: 17.67.0 + resolution: "@jsonjoy.com/json-pointer@npm:17.67.0" + dependencies: + "@jsonjoy.com/util": "npm:17.67.0" + peerDependencies: + tslib: 2 + checksum: 10/5a27c6b5b1276d357cfc3e8a05112d6305ccd17bf672190f25dfac2f4108ced170e784451d64728f60f93305c0007e3f832ddd175b8a47f3eb652cbabcec31ad + languageName: node + linkType: hard + +"@jsonjoy.com/json-pointer@npm:^1.0.2": + version: 1.0.2 + resolution: "@jsonjoy.com/json-pointer@npm:1.0.2" + dependencies: + "@jsonjoy.com/codegen": "npm:^1.0.0" + "@jsonjoy.com/util": "npm:^1.9.0" + peerDependencies: + tslib: 2 + checksum: 10/f22baeb3abc8ace2d8902d06ec297343431d4486dcf399aaaffd26ace7e62e194fe0efb4b7880e45b3b7939224ee838d3213448ef654fc8a61c91a76fe994d94 + languageName: node + linkType: hard + +"@jsonjoy.com/util@npm:17.67.0, @jsonjoy.com/util@npm:^17.65.0": + version: 17.67.0 + resolution: "@jsonjoy.com/util@npm:17.67.0" + dependencies: + "@jsonjoy.com/buffers": "npm:17.67.0" + "@jsonjoy.com/codegen": "npm:17.67.0" + peerDependencies: + tslib: 2 + checksum: 10/b0facf65c3190d6ed1ada7e5b7679d80fa5da73bfbd02f2bb2f3af1c28c0d854b6ee2350824313b7ba82c0e5191da94903b4af61255bc232dbb7feedd2f31e0c + languageName: node + linkType: hard + +"@jsonjoy.com/util@npm:^1.9.0": + version: 1.9.0 + resolution: "@jsonjoy.com/util@npm:1.9.0" + dependencies: + "@jsonjoy.com/buffers": "npm:^1.0.0" + "@jsonjoy.com/codegen": "npm:^1.0.0" + peerDependencies: + tslib: 2 + checksum: 10/1a6e5301d725a7161b93ff707eb1a954bf4552a2fa96eee9a960d3ae3ed5f993d18b56dcff29e98036341a5968c5d1b2dfe21f76695390e7f0d89b81f24c85e0 + languageName: node + linkType: hard + +"@koa/cors@npm:^5.0.0": + version: 5.0.0 + resolution: "@koa/cors@npm:5.0.0" + dependencies: + vary: "npm:^1.1.2" + checksum: 10/3a0e32fbc422a5f9a41540ce3b7499d46073ddb0e4e851394a74bac5ecd0eaa1f24a8f189b7bd6a50c5863788ae6945c52d990edf99fdd2151a4404f266fe2e7 + languageName: node + linkType: hard + +"@koa/router@npm:^15.4.0": + version: 15.4.0 + resolution: "@koa/router@npm:15.4.0" + dependencies: + debug: "npm:^4.4.3" + http-errors: "npm:^2.0.1" + koa-compose: "npm:^4.1.0" + path-to-regexp: "npm:^8.3.0" + peerDependencies: + koa: ^2.0.0 || ^3.0.0 + peerDependenciesMeta: + koa: + optional: false + checksum: 10/03332e1700ef812a5b89e941b3050b027bc71cfbaee3240f63d3763f93d3a6c740f81f09a19a1c45decaac4b477fecf146773168819e1537a22ade198324c702 + languageName: node + linkType: hard + +"@leichtgewicht/ip-codec@npm:^2.0.1": + version: 2.0.5 + resolution: "@leichtgewicht/ip-codec@npm:2.0.5" + checksum: 10/cb98c608392abe59457a14e00134e7dfa57c0c9b459871730cd4e907bb12b834cbd03e08ad8663fea9e486f260da7f1293ccd9af0376bf5524dd8536192f248c + languageName: node + linkType: hard + +"@meteorjs/browserify-sign@npm:^4.2.3": + version: 4.2.6 + resolution: "@meteorjs/browserify-sign@npm:4.2.6" + dependencies: + bn.js: "npm:^5.2.1" + brorand: "npm:^1.1.0" + browserify-rsa: "npm:^4.1.0" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + hash-base: "npm:~3.0" + hash.js: "npm:^1.0.0" + hmac-drbg: "npm:^1.0.1" + inherits: "npm:^2.0.4" + minimalistic-assert: "npm:^1.0.1" + minimalistic-crypto-utils: "npm:^1.0.1" + parse-asn1: "npm:^5.1.7" + readable-stream: "npm:^2.3.8" + safe-buffer: "npm:^5.2.1" + checksum: 10/a4e5dc58d348f373a28ba3e55b27967780e8b674a180f6408db944de888e647f6d10c1b2f7a544f8fafcfcd50e0e990a4a5cc974f38346655c3a4f029001c640 + languageName: node + linkType: hard + +"@meteorjs/create-ecdh@npm:^4.0.4": + version: 4.0.5 + resolution: "@meteorjs/create-ecdh@npm:4.0.5" + dependencies: + bn.js: "npm:^4.11.9" + brorand: "npm:^1.1.0" + hash.js: "npm:^1.0.0" + hmac-drbg: "npm:^1.0.1" + inherits: "npm:^2.0.4" + minimalistic-assert: "npm:^1.0.1" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10/e2173d7594eb0be2dd5cdd8840a9c49f0c653948ab73a6b5370bbfc22519255cc44cbd7dcd093cf9900064bf275adba3feb3de710e1fe98428e6a2f228f8c2ec + languageName: node + linkType: hard + +"@meteorjs/crypto-browserify@npm:^3.12.1": + version: 3.12.4 + resolution: "@meteorjs/crypto-browserify@npm:3.12.4" + dependencies: + "@meteorjs/browserify-sign": "npm:^4.2.3" + "@meteorjs/create-ecdh": "npm:^4.0.4" + browserify-cipher: "npm:^1.0.1" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + diffie-hellman: "npm:^5.0.3" + hash-base: "npm:~3.0.4" + inherits: "npm:^2.0.4" + pbkdf2: "npm:^3.1.2" + public-encrypt: "npm:^4.0.3" + randombytes: "npm:^2.1.0" + randomfill: "npm:^1.0.4" + checksum: 10/c6c033ee5efda6e2340dc8719eb17e838c256f0c52c7cfedb711f0e002c78d97f1466c3ececa7ba4cd042dda804987fa093113f81e65028a132728c7c5c75cad + languageName: node + linkType: hard + +"@meteorjs/rspack@npm:2.0.1": + version: 2.0.1 + resolution: "@meteorjs/rspack@npm:2.0.1" + dependencies: + fast-deep-equal: "npm:^3.1.3" + ignore-loader: "npm:^0.1.2" + node-polyfill-webpack-plugin: "npm:^4.1.0" + webpack-merge: "npm:^6.0.1" + peerDependencies: + "@rspack/cli": ">=1.3.0" + "@rspack/core": ">=1.3.0" + "@swc/core": ">=1.3.0" + checksum: 10/56712025ac211e29875834f7befb9f4cd3deaed88c71dc4065ec0eb73f26335de2e53020ed0f8305206fae5fcea03305d95a444505a33cef434ca72eee82400d + languageName: node + linkType: hard + +"@meteorjs/rspack@patch:@meteorjs/rspack@npm%3A2.0.1#~/.yarn/patches/@meteorjs-rspack-npm-2.0.1-d001eb481c.patch": + version: 2.0.1 + resolution: "@meteorjs/rspack@patch:@meteorjs/rspack@npm%3A2.0.1#~/.yarn/patches/@meteorjs-rspack-npm-2.0.1-d001eb481c.patch::version=2.0.1&hash=660a4f" + dependencies: + fast-deep-equal: "npm:^3.1.3" + ignore-loader: "npm:^0.1.2" + node-polyfill-webpack-plugin: "npm:^4.1.0" + webpack-merge: "npm:^6.0.1" + peerDependencies: + "@rspack/cli": ">=1.3.0" + "@rspack/core": ">=1.3.0" + "@swc/core": ">=1.3.0" + checksum: 10/731954011d435b7c6378fad258661cd6bae5801761cc40d8dd3f6fdfa24d9497760f2705e5e9473ed9f5c1b7ba6842446964c3467454aad16a3a8807951a25f7 + languageName: node + linkType: hard + +"@module-federation/error-codes@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/error-codes@npm:0.22.0" + checksum: 10/4edb269e9f3039899f879788c84d2bfecff94ca8e87ffcd80dbf8589d8543ec32558b3fa05c8549a8abd3ac33e856ff2aacf458dea5c0d7bea608bf12bb13359 + languageName: node + linkType: hard + +"@module-federation/runtime-core@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/runtime-core@npm:0.22.0" + dependencies: + "@module-federation/error-codes": "npm:0.22.0" + "@module-federation/sdk": "npm:0.22.0" + checksum: 10/d21969198322b6f79e0513b702d0af5097613d47819724c849b6c677c163cd10fb8c89e3ff62b798bec498ee4d8e95dec71861071bc4ed74bd86a7e43193bc05 + languageName: node + linkType: hard + +"@module-federation/runtime-tools@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/runtime-tools@npm:0.22.0" + dependencies: + "@module-federation/runtime": "npm:0.22.0" + "@module-federation/webpack-bundler-runtime": "npm:0.22.0" + checksum: 10/0e7693c1ec02fc5bef770b478c8757cad9cfefb2310d1943151d0ad079b72472d9b2c8a087299e9124dfcd6b649c83290c7fdfa333865baab4ba193f39e7b6bd + languageName: node + linkType: hard + +"@module-federation/runtime@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/runtime@npm:0.22.0" + dependencies: + "@module-federation/error-codes": "npm:0.22.0" + "@module-federation/runtime-core": "npm:0.22.0" + "@module-federation/sdk": "npm:0.22.0" + checksum: 10/eca608be999d7d2e83abc1169643c2f795a5ed950f9e2bdf7000400a30b3e1e0ca4bdaa5daa09f55e44868383d444707e40236cec1aaa7b40432b0cce800b7f3 + languageName: node + linkType: hard + +"@module-federation/sdk@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/sdk@npm:0.22.0" + checksum: 10/d7085d883730a33145052520787a7e59cf9c54b51b2946bebc7c63a6bb668bcc6cbdc27fa0b7354a62f5a7ee4e8829a66b84e644607498f2e37cfd5eb4ded0da + languageName: node + linkType: hard + +"@module-federation/webpack-bundler-runtime@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/webpack-bundler-runtime@npm:0.22.0" + dependencies: + "@module-federation/runtime": "npm:0.22.0" + "@module-federation/sdk": "npm:0.22.0" + checksum: 10/afd24406817dfc6474ebcf5be714ccf26690eb3f6f5172bda711c8f23dba149fe47293f7aa2d0733dfed0334c98d4d3d9e7c2da2be78750cae5a72d72f32ce93 + languageName: node + linkType: hard + +"@mongodb-js/saslprep@npm:^1.3.0": + version: 1.4.5 + resolution: "@mongodb-js/saslprep@npm:1.4.5" + dependencies: + sparse-bitfield: "npm:^3.0.3" + checksum: 10/40cde05e68d5ab243b1db7196b86b91c1de099a451c73fe2faa4ba3f220009f0e829a150a716de991a764068fd12f5d9303ae7d05ab3c9973d39c5588a67ebf7 + languageName: node + linkType: hard + +"@mos-connection/helper@npm:^5.0.0-alpha.0": + version: 5.0.0-alpha.0 + resolution: "@mos-connection/helper@npm:5.0.0-alpha.0" + dependencies: + "@mos-connection/model": "npm:5.0.0-alpha.0" + iconv-lite: "npm:^0.7.0" + tslib: "npm:^2.8.1" + xml-js: "npm:^1.6.11" + xmlbuilder: "npm:^15.1.1" + checksum: 10/bcfcd90839c508bcf38294a27824357c61c3ac76317beb127ad1b984a75942eaad3e04353ada962e5ee93ddbbb9e1dc70503230e74f98738519b0d64e0786a16 + languageName: node + linkType: hard + +"@mos-connection/model@npm:5.0.0-alpha.0, @mos-connection/model@npm:^5.0.0-alpha.0": + version: 5.0.0-alpha.0 resolution: "@mos-connection/model@npm:5.0.0-alpha.0" checksum: 10/e66edde4a7f0c20bc468151e5e90b49edd34dc63837a573b30400c3f1e981da2b3d6c504f361a8416eed545477598dc64999543ee5bd33fdcc70786e1330784f languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^0.2.11": - version: 0.2.12 - resolution: "@napi-rs/wasm-runtime@npm:0.2.12" - dependencies: - "@emnapi/core": "npm:^1.4.3" - "@emnapi/runtime": "npm:^1.4.3" - "@tybys/wasm-util": "npm:^0.10.0" - checksum: 10/5fd518182427980c28bc724adf06c5f32f9a8915763ef560b5f7d73607d30cd15ac86d0cbd2eb80d4cfab23fc80d0876d89ca36a9daadcb864bc00917c94187c +"@napi-rs/wasm-runtime@npm:1.0.7": + version: 1.0.7 + resolution: "@napi-rs/wasm-runtime@npm:1.0.7" + dependencies: + "@emnapi/core": "npm:^1.5.0" + "@emnapi/runtime": "npm:^1.5.0" + "@tybys/wasm-util": "npm:^0.10.1" + checksum: 10/6bc32d32d486d07b83220a9b7b2b715e39acacbacef0011ebca05c00b41d80a0535123da10fea7a7d6d7e206712bb50dc50ac3cf88b770754d44378570fb5c05 + languageName: node + linkType: hard + +"@napi-rs/wasm-runtime@npm:^0.2.11": + version: 0.2.12 + resolution: "@napi-rs/wasm-runtime@npm:0.2.12" + dependencies: + "@emnapi/core": "npm:^1.4.3" + "@emnapi/runtime": "npm:^1.4.3" + "@tybys/wasm-util": "npm:^0.10.0" + checksum: 10/5fd518182427980c28bc724adf06c5f32f9a8915763ef560b5f7d73607d30cd15ac86d0cbd2eb80d4cfab23fc80d0876d89ca36a9daadcb864bc00917c94187c + languageName: node + linkType: hard + +"@napi-rs/wasm-runtime@npm:^1.1.1": + version: 1.1.4 + resolution: "@napi-rs/wasm-runtime@npm:1.1.4" + dependencies: + "@tybys/wasm-util": "npm:^0.10.1" + peerDependencies: + "@emnapi/core": ^1.7.1 + "@emnapi/runtime": ^1.7.1 + checksum: 10/1db3dc7eeb981306b09360487bd8ce4dfa5588d273bd8ea9f07dccca1b4ade57b675414180fc9bb66966c6c50b17208b0263194993e2f7f92cc7af28bda4d1af + languageName: node + linkType: hard + +"@npmcli/agent@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/agent@npm:4.0.0" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^11.2.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10/1a81573becc60515031accc696e6405e9b894e65c12b98ef4aeee03b5617c41948633159dbf6caf5dde5b47367eeb749bdc7b7dfb21960930a9060a935c6f636 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^5.0.0": + version: 5.0.0 + resolution: "@npmcli/fs@npm:5.0.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10/4935c7719d17830d0f9fa46c50be17b2a3c945cec61760f6d0909bce47677c42e1810ca673305890f9e84f008ec4d8e841182f371e42100a8159d15f22249208 + languageName: node + linkType: hard + +"@opentelemetry/api@npm:^1.4.0, @opentelemetry/api@npm:^1.4.1": + version: 1.9.0 + resolution: "@opentelemetry/api@npm:1.9.0" + checksum: 10/a607f0eef971893c4f2ee2a4c2069aade6ec3e84e2a1f5c2aac19f65c5d9eeea41aa72db917c1029faafdd71789a1a040bdc18f40d63690e22ccae5d7070f194 + languageName: node + linkType: hard + +"@opentelemetry/core@npm:1.17.0, @opentelemetry/core@npm:^1.11.0": + version: 1.17.0 + resolution: "@opentelemetry/core@npm:1.17.0" + dependencies: + "@opentelemetry/semantic-conventions": "npm:1.17.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.7.0" + checksum: 10/809b4754faad1f51b352834a791299e73443c28a30821757233388d812aa2df9a61bd61254a9e580207e501b1be511c178e0414e5de5e2428ee559dc329ebb03 + languageName: node + linkType: hard + +"@opentelemetry/resources@npm:1.17.0": + version: 1.17.0 + resolution: "@opentelemetry/resources@npm:1.17.0" + dependencies: + "@opentelemetry/core": "npm:1.17.0" + "@opentelemetry/semantic-conventions": "npm:1.17.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.7.0" + checksum: 10/c3555c49a43addbbf2eb24c379195f295250eb163060aca01f662194e973070cabf006d0d9d6e1ed19008442bae46a765c16c23d9ca0d7ba7c4a7988c0047af2 + languageName: node + linkType: hard + +"@opentelemetry/sdk-metrics@npm:^1.12.0": + version: 1.17.0 + resolution: "@opentelemetry/sdk-metrics@npm:1.17.0" + dependencies: + "@opentelemetry/core": "npm:1.17.0" + "@opentelemetry/resources": "npm:1.17.0" + lodash.merge: "npm:^4.6.2" + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.7.0" + checksum: 10/45876ed27d564a64f2b0cc7f63699c5a5a91192cb20f124b15fb12a0e346aec1e5f65f19eb18031977e1a8119972b815573b750f4953dbc5eef150fe4d9eefc3 + languageName: node + linkType: hard + +"@opentelemetry/semantic-conventions@npm:1.17.0": + version: 1.17.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.17.0" + checksum: 10/1f6bbd4d543ad529ddb3f6b55e08940995b5958fa990bc54bfa50136fc0a93d12a9bfed7f3addb5d84b1afaade8bd4b9afc36d2fe2d65a3f6325511b3a29d851 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10/115e8ceeec6bc69dff2048b35c0ab4f8bbee12d8bb6c1f4af758604586d802b6e669dcb02dda61d078de42c2b4ddce41b3d9e726d7daa6b4b850f4adbf7333ff + languageName: node + linkType: hard + +"@pkgr/core@npm:^0.2.9": + version: 0.2.9 + resolution: "@pkgr/core@npm:0.2.9" + checksum: 10/bb2fb86977d63f836f8f5b09015d74e6af6488f7a411dcd2bfdca79d76b5a681a9112f41c45bdf88a9069f049718efc6f3900d7f1de66a2ec966068308ae517f + languageName: node + linkType: hard + +"@polka/url@npm:^1.0.0-next.24": + version: 1.0.0-next.29 + resolution: "@polka/url@npm:1.0.0-next.29" + checksum: 10/69ca11ab15a4ffec7f0b07fcc4e1f01489b3d9683a7e1867758818386575c60c213401259ba3705b8a812228d17e2bfd18e6f021194d943fff4bca389c9d4f28 + languageName: node + linkType: hard + +"@postalsys/gettext@npm:^4.1.0": + version: 4.1.1 + resolution: "@postalsys/gettext@npm:4.1.1" + checksum: 10/eb94234009a1ef72de5338265d975f63cd40bb3551ef6159b23ed72bb8a80f630a170124d5a4e51b3f5e6b4d534a6cbc5d419387323613c7355a88f23cfe9306 + languageName: node + linkType: hard + +"@rsbuild/plugin-check-syntax@npm:1.6.1": + version: 1.6.1 + resolution: "@rsbuild/plugin-check-syntax@npm:1.6.1" + dependencies: + acorn: "npm:^8.15.0" + browserslist-to-es-version: "npm:^1.2.0" + htmlparser2: "npm:10.0.0" + picocolors: "npm:^1.1.1" + source-map: "npm:^0.7.6" + peerDependencies: + "@rsbuild/core": ^1.0.0 || ^2.0.0-0 + peerDependenciesMeta: + "@rsbuild/core": + optional: true + checksum: 10/9ae6097a0800dd7bd8802dc9517d34d4e48a0d691b185e5a4b48f3830a11e7b40ed02e04db1d00edfe3c2dd8736748b02af3d58743dfaf74193b94248061c0b0 + languageName: node + linkType: hard + +"@rsdoctor/client@npm:1.5.7": + version: 1.5.7 + resolution: "@rsdoctor/client@npm:1.5.7" + checksum: 10/4cd49643ef536f5a30570540c7750a8868602750e33888f1629968ac2b8414b75f4faaf3345ce19daf4495d8f9ba4cc2752556d86a9896cc658b52b3c516aa37 + languageName: node + linkType: hard + +"@rsdoctor/core@npm:1.5.7": + version: 1.5.7 + resolution: "@rsdoctor/core@npm:1.5.7" + dependencies: + "@rsbuild/plugin-check-syntax": "npm:1.6.1" + "@rsdoctor/graph": "npm:1.5.7" + "@rsdoctor/sdk": "npm:1.5.7" + "@rsdoctor/types": "npm:1.5.7" + "@rsdoctor/utils": "npm:1.5.7" + "@rspack/resolver": "npm:0.2.8" + browserslist-load-config: "npm:^1.0.1" + es-toolkit: "npm:^1.45.1" + filesize: "npm:^10.1.6" + fs-extra: "npm:^11.1.1" + semver: "npm:^7.7.4" + source-map: "npm:^0.7.6" + checksum: 10/90ccc63c002d911a1a336bd05919078709c35a67fa4d7cb4677428674aff25929482dbb094521b8a9e823988a00ba84a41cb60b7f7b425a91dcb10f340c05d9a + languageName: node + linkType: hard + +"@rsdoctor/graph@npm:1.5.7": + version: 1.5.7 + resolution: "@rsdoctor/graph@npm:1.5.7" + dependencies: + "@rsdoctor/types": "npm:1.5.7" + "@rsdoctor/utils": "npm:1.5.7" + es-toolkit: "npm:^1.45.1" + path-browserify: "npm:1.0.1" + source-map: "npm:^0.7.6" + checksum: 10/95ccc588582e96b9b0d734f23985086cff910d012b195b42f3dad98a62985c890e02c323d09507bf17897271cd11bea27a931af22e7311147c40a6b9d5083e5b + languageName: node + linkType: hard + +"@rsdoctor/rspack-plugin@npm:1.5.7": + version: 1.5.7 + resolution: "@rsdoctor/rspack-plugin@npm:1.5.7" + dependencies: + "@rsdoctor/core": "npm:1.5.7" + "@rsdoctor/graph": "npm:1.5.7" + "@rsdoctor/sdk": "npm:1.5.7" + "@rsdoctor/types": "npm:1.5.7" + "@rsdoctor/utils": "npm:1.5.7" + peerDependencies: + "@rspack/core": "*" + peerDependenciesMeta: + "@rspack/core": + optional: true + checksum: 10/411694b90e2a99333bdce14522f73048dc7561642786b7835b9d50f116c336e20d9ffdbfac2d533b5a85b68e18fcfdb67423f2cafb11c99f8e0ed5cc811c9e09 + languageName: node + linkType: hard + +"@rsdoctor/sdk@npm:1.5.7": + version: 1.5.7 + resolution: "@rsdoctor/sdk@npm:1.5.7" + dependencies: + "@rsdoctor/client": "npm:1.5.7" + "@rsdoctor/graph": "npm:1.5.7" + "@rsdoctor/types": "npm:1.5.7" + "@rsdoctor/utils": "npm:1.5.7" + launch-editor: "npm:^2.13.2" + safer-buffer: "npm:2.1.2" + socket.io: "npm:4.8.1" + tapable: "npm:2.3.0" + checksum: 10/04a0c6421613c509f76a2c4f2822fb3206e1c25ded9b366df8355f0bea78cf346273c8a93c43b35388433c6a88e375d446dd393d57068d7f401241474a8ed018 + languageName: node + linkType: hard + +"@rsdoctor/types@npm:1.5.7": + version: 1.5.7 + resolution: "@rsdoctor/types@npm:1.5.7" + dependencies: + "@types/connect": "npm:3.4.38" + "@types/estree": "npm:1.0.5" + "@types/tapable": "npm:2.3.0" + source-map: "npm:^0.7.6" + peerDependencies: + "@rspack/core": "*" + webpack: 5.x + peerDependenciesMeta: + "@rspack/core": + optional: true + webpack: + optional: true + checksum: 10/d62084127a94a8aaded3fd9ba83995a3a2a8ae9b3c27766d44166f6b0da283e87b8eb174d52065132633e34eed798ec242c8c5b122b591349bb0e0b5c85f3ef4 + languageName: node + linkType: hard + +"@rsdoctor/utils@npm:1.5.7": + version: 1.5.7 + resolution: "@rsdoctor/utils@npm:1.5.7" + dependencies: + "@babel/code-frame": "npm:7.26.2" + "@rsdoctor/types": "npm:1.5.7" + "@types/estree": "npm:1.0.5" + acorn: "npm:^8.10.0" + acorn-import-attributes: "npm:^1.9.5" + acorn-walk: "npm:8.3.5" + deep-eql: "npm:4.1.4" + envinfo: "npm:7.21.0" + fs-extra: "npm:^11.1.1" + get-port: "npm:5.1.1" + json-stream-stringify: "npm:3.0.1" + lines-and-columns: "npm:2.0.4" + picocolors: "npm:^1.1.1" + rslog: "npm:^1.3.2" + strip-ansi: "npm:^6.0.1" + checksum: 10/dde280555811b13dd0e1cf619597ebc08170cf59647bc86953c73d09b612e8eca5f3fb09a2b6264059e3d79e6a0e2f47b50c88636a950da9bbb9a8490e16fcff + languageName: node + linkType: hard + +"@rspack/binding-darwin-arm64@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding-darwin-arm64@npm:1.7.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rspack/binding-darwin-x64@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding-darwin-x64@npm:1.7.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rspack/binding-linux-arm64-gnu@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rspack/binding-linux-arm64-musl@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rspack/binding-linux-x64-gnu@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rspack/binding-linux-x64-musl@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding-linux-x64-musl@npm:1.7.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rspack/binding-wasm32-wasi@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding-wasm32-wasi@npm:1.7.1" + dependencies: + "@napi-rs/wasm-runtime": "npm:1.0.7" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@rspack/binding-win32-arm64-msvc@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.1" + conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@nodelib/fs.scandir@npm:2.1.5": - version: 2.1.5 - resolution: "@nodelib/fs.scandir@npm:2.1.5" - dependencies: - "@nodelib/fs.stat": "npm:2.0.5" - run-parallel: "npm:^1.1.9" - checksum: 10/6ab2a9b8a1d67b067922c36f259e3b3dfd6b97b219c540877a4944549a4d49ea5ceba5663905ab5289682f1f3c15ff441d02f0447f620a42e1cb5e1937174d4b +"@rspack/binding-win32-ia32-msvc@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.1" + conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": - version: 2.0.5 - resolution: "@nodelib/fs.stat@npm:2.0.5" - checksum: 10/012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 +"@rspack/binding-win32-x64-msvc@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.1" + conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3": - version: 1.2.8 - resolution: "@nodelib/fs.walk@npm:1.2.8" +"@rspack/binding@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/binding@npm:1.7.1" + dependencies: + "@rspack/binding-darwin-arm64": "npm:1.7.1" + "@rspack/binding-darwin-x64": "npm:1.7.1" + "@rspack/binding-linux-arm64-gnu": "npm:1.7.1" + "@rspack/binding-linux-arm64-musl": "npm:1.7.1" + "@rspack/binding-linux-x64-gnu": "npm:1.7.1" + "@rspack/binding-linux-x64-musl": "npm:1.7.1" + "@rspack/binding-wasm32-wasi": "npm:1.7.1" + "@rspack/binding-win32-arm64-msvc": "npm:1.7.1" + "@rspack/binding-win32-ia32-msvc": "npm:1.7.1" + "@rspack/binding-win32-x64-msvc": "npm:1.7.1" + dependenciesMeta: + "@rspack/binding-darwin-arm64": + optional: true + "@rspack/binding-darwin-x64": + optional: true + "@rspack/binding-linux-arm64-gnu": + optional: true + "@rspack/binding-linux-arm64-musl": + optional: true + "@rspack/binding-linux-x64-gnu": + optional: true + "@rspack/binding-linux-x64-musl": + optional: true + "@rspack/binding-wasm32-wasi": + optional: true + "@rspack/binding-win32-arm64-msvc": + optional: true + "@rspack/binding-win32-ia32-msvc": + optional: true + "@rspack/binding-win32-x64-msvc": + optional: true + checksum: 10/2ebdcfb4787aa6b41f143e386915c6afddea306ea7edddf094071030e8ec49ee4ba84d1bf1ddf4813b34dba8df884bd782eebb90df5244f3c33831f85369877f + languageName: node + linkType: hard + +"@rspack/cli@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/cli@npm:1.7.1" dependencies: - "@nodelib/fs.scandir": "npm:2.1.5" - fastq: "npm:^1.6.0" - checksum: 10/40033e33e96e97d77fba5a238e4bba4487b8284678906a9f616b5579ddaf868a18874c0054a75402c9fbaaa033a25ceae093af58c9c30278e35c23c9479e79b0 + "@discoveryjs/json-ext": "npm:^0.5.7" + "@rspack/dev-server": "npm:~1.1.5" + exit-hook: "npm:^4.0.0" + webpack-bundle-analyzer: "npm:4.10.2" + peerDependencies: + "@rspack/core": ^1.0.0-alpha || ^1.x + bin: + rspack: bin/rspack.js + checksum: 10/e8b32556b4c9d229258d70c8d64f0efab19163db7e70f41405a282e56459081e3ce737e0ac7f00150ca1c6bcc91d0ac50496030bca762f9406bd4c258a785cc8 languageName: node linkType: hard -"@npmcli/agent@npm:^4.0.0": - version: 4.0.0 - resolution: "@npmcli/agent@npm:4.0.0" +"@rspack/core@npm:1.7.1": + version: 1.7.1 + resolution: "@rspack/core@npm:1.7.1" dependencies: - agent-base: "npm:^7.1.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - lru-cache: "npm:^11.2.1" - socks-proxy-agent: "npm:^8.0.3" - checksum: 10/1a81573becc60515031accc696e6405e9b894e65c12b98ef4aeee03b5617c41948633159dbf6caf5dde5b47367eeb749bdc7b7dfb21960930a9060a935c6f636 + "@module-federation/runtime-tools": "npm:0.22.0" + "@rspack/binding": "npm:1.7.1" + "@rspack/lite-tapable": "npm:1.1.0" + peerDependencies: + "@swc/helpers": ">=0.5.1" + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/0fe6c0d1fa9ffe9c6b4c0032c6c69b50bdd4ff5d3dbe508ca68630053e1413ef249b3bc2a423e5e16c0bfa1de5da3c39337e87976a22eb86039fc0226ed1880d languageName: node linkType: hard -"@npmcli/fs@npm:^5.0.0": - version: 5.0.0 - resolution: "@npmcli/fs@npm:5.0.0" +"@rspack/dev-server@npm:~1.1.5": + version: 1.1.5 + resolution: "@rspack/dev-server@npm:1.1.5" dependencies: - semver: "npm:^7.3.5" - checksum: 10/4935c7719d17830d0f9fa46c50be17b2a3c945cec61760f6d0909bce47677c42e1810ca673305890f9e84f008ec4d8e841182f371e42100a8159d15f22249208 + chokidar: "npm:^3.6.0" + http-proxy-middleware: "npm:^2.0.9" + p-retry: "npm:^6.2.0" + webpack-dev-server: "npm:5.2.2" + ws: "npm:^8.18.0" + peerDependencies: + "@rspack/core": "*" + checksum: 10/67a747d998f9a2449cb1c6c5791ffc812c9d99a7219595359ce960063f344fde9f8f2000bbc9633dc490082f69b74a20b8f319697bd19beca65bd108f5dff5e5 languageName: node linkType: hard -"@opentelemetry/api@npm:^1.4.0, @opentelemetry/api@npm:^1.4.1": - version: 1.9.0 - resolution: "@opentelemetry/api@npm:1.9.0" - checksum: 10/a607f0eef971893c4f2ee2a4c2069aade6ec3e84e2a1f5c2aac19f65c5d9eeea41aa72db917c1029faafdd71789a1a040bdc18f40d63690e22ccae5d7070f194 +"@rspack/lite-tapable@npm:1.1.0": + version: 1.1.0 + resolution: "@rspack/lite-tapable@npm:1.1.0" + checksum: 10/41ff73fe5e1b8dccaad746c9c1bd36dd67649e1ad35776f311b5ba94333a397704e11158579e25a6a7e677c51abe35e66987b1b000faef48d4e4ad2470fea150 languageName: node linkType: hard -"@opentelemetry/core@npm:1.17.0, @opentelemetry/core@npm:^1.11.0": - version: 1.17.0 - resolution: "@opentelemetry/core@npm:1.17.0" - dependencies: - "@opentelemetry/semantic-conventions": "npm:1.17.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.7.0" - checksum: 10/809b4754faad1f51b352834a791299e73443c28a30821757233388d812aa2df9a61bd61254a9e580207e501b1be511c178e0414e5de5e2428ee559dc329ebb03 +"@rspack/resolver-binding-darwin-arm64@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver-binding-darwin-arm64@npm:0.2.8" + conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@opentelemetry/resources@npm:1.17.0": - version: 1.17.0 - resolution: "@opentelemetry/resources@npm:1.17.0" - dependencies: - "@opentelemetry/core": "npm:1.17.0" - "@opentelemetry/semantic-conventions": "npm:1.17.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.7.0" - checksum: 10/c3555c49a43addbbf2eb24c379195f295250eb163060aca01f662194e973070cabf006d0d9d6e1ed19008442bae46a765c16c23d9ca0d7ba7c4a7988c0047af2 +"@rspack/resolver-binding-darwin-x64@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver-binding-darwin-x64@npm:0.2.8" + conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@opentelemetry/sdk-metrics@npm:^1.12.0": - version: 1.17.0 - resolution: "@opentelemetry/sdk-metrics@npm:1.17.0" +"@rspack/resolver-binding-linux-arm64-gnu@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver-binding-linux-arm64-gnu@npm:0.2.8" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rspack/resolver-binding-linux-arm64-musl@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver-binding-linux-arm64-musl@npm:0.2.8" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rspack/resolver-binding-linux-x64-gnu@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver-binding-linux-x64-gnu@npm:0.2.8" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rspack/resolver-binding-linux-x64-musl@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver-binding-linux-x64-musl@npm:0.2.8" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rspack/resolver-binding-wasm32-wasi@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver-binding-wasm32-wasi@npm:0.2.8" dependencies: - "@opentelemetry/core": "npm:1.17.0" - "@opentelemetry/resources": "npm:1.17.0" - lodash.merge: "npm:^4.6.2" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.7.0" - checksum: 10/45876ed27d564a64f2b0cc7f63699c5a5a91192cb20f124b15fb12a0e346aec1e5f65f19eb18031977e1a8119972b815573b750f4953dbc5eef150fe4d9eefc3 + "@napi-rs/wasm-runtime": "npm:^1.1.1" + conditions: cpu=wasm32 languageName: node linkType: hard -"@opentelemetry/semantic-conventions@npm:1.17.0": - version: 1.17.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.17.0" - checksum: 10/1f6bbd4d543ad529ddb3f6b55e08940995b5958fa990bc54bfa50136fc0a93d12a9bfed7f3addb5d84b1afaade8bd4b9afc36d2fe2d65a3f6325511b3a29d851 +"@rspack/resolver-binding-win32-arm64-msvc@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver-binding-win32-arm64-msvc@npm:0.2.8" + conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@pkgjs/parseargs@npm:^0.11.0": - version: 0.11.0 - resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 10/115e8ceeec6bc69dff2048b35c0ab4f8bbee12d8bb6c1f4af758604586d802b6e669dcb02dda61d078de42c2b4ddce41b3d9e726d7daa6b4b850f4adbf7333ff +"@rspack/resolver-binding-win32-ia32-msvc@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver-binding-win32-ia32-msvc@npm:0.2.8" + conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@pkgr/core@npm:^0.1.0": - version: 0.1.1 - resolution: "@pkgr/core@npm:0.1.1" - checksum: 10/6f25fd2e3008f259c77207ac9915b02f1628420403b2630c92a07ff963129238c9262afc9e84344c7a23b5cc1f3965e2cd17e3798219f5fd78a63d144d3cceba +"@rspack/resolver-binding-win32-x64-msvc@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver-binding-win32-x64-msvc@npm:0.2.8" + conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@pkgr/core@npm:^0.2.9": - version: 0.2.9 - resolution: "@pkgr/core@npm:0.2.9" - checksum: 10/bb2fb86977d63f836f8f5b09015d74e6af6488f7a411dcd2bfdca79d76b5a681a9112f41c45bdf88a9069f049718efc6f3900d7f1de66a2ec966068308ae517f +"@rspack/resolver@npm:0.2.8": + version: 0.2.8 + resolution: "@rspack/resolver@npm:0.2.8" + dependencies: + "@rspack/resolver-binding-darwin-arm64": "npm:0.2.8" + "@rspack/resolver-binding-darwin-x64": "npm:0.2.8" + "@rspack/resolver-binding-linux-arm64-gnu": "npm:0.2.8" + "@rspack/resolver-binding-linux-arm64-musl": "npm:0.2.8" + "@rspack/resolver-binding-linux-x64-gnu": "npm:0.2.8" + "@rspack/resolver-binding-linux-x64-musl": "npm:0.2.8" + "@rspack/resolver-binding-wasm32-wasi": "npm:0.2.8" + "@rspack/resolver-binding-win32-arm64-msvc": "npm:0.2.8" + "@rspack/resolver-binding-win32-ia32-msvc": "npm:0.2.8" + "@rspack/resolver-binding-win32-x64-msvc": "npm:0.2.8" + dependenciesMeta: + "@rspack/resolver-binding-darwin-arm64": + optional: true + "@rspack/resolver-binding-darwin-x64": + optional: true + "@rspack/resolver-binding-linux-arm64-gnu": + optional: true + "@rspack/resolver-binding-linux-arm64-musl": + optional: true + "@rspack/resolver-binding-linux-x64-gnu": + optional: true + "@rspack/resolver-binding-linux-x64-musl": + optional: true + "@rspack/resolver-binding-wasm32-wasi": + optional: true + "@rspack/resolver-binding-win32-arm64-msvc": + optional: true + "@rspack/resolver-binding-win32-ia32-msvc": + optional: true + "@rspack/resolver-binding-win32-x64-msvc": + optional: true + checksum: 10/1216853602eb1283606a8b5bd166c3ac38308520fec457b3f5e585a959fe5881a0ac69af271bdb55cbf4dddbb76b0eb79711687a3990612462bb23101630ebf9 + languageName: node + linkType: hard + +"@sec-ant/readable-stream@npm:^0.4.1": + version: 0.4.1 + resolution: "@sec-ant/readable-stream@npm:0.4.1" + checksum: 10/aac89581652ac85debe7c5303451c2ebf8bf25ca25db680e4b9b73168f6940616d9a4bbe3348981827b1159b14e2f2e6af4b7bd5735cac898c12d5c51909c102 languageName: node linkType: hard @@ -1235,6 +2261,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/merge-streams@npm:^4.0.0": + version: 4.0.0 + resolution: "@sindresorhus/merge-streams@npm:4.0.0" + checksum: 10/16551c787f5328c8ef05fd9831ade64369ccc992df78deb635ec6c44af217d2f1b43f8728c348cdc4e00585ff2fad6e00d8155199cbf6b154acc45fe65cbf0aa + languageName: node + linkType: hard + "@sinonjs/commons@npm:^3.0.1": version: 3.0.1 resolution: "@sinonjs/commons@npm:3.0.1" @@ -1244,30 +2277,30 @@ __metadata: languageName: node linkType: hard -"@sinonjs/fake-timers@npm:^13.0.0": - version: 13.0.5 - resolution: "@sinonjs/fake-timers@npm:13.0.5" +"@sinonjs/fake-timers@npm:^15.0.0": + version: 15.3.2 + resolution: "@sinonjs/fake-timers@npm:15.3.2" dependencies: "@sinonjs/commons": "npm:^3.0.1" - checksum: 10/11ee417968fc4dce1896ab332ac13f353866075a9d2a88ed1f6258f17cc4f7d93e66031b51fcddb8c203aa4d53fd980b0ae18aba06269f4682164878a992ec3f + checksum: 10/270a0f440bbcee2ad3fbe50b3c3cf311afd82e3cfcdff9572485fc17948b66c2cdfc5c906d64884ec623eced97ceb23c7639c3066ad4696976acf9f63a93c4c8 languageName: node linkType: hard -"@slack/types@npm:^2.9.0": - version: 2.14.0 - resolution: "@slack/types@npm:2.14.0" - checksum: 10/fa24a113b88e087f899078504c2ba50ab9795f7c2dd1a2d95b28217a3af20e554494f9cc3b8c8ce173120990d98e19400c95369f9067cecfcc46c08b59d2a46f +"@slack/types@npm:^2.20.1": + version: 2.20.1 + resolution: "@slack/types@npm:2.20.1" + checksum: 10/5d13017df8c8468f34e4d24579a26f5ada934a6b7139e88d894c7d1d14f47da826a20c08e2537c6ab74d7624a4b193dc2b34458bae6466173fad1fd5c053b6f6 languageName: node linkType: hard -"@slack/webhook@npm:^7.0.6": - version: 7.0.6 - resolution: "@slack/webhook@npm:7.0.6" +"@slack/webhook@npm:^7.0.9": + version: 7.0.9 + resolution: "@slack/webhook@npm:7.0.9" dependencies: - "@slack/types": "npm:^2.9.0" - "@types/node": "npm:>=18.0.0" - axios: "npm:^1.11.0" - checksum: 10/8f8083f9654e590f04731985b337f576842b2034a9261010f85d813c4e262f69d856c142b0dcf2022bfe69c22c2e97cc7d877a79989cd0f7a0cf2554ae0754ed + "@slack/types": "npm:^2.20.1" + "@types/node": "npm:>=18" + axios: "npm:^1.15.0" + checksum: 10/bf579ddc3c8bfb9937c86b9a66aeafcad6bd8b27b257eb2285c690724ede92ebba4f2dff8a39033fddbabc78ad26ab0472ec9b5775d9cc5e0b605a0945fb4d5a languageName: node linkType: hard @@ -1281,6 +2314,13 @@ __metadata: languageName: node linkType: hard +"@socket.io/component-emitter@npm:~3.1.0": + version: 3.1.2 + resolution: "@socket.io/component-emitter@npm:3.1.2" + checksum: 10/89888f00699eb34e3070624eb7b8161fa29f064aeb1389a48f02195d55dd7c52a504e52160016859f6d6dffddd54324623cdd47fd34b3d46f9ed96c18c456edc + languageName: node + linkType: hard + "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A.": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." @@ -1291,21 +2331,22 @@ __metadata: languageName: node linkType: soft -"@sofie-automation/code-standard-preset@npm:^3.0.0": - version: 3.0.0 - resolution: "@sofie-automation/code-standard-preset@npm:3.0.0" +"@sofie-automation/code-standard-preset@npm:^3.2.2": + version: 3.2.2 + resolution: "@sofie-automation/code-standard-preset@npm:3.2.2" dependencies: - "@sofie-automation/eslint-plugin": "npm:0.2.0" + "@sofie-automation/eslint-plugin": "npm:0.2.1" + "@vitest/eslint-plugin": "npm:^1.6.6" date-fns: "npm:^4.1.0" - eslint-config-prettier: "npm:^10.0.1" - eslint-plugin-jest: "npm:^28.11.0" - eslint-plugin-n: "npm:^17.15.1" - eslint-plugin-prettier: "npm:^5.2.3" + eslint-config-prettier: "npm:^10.1.8" + eslint-plugin-jest: "npm:^28.14.0" + eslint-plugin-n: "npm:^17.23.2" + eslint-plugin-prettier: "npm:^5.5.5" license-checker: "npm:^25.0.1" meow: "npm:^13.2.0" read-package-up: "npm:^11.0.0" - semver: "npm:^7.6.3" - typescript-eslint: "npm:^8.21.0" + semver: "npm:^7.7.3" + typescript-eslint: "npm:^8.54.0" peerDependencies: eslint: ^9 prettier: ^3 @@ -1313,7 +2354,7 @@ __metadata: bin: sofie-licensecheck: ./bin/checkLicenses.mjs sofie-version: ./bin/updateVersion.mjs - checksum: 10/fa61dc1f90377ad2196f2e6c33dea9988bbe9cfd6eb8b277a083ae1147c00e83e526b7520bb5548d4935fb91b7f9f1d8f9b701db419da760488c318ea42a243f + checksum: 10/e930ea2e5dfb6502f121cf25d157c57450d1c96be5c65c4d3b3695ec3251683d1373c1cd189d47daff2b4dc7b7f6854d3fb92e38a21fc03418886707324bcd92 languageName: node linkType: hard @@ -1324,28 +2365,28 @@ __metadata: "@sofie-automation/blueprints-integration": "npm:26.3.0-2" "@sofie-automation/shared-lib": "npm:26.3.0-2" fast-clone: "npm:^1.5.13" - i18next: "npm:^21.10.0" + i18next: "npm:^26.0.8" influx: "npm:^5.12.0" nanoid: "npm:^3.3.11" object-path: "npm:^0.11.8" prom-client: "npm:^15.1.3" - timecode: "npm:0.0.4" tslib: "npm:^2.8.1" type-fest: "npm:^4.41.0" - underscore: "npm:^1.13.7" + underscore: "npm:^1.13.8" peerDependencies: - mongodb: ^6.12.0 + mongodb: ^7.1.1 languageName: node linkType: soft -"@sofie-automation/eslint-plugin@npm:0.2.0": - version: 0.2.0 - resolution: "@sofie-automation/eslint-plugin@npm:0.2.0" +"@sofie-automation/eslint-plugin@npm:0.2.1": + version: 0.2.1 + resolution: "@sofie-automation/eslint-plugin@npm:0.2.1" dependencies: "@typescript-eslint/utils": "npm:^8.21.0" + tslib: "npm:^2.8.1" peerDependencies: eslint: ^9 - checksum: 10/7d2898cab2d89fcab727597a7a8ff49dacb030166f390d4b20ec27fbb53f8e330a2a034090484611f1cb0fe98bd4a1bc961e0cc6e77236d5c84065ae830fa1ad + checksum: 10/650cd6f075d531a9f88012aff314d3a9d5a0e9414f2062a13ba49ba955ad6140255ec863ee69ea2b112c31228341de7ad4f42ecf3ebd39cfdea1fba5be62da0b languageName: node linkType: hard @@ -1353,7 +2394,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: - "@slack/webhook": "npm:^7.0.6" + "@slack/webhook": "npm:^7.0.9" "@sofie-automation/blueprints-integration": "npm:26.3.0-2" "@sofie-automation/corelib": "npm:26.3.0-2" "@sofie-automation/shared-lib": "npm:26.3.0-2" @@ -1361,14 +2402,14 @@ __metadata: chrono-node: "npm:^2.9.0" deepmerge: "npm:^4.3.1" elastic-apm-node: "npm:^4.15.0" - mongodb: "npm:^6.21.0" + mongodb: "npm:^7.1.1" p-lazy: "npm:^3.1.0" p-timeout: "npm:^4.1.0" superfly-timeline: "npm:9.2.0" - threadedclass: "npm:^1.3.0" + threadedclass: "npm:^1.4.0" tslib: "npm:^2.8.1" type-fest: "npm:^4.41.0" - underscore: "npm:^1.13.7" + underscore: "npm:^1.13.8" languageName: node linkType: soft @@ -1381,12 +2422,12 @@ __metadata: "@sofie-automation/corelib": "npm:26.3.0-2" "@sofie-automation/shared-lib": "npm:26.3.0-2" deep-extend: "npm:0.6.0" - semver: "npm:^7.7.3" + semver: "npm:^7.7.4" type-fest: "npm:^4.41.0" - underscore: "npm:^1.13.7" + underscore: "npm:^1.13.8" peerDependencies: - i18next: ^21.10.0 - mongodb: ^6.12.0 + i18next: ^26.0.4 + mongodb: ^7.1.1 languageName: node linkType: soft @@ -1395,13 +2436,184 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": "npm:^5.0.0-alpha.0" - kairos-lib: "npm:^0.2.3" - timeline-state-resolver-types: "npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" + kairos-lib: "npm:^1.0.0" + timeline-state-resolver-types: "npm:10.0.0-nightly-main-20260602-133931-c0882da4d.0" tslib: "npm:^2.8.1" type-fest: "npm:^4.41.0" languageName: node linkType: soft +"@swc/core-darwin-arm64@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-darwin-arm64@npm:1.15.41" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-darwin-x64@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-darwin-x64@npm:1.15.41" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@swc/core-linux-arm-gnueabihf@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.15.41" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@swc/core-linux-arm64-gnu@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-linux-arm64-gnu@npm:1.15.41" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-arm64-musl@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-linux-arm64-musl@npm:1.15.41" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-linux-ppc64-gnu@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-linux-ppc64-gnu@npm:1.15.41" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-s390x-gnu@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-linux-s390x-gnu@npm:1.15.41" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-gnu@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-linux-x64-gnu@npm:1.15.41" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-musl@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-linux-x64-musl@npm:1.15.41" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-win32-arm64-msvc@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-win32-arm64-msvc@npm:1.15.41" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-win32-ia32-msvc@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-win32-ia32-msvc@npm:1.15.41" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@swc/core-win32-x64-msvc@npm:1.15.41": + version: 1.15.41 + resolution: "@swc/core-win32-x64-msvc@npm:1.15.41" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:^1.15.18, @swc/core@npm:^1.15.26, @swc/core@npm:^1.15.41": + version: 1.15.41 + resolution: "@swc/core@npm:1.15.41" + dependencies: + "@swc/core-darwin-arm64": "npm:1.15.41" + "@swc/core-darwin-x64": "npm:1.15.41" + "@swc/core-linux-arm-gnueabihf": "npm:1.15.41" + "@swc/core-linux-arm64-gnu": "npm:1.15.41" + "@swc/core-linux-arm64-musl": "npm:1.15.41" + "@swc/core-linux-ppc64-gnu": "npm:1.15.41" + "@swc/core-linux-s390x-gnu": "npm:1.15.41" + "@swc/core-linux-x64-gnu": "npm:1.15.41" + "@swc/core-linux-x64-musl": "npm:1.15.41" + "@swc/core-win32-arm64-msvc": "npm:1.15.41" + "@swc/core-win32-ia32-msvc": "npm:1.15.41" + "@swc/core-win32-x64-msvc": "npm:1.15.41" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.26" + peerDependencies: + "@swc/helpers": ">=0.5.17" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-ppc64-gnu": + optional: true + "@swc/core-linux-s390x-gnu": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/4d4b47885468bb6635a06040afa566e5568b55c7a43facceadca39ebd093a96e5d11701d8a92872321ae527f873701de10a9be2b3d96c1a7188e3e862c4072c4 + languageName: node + linkType: hard + +"@swc/counter@npm:^0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: 10/df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 + languageName: node + linkType: hard + +"@swc/helpers@npm:0.5.17": + version: 0.5.17 + resolution: "@swc/helpers@npm:0.5.17" + dependencies: + tslib: "npm:^2.8.0" + checksum: 10/1fc8312a78f1f99c8ec838585445e99763eeebff2356100738cdfdb8ad47d2d38df678ee6edd93a90fe319ac52da67adc14ac00eb82b606c5fb8ebc5d06ec2a2 + languageName: node + linkType: hard + +"@swc/types@npm:^0.1.26": + version: 0.1.26 + resolution: "@swc/types@npm:0.1.26" + dependencies: + "@swc/counter": "npm:^0.1.3" + checksum: 10/07de03b9da3928cdf69bda70bf2c809dd86f16ef23e357759e577bbd975529cb20218c2e54e72b00585abae2b5e04e39b8947cea7a6f4de2d40a7633be441919 + languageName: node + linkType: hard + +"@tokenizer/inflate@npm:^0.4.1": + version: 0.4.1 + resolution: "@tokenizer/inflate@npm:0.4.1" + dependencies: + debug: "npm:^4.4.3" + token-types: "npm:^6.1.1" + checksum: 10/27d58757e1a6c004e86f8a5f1a40fe47cb48aa6891864d03de6eab27d42fafc1456f396bc8bc300e16913b0a85f42034d011db0213d17e544ed201a7fc24244e + languageName: node + linkType: hard + "@tokenizer/token@npm:^0.3.0": version: 0.3.0 resolution: "@tokenizer/token@npm:0.3.0" @@ -1409,12 +2621,12 @@ __metadata: languageName: node linkType: hard -"@tybys/wasm-util@npm:^0.10.0": - version: 0.10.1 - resolution: "@tybys/wasm-util@npm:0.10.1" +"@tybys/wasm-util@npm:^0.10.0, @tybys/wasm-util@npm:^0.10.1": + version: 0.10.2 + resolution: "@tybys/wasm-util@npm:0.10.2" dependencies: tslib: "npm:^2.4.0" - checksum: 10/7fe0d239397aebb002ac4855d30c197c06a05ea8df8511350a3a5b1abeefe26167c60eda8a5508337571161e4c4b53d7c1342296123f9607af8705369de9fa7f + checksum: 10/d12f1dafe12d7a573c406b35ffef0038042b9cc9fbcc74d657267eb635499b956276afc05eebdbd81bea582e1c4c921421a1dd7243a93daaa8c8216b19395c23 languageName: node linkType: hard @@ -1468,7 +2680,7 @@ __metadata: languageName: node linkType: hard -"@types/body-parser@npm:*, @types/body-parser@npm:^1.19.6": +"@types/body-parser@npm:*": version: 1.19.6 resolution: "@types/body-parser@npm:1.19.6" dependencies: @@ -1478,12 +2690,31 @@ __metadata: languageName: node linkType: hard -"@types/connect@npm:*": - version: 3.4.36 - resolution: "@types/connect@npm:3.4.36" +"@types/bonjour@npm:^3.5.13": + version: 3.5.13 + resolution: "@types/bonjour@npm:3.5.13" + dependencies: + "@types/node": "npm:*" + checksum: 10/e827570e097bd7d625a673c9c208af2d1a22fa3885c0a1646533cf24394c839c3e5f60ac1bc60c0ddcc69c0615078c9fb2c01b42596c7c582d895d974f2409ee + languageName: node + linkType: hard + +"@types/connect-history-api-fallback@npm:^1.5.4": + version: 1.5.4 + resolution: "@types/connect-history-api-fallback@npm:1.5.4" + dependencies: + "@types/express-serve-static-core": "npm:*" + "@types/node": "npm:*" + checksum: 10/e1dee43b8570ffac02d2d47a2b4ba80d3ca0dd1840632dafb221da199e59dbe3778d3d7303c9e23c6b401f37c076935a5bc2aeae1c4e5feaefe1c371fe2073fd + languageName: node + linkType: hard + +"@types/connect@npm:*, @types/connect@npm:3.4.38": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" dependencies: "@types/node": "npm:*" - checksum: 10/4dee3d966fb527b98f0cbbdcf6977c9193fc3204ed539b7522fe5e64dfa45f9017bdda4ffb1f760062262fce7701a0ee1c2f6ce2e50af36c74d4e37052303172 + checksum: 10/7eb1bc5342a9604facd57598a6c62621e244822442976c443efb84ff745246b10d06e8b309b6e80130026a396f19bf6793b7cecd7380169f369dac3bfc46fb99 languageName: node linkType: hard @@ -1506,6 +2737,15 @@ __metadata: languageName: node linkType: hard +"@types/cors@npm:^2.8.12": + version: 2.8.19 + resolution: "@types/cors@npm:2.8.19" + dependencies: + "@types/node": "npm:*" + checksum: 10/9545cc532c9218754443f48a0c98c1a9ba4af1fe54a3425c95de75ff3158147bb39e666cb7c6bf98cc56a9c6dc7b4ce5b2cbdae6b55d5942e50c81b76ed6b825 + languageName: node + linkType: hard + "@types/deep-extend@npm:^0.6.2": version: 0.6.2 resolution: "@types/deep-extend@npm:0.6.2" @@ -1513,6 +2753,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:1.0.5": + version: 1.0.5 + resolution: "@types/estree@npm:1.0.5" + checksum: 10/7de6d928dd4010b0e20c6919e1a6c27b61f8d4567befa89252055fad503d587ecb9a1e3eab1b1901f923964d7019796db810b7fd6430acb26c32866d126fd408 + languageName: node + linkType: hard + "@types/estree@npm:^1.0.6": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" @@ -1520,27 +2767,39 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:^4.17.33": - version: 4.17.36 - resolution: "@types/express-serve-static-core@npm:4.17.36" +"@types/express-serve-static-core@npm:*": + version: 5.1.1 + resolution: "@types/express-serve-static-core@npm:5.1.1" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10/7f3d8cf7e68764c9f3e8f6a12825b69ccf5287347fc1c20b29803d4f08a4abc1153ae11d7258852c61aad50f62ef72d4c1b9c97092b0a90462c3dddec2f6026c + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^4.17.21, @types/express-serve-static-core@npm:^4.17.33": + version: 4.19.8 + resolution: "@types/express-serve-static-core@npm:4.19.8" dependencies: "@types/node": "npm:*" "@types/qs": "npm:*" "@types/range-parser": "npm:*" "@types/send": "npm:*" - checksum: 10/47d5c30a4a2a6de5dd1ceef6fed61a2e49e50e09ab3bab67a2bfa4375617c54b0397b3397ef4dad80ae3a7e400943464d857b437dabd9fed88b47256f2be774b + checksum: 10/eb1b832343c0991395c9b10e124dc805921ea7c08efe01222d83912123b8c054119d009e9e55c91af6bdbeeec153c0d35411c9c6d80781bc8c0a43e8b1a84387 languageName: node linkType: hard -"@types/express@npm:*": - version: 4.17.17 - resolution: "@types/express@npm:4.17.17" +"@types/express@npm:*, @types/express@npm:^4.17.21": + version: 4.17.25 + resolution: "@types/express@npm:4.17.25" dependencies: "@types/body-parser": "npm:*" "@types/express-serve-static-core": "npm:^4.17.33" "@types/qs": "npm:*" - "@types/serve-static": "npm:*" - checksum: 10/e2959a5fecdc53f8a524891a16e66dfc330ee0519e89c2579893179db686e10cfa6079a68e0fb8fd00eedbcaf3eabfd10916461939f3bc02ef671d848532c37e + "@types/serve-static": "npm:^1" + checksum: 10/c309fdb79fb8569b5d8d8f11268d0160b271f8b38f0a82c20a0733e526baf033eb7a921cd51d54fe4333c616de9e31caf7d4f3ef73baaf212d61f23f460b0369 languageName: node linkType: hard @@ -1558,6 +2817,15 @@ __metadata: languageName: node linkType: hard +"@types/http-proxy@npm:^1.17.8": + version: 1.17.17 + resolution: "@types/http-proxy@npm:1.17.17" + dependencies: + "@types/node": "npm:*" + checksum: 10/893e46e12be576baa471cf2fc13a4f0e413eaf30a5850de8fdbea3040e138ad4171234c59b986cf7137ff20a1582b254bf0c44cfd715d5ed772e1ab94dd75cd1 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -1593,7 +2861,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.15": +"@types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -1653,9 +2921,9 @@ __metadata: languageName: node linkType: hard -"@types/koa@npm:*, @types/koa@npm:^3.0.1": - version: 3.0.1 - resolution: "@types/koa@npm:3.0.1" +"@types/koa@npm:*, @types/koa@npm:^3.0.2": + version: 3.0.2 + resolution: "@types/koa@npm:3.0.2" dependencies: "@types/accepts": "npm:*" "@types/content-disposition": "npm:*" @@ -1665,7 +2933,7 @@ __metadata: "@types/keygrip": "npm:*" "@types/koa-compose": "npm:*" "@types/node": "npm:*" - checksum: 10/b1581d31d562bb5d9f61bc0148652abffc701c39930eb77a57b7d1f43aaad56ffa1970f6f3d4d6a0a56395a6832e2711a3278850dcc5a6c986ba6ed2cd0f4f1f + checksum: 10/1522fa11ec3520275232b4e054424142a3cdb19e7991673d6371b6cdcb5587d56858098dc196a9580b98174a66326545fc64be98d88652915b21bdebc2905dcf languageName: node linkType: hard @@ -1678,13 +2946,6 @@ __metadata: languageName: node linkType: hard -"@types/mime@npm:*": - version: 3.0.1 - resolution: "@types/mime@npm:3.0.1" - checksum: 10/4040fac73fd0cea2460e29b348c1a6173da747f3a87da0dbce80dd7a9355a3d0e51d6d9a401654f3e5550620e3718b5a899b2ec1debf18424e298a2c605346e7 - languageName: node - linkType: hard - "@types/mime@npm:^1": version: 1.3.2 resolution: "@types/mime@npm:1.3.2" @@ -1699,12 +2960,30 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=18.0.0, @types/node@npm:^22.19.8": - version: 22.19.8 - resolution: "@types/node@npm:22.19.8" +"@types/node-forge@npm:^1.3.0": + version: 1.3.14 + resolution: "@types/node-forge@npm:1.3.14" + dependencies: + "@types/node": "npm:*" + checksum: 10/500ce72435285fca145837da079b49a09a5bdf8391b0effc3eb2455783dd81ab129e574a36e1a0374a4823d889d5328177ebfd6fe45b432c0c43d48d790fe39c + languageName: node + linkType: hard + +"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=18": + version: 25.7.0 + resolution: "@types/node@npm:25.7.0" + dependencies: + undici-types: "npm:~7.21.0" + checksum: 10/1b11c865ea517ab90af870c2f58c100804e3dd8dc25a62b5e5af3aea12ad015f9faf513664babc7e0c755bb1c0744649b2ff19b7b434c1648ad8057f2dc6c71a + languageName: node + linkType: hard + +"@types/node@npm:^22.19.17": + version: 22.19.17 + resolution: "@types/node@npm:22.19.17" dependencies: undici-types: "npm:~6.21.0" - checksum: 10/a61c68d434871d4a13496e3607502b2ff8e2ff69dca7e09228de5bea3bc95eb627d09243a8cff8e0bf9ff1fa13baaf0178531748f59ae81f0569c7a2f053bfa5 + checksum: 10/8adcf9c2e9e7c524196efdd5ee60ebeda980b4ec9b5b8650e2341a2cabe46d368f3a4e4c0050b3bf26eb9cf383453ea2193ec5604079cdf9493a11a742f62018 languageName: node linkType: hard @@ -1729,6 +3008,13 @@ __metadata: languageName: node linkType: hard +"@types/retry@npm:0.12.2": + version: 0.12.2 + resolution: "@types/retry@npm:0.12.2" + checksum: 10/e5675035717b39ce4f42f339657cae9637cf0c0051cf54314a6a2c44d38d91f6544be9ddc0280587789b6afd056be5d99dbe3e9f4df68c286c36321579b1bf4a + languageName: node + linkType: hard + "@types/semver@npm:^7.7.1": version: 7.7.1 resolution: "@types/semver@npm:7.7.1" @@ -1736,24 +3022,42 @@ __metadata: languageName: node linkType: hard -"@types/send@npm:*": - version: 0.17.1 - resolution: "@types/send@npm:0.17.1" +"@types/send@npm:*, @types/send@npm:<1": + version: 0.17.6 + resolution: "@types/send@npm:0.17.6" dependencies: "@types/mime": "npm:^1" "@types/node": "npm:*" - checksum: 10/6420837887858f7aa82f2c0272f73edb42385bd0978f43095e83590a405d86c8cc6d918c30b2d542f1d8bddc9f3d16c2e8fdfca936940de71b97c45f228d1896 + checksum: 10/4948ab32ab84a81a0073f8243dd48ee766bc80608d5391060360afd1249f83c08a7476f142669ac0b0b8831c89d909a88bcb392d1b39ee48b276a91b50f3d8d1 + languageName: node + linkType: hard + +"@types/serve-index@npm:^1.9.4": + version: 1.9.4 + resolution: "@types/serve-index@npm:1.9.4" + dependencies: + "@types/express": "npm:*" + checksum: 10/72727c88d54da5b13275ebfb75dcdc4aa12417bbe9da1939e017c4c5f0c906fae843aa4e0fbfe360e7ee9df2f3d388c21abfc488f77ce58693fb57809f8ded92 languageName: node linkType: hard -"@types/serve-static@npm:*": - version: 1.15.2 - resolution: "@types/serve-static@npm:1.15.2" +"@types/serve-static@npm:^1, @types/serve-static@npm:^1.15.5": + version: 1.15.10 + resolution: "@types/serve-static@npm:1.15.10" dependencies: "@types/http-errors": "npm:*" - "@types/mime": "npm:*" "@types/node": "npm:*" - checksum: 10/d5f8f5aaa765be6417aa3f2ebe36591f4e9d2d8a7480edf7d3db041427420fd565cb921fc021271098dd2afafce2b443fc0d978faa3ae21a2a58ebde7d525e9e + "@types/send": "npm:<1" + checksum: 10/d9be72487540b9598e7d77260d533f241eb2e5db5181bb885ef2d6bc4592dad1c9e8c0e27f465d59478b2faf90edd2d535e834f20fbd9dd3c0928d43dc486404 + languageName: node + linkType: hard + +"@types/sockjs@npm:^0.3.36": + version: 0.3.36 + resolution: "@types/sockjs@npm:0.3.36" + dependencies: + "@types/node": "npm:*" + checksum: 10/b4b5381122465d80ea8b158537c00bc82317222d3fb31fd7229ff25b31fa89134abfbab969118da55622236bf3d8fee75759f3959908b5688991f492008f29bc languageName: node linkType: hard @@ -1764,6 +3068,15 @@ __metadata: languageName: node linkType: hard +"@types/tapable@npm:2.3.0": + version: 2.3.0 + resolution: "@types/tapable@npm:2.3.0" + dependencies: + tapable: "npm:^2.3.0" + checksum: 10/9c6a3d75851d114c233033a04b88c8a4b27cd5edc026c4aea08b310c1561be5fd22fe408e3419fb598352126ef0e0c228b738be4cf948da8b5ebb2342909814f + languageName: node + linkType: hard + "@types/triple-beam@npm:^1.3.2": version: 1.3.3 resolution: "@types/triple-beam@npm:1.3.3" @@ -1785,12 +3098,21 @@ __metadata: languageName: node linkType: hard -"@types/whatwg-url@npm:^11.0.2": - version: 11.0.5 - resolution: "@types/whatwg-url@npm:11.0.5" +"@types/whatwg-url@npm:^13.0.0": + version: 13.0.0 + resolution: "@types/whatwg-url@npm:13.0.0" dependencies: "@types/webidl-conversions": "npm:*" - checksum: 10/23a0c45aff51817807b473a6adb181d6e3bb0d27dde54e84883d5d5bc93358e95204d2188e7ff7fdc2cdaf157e97e1188ef0a22ec79228da300fc30d4a05b56a + checksum: 10/82018c7dc057dd4b5ee6137e54a659d2d043146eaade8afc2dda472773cc66f2abad73525020a2bf399a09b1bf448504f9e519d6b2d7495e6e781bb5de686753 + languageName: node + linkType: hard + +"@types/ws@npm:^8.5.10, @types/ws@npm:^8.5.12": + version: 8.18.1 + resolution: "@types/ws@npm:8.18.1" + dependencies: + "@types/node": "npm:*" + checksum: 10/1ce05e3174dcacf28dae0e9b854ef1c9a12da44c7ed73617ab6897c5cbe4fccbb155a20be5508ae9a7dde2f83bd80f5cf3baa386b934fc4b40889ec963e94f3a languageName: node linkType: hard @@ -1810,115 +3132,138 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.23.0": - version: 8.23.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.23.0" +"@typescript-eslint/eslint-plugin@npm:8.58.2": + version: 8.58.2 + resolution: "@typescript-eslint/eslint-plugin@npm:8.58.2" dependencies: - "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.23.0" - "@typescript-eslint/type-utils": "npm:8.23.0" - "@typescript-eslint/utils": "npm:8.23.0" - "@typescript-eslint/visitor-keys": "npm:8.23.0" - graphemer: "npm:^1.4.0" - ignore: "npm:^5.3.1" + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.58.2" + "@typescript-eslint/type-utils": "npm:8.58.2" + "@typescript-eslint/utils": "npm:8.58.2" + "@typescript-eslint/visitor-keys": "npm:8.58.2" + ignore: "npm:^7.0.5" natural-compare: "npm:^1.4.0" - ts-api-utils: "npm:^2.0.1" + ts-api-utils: "npm:^2.5.0" peerDependencies: - "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/3900a86f6edf9fee496e8814ed4c8d405f9331808174f4bca0d3af2541a947e872c9f7519a2549644ba5aae1dc06a63308bda9d8da00ef7c62f685be05502d5e + "@typescript-eslint/parser": ^8.58.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/e17da9302a66c405d6f7ca33409b177535933b96b9548825c0c66368813b21aeff9550348234ea1b882427fbdc246e5032fbe655cea6bc71aa688417f988c934 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.23.0": - version: 8.23.0 - resolution: "@typescript-eslint/parser@npm:8.23.0" +"@typescript-eslint/parser@npm:8.58.2": + version: 8.58.2 + resolution: "@typescript-eslint/parser@npm:8.58.2" dependencies: - "@typescript-eslint/scope-manager": "npm:8.23.0" - "@typescript-eslint/types": "npm:8.23.0" - "@typescript-eslint/typescript-estree": "npm:8.23.0" - "@typescript-eslint/visitor-keys": "npm:8.23.0" - debug: "npm:^4.3.4" + "@typescript-eslint/scope-manager": "npm:8.58.2" + "@typescript-eslint/types": "npm:8.58.2" + "@typescript-eslint/typescript-estree": "npm:8.58.2" + "@typescript-eslint/visitor-keys": "npm:8.58.2" + debug: "npm:^4.4.3" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/7c3ae38665effc7a730d455657ca535c1553ab154d3b983ed4df2bc5b87291b69f4b90245356d8fc5a414d6dca36b34780d9408a45ac272d4bc390b8f03bda4d + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/276da6985cd531615d99a1875d87e2d619adbc6377577849c4f3abc4402a7101dea35b7666019c0e83f3346cace002448668e4a4ebe35734d504d48689a7e836 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.23.0": - version: 8.23.0 - resolution: "@typescript-eslint/scope-manager@npm:8.23.0" +"@typescript-eslint/project-service@npm:8.58.2": + version: 8.58.2 + resolution: "@typescript-eslint/project-service@npm:8.58.2" dependencies: - "@typescript-eslint/types": "npm:8.23.0" - "@typescript-eslint/visitor-keys": "npm:8.23.0" - checksum: 10/eb4624ccd907c21ca49c4600dec0c447349d7e987cda21181c008dc5ce855590e311efabe73b79b15da0948ce5f63ce0c33613ab4a39ea95578b099b724392e3 + "@typescript-eslint/tsconfig-utils": "npm:^8.58.2" + "@typescript-eslint/types": "npm:^8.58.2" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10/fcaf2b17aa0476164a53626b2754ab01ca595e913828699276972dcafe6369dd31cb7ce73cd32b8f4dd9209a7bf480c51b87266d0c434adb27a462e24e7940af languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.23.0": - version: 8.23.0 - resolution: "@typescript-eslint/type-utils@npm:8.23.0" +"@typescript-eslint/scope-manager@npm:8.58.2, @typescript-eslint/scope-manager@npm:^8.58.0": + version: 8.58.2 + resolution: "@typescript-eslint/scope-manager@npm:8.58.2" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.23.0" - "@typescript-eslint/utils": "npm:8.23.0" - debug: "npm:^4.3.4" - ts-api-utils: "npm:^2.0.1" + "@typescript-eslint/types": "npm:8.58.2" + "@typescript-eslint/visitor-keys": "npm:8.58.2" + checksum: 10/d38da7a1d6e9d8ec87eb0d5115e3978ebe33fa5cb0f5b9b1c202ab314ebf352d33c66b0070ab3244ff79beec8fff19ec9e4063c36288ec0bc66fd24508d2b264 + languageName: node + linkType: hard + +"@typescript-eslint/tsconfig-utils@npm:8.58.2, @typescript-eslint/tsconfig-utils@npm:^8.58.2": + version: 8.58.2 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.2" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10/68d40f96150dba33ccb9aa5a1531cc0fb4c413ed6665a3d2b1651437ea162d1a3c9dc0c3dc8208e48f9eddcf51f35fa9880b18f9a03397bbafcbec8dae9391c9 + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.58.2": + version: 8.58.2 + resolution: "@typescript-eslint/type-utils@npm:8.58.2" + dependencies: + "@typescript-eslint/types": "npm:8.58.2" + "@typescript-eslint/typescript-estree": "npm:8.58.2" + "@typescript-eslint/utils": "npm:8.58.2" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.5.0" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/b036a48c5eacef10ed42aef02c859011b11b52938eab4857cfa267730820b90fd29d978a28d43174605b6b7966f095c813d9107e9f0995b92231b53983b12092 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/82eefae7f5d15cd6545deeb702ba36214903e833e58b6f460880fb3f606e1adc5eee90f55c7f0e9cb50575d17dd7c96a92e8d3e796893dd373908d8604623774 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.23.0": - version: 8.23.0 - resolution: "@typescript-eslint/types@npm:8.23.0" - checksum: 10/e2a68bc6e89226e47e495a91e0614aa5c3c4580b11f7fd99ac6728c1fce92f10755b0d7ade3cf6d3eb1209cd9cd0f29bd742f8dddc394b28bcead7025394eaa2 +"@typescript-eslint/types@npm:8.58.2, @typescript-eslint/types@npm:^8.58.2": + version: 8.58.2 + resolution: "@typescript-eslint/types@npm:8.58.2" + checksum: 10/9ffbe0542c91442c9841e157b85bc7bdc7e9a3f5ad5e6ef32f8d98556bc947f9b4151e54dec414c2c6d68eb436483259e74decd8fe56330689298980949ba5cc languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.23.0": - version: 8.23.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.23.0" +"@typescript-eslint/typescript-estree@npm:8.58.2": + version: 8.58.2 + resolution: "@typescript-eslint/typescript-estree@npm:8.58.2" dependencies: - "@typescript-eslint/types": "npm:8.23.0" - "@typescript-eslint/visitor-keys": "npm:8.23.0" - debug: "npm:^4.3.4" - fast-glob: "npm:^3.3.2" - is-glob: "npm:^4.0.3" - minimatch: "npm:^9.0.4" - semver: "npm:^7.6.0" - ts-api-utils: "npm:^2.0.1" + "@typescript-eslint/project-service": "npm:8.58.2" + "@typescript-eslint/tsconfig-utils": "npm:8.58.2" + "@typescript-eslint/types": "npm:8.58.2" + "@typescript-eslint/visitor-keys": "npm:8.58.2" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.5.0" peerDependencies: - typescript: ">=4.8.4 <5.8.0" - checksum: 10/ddc9790d460bea065eeed3760759c034aef307e72c51b5ec7d869fdc77f18c28354c9e35841b44eebbdc54241bab4154809ae8213d33593a9bff20dc3b247fc3 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/e34bfcd426045d75f91b951ed35f3f880b209434d3574c44d3aac210a16e84e4489e43d4c7b8b552b7cd44862bd58c2f411ede7b40efa5f14ede835bb3e6d3c9 languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.23.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.21.0": - version: 8.23.0 - resolution: "@typescript-eslint/utils@npm:8.23.0" +"@typescript-eslint/utils@npm:8.58.2, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.21.0, @typescript-eslint/utils@npm:^8.58.0": + version: 8.58.2 + resolution: "@typescript-eslint/utils@npm:8.58.2" dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.23.0" - "@typescript-eslint/types": "npm:8.23.0" - "@typescript-eslint/typescript-estree": "npm:8.23.0" + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.58.2" + "@typescript-eslint/types": "npm:8.58.2" + "@typescript-eslint/typescript-estree": "npm:8.58.2" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/72588d617ee5b1fa1020d008a7ff714a4a1e0fc1167aa9ff4b8ae71a37b25f43b2d40bca3380c56bb84d4092b6cac8d5d14d74e290e80217175ccf8237faf22a + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/b46e6cddf2de96618b98c2635836512c7dc63217b874698b2ed079a41762b1c3c8edae4b15690e0b0c7b896e81d39617835a19bfbe03fe4176ddded49cf813d3 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.23.0": - version: 8.23.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.23.0" +"@typescript-eslint/visitor-keys@npm:8.58.2": + version: 8.58.2 + resolution: "@typescript-eslint/visitor-keys@npm:8.58.2" dependencies: - "@typescript-eslint/types": "npm:8.23.0" - eslint-visitor-keys: "npm:^4.2.0" - checksum: 10/fd473849b85e564e31aec64feb3417a4e16e48bf21f1959fbab56258e19c21ef47bbdb523c64a8921cdc82a5083735418890b6f74b564fd1ece305c977a0f7a6 + "@typescript-eslint/types": "npm:8.58.2" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10/051dd3d3507d4b36b5ead6acd3a00813b0775405cb074ad8fb4eb951b6c7a007a6f45c57b90ffab7755b8b7636e6f9a23b787221880db174584eca0739a0a9ab languageName: node linkType: hard @@ -2064,6 +3409,28 @@ __metadata: languageName: node linkType: hard +"@vitest/eslint-plugin@npm:^1.6.6": + version: 1.6.15 + resolution: "@vitest/eslint-plugin@npm:1.6.15" + dependencies: + "@typescript-eslint/scope-manager": "npm:^8.58.0" + "@typescript-eslint/utils": "npm:^8.58.0" + peerDependencies: + "@typescript-eslint/eslint-plugin": "*" + eslint: ">=8.57.0" + typescript: ">=5.0.0" + vitest: "*" + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + typescript: + optional: true + vitest: + optional: true + checksum: 10/665b8164abc8b83a5a459048cb616af4bad94c0472f7c80de34f26a21a437144390d5986e6e6ddb1746ae635b12afbaae8a2c054f426c66519429e85760b9c3a + languageName: node + linkType: hard + "JSONStream@npm:^1.3.5": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" @@ -2099,7 +3466,7 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:^1.3.8": +"accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:^1.3.8, accepts@npm:~1.3.4, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" dependencies: @@ -2109,17 +3476,6 @@ __metadata: languageName: node linkType: hard -"acorn-class-fields@npm:^0.3.7": - version: 0.3.7 - resolution: "acorn-class-fields@npm:0.3.7" - dependencies: - acorn-private-class-elements: "npm:^0.2.7" - peerDependencies: - acorn: ^6 || ^7 || ^8 - checksum: 10/02151a4d3f985707529fefcf1388583ec7ae3c99a7aaaf9f50ffd301b73d02c6c4842e04a431853eaaabb8fee421e35df060447cc8080e226a520ceaa5295b52 - languageName: node - linkType: hard - "acorn-import-attributes@npm:^1.9.5": version: 1.9.5 resolution: "acorn-import-attributes@npm:1.9.5" @@ -2129,7 +3485,7 @@ __metadata: languageName: node linkType: hard -"acorn-jsx@npm:^5.3.1, acorn-jsx@npm:^5.3.2": +"acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" peerDependencies: @@ -2138,63 +3494,21 @@ __metadata: languageName: node linkType: hard -"acorn-private-class-elements@npm:^0.2.7": - version: 0.2.7 - resolution: "acorn-private-class-elements@npm:0.2.7" - peerDependencies: - acorn: ^6.1.0 || ^7 || ^8 - checksum: 10/e4980847da236c56bb0ed87e38c89348d609701f2927e5db0757f4dbba1b773b459d8b98b4b80525ff34b24a5c2777f85e01ecd426a518a40ce8c6234a4d9486 - languageName: node - linkType: hard - -"acorn-private-methods@npm:^0.3.3": - version: 0.3.3 - resolution: "acorn-private-methods@npm:0.3.3" - dependencies: - acorn-private-class-elements: "npm:^0.2.7" - peerDependencies: - acorn: ^6 || ^7 || ^8 - checksum: 10/da692a68fb39020d0371e4d3c44c87780120b70b0751533c6f391a765df560a92b4a0914d5782cbd4fda58eda8bc4291bc0cb6a33b635486256ca63a1fb0bf13 - languageName: node - linkType: hard - -"acorn-stage3@npm:^4.0.0": - version: 4.0.0 - resolution: "acorn-stage3@npm:4.0.0" - dependencies: - acorn-class-fields: "npm:^0.3.7" - acorn-private-methods: "npm:^0.3.3" - acorn-static-class-features: "npm:^0.2.4" - peerDependencies: - acorn: ^7.4 || ^8 - checksum: 10/10c2322b6494a104ed212b97e9925b2d03b50e4f1e370ad50d9d5e9507f79092dc046802f13ca57ff681c91ef7c02e0f68469473431833603b79630d233f0476 - languageName: node - linkType: hard - -"acorn-static-class-features@npm:^0.2.4": - version: 0.2.4 - resolution: "acorn-static-class-features@npm:0.2.4" +"acorn-walk@npm:8.3.5, acorn-walk@npm:^8.0.0": + version: 8.3.5 + resolution: "acorn-walk@npm:8.3.5" dependencies: - acorn-private-class-elements: "npm:^0.2.7" - peerDependencies: - acorn: ^6.1.0 || ^7 || ^8 - checksum: 10/145ad623afc61f8417ce5cb6544d4c938099b313e59952eb50ca30b18c4a64ea3a30295c3063a599e90a8db03863606ca97873d52f93e1890e80bd9402ab3153 + acorn: "npm:^8.11.0" + checksum: 10/f52a158a1c1f00c82702c7eb9b8ae8aad79748a7689241dcc2d797dce680f1dcb15c78f312f687eeacdfb3a4cac4b87d04af470f0201bd56c6661fca6f94b195 languageName: node linkType: hard -"acorn-walk@npm:^8.0.0": - version: 8.2.0 - resolution: "acorn-walk@npm:8.2.0" - checksum: 10/e69f7234f2adfeb16db3671429a7c80894105bd7534cb2032acf01bb26e6a847952d11a062d071420b43f8d82e33d2e57f26fe87d9cce0853e8143d8910ff1de - languageName: node - linkType: hard - -"acorn@npm:^8.0.4, acorn@npm:^8.14.0, acorn@npm:^8.15.0": - version: 8.15.0 - resolution: "acorn@npm:8.15.0" +"acorn@npm:^8.0.4, acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.15.0": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" bin: acorn: bin/acorn - checksum: 10/77f2de5051a631cf1729c090e5759148459cdb76b5f5c70f890503d629cf5052357b0ce783c0f976dd8a93c5150f59f6d18df1def3f502396a20f81282482fa4 + checksum: 10/690c673bb4d61b38ef82795fab58526471ad7f7e67c0e40c4ff1e10ecd80ce5312554ef633c9995bfc4e6d170cef165711f9ca9e49040b62c0c66fbf2dd3df2b languageName: node linkType: hard @@ -2228,7 +3542,32 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.4": +"ajv-formats@npm:^2.1.1": + version: 2.1.1 + resolution: "ajv-formats@npm:2.1.1" + dependencies: + ajv: "npm:^8.0.0" + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 10/70c263ded219bf277ffd9127f793b625f10a46113b2e901e150da41931fcfd7f5592da6d66862f4449bb157ffe65867c3294a7df1d661cc232c4163d5a1718ed + languageName: node + linkType: hard + +"ajv-keywords@npm:^5.1.0": + version: 5.1.0 + resolution: "ajv-keywords@npm:5.1.0" + dependencies: + fast-deep-equal: "npm:^3.1.3" + peerDependencies: + ajv: ^8.8.2 + checksum: 10/5021f96ab7ddd03a4005326bd06f45f448ebfbb0fe7018b1b70b6c28142fa68372bda2057359814b83fd0b2d4c8726c297f0a7557b15377be7b56ce5344533d8 + languageName: node + linkType: hard + +"ajv@npm:^6.14.0": version: 6.14.0 resolution: "ajv@npm:6.14.0" dependencies: @@ -2240,6 +3579,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^8.0.0, ajv@npm:^8.9.0": + version: 8.20.0 + resolution: "ajv@npm:8.20.0" + dependencies: + fast-deep-equal: "npm:^3.1.3" + fast-uri: "npm:^3.0.1" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + checksum: 10/5ce59c0537f4c2aca9a758b412659ec70acb4d5dde971c10ecf21d2e3d799f99acdb4a08e1f5fb2e067c8542930398aae793bb996bb07d3feb81dae22fe2ada9 + languageName: node + linkType: hard + "amqplib@npm:0.10.5": version: 0.10.5 resolution: "amqplib@npm:0.10.5" @@ -2260,6 +3611,15 @@ __metadata: languageName: node linkType: hard +"ansi-html-community@npm:^0.0.8": + version: 0.0.8 + resolution: "ansi-html-community@npm:0.0.8" + bin: + ansi-html: bin/ansi-html + checksum: 10/08df3696720edacd001a8d53b197bb5728242c55484680117dab9f7633a6320e961a939bddd88ee5c71d4a64f3ddb49444d1c694bd0668adbb3f95ba114f2386 + languageName: node + linkType: hard + "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -2267,7 +3627,7 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:^6.0.1": +"ansi-regex@npm:^6.2.2": version: 6.2.2 resolution: "ansi-regex@npm:6.2.2" checksum: 10/9b17ce2c6daecc75bcd5966b9ad672c23b184dc3ed9bf3c98a0702f0d2f736c15c10d461913568f2cf527a5e64291c7473358885dd493305c84a1cfed66ba94f @@ -2306,7 +3666,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.1.3": +"anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -2349,6 +3709,13 @@ __metadata: languageName: node linkType: hard +"array-flatten@npm:1.1.1": + version: 1.1.1 + resolution: "array-flatten@npm:1.1.1" + checksum: 10/e13c9d247241be82f8b4ec71d035ed7204baa82fae820d4db6948d30d3c4a9f2b3905eb2eec2b937d4aa3565200bd3a1c500480114cff649fa748747d2a50feb + languageName: node + linkType: hard + "array-ify@npm:^1.0.0": version: 1.0.0 resolution: "array-ify@npm:1.0.0" @@ -2403,7 +3770,7 @@ __metadata: languageName: node linkType: hard -"assert@npm:^2.1.0": +"assert@npm:^2.0.0, assert@npm:^2.1.0": version: 2.1.0 resolution: "assert@npm:2.1.0" dependencies: @@ -2473,67 +3840,70 @@ __metadata: dependencies: "@babel/core": "npm:^7.29.0" "@babel/plugin-transform-modules-commonjs": "npm:^7.28.6" - "@babel/runtime": "npm:^7.28.6" + "@babel/runtime": "npm:^7.29.2" "@koa/cors": "npm:^5.0.0" - "@koa/router": "npm:^15.3.0" + "@koa/router": "npm:^15.4.0" + "@meteorjs/rspack": "npm:2.0.1" "@mos-connection/helper": "npm:^5.0.0-alpha.0" + "@rsdoctor/rspack-plugin": "npm:1.5.7" + "@rspack/cli": "npm:1.7.1" + "@rspack/core": "npm:1.7.1" "@shopify/jest-koa-mocks": "npm:^5.3.1" - "@slack/webhook": "npm:^7.0.6" + "@slack/webhook": "npm:^7.0.9" "@sofie-automation/blueprints-integration": "portal:../packages/blueprints-integration" - "@sofie-automation/code-standard-preset": "npm:^3.0.0" + "@sofie-automation/code-standard-preset": "npm:^3.2.2" "@sofie-automation/corelib": "portal:../packages/corelib" "@sofie-automation/job-worker": "portal:../packages/job-worker" "@sofie-automation/meteor-lib": "portal:../packages/meteor-lib" "@sofie-automation/shared-lib": "portal:../packages/shared-lib" - "@types/body-parser": "npm:^1.19.6" + "@swc/core": "npm:^1.15.41" + "@swc/helpers": "npm:0.5.17" "@types/deep-extend": "npm:^0.6.2" "@types/jest": "npm:^30.0.0" - "@types/koa": "npm:^3.0.1" + "@types/koa": "npm:^3.0.2" "@types/koa-bodyparser": "npm:^4.3.13" "@types/koa-mount": "npm:^4.0.5" "@types/koa-static": "npm:^4.0.4" "@types/koa__cors": "npm:^5.0.1" - "@types/node": "npm:^22.19.8" + "@types/node": "npm:^22.19.17" "@types/semver": "npm:^7.7.1" "@types/underscore": "npm:^1.13.0" - babel-jest: "npm:^30.2.0" + babel-jest: "npm:^30.3.0" bcrypt: "npm:^6.0.0" - body-parser: "npm:^1.20.4" - commit-and-tag-version: "npm:^12.6.1" + commit-and-tag-version: "npm:^12.7.1" deep-extend: "npm:0.6.0" deepmerge: "npm:^4.3.1" ejson: "npm:^2.2.3" elastic-apm-node: "npm:^4.15.0" - eslint: "npm:^9.39.2" + eslint: "npm:^9.39.4" fast-clone: "npm:^1.5.13" - glob: "npm:^13.0.1" - i18next: "npm:^21.10.0" - i18next-conv: "npm:^10.2.0" - i18next-scanner: "npm:^4.6.0" + glob: "npm:^13.0.6" + i18next: "npm:^26.0.8" + i18next-cli: "npm:^1.56.7" + i18next-conv: "npm:^16.0.0" indexof: "npm:0.0.1" - jest: "npm:^30.2.0" - jest-util: "npm:^30.2.0" - koa: "npm:^3.1.1" + jest: "npm:^30.3.0" + jest-util: "npm:^30.3.0" + koa: "npm:^3.2.0" koa-bodyparser: "npm:^4.4.1" koa-mount: "npm:^4.2.0" koa-static: "npm:^5.0.0" legally: "npm:^3.5.10" - meteor-node-stubs: "npm:^1.2.25" + meteor-node-stubs: "npm:^1.2.27" moment: "npm:^2.30.1" nanoid: "npm:^3.3.11" ntp-client: "npm:^0.5.3" object-path: "npm:^0.11.8" - open-cli: "npm:^8.0.0" + open-cli: "npm:^9.0.0" p-lazy: "npm:^3.1.0" - prettier: "npm:^3.8.1" - semver: "npm:^7.7.3" + prettier: "npm:^3.8.3" + semver: "npm:^7.7.4" superfly-timeline: "npm:9.2.0" - threadedclass: "npm:^1.3.0" - timecode: "npm:0.0.4" - ts-jest: "npm:^29.4.6" + threadedclass: "npm:^1.4.0" + ts-jest: "npm:^29.4.9" type-fest: "npm:^4.41.0" - typescript: "npm:~5.7.3" - underscore: "npm:^1.13.7" + typescript: "npm:~5.9.3" + underscore: "npm:^1.13.8" winston: "npm:^3.19.0" yargs: "npm:^17.7.2" languageName: unknown @@ -2548,31 +3918,31 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.11.0": - version: 1.13.6 - resolution: "axios@npm:1.13.6" +"axios@npm:^1.15.0": + version: 1.15.0 + resolution: "axios@npm:1.15.0" dependencies: follow-redirects: "npm:^1.15.11" form-data: "npm:^4.0.5" - proxy-from-env: "npm:^1.1.0" - checksum: 10/a7ed83c2af3ef21d64609df0f85e76893a915a864c5934df69241001d0578082d6521a0c730bf37518ee458821b5695957cb10db9fc705f2a8996c8686ea7a89 + proxy-from-env: "npm:^2.1.0" + checksum: 10/d39a2c0ebc7ff4739401b282e726cc2673377949d6c46d60eb619458f8d7a2f7eadbcada7097f4dbc7d5c59abb4d3bf6fac33d474412bc3415d3f5aa7ed45530 languageName: node linkType: hard -"babel-jest@npm:30.2.0, babel-jest@npm:^30.2.0": - version: 30.2.0 - resolution: "babel-jest@npm:30.2.0" +"babel-jest@npm:30.3.0, babel-jest@npm:^30.3.0": + version: 30.3.0 + resolution: "babel-jest@npm:30.3.0" dependencies: - "@jest/transform": "npm:30.2.0" + "@jest/transform": "npm:30.3.0" "@types/babel__core": "npm:^7.20.5" babel-plugin-istanbul: "npm:^7.0.1" - babel-preset-jest: "npm:30.2.0" + babel-preset-jest: "npm:30.3.0" chalk: "npm:^4.1.2" graceful-fs: "npm:^4.2.11" slash: "npm:^3.0.0" peerDependencies: "@babel/core": ^7.11.0 || ^8.0.0-0 - checksum: 10/4c7351a366cf8ac2b8a2e4e438867693eb9d83ed24c29c648da4576f700767aaf72a5d14337fc3f92c50b069f5025b26c7b89e3b7b867914b7cf8997fc15f095 + checksum: 10/7c78f083b11430e69e719ddacd4089db3c055437e06b2d7b382d797a675c7a114268f0044ce98c9a32091638cb9ada53e278d46a7079a74ff845d1aa4a2b0678 languageName: node linkType: hard @@ -2589,12 +3959,12 @@ __metadata: languageName: node linkType: hard -"babel-plugin-jest-hoist@npm:30.2.0": - version: 30.2.0 - resolution: "babel-plugin-jest-hoist@npm:30.2.0" +"babel-plugin-jest-hoist@npm:30.3.0": + version: 30.3.0 + resolution: "babel-plugin-jest-hoist@npm:30.3.0" dependencies: "@types/babel__core": "npm:^7.20.5" - checksum: 10/360e87a9aa35f4cf208a10ba79e1821ea906f9e3399db2a9762cbc5076fd59f808e571d88b5b1106738d22e23f9ddefbb8137b2780b2abd401c8573b85c8a2f5 + checksum: 10/1444d633a8ad2505d5e15e458718f1bc5929a074f14179a38f53542c32d3c5158a6f7cab82f7fa6b334b0a45982252639bd7642bb0bc843c6566e44cb083925e languageName: node linkType: hard @@ -2623,15 +3993,15 @@ __metadata: languageName: node linkType: hard -"babel-preset-jest@npm:30.2.0": - version: 30.2.0 - resolution: "babel-preset-jest@npm:30.2.0" +"babel-preset-jest@npm:30.3.0": + version: 30.3.0 + resolution: "babel-preset-jest@npm:30.3.0" dependencies: - babel-plugin-jest-hoist: "npm:30.2.0" + babel-plugin-jest-hoist: "npm:30.3.0" babel-preset-current-node-syntax: "npm:^1.2.0" peerDependencies: "@babel/core": ^7.11.0 || ^8.0.0-beta.1 - checksum: 10/f75e155a8cf63ea1c5ca942bf757b934427630a1eeafdf861e9117879b3367931fc521da3c41fd52f8d59d705d1093ffb46c9474b3fd4d765d194bea5659d7d9 + checksum: 10/fd29c8ff5967c047006bde152cf5ac99ce2e1d573f6f044828cb4d06eab95b65549a38554ea97174bbe508006d2a7cb1370581d87aa73f6b3c2134f2d49aaf85 languageName: node linkType: hard @@ -2656,6 +4026,22 @@ __metadata: languageName: node linkType: hard +"base64id@npm:2.0.0, base64id@npm:~2.0.0": + version: 2.0.0 + resolution: "base64id@npm:2.0.0" + checksum: 10/e3312328429e512b0713469c5312f80b447e71592cae0a5bddf3f1adc9c89d1b2ed94156ad7bb9f529398f310df7ff6f3dbe9550735c6a759f247c088ea67364 + languageName: node + linkType: hard + +"baseline-browser-mapping@npm:^2.10.12": + version: 2.10.29 + resolution: "baseline-browser-mapping@npm:2.10.29" + bin: + baseline-browser-mapping: dist/cli.cjs + checksum: 10/df8fd128168e473abf1ebe3b7d6a9d7fead3a4d76f9f6aa3dce4dd797e5b5a1ecd32b7eb0855c21f6acdb5c48eba9e176a4f93040e47790bb05fe3fccd4ad9d6 + languageName: node + linkType: hard + "basic-auth@npm:^2.0.1": version: 2.0.1 resolution: "basic-auth@npm:2.0.1" @@ -2665,6 +4051,13 @@ __metadata: languageName: node linkType: hard +"batch@npm:0.6.1": + version: 0.6.1 + resolution: "batch@npm:0.6.1" + checksum: 10/61f9934c7378a51dce61b915586191078ef7f1c3eca707fdd58b96ff2ff56d9e0af2bdab66b1462301a73c73374239e6542d9821c0af787f3209a23365d07e7f + languageName: node + linkType: hard + "bcrypt@npm:^6.0.0": version: 6.0.0 resolution: "bcrypt@npm:6.0.0" @@ -2683,6 +4076,13 @@ __metadata: languageName: node linkType: hard +"binary-extensions@npm:^2.0.0": + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: 10/bcad01494e8a9283abf18c1b967af65ee79b0c6a9e6fcfafebfe91dbe6e0fc7272bafb73389e198b310516ae04f7ad17d79aacf6cb4c0d5d5202a7e2e52c7d98 + languageName: node + linkType: hard + "binary-search@npm:^1.3.3": version: 1.3.6 resolution: "binary-search@npm:1.3.6" @@ -2697,17 +4097,6 @@ __metadata: languageName: node linkType: hard -"bl@npm:^5.0.0": - version: 5.1.0 - resolution: "bl@npm:5.1.0" - dependencies: - buffer: "npm:^6.0.3" - inherits: "npm:^2.0.4" - readable-stream: "npm:^3.4.0" - checksum: 10/0340d3d70def4213cd9cbcd8592f7c5922d3668e7b231286c354613fac4a8411ad373cff26e06162da7423035bbd5caafce3e140a5f397be72fcd1e9d86f1179 - languageName: node - linkType: hard - "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9": version: 4.12.3 resolution: "bn.js@npm:4.12.3" @@ -2715,16 +4104,16 @@ __metadata: languageName: node linkType: hard -"bn.js@npm:^5.2.1": +"bn.js@npm:^5.2.1, bn.js@npm:^5.2.2": version: 5.2.3 resolution: "bn.js@npm:5.2.3" checksum: 10/dfb3927e0d531e6ec4f191597ce6f7f7665310c356fef5f968ada676b8058027f959af42eaa37b5f5c63617e819d3741813025ab15dd71a90f2e74698df0b58e languageName: node linkType: hard -"body-parser@npm:^1.20.4": - version: 1.20.4 - resolution: "body-parser@npm:1.20.4" +"body-parser@npm:~1.20.5": + version: 1.20.5 + resolution: "body-parser@npm:1.20.5" dependencies: bytes: "npm:~3.1.2" content-type: "npm:~1.0.5" @@ -2734,11 +4123,21 @@ __metadata: http-errors: "npm:~2.0.1" iconv-lite: "npm:~0.4.24" on-finished: "npm:~2.4.1" - qs: "npm:~6.14.0" + qs: "npm:~6.15.1" raw-body: "npm:~2.5.3" type-is: "npm:~1.6.18" unpipe: "npm:~1.0.0" - checksum: 10/ff67e28d3f426707be8697a75fdf8d564dc50c341b41f054264d8ab6e2924e519c7ce8acc9d0de05328fdc41e1d9f3f200aec9c1cfb1867d6b676a410d97c689 + checksum: 10/3ec787c0d23b16542972226ee352ed917ae404bf862b6783040e8cfc994f165432f6d48e9341ef57f489c667c880f9c5174fad559c482607f83f4db7f412de3a + languageName: node + linkType: hard + +"bonjour-service@npm:^1.2.1": + version: 1.3.0 + resolution: "bonjour-service@npm:1.3.0" + dependencies: + fast-deep-equal: "npm:^3.1.3" + multicast-dns: "npm:^7.2.5" + checksum: 10/63d516d88f15fa4b89e247e6ff7d81c21a3ef5ed035b0b043c2b38e0c839f54f4ce58fbf9b7668027bf538ac86de366939dbb55cca63930f74eeea1e278c9585 languageName: node linkType: hard @@ -2770,7 +4169,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.3": +"braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -2795,6 +4194,15 @@ __metadata: languageName: node linkType: hard +"browser-resolve@npm:^2.0.0": + version: 2.0.0 + resolution: "browser-resolve@npm:2.0.0" + dependencies: + resolve: "npm:^1.17.0" + checksum: 10/ad5314db3429a903b07d6445137588665c4677d6276298bb08f0623f05cb107762b73c78f03b4f954a712bd1ebaf98e349b9d98e423123a42804924327a5acd4 + languageName: node + linkType: hard + "browserify-aes@npm:^1.0.4, browserify-aes@npm:^1.2.0": version: 1.2.0 resolution: "browserify-aes@npm:1.2.0" @@ -2832,7 +4240,7 @@ __metadata: languageName: node linkType: hard -"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.1.0": +"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.1.0, browserify-rsa@npm:^4.1.1": version: 4.1.1 resolution: "browserify-rsa@npm:4.1.1" dependencies: @@ -2843,6 +4251,23 @@ __metadata: languageName: node linkType: hard +"browserify-sign@npm:^4.2.3": + version: 4.2.5 + resolution: "browserify-sign@npm:4.2.5" + dependencies: + bn.js: "npm:^5.2.2" + browserify-rsa: "npm:^4.1.1" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + elliptic: "npm:^6.6.1" + inherits: "npm:^2.0.4" + parse-asn1: "npm:^5.1.9" + readable-stream: "npm:^2.3.8" + safe-buffer: "npm:^5.2.1" + checksum: 10/ccfe54ab61b8e01e84c507b60912f9ae8701f4e53accc3d85c3773db13f14c51f17b684167735d28c59aaf5523ee59c66cc831ddc178bc7f598257e590ca1a35 + languageName: node + linkType: hard + "browserify-zlib@npm:^0.2.0": version: 0.2.0 resolution: "browserify-zlib@npm:0.2.0" @@ -2852,17 +4277,36 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.24.0": - version: 4.24.4 - resolution: "browserslist@npm:4.24.4" +"browserslist-load-config@npm:^1.0.1": + version: 1.0.1 + resolution: "browserslist-load-config@npm:1.0.1" + checksum: 10/872d2978d2546eb02920b7124d8269e10b3a8d26c1426f1ca844c0d4db53929789d1df5acd0b322b464af18264b58d0f3038a54656fe160c6dc1ab18b2d9491f + languageName: node + linkType: hard + +"browserslist-to-es-version@npm:^1.2.0": + version: 1.4.1 + resolution: "browserslist-to-es-version@npm:1.4.1" + dependencies: + browserslist: "npm:^4.28.1" + bin: + browserslist-to-es-version: dist/cli.js + checksum: 10/0eac7e028636e16d360a5904ccc5e3d1a749d1451da2a716e2f48bca43709766c8f74e371529f81cbc18841a21da4e020c03801ffa117df3848574b358e7d4eb + languageName: node + linkType: hard + +"browserslist@npm:^4.24.0, browserslist@npm:^4.28.1": + version: 4.28.2 + resolution: "browserslist@npm:4.28.2" dependencies: - caniuse-lite: "npm:^1.0.30001688" - electron-to-chromium: "npm:^1.5.73" - node-releases: "npm:^2.0.19" - update-browserslist-db: "npm:^1.1.1" + baseline-browser-mapping: "npm:^2.10.12" + caniuse-lite: "npm:^1.0.30001782" + electron-to-chromium: "npm:^1.5.328" + node-releases: "npm:^2.0.36" + update-browserslist-db: "npm:^1.2.3" bin: browserslist: cli.js - checksum: 10/11fda105e803d891311a21a1f962d83599319165faf471c2d70e045dff82a12128f5b50b1fcba665a2352ad66147aaa248a9d2355a80aadc3f53375eb3de2e48 + checksum: 10/cff88386e5b5ba5614c9063bd32ef94865bba22b6a381844c7d09ea1eea62a2247e7106e516abdbfda6b75b9986044c991dfe45f92f10add5ad63dccc07589ec languageName: node linkType: hard @@ -2884,10 +4328,10 @@ __metadata: languageName: node linkType: hard -"bson@npm:^6.10.4": - version: 6.10.4 - resolution: "bson@npm:6.10.4" - checksum: 10/8a79a452219a13898358a5abc93e32bc3805236334f962661da121ce15bd5cade27718ba3310ee2a143ff508489b08467eed172ecb2a658cb8d2e94fdb76b215 +"bson@npm:^7.1.1": + version: 7.2.0 + resolution: "bson@npm:7.2.0" + checksum: 10/a768e55d328a5961aa1bda0f1d1ea619e8bab911fb72018ca5c779000ae538cfc0cc8082fe75d4e941cb8ee928b6a7377b10bc0eb75872f36cdfaac8380512c6 languageName: node linkType: hard @@ -2948,7 +4392,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:~3.1.2": +"bytes@npm:3.1.2, bytes@npm:~3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: 10/a10abf2ba70c784471d6b4f58778c0beeb2b5d405148e66affa91f23a9f13d07603d0a0354667310ae1d6dc141474ffd44e2a074be0f6e2254edb8fc21445388 @@ -3048,10 +4492,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001688": - version: 1.0.30001697 - resolution: "caniuse-lite@npm:1.0.30001697" - checksum: 10/cd0ca97e71f4157ff3d26990a24122586a973a14086ad43c459c2f0f2f9876b327eee57c2315bb04bd5e826e77d0b6f55723c583c78be0eaf0f3f171afaf7eff +"caniuse-lite@npm:^1.0.30001782": + version: 1.0.30001792 + resolution: "caniuse-lite@npm:1.0.30001792" + checksum: 10/96635acd22e8bd5d02c165de659cc14265b96f5e7253acd0117ad94a3310297f2402e4a8665d6f20003bf6193c3b995d2cb9fda6b3c2f731194dc9299c90f905 languageName: node linkType: hard @@ -3066,7 +4510,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.2": +"chalk@npm:^4.0.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -3076,6 +4520,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.6.2": + version: 5.6.2 + resolution: "chalk@npm:5.6.2" + checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0 + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -3083,6 +4534,41 @@ __metadata: languageName: node linkType: hard +"chardet@npm:^2.1.1": + version: 2.1.1 + resolution: "chardet@npm:2.1.1" + checksum: 10/d56913b65e45c5c86f331988e2ef6264c131bfeadaae098ee719bf6610546c77740e37221ffec802dde56b5e4466613a4c754786f4da6b5f6c5477243454d324 + languageName: node + linkType: hard + +"chokidar@npm:^3.6.0": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10/c327fb07704443f8d15f7b4a7ce93b2f0bc0e6cea07ec28a7570aa22cd51fcf0379df589403976ea956c369f25aa82d84561947e227cd925902e1751371658df + languageName: node + linkType: hard + +"chokidar@npm:^5.0.0": + version: 5.0.0 + resolution: "chokidar@npm:5.0.0" + dependencies: + readdirp: "npm:^5.0.0" + checksum: 10/a1c2a4ee6ee81ba6409712c295a47be055fb9de1186dfbab33c1e82f28619de962ba02fc5f9d433daaedc96c35747460d8b2079ac2907de2c95e3f7cce913113 + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -3129,6 +4615,29 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^5.0.0": + version: 5.0.0 + resolution: "cli-cursor@npm:5.0.0" + dependencies: + restore-cursor: "npm:^5.0.0" + checksum: 10/1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090 + languageName: node + linkType: hard + +"cli-spinners@npm:^3.2.0": + version: 3.4.0 + resolution: "cli-spinners@npm:3.4.0" + checksum: 10/6a4021c1999011fc34ae714f055dcdafb56309abc1f8fb021ea7d9370dfc524485fe8684226015e5fe6053dd30544e74270184ff7edc3fa4d37043b8efd0a054 + languageName: node + linkType: hard + +"cli-width@npm:^4.1.0": + version: 4.1.0 + resolution: "cli-width@npm:4.1.0" + checksum: 10/b58876fbf0310a8a35c79b72ecfcf579b354e18ad04e6b20588724ea2b522799a758507a37dfe132fafaf93a9922cafd9514d9e1598e6b2cd46694853aed099f + languageName: node + linkType: hard + "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -3151,7 +4660,7 @@ __metadata: languageName: node linkType: hard -"clone-deep@npm:^4.0.0": +"clone-deep@npm:^4.0.1": version: 4.0.1 resolution: "clone-deep@npm:4.0.1" dependencies: @@ -3162,20 +4671,6 @@ __metadata: languageName: node linkType: hard -"clone-stats@npm:^1.0.0": - version: 1.0.0 - resolution: "clone-stats@npm:1.0.0" - checksum: 10/654c0425afc5c5c55a4d95b2e0c6eccdd55b5247e7a1e7cca9000b13688b96b0a157950c72c5307f9fd61f17333ad796d3cd654778f2d605438012391cc4ada5 - languageName: node - linkType: hard - -"clone@npm:^2.1.2": - version: 2.1.2 - resolution: "clone@npm:2.1.2" - checksum: 10/d9c79efba655f0bf601ab299c57eb54cbaa9860fb011aee9d89ed5ac0d12df1660ab7642fddaabb9a26b7eff0e117d4520512cb70798319ff5d30a111b5310c2 - languageName: node - linkType: hard - "co-body@npm:^6.0.0": version: 6.1.0 resolution: "co-body@npm:6.1.0" @@ -3269,6 +4764,13 @@ __metadata: languageName: node linkType: hard +"colorette@npm:^2.0.10, colorette@npm:^2.0.20": + version: 2.0.20 + resolution: "colorette@npm:2.0.20" + checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f + languageName: node + linkType: hard + "combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" @@ -3278,23 +4780,23 @@ __metadata: languageName: node linkType: hard -"commander@npm:^5.1.0": - version: 5.1.0 - resolution: "commander@npm:5.1.0" - checksum: 10/3e2ef5c003c5179250161e42ce6d48e0e69a54af970c65b7f985c70095240c260fd647453efd4c2c5a31b30ce468f373dc70f769c2f54a2c014abc4792aaca28 +"commander@npm:^14.0.2, commander@npm:^14.0.3": + version: 14.0.3 + resolution: "commander@npm:14.0.3" + checksum: 10/dfa9ebe2a433d277de5cb0252d23b10a543d245d892db858d23b516336a835c50fd4f52bee4cd13c705cc8acb6f03dc632c73dd806f7d06d3353eb09953dd17a languageName: node linkType: hard -"commander@npm:^9.0.0": - version: 9.5.0 - resolution: "commander@npm:9.5.0" - checksum: 10/41c49b3d0f94a1fbeb0463c85b13f15aa15a9e0b4d5e10a49c0a1d58d4489b549d62262b052ae0aa6cfda53299bee487bfe337825df15e342114dde543f82906 +"commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 10/9973af10727ad4b44f26703bf3e9fdc323528660a7590efe3aa9ad5042b4584c0deed84ba443f61c9d6f02dade54a5a5d3c95e306a1e1630f8374ae6db16c06d languageName: node linkType: hard -"commit-and-tag-version@npm:^12.6.1": - version: 12.6.1 - resolution: "commit-and-tag-version@npm:12.6.1" +"commit-and-tag-version@npm:^12.7.1": + version: 12.7.1 + resolution: "commit-and-tag-version@npm:12.7.1" dependencies: chalk: "npm:^2.4.2" conventional-changelog: "npm:4.0.0" @@ -3304,7 +4806,7 @@ __metadata: detect-indent: "npm:^6.1.0" detect-newline: "npm:^3.1.0" dotgitignore: "npm:^2.1.0" - fast-xml-parser: "npm:^5.2.5" + fast-xml-parser: "npm:^5.5.6" figures: "npm:^3.2.0" find-up: "npm:^5.0.0" git-semver-tags: "npm:^5.0.1" @@ -3313,7 +4815,7 @@ __metadata: yargs: "npm:^17.7.2" bin: commit-and-tag-version: bin/cli.js - checksum: 10/a7fc46f4e9b50a9071986ea7743839af7a73609bcecbb73ba3a2de15dd2d89f10174a05e8e1af2ffa1a435acf11093589d338e73408def6705278a6f8d7c91b3 + checksum: 10/6458ba7468afb684bc50717f97a240efc3c827b06e19531e0b924e74794bc9dcda6f02a88bce40ecba7806e195ca7dc046a8856da3f0895b85d1bb96d18d2fc8 languageName: node linkType: hard @@ -3327,6 +4829,30 @@ __metadata: languageName: node linkType: hard +"compressible@npm:~2.0.18": + version: 2.0.18 + resolution: "compressible@npm:2.0.18" + dependencies: + mime-db: "npm:>= 1.43.0 < 2" + checksum: 10/58321a85b375d39230405654721353f709d0c1442129e9a17081771b816302a012471a9b8f4864c7dbe02eef7f2aaac3c614795197092262e94b409c9be108f0 + languageName: node + linkType: hard + +"compression@npm:^1.7.4": + version: 1.8.1 + resolution: "compression@npm:1.8.1" + dependencies: + bytes: "npm:3.1.2" + compressible: "npm:~2.0.18" + debug: "npm:2.6.9" + negotiator: "npm:~0.6.4" + on-headers: "npm:~1.1.0" + safe-buffer: "npm:5.2.1" + vary: "npm:~1.1.2" + checksum: 10/e7552bfbd780f2003c6fe8decb44561f5cc6bc82f0c61e81122caff5ec656f37824084f52155b1e8ef31d7656cecbec9a2499b7a68e92e20780ffb39b479abb7 + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -3346,7 +4872,14 @@ __metadata: languageName: node linkType: hard -"console-browserify@npm:^1.2.0": +"connect-history-api-fallback@npm:^2.0.0": + version: 2.0.0 + resolution: "connect-history-api-fallback@npm:2.0.0" + checksum: 10/3b26bf4041fdb33deacdcb3af9ae11e9a0b413fb14c95844d74a460b55e407625b364955dcf965c654605cde9d24ad5dad423c489aa430825aab2035859aba0c + languageName: node + linkType: hard + +"console-browserify@npm:^1.1.0, console-browserify@npm:^1.2.0": version: 1.2.0 resolution: "console-browserify@npm:1.2.0" checksum: 10/4f16c471fa84909af6ae00527ce8d19dd9ed587eab85923c145cadfbc35414139f87e7bdd61746138e22cd9df45c2a1ca060370998c2c39f801d4a778105bac5 @@ -3367,7 +4900,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:^0.5.3, content-disposition@npm:~0.5.2": +"content-disposition@npm:^0.5.3, content-disposition@npm:~0.5.2, content-disposition@npm:~0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -3383,7 +4916,7 @@ __metadata: languageName: node linkType: hard -"content-type@npm:^1.0.4, content-type@npm:^1.0.5, content-type@npm:~1.0.5": +"content-type@npm:1.0.5, content-type@npm:^1.0.4, content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10/585847d98dc7fb8035c02ae2cb76c7a9bd7b25f84c447e5ed55c45c2175e83617c8813871b4ee22f368126af6b2b167df655829007b21aa10302873ea9c62662 @@ -3583,7 +5116,14 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.7.1": +"cookie-signature@npm:~1.0.6": + version: 1.0.7 + resolution: "cookie-signature@npm:1.0.7" + checksum: 10/1a62808cd30d15fb43b70e19829b64d04b0802d8ef00275b57d152de4ae6a3208ca05c197b6668d104c4d9de389e53ccc2d3bc6bcaaffd9602461417d8c40710 + languageName: node + linkType: hard + +"cookie@npm:^0.7.1, cookie@npm:~0.7.1, cookie@npm:~0.7.2": version: 0.7.2 resolution: "cookie@npm:0.7.2" checksum: 10/24b286c556420d4ba4e9bc09120c9d3db7d28ace2bd0f8ccee82422ce42322f73c8312441271e5eefafbead725980e5996cc02766dbb89a90ac7f5636ede608f @@ -3614,6 +5154,26 @@ __metadata: languageName: node linkType: hard +"cors@npm:~2.8.5": + version: 2.8.6 + resolution: "cors@npm:2.8.6" + dependencies: + object-assign: "npm:^4" + vary: "npm:^1" + checksum: 10/aa7174305b21ceb90f9c84f4eaa32f04432d333addbfdc0d1eb7310393c48902e5364aada5ac2f5d054528d63b3179238444475426fcb74e1e345077de485727 + languageName: node + linkType: hard + +"create-ecdh@npm:^4.0.4": + version: 4.0.4 + resolution: "create-ecdh@npm:4.0.4" + dependencies: + bn.js: "npm:^4.1.0" + elliptic: "npm:^6.5.3" + checksum: 10/0dd7fca9711d09e152375b79acf1e3f306d1a25ba87b8ff14c2fd8e68b83aafe0a7dd6c4e540c9ffbdd227a5fa1ad9b81eca1f233c38bb47770597ba247e614b + languageName: node + linkType: hard + "create-hash@npm:^1.1.0, create-hash@npm:^1.2.0": version: 1.2.0 resolution: "create-hash@npm:1.2.0" @@ -3641,6 +5201,13 @@ __metadata: languageName: node linkType: hard +"create-require@npm:^1.1.1": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: 10/a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -3652,6 +5219,26 @@ __metadata: languageName: node linkType: hard +"crypto-browserify@npm:^3.12.1": + version: 3.12.1 + resolution: "crypto-browserify@npm:3.12.1" + dependencies: + browserify-cipher: "npm:^1.0.1" + browserify-sign: "npm:^4.2.3" + create-ecdh: "npm:^4.0.4" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + diffie-hellman: "npm:^5.0.3" + hash-base: "npm:~3.0.4" + inherits: "npm:^2.0.4" + pbkdf2: "npm:^3.1.2" + public-encrypt: "npm:^4.0.3" + randombytes: "npm:^2.1.0" + randomfill: "npm:^1.0.4" + checksum: 10/13da0b5f61b3e8e68fcbebf0394f2b2b4d35a0d0ba6ab762720c13391d3697ea42735260a26328a6a3d872be7d4cb5abe98a7a8f88bc93da7ba59b993331b409 + languageName: node + linkType: hard + "crypto-random-string@npm:^4.0.0": version: 4.0.0 resolution: "crypto-random-string@npm:4.0.0" @@ -3682,6 +5269,13 @@ __metadata: languageName: node linkType: hard +"debounce@npm:^1.2.1": + version: 1.2.1 + resolution: "debounce@npm:1.2.1" + checksum: 10/0b95b2a9d80ed69117d890f8dab8c0f2d6066f8d20edd1d810ae51f8f366a6d4c8b1d56e97dcb9304e93d57de4d5db440d34a03def7dad50403fc3f22bf16808 + languageName: node + linkType: hard + "debug@npm:2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -3691,7 +5285,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.3": +"debug@npm:4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.3, debug@npm:~4.4.1": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -3712,6 +5306,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:~4.3.2": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a + languageName: node + linkType: hard + "debuglog@npm:^1.0.1": version: 1.0.1 resolution: "debuglog@npm:1.0.1" @@ -3748,6 +5354,15 @@ __metadata: languageName: node linkType: hard +"deep-eql@npm:4.1.4": + version: 4.1.4 + resolution: "deep-eql@npm:4.1.4" + dependencies: + type-detect: "npm:^4.0.0" + checksum: 10/f04f4d581f044a824a6322fe4f68fbee4d6780e93fc710cd9852cbc82bfc7010df00f0e05894b848abbe14dc3a25acac44f424e181ae64d12f2ab9d0a875a5ef + languageName: node + linkType: hard + "deep-equal@npm:~1.0.1": version: 1.0.1 resolution: "deep-equal@npm:1.0.1" @@ -3769,7 +5384,7 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.0.0, deepmerge@npm:^4.3.1": +"deepmerge@npm:^4.3.1": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10/058d9e1b0ff1a154468bf3837aea436abcfea1ba1d165ddaaf48ca93765fdd01a30d33c36173da8fbbed951dd0a267602bc782fe288b0fc4b7e1e7091afc4529 @@ -3783,13 +5398,13 @@ __metadata: languageName: node linkType: hard -"default-browser@npm:^5.2.1": - version: 5.2.1 - resolution: "default-browser@npm:5.2.1" +"default-browser@npm:^5.2.1, default-browser@npm:^5.4.0": + version: 5.5.0 + resolution: "default-browser@npm:5.5.0" dependencies: bundle-name: "npm:^4.1.0" default-browser-id: "npm:^5.0.0" - checksum: 10/afab7eff7b7f5f7a94d9114d1ec67273d3fbc539edf8c0f80019879d53aa71e867303c6f6d7cffeb10a6f3cfb59d4f963dba3f9c96830b4540cc7339a1bf9840 + checksum: 10/c5c5d84a4abd82850e98f06798a55dee87fc1064538bea00cc14c0fb2dccccbff5e9e07eeea80385fa653202d5d92509838b4239d610ddfa1c76a04a1f65e767 languageName: node linkType: hard @@ -3860,7 +5475,7 @@ __metadata: languageName: node linkType: hard -"destroy@npm:^1.0.4, destroy@npm:^1.2.0, destroy@npm:~1.2.0": +"destroy@npm:1.2.0, destroy@npm:^1.0.4, destroy@npm:^1.2.0, destroy@npm:~1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" checksum: 10/0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 @@ -3881,6 +5496,13 @@ __metadata: languageName: node linkType: hard +"detect-node@npm:^2.0.4": + version: 2.1.0 + resolution: "detect-node@npm:2.1.0" + checksum: 10/832184ec458353e41533ac9c622f16c19f7c02d8b10c303dfd3a756f56be93e903616c0bb2d4226183c9351c15fc0b3dba41a17a2308262afabcfa3776e6ae6e + languageName: node + linkType: hard + "dezalgo@npm:^1.0.0": version: 1.0.4 resolution: "dezalgo@npm:1.0.4" @@ -3902,6 +5524,33 @@ __metadata: languageName: node linkType: hard +"dns-packet@npm:^5.2.2": + version: 5.6.1 + resolution: "dns-packet@npm:5.6.1" + dependencies: + "@leichtgewicht/ip-codec": "npm:^2.0.1" + checksum: 10/ef5496dd5a906e22ed262cbe1a6f5d532c0893c4f1884a7aa37d4d0d8b8376a2b43f749aab087c8bb1354d67b40444f7fca8de4017b161a4cea468543061aed3 + languageName: node + linkType: hard + +"dom-serializer@npm:^2.0.0": + version: 2.0.0 + resolution: "dom-serializer@npm:2.0.0" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.2" + entities: "npm:^4.2.0" + checksum: 10/e3bf9027a64450bca0a72297ecdc1e3abb7a2912268a9f3f5d33a2e29c1e2c3502c6e9f860fc6625940bfe0cfb57a44953262b9e94df76872fdfb8151097eeb3 + languageName: node + linkType: hard + +"domain-browser@npm:4.22.0": + version: 4.22.0 + resolution: "domain-browser@npm:4.22.0" + checksum: 10/3ffbaf0cae8da717698d472ca85ab52f96c538fe1fe85e5eb3351d4e7af52423ce096b8a0c51bb318e1c9ccf9c2e94b3b0f68e5923ad0aa0c623a32b641ed11c + languageName: node + linkType: hard + "domain-browser@npm:^4.23.0": version: 4.23.0 resolution: "domain-browser@npm:4.23.0" @@ -3909,6 +5558,33 @@ __metadata: languageName: node linkType: hard +"domelementtype@npm:^2.3.0": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: 10/ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 + languageName: node + linkType: hard + +"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": + version: 5.0.3 + resolution: "domhandler@npm:5.0.3" + dependencies: + domelementtype: "npm:^2.3.0" + checksum: 10/809b805a50a9c6884a29f38aec0a4e1b4537f40e1c861950ed47d10b049febe6b79ab72adaeeebb3cc8fc1cd33f34e97048a72a9265103426d93efafa78d3e96 + languageName: node + linkType: hard + +"domutils@npm:^3.2.1": + version: 3.2.2 + resolution: "domutils@npm:3.2.2" + dependencies: + dom-serializer: "npm:^2.0.0" + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + checksum: 10/2e08842151aa406f50fe5e6d494f4ec73c2373199fa00d1f77b56ec604e566b7f226312ae35ab8160bb7f27a27c7285d574c8044779053e499282ca9198be210 + languageName: node + linkType: hard + "dot-prop@npm:^5.1.0": version: 5.3.0 resolution: "dot-prop@npm:5.3.0" @@ -3939,6 +5615,13 @@ __metadata: languageName: node linkType: hard +"duplexer@npm:^0.1.2": + version: 0.1.2 + resolution: "duplexer@npm:0.1.2" + checksum: 10/62ba61a830c56801db28ff6305c7d289b6dc9f859054e8c982abd8ee0b0a14d2e9a8e7d086ffee12e868d43e2bbe8a964be55ddbd8c8957714c87373c7a4f9b0 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -4005,10 +5688,25 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.73": - version: 1.5.91 - resolution: "electron-to-chromium@npm:1.5.91" - checksum: 10/0b2785042abccecf2a2074bfc5a7b843707835327096809834a62ff94268771c55e549dec10decfca5148f6f5ef9f8ed83671b1910670ecb349d99ec8e8f8769 +"electron-to-chromium@npm:^1.5.328": + version: 1.5.354 + resolution: "electron-to-chromium@npm:1.5.354" + checksum: 10/7da615ccaefcf07b8e99849bfbb7b96a0838634aa73d72873ecc146339cac94e6ad90cb6aed84beb22e20fe082559a58c8035caf5a3a8ebeac93c51a8bca2c67 + languageName: node + linkType: hard + +"elliptic@npm:^6.5.3, elliptic@npm:^6.6.1": + version: 6.6.1 + resolution: "elliptic@npm:6.6.1" + dependencies: + bn.js: "npm:^4.11.9" + brorand: "npm:^1.1.0" + hash.js: "npm:^1.0.0" + hmac-drbg: "npm:^1.0.1" + inherits: "npm:^2.0.4" + minimalistic-assert: "npm:^1.0.1" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10/dc678c9febd89a219c4008ba3a9abb82237be853d9fd171cd602c8fb5ec39927e65c6b5e7a1b2a4ea82ee8e0ded72275e7932bb2da04a5790c2638b818e4e1c5 languageName: node linkType: hard @@ -4047,14 +5745,14 @@ __metadata: languageName: node linkType: hard -"encodeurl@npm:^2.0.0": +"encodeurl@npm:^2.0.0, encodeurl@npm:~2.0.0": version: 2.0.0 resolution: "encodeurl@npm:2.0.0" checksum: 10/abf5cd51b78082cf8af7be6785813c33b6df2068ce5191a40ca8b1afe6a86f9230af9a9ce694a5ce4665955e5c1120871826df9c128a642e09c58d592e2807fe languageName: node linkType: hard -"encoding@npm:^0.1.13": +"encoding@npm:0.1.13, encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" dependencies: @@ -4072,6 +5770,31 @@ __metadata: languageName: node linkType: hard +"engine.io-parser@npm:~5.2.1": + version: 5.2.3 + resolution: "engine.io-parser@npm:5.2.3" + checksum: 10/eb0023fff5766e7ae9d59e52d92df53fea06d472cfd7b52e5d2c36b4c1dbf78cab5fde1052bcb3d4bb85bdb5aee10ae85d8a1c6c04676dac0c6cdf16bcba6380 + languageName: node + linkType: hard + +"engine.io@npm:~6.6.0": + version: 6.6.7 + resolution: "engine.io@npm:6.6.7" + dependencies: + "@types/cors": "npm:^2.8.12" + "@types/node": "npm:>=10.0.0" + "@types/ws": "npm:^8.5.12" + accepts: "npm:~1.3.4" + base64id: "npm:2.0.0" + cookie: "npm:~0.7.2" + cors: "npm:~2.8.5" + debug: "npm:~4.4.1" + engine.io-parser: "npm:~5.2.1" + ws: "npm:~8.18.3" + checksum: 10/58dd9adfd40667835e75bef8087e5e4a03bc61cbcc1eb542a6c9a5b4446c9c572779a1a46e399de50bf2091bd0663db6ae1eee1e4c471cddc62da84a862b1081 + languageName: node + linkType: hard + "enhanced-resolve@npm:^5.17.1": version: 5.18.0 resolution: "enhanced-resolve@npm:5.18.0" @@ -4082,10 +5805,17 @@ __metadata: languageName: node linkType: hard -"ensure-type@npm:^1.5.0": - version: 1.5.1 - resolution: "ensure-type@npm:1.5.1" - checksum: 10/a88b004b608aff24ac0474b3cbbc9a78b11106f314dcde233157a6b2022de45d43c86c03fb2edc50759426c0f852b9a354402a956b5e7dadc7af86866daef663 +"entities@npm:^4.2.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 10/ede2a35c9bce1aeccd055a1b445d41c75a14a2bb1cd22e242f20cf04d236cdcd7f9c859eb83f76885327bfae0c25bf03303665ee1ce3d47c5927b98b0e3e3d48 + languageName: node + linkType: hard + +"entities@npm:^6.0.0": + version: 6.0.1 + resolution: "entities@npm:6.0.1" + checksum: 10/62af1307202884349d2867f0aac5c60d8b57102ea0b0e768b16246099512c28e239254ad772d6834e7e14cb1b6f153fc3d0c031934e3183b086c86d3838d874a languageName: node linkType: hard @@ -4096,10 +5826,12 @@ __metadata: languageName: node linkType: hard -"eol@npm:^0.9.1": - version: 0.9.1 - resolution: "eol@npm:0.9.1" - checksum: 10/9d3fd93bb2bb5c69c7fe8dfb97b62213ed95857a2e90f5db3110415993e8a989d87fb011755ce22fdb92ca36fbe4e111b395a6f4ce00b9b51d3f00f19c2acf52 +"envinfo@npm:7.21.0": + version: 7.21.0 + resolution: "envinfo@npm:7.21.0" + bin: + envinfo: dist/cli.js + checksum: 10/2469a72802ded4e43c007dcd1c5dd44d8049b7d18276874dcc3f3f14a54bc72806fa35e82760974ca1442d82f5f9df3651048204e72791f81bcdd5f07422a561 languageName: node linkType: hard @@ -4228,6 +5960,18 @@ __metadata: languageName: node linkType: hard +"es-toolkit@npm:^1.45.1": + version: 1.46.1 + resolution: "es-toolkit@npm:1.46.1" + dependenciesMeta: + "@trivago/prettier-plugin-sort-imports@4.3.0": + unplugged: true + prettier-plugin-sort-re-exports@0.0.1: + unplugged: true + checksum: 10/15fa8e58848c3cf3f56b3fca6505362a7e19a6487613cd928197d11a12066010655ee47f74e5f412d949173f998df7ce7babcba9ff838bd40ce4ca79fca8f3c4 + languageName: node + linkType: hard + "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -4235,7 +5979,7 @@ __metadata: languageName: node linkType: hard -"escape-html@npm:^1.0.3": +"escape-html@npm:^1.0.3, escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" checksum: 10/6213ca9ae00d0ab8bccb6d8d4e0a98e76237b2410302cf7df70aaa6591d509a2a37ce8998008cbecae8fc8ffaadf3fb0229535e6a145f3ce0b211d060decbb24 @@ -4274,14 +6018,14 @@ __metadata: languageName: node linkType: hard -"eslint-config-prettier@npm:^10.0.1": - version: 10.0.1 - resolution: "eslint-config-prettier@npm:10.0.1" +"eslint-config-prettier@npm:^10.1.8": + version: 10.1.8 + resolution: "eslint-config-prettier@npm:10.1.8" peerDependencies: eslint: ">=7.0.0" bin: - eslint-config-prettier: build/bin/cli.js - checksum: 10/ba6875df0fc4fd3c7c6e2ec9c2e6a224462f7afc662f4cf849775c598a3571c1be136a9b683b12971653b3dcf3f31472aaede3076524b46ec9a77582630158e5 + eslint-config-prettier: bin/cli.js + checksum: 10/03f8e6ea1a6a9b8f9eeaf7c8c52a96499ec4b275b9ded33331a6cc738ed1d56de734097dbd0091f136f0e84bc197388bd8ec22a52a4658105883f8c8b7d8921a languageName: node linkType: hard @@ -4298,9 +6042,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:^28.11.0": - version: 28.11.0 - resolution: "eslint-plugin-jest@npm:28.11.0" +"eslint-plugin-jest@npm:^28.14.0": + version: 28.14.0 + resolution: "eslint-plugin-jest@npm:28.14.0" dependencies: "@typescript-eslint/utils": "npm:^6.0.0 || ^7.0.0 || ^8.0.0" peerDependencies: @@ -4312,45 +6056,46 @@ __metadata: optional: true jest: optional: true - checksum: 10/7f3896ec2dc03110688bb9f359a7aa1ba1a6d9a60ffbc3642361c4aaf55afcba9ce36b6609b20b1507028c2170ffe29b0f3e9cc9b7fe12fdd233740a2f9ce0a1 + checksum: 10/6032497bd97d6dd010450d5fdf535b8613a2789f4f83764ae04361c48d06d92f3d9b2e4350914b8fd857b6e611ba2b5282a1133ab8ec51b3e7053f9d336058e6 languageName: node linkType: hard -"eslint-plugin-n@npm:^17.15.1": - version: 17.15.1 - resolution: "eslint-plugin-n@npm:17.15.1" +"eslint-plugin-n@npm:^17.23.2": + version: 17.24.0 + resolution: "eslint-plugin-n@npm:17.24.0" dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.1" + "@eslint-community/eslint-utils": "npm:^4.5.0" enhanced-resolve: "npm:^5.17.1" eslint-plugin-es-x: "npm:^7.8.0" get-tsconfig: "npm:^4.8.1" globals: "npm:^15.11.0" + globrex: "npm:^0.1.2" ignore: "npm:^5.3.2" - minimatch: "npm:^9.0.5" semver: "npm:^7.6.3" + ts-declaration-location: "npm:^1.0.6" peerDependencies: eslint: ">=8.23.0" - checksum: 10/43fc161949fa0346ac7063a30580cd0db27e216b8e6a48d73d0bf4f10b88e9b65f263399843b3fe2087f766f264d16f0cbe8f2f898591516842201dc115a2d21 + checksum: 10/bbff1172f7297288d209f167febb3a31747838d5ed8050aa7d1aa2540a49b4f9932828831529f0306f2909e41ae3ae8848c145238a6990eae5a9d128a59b056c languageName: node linkType: hard -"eslint-plugin-prettier@npm:^5.2.3": - version: 5.2.3 - resolution: "eslint-plugin-prettier@npm:5.2.3" +"eslint-plugin-prettier@npm:^5.5.5": + version: 5.5.5 + resolution: "eslint-plugin-prettier@npm:5.5.5" dependencies: - prettier-linter-helpers: "npm:^1.0.0" - synckit: "npm:^0.9.1" + prettier-linter-helpers: "npm:^1.0.1" + synckit: "npm:^0.11.12" peerDependencies: "@types/eslint": ">=8.0.0" eslint: ">=8.0.0" - eslint-config-prettier: "*" + eslint-config-prettier: ">= 7.0.0 <10.0.0 || >=10.1.0" prettier: ">=3.0.0" peerDependenciesMeta: "@types/eslint": optional: true eslint-config-prettier: optional: true - checksum: 10/6444a0b89f3e2a6b38adce69761133f8539487d797f1655b3fa24f93a398be132c4f68f87041a14740b79202368d5782aa1dffd2bd7a3ea659f263d6796acf15 + checksum: 10/36c22c2fa2fd7c61ed292af1280e1d8f94dfe1671eacc5a503a249ca4b27fd226dbf6a1820457d611915926946f42729488d2dc7a5c320601e6cf1fad0d28f66 languageName: node linkType: hard @@ -4371,30 +6116,37 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.2.0, eslint-visitor-keys@npm:^4.2.1": +"eslint-visitor-keys@npm:^4.2.1": version: 4.2.1 resolution: "eslint-visitor-keys@npm:4.2.1" checksum: 10/3ee00fc6a7002d4b0ffd9dc99e13a6a7882c557329e6c25ab254220d71e5c9c4f89dca4695352949ea678eb1f3ba912a18ef8aac0a7fe094196fd92f441bfce2 languageName: node linkType: hard -"eslint@npm:^9.39.2": - version: 9.39.2 - resolution: "eslint@npm:9.39.2" +"eslint-visitor-keys@npm:^5.0.0": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: 10/f9cc1a57b75e0ef949545cac33d01e8367e302de4c1483266ed4d8646ee5c306376660196bbb38b004e767b7043d1e661cb4336b49eff634a1bbe75c1db709ec + languageName: node + linkType: hard + +"eslint@npm:^9.39.4": + version: 9.39.4 + resolution: "eslint@npm:9.39.4" dependencies: "@eslint-community/eslint-utils": "npm:^4.8.0" "@eslint-community/regexpp": "npm:^4.12.1" - "@eslint/config-array": "npm:^0.21.1" + "@eslint/config-array": "npm:^0.21.2" "@eslint/config-helpers": "npm:^0.4.2" "@eslint/core": "npm:^0.17.0" - "@eslint/eslintrc": "npm:^3.3.1" - "@eslint/js": "npm:9.39.2" + "@eslint/eslintrc": "npm:^3.3.5" + "@eslint/js": "npm:9.39.4" "@eslint/plugin-kit": "npm:^0.4.1" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" "@humanwhocodes/retry": "npm:^0.4.2" "@types/estree": "npm:^1.0.6" - ajv: "npm:^6.12.4" + ajv: "npm:^6.14.0" chalk: "npm:^4.0.0" cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.2" @@ -4413,7 +6165,7 @@ __metadata: is-glob: "npm:^4.0.0" json-stable-stringify-without-jsonify: "npm:^1.0.1" lodash.merge: "npm:^4.6.2" - minimatch: "npm:^3.1.2" + minimatch: "npm:^3.1.5" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" peerDependencies: @@ -4423,7 +6175,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10/53ff0e9c8264e7e8d40d50fdc0c0df0b701cfc5289beedfb686c214e3e7b199702f894bbd1bb48653727bb1ecbd1147cf5f555a4ae71e1daf35020cdc9072d9f + checksum: 10/de95093d710e62e8c7e753220d985587c40f4a05247ed4393ffb6e6cb43a60e825a47fc5b4263151bb2fc5871a206a31d563ccbabdceee1711072ae12db90adf languageName: node linkType: hard @@ -4438,16 +6190,6 @@ __metadata: languageName: node linkType: hard -"esprima-next@npm:^5.7.0": - version: 5.8.4 - resolution: "esprima-next@npm:5.8.4" - bin: - esparse: bin/esparse.js - esvalidate: bin/esvalidate.js - checksum: 10/3efd0523a3db5c02ef109421931774daaa0e3439c820f6ed617a7253bba90b47b6691607d764d285d8b33cc631ebbfdfe33853150301177911667adb6bab52ea - languageName: node - linkType: hard - "esprima@npm:^4.0.0": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -4490,6 +6232,13 @@ __metadata: languageName: node linkType: hard +"etag@npm:~1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 10/571aeb3dbe0f2bbd4e4fadbdb44f325fc75335cd5f6f6b6a091e6a06a9f25ed5392f0863c5442acb0646787446e816f13cbfc6edce5b07658541dff573cab1ff + languageName: node + linkType: hard + "event-target-shim@npm:^5.0.0": version: 5.0.1 resolution: "event-target-shim@npm:5.0.1" @@ -4497,14 +6246,14 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^4.0.4": +"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.4": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" checksum: 10/8030029382404942c01d0037079f1b1bc8fed524b5849c237b80549b01e2fc49709e1d0c557fa65ca4498fc9e24cff1475ef7b855121fcc15f9d61f93e282346 languageName: node linkType: hard -"events@npm:^3.3.0": +"events@npm:^3.0.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" checksum: 10/a3d47e285e28d324d7180f1e493961a2bbb4cad6412090e4dec114f4db1f5b560c7696ee8e758f55e23913ede856e3689cd3aa9ae13c56b5d8314cd3b3ddd1be @@ -4539,6 +6288,33 @@ __metadata: languageName: node linkType: hard +"execa@npm:^9.6.1": + version: 9.6.1 + resolution: "execa@npm:9.6.1" + dependencies: + "@sindresorhus/merge-streams": "npm:^4.0.0" + cross-spawn: "npm:^7.0.6" + figures: "npm:^6.1.0" + get-stream: "npm:^9.0.0" + human-signals: "npm:^8.0.1" + is-plain-obj: "npm:^4.1.0" + is-stream: "npm:^4.0.1" + npm-run-path: "npm:^6.0.0" + pretty-ms: "npm:^9.2.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^4.0.0" + yoctocolors: "npm:^2.1.1" + checksum: 10/d0f7a2185152379f8772f6d780b188f2728a95b9a68d1a897f58805d7ba6bd55eaa5e128cb66a274251a6b5e4d9388332b1417bd7d46c25e020e4e55725cf79e + languageName: node + linkType: hard + +"exit-hook@npm:^4.0.0": + version: 4.0.0 + resolution: "exit-hook@npm:4.0.0" + checksum: 10/5aa8b4e45fa943e7e174c25329750a0ffefb593ccc2eafd5d67e1d734b114c93cb36b5714548fb1c2a1dd90f3e9cdc606b5e788f428f780708774da444021fdc + languageName: node + linkType: hard + "exit-x@npm:^0.2.2": version: 0.2.2 resolution: "exit-x@npm:0.2.2" @@ -4546,17 +6322,17 @@ __metadata: languageName: node linkType: hard -"expect@npm:30.2.0, expect@npm:^30.0.0": - version: 30.2.0 - resolution: "expect@npm:30.2.0" +"expect@npm:30.3.0, expect@npm:^30.0.0": + version: 30.3.0 + resolution: "expect@npm:30.3.0" dependencies: - "@jest/expect-utils": "npm:30.2.0" + "@jest/expect-utils": "npm:30.3.0" "@jest/get-type": "npm:30.1.0" - jest-matcher-utils: "npm:30.2.0" - jest-message-util: "npm:30.2.0" - jest-mock: "npm:30.2.0" - jest-util: "npm:30.2.0" - checksum: 10/cf98ab45ab2e9f2fb9943a3ae0097f72d63a94be179a19fd2818d8fdc3b7681d31cc8ef540606eb8dd967d9c44d73fef263a614e9de260c22943ffb122ad66fd + jest-matcher-utils: "npm:30.3.0" + jest-message-util: "npm:30.3.0" + jest-mock: "npm:30.3.0" + jest-util: "npm:30.3.0" + checksum: 10/607748963fd2cf2b95ec848d59086afdff5e6b690d1ddd907f84514687f32a787896281ba49a5fda2af819238bec7fdeaf258814997d2b08eedc0968de57f3bd languageName: node linkType: hard @@ -4567,6 +6343,45 @@ __metadata: languageName: node linkType: hard +"express@npm:^4.21.2": + version: 4.22.2 + resolution: "express@npm:4.22.2" + dependencies: + accepts: "npm:~1.3.8" + array-flatten: "npm:1.1.1" + body-parser: "npm:~1.20.5" + content-disposition: "npm:~0.5.4" + content-type: "npm:~1.0.4" + cookie: "npm:~0.7.1" + cookie-signature: "npm:~1.0.6" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + finalhandler: "npm:~1.3.1" + fresh: "npm:~0.5.2" + http-errors: "npm:~2.0.0" + merge-descriptors: "npm:1.0.3" + methods: "npm:~1.1.2" + on-finished: "npm:~2.4.1" + parseurl: "npm:~1.3.3" + path-to-regexp: "npm:~0.1.12" + proxy-addr: "npm:~2.0.7" + qs: "npm:~6.15.1" + range-parser: "npm:~1.2.1" + safe-buffer: "npm:5.2.1" + send: "npm:~0.19.0" + serve-static: "npm:~1.16.2" + setprototypeof: "npm:1.2.0" + statuses: "npm:~2.0.1" + type-is: "npm:~1.6.18" + utils-merge: "npm:1.0.1" + vary: "npm:~1.1.2" + checksum: 10/defeef5f1cda5bf5ee4a6b35252563552d8d97fab1d29f502e45ef64316f5d02f49739d7c8a79e425613af3a542d0e3cd8cf7e85671ce2ddd592197abcf5acc3 + languageName: node + linkType: hard + "fast-clone@npm:^1.5.13": version: 1.5.13 resolution: "fast-clone@npm:1.5.13" @@ -4588,26 +6403,6 @@ __metadata: languageName: node linkType: hard -"fast-fifo@npm:^1.1.0": - version: 1.3.2 - resolution: "fast-fifo@npm:1.3.2" - checksum: 10/6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 - languageName: node - linkType: hard - -"fast-glob@npm:^3.3.2": - version: 3.3.3 - resolution: "fast-glob@npm:3.3.3" - dependencies: - "@nodelib/fs.stat": "npm:^2.0.2" - "@nodelib/fs.walk": "npm:^1.2.3" - glob-parent: "npm:^5.1.2" - merge2: "npm:^1.3.0" - micromatch: "npm:^4.0.8" - checksum: 10/dcc6432b269762dd47381d8b8358bf964d8f4f60286ac6aa41c01ade70bda459ff2001b516690b96d5365f68a49242966112b5d5cc9cd82395fa8f9d017c90ad - languageName: node - linkType: hard - "fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -4645,6 +6440,38 @@ __metadata: languageName: node linkType: hard +"fast-string-truncated-width@npm:^3.0.2": + version: 3.0.3 + resolution: "fast-string-truncated-width@npm:3.0.3" + checksum: 10/3a1631e48927cb558b612a90ee78a61a660823c39b024bfc113935760b5b64805dbf03c4e696c33005294db578417687432e9d13567f1a582c2c75015e8a7648 + languageName: node + linkType: hard + +"fast-string-width@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-string-width@npm:3.0.2" + dependencies: + fast-string-truncated-width: "npm:^3.0.2" + checksum: 10/5b9019769f2b00b96d43575c202f4e035a0e55eba7669a9a32351de9fa0805d0959a2afcaec6e4db5ee9b9a4c08d8e77f95abeb04b5bae2f76635cf04ddb4b80 + languageName: node + linkType: hard + +"fast-uri@npm:^3.0.1": + version: 3.1.2 + resolution: "fast-uri@npm:3.1.2" + checksum: 10/1dff04865b2a38d3e0659deadfbf72efdf83a776bfbf9667e4aa9e5a3ec31bc341cda9622136b32b7652a857c8ba11896794186e8f876f8b2b72731fce8622f6 + languageName: node + linkType: hard + +"fast-wrap-ansi@npm:^0.2.0": + version: 0.2.0 + resolution: "fast-wrap-ansi@npm:0.2.0" + dependencies: + fast-string-width: "npm:^3.0.2" + checksum: 10/e717a249dae84c9a964e6b5da05c373fadd92714b2afb2d6c7e6f766c3409c773c95b28e186dcdd397e2d7850533dbdd766845d0cd29e15d172d33128f9447d3 + languageName: node + linkType: hard + "fast-xml-builder@npm:^1.1.4": version: 1.1.4 resolution: "fast-xml-builder@npm:1.1.4" @@ -4654,25 +6481,25 @@ __metadata: languageName: node linkType: hard -"fast-xml-parser@npm:^5.2.5": - version: 5.5.8 - resolution: "fast-xml-parser@npm:5.5.8" +"fast-xml-parser@npm:^5.5.6": + version: 5.5.12 + resolution: "fast-xml-parser@npm:5.5.12" dependencies: fast-xml-builder: "npm:^1.1.4" - path-expression-matcher: "npm:^1.2.0" - strnum: "npm:^2.2.0" + path-expression-matcher: "npm:^1.5.0" + strnum: "npm:^2.2.3" bin: fxparser: src/cli/cli.js - checksum: 10/888f9a5d345e65e34b70d394798a1542603a216f06c140a9671d031b80b42c01ef2e68f2a0ceea45e7703fa80549f0e06da710f5a2faafdc910d1b6b354f0fa0 + checksum: 10/302eb610c1d991721757ca20a9d113873e6288f19d1de73fbebd9925990830abc1cc8fb1e38bfb7be7841764f6455cafff19ce696c790ce82cc944a68868d37d languageName: node linkType: hard -"fastq@npm:^1.13.0, fastq@npm:^1.6.0": - version: 1.15.0 - resolution: "fastq@npm:1.15.0" +"faye-websocket@npm:^0.11.3": + version: 0.11.4 + resolution: "faye-websocket@npm:0.11.4" dependencies: - reusify: "npm:^1.0.4" - checksum: 10/67c01b1c972e2d5b6fea197a1a39d5d582982aea69ff4c504badac71080d8396d4843b165a9686e907c233048f15a86bbccb0e7f83ba771f6fa24bcde059d0c3 + websocket-driver: "npm:>=0.5.1" + checksum: 10/22433c14c60925e424332d2794463a8da1c04848539b5f8db5fced62a7a7c71a25335a4a8b37334e3a32318835e2b87b1733d008561964121c4a0bd55f0878c3 languageName: node linkType: hard @@ -4713,6 +6540,15 @@ __metadata: languageName: node linkType: hard +"figures@npm:^6.1.0": + version: 6.1.0 + resolution: "figures@npm:6.1.0" + dependencies: + is-unicode-supported: "npm:^2.0.0" + checksum: 10/9822d13630bee8e6a9f2da866713adf13854b07e0bfde042defa8bba32d47a1c0b2afa627ce73837c674cf9a5e3edce7e879ea72cb9ea7960b2390432d8e1167 + languageName: node + linkType: hard + "file-entry-cache@npm:^8.0.0": version: 8.0.0 resolution: "file-entry-cache@npm:8.0.0" @@ -4722,14 +6558,22 @@ __metadata: languageName: node linkType: hard -"file-type@npm:^18.7.0": - version: 18.7.0 - resolution: "file-type@npm:18.7.0" +"file-type@npm:^21.3.4": + version: 21.3.4 + resolution: "file-type@npm:21.3.4" dependencies: - readable-web-to-node-stream: "npm:^3.0.2" - strtok3: "npm:^7.0.0" - token-types: "npm:^5.0.1" - checksum: 10/95b70313d697484bb9613dd822a29554e9754b49f4d62f17e399649c981a12556776b4ee83b0a62b752fc9048ac79f6cf79ad13b2a750d89afa170902c7b0029 + "@tokenizer/inflate": "npm:^0.4.1" + strtok3: "npm:^10.3.4" + token-types: "npm:^6.1.1" + uint8array-extras: "npm:^1.4.0" + checksum: 10/42d5cf6aafb998fb2f0357e96ea7c48bcce5249f899523a3a5a4c297f4fe2346cc0aff9cf04243ada5c54b6457183550b2a04902b6533cd5e64aaa344d78e0eb + languageName: node + linkType: hard + +"filesize@npm:^10.1.6": + version: 10.1.6 + resolution: "filesize@npm:10.1.6" + checksum: 10/e800837c4fc02303f1944d5a4c7b706df1c5cd95d745181852604fb00a1c2d55d2d3921252722bd2f0c86b59c94edaba23fa224776bbf977455d4034e7be1f45 languageName: node linkType: hard @@ -4742,6 +6586,21 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:~1.3.1": + version: 1.3.2 + resolution: "finalhandler@npm:1.3.2" + dependencies: + debug: "npm:2.6.9" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + on-finished: "npm:~2.4.1" + parseurl: "npm:~1.3.3" + statuses: "npm:~2.0.2" + unpipe: "npm:~1.0.0" + checksum: 10/6cb4f9f80eaeb5a0fac4fdbd27a65d39271f040a0034df16556d896bfd855fd42f09da886781b3102117ea8fceba97b903c1f8b08df1fb5740576d5e0f481eed + languageName: node + linkType: hard + "find-up-simple@npm:^1.0.0": version: 1.0.0 resolution: "find-up-simple@npm:1.0.0" @@ -4797,6 +6656,15 @@ __metadata: languageName: node linkType: hard +"flat@npm:^5.0.2": + version: 5.0.2 + resolution: "flat@npm:5.0.2" + bin: + flat: cli.js + checksum: 10/72479e651c15eab53e25ce04c31bab18cfaac0556505cac19221dbbe85bbb9686bc76e4d397e89e5bf516ce667dcf818f8b07e585568edba55abc2bf1f698fb5 + languageName: node + linkType: hard + "flatted@npm:^3.2.9": version: 3.4.2 resolution: "flatted@npm:3.4.2" @@ -4811,13 +6679,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.11": - version: 1.15.11 - resolution: "follow-redirects@npm:1.15.11" +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.11": + version: 1.16.0 + resolution: "follow-redirects@npm:1.16.0" peerDependenciesMeta: debug: optional: true - checksum: 10/07372fd74b98c78cf4d417d68d41fdaa0be4dcacafffb9e67b1e3cf090bc4771515e65020651528faab238f10f9b9c0d9707d6c1574a6c0387c5de1042cde9ba + checksum: 10/3fbe3d80b3b544c22705d837aa5d4a0d07a740d913534a2620b0a004c610af4148e3b58723536dd099aaa1c9d3a155964bde9665d6e5cb331460809a1fc572fd languageName: node linkType: hard @@ -4860,6 +6728,13 @@ __metadata: languageName: node linkType: hard +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: 10/29ba9fd347117144e97cbb8852baae5e8b2acb7d1b591ef85695ed96f5b933b1804a7fac4a15dd09ca7ac7d0cdc104410e8102aae2dd3faa570a797ba07adb81 + languageName: node + linkType: hard + "fresh@npm:^0.5.2, fresh@npm:~0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -4867,6 +6742,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.1.1": + version: 11.3.5 + resolution: "fs-extra@npm:11.3.5" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/dc8408818eec8b03efad8742d079ecab749a2f7bc9f208e429b447fcac7632fae52e09312d6d42218efe7e2efa97f03ff232d639ade4aa7fcd8c00ebe9ad0c0c + languageName: node + linkType: hard + "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -4876,16 +6762,6 @@ __metadata: languageName: node linkType: hard -"fs-mkdirp-stream@npm:^2.0.1": - version: 2.0.1 - resolution: "fs-mkdirp-stream@npm:2.0.1" - dependencies: - graceful-fs: "npm:^4.2.8" - streamx: "npm:^2.12.0" - checksum: 10/9fefd9fa3d6985aea0935944288bd20215779f683ec3af3c157cf4d4d4b0c546caae8219219f47a05a1df3b23f6a605fe64bee6ee14e550f1a670db67359ff27 - languageName: node - linkType: hard - "fs.realpath@npm:^1.0.0": version: 1.0.0 resolution: "fs.realpath@npm:1.0.0" @@ -4893,7 +6769,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.3": +"fsevents@npm:^2.3.3, fsevents@npm:~2.3.2": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -4903,7 +6779,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin": +"fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -4959,6 +6835,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.5.0": + version: 1.5.0 + resolution: "get-east-asian-width@npm:1.5.0" + checksum: 10/60bc34cd1e975055ab99f0f177e31bed3e516ff7cee9c536474383954a976abaa6b94a51d99ad158ef1e372790fa096cab7d07f166bb0778f6587954c0fbe946 + languageName: node + linkType: hard + "get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": version: 1.3.1 resolution: "get-intrinsic@npm:1.3.1" @@ -5001,6 +6884,13 @@ __metadata: languageName: node linkType: hard +"get-port@npm:5.1.1": + version: 5.1.1 + resolution: "get-port@npm:5.1.1" + checksum: 10/0162663ffe5c09e748cd79d97b74cd70e5a5c84b760a475ce5767b357fb2a57cb821cee412d646aa8a156ed39b78aab88974eddaa9e5ee926173c036c0713787 + languageName: node + linkType: hard + "get-proto@npm:^1.0.1": version: 1.0.1 resolution: "get-proto@npm:1.0.1" @@ -5011,13 +6901,6 @@ __metadata: languageName: node linkType: hard -"get-stdin@npm:^9.0.0": - version: 9.0.0 - resolution: "get-stdin@npm:9.0.0" - checksum: 10/5972bc34d05932b45512c8e2d67b040f1c1ca8afb95c56cbc480985f2d761b7e37fe90dc8abd22527f062cc5639a6930ff346e9952ae4c11a2d4275869459594 - languageName: node - linkType: hard - "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -5025,6 +6908,16 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^9.0.0": + version: 9.0.1 + resolution: "get-stream@npm:9.0.1" + dependencies: + "@sec-ant/readable-stream": "npm:^0.4.1" + is-stream: "npm:^4.0.1" + checksum: 10/ce56e6db6bcd29ca9027b0546af035c3e93dcd154ca456b54c298901eb0e5b2ce799c5d727341a100c99e14c523f267f1205f46f153f7b75b1f4da6d98a21c5e + languageName: node + linkType: hard + "get-symbol-description@npm:^1.0.0": version: 1.0.0 resolution: "get-symbol-description@npm:1.0.0" @@ -5044,15 +6937,26 @@ __metadata: languageName: node linkType: hard -"gettext-parser@npm:^4.0.3": - version: 4.2.0 - resolution: "gettext-parser@npm:4.2.0" +"gettext-converter@npm:^1.3.0": + version: 1.3.1 + resolution: "gettext-converter@npm:1.3.1" dependencies: - content-type: "npm:^1.0.4" + arrify: "npm:^2.0.1" + content-type: "npm:1.0.5" + encoding: "npm:0.1.13" + checksum: 10/b5666122cf88805eb43a460e08ba78db7d431088df482e2fc0b90c6d051e823b613fc72756ccd1e7915b9d3d365a368a27d831f11bd15060d1d671b913aa92da + languageName: node + linkType: hard + +"gettext-parser@npm:^8.0.0": + version: 8.0.0 + resolution: "gettext-parser@npm:8.0.0" + dependencies: + content-type: "npm:^1.0.5" encoding: "npm:^0.1.13" - readable-stream: "npm:^3.6.0" + readable-stream: "npm:^4.5.2" safe-buffer: "npm:^5.2.1" - checksum: 10/44f97cc4216da7e23f344b947048bb46d7349e1127f01cd976e5250249588f966d1cbe3b59f5a228f0650259140c90cc41e1d858815f7913c77f887689611fa6 + checksum: 10/e22f42241f9fea3d8379879792a97e705b0bb31539246ee1bec024f247a6e8940c4feb66b24ab6412fa0cd778263c32a1d055992f3a2bc6983d5d191712c128f languageName: node linkType: hard @@ -5100,15 +7004,6 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2": - version: 5.1.2 - resolution: "glob-parent@npm:5.1.2" - dependencies: - is-glob: "npm:^4.0.1" - checksum: 10/32cd106ce8c0d83731966d31517adb766d02c3812de49c30cfe0675c7c0ae6630c11214c54a5ae67aca882cf738d27fd7768f21aa19118b9245950554be07247 - languageName: node - linkType: hard - "glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" @@ -5118,23 +7013,25 @@ __metadata: languageName: node linkType: hard -"glob-stream@npm:^8.0.0": - version: 8.0.0 - resolution: "glob-stream@npm:8.0.0" - dependencies: - "@gulpjs/to-absolute-glob": "npm:^4.0.0" - anymatch: "npm:^3.1.3" - fastq: "npm:^1.13.0" - glob-parent: "npm:^6.0.2" - is-glob: "npm:^4.0.3" - is-negated-glob: "npm:^1.0.0" - normalize-path: "npm:^3.0.0" - streamx: "npm:^2.12.5" - checksum: 10/b1d18b6fd49086ff02e031f03e3debac747047d304b349a6dced3b7944c665344ef63496363f483acc7c6afcd6ebfb11af1652824f2c370d83c0f3905d5c67e0 +"glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: 10/32cd106ce8c0d83731966d31517adb766d02c3812de49c30cfe0675c7c0ae6630c11214c54a5ae67aca882cf738d27fd7768f21aa19118b9245950554be07247 + languageName: node + linkType: hard + +"glob-to-regex.js@npm:^1.0.0, glob-to-regex.js@npm:^1.0.1": + version: 1.2.0 + resolution: "glob-to-regex.js@npm:1.2.0" + peerDependencies: + tslib: 2 + checksum: 10/13034e642db479d75448bdd9f37de7451bef2879c394bfe3f8df6588e0479893e94059eaee77cdf50dce675607fb2395c132dcca0c9a559a6192e89b2ad0f134 languageName: node linkType: hard -"glob@npm:^10.3.10": +"glob@npm:^10.5.0": version: 10.5.0 resolution: "glob@npm:10.5.0" dependencies: @@ -5150,14 +7047,14 @@ __metadata: languageName: node linkType: hard -"glob@npm:^13.0.0, glob@npm:^13.0.1": - version: 13.0.1 - resolution: "glob@npm:13.0.1" +"glob@npm:^13.0.0, glob@npm:^13.0.6": + version: 13.0.6 + resolution: "glob@npm:13.0.6" dependencies: - minimatch: "npm:^10.1.2" - minipass: "npm:^7.1.2" - path-scurry: "npm:^2.0.0" - checksum: 10/465e8cc269ab88d7415a3906cdc0f4543a2ae54df99207204af5bc28a944396d8d893822f546a8056a78ec714e608ab4f3502532c4d6b9cc5e113adf0fe5109e + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214 languageName: node linkType: hard @@ -5198,6 +7095,13 @@ __metadata: languageName: node linkType: hard +"globrex@npm:^0.1.2": + version: 0.1.2 + resolution: "globrex@npm:0.1.2" + checksum: 10/81ce62ee6f800d823d6b7da7687f841676d60ee8f51f934ddd862e4057316d26665c4edc0358d4340a923ac00a514f8b67c787e28fe693aae16350f4e60d55e9 + languageName: node + linkType: hard + "gopd@npm:^1.0.1, gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -5205,30 +7109,30 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.8": +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 languageName: node linkType: hard -"graphemer@npm:^1.4.0": - version: 1.4.0 - resolution: "graphemer@npm:1.4.0" - checksum: 10/6dd60dba97007b21e3a829fab3f771803cc1292977fe610e240ea72afd67e5690ac9eeaafc4a99710e78962e5936ab5a460787c2a1180f1cb0ccfac37d29f897 +"gzip-size@npm:^6.0.0": + version: 6.0.0 + resolution: "gzip-size@npm:6.0.0" + dependencies: + duplexer: "npm:^0.1.2" + checksum: 10/2df97f359696ad154fc171dcb55bc883fe6e833bca7a65e457b9358f3cb6312405ed70a8da24a77c1baac0639906cd52358dc0ce2ec1a937eaa631b934c94194 languageName: node linkType: hard -"gulp-sort@npm:^2.0.0": - version: 2.0.0 - resolution: "gulp-sort@npm:2.0.0" - dependencies: - through2: "npm:^2.0.1" - checksum: 10/8645d80b26990290e8623ccf38420e319a4ea67b64ac3e4f2b5de6c20b9006973001fd989dfe0fe3b2560044d3dfbc005685126826536e01718a2628aa45d0c5 +"handle-thing@npm:^2.0.0": + version: 2.0.1 + resolution: "handle-thing@npm:2.0.1" + checksum: 10/441ec98b07f26819c70c702f6c874088eebeb551b242fe8fae4eab325746b82bf84ae7a1f6419547698accb3941fa26806c5f5f93c50e19f90e499065a711d61 languageName: node linkType: hard -"handlebars@npm:^4.7.7, handlebars@npm:^4.7.8": +"handlebars@npm:^4.7.7, handlebars@npm:^4.7.9": version: 4.7.9 resolution: "handlebars@npm:4.7.9" dependencies: @@ -5347,12 +7251,12 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.2": - version: 2.0.2 - resolution: "hasown@npm:2.0.2" +"hasown@npm:^2.0.2, hasown@npm:^2.0.3": + version: 2.0.3 + resolution: "hasown@npm:2.0.3" dependencies: function-bind: "npm:^1.1.2" - checksum: 10/7898a9c1788b2862cf0f9c345a6bec77ba4a0c0983c7f19d610c382343d4f98fa260686b225dfb1f88393a66679d2ec58ee310c1d6868c081eda7918f32cc70a + checksum: 10/619526379cda755409d856cbf3c65b82ea342151719a0a550920cf7d6a7f58f7cf079e5a78f3acd162324fc784a3d3d6f6f61aff613b47a0163c16fbe09ea89f languageName: node linkType: hard @@ -5392,13 +7296,46 @@ __metadata: languageName: node linkType: hard -"html-escaper@npm:^2.0.0": +"hpack.js@npm:^2.1.6": + version: 2.1.6 + resolution: "hpack.js@npm:2.1.6" + dependencies: + inherits: "npm:^2.0.1" + obuf: "npm:^1.0.0" + readable-stream: "npm:^2.0.1" + wbuf: "npm:^1.1.0" + checksum: 10/6910e4b9d943a78fd8e84ac42729fdab9bd406789d6204ad160af9dc5aa4750fc01f208249bf7116c11dc0678207a387b4ade24e4b628b95385b251ceeeb719c + languageName: node + linkType: hard + +"html-escaper@npm:^2.0.0, html-escaper@npm:^2.0.2": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" checksum: 10/034d74029dcca544a34fb6135e98d427acd73019796ffc17383eaa3ec2fe1c0471dcbbc8f8ed39e46e86d43ccd753a160631615e4048285e313569609b66d5b7 languageName: node linkType: hard +"html-parse-stringify@npm:^3.0.1": + version: 3.0.1 + resolution: "html-parse-stringify@npm:3.0.1" + dependencies: + void-elements: "npm:3.1.0" + checksum: 10/8743b76cc50e46d1956c1ad879d18eb9613b0d2d81e24686d633f9f69bb26b84676f64a926973de793cca479997017a63219278476d617b6c42d68246d7c07fe + languageName: node + linkType: hard + +"htmlparser2@npm:10.0.0": + version: 10.0.0 + resolution: "htmlparser2@npm:10.0.0" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.2.1" + entities: "npm:^6.0.0" + checksum: 10/768870f0e020dca19dc45df206cb6ac466c5dba6566c8fca4ca880347eed409f9977028d08644ac516bca8628ac9c7ded5a3847dc3ee1c043f049abf9e817154 + languageName: node + linkType: hard + "http-assert@npm:^1.3.0, http-assert@npm:^1.5.0": version: 1.5.0 resolution: "http-assert@npm:1.5.0" @@ -5416,6 +7353,13 @@ __metadata: languageName: node linkType: hard +"http-deceiver@npm:^1.2.7": + version: 1.2.7 + resolution: "http-deceiver@npm:1.2.7" + checksum: 10/9ae293b0acbfad6ed45d52c1f85f58ab062465872fd9079c80d78c6527634002d73c2a9d8c0296cc12d178a0b689bb5291d9979aad3ce71ab17a7517588adbf7 + languageName: node + linkType: hard + "http-errors@npm:^1.6.3, http-errors@npm:^1.7.3, http-errors@npm:~1.8.0": version: 1.8.1 resolution: "http-errors@npm:1.8.1" @@ -5429,7 +7373,7 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:^2.0.0, http-errors@npm:^2.0.1, http-errors@npm:~2.0.1": +"http-errors@npm:^2.0.0, http-errors@npm:^2.0.1, http-errors@npm:~2.0.0, http-errors@npm:~2.0.1": version: 2.0.1 resolution: "http-errors@npm:2.0.1" dependencies: @@ -5463,6 +7407,13 @@ __metadata: languageName: node linkType: hard +"http-parser-js@npm:>=0.5.1": + version: 0.5.10 + resolution: "http-parser-js@npm:0.5.10" + checksum: 10/33c53b458cfdf7e43f1517f9bcb6bed1c614b1c7c5d65581a84304110eb9eb02a48f998c7504b8bee432ef4a8ec9318e7009406b506b28b5610fed516242b20a + languageName: node + linkType: hard + "http-proxy-agent@npm:^7.0.0": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" @@ -5473,6 +7424,35 @@ __metadata: languageName: node linkType: hard +"http-proxy-middleware@npm:^2.0.9": + version: 2.0.9 + resolution: "http-proxy-middleware@npm:2.0.9" + dependencies: + "@types/http-proxy": "npm:^1.17.8" + http-proxy: "npm:^1.18.1" + is-glob: "npm:^4.0.1" + is-plain-obj: "npm:^3.0.0" + micromatch: "npm:^4.0.2" + peerDependencies: + "@types/express": ^4.17.13 + peerDependenciesMeta: + "@types/express": + optional: true + checksum: 10/4ece416a91d52e96f8136c5f4abfbf7ac2f39becbad21fa8b158a12d7e7d8f808287ff1ae342b903fd1f15f2249dee87fabc09e1f0e73106b83331c496d67660 + languageName: node + linkType: hard + +"http-proxy@npm:^1.18.1": + version: 1.18.1 + resolution: "http-proxy@npm:1.18.1" + dependencies: + eventemitter3: "npm:^4.0.0" + follow-redirects: "npm:^1.0.0" + requires-port: "npm:^1.0.0" + checksum: 10/2489e98aba70adbfd8b9d41ed1ff43528be4598c88616c558b109a09eaffe4bb35e551b6c75ac42ed7d948bb7530a22a2be6ef4f0cecacb5927be139f4274594 + languageName: node + linkType: hard + "https-browserify@npm:^1.0.0": version: 1.0.0 resolution: "https-browserify@npm:1.0.0" @@ -5497,6 +7477,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^8.0.1": + version: 8.0.1 + resolution: "human-signals@npm:8.0.1" + checksum: 10/903389a018b16f330c5e0f6e8b76d592c79552152ea892f249e5290e71c790f5722dc9b740fedd4bdef30566754a69012aaed97a6a528da0d417fad990a6f515 + languageName: node + linkType: hard + "humanize-ms@npm:^1.2.1": version: 1.2.1 resolution: "humanize-ms@npm:1.2.1" @@ -5506,70 +7493,82 @@ __metadata: languageName: node linkType: hard -"i18next-conv@npm:^10.2.0": - version: 10.2.0 - resolution: "i18next-conv@npm:10.2.0" - dependencies: - arrify: "npm:^2.0.1" - chalk: "npm:^4.0.0" - commander: "npm:^5.1.0" - gettext-parser: "npm:^4.0.3" - mkdirp: "npm:^1.0.4" - node-gettext: "npm:^3.0.0" +"hyperdyperid@npm:^1.2.0": + version: 1.2.0 + resolution: "hyperdyperid@npm:1.2.0" + checksum: 10/64abb5568ff17aa08ac0175ae55e46e22831c5552be98acdd1692081db0209f36fff58b31432017b4e1772c178962676a2cc3c54e4d5d7f020d7710cec7ad7a6 + languageName: node + linkType: hard + +"i18next-cli@npm:^1.56.7": + version: 1.56.7 + resolution: "i18next-cli@npm:1.56.7" + dependencies: + "@croct/json5-parser": "npm:^0.2.2" + "@swc/core": "npm:^1.15.26" + chokidar: "npm:^5.0.0" + commander: "npm:^14.0.3" + execa: "npm:^9.6.1" + glob: "npm:^13.0.6" + i18next-resources-for-ts: "npm:^2.1.0" + inquirer: "npm:^13.4.1" + jiti: "npm:^2.6.1" + jsonc-parser: "npm:^3.3.1" + magic-string: "npm:^0.30.21" + minimatch: "npm:^10.2.5" + ora: "npm:^9.3.0" + react: "npm:^19.2.5" + react-i18next: "npm:^17.0.4" + yaml: "npm:^2.8.3" bin: - i18next-conv: bin/index.js - checksum: 10/0aaf5ee54560c1fd4a6593bc1a7cc75c04586de66a22ce35a4831c4f750f545c3dffa017c8ef9490ac1c4bed720d95330ac6b7fabb36fe0eb73630e0069a4187 + i18next-cli: dist/esm/cli.js + checksum: 10/ca23005187c4d3448330893f4167f7df28415a7e9b23c6bc9cb0241a024c6fa4c759c0dd9f71767b7fd03ee3f3d10ae312a05c9b773d7700523d676f3a9555dd languageName: node linkType: hard -"i18next-scanner@npm:^4.6.0": - version: 4.6.0 - resolution: "i18next-scanner@npm:4.6.0" +"i18next-conv@npm:^16.0.0": + version: 16.0.0 + resolution: "i18next-conv@npm:16.0.0" dependencies: - acorn: "npm:^8.0.4" - acorn-jsx: "npm:^5.3.1" - acorn-stage3: "npm:^4.0.0" - acorn-walk: "npm:^8.0.0" - chalk: "npm:^4.1.0" - clone-deep: "npm:^4.0.0" - commander: "npm:^9.0.0" - deepmerge: "npm:^4.0.0" - ensure-type: "npm:^1.5.0" - eol: "npm:^0.9.1" - esprima-next: "npm:^5.7.0" - gulp-sort: "npm:^2.0.0" - i18next: "npm:*" - lodash: "npm:^4.0.0" - parse5: "npm:^6.0.0" - sortobject: "npm:^4.0.0" - through2: "npm:^4.0.0" - vinyl: "npm:^3.0.0" - vinyl-fs: "npm:^4.0.0" + "@postalsys/gettext": "npm:^4.1.0" + colorette: "npm:^2.0.20" + commander: "npm:^14.0.2" + gettext-converter: "npm:^1.3.0" + gettext-parser: "npm:^8.0.0" + p-from-callback: "npm:^2.0.0" bin: - i18next-scanner: bin/cli.js - checksum: 10/51673170adad6a664bd9b9757dcc54a6fffe7f1e4338de3dcc5de1fbc8d8af87223ece393f9d7c2c579c3efe24525203a8d8f60c2107adfe3d1e3dced92bd21c + i18next-conv: bin/index.js + checksum: 10/f05114e775c8e4d72184f016f94ebba28576c46bbb72c8c75c2b2d64fee25ef8b78987041899ae2e61ebde043e8cb9c3a3accc507ce5e920f6d24a7d3a000cb3 languageName: node linkType: hard -"i18next@npm:*": - version: 23.5.1 - resolution: "i18next@npm:23.5.1" +"i18next-resources-for-ts@npm:^2.1.0": + version: 2.1.0 + resolution: "i18next-resources-for-ts@npm:2.1.0" dependencies: - "@babel/runtime": "npm:^7.22.5" - checksum: 10/38e62d582b0f67eb2eee4f079c9cd512246496f2fb970f50a0be26c7c5e6ac5e772de9763ac1943919ecd816b2c0375f4b2071c67b1485a6a980c4d37348408f + "@babel/runtime": "npm:^7.28.6" + "@swc/core": "npm:^1.15.18" + chokidar: "npm:^5.0.0" + yaml: "npm:^2.8.2" + bin: + i18next-resources-for-ts: bin/i18next-resources-for-ts.js + checksum: 10/f873bf58b0e4c08ad3be2ff8159919ebd42a7e9e2c4d39649eb6e337522f4611fa9a6a2dea320c5350fad5b72abc689c9b7285c1611b007bcaaf5bc1b22d205a languageName: node linkType: hard -"i18next@npm:^21.10.0": - version: 21.10.0 - resolution: "i18next@npm:21.10.0" - dependencies: - "@babel/runtime": "npm:^7.17.2" - checksum: 10/f0d2ab888627c24b00068461eeedad33437970396539b663a4916e7ba279f6bb91e34221ee9c3608079b9d320a0cb02e9f51458f5c47c809c45676900db94abf +"i18next@npm:^26.0.8": + version: 26.0.8 + resolution: "i18next@npm:26.0.8" + peerDependencies: + typescript: ^5 || ^6 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/e25ca27671fb04d358ff7b2d5253482e4bb091bb3f26bd245b443e9e5d68c8c09388d2242a78688f5523ad0a141bd4bea361842a746e13391256aee41e4e8e91 languageName: node linkType: hard -"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": +"iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -5578,12 +7577,12 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.7.0": - version: 0.7.0 - resolution: "iconv-lite@npm:0.7.0" +"iconv-lite@npm:^0.7.0, iconv-lite@npm:^0.7.2": + version: 0.7.2 + resolution: "iconv-lite@npm:0.7.2" dependencies: safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10/5bfc897fedfb7e29991ae5ef1c061ed4f864005f8c6d61ef34aba6a3885c04bd207b278c0642b041383aeac2d11645b4319d0ca7b863b0be4be0cde1c9238ca7 + checksum: 10/24c937b532f868e938386b62410b303b7c767ce3d08dc2829cbe59464d5a26ef86ae5ad1af6b34eec43ddfea39e7d101638644b0178d67262fa87015d59f983a languageName: node linkType: hard @@ -5603,13 +7602,27 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.2.0, ignore@npm:^5.3.1, ignore@npm:^5.3.2": +"ignore-loader@npm:^0.1.2": + version: 0.1.2 + resolution: "ignore-loader@npm:0.1.2" + checksum: 10/26b5f81b24e59c575d5314e1416c4ae21fb88e65b3c60c90581288ef925fcaa2b39e7f8f96cd449e488c217337d5240fba5a916f9aec073ec04495d7a5716ac4 + languageName: node + linkType: hard + +"ignore@npm:^5.2.0, ignore@npm:^5.3.2": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 languageName: node linkType: hard +"ignore@npm:^7.0.5": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: 10/f134b96a4de0af419196f52c529d5c6120c4456ff8a6b5a14ceaaa399f883e15d58d2ce651c9b69b9388491d4669dda47285d307e827de9304a53a1824801bc6 + languageName: node + linkType: hard + "import-fresh@npm:^3.2.1": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" @@ -5717,6 +7730,26 @@ __metadata: languageName: node linkType: hard +"inquirer@npm:^13.4.1": + version: 13.4.2 + resolution: "inquirer@npm:13.4.2" + dependencies: + "@inquirer/ansi": "npm:^2.0.5" + "@inquirer/core": "npm:^11.1.9" + "@inquirer/prompts": "npm:^8.4.2" + "@inquirer/type": "npm:^4.0.5" + mute-stream: "npm:^3.0.0" + run-async: "npm:^4.0.6" + rxjs: "npm:^7.8.2" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/3313db14c253776c156ea2491cdf8a08e89bfb9872fe3a1468c6ffba6e718f90ecc7f545ee2f57d2ff1e98bdadc9cd6d403a33692408fad752fc4ce648c02bbc + languageName: node + linkType: hard + "internal-slot@npm:^1.0.5": version: 1.0.5 resolution: "internal-slot@npm:1.0.5" @@ -5735,6 +7768,20 @@ __metadata: languageName: node linkType: hard +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: 10/864d0cced0c0832700e9621913a6429ccdc67f37c1bd78fb8c6789fff35c9d167cb329134acad2290497a53336813ab4798d2794fd675d5eb33b5fdf0982b9ca + languageName: node + linkType: hard + +"ipaddr.js@npm:^2.1.0": + version: 2.4.0 + resolution: "ipaddr.js@npm:2.4.0" + checksum: 10/e29cd15cd1f3c177f1671a74ed5dc2d7908088294850a7dbc000969a370cf02d6cdd81f8ab35a4fb0397247cd93999528eb4506277cd7db52fa2a487015207cf + languageName: node + linkType: hard + "is-arguments@npm:^1.0.4": version: 1.1.1 resolution: "is-arguments@npm:1.1.1" @@ -5772,6 +7819,15 @@ __metadata: languageName: node linkType: hard +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10/078e51b4f956c2c5fd2b26bb2672c3ccf7e1faff38e0ebdba45612265f4e3d9fc3127a1fa8370bbf09eab61339203c3d3b7af5662cbf8be4030f8fac37745b0e + languageName: node + linkType: hard + "is-boolean-object@npm:^1.1.0": version: 1.1.2 resolution: "is-boolean-object@npm:1.1.2" @@ -5789,12 +7845,12 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.5.0": - version: 2.13.0 - resolution: "is-core-module@npm:2.13.0" +"is-core-module@npm:^2.16.1, is-core-module@npm:^2.5.0": + version: 2.16.2 + resolution: "is-core-module@npm:2.16.2" dependencies: - has: "npm:^1.0.3" - checksum: 10/55ccb5ccd208a1e088027065ee6438a99367e4c31c366b52fbaeac8fa23111cd17852111836d904da604801b3286d38d3d1ffa6cd7400231af8587f021099dc6 + hasown: "npm:^2.0.3" + checksum: 10/6ee7535d82bbe457685799c5f145daf4b7c6be3afbd8e90788429d557f663d6dee72a8e4b9a45d0d756c243fcb5028095999243df090e5f04c02b153786bc8c6 languageName: node linkType: hard @@ -5853,7 +7909,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -5862,6 +7918,13 @@ __metadata: languageName: node linkType: hard +"is-in-ssh@npm:^1.0.0": + version: 1.0.0 + resolution: "is-in-ssh@npm:1.0.0" + checksum: 10/d55cb39afdbca0cdc94cd493da7819c00d35048ea04fc1624aabde6e0c86aa6b91ddb38b2baf73c4b5d53cc8fbf1a8dfbf2e315594a808474b751ffb6b0d3e58 + languageName: node + linkType: hard + "is-inside-container@npm:^1.0.0": version: 1.0.0 resolution: "is-inside-container@npm:1.0.0" @@ -5882,6 +7945,13 @@ __metadata: languageName: node linkType: hard +"is-interactive@npm:^2.0.0": + version: 2.0.0 + resolution: "is-interactive@npm:2.0.0" + checksum: 10/e8d52ad490bed7ae665032c7675ec07732bbfe25808b0efbc4d5a76b1a1f01c165f332775c63e25e9a03d319ebb6b24f571a9e902669fc1e40b0a60b5be6e26c + languageName: node + linkType: hard + "is-nan@npm:^1.3.2": version: 1.3.2 resolution: "is-nan@npm:1.3.2" @@ -5892,13 +7962,6 @@ __metadata: languageName: node linkType: hard -"is-negated-glob@npm:^1.0.0": - version: 1.0.0 - resolution: "is-negated-glob@npm:1.0.0" - checksum: 10/752cb846d71403d0a26389d1f56f8e2ffdb110e994dffe41ebacd1ff4953ee1dc8e71438a00a4e398355113a755f05fc91c73da15541a11d2f080f6b39030d91 - languageName: node - linkType: hard - "is-negative-zero@npm:^2.0.2": version: 2.0.2 resolution: "is-negative-zero@npm:2.0.2" @@ -5906,6 +7969,13 @@ __metadata: languageName: node linkType: hard +"is-network-error@npm:^1.0.0": + version: 1.3.2 + resolution: "is-network-error@npm:1.3.2" + checksum: 10/6d5c0663f476acf7fd5894b5bdce14284aec6b595ad34484d430249b736372e46c345249fd1688bddb2c1380d72c28741838926d1d078264e37e779e970f9d93 + languageName: node + linkType: hard + "is-number-object@npm:^1.0.4": version: 1.0.7 resolution: "is-number-object@npm:1.0.7" @@ -5936,6 +8006,20 @@ __metadata: languageName: node linkType: hard +"is-plain-obj@npm:^3.0.0": + version: 3.0.0 + resolution: "is-plain-obj@npm:3.0.0" + checksum: 10/a6ebdf8e12ab73f33530641972a72a4b8aed6df04f762070d823808303e4f76d87d5ea5bd76f96a7bbe83d93f04ac7764429c29413bd9049853a69cb630fb21c + languageName: node + linkType: hard + +"is-plain-obj@npm:^4.1.0": + version: 4.1.0 + resolution: "is-plain-obj@npm:4.1.0" + checksum: 10/6dc45da70d04a81f35c9310971e78a6a3c7a63547ef782e3a07ee3674695081b6ca4e977fbb8efc48dae3375e0b34558d2bcd722aec9bddfa2d7db5b041be8ce + languageName: node + linkType: hard + "is-plain-object@npm:^2.0.4": version: 2.0.4 resolution: "is-plain-object@npm:2.0.4" @@ -5985,6 +8069,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^4.0.1": + version: 4.0.1 + resolution: "is-stream@npm:4.0.1" + checksum: 10/cbea3f1fc271b21ceb228819d0c12a0965a02b57f39423925f99530b4eb86935235f258f06310b67cd02b2d10b49e9a0998f5ececf110ab7d3760bae4055ad23 + languageName: node + linkType: hard + "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -6021,10 +8112,10 @@ __metadata: languageName: node linkType: hard -"is-valid-glob@npm:^1.0.0": - version: 1.0.0 - resolution: "is-valid-glob@npm:1.0.0" - checksum: 10/0155951e89291d405cbb2ff4e25a38ee7a88bc70b05f246c25d31a1d09f13d4207377e5860f67443bbda8e3e353da37047b60e586bd9c97a39c9301c30b67acb +"is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: 10/f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 languageName: node linkType: hard @@ -6081,6 +8172,13 @@ __metadata: languageName: node linkType: hard +"isomorphic-timers-promises@npm:^1.0.1": + version: 1.0.1 + resolution: "isomorphic-timers-promises@npm:1.0.1" + checksum: 10/2dabe397039081dbf30039f295333a7f9888b072dd0afa3aa7d8ba8f812a6db5efcbda0861a4be43ecfec207d56314ecf27150187b8d0f924a93103fa93eac73 + languageName: node + linkType: hard + "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": version: 3.2.0 resolution: "istanbul-lib-coverage@npm:3.2.0" @@ -6146,58 +8244,58 @@ __metadata: languageName: node linkType: hard -"jest-changed-files@npm:30.2.0": - version: 30.2.0 - resolution: "jest-changed-files@npm:30.2.0" +"jest-changed-files@npm:30.3.0": + version: 30.3.0 + resolution: "jest-changed-files@npm:30.3.0" dependencies: execa: "npm:^5.1.1" - jest-util: "npm:30.2.0" + jest-util: "npm:30.3.0" p-limit: "npm:^3.1.0" - checksum: 10/ff2275ed5839b88c12ffa66fdc5c17ba02d3e276be6b558bed92872c282d050c3fdd1a275a81187cbe35c16d6d40337b85838772836463c7a2fbd1cba9785ca0 + checksum: 10/a65834a428ec7c4512319af52a7397e5fd90088ca85e649c66cda7092fc287b0fae6c0a9d691cca99278b7dfacbbdbcce17e2bebdd81068503389089035489ce languageName: node linkType: hard -"jest-circus@npm:30.2.0": - version: 30.2.0 - resolution: "jest-circus@npm:30.2.0" +"jest-circus@npm:30.3.0": + version: 30.3.0 + resolution: "jest-circus@npm:30.3.0" dependencies: - "@jest/environment": "npm:30.2.0" - "@jest/expect": "npm:30.2.0" - "@jest/test-result": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/environment": "npm:30.3.0" + "@jest/expect": "npm:30.3.0" + "@jest/test-result": "npm:30.3.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" chalk: "npm:^4.1.2" co: "npm:^4.6.0" dedent: "npm:^1.6.0" is-generator-fn: "npm:^2.1.0" - jest-each: "npm:30.2.0" - jest-matcher-utils: "npm:30.2.0" - jest-message-util: "npm:30.2.0" - jest-runtime: "npm:30.2.0" - jest-snapshot: "npm:30.2.0" - jest-util: "npm:30.2.0" + jest-each: "npm:30.3.0" + jest-matcher-utils: "npm:30.3.0" + jest-message-util: "npm:30.3.0" + jest-runtime: "npm:30.3.0" + jest-snapshot: "npm:30.3.0" + jest-util: "npm:30.3.0" p-limit: "npm:^3.1.0" - pretty-format: "npm:30.2.0" + pretty-format: "npm:30.3.0" pure-rand: "npm:^7.0.0" slash: "npm:^3.0.0" stack-utils: "npm:^2.0.6" - checksum: 10/68bfc65d92385db1017643988215e4ff5af0b10bcab86fb749a063be6bb7d5eb556dc53dd21bedf833a19aa6ae1a781a8d27b2bea25562de02d294b3017435a9 + checksum: 10/6aba7c0282af3db4b03870ebe1fc417e651fbfc3cc260de8b73d95ede3ed390af0c94ef376877c5ef50cf8ab49d125ddcd25d6913543b63bf6caa0e22bfecc6f languageName: node linkType: hard -"jest-cli@npm:30.2.0": - version: 30.2.0 - resolution: "jest-cli@npm:30.2.0" +"jest-cli@npm:30.3.0": + version: 30.3.0 + resolution: "jest-cli@npm:30.3.0" dependencies: - "@jest/core": "npm:30.2.0" - "@jest/test-result": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/core": "npm:30.3.0" + "@jest/test-result": "npm:30.3.0" + "@jest/types": "npm:30.3.0" chalk: "npm:^4.1.2" exit-x: "npm:^0.2.2" import-local: "npm:^3.2.0" - jest-config: "npm:30.2.0" - jest-util: "npm:30.2.0" - jest-validate: "npm:30.2.0" + jest-config: "npm:30.3.0" + jest-util: "npm:30.3.0" + jest-validate: "npm:30.3.0" yargs: "npm:^17.7.2" peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -6206,36 +8304,35 @@ __metadata: optional: true bin: jest: ./bin/jest.js - checksum: 10/1cc8304f0e2608801c84cdecce9565a6178f668a6475aed3767a1d82cc539915f98e7404d7c387510313684011dc3095c15397d6725f73aac80fbd96c4155faa + checksum: 10/a80aa3a2eec0b0d6644c25ce196d485e178b9c2ad037c17764a645f2fe156563c7fb2dca07cb10d8b9da77dbb8e0c6bcb4b82ca9a59ee50f12700f06670093c1 languageName: node linkType: hard -"jest-config@npm:30.2.0": - version: 30.2.0 - resolution: "jest-config@npm:30.2.0" +"jest-config@npm:30.3.0": + version: 30.3.0 + resolution: "jest-config@npm:30.3.0" dependencies: "@babel/core": "npm:^7.27.4" "@jest/get-type": "npm:30.1.0" "@jest/pattern": "npm:30.0.1" - "@jest/test-sequencer": "npm:30.2.0" - "@jest/types": "npm:30.2.0" - babel-jest: "npm:30.2.0" + "@jest/test-sequencer": "npm:30.3.0" + "@jest/types": "npm:30.3.0" + babel-jest: "npm:30.3.0" chalk: "npm:^4.1.2" ci-info: "npm:^4.2.0" deepmerge: "npm:^4.3.1" - glob: "npm:^10.3.10" + glob: "npm:^10.5.0" graceful-fs: "npm:^4.2.11" - jest-circus: "npm:30.2.0" + jest-circus: "npm:30.3.0" jest-docblock: "npm:30.2.0" - jest-environment-node: "npm:30.2.0" + jest-environment-node: "npm:30.3.0" jest-regex-util: "npm:30.0.1" - jest-resolve: "npm:30.2.0" - jest-runner: "npm:30.2.0" - jest-util: "npm:30.2.0" - jest-validate: "npm:30.2.0" - micromatch: "npm:^4.0.8" + jest-resolve: "npm:30.3.0" + jest-runner: "npm:30.3.0" + jest-util: "npm:30.3.0" + jest-validate: "npm:30.3.0" parse-json: "npm:^5.2.0" - pretty-format: "npm:30.2.0" + pretty-format: "npm:30.3.0" slash: "npm:^3.0.0" strip-json-comments: "npm:^3.1.1" peerDependencies: @@ -6249,19 +8346,19 @@ __metadata: optional: true ts-node: optional: true - checksum: 10/296786b0a3d62de77e2f691f208d54ab541c1a73f87747d922eda643c6f25b89125ef3150170c07a6c8a316a30c15428e46237d499f688b0777f38de8a61ad16 + checksum: 10/89c49426e2be5ee0c7cf9d6ab0a1dd6eb5ea03f67a5cc57d991d3d2441762d7101a215da5596bcb5b39c47e209ab8fdf4682fd1365cef7a5e48903b689bf4116 languageName: node linkType: hard -"jest-diff@npm:30.2.0": - version: 30.2.0 - resolution: "jest-diff@npm:30.2.0" +"jest-diff@npm:30.3.0": + version: 30.3.0 + resolution: "jest-diff@npm:30.3.0" dependencies: - "@jest/diff-sequences": "npm:30.0.1" + "@jest/diff-sequences": "npm:30.3.0" "@jest/get-type": "npm:30.1.0" chalk: "npm:^4.1.2" - pretty-format: "npm:30.2.0" - checksum: 10/1fb9e4fb7dff81814b4f69eaa7db28e184d62306a3a8ea2447d02ca53d2cfa771e83ede513f67ec5239dffacfaac32ff2b49866d211e4c7516f51c1fc06ede42 + pretty-format: "npm:30.3.0" + checksum: 10/9f566259085e6badd525dc48ee6de3792cfae080abd66e170ac230359cf32c4334d92f0f48b577a31ad2a6aed4aefde81f5f4366ab44a96f78bcde975e5cc26e languageName: node linkType: hard @@ -6274,103 +8371,103 @@ __metadata: languageName: node linkType: hard -"jest-each@npm:30.2.0": - version: 30.2.0 - resolution: "jest-each@npm:30.2.0" +"jest-each@npm:30.3.0": + version: 30.3.0 + resolution: "jest-each@npm:30.3.0" dependencies: "@jest/get-type": "npm:30.1.0" - "@jest/types": "npm:30.2.0" + "@jest/types": "npm:30.3.0" chalk: "npm:^4.1.2" - jest-util: "npm:30.2.0" - pretty-format: "npm:30.2.0" - checksum: 10/f95e7dc1cef4b6a77899325702a214834ae25d01276cc31279654dc7e04f63c1925a37848dd16a0d16508c0fd3d182145f43c10af93952b7a689df3aeac198e9 + jest-util: "npm:30.3.0" + pretty-format: "npm:30.3.0" + checksum: 10/ece465cbb1c4fbb445c9cfacd33275489940684fd0d447f6d4bdb4ef81d63c1b0bc3b365be7400dbbffd8d5502fd5faf10e97025a61c27bcd3da1ea21c749381 languageName: node linkType: hard -"jest-environment-node@npm:30.2.0": - version: 30.2.0 - resolution: "jest-environment-node@npm:30.2.0" +"jest-environment-node@npm:30.3.0": + version: 30.3.0 + resolution: "jest-environment-node@npm:30.3.0" dependencies: - "@jest/environment": "npm:30.2.0" - "@jest/fake-timers": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/environment": "npm:30.3.0" + "@jest/fake-timers": "npm:30.3.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" - jest-mock: "npm:30.2.0" - jest-util: "npm:30.2.0" - jest-validate: "npm:30.2.0" - checksum: 10/7918bfea7367bd3e12dbbc4ea5afb193b5c47e480a6d1382512f051e2f028458fc9f5ef2f6260737ad41a0b1894661790ff3aaf3cbb4148a33ce2ce7aec64847 + jest-mock: "npm:30.3.0" + jest-util: "npm:30.3.0" + jest-validate: "npm:30.3.0" + checksum: 10/805732507857f283f8c5eaca78561401c16043cd9a2579fc4a3cd6139a5138c6108f4b32f7fafe5b41f9b53f2fbc63cf65eb892e15e086034b09899c9fa4fed4 languageName: node linkType: hard -"jest-haste-map@npm:30.2.0": - version: 30.2.0 - resolution: "jest-haste-map@npm:30.2.0" +"jest-haste-map@npm:30.3.0": + version: 30.3.0 + resolution: "jest-haste-map@npm:30.3.0" dependencies: - "@jest/types": "npm:30.2.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" anymatch: "npm:^3.1.3" fb-watchman: "npm:^2.0.2" fsevents: "npm:^2.3.3" graceful-fs: "npm:^4.2.11" jest-regex-util: "npm:30.0.1" - jest-util: "npm:30.2.0" - jest-worker: "npm:30.2.0" - micromatch: "npm:^4.0.8" + jest-util: "npm:30.3.0" + jest-worker: "npm:30.3.0" + picomatch: "npm:^4.0.3" walker: "npm:^1.0.8" dependenciesMeta: fsevents: optional: true - checksum: 10/a88be6b0b672144aa30fe2d72e630d639c8d8729ee2cef84d0f830eac2005ac021cd8354f8ed8ecd74223f6a8b281efb62f466f5c9e01ed17650e38761051f4c + checksum: 10/0e0cc449d57414ac2d1f9ece64a98ffc4b4041fe3fba7cf9aaeb71089f7101583b1752e88aa4440d6fa71f86ef50d630be4f31f922cdf404d78655cb9811493b languageName: node linkType: hard -"jest-leak-detector@npm:30.2.0": - version: 30.2.0 - resolution: "jest-leak-detector@npm:30.2.0" +"jest-leak-detector@npm:30.3.0": + version: 30.3.0 + resolution: "jest-leak-detector@npm:30.3.0" dependencies: "@jest/get-type": "npm:30.1.0" - pretty-format: "npm:30.2.0" - checksum: 10/c430d6ed7910b2174738fbdca4ea64cbfe805216414c0d143c1090148f1389fec99d0733c0a8ed0a86709c89b4a4085b4749ac3a2cbc7deaf3ca87457afd24fc + pretty-format: "npm:30.3.0" + checksum: 10/950ce3266067dd983f80231ce753fdfb9fe167d810b4507d84e674205c2cb96d37f38615ae502fa9277dde497ee52ce581656b48709aacf9502a4f0006bfab0e languageName: node linkType: hard -"jest-matcher-utils@npm:30.2.0": - version: 30.2.0 - resolution: "jest-matcher-utils@npm:30.2.0" +"jest-matcher-utils@npm:30.3.0": + version: 30.3.0 + resolution: "jest-matcher-utils@npm:30.3.0" dependencies: "@jest/get-type": "npm:30.1.0" chalk: "npm:^4.1.2" - jest-diff: "npm:30.2.0" - pretty-format: "npm:30.2.0" - checksum: 10/f3f1ecf68ca63c9d1d80a175637a8fc655edfd1ee83220f6e3f6bd464ecbe2f93148fdd440a5a5e5a2b0b2cc8ee84ddc3dcef58a6dbc66821c792f48d260c6d4 + jest-diff: "npm:30.3.0" + pretty-format: "npm:30.3.0" + checksum: 10/8aeef24fe2a21a3a22eb26a805c0a4c8ca8961aa1ebc07d680bf55b260f593814467bdfb60b271a3c239a411b2468f352c279cef466e35fd024d901ffa6cc942 languageName: node linkType: hard -"jest-message-util@npm:30.2.0": - version: 30.2.0 - resolution: "jest-message-util@npm:30.2.0" +"jest-message-util@npm:30.3.0": + version: 30.3.0 + resolution: "jest-message-util@npm:30.3.0" dependencies: "@babel/code-frame": "npm:^7.27.1" - "@jest/types": "npm:30.2.0" + "@jest/types": "npm:30.3.0" "@types/stack-utils": "npm:^2.0.3" chalk: "npm:^4.1.2" graceful-fs: "npm:^4.2.11" - micromatch: "npm:^4.0.8" - pretty-format: "npm:30.2.0" + picomatch: "npm:^4.0.3" + pretty-format: "npm:30.3.0" slash: "npm:^3.0.0" stack-utils: "npm:^2.0.6" - checksum: 10/e29ec76e8c8e4da5f5b25198be247535626ccf3a940e93fdd51fc6a6bcf70feaa2921baae3806182a090431d90b08c939eb13fb64249b171d2e9ae3a452a8fd2 + checksum: 10/886577543ec60b421d21987190c5e393ff3652f4f2f2b504776d73f932518827b026ab8e6ffdb1f21ff5142ddf160ba4794e56d96143baeb4ae6939e040a10bd languageName: node linkType: hard -"jest-mock@npm:30.2.0": - version: 30.2.0 - resolution: "jest-mock@npm:30.2.0" +"jest-mock@npm:30.3.0": + version: 30.3.0 + resolution: "jest-mock@npm:30.3.0" dependencies: - "@jest/types": "npm:30.2.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" - jest-util: "npm:30.2.0" - checksum: 10/cde9b56805f90bf811a9231873ee88a0fb83bf4bf50972ae76960725da65220fcb119688f2e90e1ef33fbfd662194858d7f43809d881f1c41bb55d94e62adeab + jest-util: "npm:30.3.0" + checksum: 10/9d2a9e52c2aebc486e9accaf641efa5c6589666e883b5ac1987261d0e2c105a06b885c22aeeb1cd7582e421970c95e34fe0b41bc4a8c06d7e3e4c27651e76ad1 languageName: node linkType: hard @@ -6393,186 +8490,186 @@ __metadata: languageName: node linkType: hard -"jest-resolve-dependencies@npm:30.2.0": - version: 30.2.0 - resolution: "jest-resolve-dependencies@npm:30.2.0" +"jest-resolve-dependencies@npm:30.3.0": + version: 30.3.0 + resolution: "jest-resolve-dependencies@npm:30.3.0" dependencies: jest-regex-util: "npm:30.0.1" - jest-snapshot: "npm:30.2.0" - checksum: 10/0ff1a574f8c07f2e54a4ac8ab17aea00dfe2982e99b03fbd44f4211a94b8e5a59fdc43a59f9d6c0578a10a7b56a0611ad5ab40e4893973ff3f40dd414433b194 + jest-snapshot: "npm:30.3.0" + checksum: 10/79dfbc3c8c967e7908bcb02f5116c37002f2cdc10360d179876de832c10ee87cb85cc27895b035697da477ab6ad70170f4e2907a85d35a44117646554cc72111 languageName: node linkType: hard -"jest-resolve@npm:30.2.0": - version: 30.2.0 - resolution: "jest-resolve@npm:30.2.0" +"jest-resolve@npm:30.3.0": + version: 30.3.0 + resolution: "jest-resolve@npm:30.3.0" dependencies: chalk: "npm:^4.1.2" graceful-fs: "npm:^4.2.11" - jest-haste-map: "npm:30.2.0" + jest-haste-map: "npm:30.3.0" jest-pnp-resolver: "npm:^1.2.3" - jest-util: "npm:30.2.0" - jest-validate: "npm:30.2.0" + jest-util: "npm:30.3.0" + jest-validate: "npm:30.3.0" slash: "npm:^3.0.0" unrs-resolver: "npm:^1.7.11" - checksum: 10/e1f03da6811a946f5d885ea739a973975d099cc760641f9e1f90ac9c6621408538ba1e909f789d45d6e8d2411b78fb09230f16f15669621aa407aed7511fdf01 + checksum: 10/7d88ef3f6424386e4b4e65d486ac1d3b86c142cf789f0ab945a2cd8bd830edc0314c7561a459b95062f41bc550ae7110f461dbafcc07030f61728edb00b4bcdd languageName: node linkType: hard -"jest-runner@npm:30.2.0": - version: 30.2.0 - resolution: "jest-runner@npm:30.2.0" +"jest-runner@npm:30.3.0": + version: 30.3.0 + resolution: "jest-runner@npm:30.3.0" dependencies: - "@jest/console": "npm:30.2.0" - "@jest/environment": "npm:30.2.0" - "@jest/test-result": "npm:30.2.0" - "@jest/transform": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/console": "npm:30.3.0" + "@jest/environment": "npm:30.3.0" + "@jest/test-result": "npm:30.3.0" + "@jest/transform": "npm:30.3.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" chalk: "npm:^4.1.2" emittery: "npm:^0.13.1" exit-x: "npm:^0.2.2" graceful-fs: "npm:^4.2.11" jest-docblock: "npm:30.2.0" - jest-environment-node: "npm:30.2.0" - jest-haste-map: "npm:30.2.0" - jest-leak-detector: "npm:30.2.0" - jest-message-util: "npm:30.2.0" - jest-resolve: "npm:30.2.0" - jest-runtime: "npm:30.2.0" - jest-util: "npm:30.2.0" - jest-watcher: "npm:30.2.0" - jest-worker: "npm:30.2.0" + jest-environment-node: "npm:30.3.0" + jest-haste-map: "npm:30.3.0" + jest-leak-detector: "npm:30.3.0" + jest-message-util: "npm:30.3.0" + jest-resolve: "npm:30.3.0" + jest-runtime: "npm:30.3.0" + jest-util: "npm:30.3.0" + jest-watcher: "npm:30.3.0" + jest-worker: "npm:30.3.0" p-limit: "npm:^3.1.0" source-map-support: "npm:0.5.13" - checksum: 10/d3706aa70e64a7ef8b38360d34ea6c261ba4d0b42136d7fb603c4fa71c24fa81f22c39ed2e39ee0db2363a42827810291f3ceb6a299e5996b41d701ad9b24184 + checksum: 10/f467591d2ff95f7b3138dc7c8631e751000d1fcabfdb9a94623fce3fd7b538a45628e9a1e8e8758c4d7a0c3757c393a3ef034ba986d7565e3f1b597ab7a73748 languageName: node linkType: hard -"jest-runtime@npm:30.2.0": - version: 30.2.0 - resolution: "jest-runtime@npm:30.2.0" +"jest-runtime@npm:30.3.0": + version: 30.3.0 + resolution: "jest-runtime@npm:30.3.0" dependencies: - "@jest/environment": "npm:30.2.0" - "@jest/fake-timers": "npm:30.2.0" - "@jest/globals": "npm:30.2.0" + "@jest/environment": "npm:30.3.0" + "@jest/fake-timers": "npm:30.3.0" + "@jest/globals": "npm:30.3.0" "@jest/source-map": "npm:30.0.1" - "@jest/test-result": "npm:30.2.0" - "@jest/transform": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/test-result": "npm:30.3.0" + "@jest/transform": "npm:30.3.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" chalk: "npm:^4.1.2" cjs-module-lexer: "npm:^2.1.0" collect-v8-coverage: "npm:^1.0.2" - glob: "npm:^10.3.10" + glob: "npm:^10.5.0" graceful-fs: "npm:^4.2.11" - jest-haste-map: "npm:30.2.0" - jest-message-util: "npm:30.2.0" - jest-mock: "npm:30.2.0" + jest-haste-map: "npm:30.3.0" + jest-message-util: "npm:30.3.0" + jest-mock: "npm:30.3.0" jest-regex-util: "npm:30.0.1" - jest-resolve: "npm:30.2.0" - jest-snapshot: "npm:30.2.0" - jest-util: "npm:30.2.0" + jest-resolve: "npm:30.3.0" + jest-snapshot: "npm:30.3.0" + jest-util: "npm:30.3.0" slash: "npm:^3.0.0" strip-bom: "npm:^4.0.0" - checksum: 10/81a3a9951420863f001e74c510bf35b85ae983f636f43ee1ffa1618b5a8ddafb681bc2810f71814bc8c8373e9593c89576b2325daf3c765e50057e48d5941df3 + checksum: 10/a9335405ca46e8d77c8400887566b5cf2a3544e1b067eb3b187e86ea5c74f1b8b16ecf1de3a589bfb32be95e77452a01913f187d66a41c5a4595a30d7dc1daf0 languageName: node linkType: hard -"jest-snapshot@npm:30.2.0": - version: 30.2.0 - resolution: "jest-snapshot@npm:30.2.0" +"jest-snapshot@npm:30.3.0": + version: 30.3.0 + resolution: "jest-snapshot@npm:30.3.0" dependencies: "@babel/core": "npm:^7.27.4" "@babel/generator": "npm:^7.27.5" "@babel/plugin-syntax-jsx": "npm:^7.27.1" "@babel/plugin-syntax-typescript": "npm:^7.27.1" "@babel/types": "npm:^7.27.3" - "@jest/expect-utils": "npm:30.2.0" + "@jest/expect-utils": "npm:30.3.0" "@jest/get-type": "npm:30.1.0" - "@jest/snapshot-utils": "npm:30.2.0" - "@jest/transform": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/snapshot-utils": "npm:30.3.0" + "@jest/transform": "npm:30.3.0" + "@jest/types": "npm:30.3.0" babel-preset-current-node-syntax: "npm:^1.2.0" chalk: "npm:^4.1.2" - expect: "npm:30.2.0" + expect: "npm:30.3.0" graceful-fs: "npm:^4.2.11" - jest-diff: "npm:30.2.0" - jest-matcher-utils: "npm:30.2.0" - jest-message-util: "npm:30.2.0" - jest-util: "npm:30.2.0" - pretty-format: "npm:30.2.0" + jest-diff: "npm:30.3.0" + jest-matcher-utils: "npm:30.3.0" + jest-message-util: "npm:30.3.0" + jest-util: "npm:30.3.0" + pretty-format: "npm:30.3.0" semver: "npm:^7.7.2" synckit: "npm:^0.11.8" - checksum: 10/119390b49f397ed622ba7c375fc15f97af67c4fc49a34cf829c86ee732be2b06ad3c7171c76bb842a0e84a234783f1a4c721909aa316fbe00c6abc7c5962dfbc + checksum: 10/d9f75c436587410cc8170a710d53a632e148a648ec82476ef9e618d8067246e48af7c460773304ad53eecf748b118619a6afd87212f86d680d3439787b4fec39 languageName: node linkType: hard -"jest-util@npm:30.2.0, jest-util@npm:^30.2.0": - version: 30.2.0 - resolution: "jest-util@npm:30.2.0" +"jest-util@npm:30.3.0, jest-util@npm:^30.3.0": + version: 30.3.0 + resolution: "jest-util@npm:30.3.0" dependencies: - "@jest/types": "npm:30.2.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" chalk: "npm:^4.1.2" ci-info: "npm:^4.2.0" graceful-fs: "npm:^4.2.11" - picomatch: "npm:^4.0.2" - checksum: 10/cf2f2fb83417ea69f9992121561c95cf4e9aad7946819b771b8b52addf78811101b33b51d0a39fa0c305f2751dab262feed7699de052659ff03d51827c8862f5 + picomatch: "npm:^4.0.3" + checksum: 10/4b016004637f6a53d6f54c993dc8904a4d6abe93acb8dd70622dc2ca80290a03692e834af1068969b486426e87d411144705edd4d772bb715a826d7e15b5a4b3 languageName: node linkType: hard -"jest-validate@npm:30.2.0": - version: 30.2.0 - resolution: "jest-validate@npm:30.2.0" +"jest-validate@npm:30.3.0": + version: 30.3.0 + resolution: "jest-validate@npm:30.3.0" dependencies: "@jest/get-type": "npm:30.1.0" - "@jest/types": "npm:30.2.0" + "@jest/types": "npm:30.3.0" camelcase: "npm:^6.3.0" chalk: "npm:^4.1.2" leven: "npm:^3.1.0" - pretty-format: "npm:30.2.0" - checksum: 10/61e66c6df29a1e181f8de063678dd2096bb52cc8a8ead3c9a3f853d54eca458ad04c7fb81931d9274affb67d0504a91a2a520456a139a26665810c3bf039b677 + pretty-format: "npm:30.3.0" + checksum: 10/b26e32602c65f93d4fa9ca24efa661df24b8919c5c4cb88b87852178310833df3a7fdb757afb9d769cfe13f6636385626d8ac8a2ad7af47365d309a548cd0e06 languageName: node linkType: hard -"jest-watcher@npm:30.2.0": - version: 30.2.0 - resolution: "jest-watcher@npm:30.2.0" +"jest-watcher@npm:30.3.0": + version: 30.3.0 + resolution: "jest-watcher@npm:30.3.0" dependencies: - "@jest/test-result": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/test-result": "npm:30.3.0" + "@jest/types": "npm:30.3.0" "@types/node": "npm:*" ansi-escapes: "npm:^4.3.2" chalk: "npm:^4.1.2" emittery: "npm:^0.13.1" - jest-util: "npm:30.2.0" + jest-util: "npm:30.3.0" string-length: "npm:^4.0.2" - checksum: 10/fa38d06dcc59dbbd6a9ff22dea499d3c81ed376d9993b82d01797a99bf466d48641a99b9f3670a4b5480ca31144c5e017b96b7059e4d7541358fb48cf517a2db + checksum: 10/b3a284869be1c69a8084c1129fcc08b719b8556d3af93b6cd587f9e2f948e5ce5084cb0ec62a166e3161d1d8b6dc580a88ba02abc05a0948809c65b27bd60f3a languageName: node linkType: hard -"jest-worker@npm:30.2.0": - version: 30.2.0 - resolution: "jest-worker@npm:30.2.0" +"jest-worker@npm:30.3.0": + version: 30.3.0 + resolution: "jest-worker@npm:30.3.0" dependencies: "@types/node": "npm:*" "@ungap/structured-clone": "npm:^1.3.0" - jest-util: "npm:30.2.0" + jest-util: "npm:30.3.0" merge-stream: "npm:^2.0.0" supports-color: "npm:^8.1.1" - checksum: 10/9354b0c71c80173f673da6bbc0ddaad26e4395b06532f7332e0c1e93e855b873b10139b040e01eda77f3dc5a0b67613e2bd7c56c4947ee771acfc3611de2ca29 + checksum: 10/6198e7462617e8f544b1ba593970fb7656e990aa87a2259f693edde106b5aecf63bae692e8d6adc4313efcaba283b15fc25f6834cacca12cf241da0ece722060 languageName: node linkType: hard -"jest@npm:^30.2.0": - version: 30.2.0 - resolution: "jest@npm:30.2.0" +"jest@npm:^30.3.0": + version: 30.3.0 + resolution: "jest@npm:30.3.0" dependencies: - "@jest/core": "npm:30.2.0" - "@jest/types": "npm:30.2.0" + "@jest/core": "npm:30.3.0" + "@jest/types": "npm:30.3.0" import-local: "npm:^3.2.0" - jest-cli: "npm:30.2.0" + jest-cli: "npm:30.3.0" peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -6580,7 +8677,16 @@ __metadata: optional: true bin: jest: ./bin/jest.js - checksum: 10/61c9d100750e4354cd7305d1f3ba253ffde4deaf12cb4be4d42d54f2dd5986e383a39c4a8691dbdc3839c69094a52413ed36f1886540ac37b71914a990b810d0 + checksum: 10/e8485ede8456c71915e94a7ab4fe66c983043263109d61e0665a17cb7f8e843a5a30abca4d932b0ea7aa90326aa10d4acb31d8f3cd2b3158a89c1e5ee3b92856 + languageName: node + linkType: hard + +"jiti@npm:^2.6.1": + version: 2.6.1 + resolution: "jiti@npm:2.6.1" + bin: + jiti: lib/jiti-cli.mjs + checksum: 10/8cd72c5fd03a0502564c3f46c49761090f6dadead21fa191b73535724f095ad86c2fa89ee6fe4bc3515337e8d406cc8fb2d37b73fa0c99a34584bac35cd4a4de languageName: node linkType: hard @@ -6660,6 +8766,13 @@ __metadata: languageName: node linkType: hard +"json-schema-traverse@npm:^1.0.0": + version: 1.0.0 + resolution: "json-schema-traverse@npm:1.0.0" + checksum: 10/02f2f466cdb0362558b2f1fd5e15cce82ef55d60cd7f8fa828cf35ba74330f8d767fcae5c5c2adb7851fa811766c694b9405810879bc4e1ddd78a7c0e03658ad + languageName: node + linkType: hard + "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -6667,6 +8780,13 @@ __metadata: languageName: node linkType: hard +"json-stream-stringify@npm:3.0.1": + version: 3.0.1 + resolution: "json-stream-stringify@npm:3.0.1" + checksum: 10/ac2d35bf805dbf2a1d72e1ae7c47bc8febfc36c6b8772f695f6ee5a99a7adaa60b106695d981c44d9d579c8293a706129a1c8e65b53d8ad4f4b15a0da8a23445 + languageName: node + linkType: hard + "json-stringify-safe@npm:^5.0.1": version: 5.0.1 resolution: "json-stringify-safe@npm:5.0.1" @@ -6683,6 +8803,26 @@ __metadata: languageName: node linkType: hard +"jsonc-parser@npm:^3.3.1": + version: 3.3.1 + resolution: "jsonc-parser@npm:3.3.1" + checksum: 10/9b0dc391f20b47378f843ef1e877e73ec652a5bdc3c5fa1f36af0f119a55091d147a86c1ee86a232296f55c929bba174538c2bf0312610e0817a22de131cc3f4 + languageName: node + linkType: hard + +"jsonfile@npm:^6.0.1": + version: 6.2.1 + resolution: "jsonfile@npm:6.2.1" + dependencies: + graceful-fs: "npm:^4.1.6" + universalify: "npm:^2.0.0" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10/6022bcca984bb5ac57855f80d1c7013765c2db13624292d4652b83f9f4ae93486b82ba516ad5ea91d07cd2f6e2e579b42e422ec1d680e78605f4af25644b9797 + languageName: node + linkType: hard + "jsonparse@npm:^1.2.0": version: 1.3.1 resolution: "jsonparse@npm:1.3.1" @@ -6690,12 +8830,12 @@ __metadata: languageName: node linkType: hard -"kairos-lib@npm:0.2.3, kairos-lib@npm:^0.2.3": - version: 0.2.3 - resolution: "kairos-lib@npm:0.2.3" +"kairos-lib@npm:1.0.0, kairos-lib@npm:^1.0.0": + version: 1.0.0 + resolution: "kairos-lib@npm:1.0.0" dependencies: tslib: "npm:^2.8.1" - checksum: 10/d1be6e208896839ea693924b26f185a6b7e7dff9b1f964e4283df2e2d438554f396b4cad7cb696dcd111a2a554c12c9642bf92f7037bd8059e96a118c3a4c2c9 + checksum: 10/e7a40a5b83199b529b27cbd9b445aee7027ea890209e02fd3d3b17db12b9c86871cc4a4c3e52575ce43c64c8f2712a7be88f29b0d75ab0d94faf4efba6907686 languageName: node linkType: hard @@ -6814,7 +8954,7 @@ __metadata: languageName: node linkType: hard -"koa@npm:^3.1.1": +"koa@npm:^3.2.0": version: 3.2.0 resolution: "koa@npm:3.2.0" dependencies: @@ -6847,10 +8987,13 @@ __metadata: languageName: node linkType: hard -"lead@npm:^4.0.0": - version: 4.0.0 - resolution: "lead@npm:4.0.0" - checksum: 10/7117297c29b94e4846822e5ae0a25780af834586c0862b89ff899e44547f4f742d67801f19838b34611d36eec44868604c55525e12d2a1fb0c9496a9792ca396 +"launch-editor@npm:^2.13.2, launch-editor@npm:^2.6.1": + version: 2.13.2 + resolution: "launch-editor@npm:2.13.2" + dependencies: + picocolors: "npm:^1.1.1" + shell-quote: "npm:^1.8.3" + checksum: 10/2b718ae4d3494526c9493a8c8f32e3824a79885e3b3be2e7e0db5ff74811b12af41760c4b904692cb43ddbd815ce65be245910e7ae84c3cc8ecbad4923657115 languageName: node linkType: hard @@ -6900,6 +9043,13 @@ __metadata: languageName: node linkType: hard +"lines-and-columns@npm:2.0.4": + version: 2.0.4 + resolution: "lines-and-columns@npm:2.0.4" + checksum: 10/81ac2f943f5428a46bd4ea2561c74ba674a107d8e6cc70cd317d16892a36ff3ba0dc6e599aca8b6f8668d26c85288394c6edf7a40e985ca843acab3701b80d4c + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -6957,13 +9107,6 @@ __metadata: languageName: node linkType: hard -"lodash.get@npm:^4.4.2": - version: 4.4.2 - resolution: "lodash.get@npm:4.4.2" - checksum: 10/2a4925f6e89bc2c010a77a802d1ba357e17ed1ea03c2ddf6a146429f2856a216663e694a6aa3549a318cbbba3fd8b7decb392db457e6ac0b83dc745ed0a17380 - languageName: node - linkType: hard - "lodash.ismatch@npm:^4.4.0": version: 4.4.0 resolution: "lodash.ismatch@npm:4.4.0" @@ -6992,10 +9135,13 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.0.0": - version: 4.17.23 - resolution: "lodash@npm:4.17.23" - checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233 +"log-symbols@npm:^7.0.1": + version: 7.0.1 + resolution: "log-symbols@npm:7.0.1" + dependencies: + is-unicode-supported: "npm:^2.0.0" + yoctocolors: "npm:^2.1.1" + checksum: 10/0862313d84826b551582e39659b8586c56b65130c5f4f976420e2c23985228334f2a26fc4251ac22bf0a5b415d9430e86bf332557d934c10b036f9a549d63a09 languageName: node linkType: hard @@ -7052,6 +9198,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.21": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10/57d5691f41ed40d962d8bd300148114f53db67fadbff336207db10a99f2bdf4a1be9cac3a68ee85dba575912ee1d4402e4396408196ec2d3afd043b076156221 + languageName: node + linkType: hard + "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -7171,6 +9326,30 @@ __metadata: languageName: node linkType: hard +"memfs@npm:^4.43.1": + version: 4.57.2 + resolution: "memfs@npm:4.57.2" + dependencies: + "@jsonjoy.com/fs-core": "npm:4.57.2" + "@jsonjoy.com/fs-fsa": "npm:4.57.2" + "@jsonjoy.com/fs-node": "npm:4.57.2" + "@jsonjoy.com/fs-node-builtins": "npm:4.57.2" + "@jsonjoy.com/fs-node-to-fsa": "npm:4.57.2" + "@jsonjoy.com/fs-node-utils": "npm:4.57.2" + "@jsonjoy.com/fs-print": "npm:4.57.2" + "@jsonjoy.com/fs-snapshot": "npm:4.57.2" + "@jsonjoy.com/json-pack": "npm:^1.11.0" + "@jsonjoy.com/util": "npm:^1.9.0" + glob-to-regex.js: "npm:^1.0.1" + thingies: "npm:^2.5.0" + tree-dump: "npm:^1.0.3" + tslib: "npm:^2.0.0" + peerDependencies: + tslib: 2 + checksum: 10/872b08504889b616a2ec28655509632112d80f8f0fd6d8c23219f4f62b4ff7d6a890205e3127379d985f3bdcaf82909a9f2c3a17867495f98b4678bc2af8a458 + languageName: node + linkType: hard + "memory-pager@npm:^1.0.2": version: 1.5.0 resolution: "memory-pager@npm:1.5.0" @@ -7178,13 +9357,6 @@ __metadata: languageName: node linkType: hard -"meow@npm:^12.1.1": - version: 12.1.1 - resolution: "meow@npm:12.1.1" - checksum: 10/8594c319f4671a562c1fef584422902f1bbbad09ea49cdf9bb26dc92f730fa33398dd28a8cf34fcf14167f1d1148d05a867e50911fc4286751a4fb662fdd2dc2 - languageName: node - linkType: hard - "meow@npm:^13.2.0": version: 13.2.0 resolution: "meow@npm:13.2.0" @@ -7192,6 +9364,13 @@ __metadata: languageName: node linkType: hard +"meow@npm:^14.1.0": + version: 14.1.0 + resolution: "meow@npm:14.1.0" + checksum: 10/c6a22b3912a6bc849dee0d6475cd8bb63b9307e26919ca3ace28dc1aaf3d30257071de32bba496f7b5eec3e62b03a6b7731e3d04d18efb3c3103b829aad52ca5 + languageName: node + linkType: hard + "meow@npm:^8.1.2": version: 8.1.2 resolution: "meow@npm:8.1.2" @@ -7211,10 +9390,10 @@ __metadata: languageName: node linkType: hard -"merge-descriptors@npm:^1.0.1": - version: 1.0.1 - resolution: "merge-descriptors@npm:1.0.1" - checksum: 10/5abc259d2ae25bb06d19ce2b94a21632583c74e2a9109ee1ba7fd147aa7362b380d971e0251069f8b3eb7d48c21ac839e21fa177b335e82c76ec172e30c31a26 +"merge-descriptors@npm:1.0.3, merge-descriptors@npm:^1.0.1": + version: 1.0.3 + resolution: "merge-descriptors@npm:1.0.3" + checksum: 10/52117adbe0313d5defa771c9993fe081e2d2df9b840597e966aadafde04ae8d0e3da46bac7ca4efc37d4d2b839436582659cd49c6a43eacb3fe3050896a105d1 languageName: node linkType: hard @@ -7225,16 +9404,9 @@ __metadata: languageName: node linkType: hard -"merge2@npm:^1.3.0": - version: 1.4.1 - resolution: "merge2@npm:1.4.1" - checksum: 10/7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 - languageName: node - linkType: hard - -"meteor-node-stubs@npm:^1.2.25": - version: 1.2.25 - resolution: "meteor-node-stubs@npm:1.2.25" +"meteor-node-stubs@npm:^1.2.27": + version: 1.2.27 + resolution: "meteor-node-stubs@npm:1.2.27" dependencies: "@meteorjs/crypto-browserify": "npm:^3.12.1" assert: "npm:^2.1.0" @@ -7260,18 +9432,18 @@ __metadata: url: "npm:^0.11.4" util: "npm:^0.12.5" vm-browserify: "npm:^1.1.2" - checksum: 10/57390ff99f2fa775f8b46b0faabbbfbcab9f193d264f3cb1c2e250e24cfd211c3437ad3c7646d7c3965531155fc698383e5bd5d8f1504d93a4b1cc9cbc007b0f + checksum: 10/f6499c4dd0056320247ce38a06e666a90d74f7c09b891467738af70b600415a681727ed81a159ba6c9e2d3c3ca122d6bc789675aeb4805756f3cc1eae93c5a74 languageName: node linkType: hard -"methods@npm:^1.1.2": +"methods@npm:^1.1.2, methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" checksum: 10/a385dd974faa34b5dd021b2bbf78c722881bf6f003bfe6d391d7da3ea1ed625d1ff10ddd13c57531f628b3e785be38d3eed10ad03cebd90b76932413df9a1820 languageName: node linkType: hard -"micromatch@npm:^4.0.8": +"micromatch@npm:^4.0.2": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -7300,14 +9472,14 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:^1.54.0": +"mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.54.0": version: 1.54.0 resolution: "mime-db@npm:1.54.0" checksum: 10/9e7834be3d66ae7f10eaa69215732c6d389692b194f876198dca79b2b90cbf96688d9d5d05ef7987b20f749b769b11c01766564264ea5f919c88b32a29011311 languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34, mime-types@npm:~2.1.35": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -7325,7 +9497,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:^1.3.4": +"mime@npm:1.6.0, mime@npm:^1.3.4": version: 1.6.0 resolution: "mime@npm:1.6.0" bin: @@ -7341,6 +9513,13 @@ __metadata: languageName: node linkType: hard +"mimic-function@npm:^5.0.0": + version: 5.0.1 + resolution: "mimic-function@npm:5.0.1" + checksum: 10/eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c + languageName: node + linkType: hard + "min-indent@npm:^1.0.0": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -7362,7 +9541,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.1.2": +"minimatch@npm:^10.2.2, minimatch@npm:^10.2.5": version: 10.2.5 resolution: "minimatch@npm:10.2.5" dependencies: @@ -7371,7 +9550,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.5": version: 3.1.5 resolution: "minimatch@npm:3.1.5" dependencies: @@ -7380,7 +9559,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": +"minimatch@npm:^9.0.4": version: 9.0.9 resolution: "minimatch@npm:9.0.9" dependencies: @@ -7467,10 +9646,10 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": - version: 7.1.2 - resolution: "minipass@npm:7.1.2" - checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6 languageName: node linkType: hard @@ -7494,15 +9673,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^1.0.4": - version: 1.0.4 - resolution: "mkdirp@npm:1.0.4" - bin: - mkdirp: bin/cmd.js - checksum: 10/d71b8dcd4b5af2fe13ecf3bd24070263489404fe216488c5ba7e38ece1f54daf219e72a833a3a2dc404331e870e9f44963a33399589490956bff003a3404d3b2 - languageName: node - linkType: hard - "modify-values@npm:^1.0.1": version: 1.0.1 resolution: "modify-values@npm:1.0.1" @@ -7524,31 +9694,31 @@ __metadata: languageName: node linkType: hard -"mongodb-connection-string-url@npm:^3.0.2": - version: 3.0.2 - resolution: "mongodb-connection-string-url@npm:3.0.2" +"mongodb-connection-string-url@npm:^7.0.0": + version: 7.0.1 + resolution: "mongodb-connection-string-url@npm:7.0.1" dependencies: - "@types/whatwg-url": "npm:^11.0.2" - whatwg-url: "npm:^14.1.0 || ^13.0.0" - checksum: 10/99ac939a67cc963b90cfe70a8e45250a8386c531be7d22ffa5d1f3e5dd2406b149fb823b91ac161e4a4a29dfac754b49bca8f6dd786cfc66ae0ca80db5f5f23d + "@types/whatwg-url": "npm:^13.0.0" + whatwg-url: "npm:^14.1.0" + checksum: 10/6b48e3d0674d375163613617bdecbc18860bfaa8635873cef8a2eb41cb04a317b5f541b8f732664d03c098f9937c02c3eb013289e124f6473a44ed3aa6daceee languageName: node linkType: hard -"mongodb@npm:^6.21.0": - version: 6.21.0 - resolution: "mongodb@npm:6.21.0" +"mongodb@npm:^7.1.1": + version: 7.1.1 + resolution: "mongodb@npm:7.1.1" dependencies: "@mongodb-js/saslprep": "npm:^1.3.0" - bson: "npm:^6.10.4" - mongodb-connection-string-url: "npm:^3.0.2" + bson: "npm:^7.1.1" + mongodb-connection-string-url: "npm:^7.0.0" peerDependencies: - "@aws-sdk/credential-providers": ^3.188.0 - "@mongodb-js/zstd": ^1.1.0 || ^2.0.0 - gcp-metadata: ^5.2.0 - kerberos: ^2.0.1 - mongodb-client-encryption: ">=6.0.0 <7" + "@aws-sdk/credential-providers": ^3.806.0 + "@mongodb-js/zstd": ^7.0.0 + gcp-metadata: ^7.0.1 + kerberos: ^7.0.0 + mongodb-client-encryption: ">=7.0.0 <7.1.0" snappy: ^7.3.2 - socks: ^2.7.1 + socks: ^2.8.6 peerDependenciesMeta: "@aws-sdk/credential-providers": optional: true @@ -7564,7 +9734,7 @@ __metadata: optional: true socks: optional: true - checksum: 10/28d2cab1c55c4cf58e410529ac6ae4c79a233adeb2147ba872d912819a0b496ee2dc5b9819ccbf0527618ced3b841e733b221fd1c627901e8e87ae60a8dc0553 + checksum: 10/e6085e2815d70e86aca235f0445b63bcdb5a29584244bffdda1be96642982a044d15ab95874d514409f6097cc8f790f5aad75009c6c315bd382e81ec06d71c01 languageName: node linkType: hard @@ -7575,6 +9745,13 @@ __metadata: languageName: node linkType: hard +"mrmime@npm:^2.0.0": + version: 2.0.1 + resolution: "mrmime@npm:2.0.1" + checksum: 10/1f966e2c05b7264209c4149ae50e8e830908eb64dd903535196f6ad72681fa109b794007288a3c2814f7a1ecf9ca192769909c0c374d974d604a8de5fc095d4a + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -7582,13 +9759,32 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d languageName: node linkType: hard +"multicast-dns@npm:^7.2.5": + version: 7.2.5 + resolution: "multicast-dns@npm:7.2.5" + dependencies: + dns-packet: "npm:^5.2.2" + thunky: "npm:^1.0.2" + bin: + multicast-dns: cli.js + checksum: 10/e9add8035fb7049ccbc87b1b069f05bb3b31e04fe057bf7d0116739d81295165afc2568291a4a962bee01a5074e475996816eed0f50c8110d652af5abb74f95a + languageName: node + linkType: hard + +"mute-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "mute-stream@npm:3.0.0" + checksum: 10/bee5db5c996a4585dbffc49e51fea10f3582d7f65441db9bc63126f16269541713c6ccb5a6fe37e08f627967b6eb28dd6b35e54a8dce53cf3837d7e010917b43 + languageName: node + linkType: hard + "nanoid@npm:^3.3.11": version: 3.3.11 resolution: "nanoid@npm:3.3.11" @@ -7628,6 +9824,13 @@ __metadata: languageName: node linkType: hard +"negotiator@npm:~0.6.4": + version: 0.6.4 + resolution: "negotiator@npm:0.6.4" + checksum: 10/d98c04a136583afd055746168f1067d58ce4bfe6e4c73ca1d339567f81ea1f7e665b5bd1e81f4771c67b6c2ea89b21cb2adaea2b16058c7dc31317778f931dab + languageName: node + linkType: hard + "neo-async@npm:^2.6.2": version: 2.6.2 resolution: "neo-async@npm:2.6.2" @@ -7650,13 +9853,11 @@ __metadata: checksum: 10/9a893f4f835fbc3908e0070f7bcacf36e37fd06be8008409b104c30df4092a0d9a29927b3a74cdbc1d34338274ba4116d597a41f573e06c29538a1a70d07413f languageName: node linkType: hard - -"node-gettext@npm:^3.0.0": - version: 3.0.1 - resolution: "node-gettext@npm:3.0.1" - dependencies: - lodash.get: "npm:^4.4.2" - checksum: 10/1387b048085f871a6466cfaebc3e695c30afa23c893fd4742ef9249bd4cca0e65ab5fae11356df5b5ff6dbc8d0f1903a45fb13571b3a7ce67f24d18a8262055e + +"node-forge@npm:^1": + version: 1.4.0 + resolution: "node-forge@npm:1.4.0" + checksum: 10/d70fd769768e646eda73343d4d4105ccb6869315d975905a22117431c04ae5b6df6c488e34ed275b1a66b50195a09b84b5c8aeca3b8605c20605fcb8e9f109d9 languageName: node linkType: hard @@ -7716,10 +9917,57 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.19": - version: 2.0.19 - resolution: "node-releases@npm:2.0.19" - checksum: 10/c2b33b4f0c40445aee56141f13ca692fa6805db88510e5bbb3baadb2da13e1293b738e638e15e4a8eb668bb9e97debb08e7a35409b477b5cc18f171d35a83045 +"node-polyfill-webpack-plugin@npm:^4.1.0": + version: 4.1.0 + resolution: "node-polyfill-webpack-plugin@npm:4.1.0" + dependencies: + node-stdlib-browser: "npm:^1.3.0" + type-fest: "npm:^4.27.0" + peerDependencies: + webpack: ">=5" + checksum: 10/e6d76301e2fc081bf64d85a6451a7320e4c4a415fb1cd19bf27ef0cb6ae472de9cca449af4d5948890b84de0859217369dd66085db5fcecfe02af15de03767d7 + languageName: node + linkType: hard + +"node-releases@npm:^2.0.36": + version: 2.0.44 + resolution: "node-releases@npm:2.0.44" + checksum: 10/c6bc49ac7f0855820e3649e7a31386929f3a3b364e40ad9e9933a9ef0858f4e129a10316037482215dbfd2b1756b6e6759403687ad3d244cb14b442e34d3afb2 + languageName: node + linkType: hard + +"node-stdlib-browser@npm:^1.3.0": + version: 1.3.1 + resolution: "node-stdlib-browser@npm:1.3.1" + dependencies: + assert: "npm:^2.0.0" + browser-resolve: "npm:^2.0.0" + browserify-zlib: "npm:^0.2.0" + buffer: "npm:^5.7.1" + console-browserify: "npm:^1.1.0" + constants-browserify: "npm:^1.0.0" + create-require: "npm:^1.1.1" + crypto-browserify: "npm:^3.12.1" + domain-browser: "npm:4.22.0" + events: "npm:^3.0.0" + https-browserify: "npm:^1.0.0" + isomorphic-timers-promises: "npm:^1.0.1" + os-browserify: "npm:^0.3.0" + path-browserify: "npm:^1.0.1" + pkg-dir: "npm:^5.0.0" + process: "npm:^0.11.10" + punycode: "npm:^1.4.1" + querystring-es3: "npm:^0.2.1" + readable-stream: "npm:^3.6.0" + stream-browserify: "npm:^3.0.0" + stream-http: "npm:^3.2.0" + string_decoder: "npm:^1.0.0" + timers-browserify: "npm:^2.0.4" + tty-browserify: "npm:0.0.1" + url: "npm:^0.11.4" + util: "npm:^0.12.4" + vm-browserify: "npm:^1.0.1" + checksum: 10/5d5ace50868ef1a8ce9718a5fc64e4b6712f8be75bf6ab71f2eb7b5815f55f20507e427eac2fdb384e372f58891eb34089af3b55d3f9b5b60b547c8581a1c30e languageName: node linkType: hard @@ -7781,22 +10029,13 @@ __metadata: languageName: node linkType: hard -"normalize-path@npm:3.0.0, normalize-path@npm:^3.0.0": +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" checksum: 10/88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20 languageName: node linkType: hard -"now-and-later@npm:^3.0.0": - version: 3.0.0 - resolution: "now-and-later@npm:3.0.0" - dependencies: - once: "npm:^1.4.0" - checksum: 10/5300d42932bac5d4f8d19bf90ebb53c3474ba615eab912770d1b8de896baea6dc7ef3b95158aaf601acfb0cd6b573bceb5fe30cf0224cb06ea227ef3e8fc7f3d - languageName: node - linkType: hard - "npm-normalize-package-bin@npm:^1.0.0": version: 1.0.1 resolution: "npm-normalize-package-bin@npm:1.0.1" @@ -7813,6 +10052,16 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^6.0.0": + version: 6.0.0 + resolution: "npm-run-path@npm:6.0.0" + dependencies: + path-key: "npm:^4.0.0" + unicorn-magic: "npm:^0.3.0" + checksum: 10/1a1b50aba6e6af7fd34a860ba2e252e245c4a59b316571a990356417c0cdf0414cabf735f7f52d9c330899cb56f0ab804a8e21fb12a66d53d7843e39ada4a3b6 + languageName: node + linkType: hard + "ntp-client@npm:^0.5.3": version: 0.5.3 resolution: "ntp-client@npm:0.5.3" @@ -7822,6 +10071,13 @@ __metadata: languageName: node linkType: hard +"object-assign@npm:^4": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f + languageName: node + linkType: hard + "object-filter-sequence@npm:^1.0.0": version: 1.0.0 resolution: "object-filter-sequence@npm:1.0.0" @@ -7892,6 +10148,13 @@ __metadata: languageName: node linkType: hard +"obuf@npm:^1.0.0, obuf@npm:^1.1.2": + version: 1.1.2 + resolution: "obuf@npm:1.1.2" + checksum: 10/53ff4ab3a13cc33ba6c856cf281f2965c0aec9720967af450e8fd06cfd50aceeefc791986a16bcefa14e7898b3ca9acdfcf15b9d9a1b9c7e1366581a8ad6e65e + languageName: node + linkType: hard + "on-exit-leak-free@npm:^2.1.0": version: 2.1.2 resolution: "on-exit-leak-free@npm:2.1.2" @@ -7908,6 +10171,13 @@ __metadata: languageName: node linkType: hard +"on-headers@npm:~1.1.0": + version: 1.1.0 + resolution: "on-headers@npm:1.1.0" + checksum: 10/98aa64629f986fb8cc4517dd8bede73c980e31208cba97f4442c330959f60ced3dc6214b83420491f5111fc7c4f4343abe2ea62c85f505cf041d67850f238776 + languageName: node + linkType: hard + "once@npm:^1.3.0, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -7935,6 +10205,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^7.0.0": + version: 7.0.0 + resolution: "onetime@npm:7.0.0" + dependencies: + mimic-function: "npm:^5.0.0" + checksum: 10/eb08d2da9339819e2f9d52cab9caf2557d80e9af8c7d1ae86e1a0fef027d00a88e9f5bd67494d350df360f7c559fbb44e800b32f310fb989c860214eacbb561c + languageName: node + linkType: hard + "only@npm:~0.0.2": version: 0.0.2 resolution: "only@npm:0.0.2" @@ -7942,30 +10221,52 @@ __metadata: languageName: node linkType: hard -"open-cli@npm:^8.0.0": - version: 8.0.0 - resolution: "open-cli@npm:8.0.0" +"open-cli@npm:^9.0.0": + version: 9.0.0 + resolution: "open-cli@npm:9.0.0" dependencies: - file-type: "npm:^18.7.0" - get-stdin: "npm:^9.0.0" - meow: "npm:^12.1.1" - open: "npm:^10.0.0" - tempy: "npm:^3.1.0" + file-type: "npm:^21.3.4" + meow: "npm:^14.1.0" + open: "npm:^11.0.0" + tempy: "npm:^3.2.0" bin: open-cli: cli.js - checksum: 10/2206724f05975c0a271c20e20de77877bc247cde89791a08e2bcc805766ae23a6eccf384dad7de20a44b285a164005decf4b16e3f9b91433be74d711ad207f7c + checksum: 10/a4b79287f20c87bdaf24d0add83c3a63d287cd157b7c5265e7d25be79bb448f341605bed9230e50df87ec4eb783ec1e2f756e4889a03e0dd19bb5ed5d221497b languageName: node linkType: hard -"open@npm:^10.0.0": - version: 10.1.0 - resolution: "open@npm:10.1.0" +"open@npm:^10.0.3": + version: 10.2.0 + resolution: "open@npm:10.2.0" dependencies: default-browser: "npm:^5.2.1" define-lazy-prop: "npm:^3.0.0" is-inside-container: "npm:^1.0.0" - is-wsl: "npm:^3.1.0" - checksum: 10/a9c4105243a1b3c5312bf2aeb678f78d31f00618b5100088ee01eed2769963ea1f2dd464ac8d93cef51bba2d911e1a9c0c34a753ec7b91d6b22795903ea6647a + wsl-utils: "npm:^0.1.0" + checksum: 10/e6ad9474734eac3549dcc7d85e952394856ccaee48107c453bd6a725b82e3b8ed5f427658935df27efa76b411aeef62888edea8a9e347e8e7c82632ec966b30e + languageName: node + linkType: hard + +"open@npm:^11.0.0": + version: 11.0.0 + resolution: "open@npm:11.0.0" + dependencies: + default-browser: "npm:^5.4.0" + define-lazy-prop: "npm:^3.0.0" + is-in-ssh: "npm:^1.0.0" + is-inside-container: "npm:^1.0.0" + powershell-utils: "npm:^0.1.0" + wsl-utils: "npm:^0.3.0" + checksum: 10/d4572cd0c1f40fe1713edce9e3812367acbfaac0e909a68be0ee0b5acf4ee0a567d01d432dc2d708a55aab684002ab47fe13c679213fb749fe096b77a5d8e51b + languageName: node + linkType: hard + +"opener@npm:^1.5.2": + version: 1.5.2 + resolution: "opener@npm:1.5.2" + bin: + opener: bin/opener-bin.js + checksum: 10/0504efcd6546e14c016a261f58a68acf9f2e5c23d84865d7d5470d5169788327ceaa5386253682f533b3fba4821748aa37ecb395f3dae7acb3261b9b22e36814 languageName: node linkType: hard @@ -7990,6 +10291,22 @@ __metadata: languageName: node linkType: hard +"ora@npm:^9.3.0": + version: 9.3.0 + resolution: "ora@npm:9.3.0" + dependencies: + chalk: "npm:^5.6.2" + cli-cursor: "npm:^5.0.0" + cli-spinners: "npm:^3.2.0" + is-interactive: "npm:^2.0.0" + is-unicode-supported: "npm:^2.1.0" + log-symbols: "npm:^7.0.1" + stdin-discarder: "npm:^0.3.1" + string-width: "npm:^8.1.0" + checksum: 10/2d02d6b80aad2cdec4dbad6e510ad4d7b8e804d6293ca90b40a6dde954ff6eed429f4260a3fe2e878fb7dc5c852a943add0172f3908b1a2daa82cece451151bd + languageName: node + linkType: hard + "original-url@npm:^1.2.3": version: 1.2.3 resolution: "original-url@npm:1.2.3" @@ -8030,6 +10347,13 @@ __metadata: languageName: node linkType: hard +"p-from-callback@npm:^2.0.0": + version: 2.0.0 + resolution: "p-from-callback@npm:2.0.0" + checksum: 10/34adbfd0d5ddcb48244f18060180fa93c0248243d9b96d62b626ca788f41b99dca004502c6f0af8cb199ca368855836c94a8fabfbaaaa04e6087f06b60491442 + languageName: node + linkType: hard + "p-lazy@npm:^3.1.0": version: 3.1.0 resolution: "p-lazy@npm:3.1.0" @@ -8107,6 +10431,17 @@ __metadata: languageName: node linkType: hard +"p-retry@npm:^6.2.0": + version: 6.2.1 + resolution: "p-retry@npm:6.2.1" + dependencies: + "@types/retry": "npm:0.12.2" + is-network-error: "npm:^1.0.0" + retry: "npm:^0.13.1" + checksum: 10/7104ef13703b155d70883b0d3654ecc03148407d2711a4516739cf93139e8bec383451e14925e25e3c1ae04dbace3ed53c26dc3853c1e9b9867fcbdde25f4cdc + languageName: node + linkType: hard + "p-timeout@npm:^4.1.0": version: 4.1.0 resolution: "p-timeout@npm:4.1.0" @@ -8151,17 +10486,16 @@ __metadata: languageName: node linkType: hard -"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.7": - version: 5.1.7 - resolution: "parse-asn1@npm:5.1.7" +"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.7, parse-asn1@npm:^5.1.9": + version: 5.1.9 + resolution: "parse-asn1@npm:5.1.9" dependencies: asn1.js: "npm:^4.10.1" browserify-aes: "npm:^1.2.0" evp_bytestokey: "npm:^1.0.3" - hash-base: "npm:~3.0" - pbkdf2: "npm:^3.1.2" + pbkdf2: "npm:^3.1.5" safe-buffer: "npm:^5.2.1" - checksum: 10/f82c079f4d9a4d33159c7682f9c516680f4d659fde8060697a6b3c1be4795976e826d53a1e5751a81ddc800e9c6d6fa4629b59f6d1f3241ac8447a00c89a67d3 + checksum: 10/bc3d616a8076fa8a9a34cab8af6905859a1bafd0c49c98132acc7d29b779c2b81d4a8fc610f5bedc9770cc4bfc323f7c939ad7413e9df6ba60cb931010c42f52 languageName: node linkType: hard @@ -8198,21 +10532,21 @@ __metadata: languageName: node linkType: hard -"parse5@npm:^6.0.0": - version: 6.0.1 - resolution: "parse5@npm:6.0.1" - checksum: 10/dfb110581f62bd1425725a7c784ae022a24669bd0efc24b58c71fc731c4d868193e2ebd85b74cde2dbb965e4dcf07059b1e651adbec1b3b5267531bd132fdb75 +"parse-ms@npm:^4.0.0": + version: 4.0.0 + resolution: "parse-ms@npm:4.0.0" + checksum: 10/673c801d9f957ff79962d71ed5a24850163f4181a90dd30c4e3666b3a804f53b77f1f0556792e8b2adbb5d58757907d1aa51d7d7dc75997c2a56d72937cbc8b7 languageName: node linkType: hard -"parseurl@npm:^1.3.2, parseurl@npm:^1.3.3": +"parseurl@npm:^1.3.2, parseurl@npm:^1.3.3, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" checksum: 10/407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2 languageName: node linkType: hard -"path-browserify@npm:^1.0.1": +"path-browserify@npm:1.0.1, path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" checksum: 10/7e7368a5207e7c6b9051ef045711d0dc3c2b6203e96057e408e6e74d09f383061010d2be95cb8593fe6258a767c3e9fc6b2bfc7ce8d48ae8c3d9f6994cca9ad8 @@ -8233,10 +10567,10 @@ __metadata: languageName: node linkType: hard -"path-expression-matcher@npm:^1.1.3, path-expression-matcher@npm:^1.2.0": - version: 1.2.0 - resolution: "path-expression-matcher@npm:1.2.0" - checksum: 10/eab23babd9a97d6cf4841a99825c3e990b70b2b29ea6529df9fb6a1f3953befbc68e9e282a373d7a75aff5dc6542d05a09ee2df036ff9bfddf5e1627b769875b +"path-expression-matcher@npm:^1.1.3, path-expression-matcher@npm:^1.5.0": + version: 1.5.0 + resolution: "path-expression-matcher@npm:1.5.0" + checksum: 10/28303bb9ee6831e6df14c10cd3f3f7b2d7c8d7f788d8bdb7440136fd696064c82a3e264999a0764d28e39f698275fc03a5493bec93c57ef4a22566280367dd64 languageName: node linkType: hard @@ -8254,6 +10588,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 10/8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7 + languageName: node + linkType: hard + "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -8271,13 +10612,13 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^2.0.0": - version: 2.0.0 - resolution: "path-scurry@npm:2.0.0" +"path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" dependencies: lru-cache: "npm:^11.0.0" minipass: "npm:^7.1.2" - checksum: 10/285ae0c2d6c34ae91dc1d5378ede21981c9a2f6de1ea9ca5a88b5a270ce9763b83dbadc7a324d512211d8d36b0c540427d3d0817030849d97a60fa840a2c59ec + checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3 languageName: node linkType: hard @@ -8288,6 +10629,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:~0.1.12": + version: 0.1.13 + resolution: "path-to-regexp@npm:0.1.13" + checksum: 10/f1e4bdedc4fd41a3b8dd76e8b2e1183105348c6b205badc072581ca63dc6aa7976a8a67feaffcf0e505f51ac12cb1a2de7f3fef3e9085b6849e76232d73ddcba + languageName: node + linkType: hard + "path-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -8297,7 +10645,7 @@ __metadata: languageName: node linkType: hard -"pbkdf2@npm:^3.1.2": +"pbkdf2@npm:^3.1.2, pbkdf2@npm:^3.1.5": version: 3.1.5 resolution: "pbkdf2@npm:3.1.5" dependencies: @@ -8311,28 +10659,21 @@ __metadata: languageName: node linkType: hard -"peek-readable@npm:^5.0.0": - version: 5.0.0 - resolution: "peek-readable@npm:5.0.0" - checksum: 10/d342f02dd0c8a6b4bd0e7519a93d545b2b19375200e79a7431f0f1ec3f91e22b2217fa3a15cde95f6ab388ce6fce8aae75794d84b9b39c5836eb7c5f55e7ee9e - languageName: node - linkType: hard - -"picocolors@npm:^1.1.1": +"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.3.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": version: 2.3.2 resolution: "picomatch@npm:2.3.2" checksum: 10/b788ef8148a2415b9dec12f0bb350ae6a5830f8f1950e472abc2f5225494debf7d1b75eb031df0ceaea9e8ec3e7bad599e8dbf3c60d61b42be429ba41bff4426 languageName: node linkType: hard -"picomatch@npm:^4.0.2, picomatch@npm:^4.0.3": +"picomatch@npm:^4.0.2, picomatch@npm:^4.0.3, picomatch@npm:^4.0.4": version: 4.0.4 resolution: "picomatch@npm:4.0.4" checksum: 10/f6ef80a3590827ce20378ae110ac78209cc4f74d39236370f1780f957b7ee41c12acde0e4651b90f39983506fd2f5e449994716f516db2e9752924aff8de93ce @@ -8407,6 +10748,15 @@ __metadata: languageName: node linkType: hard +"pkg-dir@npm:^5.0.0": + version: 5.0.0 + resolution: "pkg-dir@npm:5.0.0" + dependencies: + find-up: "npm:^5.0.0" + checksum: 10/b167bb8dac7bbf22b1d5e30ec223e6b064b84b63010c9d49384619a36734caf95ed23ad23d4f9bd975e8e8082b60a83395f43a89bb192df53a7c25a38ecb57d9 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.1.0 resolution: "possible-typed-array-names@npm:1.1.0" @@ -8414,6 +10764,13 @@ __metadata: languageName: node linkType: hard +"powershell-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "powershell-utils@npm:0.1.0" + checksum: 10/4cc0846bc903ef9c8ac8cc9d178185d5972160a6c8776d44cf4c27ce31c0b614fc7cd20a53e8fcaf7f5296cdb34087a5d4396bdd863492972c84f76f3cadef67 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -8421,32 +10778,41 @@ __metadata: languageName: node linkType: hard -"prettier-linter-helpers@npm:^1.0.0": - version: 1.0.0 - resolution: "prettier-linter-helpers@npm:1.0.0" +"prettier-linter-helpers@npm:^1.0.1": + version: 1.0.1 + resolution: "prettier-linter-helpers@npm:1.0.1" dependencies: fast-diff: "npm:^1.1.2" - checksum: 10/00ce8011cf6430158d27f9c92cfea0a7699405633f7f1d4a45f07e21bf78e99895911cbcdc3853db3a824201a7c745bd49bfea8abd5fb9883e765a90f74f8392 + checksum: 10/2dc35f5036a35f4c4f5e645887edda1436acb63687a7f12b2383e0a6f3c1f76b8a0a4709fe4d82e19157210feb5984b159bb714d43290022911ab53d606474ec languageName: node linkType: hard -"prettier@npm:^3.8.1": - version: 3.8.1 - resolution: "prettier@npm:3.8.1" +"prettier@npm:^3.8.3": + version: 3.8.3 + resolution: "prettier@npm:3.8.3" bin: prettier: bin/prettier.cjs - checksum: 10/3da1cf8c1ef9bea828aa618553696c312e951f810bee368f6887109b203f18ee869fe88f66e65f9cf60b7cb1f2eae859892c860a300c062ff8ec69c381fc8dbd + checksum: 10/4b3b12cbb29e4c96bed936e5d070167552500c18d37676fb3e0caae6199c42860662608e4dc116230698f6e2bb0267ef2548158224c92d40f188d309d72fdd6f languageName: node linkType: hard -"pretty-format@npm:30.2.0, pretty-format@npm:^30.0.0": - version: 30.2.0 - resolution: "pretty-format@npm:30.2.0" +"pretty-format@npm:30.3.0, pretty-format@npm:^30.0.0": + version: 30.3.0 + resolution: "pretty-format@npm:30.3.0" dependencies: "@jest/schemas": "npm:30.0.5" ansi-styles: "npm:^5.2.0" react-is: "npm:^18.3.1" - checksum: 10/725890d648e3400575eebc99a334a4cd1498e0d36746313913706bbeea20ada27e17c184a3cd45c50f705c16111afa829f3450233fc0fda5eed293c69757e926 + checksum: 10/b288db630841f2464554c5cfa7d7faf519ad7b5c05c3818e764c7cb486bcf59f240ea5576c748f8ca6625623c5856a8906642255bbe89d6cfa1a9090b0fbc6b9 + languageName: node + linkType: hard + +"pretty-ms@npm:^9.2.0": + version: 9.3.0 + resolution: "pretty-ms@npm:9.3.0" + dependencies: + parse-ms: "npm:^4.0.0" + checksum: 10/beb4e04dc17071885b827e3f33d36be279791f2f36a8c29a45c77e59979dad79a5d7e5211922c72a3f6f109bb64a707d70fcdba6746e077122afcd88ce202e98 languageName: node linkType: hard @@ -8498,10 +10864,20 @@ __metadata: languageName: node linkType: hard -"proxy-from-env@npm:^1.1.0": - version: 1.1.0 - resolution: "proxy-from-env@npm:1.1.0" - checksum: 10/f0bb4a87cfd18f77bc2fba23ae49c3b378fb35143af16cc478171c623eebe181678f09439707ad80081d340d1593cd54a33a0113f3ccb3f4bc9451488780ee23 +"proxy-addr@npm:~2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: "npm:0.2.0" + ipaddr.js: "npm:1.9.1" + checksum: 10/f24a0c80af0e75d31e3451398670d73406ec642914da11a2965b80b1898ca6f66a0e3e091a11a4327079b2b268795f6fa06691923fef91887215c3d0e8ea3f68 + languageName: node + linkType: hard + +"proxy-from-env@npm:^2.1.0": + version: 2.1.0 + resolution: "proxy-from-env@npm:2.1.0" + checksum: 10/fbbaf4dab2a6231dc9e394903a5f66f20475e36b734335790b46feb9da07c37d6b32e2c02e3e2ea4d4b23774c53d8562e5b7cc73282cb43f4a597b7eacaee2ee languageName: node linkType: hard @@ -8540,12 +10916,12 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.12.3, qs@npm:^6.5.2, qs@npm:~6.14.0": - version: 6.14.2 - resolution: "qs@npm:6.14.2" +"qs@npm:^6.12.3, qs@npm:^6.5.2, qs@npm:~6.15.1": + version: 6.15.1 + resolution: "qs@npm:6.15.1" dependencies: side-channel: "npm:^1.1.0" - checksum: 10/682933a85bb4b7bd0d66e13c0a40d9e612b5e4bcc2cb9238f711a9368cd22d91654097a74fff93551e58146db282c56ac094957dfdc60ce64ea72c3c9d7779ac + checksum: 10/ec10b9957446b3f4a38000940f6374720b4e2985209b89df197066038c951472ea24cd98d6bc6df73a0cbec75bc056f638032e3fb447345017ff7e0f0a2693ac languageName: node linkType: hard @@ -8563,20 +10939,6 @@ __metadata: languageName: node linkType: hard -"queue-microtask@npm:^1.2.2": - version: 1.2.3 - resolution: "queue-microtask@npm:1.2.3" - checksum: 10/72900df0616e473e824202113c3df6abae59150dfb73ed13273503127235320e9c8ca4aaaaccfd58cf417c6ca92a6e68ee9a5c3182886ae949a768639b388a7b - languageName: node - linkType: hard - -"queue-tick@npm:^1.0.1": - version: 1.0.1 - resolution: "queue-tick@npm:1.0.1" - checksum: 10/f447926c513b64a857906f017a3b350f7d11277e3c8d2a21a42b7998fa1a613d7a829091e12d142bb668905c8f68d8103416c7197856efb0c72fa835b8e254b5 - languageName: node - linkType: hard - "quick-format-unescaped@npm:^4.0.3": version: 4.0.4 resolution: "quick-format-unescaped@npm:4.0.4" @@ -8610,7 +10972,7 @@ __metadata: languageName: node linkType: hard -"range-parser@npm:^1.2.0": +"range-parser@npm:^1.2.0, range-parser@npm:^1.2.1, range-parser@npm:~1.2.1": version: 1.2.1 resolution: "range-parser@npm:1.2.1" checksum: 10/ce21ef2a2dd40506893157970dc76e835c78cf56437e26e19189c48d5291e7279314477b06ac38abd6a401b661a6840f7b03bd0b1249da9b691deeaa15872c26 @@ -8629,6 +10991,28 @@ __metadata: languageName: node linkType: hard +"react-i18next@npm:^17.0.4": + version: 17.0.6 + resolution: "react-i18next@npm:17.0.6" + dependencies: + "@babel/runtime": "npm:^7.29.2" + html-parse-stringify: "npm:^3.0.1" + use-sync-external-store: "npm:^1.6.0" + peerDependencies: + i18next: ">= 26.0.1" + react: ">= 16.8.0" + typescript: ^5 || ^6 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + checksum: 10/976d8ac0993ebb186d37f485a9c2e1d8b67c05e6a5d0f22a28a5d8e21c9aa368b46c6ea1c8d0feacbb1ea51375e15496d325b8460e468009671e1456021984fb + languageName: node + linkType: hard + "react-is@npm:^18.3.1": version: 18.3.1 resolution: "react-is@npm:18.3.1" @@ -8636,6 +11020,13 @@ __metadata: languageName: node linkType: hard +"react@npm:^19.2.5": + version: 19.2.5 + resolution: "react@npm:19.2.5" + checksum: 10/1c3c7ffecb90b7f89a5c3ef635e6811f3a84600097f203b918150cb7e6b0a52915e858e5b4c82317a520dffccfa46ee4819ccf92c59c5b2d6c25cffe258dd20c + languageName: node + linkType: hard + "read-installed@npm:~4.0.3": version: 4.0.3 resolution: "read-installed@npm:4.0.3" @@ -8734,18 +11125,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": - version: 3.6.2 - resolution: "readable-stream@npm:3.6.2" - dependencies: - inherits: "npm:^2.0.3" - string_decoder: "npm:^1.1.1" - util-deprecate: "npm:^1.0.1" - checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048 - languageName: node - linkType: hard - -"readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.1, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -8760,25 +11140,27 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^4.0.0": - version: 4.5.2 - resolution: "readable-stream@npm:4.5.2" +"readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048 + languageName: node + linkType: hard + +"readable-stream@npm:^4.0.0, readable-stream@npm:^4.5.2": + version: 4.7.0 + resolution: "readable-stream@npm:4.7.0" dependencies: abort-controller: "npm:^3.0.0" buffer: "npm:^6.0.3" events: "npm:^3.3.0" process: "npm:^0.11.10" string_decoder: "npm:^1.3.0" - checksum: 10/01b128a559c5fd76a898495f858cf0a8839f135e6a69e3409f986e88460134791657eb46a2ff16826f331682a3c4d0c5a75cef5e52ef259711021ba52b1c2e82 - languageName: node - linkType: hard - -"readable-web-to-node-stream@npm:^3.0.2": - version: 3.0.2 - resolution: "readable-web-to-node-stream@npm:3.0.2" - dependencies: - readable-stream: "npm:^3.6.0" - checksum: 10/d3a5bf9d707c01183d546a64864aa63df4d9cb835dfd2bf89ac8305e17389feef2170c4c14415a10d38f9b9bfddf829a57aaef7c53c8b40f11d499844bf8f1a4 + checksum: 10/bdf096c8ff59452ce5d08f13da9597f9fcfe400b4facfaa88e74ec057e5ad1fdfa140ffe28e5ed806cf4d2055f0b812806e962bca91dce31bc4cef08e53be3a4 languageName: node linkType: hard @@ -8794,6 +11176,22 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:^5.0.0": + version: 5.0.0 + resolution: "readdirp@npm:5.0.0" + checksum: 10/a17a591b51d8b912083660df159e8bd17305dc1a9ef27c869c818bd95ff59e3a6496f97e91e724ef433e789d559d24e39496ea1698822eb5719606dc9c1a923d + languageName: node + linkType: hard + +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10/196b30ef6ccf9b6e18c4e1724b7334f72a093d011a99f3b5920470f0b3406a51770867b3e1ae9711f227ef7a7065982f6ee2ce316746b2cb42c88efe44297fe7 + languageName: node + linkType: hard + "real-require@npm:^0.2.0": version: 0.2.0 resolution: "real-require@npm:0.2.0" @@ -8829,20 +11227,6 @@ __metadata: languageName: node linkType: hard -"remove-trailing-separator@npm:^1.1.0": - version: 1.1.0 - resolution: "remove-trailing-separator@npm:1.1.0" - checksum: 10/d3c20b5a2d987db13e1cca9385d56ecfa1641bae143b620835ac02a6b70ab88f68f117a0021838db826c57b31373d609d52e4f31aca75fc490c862732d595419 - languageName: node - linkType: hard - -"replace-ext@npm:^2.0.0": - version: 2.0.0 - resolution: "replace-ext@npm:2.0.0" - checksum: 10/ed640ac90d24cce4be977642847d138908d430049cc097633be33b072143515cc7d29699675a0c35f6dc3c3c73cb529ed352d59649cf15931740eb31ae083c1e - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -8850,6 +11234,13 @@ __metadata: languageName: node linkType: hard +"require-from-string@npm:^2.0.2": + version: 2.0.2 + resolution: "require-from-string@npm:2.0.2" + checksum: 10/839a3a890102a658f4cb3e7b2aa13a1f80a3a976b512020c3d1efc418491c48a886b6e481ea56afc6c4cb5eef678f23b2a4e70575e7534eccadf5e30ed2e56eb + languageName: node + linkType: hard + "require-in-the-middle@npm:^8.0.0": version: 8.0.1 resolution: "require-in-the-middle@npm:8.0.1" @@ -8890,15 +11281,6 @@ __metadata: languageName: node linkType: hard -"resolve-options@npm:^2.0.0": - version: 2.0.0 - resolution: "resolve-options@npm:2.0.0" - dependencies: - value-or-function: "npm:^4.0.0" - checksum: 10/b28584cc089099af42e36292c32bd9af8bc9e28e3ca73c172c0a172d7ed5afb01c75cc2275268c327dceba77a5555b33fbd55617be138874040279fe6ff02fbf - languageName: node - linkType: hard - "resolve-path@npm:^1.4.0": version: 1.4.0 resolution: "resolve-path@npm:1.4.0" @@ -8916,29 +11298,41 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.10.0": - version: 1.22.6 - resolution: "resolve@npm:1.22.6" +"resolve@npm:^1.10.0, resolve@npm:^1.17.0": + version: 1.22.12 + resolution: "resolve@npm:1.22.12" dependencies: - is-core-module: "npm:^2.13.0" + es-errors: "npm:^1.3.0" + is-core-module: "npm:^2.16.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/b57acf016c94aded442f3c92dda4c4e9370ebe5b337ca2dbada3c022ce7c75cd20d5e31a855f884321c7379d6f2c7e640852024ae83f976e15367a1c4cf14de5 + checksum: 10/1d2a081e4b7198e2a70abd7bbbf8aea5380c2d074b6c870035aab50ebfb7312b6492b3588e752faef83a75147862a3d3e09b222bc9afd536804181fd3a515ef9 languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin": - version: 1.22.6 - resolution: "resolve@patch:resolve@npm%3A1.22.6#optional!builtin::version=1.22.6&hash=c3c19d" +"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin": + version: 1.22.12 + resolution: "resolve@patch:resolve@npm%3A1.22.12#optional!builtin::version=1.22.12&hash=c3c19d" dependencies: - is-core-module: "npm:^2.13.0" + es-errors: "npm:^1.3.0" + is-core-module: "npm:^2.16.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/d63580488eaffef80d16930ed76ffc786d6f51ac02e5821a8fb54a9c7bef4d355472123abdd36fbc0c68704495e09581f0feba75dc4b0b946818f96ece5c3e2a + checksum: 10/f80ad2c2b6820331cbe079198a184ffce322cfeca140065118066276bc08b03d5fa2c1ce652aeb584ec74050d1f656f46f034cc0dd9300452c5ab7866907f8c0 + languageName: node + linkType: hard + +"restore-cursor@npm:^5.0.0": + version: 5.1.0 + resolution: "restore-cursor@npm:5.1.0" + dependencies: + onetime: "npm:^7.0.0" + signal-exit: "npm:^4.1.0" + checksum: 10/838dd54e458d89cfbc1a923b343c1b0f170a04100b4ce1733e97531842d7b440463967e521216e8ab6c6f8e89df877acc7b7f4c18ec76e99fb9bf5a60d358d2c languageName: node linkType: hard @@ -8949,10 +11343,10 @@ __metadata: languageName: node linkType: hard -"reusify@npm:^1.0.4": - version: 1.0.4 - resolution: "reusify@npm:1.0.4" - checksum: 10/14222c9e1d3f9ae01480c50d96057228a8524706db79cdeb5a2ce5bb7070dd9f409a6f84a02cbef8cdc80d39aef86f2dd03d155188a1300c599b05437dcd2ffb +"retry@npm:^0.13.1": + version: 0.13.1 + resolution: "retry@npm:0.13.1" + checksum: 10/6125ec2e06d6e47e9201539c887defba4e47f63471db304c59e4b82fc63c8e89ca06a77e9d34939a9a42a76f00774b2f46c0d4a4cbb3e287268bd018ed69426d languageName: node linkType: hard @@ -8966,6 +11360,13 @@ __metadata: languageName: node linkType: hard +"rslog@npm:^1.3.2": + version: 1.3.2 + resolution: "rslog@npm:1.3.2" + checksum: 10/69f575564ac93be5e3a7f3b7dd086e93fcca96e25dbb2fbd19ff0db9d0f2ace50b782b06b03c6b3f351252e45a80df85caf0e1ecdcca694c41f937efd67ac6bd + languageName: node + linkType: hard + "run-applescript@npm:^7.0.0": version: 7.0.0 resolution: "run-applescript@npm:7.0.0" @@ -8973,12 +11374,19 @@ __metadata: languageName: node linkType: hard -"run-parallel@npm:^1.1.9": - version: 1.2.0 - resolution: "run-parallel@npm:1.2.0" +"run-async@npm:^4.0.6": + version: 4.0.6 + resolution: "run-async@npm:4.0.6" + checksum: 10/d23929e36d0422b871a8964d5cfcb1b88295950ea5f72e1dfed458d4c3f3a33a7395e08167d8a4446f2110cfaac7d7653d9c804d2becab8afa8a63e16b97da81 + languageName: node + linkType: hard + +"rxjs@npm:^7.8.2": + version: 7.8.2 + resolution: "rxjs@npm:7.8.2" dependencies: - queue-microtask: "npm:^1.2.2" - checksum: 10/cb4f97ad25a75ebc11a8ef4e33bb962f8af8516bb2001082ceabd8902e15b98f4b84b4f8a9b222e5d57fc3bd1379c483886ed4619367a7680dad65316993021d + tslib: "npm:^2.1.0" + checksum: 10/03dff09191356b2b87d94fbc1e97c4e9eb3c09d4452399dddd451b09c2f1ba8d56925a40af114282d7bc0c6fe7514a2236ca09f903cf70e4bbf156650dddb49d languageName: node linkType: hard @@ -9001,7 +11409,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 @@ -9026,7 +11434,7 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": +"safer-buffer@npm:2.1.2, safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: 10/7eaf7a0cf37cc27b42fb3ef6a9b1df6e93a1c6d98c6c6702b02fe262d5fcbd89db63320793b99b21cb5348097d0a53de81bd5f4e8b86e20cc9412e3f1cfb4e83 @@ -9040,6 +11448,35 @@ __metadata: languageName: node linkType: hard +"schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0": + version: 4.3.3 + resolution: "schema-utils@npm:4.3.3" + dependencies: + "@types/json-schema": "npm:^7.0.9" + ajv: "npm:^8.9.0" + ajv-formats: "npm:^2.1.1" + ajv-keywords: "npm:^5.1.0" + checksum: 10/dba77a46ad7ff0c906f7f09a1a61109e6cb56388f15a68070b93c47a691f516c6a3eb454f81a8cceb0a0e55b87f8b05770a02bfb1f4e0a3143b5887488b2f900 + languageName: node + linkType: hard + +"select-hose@npm:^2.0.0": + version: 2.0.0 + resolution: "select-hose@npm:2.0.0" + checksum: 10/08cdd629a394d20e9005e7956f0624307c702cf950cc0458953e9b87ea961d3b1b72ac02266bdb93ac1eec4fcf42b41db9cabe93aa2b7683d71513d133c44fb5 + languageName: node + linkType: hard + +"selfsigned@npm:^2.4.1": + version: 2.4.1 + resolution: "selfsigned@npm:2.4.1" + dependencies: + "@types/node-forge": "npm:^1.3.0" + node-forge: "npm:^1" + checksum: 10/52536623f1cfdeb2f8b9198377f2ce7931c677ea69421238d1dc1ea2983bbe258e56c19e7d1af87035cad7270f19b7e996eaab1212e724d887722502f68e17f2 + languageName: node + linkType: hard + "semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -9058,12 +11495,60 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.2, semver@npm:^7.7.3": - version: 7.7.3 - resolution: "semver@npm:7.7.3" +"semver@npm:^7.0.0, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.3, semver@npm:^7.7.2, semver@npm:^7.7.3, semver@npm:^7.7.4": + version: 7.7.4 + resolution: "semver@npm:7.7.4" bin: semver: bin/semver.js - checksum: 10/8dbc3168e057a38fc322af909c7f5617483c50caddba135439ff09a754b20bdd6482a5123ff543dad4affa488ecf46ec5fb56d61312ad20bb140199b88dfaea9 + checksum: 10/26bdc6d58b29528f4142d29afb8526bc335f4fc04c4a10f2b98b217f277a031c66736bf82d3d3bb354a2f6a3ae50f18fd62b053c4ac3f294a3d10a61f5075b75 + languageName: node + linkType: hard + +"send@npm:~0.19.0, send@npm:~0.19.1": + version: 0.19.2 + resolution: "send@npm:0.19.2" + dependencies: + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + fresh: "npm:~0.5.2" + http-errors: "npm:~2.0.1" + mime: "npm:1.6.0" + ms: "npm:2.1.3" + on-finished: "npm:~2.4.1" + range-parser: "npm:~1.2.1" + statuses: "npm:~2.0.2" + checksum: 10/e932a592f62c58560b608a402d52333a8ae98a5ada076feb5db1d03adaa77c3ca32a7befa1c4fd6dedc186e88f342725b0cb4b3d86835eaf834688b259bef18d + languageName: node + linkType: hard + +"serve-index@npm:^1.9.1": + version: 1.9.2 + resolution: "serve-index@npm:1.9.2" + dependencies: + accepts: "npm:~1.3.8" + batch: "npm:0.6.1" + debug: "npm:2.6.9" + escape-html: "npm:~1.0.3" + http-errors: "npm:~1.8.0" + mime-types: "npm:~2.1.35" + parseurl: "npm:~1.3.3" + checksum: 10/fdfada071e795da265845acca05be9b498443cb5b84f8c9fd4632f01ea107ecca110725a7963a2b4b3146ec01f41c5f95df4405ff61eda13e6f229474a9ed5a6 + languageName: node + linkType: hard + +"serve-static@npm:~1.16.2": + version: 1.16.3 + resolution: "serve-static@npm:1.16.3" + dependencies: + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + parseurl: "npm:~1.3.3" + send: "npm:~0.19.1" + checksum: 10/149d6718dd9e53166784d0a65535e21a7c01249d9c51f57224b786a7306354c6807e7811a9f6c143b45c863b1524721fca2f52b5c81a8b5194e3dde034a03b9c languageName: node linkType: hard @@ -9158,6 +11643,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:^1.8.3": + version: 1.8.3 + resolution: "shell-quote@npm:1.8.3" + checksum: 10/5473e354637c2bd698911224129c9a8961697486cff1fb221f234d71c153fc377674029b0223d1d3c953a68d451d79366abfe53d1a0b46ee1f28eb9ade928f4c + languageName: node + linkType: hard + "side-channel-list@npm:^1.0.0": version: 1.0.0 resolution: "side-channel-list@npm:1.0.0" @@ -9213,13 +11705,24 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f languageName: node linkType: hard +"sirv@npm:^2.0.3": + version: 2.0.4 + resolution: "sirv@npm:2.0.4" + dependencies: + "@polka/url": "npm:^1.0.0-next.24" + mrmime: "npm:^2.0.0" + totalist: "npm:^3.0.0" + checksum: 10/24f42cf06895017e589c9d16fc3f1c6c07fe8b0dbafce8a8b46322cfba67b7f2498610183954cb0e9d089c8cb60002a7ee7e8bca6a91a0d7042bfbc3473c95c3 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -9241,6 +11744,52 @@ __metadata: languageName: node linkType: hard +"socket.io-adapter@npm:~2.5.2": + version: 2.5.6 + resolution: "socket.io-adapter@npm:2.5.6" + dependencies: + debug: "npm:~4.4.1" + ws: "npm:~8.18.3" + checksum: 10/2bbefcc6f3d5dedab3105af03091b8863079173ab5610118d0ce94a0cf40fd87956c304f4f06445e361296b1966034be1ff0ba4e87b3c2baec216bbdec43b6e6 + languageName: node + linkType: hard + +"socket.io-parser@npm:~4.2.4": + version: 4.2.6 + resolution: "socket.io-parser@npm:4.2.6" + dependencies: + "@socket.io/component-emitter": "npm:~3.1.0" + debug: "npm:~4.4.1" + checksum: 10/cf8f3e9feee42b2405065bccef732894a0b07b7cb734c4954eb8876d7a0038e7df6e6d06fab3a25f816f5d996917391833d5f06136e8c8c6fbecf5da962c1c9e + languageName: node + linkType: hard + +"socket.io@npm:4.8.1": + version: 4.8.1 + resolution: "socket.io@npm:4.8.1" + dependencies: + accepts: "npm:~1.3.4" + base64id: "npm:~2.0.0" + cors: "npm:~2.8.5" + debug: "npm:~4.3.2" + engine.io: "npm:~6.6.0" + socket.io-adapter: "npm:~2.5.2" + socket.io-parser: "npm:~4.2.4" + checksum: 10/b9b362b7f63fc7ebb58482b8a3ade6c971da7783b7611dfeebaa8b02be23cb948137ec218491ccda8be57e434e97d65b64edf1e9811e5245b23a888d41636f4a + languageName: node + linkType: hard + +"sockjs@npm:^0.3.24": + version: 0.3.24 + resolution: "sockjs@npm:0.3.24" + dependencies: + faye-websocket: "npm:^0.11.3" + uuid: "npm:^8.3.2" + websocket-driver: "npm:^0.7.4" + checksum: 10/36312ec9772a0e536b69b72e9d1c76bd3d6ecf885c5d8fd6e59811485c916b8ce75f46ec57532f436975815ee14aa9a0e22ae3d9e5c0b18ea37b56d0aaaf439c + languageName: node + linkType: hard + "socks-proxy-agent@npm:^8.0.3": version: 8.0.5 resolution: "socks-proxy-agent@npm:8.0.5" @@ -9271,13 +11820,6 @@ __metadata: languageName: node linkType: hard -"sortobject@npm:^4.0.0": - version: 4.16.0 - resolution: "sortobject@npm:4.16.0" - checksum: 10/58473d0fc46c06050eadc1e7c0ea8cc9444c627f8f5ac85f4aeb0236198999970dd731be9d88d1cbd0a8f9108eb6646727b488661eedfb21826c3a95d4531d36 - languageName: node - linkType: hard - "source-map-support@npm:0.5.13": version: 0.5.13 resolution: "source-map-support@npm:0.5.13" @@ -9295,6 +11837,13 @@ __metadata: languageName: node linkType: hard +"source-map@npm:^0.7.6": + version: 0.7.6 + resolution: "source-map@npm:0.7.6" + checksum: 10/c8d2da7c57c14f3fd7568f764b39ad49bbf9dd7632b86df3542b31fed117d4af2fb74a4f886fc06baf7a510fee68e37998efc3080aacdac951c36211dc29a7a3 + languageName: node + linkType: hard + "source-map@npm:^0.8.0-beta.0": version: 0.8.0-beta.0 resolution: "source-map@npm:0.8.0-beta.0" @@ -9376,6 +11925,33 @@ __metadata: languageName: node linkType: hard +"spdy-transport@npm:^3.0.0": + version: 3.0.0 + resolution: "spdy-transport@npm:3.0.0" + dependencies: + debug: "npm:^4.1.0" + detect-node: "npm:^2.0.4" + hpack.js: "npm:^2.1.6" + obuf: "npm:^1.1.2" + readable-stream: "npm:^3.0.6" + wbuf: "npm:^1.7.3" + checksum: 10/b93b606b209ca785456bd850b8925f21a76522ee5b46701235ecff3eba17686560c27575f91863842dc843a39772f6d2f5a8755df9eaff0924d20598df18828d + languageName: node + linkType: hard + +"spdy@npm:^4.0.2": + version: 4.0.2 + resolution: "spdy@npm:4.0.2" + dependencies: + debug: "npm:^4.1.0" + handle-thing: "npm:^2.0.0" + http-deceiver: "npm:^1.2.7" + select-hose: "npm:^2.0.0" + spdy-transport: "npm:^3.0.0" + checksum: 10/d29b89e48e7d762e505a2f83b1bc2c92268bd518f1b411864ab42a9e032e387d10467bbce0d8dbf8647bf4914a063aa1d303dff85e248f7a57f81a7b18ac34ef + languageName: node + linkType: hard + "split2@npm:^3.2.2": version: 3.2.2 resolution: "split2@npm:3.2.2" @@ -9454,13 +12030,20 @@ __metadata: languageName: node linkType: hard -"statuses@npm:^2.0.1, statuses@npm:~2.0.2": +"statuses@npm:^2.0.1, statuses@npm:~2.0.1, statuses@npm:~2.0.2": version: 2.0.2 resolution: "statuses@npm:2.0.2" checksum: 10/6927feb50c2a75b2a4caab2c565491f7a93ad3d8dbad7b1398d52359e9243a20e2ebe35e33726dee945125ef7a515e9097d8a1b910ba2bbd818265a2f6c39879 languageName: node linkType: hard +"stdin-discarder@npm:^0.3.1": + version: 0.3.2 + resolution: "stdin-discarder@npm:0.3.2" + checksum: 10/63c6912146efe079fd048ecc02e5c3bf5aaa4cb268ad4e365603d845444dd3048daa45868c2690c5fe2d020ba47273c8a20df684a8c424fb4bd7f359c795c2f5 + languageName: node + linkType: hard + "stream-browserify@npm:^3.0.0": version: 3.0.0 resolution: "stream-browserify@npm:3.0.0" @@ -9480,15 +12063,6 @@ __metadata: languageName: node linkType: hard -"stream-composer@npm:^1.0.2": - version: 1.0.2 - resolution: "stream-composer@npm:1.0.2" - dependencies: - streamx: "npm:^2.13.2" - checksum: 10/338b8e088f2eb2c91b0e06907db436525da3620991b13499e57441548e62d3585be185505901b0380cad425889572794e5fe178dd326f5efde654b3ab26df3d3 - languageName: node - linkType: hard - "stream-http@npm:^3.2.0": version: 3.2.0 resolution: "stream-http@npm:3.2.0" @@ -9501,16 +12075,6 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0": - version: 2.15.1 - resolution: "streamx@npm:2.15.1" - dependencies: - fast-fifo: "npm:^1.1.0" - queue-tick: "npm:^1.0.1" - checksum: 10/5c5143d832b4d4c2cba09d3e77dcc099f62bfc44bffac38e7b196cdd7f17dcd46bc2012c614fad934c0d706712c2e9455e485435810504cf748906b2f1746837 - languageName: node - linkType: hard - "string-length@npm:^4.0.2": version: 4.0.2 resolution: "string-length@npm:4.0.2" @@ -9543,6 +12107,16 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^8.1.0": + version: 8.2.0 + resolution: "string-width@npm:8.2.0" + dependencies: + get-east-asian-width: "npm:^1.5.0" + strip-ansi: "npm:^7.1.2" + checksum: 10/c4f62877ec08fca155e84a260eb4f58f473cfe5169bd1c1e21ffb563d8e0b7f6d705cc3d250f2ed6bb4f30ee9732ad026f54afaac77aa487e3d1dc1b1969e51b + languageName: node + linkType: hard + "string.prototype.trim@npm:^1.2.8": version: 1.2.8 resolution: "string.prototype.trim@npm:1.2.8" @@ -9576,7 +12150,7 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": +"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" dependencies: @@ -9603,12 +12177,12 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1": - version: 7.1.2 - resolution: "strip-ansi@npm:7.1.2" +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.2": + version: 7.2.0 + resolution: "strip-ansi@npm:7.2.0" dependencies: - ansi-regex: "npm:^6.0.1" - checksum: 10/db0e3f9654e519c8a33c50fc9304d07df5649388e7da06d3aabf66d29e5ad65d5e6315d8519d409c15b32fa82c1df7e11ed6f8cd50b0e4404463f0c9d77c8d0b + ansi-regex: "npm:^6.2.2" + checksum: 10/96da3bc6d73cfba1218625a3d66cf7d37a69bf0920d8735b28f9eeaafcdb6c1fe8440e1ae9eb1ba0ca355dbe8702da872e105e2e939fa93e7851b3cb5dd7d316 languageName: node linkType: hard @@ -9633,6 +12207,13 @@ __metadata: languageName: node linkType: hard +"strip-final-newline@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-final-newline@npm:4.0.0" + checksum: 10/b5fe48f695d74863153a3b3155220e6e9bf51f4447832998c8edec38e6559b3af87a9fe5ac0df95570a78a26f5fa91701358842eab3c15480e27980b154a145f + languageName: node + linkType: hard + "strip-indent@npm:^3.0.0": version: 3.0.0 resolution: "strip-indent@npm:3.0.0" @@ -9649,20 +12230,19 @@ __metadata: languageName: node linkType: hard -"strnum@npm:^2.2.0": - version: 2.2.2 - resolution: "strnum@npm:2.2.2" - checksum: 10/c55813cfded750dc84556b4881ffc7cee91382ff15a48f1fba0ff7a678e1640ed96ca40806fbd55724940fd7d51cf752469b2d862e196e4adefb6c7d5d9cd73b +"strnum@npm:^2.2.3": + version: 2.2.3 + resolution: "strnum@npm:2.2.3" + checksum: 10/fb70206301858c319f59ed34fecedf90ac3b821692c2accd403d9d4a3384223a09df8fd92b130bbd4e885b67b7790715c003405ce5f959d9cabbf07d41d62aa8 languageName: node linkType: hard -"strtok3@npm:^7.0.0": - version: 7.0.0 - resolution: "strtok3@npm:7.0.0" +"strtok3@npm:^10.3.4": + version: 10.3.5 + resolution: "strtok3@npm:10.3.5" dependencies: "@tokenizer/token": "npm:^0.3.0" - peek-readable: "npm:^5.0.0" - checksum: 10/4f2269679fcfce1e9fe0600eff361ea4c687ae0a0e8d9dab6703811071cd92545cbcb32d4ace3d3aa591f777cec1a3e8aeecd5efd71ae216fd2962a7a238b4ab + checksum: 10/7279dc97a7207a5664ea07cf5304b94968db4f02d64d2732be8e7a3a31a876375126749cd36a00d0bd54c891875f3e47175f8194d40c64118f3265dbc241aaca languageName: node linkType: hard @@ -9709,7 +12289,7 @@ __metadata: languageName: node linkType: hard -"synckit@npm:^0.11.8": +"synckit@npm:^0.11.12, synckit@npm:^0.11.8": version: 0.11.12 resolution: "synckit@npm:0.11.12" dependencies: @@ -9718,20 +12298,17 @@ __metadata: languageName: node linkType: hard -"synckit@npm:^0.9.1": - version: 0.9.2 - resolution: "synckit@npm:0.9.2" - dependencies: - "@pkgr/core": "npm:^0.1.0" - tslib: "npm:^2.6.2" - checksum: 10/d45c4288be9c0232343650643892a7edafb79152c0c08d7ae5d33ca2c296b67a0e15f8cb5c9153969612c4ea5cd5686297542384aab977db23cfa6653fe02027 +"tapable@npm:2.3.0": + version: 2.3.0 + resolution: "tapable@npm:2.3.0" + checksum: 10/496a841039960533bb6e44816a01fffc2a1eb428bb2051ecab9e87adf07f19e1f937566cbbbb09dceff31163c0ffd81baafcad84db900b601f0155dd0b37e9f2 languageName: node linkType: hard -"tapable@npm:^2.2.0": - version: 2.2.1 - resolution: "tapable@npm:2.2.1" - checksum: 10/1769336dd21481ae6347611ca5fca47add0962fd8e80466515032125eca0084a4f0ede11e65341b9c0018ef4e1cf1ad820adbb0fba7cc99865c6005734000b0a +"tapable@npm:^2.2.0, tapable@npm:^2.3.0": + version: 2.3.3 + resolution: "tapable@npm:2.3.3" + checksum: 10/21fb64a7ae1a0e11d855a6c33a22ae5ecf7e2f23170c942da673b44bf4c3aae8aa52451ef2792d0ce36c7feca13dceafa4f135105d66fc06912632488c0913fd languageName: node linkType: hard @@ -9757,15 +12334,6 @@ __metadata: languageName: node linkType: hard -"teex@npm:^1.0.1": - version: 1.0.1 - resolution: "teex@npm:1.0.1" - dependencies: - streamx: "npm:^2.12.5" - checksum: 10/36bf7ce8bb5eb428ad7b14b695ee7fb0a02f09c1a9d8181cc42531208543a920b299d711bf78dad4ff9bcf36ac437ae8e138053734746076e3e0e7d6d76eef64 - languageName: node - linkType: hard - "temp-dir@npm:^3.0.0": version: 3.0.0 resolution: "temp-dir@npm:3.0.0" @@ -9773,15 +12341,15 @@ __metadata: languageName: node linkType: hard -"tempy@npm:^3.1.0": - version: 3.1.0 - resolution: "tempy@npm:3.1.0" +"tempy@npm:^3.2.0": + version: 3.2.0 + resolution: "tempy@npm:3.2.0" dependencies: is-stream: "npm:^3.0.0" temp-dir: "npm:^3.0.0" type-fest: "npm:^2.12.2" unique-string: "npm:^3.0.0" - checksum: 10/f5540bc24dcd9d41ab0b31e9eed73c3ef825080f1c8b1e854e4b73059155c889f72f5f7c15e8cd462d59aa10c9726e423c81d6a365d614b538c6cc78a1209cc6 + checksum: 10/02ed53acb75719924c83bdd04e99ceb5bbae009b68ab24934750f5bcf26d8da251d4ee19f24cea749206af4051a0fd1cab060f63264f384dc0aaae150bf72bf4 languageName: node linkType: hard @@ -9810,6 +12378,15 @@ __metadata: languageName: node linkType: hard +"thingies@npm:^2.5.0": + version: 2.6.0 + resolution: "thingies@npm:2.6.0" + peerDependencies: + tslib: ^2 + checksum: 10/722ca22cb54b6071ca489731b092538448d7634dd6b17ec9b89e846bea40bf0111084bdda8403f0970d716f33703e188978596cce9cd331a93d5d37882b39d74 + languageName: node + linkType: hard + "thread-stream@npm:^2.6.0": version: 2.7.0 resolution: "thread-stream@npm:2.7.0" @@ -9819,19 +12396,19 @@ __metadata: languageName: node linkType: hard -"threadedclass@npm:^1.3.0": - version: 1.3.0 - resolution: "threadedclass@npm:1.3.0" +"threadedclass@npm:^1.4.0": + version: 1.4.0 + resolution: "threadedclass@npm:1.4.0" dependencies: callsites: "npm:^3.1.0" eventemitter3: "npm:^4.0.4" is-running: "npm:^2.1.0" tslib: "npm:^1.13.0" - checksum: 10/9e048e82ee745ee2009dabb0015330fa9d4f4d83629c799c6059f77a6a1c6a8b0392e6e8c2a28834a88532be6b86ac276cf1f0133a855ea867b0217021350043 + checksum: 10/f99d94cbc41a5118e705a1a4ad0a5c5f2a91c7d3635fa45a04bc5e7d37749240f5034fe29b31025dcaa358be26e21a01f7a1bd13877a1435f015b05debd7bd5c languageName: node linkType: hard -"through2@npm:^2.0.0, through2@npm:^2.0.1": +"through2@npm:^2.0.0": version: 2.0.5 resolution: "through2@npm:2.0.5" dependencies: @@ -9841,15 +12418,6 @@ __metadata: languageName: node linkType: hard -"through2@npm:^4.0.0": - version: 4.0.2 - resolution: "through2@npm:4.0.2" - dependencies: - readable-stream: "npm:3" - checksum: 10/72c246233d9a989bbebeb6b698ef0b7b9064cb1c47930f79b25d87b6c867e075432811f69b7b2ac8da00ca308191c507bdab913944be8019ac43b036ce88f6ba - languageName: node - linkType: hard - "through@npm:2, through@npm:>=2.2.7 <3": version: 2.3.8 resolution: "through@npm:2.3.8" @@ -9857,24 +12425,24 @@ __metadata: languageName: node linkType: hard -"timecode@npm:0.0.4": - version: 0.0.4 - resolution: "timecode@npm:0.0.4" - checksum: 10/decc0ed2a030b281a4ac2d812a881e9003ea2e86fa3c08796bd6e383524c31082caf01dbd348a3e1d877901f817b052cdcfb2a3c854d2bed3a4f0009c919b874 +"thunky@npm:^1.0.2": + version: 1.1.0 + resolution: "thunky@npm:1.1.0" + checksum: 10/825e3bd07ab3c9fd6f753c457a60957c628cacba5dd0656fd93b037c445e2828b43cf0805a9f2b16b0c5f5a10fd561206271acddb568df4f867f0aea0eb2772f languageName: node linkType: hard -"timeline-state-resolver-types@npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0": - version: 10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0 - resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" +"timeline-state-resolver-types@npm:10.0.0-nightly-main-20260602-133931-c0882da4d.0": + version: 10.0.0-nightly-main-20260602-133931-c0882da4d.0 + resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-main-20260602-133931-c0882da4d.0" dependencies: - kairos-lib: "npm:0.2.3" + kairos-lib: "npm:1.0.0" tslib: "npm:^2.8.1" - checksum: 10/5558f4a80ebc60f3f86e76172965fce0f415cfd76fd109f0c84ad6873521a1dbe054a60e1c8d87535b2e16df3be25e637099742b38628be9c794f1701762903a + checksum: 10/8021f03e15a7b6f581ea4c5b77e809238a3e8120a7b0a8eed40d3a1afcb03749aaed33177b96a0c42078dcd19da08703f125ee3a1f28739bfcb9782344321bc8 languageName: node linkType: hard -"timers-browserify@npm:^2.0.12": +"timers-browserify@npm:^2.0.12, timers-browserify@npm:^2.0.4": version: 2.0.12 resolution: "timers-browserify@npm:2.0.12" dependencies: @@ -9883,13 +12451,13 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12": - version: 0.2.15 - resolution: "tinyglobby@npm:0.2.15" +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": + version: 0.2.16 + resolution: "tinyglobby@npm:0.2.16" dependencies: fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.3" - checksum: 10/d72bd826a8b0fa5fa3929e7fe5ba48fceb2ae495df3a231b6c5408cd7d8c00b58ab5a9c2a76ba56a62ee9b5e083626f1f33599734bed1ffc4b792406408f0ca2 + picomatch: "npm:^4.0.4" + checksum: 10/5c2c41b572ada38449e7c86a5fe034f204a1dbba577225a761a14f29f48dc3f2fc0d81a6c56fcc67c5a742cc3aa9fb5e2ca18dbf22b610b0bc0e549b34d5a0f8 languageName: node linkType: hard @@ -9920,15 +12488,6 @@ __metadata: languageName: node linkType: hard -"to-through@npm:^3.0.0": - version: 3.0.0 - resolution: "to-through@npm:3.0.0" - dependencies: - streamx: "npm:^2.12.5" - checksum: 10/404ad1a346babab53d75d3b4deb779916760fc9e605f4e64ec789366edf08e75ad592a262ca566e7864f77c03375151dcfac4744ff7fd52417cb2a2e9fc60795 - languageName: node - linkType: hard - "toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" @@ -9936,13 +12495,21 @@ __metadata: languageName: node linkType: hard -"token-types@npm:^5.0.1": - version: 5.0.1 - resolution: "token-types@npm:5.0.1" +"token-types@npm:^6.1.1": + version: 6.1.2 + resolution: "token-types@npm:6.1.2" dependencies: + "@borewit/text-codec": "npm:^0.2.1" "@tokenizer/token": "npm:^0.3.0" ieee754: "npm:^1.2.1" - checksum: 10/0985369bbea9f53a5ccd79bb9899717b41401a813deb2c7fb1add5d0baf2f702aaf6da78f6e0ccf346d5a9f7acaa7cb5efed7d092d89d8c1e6962959e9509bc0 + checksum: 10/0c7811a2da5a0ca474c795d883d871a184d1d54f67058d66084110f0b246fff66151885dbcb91d66533e776478bf57f3b4fac69ce03b805a0e1060def87947de + languageName: node + linkType: hard + +"totalist@npm:^3.0.0": + version: 3.0.1 + resolution: "totalist@npm:3.0.1" + checksum: 10/5132d562cf88ff93fd710770a92f31dbe67cc19b5c6ccae2efc0da327f0954d211bbfd9456389655d726c624f284b4a23112f56d1da931ca7cfabbe1f45e778a languageName: node linkType: hard @@ -9955,12 +12522,21 @@ __metadata: languageName: node linkType: hard -"tr46@npm:^5.0.0": - version: 5.0.0 - resolution: "tr46@npm:5.0.0" +"tr46@npm:^5.1.0": + version: 5.1.1 + resolution: "tr46@npm:5.1.1" dependencies: punycode: "npm:^2.3.1" - checksum: 10/29155adb167d048d3c95d181f7cb5ac71948b4e8f3070ec455986e1f34634acae50ae02a3c8d448121c3afe35b76951cd46ed4c128fd80264280ca9502237a3e + checksum: 10/833a0e1044574da5790148fd17866d4ddaea89e022de50279967bcd6b28b4ce0d30d59eb3acf9702b60918975b3bad481400337e3a2e6326cffa5c77b874753d + languageName: node + linkType: hard + +"tree-dump@npm:^1.0.3, tree-dump@npm:^1.1.0": + version: 1.1.0 + resolution: "tree-dump@npm:1.1.0" + peerDependencies: + tslib: 2 + checksum: 10/2c20118d2671996aa6f1ba1310cef1404fb525bde5d989ab542013f62b23a3633c0f0b32cbd516ee6205051ec21912b2470dabca006d19c9eba0740b567e2b60 languageName: node linkType: hard @@ -9985,26 +12561,37 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^2.0.1": - version: 2.0.1 - resolution: "ts-api-utils@npm:2.0.1" +"ts-api-utils@npm:^2.5.0": + version: 2.5.0 + resolution: "ts-api-utils@npm:2.5.0" peerDependencies: typescript: ">=4.8.4" - checksum: 10/2e68938cd5acad6b5157744215ce10cd097f9f667fd36b5fdd5efdd4b0c51063e855459d835f94f6777bb8a0f334916b6eb5c1eedab8c325feb34baa39238898 + checksum: 10/d5f1936f5618c6ab6942a97b78802217540ced00e7501862ae1f578d9a3aa189fc06050e64cb8951d21f7088e5fd35f53d2bf0d0370a883861c7b05e993ebc44 languageName: node linkType: hard -"ts-jest@npm:^29.4.6": - version: 29.4.6 - resolution: "ts-jest@npm:29.4.6" +"ts-declaration-location@npm:^1.0.6": + version: 1.0.7 + resolution: "ts-declaration-location@npm:1.0.7" + dependencies: + picomatch: "npm:^4.0.2" + peerDependencies: + typescript: ">=4.0.0" + checksum: 10/a7932fc75d41f10c16089f8f5a5c1ea49d6afca30f09c91c1df14d0a8510f72bcb9f8a395c04f060b66b855b6bd7ea4df81b335fb9d21bef402969a672a4afa7 + languageName: node + linkType: hard + +"ts-jest@npm:^29.4.9": + version: 29.4.9 + resolution: "ts-jest@npm:29.4.9" dependencies: bs-logger: "npm:^0.2.6" fast-json-stable-stringify: "npm:^2.1.0" - handlebars: "npm:^4.7.8" + handlebars: "npm:^4.7.9" json5: "npm:^2.2.3" lodash.memoize: "npm:^4.1.2" make-error: "npm:^1.3.6" - semver: "npm:^7.7.3" + semver: "npm:^7.7.4" type-fest: "npm:^4.41.0" yargs-parser: "npm:^21.1.1" peerDependencies: @@ -10014,7 +12601,7 @@ __metadata: babel-jest: ^29.0.0 || ^30.0.0 jest: ^29.0.0 || ^30.0.0 jest-util: ^29.0.0 || ^30.0.0 - typescript: ">=4.3 <6" + typescript: ">=4.3 <7" peerDependenciesMeta: "@babel/core": optional: true @@ -10030,7 +12617,7 @@ __metadata: optional: true bin: ts-jest: cli.js - checksum: 10/e0ff9e13f684166d5331808b288043b8054f49a1c2970480a92ba3caec8d0ff20edd092f2a4e7a3ad8fcb9ba4d674bee10ec7ee75046d8066bbe43a7d16cf72e + checksum: 10/f5e81b1e13fff08da5b92d5a72f984f3393f40df73a1ae54473a780436b95dddb1452c78256e6d70a701c09ea427449657a5fbb3d142dc7e7a82eb192e80c3db languageName: node linkType: hard @@ -10041,7 +12628,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.4.0, tslib@npm:^2.6.0, tslib@npm:^2.6.2, tslib@npm:^2.8.1": +"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -10078,6 +12665,13 @@ __metadata: languageName: node linkType: hard +"type-detect@npm:^4.0.0": + version: 4.1.0 + resolution: "type-detect@npm:4.1.0" + checksum: 10/e363bf0352427a79301f26a7795a27718624c49c576965076624eb5495d87515030b207217845f7018093adcbe169b2d119bb9b7f1a31a92bfbb1ab9639ca8dd + languageName: node + linkType: hard + "type-fest@npm:^0.18.0": version: 0.18.1 resolution: "type-fest@npm:0.18.1" @@ -10120,7 +12714,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.41.0, type-fest@npm:^4.6.0, type-fest@npm:^4.7.1": +"type-fest@npm:^4.27.0, type-fest@npm:^4.41.0, type-fest@npm:^4.6.0, type-fest@npm:^4.7.1": version: 4.41.0 resolution: "type-fest@npm:4.41.0" checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212 @@ -10202,37 +12796,38 @@ __metadata: languageName: node linkType: hard -"typescript-eslint@npm:^8.21.0": - version: 8.23.0 - resolution: "typescript-eslint@npm:8.23.0" +"typescript-eslint@npm:^8.54.0": + version: 8.58.2 + resolution: "typescript-eslint@npm:8.58.2" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.23.0" - "@typescript-eslint/parser": "npm:8.23.0" - "@typescript-eslint/utils": "npm:8.23.0" + "@typescript-eslint/eslint-plugin": "npm:8.58.2" + "@typescript-eslint/parser": "npm:8.58.2" + "@typescript-eslint/typescript-estree": "npm:8.58.2" + "@typescript-eslint/utils": "npm:8.58.2" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/c7a8b95129e944dd54a3f9312c14fbd9f589d863c30f45c8f3cf6001bb98398cf6ff41b5d51aa84d413853021d35ae703e8d0c067b409afa5acdc6bfc8bb1982 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/fd8b67208f6af32b78df004402e6de1324a64ec1429b707edf295a0257a1ca6ddab452c18494958461d0d86a74a81336ae7229fe430bd1092c0d2ceb00a9f61d languageName: node linkType: hard -"typescript@npm:~5.7.3": - version: 5.7.3 - resolution: "typescript@npm:5.7.3" +"typescript@npm:~5.9.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/6a7e556de91db3d34dc51cd2600e8e91f4c312acd8e52792f243c7818dfadb27bae677175fad6947f9c81efb6c57eb6b2d0c736f196a6ee2f1f7d57b74fc92fa + checksum: 10/c089d9d3da2729fd4ac517f9b0e0485914c4b3c26f80dc0cffcb5de1719a17951e92425d55db59515c1a7ddab65808466debb864d0d56dcf43f27007d0709594 languageName: node linkType: hard -"typescript@patch:typescript@npm%3A~5.7.3#optional!builtin": - version: 5.7.3 - resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=5786d5" +"typescript@patch:typescript@npm%3A~5.9.3#optional!builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/dc58d777eb4c01973f7fbf1fd808aad49a0efdf545528dab9b07d94fdcb65b8751742804c3057e9619a4627f2d9cc85547fdd49d9f4326992ad0181b49e61d81 + checksum: 10/696e1b017bc2635f4e0c94eb4435357701008e2f272f553d06e35b494b8ddc60aa221145e286c28ace0c89ee32827a28c2040e3a69bdc108b1a5dc8fb40b72e3 languageName: node linkType: hard @@ -10245,6 +12840,13 @@ __metadata: languageName: node linkType: hard +"uint8array-extras@npm:^1.4.0": + version: 1.5.0 + resolution: "uint8array-extras@npm:1.5.0" + checksum: 10/94fd56a2dda6a7445f5176f301f491814c87757d38e4b3c932299ab54d69ec504830e5d5c18ffa20cf694a69a210315be8b4a2c9952c6334da817ea2d2e1dce0 + languageName: node + linkType: hard + "unbox-primitive@npm:^1.0.2": version: 1.0.2 resolution: "unbox-primitive@npm:1.0.2" @@ -10257,7 +12859,7 @@ __metadata: languageName: node linkType: hard -"underscore@npm:^1.13.7": +"underscore@npm:^1.13.8": version: 1.13.8 resolution: "underscore@npm:1.13.8" checksum: 10/b50ac5806d059cc180b1bd9adea6f7ed500021f4dc782dfc75d66a90337f6f0506623c1b37863f4a9bf64ffbeb5769b638a54b7f2f5966816189955815953139 @@ -10271,6 +12873,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.21.0": + version: 7.21.0 + resolution: "undici-types@npm:7.21.0" + checksum: 10/e38c0efbeaeabeb84f5d8a49d127fcae1626af09785b04fde4915e8312b47c5f81106c61e655cb63c59e429522460a313183168913e55b485a36e06d67689798 + languageName: node + linkType: hard + "unicode-byte-truncate@npm:^1.0.0": version: 1.0.0 resolution: "unicode-byte-truncate@npm:1.0.0" @@ -10295,6 +12904,13 @@ __metadata: languageName: node linkType: hard +"unicorn-magic@npm:^0.3.0": + version: 0.3.0 + resolution: "unicorn-magic@npm:0.3.0" + checksum: 10/bdd7d7c522f9456f32a0b77af23f8854f9a7db846088c3868ec213f9550683ab6a2bdf3803577eacbafddb4e06900974385841ccb75338d17346ccef45f9cb01 + languageName: node + linkType: hard + "unique-filename@npm:^5.0.0": version: 5.0.0 resolution: "unique-filename@npm:5.0.0" @@ -10322,6 +12938,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^2.0.0": + version: 2.0.1 + resolution: "universalify@npm:2.0.1" + checksum: 10/ecd8469fe0db28e7de9e5289d32bd1b6ba8f7183db34f3bfc4ca53c49891c2d6aa05f3fb3936a81285a905cc509fb641a0c3fc131ec786167eff41236ae32e60 + languageName: node + linkType: hard + "unpipe@npm:~1.0.0": version: 1.0.0 resolution: "unpipe@npm:1.0.0" @@ -10396,9 +13019,9 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.1.1": - version: 1.1.2 - resolution: "update-browserslist-db@npm:1.1.2" +"update-browserslist-db@npm:^1.2.3": + version: 1.2.3 + resolution: "update-browserslist-db@npm:1.2.3" dependencies: escalade: "npm:^3.2.0" picocolors: "npm:^1.1.1" @@ -10406,7 +13029,7 @@ __metadata: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 10/e7bf8221dfb21eba4a770cd803df94625bb04f65a706aa94c567de9600fe4eb6133fda016ec471dad43b9e7959c1bffb6580b5e20a87808d2e8a13e3892699a9 + checksum: 10/059f774300efb4b084a49293143c511f3ae946d40397b5c30914e900cd5691a12b8e61b41dd54ed73d3b56c8204165a0333107dd784ccf8f8c81790bcc423175 languageName: node linkType: hard @@ -10439,6 +13062,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.6.0": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/b40ad2847ba220695bff2d4ba4f4d60391c0fb4fb012faa7a4c18eb38b69181936f5edc55a522c4d20a788d1a879b73c3810952c9d0fd128d01cb3f22042c09e + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -10453,7 +13085,7 @@ __metadata: languageName: node linkType: hard -"util@npm:^0.12.5": +"util@npm:^0.12.4, util@npm:^0.12.5": version: 0.12.5 resolution: "util@npm:0.12.5" dependencies: @@ -10466,6 +13098,22 @@ __metadata: languageName: node linkType: hard +"utils-merge@npm:1.0.1": + version: 1.0.1 + resolution: "utils-merge@npm:1.0.1" + checksum: 10/5d6949693d58cb2e636a84f3ee1c6e7b2f9c16cb1d42d0ecb386d8c025c69e327205aa1c69e2868cc06a01e5e20681fbba55a4e0ed0cce913d60334024eae798 + languageName: node + linkType: hard + +"uuid@npm:^8.3.2": + version: 8.3.2 + resolution: "uuid@npm:8.3.2" + bin: + uuid: dist/bin/uuid + checksum: 10/9a5f7aa1d6f56dd1e8d5f2478f855f25c645e64e26e347a98e98d95781d5ed20062d6cca2eecb58ba7c84bc3910be95c0451ef4161906abaab44f9cb68ffbdd1 + languageName: node + linkType: hard + "v8-to-istanbul@npm:^9.0.1": version: 9.1.0 resolution: "v8-to-istanbul@npm:9.1.0" @@ -10487,86 +13135,27 @@ __metadata: languageName: node linkType: hard -"value-or-function@npm:^4.0.0": - version: 4.0.0 - resolution: "value-or-function@npm:4.0.0" - checksum: 10/16b6aed84b8f9732a7eb7a5035a1480be3689d097a73b1154fb827caf021d5f2b6f60c0dfe694bfc8c9605f06cfc093dc428efdc3d24cb2768fbe202ffd42ae1 - languageName: node - linkType: hard - -"vary@npm:^1.1.2": +"vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: 10/31389debef15a480849b8331b220782230b9815a8e0dbb7b9a8369559aed2e9a7800cd904d4371ea74f4c3527db456dc8e7ac5befce5f0d289014dbdf47b2242 languageName: node linkType: hard -"vinyl-contents@npm:^2.0.0": - version: 2.0.0 - resolution: "vinyl-contents@npm:2.0.0" - dependencies: - bl: "npm:^5.0.0" - vinyl: "npm:^3.0.0" - checksum: 10/10d72a032e6317bf89713565d616df8726ee41601a41c48c7d778e61ab557c0a5fdee883ceecbfb33da4a5e11ea80e76e5ae63c1d13fda61edbb5ef50445c8b2 - languageName: node - linkType: hard - -"vinyl-fs@npm:^4.0.0": - version: 4.0.0 - resolution: "vinyl-fs@npm:4.0.0" - dependencies: - fs-mkdirp-stream: "npm:^2.0.1" - glob-stream: "npm:^8.0.0" - graceful-fs: "npm:^4.2.11" - iconv-lite: "npm:^0.6.3" - is-valid-glob: "npm:^1.0.0" - lead: "npm:^4.0.0" - normalize-path: "npm:3.0.0" - resolve-options: "npm:^2.0.0" - stream-composer: "npm:^1.0.2" - streamx: "npm:^2.14.0" - to-through: "npm:^3.0.0" - value-or-function: "npm:^4.0.0" - vinyl: "npm:^3.0.0" - vinyl-sourcemap: "npm:^2.0.0" - checksum: 10/22ae47c018600e6973b8a0a0c098927b09f60c4963cc5f717be04e774215774aa15ea97400803483d3dadafc5cff1a6744c3a2ab0322528234dc4e93ae1a55aa - languageName: node - linkType: hard - -"vinyl-sourcemap@npm:^2.0.0": - version: 2.0.0 - resolution: "vinyl-sourcemap@npm:2.0.0" - dependencies: - convert-source-map: "npm:^2.0.0" - graceful-fs: "npm:^4.2.10" - now-and-later: "npm:^3.0.0" - streamx: "npm:^2.12.5" - vinyl: "npm:^3.0.0" - vinyl-contents: "npm:^2.0.0" - checksum: 10/f23fc251a3eb72100690e5e93685ef776d8fee20e076f29655536a31b5235426b9404eea76b6b268fa00648437acc98aad54a7e76661b97305706c487a54afdb - languageName: node - linkType: hard - -"vinyl@npm:^3.0.0": - version: 3.0.0 - resolution: "vinyl@npm:3.0.0" - dependencies: - clone: "npm:^2.1.2" - clone-stats: "npm:^1.0.0" - remove-trailing-separator: "npm:^1.1.0" - replace-ext: "npm:^2.0.0" - teex: "npm:^1.0.1" - checksum: 10/3371947a92c4b65c7adb944b22586480ffc723ec62347d09b64e593193cb523ce5f472d52549f0e0bbfa82db6c320cae46739461594b0602bba0419d0d7800fb - languageName: node - linkType: hard - -"vm-browserify@npm:^1.1.2": +"vm-browserify@npm:^1.0.1, vm-browserify@npm:^1.1.2": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" checksum: 10/ad5b17c9f7a9d9f1ed0e24c897782ab7a587c1fd40f370152482e1af154c7cf0b0bacc45c5ae76a44289881e083ae4ae127808fdff864aa9b562192aae8b5c3b languageName: node linkType: hard +"void-elements@npm:3.1.0": + version: 3.1.0 + resolution: "void-elements@npm:3.1.0" + checksum: 10/0390f818107fa8fce55bb0a5c3f661056001c1d5a2a48c28d582d4d847347c2ab5b7f8272314cac58acf62345126b6b09bea623a185935f6b1c3bbce0dfd7f7f + languageName: node + linkType: hard + "walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -10576,6 +13165,15 @@ __metadata: languageName: node linkType: hard +"wbuf@npm:^1.1.0, wbuf@npm:^1.7.3": + version: 1.7.3 + resolution: "wbuf@npm:1.7.3" + dependencies: + minimalistic-assert: "npm:^1.0.0" + checksum: 10/c18b51c4e1fb19705c94b93c0cf093ba014606abceee949399d56074ef1863bf4897a8d884be24e8d224d18c9ce411cf6924006d0a5430492729af51256e067a + languageName: node + linkType: hard + "webidl-conversions@npm:^4.0.2": version: 4.0.2 resolution: "webidl-conversions@npm:4.0.2" @@ -10590,13 +13188,128 @@ __metadata: languageName: node linkType: hard -"whatwg-url@npm:^14.1.0 || ^13.0.0": - version: 14.1.0 - resolution: "whatwg-url@npm:14.1.0" +"webpack-bundle-analyzer@npm:4.10.2": + version: 4.10.2 + resolution: "webpack-bundle-analyzer@npm:4.10.2" + dependencies: + "@discoveryjs/json-ext": "npm:0.5.7" + acorn: "npm:^8.0.4" + acorn-walk: "npm:^8.0.0" + commander: "npm:^7.2.0" + debounce: "npm:^1.2.1" + escape-string-regexp: "npm:^4.0.0" + gzip-size: "npm:^6.0.0" + html-escaper: "npm:^2.0.2" + opener: "npm:^1.5.2" + picocolors: "npm:^1.0.0" + sirv: "npm:^2.0.3" + ws: "npm:^7.3.1" + bin: + webpack-bundle-analyzer: lib/bin/analyzer.js + checksum: 10/cb7ff9d01dc04ef23634f439ab9fe739e022cce5595cb340e01d106ed474605ce4ef50b11b47e444507d341b16650dcb3610e88944020ca6c1c38e88072d43ba + languageName: node + linkType: hard + +"webpack-dev-middleware@npm:^7.4.2": + version: 7.4.5 + resolution: "webpack-dev-middleware@npm:7.4.5" + dependencies: + colorette: "npm:^2.0.10" + memfs: "npm:^4.43.1" + mime-types: "npm:^3.0.1" + on-finished: "npm:^2.4.1" + range-parser: "npm:^1.2.1" + schema-utils: "npm:^4.0.0" + peerDependencies: + webpack: ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + checksum: 10/50e9b162d740b81f14c0926beb5fa01fc6d2ae16740bab709320dd5ea1a52ebcc48b66f3db5a7262fc4dc31a7e18590db766cef5da90e77a39e3a26d3b5b1001 + languageName: node + linkType: hard + +"webpack-dev-server@npm:5.2.2": + version: 5.2.2 + resolution: "webpack-dev-server@npm:5.2.2" + dependencies: + "@types/bonjour": "npm:^3.5.13" + "@types/connect-history-api-fallback": "npm:^1.5.4" + "@types/express": "npm:^4.17.21" + "@types/express-serve-static-core": "npm:^4.17.21" + "@types/serve-index": "npm:^1.9.4" + "@types/serve-static": "npm:^1.15.5" + "@types/sockjs": "npm:^0.3.36" + "@types/ws": "npm:^8.5.10" + ansi-html-community: "npm:^0.0.8" + bonjour-service: "npm:^1.2.1" + chokidar: "npm:^3.6.0" + colorette: "npm:^2.0.10" + compression: "npm:^1.7.4" + connect-history-api-fallback: "npm:^2.0.0" + express: "npm:^4.21.2" + graceful-fs: "npm:^4.2.6" + http-proxy-middleware: "npm:^2.0.9" + ipaddr.js: "npm:^2.1.0" + launch-editor: "npm:^2.6.1" + open: "npm:^10.0.3" + p-retry: "npm:^6.2.0" + schema-utils: "npm:^4.2.0" + selfsigned: "npm:^2.4.1" + serve-index: "npm:^1.9.1" + sockjs: "npm:^0.3.24" + spdy: "npm:^4.0.2" + webpack-dev-middleware: "npm:^7.4.2" + ws: "npm:^8.18.0" + peerDependencies: + webpack: ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + bin: + webpack-dev-server: bin/webpack-dev-server.js + checksum: 10/59517409cd38c01a875a03b9658f3d20d492b5b8bead9ded4a0f3d33e6857daf2d352fe89f0181dcaea6d0fbe84b0494cb4750a87120fe81cdbb3c32b499451c + languageName: node + linkType: hard + +"webpack-merge@npm:^6.0.1": + version: 6.0.1 + resolution: "webpack-merge@npm:6.0.1" + dependencies: + clone-deep: "npm:^4.0.1" + flat: "npm:^5.0.2" + wildcard: "npm:^2.0.1" + checksum: 10/39ab911c26237922295d9b3d0617c8ea0c438c35a3b21b05506616a10423f5ece1962bccbedec932c5db61af57999b6d055d56d1f1755c63e2701bd4a55c3887 + languageName: node + linkType: hard + +"websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4": + version: 0.7.4 + resolution: "websocket-driver@npm:0.7.4" + dependencies: + http-parser-js: "npm:>=0.5.1" + safe-buffer: "npm:>=5.1.0" + websocket-extensions: "npm:>=0.1.1" + checksum: 10/17197d265d5812b96c728e70fd6fe7d067471e121669768fe0c7100c939d997ddfc807d371a728556e24fc7238aa9d58e630ea4ff5fd4cfbb40f3d0a240ef32d + languageName: node + linkType: hard + +"websocket-extensions@npm:>=0.1.1": + version: 0.1.4 + resolution: "websocket-extensions@npm:0.1.4" + checksum: 10/b5399b487d277c78cdd2aef63764b67764aa9899431e3a2fa272c6ad7236a0fb4549b411d89afa76d5afd664c39d62fc19118582dc937e5bb17deb694f42a0d1 + languageName: node + linkType: hard + +"whatwg-url@npm:^14.1.0": + version: 14.2.0 + resolution: "whatwg-url@npm:14.2.0" dependencies: - tr46: "npm:^5.0.0" + tr46: "npm:^5.1.0" webidl-conversions: "npm:^7.0.0" - checksum: 10/3afd325de6cf3a367820ce7c3566a1f78eb1409c4f27b1867c74c76dab096d26acedf49a8b9b71db53df7d806ec2e9ae9ed96990b2f7d1abe6ecf1fe753af6eb + checksum: 10/f0a95b0601c64f417c471536a2d828b4c16fe37c13662483a32f02f183ed0f441616609b0663fb791e524e8cd56d9a86dd7366b1fc5356048ccb09b576495e7c languageName: node linkType: hard @@ -10661,6 +13374,13 @@ __metadata: languageName: node linkType: hard +"wildcard@npm:^2.0.1": + version: 2.0.1 + resolution: "wildcard@npm:2.0.1" + checksum: 10/e0c60a12a219e4b12065d1199802d81c27b841ed6ad6d9d28240980c73ceec6f856771d575af367cbec2982d9ae7838759168b551776577f155044f5a5ba843c + languageName: node + linkType: hard + "winston-transport@npm:^4.9.0": version: 4.9.0 resolution: "winston-transport@npm:4.9.0" @@ -10737,6 +13457,70 @@ __metadata: languageName: node linkType: hard +"ws@npm:^7.3.1": + version: 7.5.10 + resolution: "ws@npm:7.5.10" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/9c796b84ba80ffc2c2adcdfc9c8e9a219ba99caa435c9a8d45f9ac593bba325563b3f83edc5eb067cc6d21b9a6bf2c930adf76dd40af5f58a5ca6859e81858f0 + languageName: node + linkType: hard + +"ws@npm:^8.18.0": + version: 8.20.1 + resolution: "ws@npm:8.20.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/8c4d2b06dc65381b6bfab1f2e584275dabd30a99a5ce058b4dc76f3d03fad1921cef3a21d8f53127d30a808cfd1864aa2fe6890a5d43359f682457315baec873 + languageName: node + linkType: hard + +"ws@npm:~8.18.3": + version: 8.18.3 + resolution: "ws@npm:8.18.3" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/725964438d752f0ab0de582cd48d6eeada58d1511c3f613485b5598a83680bedac6187c765b0fe082e2d8cc4341fc57707c813ae780feee82d0c5efe6a4c61b6 + languageName: node + linkType: hard + +"wsl-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "wsl-utils@npm:0.1.0" + dependencies: + is-wsl: "npm:^3.1.0" + checksum: 10/de4c92187e04c3c27b4478f410a02e81c351dc85efa3447bf1666f34fc80baacd890a6698ec91995631714086992036013286aea3d77e6974020d40a08e00aec + languageName: node + linkType: hard + +"wsl-utils@npm:^0.3.0": + version: 0.3.1 + resolution: "wsl-utils@npm:0.3.1" + dependencies: + is-wsl: "npm:^3.1.0" + powershell-utils: "npm:^0.1.0" + checksum: 10/46800b92fa4974f2a846a93f0b8c409a455c35897d001a7599b5524766b603c8fb0945d2b21ad6ad27d4b0ae7e72ca2e58d832ccfcaabf659399921c6448b1d0 + languageName: node + linkType: hard + "xml-js@npm:^1.6.11": version: 1.6.11 resolution: "xml-js@npm:1.6.11" @@ -10790,7 +13574,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.6.0": +"yaml@npm:^2.6.0, yaml@npm:^2.8.2, yaml@npm:^2.8.3": version: 2.8.3 resolution: "yaml@npm:2.8.3" bin: @@ -10856,3 +13640,10 @@ __metadata: checksum: 10/f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard + +"yoctocolors@npm:^2.1.1": + version: 2.1.2 + resolution: "yoctocolors@npm:2.1.2" + checksum: 10/6ee42d665a4cc161c7de3f015b2a65d6c65d2808bfe3b99e228bd2b1b784ef1e54d1907415c025fc12b400f26f372bfc1b71966c6c738d998325ca422eb39363 + languageName: node + linkType: hard diff --git a/package.json b/package.json index f929601411d..db3144fe83b 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,10 @@ "devDependencies": { "concurrently": "^9.2.1", "husky": "^9.1.7", - "lint-staged": "^15.5.2", + "lint-staged": "^16.4.0", "rimraf": "^6.1.3", "semver": "^7.7.4", - "snyk-nodejs-lockfile-parser": "^2.6.0" + "snyk-nodejs-lockfile-parser": "^2.7.0" }, - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/packages/blueprints-integration/jest.config.js b/packages/blueprints-integration/jest.config.js index 4b068877330..0d69f4ed7cc 100644 --- a/packages/blueprints-integration/jest.config.js +++ b/packages/blueprints-integration/jest.config.js @@ -9,16 +9,19 @@ module.exports = { diagnostics: { ignoreCodes: [ 151002, // hybrid module kind (Node16/18/Next) + 2823, // Import attributes not supported in CJS mode (ts-jest forces CJS, emits require() anyway) ], }, }, ], }, moduleNameMapper: { + '^@sofie-automation/shared-lib/dist/(.+)\\.js$': '/../shared-lib/src/$1', + '^@sofie-automation/shared-lib/dist/(.+)$': '/../shared-lib/src/$1', '(.+)\\.js$': '$1', }, testMatch: ['**/__tests__/**/*.spec.(ts|js)'], - testPathIgnorePatterns: ['integrationTests'], + testPathIgnorePatterns: ['integrationTests', 'dist'], testEnvironment: 'node', coverageThreshold: { global: { diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 06172465c6b..3b3db766303 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -42,5 +42,5 @@ "publishConfig": { "access": "public" }, - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 63a527b8e4c..5935853dd35 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -18,6 +18,9 @@ import type { IFixUpConfigContext, IOnTakeContext, IOnSetAsNextContext, + IExternalEventContext, + IPlaylistSnapshotCreatedContext, + IBlueprintPlaylistSnapshotInfo, } from '../context/index.js' import type { IngestAdlib, ExtendedIngestRundown, IngestRundown } from '../ingest.js' import type { IBlueprintExternalMessageQueueObj } from '../message.js' @@ -41,7 +44,7 @@ import type { IBlueprintShowStyleVariant, IOutputLayer, ISourceLayer } from '../ import type { SourceLayerType } from '../content.js' import type { TSR, OnGenerateTimelineObj, TimelineObjectCoreExt } from '../timeline.js' import type { IBlueprintConfig } from '../common.js' -import type { ReadonlyDeep } from 'type-fest' +import type { JsonValue, ReadonlyDeep } from 'type-fest' import type { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import type { JSONBlob } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import type { BlueprintConfigCoreConfig, BlueprintManifestBase, BlueprintManifestType, IConfigMessage } from './base.js' @@ -51,6 +54,8 @@ import type { ABResolverConfiguration } from '../abPlayback.js' import type { SofieIngestSegment } from '../ingest-types.js' import { PackageStatusMessage } from '@sofie-automation/shared-lib/dist/packageStatusMessages' import { BlueprintPlayoutPersistentStore } from '../context/playoutStore.js' +import type { ITranslatableMessage } from '../translations.js' +import type { BlueprintExternalEvent, BlueprintExternalEventSubscription } from '../externalEvent.js' export { PackageStatusMessage } @@ -116,7 +121,8 @@ export interface ShowStyleBlueprintManifest< context: ISyncIngestUpdateToPartInstanceContext, existingPartInstance: BlueprintSyncIngestPartInstance, newData: BlueprintSyncIngestNewData, - playoutStatus: 'previous' | 'current' | 'next' + playoutStatus: 'previous' | 'current' | 'next', + playoutPersistentState: BlueprintPlayoutPersistentStore ) => void /** @@ -131,6 +137,17 @@ export interface ShowStyleBlueprintManifest< triggerMode?: string ) => Promise + /** + * Handle a batch of external events (e.g. TSR device state changes). + * Called when one or more events matching the rundown's {@link BlueprintResultRundown.externalEventSubscriptions} are received. + * Events are batched to avoid queuing a handler per event during bursts. + */ + onExternalEvent?: ( + context: IExternalEventContext, + playoutPersistentState: BlueprintPlayoutPersistentStore, + events: BlueprintExternalEvent[] + ) => Promise + /** Execute an action defined by an IBlueprintActionManifest */ executeAction?: ( context: IActionExecutionContext, @@ -141,12 +158,7 @@ export interface ShowStyleBlueprintManifest< privateData: unknown | undefined, publicData: unknown | undefined, actionOptions: { [key: string]: any } | undefined - ) => Promise<{ - /** - * To be set if the payload sent by the user is invalid. When set, a 409 `ValidationFailed` message will be displayed to the User. - */ - validationErrors: any - } | void> + ) => Promise /** Generate adlib piece from ingest data */ getAdlibItem?: ( @@ -204,6 +216,25 @@ export interface ShowStyleBlueprintManifest< // Events + /** + * Called after a rundown playlist snapshot has been generated, before Meteor persists the snapshot file. + * + * Use this to run show-specific side effects (e.g. TSR actions) when a playlist snapshot is taken. + * The callback receives {@link IPlaylistSnapshotCreatedContext} with `listPlayoutDevices` and `executeTSRAction`. + * + * For playlists containing multiple rundowns, only one show-style blueprint is invoked per snapshot: + * current part, then next part, then first rundown by name. + * + * Errors are logged by Core and do not fail snapshot generation or storage. + * + * @param context Show-style and studio context with TSR actions for the studio worker job. + * @param info Metadata about the snapshot (not the snapshot JSON). + */ + onPlaylistSnapshotCreated?: ( + context: IPlaylistSnapshotCreatedContext, + info: IBlueprintPlaylistSnapshotInfo + ) => Promise + /** * Called at the final stage of RundownPlaylist activation, before the updated timeline is submitted to the Playout Gateway, * This is a good place to prepare any external systems for the rundown going live. @@ -289,6 +320,8 @@ export interface BlueprintResultRundown { globalActions: IBlueprintActionManifest[] globalPieces?: IBlueprintRundownPiece[] baseline: BlueprintResultBaseline + /** Subscriptions to external events (e.g. TSR device state changes) for this rundown */ + externalEventSubscriptions?: BlueprintExternalEventSubscription[] } export interface BlueprintResultSegment { segment: IBlueprintSegment @@ -381,3 +414,20 @@ export interface IShowStyleVariantConfigPreset { config: Partial } + +export interface BlueprintExecuteActionResult { + /** + * User friendly error message to return to the caller if the action was rejected. + */ + message: ITranslatableMessage + + /** + * HTTP error code for the action. If set, must be in the range 400-499 + */ + errorCode?: number + + /** + * Additional details payload to provide to the caller + */ + details?: JsonValue +} diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index bd988641de5..acc01e9c429 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -9,6 +9,9 @@ import type { IStudioBaselineContext, IStudioUserContext, IProcessIngestDataContext, + ISystemSnapshotCreatedContext, + IBlueprintSystemSnapshotInfo, + BlueprintSnapshotType as _BlueprintSnapshotType, } from '../context/index.js' import type { IBlueprintShowStyleBase } from '../showStyle.js' import type { @@ -41,6 +44,26 @@ import type { MosGatewayConfig } from '@sofie-automation/shared-lib/dist/generat import type { PlayoutGatewayConfig } from '@sofie-automation/shared-lib/dist/generated/PlayoutGatewayConfigTypes' import type { LiveStatusGatewayConfig } from '@sofie-automation/shared-lib/dist/generated/LiveStatusGatewayOptionsTypes' +/** + * Context provided to device status message functions. + * Contains the device name, device ID, and any additional context from the TSR status detail. + */ +export interface DeviceStatusContext { + /** Human-readable name of the device */ + deviceName: string + /** Internal device ID */ + deviceId?: string + /** Additional context values from the TSR error (e.g., host, port, channel, etc.) */ + [key: string]: unknown +} + +/** + * A function that receives device status context and returns a custom status message. + * Return `undefined` to fall back to the default TSR message. + * Return an empty string `''` to suppress the message entirely. + */ +export type DeviceStatusMessageFunction = (context: DeviceStatusContext) => string | undefined + export interface StudioBlueprintManifest< TRawConfig = IBlueprintConfig, TProcessedConfig = unknown, @@ -56,6 +79,64 @@ export interface StudioBlueprintManifest< /** Translations connected to the studio (as stringified JSON) */ translations?: string + /** + * Alternate device status messages, to override the default messages from TSR devices. + * Keys are status code strings from TSR devices (e.g., 'DEVICE_ATEM_DISCONNECTED'). + * + * Import status codes from 'timeline-state-resolver-types' for type safety. + * Values can be: + * - String templates using {{variable}} syntax for interpolation with context values + * - Functions that receive DeviceStatusContext and return a custom message string + * - Empty string to suppress the status message entirely + * + * @example + * ```typescript + * import { AtemStatusCode, CasparCGStatusCode } from 'timeline-state-resolver-types' + * + * deviceStatusMessages: { + * // String template with placeholders + * [AtemStatusCode.DISCONNECTED]: 'Vision mixer offline - check network to {{host}}', + * [AtemStatusCode.PSU_FAULT]: 'PSU {{psuNumber}} needs attention', + * + * // Function for complex conditional logic + * [CasparCGStatusCode.CHANNEL_ERROR]: (context) => { + * const channel = context.channel as number + * if (channel === 1) return 'Primary graphics output failed!' + * return `Graphics channel ${channel} error on ${context.deviceName}` + * }, + * + * // Suppress a noisy message + * [SomeStatusCode.NOISY_STATUS]: '', + * } + * ``` + */ + deviceStatusMessages?: Record + + /** + * Alternate device action error messages, to override the default messages from TSR devices. + * Keys are action error code strings from TSR devices (e.g., 'ACTION_HTTPSEND_REQUEST_FAILED'). + * + * Similar to deviceStatusMessages but applies to device action execution failures + * (e.g., HTTP Send failures, device restart failures) rather than ongoing status errors. + * + * Import action error codes from 'timeline-state-resolver-types' for type safety. + * Values can be: + * - String templates using {{variable}} syntax for interpolation with context values + * - Functions that receive DeviceStatusContext and return a custom message string + * - Empty string to suppress the message entirely (action result will show as generic error) + * + * @example + * ```typescript + * import { HttpSendActionErrorCode } from 'timeline-state-resolver-types' + * + * deviceActionMessages: { + * [HttpSendActionErrorCode.REQUEST_FAILED]: 'Failed to trigger graphics: {{errorMessage}}', + * [HttpSendActionErrorCode.MISSING_URL]: 'HTTP action not configured - missing URL', + * } + * ``` + */ + deviceActionMessages?: Record + /** Returns the items used to build the baseline (default state) of a studio, this is the baseline used when there's no active rundown */ getBaseline: (context: IStudioBaselineContext) => BlueprintResultStudioBaseline @@ -140,6 +221,27 @@ export interface StudioBlueprintManifest< previousNrcsIngestRundown: IngestRundown | undefined, changes: NrcsIngestChangeDetails | UserOperationChange | PlayoutOperationChange ) => Promise + + /** + * Called after a system snapshot has been stored to disk. + * + * Use this to run studio-level side effects (e.g. TSR actions on playout devices) at snapshot time. + * The callback receives {@link ISystemSnapshotCreatedContext} with `listPlayoutDevices` and `executeTSRAction`. + * + * Invoked once per studio: + * - Studio-scoped snapshots (`studioId` in snapshot options): once for that studio. + * - Full-system snapshots (no `studioId`): once per studio included in the snapshot. + * - Debug snapshots: once for the target studio when `info.type` is `'debug'` ({@link _BlueprintSnapshotType}). + * + * Errors are logged by Core and do not fail snapshot storage. + * + * @param context Studio context and TSR actions for the studio worker job. + * @param info Metadata about the snapshot (not the snapshot JSON). + */ + onSystemSnapshotCreated?: ( + context: ISystemSnapshotCreatedContext, + info: IBlueprintSystemSnapshotInfo + ) => Promise } export interface BlueprintResultStudioBaseline { diff --git a/packages/blueprints-integration/src/api/system.ts b/packages/blueprints-integration/src/api/system.ts index 3d905ac7cc2..9e7a9069d88 100644 --- a/packages/blueprints-integration/src/api/system.ts +++ b/packages/blueprints-integration/src/api/system.ts @@ -2,6 +2,10 @@ import type { IBlueprintTriggeredActions } from '../triggers.js' import type { BlueprintManifestBase, BlueprintManifestType } from './base.js' import type { ICoreSystemApplyConfigContext } from '../context/systemApplyConfigContext.js' import type { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' +import type { SystemErrorCode } from '@sofie-automation/shared-lib/dist/systemErrorMessages' + +// Re-export so blueprints can import from blueprints-integration +export { SystemErrorCode } from '@sofie-automation/shared-lib/dist/systemErrorMessages' export interface SystemBlueprintManifest extends BlueprintManifestBase { blueprintType: BlueprintManifestType.SYSTEM @@ -9,6 +13,24 @@ export interface SystemBlueprintManifest extends BlueprintManifestBase { /** Translations connected to the studio (as stringified JSON) */ translations?: string + /** + * Alternate system error messages, to override the builtin ones produced by Sofie. + * Keys are SystemErrorCode values (e.g., 'DATABASE_CONNECTION_LOST'). + * + * Templates use {{variable}} syntax for interpolation with context values. + * + * @example + * ```typescript + * import { SystemErrorCode } from '@sofie-automation/blueprints-integration' + * + * systemErrorMessages: { + * [SystemErrorCode.DATABASE_CONNECTION_LOST]: 'Database offline - contact IT support', + * [SystemErrorCode.SERVICE_UNAVAILABLE]: 'Service {{serviceName}} is not responding', + * } + * ``` + */ + systemErrorMessages?: Partial> + /** * Apply the config by generating the data to be saved into the db. * This should be written to give a predictable and stable result, it can be called with the same config multiple times diff --git a/packages/blueprints-integration/src/context/adlibActionContext.ts b/packages/blueprints-integration/src/context/adlibActionContext.ts index 00da91558df..836d3daae2e 100644 --- a/packages/blueprints-integration/src/context/adlibActionContext.ts +++ b/packages/blueprints-integration/src/context/adlibActionContext.ts @@ -1,11 +1,11 @@ -import type { DatastorePersistenceMode, Time } from '../common.js' +import type { DatastorePersistenceMode } from '../common.js' import type { IEventContext } from './index.js' import type { IShowStyleUserContext } from './showStyleContext.js' import { IPartAndPieceActionContext } from './partsAndPieceActionContext.js' import { IExecuteTSRActionsContext, ITriggerIngestChangeContext } from './executeTsrActionContext.js' -import { IBlueprintPart, IBlueprintPartInstance, IBlueprintPiece } from '../index.js' import { IRouteSetMethods } from './routeSetContext.js' import { ITTimersContext } from './tTimersContext.js' +import type { IPlayoutActionContext } from './playoutActionContext.js' /** Actions */ export interface IDataStoreMethods { @@ -30,23 +30,10 @@ export interface IActionExecutionContext IExecuteTSRActionsContext, ITriggerIngestChangeContext, IRouteSetMethods, - ITTimersContext { + ITTimersContext, + IPlayoutActionContext { /** Fetch the showstyle config for the specified part */ // getNextShowStyleConfig(): Readonly<{ [key: string]: ConfigItemValue }> - - /** Move the next part through the rundown. Can move by either a number of parts, or segments in either direction. */ - moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickloop?: boolean): Promise - /** Set flag to perform take after executing the current action. Returns state of the flag after each call. */ - takeAfterExecuteAction(take: boolean): Promise - /** Inform core that a take out of the current partinstance should be blocked until the specified time */ - blockTakeUntil(time: Time | null): Promise - - /** Insert a queued part to follow the current part */ - queuePart(part: IBlueprintPart, pieces: IBlueprintPiece[]): Promise - - /** Insert a queued part to follow the taken part */ - queuePartAfterTake(part: IBlueprintPart, pieces: IBlueprintPiece[]): void - /** Misc actions */ // updateAction(newManifest: Pick): void // only updates itself. to allow for the next one to do something different // executePeripheralDeviceAction(deviceId: string, functionName: string, args: any[]): Promise diff --git a/packages/blueprints-integration/src/context/executeTsrActionContext.ts b/packages/blueprints-integration/src/context/executeTsrActionContext.ts index a8efe08ca41..36774a38112 100644 --- a/packages/blueprints-integration/src/context/executeTsrActionContext.ts +++ b/packages/blueprints-integration/src/context/executeTsrActionContext.ts @@ -2,7 +2,7 @@ import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model import { IBlueprintPlayoutDevice, TSR } from '../index.js' export interface IExecuteTSRActionsContext { - /** Returns a list of the PeripheralDevices */ + /** Returns playout-gateway subdevices for the studio, or an empty list if none are configured. */ listPlayoutDevices(): Promise /** Execute an action on a certain PeripheralDevice */ executeTSRAction( diff --git a/packages/blueprints-integration/src/context/externalEventContext.ts b/packages/blueprints-integration/src/context/externalEventContext.ts new file mode 100644 index 00000000000..527f3cb24f5 --- /dev/null +++ b/packages/blueprints-integration/src/context/externalEventContext.ts @@ -0,0 +1,19 @@ +import type { IEventContext } from './index.js' +import type { IShowStyleUserContext } from './showStyleContext.js' +import type { IPartAndPieceActionContext } from './partsAndPieceActionContext.js' +import type { IExecuteTSRActionsContext, ITriggerIngestChangeContext } from './executeTsrActionContext.js' +import type { IRouteSetMethods } from './routeSetContext.js' +import type { IDataStoreMethods } from './adlibActionContext.js' +import type { IPlayoutActionContext } from './playoutActionContext.js' + +/** Context provided to the blueprint's `onExternalEvent` handler. */ +export interface IExternalEventContext + extends + IShowStyleUserContext, + IEventContext, + IDataStoreMethods, + IPartAndPieceActionContext, + IExecuteTSRActionsContext, + ITriggerIngestChangeContext, + IRouteSetMethods, + IPlayoutActionContext {} diff --git a/packages/blueprints-integration/src/context/index.ts b/packages/blueprints-integration/src/context/index.ts index a1cba0ab9fe..78fe281ccdd 100644 --- a/packages/blueprints-integration/src/context/index.ts +++ b/packages/blueprints-integration/src/context/index.ts @@ -1,7 +1,9 @@ export * from './adlibActionContext.js' export * from './baseContext.js' export * from './eventContext.js' +export * from './externalEventContext.js' export * from './fixUpConfigContext.js' +export * from './playoutActionContext.js' export * from './onSetAsNextContext.js' export * from './onTakeContext.js' export * from './packageInfoContext.js' @@ -9,5 +11,7 @@ export * from './playoutStore.js' export * from './processIngestDataContext.js' export * from './rundownContext.js' export * from './showStyleContext.js' +export * from './snapshotContext.js' export * from './studioContext.js' export * from './syncIngestChangesContext.js' +export * from './tTimersContext.js' diff --git a/packages/blueprints-integration/src/context/onSetAsNextContext.ts b/packages/blueprints-integration/src/context/onSetAsNextContext.ts index 114ef0f47ca..88ff19d6b23 100644 --- a/packages/blueprints-integration/src/context/onSetAsNextContext.ts +++ b/packages/blueprints-integration/src/context/onSetAsNextContext.ts @@ -1,5 +1,6 @@ import { IBlueprintMutatablePart, + IBlueprintMutatablePartInstance, IBlueprintPart, IBlueprintPartInstance, IBlueprintPiece, @@ -75,10 +76,16 @@ export interface IOnSetAsNextContext /** Update a piecesInstance */ updatePieceInstance(pieceInstanceId: string, piece: Partial): Promise - /** Update a partInstance */ + /** + * Update a partInstance + * @param part Which part to update + * @param props Properties of the Part itself + * @param instanceProps Properties of the PartInstance (runtime state) + */ updatePartInstance( part: 'current' | 'next', - props: Partial + props: Partial, + instanceProps?: Partial ): Promise /** diff --git a/packages/blueprints-integration/src/context/partsAndPieceActionContext.ts b/packages/blueprints-integration/src/context/partsAndPieceActionContext.ts index a5a2b9c998c..cf7f570fa2b 100644 --- a/packages/blueprints-integration/src/context/partsAndPieceActionContext.ts +++ b/packages/blueprints-integration/src/context/partsAndPieceActionContext.ts @@ -1,6 +1,7 @@ import { ReadonlyDeep } from 'type-fest' import { IBlueprintMutatablePart, + IBlueprintMutatablePartInstance, IBlueprintPart, IBlueprintPartInstance, IBlueprintPiece, @@ -67,10 +68,16 @@ export interface IPartAndPieceActionContext { /** Update a piecesInstance */ updatePieceInstance(pieceInstanceId: string, piece: Partial): Promise - /** Update a partInstance */ + /** + * Update a partInstance + * @param part Which part to update + * @param props Properties of the Part itself + * @param instanceProps Properties of the PartInstance (runtime state) + */ updatePartInstance( part: 'current' | 'next', - props: Partial + props: Partial, + instanceProps?: Partial ): Promise /** Inform core that a take out of the partinstance should be blocked until the specified time */ blockTakeUntil(time: Time | null): Promise diff --git a/packages/blueprints-integration/src/context/playoutActionContext.ts b/packages/blueprints-integration/src/context/playoutActionContext.ts new file mode 100644 index 00000000000..6f27bde200f --- /dev/null +++ b/packages/blueprints-integration/src/context/playoutActionContext.ts @@ -0,0 +1,15 @@ +import type { IBlueprintPart, IBlueprintPartInstance, IBlueprintPiece } from '../index.js' + +/** + * The playout-action methods shared between {@link IActionExecutionContext} and {@link IExternalEventContext}. + */ +export interface IPlayoutActionContext { + /** Move the next part through the rundown. Can move by either a number of parts, or segments in either direction. */ + moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickloop?: boolean): Promise + /** Set flag to perform a take after the current handler completes. Returns state of the flag after each call. */ + takeAfterExecuteAction(take: boolean): Promise + /** Insert a queued part to follow the current part */ + queuePart(part: IBlueprintPart, pieces: IBlueprintPiece[]): Promise + /** Insert a queued part to follow the taken part */ + queuePartAfterTake(part: IBlueprintPart, pieces: IBlueprintPiece[]): void +} diff --git a/packages/blueprints-integration/src/context/rundownContext.ts b/packages/blueprints-integration/src/context/rundownContext.ts index cb57cbd5692..25e3f762d96 100644 --- a/packages/blueprints-integration/src/context/rundownContext.ts +++ b/packages/blueprints-integration/src/context/rundownContext.ts @@ -5,11 +5,15 @@ import type { IShowStyleContext } from './showStyleContext.js' import type { IExecuteTSRActionsContext } from './executeTsrActionContext.js' import type { IDataStoreMethods } from './adlibActionContext.js' import { ITTimersContext } from './tTimersContext.js' +import type { Time } from '../common.js' export interface IRundownContext extends IShowStyleContext { readonly rundownId: string readonly playlistId: string readonly rundown: Readonly + + /** Actual time of playback starting for the playlist (undefined if not started) */ + readonly startedPlayback: Time | undefined } export interface IRundownUserContext extends IUserNotesContext, IRundownContext {} diff --git a/packages/blueprints-integration/src/context/snapshotContext.ts b/packages/blueprints-integration/src/context/snapshotContext.ts new file mode 100644 index 00000000000..b563b360ad7 --- /dev/null +++ b/packages/blueprints-integration/src/context/snapshotContext.ts @@ -0,0 +1,88 @@ +import type { IExecuteTSRActionsContext } from './executeTsrActionContext.js' +import type { IShowStyleContext } from './showStyleContext.js' +import type { IStudioContext } from './studioContext.js' + +/** + * How the system snapshot hook was triggered. + * - `system` — system snapshot (settings, REST, automatic snapshot before migration) + * - `debug` — debug snapshot capture from the rundown UI + */ +export type BlueprintSnapshotType = 'system' | 'debug' + +/** Options that were used when the system snapshot was created. */ +export interface IBlueprintSystemSnapshotOptions { + /** Studio this hook invocation runs for (same as context; use `fullSystem` for snapshot scope). */ + studioId?: string + /** Whether peripheral device state snapshots were included in the stored snapshot file. */ + withDeviceSnapshots?: boolean + /** + * `true` when the stored snapshot is a full-system snapshot (all studios). + * `false` when the snapshot was filtered to a single studio. + */ + fullSystem?: boolean +} + +/** + * Metadata passed to {@link StudioBlueprintManifest.onSystemSnapshotCreated}. + * Does not include the snapshot file contents. + */ +export interface IBlueprintSystemSnapshotInfo { + /** Id of the stored snapshot (same value for every studio when `fullSystem` is true). */ + snapshotId: string + /** Human-readable reason provided when the snapshot was requested (e.g. UI label, cron message). */ + reason: string + type: BlueprintSnapshotType + /** Snapshot creation options relevant to this hook invocation. */ + options: IBlueprintSystemSnapshotOptions +} + +/** + * Context for {@link StudioBlueprintManifest.onSystemSnapshotCreated}. + * Provides studio config, mappings, and TSR device actions for the studio worker job. + */ +export interface ISystemSnapshotCreatedContext extends IStudioContext, IExecuteTSRActionsContext {} + +/** Options that were used when the playlist snapshot was generated. */ +export interface IBlueprintPlaylistSnapshotOptions { + /** When true, all part/piece instances are included; when false, only recent/non-reset instances. */ + full: boolean + /** When true and the playlist is activated, the timeline is included in the snapshot data. */ + withTimeline: boolean +} + +/** Minimal playlist state exposed to the blueprint (not the full DB document). */ +export interface IBlueprintPlaylistSnapshotPlaylistInfo { + /** Playlist display name. */ + name: string + /** Whether the playlist had an active activation at snapshot time. */ + active: boolean + /** Whether the playlist was in rehearsal mode at snapshot time. */ + rehearsal: boolean +} + +/** + * Metadata passed to {@link ShowStyleBlueprintManifest.onPlaylistSnapshotCreated}. + * Does not include the snapshot file contents. + */ +export interface IBlueprintPlaylistSnapshotInfo { + /** Id assigned to this snapshot before generation completes. */ + snapshotId: string + /** Id of the rundown playlist that was snapshotted. */ + playlistId: string + /** Human-readable reason provided when the snapshot was requested. */ + reason: string + /** Snapshot generation options. */ + options: IBlueprintPlaylistSnapshotOptions + /** Summary of playlist state at snapshot time. */ + playlist: IBlueprintPlaylistSnapshotPlaylistInfo +} + +/** + * Context for {@link ShowStyleBlueprintManifest.onPlaylistSnapshotCreated}. + * Provides show-style and studio config, and TSR device actions. + * + * For playlists with multiple rundowns/show styles, only one show-style blueprint is invoked per snapshot. + * Rundown selection order: current part, then next part, then first rundown by name (see job-worker + * `pickRundownForPlaylistSnapshot`). + */ +export interface IPlaylistSnapshotCreatedContext extends IShowStyleContext, IExecuteTSRActionsContext {} diff --git a/packages/blueprints-integration/src/context/syncIngestChangesContext.ts b/packages/blueprints-integration/src/context/syncIngestChangesContext.ts index 668e5bfd3e1..4545c03956a 100644 --- a/packages/blueprints-integration/src/context/syncIngestChangesContext.ts +++ b/packages/blueprints-integration/src/context/syncIngestChangesContext.ts @@ -1,6 +1,7 @@ import type { IRundownUserContext } from './rundownContext.js' import type { IBlueprintMutatablePart, + IBlueprintMutatablePartInstance, IBlueprintPartInstance, IBlueprintPiece, IBlueprintPieceInstance, @@ -38,8 +39,15 @@ export interface ISyncIngestUpdateToPartInstanceContext extends IRundownUserCont // /** Remove a ActionInstance */ // removeActionInstances(...actionInstanceIds: string[]): string[] - /** Update a partInstance */ - updatePartInstance(props: Partial): IBlueprintPartInstance + /** + * Update a partInstance + * @param props Properties of the Part itself + * @param instanceProps Properties of the PartInstance (runtime state) + */ + updatePartInstance( + props: Partial, + instanceProps?: Partial + ): IBlueprintPartInstance /** Remove the partInstance. This is only valid when `playstatus: 'next'` */ removePartInstance(): void diff --git a/packages/blueprints-integration/src/context/tTimersContext.ts b/packages/blueprints-integration/src/context/tTimersContext.ts index 7b00d9258a1..39da73f0719 100644 --- a/packages/blueprints-integration/src/context/tTimersContext.ts +++ b/packages/blueprints-integration/src/context/tTimersContext.ts @@ -1,5 +1,75 @@ export type IPlaylistTTimerIndex = 1 | 2 | 3 +export type RundownTTimerMode = RundownTTimerModeFreeRun | RundownTTimerModeCountdown | RundownTTimerModeTimeOfDay + +export interface RundownTTimerModeFreeRun { + readonly type: 'freeRun' +} +export interface RundownTTimerModeCountdown { + readonly type: 'countdown' + /** + * The original duration of the countdown in milliseconds, so that we know what value to reset to + */ + readonly duration: number + + /** + * If the countdown should stop at zero, or continue into negative values + */ + readonly stopAtZero: boolean +} +export interface RundownTTimerModeTimeOfDay { + readonly type: 'timeOfDay' + + /** + * The raw target string of the timer, as provided when setting the timer + * (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number) + */ + readonly targetRaw: string | number + + /** + * If the countdown should stop at zero, or continue into negative values + */ + readonly stopAtZero: boolean +} + +/** + * Timing state for a timer, optimized for efficient client rendering. + * When running, the client calculates current time from zeroTime. + * When paused, the duration is frozen and sent directly. + * pauseTime indicates when the timer should automatically pause (when current part ends and overrun begins). + * + * Client rendering logic: + * ```typescript + * if (state.paused === true) { + * // Manually paused by user or already pushing/overrun + * duration = state.duration + * } else if (state.pauseTime && now >= state.pauseTime) { + * // Auto-pause at overrun (current part ended) + * duration = state.zeroTime - state.pauseTime + * } else { + * // Running normally + * duration = state.zeroTime - now + * } + * ``` + */ +export type TimerState = + | { + /** Whether the timer is paused */ + paused: false + /** The absolute timestamp (ms) when the timer reaches/reached zero */ + zeroTime: number + /** Optional timestamp when the timer should pause (when current part ends) */ + pauseTime?: number | null + } + | { + /** Whether the timer is paused */ + paused: true + /** The frozen duration value in milliseconds */ + duration: number + /** Optional timestamp when the timer should pause (null when already paused/pushing) */ + pauseTime?: number | null + } + export interface ITTimersContext { /** * Get a T-timer by its index @@ -20,11 +90,19 @@ export interface IPlaylistTTimer { /** The label of the T-timer */ readonly label: string + /** + * The current mode of the T-timer + * Null if the T-timer is not initialized + * This defines how the timer behaves + */ + readonly mode: RundownTTimerMode | null + /** * The current state of the T-timer * Null if the T-timer is not initialized + * This contains the timing information needed to calculate the current time of the timer */ - readonly state: IPlaylistTTimerState | null + readonly state: TimerState | null /** Set the label of the T-timer */ setLabel(label: string): void @@ -67,11 +145,34 @@ export interface IPlaylistTTimer { /** * If the timer can be restarted, restore it to its initial/restarted state - * Note: This is supported by the countdown and timeOfDay modes + * Note: This is supported by the countdown, freeRun, and timeOfDay modes * @returns True if the timer was restarted, false if it could not be restarted */ restart(): boolean + /** + * Set the duration of a countdown timer + * This resets both the original duration (what restart() resets to) and the current countdown value. + * @param duration New duration in milliseconds + * @throws If timer is not in countdown mode or not initialized + */ + setDuration(duration: number): void + + /** + * Update the original duration (reset-to value) and/or current duration of a countdown timer + * This allows you to independently update: + * - `original`: The duration the timer resets to when restart() is called + * - `current`: The current countdown value (what's displayed now) + * + * If only `original` is provided, the current duration is recalculated to preserve elapsed time. + * If only `current` is provided, just the current countdown is updated. + * If both are provided, both values are updated independently. + * + * @param options Object with optional `original` and/or `current` duration in milliseconds + * @throws If timer is not in countdown mode or not initialized + */ + setDuration(options: { original?: number; current?: number }): void + /** * Clear any projection (manual or anchor-based) for this timer * This removes both manual projections set via setProjectedTime/setProjectedDuration @@ -116,49 +217,41 @@ export interface IPlaylistTTimer { * If false (default), we're progressing normally (projection counts down in real-time). */ setProjectedDuration(duration: number, paused?: boolean): void -} -export type IPlaylistTTimerState = - | IPlaylistTTimerStateCountdown - | IPlaylistTTimerStateFreeRun - | IPlaylistTTimerStateTimeOfDay - -export interface IPlaylistTTimerStateCountdown { - /** The mode of the T-timer */ - readonly mode: 'countdown' - /** The current time of the countdown, in milliseconds */ - readonly currentTime: number - /** The total duration of the countdown, in milliseconds */ - readonly duration: number - /** Whether the timer is currently paused */ - readonly paused: boolean - - /** If the countdown is set to stop at zero, or continue into negative values */ - readonly stopAtZero: boolean -} -export interface IPlaylistTTimerStateFreeRun { - /** The mode of the T-timer */ - readonly mode: 'freeRun' - /** The current time of the freerun, in milliseconds */ - readonly currentTime: number - /** Whether the timer is currently paused */ - readonly paused: boolean -} + /** + * Get the current value of the timer in milliseconds. + * + * Sign convention — **positive = time remaining / not yet elapsed; negative = overrun / elapsed past zero**: + * - **countdown**: positive while counting down, negative once past zero (overrunning) + * - **timeOfDay**: positive while counting down to the target time, negative once past it + * - **freeRun**: negative (elapsed time, counting upward from zero into negative territory) + * + * @returns Timer value in milliseconds, or null if the timer has not been started + */ + getDuration(): number | null -export interface IPlaylistTTimerStateTimeOfDay { - /** The mode of the T-timer */ - readonly mode: 'timeOfDay' - /** The current remaining time of the timer, in milliseconds */ - readonly currentTime: number - /** The target timestamp of the timer, in milliseconds */ - readonly targetTime: number + /** + * Get the zero time (reference point) for the timer + * - For countdown/timeOfDay timers: the absolute timestamp when the timer reaches zero + * - For freeRun timers: the absolute timestamp when the timer started (what it counts from) + * For paused timers, calculates when zero would be if resumed now. + * @returns Unix timestamp in milliseconds, or null if timer is not initialized + */ + getZeroTime(): number | null /** - * The raw target string of the timer, as provided when setting the timer - * (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number) + * Get the projected duration in milliseconds + * This returns the projected timer value when we expect to reach the anchor part. + * Used to calculate over/under (how far ahead or behind schedule we are). + * @returns Projected duration in milliseconds, or null if no projection is set */ - readonly targetRaw: string | number + getProjectedDuration(): number | null - /** If the countdown is set to stop at zero, or continue into negative values */ - readonly stopAtZero: boolean + /** + * Get the projected zero time (reference point) + * This returns when we project the timer will reach zero based on scheduled durations. + * For paused projections (when pushing/delayed), calculates when zero would be if resumed now. + * @returns Unix timestamp in milliseconds, or null if no projection is set + */ + getProjectedZeroTime(): number | null } diff --git a/packages/blueprints-integration/src/documents/partInstance.ts b/packages/blueprints-integration/src/documents/partInstance.ts index 9f30ff1bfb2..b4509cf9f22 100644 --- a/packages/blueprints-integration/src/documents/partInstance.ts +++ b/packages/blueprints-integration/src/documents/partInstance.ts @@ -1,10 +1,27 @@ import type { Time } from '../common.js' import type { IBlueprintPartDB } from './part.js' +import type { ITranslatableMessage } from '../translations.js' export type PartEndState = unknown +/** + * Properties of a PartInstance that can be modified at runtime by blueprints. + * These are runtime state properties, distinct from the planned Part properties. + */ +export interface IBlueprintMutatablePartInstance { + /** + * If set, this PartInstance exists and is valid as being next, but it cannot be taken in its current state. + * This can be used to block taking a PartInstance that requires user action to resolve. + * This is a runtime validation issue, distinct from the planned `invalidReason` on the Part itself. + */ + invalidReason?: ITranslatableMessage +} + /** The Part instance sent from Core */ -export interface IBlueprintPartInstance { +export interface IBlueprintPartInstance< + TPrivateData = unknown, + TPublicData = unknown, +> extends IBlueprintMutatablePartInstance { _id: string /** The segment ("Title") this line belongs to */ segmentId: string diff --git a/packages/blueprints-integration/src/documents/playlistTiming.ts b/packages/blueprints-integration/src/documents/playlistTiming.ts index 672701d2ba8..41ae942dfa0 100644 --- a/packages/blueprints-integration/src/documents/playlistTiming.ts +++ b/packages/blueprints-integration/src/documents/playlistTiming.ts @@ -4,6 +4,7 @@ export enum PlaylistTimingType { None = 'none', ForwardTime = 'forward-time', BackTime = 'back-time', + Duration = 'duration', } export interface PlaylistTimingBase { @@ -49,4 +50,29 @@ export interface PlaylistTimingBackTime extends PlaylistTimingBase { expectedEnd: Time } -export type RundownPlaylistTiming = PlaylistTimingNone | PlaylistTimingForwardTime | PlaylistTimingBackTime +/** + * This mode is intended for shows with a "floating start", + * meaning they will start based on when the show before them on the channel ends. + * In this mode, we will preserve the Duration and automatically calculate the expectedEnd + * based on the _actual_ start of the show (playlist.startedPlayback). + * + * The optional expectedStart property allows setting a start property of the show that will not affect + * timing calculations, only purpose is to drive UI and inform the users about the preliminary plan as + * planned in the editorial planning tool. + */ +export interface PlaylistTimingDuration extends PlaylistTimingBase { + type: PlaylistTimingType.Duration + /** A stipulated start time, to drive UIs pre-show, but not affecting calculations during the show. + */ + expectedStart?: Time + /** Planned duration of the rundown playlist + * When the show starts, an expectedEnd gets automatically calculated with this as an offset from that starting point + */ + expectedDuration: number +} + +export type RundownPlaylistTiming = + | PlaylistTimingNone + | PlaylistTimingForwardTime + | PlaylistTimingBackTime + | PlaylistTimingDuration diff --git a/packages/blueprints-integration/src/documents/rundown.ts b/packages/blueprints-integration/src/documents/rundown.ts index 9daa30383a6..f8c08405731 100644 --- a/packages/blueprints-integration/src/documents/rundown.ts +++ b/packages/blueprints-integration/src/documents/rundown.ts @@ -55,6 +55,9 @@ export interface IBlueprintRundownDBData { export interface IBlueprintSegmentRundown { externalId: string + /** Rundown timing information */ + timing: RundownPlaylistTiming + /** Arbitraty data storage for internal use in the blueprints */ privateData?: TPrivateData /** Arbitraty data relevant for other systems, made available to them through APIs */ diff --git a/packages/blueprints-integration/src/documents/segment.ts b/packages/blueprints-integration/src/documents/segment.ts index a0308e8027c..06f5a8ead0a 100644 --- a/packages/blueprints-integration/src/documents/segment.ts +++ b/packages/blueprints-integration/src/documents/segment.ts @@ -1,4 +1,5 @@ import { UserEditingDefinition, UserEditingProperties } from '../userEditing.js' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' export enum SegmentDisplayMode { Timeline = 'timeline', @@ -40,7 +41,19 @@ export interface IBlueprintSegment = { + type: 'tsr' + /** The id of the playout device, e.g. `'atem0'` */ + deviceId: string + /** The type of the playout device */ + deviceType: TDevice + /** The event key to subscribe to, e.g. `'me.0.programInput'` */ + event: keyof TSR.TSREventTypesMap[TDevice] +} + +/** + * A subscription to a named event on any TSR playout device. + * + * This is a discriminated union over all known device types, so `deviceType` and `event` + * are always correlated — the compiler will reject an ATEM event key on an Abstract device, etc. + */ +export type BlueprintExternalEventSubscription = { + [TDevice in TSREventDeviceType]: TSRExternalEventSubscription +}[TSREventDeviceType] diff --git a/packages/blueprints-integration/src/index.ts b/packages/blueprints-integration/src/index.ts index ac46527404c..c73d6280872 100644 --- a/packages/blueprints-integration/src/index.ts +++ b/packages/blueprints-integration/src/index.ts @@ -5,6 +5,7 @@ export * from './common.js' export * from './content.js' export * from './context/index.js' export * from './documents/index.js' +export * from './externalEvent.js' export * from './ingest.js' export * from './ingest-types.js' export * from './lib.js' diff --git a/packages/blueprints-integration/src/userEditing.ts b/packages/blueprints-integration/src/userEditing.ts index 0d301659504..54dac5d6d91 100644 --- a/packages/blueprints-integration/src/userEditing.ts +++ b/packages/blueprints-integration/src/userEditing.ts @@ -8,10 +8,35 @@ import { DefaultUserOperationsTypes } from './ingest.js' * Description of a user performed editing operation allowed on an document */ export type UserEditingDefinition = + | UserEditingDefinitionState | UserEditingDefinitionAction | UserEditingDefinitionForm | UserEditingDefinitionSofieDefault +/** + * A simple 'state' that can be signlaled to the user, but has no associated action. This is useful for indicating + * things like "This piece is being held" or "This piece is being affected by a global action" + */ +export interface UserEditingDefinitionState { + type: UserEditingType.STATE + /** Id of this operation */ + id: string + /** Label to show to the user for this operation */ + label: ITranslatableMessage + /** Icon to show when this action is 'active' + * + * This can either be a relative URL to an image in the Blueprints assets or a `data:` URL + */ + icon?: string + /** Icon to show when this action is 'disabled' + * + * This can either be a relative URL to an image in the Blueprints assets or a `data:` URL + */ + iconInactive?: string + /** Whether this action should be indicated as being active */ + isActive?: boolean +} + /** * A simple 'action' that can be performed */ @@ -65,6 +90,8 @@ export interface UserEditingDefinitionSofieDefault { } export enum UserEditingType { + /** State */ + STATE = 'state', /** Action */ ACTION = 'action', /** Form */ diff --git a/packages/blueprints-integration/tsconfig.build.json b/packages/blueprints-integration/tsconfig.build.json index 7d4b98c2b9b..e781b5ba26d 100755 --- a/packages/blueprints-integration/tsconfig.build.json +++ b/packages/blueprints-integration/tsconfig.build.json @@ -12,7 +12,9 @@ }, "resolveJsonModule": true, "types": ["node"], - "composite": true + "composite": true, + "module": "node20", + "esModuleInterop": true }, "references": [ { diff --git a/packages/corelib/jest.config.js b/packages/corelib/jest.config.js index 04b8ea8dd1c..a992fb8e2d2 100644 --- a/packages/corelib/jest.config.js +++ b/packages/corelib/jest.config.js @@ -9,12 +9,15 @@ module.exports = { diagnostics: { ignoreCodes: [ 151002, // hybrid module kind (Node16/18/Next) + 2823, // Import attributes not supported in CJS mode (ts-jest forces CJS, emits require() anyway) ], }, }, ], }, moduleNameMapper: { + '^@sofie-automation/shared-lib/dist/(.+)\\.js$': '/../shared-lib/src/$1', + '^@sofie-automation/shared-lib/dist/(.+)$': '/../shared-lib/src/$1', '(.+)\\.js$': '$1', }, testMatch: ['**/__tests__/**/*.(spec|test).(ts|js)'], diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 8dfba527482..18ccd7801d3 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -39,19 +39,18 @@ "@sofie-automation/blueprints-integration": "26.3.0-2", "@sofie-automation/shared-lib": "26.3.0-2", "fast-clone": "^1.5.13", - "i18next": "^21.10.0", + "i18next": "^26.0.8", "influx": "^5.12.0", "nanoid": "^3.3.11", "object-path": "^0.11.8", "prom-client": "^15.1.3", - "timecode": "0.0.4", "tslib": "^2.8.1", "type-fest": "^4.41.0", - "underscore": "^1.13.7" + "underscore": "^1.13.8" }, "peerDependencies": { - "mongodb": "^6.12.0" + "mongodb": "^7.1.1" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/packages/corelib/src/StatusMessageResolver.ts b/packages/corelib/src/StatusMessageResolver.ts new file mode 100644 index 00000000000..f1c22b7eca9 --- /dev/null +++ b/packages/corelib/src/StatusMessageResolver.ts @@ -0,0 +1,159 @@ +import { BlueprintId } from './dataModel/Ids.js' +import { generateTranslation } from './lib.js' +import { ITranslatableMessage, wrapTranslatableMessageFromBlueprints } from './TranslatableMessage.js' +import { SystemErrorCode } from '@sofie-automation/shared-lib/dist/systemErrorMessages' +import type { DeviceStatusContext, DeviceStatusMessageFunction } from '@sofie-automation/blueprints-integration' + +// Re-export for consumers of corelib +export type { DeviceStatusContext, DeviceStatusMessageFunction } + +/** + * Default error messages for system errors + */ +const DEFAULT_SYSTEM_ERROR_MESSAGES: Record = { + [SystemErrorCode.DATABASE_CONNECTION_LOST]: generateTranslation('Database connection to {{database}} lost'), + [SystemErrorCode.INSUFFICIENT_RESOURCES]: generateTranslation( + 'Insufficient {{resource}}: {{available}} available, {{required}} required' + ), + [SystemErrorCode.SERVICE_UNAVAILABLE]: generateTranslation('Service {{service}} unavailable: {{reason}}'), +} + +/** + * Device status messages map - can contain string templates or functions. + * Functions are evaluated at runtime, strings use {{variable}} interpolation. + */ +export type DeviceStatusMessages = Record + +/** + * System error messages map - string templates only. + * Uses {{variable}} interpolation. + */ +export type SystemErrorMessages = Partial> + +/** + * Resolves status messages with blueprint customizations. + * Works with runtime blueprint manifests (evaluated at setStatus time). + * + * For device statuses, the default message templates come from TSR devices. + * Studio blueprints can override these by providing custom templates or functions in deviceStatusMessages. + * + * For system errors, System blueprints can override the defaults in systemErrorMessages. + * + * @example + * ```typescript + * import { AtemStatusCode, AtemStatusMessages } from 'timeline-state-resolver-types' + * + * // Get blueprint manifest at runtime (from evalBlueprint or similar) + * const studioManifest = await getBlueprintManifest(studioBlueprintId) + * + * // Create resolver for device statuses + * const resolver = new StatusMessageResolver( + * studioBlueprintId, + * studioManifest.deviceStatusMessages + * ) + * + * // Resolve device status - pass default message from TSR + * const message = resolver.getDeviceStatusMessage( + * AtemStatusCode.DISCONNECTED, + * { deviceName: 'Vision Mixer', host: '192.168.1.10' }, + * AtemStatusMessages[AtemStatusCode.DISCONNECTED] + * ) + * ``` + */ +export class StatusMessageResolver { + readonly #blueprintId: BlueprintId | undefined + readonly #deviceStatusMessages: DeviceStatusMessages | undefined + readonly #systemErrorMessages: SystemErrorMessages | undefined + + constructor( + blueprintId: BlueprintId | undefined, + deviceStatusMessages?: DeviceStatusMessages, + systemErrorMessages?: SystemErrorMessages + ) { + this.#blueprintId = blueprintId + this.#deviceStatusMessages = deviceStatusMessages + this.#systemErrorMessages = systemErrorMessages + } + + /** + * Get a translatable message for a device status. + * + * @param errorCode - The status code string from TSR (e.g., 'DEVICE_ATEM_DISCONNECTED') + * @param context - Context values for message interpolation (deviceName, deviceId, and TSR status context) + * @param defaultMessage - The default message template from TSR (e.g., 'ATEM disconnected') + * @returns ITranslatableMessage with the resolved message, or null if suppressed + */ + getDeviceStatusMessage( + errorCode: string, + context: DeviceStatusContext, + defaultMessage: string + ): ITranslatableMessage | null { + // Check blueprint messages from Studio blueprint manifest + if (this.#deviceStatusMessages) { + const blueprintMessage = this.#deviceStatusMessages[errorCode] + + // Evaluate if function or use as string template + let result: string | undefined + if (typeof blueprintMessage === 'function') { + try { + result = blueprintMessage(context) + } catch { + // Blueprint function threw - fall through to default message + } + } else { + result = blueprintMessage + } + + if (result === '') { + // Empty string means suppress the message + return null + } + + if (typeof result === 'string') { + // Custom message from blueprint - wrap with blueprint namespace if ID provided + return this.#blueprintId + ? wrapTranslatableMessageFromBlueprints({ key: result, args: context }, [this.#blueprintId]) + : { key: result, args: context } + } + + // undefined or not found - fall through to default + } + + // Use default message from TSR as-is (TSR messages already include device name prefix) + return { + key: defaultMessage, + args: context, + } + } + + /** + * Get a translatable message for a system error. + * Uses customizations from the System blueprint if available. + */ + getSystemErrorMessage( + errorCode: SystemErrorCode | string, + args: { [k: string]: unknown } + ): ITranslatableMessage | null { + // Check blueprint messages from System blueprint manifest + if (this.#systemErrorMessages) { + const blueprintMessage = this.#systemErrorMessages[errorCode] + + if (blueprintMessage === '') { + // Empty string means suppress the message + return null + } + + if (typeof blueprintMessage === 'string') { + return this.#blueprintId + ? wrapTranslatableMessageFromBlueprints({ key: blueprintMessage, args }, [this.#blueprintId]) + : { key: blueprintMessage, args } + } + } + + // Use default message + return { + key: DEFAULT_SYSTEM_ERROR_MESSAGES[errorCode as SystemErrorCode]?.key ?? errorCode, + args, + } + } +} diff --git a/packages/corelib/src/TranslatableMessage.ts b/packages/corelib/src/TranslatableMessage.ts index b3a905b777f..a6504a4f8c1 100644 --- a/packages/corelib/src/TranslatableMessage.ts +++ b/packages/corelib/src/TranslatableMessage.ts @@ -1,5 +1,4 @@ import { ITranslatableMessage as IBlueprintTranslatableMessage } from '@sofie-automation/blueprints-integration' -import { TFunction } from 'i18next' import { ReadonlyDeep } from 'type-fest' import { BlueprintId } from './dataModel/Ids.js' @@ -15,12 +14,12 @@ export interface ITranslatableMessage extends IBlueprintTranslatableMessage { * Convenience function to translate a message using a supplied translation function. * * @param {ITranslatableMessage} translatable - the translatable to translate - * @param {TFunction} i18nTranslator - the translation function to use + * @param i18nTranslator - the translation function to use * @returns the translation with arguments applied */ export function translateMessage( translatable: ITranslatableMessage | ReadonlyDeep, - i18nTranslator: TFunction + i18nTranslator: (key: string, options?: any) => string ): string { // the reason for injecting the translation function rather than including the inited function from i18n.ts // is to avoid a situation where this is accidentally used from the server side causing an error diff --git a/packages/corelib/src/__tests__/StatusMessageResolver.test.ts b/packages/corelib/src/__tests__/StatusMessageResolver.test.ts new file mode 100644 index 00000000000..9b80081d82f --- /dev/null +++ b/packages/corelib/src/__tests__/StatusMessageResolver.test.ts @@ -0,0 +1,296 @@ +import { StatusMessageResolver } from '../StatusMessageResolver.js' +import { SystemErrorCode } from '@sofie-automation/shared-lib/dist/systemErrorMessages' +import { protectString } from '../protectedString.js' + +// Mock device status codes (these would come from TSR in production) +const MockDeviceStatusCode = { + HTTP_TIMEOUT: 'DEVICE_HTTP_TIMEOUT', + CASPARCG_DISCONNECTED: 'DEVICE_CASPARCG_DISCONNECTED', + CASPARCG_FILE_NOT_FOUND: 'DEVICE_CASPARCG_FILE_NOT_FOUND', +} as const + +// Mock default messages (these would come from TSR in production) +const MockDeviceStatusMessages = { + [MockDeviceStatusCode.HTTP_TIMEOUT]: '{{deviceName}}: HTTP request to {{url}} timed out after {{timeout}}ms', + [MockDeviceStatusCode.CASPARCG_DISCONNECTED]: '{{deviceName}}: CasparCG server at {{host}}:{{port}} disconnected', + [MockDeviceStatusCode.CASPARCG_FILE_NOT_FOUND]: '{{deviceName}}: File "{{fileName}}" not found on CasparCG server', +} + +describe('StatusMessageResolver', () => { + describe('Device statuses', () => { + it('returns default message from TSR when no blueprint provided', () => { + const resolver = new StatusMessageResolver(undefined, undefined, undefined) + const message = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.HTTP_TIMEOUT, + { + deviceName: 'Graphics Server', + url: 'http://graphics/api', + timeout: 5000, + }, + MockDeviceStatusMessages[MockDeviceStatusCode.HTTP_TIMEOUT] + ) + + expect(message).toBeTruthy() + expect(message?.key).toBe('{{deviceName}}: HTTP request to {{url}} timed out after {{timeout}}ms') + expect(message?.args).toMatchObject({ + deviceName: 'Graphics Server', + url: 'http://graphics/api', + timeout: 5000, + }) + }) + + it('returns blueprint custom message when provided', () => { + const resolver = new StatusMessageResolver( + protectString('studioBlueprint123'), + { + [MockDeviceStatusCode.HTTP_TIMEOUT]: 'Graphics system {{deviceName}} not responding', + }, + undefined + ) + + const message = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.HTTP_TIMEOUT, + { + deviceName: 'Graphics Server', + url: 'http://graphics/api', + timeout: 5000, + }, + MockDeviceStatusMessages[MockDeviceStatusCode.HTTP_TIMEOUT] + ) + + expect(message).toBeTruthy() + expect(message?.key).toBe('Graphics system {{deviceName}} not responding') + expect(message?.args?.deviceName).toBe('Graphics Server') + }) + + it('returns null when blueprint provides empty string (suppression)', () => { + const resolver = new StatusMessageResolver( + protectString('studioBlueprint123'), + { + [MockDeviceStatusCode.HTTP_TIMEOUT]: '', + }, + undefined + ) + + const message = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.HTTP_TIMEOUT, + { + deviceName: 'Graphics Server', + }, + MockDeviceStatusMessages[MockDeviceStatusCode.HTTP_TIMEOUT] + ) + + expect(message).toBeNull() + }) + + it('evaluates function-based status messages', () => { + const resolver = new StatusMessageResolver( + protectString('studioBlueprint123'), + { + [MockDeviceStatusCode.HTTP_TIMEOUT]: (context) => { + const timeout = context.timeout as number + if (timeout > 10000) { + return `${context.deviceName}: Critical timeout (${timeout}ms) - check network` + } + return `${context.deviceName}: Request timeout` + }, + }, + undefined + ) + + // Test with small timeout + const shortMessage = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.HTTP_TIMEOUT, + { + deviceName: 'Graphics Server', + timeout: 5000, + }, + MockDeviceStatusMessages[MockDeviceStatusCode.HTTP_TIMEOUT] + ) + + expect(shortMessage?.key).toBe('Graphics Server: Request timeout') + + // Test with long timeout + const longMessage = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.HTTP_TIMEOUT, + { + deviceName: 'Graphics Server', + timeout: 15000, + }, + MockDeviceStatusMessages[MockDeviceStatusCode.HTTP_TIMEOUT] + ) + + expect(longMessage?.key).toBe('Graphics Server: Critical timeout (15000ms) - check network') + }) + + it('suppresses message when function returns empty string', () => { + const resolver = new StatusMessageResolver( + protectString('studioBlueprint123'), + { + [MockDeviceStatusCode.HTTP_TIMEOUT]: () => '', // Always suppress + }, + undefined + ) + + const message = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.HTTP_TIMEOUT, + { + deviceName: 'Graphics Server', + }, + MockDeviceStatusMessages[MockDeviceStatusCode.HTTP_TIMEOUT] + ) + + expect(message).toBeNull() + }) + + it('returns default for CasparCG errors', () => { + const resolver = new StatusMessageResolver(undefined, undefined, undefined) + const message = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.CASPARCG_FILE_NOT_FOUND, + { + deviceName: 'CasparCG1', + fileName: 'video.mp4', + }, + MockDeviceStatusMessages[MockDeviceStatusCode.CASPARCG_FILE_NOT_FOUND] + ) + + expect(message).toBeTruthy() + expect(message?.key).toContain('File') + expect(message?.key).toContain('not found') + }) + + it('falls back to TSR default when blueprint has no customization for that status code', () => { + const resolver = new StatusMessageResolver( + protectString('studioBlueprint123'), + { + [MockDeviceStatusCode.HTTP_TIMEOUT]: 'Custom timeout message', + }, + undefined + ) + + // Ask for a different error that has no customization + const message = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.CASPARCG_DISCONNECTED, + { + deviceName: 'CasparCG1', + host: 'localhost', + }, + MockDeviceStatusMessages[MockDeviceStatusCode.CASPARCG_DISCONNECTED] + ) + + expect(message).toBeTruthy() + expect(message?.key).toContain('CasparCG') + }) + }) + + describe('System errors', () => { + it('returns default message when no blueprint provided', () => { + const resolver = new StatusMessageResolver(undefined, undefined, undefined) + const message = resolver.getSystemErrorMessage(SystemErrorCode.DATABASE_CONNECTION_LOST, { + database: 'MongoDB', + }) + + expect(message).toBeTruthy() + expect(message?.key).toContain('Database') + expect(message?.args?.database).toBe('MongoDB') + }) + + it('returns blueprint custom message', () => { + const resolver = new StatusMessageResolver(protectString('systemBlueprint123'), undefined, { + [SystemErrorCode.DATABASE_CONNECTION_LOST]: 'System database offline - please wait', + }) + + const message = resolver.getSystemErrorMessage(SystemErrorCode.DATABASE_CONNECTION_LOST, { + database: 'MongoDB', + }) + + expect(message?.key).toBe('System database offline - please wait') + }) + + it('suppresses message with empty string', () => { + const resolver = new StatusMessageResolver(protectString('systemBlueprint123'), undefined, { + [SystemErrorCode.INSUFFICIENT_RESOURCES]: '', + }) + + const message = resolver.getSystemErrorMessage(SystemErrorCode.INSUFFICIENT_RESOURCES, { + resource: 'memory', + }) + + expect(message).toBeNull() + }) + }) + + describe('Combined blueprints', () => { + it('resolves device statuses and system errors with same blueprint ID', () => { + // Note: In practice, device statuses use Studio blueprint ID and system errors use System blueprint ID + // But the resolver doesn't enforce this - it just associates the ID with any messages it generates + const resolver = new StatusMessageResolver( + protectString('blueprint123'), + { + [MockDeviceStatusCode.HTTP_TIMEOUT]: 'Custom device status', + }, + { + [SystemErrorCode.DATABASE_CONNECTION_LOST]: 'Custom system error', + } + ) + + const deviceMessage = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.HTTP_TIMEOUT, + { deviceName: 'Server' }, + MockDeviceStatusMessages[MockDeviceStatusCode.HTTP_TIMEOUT] + ) + + const systemMessage = resolver.getSystemErrorMessage(SystemErrorCode.DATABASE_CONNECTION_LOST, { + database: 'MongoDB', + }) + + expect(deviceMessage?.key).toBe('Custom device status') + expect(systemMessage?.key).toBe('Custom system error') + }) + + it('includes correct blueprint namespace for messages', () => { + const resolver = new StatusMessageResolver( + protectString('blueprint123'), + { + [MockDeviceStatusCode.HTTP_TIMEOUT]: 'Custom device status', + }, + { + [SystemErrorCode.DATABASE_CONNECTION_LOST]: 'Custom system error', + } + ) + + const deviceMessage = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.HTTP_TIMEOUT, + { deviceName: 'Server' }, + MockDeviceStatusMessages[MockDeviceStatusCode.HTTP_TIMEOUT] + ) + + const systemMessage = resolver.getSystemErrorMessage(SystemErrorCode.DATABASE_CONNECTION_LOST, { + database: 'MongoDB', + }) + + // Both messages should have the same blueprint namespace + expect(deviceMessage?.namespaces).toContain('blueprint_blueprint123') + expect(systemMessage?.namespaces).toContain('blueprint_blueprint123') + }) + + it('does not add namespace when no blueprint ID provided', () => { + const resolver = new StatusMessageResolver( + undefined, + { + [MockDeviceStatusCode.HTTP_TIMEOUT]: 'Custom device status', + }, + undefined + ) + + const message = resolver.getDeviceStatusMessage( + MockDeviceStatusCode.HTTP_TIMEOUT, + { deviceName: 'Server' }, + MockDeviceStatusMessages[MockDeviceStatusCode.HTTP_TIMEOUT] + ) + + // No namespace when no blueprint ID + expect(message?.namespaces).toBeUndefined() + }) + }) +}) diff --git a/packages/corelib/src/__tests__/lib.spec.ts b/packages/corelib/src/__tests__/lib.spec.ts index a2d17cdcbe5..d22e5dd7bc6 100644 --- a/packages/corelib/src/__tests__/lib.spec.ts +++ b/packages/corelib/src/__tests__/lib.spec.ts @@ -146,7 +146,7 @@ describe('Lib', () => { errorCode: 42, key: UserErrorMessage.ValidationFailed, userMessage: { - key: 'Validation failed!', + key: 'Validation failed! {{message}}', args: {}, }, rawError: { diff --git a/packages/corelib/src/dataModel/PartInstance.ts b/packages/corelib/src/dataModel/PartInstance.ts index 1ec7439353e..e5e73d7dec3 100644 --- a/packages/corelib/src/dataModel/PartInstance.ts +++ b/packages/corelib/src/dataModel/PartInstance.ts @@ -1,7 +1,7 @@ import { PartEndState, Time } from '@sofie-automation/blueprints-integration' import { PartCalculatedTimings } from '../playout/timings.js' import { PartInstanceId, RundownId, RundownPlaylistActivationId, SegmentId, SegmentPlayoutId } from './Ids.js' -import { DBPart } from './Part.js' +import { DBPart, PartInvalidReason } from './Part.js' export interface DBPartInstance { _id: PartInstanceId @@ -40,6 +40,13 @@ export interface DBPartInstance { /** If taking out of the current part is blocked, this is the time it is blocked until */ blockTakeUntil?: number + + /** + * If set, this PartInstance exists and is valid as being next, but it cannot be taken in its current state. + * This can be used to block taking a PartInstance that requires user action to resolve. + * This is a runtime validation issue, distinct from the planned `invalidReason` on the Part itself. + */ + invalidReason?: PartInvalidReason } export interface PartInstance extends DBPartInstance { diff --git a/packages/corelib/src/dataModel/PieceContentStatus.ts b/packages/corelib/src/dataModel/PieceContentStatus.ts index d294ed3dca1..c2a46ba3dcf 100644 --- a/packages/corelib/src/dataModel/PieceContentStatus.ts +++ b/packages/corelib/src/dataModel/PieceContentStatus.ts @@ -31,6 +31,11 @@ export interface UIPieceContentStatus { status: PieceContentStatusObj } +export interface SplitBoxPreviewUrls { + thumbnailUrl?: string + previewUrl?: string +} + export interface PieceContentStatusObj { status: PieceStatusCode messages: ITranslatableMessage[] @@ -47,4 +52,11 @@ export interface PieceContentStatusObj { contentDuration: number | undefined progress: number | undefined + + /** + * Per-box preview URLs for SPLITS pieces. + * Same length and order as `SplitsContent.boxSourceConfiguration`. + * Non-file boxes (camera, remote, etc.) use `{}`. + */ + boxPreviews?: SplitBoxPreviewUrls[] } diff --git a/packages/corelib/src/dataModel/Rundown.ts b/packages/corelib/src/dataModel/Rundown.ts index b288fe10dd0..5299e8c5792 100644 --- a/packages/corelib/src/dataModel/Rundown.ts +++ b/packages/corelib/src/dataModel/Rundown.ts @@ -1,4 +1,8 @@ -import { RundownPlaylistTiming, Time } from '@sofie-automation/blueprints-integration' +import { + BlueprintExternalEventSubscription, + RundownPlaylistTiming, + Time, +} from '@sofie-automation/blueprints-integration' import { RundownId, StudioId, @@ -84,6 +88,9 @@ export interface Rundown { * User editing definitions for this rundown */ userEditOperations?: CoreUserEditingDefinition[] + + /** Subscriptions to external device events, as declared by the blueprint */ + externalEventSubscriptions?: BlueprintExternalEventSubscription[] } /** A description of where a Rundown originated from */ diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist/RundownPlaylist.ts similarity index 60% rename from packages/corelib/src/dataModel/RundownPlaylist.ts rename to packages/corelib/src/dataModel/RundownPlaylist/RundownPlaylist.ts index 06cf6d3ff5f..ae5924f79f1 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist/RundownPlaylist.ts @@ -8,9 +8,10 @@ import { RundownPlaylistId, StudioId, RundownId, -} from './Ids.js' -import { RundownPlaylistNote } from './Notes.js' +} from '../Ids.js' +import { RundownPlaylistNote } from '../Notes.js' import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' +import { RundownTTimer } from './TTimers.js' /** Details of an ab-session requested by the blueprints in onTimelineGenerate */ export interface ABSessionInfo { @@ -94,144 +95,6 @@ export interface QuickLoopProps { forceAutoNext: ForceQuickLoopAutoNext } -export type RundownTTimerMode = RundownTTimerModeFreeRun | RundownTTimerModeCountdown | RundownTTimerModeTimeOfDay - -export interface RundownTTimerModeFreeRun { - readonly type: 'freeRun' -} -export interface RundownTTimerModeCountdown { - readonly type: 'countdown' - /** - * The original duration of the countdown in milliseconds, so that we know what value to reset to - */ - readonly duration: number - - /** - * If the countdown should stop at zero, or continue into negative values - */ - readonly stopAtZero: boolean -} -export interface RundownTTimerModeTimeOfDay { - readonly type: 'timeOfDay' - - /** - * The raw target string of the timer, as provided when setting the timer - * (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number) - */ - readonly targetRaw: string | number - - /** - * If the countdown should stop at zero, or continue into negative values - */ - readonly stopAtZero: boolean -} - -/** - * Timing state for a timer, optimized for efficient client rendering. - * When running, the client calculates current time from zeroTime. - * When paused, the duration is frozen and sent directly. - * pauseTime indicates when the timer should automatically pause (when current part ends and overrun begins). - * - * Client rendering logic: - * ```typescript - * if (state.paused === true) { - * // Manually paused by user or already pushing/overrun - * duration = state.duration - * } else if (state.pauseTime && now >= state.pauseTime) { - * // Auto-pause at overrun (current part ended) - * duration = state.zeroTime - state.pauseTime - * } else { - * // Running normally - * duration = state.zeroTime - now - * } - * ``` - */ -export type TimerState = - | { - /** Whether the timer is paused */ - paused: false - /** The absolute timestamp (ms) when the timer reaches/reached zero */ - zeroTime: number - /** Optional timestamp when the timer should pause (when current part ends) */ - pauseTime?: number | null - } - | { - /** Whether the timer is paused */ - paused: true - /** The frozen duration value in milliseconds */ - duration: number - /** Optional timestamp when the timer should pause (null when already paused/pushing) */ - pauseTime?: number | null - } - -/** - * Calculate the current duration for a timer state. - * Handles paused, auto-pause (pauseTime), and running states. - * - * @param state The timer state - * @param now Current timestamp in milliseconds - * @returns The current duration in milliseconds - */ -export function timerStateToDuration(state: TimerState, now: number): number { - if (state.paused) { - // Manually paused by user or already pushing/overrun - return state.duration - } else if (state.pauseTime && now >= state.pauseTime) { - // Auto-pause at overrun (current part ended) - return state.zeroTime - state.pauseTime - } else { - // Running normally - return state.zeroTime - now - } -} - -export type RundownTTimerIndex = 1 | 2 | 3 - -export interface RundownTTimer { - readonly index: RundownTTimerIndex - - /** A label for the timer */ - label: string - - /** The current mode of the timer, or null if not configured - * - * This defines how the timer behaves - */ - mode: RundownTTimerMode | null - - /** The current state of the timer, or null if not configured - * - * This contains the information needed to calculate the current time of the timer - */ - state: TimerState | null - - /** The projected time when we expect to reach the anchor part, for calculating over/under diff. - * - * Based on scheduled durations of remaining parts and segments up to the anchor. - * The over/under diff is calculated as the difference between this projection and the timer's target (state.zeroTime). - * - * Running means we are progressing towards the anchor (projection moves with real time) - * Paused means we are pushing (e.g. overrunning the current segment, so the anchor is being delayed) - * - * Calculated automatically when anchorPartId is set, or can be set manually by a blueprint if custom logic is needed. - */ - projectedState?: TimerState - - /** The target Part that this timer is counting towards (the "timing anchor") - * - * This is typically a "break" part or other milestone in the rundown. - * When set, the server calculates projectedState based on when we expect to reach this part. - * If not set, projectedState is not calculated automatically but can still be set manually by a blueprint. - */ - anchorPartId?: PartId - - /* - * Future ideas: - * allowUiControl: boolean - * display: { ... } // some kind of options for how to display in the ui - */ -} - export interface DBRundownPlaylist { _id: RundownPlaylistId /** External ID (source) of the playlist */ diff --git a/packages/corelib/src/dataModel/RundownPlaylist/TTimers.ts b/packages/corelib/src/dataModel/RundownPlaylist/TTimers.ts new file mode 100644 index 00000000000..ef974b741a5 --- /dev/null +++ b/packages/corelib/src/dataModel/RundownPlaylist/TTimers.ts @@ -0,0 +1,166 @@ +import { PartId } from '../Ids.js' + +export type RundownTTimerMode = RundownTTimerModeFreeRun | RundownTTimerModeCountdown | RundownTTimerModeTimeOfDay + +export interface RundownTTimerModeFreeRun { + readonly type: 'freeRun' +} +export interface RundownTTimerModeCountdown { + readonly type: 'countdown' + /** + * The original duration of the countdown in milliseconds, so that we know what value to reset to + */ + readonly duration: number + + /** + * If the countdown should stop at zero, or continue into negative values + */ + readonly stopAtZero: boolean +} +export interface RundownTTimerModeTimeOfDay { + readonly type: 'timeOfDay' + + /** + * The raw target string of the timer, as provided when setting the timer + * (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number) + */ + readonly targetRaw: string | number + + /** + * If the countdown should stop at zero, or continue into negative values + */ + readonly stopAtZero: boolean +} + +/** + * Timing state for a timer, optimized for efficient client rendering. + * When running, the client calculates current time from zeroTime. + * When paused, the duration is frozen and sent directly. + * pauseTime indicates when the timer should automatically pause (when current part ends and overrun begins). + * + * Client rendering logic: + * ```typescript + * if (state.paused === true) { + * // Manually paused by user or already pushing/overrun + * duration = state.duration + * } else if (state.pauseTime && now >= state.pauseTime) { + * // Auto-pause at overrun (current part ended) + * duration = state.zeroTime - state.pauseTime + * } else { + * // Running normally + * duration = state.zeroTime - now + * } + * ``` + */ +export type TimerState = + | { + /** Whether the timer is paused */ + paused: false + /** The absolute timestamp (ms) when the timer reaches/reached zero */ + zeroTime: number + /** Optional timestamp when the timer should pause (when current part ends) */ + pauseTime?: number | null + } + | { + /** Whether the timer is paused */ + paused: true + /** The frozen duration value in milliseconds */ + duration: number + /** Optional timestamp when the timer should pause (null when already paused/pushing) */ + pauseTime?: number | null + } + +/** + * Calculate the current duration for a timer state. + * Handles paused, auto-pause (pauseTime), and running states. + * + * @param state The timer state + * @param now Current timestamp in milliseconds + * @returns The current duration in milliseconds + */ +export function timerStateToDuration(state: TimerState, now: number): number { + if (state.paused) { + // Manually paused by user or already pushing/overrun + return state.duration + } else if (state.pauseTime != null && now >= state.pauseTime) { + // Auto-pause at overrun (current part ended) + return state.zeroTime - state.pauseTime + } else { + // Running normally + return state.zeroTime - now + } +} + +/** + * Get the zero time (reference timestamp) for a timer state. + * - For countdown/timeOfDay timers: when the timer reaches zero + * - For freeRun timers: when the timer started (what it counts from) + * For paused timers, calculates when zero would be if resumed now. + * + * @param state The timer state + * @param now Current timestamp in milliseconds + * @returns The zero time timestamp in milliseconds + */ +export function timerStateToZeroTime(state: TimerState, now: number): number { + if (state.paused) { + // Calculate when zero would be if we resumed now + return now + state.duration + } else if (state.pauseTime && now >= state.pauseTime) { + // Auto-pause at overrun (current part ended) + return state.zeroTime - state.pauseTime + now + } else { + // Already have the zero time + return state.zeroTime + } +} + +export type RundownTTimerIndex = 1 | 2 | 3 + +export function isRundownTTimerIndex(index: unknown): index is RundownTTimerIndex { + return typeof index === 'number' && (index === 1 || index === 2 || index === 3) +} + +export interface RundownTTimer { + readonly index: RundownTTimerIndex + + /** A label for the timer */ + label: string + + /** The current mode of the timer, or null if not configured + * + * This defines how the timer behaves + */ + mode: RundownTTimerMode | null + + /** The current state of the timer, or null if not configured + * + * This contains the information needed to calculate the current time of the timer + */ + state: TimerState | null + + /** The projected time when we expect to reach the anchor part, for calculating over/under diff. + * + * Based on scheduled durations of remaining parts and segments up to the anchor. + * The over/under diff is calculated as the difference between this projection and the timer's target (state.zeroTime). + * + * Running means we are progressing towards the anchor (projection moves with real time) + * Paused means we are pushing (e.g. overrunning the current segment, so the anchor is being delayed) + * + * Calculated automatically when anchorPartId is set, or can be set manually by a blueprint if custom logic is needed. + */ + projectedState?: TimerState + + /** The target Part that this timer is counting towards (the "timing anchor") + * + * This is typically a "break" part or other milestone in the rundown. + * When set, the server calculates projectedState based on when we expect to reach this part. + * If not set, projectedState is not calculated automatically but can still be set manually by a blueprint. + */ + anchorPartId?: PartId + + /* + * Future ideas: + * allowUiControl: boolean + * display: { ... } // some kind of options for how to display in the ui + */ +} diff --git a/packages/corelib/src/dataModel/Segment.ts b/packages/corelib/src/dataModel/Segment.ts index 0787d9ad01d..b5a0bb6518f 100644 --- a/packages/corelib/src/dataModel/Segment.ts +++ b/packages/corelib/src/dataModel/Segment.ts @@ -1,4 +1,5 @@ import { SegmentDisplayMode, SegmentTimingInfo } from '@sofie-automation/blueprints-integration' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { SegmentId, RundownId } from './Ids.js' import { SegmentNote } from './Notes.js' import { CoreUserEditingDefinition, CoreUserEditingProperties } from './UserEditingDefinitions.js' @@ -34,8 +35,8 @@ export interface DBSegment { /** User-facing identifier that can be used by the User to identify the contents of a segment in the Rundown source system */ identifier?: string - /** Show the minishelf of the segment */ - showShelf?: boolean + /** Control display of the segment minishelf. Unset means hidden. */ + displayMinishelf?: ShelfButtonSize /** Segment display mode. Default mode is *SegmentDisplayMode.Timeline* */ displayAs?: SegmentDisplayMode diff --git a/packages/corelib/src/dataModel/UserEditingDefinitions.ts b/packages/corelib/src/dataModel/UserEditingDefinitions.ts index fe0911e986a..89336137ff9 100644 --- a/packages/corelib/src/dataModel/UserEditingDefinitions.ts +++ b/packages/corelib/src/dataModel/UserEditingDefinitions.ts @@ -8,10 +8,29 @@ import type { import type { ITranslatableMessage } from '../TranslatableMessage.js' export type CoreUserEditingDefinition = + | CoreUserEditingDefinitionState | CoreUserEditingDefinitionAction | CoreUserEditingDefinitionForm | CoreUserEditingDefinitionSofie +export interface CoreUserEditingDefinitionState { + type: UserEditingType.STATE + /** Id of this operation */ + id: string + /** Label to show to the user for this operation */ + label: ITranslatableMessage + /** Icon to show when this state is 'active'. + * + * This can either be a relative URL to an image in the Blueprints assets or a `data:` URL */ + icon?: string + /** Icon to show when this state is 'disabled'. + * + * This can either be a relative URL to an image in the Blueprints assets or a `data:` URL */ + iconInactive?: string + /** Whether this state should be indicated as being active */ + isActive?: boolean +} + export interface CoreUserEditingDefinitionAction { type: UserEditingType.ACTION /** Id of this operation */ diff --git a/packages/corelib/src/error.ts b/packages/corelib/src/error.ts index 3cbf71c558b..7bd399b9eea 100644 --- a/packages/corelib/src/error.ts +++ b/packages/corelib/src/error.ts @@ -64,6 +64,7 @@ export enum UserErrorMessage { IdempotencyKeyAlreadyUsed = 48, RateLimitExceeded = 49, SystemSingleStudio = 50, + TakePartInstanceInvalid = 51, } const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { @@ -116,7 +117,7 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { [UserErrorMessage.DeviceAlreadyAttachedToStudio]: t(`Device is already attached to another studio.`), [UserErrorMessage.ShowStyleBaseNotFound]: t(`ShowStyleBase not found!`), [UserErrorMessage.NoMigrationsToApply]: t(`No migrations to apply`), - [UserErrorMessage.ValidationFailed]: t('Validation failed!'), + [UserErrorMessage.ValidationFailed]: t('Validation failed! {{message}}'), [UserErrorMessage.AdlibTestingNotAllowed]: t(`Rehearsal mode is not allowed`), [UserErrorMessage.AdlibTestingAlreadyActive]: t(`Rehearsal mode is already active`), [UserErrorMessage.BucketNotFound]: t(`Bucket not found!`), @@ -126,6 +127,7 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { [UserErrorMessage.IdempotencyKeyAlreadyUsed]: t(`Idempotency-Key is already used`), [UserErrorMessage.RateLimitExceeded]: t(`Rate limit exceeded`), [UserErrorMessage.SystemSingleStudio]: t(`System must have exactly one studio`), + [UserErrorMessage.TakePartInstanceInvalid]: t(`Part has issues and cannot be taken`), } export interface SerializedUserError { diff --git a/packages/corelib/src/index.ts b/packages/corelib/src/index.ts index c441bb34c64..bdd28f74029 100644 --- a/packages/corelib/src/index.ts +++ b/packages/corelib/src/index.ts @@ -1,4 +1,13 @@ // Re-export to reduce dependency duplication -export { Timecode } from 'timecode' +export { Timecode } from './timecode.js' export { MOS } from '@sofie-automation/shared-lib/dist/mos' + +// Status message resolver +export { StatusMessageResolver } from './StatusMessageResolver.js' +export type { + DeviceStatusContext, + DeviceStatusMessageFunction, + DeviceStatusMessages, + SystemErrorMessages, +} from './StatusMessageResolver.js' diff --git a/packages/corelib/src/lib.ts b/packages/corelib/src/lib.ts index 289532b916a..0ff295fc144 100644 --- a/packages/corelib/src/lib.ts +++ b/packages/corelib/src/lib.ts @@ -3,7 +3,7 @@ import { ReadonlyDeep } from 'type-fest' import fastClone from 'fast-clone' import { ProtectedString, protectString } from './protectedString.js' import * as objectPath from 'object-path' -import { Timecode } from 'timecode' +import { Timecode } from './timecode.js' import { iterateDeeply, iterateDeeplyEnum, Time } from '@sofie-automation/blueprints-integration' import { IStudioSettings } from './dataModel/Studio.js' import { customAlphabet as createNanoid } from 'nanoid' diff --git a/packages/corelib/src/playout/__tests__/timings.test.ts b/packages/corelib/src/playout/__tests__/timings.test.ts index 131e63d444c..f4e3c4e7dd1 100644 --- a/packages/corelib/src/playout/__tests__/timings.test.ts +++ b/packages/corelib/src/playout/__tests__/timings.test.ts @@ -1,6 +1,6 @@ import { IBlueprintPieceType } from '@sofie-automation/blueprints-integration' import {} from 'type-fest' -import { RundownHoldState } from '../../dataModel/RundownPlaylist.js' +import { RundownHoldState } from '../../dataModel/RundownPlaylist/RundownPlaylist.js' import { literal } from '../../lib.js' import { calculatePartTimings, CalculateTimingsPiece, PartCalculatedTimings } from '../timings.js' diff --git a/packages/corelib/src/playout/rundownTiming.ts b/packages/corelib/src/playout/rundownTiming.ts index ea2ec02e9b8..b0e911f9979 100644 --- a/packages/corelib/src/playout/rundownTiming.ts +++ b/packages/corelib/src/playout/rundownTiming.ts @@ -13,6 +13,7 @@ import { PlaylistTimingBackTime, + PlaylistTimingDuration, PlaylistTimingForwardTime, PlaylistTimingNone, PlaylistTimingType, @@ -34,6 +35,10 @@ export namespace PlaylistTiming { return timing.type === PlaylistTimingType.BackTime } + export function isPlaylistDurationTimed(timing: RundownPlaylistTiming): timing is PlaylistTimingDuration { + return timing.type === PlaylistTimingType.Duration + } + export function getExpectedStart(timing: RundownPlaylistTiming): number | undefined { if (PlaylistTiming.isPlaylistTimingForwardTime(timing)) { return timing.expectedStart @@ -42,6 +47,8 @@ export namespace PlaylistTiming { timing.expectedStart || (timing.expectedDuration ? timing.expectedEnd - timing.expectedDuration : undefined) ) + } else if (PlaylistTiming.isPlaylistDurationTimed(timing)) { + return timing.expectedStart } else { return undefined } @@ -55,19 +62,56 @@ export namespace PlaylistTiming { timing.expectedEnd || (timing.expectedDuration ? timing.expectedStart + timing.expectedDuration : undefined) ) + } else if (PlaylistTiming.isPlaylistDurationTimed(timing)) { + return timing.expectedStart && timing.expectedDuration + ? timing.expectedStart + timing.expectedDuration + : undefined } else { return undefined } } export function getExpectedDuration(timing: RundownPlaylistTiming): number | undefined { - if (PlaylistTiming.isPlaylistTimingForwardTime(timing)) { - return timing.expectedDuration - } else if (PlaylistTiming.isPlaylistTimingBackTime(timing)) { - return timing.expectedDuration - } else { - return undefined + return timing.expectedDuration + } + + export function getEstimatedEnd( + timing: RundownPlaylistTiming, + now: number, + remainingPlaylistDuration?: number, + startedPlayback?: number + ): number | undefined { + if (PlaylistTiming.isPlaylistDurationTimed(timing) && timing.expectedDuration) { + if (startedPlayback) { + return startedPlayback + timing.expectedDuration + } else if (timing.expectedStart) { + return timing.expectedStart + timing.expectedDuration + } + } + + if (remainingPlaylistDuration !== undefined) { + const frontAnchor = startedPlayback ? now : Math.max(now, PlaylistTiming.getExpectedStart(timing) ?? now) + + return frontAnchor + remainingPlaylistDuration } + return undefined + } + + export function getRemainingDuration( + timing: RundownPlaylistTiming, + now: number, + remainingPlaylistDuration?: number, + startedPlayback?: number + ): number | undefined { + if (PlaylistTiming.isPlaylistDurationTimed(timing) && timing.expectedDuration) { + if (startedPlayback) { + return startedPlayback + timing.expectedDuration - now + } else { + return timing.expectedDuration + } + } + + return remainingPlaylistDuration } export function sortTimings( diff --git a/packages/corelib/src/playout/stateCacheResolver.ts b/packages/corelib/src/playout/stateCacheResolver.ts index 962d1637084..fdbe262be72 100644 --- a/packages/corelib/src/playout/stateCacheResolver.ts +++ b/packages/corelib/src/playout/stateCacheResolver.ts @@ -11,7 +11,7 @@ import { SegmentId, RundownId, PartId, PieceId, RundownPlaylistActivationId } fr import { DBPart, PartExtended } from '../dataModel/Part.js' import { Piece, PieceExtended } from '../dataModel/Piece.js' import { PieceInstance, PieceInstancePiece } from '../dataModel/PieceInstance.js' -import { DBRundownPlaylist, QuickLoopMarkerType } from '../dataModel/RundownPlaylist.js' +import { DBRundownPlaylist, QuickLoopMarkerType } from '../dataModel/RundownPlaylist/RundownPlaylist.js' import { DBSegment, SegmentExtended, SegmentOrphanedReason } from '../dataModel/Segment.js' import { literal, groupByToMap, Complete } from '../lib.js' import { FindOptions, mongoWhereFilter } from '../mongo.js' diff --git a/packages/corelib/src/playout/stateCacheResolverTypes.ts b/packages/corelib/src/playout/stateCacheResolverTypes.ts index 6afee8ac543..a94dde03c65 100644 --- a/packages/corelib/src/playout/stateCacheResolverTypes.ts +++ b/packages/corelib/src/playout/stateCacheResolverTypes.ts @@ -11,7 +11,7 @@ import { DBPart, PartExtended } from '../dataModel/Part.js' import { Piece } from '../dataModel/Piece.js' import { PieceInstance } from '../dataModel/PieceInstance.js' import { Rundown } from '../dataModel/Rundown.js' -import { DBRundownPlaylist } from '../dataModel/RundownPlaylist.js' +import { DBRundownPlaylist } from '../dataModel/RundownPlaylist/RundownPlaylist.js' import { DBSegment, SegmentExtended } from '../dataModel/Segment.js' import { FindOneOptions, FindOptions, MongoQuery } from '../mongo.js' import { PartInstance } from '../dataModel/PartInstance.js' diff --git a/packages/corelib/src/playout/timings.ts b/packages/corelib/src/playout/timings.ts index a8ae6674e56..8737aedfdad 100644 --- a/packages/corelib/src/playout/timings.ts +++ b/packages/corelib/src/playout/timings.ts @@ -3,7 +3,7 @@ import { DBPartInstance } from '../dataModel/PartInstance.js' import { DBPart } from '../dataModel/Part.js' import { PieceInstance, PieceInstancePiece } from '../dataModel/PieceInstance.js' import { Piece } from '../dataModel/Piece.js' -import { RundownHoldState } from '../dataModel/RundownPlaylist.js' +import { RundownHoldState } from '../dataModel/RundownPlaylist/RundownPlaylist.js' import { ReadonlyDeep } from 'type-fest' /** diff --git a/packages/corelib/src/protectedString.ts b/packages/corelib/src/protectedString.ts index 48914f5898f..a19b2b4a205 100644 --- a/packages/corelib/src/protectedString.ts +++ b/packages/corelib/src/protectedString.ts @@ -1,6 +1,6 @@ export { - ProtectedString, - ProtectedStringProperties, + type ProtectedString, + type ProtectedStringProperties, protectString, protectStringArray, protectStringObject, @@ -10,6 +10,6 @@ export { unprotectObjectArray, unDeepString, isProtectedString, - ProtectId, - UnprotectedStringProperties, + type ProtectId, + type UnprotectedStringProperties, } from '@sofie-automation/shared-lib/dist/lib/protectedString' diff --git a/packages/corelib/src/pubsub.ts b/packages/corelib/src/pubsub.ts index 6cb7a1dacde..78afff237a0 100644 --- a/packages/corelib/src/pubsub.ts +++ b/packages/corelib/src/pubsub.ts @@ -7,7 +7,7 @@ import { RundownBaselineAdLibAction } from './dataModel/RundownBaselineAdLibActi import { RundownBaselineAdLibItem } from './dataModel/RundownBaselineAdLibPiece.js' import { DBPartInstance } from './dataModel/PartInstance.js' import { DBRundown } from './dataModel/Rundown.js' -import { DBRundownPlaylist } from './dataModel/RundownPlaylist.js' +import { DBRundownPlaylist } from './dataModel/RundownPlaylist/RundownPlaylist.js' import { DBSegment } from './dataModel/Segment.js' import { DBShowStyleBase } from './dataModel/ShowStyleBase.js' import { DBShowStyleVariant } from './dataModel/ShowStyleVariant.js' diff --git a/packages/corelib/src/snapshots.ts b/packages/corelib/src/snapshots.ts index 031addbb6b4..e9147b97498 100644 --- a/packages/corelib/src/snapshots.ts +++ b/packages/corelib/src/snapshots.ts @@ -12,7 +12,7 @@ import { DBRundown } from './dataModel/Rundown.js' import { RundownBaselineAdLibAction } from './dataModel/RundownBaselineAdLibAction.js' import { RundownBaselineAdLibItem } from './dataModel/RundownBaselineAdLibPiece.js' import { RundownBaselineObj } from './dataModel/RundownBaselineObj.js' -import { DBRundownPlaylist } from './dataModel/RundownPlaylist.js' +import { DBRundownPlaylist } from './dataModel/RundownPlaylist/RundownPlaylist.js' import { DBSegment } from './dataModel/Segment.js' import { SofieIngestDataCacheObj } from './dataModel/SofieIngestDataCache.js' import { TimelineComplete } from './dataModel/Timeline.js' diff --git a/packages/corelib/src/timecode.ts b/packages/corelib/src/timecode.ts new file mode 100644 index 00000000000..d4fd500638a --- /dev/null +++ b/packages/corelib/src/timecode.ts @@ -0,0 +1,257 @@ +const zeroPad = function (number: number) { + const pad = number < 10 ? '0' : '' + return pad + Math.floor(number) +} + +export interface TimecodeInitArgs { + framerate?: string + timecode?: string | number | Date + drop_frame?: boolean +} + +/** + * This is a JavaScript module for manipulating SMPTE timecodes. + * Based upon timecode from npm, but ported to typescript to resolve build issues due to older syntax + * + * https://github.com/reidransom/timecode.js + * The MIT License (MIT) Copyright (c) 2016 Reid Ransom + */ +export class Timecode { + #framerate: string + #int_framerate: number + #drop_frame: boolean + #hours: number + #minutes: number + #seconds: number + #frames: number + #frame_count: number + + get hours(): number { + return this.#hours + } + get minutes(): number { + return this.#minutes + } + get seconds(): number { + return this.#seconds + } + get frames(): number { + return this.#frames + } + get frame_count(): number { + return this.#frame_count + } + + private constructor(args?: TimecodeInitArgs) { + this.#framerate = args?.framerate ?? '29.97' + this.#int_framerate = this.#getIntFramerate() + this.#drop_frame = !!args?.drop_frame + this.#hours = 0 + this.#minutes = 0 + this.#seconds = 0 + this.#frames = 0 + this.#frame_count = 0 + this.set(args?.timecode ?? 0) + } + + static init(args?: TimecodeInitArgs): Timecode { + // Future: drop this static init + return new Timecode(args) + } + + set(timecode: string | number | Date): void { + if (typeof timecode === 'string') { + this.#partsFromString(timecode) + this.#timecodeToFrameNumber() + this.#frameNumberToTimecode() + } else if (typeof timecode === 'number') { + this.#frame_count = timecode + this.#frameNumberToTimecode() + } else if (timecode instanceof Date) { + this.#frame_count = this.#dateToFrameNumber(timecode) + this.#frameNumberToTimecode() + } else { + // throw an error + } + } + + add(...offsets: (string | number | Date | Timecode)[]): void { + /* + // This takes one or more Timecode objects as arguments + // If this has been initialized, add to this, otherwise just add timecodes given. + var timecodes = []; + if (this.frame_count) { + timecodes.push(this); + } + */ + this.#calculate('+', offsets) + } + subtract(...offsets: (string | number | Date | Timecode)[]): void { + this.#calculate('-', offsets) + } + + toString(): string { + const delim = this.#drop_frame ? ';' : ':' + return ( + zeroPad(this.hours) + + ':' + + zeroPad(this.minutes) + + ':' + + zeroPad(this.seconds) + + delim + + zeroPad(this.frames) + ) + } + + #calculate(sign: string, timecodes: (string | number | Date | Timecode)[]) { + // all timecodes are calculated in place + for (const timecode of timecodes) { + let parsedTimecode: Timecode + + // if a string, number or Date is given, convert it to a timecode + if (typeof timecode === 'string' || typeof timecode === 'number' || timecode instanceof Date) { + parsedTimecode = Timecode.init({ + framerate: this.#framerate, + timecode: timecode, + drop_frame: this.#drop_frame, + }) + } else if (timecode instanceof Timecode) { + parsedTimecode = timecode + } else { + throw new Error('Expected timecode to be a string, number, Date or Timecode object.') + } + + // make sure this is a valid timecode + if (parsedTimecode.frame_count) { + if (parsedTimecode.#framerate != this.#framerate) { + throw new Error('Timecode framerates must match to do calculations.') + } + let frame_count: number + if (sign === '-') { + frame_count = parsedTimecode.frame_count * -1 + } else if (sign === '+') { + frame_count = parsedTimecode.frame_count + } else { + throw new Error('Expected sign to be + or -.') + } + this.#frame_count = this.frame_count + frame_count + this.#frameNumberToTimecode() + } + } + } + + #getIntFramerate() { + if (this.#framerate === 'ms') { + return 1000 + } else { + return Math.round(Number(this.#framerate)) + } + } + #partsFromString(timecode: string) { + // Parses timecode strings non-drop 'hh:mm:ss:ff', drop 'hh:mm:ss;ff', or milliseconds 'hh:mm:ss:fff' + if (timecode.length === 11) { + this.#frames = Number(timecode.slice(9, 11)) + } else if (timecode.length === 12 && this.#framerate === 'ms') { + this.#frames = Number(timecode.slice(9, 12)) + } else { + throw new Error('Timecode string parsing error. ' + timecode) + } + this.#hours = Number(timecode.slice(0, 2)) + this.#minutes = Number(timecode.slice(3, 5)) + this.#seconds = Number(timecode.slice(6, 8)) + } + #frameNumberToTimecode() { + // Converts frame_count to timecode + let frame_count = this.#frame_count + if (this.#drop_frame) { + const parts = this.#frameNumberToDropFrameTimecode(frame_count) + this.#hours = parts[0] + this.#minutes = parts[1] + this.#seconds = parts[2] + this.#frames = parts[3] + } else { + this.#hours = frame_count / (3600 * this.#int_framerate) + if (this.#hours > 23) { + this.#hours = this.#hours % 24 + frame_count = frame_count - 23 * 3600 * this.#int_framerate + } + this.#minutes = (frame_count % (3600 * this.#int_framerate)) / (60 * this.#int_framerate) + this.#seconds = + ((frame_count % (3600 * this.#int_framerate)) % (60 * this.#int_framerate)) / this.#int_framerate + this.#frames = + ((frame_count % (3600 * this.#int_framerate)) % (60 * this.#int_framerate)) % this.#int_framerate + this.#hours = Math.floor(this.#hours) + this.#minutes = Math.floor(this.#minutes) + this.#seconds = Math.floor(this.#seconds) + this.#frames = Math.floor(this.#frames) + } + } + #timecodeToFrameNumber() { + // converts the current timecode to frame_count. + if (this.#drop_frame) { + this.#frame_count = this.#dropFrameTimecodeToFrameNumber([ + this.#hours, + this.#minutes, + this.#seconds, + this.#frames, + ]) + } else { + this.#frame_count = + (this.#hours * 3600 + this.#minutes * 60 + this.#seconds) * this.#int_framerate + this.#frames + } + } + #frameNumberToDropFrameTimecode(frame_number: number) { + const framerate = Number(this.#framerate) + const drop_frames = Math.round(framerate * 0.066666) + const frames_per_hour = Math.round(framerate * 60 * 60) + const frames_per_24_hours = frames_per_hour * 24 + const frames_per_10_minutes = Math.round(framerate * 60 * 10) + const frames_per_minute = Math.round(framerate * 60) + // Roll over clock if greater than 24 hours + frame_number = frame_number % frames_per_24_hours + // If time is negative, count back from 24 hours + if (frame_number < 0) { + frame_number = frames_per_24_hours + frame_number + } + const d = Math.floor(frame_number / frames_per_10_minutes) + const m = frame_number % frames_per_10_minutes + if (m > drop_frames) { + frame_number = + frame_number + drop_frames * 9 * d + drop_frames * Math.floor((m - drop_frames) / frames_per_minute) + } else { + frame_number = frame_number + drop_frames * 9 * d + } + return [ + Math.floor(Math.floor(Math.floor(frame_number / this.#int_framerate) / 60) / 60), + Math.floor(Math.floor(frame_number / this.#int_framerate) / 60) % 60, + Math.floor(frame_number / this.#int_framerate) % 60, + frame_number % this.#int_framerate, + ] + } + #dropFrameTimecodeToFrameNumber(timecode_as_list: number[]) { + const hours = timecode_as_list[0] + const minutes = timecode_as_list[1] + const seconds = timecode_as_list[2] + const frames = timecode_as_list[3] + const drop_frames = Math.round(Number(this.#framerate) * 0.066666) + const hour_frames = this.#int_framerate * 60 * 60 + const minute_frames = this.#int_framerate * 60 + const total_minutes = hours * 60 + minutes + const frame_number = + hour_frames * hours + + minute_frames * minutes + + this.#int_framerate * seconds + + frames - + drop_frames * (total_minutes - Math.floor(total_minutes / 10)) + return frame_number + } + + /** + * Converts the hour, minute, second, millisecond part of Date() object to the number of + * frames using the current framerate + */ + #dateToFrameNumber(dt: Date): number { + const midnight = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0, 0, 0) + return Math.floor(((dt.getTime() - midnight.getTime()) / 1000) * Number(this.#framerate)) + } +} diff --git a/packages/corelib/src/typings/Timecode.d.ts b/packages/corelib/src/typings/Timecode.d.ts deleted file mode 100644 index f53137bdff6..00000000000 --- a/packages/corelib/src/typings/Timecode.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare module 'timecode' { - export class TimecodeClass { - init(initParams: { framerate: string; timecode: string | number | Date; drop_frame?: boolean }): TimecodeClass - set(timecode: string | number | Date): void - hours: number - minutes: number - seconds: number - frames: number - frame_count: number - add(args: string | number | Date): void - subtract(args: string | number | Date): void - toString(): string - } - export const Timecode: TimecodeClass -} diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 1aac018f184..c6af340fed0 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -1,4 +1,5 @@ import { PlayoutChangedResults } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' +import type { PeripheralDeviceExternalEvent } from '@sofie-automation/shared-lib/dist/peripheralDevice/externalEvents' import { AdLibActionId, BucketAdLibActionId, @@ -13,13 +14,15 @@ import { RundownId, RundownPlaylistId, SegmentId, + SnapshotId, StudioId, } from '../dataModel/Ids.js' import { JSONBlob } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { CoreRundownPlaylistSnapshot } from '../snapshots.js' -import { NoteSeverity } from '@sofie-automation/blueprints-integration' +import { BlueprintSnapshotType, NoteSeverity } from '@sofie-automation/blueprints-integration' import { ITranslatableMessage } from '../TranslatableMessage.js' -import { QuickLoopMarker } from '../dataModel/RundownPlaylist.js' +import { QuickLoopMarker } from '../dataModel/RundownPlaylist/RundownPlaylist.js' +import { RundownTTimerIndex } from '../dataModel/RundownPlaylist/TTimers.js' /** List of all Jobs performed by the Worker related to a certain Studio */ export enum StudioJobs { @@ -125,6 +128,11 @@ export enum StudioJobs { * ( typically when using the "now"-feature ) */ OnTimelineTriggerTime = 'onTimelineTriggerTime', + /** + * Called by a gateway when a playout device emits an external event. + * Events from multiple gateways or rapid bursts are merged into a single job. + */ + OnExternalEvents = 'onExternalEvents', /** * Recalculate T-Timer projections based on current playlist state @@ -173,6 +181,11 @@ export enum StudioJobs { */ RestorePlaylistSnapshot = 'restorePlaylistSnapshot', + /** + * Invoke {@link StudioBlueprintManifest.onSystemSnapshotCreated} for the studio after a system or debug snapshot is stored. + */ + OnSystemSnapshotCreated = 'onSystemSnapshotCreated', + /** * Run the Blueprint applyConfig for the studio */ @@ -217,6 +230,43 @@ export enum StudioJobs { * During playout it is hard to track removal of PieceInstances (particularly when resetting PieceInstances) */ CleanupOrphanedExpectedPackageReferences = 'cleanupOrphanedExpectedPackageReferences', + /** + * Configure a T-timer as a countdown + */ + TTimerStartCountdown = 'tTimerStartCountdown', + + /** + * Configure a T-timer as a free-running timer + */ + TTimerStartFreeRun = 'tTimerStartFreeRun', + /** + * Pause a T-timer + */ + TTimerPause = 'tTimerPause', + /** + * Resume a T-timer + */ + TTimerResume = 'tTimerResume', + /** + * Restart a T-timer + */ + TTimerRestart = 'tTimerRestart', + /** + * Clear the projection state of a T-timer + */ + TTimerClearProjected = 'tTimerClearProjected', + /** + * Set the projection anchor part of a T-timer + */ + TTimerSetProjectedAnchorPart = 'tTimerSetProjectedAnchorPart', + /** + * Set the projection time of a T-timer + */ + TTimerSetProjectedTime = 'tTimerSetProjectedTime', + /** + * Set the projection duration of a T-timer + */ + TTimerSetProjectedDuration = 'tTimerSetProjectedDuration', } export interface RundownPlayoutPropsBase { @@ -284,11 +334,6 @@ export interface ExecuteBucketAdLibOrActionProps extends RundownPlayoutPropsBase externalId: string triggerMode?: string } -export interface ExecuteBucketAdLibOrActionProps extends RundownPlayoutPropsBase { - bucketId: BucketId - externalId: string - triggerMode?: string -} export interface ExecuteActionResult { queuedPartInstanceId?: PartInstanceId taken?: boolean @@ -309,6 +354,9 @@ export interface OnPlayoutPlaybackChangedProps extends RundownPlayoutPropsBase { export interface OnTimelineTriggerTimeProps { results: Array<{ id: string; time: number }> } +export interface OnExternalEventsProps { + events: PeripheralDeviceExternalEvent[] +} export type OrderRestoreToDefaultProps = RundownPlayoutPropsBase export interface OrderMoveRundownToPlaylistProps { @@ -324,10 +372,30 @@ export type DebugRegenerateNextPartInstanceProps = RundownPlayoutPropsBase export type DebugSyncInfinitesForNextPartInstanceProps = RundownPlayoutPropsBase export interface GeneratePlaylistSnapshotProps extends RundownPlayoutPropsBase { - // Include all Instances, or just recent ones + /** Include all part/piece instances, or only recent/non-reset instances. */ full: boolean - // Include the Timeline + /** Include the timeline if the playlist is activated. */ withTimeline: boolean + /** Id of the snapshot (assigned in Meteor before the worker job is queued). Passed to the playlist snapshot blueprint hook. */ + snapshotId?: SnapshotId + /** Human-readable reason for creating the snapshot. Passed to the playlist snapshot blueprint hook. */ + reason?: string +} + +/** Props for {@link StudioJobs.OnSystemSnapshotCreated}. */ +export interface OnSystemSnapshotCreatedProps { + /** Id of the stored snapshot file. */ + snapshotId: SnapshotId + /** Human-readable reason from the snapshot request. */ + reason: string + type: BlueprintSnapshotType + /** Snapshot options; `studioId` is the studio this worker job runs for. */ + options: { + studioId?: StudioId + withDeviceSnapshots?: boolean + /** True when the stored snapshot is a full-system snapshot (not filtered to a single studio). */ + fullSystem?: boolean + } } export interface GeneratePlaylistSnapshotResult { /** @@ -382,6 +450,35 @@ export interface SwitchRouteSetProps { routeSetId: string state: boolean | 'toggle' } +export interface TTimerPropsBase extends RundownPlayoutPropsBase { + timerIndex: RundownTTimerIndex +} +export interface TTimerStartCountdownProps extends TTimerPropsBase { + duration: number + stopAtZero: boolean + startPaused: boolean +} + +export interface TTimerStartFreeRunProps extends TTimerPropsBase { + startPaused: boolean +} +export type TTimerPauseProps = TTimerPropsBase +export type TTimerResumeProps = TTimerPropsBase +export type TTimerRestartProps = TTimerPropsBase + +export type TTimerClearProjectedProps = TTimerPropsBase +export interface TTimerSetProjectedAnchorPartProps extends TTimerPropsBase { + partId?: PartId + externalId?: string +} +export interface TTimerSetProjectedTimeProps extends TTimerPropsBase { + time: number + paused: boolean +} +export interface TTimerSetProjectedDurationProps extends TTimerPropsBase { + duration: number + paused: boolean +} export interface CleanupOrphanedExpectedPackageReferencesProps { playlistId: RundownPlaylistId @@ -423,6 +520,7 @@ export type StudioJobFunc = { [StudioJobs.OnPlayoutPlaybackChanged]: (data: OnPlayoutPlaybackChangedProps) => void [StudioJobs.OnTimelineTriggerTime]: (data: OnTimelineTriggerTimeProps) => void + [StudioJobs.OnExternalEvents]: (data: OnExternalEventsProps) => void [StudioJobs.RecalculateTTimerProjections]: () => void @@ -437,6 +535,7 @@ export type StudioJobFunc = { [StudioJobs.GeneratePlaylistSnapshot]: (data: GeneratePlaylistSnapshotProps) => GeneratePlaylistSnapshotResult [StudioJobs.RestorePlaylistSnapshot]: (data: RestorePlaylistSnapshotProps) => RestorePlaylistSnapshotResult + [StudioJobs.OnSystemSnapshotCreated]: (data: OnSystemSnapshotCreatedProps) => void [StudioJobs.DebugCrash]: (data: DebugRegenerateNextPartInstanceProps) => void [StudioJobs.BlueprintUpgradeForStudio]: () => void @@ -452,6 +551,16 @@ export type StudioJobFunc = { [StudioJobs.SwitchRouteSet]: (data: SwitchRouteSetProps) => void [StudioJobs.CleanupOrphanedExpectedPackageReferences]: (data: CleanupOrphanedExpectedPackageReferencesProps) => void + [StudioJobs.TTimerStartCountdown]: (data: TTimerStartCountdownProps) => void + + [StudioJobs.TTimerStartFreeRun]: (data: TTimerStartFreeRunProps) => void + [StudioJobs.TTimerPause]: (data: TTimerPauseProps) => void + [StudioJobs.TTimerResume]: (data: TTimerResumeProps) => void + [StudioJobs.TTimerRestart]: (data: TTimerRestartProps) => void + [StudioJobs.TTimerClearProjected]: (data: TTimerClearProjectedProps) => void + [StudioJobs.TTimerSetProjectedAnchorPart]: (data: TTimerSetProjectedAnchorPartProps) => void + [StudioJobs.TTimerSetProjectedTime]: (data: TTimerSetProjectedTimeProps) => void + [StudioJobs.TTimerSetProjectedDuration]: (data: TTimerSetProjectedDurationProps) => void } export function getStudioQueueName(id: StudioId): string { diff --git a/packages/corelib/tsconfig.build.json b/packages/corelib/tsconfig.build.json index 44c5873a35b..6973df86b91 100755 --- a/packages/corelib/tsconfig.build.json +++ b/packages/corelib/tsconfig.build.json @@ -3,7 +3,7 @@ "include": ["src/**/*.ts"], "exclude": ["node_modules/**", "**/*spec.ts", "**/__tests__/*", "**/__mocks__/*"], "compilerOptions": { - "target": "es2019", + "target": "es2024", "rootDir": "./src", "outDir": "./dist", "baseUrl": "./", @@ -14,7 +14,8 @@ "resolveJsonModule": true, "types": ["node"], "esModuleInterop": true, - "composite": true + "composite": true, + "module": "node20" }, "references": [ { diff --git a/packages/documentation/docs/for-developers/for-blueprint-developers/error-message-customization.md b/packages/documentation/docs/for-developers/for-blueprint-developers/error-message-customization.md new file mode 100644 index 00000000000..85def06f16e --- /dev/null +++ b/packages/documentation/docs/for-developers/for-blueprint-developers/error-message-customization.md @@ -0,0 +1,214 @@ +--- +sidebar_position: 12 +--- + +# Status Message Customization + +Blueprints can customize the status messages displayed to users in the Sofie UI. This allows you to replace technical status messages with human-friendly ones that are relevant to your broadcast environment. + +## Overview + +Sofie displays error notifications from several sources: + +- **Device statuses** - from TSR devices (ATEM, CasparCG, vMix, etc.) - customized via **Studio Blueprint** +- **Package errors** - from the Package Manager (media files, thumbnails) - customized via **ShowStyle Blueprint** +- **System errors** - from Sofie Core itself - customized via **System Blueprint** + +Each error type is customized in a different blueprint type, matching where the errors originate. + +## Device Status Messages (Studio Blueprint) + +Device status messages come from TSR (Timeline State Resolver) integrations. Customize them in your **Studio Blueprint**: + +```typescript +import { StudioBlueprintManifest } from '@sofie-automation/blueprints-integration' +import { AtemStatusCode, CasparCGStatusCode } from 'timeline-state-resolver-types' + +export const manifest: StudioBlueprintManifest = { + // ... other manifest properties ... + + deviceStatusMessages: { + // Simple string template with placeholders + [AtemStatusCode.DISCONNECTED]: 'Vision mixer offline - check network to {{host}}', + + // Use {{deviceName}} for the configured device name + [CasparCGStatusCode.DISCONNECTED]: '{{deviceName}}: Graphics server offline ({{host}}:{{port}})', + + // Empty string suppresses the error entirely + [AtemStatusCode.SOME_NOISY_WARNING]: '', + }, +} +``` + +### Using Placeholders + +Error messages support `{{placeholder}}` syntax for dynamic values. Common placeholders include: + +- `{{deviceName}}` - The configured name of the device in Sofie +- `{{deviceId}}` - The internal device ID +- Additional context from the specific error (e.g., `{{host}}`, `{{port}}`, `{{channel}}`) + +### Function-Based Messages + +For complex logic, use a function instead of a string. Functions can return: +- A **string** - Use this as the custom message +- **`undefined`** - Fall back to the default TSR message +- **`''`** (empty string) - Suppress the message entirely + +```typescript +import { DeviceStatusContext } from '@sofie-automation/blueprints-integration' + +deviceStatusMessages: { + [CasparCGStatusCode.CHANNEL_ERROR]: (context: DeviceStatusContext) => { + const channel = context.channel as number + if (channel === 1) return 'Primary graphics output failed!' + if (channel === 2) return 'Secondary graphics output failed!' + return `Graphics channel ${channel} error on ${context.deviceName}` + }, + + // Return undefined to use the default TSR message + [SomeErrorCode.COMPLEX_ERROR]: (context) => { + if (context.isExpected) return undefined // Fall back to default + return `Unexpected error on ${context.deviceName}` + }, + + // Return empty string to suppress + [SomeErrorCode.NOISY_WARNING]: (context) => { + if (context.severity === 'low') return '' // Suppress low severity + return `Warning on ${context.deviceName}` + }, +} +``` + +### Available Status Codes + +Import error codes from `timeline-state-resolver-types` for type safety: + +```typescript +import { + AtemStatusCode, + CasparCGStatusCode, + VmixStatusCode, + OBSStatusCode, + // ... other device status codes +} from 'timeline-state-resolver-types' +``` + +Each device type exports its own error codes. Check the TSR documentation or source code for the complete list. + +## Package Status Messages (ShowStyle Blueprint) + +Package Manager messages are customized in your **ShowStyle Blueprint**: + +```typescript +import { ShowStyleBlueprintManifest, PackageStatusMessage } from '@sofie-automation/blueprints-integration' + +export const manifest: ShowStyleBlueprintManifest = { + // ... other manifest properties ... + + packageStatusMessages: { + [PackageStatusMessage.MISSING_FILE_PATH]: 'Media file path not configured - check ingest settings', + [PackageStatusMessage.SCAN_FAILED]: 'Could not scan media file - check file permissions', + [PackageStatusMessage.FILE_NOT_FOUND]: '', // Suppress this message + }, +} +``` + +## System Error Messages (System Blueprint) + +System-level errors from Sofie Core are customized in your **System Blueprint**: + +```typescript +import { SystemBlueprintManifest, SystemErrorCode } from '@sofie-automation/blueprints-integration' + +export const manifest: SystemBlueprintManifest = { + // ... other manifest properties ... + + systemErrorMessages: { + [SystemErrorCode.DATABASE_CONNECTION_LOST]: 'Database offline - contact IT support', + [SystemErrorCode.SERVICE_UNAVAILABLE]: 'Service {{serviceName}} is not responding', + }, +} +``` + +## Message Resolution + +When an error occurs, Sofie resolves the message as follows: + +1. **Check blueprint customization** - Look for matching error code in the appropriate blueprint +2. **If function** - Call it with the error context: + - Returns `undefined` → Use default TSR message + - Returns `''` (empty string) → Suppress the message + - Returns a string → Use that as the message +3. **If string** - Interpolate placeholders with context values +4. **If empty string `''`** - Suppress the message entirely +5. **If not found** - Use the default message from TSR/Sofie + +Device status resolution happens **server-side** when the status is received, ensuring consistent messages across all connected clients. + +```mermaid +flowchart TD + A[Device reports status with code & context] --> B{Blueprint has customization?} + B -->|Yes, function| C[Call function with context] + B -->|Yes, string| D{Empty string?} + B -->|No| E[Use default TSR message] + C --> F{Function returns?} + D -->|Yes| H[Suppress message] + D -->|No| G[Interpolate & display] + F -->|undefined| E + F -->|empty string| H + F -->|string| G + E --> G +``` + +## Complete Example + +Here's a complete Studio Blueprint example: + +```typescript +import { + StudioBlueprintManifest, + DeviceStatusContext, +} from '@sofie-automation/blueprints-integration' +import { AtemStatusCode, CasparCGStatusCode } from 'timeline-state-resolver-types' + +export const deviceStatusMessages: StudioBlueprintManifest['deviceStatusMessages'] = { + // Simple string with placeholders + [AtemStatusCode.DISCONNECTED]: '🎬 Vision mixer offline - check ATEM at {{host}}', + [AtemStatusCode.PSU_FAULT]: '⚠️ ATEM PSU {{psuNumber}} fault - check hardware', + + // Graphics server with host:port + [CasparCGStatusCode.DISCONNECTED]: '{{deviceName}}: Graphics offline ({{host}}:{{port}})', + + // Function for complex logic + [CasparCGStatusCode.CHANNEL_ERROR]: (context: DeviceStatusContext) => { + const channel = context.channel as number + const channelNames: Record = { + 1: 'Program graphics', + 2: 'Preview graphics', + 3: 'DSK graphics', + } + const name = channelNames[channel] || `Channel ${channel}` + return `${name} failed on ${context.deviceName}` + }, +} + +export const manifest: StudioBlueprintManifest = { + blueprintType: 'studio', + // ... other required properties ... + deviceStatusMessages, +} +``` + +## Tips + +- **Keep messages actionable** - Tell users what to do, not just what's wrong +- **Use emoji sparingly** - They can help draw attention to critical errors +- **Test with real devices** - Disconnect devices to verify your messages appear correctly +- **Check TSR source** - Device status codes and their context values are defined in TSR integrations +- **Use functions for complex cases** - Conditional logic, pluralization, severity-based filtering + +## See Also + +- [TSR Device Integrations](https://github.com/nrkno/sofie-timeline-state-resolver) - Device status codes +- [Demo Blueprints](https://github.com/nrkno/sofie-demo-blueprints) - Working examples diff --git a/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md b/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md index 0dfe9486a1b..c2eed2ddd55 100644 --- a/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md +++ b/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md @@ -12,7 +12,7 @@ Documentation for this page is yet to be written. Technically, a Blueprint is a JavaScript object, implementing one of the `BlueprintManifestBase` interfaces. -Sofie doesn't have a built-in package manager or import, so all dependencies need to be bundled into a single `*.js` file bundle using a bundler such as [Rollup](https://rollupjs.org/) or [webpack](https://webpack.js.org/). The community has built a set of utilities called [SuperFlyTV/sofie-blueprint-tools](https://github.com/SuperFlyTV/sofie-blueprint-tools/) that acts as a nascent framework for building & bundling Blueprints written in TypeScript. +Sofie doesn't have a built-in package manager or import, so all dependencies need to be bundled into a single `*.js` file bundle using a bundler such as [Rollup](https://rollupjs.org/) or [webpack](https://webpack.js.org/). The community has built a set of utilities called [SuperFlyTV/sofie-blueprint-tools](https://github.com/SuperFlyTV/sofie-blueprint-tools/) that acts as a nascent framework for building & bundling Blueprints written in TypeScript. :::info Note that the Runtime Environment for Blueprints in Sofie is plain JavaScript at [ES2015 level](https://en.wikipedia.org/wiki/ECMAScript_version_history#6th_edition_%E2%80%93_ECMAScript_2015), so other ways of building Blueprints are also possible. @@ -28,10 +28,14 @@ Currently, there are three types of Blueprints: These blueprints interpret the data coming from the [NRCS](../../user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md), meaning that they need to support the particular data structures that a given Ingest Gateway uses to store incoming data from the Rundown editor. They will need to convert Rundown Pages, Cues, Items, pieces of show script and other types of objects into [Sofie concepts](../../user-guide/concepts-and-architecture.md) such as Segments, Parts, Pieces and AdLibs. +Optional event hooks include playlist snapshot callbacks — see [Snapshot hooks](./snapshot-hooks.md). + # Studio Blueprints These blueprints provide a "baseline" Timeline that is being used by your Studio whenever there isn't a Rundown active. They also handle combining Rundowns into RundownPlaylists. Via the [`applyConfig`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie-automation_blueprints-integration.StudioBlueprintManifest.html#applyconfig) method, these Blueprints enable a _Configuration-as-Code_ approach to configuring connections to various elements of your Control Room and Studio. +Optional event hooks include system snapshot callbacks (with TSR device access) — see [Snapshot hooks](./snapshot-hooks.md). + # System Blueprints -These blueprints exist to allow a _Configuration-as-Code_ approach to an entire Sofie system. This is done via the [`applyConfig`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie-automation_blueprints-integration.SystemBlueprintManifest.html#applyconfig) providing personality information such as global system configuration or system-wide HotKeys via the Blueprints. \ No newline at end of file +These blueprints exist to allow a _Configuration-as-Code_ approach to an entire Sofie system. This is done via the [`applyConfig`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie-automation_blueprints-integration.SystemBlueprintManifest.html#applyconfig) providing personality information such as global system configuration or system-wide HotKeys via the Blueprints. diff --git a/packages/documentation/docs/for-developers/for-blueprint-developers/snapshot-hooks.md b/packages/documentation/docs/for-developers/for-blueprint-developers/snapshot-hooks.md new file mode 100644 index 00000000000..2cf6ec99bef --- /dev/null +++ b/packages/documentation/docs/for-developers/for-blueprint-developers/snapshot-hooks.md @@ -0,0 +1,133 @@ +--- +title: Snapshot hooks +--- + +# Snapshot hooks + +Sofie can store **snapshots** of system configuration and rundown playlist state. Blueprints can run optional callbacks when those snapshots are **created**, which is useful for driving external systems—for example executing [TSR actions](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.IExecuteTSRActionsContext.html) on playout devices when a snapshot is stored. + +What actually triggers each hook depends on the snapshot type (see below). In short: **cron** only stores **playlist** snapshots (when enabled in system settings), not system snapshots. **Debug capture** is a separate user-triggered flow that runs both hooks. + +There are two hooks, on different blueprint types: + +| Snapshot type | Blueprint | Callback | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| System (and debug capture) | [Studio](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.StudioBlueprintManifest.html) | [`onSystemSnapshotCreated`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.StudioBlueprintManifest.html#onsystemsnapshotcreated) | +| Rundown playlist | [Show Style](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.ShowStyleBlueprintManifest.html) | [`onPlaylistSnapshotCreated`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.ShowStyleBlueprintManifest.html#onplaylistsnapshotcreated) | + +Restore operations do **not** invoke these hooks (creation only). + +## Why studio and show style (not system blueprint) + +Playout devices and TSR actions are **studio-scoped**. The studio blueprint hook runs in a studio worker job with access to `listPlayoutDevices()` and `executeTSRAction()`. The show style hook runs when playlist playout data is snapshotted and uses the same TSR APIs. + +The [system blueprint](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.SystemBlueprintManifest.html) has no device context and is not used for these callbacks. + +## `onSystemSnapshotCreated` (studio blueprint) + +Called **after** the system snapshot file has been stored. + +### It runs when: + +Typical triggers: **Settings → Snapshots** (system snapshot), **REST/API** (`POST /snapshots` with type `system`), and **debug capture** (see below). Automatic system snapshots taken before migration use the same path for full-system snapshots. + +- **Studio-scoped system snapshot** (`studioId` set in snapshot options): one invocation for that studio. +- **Full-system snapshot** (no `studioId`, all studios in the file): one invocation **per studio** included in the snapshot. +- **Debug snapshot** ([`storeDebugSnapshot`](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/server/api/snapshot.ts)): one invocation for the target studio (`info.type` is `'debug'`). This is available from the rundown UI / triggered actions (“create snapshot for debug”), not from cron. The embedded system data inside the debug file does not fire additional system hooks. + +If no studio is in scope (empty studio list), the hook is not called. + +### Context + +[`ISystemSnapshotCreatedContext`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.ISystemSnapshotCreatedContext.html) — studio config, mappings, and [`IExecuteTSRActionsContext`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.IExecuteTSRActionsContext.html). + +### Info payload + +[`IBlueprintSystemSnapshotInfo`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.IBlueprintSystemSnapshotInfo.html) — metadata only; the snapshot JSON is **not** passed into the blueprint VM. + +| Field | Description | +| ----------------------------- | ------------------------------------------------------------ | +| `snapshotId` | Id of the stored snapshot | +| `reason` | Human-readable reason from the request (UI, API, cron, etc.) | +| `type` | [`BlueprintSnapshotType`](https://sofie-automation.github.io/sofie-core/typedoc/types/_sofie_automation_blueprints_integration.BlueprintSnapshotType.html) (`system` or `debug`) | +| `options.studioId` | Studio this invocation is for | +| `options.withDeviceSnapshots` | Whether device state was included in the file | +| `options.fullSystem` | `true` if the stored snapshot is a full-system snapshot | + +### Example + +```ts +async onSystemSnapshotCreated(context, info) { + const devices = await context.listPlayoutDevices() + if (devices.length === 0) return + + await context.executeTSRAction(devices[0].deviceId, 'mySnapshotAction', { + snapshotId: info.snapshotId, + reason: info.reason, + fullSystem: info.options.fullSystem ?? false, + }) +} +``` + +## `onPlaylistSnapshotCreated` (show style blueprint) + +Called **after** playlist snapshot data has been generated in the job-worker, **before** Meteor writes the snapshot file to disk. + +### It runs when: + +- User triggers “store snapshot” on a rundown playlist (rundown header, shelf, after-broadcast form, triggered actions, etc.) +- **REST/API** playlist snapshots +- **Cron** — when `coreSystem.settings.cron.storeRundownSnapshots.enabled` is true ([`meteor/server/cronjobs.ts`](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/server/cronjobs.ts)); optional filter by playlist name +- **Debug capture** — for each **active** playlist in the studio, when the user runs debug snapshot capture (same UI/trigger path as above; one hook per active playlist) + +### Show style selection + +Playlists may contain multiple rundowns (and show styles). Only **one** show style blueprint is invoked per snapshot: + +1. Show style of the rundown for the **current** part instance, if set +2. Otherwise show style of the **next** part instance +3. Otherwise show style of the **first** rundown in the playlist (sorted by name) + +If the playlist has no rundowns, the hook is skipped. + +### Context + +[`IPlaylistSnapshotCreatedContext`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.IPlaylistSnapshotCreatedContext.html) — show style and studio config, and `IExecuteTSRActionsContext`. + +### Info payload + +[`IBlueprintPlaylistSnapshotInfo`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.IBlueprintPlaylistSnapshotInfo.html) — metadata only; not the full snapshot blob. + +| Field | Description | +| ---------------------- | --------------------------------------------- | +| `snapshotId` | Id assigned before generation | +| `playlistId` | Rundown playlist id | +| `reason` | Human-readable reason from the request | +| `options.full` | All part/piece instances vs recent only | +| `options.withTimeline` | Timeline included when playlist was activated | +| `playlist.name` | Playlist name at snapshot time | +| `playlist.active` | Whether the playlist had an activation | +| `playlist.rehearsal` | Rehearsal mode flag | + +### Example + +```ts +async onPlaylistSnapshotCreated(context, info) { + const devices = await context.listPlayoutDevices() + + await context.executeTSRAction(devices[0].deviceId, 'playlistSnapshotAction', { + playlistId: info.playlistId, + active: info.playlist.active, + reason: info.reason, + }) +} +``` + +## Error handling + +If a hook throws or rejects, Core **logs the error** and continues. Snapshot generation and storage are not aborted. This matches other playout event hooks such as `onRundownActivate`. + +## Related API + +- Type definitions: [`snapshotContext.ts`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/context/snapshotContext.ts) in `@sofie-automation/blueprints-integration` +- TSR methods: [`IExecuteTSRActionsContext`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.IExecuteTSRActionsContext.html) (also used by `onTake`, `onRundownActivate`, adlib actions, etc.) diff --git a/packages/documentation/docs/for-developers/for-blueprint-developers/splits-box-previews.md b/packages/documentation/docs/for-developers/for-blueprint-developers/splits-box-previews.md new file mode 100644 index 00000000000..5fac1b950fc --- /dev/null +++ b/packages/documentation/docs/for-developers/for-blueprint-developers/splits-box-previews.md @@ -0,0 +1,169 @@ +--- +title: SPLITS box previews +sidebar_position: 11 +--- + +# SPLITS box previews + +Sofie can show **package-manager thumbnails and preview video inside each box** of a SPLITS (multi-box / DVE) piece. + +Blueprints define which sources sit in each box and which media files they use. Core resolves preview URLs at runtime and publishes them on the `uiPieceContentStatuses` publication. The WebUI draws them on the dashboard shelf buttons and in hover popups. + +| Part | Responsibility | +| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Blueprint | [`SplitsContent`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/content.ts) (`boxSourceConfiguration`), optional [`expectedPackages`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/documents/pieceGeneric.ts) (same `MEDIA_FILE` pattern as VT), optional [`popUpPreview`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/previews.ts) | +| Core | `status.boxPreviews[]` — one entry per box, same order as `boxSourceConfiguration` | +| WebUI | Merges `boxPreviews` into split layouts; most inline surfaces stay colour-only | + +For SPLITS pieces, piece-level `thumbnailUrl` / `previewUrl` on content status are always empty. Use `boxPreviews` only. + +## What to put on the piece + +Each SPLITS piece that should show media previews needs layout on `content` and, for VT (or other file-based) boxes, package expectations on the same [`IBlueprintPieceGeneric`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/documents/pieceGeneric.ts). + +### `boxSourceConfiguration` + +[`SplitsContent`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/content.ts) stores boxes in `boxSourceConfiguration`. Index `0` is the rearmost layer. + +| Box | `SourceLayerType` | Blueprint must set | +| --------------- | ----------------------------------- | ----------------------------------------------------------------------- | +| VT clip | `VT` (or `LIVE_SPEAK` on your show) | `fileName`, `studioLabel`, `switcherInput`, `geometry`, usual VT fields | +| Camera | `CAMERA` | `studioLabel`, `switcherInput`, `geometry` | +| Remote | `REMOTE` | Same as camera | +| Graphics / Nora | `GRAPHICS` (per show) | Your existing pattern; include `fileName` if the box uses a file | + +Any VT (or file-based) box that should show a thumbnail **must** have `fileName` on that box entry. Without it, Core cannot match media and the WebUI shows only a layer-colour block. + +**Geometry:** `geometry.x`, `geometry.y`, and `geometry.scale` are fractions of the layout (0–1). Optional `geometry.crop` (`left`, `top`, `right`, `bottom`, also 0–1) is applied in the WebUI with CSS `clip-path` — for example a 9:16 portrait window inside a square cell. + +Your ingest may still use a nested `{ geometry, source }[]` shape internally. Sofie does **not** store that on the piece; map it into `SplitsContent` when building pieces. + +### `expectedPackages` + +`expectedPackages` on pieces is **not new**. The `MEDIA_FILE` shape is the same one you already use on standalone VT pieces. Some shows may already attach packages to SPLITS pieces for Package Manager / playout. + +What **is** new is how Core uses them for the UI: + +| Before split box previews | After | +| ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| SPLITS + `expectedPackages` → one piece-level `thumbnailUrl` / `previewUrl` (first package only) | Per-box URLs in `status.boxPreviews[]`, matched to each VT box `fileName` | +| SPLITS without `expectedPackages` → content status did not read VT media from `boxSourceConfiguration` | Core resolves previews from `fileName` via MediaObjects (and packages when present) | + +#### Rules + +- Add one [`ExpectedPackage.PackageType.MEDIA_FILE`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/shared-lib/src/package-manager/package.ts) per **distinct** file in the layout. +- `content.filePath` must match the box `fileName` (same string as playout and Package Manager). +- Reuse your existing VT package helpers (`sources`, `version`, accessors, containers). +- Do **not** put `thumbnailUrl` or `previewUrl` on content or packages. +- Two boxes sharing one file → one package entry; both boxes still need that `fileName`. + +If VT boxes have no `expectedPackages`, Core can still fill previews from **MediaObjects** only. Prefer `expectedPackages` whenever your VT pieces already use them — routing, status, and Package Manager thumbnails stay consistent. + +### Source layer + +- `sourceLayerId` must point to a layer with `type: SPLITS` in the show-style blueprint. +- Timeline / playout for splits is unchanged; only content status and preview UI are affected. + +## Optional `popUpPreview` + +You may set `content.popUpPreview` with [`PreviewType.Split`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/previews.ts) (`boxes`, optional `background` asset). + +Preview **URLs still come from** `UIPieceContentStatus`, not from the blueprint preview object. If `popUpPreview` is omitted, SPLITS pieces still get a default hover popup built from `SplitsContent`. + +## Published status: `boxPreviews` + +The `uiPieceContentStatuses` publication includes `status.boxPreviews` on [`PieceContentStatusObj`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PieceContentStatus.ts). + +| Field | Description | +| ------------------------------------------ | -------------------------------------------------------- | +| `boxPreviews` | Array, same length and order as `boxSourceConfiguration` | +| `boxPreviews[i]` | `{}` for camera / remote / other non-file boxes | +| `boxPreviews[i].thumbnailUrl` | Thumbnail for box `i` when media is ready | +| `boxPreviews[i].previewUrl` | Preview video for box `i` (hover scrub) | +| `thumbnailUrl`, `previewUrl` (piece-level) | Always unset for SPLITS | + +## How Core resolves preview URLs + +Implementation: [`checkPieceContentStatus.ts`](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts). Helpers: [`splitBoxMedia.ts`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/shared-lib/src/package-manager/splitBoxMedia.ts). + +### Via expected packages + +Runs when `piece.expectedPackages` is set (same entry point as other layers). + +1. For each `MEDIA_FILE` package, resolve thumbnail/preview URLs from Package Manager side effects on routed containers. +2. Key URLs in memory by normalized `filePath`. +3. Build `boxPreviews[]` with [`buildPublishedBoxPreviews`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/shared-lib/src/package-manager/splitBoxMedia.ts) (zip to box order). +4. For any VT box still missing URLs, fill from MediaObjects by `fileName`. + +`contentDuration` on status comes from the package scan (same as VT pieces). + +### Via MediaObjects only + +Runs when the piece has **no** `expectedPackages` but the layer is SPLITS. + +1. Collect distinct media ids from VT / LIVE_SPEAK / GRAPHICS / TRANSITION boxes with `fileName`. +2. Load each MediaObject and build `boxPreviews[]`. +3. Set `contentDuration` from the longest stream duration found in mediainfo. + +Studio setting `mockPieceContentStatus` returns fake per-box URLs when `boxSourceConfiguration` is present (dev only). + +## Where previews appear in the WebUI + +| Surface | Component | Notes | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| Dashboard / bucket / ad-lib buttons | `DashboardPieceButtonSplitPreview` (via `MediaBox`) | Inline box thumbnails when the layout enables thumbnails (buttons, or list with thumbnails enabled) | +| Hover popup | [`BoxLayoutPreview`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/webui/src/client/ui/PreviewPopUp/Previews/BoxLayoutPreview.tsx) | Shows box thumbnails and preview video; supports scrub when `previewUrl` exists | + +These surfaces are **colour-only inline** (no visible box thumbnails), but will still show box media in the hover popup where supported: + +- Storyboard thumbnails and secondary pieces +- Segment timeline SPLITS strip +- OPL main-piece line +- Clock view / camera screen indicators + +Shared helpers: [`getSplitPreview`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/webui/src/client/lib/ui/splitPreview.ts), [`RenderSplitPreview`](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/webui/src/client/lib/SplitPreviewBox.tsx). + +## Example piece + +```typescript +import { SourceLayerType, ExpectedPackage } from '@sofie-automation/blueprints-integration' + +const piece = { + sourceLayerId: '…', // SPLITS layer + content: { + boxSourceConfiguration: [ + { + type: SourceLayerType.CAMERA, + studioLabel: 'Cam 1', + switcherInput: 1, + geometry: { x: 0.1, y: 0.2, scale: 0.4 }, + }, + { + type: SourceLayerType.VT, + studioLabel: 'Snow clip', + switcherInput: '', + fileName: 'clips/head3_Snow.mp4', + geometry: { + x: 0.35, + y: 0.7, + scale: 0.3, + crop: { left: 7 / 32, top: 0, right: 7 / 32, bottom: 0 }, + }, + }, + ], + }, + expectedPackages: [ + { + _id: 'split_vt_clips_head3_snow', + type: ExpectedPackage.PackageType.MEDIA_FILE, + content: { filePath: 'clips/head3_Snow.mp4' }, + version: { + /* same as standalone VT pieces */ + }, + sources: [ + /* same as standalone VT pieces */ + ], + }, + ], +} +``` diff --git a/packages/documentation/docs/for-developers/url-query-parameters.md b/packages/documentation/docs/for-developers/url-query-parameters.md index 3cc86e15a65..b30474946d8 100644 --- a/packages/documentation/docs/for-developers/url-query-parameters.md +++ b/packages/documentation/docs/for-developers/url-query-parameters.md @@ -21,5 +21,6 @@ Appending query parameter(s) to the URL will allow you to modify the behaviour o | `show_hidden_source_layers=1` | _Default value is `0`._ | | `speak=1` | Experimental feature that starts playing an audible countdown 10 seconds before each planned _Take_. _Default value is `0`._ | | `vibrate=1` | Experimental feature that triggers the vibration API in the web browser 3 seconds before each planned _Take_. _Default value is `0`._ | -| `zoom=1,...` | Sets the scaling of the entire GUI. _The unit is a percentage where `100` is the default scaling._ | +| `zoom=100,...` | Sets the scaling of the entire GUI. _The unit is a percentage where `100` is the default scaling._ **Passing any `zoom` parameter will cause it to be stored in the browser's local storage as `uiZoomLevel` and will then be used for future sessions without notifying the user that they are using the Sofie GUI at a non-standard size!** | | `hideRundownHeader=1` | Hides header on [Rundown view](../user-guide/features/sofie-views-and-screens#rundown-view) and [Active Rundown screen](../user-guide/features/sofie-views-and-screens#active-rundown-screen). _Default value is `0`._ | +| `lockView=1` | Locks the [Active Rundown screen](../user-guide/features/sofie-views-and-screens#active-rundown-screen) for unattended or kiosk use: hides exit controls (header close button and context menu “Close Rundown”), disables the in-app navigation confirmation when a rundown is active, and shows the [Screensaver](../user-guide/features/sofie-views-and-screens#screensaver) when no rundown is active instead of a message with a link back to the lobby. Only applies on `/activeRundown/:studioId` routes (ignored on `/rundown/:playlistId`). _Default value is `0`._ | diff --git a/packages/documentation/docs/user-guide/features/sofie-views-and-screens.mdx b/packages/documentation/docs/user-guide/features/sofie-views-and-screens.mdx index 328d377d0b9..c5efcb82d3a 100644 --- a/packages/documentation/docs/user-guide/features/sofie-views-and-screens.mdx +++ b/packages/documentation/docs/user-guide/features/sofie-views-and-screens.mdx @@ -347,6 +347,20 @@ Example: [http://127.0.0.1/countdowns/studio0/camera?sourceLayerIds=camera0,dve0 A page which automatically displays the currently active rundown. Can be useful for the producer to have on a secondary screen. +When no rundown is active, a message is shown with a link back to the rundown list. + +This screen can be configured using query parameters: + +| Query parameter | Type | Description | Default | +| :-------------- | :---- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------ | +| `lockView` | 0 / 1 | Locks the view for unattended or kiosk displays. Hides the header close button and the context menu “Close Rundown” action, and disables the in-app navigation confirmation when a rundown is active. When no rundown is active, shows the [Screensaver](#screensaver) instead of the default idle message. Does not block browser back, manual URL changes, or closing the tab. Only applies on Active Rundown routes (not on a specific `/rundown/:playlistId` URL). | `0` | + +Example (locked Active Rundown for a secondary monitor): + +`http://localhost:3000/activeRundown/studio0?lockView=1` + +Other query parameters for the rundown view itself (such as `hideRundownHeader` and layout selection) can be combined with `lockView`. See [URL Query Parameters](../../for-developers/url-query-parameters.md). + ### Active Rundown Shelf Screen `/activeRundown/:studioId/shelf` @@ -357,6 +371,10 @@ A screen which automatically displays the currently active rundown, and shows th A shelf layout can be selected by modifying the query string, see [Shelf Layouts](#shelf-layouts). +The `lockView` parameter described above also applies to this route. Example: + +`http://localhost:3000/activeRundown/studio0/shelf?lockView=1&layout=Stream` + ### Specific Rundown Shelf Screen `/rundown/:rundownId/shelf` @@ -379,7 +397,7 @@ Each embedded screen shows a label to identify it. This screen is mostly intende ### Screensaver -When big screen displays \(like Prompter Screen and the Presenter Screen\) do not have any meaningful content to show, an animated screensaver showing the current time and the next planned show will be displayed. If no Rundown is upcoming, the Studio name will be displayed. +When big screen displays \(like Prompter Screen, the Presenter Screen, and the Active Rundown Screen with `lockView=1`\) do not have any meaningful content to show, an animated screensaver showing the current time and the next planned show will be displayed. If no Rundown is upcoming, the Studio name will be displayed. ![A screensaver showing the next scheduled show](/img/docs/main/features/next-scheduled-show-example.png) diff --git a/packages/documentation/docs/user-guide/installation/quick-install.md b/packages/documentation/docs/user-guide/installation/quick-install.md index d9fc1331d15..2f84c932a8c 100644 --- a/packages/documentation/docs/user-guide/installation/quick-install.md +++ b/packages/documentation/docs/user-guide/installation/quick-install.md @@ -48,7 +48,7 @@ services: core: hostname: core - image: sofietv/tv-automation-server-core:release52 + image: sofietv/tv-automation-server-core:v26.3.0-1 restart: always ports: - '3000:3000' # Same port as meteor uses by default @@ -69,7 +69,7 @@ services: condition: service_healthy playout-gateway: - image: sofietv/tv-automation-playout-gateway:release52 + image: sofietv/tv-automation-playout-gateway:v26.3.0-1 restart: always environment: DEVICE_ID: playoutGateway0 @@ -96,7 +96,7 @@ services: # - core # mos-gateway: - # image: sofietv/tv-automation-mos-gateway:release52 + # image: sofietv/tv-automation-mos-gateway:v26.3.0-1 # restart: always # ports: # - "10540:10540" # MOS Lower port diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 5bcc19685cd..c77926093c0 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -18,11 +18,11 @@ "node": ">=22.20.0" }, "devDependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/preset-classic": "3.9.2", - "@docusaurus/theme-mermaid": "^3.9.2", - "@docusaurus/types": "3.9.2", + "@docusaurus/core": "3.10.0", + "@docusaurus/module-type-aliases": "3.10.0", + "@docusaurus/preset-classic": "3.10.0", + "@docusaurus/theme-mermaid": "^3.10.0", + "@docusaurus/types": "3.10.0", "@mdx-js/react": "^3.1.1", "@svgr/webpack": "^8.1.0", "clsx": "^2.1.1", diff --git a/packages/documentation/releases/releases.mdx b/packages/documentation/releases/releases.mdx index 487525e51eb..410800c8e3d 100644 --- a/packages/documentation/releases/releases.mdx +++ b/packages/documentation/releases/releases.mdx @@ -4,10 +4,30 @@ hide_table_of_contents: true slug: / --- +import LatestVersionNumber from '../src/components/LatestVersionNumber.jsx' + -import GitHubReleases from '../src/components/GitHubReleases' +Sofie project is working in a quarterly release cycle. The releases are done at the end of every March, June, September, +and beginning of December. The main Sofie components do not follow semantic versioning and instead use a `year.month` +system. For example, the June release of year 2026 will be versioned as `26.06.0`. Fixes for bugs found after a release +is done are published as patch releases, with the patch number increased for every bugfix. + +:::info +

+The latest version is: **** +
+ +Use our **[Quick install](/docs/user-guide/installation/quick-install)** guide to install Sofie using **Docker**. + +Find Sofie release images on [DockerHub](https://hub.docker.com/u/sofietv) and [GitHub Container Registry](https://github.com/orgs/Sofie-Automation/packages). +::: + +When a version is released, the `HEAD` of the Git repositories is branched off into a new branch called `releases/YY.mm` +and a Git tag is created for it. -Current, future, and past releases of _Sofie_ are all tracked on [**our GitHub repository**](https://github.com/Sofie-Automation/Sofie-TV-automation/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3ARelease). +## Historic versioning - +The new versioning system was implemented in March 2026 by the Sofie Technical Steering Committee. All earlier versions follow a different versioning system, +where `1.xx.xx` is used, with the major field fixed at `1`, the minor field indicating a development cycle of +variable length, and the patch field used for bugfix releases, starting at `0`. \ No newline at end of file diff --git a/packages/documentation/src/components/HomepageFeatures.js b/packages/documentation/src/components/HomepageFeatures.jsx similarity index 100% rename from packages/documentation/src/components/HomepageFeatures.js rename to packages/documentation/src/components/HomepageFeatures.jsx diff --git a/packages/documentation/src/components/HomepagePRs.css b/packages/documentation/src/components/HomepagePRs.css new file mode 100644 index 00000000000..5a0ae03a371 --- /dev/null +++ b/packages/documentation/src/components/HomepagePRs.css @@ -0,0 +1,109 @@ +.homepage-pr-gallery { + margin-top: 2em; + margin-bottom: 2em; +} + +.homepage-prs-loading { + display: grid; + align-items: center; + justify-content: center; + min-height: 15em; + text-align: center; + font-size: 1.5em; + color: var(--ifm-color-gray-400); +} + +.homepage-prs-error { + display: grid; + align-items: center; + justify-content: center; + min-height: 15em; + text-align: center; + font-size: 1.5em; + color: var(--ifm-color-danger); +} + +.homepage-prs-title { + text-align: center; + font-size: 4em; + margin-top: 1em; + margin-bottom: 1em; + background-clip: text; + color: transparent; + background-image: linear-gradient(to bottom, var(--ifm-color-primary), var(--ifm-color-primary-darkest)); +} + +.homepage-pr-gallery-row { + margin-bottom: 1em; + overflow-x: hidden; +} + +.homepage-pr-gallery-row-inner { + display: flex; + flex-direction: row; + gap: 1em; + opacity: 1; +} + +.homepage-pr-gallery-pull { + display: flex; + flex-direction: column; + border: var(--ifm-global-border-width) solid var(--ifm-color-emphasis-300); + border-radius: var(--ifm-global-radius); + gap: 0.5em; + flex: 0 0 400px; + padding: 0.5em; +} + +.homepage-pr-gallery-pull-header { + display: flex; + align-items: flex-start; + gap: 0.5em; + max-width: 100%; + min-width: 0; +} + +.homepage-pr-gallery-pull-number { + color: var(--ifm-color-gray-600); +} + +.homepage-pr-gallery-pull-title { + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 40ch; +} + +.homepage-pr-gallery-pull-title a { + color: inherit; + text-decoration: none; +} + +.homepage-pr-gallery-pull-title a:hover { + color: inherit; + text-decoration: underline; +} + +.homepage-pr-gallery-pull-sponsor { + align-self: flex-start; + background-color: var(--label-color, var(--ifm-color-primary)); + border-radius: 1em; + padding: 0 0.5em; + color: contrast-color(var(--label-color, var(--ifm-color-primary))); +} + +.homepage-pr-gallery-pull-author { + display: flex; + flex-direction: row; + gap: 0.25em; +} + +.homepage-pr-gallery-pull-author-avatar { + width: 1.5em; + height: 1.5em; + border-radius: 50%; + background-image: var(--user-avatar); + background-size: cover; + overflow: hidden; +} \ No newline at end of file diff --git a/packages/documentation/src/components/HomepagePRs.jsx b/packages/documentation/src/components/HomepagePRs.jsx new file mode 100644 index 00000000000..ab99dcf09d3 --- /dev/null +++ b/packages/documentation/src/components/HomepagePRs.jsx @@ -0,0 +1,174 @@ +import React, { useEffect, useLayoutEffect, useState, useRef } from "react"; +import './HomepagePRs.css'; + +const GITHUB_API_URL = 'https://api.github.com' + +const GITHUB_ORGANIZATION = "Sofie-Automation" +const GITHUB_REPO = "sofie-core" + +export function HomepagePRs() { + const [isReady, setIsReady] = useState("error") + const [pulls, setPulls] = useState([]) + + useEffect(() => { + let mounted = true + fetch(`${GITHUB_API_URL}/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/pulls?state=all&sort=updated&direction=desc&per_page=100`, { + headers: [['Accept', 'application/vnd.github.text+json']], + }) + .then((value) => { + if (value.ok) { + return value.json() + } else { + throw new Error(value.status) + } + }) + .then((data) => { + if (!mounted) return + setPulls(data.filter((pull) => !pull.draft && (pull.state === 'closed' && pull.merged_at || pull.state === 'open'))) + setIsReady("done") + }) + .catch((error) => { + if (!mounted) return + console.error(error) + setIsReady("error") + }) + + return () => { + mounted = false + } + }, []) + + return ( + +
+

What's the latest in Sofie?

+
+ {isReady === "error" &&
Failed to load recent pull requests from GitHub.
} + {isReady === "loading" &&
Loading recent pull requests...
} + {isReady === "done" &&
+ {chunkArray(pulls, 3).map((chunk, chunkIndex) => ( + + ))} +
} +
+ ) +} + +function AnimatedRow(props) { + const { pulls, speed } = props + const ref = useRef() + const pausedRef = useRef(false) + + const [elementWidth, setElementWidth] = useState(0) + const [containerWidth, setContainerWidth] = useState(0) + + useLayoutEffect(() => { + const element = ref.current + if (!element) return + + console.log(elementWidth, containerWidth) + + let animationFrameId + let lastTimestamp = null + + let scrollAmount = 0 + + function animate(timestamp) { + if (elementWidth <= containerWidth) return // No need to scroll if content fits + if (elementWidth - scrollAmount < containerWidth) { + scrollAmount = 0 // Reset scroll to start when we've scrolled through all content + } + if (lastTimestamp !== null && !pausedRef.current) { + const delta = timestamp - lastTimestamp + scrollAmount += delta * (speed || 0.05) // Adjust the speed here (0.05 is the default speed factor) + element.style.transform = `translateX(-${scrollAmount}px)`; + } + lastTimestamp = timestamp + animationFrameId = requestAnimationFrame(animate) + } + + animationFrameId = requestAnimationFrame(animate) + + return () => { + cancelAnimationFrame(animationFrameId) + } + }, [speed, elementWidth, containerWidth]) + + useLayoutEffect(() => { + const element = ref.current + if (!element) return + + const updateWidth = () => { + setElementWidth(element.scrollWidth) + } + const updateContainerWidth = () => { + setContainerWidth(element.clientWidth) + } + + updateWidth() + updateContainerWidth() + + window.addEventListener('resize', updateWidth) + window.addEventListener('resize', updateContainerWidth) + return () => { + window.removeEventListener('resize', updateWidth) + window.removeEventListener('resize', updateContainerWidth) + } + }, [pulls]) + + function handleMouseEnter() { + pausedRef.current = true + } + + function handleMouseLeave() { + pausedRef.current = false + } + + return ( +
+
+ {pulls.map((pull) => { + return ( +
+
+
#{pull['number']}
+ +
+ label.name.match(/contr(\w+) (from|by)/i))} /> +
+
+
{pull.user.login}
+
+
+ ) + })} +
+
+ ) +} + +function chunkArray(arr, chunkCount) { + const arrLength = arr.length; + const chunkSize = Math.floor(arrLength / chunkCount); + + const result = []; + for (let i = 0; i < chunkCount - 1; i++) { + result.push(arr.slice(i * chunkSize, (i + 1) * chunkSize)) + } + + // last chunk contains the remainer of stuff + result.push(arr.slice((chunkCount - 1) * chunkSize)) + return result +} + +function SponsorBadge(props) { + if (!props.sponsorLabel) return null + + const { sponsorLabel: label } = props + + return ( +
{label.name}
+ ) +} diff --git a/packages/documentation/src/components/LatestVersionNumber.jsx b/packages/documentation/src/components/LatestVersionNumber.jsx new file mode 100644 index 00000000000..b9334705dbc --- /dev/null +++ b/packages/documentation/src/components/LatestVersionNumber.jsx @@ -0,0 +1,20 @@ +import React, { useState, useEffect } from 'react' + +import versions from '../../versions.json'; + +const FALLBACK_VERSION = "26.03.0"; // Fallback version if versions.json cannot be loaded + +export default function LatestVersionNumber() { + try { + // The first item in versions.json is always the latest archived version + const latestVersion = versions[0]; + + if (!latestVersion) { + return {FALLBACK_VERSION}; // Fallback if versions.json is empty + } + + return {latestVersion}; + } catch (error) { + return {FALLBACK_VERSION}; + } +} diff --git a/packages/documentation/src/pages/index.js b/packages/documentation/src/pages/index.js index 7a05e39d5bf..86257eaf9de 100644 --- a/packages/documentation/src/pages/index.js +++ b/packages/documentation/src/pages/index.js @@ -5,6 +5,7 @@ import Link from '@docusaurus/Link' import useDocusaurusContext from '@docusaurus/useDocusaurusContext' import styles from './index.module.css' import HomepageFeatures from '../components/HomepageFeatures' +import { HomepagePRs } from '../components/HomepagePRs' function HomepageHeader() { const { siteConfig } = useDocusaurusContext() @@ -33,6 +34,7 @@ export default function Home() {
+
) diff --git a/packages/eslint.config.mjs b/packages/eslint.config.mjs index 83032f608d9..12b3d347359 100644 --- a/packages/eslint.config.mjs +++ b/packages/eslint.config.mjs @@ -58,6 +58,29 @@ extendedRules.push( }, pluginReact.configs.flat.recommended, pluginReact.configs.flat['jsx-runtime'], + { + files: ['webui/src/**/*', 'shared-lib/src/**/*', 'server-core-integration/src/**/*'], + rules: { + // Override default behaviour for ESM and verbatimModuleSyntax + 'n/no-missing-import': [ + 'error', + { + ignoreTypeImport: true, + resolverConfig: { + // The default aliases drop the js version, breaking the /dist imports + extensionAlias: { + '.js': ['.ts', '.tsx', '.js'], + '.cjs': ['.cts', '.cjs'], + '.mjs': ['.mts', '.mjs'], + }, + }, + }, + ], + 'no-duplicate-imports': 'error', + '@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports' }], + '@typescript-eslint/no-import-type-side-effects': 'error', + }, + }, { files: ['webui/src/**/*'], languageOptions: { diff --git a/packages/job-worker/jest.config.js b/packages/job-worker/jest.config.js index e7da1d6c66d..1dc441882f9 100644 --- a/packages/job-worker/jest.config.js +++ b/packages/job-worker/jest.config.js @@ -17,6 +17,7 @@ module.exports = { 6133, // Declared but not used 6192, // All imports are unused 151002, // hybrid module kind (Node16/18/Next) + 2823, // Import attributes not supported in CJS mode (ts-jest forces CJS, emits require() anyway) ], }, }, @@ -24,6 +25,8 @@ module.exports = { '^.+\\.(js|jsx|mjs)$': path.resolve('./scripts/babel-jest.mjs'), }, moduleNameMapper: { + '^@sofie-automation/shared-lib/dist/(.+)\\.js$': '/../shared-lib/src/$1', + '^@sofie-automation/shared-lib/dist/(.+)$': '/../shared-lib/src/$1', '(.+)\\.js$': '$1', }, transformIgnorePatterns: ['node_modules/(?!(debounce-fn|p-queue|p-timeout)/)', '\\.pnp\\.[^\\/]+$'], diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 5dd185767e6..86500757af4 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -35,7 +35,7 @@ "/LICENSE" ], "dependencies": { - "@slack/webhook": "^7.0.6", + "@slack/webhook": "^7.0.9", "@sofie-automation/blueprints-integration": "26.3.0-2", "@sofie-automation/corelib": "26.3.0-2", "@sofie-automation/shared-lib": "26.3.0-2", @@ -43,20 +43,20 @@ "chrono-node": "^2.9.0", "deepmerge": "^4.3.1", "elastic-apm-node": "^4.15.0", - "mongodb": "^6.21.0", + "mongodb": "^7.1.1", "p-lazy": "^3.1.0", "p-timeout": "^4.1.0", "superfly-timeline": "9.2.0", - "threadedclass": "^1.3.0", + "threadedclass": "^1.4.0", "tslib": "^2.8.1", "type-fest": "^4.41.0", - "underscore": "^1.13.7" + "underscore": "^1.13.8" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "packageManager": "yarn@4.12.0", + "packageManager": "yarn@4.14.1", "devDependencies": { - "jest": "^30.2.0", + "jest": "^30.3.0", "jest-mock-extended": "^4.0.0", - "typescript": "~5.7.3" + "typescript": "~5.9.3" } } diff --git a/packages/job-worker/src/__mocks__/collection.ts b/packages/job-worker/src/__mocks__/collection.ts index 065f9ee0980..d8a0b086ef6 100644 --- a/packages/job-worker/src/__mocks__/collection.ts +++ b/packages/job-worker/src/__mocks__/collection.ts @@ -17,7 +17,7 @@ import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceIns import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineObj' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' @@ -87,13 +87,13 @@ export class MockMongoCollection }> imp this.#ops.length = 0 } - async findFetch(selector?: MongoQuery, options?: FindOptions): Promise { + async findFetch(selector?: MongoQuery, options?: FindOptions): Promise { this.#ops.push({ type: 'findFetch', args: [selector, options] }) return this.findFetchInner(selector, options) } - private async findFetchInner(selector?: MongoQuery, options?: FindOptions): Promise { + private async findFetchInner(selector?: MongoQuery, options?: FindOptions): Promise { if (typeof selector === 'string') selector = { _id: selector } selector = selector ?? {} @@ -153,7 +153,7 @@ export class MockMongoCollection }> imp return clone(matchedDocs) } - async findOne(selector?: MongoQuery | TDoc['_id'], options?: FindOptions): Promise { + async findOne(selector?: MongoQuery | TDoc['_id'], options?: FindOptions): Promise { this.#ops.push({ type: 'findOne', args: [selector, options] }) const docs = await this.findFetchInner(selector, { diff --git a/packages/job-worker/src/__mocks__/context.ts b/packages/job-worker/src/__mocks__/context.ts index a7c40e746bd..a571c58fd86 100644 --- a/packages/job-worker/src/__mocks__/context.ts +++ b/packages/job-worker/src/__mocks__/context.ts @@ -12,12 +12,12 @@ import { IBlueprintSegment, ISegmentUserContext, IShowStyleContext, - IStudioSettings, IngestSegment, PlaylistTimingType, ShowStyleBlueprintManifest, StudioBlueprintManifest, } from '@sofie-automation/blueprints-integration' +import type { IStudioSettings } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { RundownId, RundownPlaylistId, diff --git a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index c8813447219..3cfa402f069 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -1,4 +1,5 @@ import { IBlueprintPieceType, PieceLifespan, PlaylistTimingType } from '@sofie-automation/blueprints-integration' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { PartId, @@ -14,7 +15,7 @@ import { import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { EmptyPieceTimelineObjectsBlob, Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' @@ -113,6 +114,7 @@ export function defaultStudio(_id: StudioId): DBStudio { allowPieceDirectPlay: true, enableBuckets: true, enableEvaluationForm: true, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, }), routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), diff --git a/packages/job-worker/src/__mocks__/helpers/snapshot.ts b/packages/job-worker/src/__mocks__/helpers/snapshot.ts index 95b143b6fc2..0e530f4645e 100644 --- a/packages/job-worker/src/__mocks__/helpers/snapshot.ts +++ b/packages/job-worker/src/__mocks__/helpers/snapshot.ts @@ -3,7 +3,7 @@ import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartIns import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { TimelineObjGeneric, TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { clone } from '@sofie-automation/corelib/dist/lib' diff --git a/packages/job-worker/src/__mocks__/presetCollections.ts b/packages/job-worker/src/__mocks__/presetCollections.ts index 15989d4ca3e..da8a155fab6 100644 --- a/packages/job-worker/src/__mocks__/presetCollections.ts +++ b/packages/job-worker/src/__mocks__/presetCollections.ts @@ -430,6 +430,7 @@ export async function setupMockPeripheralDevice( created: 1234, status: { statusCode: StatusCode.GOOD, + statusDetails: [], }, lastSeen: 1234, lastConnected: 1234, diff --git a/packages/job-worker/src/__tests__/rundownPlaylist.test.ts b/packages/job-worker/src/__tests__/rundownPlaylist.test.ts index 73e39527151..7c3282afbb1 100644 --- a/packages/job-worker/src/__tests__/rundownPlaylist.test.ts +++ b/packages/job-worker/src/__tests__/rundownPlaylist.test.ts @@ -1,7 +1,7 @@ import { PlaylistTimingType } from '@sofie-automation/blueprints-integration' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { protectString, protectStringArray } from '@sofie-automation/corelib/dist/protectedString' import { ProcessedShowStyleCompound } from '../jobs/index.js' import { ReadonlyDeep } from 'type-fest' diff --git a/packages/job-worker/src/blueprints/__tests__/config.test.ts b/packages/job-worker/src/blueprints/__tests__/config.test.ts index c1c6d523f65..62204f07c77 100644 --- a/packages/job-worker/src/blueprints/__tests__/config.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/config.test.ts @@ -5,6 +5,7 @@ import { preprocessStudioConfig, retrieveBlueprintConfigRefs } from '../config.j import { getShowStyleConfigRef, getStudioConfigRef } from '../configRefs.js' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' describe('Test blueprint config', () => { test('compileStudioConfig', () => { @@ -19,6 +20,7 @@ describe('Test blueprint config', () => { allowPieceDirectPlay: true, enableBuckets: true, enableEvaluationForm: true, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, }), blueprintConfigWithOverrides: wrapDefaultObject({ sdfsdf: 'one', another: 5 }), }) @@ -46,6 +48,7 @@ describe('Test blueprint config', () => { allowPieceDirectPlay: true, enableBuckets: true, enableEvaluationForm: true, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, }), blueprintConfigWithOverrides: wrapDefaultObject({ sdfsdf: 'one', another: 5 }), }) diff --git a/packages/job-worker/src/blueprints/__tests__/context-OnSetAsNextContext.test.ts b/packages/job-worker/src/blueprints/__tests__/context-OnSetAsNextContext.test.ts index 7bb1aaf9861..788ad5dfb23 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-OnSetAsNextContext.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-OnSetAsNextContext.test.ts @@ -9,7 +9,7 @@ import { OnSetAsNextContext } from '../context/index.js' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' describe('Test blueprint api context', () => { async function getTestee(setManually = false, rehearsal?: boolean) { @@ -186,7 +186,25 @@ describe('Test blueprint api context', () => { await context.updatePartInstance('next', { title: 'My Part' } as Partial>) expect(mockActionService.updatePartInstance).toHaveBeenCalledTimes(1) - expect(mockActionService.updatePartInstance).toHaveBeenCalledWith('next', { title: 'My Part' }) + expect(mockActionService.updatePartInstance).toHaveBeenCalledWith('next', { title: 'My Part' }, {}) + }) + + test('updatePartInstance with instanceProps', async () => { + const { context, mockActionService } = await getTestee() + + await context.updatePartInstance( + 'next', + { title: 'My Part' } as Partial>, + { invalidReason: { key: 'test' } } + ) + expect(mockActionService.updatePartInstance).toHaveBeenCalledTimes(1) + expect(mockActionService.updatePartInstance).toHaveBeenCalledWith( + 'next', + { title: 'My Part' }, + { + invalidReason: { key: 'test' }, + } + ) }) test('manuallySelected when false', async () => { diff --git a/packages/job-worker/src/blueprints/__tests__/context-OnTakeContext.test.ts b/packages/job-worker/src/blueprints/__tests__/context-OnTakeContext.test.ts index 8ea794c883d..d640e1edb8f 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-OnTakeContext.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-OnTakeContext.test.ts @@ -9,7 +9,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { PlayoutModelImpl } from '../../playout/model/implementation/PlayoutModelImpl.js' -import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' describe('Test blueprint api context', () => { async function getTestee(rehearsal?: boolean) { @@ -201,7 +201,25 @@ describe('Test blueprint api context', () => { await context.updatePartInstance('next', { title: 'My Part' } as Partial>) expect(mockActionService.updatePartInstance).toHaveBeenCalledTimes(1) - expect(mockActionService.updatePartInstance).toHaveBeenCalledWith('next', { title: 'My Part' }) + expect(mockActionService.updatePartInstance).toHaveBeenCalledWith('next', { title: 'My Part' }, {}) + }) + + test('updatePartInstance with instanceProps', async () => { + const { context, mockActionService } = await getTestee() + + await context.updatePartInstance( + 'next', + { title: 'My Part' } as Partial>, + { invalidReason: { key: 'test' } } + ) + expect(mockActionService.updatePartInstance).toHaveBeenCalledTimes(1) + expect(mockActionService.updatePartInstance).toHaveBeenCalledWith( + 'next', + { title: 'My Part' }, + { + invalidReason: { key: 'test' }, + } + ) }) test('isRehearsal when true', async () => { diff --git a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts index b61faf8c176..f43ef2af642 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts @@ -7,7 +7,7 @@ import { JobContext, ProcessedShowStyleCompound } from '../../jobs/index.js' import { mock } from 'jest-mock-extended' import { PartAndPieceInstanceActionService } from '../context/services/PartAndPieceInstanceActionService.js' import { ProcessedShowStyleConfig } from '../config.js' -import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' describe('Test blueprint api context', () => { async function getTestee(rehearsal?: boolean) { @@ -172,7 +172,25 @@ describe('Test blueprint api context', () => { await context.updatePartInstance('next', { title: 'My Part' } as Partial>) expect(mockActionService.updatePartInstance).toHaveBeenCalledTimes(1) - expect(mockActionService.updatePartInstance).toHaveBeenCalledWith('next', { title: 'My Part' }) + expect(mockActionService.updatePartInstance).toHaveBeenCalledWith('next', { title: 'My Part' }, {}) + }) + + test('updatePartInstance with instanceProps', async () => { + const { context, mockActionService } = await getTestee() + + await context.updatePartInstance( + 'next', + { title: 'My Part' } as Partial>, + { invalidReason: { key: 'test' } } + ) + expect(mockActionService.updatePartInstance).toHaveBeenCalledTimes(1) + expect(mockActionService.updatePartInstance).toHaveBeenCalledWith( + 'next', + { title: 'My Part' }, + { + invalidReason: { key: 'test' }, + } + ) }) test('isRehearsal when true', async () => { diff --git a/packages/job-worker/src/blueprints/__tests__/context-events.test.ts b/packages/job-worker/src/blueprints/__tests__/context-events.test.ts index f698171782b..0ce3a1d4b26 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-events.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-events.test.ts @@ -3,7 +3,7 @@ import { PartEventContext, RundownDataChangedEventContext, RundownTimingEventCon import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { getRandomId } from '@sofie-automation/corelib/dist/lib' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context.js' diff --git a/packages/job-worker/src/blueprints/context/GetRundownContext.ts b/packages/job-worker/src/blueprints/context/GetRundownContext.ts index 88a4f32effd..a4713ffaf60 100644 --- a/packages/job-worker/src/blueprints/context/GetRundownContext.ts +++ b/packages/job-worker/src/blueprints/context/GetRundownContext.ts @@ -3,7 +3,7 @@ import { ReadonlyDeep } from 'type-fest' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { getRandomString } from '@sofie-automation/corelib/dist/lib' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { WatchedPackagesHelper } from './watchedPackages.js' import { JobContext, ProcessedShowStyleCompound } from '../../jobs/index.js' import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' diff --git a/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts b/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts index 1d168e84f88..5dbf068a8be 100644 --- a/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts +++ b/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts @@ -3,6 +3,7 @@ import { ContextInfo } from './CommonContext.js' import { ShowStyleUserContext } from './ShowStyleUserContext.js' import { IBlueprintMutatablePart, + IBlueprintMutatablePartInstance, IBlueprintPart, IBlueprintPartInstance, IBlueprintPiece, @@ -29,7 +30,7 @@ import { getOrderedPartsAfterPlayhead } from '../../playout/lookahead/util.js' import { convertPartToBlueprints, emitIngestOperation } from './lib.js' import { TTimersService } from './services/TTimersService.js' import type { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' -import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' export class OnSetAsNextContext extends ShowStyleUserContext @@ -130,9 +131,10 @@ export class OnSetAsNextContext async updatePartInstance( part: 'current' | 'next', - props: Partial> + props: Partial>, + instanceProps: Partial = {} ): Promise> { - return this.partAndPieceInstanceService.updatePartInstance(part, props) + return this.partAndPieceInstanceService.updatePartInstance(part, props, instanceProps) } async removePieceInstances(part: 'current' | 'next', pieceInstanceIds: string[]): Promise { diff --git a/packages/job-worker/src/blueprints/context/OnTakeContext.ts b/packages/job-worker/src/blueprints/context/OnTakeContext.ts index e028b31f1d8..02ad9849cc1 100644 --- a/packages/job-worker/src/blueprints/context/OnTakeContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTakeContext.ts @@ -1,5 +1,6 @@ import { IBlueprintMutatablePart, + IBlueprintMutatablePartInstance, IBlueprintPart, IBlueprintPartInstance, IBlueprintPiece, @@ -33,7 +34,7 @@ import { getOrderedPartsAfterPlayhead } from '../../playout/lookahead/util.js' import { convertPartToBlueprints, emitIngestOperation } from './lib.js' import type { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' import { TTimersService } from './services/TTimersService.js' -import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContext, IEventContext { readonly #tTimersService: TTimersService @@ -131,9 +132,10 @@ export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContex async updatePartInstance( part: 'current' | 'next', - props: Partial + props: Partial, + instanceProps: Partial = {} ): Promise { - return this.partAndPieceInstanceService.updatePartInstance(part, props) + return this.partAndPieceInstanceService.updatePartInstance(part, props, instanceProps) } async stopPiecesOnLayers(sourceLayerIds: string[], timeOffset?: number): Promise { diff --git a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts index c718d6bf516..3ea1f818b07 100644 --- a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts @@ -2,13 +2,17 @@ import { IBlueprintPartInstance, IBlueprintPieceInstance, ITimelineEventContext, + Time, } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { clone } from '@sofie-automation/corelib/dist/lib' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { ABSessionInfo, DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + ABSessionInfo, + DBRundownPlaylist, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { getCurrentTime } from '../../lib/index.js' import { PieceInstance, ResolvedPieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config.js' @@ -30,6 +34,7 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli readonly abSessionsHelper: AbSessionHelper readonly #pieceInstanceCache = new Map>() + readonly #startedPlayback: Time | undefined constructor( studio: ReadonlyDeep, @@ -71,6 +76,12 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli partInstances, clone(playlist.trackedAbSessions ?? []) ) + + this.#startedPlayback = playlist.startedPlayback + } + + override get startedPlayback(): Time | undefined { + return this.#startedPlayback } getCurrentTime(): number { diff --git a/packages/job-worker/src/blueprints/context/PartEventContext.ts b/packages/job-worker/src/blueprints/context/PartEventContext.ts index eb89fac04d4..66e26e26653 100644 --- a/packages/job-worker/src/blueprints/context/PartEventContext.ts +++ b/packages/job-worker/src/blueprints/context/PartEventContext.ts @@ -1,4 +1,4 @@ -import type { IBlueprintPartInstance, IPartEventContext } from '@sofie-automation/blueprints-integration' +import type { IBlueprintPartInstance, IPartEventContext, Time } from '@sofie-automation/blueprints-integration' import type { ReadonlyDeep } from 'type-fest' import type { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import type { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' @@ -9,10 +9,11 @@ import { RundownContext } from './RundownContext.js' import { TTimersService } from './services/TTimersService.js' import type { PlayoutModel } from '../../playout/model/PlayoutModel.js' import type { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' -import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' export class PartEventContext extends RundownContext implements IPartEventContext { readonly #tTimersService: TTimersService + readonly #startedPlayback: Time | undefined readonly part: Readonly @@ -38,6 +39,11 @@ export class PartEventContext extends RundownContext implements IPartEventContex this.#tTimersService = TTimersService.withPlayoutModel(playoutModel, context) this.part = convertPartInstanceToBlueprints(partInstance) + this.#startedPlayback = playoutModel.playlist.startedPlayback + } + + override get startedPlayback(): Time | undefined { + return this.#startedPlayback } getCurrentTime(): number { diff --git a/packages/job-worker/src/blueprints/context/PlaylistSnapshotCreatedContext.ts b/packages/job-worker/src/blueprints/context/PlaylistSnapshotCreatedContext.ts new file mode 100644 index 00000000000..380d571e036 --- /dev/null +++ b/packages/job-worker/src/blueprints/context/PlaylistSnapshotCreatedContext.ts @@ -0,0 +1,44 @@ +import { IBlueprintPlayoutDevice, IPlaylistSnapshotCreatedContext, TSR } from '@sofie-automation/blueprints-integration' +import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids' +import { ReadonlyDeep } from 'type-fest' +import { JobContext, JobStudio, ProcessedShowStyleCompound } from '../../jobs/index.js' +import { executePeripheralDeviceAction, listPlayoutDevicesForStudio } from '../../peripheralDevice.js' +import { ProcessedShowStyleConfig, ProcessedStudioConfig } from '../config.js' +import { ContextInfo } from './CommonContext.js' +import { ShowStyleContext } from './ShowStyleContext.js' + +/** + * Blueprint context for {@link ShowStyleBlueprintManifest.onPlaylistSnapshotCreated}. + * + * Extends {@link ShowStyleContext} with TSR playout device listing and actions scoped to the studio worker job. + */ +export class PlaylistSnapshotCreatedContext extends ShowStyleContext implements IPlaylistSnapshotCreatedContext { + private readonly _context: JobContext + + constructor( + context: JobContext, + contextInfo: ContextInfo, + studio: ReadonlyDeep, + studioBlueprintConfig: ProcessedStudioConfig, + showStyle: ReadonlyDeep, + showStyleBlueprintConfig: ProcessedShowStyleConfig + ) { + super(contextInfo, studio, studioBlueprintConfig, showStyle, showStyleBlueprintConfig) + this._context = context + } + + /** @inheritdoc */ + async listPlayoutDevices(): Promise { + return listPlayoutDevicesForStudio(this._context) + } + + /** @inheritdoc */ + async executeTSRAction( + deviceId: PeripheralDeviceId, + actionId: string, + payload: Record, + timeoutMs?: number + ): Promise { + return executePeripheralDeviceAction(this._context, deviceId, timeoutMs ?? null, actionId, payload) + } +} diff --git a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts index 5335d041bc6..4c40edab57f 100644 --- a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts @@ -4,6 +4,7 @@ import { IRundownActivationContext, IRundownActivationContextState, TSR, + Time, } from '@sofie-automation/blueprints-integration' import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids' import { ReadonlyDeep } from 'type-fest' @@ -15,7 +16,7 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { setTimelineDatastoreValue, removeTimelineDatastoreValue } from '../../playout/datastore.js' import { TTimersService } from './services/TTimersService.js' import type { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' -import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' export class RundownActivationContext extends RundownEventContext implements IRundownActivationContext { private readonly _playoutModel: PlayoutModel @@ -58,6 +59,10 @@ export class RundownActivationContext extends RundownEventContext implements IRu return this._currentState } + get startedPlayback(): Time | undefined { + return this._playoutModel.playlist.startedPlayback + } + async listPlayoutDevices(): Promise { return listPlayoutDevices(this._context, this._playoutModel) } diff --git a/packages/job-worker/src/blueprints/context/RundownContext.ts b/packages/job-worker/src/blueprints/context/RundownContext.ts index 98092fbc2d6..5dc22690c01 100644 --- a/packages/job-worker/src/blueprints/context/RundownContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownContext.ts @@ -1,4 +1,5 @@ import { IRundownContext, IBlueprintSegmentRundown } from '@sofie-automation/blueprints-integration' +import type { Time } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' @@ -31,4 +32,8 @@ export class RundownContext extends ShowStyleContext implements IRundownContext this._rundown = rundown this.playlistId = unprotectString(rundown.playlistId) } + + get startedPlayback(): Time | undefined { + return undefined + } } diff --git a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts index 61e2dcb4863..182c3b3a078 100644 --- a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts +++ b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts @@ -14,9 +14,11 @@ import { IBlueprintPieceInstance, OmitId, IBlueprintMutatablePart, + IBlueprintMutatablePartInstance, IBlueprintPartInstance, SomeContent, WithTimeline, + Time, } from '@sofie-automation/blueprints-integration' import { postProcessPieces, postProcessTimelineObjects } from '../postProcess.js' import { @@ -24,6 +26,7 @@ import { convertPieceInstanceToBlueprints, convertPartInstanceToBlueprints, convertPartialBlueprintMutablePartToCore, + convertPartialBlueprintMutatablePartInstanceToCore, } from './lib.js' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { JobContext, JobStudio, ProcessedShowStyleCompound } from '../../jobs/index.js' @@ -34,11 +37,8 @@ import { import { EXPECTED_INGEST_TO_PLAYOUT_TIME } from '@sofie-automation/shared-lib/dist/core/constants' import { getCurrentTime } from '../../lib/index.js' import { TTimersService } from './services/TTimersService.js' -import type { - DBRundownPlaylist, - RundownTTimer, - RundownTTimerIndex, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { RundownTTimer, RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import type { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' export class SyncIngestUpdateToPartInstanceContext @@ -61,6 +61,10 @@ export class SyncIngestUpdateToPartInstanceContext return Array.from(this.#changedTTimers.values()) } + public get startedPlayback(): Time | undefined { + return this.#playoutModel.playlist.startedPlayback + } + constructor( context: JobContext, playoutModel: PlayoutModel, @@ -201,7 +205,10 @@ export class SyncIngestUpdateToPartInstanceContext return convertPieceInstanceToBlueprints(pieceInstance.pieceInstance) } - updatePartInstance(updatePart: Partial): IBlueprintPartInstance { + updatePartInstance( + updatePart: Partial, + instanceProps: Partial = {} + ): IBlueprintPartInstance { if (!this.#partInstance) throw new Error(`PartInstance has been removed`) // for autoNext, the new expectedDuration cannot be shorter than the time a part has been on-air for @@ -219,8 +226,24 @@ export class SyncIngestUpdateToPartInstanceContext updatePart, this.showStyleCompound.blueprintId ) + const playoutUpdatePartInstance = convertPartialBlueprintMutatablePartInstanceToCore( + instanceProps, + this.showStyleCompound.blueprintId + ) + + const partPropsUpdated = this.#partInstance.updatePartProps(playoutUpdatePart) + let instancePropsUpdated = false + + if (playoutUpdatePartInstance) { + instancePropsUpdated = true + + if (this.playStatus === 'next') { + // Only allow changing the invalidReason for the 'next' PartInstance + this.#partInstance.setInvalidReason(playoutUpdatePartInstance.invalidReason) + } + } - if (!this.#partInstance.updatePartProps(playoutUpdatePart)) { + if (!partPropsUpdated && !instancePropsUpdated) { throw new Error(`Cannot update PartInstance. Some valid properties must be defined`) } diff --git a/packages/job-worker/src/blueprints/context/SystemSnapshotCreatedContext.ts b/packages/job-worker/src/blueprints/context/SystemSnapshotCreatedContext.ts new file mode 100644 index 00000000000..75ba0740870 --- /dev/null +++ b/packages/job-worker/src/blueprints/context/SystemSnapshotCreatedContext.ts @@ -0,0 +1,42 @@ +import { IBlueprintPlayoutDevice, ISystemSnapshotCreatedContext, TSR } from '@sofie-automation/blueprints-integration' +import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids' +import { ReadonlyDeep } from 'type-fest' +import { JobContext, JobStudio } from '../../jobs/index.js' +import { executePeripheralDeviceAction, listPlayoutDevicesForStudio } from '../../peripheralDevice.js' +import { ProcessedStudioConfig } from '../config.js' +import { ContextInfo } from './CommonContext.js' +import { StudioContext } from './StudioContext.js' + +/** + * Blueprint context for {@link StudioBlueprintManifest.onSystemSnapshotCreated}. + * + * Extends {@link StudioContext} with TSR playout device listing and actions scoped to the studio worker job. + */ +export class SystemSnapshotCreatedContext extends StudioContext implements ISystemSnapshotCreatedContext { + private readonly _context: JobContext + + constructor( + context: JobContext, + contextInfo: ContextInfo, + studio: ReadonlyDeep, + studioBlueprintConfig: ProcessedStudioConfig + ) { + super(contextInfo, studio, studioBlueprintConfig) + this._context = context + } + + /** @inheritdoc */ + async listPlayoutDevices(): Promise { + return listPlayoutDevicesForStudio(this._context) + } + + /** @inheritdoc */ + async executeTSRAction( + deviceId: PeripheralDeviceId, + actionId: string, + payload: Record, + timeoutMs?: number + ): Promise { + return executePeripheralDeviceAction(this._context, deviceId, timeoutMs ?? null, actionId, payload) + } +} diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 80b4b312448..b123de098ed 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -2,6 +2,7 @@ import { IActionExecutionContext, IDataStoreActionExecutionContext, IBlueprintMutatablePart, + IBlueprintMutatablePartInstance, IBlueprintPart, IBlueprintPartInstance, IBlueprintPiece, @@ -40,7 +41,7 @@ import { getOrderedPartsAfterPlayhead } from '../../playout/lookahead/util.js' import { convertPartToBlueprints, emitIngestOperation } from './lib.js' import { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' import { TTimersService } from './services/TTimersService.js' -import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' export class DatastoreActionExecutionContext extends ShowStyleUserContext @@ -210,9 +211,10 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct async updatePartInstance( part: 'current' | 'next', - props: Partial + props: Partial, + instanceProps: Partial = {} ): Promise { - return this.partAndPieceInstanceService.updatePartInstance(part, props) + return this.partAndPieceInstanceService.updatePartInstance(part, props, instanceProps) } async stopPiecesOnLayers(sourceLayerIds: string[], timeOffset?: number): Promise { diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 5dff489971c..271343fb8b1 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -1,6 +1,6 @@ import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { DBPart, PartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { deserializePieceTimelineObjectsBlob, @@ -15,6 +15,7 @@ import { import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { CoreUserEditingDefinition, + CoreUserEditingDefinitionState, CoreUserEditingDefinitionAction, CoreUserEditingDefinitionForm, CoreUserEditingProperties, @@ -35,6 +36,7 @@ import { IBlueprintAdLibPieceDB, IBlueprintConfig, IBlueprintMutatablePart, + IBlueprintMutatablePartInstance, IBlueprintPartDB, IBlueprintPartInstance, IBlueprintPiece, @@ -52,11 +54,15 @@ import { IOutputLayer, ISourceLayer, ITranslatableMessage, + NoteSeverity, PieceAbSessionInfo, RundownPlaylistTiming, } from '@sofie-automation/blueprints-integration' import { JobContext, ProcessedShowStyleBase, ProcessedShowStyleVariant } from '../../jobs/index.js' -import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + DBRundownPlaylist, + QuickLoopMarkerType, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import _ from 'underscore' import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage' @@ -67,6 +73,7 @@ import { UserEditingProperties, UserEditingDefinitionSofieDefault, UserEditingType, + UserEditingDefinitionState, } from '@sofie-automation/blueprints-integration/dist/userEditing' import type { PlayoutMutatablePart } from '../../playout/model/PlayoutPartInstanceModel.js' import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration/dist/context/quickLoopInfo' @@ -217,6 +224,7 @@ export function convertPartInstanceToBlueprints(partInstance: ReadonlyDeep): IB isHidden: segment.isHidden, identifier: segment.identifier, displayAs: segment.displayAs, - showShelf: segment.showShelf, + displayMinishelf: segment.displayMinishelf, + // Legacy compatibility field. This should never be set by Core. + showShelf: undefined, segmentTiming: segment.segmentTiming, userEditOperations: translateUserEditsToBlueprint(segment.userEditOperations), userEditProperties: translateUserEditPropertiesToBlueprint(segment.userEditProperties), @@ -446,6 +456,7 @@ export function convertRundownToBlueprintSegmentRundown( ): IBlueprintSegmentRundown { const obj: Complete = { externalId: rundown.externalId, + timing: skipClone ? rundown.timing : clone(rundown.timing), privateData: skipClone ? rundown.privateData : clone(rundown.privateData), publicData: skipClone ? rundown.publicData : clone(rundown.publicData), } @@ -562,6 +573,15 @@ function translateUserEditsToBlueprint( return _.compact( userEdits.map((userEdit) => { switch (userEdit.type) { + case UserEditingType.STATE: + return literal({ + type: UserEditingType.STATE, + id: userEdit.id, + label: omit(userEdit.label, 'namespaces'), + icon: userEdit.icon, + iconInactive: userEdit.iconInactive, + isActive: userEdit.isActive, + }) case UserEditingType.ACTION: return literal({ type: UserEditingType.ACTION, @@ -625,6 +645,15 @@ export function translateUserEditsFromBlueprint( return _.compact( userEdits.map((userEdit) => { switch (userEdit.type) { + case UserEditingType.STATE: + return literal({ + type: UserEditingType.STATE, + id: userEdit.id, + label: wrapTranslatableMessageFromBlueprints(userEdit.label, blueprintIds), + icon: userEdit.icon, + iconInactive: userEdit.iconInactive, + isActive: userEdit.isActive, + }) case UserEditingType.ACTION: return literal({ type: UserEditingType.ACTION, @@ -719,6 +748,39 @@ export function convertPartialBlueprintMutablePartToCore( return playoutUpdatePart } + +export interface PlayoutMutatablePartInstance extends Omit { + invalidReason?: PartInvalidReason +} + +/** + * Converts a partial IBlueprintMutatablePartInstance and wraps translatable messages with blueprint namespace + */ +export function convertPartialBlueprintMutatablePartInstanceToCore( + instanceProps: Partial, + blueprintId: BlueprintId +): Partial { + const result: Partial = { + ...instanceProps, + invalidReason: undefined, + } + + if (instanceProps.invalidReason) { + result.invalidReason = { + message: wrapTranslatableMessageFromBlueprints(instanceProps.invalidReason, [blueprintId]), + severity: NoteSeverity.ERROR, + } + } else if ('invalidReason' in instanceProps) { + // Explicitly clearing invalidReason + result.invalidReason = undefined + } else { + // Not touching invalidReason at all + delete result.invalidReason + } + + return result +} + export function createBlueprintQuickLoopInfo(playlist: ReadonlyDeep): BlueprintQuickLookInfo | null { const playlistLoopProps = playlist.quickLoop if (!playlistLoopProps) return null diff --git a/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts b/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts index 8f72b9f89d6..45b785c4347 100644 --- a/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts +++ b/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts @@ -3,6 +3,7 @@ import { PlayoutModel } from '../../../playout/model/PlayoutModel.js' import { PlayoutPartInstanceModel } from '../../../playout/model/PlayoutPartInstanceModel.js' import { IBlueprintMutatablePart, + IBlueprintMutatablePartInstance, IBlueprintPart, IBlueprintPartInstance, IBlueprintPiece, @@ -20,6 +21,7 @@ import { convertPartInstanceToBlueprints, convertPartToBlueprints, convertPartialBlueprintMutablePartToCore, + convertPartialBlueprintMutatablePartInstanceToCore, convertPieceInstanceToBlueprints, convertPieceToBlueprints, convertResolvedPieceInstanceToBlueprints, @@ -363,7 +365,8 @@ export class PartAndPieceInstanceActionService { async updatePartInstance( part: 'current' | 'next', - props: Partial + props: Partial, + instanceProps: Partial ): Promise { const partInstance = this.#getPartInstance(part) if (!partInstance) { @@ -371,8 +374,23 @@ export class PartAndPieceInstanceActionService { } const playoutUpdatePart = convertPartialBlueprintMutablePartToCore(props, this.showStyleCompound.blueprintId) + const playoutUpdatePartInstance = convertPartialBlueprintMutatablePartInstanceToCore( + instanceProps, + this.showStyleCompound.blueprintId + ) + + const partPropsUpdated = partInstance.updatePartProps(playoutUpdatePart) + let instancePropsUpdated = false + + if (playoutUpdatePartInstance && 'invalidReason' in playoutUpdatePartInstance) { + if (part !== 'next') { + throw new Error(`Can only set invalidReason on the next PartInstance`) + } + partInstance.setInvalidReason(playoutUpdatePartInstance.invalidReason) + instancePropsUpdated = true + } - if (!partInstance.updatePartProps(playoutUpdatePart)) { + if (!partPropsUpdated && !instancePropsUpdated) { throw new Error('Some valid properties must be defined') } diff --git a/packages/job-worker/src/blueprints/context/services/TTimersService.ts b/packages/job-worker/src/blueprints/context/services/TTimersService.ts index ab0a67452da..0353a8f0ada 100644 --- a/packages/job-worker/src/blueprints/context/services/TTimersService.ts +++ b/packages/job-worker/src/blueprints/context/services/TTimersService.ts @@ -1,14 +1,18 @@ import type { IPlaylistTTimer, - IPlaylistTTimerState, + RundownTTimerMode, + TimerState, } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' import type { RundownTTimer, RundownTTimerIndex, - TimerState, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' +import { + timerStateToDuration, + timerStateToZeroTime, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import type { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' +import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import type { PlayoutModel } from '../../../playout/model/PlayoutModel.js' import { ReadonlyDeep } from 'type-fest' @@ -76,41 +80,11 @@ export class PlaylistTTimerImpl implements IPlaylistTTimer { get label(): string { return this.#timer.label } - get state(): IPlaylistTTimerState | null { - const rawMode = this.#timer.mode - const rawState = this.#timer.state - - if (!rawMode || !rawState) return null - - const currentTime = rawState.paused ? rawState.duration : rawState.zeroTime - getCurrentTime() - - switch (rawMode.type) { - case 'countdown': - return { - mode: 'countdown', - currentTime, - duration: rawMode.duration, - paused: rawState.paused, - stopAtZero: rawMode.stopAtZero, - } - case 'freeRun': - return { - mode: 'freeRun', - currentTime, - paused: rawState.paused, - } - case 'timeOfDay': - return { - mode: 'timeOfDay', - currentTime, - targetTime: rawState.paused ? 0 : rawState.zeroTime, - targetRaw: rawMode.targetRaw, - stopAtZero: rawMode.stopAtZero, - } - default: - assertNever(rawMode) - return null - } + get mode(): RundownTTimerMode | null { + return this.#timer.mode + } + get state(): TimerState | null { + return this.#timer.state } constructor( @@ -195,6 +169,77 @@ export class PlaylistTTimerImpl implements IPlaylistTTimer { return true } + setDuration(durationOrOptions: number | { original?: number; current?: number }): void { + // Handle overloaded signatures + if (typeof durationOrOptions === 'number') { + // Simple case: reset timer to this duration + return this.setDuration({ original: durationOrOptions, current: durationOrOptions }) + } + + // Options case: independently update original and/or current + const options = durationOrOptions + + if (options.original !== undefined && options.original <= 0) { + throw new Error('Original duration must be greater than zero') + } + if (options.current !== undefined && options.current <= 0) { + throw new Error('Current duration must be greater than zero') + } + + if (!this.#timer.mode || this.#timer.mode.type !== 'countdown') { + throw new Error('Timer must be in countdown mode to update duration') + } + + if (!this.#timer.state) { + throw new Error('Timer is not initialized') + } + + if (!options.original && !options.current) { + throw new Error('At least one of original or current duration must be provided') + } + + const now = getCurrentTime() + const state = this.#timer.state + + // Calculate current elapsed time using built-in function (handles pauseTime correctly) + const remaining = timerStateToDuration(state, now) + const elapsed = this.#timer.mode.duration - remaining + + let newOriginalDuration: number + let newCurrentRemaining: number + + if (options.original !== undefined && options.current !== undefined) { + // Both specified: use both values independently + newOriginalDuration = options.original + newCurrentRemaining = options.current + } else if (options.original !== undefined) { + // Only original specified: preserve elapsed time + newOriginalDuration = options.original + newCurrentRemaining = Math.max(0, newOriginalDuration - elapsed) + } else if (options.current !== undefined) { + // Only current specified: keep original unchanged + newOriginalDuration = this.#timer.mode.duration + newCurrentRemaining = options.current + } else { + // This should be unreachable due to earlier check + throw new Error('Invalid duration update options') + } + + // Update both mode and state + this.#timer = { + ...this.#timer, + mode: { + ...this.#timer.mode, + duration: newOriginalDuration, + }, + state: state.paused + ? { paused: true, duration: newCurrentRemaining } + : { paused: false, zeroTime: now + newCurrentRemaining }, + } + + this.#emitChange(this.#timer) + } + clearProjected(): void { this.#timer = { ...this.#timer, @@ -248,4 +293,36 @@ export class PlaylistTTimerImpl implements IPlaylistTTimer { } this.#emitChange(this.#timer) } + + getDuration(): number | null { + if (!this.#timer.state) { + return null + } + + return timerStateToDuration(this.#timer.state, getCurrentTime()) + } + + getZeroTime(): number | null { + if (!this.#timer.state) { + return null + } + + return timerStateToZeroTime(this.#timer.state, getCurrentTime()) + } + + getProjectedDuration(): number | null { + if (!this.#timer.projectedState) { + return null + } + + return timerStateToDuration(this.#timer.projectedState, getCurrentTime()) + } + + getProjectedZeroTime(): number | null { + if (!this.#timer.projectedState) { + return null + } + + return timerStateToZeroTime(this.#timer.projectedState, getCurrentTime()) + } } diff --git a/packages/job-worker/src/blueprints/context/services/__tests__/PartAndPieceInstanceActionService.test.ts b/packages/job-worker/src/blueprints/context/services/__tests__/PartAndPieceInstanceActionService.test.ts index 5977eb1449e..1fa68008e73 100644 --- a/packages/job-worker/src/blueprints/context/services/__tests__/PartAndPieceInstanceActionService.test.ts +++ b/packages/job-worker/src/blueprints/context/services/__tests__/PartAndPieceInstanceActionService.test.ts @@ -4,6 +4,7 @@ import { IBlueprintPart, IBlueprintPiece, IBlueprintPieceType, + NoteSeverity, PieceLifespan, } from '@sofie-automation/blueprints-integration' import { PlayoutModel } from '../../../../playout/model/PlayoutModel.js' @@ -36,7 +37,7 @@ import { PlayoutPartInstanceModelImpl } from '../../../../playout/model/implemen import { writePartInstancesAndPieceInstances } from '../../../../playout/model/implementation/SavePlayoutModel.js' import { PlayoutPieceInstanceModel } from '../../../../playout/model/PlayoutPieceInstanceModel.js' import { DatabasePersistedModel } from '../../../../modelBase.js' -import { SelectedPartInstance } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { SelectedPartInstance } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import * as PlayoutAdlib from '../../../../playout/adlibUtils.js' type TinnerStopPieces = jest.MockedFunction @@ -1839,7 +1840,7 @@ describe('Test blueprint api context', () => { await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { service } = await getTestee(jobContext, playoutModel) - await expect(service.updatePartInstance('current', { title: 'new' })).rejects.toThrow( + await expect(service.updatePartInstance('current', { title: 'new' }, {})).rejects.toThrow( 'PartInstance could not be found' ) }) @@ -1848,17 +1849,17 @@ describe('Test blueprint api context', () => { await setPartInstances(jobContext, playlistId, partInstance, undefined) await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { const { service } = await getTestee(jobContext, playoutModel) - await expect(service.updatePartInstance('current', {})).rejects.toThrow( + await expect(service.updatePartInstance('current', {}, {})).rejects.toThrow( 'Some valid properties must be defined' ) await expect( - service.updatePartInstance('current', { _id: 'bad', nope: 'ok' } as any) + service.updatePartInstance('current', { _id: 'bad', nope: 'ok' } as any, {}) ).rejects.toThrow('Some valid properties must be defined') - await expect(service.updatePartInstance('next', { title: 'new' })).rejects.toThrow( + await expect(service.updatePartInstance('next', { title: 'new' }, {})).rejects.toThrow( 'PartInstance could not be found' ) - await service.updatePartInstance('current', { title: 'new' }) + await service.updatePartInstance('current', { title: 'new' }, {}) }) }) test('good', async () => { @@ -1886,7 +1887,7 @@ describe('Test blueprint api context', () => { classes: ['123'], badProperty: 9, // This will be dropped } - const resultPart = await service.updatePartInstance('next', partInstance0Delta) + const resultPart = await service.updatePartInstance('next', partInstance0Delta, {}) const partInstance1 = playoutModel.nextPartInstance! as PlayoutPartInstanceModelImpl expect(partInstance1).toBeTruthy() @@ -1907,6 +1908,54 @@ describe('Test blueprint api context', () => { expect(service.currentPartState).toEqual(ActionPartChange.NONE) }) }) + test('invalidReason on current - throws error', async () => { + const { jobContext, playlistId, rundownId } = await setupMyDefaultRundown() + + const partInstance = (await jobContext.mockCollections.PartInstances.findOne({ + rundownId, + })) as DBPartInstance + expect(partInstance).toBeTruthy() + + // Set a current part instance + await setPartInstances(jobContext, playlistId, partInstance, undefined) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { service } = await getTestee(jobContext, playoutModel) + + await expect( + service.updatePartInstance('current', {}, { invalidReason: { key: 'test' } }) + ).rejects.toThrow('Can only set invalidReason on the next PartInstance') + }) + }) + test('invalidReason on next - sets and clears', async () => { + const { jobContext, playlistId, rundownId } = await setupMyDefaultRundown() + + const partInstance = (await jobContext.mockCollections.PartInstances.findOne({ + rundownId, + })) as DBPartInstance + expect(partInstance).toBeTruthy() + + // Set as next part instance + await setPartInstances(jobContext, playlistId, undefined, partInstance) + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { service } = await getTestee(jobContext, playoutModel) + + // Set invalidReason + const invalidReason = { key: 'test_error', args: { foo: 'bar' } } + await service.updatePartInstance('next', {}, { invalidReason }) + const partInstance1 = playoutModel.nextPartInstance! as PlayoutPartInstanceModelImpl + expect(partInstance1.partInstance.invalidReason).toEqual({ + message: { + ...invalidReason, + namespaces: [expect.any(String)], + }, + severity: NoteSeverity.ERROR, + }) + + // Clear invalidReason + await service.updatePartInstance('next', {}, { invalidReason: undefined }) + expect(partInstance1.partInstance.invalidReason).toBeUndefined() + }) + }) }) }) }) diff --git a/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts b/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts index 72236e2d51b..a9b652e5127 100644 --- a/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts +++ b/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts @@ -2,12 +2,17 @@ import { useFakeCurrentTime, useRealCurrentTime } from '../../../../__mocks__/time.js' import { TTimersService, PlaylistTTimerImpl } from '../TTimersService.js' import type { PlayoutModel } from '../../../../playout/model/PlayoutModel.js' -import type { RundownTTimer, RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { + RundownTTimer, + RundownTTimerIndex, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { mock, MockProxy } from 'jest-mock-extended' import type { ReadonlyDeep } from 'type-fest' import type { JobContext } from '../../../../jobs/index.js' +const FAKE_NOW = 1_750_000_000_000 // 2025-06-15 ~18:13 UTC + function createMockJobContext(): MockProxy { return mock() } @@ -36,7 +41,7 @@ function createEmptyTTimers(): [RundownTTimer, RundownTTimer, RundownTTimer] { describe('TTimersService', () => { beforeEach(() => { - useFakeCurrentTime(10000) + useFakeCurrentTime(FAKE_NOW) }) afterEach(() => { @@ -155,7 +160,7 @@ describe('TTimersService', () => { describe('PlaylistTTimerImpl', () => { beforeEach(() => { - useFakeCurrentTime(10000) + useFakeCurrentTime(FAKE_NOW) }) afterEach(() => { @@ -183,137 +188,6 @@ describe('PlaylistTTimerImpl', () => { expect(timer.label).toBe('Custom Label') }) - - it('should return null state when no mode is set', () => { - const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const mockPlayoutModel = createMockPlayoutModel(tTimers) - const mockJobContext = createMockJobContext() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - - expect(timer.state).toBeNull() - }) - - it('should return running freeRun state', () => { - const tTimers = createEmptyTTimers() - tTimers[0].mode = { type: 'freeRun' } - tTimers[0].state = { paused: false, zeroTime: 15000 } - const updateFn = jest.fn() - const mockPlayoutModel = createMockPlayoutModel(tTimers) - const mockJobContext = createMockJobContext() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - - expect(timer.state).toEqual({ - mode: 'freeRun', - currentTime: 5000, // 10000 - 5000 - paused: false, // pauseTime is null = running - }) - }) - - it('should return paused freeRun state', () => { - const tTimers = createEmptyTTimers() - tTimers[0].mode = { type: 'freeRun' } - tTimers[0].state = { paused: true, duration: 3000 } - const updateFn = jest.fn() - const mockPlayoutModel = createMockPlayoutModel(tTimers) - const mockJobContext = createMockJobContext() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - - expect(timer.state).toEqual({ - mode: 'freeRun', - currentTime: 3000, // 8000 - 5000 - paused: true, // pauseTime is set = paused - }) - }) - - it('should return running countdown state', () => { - const tTimers = createEmptyTTimers() - tTimers[0].mode = { - type: 'countdown', - duration: 60000, - stopAtZero: true, - } - tTimers[0].state = { paused: false, zeroTime: 15000 } - const updateFn = jest.fn() - const mockPlayoutModel = createMockPlayoutModel(tTimers) - const mockJobContext = createMockJobContext() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - - expect(timer.state).toEqual({ - mode: 'countdown', - currentTime: 5000, // 10000 - 5000 - duration: 60000, - paused: false, // pauseTime is null = running - stopAtZero: true, - }) - }) - - it('should return paused countdown state', () => { - const tTimers = createEmptyTTimers() - tTimers[0].mode = { - type: 'countdown', - duration: 60000, - stopAtZero: false, - } - tTimers[0].state = { paused: true, duration: 2000 } - const updateFn = jest.fn() - const mockPlayoutModel = createMockPlayoutModel(tTimers) - const mockJobContext = createMockJobContext() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - - expect(timer.state).toEqual({ - mode: 'countdown', - currentTime: 2000, // 7000 - 5000 - duration: 60000, - paused: true, // pauseTime is set = paused - stopAtZero: false, - }) - }) - - it('should return timeOfDay state', () => { - const tTimers = createEmptyTTimers() - tTimers[0].mode = { - type: 'timeOfDay', - targetRaw: '15:30', - stopAtZero: true, - } - tTimers[0].state = { paused: false, zeroTime: 20000 } // 10 seconds in the future - const updateFn = jest.fn() - const mockPlayoutModel = createMockPlayoutModel(tTimers) - const mockJobContext = createMockJobContext() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - - expect(timer.state).toEqual({ - mode: 'timeOfDay', - currentTime: 10000, // targetTime - getCurrentTime() = 20000 - 10000 - targetTime: 20000, - targetRaw: '15:30', - stopAtZero: true, - }) - }) - - it('should return timeOfDay state with numeric targetRaw', () => { - const tTimers = createEmptyTTimers() - const targetTimestamp = 1737331200000 - tTimers[0].mode = { - type: 'timeOfDay', - targetRaw: targetTimestamp, - stopAtZero: false, - } - tTimers[0].state = { paused: false, zeroTime: targetTimestamp } - const updateFn = jest.fn() - const mockPlayoutModel = createMockPlayoutModel(tTimers) - const mockJobContext = createMockJobContext() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - - expect(timer.state).toEqual({ - mode: 'timeOfDay', - currentTime: targetTimestamp - 10000, // targetTime - getCurrentTime() - targetTime: targetTimestamp, - targetRaw: targetTimestamp, - stopAtZero: false, - }) - }) }) describe('setLabel', () => { @@ -374,7 +248,7 @@ describe('PlaylistTTimerImpl', () => { duration: 60000, stopAtZero: true, }, - state: { paused: false, zeroTime: 70000 }, + state: { paused: false, zeroTime: FAKE_NOW + 60_000 }, }) }) @@ -416,7 +290,7 @@ describe('PlaylistTTimerImpl', () => { mode: { type: 'freeRun', }, - state: { paused: false, zeroTime: 10000 }, + state: { paused: false, zeroTime: FAKE_NOW }, }) }) @@ -563,7 +437,7 @@ describe('PlaylistTTimerImpl', () => { it('should pause a running freeRun timer', () => { const tTimers = createEmptyTTimers() tTimers[0].mode = { type: 'freeRun' } - tTimers[0].state = { paused: false, zeroTime: 5000 } + tTimers[0].state = { paused: false, zeroTime: FAKE_NOW - 5_000 } const updateFn = jest.fn() const mockPlayoutModel = createMockPlayoutModel(tTimers) const mockJobContext = createMockJobContext() @@ -585,7 +459,7 @@ describe('PlaylistTTimerImpl', () => { it('should pause a running countdown timer', () => { const tTimers = createEmptyTTimers() tTimers[0].mode = { type: 'countdown', duration: 60000, stopAtZero: true } - tTimers[0].state = { paused: false, zeroTime: 70000 } + tTimers[0].state = { paused: false, zeroTime: FAKE_NOW + 60_000 } const updateFn = jest.fn() const mockPlayoutModel = createMockPlayoutModel(tTimers) const mockJobContext = createMockJobContext() @@ -658,7 +532,7 @@ describe('PlaylistTTimerImpl', () => { mode: { type: 'freeRun', }, - state: { paused: false, zeroTime: 7000 }, // adjusted for pause duration + state: { paused: false, zeroTime: FAKE_NOW - 3_000 }, // adjusted for pause duration }) }) @@ -732,7 +606,7 @@ describe('PlaylistTTimerImpl', () => { duration: 60000, stopAtZero: true, }, - state: { paused: false, zeroTime: 70000 }, // reset to now + duration + state: { paused: false, zeroTime: FAKE_NOW + 60_000 }, // reset to now + duration }) }) @@ -764,7 +638,7 @@ describe('PlaylistTTimerImpl', () => { }) }) - it('should return false for freeRun timer', () => { + it('should restart a freeRun timer', () => { const tTimers = createEmptyTTimers() tTimers[0].mode = { type: 'freeRun' } tTimers[0].state = { paused: false, zeroTime: 5000 } @@ -775,8 +649,13 @@ describe('PlaylistTTimerImpl', () => { const result = timer.restart() - expect(result).toBe(false) - expect(updateFn).not.toHaveBeenCalled() + expect(result).toBe(true) + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { type: 'freeRun' }, + state: { paused: false, zeroTime: FAKE_NOW }, // reset to now + }) }) it('should restart a timeOfDay timer with valid targetRaw', () => { @@ -948,7 +827,7 @@ describe('PlaylistTTimerImpl', () => { const mockJobContext = createMockJobContext() const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - timer.setProjectedTime(50000, true) + timer.setProjectedTime(FAKE_NOW + 50_000, true) expect(updateFn).toHaveBeenCalledWith({ index: 1, @@ -956,7 +835,7 @@ describe('PlaylistTTimerImpl', () => { mode: null, state: null, anchorPartId: undefined, - projectedState: { paused: true, duration: 40000 }, // 50000 - 10000 (current time) + projectedState: { paused: true, duration: 50000 }, // FAKE_NOW + 50_000 - FAKE_NOW (current time) }) }) @@ -984,11 +863,11 @@ describe('PlaylistTTimerImpl', () => { const mockJobContext = createMockJobContext() const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - timer.setProjectedTime(50000) + timer.setProjectedTime(FAKE_NOW + 50_000) expect(updateFn).toHaveBeenCalledWith( expect.objectContaining({ - projectedState: { paused: false, zeroTime: 50000 }, + projectedState: { paused: false, zeroTime: FAKE_NOW + 50_000 }, }) ) }) @@ -1010,7 +889,7 @@ describe('PlaylistTTimerImpl', () => { mode: null, state: null, anchorPartId: undefined, - projectedState: { paused: false, zeroTime: 40000 }, // 10000 (current) + 30000 (duration) + projectedState: { paused: false, zeroTime: FAKE_NOW + 30_000 }, // FAKE_NOW + 30000 (duration) }) }) @@ -1061,9 +940,209 @@ describe('PlaylistTTimerImpl', () => { expect(updateFn).toHaveBeenCalledWith( expect.objectContaining({ - projectedState: { paused: false, zeroTime: 40000 }, + projectedState: { paused: false, zeroTime: FAKE_NOW + 30_000 }, }) ) }) }) + + describe('getDuration', () => { + it('should return null when timer has no state', () => { + const tTimers = createEmptyTTimers() + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getDuration()).toBeNull() + }) + + it('should return remaining time for a running countdown timer', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60_000, stopAtZero: true } + tTimers[0].state = { paused: false, zeroTime: FAKE_NOW + 40_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getDuration()).toBe(40_000) + }) + + it('should return negative time when countdown has overrun', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60_000, stopAtZero: false } + tTimers[0].state = { paused: false, zeroTime: FAKE_NOW - 5_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getDuration()).toBe(-5_000) + }) + + it('should return the stored duration for a paused timer', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60_000, stopAtZero: true } + tTimers[0].state = { paused: true, duration: 30_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getDuration()).toBe(30_000) + }) + + it('should return negative elapsed time for a running freeRun timer', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'freeRun' } + tTimers[0].state = { paused: false, zeroTime: FAKE_NOW - 10_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getDuration()).toBe(-10_000) + }) + }) + + describe('getZeroTime', () => { + it('should return null when timer has no state', () => { + const tTimers = createEmptyTTimers() + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getZeroTime()).toBeNull() + }) + + it('should return the zeroTime directly for a running timer', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60_000, stopAtZero: true } + tTimers[0].state = { paused: false, zeroTime: FAKE_NOW + 40_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getZeroTime()).toBe(FAKE_NOW + 40_000) + }) + + it('should calculate when zero would be if a paused timer resumed now', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60_000, stopAtZero: true } + tTimers[0].state = { paused: true, duration: 30_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getZeroTime()).toBe(FAKE_NOW + 30_000) + }) + }) + + describe('getProjectedDuration', () => { + it('should return null when no projection is set', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60_000, stopAtZero: true } + tTimers[0].state = { paused: false, zeroTime: FAKE_NOW + 40_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getProjectedDuration()).toBeNull() + }) + + it('should return remaining time to anchor for a running projection', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60_000, stopAtZero: true } + tTimers[0].state = { paused: false, zeroTime: FAKE_NOW + 40_000 } + tTimers[0].projectedState = { paused: false, zeroTime: FAKE_NOW + 30_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getProjectedDuration()).toBe(30_000) + }) + + it('should return the stored duration for a paused projection (pushing)', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60_000, stopAtZero: true } + tTimers[0].state = { paused: false, zeroTime: FAKE_NOW + 40_000 } + tTimers[0].projectedState = { paused: true, duration: 25_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getProjectedDuration()).toBe(25_000) + }) + }) + + describe('getProjectedZeroTime', () => { + it('should return null when no projection is set', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60_000, stopAtZero: true } + tTimers[0].state = { paused: false, zeroTime: FAKE_NOW + 40_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getProjectedZeroTime()).toBeNull() + }) + + it('should return the projected zeroTime for a running projection', () => { + const tTimers = createEmptyTTimers() + tTimers[0].projectedState = { paused: false, zeroTime: FAKE_NOW + 30_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getProjectedZeroTime()).toBe(FAKE_NOW + 30_000) + }) + + it('should calculate when zero would be if paused projection resumed now', () => { + const tTimers = createEmptyTTimers() + tTimers[0].projectedState = { paused: true, duration: 25_000 } + const timer = new PlaylistTTimerImpl( + tTimers[0], + jest.fn(), + createMockPlayoutModel(tTimers), + createMockJobContext() + ) + + expect(timer.getProjectedZeroTime()).toBe(FAKE_NOW + 25_000) + }) + }) }) diff --git a/packages/job-worker/src/db/collection.ts b/packages/job-worker/src/db/collection.ts index 54925c82eee..913d8ce9fc2 100644 --- a/packages/job-worker/src/db/collection.ts +++ b/packages/job-worker/src/db/collection.ts @@ -28,7 +28,7 @@ class WrappedCollection }> implements I return this.#collection } - async findFetch(selector: MongoQuery, options?: FindOptions): Promise> { + async findFetch(selector: MongoQuery, options?: FindOptions): Promise> { const span = startSpanManual('WrappedCollection.findFetch') if (span) { span.addLabels({ @@ -41,7 +41,7 @@ class WrappedCollection }> implements I return res as any } - async findOne(selector: MongoQuery | TDoc['_id'], options?: FindOptions): Promise { + async findOne(selector: MongoQuery | TDoc['_id'], options?: FindOptions): Promise { const span = startSpanManual('WrappedCollection.findOne') if (span) { span.addLabels({ diff --git a/packages/job-worker/src/db/collections.ts b/packages/job-worker/src/db/collections.ts index 0d7060e7a09..b531a80a6b4 100644 --- a/packages/job-worker/src/db/collections.ts +++ b/packages/job-worker/src/db/collections.ts @@ -28,7 +28,7 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineObj' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' @@ -53,8 +53,8 @@ export interface IReadOnlyCollection }> readonly rawCollection: MongoCollection - findFetch(selector?: MongoQuery, options?: FindOptions): Promise> - findOne(selector?: MongoQuery | TDoc['_id'], options?: FindOptions): Promise + findFetch(selector?: MongoQuery, options?: FindOptions): Promise> + findOne(selector?: MongoQuery | TDoc['_id'], options?: FindOptions): Promise count(selector?: MongoQuery | TDoc['_id'], options?: CountOptions): Promise /** diff --git a/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts b/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts index 54b97fb0110..272ea655cf2 100644 --- a/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts +++ b/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts @@ -9,7 +9,7 @@ import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/cont import { getCurrentTime } from '../../lib/index.js' import { queueExternalMessages } from '../handle.js' import { setupMockShowStyleCompound } from '../../__mocks__/presetCollections.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { getRandomId, omit } from '@sofie-automation/corelib/dist/lib' import { ExternalMessageQueueRunner } from '../ExternalMessageQueue.js' import { InvalidateWorkerDataCache, WorkerDataCacheWrapper } from '../../workers/caches.js' diff --git a/packages/job-worker/src/events/handle.ts b/packages/job-worker/src/events/handle.ts index 10476bfe61b..a45297f3f53 100644 --- a/packages/job-worker/src/events/handle.ts +++ b/packages/job-worker/src/events/handle.ts @@ -12,7 +12,7 @@ import { getRandomId, omit, removeNullyProperties } from '@sofie-automation/core import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' import { ICollection, MongoModifier } from '../db/index.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ExternalMessageQueueObjId } from '@sofie-automation/corelib/dist/dataModel/Ids' async function getBlueprintAndDependencies(context: JobContext, rundown: ReadonlyDeep) { diff --git a/packages/job-worker/src/ingest/__tests__/ingest.test.ts b/packages/job-worker/src/ingest/__tests__/ingest.test.ts index 1e207ac14fa..1c0abc828a2 100644 --- a/packages/job-worker/src/ingest/__tests__/ingest.test.ts +++ b/packages/job-worker/src/ingest/__tests__/ingest.test.ts @@ -19,7 +19,7 @@ import { RundownOrphanedReason, RundownSource, } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { clone, getRandomString, literal } from '@sofie-automation/corelib/dist/lib' import { sortPartsInSortedSegments, sortSegmentsInRundowns } from '@sofie-automation/corelib/dist/playout/playlist' diff --git a/packages/job-worker/src/ingest/__tests__/showShelfCompat.test.ts b/packages/job-worker/src/ingest/__tests__/showShelfCompat.test.ts new file mode 100644 index 00000000000..c7d96a33628 --- /dev/null +++ b/packages/job-worker/src/ingest/__tests__/showShelfCompat.test.ts @@ -0,0 +1,182 @@ +import '../../__mocks__/_extendJest.js' +import { setupDefaultJobEnvironment } from '../../__mocks__/context.js' +import { setupMockPeripheralDevice, setupMockShowStyleCompound } from '../../__mocks__/presetCollections.js' +import { + BlueprintResultSegment, + IBlueprintSegment, + IngestRundown, + IngestSegment, +} from '@sofie-automation/blueprints-integration' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' +import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { + PeripheralDevice, + PeripheralDeviceCategory, + PeripheralDeviceType, + PERIPHERAL_SUBTYPE_PROCESS, +} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' +import { RundownSource } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { wrapGenericIngestJob } from '../jobWrappers.js' +import { handleUpdatedRundown } from '../ingestRundownJobs.js' +import { logger } from '../../logging.js' + +const handleUpdatedRundownWrapped = wrapGenericIngestJob(handleUpdatedRundown) + +function createRundownSource(peripheralDevice: PeripheralDevice): RundownSource { + return { + type: 'nrcs', + peripheralDeviceId: peripheralDevice._id, + nrcsName: peripheralDevice.nrcsName, + } +} + +describe('Blueprint segment legacy showShelf compatibility', () => { + let context = setupDefaultJobEnvironment() + let device: PeripheralDevice + + beforeAll(async () => { + context = setupDefaultJobEnvironment() + + const showStyleCompound = await setupMockShowStyleCompound(context) + context.setStudio({ + ...(context.rawStudio as DBStudio), + settingsWithOverrides: wrapDefaultObject({ + ...context.studio.settings, + // keep defaults; not relevant here + }), + supportedShowStyleBase: [showStyleCompound._id], + }) + + device = await setupMockPeripheralDevice( + context, + PeripheralDeviceCategory.INGEST, + PeripheralDeviceType.MOS, + PERIPHERAL_SUBTYPE_PROCESS + ) + + jest.clearAllMocks() + }) + + beforeEach(async () => { + await context.clearAllRundownsAndPlaylists() + }) + + async function createRundownWithSingleSegment( + overrideGetSegment: (ingestSegment: IngestSegment) => BlueprintResultSegment + ) { + context.updateShowStyleBlueprint({ + getSegment: (_ctx, ingestSegment) => overrideGetSegment(ingestSegment), + }) + + const ingest: IngestRundown = { + externalId: 'rundown_showShelfCompat', + name: 'Rundown', + type: 'mock', + payload: undefined, + segments: [ + { + externalId: 'segment0', + name: 'Segment 0', + rank: 0, + payload: undefined, + parts: [ + { + externalId: 'part0', + name: 'Part 0', + rank: 0, + payload: undefined, + }, + ], + }, + ], + } + + await handleUpdatedRundownWrapped(context, { + rundownExternalId: ingest.externalId, + ingestRundown: ingest, + isCreateAction: true, + rundownSource: createRundownSource(device), + }) + + const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: ingest.externalId })) as DBRundown + expect(rundown).toBeTruthy() + + const segments = await context.mockCollections.Segments.findFetch({ rundownId: rundown._id }) + expect(segments).toHaveLength(1) + + return { rundown, segment: segments[0] as any } + } + + test('showShelf:true maps to displayMinishelf:inherit and does not persist showShelf', async () => { + const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined) + try { + const { segment } = await createRundownWithSingleSegment((ingestSegment) => { + const seg: IBlueprintSegment = { + name: ingestSegment.name, + showShelf: true, + } + return { + segment: seg, + parts: [], + } + }) + + expect(segment.displayMinishelf).toBe(ShelfButtonSize.INHERIT) + expect(segment.showShelf).toBeUndefined() + + // A warning should be emitted via the blueprint context + expect( + warnSpy.mock.calls.some((args) => + args.some((arg) => String(arg).includes('Deprecated blueprint segment field "showShelf" used')) + ) + ).toBe(true) + } finally { + warnSpy.mockRestore() + } + }) + + test('showShelf:false results in minishelf hidden (no displayMinishelf) and does not persist showShelf', async () => { + const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined) + try { + const { segment } = await createRundownWithSingleSegment((ingestSegment) => { + const seg: IBlueprintSegment = { + name: ingestSegment.name, + showShelf: false, + } + return { + segment: seg, + parts: [], + } + }) + + expect(segment.displayMinishelf).toBeUndefined() + expect(segment.showShelf).toBeUndefined() + expect( + warnSpy.mock.calls.some((args) => + args.some((arg) => String(arg).includes('Deprecated blueprint segment field "showShelf" used')) + ) + ).toBe(true) + } finally { + warnSpy.mockRestore() + } + }) + + test('displayMinishelf wins over legacy showShelf', async () => { + const { segment } = await createRundownWithSingleSegment((ingestSegment) => { + const seg: IBlueprintSegment = { + name: ingestSegment.name, + showShelf: true, + displayMinishelf: ShelfButtonSize.COMPACT, + } + return { + segment: seg, + parts: [], + } + }) + + expect(segment.displayMinishelf).toBe(ShelfButtonSize.COMPACT) + expect(segment.showShelf).toBeUndefined() + }) +}) diff --git a/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts b/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts index 6fd99f48620..ce581611713 100644 --- a/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts +++ b/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts @@ -17,7 +17,10 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { PlayoutModelImpl } from '../../playout/model/implementation/PlayoutModelImpl.js' import { PlaylistTimingType, ShowStyleBlueprintManifest } from '@sofie-automation/blueprints-integration' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBRundownPlaylist, SelectedPartInstance } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + DBRundownPlaylist, + SelectedPartInstance, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { PlayoutRundownModelImpl } from '../../playout/model/implementation/PlayoutRundownModelImpl.js' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' diff --git a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts index cc40fff7157..4e55c5c1dc2 100644 --- a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts +++ b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts @@ -7,6 +7,7 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { saveIntoDb } from '../../db/changes.js' import { ensureNextPartIsValid as ensureNextPartIsValidRaw } from '../updateNext.js' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context.js' +import { setupMockShowStyleCompound } from '../../__mocks__/presetCollections.js' import { runJobWithPlayoutModel } from '../../playout/lock.js' jest.mock('../../playout/setNext') @@ -538,6 +539,47 @@ describe('ensureNextPartIsValid', () => { false ) }) + test('Next part instance is orphaned: "deleted" and `syncIngestUpdateToPartInstance` exists', async () => { + const showStyleCompound = await setupMockShowStyleCompound(context) + await context.mockCollections.Rundowns.update(rundownId, { + $set: { + showStyleBaseId: showStyleCompound._id, + showStyleVariantId: showStyleCompound.showStyleVariantId, + }, + }) + context.updateShowStyleBlueprint({ + syncIngestUpdateToPartInstance: jest.fn(), + }) + + const instanceId: PartInstanceId = protectString('orphaned_first_part_with_callback') + await context.mockCollections.PartInstances.insertOne( + literal({ + _id: instanceId, + rundownId: rundownId, + segmentId: protectString('mock_segment1'), + playlistActivationId: protectString('active'), + segmentPlayoutId: protectString(''), + takeCount: 0, + rehearsal: false, + part: literal({ + _id: protectString('orphan_with_callback_1'), + _rank: 1.5, + rundownId: rundownId, + segmentId: protectString('mock_segment1'), + externalId: 'o1-callback', + title: 'Orphan 1 Callback', + expectedDurationWithTransition: undefined, + }), + orphaned: 'deleted', + }) + ) + + await resetPartIds(null, instanceId, false) + + await expect(ensureNextPartIsValid()).resolves.toBeFalsy() + + expect(setNextPartMock).not.toHaveBeenCalled() + }) test('Next part is invalid, but instance is not', async () => { // Insert a temporary instance const instanceId: PartInstanceId = protectString('orphaned_first_part') diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index 18c981dc4be..245eac2d80a 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -21,7 +21,7 @@ import { import { ReadonlyDeep } from 'type-fest' import { IngestDatabasePersistedModel, IngestModel, IngestModelReadonly } from './model/IngestModel.js' import { JobContext } from '../jobs/index.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { runJobWithPlaylistLock, runWithPlayoutModel } from '../playout/lock.js' import { CommitIngestData } from './lock.js' diff --git a/packages/job-worker/src/ingest/generationRundown.ts b/packages/job-worker/src/ingest/generationRundown.ts index 113f6dfb424..be540a1656f 100644 --- a/packages/job-worker/src/ingest/generationRundown.ts +++ b/packages/job-worker/src/ingest/generationRundown.ts @@ -299,7 +299,8 @@ export async function regenerateRundownAndBaselineFromIngestData( showStyleBlueprint, rundownSource, rundownNotes, - translateUserEditsFromBlueprint(rundownRes.rundown.userEditOperations, translationNamespaces) + translateUserEditsFromBlueprint(rundownRes.rundown.userEditOperations, translationNamespaces), + rundownRes.externalEventSubscriptions ) // get the rundown separetely to ensure it exists now diff --git a/packages/job-worker/src/ingest/generationSegment.ts b/packages/job-worker/src/ingest/generationSegment.ts index f563b6db1d9..6b7cf909e44 100644 --- a/packages/job-worker/src/ingest/generationSegment.ts +++ b/packages/job-worker/src/ingest/generationSegment.ts @@ -3,6 +3,7 @@ import { SegmentNote, PartNote } from '@sofie-automation/corelib/dist/dataModel/ import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { literal } from '@sofie-automation/corelib/dist/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { RawPartNote, SegmentUserContext } from '../blueprints/context/index.js' import { WatchedPackagesHelper } from '../blueprints/context/watchedPackages.js' import { postProcessAdLibActions, postProcessAdLibPieces, postProcessPieces } from '../blueprints/postProcess.js' @@ -235,6 +236,14 @@ async function generateSegmentWithBlueprints( try { const blueprintSegment = await blueprint.blueprint.getSegment(blueprintContext, ingestSegment) + + const legacyShowShelf = (blueprintSegment?.segment as any)?.showShelf + if (legacyShowShelf !== undefined) { + blueprintContext.logWarning( + 'Deprecated blueprint segment field "showShelf" used. Use "displayMinishelf" instead.' + ) + } + return { blueprintSegment, blueprintNotes: blueprintContext.notes, @@ -283,9 +292,28 @@ function updateModelWithGeneratedSegment( const segmentNotes = extractAndWrapSegmentNotes(blueprintId, blueprintNotes, knownPartExternalIds) + const blueprintSegmentSegment = blueprintSegment.segment as any + const legacyShowShelf: boolean | undefined = blueprintSegmentSegment?.showShelf + + // Normalize legacy blueprint output (showShelf) into the new field (displayMinishelf) + const sanitizedSegment = { ...(blueprintSegmentSegment ?? {}) } + delete sanitizedSegment.showShelf + if (sanitizedSegment.displayMinishelf === undefined && legacyShowShelf !== undefined) { + if (legacyShowShelf === true) { + sanitizedSegment.displayMinishelf = ShelfButtonSize.INHERIT + } else { + // showShelf === false means hidden, which is encoded by leaving displayMinishelf unset + } + + logger.warn( + `Deprecated blueprint segment field "showShelf" used during ingest. ` + + `blueprintId=${blueprintId}, segmentExternalId=${ingestSegment.externalId}` + ) + } + const segmentModel = ingestModel.replaceSegment( literal({ - ...blueprintSegment.segment, + ...sanitizedSegment, externalId: ingestSegment.externalId, _rank: ingestSegment.rank, notes: segmentNotes, diff --git a/packages/job-worker/src/ingest/model/IngestModel.ts b/packages/job-worker/src/ingest/model/IngestModel.ts index 946237b8571..95745f4187c 100644 --- a/packages/job-worker/src/ingest/model/IngestModel.ts +++ b/packages/job-worker/src/ingest/model/IngestModel.ts @@ -24,7 +24,7 @@ import { RundownNote } from '@sofie-automation/corelib/dist/dataModel/Notes' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { ProcessedShowStyleBase, ProcessedShowStyleVariant } from '../../jobs/showStyle.js' import { WrappedShowStyleBlueprint } from '../../blueprints/cache.js' -import { IBlueprintRundown } from '@sofie-automation/blueprints-integration' +import type { BlueprintExternalEventSubscription, IBlueprintRundown } from '@sofie-automation/blueprints-integration' import type { INotificationsModel } from '../../notifications/NotificationsModel.js' import type { IngestExpectedPackage } from './IngestExpectedPackage.js' @@ -214,7 +214,8 @@ export interface IngestModel extends IngestModelReadonly, BaseModel, INotificati showStyleBlueprint: ReadonlyDeep, source: RundownSource, rundownNotes: RundownNote[], - userEdits: CoreUserEditingDefinition[] | undefined + userEdits: CoreUserEditingDefinition[] | undefined, + externalEventSubscriptions: BlueprintExternalEventSubscription[] | undefined ): ReadonlyDeep /** diff --git a/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts b/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts index 47b916a9322..0f319d8d248 100644 --- a/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts +++ b/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts @@ -50,7 +50,7 @@ import { RundownNote } from '@sofie-automation/corelib/dist/dataModel/Notes' import { diffAndReturnLatestObjects } from './utils.js' import _ from 'underscore' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { IBlueprintRundown } from '@sofie-automation/blueprints-integration' +import type { BlueprintExternalEventSubscription, IBlueprintRundown } from '@sofie-automation/blueprints-integration' import { getCurrentTime, getSystemVersion } from '../../../lib/index.js' import { WrappedShowStyleBlueprint } from '../../../blueprints/cache.js' import { SaveIngestModelHelper } from './SaveIngestModel.js' @@ -409,7 +409,8 @@ export class IngestModelImpl implements IngestModel, IngestDatabasePersistedMode showStyleBlueprint: ReadonlyDeep, source: RundownSource, rundownNotes: RundownNote[], - userEditOperations: CoreUserEditingDefinition[] | undefined + userEditOperations: CoreUserEditingDefinition[] | undefined, + externalEventSubscriptions: BlueprintExternalEventSubscription[] | undefined ): ReadonlyDeep { const newRundown = literal>({ ...clone(rundownData as Complete), @@ -420,6 +421,7 @@ export class IngestModelImpl implements IngestModel, IngestDatabasePersistedMode showStyleVariantId: showStyleVariant._id, showStyleBaseId: showStyleBase._id, userEditOperations: clone(userEditOperations), + externalEventSubscriptions: clone(externalEventSubscriptions), orphaned: undefined, importVersions: { diff --git a/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts b/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts index c89d0ac8a6e..79490c208fb 100644 --- a/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts +++ b/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts @@ -18,7 +18,7 @@ import { MockJobContext, setupDefaultJobEnvironment } from '../../../__mocks__/c import { setupMockIngestDevice, setupMockShowStyleCompound } from '../../../__mocks__/presetCollections.js' import { fixSnapshot } from '../../../__mocks__/helpers/snapshot.js' import { DBRundown, RundownOrphanedReason, RundownSource } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { MongoQuery } from '../../../db/index.js' import { handleRemovedRundown } from '../../ingestRundownJobs.js' import { MOS } from '@sofie-automation/corelib' diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index 41de01b1bfa..2756929ce57 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -36,6 +36,7 @@ import { setNextPart } from '../playout/setNext.js' import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import type { WrappedShowStyleBlueprint } from '../blueprints/cache.js' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { PersistentPlayoutStateStore } from '../blueprints/context/services/PersistantStateStore.js' type PlayStatus = 'previous' | 'current' | 'next' export interface PartInstanceToSync { @@ -144,27 +145,44 @@ export class SyncChangesToPartInstancesWorker { instanceToSync.playStatus ) // TODO - how can we limit the frequency we run this? (ie, how do we know nothing affecting this has changed) + + // Snapshot t-timers before update in case we need to rollback + const tTimersSnapshot = [...this.#playoutModel.playlist.tTimers] + try { if (!this.#blueprint.blueprint.syncIngestUpdateToPartInstance) throw new Error('Blueprint does not have syncIngestUpdateToPartInstance') + const blueprintPersistentState = new PersistentPlayoutStateStore( + this.#playoutModel.playlist.privatePlayoutPersistentState, + this.#playoutModel.playlist.publicPlayoutPersistentState + ) + // The blueprint handles what in the updated part is going to be synced into the partInstance: this.#blueprint.blueprint.syncIngestUpdateToPartInstance( syncContext, existingResultPartInstance, newResultData, - instanceToSync.playStatus + instanceToSync.playStatus, + blueprintPersistentState ) // Persist t-timer changes for (const timer of syncContext.changedTTimers) { this.#playoutModel.updateTTimer(timer) } + + blueprintPersistentState.saveToModel(this.#playoutModel) } catch (err) { logger.error(`Error in showStyleBlueprint.syncIngestUpdateToPartInstance: ${stringifyError(err)}`) // Operation failed, rollback the changes existingPartInstance.snapshotRestore(partInstanceSnapshot) + + // Also restore t-timers to prevent partial updates + for (let i = 0; i < tTimersSnapshot.length; i++) { + this.#playoutModel.updateTTimer(tTimersSnapshot[i]) + } } if (instanceToSync.playStatus === 'next' && syncContext.hasRemovedPartInstance) { diff --git a/packages/job-worker/src/ingest/updateNext.ts b/packages/job-worker/src/ingest/updateNext.ts index a6d46198432..f5815a10336 100644 --- a/packages/job-worker/src/ingest/updateNext.ts +++ b/packages/job-worker/src/ingest/updateNext.ts @@ -5,8 +5,13 @@ import { setNextPart } from '../playout/setNext.js' import { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' /** - * Make sure that the nextPartInstance for the current Playlist is still correct - * This will often change the nextPartInstance + * Make sure that the nextPartInstance for the current Playlist is still correct. + * This will often change the nextPartInstance. + * + * If the selected next part has been deleted by ingest, the default behavior is to + * drop it and autoselect a replacement. If the show-style blueprint defines + * `syncIngestUpdateToPartInstance`, the deleted next part is kept selected here so + * the blueprint can decide whether to remove it or preserve it. * @param context Context of the job being run * @param playoutModel Playout Model to operate on * @returns Whether the timeline should be updated following this operation @@ -43,14 +48,22 @@ export async function ensureNextPartIsValid(context: JobContext, playoutModel: P const orderedSegments = playoutModel.getAllOrderedSegments() const orderedParts = playoutModel.getAllOrderedParts() + const nextPartIsDeleted = nextPartInstance?.partInstance.orphaned === 'deleted' - if (!nextPartInstance || nextPartInstance.partInstance.orphaned === 'deleted') { - // Don't have a nextPart or it has been deleted, so autoselect something + if (nextPartIsDeleted && (await hasSyncIngestUpdateToPartInstance(context, playoutModel, nextPartInstance))) { + // Source has deleted this part, but it is selected as next. + // Keep it selected, and let blueprint function `syncIngestUpdateToPartInstance` decide what to do with it. + span?.end() + return false + } + + if (!nextPartInstance || nextPartIsDeleted) { + // Don't have a nextPart, so autoselect something const newNextPart = selectNextPart( context, playlist, currentPartInstance?.partInstance ?? null, - nextPartInstance?.partInstance ?? null, + null, orderedSegments, orderedParts, { ignoreUnplayable: true, ignoreQuickLoop: false } @@ -97,3 +110,26 @@ export async function ensureNextPartIsValid(context: JobContext, playoutModel: P span?.end() return false } + +async function hasSyncIngestUpdateToPartInstance( + context: JobContext, + playoutModel: PlayoutModel, + nextPartInstance: PlayoutModel['nextPartInstance'] +): Promise { + if (!nextPartInstance) return false + const rundown = playoutModel.getRundown(nextPartInstance.partInstance.part.rundownId) + if (!rundown) return false + if (!rundown.rundown.showStyleVariantId) return false + + try { + const showStyle = await context.getShowStyleCompound( + rundown.rundown.showStyleVariantId, + rundown.rundown.showStyleBaseId + ) + const blueprint = await context.getShowStyleBlueprint(showStyle._id) + + return !!blueprint.blueprint.syncIngestUpdateToPartInstance + } catch { + return false + } +} diff --git a/packages/job-worker/src/peripheralDevice.ts b/packages/job-worker/src/peripheralDevice.ts index 84e6c51277a..2438bd561e9 100644 --- a/packages/job-worker/src/peripheralDevice.ts +++ b/packages/job-worker/src/peripheralDevice.ts @@ -1,6 +1,7 @@ import { IBlueprintPlayoutDevice, TSR } from '@sofie-automation/blueprints-integration' import { PeripheralDeviceCommandId, PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' +import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' +import { ReadonlyDeep } from 'type-fest' import { clone, Complete, getRandomId, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' import { JobContext } from './jobs/index.js' import { getCurrentTime } from './lib/index.js' @@ -187,19 +188,41 @@ async function executePeripheralDeviceGenericFunction( return result.promise } +/** + * Lists TSR subdevices for blueprint use when no {@link PlayoutModel} is loaded. + * + * Used by snapshot hooks and other studio-scoped blueprint callbacks outside active playout. + */ +export async function listPlayoutDevicesForStudio(context: JobContext): Promise { + const parentDevices = await context.directCollections.PeripheralDevices.findFetch({ + 'studioAndConfigId.studioId': context.studioId, + type: PeripheralDeviceType.PLAYOUT, + }) + return listPlayoutDevicesFromParentDevices(context, parentDevices) +} + export async function listPlayoutDevices( context: JobContext, playoutModel: PlayoutModel ): Promise { - const parentDevicesMap = normalizeArrayToMap( - playoutModel.peripheralDevices.filter( - (doc) => doc.studioAndConfigId?.studioId === context.studioId && doc.type === PeripheralDeviceType.PLAYOUT - ), - '_id' + const parentDevices = playoutModel.peripheralDevices.filter( + (doc) => doc.studioAndConfigId?.studioId === context.studioId && doc.type === PeripheralDeviceType.PLAYOUT ) + return listPlayoutDevicesFromParentDevices(context, parentDevices) +} + +/** + * Resolves playout-gateway subdevices for the given parent playout peripheral devices. + * Returns an empty list when there are no parent devices. + */ +async function listPlayoutDevicesFromParentDevices( + context: JobContext, + parentDevices: ReadonlyDeep +): Promise { + const parentDevicesMap = normalizeArrayToMap(parentDevices, '_id') const parentDeviceIds = Array.from(parentDevicesMap.keys()) if (parentDeviceIds.length === 0) { - throw new Error('No parent devices are configured') + return [] } const devices = await context.directCollections.PeripheralDevices.findFetch({ diff --git a/packages/job-worker/src/playout/__tests__/actions.test.ts b/packages/job-worker/src/playout/__tests__/actions.test.ts index bc381cdd0c9..d3a90d67431 100644 --- a/packages/job-worker/src/playout/__tests__/actions.test.ts +++ b/packages/job-worker/src/playout/__tests__/actions.test.ts @@ -1,5 +1,5 @@ import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context.js' diff --git a/packages/job-worker/src/playout/__tests__/infinites.test.ts b/packages/job-worker/src/playout/__tests__/infinites.test.ts index e4e623d405f..fca3c8da0c5 100644 --- a/packages/job-worker/src/playout/__tests__/infinites.test.ts +++ b/packages/job-worker/src/playout/__tests__/infinites.test.ts @@ -1,4 +1,4 @@ -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context.js' import { ReadonlyDeep, SetRequired } from 'type-fest' import { PlayoutModel } from '../model/PlayoutModel.js' diff --git a/packages/job-worker/src/playout/__tests__/lib.ts b/packages/job-worker/src/playout/__tests__/lib.ts index f48d1d3ccb7..d64a134e554 100644 --- a/packages/job-worker/src/playout/__tests__/lib.ts +++ b/packages/job-worker/src/playout/__tests__/lib.ts @@ -1,7 +1,7 @@ import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { sortPartsInSortedSegments } from '@sofie-automation/corelib/dist/playout/playlist' import { JobContext } from '../../jobs/index.js' diff --git a/packages/job-worker/src/playout/__tests__/playout.test.ts b/packages/job-worker/src/playout/__tests__/playout.test.ts index 912b118cc26..ad49430f9ab 100644 --- a/packages/job-worker/src/playout/__tests__/playout.test.ts +++ b/packages/job-worker/src/playout/__tests__/playout.test.ts @@ -22,7 +22,7 @@ import { handleResetRundownPlaylist, } from '../activePlaylistJobs.js' import { getSelectedPartInstances } from './lib.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { UserErrorMessage } from '@sofie-automation/corelib/dist/error' import * as peripheralDeviceLib from '../../peripheralDevice.js' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' diff --git a/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts b/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts index fe4caad1902..adf93459e23 100644 --- a/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts +++ b/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts @@ -8,7 +8,7 @@ import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/cont import { PlayoutSegmentModelImpl } from '../model/implementation/PlayoutSegmentModelImpl.js' import { PlayoutSegmentModel } from '../model/PlayoutSegmentModel.js' import { selectNextPart } from '../selectNextPart.js' -import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' class MockPart { diff --git a/packages/job-worker/src/playout/__tests__/snapshotHooks.test.ts b/packages/job-worker/src/playout/__tests__/snapshotHooks.test.ts new file mode 100644 index 00000000000..d2feda87c73 --- /dev/null +++ b/packages/job-worker/src/playout/__tests__/snapshotHooks.test.ts @@ -0,0 +1,212 @@ +import { CoreRundownPlaylistSnapshot } from '@sofie-automation/corelib/dist/snapshots' +import { getRandomId, literal } from '@sofie-automation/corelib/dist/lib' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context.js' +import { defaultRundown, defaultRundownPlaylist } from '../../__mocks__/defaultCollectionObjects.js' +import { setupDefaultRundownPlaylist, setupMockShowStyleCompound } from '../../__mocks__/presetCollections.js' +import { listPlayoutDevicesForStudio } from '../../peripheralDevice.js' +import { handleGeneratePlaylistSnapshot } from '../snapshot.js' +import { handleOnSystemSnapshotCreated, pickRundownForPlaylistSnapshot } from '../snapshotHooks.js' + +describe('Snapshot blueprint hooks', () => { + let context: MockJobContext + + beforeEach(async () => { + context = setupDefaultJobEnvironment() + await setupMockShowStyleCompound(context) + }) + + describe('onPlaylistSnapshotCreated', () => { + test('invokes the show style blueprint callback', async () => { + const onPlaylistSnapshotCreated = jest.fn() + context.updateShowStyleBlueprint({ onPlaylistSnapshotCreated }) + + const { playlistId } = await setupDefaultRundownPlaylist(context) + const snapshotId = getRandomId() + + await handleGeneratePlaylistSnapshot(context, { + playlistId, + full: false, + withTimeline: false, + snapshotId, + reason: 'test reason', + }) + + expect(onPlaylistSnapshotCreated).toHaveBeenCalledTimes(1) + expect(onPlaylistSnapshotCreated.mock.calls[0][1]).toMatchObject({ + snapshotId: expect.any(String), + playlistId: expect.any(String), + reason: 'test reason', + options: { + full: false, + withTimeline: false, + }, + playlist: { + name: expect.any(String), + active: expect.any(Boolean), + rehearsal: expect.any(Boolean), + }, + }) + }) + + test('does nothing when the callback is not defined', async () => { + context.updateShowStyleBlueprint({ onPlaylistSnapshotCreated: undefined }) + + const { playlistId } = await setupDefaultRundownPlaylist(context) + + await expect( + handleGeneratePlaylistSnapshot(context, { + playlistId, + full: false, + withTimeline: false, + }) + ).resolves.toBeDefined() + }) + }) + + describe('pickRundownForPlaylistSnapshot', () => { + test('prefers current part rundown over next when both are set', () => { + const studioId = protectString('studio0') + const playlistId = getRandomId() + const showStyleBaseId = getRandomId() + const showStyleVariantId = getRandomId() + + const rundownCurrent = defaultRundown( + 'rundown_current', + studioId, + null, + playlistId, + showStyleBaseId, + showStyleVariantId + ) + rundownCurrent.name = 'ZZZ Current' + const rundownNext = defaultRundown( + 'rundown_next', + studioId, + null, + playlistId, + showStyleBaseId, + showStyleVariantId + ) + rundownNext.name = 'AAA Next' + + const playlist = defaultRundownPlaylist(playlistId, studioId) + playlist.currentPartInfo = { + partInstanceId: getRandomId(), + rundownId: rundownCurrent._id, + manuallySelected: false, + consumesQueuedSegmentId: false, + } + playlist.nextPartInfo = { + partInstanceId: getRandomId(), + rundownId: rundownNext._id, + manuallySelected: false, + consumesQueuedSegmentId: false, + } + + const snapshot = literal({ + version: '1', + playlistId, + playlist, + rundowns: [rundownCurrent, rundownNext], + ingestData: [], + sofieIngestData: [], + baselineObjs: [], + baselineAdlibs: [], + segments: [], + parts: [], + partInstances: [], + pieces: [], + pieceInstances: [], + adLibPieces: [], + adLibActions: [], + baselineAdLibActions: [], + expectedPlayoutItems: [], + expectedPackages: [], + }) + + expect(pickRundownForPlaylistSnapshot(playlist, snapshot)?._id).toEqual(rundownCurrent._id) + }) + }) + + describe('onSystemSnapshotCreated', () => { + test('invokes the studio blueprint callback', async () => { + const onSystemSnapshotCreated = jest.fn() + context.updateStudioBlueprint({ onSystemSnapshotCreated }) + + const snapshotId = getRandomId() + await handleOnSystemSnapshotCreated(context, { + snapshotId, + reason: 'system test', + type: 'system', + options: { + studioId: context.studioId, + withDeviceSnapshots: true, + }, + }) + + expect(onSystemSnapshotCreated).toHaveBeenCalledTimes(1) + expect(onSystemSnapshotCreated.mock.calls[0][1]).toMatchObject({ + snapshotId: expect.any(String), + reason: 'system test', + type: 'system', + options: { + studioId: expect.any(String), + withDeviceSnapshots: true, + }, + }) + }) + + test('passes fullSystem flag from job props', async () => { + const onSystemSnapshotCreated = jest.fn() + context.updateStudioBlueprint({ onSystemSnapshotCreated }) + + await handleOnSystemSnapshotCreated(context, { + snapshotId: getRandomId(), + reason: 'full system test', + type: 'system', + options: { + studioId: context.studioId, + fullSystem: true, + }, + }) + + expect(onSystemSnapshotCreated.mock.calls[0][1].options).toMatchObject({ + fullSystem: true, + }) + + onSystemSnapshotCreated.mockClear() + + await handleOnSystemSnapshotCreated(context, { + snapshotId: getRandomId(), + reason: 'studio scoped test', + type: 'system', + options: { + studioId: context.studioId, + fullSystem: false, + }, + }) + + expect(onSystemSnapshotCreated.mock.calls[0][1].options.fullSystem).toBe(false) + }) + + test('does nothing when the callback is not defined', async () => { + context.updateStudioBlueprint({ onSystemSnapshotCreated: undefined }) + + await expect( + handleOnSystemSnapshotCreated(context, { + snapshotId: getRandomId(), + reason: 'system test', + type: 'debug', + options: { + studioId: context.studioId, + }, + }) + ).resolves.toBeUndefined() + }) + + test('listPlayoutDevices returns an empty array when no playout gateway is configured', async () => { + await expect(listPlayoutDevicesForStudio(context)).resolves.toEqual([]) + }) + }) +}) diff --git a/packages/job-worker/src/playout/__tests__/tTimers.test.ts b/packages/job-worker/src/playout/__tests__/tTimers.test.ts index d323e939abd..0f6309dd74d 100644 --- a/packages/job-worker/src/playout/__tests__/tTimers.test.ts +++ b/packages/job-worker/src/playout/__tests__/tTimers.test.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import { useFakeCurrentTime, useRealCurrentTime } from '../../__mocks__/time.js' import { validateTTimerIndex, @@ -10,7 +11,6 @@ import { calculateNextTimeOfDayTarget, createTimeOfDayTTimer, } from '../tTimers.js' -import type { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' describe('tTimers utils', () => { beforeEach(() => { @@ -289,7 +289,7 @@ describe('tTimers utils', () => { }) }) - it('should return null for freeRun timer', () => { + it('should restart a running freeRun timer', () => { const timer: RundownTTimer = { index: 2, label: 'Test', @@ -299,7 +299,34 @@ describe('tTimers utils', () => { state: { paused: false, zeroTime: 5000 }, } - expect(restartTTimer(timer)).toBeNull() + expect(restartTTimer(timer)).toEqual({ + index: 2, + label: 'Test', + mode: { + type: 'freeRun', + }, + state: { paused: false, zeroTime: 10000 }, // now + }) + }) + + it('should restart a paused freeRun timer (stays paused)', () => { + const timer: RundownTTimer = { + index: 2, + label: 'Test', + mode: { + type: 'freeRun', + }, + state: { paused: true, duration: 12345 }, + } + + expect(restartTTimer(timer)).toEqual({ + index: 2, + label: 'Test', + mode: { + type: 'freeRun', + }, + state: { paused: true, duration: 0 }, + }) }) it('should return null for timer with no mode', () => { diff --git a/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts b/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts index 6704e8255ed..836bd5fb576 100644 --- a/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts +++ b/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts @@ -1,9 +1,21 @@ import { setupDefaultJobEnvironment, MockJobContext } from '../../__mocks__/context.js' -import { handleRecalculateTTimerProjections } from '../tTimersJobs.js' +import { + handleRecalculateTTimerProjections, + handleTTimerClearProjected, + handleTTimerSetProjectedAnchorPart, + handleTTimerSetProjectedDuration, + handleTTimerSetProjectedTime, +} from '../tTimersJobs.js' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { PartId, RundownPlaylistId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { literal } from '@sofie-automation/corelib/dist/lib' +import { + defaultPart, + defaultRundown, + defaultRundownPlaylist, + defaultSegment, +} from '../../__mocks__/defaultCollectionObjects.js' describe('tTimersJobs', () => { let context: MockJobContext @@ -208,4 +220,104 @@ describe('tTimersJobs', () => { await expect(handleRecalculateTTimerProjections(context)).resolves.toBeUndefined() }) }) + + describe('projection endpoints', () => { + it('handleTTimerClearProjected should clear projectedState and anchorPartId', async () => { + const playlistId = protectString('playlist-proj-clear') + + const playlist = defaultRundownPlaylist(playlistId, context.studioId) + playlist.tTimers[0] = { + ...playlist.tTimers[0], + anchorPartId: protectString('somePart'), + projectedState: literal({ paused: true, duration: 1234 }), + } + await context.directCollections.RundownPlaylists.insertOne(literal(playlist)) + + await expect(handleTTimerClearProjected(context, { playlistId, timerIndex: 1 })).resolves.toBeUndefined() + + const updatedPlaylist = await context.directCollections.RundownPlaylists.findOne(playlistId) + expect(updatedPlaylist).toBeTruthy() + expect(updatedPlaylist?.tTimers[0].anchorPartId).toBeUndefined() + expect(updatedPlaylist?.tTimers[0].projectedState).toBeUndefined() + }) + + it('handleTTimerSetProjectedTime should set projectedState and clear anchorPartId', async () => { + const playlistId = protectString('playlist-proj-time') + + const playlist = defaultRundownPlaylist(playlistId, context.studioId) + playlist.tTimers[0] = { + ...playlist.tTimers[0], + anchorPartId: protectString('somePart'), + projectedState: undefined, + } + await context.directCollections.RundownPlaylists.insertOne(literal(playlist)) + + await expect( + handleTTimerSetProjectedTime(context, { playlistId, timerIndex: 1, time: 1234567890, paused: false }) + ).resolves.toBeUndefined() + + const updatedPlaylist = await context.directCollections.RundownPlaylists.findOne(playlistId) + expect(updatedPlaylist?.tTimers[0].anchorPartId).toBeUndefined() + expect(updatedPlaylist?.tTimers[0].projectedState).toEqual(literal({ paused: false, zeroTime: 1234567890 })) + }) + + it('handleTTimerSetProjectedDuration should set paused projectedState and clear anchorPartId', async () => { + const playlistId = protectString('playlist-proj-duration') + + const playlist = defaultRundownPlaylist(playlistId, context.studioId) + playlist.tTimers[0] = { + ...playlist.tTimers[0], + anchorPartId: protectString('somePart'), + } + await context.directCollections.RundownPlaylists.insertOne(literal(playlist)) + + await expect( + handleTTimerSetProjectedDuration(context, { playlistId, timerIndex: 1, duration: 5000, paused: true }) + ).resolves.toBeUndefined() + + const updatedPlaylist = await context.directCollections.RundownPlaylists.findOne(playlistId) + expect(updatedPlaylist?.tTimers[0].anchorPartId).toBeUndefined() + expect(updatedPlaylist?.tTimers[0].projectedState).toEqual(literal({ paused: true, duration: 5000 })) + }) + + it('handleTTimerSetProjectedAnchorPart should set anchorPartId for matching externalId', async () => { + const playlistId = protectString('playlist-proj-anchor') + const rundown = defaultRundown( + 'rundown0', + context.studioId, + null, + playlistId, + protectString('showStyleBase0'), + protectString('showStyleVariant0') + ) + const segmentId = protectString('segment0') + const segment = defaultSegment(segmentId, rundown._id) + const partId = protectString('part0') + const part = defaultPart(partId, rundown._id, segmentId) + part.externalId = 'myPartExternalId' + + const playlist = defaultRundownPlaylist(playlistId, context.studioId) + playlist.rundownIdsInOrder = [rundown._id] + playlist.tTimers[0] = { + ...playlist.tTimers[0], + projectedState: literal({ paused: true, duration: 999 }), + } + + await context.directCollections.RundownPlaylists.insertOne(literal(playlist)) + await context.directCollections.Rundowns.insertOne(literal(rundown)) + await context.directCollections.Segments.insertOne(literal(segment)) + await context.directCollections.Parts.insertOne(literal(part)) + + await expect( + handleTTimerSetProjectedAnchorPart(context, { + playlistId, + timerIndex: 1, + externalId: 'myPartExternalId', + }) + ).resolves.toBeUndefined() + + const updatedPlaylist = await context.directCollections.RundownPlaylists.findOne(playlistId) + expect(updatedPlaylist?.tTimers[0].anchorPartId).toEqual(partId) + }) + }) }) diff --git a/packages/job-worker/src/playout/__tests__/timeline.test.ts b/packages/job-worker/src/playout/__tests__/timeline.test.ts index 8f40ee2dfa9..88e5462d50d 100644 --- a/packages/job-worker/src/playout/__tests__/timeline.test.ts +++ b/packages/job-worker/src/playout/__tests__/timeline.test.ts @@ -11,7 +11,7 @@ import { } from '../../__mocks__/presetCollections.js' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context.js' import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { handleTakeNextPart } from '../take.js' import { handleActivateHold } from '../holdJobs.js' import { handleActivateRundownPlaylist, handleDeactivateRundownPlaylist } from '../activePlaylistJobs.js' diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts index 754ee919940..b326cdeb313 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts @@ -7,7 +7,7 @@ import { } from '@sofie-automation/blueprints-integration' import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PieceInstancePiece, ResolvedPieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { ABSessionAssignments } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ABSessionAssignments } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abPlaybackResolver.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abPlaybackResolver.spec.ts index 28a9aa79ce9..e90b0077380 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abPlaybackResolver.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abPlaybackResolver.spec.ts @@ -639,6 +639,56 @@ describe('resolveAbAssignmentsFromRequests', () => { expectGotPlayer(res, 'd', undefined) }) + test('Autonext lookahead assignment with more clips than players', () => { + const requests: SessionRequest[] = [ + // first part + { + id: 'a', + name: 'a', + start: 1000, + end: 5000, + playerId: 1, + pieceNames: ['p1'], + }, + // second part + { + id: 'b', + name: 'b', + start: 5000, + end: 10000, + playerId: 2, + pieceNames: ['p2'], + }, + // third part + { + id: 'c', + name: 'c', + start: 10000, + end: 15000, + playerId: 1, + pieceNames: ['p3'], + }, + // lookaheads (in order of future use) + { + id: 'z', + name: 'z', + start: 0, + end: 100, + lookaheadRank: 1, + pieceNames: [], + }, + ] + + const res = resolveAbAssignmentsFromRequests(resolverOptions, TWO_SLOTS, requests, 5100) + expect(res).toBeTruthy() + expect(res.failedOptional).toEqual([]) + expect(res.failedRequired).toEqual([]) + expectGotPlayer(res, 'a', 1) + expectGotPlayer(res, 'b', 2) + expectGotPlayer(res, 'c', 1) + expectGotPlayer(res, 'z', 2) + }) + test('Preserve on-air optional over a required', () => { const requests: SessionRequest[] = [ // current part diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts index 8cc718ff50c..29b18a72c07 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts @@ -1,7 +1,7 @@ import { PartInstanceId, PieceInstanceInfiniteId, PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { ABSessionInfo } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ABSessionInfo } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { clone, getRandomId, omit } from '@sofie-automation/corelib/dist/lib' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts index 0e416cb2556..6aea1fad529 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts @@ -1,5 +1,5 @@ import { ABResolverConfiguration, TSR } from '@sofie-automation/blueprints-integration' -import { ABSessionAssignments } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ABSessionAssignments } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' diff --git a/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts b/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts index e968b55e420..bb857c58933 100644 --- a/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts +++ b/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts @@ -361,6 +361,8 @@ function assignPlayersForLookahead( Array.from(lastSessionPerSlot.entries()), (session) => session[1] < safeNow ) + // We want to assign the ones that are clear soonest first, as they are more likely to be ready in time + playersClearSoon.sort((endA, endB) => endA[1] - endB[1]) // Assign the players which are clear right now const playersClearNowIds = new Set(playersClearNow.map((p) => p[0])) diff --git a/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts b/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts index 6c99b94f854..99f1e2df36c 100644 --- a/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts +++ b/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts @@ -1,6 +1,6 @@ import { OnGenerateTimelineObj, TSR } from '@sofie-automation/blueprints-integration' import { ResolvedPieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { ABSessionAssignments } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ABSessionAssignments } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import _ from 'underscore' import { SessionRequest } from './abPlaybackResolver.js' diff --git a/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts b/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts index 18df420b073..69c863f436c 100644 --- a/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts +++ b/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts @@ -2,7 +2,7 @@ import { AB_MEDIA_PLAYER_AUTO } from '@sofie-automation/blueprints-integration' import { PartId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { ABSessionInfo } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ABSessionInfo } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { getRandomString, omit } from '@sofie-automation/corelib/dist/lib' import { protectString, unpartialString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' diff --git a/packages/job-worker/src/playout/abPlayback/applyAssignments.ts b/packages/job-worker/src/playout/abPlayback/applyAssignments.ts index bb3096488b4..f3682550096 100644 --- a/packages/job-worker/src/playout/abPlayback/applyAssignments.ts +++ b/packages/job-worker/src/playout/abPlayback/applyAssignments.ts @@ -6,7 +6,10 @@ import { ABTimelineLayerChangeRules, AbPlayerId, } from '@sofie-automation/blueprints-integration' -import { ABSessionAssignment, ABSessionAssignments } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + ABSessionAssignment, + ABSessionAssignments, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { logger } from '../../logging.js' import _ from 'underscore' diff --git a/packages/job-worker/src/playout/abPlayback/index.ts b/packages/job-worker/src/playout/abPlayback/index.ts index a4598d260f7..eab0754bd47 100644 --- a/packages/job-worker/src/playout/abPlayback/index.ts +++ b/packages/job-worker/src/playout/abPlayback/index.ts @@ -3,7 +3,7 @@ import { ABSessionAssignment, ABSessionAssignments, DBRundownPlaylist, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { endTrace, sendTrace, startTrace } from '@sofie-automation/corelib/dist/influxdb' import { WrappedShowStyleBlueprint } from '../../blueprints/cache.js' diff --git a/packages/job-worker/src/playout/activePlaylistJobs.ts b/packages/job-worker/src/playout/activePlaylistJobs.ts index 8a5d6bb376f..65e95970b07 100644 --- a/packages/job-worker/src/playout/activePlaylistJobs.ts +++ b/packages/job-worker/src/playout/activePlaylistJobs.ts @@ -1,4 +1,4 @@ -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { ActivateRundownPlaylistProps, diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 78bc5bdc622..f1c2aed0ddd 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -14,8 +14,11 @@ import { PlayoutModel, PlayoutModelPreInit } from './model/PlayoutModel.js' import { runJobWithPlaylistLock } from './lock.js' import { updateTimeline } from './timeline/generate.js' import { performTakeToNextedPart } from './take.js' -import { ActionUserData } from '@sofie-automation/blueprints-integration' -import { DBRundownPlaylist, SelectedPartInstance } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ActionUserData, BlueprintExecuteActionResult } from '@sofie-automation/blueprints-integration' +import { + DBRundownPlaylist, + SelectedPartInstance, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { logger } from '../logging.js' import { AdLibActionId, @@ -35,6 +38,7 @@ import type { INoteBase } from '@sofie-automation/corelib/dist/dataModel/Notes' import { NotificationsModelHelper } from '../notifications/NotificationsModelHelper.js' import type { INotificationsModel } from '../notifications/NotificationsModel.js' import { PersistentPlayoutStateStore } from '../blueprints/context/services/PersistantStateStore.js' +import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' /** * Execute an AdLib Action @@ -246,7 +250,7 @@ export async function executeActionInner( )} (${actionParameters.triggerMode})` ) - let result: ExecuteActionResult | void + let result: BlueprintExecuteActionResult | void try { const blueprintPersistentState = new PersistentPlayoutStateStore( @@ -271,15 +275,21 @@ export async function executeActionInner( throw UserError.fromUnknown(err) } - if (result && result.validationErrors) { + // If the blueprint returned an error, abort the action and throw the error + if (result && typeof result === 'object' && result.message) { + const messageStr = interpollateTranslation(result.message.key, result.message.args) + const statusCode = Number.isFinite(result.errorCode) + ? Math.max(Math.min(Math.round(result.errorCode as number), 499), 400) + : 409 throw UserError.from( - new Error( - `AdLib Action "${actionParameters.actionId}" validation failed: ${JSON.stringify(result.validationErrors)}` - ), + new Error(messageStr), UserErrorMessage.ValidationFailed, - undefined, - 409 + { message: messageStr, rawMessage: result.message, details: result.details }, + statusCode ) + } else if (result !== undefined) { + // Unexpected return value — does not match the BlueprintExecuteActionResult shape; treat as success but warn so it can be investigated + logger.warn(`executeAction returned an unexpected value: ${JSON.stringify(result)}`) } // Store any notes generated by the action @@ -299,12 +309,12 @@ export async function executeActionInner( } } -async function applyAnyExecutionSideEffects( +export async function applyAnyExecutionSideEffects( context: JobContext, playoutModel: PlayoutModel, actionContext: ActionExecutionContext, now: number -) { +): Promise { await applyActionSideEffects(context, playoutModel, actionContext) if (actionContext.takeAfterExecute) { @@ -362,13 +372,13 @@ async function executeDataStoreAction( } } -function storeNotificationsForCategory( +export function storeNotificationsForCategory( notificationHelper: INotificationsModel, notificationCategory: string, blueprintId: BlueprintId, notes: INoteBase[], partInstanceInfo: SelectedPartInstance | null -) { +): void { for (const note of notes) { notificationHelper.setNotification(notificationCategory, { ...convertNoteToNotification(note, [blueprintId]), diff --git a/packages/job-worker/src/playout/adlibJobs.ts b/packages/job-worker/src/playout/adlibJobs.ts index 0d0bf2e3074..40fdac76b94 100644 --- a/packages/job-worker/src/playout/adlibJobs.ts +++ b/packages/job-worker/src/playout/adlibJobs.ts @@ -1,7 +1,7 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { assertNever, clone } from '@sofie-automation/corelib/dist/lib' import { logger } from '../logging.js' import { JobContext, ProcessedShowStyleCompound } from '../jobs/index.js' diff --git a/packages/job-worker/src/playout/adlibUtils.ts b/packages/job-worker/src/playout/adlibUtils.ts index f422efd0d2f..9a54c0d62af 100644 --- a/packages/job-worker/src/playout/adlibUtils.ts +++ b/packages/job-worker/src/playout/adlibUtils.ts @@ -26,7 +26,7 @@ import { ReadonlyDeep } from 'type-fest' import { PlayoutRundownModel } from './model/PlayoutRundownModel.js' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' export async function innerStartOrQueueAdLibPiece( context: JobContext, diff --git a/packages/job-worker/src/playout/bucketAdlibJobs.ts b/packages/job-worker/src/playout/bucketAdlibJobs.ts index 85da8778c22..38ef26cf97e 100644 --- a/packages/job-worker/src/playout/bucketAdlibJobs.ts +++ b/packages/job-worker/src/playout/bucketAdlibJobs.ts @@ -5,7 +5,7 @@ import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/erro import { BucketId, ShowStyleBaseId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { innerStartOrQueueAdLibPiece } from './adlibUtils.js' import { executeAdlibActionAndSaveModel } from './adlibAction.js' -import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { createPlayoutModelfromInitModel, loadPlayoutModelPreInit } from './model/implementation/LoadPlayoutModel.js' /** diff --git a/packages/job-worker/src/playout/externalEvents.ts b/packages/job-worker/src/playout/externalEvents.ts new file mode 100644 index 00000000000..86fd0389e79 --- /dev/null +++ b/packages/job-worker/src/playout/externalEvents.ts @@ -0,0 +1,127 @@ +import { OnExternalEventsProps } from '@sofie-automation/corelib/dist/worker/studio' +import { PeripheralDeviceExternalEvent } from '@sofie-automation/shared-lib/dist/peripheralDevice/externalEvents' +import { logger } from '../logging.js' +import { JobContext } from '../jobs/index.js' +import { PlayoutModel } from './model/PlayoutModel.js' +import { runJobWithPlayoutModel } from './lock.js' +import { runJobWithStudioPlayoutModel } from '../studio/lock.js' +import { ActionExecutionContext } from '../blueprints/context/adlibActions.js' +import { WatchedPackagesHelper } from '../blueprints/context/watchedPackages.js' +import { PartAndPieceInstanceActionService } from '../blueprints/context/services/PartAndPieceInstanceActionService.js' +import { PersistentPlayoutStateStore } from '../blueprints/context/services/PersistantStateStore.js' +import { applyAnyExecutionSideEffects, storeNotificationsForCategory } from './adlibAction.js' +import { getCurrentTime } from '../lib/index.js' +import { getRandomId } from '@sofie-automation/corelib/dist/lib' +import { BlueprintExternalEvent } from '@sofie-automation/blueprints-integration' + +/** + * Called by sofie-core when one or more external events have been received from a gateway. + * + * Events from multiple gateways, or from rapid bursts on a single gateway, are merged into a + * single job invocation by the queue manager to prevent flooding. + * + * For each active playlist in the studio, the show-style blueprint's `onExternalEvent` handler + * is invoked if it is defined. + */ +export async function handleOnExternalEvents(context: JobContext, data: OnExternalEventsProps): Promise { + if (!data.events.length) return + + logger.debug(`handleOnExternalEvents: received ${data.events.length} event(s)`) + + await runJobWithStudioPlayoutModel(context, async (studioPlayoutModel) => { + const activePlaylists = studioPlayoutModel.getActiveRundownPlaylists() + if (activePlaylists.length === 0) { + logger.debug('handleOnExternalEvents: no active playlists — events discarded') + return + } + + for (const playlist of activePlaylists) { + await runJobWithPlayoutModel(context, { playlistId: playlist._id }, null, async (playoutModel) => { + await executeOnExternalEventsForPlaylist(context, playoutModel, data.events) + }) + } + }) +} + +async function executeOnExternalEventsForPlaylist( + context: JobContext, + playoutModel: PlayoutModel, + wireEvents: PeripheralDeviceExternalEvent[] +): Promise { + const playlist = playoutModel.playlist + + const activePartInfo = playlist.currentPartInfo ?? playlist.nextPartInfo + if (!activePartInfo) { + logger.error( + `handleOnExternalEvents: playlist "${playlist._id}" has neither currentPartInfo nor nextPartInfo — events will be lost` + ) + return + } + + const currentRundown = playoutModel.getRundown(activePartInfo.rundownId) + if (!currentRundown) { + logger.error( + `executeOnExternalEventsForPlaylist: rundown "${activePartInfo.rundownId}" not found in playlist "${playlist._id}" — events will be lost` + ) + return + } + + const showStyle = await context.getShowStyleCompound( + currentRundown.rundown.showStyleVariantId, + currentRundown.rundown.showStyleBaseId + ) + const blueprint = await context.getShowStyleBlueprint(showStyle._id) + + if (!blueprint.blueprint.onExternalEvent) { + logger.debug( + `executeOnExternalEventsForPlaylist: blueprint for show style "${showStyle._id}" has no onExternalEvent handler — events discarded` + ) + return + } + + logger.debug( + `executeOnExternalEventsForPlaylist: invoking onExternalEvent for playlist "${playlist._id}" with ${wireEvents.length} event(s): ${wireEvents.map((e) => `${e.type}/${(e as { event?: string }).event ?? '?'}`).join(', ')}` + ) + + const now = getCurrentTime() + + // Future: This may want to become a different context, but for now the types align cleanly + const actionContext = new ActionExecutionContext( + { + name: `${currentRundown.rundown.name}(${playlist.name})`, + identifier: `playlist=${playlist._id},rundown=${currentRundown.rundown._id},activePartInstance=${ + activePartInfo.partInstanceId + },execution=${getRandomId()}`, + }, + context, + playoutModel, + showStyle, + context.getShowStyleBlueprintConfig(showStyle), + WatchedPackagesHelper.empty(context), + new PartAndPieceInstanceActionService(context, playoutModel, showStyle, currentRundown) + ) + + const persistentState = new PersistentPlayoutStateStore( + playlist.privatePlayoutPersistentState, + playlist.publicPlayoutPersistentState + ) + + // Cast the wire events to blueprint events. + // PeripheralDeviceExternalTSREvent has the same shape as BlueprintExternalTSREvent, but uses + // `deviceType: string` (loose) rather than the strongly-typed TSR enum, to accommodate custom + // TSR plugin device types that do not appear in the closed enum. + const blueprintEvents = wireEvents as unknown as BlueprintExternalEvent[] + + await blueprint.blueprint.onExternalEvent(actionContext, persistentState, blueprintEvents) + persistentState.saveToModel(playoutModel) + + storeNotificationsForCategory( + playoutModel, + `externalEvent:${getRandomId()}`, + blueprint.blueprintId, + actionContext.notes, + playlist.currentPartInfo ?? playlist.nextPartInfo + ) + + await applyAnyExecutionSideEffects(context, playoutModel, actionContext, now) +} diff --git a/packages/job-worker/src/playout/holdJobs.ts b/packages/job-worker/src/playout/holdJobs.ts index cf4f6e651c4..bd10968aa3a 100644 --- a/packages/job-worker/src/playout/holdJobs.ts +++ b/packages/job-worker/src/playout/holdJobs.ts @@ -1,5 +1,5 @@ import { PartHoldMode } from '@sofie-automation/blueprints-integration' -import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { ActivateHoldProps, DeactivateHoldProps } from '@sofie-automation/corelib/dist/worker/studio' import { JobContext } from '../jobs/index.js' diff --git a/packages/job-worker/src/playout/lock.ts b/packages/job-worker/src/playout/lock.ts index 0dad5256174..94b41fc38f1 100644 --- a/packages/job-worker/src/playout/lock.ts +++ b/packages/job-worker/src/playout/lock.ts @@ -1,5 +1,5 @@ import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { RundownPlayoutPropsBase } from '@sofie-automation/corelib/dist/worker/studio' import { logger } from '../logging.js' import { ReadonlyDeep } from 'type-fest' diff --git a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts index a515e2e0b4f..de4b4430fc4 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts @@ -11,7 +11,10 @@ import { LookaheadMode, PlaylistTimingType, TSR } from '@sofie-automation/bluepr import { setupDefaultJobEnvironment, MockJobContext } from '../../../__mocks__/context.js' import { runJobWithPlayoutModel } from '../../../playout/lock.js' import { defaultRundownPlaylist } from '../../../__mocks__/defaultCollectionObjects.js' -import { DBRundownPlaylist, RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + DBRundownPlaylist, + RundownHoldState, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' jest.mock('../findForLayer') type TfindLookaheadForLayer = jest.MockedFunction diff --git a/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts index ef62434b0e8..961d408857c 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts @@ -10,7 +10,7 @@ import { runJobWithPlayoutModel } from '../../../playout/lock.js' import { defaultRundownPlaylist } from '../../../__mocks__/defaultCollectionObjects.js' import _ from 'underscore' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { wrapPartToTemporaryInstance } from '@sofie-automation/corelib/dist/playout/stateCacheResolver' diff --git a/packages/job-worker/src/playout/lookahead/index.ts b/packages/job-worker/src/playout/lookahead/index.ts index cfcb8b42801..447bb3352ce 100644 --- a/packages/job-worker/src/playout/lookahead/index.ts +++ b/packages/job-worker/src/playout/lookahead/index.ts @@ -24,7 +24,7 @@ import { LookaheadTimelineObject } from './findObjects.js' import { hasPieceInstanceDefinitelyEnded, TimelinePlayoutState } from '../timeline/lib.js' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { ReadonlyDeep } from 'type-fest' -import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { filterPieceInstancesForNextPartWithOffset } from './lookaheadOffset.js' const LOOKAHEAD_OBJ_PRIORITY = 0.1 diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index f84a098b281..86946144222 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -17,8 +17,8 @@ import { DBRundownPlaylist, QuickLoopMarker, RundownHoldState, - RundownTTimer, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import { ReadonlyDeep } from 'type-fest' import { StudioPlayoutModelBase, StudioPlayoutModelBaseReadonly } from '../../studio/model/StudioPlayoutModel.js' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' diff --git a/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts b/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts index 6cb43e43e1f..a247fcb0f04 100644 --- a/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts @@ -11,6 +11,7 @@ import { IBlueprintMutatablePart, PieceLifespan, Time } from '@sofie-automation/ import { PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' import { PlayoutPieceInstanceModel } from './PlayoutPieceInstanceModel.js' import { CoreUserEditingDefinition } from '@sofie-automation/corelib/dist/dataModel/UserEditingDefinitions' +import { PartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part' /** * Token returned when making a backup copy of a PlayoutPartInstanceModel @@ -56,6 +57,14 @@ export interface PlayoutPartInstanceModel { */ blockTakeUntil(timestamp: Time | null): void + /** + * Set the invalid reason for this PartInstance. + * This indicates a runtime validation issue that prevents taking the part. + * This is distinct from the planned `invalidReason` on the Part itself. + * @param reason The reason the part is invalid, or undefined to clear + */ + setInvalidReason(reason: PartInvalidReason | undefined): void + /** * Get a PieceInstance which belongs to this PartInstance * @param id Id of the PieceInstance diff --git a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts index c811d2fdcd9..2bac8edbedf 100644 --- a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts @@ -1,5 +1,5 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DatabasePersistedModel } from '../../../modelBase.js' import { IngestModelReadonly } from '../../../ingest/model/IngestModel.js' import { PlaylistLock } from '../../../jobs/lock.js' diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 25c7754c24f..92c3d74cbbc 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -16,9 +16,9 @@ import { DBRundownPlaylist, QuickLoopMarker, RundownHoldState, - RundownTTimer, SelectedPartInstance, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import { ReadonlyDeep } from 'type-fest' import { JobContext } from '../../../jobs/index.js' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts index 6294c5c00c4..06e662494ed 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts @@ -25,7 +25,7 @@ import { PlayoutPieceInstanceModel } from '../PlayoutPieceInstanceModel.js' import { PlayoutPieceInstanceModelImpl } from './PlayoutPieceInstanceModelImpl.js' import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import _ from 'underscore' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { DBPart, PartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part' import { PlayoutMutatablePartSampleKeys } from '../../../blueprints/context/lib.js' import { QuickLoopService } from '../services/QuickLoopService.js' @@ -217,6 +217,10 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { this.#compareAndSetPartInstanceValue('blockTakeUntil', timestamp ?? undefined) } + setInvalidReason(reason: PartInvalidReason | undefined): void { + this.#compareAndSetPartInstanceValue('invalidReason', reason) + } + getPieceInstance(id: PieceInstanceId): PlayoutPieceInstanceModel | undefined { return this.pieceInstancesImpl.get(id) ?? undefined } diff --git a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts index 1e1538012b7..3827f8cdae3 100644 --- a/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts +++ b/packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts @@ -1,9 +1,5 @@ -import { - ForceQuickLoopAutoNext, - JSONBlobStringify, - PieceLifespan, - StatusCode, -} from '@sofie-automation/blueprints-integration' +import { JSONBlobStringify, PieceLifespan, StatusCode } from '@sofie-automation/blueprints-integration' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { PartInstanceId, @@ -19,7 +15,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { EmptyPieceTimelineObjectsBlob, Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' @@ -321,6 +317,7 @@ function setupMockPlayoutGateway(id: PeripheralDeviceId): PeripheralDevice { status: { statusCode: StatusCode.GOOD, messages: [], + statusDetails: [], }, token: '', } diff --git a/packages/job-worker/src/playout/model/services/QuickLoopService.ts b/packages/job-worker/src/playout/model/services/QuickLoopService.ts index c81ae70613e..59f3d632948 100644 --- a/packages/job-worker/src/playout/model/services/QuickLoopService.ts +++ b/packages/job-worker/src/playout/model/services/QuickLoopService.ts @@ -4,7 +4,7 @@ import { QuickLoopMarker, QuickLoopMarkerType, QuickLoopProps, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' @@ -80,7 +80,7 @@ export class QuickLoopService { getUpdatedProps(hasJustSetMarker?: 'start' | 'end'): QuickLoopProps | undefined { if (this.playoutModel.playlist.quickLoop == null) return undefined - const quickLoopProps = clone(this.playoutModel.playlist.quickLoop) + const quickLoopProps = clone(this.playoutModel.playlist.quickLoop) const wasLoopRunning = quickLoopProps.running this.resetDynamicallyInsertedPartOverrideIfNoLongerNeeded(quickLoopProps) @@ -145,12 +145,12 @@ export class QuickLoopService { if (!this.playoutModel.playlist.quickLoop) return undefined if (this.playoutModel.playlist.quickLoop.locked) { - const quickLoopProps = clone(this.playoutModel.playlist.quickLoop) + const quickLoopProps = clone(this.playoutModel.playlist.quickLoop) quickLoopProps.running = false return quickLoopProps } - const quickLoopProps = clone(this.playoutModel.playlist.quickLoop) + const quickLoopProps = clone(this.playoutModel.playlist.quickLoop) delete quickLoopProps.start delete quickLoopProps.end quickLoopProps.running = false diff --git a/packages/job-worker/src/playout/quickLoopMarkers.ts b/packages/job-worker/src/playout/quickLoopMarkers.ts index 6989c745717..c35f70ef3e6 100644 --- a/packages/job-worker/src/playout/quickLoopMarkers.ts +++ b/packages/job-worker/src/playout/quickLoopMarkers.ts @@ -6,7 +6,10 @@ import { updateTimeline } from './timeline/generate.js' import { selectNextPart } from './selectNextPart.js' import { setNextPart } from './setNext.js' import { resetPartInstancesWithPieceInstances } from './lib.js' -import { QuickLoopMarker, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + QuickLoopMarker, + QuickLoopMarkerType, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { clone } from 'underscore' import { PlayoutModel } from './model/PlayoutModel.js' diff --git a/packages/job-worker/src/playout/selectNextPart.ts b/packages/job-worker/src/playout/selectNextPart.ts index da8184a5ccb..19cc81c4415 100644 --- a/packages/job-worker/src/playout/selectNextPart.ts +++ b/packages/job-worker/src/playout/selectNextPart.ts @@ -1,7 +1,10 @@ import { DBPart, isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { JobContext } from '../jobs/index.js' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + DBRundownPlaylist, + QuickLoopMarkerType, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index 2fb38067aed..39e39fec630 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -16,7 +16,7 @@ import { PRESERVE_UNSYNCED_PLAYING_SEGMENT_CONTENTS } from '@sofie-automation/sh import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import _ from 'underscore' import { resetPartInstancesWithPieceInstances } from './lib.js' -import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { SelectNextPartResult } from './selectNextPart.js' import { ReadonlyDeep } from 'type-fest' diff --git a/packages/job-worker/src/playout/setNextJobs.ts b/packages/job-worker/src/playout/setNextJobs.ts index f9ce6988de8..4d7ba436692 100644 --- a/packages/job-worker/src/playout/setNextJobs.ts +++ b/packages/job-worker/src/playout/setNextJobs.ts @@ -1,6 +1,6 @@ import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPart, isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' -import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { SetNextPartProps, diff --git a/packages/job-worker/src/playout/snapshot.ts b/packages/job-worker/src/playout/snapshot.ts index 95d9cb3b4d7..b1a6e1a0720 100644 --- a/packages/job-worker/src/playout/snapshot.ts +++ b/packages/job-worker/src/playout/snapshot.ts @@ -32,8 +32,9 @@ import { saveIntoDb } from '../db/changes.js' import { getPartId, getSegmentId } from '../ingest/lib.js' import { assertNever, getHash, getRandomId, literal, omit } from '@sofie-automation/corelib/dist/lib' import { logger } from '../logging.js' +import { invokeOnPlaylistSnapshotCreated } from './snapshotHooks.js' import { JSONBlobParse, JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { RundownOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { SofieIngestDataCacheObj } from '@sofie-automation/corelib/dist/dataModel/SofieIngestDataCache' import * as PackagesPreR53 from '@sofie-automation/corelib/dist/dataModel/Old/ExpectedPackagesR52' @@ -62,8 +63,8 @@ export async function handleGeneratePlaylistSnapshot( context: JobContext, props: GeneratePlaylistSnapshotProps ): Promise { - const snapshot = await runWithPlaylistLock(context, props.playlistId, async () => { - const snapshotId: SnapshotId = getRandomId() + const lockedResult = await runWithPlaylistLock(context, props.playlistId, async () => { + const snapshotId: SnapshotId = props.snapshotId ?? getRandomId() logger.info(`Generating RundownPlaylist snapshot "${snapshotId}" for RundownPlaylist "${props.playlistId}"`) const playlist = await context.directCollections.RundownPlaylists.findOne(props.playlistId) @@ -142,7 +143,7 @@ export async function handleGeneratePlaylistSnapshot( : undefined logger.info(`Snapshot generation done`) - return literal({ + const coreSnapshot = literal({ version: getSystemVersion(), playlistId: playlist._id, playlist, @@ -163,10 +164,14 @@ export async function handleGeneratePlaylistSnapshot( expectedPackages, timeline, }) + + return { coreSnapshot, snapshotId } }) + await invokeOnPlaylistSnapshotCreated(context, props, lockedResult.coreSnapshot, lockedResult.snapshotId) + return { - snapshotJson: JSONBlobStringify(snapshot), + snapshotJson: JSONBlobStringify(lockedResult.coreSnapshot), } } diff --git a/packages/job-worker/src/playout/snapshotHooks.ts b/packages/job-worker/src/playout/snapshotHooks.ts new file mode 100644 index 00000000000..f8d65b4c3ad --- /dev/null +++ b/packages/job-worker/src/playout/snapshotHooks.ts @@ -0,0 +1,163 @@ +import { IBlueprintPlaylistSnapshotInfo, IBlueprintSystemSnapshotInfo } from '@sofie-automation/blueprints-integration' +import { + OnSystemSnapshotCreatedProps, + GeneratePlaylistSnapshotProps, +} from '@sofie-automation/corelib/dist/worker/studio' +import { SnapshotId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { CoreRundownPlaylistSnapshot } from '@sofie-automation/corelib/dist/snapshots' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { ReadonlyDeep } from 'type-fest' +import { PlaylistSnapshotCreatedContext } from '../blueprints/context/PlaylistSnapshotCreatedContext.js' +import { SystemSnapshotCreatedContext } from '../blueprints/context/SystemSnapshotCreatedContext.js' +import { JobContext } from '../jobs/index.js' +import { logger } from '../logging.js' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' + +/** + * Invokes {@link ShowStyleBlueprintManifest.onPlaylistSnapshotCreated} if defined. + * + * Called from {@link handleGeneratePlaylistSnapshot} after the playlist lock is released. + * Blueprint resolution and hook errors are caught and logged; they do not abort snapshot generation. + */ +export async function invokeOnPlaylistSnapshotCreated( + context: JobContext, + props: GeneratePlaylistSnapshotProps, + snapshot: CoreRundownPlaylistSnapshot, + snapshotId: SnapshotId +): Promise { + const playlist = snapshot.playlist + const rundown = pickRundownForPlaylistSnapshot(playlist, snapshot) + if (!rundown) { + logger.info(`Skipping onPlaylistSnapshotCreated for playlist "${playlist._id}": no rundown found in playlist`) + return + } + + try { + const showStyle = await context.getShowStyleCompound(rundown.showStyleVariantId, rundown.showStyleBaseId) + const blueprint = await context.getShowStyleBlueprint(showStyle._id) + if (!blueprint.blueprint.onPlaylistSnapshotCreated) return + + const info: IBlueprintPlaylistSnapshotInfo = { + snapshotId: unprotectString(snapshotId), + playlistId: unprotectString(playlist._id), + reason: props.reason ?? '', + options: { + full: props.full, + withTimeline: props.withTimeline, + }, + playlist: { + name: playlist.name, + active: !!playlist.activationId, + rehearsal: !!playlist.rehearsal, + }, + } + + const blueprintContext = new PlaylistSnapshotCreatedContext( + context, + { + name: 'onPlaylistSnapshotCreated', + identifier: `studioId=${context.studioId},playlistId=${playlist._id},snapshotId=${snapshotId}`, + }, + context.studio, + context.getStudioBlueprintConfig(), + showStyle, + context.getShowStyleBlueprintConfig(showStyle) + ) + await blueprint.blueprint.onPlaylistSnapshotCreated(blueprintContext, info) + } catch (err) { + logger.error( + `Error in showStyleBlueprint.onPlaylistSnapshotCreated (rundownId=${rundown._id}, showStyleBaseId=${rundown.showStyleBaseId}, showStyleVariantId=${rundown.showStyleVariantId}): ${stringifyError(err)}` + ) + } +} + +/** + * Chooses which rundown (and thus show-style blueprint) to use for a playlist snapshot hook. + * + * Priority: current part → next part (from playlist part info, then part instances in the snapshot), + * otherwise the first rundown sorted by name. Matches presenter-style resolution (`current` before `next`). + */ +export function pickRundownForPlaylistSnapshot( + playlist: ReadonlyDeep, + snapshot: CoreRundownPlaylistSnapshot +): ReadonlyDeep | undefined { + const rundowns = [...snapshot.rundowns].sort((a, b) => a.name.localeCompare(b.name)) + if (rundowns.length === 0) return undefined + + const partInstanceById = new Map(snapshot.partInstances.map((p) => [p._id, p])) + + const currentPartInfo = playlist.currentPartInfo + if (currentPartInfo) { + if (currentPartInfo.rundownId) { + const rundown = rundowns.find((r) => r._id === currentPartInfo.rundownId) + if (rundown) return rundown + } + const currentPartInstanceId = currentPartInfo.partInstanceId + if (currentPartInstanceId) { + const currentPartInstance = partInstanceById.get(currentPartInstanceId) + if (currentPartInstance) { + const rundown = rundowns.find((r) => r._id === currentPartInstance.rundownId) + if (rundown) return rundown + } + } + } + + const nextPartInfo = playlist.nextPartInfo + if (nextPartInfo) { + if (nextPartInfo.rundownId) { + const rundown = rundowns.find((r) => r._id === nextPartInfo.rundownId) + if (rundown) return rundown + } + const nextPartInstanceId = nextPartInfo.partInstanceId + if (nextPartInstanceId) { + const nextPartInstance = partInstanceById.get(nextPartInstanceId) + if (nextPartInstance) { + const rundown = rundowns.find((r) => r._id === nextPartInstance.rundownId) + if (rundown) return rundown + } + } + } + + return rundowns[0] +} + +/** + * Worker job handler for {@link StudioJobs.OnSystemSnapshotCreated}. + * + * Invokes {@link StudioBlueprintManifest.onSystemSnapshotCreated} for the studio of the worker job. + * Queued from Meteor after a system or debug snapshot has been stored. + */ +export async function handleOnSystemSnapshotCreated( + context: JobContext, + props: OnSystemSnapshotCreatedProps +): Promise { + if (!context.studioBlueprint.blueprint.onSystemSnapshotCreated) return + + const info: IBlueprintSystemSnapshotInfo = { + snapshotId: unprotectString(props.snapshotId), + reason: props.reason, + type: props.type, + options: { + studioId: props.options.studioId ? unprotectString(props.options.studioId) : undefined, + withDeviceSnapshots: props.options.withDeviceSnapshots, + fullSystem: props.options.fullSystem, + }, + } + + try { + const blueprintContext = new SystemSnapshotCreatedContext( + context, + { + name: 'onSystemSnapshotCreated', + identifier: `studioId=${context.studioId},snapshotId=${props.snapshotId}`, + }, + context.studio, + context.getStudioBlueprintConfig() + ) + await context.studioBlueprint.blueprint.onSystemSnapshotCreated(blueprintContext, info) + } catch (err) { + logger.error(`Error in studioBlueprint.onSystemSnapshotCreated: ${stringifyError(err)}`) + } +} diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index 55c5c806cc4..fb83afb753c 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -1,9 +1,8 @@ import type { RundownTTimerIndex, - RundownTTimerMode, RundownTTimer, - TimerState, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' +import type { RundownTTimerMode, TimerState } from '@sofie-automation/blueprints-integration' import { literal } from '@sofie-automation/corelib/dist/lib' import { getCurrentTime } from '../lib/index.js' import type { ReadonlyDeep } from 'type-fest' @@ -74,6 +73,11 @@ export function restartTTimer(timer: ReadonlyDeep): ReadonlyDeep< ? { paused: true, duration: timer.mode.duration } : { paused: false, zeroTime: getCurrentTime() + timer.mode.duration }, } + } else if (timer.mode.type === 'freeRun') { + // Reset the free-run timer back to zero, preserving paused/running state + return timer.state.paused + ? { ...timer, state: { paused: true, duration: 0 } } + : { ...timer, state: { paused: false, zeroTime: getCurrentTime() } } } else if (timer.mode.type === 'timeOfDay') { const nextTime = calculateNextTimeOfDayTarget(timer.mode.targetRaw) // If we can't calculate the next time, or it's the same, we can't restart @@ -209,7 +213,7 @@ export function recalculateTTimerProjections(context: JobContext, playoutModel: // If no timers have anchors, nothing to do if (timerAnchors.size === 0) { if (span) span.end() - return undefined + return } const currentPartInstance = playoutModel.currentPartInstance?.partInstance @@ -236,6 +240,8 @@ export function recalculateTTimerProjections(context: JobContext, playoutModel: let segmentAccumulator = 0 let isPushing = false let currentSegmentId: SegmentId | undefined = undefined + let currentPartRemainingTime = 0 + let startedInCurrentSegment = false // Handle current part/segment // TODO: We should consider how to handle the case where the current part is untimed - for now, we just skip it in the calculations @@ -258,25 +264,41 @@ export function recalculateTTimerProjections(context: JobContext, playoutModel: isPushing = remaining < 0 totalAccumulator = Math.max(0, remaining) + currentPartRemainingTime = totalAccumulator } } else { // Segment budget timing - we're already inside a budgeted segment + // For parts within the current budgeted segment, calculate based on + // remaining part durations. When we leave this segment, use the segment budget. + startedInCurrentSegment = true const segmentStartedPlayback = playlist.segmentsStartedPlayback?.[currentPartInstance.segmentId as unknown as string] + + // Calculate remaining time in current part + const currentPartDuration = + currentPartInstance.part.expectedDurationWithTransition ?? currentPartInstance.part.expectedDuration + if (currentPartDuration) { + const currentPartStartedPlayback = currentPartInstance.timings?.plannedStartedPlayback + const startedPlayback = + currentPartStartedPlayback && currentPartStartedPlayback <= now ? currentPartStartedPlayback : now + const playOffset = currentPartInstance.timings?.playOffset || 0 + const elapsed = now - startedPlayback - playOffset + const partRemaining = currentPartDuration - elapsed + currentPartRemainingTime = Math.max(0, partRemaining) + // Set totalAccumulator to current part remaining for correct anchor calculations + // within the same segment + totalAccumulator = currentPartRemainingTime + } + + // Check if we're pushing (segment budget overrun) if (segmentStartedPlayback) { const segmentElapsed = now - segmentStartedPlayback - const remaining = currentSegmentBudget - segmentElapsed - isPushing = remaining < 0 - totalAccumulator = Math.max(0, remaining) - } else { - totalAccumulator = currentSegmentBudget + const segmentRemaining = currentSegmentBudget - segmentElapsed + isPushing = segmentRemaining < 0 } } } - // Save remaining current part time for pauseTime calculation - const currentPartRemainingTime = totalAccumulator - // Add the next part to the beginning of playablePartsSlice // getOrderedPartsAfterPlayhead excludes both current and next, so we need to prepend next // This allows the loop to handle it normally, including detecting if it's an anchor @@ -295,7 +317,9 @@ export function recalculateTTimerProjections(context: JobContext, playoutModel: const segmentBudget = lastSegment?.segment.segmentTiming?.budgetDuration // Use budget if it exists, otherwise use accumulated part durations - if (segmentBudget !== undefined) { + // BUT: if we started in this segment (budgeted), don't add the budget again + // because we already accounted for parts via totalAccumulator + segmentAccumulator + if (segmentBudget !== undefined && !startedInCurrentSegment) { totalAccumulator += segmentBudget } else { totalAccumulator += segmentAccumulator @@ -305,6 +329,7 @@ export function recalculateTTimerProjections(context: JobContext, playoutModel: // Reset for new segment segmentAccumulator = 0 currentSegmentId = part.segmentId + startedInCurrentSegment = false } // Check if this part is an anchor diff --git a/packages/job-worker/src/playout/tTimersJobs.ts b/packages/job-worker/src/playout/tTimersJobs.ts index a639ea1db04..ad520043bb3 100644 --- a/packages/job-worker/src/playout/tTimersJobs.ts +++ b/packages/job-worker/src/playout/tTimersJobs.ts @@ -1,6 +1,34 @@ +import { + TTimerPropsBase, + TTimerClearProjectedProps, + TTimerPauseProps, + TTimerRestartProps, + TTimerResumeProps, + TTimerSetProjectedAnchorPartProps, + TTimerSetProjectedDurationProps, + TTimerSetProjectedTimeProps, + TTimerStartCountdownProps, + TTimerStartFreeRunProps, +} from '@sofie-automation/corelib/dist/worker/studio' import { JobContext } from '../jobs/index.js' -import { recalculateTTimerProjections } from './tTimers.js' import { runWithPlayoutModel, runWithPlaylistLock } from './lock.js' +import { recalculateTTimerProjections } from './tTimers.js' +import { runJobWithPlayoutModel } from './lock.js' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { PlaylistTTimerImpl, TTimersService } from '../blueprints/context/services/TTimersService.js' +import type { PlayoutModel } from './model/PlayoutModel.js' + +async function runTTimerJob( + context: JobContext, + data: TTimerPropsBase, + fcn: (playoutModel: PlayoutModel, timer: PlaylistTTimerImpl) => Promise | void +): Promise { + return runJobWithPlayoutModel(context, data, null, async (playoutModel) => { + const timersService = TTimersService.withPlayoutModel(playoutModel, context) + const timer = timersService.getTimer(data.timerIndex) as PlaylistTTimerImpl + return fcn(playoutModel, timer) + }) +} /** * Handle RecalculateTTimerProjections job @@ -42,3 +70,85 @@ export async function handleRecalculateTTimerProjections(context: JobContext): P }) } } + +export async function handleTTimerStartCountdown(context: JobContext, data: TTimerStartCountdownProps): Promise { + return runTTimerJob(context, data, async (_playoutModel, timer) => { + timer.startCountdown(data.duration * 1000, { + stopAtZero: data.stopAtZero, + startPaused: data.startPaused, + }) + }) +} + +export async function handleTTimerStartFreeRun(context: JobContext, data: TTimerStartFreeRunProps): Promise { + return runTTimerJob(context, data, async (_playoutModel, timer) => { + timer.startFreeRun({ + startPaused: data.startPaused, + }) + }) +} + +export async function handleTTimerPause(_context: JobContext, data: TTimerPauseProps): Promise { + return runTTimerJob(_context, data, async (_playoutModel, timer) => { + timer.pause() + }) +} + +export async function handleTTimerResume(context: JobContext, data: TTimerResumeProps): Promise { + return runTTimerJob(context, data, async (_playoutModel, timer) => { + timer.resume() + }) +} + +export async function handleTTimerRestart(context: JobContext, data: TTimerRestartProps): Promise { + return runTTimerJob(context, data, async (_playoutModel, timer) => { + timer.restart() + }) +} + +export async function handleTTimerClearProjected(context: JobContext, data: TTimerClearProjectedProps): Promise { + return runTTimerJob(context, data, async (_playoutModel, timer) => { + timer.clearProjected() + }) +} + +export async function handleTTimerSetProjectedAnchorPart( + context: JobContext, + data: TTimerSetProjectedAnchorPartProps +): Promise { + return runTTimerJob(context, data, async (playoutModel, timer) => { + const providedPartId = data.partId ? unprotectString(data.partId) : undefined + + const part = + (data.externalId + ? playoutModel.getAllOrderedParts().find((p) => p.externalId === data.externalId) + : undefined) ?? + (providedPartId + ? playoutModel + .getAllOrderedParts() + .find((p) => unprotectString(p._id) === providedPartId || p.externalId === providedPartId) + : undefined) + + if (!part) return + + timer.setProjectedAnchorPart(unprotectString(part._id)) + }) +} + +export async function handleTTimerSetProjectedTime( + context: JobContext, + data: TTimerSetProjectedTimeProps +): Promise { + return runTTimerJob(context, data, async (_playoutModel, timer) => { + timer.setProjectedTime(data.time, data.paused) + }) +} + +export async function handleTTimerSetProjectedDuration( + context: JobContext, + data: TTimerSetProjectedDurationProps +): Promise { + return runTTimerJob(context, data, async (_playoutModel, timer) => { + timer.setProjectedDuration(data.duration, data.paused) + }) +} diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index e4542916874..539dbdabd73 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -1,6 +1,9 @@ import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { RundownHoldState, SelectedPartInstance } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + RundownHoldState, + SelectedPartInstance, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { logger } from '../logging.js' import { JobContext, ProcessedShowStyleCompound } from '../jobs/index.js' @@ -227,6 +230,10 @@ export async function performTakeToNextedPart( if (!takeRundown) throw new Error(`takeRundown: takeRundown not found! ("${takePartInstance.partInstance.rundownId}")`) + if (takePartInstance.partInstance.invalidReason) { + throw UserError.create(UserErrorMessage.TakePartInstanceInvalid) + } + const showStyle = await pShowStyle const blueprint = await context.getShowStyleBlueprint(showStyle._id) diff --git a/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts b/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts index 15133e9d57b..d8ceb5dd71b 100644 --- a/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts +++ b/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts @@ -1,5 +1,8 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { DBRundownPlaylist, SelectedPartInstance } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + DBRundownPlaylist, + SelectedPartInstance, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { setupDefaultJobEnvironment } from '../../../__mocks__/context.js' import { buildTimelineObjsForRundown, RundownTimelineResult, RundownTimelineTimingContext } from '../rundown.js' import { protectString } from '@sofie-automation/corelib/dist/protectedString' diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index 7af782d4ee9..5c4bac75627 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -7,7 +7,10 @@ import { } from '@sofie-automation/blueprints-integration' import { PartInstanceId, PieceInstanceId, PieceInstanceInfiniteId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PieceInstanceInfinite } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { DBRundownPlaylist, RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + DBRundownPlaylist, + RundownHoldState, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { TimelineObjGroupPart, TimelineObjRundown, diff --git a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts index 399f999e7ad..d7a16567ba2 100644 --- a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts +++ b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts @@ -1,5 +1,5 @@ import { PartInstanceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { OnTimelineTriggerTimeProps } from '@sofie-automation/corelib/dist/worker/studio' import { logger } from '../../logging.js' import { JobContext } from '../../jobs/index.js' diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index d6b97f8894c..12c1a545582 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -2,11 +2,11 @@ import { BlueprintMapping, BlueprintMappings, BlueprintParentDeviceSettings, - IStudioSettings, JSONBlobParse, StudioRouteBehavior, TSR, } from '@sofie-automation/blueprints-integration' +import { IStudioSettings, ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { MappingsExt, StudioDeviceSettings, @@ -186,6 +186,7 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data allowPieceDirectPlay: true, enableBuckets: true, enableEvaluationForm: true, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, } const packageContainerSettings = result.packageContainerSettings ?? { diff --git a/packages/job-worker/src/rundown.ts b/packages/job-worker/src/rundown.ts index ec50085bb69..2980a4a697a 100644 --- a/packages/job-worker/src/rundown.ts +++ b/packages/job-worker/src/rundown.ts @@ -1,5 +1,5 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ReadonlyDeep } from 'type-fest' /** Return true if the rundown is allowed to be moved out of that playlist */ diff --git a/packages/job-worker/src/rundownPlaylists.ts b/packages/job-worker/src/rundownPlaylists.ts index 33faf33e29a..894d140cb41 100644 --- a/packages/job-worker/src/rundownPlaylists.ts +++ b/packages/job-worker/src/rundownPlaylists.ts @@ -1,6 +1,9 @@ import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + DBRundownPlaylist, + QuickLoopMarkerType, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { clone, diff --git a/packages/job-worker/src/studio/lib.ts b/packages/job-worker/src/studio/lib.ts index aaa91ffc3bd..5b0351c9b86 100644 --- a/packages/job-worker/src/studio/lib.ts +++ b/packages/job-worker/src/studio/lib.ts @@ -1,5 +1,5 @@ import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { JobContext } from '../jobs/index.js' diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts index 788efee63ee..953eafd02f8 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts @@ -1,6 +1,6 @@ import type { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import type { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import type { TimelineComplete, TimelineCompleteGenerationVersions, diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts index 4dba1885ef1..1ae07e92394 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts @@ -1,6 +1,6 @@ import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { serializeTimelineBlob, TimelineComplete, diff --git a/packages/job-worker/src/workers/studio/jobs.ts b/packages/job-worker/src/workers/studio/jobs.ts index 89928fd3b9f..98c8c95d8e3 100644 --- a/packages/job-worker/src/workers/studio/jobs.ts +++ b/packages/job-worker/src/workers/studio/jobs.ts @@ -35,6 +35,7 @@ import { handleRestoreRundownsInPlaylistToDefaultOrder, } from '../../rundownPlaylists.js' import { handleGeneratePlaylistSnapshot, handleRestorePlaylistSnapshot } from '../../playout/snapshot.js' +import { handleOnSystemSnapshotCreated } from '../../playout/snapshotHooks.js' import { handleBlueprintFixUpConfigForStudio, handleBlueprintIgnoreFixUpConfigForStudio, @@ -42,6 +43,7 @@ import { handleBlueprintValidateConfigForStudio, } from '../../playout/upgrade.js' import { handleTimelineTriggerTime, handleOnPlayoutPlaybackChanged } from '../../playout/timings/index.js' +import { handleOnExternalEvents } from '../../playout/externalEvents.js' import { handleExecuteAdlibAction } from '../../playout/adlibAction.js' import { handleTakeNextPart } from '../../playout/take.js' import { handleClearQuickLoopMarkers, handleSetQuickLoopMarker } from '../../playout/quickLoopMarkers.js' @@ -49,7 +51,18 @@ import { handleActivateAdlibTesting } from '../../playout/adlibTesting.js' import { handleExecuteBucketAdLibOrAction } from '../../playout/bucketAdlibJobs.js' import { handleSwitchRouteSet } from '../../studio/routeSet.js' import { handleCleanupOrphanedExpectedPackageReferences } from '../../playout/expectedPackages.js' -import { handleRecalculateTTimerProjections } from '../../playout/tTimersJobs.js' +import { + handleRecalculateTTimerProjections, + handleTTimerClearProjected, + handleTTimerPause, + handleTTimerRestart, + handleTTimerResume, + handleTTimerSetProjectedAnchorPart, + handleTTimerSetProjectedDuration, + handleTTimerSetProjectedTime, + handleTTimerStartCountdown, + handleTTimerStartFreeRun, +} from '../../playout/tTimersJobs.js' type ExecutableFunction = ( context: JobContext, @@ -87,6 +100,7 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.OnPlayoutPlaybackChanged]: handleOnPlayoutPlaybackChanged, [StudioJobs.OnTimelineTriggerTime]: handleTimelineTriggerTime, + [StudioJobs.OnExternalEvents]: handleOnExternalEvents, [StudioJobs.RecalculateTTimerProjections]: handleRecalculateTTimerProjections, @@ -101,6 +115,7 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.GeneratePlaylistSnapshot]: handleGeneratePlaylistSnapshot, [StudioJobs.RestorePlaylistSnapshot]: handleRestorePlaylistSnapshot, + [StudioJobs.OnSystemSnapshotCreated]: handleOnSystemSnapshotCreated, [StudioJobs.DebugCrash]: handleDebugCrash, [StudioJobs.BlueprintUpgradeForStudio]: handleBlueprintUpgradeForStudio, @@ -116,4 +131,14 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.SwitchRouteSet]: handleSwitchRouteSet, [StudioJobs.CleanupOrphanedExpectedPackageReferences]: handleCleanupOrphanedExpectedPackageReferences, + + [StudioJobs.TTimerStartCountdown]: handleTTimerStartCountdown, + [StudioJobs.TTimerStartFreeRun]: handleTTimerStartFreeRun, + [StudioJobs.TTimerPause]: handleTTimerPause, + [StudioJobs.TTimerResume]: handleTTimerResume, + [StudioJobs.TTimerRestart]: handleTTimerRestart, + [StudioJobs.TTimerClearProjected]: handleTTimerClearProjected, + [StudioJobs.TTimerSetProjectedAnchorPart]: handleTTimerSetProjectedAnchorPart, + [StudioJobs.TTimerSetProjectedTime]: handleTTimerSetProjectedTime, + [StudioJobs.TTimerSetProjectedDuration]: handleTTimerSetProjectedDuration, } diff --git a/packages/job-worker/tsconfig.build.json b/packages/job-worker/tsconfig.build.json index 7b0825b3e39..8951b0dc26b 100755 --- a/packages/job-worker/tsconfig.build.json +++ b/packages/job-worker/tsconfig.build.json @@ -16,7 +16,8 @@ "types": ["node"], "skipLibCheck": true, "esModuleInterop": true, - "composite": true + "composite": true, + "module": "node20" }, "references": [ // diff --git a/packages/live-status-gateway-api/api/asyncapi.yaml b/packages/live-status-gateway-api/api/asyncapi.yaml index cb1004b8512..49ba1e4dd52 100644 --- a/packages/live-status-gateway-api/api/asyncapi.yaml +++ b/packages/live-status-gateway-api/api/asyncapi.yaml @@ -37,6 +37,8 @@ channels: $ref: './topics/studio/studioTopic.yaml' activePlaylist: $ref: './topics/activePlaylist/activePlaylistTopic.yaml' + resolvedPlaylist: + $ref: './topics/resolvedPlaylist/resolvedPlaylistTopic.yaml' activePieces: $ref: './topics/activePieces/activePiecesTopic.yaml' segments: diff --git a/packages/live-status-gateway-api/api/components/layers/outputLayer/outputLayer-example.yaml b/packages/live-status-gateway-api/api/components/layers/outputLayer/outputLayer-example.yaml new file mode 100644 index 00000000000..f23d6be6180 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/layers/outputLayer/outputLayer-example.yaml @@ -0,0 +1,6 @@ +id: 'ol_pgm' +name: 'PGM' +isFlattened: false +isPGM: true +sourceLayerIds: + - 'sl_camera' diff --git a/packages/live-status-gateway-api/api/components/layers/outputLayer/outputLayer.yaml b/packages/live-status-gateway-api/api/components/layers/outputLayer/outputLayer.yaml new file mode 100644 index 00000000000..beeb940f2fd --- /dev/null +++ b/packages/live-status-gateway-api/api/components/layers/outputLayer/outputLayer.yaml @@ -0,0 +1,27 @@ +$defs: + outputLayer: + type: object + title: OutputLayer + description: Definition of an output layer used in a segment + properties: + id: + type: string + description: Unique id of the output layer + name: + type: string + description: User-presentable name of the output layer + isFlattened: + type: boolean + description: Whether the output layer is flattened + isPGM: + type: boolean + description: Whether PGM treatment should be in effect + sourceLayerIds: + description: The set of sourceLayer ids that feed this output layer + type: array + items: + type: string + required: [id, name, isFlattened, isPGM, sourceLayerIds] + additionalProperties: false + examples: + - $ref: './outputLayer-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/layers/sourceLayer/sourceLayer-example.yaml b/packages/live-status-gateway-api/api/components/layers/sourceLayer/sourceLayer-example.yaml new file mode 100644 index 00000000000..70ab371c1e5 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/layers/sourceLayer/sourceLayer-example.yaml @@ -0,0 +1,6 @@ +id: 'sl_camera' +name: 'Camera' +abbreviation: 'CAM' +isHidden: false +type: 1 +rank: 0 diff --git a/packages/live-status-gateway-api/api/components/layers/sourceLayer/sourceLayer.yaml b/packages/live-status-gateway-api/api/components/layers/sourceLayer/sourceLayer.yaml new file mode 100644 index 00000000000..d2559f16f11 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/layers/sourceLayer/sourceLayer.yaml @@ -0,0 +1,28 @@ +$defs: + sourceLayer: + type: object + title: SourceLayer + description: Definition of a source layer used in a segment + properties: + id: + type: string + description: Unique id of the source layer + name: + type: string + description: User-presentable name of the source layer + abbreviation: + type: string + description: Abbreviation for display + isHidden: + type: boolean + description: Whether the source layer is hidden in the UI + type: + type: number + description: Source layer content type (numeric enum) + rank: + type: number + description: Rank for ordering + required: [id, name, abbreviation, isHidden, type, rank] + additionalProperties: false + examples: + - $ref: './sourceLayer-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/part/partInvalidReason/partInvalidReason-example.yaml b/packages/live-status-gateway-api/api/components/part/partInvalidReason/partInvalidReason-example.yaml new file mode 100644 index 00000000000..e805e00be52 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/part/partInvalidReason/partInvalidReason-example.yaml @@ -0,0 +1,3 @@ +message: 'Invalid clip reference' +severity: warning +color: '#ff0000' diff --git a/packages/live-status-gateway-api/api/components/part/partInvalidReason/partInvalidReason.yaml b/packages/live-status-gateway-api/api/components/part/partInvalidReason/partInvalidReason.yaml new file mode 100644 index 00000000000..3c8816ad8c8 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/part/partInvalidReason/partInvalidReason.yaml @@ -0,0 +1,19 @@ +$defs: + partInvalidReason: + title: PartInvalidReason + description: Explanation for why a part is invalid + type: object + properties: + message: + type: string + description: Human-readable message explaining why the part is invalid + severity: + description: Severity hint for displaying the invalid reason + $ref: '../../notifications/notificationSeverity.yaml#/$defs/severity' + color: + description: Optional UI color hint + type: string + required: [message] + additionalProperties: false + examples: + - $ref: './partInvalidReason-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart-example.yaml b/packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart-example.yaml new file mode 100644 index 00000000000..e4cc4ab726e --- /dev/null +++ b/packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart-example.yaml @@ -0,0 +1,17 @@ +$ref: '../partBase/partBase-example.yaml' +instanceId: 'partInstance_current_1' +externalId: 'ext_part_1' +rank: 10 +invalid: false +floated: false +untimed: false +invalidReason: + $ref: '../partInvalidReason/partInvalidReason-example.yaml' +state: current +createdByAdLib: false +publicData: + partType: 'intro' +timing: + $ref: '../../timing/resolvedPartTiming/resolvedPartTiming-example.yaml' +pieces: + - $ref: '../../piece/resolvedPiece/resolvedPiece-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart.yaml b/packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart.yaml new file mode 100644 index 00000000000..bfe96a31976 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart.yaml @@ -0,0 +1,56 @@ +$defs: + resolvedPartState: + title: ResolvedPartState + type: string + enum: + - current + - next + + resolvedPart: + title: ResolvedPart + description: A part within a resolved segment + allOf: + - $ref: '../partBase/partBase.yaml#/$defs/partBase' + - type: object + title: ResolvedPart + description: A part within a resolved segment + properties: + instanceId: + type: string + description: Unique id of the part instance + externalId: + type: string + description: Id normally sourced from the ingest system + rank: + type: number + description: Rank for ordering + invalid: + type: boolean + description: Whether this part is invalid and should not be taken + floated: + type: boolean + description: Whether this part is floated and cannot be taken/nexted + untimed: + type: boolean + description: Whether this part is excluded from normal timing calculations + invalidReason: + description: Optional explanation for why the part is invalid + $ref: '../partInvalidReason/partInvalidReason.yaml#/$defs/partInvalidReason' + state: + description: Set only for the current or next part + $ref: '#/$defs/resolvedPartState' + createdByAdLib: + type: boolean + description: Whether this part was created by an adlib + publicData: + description: Optional arbitrary data + timing: + $ref: '../../timing/resolvedPartTiming/resolvedPartTiming.yaml#/$defs/resolvedPartTiming' + pieces: + description: Pieces in this part + type: array + items: + $ref: '../../piece/resolvedPiece/resolvedPiece.yaml#/$defs/resolvedPiece' + required: [instanceId, externalId, rank, createdByAdLib, timing, pieces, invalid, floated, untimed] + examples: + - $ref: './resolvedPart-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/piece/resolvedPiece/resolvedPiece-example.yaml b/packages/live-status-gateway-api/api/components/piece/resolvedPiece/resolvedPiece-example.yaml new file mode 100644 index 00000000000..747954f547e --- /dev/null +++ b/packages/live-status-gateway-api/api/components/piece/resolvedPiece/resolvedPiece-example.yaml @@ -0,0 +1,17 @@ +id: 'piece_1' +instanceId: 'pieceInstance_1' +externalId: 'ext_piece_1' +name: 'Camera 1' +priority: 0 +sourceLayerId: 'sl_camera' +outputLayerId: 'ol_pgm' +createdByAdLib: false +invalid: false +publicData: + switcherSource: 1 +timing: + $ref: '../../timing/resolvedPieceTiming/resolvedPieceTiming-example.yaml' +tags: + - 'camera' +abSessions: + - $ref: '../pieceStatus/abSessionAssignment-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/piece/resolvedPiece/resolvedPiece.yaml b/packages/live-status-gateway-api/api/components/piece/resolvedPiece/resolvedPiece.yaml new file mode 100644 index 00000000000..8164862f97d --- /dev/null +++ b/packages/live-status-gateway-api/api/components/piece/resolvedPiece/resolvedPiece.yaml @@ -0,0 +1,51 @@ +$defs: + resolvedPiece: + type: object + title: ResolvedPiece + description: A piece within a resolved part + properties: + id: + type: string + description: Unique id of the piece + instanceId: + type: string + description: Unique id of the piece instance + externalId: + type: string + description: Id normally sourced from the ingest system + name: + type: string + description: User-facing name of the piece + priority: + type: number + description: Priority of the piece + sourceLayerId: + type: string + description: Id of the source layer for this piece + outputLayerId: + type: string + description: Id of the output layer for this piece + createdByAdLib: + type: boolean + description: Whether this piece was created by an adlib + invalid: + type: boolean + description: Whether this piece is invalid and should be ignored + publicData: + description: Optional arbitrary data + timing: + $ref: '../../timing/resolvedPieceTiming/resolvedPieceTiming.yaml#/$defs/resolvedPieceTiming' + tags: + description: Optional tags attached to this piece + type: array + items: + type: string + abSessions: + description: Optional AB playback session assignments for this piece + type: array + items: + $ref: '../pieceStatus/abSessionAssignment.yaml' + required: [id, externalId, name, priority, sourceLayerId, outputLayerId, createdByAdLib, invalid, timing] + additionalProperties: false + examples: + - $ref: './resolvedPiece-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml index d09a8222ef3..bf7b64e48b0 100644 --- a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml +++ b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml @@ -15,3 +15,5 @@ timing: $ref: '../../timing/activePlaylistTiming/activePlaylistTiming-example.yaml' quickLoop: $ref: '../../quickLoop/activePlaylistQuickLoop/activePlaylistQuickLoop-example.yaml' +tTimers: + $ref: '../../tTimers/tTimerStatus/tTimerStatus-array-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml index 21e7277c149..fbaf22ffe4b 100644 --- a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml +++ b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml @@ -46,7 +46,14 @@ $defs: $ref: '../../timing/activePlaylistTiming/activePlaylistTiming.yaml#/$defs/activePlaylistTiming' quickLoop: $ref: '../../quickLoop/activePlaylistQuickLoop/activePlaylistQuickLoop.yaml#/$defs/activePlaylistQuickLoop' - required: [event, id, externalId, name, rundownIds, currentPart, currentSegment, nextPart, timing] + tTimers: + description: Status of the 3 T-timers in the playlist + type: array + items: + $ref: '../../tTimers/tTimerStatus.yaml#/$defs/tTimerStatus' + minItems: 3 + maxItems: 3 + required: [event, id, externalId, name, rundownIds, currentPart, currentSegment, nextPart, timing, tTimers] additionalProperties: false examples: - $ref: './activePlaylistEvent-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/playlist/playlistStatus/playlistStatus.yaml b/packages/live-status-gateway-api/api/components/playlist/playlistStatus/playlistStatus.yaml index 6fa4d4a16c7..d05d115f75f 100644 --- a/packages/live-status-gateway-api/api/components/playlist/playlistStatus/playlistStatus.yaml +++ b/packages/live-status-gateway-api/api/components/playlist/playlistStatus/playlistStatus.yaml @@ -21,6 +21,5 @@ $defs: - rehearsal - activated required: [id, externalId, name, activationStatus] - additionalProperties: false examples: - $ref: './playlistStatus-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/resolvedPlaylist/messages/resolvedPlaylistMessage.yaml b/packages/live-status-gateway-api/api/components/resolvedPlaylist/messages/resolvedPlaylistMessage.yaml new file mode 100644 index 00000000000..b31865c719a --- /dev/null +++ b/packages/live-status-gateway-api/api/components/resolvedPlaylist/messages/resolvedPlaylistMessage.yaml @@ -0,0 +1,11 @@ +components: + messages: + resolvedPlaylistMessage: + name: resolvedPlaylist + messageId: resolvedPlaylistUpdate + description: Resolved Playlist status + payload: + $ref: '../resolvedPlaylistEvent/resolvedPlaylistEvent.yaml#/$defs/resolvedPlaylistEvent' + examples: + - payload: + $ref: '../resolvedPlaylistEvent/resolvedPlaylistEvent-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/resolvedPlaylist/resolvedPlaylistEvent/resolvedPlaylistEvent-example.yaml b/packages/live-status-gateway-api/api/components/resolvedPlaylist/resolvedPlaylistEvent/resolvedPlaylistEvent-example.yaml new file mode 100644 index 00000000000..3891e2e5395 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/resolvedPlaylist/resolvedPlaylistEvent/resolvedPlaylistEvent-example.yaml @@ -0,0 +1,18 @@ +$ref: '../../playlist/playlistStatus/playlistStatus-example.yaml' +event: resolvedPlaylist +currentPartInstanceId: 'partInstance_current_1' +nextPartInstanceId: 'partInstance_next_1' +playoutState: + mode: 'AUTO' +publicData: + category: 'Evening News' +timing: + $ref: '../../timing/resolvedPlaylistTiming/resolvedPlaylistTiming-example.yaml' +tTimers: + - $ref: '../../tTimers/tTimerStatus-countdownRunning-example.yaml' + - $ref: '../../tTimers/tTimerStatus-freeRunRunning-example.yaml' + - $ref: '../../tTimers/tTimerStatus-unconfigured-example.yaml' +quickLoop: + $ref: '../../quickLoop/activePlaylistQuickLoop/activePlaylistQuickLoop-example.yaml' +rundowns: + - $ref: '../../rundown/resolvedRundown/resolvedRundown-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/resolvedPlaylist/resolvedPlaylistEvent/resolvedPlaylistEvent.yaml b/packages/live-status-gateway-api/api/components/resolvedPlaylist/resolvedPlaylistEvent/resolvedPlaylistEvent.yaml new file mode 100644 index 00000000000..ebdf7ebe87a --- /dev/null +++ b/packages/live-status-gateway-api/api/components/resolvedPlaylist/resolvedPlaylistEvent/resolvedPlaylistEvent.yaml @@ -0,0 +1,51 @@ +title: Resolved Playlist +description: Resolved Playlist schema for websocket subscriptions +$defs: + resolvedPlaylistEvent: + title: ResolvedPlaylistEvent + description: Resolved playlist details, including the base playlist status. + allOf: + - $ref: '../../playlist/playlistStatus/playlistStatus.yaml#/$defs/playlistStatus' + - type: object + title: ResolvedPlaylistEvent + description: Resolved playlist details, including rundown/segment/part/piece structure. + properties: + event: + type: string + const: resolvedPlaylist + currentPartInstanceId: + description: Instance id of the current part, if any + oneOf: + - type: string + - type: 'null' + nextPartInstanceId: + description: Instance id of the next part, if any + oneOf: + - type: string + - type: 'null' + playoutState: + description: Blueprint-defined playout state, used to expose arbitrary information about playout + publicData: + description: Optional arbitrary data + timing: + $ref: '../../timing/resolvedPlaylistTiming/resolvedPlaylistTiming.yaml#/$defs/resolvedPlaylistTiming' + tTimers: + description: Status of the 3 T-timers in the playlist + type: array + items: + $ref: '../../tTimers/tTimerStatus.yaml#/$defs/tTimerStatus' + minItems: 3 + maxItems: 3 + quickLoop: + description: Information about the current quickLoop, if any + oneOf: + - type: 'null' + - $ref: '../../quickLoop/activePlaylistQuickLoop/activePlaylistQuickLoop.yaml#/$defs/activePlaylistQuickLoop' + rundowns: + description: The rundowns in the playlist, in rank order + type: array + items: + $ref: '../../rundown/resolvedRundown/resolvedRundown.yaml#/$defs/resolvedRundown' + required: [event, currentPartInstanceId, nextPartInstanceId, timing, tTimers, rundowns] + examples: + - $ref: './resolvedPlaylistEvent-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/rundown/resolvedRundown/resolvedRundown-example.yaml b/packages/live-status-gateway-api/api/components/rundown/resolvedRundown/resolvedRundown-example.yaml new file mode 100644 index 00000000000..b60f0f21f25 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/rundown/resolvedRundown/resolvedRundown-example.yaml @@ -0,0 +1,9 @@ +id: 'rd_1' +externalId: 'ext_rd_1' +name: 'Rundown 1' +rank: 10 +description: '' +publicData: + ingestSource: 'NRK' +segments: + - $ref: '../../segment/resolvedSegment/resolvedSegment-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/rundown/resolvedRundown/resolvedRundown.yaml b/packages/live-status-gateway-api/api/components/rundown/resolvedRundown/resolvedRundown.yaml new file mode 100644 index 00000000000..2e7abd79883 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/rundown/resolvedRundown/resolvedRundown.yaml @@ -0,0 +1,32 @@ +$defs: + resolvedRundown: + type: object + title: ResolvedRundown + description: A rundown within a resolved playlist + properties: + id: + type: string + description: Unique id of the rundown + externalId: + type: string + description: Id normally sourced from the ingest system + name: + type: string + description: User-presentable name of the rundown + rank: + type: number + description: Rank for ordering + description: + type: string + description: Optional description + publicData: + description: Optional arbitrary data + segments: + description: Segments in this rundown + type: array + items: + $ref: '../../segment/resolvedSegment/resolvedSegment.yaml#/$defs/resolvedSegment' + required: [id, externalId, name, rank, segments] + additionalProperties: false + examples: + - $ref: './resolvedRundown-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/segment/resolvedSegment/resolvedSegment-example.yaml b/packages/live-status-gateway-api/api/components/segment/resolvedSegment/resolvedSegment-example.yaml new file mode 100644 index 00000000000..1856c65444b --- /dev/null +++ b/packages/live-status-gateway-api/api/components/segment/resolvedSegment/resolvedSegment-example.yaml @@ -0,0 +1,16 @@ +$ref: '../segmentBase/segmentBase-example.yaml' +externalId: 'ext_seg_1' +identifier: 'A' +name: 'Headlines' +rank: 1 +isHidden: false +sourceLayers: + - $ref: '../../layers/sourceLayer/sourceLayer-example.yaml' +outputLayers: + - $ref: '../../layers/outputLayer/outputLayer-example.yaml' +publicData: + slug: 'HEAD' +timing: + $ref: '../../timing/resolvedSegmentTiming/resolvedSegmentTiming-example.yaml' +parts: + - $ref: '../../part/resolvedPart/resolvedPart-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/segment/resolvedSegment/resolvedSegment.yaml b/packages/live-status-gateway-api/api/components/segment/resolvedSegment/resolvedSegment.yaml new file mode 100644 index 00000000000..02415407503 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/segment/resolvedSegment/resolvedSegment.yaml @@ -0,0 +1,47 @@ +$defs: + resolvedSegment: + title: ResolvedSegment + description: A segment within a resolved rundown + allOf: + - $ref: '../segmentBase/segmentBase.yaml#/$defs/segmentBase' + - type: object + title: ResolvedSegment + description: A segment within a resolved rundown + properties: + externalId: + type: string + description: Id normally sourced from the ingest system + identifier: + type: string + description: User-facing identifier from the source system + name: + type: string + description: Name of the segment + rank: + type: number + description: Rank for ordering + isHidden: + type: boolean + description: Whether the segment is hidden + sourceLayers: + description: Source layers used in this segment + type: array + items: + $ref: '../../layers/sourceLayer/sourceLayer.yaml#/$defs/sourceLayer' + outputLayers: + description: Output layers used in this segment + type: array + items: + $ref: '../../layers/outputLayer/outputLayer.yaml#/$defs/outputLayer' + publicData: + description: Optional arbitrary data + timing: + $ref: '../../timing/resolvedSegmentTiming/resolvedSegmentTiming.yaml#/$defs/resolvedSegmentTiming' + parts: + description: Parts in this segment + type: array + items: + $ref: '../../part/resolvedPart/resolvedPart.yaml#/$defs/resolvedPart' + required: [externalId, identifier, name, rank, isHidden, sourceLayers, outputLayers, timing, parts] + examples: + - $ref: './resolvedSegment-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/subscriptions/subscriptionName.yaml b/packages/live-status-gateway-api/api/components/subscriptions/subscriptionName.yaml index f4e4e9369c1..fff3fa9f2a1 100644 --- a/packages/live-status-gateway-api/api/components/subscriptions/subscriptionName.yaml +++ b/packages/live-status-gateway-api/api/components/subscriptions/subscriptionName.yaml @@ -6,6 +6,7 @@ $defs: enum: - studio - activePlaylist + - resolvedPlaylist - activePieces - segments - adLibs diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml new file mode 100644 index 00000000000..aab940cecd5 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml @@ -0,0 +1,6 @@ +$defs: + tTimerIndex: + type: integer + title: TTimerIndex + description: Timer index (1-3). The playlist always has 3 T-timer slots. + enum: [1, 2, 3] diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-countdownRunning-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-countdownRunning-example.yaml new file mode 100644 index 00000000000..6b2dd4b69c2 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-countdownRunning-example.yaml @@ -0,0 +1,12 @@ +index: 1 +label: 'Segment Timer' +configured: true +mode: + $ref: './timerModeCountdown-example.yaml' +state: + $ref: './timerStateRunning-example.yaml' +projected: + paused: false + zeroTime: 1706371925000 + pauseTime: null +anchorPartId: 'part_break_1' diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-freeRunRunning-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-freeRunRunning-example.yaml new file mode 100644 index 00000000000..badefde0100 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-freeRunRunning-example.yaml @@ -0,0 +1,11 @@ +index: 2 +label: 'Show Timer' +configured: true +mode: + $ref: './timerModeFreeRun-example.yaml' +state: + paused: false + zeroTime: 1706371800000 + pauseTime: null +projected: null +anchorPartId: null diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-unconfigured-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-unconfigured-example.yaml new file mode 100644 index 00000000000..b9ebff63135 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus-unconfigured-example.yaml @@ -0,0 +1,7 @@ +index: 3 +label: '' +configured: false +mode: null +state: null +projected: null +anchorPartId: null diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus.yaml new file mode 100644 index 00000000000..6ce3e8051eb --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus.yaml @@ -0,0 +1,3 @@ +$defs: + tTimerStatus: + $ref: './tTimerStatus/tTimerStatus.yaml#/$defs/tTimerStatus' diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-array-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-array-example.yaml new file mode 100644 index 00000000000..df04a390b0b --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-array-example.yaml @@ -0,0 +1,34 @@ +- index: 1 + label: 'Segment Timer' + configured: true + mode: + type: countdown + duration: 120000 + stopAtZero: true + state: + paused: false + zeroTime: 1706371920000 + pauseTime: 1706371900000 + projected: + paused: false + zeroTime: 1706371925000 + pauseTime: null + anchorPartId: 'part_break_1' +- index: 2 + label: 'Show Timer' + configured: true + mode: + type: freeRun + state: + paused: false + zeroTime: 1706371800000 + pauseTime: null + projected: null + anchorPartId: null +- index: 3 + label: '' + configured: false + mode: null + state: null + projected: null + anchorPartId: null diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-configured-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-configured-example.yaml new file mode 100644 index 00000000000..76690869e47 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-configured-example.yaml @@ -0,0 +1,16 @@ +index: 1 +label: 'Segment Timer' +configured: true +mode: + type: countdown + duration: 120000 + stopAtZero: true +state: + paused: false + zeroTime: 1706371920000 + pauseTime: 1706371900000 +projected: + paused: false + zeroTime: 1706371925000 + pauseTime: null +anchorPartId: 'part_break_1' diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-unconfigured-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-unconfigured-example.yaml new file mode 100644 index 00000000000..b9ebff63135 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-unconfigured-example.yaml @@ -0,0 +1,7 @@ +index: 3 +label: '' +configured: false +mode: null +state: null +projected: null +anchorPartId: null diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml new file mode 100644 index 00000000000..66ca55ae819 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml @@ -0,0 +1,48 @@ +$defs: + tTimerStatus: + type: object + title: TTimerStatus + description: Status of a single T-timer in the playlist + properties: + index: + $ref: '../tTimerIndex.yaml#/$defs/tTimerIndex' + label: + type: string + description: User-defined label for the timer + configured: + type: boolean + description: Whether the timer has been configured (mode and state are not null) + mode: + description: >- + Timer configuration/mode defining the timer's behavior. + Null if not configured. + oneOf: + - type: 'null' + - $ref: '../timerMode.yaml#/$defs/timerMode' + state: + description: >- + Current runtime state of the timer. + Null if not configured. + oneOf: + - type: 'null' + - $ref: '../timerState.yaml#/$defs/timerState' + projected: + description: >- + Projected timing for when we expect to reach an anchor part. + Used to calculate over/under diff. Has the same structure as state. + Running means progressing towards anchor, paused means pushing/delaying anchor. + oneOf: + - type: 'null' + - $ref: '../timerState.yaml#/$defs/timerState' + anchorPartId: + description: >- + The ID of the target Part that this timer is counting towards (the "timing anchor"). + Optional - null if no anchor is set. + oneOf: + - type: string + - type: 'null' + required: [index, label, configured, mode, state] + additionalProperties: false + examples: + - $ref: './tTimerStatus-configured-example.yaml' + - $ref: './tTimerStatus-unconfigured-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerMode.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerMode.yaml new file mode 100644 index 00000000000..617702e5509 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerMode.yaml @@ -0,0 +1,21 @@ +$defs: + timerModeCountdown: + $ref: './timerMode/countdown/timerModeCountdown.yaml#/$defs/timerModeCountdown' + + timerModeFreeRun: + $ref: './timerMode/freeRun/timerModeFreeRun.yaml#/$defs/timerModeFreeRun' + + timerModeTimeOfDay: + $ref: './timerMode/timeOfDay/timerModeTimeOfDay.yaml#/$defs/timerModeTimeOfDay' + + timerMode: + title: TimerMode + description: Configuration/mode of a T-timer (defines behavior type) + oneOf: + - $ref: '#/$defs/timerModeCountdown' + - $ref: '#/$defs/timerModeFreeRun' + - $ref: '#/$defs/timerModeTimeOfDay' + examples: + - $ref: './timerModeCountdown-example.yaml' + - $ref: './timerModeFreeRun-example.yaml' + - $ref: './timerModeTimeOfDay-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerMode/countdown/timerModeCountdown-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerMode/countdown/timerModeCountdown-example.yaml new file mode 100644 index 00000000000..c7111fab5d2 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerMode/countdown/timerModeCountdown-example.yaml @@ -0,0 +1,3 @@ +type: countdown +duration: 120000 +stopAtZero: true diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerMode/countdown/timerModeCountdown.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerMode/countdown/timerModeCountdown.yaml new file mode 100644 index 00000000000..776039d78fe --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerMode/countdown/timerModeCountdown.yaml @@ -0,0 +1,20 @@ +$defs: + timerModeCountdown: + type: object + title: TimerModeCountdown + description: Countdown timer mode - counts down from a duration to zero + properties: + type: + type: string + const: countdown + duration: + type: integer + minimum: 0 + description: The original countdown duration in milliseconds (used for reset) + stopAtZero: + type: boolean + description: Whether timer stops at zero or continues into negative values + required: [type, duration, stopAtZero] + additionalProperties: false + examples: + - $ref: './timerModeCountdown-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerMode/freeRun/timerModeFreeRun-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerMode/freeRun/timerModeFreeRun-example.yaml new file mode 100644 index 00000000000..5c4c5a508d9 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerMode/freeRun/timerModeFreeRun-example.yaml @@ -0,0 +1 @@ +type: freeRun diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerMode/freeRun/timerModeFreeRun.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerMode/freeRun/timerModeFreeRun.yaml new file mode 100644 index 00000000000..f8b31c12a01 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerMode/freeRun/timerModeFreeRun.yaml @@ -0,0 +1,13 @@ +$defs: + timerModeFreeRun: + type: object + title: TimerModeFreeRun + description: Free-running timer mode - counts up from start time + properties: + type: + type: string + const: freeRun + required: [type] + additionalProperties: false + examples: + - $ref: './timerModeFreeRun-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerMode/timeOfDay/timerModeTimeOfDay-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerMode/timeOfDay/timerModeTimeOfDay-example.yaml new file mode 100644 index 00000000000..815e88d1853 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerMode/timeOfDay/timerModeTimeOfDay-example.yaml @@ -0,0 +1,3 @@ +type: timeOfDay +targetRaw: '14:30' +stopAtZero: false diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerMode/timeOfDay/timerModeTimeOfDay.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerMode/timeOfDay/timerModeTimeOfDay.yaml new file mode 100644 index 00000000000..80269748e72 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerMode/timeOfDay/timerModeTimeOfDay.yaml @@ -0,0 +1,23 @@ +$defs: + timerModeTimeOfDay: + type: object + title: TimerModeTimeOfDay + description: Time-of-day timer mode - counts down/up to a specific time + properties: + type: + type: string + const: timeOfDay + targetRaw: + description: >- + The raw target string as provided when setting the timer + (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number) + oneOf: + - type: string + - type: number + stopAtZero: + type: boolean + description: Whether timer stops at zero or continues into negative values + required: [type, targetRaw, stopAtZero] + additionalProperties: false + examples: + - $ref: './timerModeTimeOfDay-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerModeCountdown-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerModeCountdown-example.yaml new file mode 100644 index 00000000000..c7111fab5d2 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerModeCountdown-example.yaml @@ -0,0 +1,3 @@ +type: countdown +duration: 120000 +stopAtZero: true diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerModeFreeRun-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerModeFreeRun-example.yaml new file mode 100644 index 00000000000..5c4c5a508d9 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerModeFreeRun-example.yaml @@ -0,0 +1 @@ +type: freeRun diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerModeTimeOfDay-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerModeTimeOfDay-example.yaml new file mode 100644 index 00000000000..28ae157dd38 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerModeTimeOfDay-example.yaml @@ -0,0 +1,3 @@ +type: timeOfDay +targetRaw: '14:30' +stopAtZero: true diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerState.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerState.yaml new file mode 100644 index 00000000000..c2667af7bae --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerState.yaml @@ -0,0 +1,19 @@ +$defs: + timerStateRunning: + $ref: './timerState/running/timerStateRunning.yaml#/$defs/timerStateRunning' + + timerStatePaused: + $ref: './timerState/paused/timerStatePaused.yaml#/$defs/timerStatePaused' + + timerState: + title: TimerState + description: >- + Runtime state of a timer, optimized for efficient client rendering. + When running, the client calculates current time from zeroTime. + When paused, the duration is frozen and sent directly. + oneOf: + - $ref: '#/$defs/timerStateRunning' + - $ref: '#/$defs/timerStatePaused' + examples: + - $ref: './timerStateRunning-example.yaml' + - $ref: './timerStatePaused-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerState/paused/timerStatePaused-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerState/paused/timerStatePaused-example.yaml new file mode 100644 index 00000000000..ff49a1e79f6 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerState/paused/timerStatePaused-example.yaml @@ -0,0 +1,3 @@ +paused: true +duration: 45000 +pauseTime: null diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerState/paused/timerStatePaused.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerState/paused/timerStatePaused.yaml new file mode 100644 index 00000000000..bccabf7e14e --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerState/paused/timerStatePaused.yaml @@ -0,0 +1,27 @@ +$defs: + timerStatePaused: + type: object + title: TimerStatePaused + description: Timer state when paused (frozen at a specific duration) + properties: + paused: + type: boolean + const: true + description: Whether the timer is paused + duration: + type: integer + description: >- + Frozen duration value in milliseconds. + For countdown timers, this is remaining time. + For free-run timers, this is elapsed time. + pauseTime: + description: >- + Optional timestamp when the timer should pause. + Typically null when already paused. + oneOf: + - type: integer + - type: 'null' + required: [paused, duration] + additionalProperties: false + examples: + - $ref: './timerStatePaused-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerState/running/timerStateRunning-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerState/running/timerStateRunning-example.yaml new file mode 100644 index 00000000000..b5e5104a25b --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerState/running/timerStateRunning-example.yaml @@ -0,0 +1,3 @@ +paused: false +zeroTime: 1706371920000 +pauseTime: 1706371900000 diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerState/running/timerStateRunning.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerState/running/timerStateRunning.yaml new file mode 100644 index 00000000000..0ed08effe6f --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerState/running/timerStateRunning.yaml @@ -0,0 +1,28 @@ +$defs: + timerStateRunning: + type: object + title: TimerStateRunning + description: Timer state when running (progressing with real time) + properties: + paused: + type: boolean + const: false + description: Whether the timer is paused + zeroTime: + type: number + description: >- + Unix timestamp (ms) when the timer reaches/reached zero. + For countdown timers, this is when time runs out. + For free-run timers, this is when the timer started. + Client calculates current value relative to this timestamp. + pauseTime: + description: >- + Optional timestamp when the timer should automatically pause + (e.g., when current part ends and overrun begins). + oneOf: + - type: number + - type: 'null' + required: [paused, zeroTime] + additionalProperties: false + examples: + - $ref: './timerStateRunning-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerStatePaused-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerStatePaused-example.yaml new file mode 100644 index 00000000000..d5b1538420d --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerStatePaused-example.yaml @@ -0,0 +1,3 @@ +paused: true +duration: 35000 +pauseTime: null diff --git a/packages/live-status-gateway-api/api/components/tTimers/timerStateRunning-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/timerStateRunning-example.yaml new file mode 100644 index 00000000000..b5e5104a25b --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/timerStateRunning-example.yaml @@ -0,0 +1,3 @@ +paused: false +zeroTime: 1706371920000 +pauseTime: 1706371900000 diff --git a/packages/live-status-gateway-api/api/components/timing/activePlaylistTiming/activePlaylistTimingMode.yaml b/packages/live-status-gateway-api/api/components/timing/activePlaylistTiming/activePlaylistTimingMode.yaml index 90520f52a07..73dbf67dcb0 100644 --- a/packages/live-status-gateway-api/api/components/timing/activePlaylistTiming/activePlaylistTimingMode.yaml +++ b/packages/live-status-gateway-api/api/components/timing/activePlaylistTiming/activePlaylistTimingMode.yaml @@ -5,3 +5,4 @@ enum: - none - forward-time - back-time + - duration diff --git a/packages/live-status-gateway-api/api/components/timing/resolvedPartTiming/resolvedPartTiming-example.yaml b/packages/live-status-gateway-api/api/components/timing/resolvedPartTiming/resolvedPartTiming-example.yaml new file mode 100644 index 00000000000..6943e03105b --- /dev/null +++ b/packages/live-status-gateway-api/api/components/timing/resolvedPartTiming/resolvedPartTiming-example.yaml @@ -0,0 +1,7 @@ +startMs: 0 +durationMs: 15000 +plannedStartedPlayback: 1706371805000 +reportedStartedPlayback: 1706371806000 +playOffsetMs: 1000 +setAsNext: 1706371804000 +take: 1706371806000 diff --git a/packages/live-status-gateway-api/api/components/timing/resolvedPartTiming/resolvedPartTiming.yaml b/packages/live-status-gateway-api/api/components/timing/resolvedPartTiming/resolvedPartTiming.yaml new file mode 100644 index 00000000000..cff47b343b8 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/timing/resolvedPartTiming/resolvedPartTiming.yaml @@ -0,0 +1,31 @@ +$defs: + resolvedPartTiming: + type: object + title: ResolvedPartTiming + description: Timing information about the part, relative to the start of the segment + properties: + startMs: + description: Start offset of the part (ms) from the beginning of the segment + type: number + durationMs: + description: Resolved duration of the part (ms) + type: number + plannedStartedPlayback: + description: Unix timestamp (ms) when the part was planned to start playback + type: number + reportedStartedPlayback: + description: Unix timestamp (ms) when the part was reported when it actually started playback + type: number + playOffsetMs: + description: Milliseconds into the part where playback started when taken (eg via “Play/Set Next from Here”; 0 = from the beginning) + type: number + setAsNext: + description: Unix timestamp (ms) when the part was set as next + type: number + take: + description: Unix timestamp (ms) when the part was taken + type: number + required: [startMs, durationMs] + additionalProperties: false + examples: + - $ref: './resolvedPartTiming-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/timing/resolvedPieceTiming/resolvedPieceTiming-example.yaml b/packages/live-status-gateway-api/api/components/timing/resolvedPieceTiming/resolvedPieceTiming-example.yaml new file mode 100644 index 00000000000..b63e390e108 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/timing/resolvedPieceTiming/resolvedPieceTiming-example.yaml @@ -0,0 +1,3 @@ +startMs: 6500 +durationMs: 5000 +prerollMs: 250 diff --git a/packages/live-status-gateway-api/api/components/timing/resolvedPieceTiming/resolvedPieceTiming.yaml b/packages/live-status-gateway-api/api/components/timing/resolvedPieceTiming/resolvedPieceTiming.yaml new file mode 100644 index 00000000000..2997d9443bc --- /dev/null +++ b/packages/live-status-gateway-api/api/components/timing/resolvedPieceTiming/resolvedPieceTiming.yaml @@ -0,0 +1,18 @@ +$defs: + resolvedPieceTiming: + type: object + title: ResolvedPieceTiming + description: Timing information about the piece, relative to the start of the part + properties: + startMs: + description: Start offset of the piece (ms) from the beginning of the part + type: number + durationMs: + description: Resolved duration of the piece (ms) + type: number + prerollMs: + description: Preroll duration for the piece (ms) + type: number + additionalProperties: false + examples: + - $ref: './resolvedPieceTiming-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/timing/resolvedPlaylistTiming/resolvedPlaylistTiming-example.yaml b/packages/live-status-gateway-api/api/components/timing/resolvedPlaylistTiming/resolvedPlaylistTiming-example.yaml new file mode 100644 index 00000000000..714e340bcf1 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/timing/resolvedPlaylistTiming/resolvedPlaylistTiming-example.yaml @@ -0,0 +1,4 @@ +type: forward +startedPlayback: 1706371800000 +expectedDurationMs: 1800000 +expectedEnd: 1706373600000 diff --git a/packages/live-status-gateway-api/api/components/timing/resolvedPlaylistTiming/resolvedPlaylistTiming.yaml b/packages/live-status-gateway-api/api/components/timing/resolvedPlaylistTiming/resolvedPlaylistTiming.yaml new file mode 100644 index 00000000000..04e7b6010db --- /dev/null +++ b/packages/live-status-gateway-api/api/components/timing/resolvedPlaylistTiming/resolvedPlaylistTiming.yaml @@ -0,0 +1,35 @@ +$defs: + resolvedPlaylistTimingType: + title: ResolvedPlaylistTimingType + type: string + description: Whether the playlist is forward-timed from a start time or back-timed from an end time + enum: + - forward + - back + + resolvedPlaylistTiming: + type: object + title: ResolvedPlaylistTiming + description: Timing information about the playlist using absolute Unix timestamps (ms) and expected duration. + properties: + type: + $ref: '#/$defs/resolvedPlaylistTimingType' + startedPlayback: + description: Unix timestamp (ms) when the playlist actually started playback (first timed part confirmed started). Null until known. + oneOf: + - type: number + - type: 'null' + expectedDurationMs: + description: Expected duration of the playlist (ms). Null when unknown/unavailable. + oneOf: + - type: number + - type: 'null' + expectedEnd: + description: Unix timestamp (ms) when the playlist is expected/projected to end. Null when it cannot be resolved from timing configuration. + oneOf: + - type: number + - type: 'null' + required: [type, startedPlayback, expectedDurationMs, expectedEnd] + additionalProperties: false + examples: + - $ref: './resolvedPlaylistTiming-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/timing/resolvedSegmentTiming/resolvedSegmentTiming-example.yaml b/packages/live-status-gateway-api/api/components/timing/resolvedSegmentTiming/resolvedSegmentTiming-example.yaml new file mode 100644 index 00000000000..440aeac95b1 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/timing/resolvedSegmentTiming/resolvedSegmentTiming-example.yaml @@ -0,0 +1,3 @@ +startMs: 0 +endMs: 120000 +budgetDurationMs: 120000 diff --git a/packages/live-status-gateway-api/api/components/timing/resolvedSegmentTiming/resolvedSegmentTiming.yaml b/packages/live-status-gateway-api/api/components/timing/resolvedSegmentTiming/resolvedSegmentTiming.yaml new file mode 100644 index 00000000000..e27eac9361e --- /dev/null +++ b/packages/live-status-gateway-api/api/components/timing/resolvedSegmentTiming/resolvedSegmentTiming.yaml @@ -0,0 +1,19 @@ +$defs: + resolvedSegmentTiming: + type: object + title: ResolvedSegmentTiming + description: Timing information about the segment, relative to the start of the segment + properties: + startMs: + description: Start offset of the segment (ms). For segments, this is typically 0. + type: number + endMs: + description: End offset of the segment (ms) from the beginning of the segment + type: number + budgetDurationMs: + description: Planned/budget duration for the segment (ms) + type: number + required: [startMs, endMs, budgetDurationMs] + additionalProperties: false + examples: + - $ref: './resolvedSegmentTiming-example.yaml' diff --git a/packages/live-status-gateway-api/api/topics/resolvedPlaylist/resolvedPlaylistTopic.yaml b/packages/live-status-gateway-api/api/topics/resolvedPlaylist/resolvedPlaylistTopic.yaml new file mode 100644 index 00000000000..143403fe04b --- /dev/null +++ b/packages/live-status-gateway-api/api/topics/resolvedPlaylist/resolvedPlaylistTopic.yaml @@ -0,0 +1,8 @@ +description: Topic for resolved playlist updates + +subscribe: + operationId: receiveResolvedPlaylist + description: Server sends resolved playlist events + message: + oneOf: + - $ref: '../../components/resolvedPlaylist/messages/resolvedPlaylistMessage.yaml#/components/messages/resolvedPlaylistMessage' diff --git a/packages/live-status-gateway-api/package.json b/packages/live-status-gateway-api/package.json index d77ed143443..328e44037c7 100644 --- a/packages/live-status-gateway-api/package.json +++ b/packages/live-status-gateway-api/package.json @@ -44,14 +44,15 @@ "tslib": "^2.8.1" }, "devDependencies": { - "@apidevtools/json-schema-ref-parser": "^15.2.2", + "@apidevtools/json-schema-ref-parser": "^15.3.5", "@asyncapi/generator": "^2.11.0", - "@asyncapi/html-template": "^3.5.4", + "@asyncapi/html-template": "^3.5.6", "@asyncapi/modelina": "^5.10.1", "@asyncapi/nodejs-ws-template": "^0.10.0", "@asyncapi/parser": "^3.6.0", - "yaml": "^2.8.2" + "prettier": "^3.8.3", + "yaml": "^2.8.3" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/packages/live-status-gateway-api/scripts/generate-schema-types.mjs b/packages/live-status-gateway-api/scripts/generate-schema-types.mjs index 517c3eb7789..e275e19acaa 100644 --- a/packages/live-status-gateway-api/scripts/generate-schema-types.mjs +++ b/packages/live-status-gateway-api/scripts/generate-schema-types.mjs @@ -1,8 +1,8 @@ import { TypeScriptGenerator } from '@asyncapi/modelina' import { fromFile, Parser } from '@asyncapi/parser' import fs from 'fs/promises' -import cp from 'child_process' import * as path from 'path' +import * as prettier from 'prettier' const BANNER = '/* eslint-disable */\n/**\n * This file was automatically generated using and @asyncapi/parser @asyncapi/modelina.\n * DO NOT MODIFY IT BY HAND. Instead, modify the source AsyncAPI schema files,\n * and run "yarn generate-schema-types" to regenerate this file.\n */\n' @@ -67,7 +67,7 @@ if (!asyncApiDoc.document) { const filteredDiagnostics = asyncApiDoc.diagnostics.filter((d) => d.code !== 'asyncapi-latest-version') console.error('No document was produced from the asyncapi parser') - console.error(JSON.stringify(filteredDiagnostics.diagnostics)) + console.error(JSON.stringify(filteredDiagnostics)) // eslint-disable-next-line n/no-process-exit process.exit(5) @@ -120,28 +120,10 @@ const allModelsString = '};' const fileName = path.resolve('src/generated/schema.ts') -await fs.writeFile(fileName, allModelsString) -// Prettier format the output file: -await runCmd(`npx prettier --write "${fileName}"`, { - // Run from repo root, so that prettier picks up the config - cwd: path.resolve('../..'), -}) +// Format with Prettier before writing: +const prettierConfig = await prettier.resolveConfig(fileName) +const formatted = await prettier.format(allModelsString, { ...prettierConfig, filepath: fileName }) +await fs.writeFile(fileName, formatted) console.log(`Schema types written to ${fileName}`) - -async function runCmd(cmd, options) { - await new Promise((resolve, reject) => { - const child = cp.exec(cmd, options || {}, (err, stdout, stderr) => { - if (err) { - console.error('stderr', stderr) - reject(err) - } else { - resolve(stdout) - } - }) - - child.stdout.pipe(process.stdout) - child.stderr.pipe(process.stderr) - }) -} diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index 71d5675007d..1632bfd455d 100644 --- a/packages/live-status-gateway-api/src/generated/asyncapi.yaml +++ b/packages/live-status-gateway-api/src/generated/asyncapi.yaml @@ -155,6 +155,7 @@ channels: enum: - studio - activePlaylist + - resolvedPlaylist - activePieces - segments - adLibs @@ -275,7 +276,7 @@ channels: playlists: description: The playlists that are currently loaded in the studio type: array - items: + items: &a31 title: PlaylistStatus type: object properties: @@ -301,7 +302,6 @@ channels: - externalId - name - activationStatus - additionalProperties: false examples: - &a11 id: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ @@ -401,7 +401,7 @@ channels: pieces: description: All pieces in this part type: array - items: &a30 + items: &a52 type: object title: PieceStatus properties: @@ -427,7 +427,7 @@ channels: abSessions: description: AB playback session assignments for this Piece type: array - items: + items: &a35 type: object title: AbSessionAssignment properties: @@ -464,7 +464,8 @@ channels: publicData: switcherSource: 1 abSessions: - - poolName: VTR + - &a37 + poolName: VTR sessionName: clip_intro playerId: 1 publicData: @@ -475,7 +476,7 @@ channels: - segmentId - pieces examples: - - &a26 + - &a27 segmentId: n1mOVd5_K5tt4sfk6HYfTuwumGQ_ pieces: &a15 - *a13 @@ -523,7 +524,7 @@ channels: required: - timing examples: - - &a24 + - &a25 timing: *a14 segmentId: n1mOVd5_K5tt4sfk6HYfTuwumGQ_ pieces: *a15 @@ -537,7 +538,7 @@ channels: - type: object title: CurrentSegment allOf: - - &a32 + - &a34 title: SegmentBase type: object properties: @@ -556,7 +557,7 @@ channels: title: CurrentSegmentTiming description: Timing information about the current segment allOf: - - &a33 + - &a54 type: object title: SegmentTiming properties: @@ -632,7 +633,7 @@ channels: - timing - parts examples: - - &a25 + - &a26 timing: *a19 parts: - *a20 @@ -660,6 +661,7 @@ channels: - none - forward-time - back-time + - duration startedPlayback: description: Unix timestamp of when the playlist started (milliseconds) type: number @@ -680,11 +682,11 @@ channels: - timingMode additionalProperties: false examples: - - &a27 + - &a28 timingMode: forward-time expectedStart: 1728895750727 expectedDurationMs: 180000 - quickLoop: + quickLoop: &a33 description: Information about the current quickLoop, if any type: object title: ActivePlaylistQuickLoop @@ -734,11 +736,235 @@ channels: - locked - running examples: - - &a28 + - &a29 locked: false running: true start: *a23 end: *a23 + tTimers: + description: Status of the 3 T-timers in the playlist + type: array + items: &a32 + type: object + title: TTimerStatus + description: Status of a single T-timer in the playlist + properties: + index: + type: integer + title: TTimerIndex + description: Timer index (1-3). The playlist always has 3 T-timer slots. + enum: + - 1 + - 2 + - 3 + label: + type: string + description: User-defined label for the timer + configured: + type: boolean + description: Whether the timer has been configured (mode and state are not null) + mode: + description: Timer configuration/mode defining the timer's behavior. Null if not + configured. + oneOf: + - type: "null" + - title: TimerMode + description: Configuration/mode of a T-timer (defines behavior type) + oneOf: + - type: object + title: TimerModeCountdown + description: Countdown timer mode - counts down from a duration to zero + properties: + type: + type: string + const: countdown + duration: + type: integer + minimum: 0 + description: The original countdown duration in milliseconds (used for reset) + stopAtZero: + type: boolean + description: Whether timer stops at zero or continues into negative values + required: + - type + - duration + - stopAtZero + additionalProperties: false + examples: + - type: countdown + duration: 120000 + stopAtZero: true + - type: object + title: TimerModeFreeRun + description: Free-running timer mode - counts up from start time + properties: + type: + type: string + const: freeRun + required: + - type + additionalProperties: false + examples: + - type: freeRun + - type: object + title: TimerModeTimeOfDay + description: Time-of-day timer mode - counts down/up to a specific time + properties: + type: + type: string + const: timeOfDay + targetRaw: + description: The raw target string as provided when setting the timer (e.g. + "14:30", "2023-12-31T23:59:59Z", or a + timestamp number) + oneOf: + - type: string + - type: number + stopAtZero: + type: boolean + description: Whether timer stops at zero or continues into negative values + required: + - type + - targetRaw + - stopAtZero + additionalProperties: false + examples: + - type: timeOfDay + targetRaw: 14:30 + stopAtZero: false + examples: + - &a47 + type: countdown + duration: 120000 + stopAtZero: true + - &a49 + type: freeRun + - type: timeOfDay + targetRaw: 14:30 + stopAtZero: true + state: + description: Current runtime state of the timer. Null if not configured. + oneOf: + - type: "null" + - &a24 + title: TimerState + description: Runtime state of a timer, optimized for efficient client rendering. + When running, the client calculates current time + from zeroTime. When paused, the duration is frozen + and sent directly. + oneOf: + - type: object + title: TimerStateRunning + description: Timer state when running (progressing with real time) + properties: + paused: + type: boolean + const: false + description: Whether the timer is paused + zeroTime: + type: number + description: Unix timestamp (ms) when the timer reaches/reached zero. For + countdown timers, this is when time runs + out. For free-run timers, this is when the + timer started. Client calculates current + value relative to this timestamp. + pauseTime: + description: Optional timestamp when the timer should automatically pause (e.g., + when current part ends and overrun + begins). + oneOf: + - type: number + - type: "null" + required: + - paused + - zeroTime + additionalProperties: false + examples: + - paused: false + zeroTime: 1706371920000 + pauseTime: 1706371900000 + - type: object + title: TimerStatePaused + description: Timer state when paused (frozen at a specific duration) + properties: + paused: + type: boolean + const: true + description: Whether the timer is paused + duration: + type: integer + description: Frozen duration value in milliseconds. For countdown timers, this + is remaining time. For free-run timers, + this is elapsed time. + pauseTime: + description: Optional timestamp when the timer should pause. Typically null when + already paused. + oneOf: + - type: integer + - type: "null" + required: + - paused + - duration + additionalProperties: false + examples: + - paused: true + duration: 45000 + pauseTime: null + examples: + - &a48 + paused: false + zeroTime: 1706371920000 + pauseTime: 1706371900000 + - paused: true + duration: 35000 + pauseTime: null + projected: + description: Projected timing for when we expect to reach an anchor part. Used + to calculate over/under diff. Has the same structure + as state. Running means progressing towards anchor, + paused means pushing/delaying anchor. + oneOf: + - type: "null" + - *a24 + anchorPartId: + description: The ID of the target Part that this timer is counting towards (the + "timing anchor"). Optional - null if no anchor is set. + oneOf: + - type: string + - type: "null" + required: + - index + - label + - configured + - mode + - state + additionalProperties: false + examples: + - index: 1 + label: Segment Timer + configured: true + mode: + type: countdown + duration: 120000 + stopAtZero: true + state: + paused: false + zeroTime: 1706371920000 + pauseTime: 1706371900000 + projected: + paused: false + zeroTime: 1706371925000 + pauseTime: null + anchorPartId: part_break_1 + - index: 3 + label: "" + configured: false + mode: null + state: null + projected: null + anchorPartId: null + minItems: 3 + maxItems: 3 required: - event - id @@ -749,24 +975,654 @@ channels: - currentSegment - nextPart - timing + - tTimers additionalProperties: false examples: - - &a29 + - &a30 event: activePlaylist id: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ externalId: 1ZIYVYL1aEkNEJbeGsmRXr5s8wtkyxfPRjNSTxZfcoEI name: Playlist 0 rundownIds: - y9HauyWkcxQS3XaAOsW40BRLLsI_ - currentPart: *a24 - currentSegment: *a25 - nextPart: *a26 + currentPart: *a25 + currentSegment: *a26 + nextPart: *a27 + publicData: + category: Evening News + timing: *a28 + quickLoop: *a29 + tTimers: + - index: 1 + label: Segment Timer + configured: true + mode: + type: countdown + duration: 120000 + stopAtZero: true + state: + paused: false + zeroTime: 1706371920000 + pauseTime: 1706371900000 + projected: + paused: false + zeroTime: 1706371925000 + pauseTime: null + anchorPartId: part_break_1 + - index: 2 + label: Show Timer + configured: true + mode: + type: freeRun + state: + paused: false + zeroTime: 1706371800000 + pauseTime: null + projected: null + anchorPartId: null + - index: 3 + label: "" + configured: false + mode: null + state: null + projected: null + anchorPartId: null + examples: + - payload: *a30 + resolvedPlaylist: + description: Topic for resolved playlist updates + subscribe: + operationId: receiveResolvedPlaylist + description: Server sends resolved playlist events + message: + oneOf: + - name: resolvedPlaylist + messageId: resolvedPlaylistUpdate + description: Resolved Playlist status + payload: + title: ResolvedPlaylistEvent + description: Resolved playlist details, including the base playlist status. + allOf: + - *a31 + - type: object + title: ResolvedPlaylistEvent + description: Resolved playlist details, including rundown/segment/part/piece + structure. + properties: + event: + type: string + const: resolvedPlaylist + currentPartInstanceId: + description: Instance id of the current part, if any + oneOf: + - type: string + - type: "null" + nextPartInstanceId: + description: Instance id of the next part, if any + oneOf: + - type: string + - type: "null" + playoutState: + description: Blueprint-defined playout state, used to expose arbitrary + information about playout + publicData: + description: Optional arbitrary data + timing: + type: object + title: ResolvedPlaylistTiming + description: Timing information about the playlist using absolute Unix + timestamps (ms) and expected duration. + properties: + type: + title: ResolvedPlaylistTimingType + type: string + description: Whether the playlist is forward-timed from a start time or + back-timed from an end time + enum: + - forward + - back + startedPlayback: + description: Unix timestamp (ms) when the playlist actually started playback + (first timed part confirmed started). Null until + known. + oneOf: + - type: number + - type: "null" + expectedDurationMs: + description: Expected duration of the playlist (ms). Null when + unknown/unavailable. + oneOf: + - type: number + - type: "null" + expectedEnd: + description: Unix timestamp (ms) when the playlist is expected/projected to end. + Null when it cannot be resolved from timing + configuration. + oneOf: + - type: number + - type: "null" + required: + - type + - startedPlayback + - expectedDurationMs + - expectedEnd + additionalProperties: false + examples: + - &a46 + type: forward + startedPlayback: 1706371800000 + expectedDurationMs: 1800000 + expectedEnd: 1706373600000 + tTimers: + description: Status of the 3 T-timers in the playlist + type: array + items: *a32 + minItems: 3 + maxItems: 3 + quickLoop: + description: Information about the current quickLoop, if any + oneOf: + - type: "null" + - *a33 + rundowns: + description: The rundowns in the playlist, in rank order + type: array + items: + type: object + title: ResolvedRundown + description: A rundown within a resolved playlist + properties: + id: + type: string + description: Unique id of the rundown + externalId: + type: string + description: Id normally sourced from the ingest system + name: + type: string + description: User-presentable name of the rundown + rank: + type: number + description: Rank for ordering + description: + type: string + description: Optional description + publicData: + description: Optional arbitrary data + segments: + description: Segments in this rundown + type: array + items: + title: ResolvedSegment + description: A segment within a resolved rundown + allOf: + - *a34 + - type: object + title: ResolvedSegment + description: A segment within a resolved rundown + properties: + externalId: + type: string + description: Id normally sourced from the ingest system + identifier: + type: string + description: User-facing identifier from the source system + name: + type: string + description: Name of the segment + rank: + type: number + description: Rank for ordering + isHidden: + type: boolean + description: Whether the segment is hidden + sourceLayers: + description: Source layers used in this segment + type: array + items: + type: object + title: SourceLayer + description: Definition of a source layer used in a segment + properties: + id: + type: string + description: Unique id of the source layer + name: + type: string + description: User-presentable name of the source layer + abbreviation: + type: string + description: Abbreviation for display + isHidden: + type: boolean + description: Whether the source layer is hidden in the UI + type: + type: number + description: Source layer content type (numeric enum) + rank: + type: number + description: Rank for ordering + required: + - id + - name + - abbreviation + - isHidden + - type + - rank + additionalProperties: false + examples: + - &a41 + id: sl_camera + name: Camera + abbreviation: CAM + isHidden: false + type: 1 + rank: 0 + outputLayers: + description: Output layers used in this segment + type: array + items: + type: object + title: OutputLayer + description: Definition of an output layer used in a segment + properties: + id: + type: string + description: Unique id of the output layer + name: + type: string + description: User-presentable name of the output layer + isFlattened: + type: boolean + description: Whether the output layer is flattened + isPGM: + type: boolean + description: Whether PGM treatment should be in effect + sourceLayerIds: + description: The set of sourceLayer ids that feed this output layer + type: array + items: + type: string + required: + - id + - name + - isFlattened + - isPGM + - sourceLayerIds + additionalProperties: false + examples: + - &a42 + id: ol_pgm + name: PGM + isFlattened: false + isPGM: true + sourceLayerIds: + - sl_camera + publicData: + description: Optional arbitrary data + timing: + type: object + title: ResolvedSegmentTiming + description: Timing information about the segment, relative to the start of the + segment + properties: + startMs: + description: Start offset of the segment (ms). For segments, this is typically + 0. + type: number + endMs: + description: End offset of the segment (ms) from the beginning of the segment + type: number + budgetDurationMs: + description: Planned/budget duration for the segment (ms) + type: number + required: + - startMs + - endMs + - budgetDurationMs + additionalProperties: false + examples: + - &a43 + startMs: 0 + endMs: 120000 + budgetDurationMs: 120000 + parts: + description: Parts in this segment + type: array + items: + title: ResolvedPart + description: A part within a resolved segment + allOf: + - *a17 + - type: object + title: ResolvedPart + description: A part within a resolved segment + properties: + instanceId: + type: string + description: Unique id of the part instance + externalId: + type: string + description: Id normally sourced from the ingest system + rank: + type: number + description: Rank for ordering + invalid: + type: boolean + description: Whether this part is invalid and should not be taken + floated: + type: boolean + description: Whether this part is floated and cannot be taken/nexted + untimed: + type: boolean + description: Whether this part is excluded from normal timing calculations + invalidReason: + description: Optional explanation for why the part is invalid + title: PartInvalidReason + type: object + properties: + message: + type: string + description: Human-readable message explaining why the part is invalid + severity: + description: Severity hint for displaying the invalid reason + type: string + title: NotificationSeverity + enum: &a69 + - warning + - error + - info + color: + description: Optional UI color hint + type: string + required: + - message + additionalProperties: false + examples: + - &a38 + message: Invalid clip reference + severity: warning + color: "#ff0000" + state: + description: Set only for the current or next part + title: ResolvedPartState + type: string + enum: + - current + - next + createdByAdLib: + type: boolean + description: Whether this part was created by an adlib + publicData: + description: Optional arbitrary data + timing: + type: object + title: ResolvedPartTiming + description: Timing information about the part, relative to the start of the + segment + properties: + startMs: + description: Start offset of the part (ms) from the beginning of the segment + type: number + durationMs: + description: Resolved duration of the part (ms) + type: number + plannedStartedPlayback: + description: Unix timestamp (ms) when the part was planned to start playback + type: number + reportedStartedPlayback: + description: Unix timestamp (ms) when the part was reported when it actually + started playback + type: number + playOffsetMs: + description: Milliseconds into the part where playback started when taken (eg + via “Play/Set Next from + Here”; 0 = from the + beginning) + type: number + setAsNext: + description: Unix timestamp (ms) when the part was set as next + type: number + take: + description: Unix timestamp (ms) when the part was taken + type: number + required: + - startMs + - durationMs + additionalProperties: false + examples: + - &a39 + startMs: 0 + durationMs: 15000 + plannedStartedPlayback: 1706371805000 + reportedStartedPlayback: 1706371806000 + playOffsetMs: 1000 + setAsNext: 1706371804000 + take: 1706371806000 + pieces: + description: Pieces in this part + type: array + items: + type: object + title: ResolvedPiece + description: A piece within a resolved part + properties: + id: + type: string + description: Unique id of the piece + instanceId: + type: string + description: Unique id of the piece instance + externalId: + type: string + description: Id normally sourced from the ingest system + name: + type: string + description: User-facing name of the piece + priority: + type: number + description: Priority of the piece + sourceLayerId: + type: string + description: Id of the source layer for this piece + outputLayerId: + type: string + description: Id of the output layer for this piece + createdByAdLib: + type: boolean + description: Whether this piece was created by an adlib + invalid: + type: boolean + description: Whether this piece is invalid and should be ignored + publicData: + description: Optional arbitrary data + timing: + type: object + title: ResolvedPieceTiming + description: Timing information about the piece, relative to the start of the + part + properties: + startMs: + description: Start offset of the piece (ms) from the beginning of the part + type: number + durationMs: + description: Resolved duration of the piece (ms) + type: number + prerollMs: + description: Preroll duration for the piece (ms) + type: number + additionalProperties: false + examples: + - &a36 + startMs: 6500 + durationMs: 5000 + prerollMs: 250 + tags: + description: Optional tags attached to this piece + type: array + items: + type: string + abSessions: + description: Optional AB playback session assignments for this piece + type: array + items: *a35 + required: + - id + - externalId + - name + - priority + - sourceLayerId + - outputLayerId + - createdByAdLib + - invalid + - timing + additionalProperties: false + examples: + - &a40 + id: piece_1 + instanceId: pieceInstance_1 + externalId: ext_piece_1 + name: Camera 1 + priority: 0 + sourceLayerId: sl_camera + outputLayerId: ol_pgm + createdByAdLib: false + invalid: false + publicData: + switcherSource: 1 + timing: *a36 + tags: + - camera + abSessions: + - *a37 + required: + - instanceId + - externalId + - rank + - createdByAdLib + - timing + - pieces + - invalid + - floated + - untimed + examples: + - &a44 + instanceId: partInstance_current_1 + externalId: ext_part_1 + rank: 10 + invalid: false + floated: false + untimed: false + invalidReason: *a38 + state: current + createdByAdLib: false + publicData: + partType: intro + timing: *a39 + pieces: + - *a40 + id: H5CBGYjThrMSmaYvRaa5FVKJIzk_ + name: Intro + autoNext: false + required: + - externalId + - identifier + - name + - rank + - isHidden + - sourceLayers + - outputLayers + - timing + - parts + examples: + - &a45 + externalId: ext_seg_1 + identifier: A + name: Headlines + rank: 1 + isHidden: false + sourceLayers: + - *a41 + outputLayers: + - *a42 + publicData: + slug: HEAD + timing: *a43 + parts: + - *a44 + id: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ + required: + - id + - externalId + - name + - rank + - segments + additionalProperties: false + examples: + - &a50 + id: rd_1 + externalId: ext_rd_1 + name: Rundown 1 + rank: 10 + description: "" + publicData: + ingestSource: NRK + segments: + - *a45 + required: + - event + - currentPartInstanceId + - nextPartInstanceId + - timing + - tTimers + - rundowns + examples: + - &a51 + event: resolvedPlaylist + currentPartInstanceId: partInstance_current_1 + nextPartInstanceId: partInstance_next_1 + playoutState: + mode: AUTO publicData: category: Evening News - timing: *a27 - quickLoop: *a28 + timing: *a46 + tTimers: + - index: 1 + label: Segment Timer + configured: true + mode: *a47 + state: *a48 + projected: + paused: false + zeroTime: 1706371925000 + pauseTime: null + anchorPartId: part_break_1 + - index: 2 + label: Show Timer + configured: true + mode: *a49 + state: + paused: false + zeroTime: 1706371800000 + pauseTime: null + projected: null + anchorPartId: null + - index: 3 + label: "" + configured: false + mode: null + state: null + projected: null + anchorPartId: null + quickLoop: *a29 + rundowns: + - *a50 + id: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ + externalId: 1ZIYVYL1aEkNEJbeGsmRXr5s8wtkyxfPRjNSTxZfcoEI + name: Playlist 0 + activationStatus: rehearsal examples: - - payload: *a29 + - payload: *a51 activePieces: description: Topic for active pieces updates subscribe: @@ -791,20 +1647,20 @@ channels: activePieces: description: Pieces that are currently active (on air) type: array - items: *a30 + items: *a52 required: - event - rundownPlaylistId - activePieces additionalProperties: false examples: - - &a31 + - &a53 event: activePieces rundownPlaylistId: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ activePieces: - *a13 examples: - - payload: *a31 + - payload: *a53 segments: description: Topic for Segment updates subscribe: @@ -833,7 +1689,7 @@ channels: type: object title: Segment allOf: - - *a32 + - *a34 - type: object title: Segment properties: @@ -847,7 +1703,7 @@ channels: name: description: Name of the segment type: string - timing: *a33 + timing: *a54 publicData: description: Optional arbitrary data required: @@ -860,7 +1716,7 @@ channels: - name - timing examples: - - &a34 + - &a55 identifier: Segment 0 identifier rundownId: y9HauyWkcxQS3XaAOsW40BRLLsI_ name: Segment 0 @@ -876,13 +1732,13 @@ channels: - rundownPlaylistId - segments examples: - - &a35 + - &a56 event: segments rundownPlaylistId: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ segments: - - *a34 + - *a55 examples: - - payload: *a35 + - payload: *a56 adLibs: description: Topic for AdLibs updates subscribe: @@ -912,7 +1768,7 @@ channels: items: title: AdLibStatus allOf: - - &a40 + - &a61 title: AdLibBase type: object properties: @@ -947,7 +1803,7 @@ channels: - label additionalProperties: false examples: - - &a36 + - &a57 name: pvw label: Preview tags: @@ -967,15 +1823,15 @@ channels: - sourceLayer - actionType examples: - - &a41 + - &a62 id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: Music video clip sourceLayer: Video Clip - actionType: &a37 - - *a36 - tags: &a38 + actionType: &a58 + - *a57 + tags: &a59 - music_video - publicData: &a39 + publicData: &a60 fileName: MV000123.mxf optionsSchema: '{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Play Video @@ -1007,15 +1863,15 @@ channels: - segmentId - partId examples: - - &a42 + - &a63 segmentId: HsD8_QwE1ZmR5vN3XcK_Ab7y partId: JkL3_OpR6WxT1bF8Vq2_Zy9u id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: Music video clip sourceLayer: Video Clip - actionType: *a37 - tags: *a38 - publicData: *a39 + actionType: *a58 + tags: *a59 + publicData: *a60 optionsSchema: '{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Play Video Clip","type":"object","properties":{"type":"adlib_action_video_clip","label":{"type":"string"},"clipId":{"type":"string"},"vo":{"type":"boolean"},"target":{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Object @@ -1039,9 +1895,9 @@ channels: items: title: GlobalAdLibStatus allOf: - - *a40 + - *a61 examples: - - *a41 + - *a62 required: - event - rundownPlaylistId @@ -1049,15 +1905,15 @@ channels: - globalAdLibs additionalProperties: false examples: - - &a43 + - &a64 event: adLibs rundownPlaylistId: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ adLibs: - - *a42 + - *a63 globalAdLibs: - - *a41 + - *a62 examples: - - payload: *a43 + - payload: *a64 packages: description: Packages topic for websocket subscriptions. Packages are assets that need to be prepared by Sofie Package Manager or third-party systems @@ -1165,7 +2021,7 @@ channels: - pieceOrAdLibId additionalProperties: false examples: - - &a44 + - &a65 packageName: MV000123.mxf status: ok rundownId: y9HauyWkcxQS3XaAOsW40BRLLsI_ @@ -1183,7 +2039,7 @@ channels: - event: packages rundownPlaylistId: y9HauyWkcxQS3XaAOsW40BRLLsI_ packages: - - *a44 + - *a65 buckets: description: Buckets schema for websocket subscriptions subscribe: @@ -1219,7 +2075,7 @@ channels: items: title: BucketAdLibStatus allOf: - - *a40 + - *a61 - type: object title: BucketAdLibStatus properties: @@ -1230,14 +2086,14 @@ channels: required: - externalId examples: - - &a45 + - &a66 externalId: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: Music video clip sourceLayer: Video Clip - actionType: *a37 - tags: *a38 - publicData: *a39 + actionType: *a58 + tags: *a59 + publicData: *a60 optionsSchema: '{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Play Video Clip","type":"object","properties":{"type":"adlib_action_video_clip","label":{"type":"string"},"clipId":{"type":"string"},"vo":{"type":"boolean"},"target":{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Object @@ -1261,22 +2117,22 @@ channels: - adLibs additionalProperties: false examples: - - &a46 + - &a67 id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: My Bucket adLibs: - - *a45 + - *a66 required: - event - buckets additionalProperties: false examples: - - &a47 + - &a68 event: buckets buckets: - - *a46 + - *a67 examples: - - payload: *a47 + - payload: *a68 notifications: description: Notifications topic for websocket subscriptions. subscribe: @@ -1317,10 +2173,7 @@ channels: type: string title: NotificationSeverity description: Severity level of the notification. - enum: - - warning - - error - - info + enum: *a69 message: type: string description: The message of the notification @@ -1340,7 +2193,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: &a48 + enum: &a70 - rundown - playlist - partInstance @@ -1352,7 +2205,7 @@ channels: type: string additionalProperties: false examples: - - &a49 + - &a71 type: rundown studioId: studio01 rundownId: rd123 @@ -1368,14 +2221,14 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a70 studioId: type: string playlistId: type: string additionalProperties: false examples: - - &a50 + - &a72 type: playlist studioId: studio01 playlistId: pl456 @@ -1392,7 +2245,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a70 studioId: type: string rundownId: @@ -1401,7 +2254,7 @@ channels: type: string additionalProperties: false examples: - - &a51 + - &a73 type: partInstance studioId: studio01 rundownId: rd123 @@ -1420,7 +2273,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a70 studioId: type: string rundownId: @@ -1431,7 +2284,7 @@ channels: type: string additionalProperties: false examples: - - &a52 + - &a74 type: pieceInstance studioId: studio01 rundownId: rd123 @@ -1447,17 +2300,17 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a70 additionalProperties: false examples: - - &a53 + - &a75 type: unknown examples: - - *a49 - - *a50 - - *a51 - - *a52 - - *a53 + - *a71 + - *a72 + - *a73 + - *a74 + - *a75 created: type: integer format: int64 @@ -1468,11 +2321,11 @@ channels: description: Unix timestamp of last modification additionalProperties: false examples: - - &a54 + - &a76 _id: notif123 severity: error message: disk.space.low - relatedTo: *a52 + relatedTo: *a74 created: 1694784932 modified: 1694784950 required: @@ -1480,9 +2333,9 @@ channels: - activeNotifications additionalProperties: false examples: - - &a55 + - &a77 event: notifications activeNotifications: - - *a54 + - *a76 examples: - - payload: *a55 + - payload: *a77 diff --git a/packages/live-status-gateway-api/src/generated/schema.ts b/packages/live-status-gateway-api/src/generated/schema.ts index dca47bd84fd..a589641f698 100644 --- a/packages/live-status-gateway-api/src/generated/schema.ts +++ b/packages/live-status-gateway-api/src/generated/schema.ts @@ -60,6 +60,7 @@ interface SubscriptionRequestDetails { enum SubscriptionName { STUDIO = 'studio', ACTIVE_PLAYLIST = 'activePlaylist', + RESOLVED_PLAYLIST = 'resolvedPlaylist', ACTIVE_PIECES = 'activePieces', SEGMENTS = 'segments', AD_LIBS = 'adLibs', @@ -142,6 +143,7 @@ interface PlaylistStatus { * Whether this playlist is currently active or in rehearsal */ activationStatus: PlaylistActivationStatus + additionalProperties?: Record } /** @@ -190,6 +192,10 @@ interface ActivePlaylistEvent { * Information about the current quickLoop, if any */ quickLoop?: ActivePlaylistQuickLoop + /** + * Status of the 3 T-timers in the playlist + */ + tTimers: TTimerStatus[] } interface CurrentPartStatus { @@ -419,6 +425,7 @@ enum ActivePlaylistTimingMode { NONE = 'none', FORWARD_MINUS_TIME = 'forward-time', BACK_MINUS_TIME = 'back-time', + DURATION = 'duration', } /** @@ -477,6 +484,579 @@ enum QuickLoopMarkerType { PART = 'part', } +/** + * Status of a single T-timer in the playlist + */ +interface TTimerStatus { + /** + * Timer index (1-3). The playlist always has 3 T-timer slots. + */ + index: TTimerIndex + /** + * User-defined label for the timer + */ + label: string + /** + * Whether the timer has been configured (mode and state are not null) + */ + configured: boolean + /** + * Timer configuration/mode defining the timer's behavior. Null if not configured. + */ + mode: TimerModeCountdown | TimerModeFreeRun | TimerModeTimeOfDay | null + /** + * Current runtime state of the timer. Null if not configured. + */ + state: TimerStateRunning | TimerStatePaused | null + /** + * Projected timing for when we expect to reach an anchor part. Used to calculate over/under diff. Has the same structure as state. Running means progressing towards anchor, paused means pushing/delaying anchor. + */ + projected?: TimerStateRunning | TimerStatePaused | null + /** + * The ID of the target Part that this timer is counting towards (the "timing anchor"). Optional - null if no anchor is set. + */ + anchorPartId?: string | null +} + +/** + * Timer index (1-3). The playlist always has 3 T-timer slots. + */ +enum TTimerIndex { + NUMBER_1 = 1, + NUMBER_2 = 2, + NUMBER_3 = 3, +} + +/** + * Countdown timer mode - counts down from a duration to zero + */ +interface TimerModeCountdown { + type: 'countdown' + /** + * The original countdown duration in milliseconds (used for reset) + */ + duration: number + /** + * Whether timer stops at zero or continues into negative values + */ + stopAtZero: boolean +} + +/** + * Free-running timer mode - counts up from start time + */ +interface TimerModeFreeRun { + type: 'freeRun' +} + +/** + * Time-of-day timer mode - counts down/up to a specific time + */ +interface TimerModeTimeOfDay { + type: 'timeOfDay' + /** + * The raw target string as provided when setting the timer (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number) + */ + targetRaw: string | number + /** + * Whether timer stops at zero or continues into negative values + */ + stopAtZero: boolean +} + +/** + * Timer state when running (progressing with real time) + */ +interface TimerStateRunning { + /** + * Whether the timer is paused + */ + paused: boolean + /** + * Unix timestamp (ms) when the timer reaches/reached zero. For countdown timers, this is when time runs out. For free-run timers, this is when the timer started. Client calculates current value relative to this timestamp. + */ + zeroTime: number + /** + * Optional timestamp when the timer should automatically pause (e.g., when current part ends and overrun begins). + */ + pauseTime?: number | null +} + +/** + * Timer state when paused (frozen at a specific duration) + */ +interface TimerStatePaused { + /** + * Whether the timer is paused + */ + paused: boolean + /** + * Frozen duration value in milliseconds. For countdown timers, this is remaining time. For free-run timers, this is elapsed time. + */ + duration: number + /** + * Optional timestamp when the timer should pause. Typically null when already paused. + */ + pauseTime?: number | null +} + +/** + * Resolved playlist details, including the base playlist status. + */ +interface ResolvedPlaylistEvent { + /** + * Unique id of the playlist + */ + id: string + /** + * Id normally sourced from the ingest system + */ + externalId: string + /** + * The user defined playlist name + */ + name: string + /** + * Whether this playlist is currently active or in rehearsal + */ + activationStatus: PlaylistActivationStatus + event: 'resolvedPlaylist' + /** + * Instance id of the current part, if any + */ + currentPartInstanceId: string | null + /** + * Instance id of the next part, if any + */ + nextPartInstanceId: string | null + /** + * Blueprint-defined playout state, used to expose arbitrary information about playout + */ + playoutState?: any + /** + * Optional arbitrary data + */ + publicData?: any + /** + * Timing information about the playlist using absolute Unix timestamps (ms) and expected duration. + */ + timing: ResolvedPlaylistTiming + /** + * Status of the 3 T-timers in the playlist + */ + tTimers: TTimerStatus[] + /** + * Information about the current quickLoop, if any + */ + quickLoop?: ActivePlaylistQuickLoop | null + /** + * The rundowns in the playlist, in rank order + */ + rundowns: ResolvedRundown[] + additionalProperties?: Record +} + +/** + * Timing information about the playlist using absolute Unix timestamps (ms) and expected duration. + */ +interface ResolvedPlaylistTiming { + /** + * Whether the playlist is forward-timed from a start time or back-timed from an end time + */ + type: ResolvedPlaylistTimingType + /** + * Unix timestamp (ms) when the playlist actually started playback (first timed part confirmed started). Null until known. + */ + startedPlayback: number | null + /** + * Expected duration of the playlist (ms). Null when unknown/unavailable. + */ + expectedDurationMs: number | null + /** + * Unix timestamp (ms) when the playlist is expected/projected to end. Null when it cannot be resolved from timing configuration. + */ + expectedEnd: number | null +} + +/** + * Whether the playlist is forward-timed from a start time or back-timed from an end time + */ +enum ResolvedPlaylistTimingType { + FORWARD = 'forward', + BACK = 'back', +} + +/** + * A rundown within a resolved playlist + */ +interface ResolvedRundown { + /** + * Unique id of the rundown + */ + id: string + /** + * Id normally sourced from the ingest system + */ + externalId: string + /** + * User-presentable name of the rundown + */ + name: string + /** + * Rank for ordering + */ + rank: number + /** + * Optional description + */ + description?: string + /** + * Optional arbitrary data + */ + publicData?: any + /** + * Segments in this rundown + */ + segments: ResolvedSegment[] +} + +/** + * A segment within a resolved rundown + */ +interface ResolvedSegment { + /** + * Unique id of the segment + */ + id: string + /** + * Id normally sourced from the ingest system + */ + externalId: string + /** + * User-facing identifier from the source system + */ + identifier: string + /** + * Name of the segment + */ + name: string + /** + * Rank for ordering + */ + rank: number + /** + * Whether the segment is hidden + */ + isHidden: boolean + /** + * Source layers used in this segment + */ + sourceLayers: SourceLayer[] + /** + * Output layers used in this segment + */ + outputLayers: OutputLayer[] + /** + * Optional arbitrary data + */ + publicData?: any + /** + * Timing information about the segment, relative to the start of the segment + */ + timing: ResolvedSegmentTiming + /** + * Parts in this segment + */ + parts: ResolvedPart[] + additionalProperties?: Record +} + +/** + * Definition of a source layer used in a segment + */ +interface SourceLayer { + /** + * Unique id of the source layer + */ + id: string + /** + * User-presentable name of the source layer + */ + name: string + /** + * Abbreviation for display + */ + abbreviation: string + /** + * Whether the source layer is hidden in the UI + */ + isHidden: boolean + /** + * Source layer content type (numeric enum) + */ + type: number + /** + * Rank for ordering + */ + rank: number +} + +/** + * Definition of an output layer used in a segment + */ +interface OutputLayer { + /** + * Unique id of the output layer + */ + id: string + /** + * User-presentable name of the output layer + */ + name: string + /** + * Whether the output layer is flattened + */ + isFlattened: boolean + /** + * Whether PGM treatment should be in effect + */ + isPGM: boolean + /** + * The set of sourceLayer ids that feed this output layer + */ + sourceLayerIds: string[] +} + +/** + * Timing information about the segment, relative to the start of the segment + */ +interface ResolvedSegmentTiming { + /** + * Start offset of the segment (ms). For segments, this is typically 0. + */ + startMs: number + /** + * End offset of the segment (ms) from the beginning of the segment + */ + endMs: number + /** + * Planned/budget duration for the segment (ms) + */ + budgetDurationMs: number +} + +/** + * A part within a resolved segment + */ +interface ResolvedPart { + /** + * Unique id of the part + */ + id: string + /** + * User-presentable name of the part + */ + name: string + /** + * If this part will progress to the next automatically + */ + autoNext?: boolean + /** + * Unique id of the part instance + */ + instanceId: string + /** + * Id normally sourced from the ingest system + */ + externalId: string + /** + * Rank for ordering + */ + rank: number + /** + * Whether this part is invalid and should not be taken + */ + invalid: boolean + /** + * Whether this part is floated and cannot be taken/nexted + */ + floated: boolean + /** + * Whether this part is excluded from normal timing calculations + */ + untimed: boolean + /** + * Optional explanation for why the part is invalid + */ + invalidReason?: PartInvalidReason + /** + * Set only for the current or next part + */ + state?: ResolvedPartState + /** + * Whether this part was created by an adlib + */ + createdByAdLib: boolean + /** + * Optional arbitrary data + */ + publicData?: any + /** + * Timing information about the part, relative to the start of the segment + */ + timing: ResolvedPartTiming + /** + * Pieces in this part + */ + pieces: ResolvedPiece[] + additionalProperties?: Record +} + +/** + * Optional explanation for why the part is invalid + */ +interface PartInvalidReason { + /** + * Human-readable message explaining why the part is invalid + */ + message: string + /** + * Severity hint for displaying the invalid reason + */ + severity?: NotificationSeverity + /** + * Optional UI color hint + */ + color?: string +} + +/** + * Severity level of the notification. + */ +enum NotificationSeverity { + WARNING = 'warning', + ERROR = 'error', + INFO = 'info', +} + +/** + * Set only for the current or next part + */ +enum ResolvedPartState { + CURRENT = 'current', + NEXT = 'next', +} + +/** + * Timing information about the part, relative to the start of the segment + */ +interface ResolvedPartTiming { + /** + * Start offset of the part (ms) from the beginning of the segment + */ + startMs: number + /** + * Resolved duration of the part (ms) + */ + durationMs: number + /** + * Unix timestamp (ms) when the part was planned to start playback + */ + plannedStartedPlayback?: number + /** + * Unix timestamp (ms) when the part was reported when it actually started playback + */ + reportedStartedPlayback?: number + /** + * Milliseconds into the part where playback started when taken (eg via “Play/Set Next from Here”; 0 = from the beginning) + */ + playOffsetMs?: number + /** + * Unix timestamp (ms) when the part was set as next + */ + setAsNext?: number + /** + * Unix timestamp (ms) when the part was taken + */ + take?: number +} + +/** + * A piece within a resolved part + */ +interface ResolvedPiece { + /** + * Unique id of the piece + */ + id: string + /** + * Unique id of the piece instance + */ + instanceId?: string + /** + * Id normally sourced from the ingest system + */ + externalId: string + /** + * User-facing name of the piece + */ + name: string + /** + * Priority of the piece + */ + priority: number + /** + * Id of the source layer for this piece + */ + sourceLayerId: string + /** + * Id of the output layer for this piece + */ + outputLayerId: string + /** + * Whether this piece was created by an adlib + */ + createdByAdLib: boolean + /** + * Whether this piece is invalid and should be ignored + */ + invalid: boolean + /** + * Optional arbitrary data + */ + publicData?: any + /** + * Timing information about the piece, relative to the start of the part + */ + timing: ResolvedPieceTiming + /** + * Optional tags attached to this piece + */ + tags?: string[] + /** + * Optional AB playback session assignments for this piece + */ + abSessions?: AbSessionAssignment[] +} + +/** + * Timing information about the piece, relative to the start of the part + */ +interface ResolvedPieceTiming { + /** + * Start offset of the piece (ms) from the beginning of the part + */ + startMs?: number + /** + * Resolved duration of the piece (ms) + */ + durationMs?: number + /** + * Preroll duration for the piece (ms) + */ + prerollMs?: number +} + interface ActivePiecesEvent { event: 'activePieces' /** @@ -836,15 +1416,6 @@ interface NotificationObj { modified?: number } -/** - * Severity level of the notification. - */ -enum NotificationSeverity { - WARNING = 'warning', - ERROR = 'error', - INFO = 'info', -} - interface NotificationTargetRundown { /** * Possible NotificationTarget types @@ -911,6 +1482,7 @@ export type Slash = | NotificationsEvent | PackagesEvent | PongEvent + | ResolvedPlaylistEvent | SegmentsEvent | StudioEvent | SubscriptionStatusError @@ -948,6 +1520,28 @@ export { ActivePlaylistQuickLoop, QuickLoopMarker, QuickLoopMarkerType, + TTimerStatus, + TTimerIndex, + TimerModeCountdown, + TimerModeFreeRun, + TimerModeTimeOfDay, + TimerStateRunning, + TimerStatePaused, + ResolvedPlaylistEvent, + ResolvedPlaylistTiming, + ResolvedPlaylistTimingType, + ResolvedRundown, + ResolvedSegment, + SourceLayer, + OutputLayer, + ResolvedSegmentTiming, + ResolvedPart, + PartInvalidReason, + NotificationSeverity, + ResolvedPartState, + ResolvedPartTiming, + ResolvedPiece, + ResolvedPieceTiming, ActivePiecesEvent, SegmentsEvent, Segment, @@ -964,7 +1558,6 @@ export { BucketAdLibStatus, NotificationsEvent, NotificationObj, - NotificationSeverity, NotificationTargetRundown, NotificationTargetType, NotificationTargetRundownPlaylist, diff --git a/packages/live-status-gateway-api/tsconfig.build.json b/packages/live-status-gateway-api/tsconfig.build.json index f42f059c434..efd41857d6e 100644 --- a/packages/live-status-gateway-api/tsconfig.build.json +++ b/packages/live-status-gateway-api/tsconfig.build.json @@ -3,9 +3,11 @@ "include": ["src/**/*.ts"], "exclude": ["node_modules/**", "**/*spec.ts", "**/__tests__/*", "**/__mocks__/*"], "compilerOptions": { - "target": "es2019", + "target": "es2024", "outDir": "./dist", "rootDir": "./src", + "declaration": true, + "declarationMap": true, "baseUrl": "./", "paths": { "*": ["./node_modules/*"], @@ -13,6 +15,7 @@ }, "resolveJsonModule": true, "types": ["node"], - "composite": true + "composite": true, + "module": "node20" } } diff --git a/packages/live-status-gateway/jest.config.js b/packages/live-status-gateway/jest.config.js index 897900a3ef0..df782a5e254 100644 --- a/packages/live-status-gateway/jest.config.js +++ b/packages/live-status-gateway/jest.config.js @@ -9,12 +9,17 @@ module.exports = { diagnostics: { ignoreCodes: [ 151002, // hybrid module kind (Node16/18/Next) + 2823, // Import attributes not supported in CJS mode (ts-jest forces CJS, emits require() anyway) ], }, }, ], }, moduleNameMapper: { + // Jest is not happy with esm modules, we need to point it to the source files instead + '^@sofie-automation/shared-lib/dist/(.+)\\.js$': '/../shared-lib/src/$1', + '^@sofie-automation/shared-lib/dist/(.+)$': '/../shared-lib/src/$1', + '^@sofie-automation/server-core-integration$': '/../server-core-integration/src/index.ts', '(.+)\\.js$': '$1', }, testMatch: ['**/__tests__/**/*.(spec|test).(ts|js)'], diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 6b7b6ec140c..2ec7bd15ab8 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -55,9 +55,9 @@ "fast-clone": "^1.5.13", "influx": "^5.12.0", "tslib": "^2.8.1", - "underscore": "^1.13.7", + "underscore": "^1.13.8", "winston": "^3.19.0", - "ws": "^8.19.0" + "ws": "^8.20.0" }, "devDependencies": { "type-fest": "^4.41.0" diff --git a/packages/live-status-gateway/sample-client/index.html b/packages/live-status-gateway/sample-client/index.html index bcf2f8a3038..b1771636cc2 100644 --- a/packages/live-status-gateway/sample-client/index.html +++ b/packages/live-status-gateway/sample-client/index.html @@ -8,6 +8,10 @@
Time of day:
Segment remaining:
Part remaining:
+
+ T-Timers: +
+
Active Pieces:
diff --git a/packages/live-status-gateway/sample-client/script.js b/packages/live-status-gateway/sample-client/script.js index a31abd9471a..06c96ac2d91 100644 --- a/packages/live-status-gateway/sample-client/script.js +++ b/packages/live-status-gateway/sample-client/script.js @@ -70,6 +70,7 @@ const TIME_OF_DAY_SPAN_ID = 'time-of-day' const SEGMENT_DURATION_SPAN_CLASS = 'segment-duration' const SEGMENT_REMAINIG_SPAN_ID = 'segment-remaining' const PART_REMAINIG_SPAN_ID = 'part-remaining' +const T_TIMERS_DIV_ID = 't-timers' const ACTIVE_PIECES_SPAN_ID = 'active-pieces' const NEXT_PIECES_SPAN_ID = 'next-pieces' const SEGMENTS_DIV_ID = 'segments' @@ -86,6 +87,8 @@ function handleActivePlaylist(data) { '
  • ' + activePlaylist.nextPart.pieces.map((p) => `${p.name} [${p.tags || []}]`).join('
  • ') + '
    • ' + + handleTTimers(data.tTimers) } let activePieces = {} function handleActivePieces(data) { @@ -124,6 +127,7 @@ setInterval(() => { if (partEndTime) partRemainingEl.textContent = formatMillisecondsToTime(Math.ceil(partEndTime / 1000) * 1000 - now) updateClock() + updateTTimers(activePlaylist.tTimers) }, 100) function updateClock() { @@ -182,3 +186,119 @@ function formatMillisecondsToTime(milliseconds) { return `${isNegative ? '+' : ''}${formattedHours}:${formattedMinutes}:${formattedSeconds}` } + +function formatTimestampToTimeOfDay(timestamp) { + const date = new Date(timestamp) + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + return `${hours}:${minutes}:${seconds}` +} + +function handleTTimers(tTimers) { + const tTimersDiv = document.getElementById(T_TIMERS_DIV_ID) + if (!tTimersDiv || !tTimers) return + + const ul = document.createElement('ul') + + tTimers.forEach((timer) => { + const li = document.createElement('li') + li.id = `t-timer-${timer.index}` + li.textContent = `Timer ${timer.index}:` + + const detailUl = document.createElement('ul') + + if (timer.configured) { + // Type + const typeLi = document.createElement('li') + typeLi.textContent = `Type: "${timer.mode.type}"` + detailUl.appendChild(typeLi) + + // Label + const labelLi = document.createElement('li') + labelLi.textContent = `Label: ${timer.label ? JSON.stringify(timer.label) : '(no label)'}` + detailUl.appendChild(labelLi) + + // Value + const valueLi = document.createElement('li') + valueLi.appendChild(document.createTextNode('Value: ')) + const valueSpan = document.createElement('span') + valueSpan.id = `t-timer-value-${timer.index}` + valueLi.appendChild(valueSpan) + detailUl.appendChild(valueLi) + + // Projected (if available) + if (timer.projected && timer.anchorPartId) { + const projectedLi = document.createElement('li') + projectedLi.id = `t-timer-projected-${timer.index}` + detailUl.appendChild(projectedLi) + } + } else { + // Show "Not set" for unconfigured timers + const notSetLi = document.createElement('li') + notSetLi.textContent = 'Not set' + detailUl.appendChild(notSetLi) + } + + li.appendChild(detailUl) + ul.appendChild(li) + }) + + tTimersDiv.innerHTML = '' + tTimersDiv.appendChild(ul) +} + +function updateTTimers(tTimers) { + if (!tTimers) return + + const now = ENABLE_SYNCED_TICKS ? Math.floor(Date.now() / 1000) * 1000 : Date.now() + + tTimers.forEach((timer) => { + if (!timer.configured) return + + const valueSpan = document.getElementById(`t-timer-value-${timer.index}`) + if (!valueSpan) return + + // Calculate current timer value + let currentTime + if (timer.state.paused) { + currentTime = timer.state.duration + valueSpan.textContent = formatMillisecondsToTime(currentTime) + ' (paused)' + } else if (timer.state.pauseTime && now >= timer.state.pauseTime) { + // Timer has reached its pauseTime - freeze at that moment + currentTime = timer.state.zeroTime - timer.state.pauseTime + valueSpan.textContent = + formatMillisecondsToTime(currentTime) + + ` (pauseTime: ${formatTimestampToTimeOfDay(timer.state.pauseTime)})` + } else { + currentTime = timer.state.zeroTime - now + valueSpan.textContent = + formatMillisecondsToTime(currentTime) + + ` (zeroTime: ${formatTimestampToTimeOfDay(timer.state.zeroTime)})` + } + + // Update projected time if available + const projectedLi = document.getElementById(`t-timer-projected-${timer.index}`) + if (projectedLi && timer.projected) { + let projectedTime + let projectedInfo = '' + if (timer.projected.paused) { + projectedTime = timer.projected.duration + projectedInfo = ' (paused)' + } else if (timer.projected.pauseTime && now >= timer.projected.pauseTime) { + // Projected timer has reached its pauseTime - freeze at that moment + projectedTime = timer.projected.zeroTime - timer.projected.pauseTime + projectedInfo = ` (pauseTime: ${formatTimestampToTimeOfDay(timer.projected.pauseTime)})` + } else { + projectedTime = timer.projected.zeroTime - now + projectedInfo = ` (zeroTime: ${formatTimestampToTimeOfDay(timer.projected.zeroTime)})` + } + + const diff = currentTime - projectedTime + const diffStr = formatMillisecondsToTime(Math.abs(diff)) + const status = diff > 0 ? 'under' : 'over' + + projectedLi.textContent = `Projected: ${formatMillisecondsToTime(projectedTime)}${projectedInfo} (${diffStr} ${status})` + } + }) +} diff --git a/packages/live-status-gateway/src/collections/notifications/playlistNotificationsHandler.ts b/packages/live-status-gateway/src/collections/notifications/playlistNotificationsHandler.ts index 5c8d407406c..d7ab1152d37 100644 --- a/packages/live-status-gateway/src/collections/notifications/playlistNotificationsHandler.ts +++ b/packages/live-status-gateway/src/collections/notifications/playlistNotificationsHandler.ts @@ -1,6 +1,6 @@ import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleToNextTick' import { PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' @@ -74,6 +74,7 @@ export class PlaylistNotificationsHandler extends PublicationCollection< this.setupSubscription(this._studioId, this._currentPlaylistId) } } else { + this.stopSubscription() this.clearAndNotify() } } diff --git a/packages/live-status-gateway/src/collections/notifications/rundownNotificationsHandler.ts b/packages/live-status-gateway/src/collections/notifications/rundownNotificationsHandler.ts index 6b253cf2ae8..221895ab1b7 100644 --- a/packages/live-status-gateway/src/collections/notifications/rundownNotificationsHandler.ts +++ b/packages/live-status-gateway/src/collections/notifications/rundownNotificationsHandler.ts @@ -9,7 +9,7 @@ import { CollectionHandlers } from '../../liveStatusServer.js' import { PublicationCollection } from '../../publicationCollection.js' import { DBNotificationObj, DBNotificationTargetRundown } from '@sofie-automation/corelib/dist/dataModel/Notifications' import { PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' const PLAYLIST_KEYS = ['_id', 'rundownIdsInOrder'] as const type Playlist = PickKeys diff --git a/packages/live-status-gateway/src/collections/partHandler.ts b/packages/live-status-gateway/src/collections/partHandler.ts index 0ad89705623..97ccaf80916 100644 --- a/packages/live-status-gateway/src/collections/partHandler.ts +++ b/packages/live-status-gateway/src/collections/partHandler.ts @@ -1,7 +1,7 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler.js' import { PublicationCollection } from '../publicationCollection.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { SelectedPartInstances } from './partInstancesHandler.js' diff --git a/packages/live-status-gateway/src/collections/partInstancesHandler.ts b/packages/live-status-gateway/src/collections/partInstancesHandler.ts index 0089bc13c21..ff65be6b088 100644 --- a/packages/live-status-gateway/src/collections/partInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/partInstancesHandler.ts @@ -1,7 +1,7 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler.js' import { PublicationCollection } from '../publicationCollection.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' @@ -145,6 +145,7 @@ export class PartInstancesHandler extends PublicationCollection< this.clearAndNotify() } } else { + this.stopSubscription() this.clearAndNotify() } } diff --git a/packages/live-status-gateway/src/collections/partInstancesInPlaylistHandler.ts b/packages/live-status-gateway/src/collections/partInstancesInPlaylistHandler.ts new file mode 100644 index 00000000000..1210eded0ec --- /dev/null +++ b/packages/live-status-gateway/src/collections/partInstancesInPlaylistHandler.ts @@ -0,0 +1,103 @@ +import { Logger } from 'winston' +import { CoreHandler } from '../coreHandler.js' +import { PublicationCollection } from '../publicationCollection.js' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { RundownId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { CollectionHandlers } from '../liveStatusServer.js' +import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' +import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleToNextTick' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' + +export interface PartInstancesInPlaylist { + all: DBPartInstance[] +} + +const PLAYLIST_KEYS = ['_id', 'activationId', 'rundownIdsInOrder'] as const +type Playlist = Pick + +/** + * Maintains part instances for the currently active playlist. + * Subscription is re-created when rundown set or activation id changes. + */ +export class PartInstancesInPlaylistHandler extends PublicationCollection< + PartInstancesInPlaylist, + CorelibPubSub.partInstances, + CollectionName.PartInstances +> { + private _currentPlaylist: Playlist | undefined + private _rundownIds: RundownId[] = [] + private _activationId: RundownPlaylistActivationId | undefined + + private _throttledUpdateAndNotify = throttleToNextTick(() => { + this.updateAndNotify() + }) + + constructor(logger: Logger, coreHandler: CoreHandler) { + super(CollectionName.PartInstances, CorelibPubSub.partInstances, logger, coreHandler) + this._collectionData = { + all: [], + } + } + + init(handlers: CollectionHandlers): void { + super.init(handlers) + handlers.playlistHandler.subscribe(this.onPlaylistUpdate, PLAYLIST_KEYS) + } + + protected changed(): void { + this._throttledUpdateAndNotify() + } + + private updateCollectionData(): boolean { + if (!this._collectionData) return false + const collection = this.getCollectionOrFail() + const allPartInstances = collection.find(undefined) + + const hasAnythingChanged = !areElementsShallowEqual(this._collectionData.all, allPartInstances) + if (hasAnythingChanged) this._collectionData.all = allPartInstances + + return hasAnythingChanged + } + + private clearCollectionData() { + if (!this._collectionData) return + this._collectionData.all = [] + } + + private onPlaylistUpdate = (data: Playlist | undefined): void => { + const prevRundownIds = [...this._rundownIds] + const prevActivationId = this._activationId + + this._currentPlaylist = data + this._rundownIds = this._currentPlaylist ? this._currentPlaylist.rundownIdsInOrder : [] + this._activationId = this._currentPlaylist?.activationId + + if (this._currentPlaylist && this._rundownIds.length && this._activationId) { + const sameSubscription = + areElementsShallowEqual(this._rundownIds, prevRundownIds) && prevActivationId === this._activationId + if (!sameSubscription) { + this.stopSubscription() + this.setupSubscription(this._rundownIds, this._activationId) + } else if (this._subscriptionId) { + this.updateAndNotify() + } else { + this.clearAndNotify() + } + } else { + this.stopSubscription() + this.clearAndNotify() + } + } + + private clearAndNotify() { + this.clearCollectionData() + this.notify(this._collectionData) + } + + private updateAndNotify() { + const hasAnythingChanged = this.updateCollectionData() + if (hasAnythingChanged) this.notify(this._collectionData) + } +} diff --git a/packages/live-status-gateway/src/collections/pieceContentStatusesHandler.ts b/packages/live-status-gateway/src/collections/pieceContentStatusesHandler.ts index 5fc0969ef68..414fd7af195 100644 --- a/packages/live-status-gateway/src/collections/pieceContentStatusesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceContentStatusesHandler.ts @@ -1,7 +1,7 @@ import { CustomCollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { UIPieceContentStatus } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleToNextTick' import { PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' @@ -58,6 +58,7 @@ export class PieceContentStatusesHandler extends PublicationCollection< this.setupSubscription(this._currentPlaylistId) } } else { + this.stopSubscription() this.clearAndNotify() } } diff --git a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts index ae584422742..a925b44bea3 100644 --- a/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts +++ b/packages/live-status-gateway/src/collections/pieceInstancesHandler.ts @@ -1,7 +1,7 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler.js' import { PublicationCollection } from '../publicationCollection.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' @@ -241,6 +241,7 @@ export class PieceInstancesHandler extends PublicationCollection< this.clearAndNotify() } } else { + this.stopSubscription() this.clearAndNotify() } } diff --git a/packages/live-status-gateway/src/collections/pieceInstancesInPlaylistHandler.ts b/packages/live-status-gateway/src/collections/pieceInstancesInPlaylistHandler.ts new file mode 100644 index 00000000000..d24a0716272 --- /dev/null +++ b/packages/live-status-gateway/src/collections/pieceInstancesInPlaylistHandler.ts @@ -0,0 +1,125 @@ +import { Logger } from 'winston' +import { CoreHandler } from '../coreHandler.js' +import { PublicationCollection } from '../publicationCollection.js' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import _ from 'underscore' +import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleToNextTick' +import { CollectionHandlers } from '../liveStatusServer.js' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { PartInstancesInPlaylistHandler } from './partInstancesInPlaylistHandler.js' +import { PartInstanceId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartInstancesInPlaylist } from './partInstancesInPlaylistHandler.js' + +/** Playlist fields needed to scope piece-instances to active playlist context. */ +const PLAYLIST_KEYS = ['rundownIdsInOrder', 'activationId'] as const +type Playlist = Pick + +/** + * Publishes piece instances for the active playlist. + * Scope is derived from both rundown ids and currently available part-instance ids. + */ +export class PieceInstancesInPlaylistHandler extends PublicationCollection< + PieceInstance[], + CorelibPubSub.pieceInstances, + CollectionName.PieceInstances +> { + private _currentRundownIds: Playlist['rundownIdsInOrder'] | undefined + private _currentActivationId: RundownPlaylistActivationId | undefined + private _partInstanceIds: PartInstanceId[] = [] + + private _throttledUpdateAndNotify = throttleToNextTick(() => { + this.updateAndNotify() + }) + + constructor(logger: Logger, coreHandler: CoreHandler) { + super(CollectionName.PieceInstances, CorelibPubSub.pieceInstances, logger, coreHandler) + this._collectionData = [] + } + + init(handlers: CollectionHandlers & { partInstancesInPlaylistHandler: PartInstancesInPlaylistHandler }): void { + super.init(handlers) + handlers.playlistHandler.subscribe(this.onPlaylistUpdate, PLAYLIST_KEYS) + handlers.partInstancesInPlaylistHandler.subscribe(this.onPartInstancesInPlaylistUpdate, ['all']) + } + + protected changed(): void { + this._throttledUpdateAndNotify() + } + + private onPlaylistUpdate = (playlist: Playlist | undefined): void => { + const prevRundownIds = this._currentRundownIds + const prevActivationId = this._currentActivationId + + const newActivationId = playlist?.activationId + if (newActivationId !== prevActivationId) { + // Ensure no stale piece instances from the previous activation leak through + this.stopSubscription() + this.clearAndNotify() + } + + this._currentRundownIds = playlist?.rundownIdsInOrder + this._currentActivationId = newActivationId + this.resubscribe(prevRundownIds, this._partInstanceIds, prevActivationId) + } + + private onPartInstancesInPlaylistUpdate = (data: PartInstancesInPlaylist | undefined): void => { + const prevPartInstanceIds = this._partInstanceIds + const prevActivationId = this._currentActivationId + this._partInstanceIds = data?.all?.flatMap((pi: any) => (pi._id ? [pi._id] : [])).sort() ?? [] + this.resubscribe(this._currentRundownIds, prevPartInstanceIds, prevActivationId) + } + + private resubscribe( + prevRundownIds: Playlist['rundownIdsInOrder'] | undefined, + prevPartInstanceIds: PartInstanceId[], + prevActivationId: RundownPlaylistActivationId | undefined + ): void { + // No rundown scope -> nothing should be published. + if (!this._currentRundownIds || this._currentRundownIds.length === 0) { + this.stopSubscription() + this.clearAndNotify() + return + } + // No active playlist context -> nothing should be published. + if (!this._currentActivationId) { + this.stopSubscription() + this.clearAndNotify() + return + } + // No active/derived part instances -> no matching piece instances can exist. + if (!this._partInstanceIds.length) { + this.stopSubscription() + this.clearAndNotify() + return + } + + const sameSubscription = + _.isEqual(prevRundownIds, this._currentRundownIds) && + _.isEqual(prevPartInstanceIds, this._partInstanceIds) && + prevActivationId === this._currentActivationId + + if (!sameSubscription) { + // Subscription arguments changed; recreate the server-side observer with new filters. + this.stopSubscription() + this.setupSubscription(this._currentRundownIds, this._partInstanceIds, {}) + } else if (this._subscriptionId) { + // Filter scope is unchanged and subscription is alive; just republish latest local snapshot. + this.updateAndNotify() + } else { + this.clearAndNotify() + } + } + + private updateAndNotify(): void { + const collection = this.getCollectionOrFail() + this._collectionData = collection.find(undefined) + this.notify(this._collectionData) + } + + private clearAndNotify() { + this._collectionData = [] + this.notify(this._collectionData) + } +} diff --git a/packages/live-status-gateway/src/collections/piecesInPlaylistHandler.ts b/packages/live-status-gateway/src/collections/piecesInPlaylistHandler.ts new file mode 100644 index 00000000000..ac3ea0ebef2 --- /dev/null +++ b/packages/live-status-gateway/src/collections/piecesInPlaylistHandler.ts @@ -0,0 +1,72 @@ +import { Logger } from 'winston' +import { CoreHandler } from '../coreHandler.js' +import { PublicationCollection } from '../publicationCollection.js' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import _ from 'underscore' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { CollectionHandlers } from '../liveStatusServer.js' +import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleToNextTick' + +const PLAYLIST_KEYS = ['rundownIdsInOrder'] as const +type Playlist = Pick + +/** Publishes all pieces belonging to rundowns in the currently selected playlist. */ +export class PiecesInPlaylistHandler extends PublicationCollection< + Piece[], + CorelibPubSub.pieces, + CollectionName.Pieces +> { + private _currentRundownIds: Playlist['rundownIdsInOrder'] | undefined + + private _throttledUpdateAndNotify = throttleToNextTick(() => { + this.updateAndNotify() + }) + + constructor(logger: Logger, coreHandler: CoreHandler) { + super(CollectionName.Pieces, CorelibPubSub.pieces, logger, coreHandler) + this._collectionData = [] + } + + init(handlers: CollectionHandlers): void { + super.init(handlers) + handlers.playlistHandler.subscribe(this.onPlaylistUpdate, PLAYLIST_KEYS) + } + + protected changed(): void { + this._throttledUpdateAndNotify() + } + + private updateAndNotify() { + const collection = this.getCollectionOrFail() + const pieces = collection.find(undefined) + this._collectionData = pieces + this.notify(this._collectionData) + } + + private onPlaylistUpdate = (playlist: Playlist | undefined): void => { + const rundownIds = playlist?.rundownIdsInOrder + const prevRundownIds = this._currentRundownIds + this._currentRundownIds = rundownIds + + if (rundownIds && rundownIds.length) { + const sameSubscription = _.isEqual(prevRundownIds, rundownIds) && this._subscriptionId + if (!sameSubscription) { + this.setupSubscription(rundownIds, null) + } else if (this._subscriptionId) { + this.updateAndNotify() + } else { + this.clearAndNotify() + } + } else { + this.stopSubscription() + this.clearAndNotify() + } + } + + private clearAndNotify() { + this._collectionData = [] + this.notify(this._collectionData) + } +} diff --git a/packages/live-status-gateway/src/collections/playlistHandler.ts b/packages/live-status-gateway/src/collections/playlistHandler.ts index b691caf27fb..131a6f5a7e0 100644 --- a/packages/live-status-gateway/src/collections/playlistHandler.ts +++ b/packages/live-status-gateway/src/collections/playlistHandler.ts @@ -2,7 +2,7 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler.js' import { PublicationCollection } from '../publicationCollection.js' import { CollectionBase } from '../collectionBase.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { CollectionHandlers } from '../liveStatusServer.js' diff --git a/packages/live-status-gateway/src/collections/rundownContentHandlerBase.ts b/packages/live-status-gateway/src/collections/rundownContentHandlerBase.ts index c354ecae1f0..fb4f443dfe0 100644 --- a/packages/live-status-gateway/src/collections/rundownContentHandlerBase.ts +++ b/packages/live-status-gateway/src/collections/rundownContentHandlerBase.ts @@ -3,11 +3,10 @@ import { CoreHandler } from '../coreHandler.js' import { PublicationCollection } from '../publicationCollection.js' import { CorelibPubSubCollections, CorelibPubSubTypes } from '@sofie-automation/corelib/dist/pubsub' import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { CollectionHandlers } from '../liveStatusServer.js' import { PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' -import { CollectionDocCheck } from '@sofie-automation/server-core-integration' -import { ParametersOfFunctionOrNever } from '@sofie-automation/server-core-integration/dist/lib/subscriptions' +import type { CollectionDocCheck, ParametersOfFunctionOrNever } from '@sofie-automation/server-core-integration' const PLAYLIST_KEYS = ['currentPartInfo', 'nextPartInfo'] as const type Playlist = PickKeys diff --git a/packages/live-status-gateway/src/collections/rundownHandler.ts b/packages/live-status-gateway/src/collections/rundownHandler.ts index 74f7decdc3c..5d9cdf9092b 100644 --- a/packages/live-status-gateway/src/collections/rundownHandler.ts +++ b/packages/live-status-gateway/src/collections/rundownHandler.ts @@ -1,7 +1,7 @@ import { Logger } from 'winston' import { CoreHandler } from '../coreHandler.js' import { PublicationCollection } from '../publicationCollection.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' diff --git a/packages/live-status-gateway/src/collections/segmentHandler.ts b/packages/live-status-gateway/src/collections/segmentHandler.ts index 53f1b6c9337..09b419d401b 100644 --- a/packages/live-status-gateway/src/collections/segmentHandler.ts +++ b/packages/live-status-gateway/src/collections/segmentHandler.ts @@ -7,7 +7,7 @@ import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/I import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' import { SegmentsHandler } from './segmentsHandler.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { CollectionHandlers } from '../liveStatusServer.js' import { PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' diff --git a/packages/live-status-gateway/src/collections/showStyleBasesHandler.ts b/packages/live-status-gateway/src/collections/showStyleBasesHandler.ts new file mode 100644 index 00000000000..20321dead53 --- /dev/null +++ b/packages/live-status-gateway/src/collections/showStyleBasesHandler.ts @@ -0,0 +1,91 @@ +import { Logger } from 'winston' +import { CoreHandler } from '../coreHandler.js' +import { PublicationCollection } from '../publicationCollection.js' +import { CollectionHandlers } from '../liveStatusServer.js' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBShowStyleBase, OutputLayers, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { IOutputLayer, ISourceLayer } from '@sofie-automation/blueprints-integration' +import { ShowStyleBaseExt } from './showStyleBaseHandler.js' +import _ from 'underscore' + +function buildShowStyleBaseExt(showStyleBase: DBShowStyleBase): ShowStyleBaseExt { + const sourceLayers: SourceLayers = applyAndValidateOverrides(showStyleBase.sourceLayersWithOverrides).obj + const outputLayers: OutputLayers = applyAndValidateOverrides(showStyleBase.outputLayersWithOverrides).obj + + const sourceLayerNamesById = new Map() + const outputLayerNamesById = new Map() + + for (const [layerId, sourceLayer] of Object.entries(sourceLayers)) { + if (sourceLayer === undefined || sourceLayer === null) continue + sourceLayerNamesById.set(layerId, sourceLayer.name) + } + for (const [layerId, outputLayer] of Object.entries(outputLayers)) { + if (outputLayer === undefined || outputLayer === null) continue + outputLayerNamesById.set(layerId, outputLayer.name) + } + + return { + ...showStyleBase, + sourceLayerNamesById, + outputLayerNamesById, + sourceLayers, + } as ShowStyleBaseExt +} + +/** + * Subscribes to all ShowStyleBases used by rundowns in the active playlist. + * This enables mixed-showstyle playlists to resolve each rundown/segment with the correct showstyle. + */ +export class ShowStyleBasesHandler extends PublicationCollection< + ShowStyleBaseExt[], + CorelibPubSub.showStyleBases, + CollectionName.ShowStyleBases +> { + private _showStyleBaseIds: ShowStyleBaseId[] = [] + + constructor(logger: Logger, coreHandler: CoreHandler) { + super(CollectionName.ShowStyleBases, CorelibPubSub.showStyleBases, logger, coreHandler) + } + + init(handlers: CollectionHandlers): void { + super.init(handlers) + handlers.rundownsHandler.subscribe(this.onRundownsUpdate) + } + + protected changed(): void { + this.updateAndNotify() + } + + private updateAndNotify(): void { + const collection = this.getCollectionOrFail() + const all = collection.find(undefined) as unknown as DBShowStyleBase[] + + const showStyles = all.map(buildShowStyleBaseExt) + + this._collectionData = showStyles + this.notify(this._collectionData) + } + + private onRundownsUpdate = (rundowns: DBRundown[] | undefined): void => { + const ids = Array.from( + new Set( + (rundowns ?? []).map((rundown) => rundown.showStyleBaseId).filter((id): id is ShowStyleBaseId => !!id) + ) + ).sort() + + if (_.isEqual(this._showStyleBaseIds, ids)) return + this._showStyleBaseIds = ids + + this.stopSubscription() + if (this._showStyleBaseIds.length > 0) { + this.setupSubscription(this._showStyleBaseIds) + } else { + this._collectionData = [] + this.notify(this._collectionData) + } + } +} diff --git a/packages/live-status-gateway/src/config.ts b/packages/live-status-gateway/src/config.ts index 95366edebda..ef71d6892b6 100644 --- a/packages/live-status-gateway/src/config.ts +++ b/packages/live-status-gateway/src/config.ts @@ -11,6 +11,7 @@ let deviceToken: string = process.env.DEVICE_TOKEN || '' let disableWatchdog: boolean = process.env.DISABLE_WATCHDOG === '1' || false let unsafeSSL: boolean = process.env.UNSAFE_SSL === '1' || false const certs: string[] = (process.env.CERTIFICATES || '').split(';') || [] +let healthPort: number | undefined = parseInt(process.env.HEALTH_PORT + '') || undefined let prevProcessArg = '' process.argv.forEach((val) => { @@ -37,12 +38,14 @@ process.argv.forEach((val) => { } else if (val.match(/-unsafeSSL/i)) { // Will cause the Node applocation to blindly accept all certificates. Not recommenced unless in local, controlled networks. unsafeSSL = true + } else if (prevProcessArg.match(/-healthPort/i)) { + healthPort = parseInt(val) } prevProcessArg = nextPrevProcessArg + '' }) const config: Config = { - process: { + certificates: { unsafeSSL: unsafeSSL, certificates: certs.filter((c) => c !== undefined && c !== null && c.length !== 0), }, @@ -55,6 +58,9 @@ const config: Config = { port: port, watchdog: !disableWatchdog, }, + health: { + port: healthPort, + }, } export { config, logPath, logLevel, disableWatchdog } diff --git a/packages/live-status-gateway/src/connector.ts b/packages/live-status-gateway/src/connector.ts index d38c3b25853..cfb694413a9 100644 --- a/packages/live-status-gateway/src/connector.ts +++ b/packages/live-status-gateway/src/connector.ts @@ -1,28 +1,33 @@ import { CoreHandler, CoreConfig } from './coreHandler.js' import { Logger } from 'winston' -import { Process } from './process.js' import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids' import { LiveStatusServer } from './liveStatusServer.js' +import { + CertificatesConfig, + HealthConfig, + HealthEndpoints, + IConnector, + loadDDPTLSOptions, + stringifyError, +} from '@sofie-automation/server-core-integration' export interface Config { - process: ProcessConfig + certificates: CertificatesConfig device: DeviceConfig core: CoreConfig + health: HealthConfig } -export interface ProcessConfig { - /** Will cause the Node applocation to blindly accept all certificates. Not recommenced unless in local, controlled networks. */ - unsafeSSL: boolean - /** Paths to certificates to load, for SSL-connections */ - certificates: string[] -} + export interface DeviceConfig { deviceId: PeripheralDeviceId deviceToken: string } -export class Connector { +export class Connector implements IConnector { + public initialized = false + public initializedError: string | undefined = undefined + private coreHandler: CoreHandler | undefined private _logger: Logger - private _process: Process | undefined private _liveStatusServer: LiveStatusServer | undefined constructor(logger: Logger) { @@ -32,13 +37,14 @@ export class Connector { public async init(config: Config): Promise { try { this._logger.info('Initializing Process...') - this._process = new Process(this._logger) - this._process.init(config.process) + const tlsOptions = loadDDPTLSOptions(this._logger, config.certificates) this._logger.info('Process initialized') this._logger.info('Initializing Core...') this.coreHandler = new CoreHandler(this._logger, config.device) - await this.coreHandler.init(config.core, this._process) + new HealthEndpoints(this, this.coreHandler, config.health) + + await this.coreHandler.init(config.core, tlsOptions) this._logger.info('Core initialized') if (!this.coreHandler.studioId) throw new Error('Device has no studioId') @@ -47,12 +53,15 @@ export class Connector { await this._liveStatusServer.init() this._logger.info('Initialization done') + this.initialized = true return } catch (e: any) { this._logger.error('Error during initialization:') this._logger.error(e) this._logger.error(e.stack) + this.initializedError = stringifyError(e) + try { if (this.coreHandler) { this.coreHandler.destroy().catch(this._logger.error) diff --git a/packages/live-status-gateway/src/coreHandler.ts b/packages/live-status-gateway/src/coreHandler.ts index a78966e33f7..eaecb663db3 100644 --- a/packages/live-status-gateway/src/coreHandler.ts +++ b/packages/live-status-gateway/src/coreHandler.ts @@ -3,6 +3,8 @@ import { CoreConnection, CoreOptions, DDPConnectorOptions, + DDPTLSOptions, + ICoreHandler, Observer, PeripheralDevicePubSub, PeripheralDevicePubSubCollections, @@ -10,14 +12,15 @@ import { PeripheralDevicePubSubTypes, SubscriptionId, stringifyError, + ParametersOfFunctionOrNever, } from '@sofie-automation/server-core-integration' import { DeviceConfig } from './connector.js' import { Logger } from 'winston' -import { Process } from './process.js' import { LIVE_STATUS_DEVICE_CONFIG } from './configManifest.js' import { PeripheralDeviceCategory, PeripheralDeviceType, + PeripheralDeviceStatusObject, } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { PeripheralDeviceCommandId, StudioId } from '@sofie-automation/shared-lib/dist/core/model/Ids' @@ -25,7 +28,6 @@ import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' import { PeripheralDeviceCommand } from '@sofie-automation/shared-lib/dist/core/model/PeripheralDeviceCommand' import { LiveStatusGatewayConfig } from '@sofie-automation/shared-lib/dist/generated/LiveStatusGatewayOptionsTypes' import { CorelibPubSubTypes, CorelibPubSubCollections } from '@sofie-automation/corelib/dist/pubsub' -import { ParametersOfFunctionOrNever } from '@sofie-automation/server-core-integration/dist/lib/subscriptions' export interface CoreConfig { host: string @@ -36,7 +38,7 @@ export interface CoreConfig { /** * Represents a connection between the Gateway and Core */ -export class CoreHandler { +export class CoreHandler implements ICoreHandler { core!: CoreConnection< CorelibPubSubTypes & PeripheralDevicePubSubTypes, CorelibPubSubCollections & PeripheralDevicePubSubCollections @@ -45,30 +47,27 @@ export class CoreHandler { public _observers: Array = [] public deviceSettings: LiveStatusGatewayConfig = {} - public errorReporting = false - public multithreading = false - public reportAllCommands = false - private _deviceOptions: DeviceConfig - private _onConnected?: () => any private _executedFunctions = new Set() private _coreConfig?: CoreConfig - private _process?: Process private _studioId: StudioId | undefined private _statusInitialized = false private _statusDestroyed = false + public get connectedToCore(): boolean { + return this.core && this.core.connected + } + constructor(logger: Logger, deviceOptions: DeviceConfig) { this.logger = logger this._deviceOptions = deviceOptions } - async init(config: CoreConfig, process: Process): Promise { + async init(config: CoreConfig, tlsOptions: DDPTLSOptions): Promise { this._statusInitialized = false this._coreConfig = config - this._process = process this.core = new CoreConnection( this.getCoreConnectionOptions() @@ -79,7 +78,6 @@ export class CoreHandler { this.setupObserversAndSubscriptions().catch((e) => { this.logger.error('Core Error during setupObserversAndSubscriptions:', e) }) - if (this._onConnected) this._onConnected() }) this.core.onDisconnected(() => { this.logger.warn('Core Disconnected!') @@ -91,11 +89,7 @@ export class CoreHandler { const ddpConfig: DDPConnectorOptions = { host: config.host, port: config.port, - } - if (this._process && this._process.certificates.length) { - ddpConfig.tlsOpts = { - ca: this._process.certificates, - } + tlsOpts: tlsOptions, } await this.core.init(ddpConfig) @@ -103,7 +97,6 @@ export class CoreHandler { this.logger.info('Core id: ' + this.core.deviceId) await this.setupObserversAndSubscriptions() - if (this._onConnected) this._onConnected() this._statusInitialized = true await this.updateCoreStatus() @@ -186,9 +179,6 @@ export class CoreHandler { return options } - onConnected(fcn: () => any): void { - this._onConnected = fcn - } onDeviceChanged(): void { const col = this.core.getCollection(PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice) @@ -325,23 +315,25 @@ export class CoreHandler { this.logger.info('getDevicesInfo') return [] } - async updateCoreStatus(): Promise { + getCoreStatus(): PeripheralDeviceStatusObject { let statusCode = StatusCode.GOOD - const messages: Array = [] + const statusDetails: Array<{ message: string }> = [] if (!this._statusInitialized) { statusCode = StatusCode.BAD - messages.push('Starting up...') + statusDetails.push({ message: 'Starting up...' }) } if (this._statusDestroyed) { statusCode = StatusCode.BAD - messages.push('Shut down') + statusDetails.push({ message: 'Shut down' }) } - - return this.core.setStatus({ - statusCode: statusCode, - messages: messages, - }) + return { + statusCode, + statusDetails, + } + } + async updateCoreStatus(): Promise { + return this.core.setStatus(this.getCoreStatus()) } private _getVersions() { const versions: { [packageName: string]: string } = {} diff --git a/packages/live-status-gateway/src/liveStatusServer.ts b/packages/live-status-gateway/src/liveStatusServer.ts index 00baa68feaa..2071cd6a1cc 100644 --- a/packages/live-status-gateway/src/liveStatusServer.ts +++ b/packages/live-status-gateway/src/liveStatusServer.ts @@ -3,9 +3,10 @@ import { CoreHandler } from './coreHandler.js' import { WebSocket, WebSocketServer } from 'ws' import { StudioHandler } from './collections/studioHandler.js' import { ShowStyleBaseHandler } from './collections/showStyleBaseHandler.js' +import { ShowStyleBasesHandler } from './collections/showStyleBasesHandler.js' import { PlaylistHandler, PlaylistsHandler } from './collections/playlistHandler.js' import { RundownHandler } from './collections/rundownHandler.js' -// import { RundownsHandler } from './collections/rundownsHandler.js' +import { RundownsHandler } from './collections/rundownsHandler.js' import { SegmentHandler } from './collections/segmentHandler.js' // import { PartHandler } from './collections/part.js' import { PartInstancesHandler } from './collections/partInstancesHandler.js' @@ -34,19 +35,29 @@ import { NotificationsHandler } from './collections/notifications/notificationsH import { NotificationsTopic } from './topics/notificationsTopic.js' import { PlaylistNotificationsHandler } from './collections/notifications/playlistNotificationsHandler.js' import { RundownNotificationsHandler } from './collections/notifications/rundownNotificationsHandler.js' +import { wsConnectionsGauge } from './wsMetrics.js' +import { ResolvedPlaylistTopic } from './topics/resolvedPlaylistTopic.js' +import { PartInstancesInPlaylistHandler } from './collections/partInstancesInPlaylistHandler.js' +import { PiecesInPlaylistHandler } from './collections/piecesInPlaylistHandler.js' +import { PieceInstancesInPlaylistHandler } from './collections/pieceInstancesInPlaylistHandler.js' export interface CollectionHandlers { studioHandler: StudioHandler showStyleBaseHandler: ShowStyleBaseHandler + showStyleBasesHandler: ShowStyleBasesHandler playlistHandler: PlaylistHandler playlistsHandler: PlaylistsHandler rundownHandler: RundownHandler + rundownsHandler: RundownsHandler segmentsHandler: SegmentsHandler segmentHandler: SegmentHandler partsHandler: PartsHandler partHandler: PartHandler partInstancesHandler: PartInstancesHandler + partInstancesInPlaylistHandler: PartInstancesInPlaylistHandler pieceInstancesHandler: PieceInstancesHandler + piecesInPlaylistHandler: PiecesInPlaylistHandler + pieceInstancesInPlaylistHandler: PieceInstancesInPlaylistHandler adLibActionsHandler: AdLibActionsHandler adLibsHandler: AdLibsHandler globalAdLibActionsHandler: GlobalAdLibActionsHandler @@ -77,15 +88,20 @@ export class LiveStatusServer { const studioHandler = new StudioHandler(this._logger, this._coreHandler) const showStyleBaseHandler = new ShowStyleBaseHandler(this._logger, this._coreHandler) + const showStyleBasesHandler = new ShowStyleBasesHandler(this._logger, this._coreHandler) const playlistHandler = new PlaylistHandler(this._logger, this._coreHandler) const playlistsHandler = playlistHandler.playlistsHandler - const rundownHandler = new RundownHandler(this._logger, this._coreHandler) + const rundownsHandler = new RundownsHandler(this._logger, this._coreHandler) + const rundownHandler = new RundownHandler(this._logger, this._coreHandler, rundownsHandler) const segmentsHandler = new SegmentsHandler(this._logger, this._coreHandler) const segmentHandler = new SegmentHandler(this._logger, this._coreHandler, segmentsHandler) const partsHandler = new PartsHandler(this._logger, this._coreHandler) const partHandler = new PartHandler(this._logger, this._coreHandler, partsHandler) const partInstancesHandler = new PartInstancesHandler(this._logger, this._coreHandler) + const partInstancesInPlaylistHandler = new PartInstancesInPlaylistHandler(this._logger, this._coreHandler) + const piecesInPlaylistHandler = new PiecesInPlaylistHandler(this._logger, this._coreHandler) const pieceInstancesHandler = new PieceInstancesHandler(this._logger, this._coreHandler) + const pieceInstancesInPlaylistHandler = new PieceInstancesInPlaylistHandler(this._logger, this._coreHandler) const adLibActionsHandler = new AdLibActionsHandler(this._logger, this._coreHandler) const adLibsHandler = new AdLibsHandler(this._logger, this._coreHandler) const globalAdLibActionsHandler = new GlobalAdLibActionsHandler(this._logger, this._coreHandler) @@ -101,15 +117,20 @@ export class LiveStatusServer { const handlers: CollectionHandlers = { studioHandler, showStyleBaseHandler, + showStyleBasesHandler, playlistHandler, playlistsHandler, rundownHandler, + rundownsHandler, segmentsHandler, segmentHandler, partsHandler, partHandler, partInstancesHandler, + partInstancesInPlaylistHandler, pieceInstancesHandler, + piecesInPlaylistHandler, + pieceInstancesInPlaylistHandler, adLibActionsHandler, adLibsHandler, globalAdLibActionsHandler, @@ -131,6 +152,7 @@ export class LiveStatusServer { const activePiecesTopic = new ActivePiecesTopic(this._logger, handlers) const activePlaylistTopic = new ActivePlaylistTopic(this._logger, handlers) const segmentsTopic = new SegmentsTopic(this._logger, handlers) + const resolvedPlaylistTopic = new ResolvedPlaylistTopic(this._logger, handlers) const adLibsTopic = new AdLibsTopic(this._logger, handlers) const notificationsTopic = new NotificationsTopic(this._logger, handlers) const packageStatusTopic = new PackagesTopic(this._logger, handlers) @@ -138,6 +160,7 @@ export class LiveStatusServer { rootChannel.addTopic(SubscriptionName.STUDIO, studioTopic) rootChannel.addTopic(SubscriptionName.ACTIVE_PLAYLIST, activePlaylistTopic) + rootChannel.addTopic(SubscriptionName.RESOLVED_PLAYLIST, resolvedPlaylistTopic) rootChannel.addTopic(SubscriptionName.ACTIVE_PIECES, activePiecesTopic) rootChannel.addTopic(SubscriptionName.SEGMENTS, segmentsTopic) rootChannel.addTopic(SubscriptionName.AD_LIBS, adLibsTopic) @@ -153,8 +176,10 @@ export class LiveStatusServer { this._logger.info(`Closing websocket`) rootChannel.removeSubscriber(ws) this._clients.delete(ws) + wsConnectionsGauge.set(this._clients.size) }) this._clients.add(ws) + wsConnectionsGauge.set(this._clients.size) if (typeof request.url === 'string' && request.url === '/') { rootChannel.addSubscriber(ws) diff --git a/packages/live-status-gateway/src/process.ts b/packages/live-status-gateway/src/process.ts deleted file mode 100644 index 58eaef7cdd1..00000000000 --- a/packages/live-status-gateway/src/process.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Logger } from 'winston' -import * as fs from 'fs' -import { ProcessConfig } from './connector.js' - -export class Process { - logger: Logger - - public certificates: Buffer[] = [] - - constructor(logger: Logger) { - this.logger = logger - } - init(processConfig: ProcessConfig): void { - if (processConfig.unsafeSSL) { - this.logger.info('Disabling NODE_TLS_REJECT_UNAUTHORIZED, be sure to ONLY DO THIS ON A LOCAL NETWORK!') - process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' - } - - if (processConfig.certificates.length) { - this.logger.info(`Loading certificates...`) - for (const certificate of processConfig.certificates) { - try { - this.certificates.push(fs.readFileSync(certificate)) - this.logger.info(`Using certificate "${certificate}"`) - } catch (error) { - this.logger.error(`Error loading certificate "${certificate}"`, error) - } - } - } - } -} diff --git a/packages/live-status-gateway/src/publicationCollection.ts b/packages/live-status-gateway/src/publicationCollection.ts index 8300a7fd33c..792f080f4a9 100644 --- a/packages/live-status-gateway/src/publicationCollection.ts +++ b/packages/live-status-gateway/src/publicationCollection.ts @@ -5,8 +5,8 @@ import { CollectionDocCheck, PeripheralDevicePubSubCollections, ProtectedString, + ParametersOfFunctionOrNever, } from '@sofie-automation/server-core-integration' -import { ParametersOfFunctionOrNever } from '@sofie-automation/server-core-integration/dist/lib/subscriptions' import { Logger } from 'winston' import { CollectionBase, DEFAULT_THROTTLE_PERIOD_MS } from './collectionBase.js' import { CoreHandler } from './coreHandler.js' diff --git a/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts index 12c5ccb7c3e..e482e95c57a 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePieces.spec.ts @@ -6,7 +6,7 @@ import { makeTestShowStyleBase, } from './utils.js' import { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler.js' -import { protectString } from '@sofie-automation/server-core-integration/dist' +import { protectString } from '@sofie-automation/server-core-integration' import { PartialDeep } from 'type-fest' import { literal } from '@sofie-automation/corelib/dist/lib' import { SelectedPieceInstances } from '../../collections/pieceInstancesHandler.js' diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 702ee867c6d..57855cef858 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -8,7 +8,7 @@ import { } from './utils.js' import { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler.js' import { SelectedPartInstances } from '../../collections/partInstancesHandler.js' -import { protectString, unprotectString, unprotectStringArray } from '@sofie-automation/server-core-integration/dist' +import { protectString, unprotectString, unprotectStringArray } from '@sofie-automation/server-core-integration' import { PartialDeep } from 'type-fest' import { literal } from '@sofie-automation/corelib/dist/lib' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' @@ -21,6 +21,12 @@ import { SegmentCountdownType, } from '@sofie-automation/live-status-gateway-api' +const DEFAULT_UNCONFIGURED_T_TIMERS: ActivePlaylistEvent['tTimers'] = [ + { index: 1, label: '', configured: false, mode: null, state: null, projected: null, anchorPartId: null }, + { index: 2, label: '', configured: false, mode: null, state: null, projected: null, anchorPartId: null }, + { index: 3, label: '', configured: false, mode: null, state: null, projected: null, anchorPartId: null }, +] + function makeEmptyTestPartInstances(): SelectedPartInstances { return { previous: undefined, @@ -63,6 +69,7 @@ describe('ActivePlaylistTopic', () => { timingMode: ActivePlaylistTimingMode.NONE, }, quickLoop: undefined, + tTimers: DEFAULT_UNCONFIGURED_T_TIMERS, } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -164,6 +171,7 @@ describe('ActivePlaylistTopic', () => { timingMode: ActivePlaylistTimingMode.NONE, }, quickLoop: undefined, + tTimers: DEFAULT_UNCONFIGURED_T_TIMERS, } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -270,6 +278,7 @@ describe('ActivePlaylistTopic', () => { timingMode: ActivePlaylistTimingMode.NONE, }, quickLoop: undefined, + tTimers: DEFAULT_UNCONFIGURED_T_TIMERS, } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -278,4 +287,54 @@ describe('ActivePlaylistTopic', () => { JSON.parse(JSON.stringify(expectedStatus)) ) }) + + it('maps a configured t-timer', async () => { + const handlers = makeMockHandlers() + const topic = new ActivePlaylistTopic(makeMockLogger(), handlers) + const mockSubscriber = makeMockSubscriber() + + const playlist = makeTestPlaylist() + playlist.activationId = protectString('somethingRandom') + playlist.tTimers = [ + { + index: 1, + label: 'Segment Timer', + mode: { type: 'countdown', duration: 120000, stopAtZero: true }, + state: { paused: false, zeroTime: 1706371920000, pauseTime: null }, + projectedState: undefined, + anchorPartId: undefined, + }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ] as any + handlers.playlistHandler.notify(playlist) + + const testShowStyleBase = makeTestShowStyleBase() + handlers.showStyleBaseHandler.notify(testShowStyleBase as ShowStyleBaseExt) + + const testPartInstancesMap = makeEmptyTestPartInstances() + handlers.partInstancesHandler.notify(testPartInstancesMap) + + topic.addSubscriber(mockSubscriber) + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockSubscriber.send).toHaveBeenCalledTimes(1) + const emitted = JSON.parse(mockSubscriber.send.mock.calls[0][0] as string) as ActivePlaylistEvent + expect(emitted.tTimers[0]).toMatchObject({ + index: 1, + label: 'Segment Timer', + configured: true, + mode: { type: 'countdown', duration: 120000, stopAtZero: true }, + state: { paused: false, zeroTime: 1706371920000, pauseTime: null }, + projected: null, + anchorPartId: null, + }) + expect(emitted.tTimers[1]).toMatchObject({ + index: 2, + label: '', + configured: false, + mode: null, + state: null, + }) + }) }) diff --git a/packages/live-status-gateway/src/topics/__tests__/packagesTopic.spec.ts b/packages/live-status-gateway/src/topics/__tests__/packagesTopic.spec.ts index 5045e74e7c0..6a7369fbd61 100644 --- a/packages/live-status-gateway/src/topics/__tests__/packagesTopic.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/packagesTopic.spec.ts @@ -3,7 +3,7 @@ import { makeMockHandlers, makeMockLogger, makeMockSubscriber } from './utils.js import { PackagesTopic } from '../packagesTopic.js' import { UIPieceContentStatus } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { PackagesEvent, PackageStatus } from '@sofie-automation/live-status-gateway-api' function makeTestUIPieceContentStatuses(): UIPieceContentStatus[] { diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 23b70507c10..32655366969 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -1,5 +1,5 @@ import { PlaylistTimingType } from '@sofie-automation/blueprints-integration/dist/documents/playlistTiming' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { mock, MockProxy } from 'jest-mock-extended' import { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler.js' @@ -77,6 +77,7 @@ export function makeMockHandlers(): CollectionHandlers { playlistHandler: makeMockHandler(), playlistsHandler: makeMockHandler(), rundownHandler: makeMockHandler(), + rundownsHandler: makeMockHandler(), segmentHandler: makeMockHandler(), segmentsHandler: makeMockHandler(), showStyleBaseHandler: makeMockHandler(), diff --git a/packages/live-status-gateway/src/topics/activePiecesTopic.ts b/packages/live-status-gateway/src/topics/activePiecesTopic.ts index c32890427d9..7515e9ee41d 100644 --- a/packages/live-status-gateway/src/topics/activePiecesTopic.ts +++ b/packages/live-status-gateway/src/topics/activePiecesTopic.ts @@ -8,7 +8,7 @@ import { SelectedPieceInstances, PieceInstanceMin } from '../collections/pieceIn import { toPieceStatus } from './helpers/pieceStatus.js' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CollectionHandlers } from '../liveStatusServer.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' import { ActivePiecesEvent } from '@sofie-automation/live-status-gateway-api' diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index aa4b0093292..a2c75962da7 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -5,7 +5,8 @@ import { DBRundownPlaylist, QuickLoopMarker, QuickLoopMarkerType, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { assertNever, literal } from '@sofie-automation/shared-lib/dist/lib/lib' import { SelectedPartInstances } from '../collections/partInstancesHandler.js' @@ -30,12 +31,23 @@ import { ActivePlaylistQuickLoop, QuickLoopMarker as QuickLoopMarkerStatus, QuickLoopMarkerType as QuickLoopMarkerStatusType, + TTimerStatus, + TTimerIndex, + TimerModeCountdown, + TimerModeFreeRun, + TimerModeTimeOfDay, + TimerStateRunning, + TimerStatePaused, } from '@sofie-automation/live-status-gateway-api' import { CollectionHandlers } from '../liveStatusServer.js' import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' import { Complete, PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' +// Union types for timer modes and states (not exported from API package) +type TimerMode = TimerModeCountdown | TimerModeFreeRun | TimerModeTimeOfDay +type TimerState = TimerStateRunning | TimerStatePaused + const THROTTLE_PERIOD_MS = 100 const PLAYLIST_KEYS = [ @@ -51,6 +63,7 @@ const PLAYLIST_KEYS = [ 'timing', 'startedPlayback', 'quickLoop', + 'tTimers', ] as const type Playlist = PickKeys @@ -168,10 +181,12 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket ? this._activePlaylist.timing.expectedStart : undefined, expectedEnd: - this._activePlaylist.timing.type !== PlaylistTimingType.None + this._activePlaylist.timing.type !== PlaylistTimingType.None && + this._activePlaylist.timing.type !== PlaylistTimingType.Duration ? this._activePlaylist.timing.expectedEnd : undefined, }, + tTimers: this.transformTTimers(this._activePlaylist.tTimers), }) : literal>({ event: 'activePlaylist', @@ -188,6 +203,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket timing: { timingMode: ActivePlaylistTimingMode.NONE, }, + tTimers: this.transformTTimers(null), }) this.sendMessage(subscribers, message) @@ -251,6 +267,55 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket } } + private transformTTimers(tTimers: RundownTTimer[] | null | undefined): [TTimerStatus, TTimerStatus, TTimerStatus] { + // Always return exactly 3 timers + if (!tTimers || tTimers.length === 0) { + return [ + { + index: 1 as TTimerIndex, + label: '', + configured: false, + mode: null, + state: null, + projected: null, + anchorPartId: null, + }, + { + index: 2 as TTimerIndex, + label: '', + configured: false, + mode: null, + state: null, + projected: null, + anchorPartId: null, + }, + { + index: 3 as TTimerIndex, + label: '', + configured: false, + mode: null, + state: null, + projected: null, + anchorPartId: null, + }, + ] + } + + return [this.transformTTimer(tTimers[0]), this.transformTTimer(tTimers[1]), this.transformTTimer(tTimers[2])] + } + + private transformTTimer({ index, label, mode, state, projectedState, anchorPartId }: RundownTTimer): TTimerStatus { + return { + index: index as TTimerIndex, + label, + configured: !!(mode && state), + mode: mode as TimerMode | null, + state: state as TimerState | null, + projected: projectedState ? (projectedState as TimerState) : null, + anchorPartId: anchorPartId ? unprotectString(anchorPartId) : null, + } + } + private isDataInconsistent() { return ( this._currentPartInstance?._id !== this._activePlaylist?.currentPartInfo?.partInstanceId || @@ -338,6 +403,8 @@ function translatePlaylistTimingType(type: PlaylistTimingType): ActivePlaylistTi return ActivePlaylistTimingMode.BACK_MINUS_TIME case PlaylistTimingType.ForwardTime: return ActivePlaylistTimingMode.FORWARD_MINUS_TIME + case PlaylistTimingType.Duration: + return ActivePlaylistTimingMode.DURATION default: assertNever(type) // Cast and return the value anyway, so that the application works diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index 9849f9184f7..9acca418663 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -1,6 +1,6 @@ import { Logger } from 'winston' import { WebSocket } from 'ws' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { WebSocketTopicBase, WebSocketTopic } from '../wsHandler.js' import { literal } from '@sofie-automation/corelib/dist/lib' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' diff --git a/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/quickLoop/toQuickLoopStatus.ts b/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/quickLoop/toQuickLoopStatus.ts new file mode 100644 index 00000000000..da33b90f1d8 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/quickLoop/toQuickLoopStatus.ts @@ -0,0 +1,71 @@ +import type { ActivePlaylistQuickLoop, QuickLoopMarker } from '@sofie-automation/live-status-gateway-api' +import { QuickLoopMarkerType } from '@sofie-automation/live-status-gateway-api' +import { QuickLoopMarkerType as CoreQuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import type { + QuickLoopMarker as CoreQuickLoopMarker, + QuickLoopProps, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import type { Logger } from 'winston' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' + +type ToQuickLoopStatusProps = { + quickLoop: QuickLoopProps | undefined + segmentsById: Record + partsById: Record + logger?: Logger +} + +function toMarker( + marker: CoreQuickLoopMarker | undefined, + segmentsById: ToQuickLoopStatusProps['segmentsById'], + partsById: ToQuickLoopStatusProps['partsById'], + logger?: Logger +): QuickLoopMarker | undefined { + if (!marker) return undefined + + switch (marker.type) { + case CoreQuickLoopMarkerType.PLAYLIST: + return { markerType: QuickLoopMarkerType.PLAYLIST } + case CoreQuickLoopMarkerType.RUNDOWN: + return { markerType: QuickLoopMarkerType.RUNDOWN, rundownId: unprotectString(marker.id) } + case CoreQuickLoopMarkerType.SEGMENT: { + const seg = segmentsById[unprotectString(marker.id)] + return { + markerType: QuickLoopMarkerType.SEGMENT, + rundownId: seg?.rundownId ? unprotectString(seg.rundownId as any) : undefined, + segmentId: unprotectString(marker.id), + } + } + case CoreQuickLoopMarkerType.PART: { + const part = partsById[unprotectString(marker.id)] + return { + markerType: QuickLoopMarkerType.PART, + rundownId: part?.rundownId ? unprotectString(part.rundownId as any) : undefined, + segmentId: part?.segmentId ? unprotectString(part.segmentId as any) : undefined, + partId: unprotectString(marker.id), + } + } + default: + // If corelib adds a new marker type, just omit it rather than crashing conversion + logger?.warn('Unknown QuickLoop markerType encountered; omitting marker', { + markerType: String((marker as any).type), + }) + return undefined + } +} + +export function toQuickLoopStatus({ + quickLoop, + segmentsById, + partsById, + logger, +}: ToQuickLoopStatusProps): ActivePlaylistQuickLoop | null { + if (!quickLoop) return null + + return { + locked: quickLoop.locked, + running: quickLoop.running, + start: toMarker(quickLoop.start, segmentsById, partsById, logger), + end: toMarker(quickLoop.end, segmentsById, partsById, logger), + } +} diff --git a/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timers/__tests__/toTTimers.spec.ts b/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timers/__tests__/toTTimers.spec.ts new file mode 100644 index 00000000000..6bb1e3919b7 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timers/__tests__/toTTimers.spec.ts @@ -0,0 +1,51 @@ +import { toTTimers } from '../toTTimers.js' + +type TimerInput = { + index: 1 | 2 | 3 + label: string + mode: any + state: any + projectedState?: any + anchorPartId?: any +} + +function makeTimer(index: 1 | 2 | 3, overrides: Partial = {}): TimerInput { + return { + index, + label: `Timer ${index}`, + mode: null, + state: null, + ...overrides, + } +} + +describe('toTTimers', () => { + it('returns 3 empty timers for nullish/empty input', () => { + expect(toTTimers(undefined)).toMatchObject([{ index: 1 }, { index: 2 }, { index: 3 }]) + expect(toTTimers(null)).toMatchObject([{ index: 1 }, { index: 2 }, { index: 3 }]) + expect(toTTimers([])).toMatchObject([{ index: 1 }, { index: 2 }, { index: 3 }]) + }) + + it('does not throw for length 1 or 2 and places by index', () => { + expect(() => toTTimers([makeTimer(2)] as any)).not.toThrow() + expect(toTTimers([makeTimer(2)] as any).map((t) => t.index)).toEqual([1, 2, 3]) + + expect(() => toTTimers([makeTimer(1), makeTimer(3)] as any)).not.toThrow() + expect(toTTimers([makeTimer(1), makeTimer(3)] as any).map((t) => t.label)).toEqual(['Timer 1', '', 'Timer 3']) + }) + + it('is index-aware (out-of-order input lands in correct slots)', () => { + const result = toTTimers([makeTimer(3), makeTimer(1)] as any) + expect(result.map((t) => t.label)).toEqual(['Timer 1', '', 'Timer 3']) + }) + + it('uses first timer when duplicate indices occur', () => { + const result = toTTimers([makeTimer(2, { label: 'First' }), makeTimer(2, { label: 'Second' })] as any) + expect(result[1].label).toBe('First') + }) + + it('ignores invalid indices', () => { + const result = toTTimers([{ ...makeTimer(1), index: 4 } as any, makeTimer(1)] as any) + expect(result.map((t) => t.label)).toEqual(['Timer 1', '', '']) + }) +}) diff --git a/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timers/toTTimers.ts b/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timers/toTTimers.ts new file mode 100644 index 00000000000..645e91c7672 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timers/toTTimers.ts @@ -0,0 +1,50 @@ +import type { TTimerStatus, TTimerIndex } from '@sofie-automation/live-status-gateway-api' +import { + type RundownTTimer, + isRundownTTimerIndex, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' + +function emptyTimer(index: number): TTimerStatus { + return { + index: index as TTimerIndex, + label: '', + configured: false, + mode: null, + state: null, + projected: null, + anchorPartId: null, + } +} + +function toTTimer(timer: RundownTTimer): TTimerStatus { + const { index, label, mode, state, projectedState, anchorPartId } = timer + return { + index: index as TTimerIndex, + label, + configured: !!(mode && state), + mode: mode as TTimerStatus['mode'], + state: state as TTimerStatus['state'], + projected: projectedState ? (projectedState as NonNullable) : null, + anchorPartId: anchorPartId ? unprotectString(anchorPartId) : null, + } +} + +export function toTTimers(tTimers: RundownTTimer[] | null | undefined): [TTimerStatus, TTimerStatus, TTimerStatus] { + // Always return exactly 3 timers + const slots: [TTimerStatus, TTimerStatus, TTimerStatus] = [emptyTimer(1), emptyTimer(2), emptyTimer(3)] + if (!tTimers || tTimers.length === 0) return slots + + const filled = new Set() + + for (const timer of tTimers) { + if (!isRundownTTimerIndex(timer.index)) continue + const slotIndex = timer.index - 1 + if (slotIndex < 0 || slotIndex > 2) continue + if (filled.has(slotIndex)) continue + slots[slotIndex] = toTTimer(timer) + filled.add(slotIndex) + } + + return slots +} diff --git a/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timing/toActivePlaylistTiming.ts b/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timing/toActivePlaylistTiming.ts new file mode 100644 index 00000000000..9c9faa0523b --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timing/toActivePlaylistTiming.ts @@ -0,0 +1,42 @@ +import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' +import { PlaylistTimingType } from '@sofie-automation/blueprints-integration' +import type { ActivePlaylistEvent } from '@sofie-automation/live-status-gateway-api' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' + +import { translatePlaylistTimingType } from './translatePlaylistTimingType.js' + +/** + * Properties required to convert playlist timing into API timing. + * + * @typedef {object} ToActivePlaylistTimingProps + */ +type ToActivePlaylistTimingProps = { + startedPlaybackState: DBRundownPlaylist['startedPlayback'] + timingState: DBRundownPlaylist['timing'] +} + +/** + * Converts internal playlist timing fields into API `ActivePlaylistEvent.timing`. + * + * @param {ToActivePlaylistTimingProps} props Playlist timing source fields. + * @returns {ActivePlaylistEvent['timing']} Converted timing payload. + */ +export function toActivePlaylistTiming({ + startedPlaybackState, + timingState, +}: ToActivePlaylistTimingProps): ActivePlaylistEvent['timing'] { + const timingMode = translatePlaylistTimingType(timingState.type) + const expectedStart = timingState.type !== PlaylistTimingType.None ? timingState.expectedStart : undefined + const expectedEnd = + timingState.type !== PlaylistTimingType.None && 'expectedEnd' in timingState + ? timingState.expectedEnd + : undefined + + return literal({ + timingMode, + startedPlayback: startedPlaybackState, + expectedDurationMs: timingState.expectedDuration, + expectedStart, + expectedEnd, + }) +} diff --git a/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timing/translatePlaylistTimingType.ts b/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timing/translatePlaylistTimingType.ts new file mode 100644 index 00000000000..0eb5b2c2d33 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/activePlaylistConversion/timing/translatePlaylistTimingType.ts @@ -0,0 +1,20 @@ +import { PlaylistTimingType } from '@sofie-automation/blueprints-integration' +import { ActivePlaylistTimingMode } from '@sofie-automation/live-status-gateway-api' +import { assertNever } from '@sofie-automation/shared-lib/dist/lib/lib' + +export function translatePlaylistTimingType(type: PlaylistTimingType): ActivePlaylistTimingMode { + switch (type) { + case PlaylistTimingType.None: + return ActivePlaylistTimingMode.NONE + case PlaylistTimingType.BackTime: + return ActivePlaylistTimingMode.BACK_MINUS_TIME + case PlaylistTimingType.ForwardTime: + return ActivePlaylistTimingMode.FORWARD_MINUS_TIME + case PlaylistTimingType.Duration: + return ActivePlaylistTimingMode.DURATION + default: + assertNever(type) + // Cast and return the value anyway, so that the application works + return type as any as ActivePlaylistTimingMode + } +} diff --git a/packages/live-status-gateway/src/topics/helpers/notification/toNotificationStatus.ts b/packages/live-status-gateway/src/topics/helpers/notification/toNotificationStatus.ts index 4628e876250..9770366ed56 100644 --- a/packages/live-status-gateway/src/topics/helpers/notification/toNotificationStatus.ts +++ b/packages/live-status-gateway/src/topics/helpers/notification/toNotificationStatus.ts @@ -16,7 +16,7 @@ export function toNotificationStatus(dbNotification: DBNotificationObj): Notific }) } -function toNotificationSeverity(severity: NoteSeverity): NotificationSeverity { +export function toNotificationSeverity(severity: NoteSeverity): NotificationSeverity { switch (severity) { case NoteSeverity.WARNING: return NotificationSeverity.WARNING diff --git a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts index b82099ef4e5..e26d8c50373 100644 --- a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts +++ b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts @@ -3,7 +3,7 @@ import type { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler.js import type { PieceInstanceMin } from '../../collections/pieceInstancesHandler.js' import type { AbSessionAssignment, PieceStatus } from '@sofie-automation/live-status-gateway-api' import { clone } from '@sofie-automation/corelib/dist/lib' -import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import type { PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' const _PLAYLIST_AB_SESSION_KEYS = ['assignedAbSessions', 'trackedAbSessions'] as const diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/conversionContext.spec.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/conversionContext.spec.ts new file mode 100644 index 00000000000..923492179e7 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/conversionContext.spec.ts @@ -0,0 +1,191 @@ +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { + createResolvedPlaylistConversionContext, + findCurrentPartInstance, + findNextPartInstance, + getOrderedPartsInRundown, + getOrderedSegmentsInRundown, +} from '../context/conversionContext.js' +import { + makePart, + makePartInstance, + makePieceInstance, + makePlaylist, + makeRundown, + makeSegment, + makeTestShowStyleBaseExt, +} from './resolvedPlaylistConversionTestUtils.js' + +describe('conversionContext', () => { + it('builds sorted lookup context and current/next pointers', () => { + const currentId = protectString('current_pi') + const nextId = protectString('next_pi') + const playlist = makePlaylist({ + currentPartInfo: { partInstanceId: currentId }, + nextPartInfo: { partInstanceId: nextId }, + }) + const ctx = createResolvedPlaylistConversionContext({ + playlistState: playlist, + rundownsState: [makeRundown('rundown0')], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [makeSegment('segment1', 'rundown0', 2), makeSegment('segment0', 'rundown0', 1)], + partsState: [makePart('part1', 'rundown0', 'segment0', 2), makePart('part0', 'rundown0', 'segment0', 1)], + partInstancesInPlaylistState: [ + makePartInstance('next_pi', 'part1', 'segment0', 'rundown0'), + makePartInstance('current_pi', 'part0', 'segment0', 'rundown0'), + ], + piecesInPlaylistState: [makePieceInstance('pi0').piece], + pieceInstancesInPlaylistState: [makePieceInstance('pi0')], + }) + + expect(getOrderedSegmentsInRundown(ctx, 'rundown0').map((s) => String(s._id))).toEqual(['segment0', 'segment1']) + expect(getOrderedPartsInRundown(ctx, 'rundown0').map((p) => String(p._id))).toEqual(['part0', 'part1']) + expect(String(ctx.currentPartInstance?._id)).toBe('current_pi') + expect(String(ctx.nextPartInstance?._id)).toBe('next_pi') + expect(ctx.orderedRundownIds).toEqual(['rundown0', 'rundown1']) + }) + + it('orders parts by segment order, then by part rank', () => { + const ctx = createResolvedPlaylistConversionContext({ + playlistState: makePlaylist(), + rundownsState: [makeRundown('rundown0')], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [makeSegment('segment1', 'rundown0', 2), makeSegment('segment0', 'rundown0', 1)], + partsState: [ + // Deliberately interleave segments and ranks: + // - segment0 should still come first overall (because segment order) + // - within each segment, sort by part rank + makePart('partA', 'rundown0', 'segment0', 20), + makePart('partC', 'rundown0', 'segment1', 2), + makePart('partB', 'rundown0', 'segment0', 10), + makePart('partD', 'rundown0', 'segment1', 1), + ], + partInstancesInPlaylistState: [], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + expect(getOrderedPartsInRundown(ctx, 'rundown0').map((p) => String(p._id))).toEqual([ + 'partB', + 'partA', + 'partD', + 'partC', + ]) + }) + + it('maps rundowns to their own showStyleBaseId', () => { + const playlist = makePlaylist() + const rundown0 = makeRundown('rundown0') + const rundown1 = makeRundown('rundown1') + rundown0.showStyleBaseId = protectString('showStyleBaseA') + rundown1.showStyleBaseId = protectString('showStyleBaseB') + + const ctx = createResolvedPlaylistConversionContext({ + playlistState: playlist, + rundownsState: [rundown0, rundown1], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [], + partsState: [], + partInstancesInPlaylistState: [], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + expect(String(ctx.rundownsToShowStyles.get(protectString('rundown0')))).toBe('showStyleBaseA') + expect(String(ctx.rundownsToShowStyles.get(protectString('rundown1')))).toBe('showStyleBaseB') + }) + + it('omits missing rundowns from rundownsToShowStyles', () => { + const playlist = makePlaylist() + const rundown0 = makeRundown('rundown0') + rundown0.showStyleBaseId = protectString('showStyleBaseA') + + const ctx = createResolvedPlaylistConversionContext({ + playlistState: playlist, + rundownsState: [rundown0], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [], + partsState: [], + partInstancesInPlaylistState: [], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + expect(String(ctx.rundownsToShowStyles.get(protectString('rundown0')))).toBe('showStyleBaseA') + expect(ctx.rundownsToShowStyles.has(protectString('rundown1'))).toBe(false) + }) + + it('query adapters filter data correctly', () => { + const ctx = createResolvedPlaylistConversionContext({ + playlistState: makePlaylist(), + rundownsState: [makeRundown('rundown0')], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [makeSegment('segment0', 'rundown0', 0)], + partsState: [makePart('part0', 'rundown0', 'segment0', 0)], + partInstancesInPlaylistState: [makePartInstance('pi0', 'part0', 'segment0', 'rundown0')], + piecesInPlaylistState: [makePieceInstance('x').piece], + pieceInstancesInPlaylistState: [makePieceInstance('x')], + }) + + expect(String(ctx.accessors.segmentsFindOne({ _id: protectString('segment0') } as any, {} as any)?._id)).toBe( + 'segment0' + ) + expect( + ctx.accessors.getActivePartInstances({ _id: protectString('playlist0') } as any, { + _id: protectString('pi0'), + }).length + ).toBe(1) + expect(ctx.accessors.piecesFind({ _id: protectString('piece_x') } as any).length).toBe(1) + expect(ctx.accessors.pieceInstancesFind({ _id: protectString('x') } as any).length).toBe(1) + }) + + it('getActivePartInstances omits reset part instances', () => { + const resetInstance = makePartInstance('pi_reset', 'part0', 'segment0', 'rundown0') + resetInstance.reset = true + + const ctx = createResolvedPlaylistConversionContext({ + playlistState: makePlaylist(), + rundownsState: [makeRundown('rundown0')], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [makeSegment('segment0', 'rundown0', 0)], + partsState: [makePart('part0', 'rundown0', 'segment0', 0)], + partInstancesInPlaylistState: [makePartInstance('pi0', 'part0', 'segment0', 'rundown0'), resetInstance], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + expect( + ctx.accessors.getActivePartInstances({ _id: protectString('playlist0') } as any).map((p) => String(p._id)) + ).toEqual(['pi0']) + expect( + ctx.accessors.getActivePartInstances({ _id: protectString('playlist0') } as any, { + _id: protectString('pi_reset'), + }).length + ).toBe(0) + }) + + it('throws when required playlist/showStyle is missing', () => { + expect(() => + createResolvedPlaylistConversionContext({ + playlistState: undefined, + rundownsState: [], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [], + partsState: [], + partInstancesInPlaylistState: [], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + ).toThrow('Missing playlist or showStyleBaseExt') + }) + + it('findCurrentPartInstance/findNextPartInstance return undefined when ids are absent', () => { + const playlist = makePlaylist() + expect( + findCurrentPartInstance(playlist, [makePartInstance('pi0', 'part0', 'segment0', 'rundown0')]) + ).toBeUndefined() + expect( + findNextPartInstance(playlist, [makePartInstance('pi1', 'part1', 'segment0', 'rundown0')]) + ).toBeUndefined() + }) +}) diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/resolvedPlaylistConversionTestUtils.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/resolvedPlaylistConversionTestUtils.ts new file mode 100644 index 00000000000..f55f8d4edb0 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/resolvedPlaylistConversionTestUtils.ts @@ -0,0 +1,134 @@ +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { PlaylistTimingType } from '@sofie-automation/blueprints-integration' +import { ShowStyleBaseExt } from '../../../../collections/showStyleBaseHandler.js' +import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' + +export function makeTestShowStyleBaseExt(): ShowStyleBaseExt { + return { + _id: protectString('showStyleBase0'), + sourceLayerNamesById: new Map([ + ['sl1', 'Source Layer 1'], + ['sl2', 'Source Layer 2'], + ]), + outputLayerNamesById: new Map([ + ['ol1', 'Output Layer 1'], + ['ol2', 'Output Layer 2'], + ]), + } as unknown as ShowStyleBaseExt +} + +export function makePlaylist(overrides: Partial = {}): any { + return { + _id: protectString('playlist0'), + externalId: 'ext_playlist_0', + activationId: undefined, + rehearsal: false, + name: 'Playlist 0', + rundownIdsInOrder: [protectString('rundown0'), protectString('rundown1')], + quickLoop: null, + currentPartInfo: null, + nextPartInfo: null, + publicData: { playlistPublic: true }, + publicPlayoutPersistentState: { playout: true }, + timing: { type: PlaylistTimingType.None, expectedDuration: 10000 }, + tTimers: [], + ...overrides, + } +} + +export function makeRundown(id: string, rank = 0): any { + return { + _id: protectString(id), + showStyleBaseId: protectString('showStyleBase0'), + showStyleVariantId: protectString('showStyleVariant0'), + externalId: `ext_${id}`, + name: `Rundown ${id}`, + description: `Description ${id}`, + publicData: { rank }, + } +} + +export function makeSegment(id: string, rundownId: string, rank: number): any { + return { + _id: protectString(id), + externalId: `ext_${id}`, + identifier: `ident_${id}`, + name: `Segment ${id}`, + _rank: rank, + rundownId: protectString(rundownId), + segmentTiming: { budgetDuration: 3000 }, + } +} + +export function makePart(id: string, rundownId: string, segmentId: string, rank: number): any { + return { + _id: protectString(id), + externalId: `ext_${id}`, + title: `Part ${id}`, + _rank: rank, + rundownId: protectString(rundownId), + segmentId: protectString(segmentId), + autoNext: true, + } +} + +export function makePartInstance(id: string, partId: string, segmentId: string, rundownId: string): any { + return { + _id: protectString(id), + rundownId: protectString(rundownId), + segmentId: protectString(segmentId), + part: { + _id: protectString(partId), + externalId: `ext_${partId}`, + title: `Part ${partId}`, + _rank: 1, + autoNext: false, + publicData: { fromPart: true }, + }, + timings: { + plannedStartedPlayback: 10, + reportedStartedPlayback: 11, + playOffset: 12, + setAsNext: 13, + take: 14, + }, + } +} + +export function makePieceInstance(id: string): any { + return { + _id: protectString(id), + priority: 9, + dynamicallyInserted: true, + resolvedEndCap: 1500, + piece: { + _id: protectString(`piece_${id}`), + externalId: `ext_piece_${id}`, + name: `Piece ${id}`, + sourceLayerId: 'sl1', + outputLayerId: 'ol1', + publicData: { piecePublic: true }, + enable: { + start: 100, + duration: 200, + }, + tags: ['tag1'], + abSessions: [ + { + poolName: 'poolA', + sessionName: 'sessionA', + playerId: 'playerA', + }, + ], + }, + } +} + +export function makeQuickLoop(): any { + return { + locked: false, + running: true, + start: { type: QuickLoopMarkerType.PLAYLIST }, + end: { type: QuickLoopMarkerType.RUNDOWN, id: protectString('rundown0') }, + } +} diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/serializeResolvedSegment.spec.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/serializeResolvedSegment.spec.ts new file mode 100644 index 00000000000..38ea65c9367 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/serializeResolvedSegment.spec.ts @@ -0,0 +1,47 @@ +import { serializePart, serializePiece, serializeSegmentExtended } from '../segments/serializeResolvedSegment.js' + +describe('serializeResolvedSegment helpers', () => { + it('serializePiece omits outputLayer and keeps transport-safe fields', () => { + const result = serializePiece({ + renderedInPoint: 1, + renderedDuration: 2, + instance: { _id: 'inst0' }, + outputLayer: { circular: true }, + }) + + expect(result).toEqual({ + renderedInPoint: 1, + renderedDuration: 2, + instance: { _id: 'inst0' }, + }) + }) + + it('serializePart serializes nested pieces', () => { + const result = serializePart({ + partId: 'part0', + instance: { _id: 'pi0' }, + renderedDuration: 2000, + startsAt: 1000, + willProbablyAutoNext: false, + pieces: [{ instance: { _id: 'piece0' } }], + }) + expect(result.pieces).toEqual([ + { renderedInPoint: undefined, renderedDuration: undefined, instance: { _id: 'piece0' } }, + ]) + }) + + it('serializeSegmentExtended strips cyclic refs from layers', () => { + const result = serializeSegmentExtended({ + _id: 'segment0', + outputLayers: { + ol1: { _id: 'ol1', sourceLayers: [{ _id: 'sl1' }] }, + }, + sourceLayers: { + sl1: { _id: 'sl1', pieces: [{ _id: 'pieceA' }, { instance: { _id: 'pieceB' } }] }, + }, + }) + + expect(result.outputLayers.ol1.sourceLayers).toEqual([]) + expect(result.sourceLayers.sl1.pieces).toEqual(['pieceA', 'pieceB']) + }) +}) diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPartStatus.spec.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPartStatus.spec.ts new file mode 100644 index 00000000000..4d8ba83a5b0 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPartStatus.spec.ts @@ -0,0 +1,108 @@ +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { ResolvedPartState } from '@sofie-automation/live-status-gateway-api' +import { NoteSeverity } from '@sofie-automation/blueprints-integration' +import { toResolvedPartStatus } from '../parts/toResolvedPartStatus.js' +import { + makePartInstance, + makePieceInstance, + makePlaylist, + makeRundown, + makeSegment, + makeTestShowStyleBaseExt, +} from './resolvedPlaylistConversionTestUtils.js' +import { createResolvedPlaylistConversionContext } from '../context/conversionContext.js' + +describe('toResolvedPartStatus', () => { + it('maps current part state and nested pieces', () => { + const currentPartInstanceId = protectString('current_pi') + const ctx = createResolvedPlaylistConversionContext({ + playlistState: makePlaylist({ + currentPartInfo: { partInstanceId: currentPartInstanceId }, + }), + rundownsState: [makeRundown('rundown0')], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [makeSegment('segment0', 'rundown0', 0)], + partsState: [], + partInstancesInPlaylistState: [makePartInstance('current_pi', 'part0', 'segment0', 'rundown0')], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + const partExtended = { + partId: protectString('part0'), + instance: { + ...makePartInstance('current_pi', 'part0', 'segment0', 'rundown0'), + orphaned: 'adlib-part', + part: { + ...makePartInstance('current_pi', 'part0', 'segment0', 'rundown0').part, + invalid: true, + floated: true, + untimed: true, + invalidReason: { + message: { key: 'Invalid {{foo}}', args: { foo: 'bar' }, namespaces: ['blueprint_test'] }, + severity: NoteSeverity.WARNING, + color: '#ff0000', + }, + }, + }, + startsAt: 1000, + renderedDuration: 2000, + pieces: [{ instance: makePieceInstance('piece0') }], + } as any + + const result = toResolvedPartStatus(ctx, partExtended) + expect(result.state).toBe(ResolvedPartState.CURRENT) + expect(result.createdByAdLib).toBe(true) + expect(result.id).toBe('part0') + expect(result.instanceId).toBe('current_pi') + expect(result.invalid).toBe(true) + expect(result.floated).toBe(true) + expect(result.untimed).toBe(true) + expect(result.invalidReason).toMatchObject({ + message: 'Invalid bar', + severity: 'warning', + color: '#ff0000', + }) + expect(result.timing).toMatchObject({ + startMs: 1000, + durationMs: 2000, + plannedStartedPlayback: 10, + reportedStartedPlayback: 11, + playOffsetMs: 12, + setAsNext: 13, + take: 14, + }) + expect(result.pieces).toHaveLength(1) + }) + + it('maps next part state and default timing fallbacks', () => { + const nextPartInstanceId = protectString('next_pi') + const ctx = createResolvedPlaylistConversionContext({ + playlistState: makePlaylist({ + nextPartInfo: { partInstanceId: nextPartInstanceId }, + }), + rundownsState: [makeRundown('rundown0')], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [makeSegment('segment0', 'rundown0', 0)], + partsState: [], + partInstancesInPlaylistState: [makePartInstance('next_pi', 'part1', 'segment0', 'rundown0')], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + const partExtended = { + partId: protectString('part1'), + instance: makePartInstance('next_pi', 'part1', 'segment0', 'rundown0'), + } as any + + const result = toResolvedPartStatus(ctx, partExtended) + expect(result.state).toBe(ResolvedPartState.NEXT) + expect(result.invalid).toBe(false) + expect(result.floated).toBe(false) + expect(result.untimed).toBe(false) + expect(result.invalidReason).toBeUndefined() + expect(result.timing.startMs).toBe(0) + expect(result.timing.durationMs).toBe(0) + expect(result.pieces).toEqual([]) + }) +}) diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPieceStatus.spec.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPieceStatus.spec.ts new file mode 100644 index 00000000000..ce84424566f --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPieceStatus.spec.ts @@ -0,0 +1,69 @@ +import { toResolvedPieceStatus } from '../pieces/toResolvedPieceStatus.js' +import { makePieceInstance } from './resolvedPlaylistConversionTestUtils.js' + +describe('toResolvedPieceStatus', () => { + it('maps rendered timings and basic fields', () => { + const pieceExtended = { + renderedInPoint: 345, + renderedDuration: 678, + instance: makePieceInstance('inst0'), + } as any + + const result = toResolvedPieceStatus(pieceExtended) + expect(result).toMatchObject({ + id: 'piece_inst0', + instanceId: 'inst0', + createdByAdLib: true, + externalId: 'ext_piece_inst0', + name: 'Piece inst0', + priority: 9, + sourceLayerId: 'sl1', + outputLayerId: 'ol1', + invalid: false, + tags: ['tag1'], + timing: { + startMs: 345, + durationMs: 678, + prerollMs: undefined, + }, + abSessions: [{ poolName: 'poolA', sessionName: 'sessionA', playerId: 'playerA' }], + }) + }) + + it('falls back to computed duration from userDuration or resolvedEndCap', () => { + const pieceWithUserDuration = { + instance: { + ...makePieceInstance('inst1'), + userDuration: { endRelativeToPart: 500 }, + }, + } as any + const first = toResolvedPieceStatus(pieceWithUserDuration) + expect(first.timing.startMs).toBe(100) + expect(first.timing.durationMs).toBe(400) + + const pieceWithResolvedEndCap = { + instance: { + ...makePieceInstance('inst2'), + userDuration: undefined, + resolvedEndCap: 350, + }, + } as any + const second = toResolvedPieceStatus(pieceWithResolvedEndCap) + expect(second.timing.durationMs).toBe(250) + }) + + it('filters invalid abSessions entries', () => { + const pieceExtended = { + instance: { + ...makePieceInstance('inst3'), + piece: { + ...makePieceInstance('inst3').piece, + abSessions: [{ poolName: 'ok', sessionName: 'ok', playerId: 'p0' }, { poolName: 'bad' }], + }, + }, + } as any + + const result = toResolvedPieceStatus(pieceExtended) + expect(result.abSessions).toEqual([{ poolName: 'ok', sessionName: 'ok', playerId: 'p0' }]) + }) +}) diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPlaylistStatus.spec.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPlaylistStatus.spec.ts new file mode 100644 index 00000000000..d32b7b4c557 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedPlaylistStatus.spec.ts @@ -0,0 +1,121 @@ +import { PlaylistTimingType } from '@sofie-automation/blueprints-integration' +import { PlaylistActivationStatus, ResolvedPlaylistTimingType } from '@sofie-automation/live-status-gateway-api' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { toResolvedPlaylistStatus } from '../events/toResolvedPlaylistStatus.js' +import { + makePart, + makePartInstance, + makePieceInstance, + makePlaylist, + makeQuickLoop, + makeRundown, + makeSegment, + makeTestShowStyleBaseExt, +} from './resolvedPlaylistConversionTestUtils.js' + +jest.mock('../rundowns/toResolvedRundownStatus.js', () => ({ + toResolvedRundownStatus: jest.fn((_ctx, rundownId) => ({ id: rundownId })), +})) + +describe('toResolvedPlaylistStatus', () => { + it('returns empty payload when playlist or showStyle is missing', () => { + const result = toResolvedPlaylistStatus({ + playlistState: undefined, + rundownsState: [], + showStyleBaseExtState: undefined, + segmentsState: [], + partsState: [], + partInstancesInPlaylistState: [], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + expect(result).toMatchObject({ + event: 'resolvedPlaylist', + id: '', + externalId: '', + name: '', + activationStatus: PlaylistActivationStatus.DEACTIVATED, + rundowns: [], + timing: { + type: ResolvedPlaylistTimingType.FORWARD, + startedPlayback: null, + expectedDurationMs: null, + expectedEnd: null, + }, + }) + }) + + it('assembles full payload and maps activation/timing/quickLoop', () => { + const playlist = makePlaylist({ + activationId: protectString('activation0'), + rehearsal: true, + quickLoop: makeQuickLoop(), + currentPartInfo: { partInstanceId: protectString('current_pi') }, + nextPartInfo: { partInstanceId: protectString('next_pi') }, + startedPlayback: 1706371806000, + timing: { type: PlaylistTimingType.BackTime, expectedDuration: 15000, expectedEnd: 1706371821000 }, + }) + + const result = toResolvedPlaylistStatus({ + playlistState: playlist, + rundownsState: [makeRundown('rundown0'), makeRundown('rundown1')], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [makeSegment('segment0', 'rundown0', 0)], + partsState: [makePart('part0', 'rundown0', 'segment0', 0)], + partInstancesInPlaylistState: [ + makePartInstance('current_pi', 'part0', 'segment0', 'rundown0'), + makePartInstance('next_pi', 'part0', 'segment0', 'rundown0'), + ], + piecesInPlaylistState: [makePieceInstance('piece0').piece], + pieceInstancesInPlaylistState: [makePieceInstance('piece0')], + }) + + expect(result.activationStatus).toBe(PlaylistActivationStatus.REHEARSAL) + expect(result.currentPartInstanceId).toBe('current_pi') + expect(result.nextPartInstanceId).toBe('next_pi') + expect(result.timing).toMatchObject({ + type: ResolvedPlaylistTimingType.BACK, + startedPlayback: 1706371806000, + expectedDurationMs: 15000, + expectedEnd: 1706371821000, + }) + expect(result.quickLoop).toBeDefined() + expect(result.rundowns).toEqual([{ id: 'rundown0' }, { id: 'rundown1' }]) + }) + + it('warns and omits quickLoop markers with unknown markerType', () => { + const unknownMarkerType = 'unknownMarkerType' + const mockLogger = { warn: jest.fn() } as any + + const playlist = makePlaylist({ + quickLoop: { + locked: false, + running: true, + start: { type: unknownMarkerType }, + end: { type: 'rundown', id: protectString('rundown0') }, + } as any, + }) + + const result = toResolvedPlaylistStatus({ + playlistState: playlist, + rundownsState: [makeRundown('rundown0'), makeRundown('rundown1')], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [], + partsState: [], + partInstancesInPlaylistState: [], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + logger: mockLogger, + }) + + expect(result.quickLoop?.start).toBeUndefined() + expect(result.quickLoop?.end).toMatchObject({ markerType: 'rundown', rundownId: 'rundown0' }) + + expect(mockLogger.warn).toHaveBeenCalledTimes(1) + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Unknown QuickLoop markerType encountered; omitting marker', + expect.objectContaining({ markerType: unknownMarkerType }) + ) + }) +}) diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedRundownStatus.spec.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedRundownStatus.spec.ts new file mode 100644 index 00000000000..2a0daf4ef45 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedRundownStatus.spec.ts @@ -0,0 +1,72 @@ +import { toResolvedRundownStatus } from '../rundowns/toResolvedRundownStatus.js' +import { + makePart, + makePartInstance, + makePlaylist, + makeRundown, + makeSegment, + makeTestShowStyleBaseExt, +} from './resolvedPlaylistConversionTestUtils.js' +import { createResolvedPlaylistConversionContext } from '../context/conversionContext.js' + +jest.mock('../segments/toResolvedSegmentStatus.js', () => ({ + toResolvedSegmentStatus: jest.fn((_ctx, segment) => ({ id: String(segment._id) })), +})) + +describe('toResolvedRundownStatus', () => { + it('maps rundown fields and ordered segments', () => { + const ctx = createResolvedPlaylistConversionContext({ + playlistState: makePlaylist(), + rundownsState: [makeRundown('rundown0')], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [makeSegment('segment1', 'rundown0', 2), makeSegment('segment0', 'rundown0', 1)], + partsState: [makePart('part0', 'rundown0', 'segment0', 0)], + partInstancesInPlaylistState: [makePartInstance('pi0', 'part0', 'segment0', 'rundown0')], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + const result = toResolvedRundownStatus(ctx, 'rundown0') + expect(result).toMatchObject({ + id: 'rundown0', + externalId: 'ext_rundown0', + name: 'Rundown rundown0', + rank: 0, + description: 'Description rundown0', + }) + expect(result.segments).toEqual([{ id: 'segment0' }, { id: 'segment1' }]) + }) + + it('returns empty defaults when rundown is missing', () => { + const ctx = createResolvedPlaylistConversionContext({ + playlistState: makePlaylist({ rundownIdsInOrder: ['rundown0'] }), + rundownsState: [], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [], + partsState: [], + partInstancesInPlaylistState: [], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + const result = toResolvedRundownStatus(ctx, 'rundown0') + expect(result.externalId).toBe('') + expect(result.segments).toEqual([]) + expect(result.rank).toBe(0) + }) + + it('throws when rundownId is not in orderedRundownIds', () => { + const ctx = createResolvedPlaylistConversionContext({ + playlistState: makePlaylist({ rundownIdsInOrder: ['rundown0'] }), + rundownsState: [makeRundown('rundown0')], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [], + partsState: [], + partInstancesInPlaylistState: [], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + expect(() => toResolvedRundownStatus(ctx, 'missing')).toThrow(/orderedRundownIds/) + }) +}) diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedSegmentStatus.spec.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedSegmentStatus.spec.ts new file mode 100644 index 00000000000..d2350ce9e9b --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/__tests__/toResolvedSegmentStatus.spec.ts @@ -0,0 +1,118 @@ +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { getResolvedSegment } from '@sofie-automation/corelib/dist/playout/stateCacheResolver' +import { toResolvedSegmentStatus } from '../segments/toResolvedSegmentStatus.js' +import { + makePart, + makePartInstance, + makePieceInstance, + makePlaylist, + makeRundown, + makeSegment, + makeTestShowStyleBaseExt, +} from './resolvedPlaylistConversionTestUtils.js' +import { createResolvedPlaylistConversionContext } from '../context/conversionContext.js' + +jest.mock('@sofie-automation/corelib/dist/playout/stateCacheResolver', () => ({ + getResolvedSegment: jest.fn(), +})) + +describe('toResolvedSegmentStatus', () => { + it('maps resolved segment to API shape and sorts layers', () => { + ;(getResolvedSegment as jest.Mock).mockReturnValue({ + segmentExtended: { + sourceLayers: { + sl2: { _rank: 2, name: 'SL2', abbreviation: 'S2', isHidden: true, type: 3 }, + sl1: { _rank: 1, name: 'SL1', abbreviation: 'S1', isHidden: false, type: 2 }, + }, + outputLayers: { + ol2: { name: 'Output B', isFlattened: true, isPGM: false, sourceLayers: [{ _id: 'sl2' }] }, + ol1: { name: 'Output A', isFlattened: false, isPGM: true, sourceLayers: [{ _id: 'sl1' }] }, + }, + }, + parts: [ + { + partId: protectString('part0'), + instance: makePartInstance('pi0', 'part0', 'segment0', 'rundown0'), + pieces: [{ instance: makePieceInstance('piece0') }], + }, + ], + }) + + const segment = makeSegment('segment0', 'rundown0', 1) + const rundown = makeRundown('rundown0') + const ctx = createResolvedPlaylistConversionContext({ + playlistState: makePlaylist(), + rundownsState: [rundown], + showStyleBaseExtState: makeTestShowStyleBaseExt(), + segmentsState: [segment], + partsState: [makePart('part0', 'rundown0', 'segment0', 1)], + partInstancesInPlaylistState: [makePartInstance('pi0', 'part0', 'segment0', 'rundown0')], + piecesInPlaylistState: [makePieceInstance('piece0').piece], + pieceInstancesInPlaylistState: [makePieceInstance('piece0')], + }) + + const result = toResolvedSegmentStatus(ctx, segment, rundown) + expect(result).toMatchObject({ + id: 'segment0', + externalId: 'ext_segment0', + identifier: 'ident_segment0', + name: 'Segment segment0', + rank: 1, + isHidden: false, + timing: { startMs: 0, endMs: 3000, budgetDurationMs: 3000 }, + }) + expect(result.sourceLayers.map((s) => s.id)).toEqual(['sl1', 'sl2']) + expect(result.outputLayers.map((s) => s.id)).toEqual(['ol1', 'ol2']) + expect(result.parts).toHaveLength(1) + }) + + it('resolves per-rundown showStyleBaseExt in mixed-showstyle playlists', () => { + const showStyleBaseExtA = makeTestShowStyleBaseExt() + const showStyleBaseExtB = { + ...makeTestShowStyleBaseExt(), + _id: protectString('showStyleBaseB'), + sourceLayerNamesById: new Map([['sourceLayerX', 'SourceLayerX_fromShowStyleB']]), + outputLayerNamesById: new Map([['outputLayerX', 'OutputLayerX_fromShowStyleB']]), + } + + ;(getResolvedSegment as jest.Mock).mockReturnValue({ + segmentExtended: { + sourceLayers: { + sourceLayerX: { _rank: 1 }, + }, + outputLayers: { + outputLayerX: { sourceLayers: [{ _id: 'sourceLayerX' }] }, + }, + }, + parts: [], + }) + + const rundown0 = makeRundown('rundown0') + const rundown1 = makeRundown('rundown1') + rundown0.showStyleBaseId = showStyleBaseExtA._id + rundown1.showStyleBaseId = showStyleBaseExtB._id + + const segment = makeSegment('segment1', 'rundown1', 1) + const ctx = createResolvedPlaylistConversionContext({ + playlistState: makePlaylist(), + rundownsState: [rundown0, rundown1], + // Fallback showstyle: + showStyleBaseExtState: showStyleBaseExtA, + showStyleBaseExtsByIdState: new Map([ + [showStyleBaseExtA._id, showStyleBaseExtA], + [showStyleBaseExtB._id, showStyleBaseExtB], + ]), + segmentsState: [segment], + partsState: [], + partInstancesInPlaylistState: [], + piecesInPlaylistState: [], + pieceInstancesInPlaylistState: [], + }) + + const result = toResolvedSegmentStatus(ctx, segment, rundown1) + + expect(getResolvedSegment).toHaveBeenCalledWith(expect.objectContaining({ showStyleBase: showStyleBaseExtB })) + expect(result.sourceLayers).toMatchObject([{ id: 'sourceLayerX', name: 'SourceLayerX_fromShowStyleB' }]) + expect(result.outputLayers).toMatchObject([{ id: 'outputLayerX', name: 'OutputLayerX_fromShowStyleB' }]) + }) +}) diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/context/conversionContext.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/context/conversionContext.ts new file mode 100644 index 00000000000..fcbba15458a --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/context/conversionContext.ts @@ -0,0 +1,232 @@ +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { mongoWhereFilter } from '@sofie-automation/corelib/dist/mongo' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import type { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { PartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import type { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { StateCacheResolverDataAccess } from '@sofie-automation/corelib/dist/playout/stateCacheResolverTypes' +import { RundownId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ShowStyleBaseExt } from '../../../../collections/showStyleBaseHandler.js' + +type PlaylistState = Pick< + DBRundownPlaylist, + | '_id' + | 'studioId' + | 'created' + | 'modified' + | 'externalId' + | 'activationId' + | 'rehearsal' + | 'name' + | 'rundownIdsInOrder' + | 'quickLoop' + | 'currentPartInfo' + | 'nextPartInfo' + | 'previousPartInfo' + | 'publicData' + | 'publicPlayoutPersistentState' + | 'timing' + | 'startedPlayback' + | 'tTimers' +> + +export type ToResolvedPlaylistStatusProps = { + playlistState: PlaylistState | undefined + rundownsState: DBRundown[] + showStyleBaseExtState: ShowStyleBaseExt | undefined + showStyleBaseExtsByIdState?: ReadonlyMap + segmentsState: DBSegment[] + partsState: DBPart[] + partInstancesInPlaylistState: PartInstance[] + piecesInPlaylistState: Piece[] + pieceInstancesInPlaylistState: PieceInstance[] +} + +export type ResolvedPlaylistConversionContext = Readonly<{ + playlist: PlaylistState + rundownsById: Map + showStyleBaseExt: ShowStyleBaseExt + + rundownsToShowStyles: ReadonlyMap + rundownsToShowStyleBaseExt: ReadonlyMap + orderedRundownIds: string[] + + segmentsByRundownId: Map + partsByRundownId: Map + + currentPartInstance: PartInstance | undefined + nextPartInstance: PartInstance | undefined + + accessors: ReturnType +}> + +/** + * Creates a normalized lookup context used by all resolved-playlist converters. + * This keeps conversion functions deterministic and avoids repeated map/filter work. + */ +export function createResolvedPlaylistConversionContext( + props: ToResolvedPlaylistStatusProps +): ResolvedPlaylistConversionContext { + const playlist = props.playlistState + const showStyleBaseExt = props.showStyleBaseExtState + + if (!playlist || !showStyleBaseExt) { + throw new Error('Missing playlist or showStyleBaseExt') + } + + const orderedRundownIds = (playlist.rundownIdsInOrder ?? []).map((r) => unprotectString(r)) + const rundownsById = new Map((props.rundownsState ?? []).map((r) => [unprotectString(r._id), r])) + const rundownsToShowStyles = new Map() + const rundownsToShowStyleBaseExt = new Map() + for (const rundownId of playlist.rundownIdsInOrder ?? []) { + const rundown = rundownsById.get(unprotectString(rundownId)) + if (!rundown) continue + rundownsToShowStyles.set(rundownId, rundown.showStyleBaseId) + const showStyle = props.showStyleBaseExtsByIdState?.get(rundown.showStyleBaseId) + if (showStyle) rundownsToShowStyleBaseExt.set(rundownId, showStyle) + } + + const segmentsByRundownId = groupByRundownId(props.segmentsState) + const partsByRundownId = groupByRundownId(props.partsState) + + const currentPartInstance = findCurrentPartInstance(playlist, props.partInstancesInPlaylistState) + const nextPartInstance = findNextPartInstance(playlist, props.partInstancesInPlaylistState) + + const accessors = createQueryAdapters({ + segmentsState: props.segmentsState, + partsState: props.partsState, + partInstancesInPlaylistState: props.partInstancesInPlaylistState, + piecesInPlaylistState: props.piecesInPlaylistState, + pieceInstancesInPlaylistState: props.pieceInstancesInPlaylistState, + }) + + return { + playlist, + rundownsById, + showStyleBaseExt, + rundownsToShowStyles, + rundownsToShowStyleBaseExt, + orderedRundownIds, + segmentsByRundownId, + partsByRundownId, + currentPartInstance, + nextPartInstance, + accessors, + } +} + +function groupByRundownId(items: T[]): Map { + const map = new Map() + for (const item of items ?? []) { + const rundownId = String(item.rundownId) + const list = map.get(rundownId) ?? ([] as T[]) + list.push(item) + map.set(rundownId, list) + } + return map +} + +/** Returns segments in rundown order so API consumers get stable ranks. */ +export function getOrderedSegmentsInRundown(ctx: ResolvedPlaylistConversionContext, rundownId: string): DBSegment[] { + const list = ctx.segmentsByRundownId.get(String(rundownId)) ?? [] + return list.slice().sort((a, b) => a._rank - b._rank) +} + +/** Returns parts in rundown order for state resolver and API output parity. */ +export function getOrderedPartsInRundown(ctx: ResolvedPlaylistConversionContext, rundownId: string): DBPart[] { + const normalizedRundownId = String(rundownId) + + const partsInRundown = ctx.partsByRundownId.get(normalizedRundownId) ?? [] + if (partsInRundown.length === 0) return [] + + // Part._rank is only meaningful within a segment, so we must preserve segment order. + const partsBySegmentId = new Map() + for (const part of partsInRundown) { + const segmentId = String(part.segmentId) + const list = partsBySegmentId.get(segmentId) ?? [] + list.push(part) + partsBySegmentId.set(segmentId, list) + } + + const orderedSegments = getOrderedSegmentsInRundown(ctx, normalizedRundownId) + const orderedParts: DBPart[] = [] + for (const segment of orderedSegments) { + const segmentParts = partsBySegmentId.get(String(segment._id)) + if (!segmentParts?.length) continue + segmentParts.sort((a, b) => a._rank - b._rank) + orderedParts.push(...segmentParts) + } + + return orderedParts +} + +/** Finds the current part instance referenced by playlist state. */ +export function findCurrentPartInstance( + playlist: PlaylistState, + partInstancesInPlaylistState: PartInstance[] +): PartInstance | undefined { + return partInstancesInPlaylistState.find((pi) => pi._id === playlist.currentPartInfo?.partInstanceId) +} + +/** Finds the next part instance referenced by playlist state. */ +export function findNextPartInstance( + playlist: PlaylistState, + partInstancesInPlaylistState: PartInstance[] +): PartInstance | undefined { + return partInstancesInPlaylistState.find((pi) => pi._id === playlist.nextPartInfo?.partInstanceId) +} + +function createQueryAdapters({ + segmentsState, + partsState, + partInstancesInPlaylistState, + piecesInPlaylistState, + pieceInstancesInPlaylistState, +}: { + segmentsState: DBSegment[] + partsState: DBPart[] + partInstancesInPlaylistState: PartInstance[] + piecesInPlaylistState: Piece[] + pieceInstancesInPlaylistState: PieceInstance[] +}): StateCacheResolverDataAccess { + // Adapter surface expected by `getResolvedSegment` + return { + segmentsFindOne: (selector, _options): DBSegment | undefined => { + if (!selector) return undefined + if (typeof selector === 'string') { + const normalizedSelectorId = unprotectString(selector as any) + return segmentsState.find((s) => unprotectString(s._id) === normalizedSelectorId) + } + + return mongoWhereFilter(segmentsState, selector as never)[0] + }, + getSegmentsAndPartsSync: (playlistPick, segmentsQuery, partsQuery) => { + const rundownIds = new Set(playlistPick.rundownIdsInOrder ?? []) + const segmentsInRundowns = segmentsState.filter((s) => rundownIds.has(s.rundownId)) + const partsInRundowns = partsState.filter((p) => rundownIds.has(p.rundownId)) + return { + segments: segmentsQuery + ? mongoWhereFilter(segmentsInRundowns, segmentsQuery as never) + : segmentsInRundowns, + parts: partsQuery ? mongoWhereFilter(partsInRundowns, partsQuery as never) : partsInRundowns, + } + }, + getActivePartInstances: (_playlistPick: Pick, selector?: unknown) => { + const mergedSelector = selector ? { ...(selector as any), reset: { $ne: true } } : { reset: { $ne: true } } + return mongoWhereFilter(partInstancesInPlaylistState, mergedSelector as never) + }, + piecesFind: (selector) => { + if (!selector) return piecesInPlaylistState + if (typeof selector === 'string') return piecesInPlaylistState.filter((p) => p._id === selector) + return mongoWhereFilter(piecesInPlaylistState, selector as never) + }, + pieceInstancesFind: (selector) => { + if (!selector) return pieceInstancesInPlaylistState + if (typeof selector === 'string') return pieceInstancesInPlaylistState.filter((pi) => pi._id === selector) + return mongoWhereFilter(pieceInstancesInPlaylistState, selector as never) + }, + } +} diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/events/toResolvedPlaylistStatus.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/events/toResolvedPlaylistStatus.ts new file mode 100644 index 00000000000..73ae60fa19f --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/events/toResolvedPlaylistStatus.ts @@ -0,0 +1,121 @@ +import type { Logger } from 'winston' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' +import { + PlaylistActivationStatus, + ResolvedPlaylistTimingType, + type ResolvedPlaylistEvent, +} from '@sofie-automation/live-status-gateway-api' +import { createResolvedPlaylistConversionContext, ToResolvedPlaylistStatusProps } from '../context/conversionContext.js' +import { toResolvedRundownStatus } from '../rundowns/toResolvedRundownStatus.js' +import { toTTimers } from '../../activePlaylistConversion/timers/toTTimers.js' +import { toQuickLoopStatus } from '../../activePlaylistConversion/quickLoop/toQuickLoopStatus.js' +import { PlaylistTimingType } from '@sofie-automation/blueprints-integration' +import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' + +/** Converts playlist-scoped collection snapshots into the `resolvedPlaylist` websocket event shape. */ +export function toResolvedPlaylistStatus({ + playlistState, + rundownsState, + showStyleBaseExtState, + segmentsState, + partsState, + partInstancesInPlaylistState, + piecesInPlaylistState, + pieceInstancesInPlaylistState, + logger, +}: ToResolvedPlaylistStatusProps & { logger?: Logger }): ResolvedPlaylistEvent { + // Keep payload shape stable before all dependencies are available. + if (!playlistState || !showStyleBaseExtState) { + return literal({ + event: 'resolvedPlaylist' as const, + id: '', + externalId: '', + name: '', + activationStatus: PlaylistActivationStatus.DEACTIVATED, + currentPartInstanceId: null, + nextPartInstanceId: null, + timing: { + type: ResolvedPlaylistTimingType.FORWARD, + startedPlayback: null, + expectedDurationMs: null, + expectedEnd: null, + }, + tTimers: toTTimers(null), + quickLoop: null, + rundowns: [], + }) + } + + const ctx = createResolvedPlaylistConversionContext({ + playlistState, + rundownsState, + showStyleBaseExtState, + segmentsState, + partsState, + partInstancesInPlaylistState, + piecesInPlaylistState, + pieceInstancesInPlaylistState, + }) + + let activationStatus: PlaylistActivationStatus = + ctx.playlist.activationId === undefined + ? PlaylistActivationStatus.DEACTIVATED + : PlaylistActivationStatus.ACTIVATED + if (ctx.playlist.activationId && ctx.playlist.rehearsal) activationStatus = PlaylistActivationStatus.REHEARSAL + + const currentPartInstanceId = ctx.playlist.currentPartInfo?.partInstanceId + ? unprotectString(ctx.playlist.currentPartInfo.partInstanceId) + : null + const nextPartInstanceId = ctx.playlist.nextPartInfo?.partInstanceId + ? unprotectString(ctx.playlist.nextPartInfo.partInstanceId) + : null + + const segmentsById = Object.fromEntries( + (segmentsState ?? []).map((s) => [unprotectString(s._id), { rundownId: s.rundownId }]) + ) as Record + const partsById = Object.fromEntries( + (partsState ?? []).map((p) => [unprotectString(p._id), { rundownId: p.rundownId, segmentId: p.segmentId }]) + ) as Record< + string, + | { rundownId?: (typeof partsState)[number]['rundownId']; segmentId?: (typeof partsState)[number]['segmentId'] } + | undefined + > + + const timingState = ctx.playlist.timing + const expectedDurationMs: number | null = timingState?.expectedDuration ?? null + const timingType = + timingState?.type === PlaylistTimingType.BackTime + ? ResolvedPlaylistTimingType.BACK + : ResolvedPlaylistTimingType.FORWARD + const expectedEnd: number | null = timingState ? (PlaylistTiming.getExpectedEnd(timingState) ?? null) : null + + const playoutState = ctx.playlist.publicPlayoutPersistentState + const publicData = ctx.playlist.publicData + + return literal({ + event: 'resolvedPlaylist' as const, + id: unprotectString(ctx.playlist._id), + externalId: ctx.playlist.externalId, + name: ctx.playlist.name, + activationStatus, + currentPartInstanceId, + nextPartInstanceId, + ...(playoutState !== undefined ? { playoutState } : {}), + ...(publicData !== undefined ? { publicData } : {}), + timing: { + type: timingType, + startedPlayback: ctx.playlist.startedPlayback ?? null, + expectedDurationMs, + expectedEnd, + }, + tTimers: toTTimers(ctx.playlist.tTimers ?? null), + quickLoop: toQuickLoopStatus({ + quickLoop: ctx.playlist.quickLoop, + segmentsById, + partsById, + logger, + }), + rundowns: ctx.orderedRundownIds.map((rundownId) => toResolvedRundownStatus(ctx, rundownId)), + }) +} diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/parts/toResolvedPartStatus.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/parts/toResolvedPartStatus.ts new file mode 100644 index 00000000000..a436cc5c4fc --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/parts/toResolvedPartStatus.ts @@ -0,0 +1,65 @@ +import { toResolvedPieceStatus } from '../pieces/toResolvedPieceStatus.js' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { ResolvedPlaylistConversionContext } from '../context/conversionContext.js' +import type { PartExtended } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { PartInvalidReason as CorePartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part' +import { PartInvalidReason, ResolvedPart, ResolvedPartState } from '@sofie-automation/live-status-gateway-api' +import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { toNotificationSeverity } from '../../notification/toNotificationStatus.js' + +/** Converts a resolved `PartExtended` model into the gateway API `ResolvedPart` shape. */ +export function toResolvedPartStatus(ctx: ResolvedPlaylistConversionContext, partExtended: PartExtended): ResolvedPart { + const part = partExtended + const instance = part.instance + const basePart = instance?.part ?? {} + + const instanceId = instance?._id ? unprotectString(instance._id) : '' + let state: ResolvedPartState | undefined + if (ctx.playlist.currentPartInfo?.partInstanceId && instance?._id === ctx.playlist.currentPartInfo.partInstanceId) { + state = ResolvedPartState.CURRENT + } else if ( + ctx.playlist.nextPartInfo?.partInstanceId && + instance?._id === ctx.playlist.nextPartInfo.partInstanceId + ) { + state = ResolvedPartState.NEXT + } + + const timings = instance?.timings ?? {} + const createdByAdLib = instance?.orphaned === 'adlib-part' + + return { + id: unprotectString(part.partId ?? basePart._id), + instanceId, + createdByAdLib: createdByAdLib, + externalId: basePart.externalId ?? '', + name: basePart.title ?? '', + rank: basePart._rank ?? 0, + autoNext: !!basePart.autoNext, + invalid: !!basePart.invalid, + floated: !!basePart.floated, + untimed: !!basePart.untimed, + invalidReason: basePart.invalidReason ? toApiPartInvalidReason(basePart.invalidReason) : undefined, + state, + publicData: basePart.publicData, + timing: { + startMs: part.startsAt ?? 0, + durationMs: part.renderedDuration ?? 0, + plannedStartedPlayback: timings.plannedStartedPlayback ?? 0, + reportedStartedPlayback: timings.reportedStartedPlayback ?? 0, + playOffsetMs: timings.playOffset ?? undefined, + setAsNext: timings.setAsNext ?? 0, + take: timings.take ?? 0, + }, + pieces: part.pieces?.map((piece) => toResolvedPieceStatus(piece)) ?? [], + } +} + +function toApiPartInvalidReason(invalidReason: CorePartInvalidReason): PartInvalidReason { + const msg = invalidReason.message + + return { + message: interpollateTranslation(msg.key, msg.args ?? {}), + severity: invalidReason.severity ? toNotificationSeverity(invalidReason.severity) : undefined, + color: invalidReason.color, + } +} diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/pieces/toResolvedPieceStatus.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/pieces/toResolvedPieceStatus.ts new file mode 100644 index 00000000000..7775703b206 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/pieces/toResolvedPieceStatus.ts @@ -0,0 +1,78 @@ +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import type { PieceExtended } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { AbSessionAssignment, ResolvedPiece } from '@sofie-automation/live-status-gateway-api' + +/** Converts a resolved `PieceExtended` model into the gateway API `ResolvedPiece` shape. */ +export function toResolvedPieceStatus(pieceExtended: PieceExtended): ResolvedPiece { + const piece = pieceExtended + const instance = piece.instance + const basePiece = instance?.piece ?? {} + const startMs = getStartMs(piece, basePiece) + const durationMs = getDurationMs(piece, instance, startMs, basePiece) + const createdByAdLib = !!instance?.dynamicallyInserted + const abSessions = toAbSessions(basePiece) + + return { + id: unprotectString(basePiece._id), + instanceId: instance?._id ? unprotectString(instance._id) : undefined, + createdByAdLib, + externalId: basePiece.externalId ?? '', + name: basePiece.name ?? '', + priority: instance?.priority ?? 0, + sourceLayerId: basePiece.sourceLayerId ?? '', + outputLayerId: basePiece.outputLayerId ?? '', + invalid: !!basePiece.invalid, + publicData: basePiece.publicData, + timing: { + startMs, + durationMs, + prerollMs: basePiece.prerollDuration, + }, + tags: basePiece.tags ? [...basePiece.tags] : [], + abSessions, + } +} + +/** Resolves piece start offset from rendered data or blueprint timing metadata. */ +function getStartMs( + piece: PieceExtended, + basePiece: NonNullable['piece'] | Record +) { + if (typeof piece.renderedInPoint === 'number') return piece.renderedInPoint + if (typeof basePiece?.enable?.start === 'number') return basePiece.enable.start + return undefined +} + +/** Resolves duration using runtime render first, then fallback timing metadata. */ +function getDurationMs( + piece: PieceExtended, + instance: PieceExtended['instance'], + startMs: number | undefined, + basePiece: NonNullable['piece'] | Record +) { + if (typeof piece.renderedDuration === 'number') return piece.renderedDuration + + const durationFromEnable = typeof basePiece?.enable?.duration === 'number' ? basePiece.enable.duration : undefined + const durationFromUserDuration = + typeof instance?.userDuration?.endRelativeToPart === 'number' && typeof startMs === 'number' + ? Math.max(0, instance.userDuration.endRelativeToPart - startMs) + : undefined + const durationFromResolvedEndCap = + typeof instance?.resolvedEndCap === 'number' && typeof startMs === 'number' + ? Math.max(0, instance.resolvedEndCap - startMs) + : undefined + + return durationFromUserDuration ?? durationFromResolvedEndCap ?? durationFromEnable +} + +/** Normalizes ad-lib AB sessions to a strict API-safe shape. */ +function toAbSessions(basePiece: NonNullable['piece'] | Record) { + if (!Array.isArray(basePiece.abSessions)) return undefined + + return basePiece.abSessions + .filter( + (s): s is AbSessionAssignment => + !!s && typeof s.poolName === 'string' && typeof s.sessionName === 'string' && 'playerId' in s + ) + .map((s) => ({ poolName: s.poolName, sessionName: s.sessionName, playerId: s.playerId })) +} diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/rundowns/toResolvedRundownStatus.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/rundowns/toResolvedRundownStatus.ts new file mode 100644 index 00000000000..529095a8d8a --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/rundowns/toResolvedRundownStatus.ts @@ -0,0 +1,25 @@ +import { ResolvedPlaylistConversionContext, getOrderedSegmentsInRundown } from '../context/conversionContext.js' +import { toResolvedSegmentStatus } from '../segments/toResolvedSegmentStatus.js' +import type { ResolvedRundown } from '@sofie-automation/live-status-gateway-api' + +/** Converts a rundown id into a fully expanded `ResolvedRundown` payload. */ +export function toResolvedRundownStatus(ctx: ResolvedPlaylistConversionContext, rundownId: string): ResolvedRundown { + const rundownRank = ctx.orderedRundownIds.indexOf(rundownId) + if (rundownRank === -1) { + throw new Error(`Rundown "${rundownId}" is not in orderedRundownIds`) + } + + const rundown = ctx.rundownsById.get(rundownId) + const orderedSegments = getOrderedSegmentsInRundown(ctx, rundownId) + const rank = rundownRank + + return { + id: rundownId, + externalId: rundown?.externalId ?? '', + name: rundown?.name ?? '', + rank, + description: rundown?.description ?? '', + publicData: rundown?.publicData, + segments: rundown ? orderedSegments.map((segment) => toResolvedSegmentStatus(ctx, segment, rundown)) : [], + } +} diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/segments/serializeResolvedSegment.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/segments/serializeResolvedSegment.ts new file mode 100644 index 00000000000..37226927961 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/segments/serializeResolvedSegment.ts @@ -0,0 +1,54 @@ +/** Serializes `PieceExtended` into a JSON-safe shape without circular references. */ +export function serializePiece(pieceExtended: unknown): any { + const piece = pieceExtended as any + // `PieceExtended` contains references to `outputLayers/sourceLayers` which then contains pieces again -> circular. + // We omit `outputLayer` and keep the values used by consumers. + const { outputLayer: _outputLayer, ...rest } = piece ?? {} + return { + renderedInPoint: rest.renderedInPoint, + renderedDuration: rest.renderedDuration, + instance: rest.instance, + } +} + +/** Serializes `PartExtended` and nested pieces for deterministic debug logging/testing. */ +export function serializePart(partExtended: unknown): any { + const part = (partExtended as any) ?? {} + return { + partId: part?.partId, + instance: part?.instance, + renderedDuration: part?.renderedDuration, + startsAt: part?.startsAt, + willProbablyAutoNext: part?.willProbablyAutoNext, + pieces: part.pieces?.map(serializePiece) ?? [], + } +} + +/** Serializes `SegmentExtended` while pruning cyclical layer/piece references. */ +export function serializeSegmentExtended(segmentExtended: unknown): any { + const segment = segmentExtended as any + if (!segment) return segment + const { outputLayers, sourceLayers, ...rest } = segment + + const safeOutputLayers: Record = {} + for (const [layerId, layer] of Object.entries(outputLayers ?? {})) { + safeOutputLayers[layerId] = { + ...layer, + sourceLayers: [], + } + } + + const safeSourceLayers: Record = {} + for (const [layerId, layer] of Object.entries(sourceLayers ?? {})) { + safeSourceLayers[layerId] = { + ...layer, + pieces: (layer.pieces ?? []).map((p: any) => p?.instance?._id ?? p?._id), + } + } + + return { + ...rest, + outputLayers: safeOutputLayers, + sourceLayers: safeSourceLayers, + } +} diff --git a/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/segments/toResolvedSegmentStatus.ts b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/segments/toResolvedSegmentStatus.ts new file mode 100644 index 00000000000..bd04992b55c --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/resolvedPlaylistConversion/segments/toResolvedSegmentStatus.ts @@ -0,0 +1,114 @@ +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { getResolvedSegment } from '@sofie-automation/corelib/dist/playout/stateCacheResolver' +import { ResolvedPlaylistConversionContext, getOrderedPartsInRundown } from '../context/conversionContext.js' +import { toResolvedPartStatus } from '../parts/toResolvedPartStatus.js' +import type { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { DBSegment, SegmentExtended } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { ResolvedSegment, SourceLayer, OutputLayer } from '@sofie-automation/live-status-gateway-api' +import { literal } from '@sofie-automation/server-core-integration' + +export function toResolvedSegmentStatus( + ctx: ResolvedPlaylistConversionContext, + segment: DBSegment, + rundown: Rundown +): ResolvedSegment { + const segmentShowStyleBaseExt = ctx.rundownsToShowStyleBaseExt.get(segment.rundownId) ?? ctx.showStyleBaseExt + + const resolvedSegment = getResolvedSegment({ + showStyleBase: segmentShowStyleBaseExt, + studio: undefined, + playlist: ctx.playlist, + rundown, + segment, + segmentsToReceiveOnRundownEndFromSet: getSegmentsToReceiveOnRundownEndFromSet(ctx, segment), + rundownsToReceiveOnShowStyleEndFrom: [], + rundownsToShowStyles: ctx.rundownsToShowStyles, + orderedAllPartIds: getOrderedPartsInRundown(ctx, unprotectString(segment.rundownId)).map((p) => p._id), + currentPartInstance: ctx.currentPartInstance, + nextPartInstance: ctx.nextPartInstance, + accessors: ctx.accessors, + options: { + getCurrentTime: () => Date.now(), + invalidateAfter: (_timeoutMs: number) => {}, + includeDisabledPieces: false, + showHiddenSourceLayers: false, + defaultDisplayDuration: 0, + }, + }) + + const segmentExtended = resolvedSegment.segmentExtended + const sourceLayers = toSourceLayers(segmentShowStyleBaseExt, segmentExtended) + const outputLayers = toOutputLayers(segmentShowStyleBaseExt, segmentExtended) + + const budgetDurationMs = segment?.segmentTiming?.budgetDuration ?? 0 + + return { + id: unprotectString(segment._id), + externalId: segment.externalId, + identifier: segment.identifier ?? '', + name: segment.name ?? '', + rank: segment._rank, + isHidden: segment.isHidden ?? false, + sourceLayers, + outputLayers, + publicData: segment.publicData, + timing: { + startMs: 0, + endMs: budgetDurationMs, + budgetDurationMs, + }, + parts: (resolvedSegment.parts ?? []).map((part) => toResolvedPartStatus(ctx, part)), + } +} + +/** Limits "receive on rundown end" evaluation to segments in the same rundown. */ +function getSegmentsToReceiveOnRundownEndFromSet( + ctx: ResolvedPlaylistConversionContext, + segment: DBSegment +): Set { + const segmentsInThisRundown = ctx.segmentsByRundownId.get(String(segment.rundownId)) ?? [] + return new Set(segmentsInThisRundown.map((s) => s._id)) +} + +/** Maps source layers from resolved segment output to API shape sorted by rank. */ +function toSourceLayers( + showStyleBaseExt: ResolvedPlaylistConversionContext['showStyleBaseExt'], + segmentExtended: SegmentExtended +): SourceLayer[] { + type SourceLayerExtended = NonNullable[string] + if (!segmentExtended.sourceLayers) return [] + + return Object.entries(segmentExtended.sourceLayers) + .map(([id, layer]) => ({ + id, + name: layer?.name ?? showStyleBaseExt?.sourceLayerNamesById?.get?.(id) ?? '', + abbreviation: layer?.abbreviation ?? '', + isHidden: layer?.isHidden ?? false, + type: layer?.type ?? 0, + rank: layer?._rank ?? 0, + })) + .sort((a, b) => a.rank - b.rank) +} + +/** Maps output layers from resolved segment output to API shape sorted by name. */ +function toOutputLayers( + showStyleBaseExt: ResolvedPlaylistConversionContext['showStyleBaseExt'], + segmentExtended: SegmentExtended +): OutputLayer[] { + type OutputLayerExtended = NonNullable[string] + if (!segmentExtended.outputLayers) return [] + + return Object.entries(segmentExtended.outputLayers) + .map(([id, layer]) => + literal({ + id, + name: layer?.name ?? showStyleBaseExt?.outputLayerNamesById?.get?.(id) ?? '', + isFlattened: layer?.isFlattened ?? false, + isPGM: layer?.isPGM ?? false, + sourceLayerIds: (layer?.sourceLayers ?? []) + .map((sl) => sl?._id) + .filter((slId): slId is string => !!slId), + }) + ) + .sort((a, b) => (a.name || '').localeCompare(b.name || '')) +} diff --git a/packages/live-status-gateway/src/topics/packagesTopic.ts b/packages/live-status-gateway/src/topics/packagesTopic.ts index b87202b6318..cbebfe365de 100644 --- a/packages/live-status-gateway/src/topics/packagesTopic.ts +++ b/packages/live-status-gateway/src/topics/packagesTopic.ts @@ -1,6 +1,6 @@ import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { UIPieceContentStatus } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { assertNever } from '@sofie-automation/server-core-integration' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' diff --git a/packages/live-status-gateway/src/topics/resolvedPlaylistTopic.ts b/packages/live-status-gateway/src/topics/resolvedPlaylistTopic.ts new file mode 100644 index 00000000000..c8a1ce5a3ef --- /dev/null +++ b/packages/live-status-gateway/src/topics/resolvedPlaylistTopic.ts @@ -0,0 +1,147 @@ +import { Logger } from 'winston' +import { WebSocket } from 'ws' +import { WebSocketTopicBase, WebSocketTopic } from '../wsHandler.js' +import { CollectionHandlers } from '../liveStatusServer.js' +import { toResolvedPlaylistStatus } from './helpers/resolvedPlaylistConversion/events/toResolvedPlaylistStatus.js' +import type { ToResolvedPlaylistStatusProps } from './helpers/resolvedPlaylistConversion/context/conversionContext.js' +import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { DBPartInstance, PartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { PartInstancesInPlaylist } from '../collections/partInstancesInPlaylistHandler.js' +import { ShowStyleBaseExt } from '../collections/showStyleBaseHandler.js' +import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' + +const THROTTLE_PERIOD_MS = 100 + +const PLAYLIST_KEYS = [ + '_id', + 'studioId', + 'created', + 'modified', + 'externalId', + 'activationId', + 'rehearsal', + 'name', + 'rundownIdsInOrder', + 'quickLoop', + 'currentPartInfo', + 'nextPartInfo', + 'previousPartInfo', + 'publicData', + 'publicPlayoutPersistentState', + 'timing', + 'tTimers', +] as const +type PlaylistState = Pick + +/** + * Aggregates playlist-scoped collections and publishes the `resolvedPlaylist` topic. + * Data is cached locally and merged into a single event on every source update. + */ +export class ResolvedPlaylistTopic extends WebSocketTopicBase implements WebSocketTopic { + private _playlist: PlaylistState | undefined + private _rundowns: DBRundown[] = [] + private _segments: DBSegment[] = [] + private _parts: DBPart[] = [] + private _partInstancesInPlaylist: DBPartInstance[] = [] + private _showStyleBaseExt: ShowStyleBaseExt | undefined + private _showStyleBaseExtsById: ReadonlyMap = new Map() + private _piecesInPlaylist: Piece[] = [] + private _pieceInstancesInPlaylist: PieceInstance[] = [] + + constructor(logger: Logger, handlers: CollectionHandlers) { + super('resolvedPlaylist', logger, THROTTLE_PERIOD_MS) + + handlers.playlistHandler.subscribe(this.onPlaylistUpdate, PLAYLIST_KEYS) + handlers.rundownsHandler.subscribe(this.onRundownsUpdate) + handlers.showStyleBaseHandler.subscribe(this.onShowStyleBaseUpdate) + handlers.showStyleBasesHandler.subscribe(this.onShowStyleBasesUpdate) + handlers.segmentsHandler.subscribe(this.onSegmentsUpdate) + handlers.partsHandler.subscribe(this.onPartsUpdate) + handlers.partInstancesInPlaylistHandler.subscribe(this.onPartInstancesInPlaylistUpdate, ['all']) + handlers.piecesInPlaylistHandler.subscribe(this.onPiecesInPlaylistUpdate) + handlers.pieceInstancesInPlaylistHandler.subscribe(this.onPieceInstancesInPlaylistUpdate) + } + + /** Builds and publishes the current resolved playlist snapshot to all subscribers. */ + sendStatus(subscribers: Iterable): void { + if (!this._playlist || !this._showStyleBaseExt) return + + const message = toResolvedPlaylistStatus({ + playlistState: this._playlist, + rundownsState: this._rundowns, + showStyleBaseExtState: this._showStyleBaseExt, + showStyleBaseExtsByIdState: this._showStyleBaseExtsById, + segmentsState: this._segments, + partsState: this._parts, + partInstancesInPlaylistState: this._partInstancesInPlaylist as PartInstance[], + piecesInPlaylistState: this._piecesInPlaylist, + pieceInstancesInPlaylistState: this._pieceInstancesInPlaylist, + logger: this._logger, + }) + + this.sendMessage(subscribers, message) + } + + private onPlaylistUpdate = (playlist: PlaylistState | undefined): void => { + this.logUpdateReceived('playlist', `rundownPlaylistId ${playlist?._id}`) + this._playlist = playlist + this.throttledSendStatusToAll() + } + + private onRundownsUpdate = (rundowns: ToResolvedPlaylistStatusProps['rundownsState'] | undefined): void => { + this.logUpdateReceived('rundowns', `${rundowns?.length ?? 0} rundowns`) + this._rundowns = rundowns ?? [] + this.throttledSendStatusToAll() + } + + private onShowStyleBaseUpdate = (showStyleBase: ToResolvedPlaylistStatusProps['showStyleBaseExtState']): void => { + this.logUpdateReceived('showStyleBase') + this._showStyleBaseExt = showStyleBase + this.throttledSendStatusToAll() + } + + private onShowStyleBasesUpdate = (showStyleBases: ShowStyleBaseExt[] | undefined): void => { + this.logUpdateReceived('showStyleBases', `${showStyleBases?.length ?? 0} showStyleBases`) + this._showStyleBaseExtsById = new Map((showStyleBases ?? []).map((showStyle) => [showStyle._id, showStyle])) + this.throttledSendStatusToAll() + } + + private onSegmentsUpdate = (segments: ToResolvedPlaylistStatusProps['segmentsState'] | undefined): void => { + this.logUpdateReceived('segments') + this._segments = segments ?? [] + this.throttledSendStatusToAll() + } + + private onPartsUpdate = (parts: ToResolvedPlaylistStatusProps['partsState'] | undefined): void => { + this.logUpdateReceived('parts', `${parts?.length ?? 0} parts`) + this._parts = parts ?? [] + this.throttledSendStatusToAll() + } + + private onPartInstancesInPlaylistUpdate = (data: Pick | undefined): void => { + this.logUpdateReceived('partInstancesInPlaylist') + this._partInstancesInPlaylist = data?.all ?? [] + this.throttledSendStatusToAll() + } + + private onPiecesInPlaylistUpdate = ( + pieces: ToResolvedPlaylistStatusProps['piecesInPlaylistState'] | undefined + ): void => { + this.logUpdateReceived('piecesInPlaylist', `${pieces?.length ?? 0} pieces`) + this._piecesInPlaylist = pieces ?? [] + this.throttledSendStatusToAll() + } + + private onPieceInstancesInPlaylistUpdate = ( + pieceInstances: ToResolvedPlaylistStatusProps['pieceInstancesInPlaylistState'] | undefined + ): void => { + this.logUpdateReceived('pieceInstancesInPlaylist', `${pieceInstances?.length ?? 0} pieceInstances`) + this._pieceInstancesInPlaylist = pieceInstances ?? [] + this.throttledSendStatusToAll() + } +} diff --git a/packages/live-status-gateway/src/topics/root.ts b/packages/live-status-gateway/src/topics/root.ts index 807af7ad1cf..5215ba6031b 100644 --- a/packages/live-status-gateway/src/topics/root.ts +++ b/packages/live-status-gateway/src/topics/root.ts @@ -10,6 +10,7 @@ import { SubscriptionStatus, SubscriptionName, } from '@sofie-automation/live-status-gateway-api' +import { activeSubscriptionsGauge, subscriptionSubscribersGauge } from '../wsMetrics.js' enum PublishMsg { ping = 'ping', @@ -41,6 +42,7 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic { removeSubscriber(ws: WebSocket): void { super.removeSubscriber(ws) this._topics.forEach((h) => h.removeSubscriber(ws)) + this._updateSubscriptionMetrics() } processMessage(ws: WebSocket, msg: object): void { @@ -74,6 +76,16 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic { if (Object.values(SubscriptionName).includes(channel)) this._topics.set(channel, topic) } + private _updateSubscriptionMetrics(): void { + let total = 0 + for (const [name, topic] of this._topics) { + const count = topic.subscriberCount + subscriptionSubscribersGauge.set({ subscription: name }, count) + total += count + } + activeSubscriptionsGauge.set(total) + } + subscribe(ws: WebSocket, name: SubscriptionName, reqid: number): void { const topic = this._topics.get(name) const curUnsubscribed = @@ -91,6 +103,7 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic { }) ) topic.addSubscriber(ws) + this._updateSubscriptionMetrics() } else { this.sendMessage( ws, @@ -112,6 +125,7 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic { const curSubscribed = topic && topic.hasSubscriber(ws) && Object.values(SubscriptionName).includes(name) if (curSubscribed) { topic.removeSubscriber(ws) + this._updateSubscriptionMetrics() this.sendMessage( ws, literal({ diff --git a/packages/live-status-gateway/src/topics/segmentsTopic.ts b/packages/live-status-gateway/src/topics/segmentsTopic.ts index d7171d6af1e..2d10c3386dd 100644 --- a/packages/live-status-gateway/src/topics/segmentsTopic.ts +++ b/packages/live-status-gateway/src/topics/segmentsTopic.ts @@ -1,6 +1,6 @@ import { Logger } from 'winston' import { WebSocket } from 'ws' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { WebSocketTopicBase, WebSocketTopic } from '../wsHandler.js' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { groupByToMap } from '@sofie-automation/corelib/dist/lib' diff --git a/packages/live-status-gateway/src/topics/studioTopic.ts b/packages/live-status-gateway/src/topics/studioTopic.ts index d1f533b8f67..73b224000ef 100644 --- a/packages/live-status-gateway/src/topics/studioTopic.ts +++ b/packages/live-status-gateway/src/topics/studioTopic.ts @@ -2,7 +2,7 @@ import { Logger } from 'winston' import { WebSocket } from 'ws' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' import { StudioEvent, PlaylistStatus, PlaylistActivationStatus } from '@sofie-automation/live-status-gateway-api' import { WebSocketTopicBase, WebSocketTopic } from '../wsHandler.js' diff --git a/packages/live-status-gateway/src/wsHandler.ts b/packages/live-status-gateway/src/wsHandler.ts index b3d7f36b761..fef08aa3c3c 100644 --- a/packages/live-status-gateway/src/wsHandler.ts +++ b/packages/live-status-gateway/src/wsHandler.ts @@ -23,6 +23,10 @@ export abstract class WebSocketTopicBase { : this.sendStatusToAll } + get subscriberCount(): number { + return this._subscribers.size + } + addSubscriber(ws: WebSocket): void { this._logger.info(`${this._name} adding a websocket subscriber`) this._subscribers.add(ws) @@ -77,6 +81,7 @@ export abstract class WebSocketTopicBase { } export interface WebSocketTopic { + subscriberCount: number addSubscriber(ws: WebSocket): void hasSubscriber(ws: WebSocket): boolean removeSubscriber(ws: WebSocket): void diff --git a/packages/live-status-gateway/src/wsMetrics.ts b/packages/live-status-gateway/src/wsMetrics.ts new file mode 100644 index 00000000000..b07130769a0 --- /dev/null +++ b/packages/live-status-gateway/src/wsMetrics.ts @@ -0,0 +1,17 @@ +import { MetricsGauge } from '@sofie-automation/server-core-integration' + +export const wsConnectionsGauge = new MetricsGauge({ + name: 'sofie_lsg_websocket_connections', + help: 'Number of open WebSocket connections', +}) + +export const activeSubscriptionsGauge = new MetricsGauge({ + name: 'sofie_lsg_active_subscriptions_total', + help: 'Total number of active subscriptions across all topics', +}) + +export const subscriptionSubscribersGauge = new MetricsGauge({ + name: 'sofie_lsg_subscription_subscribers', + help: 'Number of subscribers per subscription', + labelNames: ['subscription'] as const, +}) diff --git a/packages/live-status-gateway/tsconfig.build.json b/packages/live-status-gateway/tsconfig.build.json index 32e9d14c3c7..4a550821578 100644 --- a/packages/live-status-gateway/tsconfig.build.json +++ b/packages/live-status-gateway/tsconfig.build.json @@ -15,7 +15,8 @@ "skipLibCheck": true, "esModuleInterop": true, "declaration": true, - "composite": true + "composite": true, + "module": "node20" }, "references": [ { "path": "../shared-lib/tsconfig.build.json" }, diff --git a/packages/meteor-lib/jest.config.js b/packages/meteor-lib/jest.config.js index 04b8ea8dd1c..a992fb8e2d2 100644 --- a/packages/meteor-lib/jest.config.js +++ b/packages/meteor-lib/jest.config.js @@ -9,12 +9,15 @@ module.exports = { diagnostics: { ignoreCodes: [ 151002, // hybrid module kind (Node16/18/Next) + 2823, // Import attributes not supported in CJS mode (ts-jest forces CJS, emits require() anyway) ], }, }, ], }, moduleNameMapper: { + '^@sofie-automation/shared-lib/dist/(.+)\\.js$': '/../shared-lib/src/$1', + '^@sofie-automation/shared-lib/dist/(.+)$': '/../shared-lib/src/$1', '(.+)\\.js$': '$1', }, testMatch: ['**/__tests__/**/*.(spec|test).(ts|js)'], diff --git a/packages/meteor-lib/package.json b/packages/meteor-lib/package.json index 45f975e4a33..3e0ec5f3b80 100644 --- a/packages/meteor-lib/package.json +++ b/packages/meteor-lib/package.json @@ -41,9 +41,9 @@ "@sofie-automation/corelib": "26.3.0-2", "@sofie-automation/shared-lib": "26.3.0-2", "deep-extend": "0.6.0", - "semver": "^7.7.3", + "semver": "^7.7.4", "type-fest": "^4.41.0", - "underscore": "^1.13.7" + "underscore": "^1.13.8" }, "devDependencies": { "@types/deep-extend": "^0.6.2", @@ -51,9 +51,9 @@ "@types/underscore": "^1.13.0" }, "peerDependencies": { - "i18next": "^21.10.0", - "mongodb": "^6.12.0" + "i18next": "^26.0.4", + "mongodb": "^7.1.1" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/packages/meteor-lib/src/api/userActions.ts b/packages/meteor-lib/src/api/userActions.ts index 9f4c89afd01..8ced15f220f 100644 --- a/packages/meteor-lib/src/api/userActions.ts +++ b/packages/meteor-lib/src/api/userActions.ts @@ -30,7 +30,7 @@ import { SnapshotId, StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { QuickLoopMarker } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { QuickLoopMarker } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' export interface NewUserActionAPI { take( diff --git a/packages/meteor-lib/src/collections/RundownLayouts.ts b/packages/meteor-lib/src/collections/RundownLayouts.ts index 3d26dcbaba5..864155ed63b 100644 --- a/packages/meteor-lib/src/collections/RundownLayouts.ts +++ b/packages/meteor-lib/src/collections/RundownLayouts.ts @@ -14,7 +14,6 @@ export enum RundownLayoutType { RUNDOWN_VIEW_LAYOUT = 'rundown_view_layout', RUNDOWN_LAYOUT = 'rundown_layout', DASHBOARD_LAYOUT = 'dashboard_layout', - RUNDOWN_HEADER_LAYOUT = 'rundown_header_layout', MINI_SHELF_LAYOUT = 'mini_shelf_layout', CLOCK_PRESENTER_VIEW_LAYOUT = 'clock_presenter_view_layout', } @@ -23,7 +22,6 @@ export enum CustomizableRegions { RundownView = 'rundown_view_layouts', Shelf = 'shelf_layouts', MiniShelf = 'mini_shelf_layouts', - RundownHeader = 'rundown_header_layouts', PresenterView = 'presenter_view_layouts', } @@ -47,15 +45,12 @@ export enum RundownLayoutElementType { PLAYLIST_START_TIMER = 'playlist_start_timer', PLAYLIST_END_TIMER = 'playlist_end_timer', NEXT_BREAK_TIMING = 'next_break_timing', - END_WORDS = 'end_words', SEGMENT_TIMING = 'segment_timing', PART_TIMING = 'part_timing', TEXT_LABEL = 'text_label', PLAYLIST_NAME = 'playlist_name', STUDIO_NAME = 'studio_name', TIME_OF_DAY = 'time_of_day', - SYSTEM_STATUS = 'system_status', - SHOWSTYLE_DISPLAY = 'showstyle_display', SEGMENT_NAME = 'segment_name', PART_NAME = 'part_name', COLORED_BOX = 'colored_box', @@ -155,11 +150,6 @@ export interface RundownLayoutNextBreakTiming extends RundownLayoutElementBase { type: RundownLayoutElementType.NEXT_BREAK_TIMING } -export interface RundownLayoutEndWords extends RundownLayoutElementBase, RequiresActiveLayers { - type: RundownLayoutElementType.PLAYLIST_END_TIMER - hideLabel: boolean -} - export interface RundownLayoutSegmentTiming extends RundownLayoutElementBase, RequiresActiveLayers { type: RundownLayoutElementType.SEGMENT_TIMING timingType: 'count_down' | 'count_up' @@ -192,14 +182,6 @@ export interface RundownLayoutTimeOfDay extends RundownLayoutElementBase { hideLabel: boolean } -export interface RundownLayoutSytemStatus extends RundownLayoutElementBase { - type: RundownLayoutElementType.SYSTEM_STATUS -} - -export interface RundownLayoutShowStyleDisplay extends RundownLayoutElementBase { - type: RundownLayoutElementType.SHOWSTYLE_DISPLAY -} - export interface RundownLayoutSegmentName extends RundownLayoutElementBase { type: RundownLayoutElementType.SEGMENT_NAME segment: 'current' | 'next' @@ -287,15 +269,12 @@ export type DashboardLayoutNextInfo = DashboardPanel export type DashboardLayoutPlaylistStartTimer = DashboardPanel export type DashboardLayoutNextBreakTiming = DashboardPanel export type DashboardLayoutPlaylistEndTimer = DashboardPanel -export type DashboardLayoutEndsWords = DashboardPanel export type DashboardLayoutSegmentCountDown = DashboardPanel export type DashboardLayoutPartCountDown = DashboardPanel export type DashboardLayoutTextLabel = DashboardPanel export type DashboardLayoutPlaylistName = DashboardPanel export type DashboardLayoutStudioName = DashboardPanel export type DashboardLayoutTimeOfDay = DashboardPanel -export type DashboardLayoutSystemStatus = DashboardPanel -export type DashboardLayoutShowStyleDisplay = DashboardPanel export type DashboardLayoutSegmentName = DashboardPanel export type DashboardLayoutPartName = DashboardPanel export type DashboardLayoutColoredBox = DashboardPanel @@ -349,7 +328,6 @@ export interface RundownViewLayout extends RundownLayoutBase { exposeAsSelectableLayout: boolean shelfLayout: RundownLayoutId miniShelfLayout: RundownLayoutId - rundownHeaderLayout: RundownLayoutId liveLineProps?: RequiresActiveLayers /** Hide the rundown divider header in playlists */ hideRundownDivider: boolean @@ -382,18 +360,6 @@ export interface RundownLayout extends RundownLayoutShelfBase { type: RundownLayoutType.RUNDOWN_LAYOUT } -export interface RundownLayoutRundownHeader extends RundownLayoutBase { - type: RundownLayoutType.RUNDOWN_HEADER_LAYOUT - plannedEndText: string - nextBreakText: string - /** When true, hide the Planned End timer when there is a rundown marked as a break in the future */ - hideExpectedEndBeforeBreak: boolean - /** When a rundown is marked as a break, show the Next Break timing */ - showNextBreakTiming: boolean - /** If true, don't treat the last rundown as a break even if it's marked as one */ - lastRundownIsNotBreak: boolean -} - export interface RundownLayoutPresenterView extends RundownLayoutBase { type: RundownLayoutType.CLOCK_PRESENTER_VIEW_LAYOUT } diff --git a/packages/meteor-lib/src/triggers/actionFactory.ts b/packages/meteor-lib/src/triggers/actionFactory.ts index 86ef61684f3..ddc6dc42fc4 100644 --- a/packages/meteor-lib/src/triggers/actionFactory.ts +++ b/packages/meteor-lib/src/triggers/actionFactory.ts @@ -9,7 +9,7 @@ import { Time, } from '@sofie-automation/blueprints-integration' import { TFunction } from 'i18next' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import RundownViewEventBus, { RundownViewEvents } from '../triggers/RundownViewEventBus.js' diff --git a/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts b/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts index 9f8720c49f0..20eee62ebe7 100644 --- a/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts +++ b/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts @@ -13,7 +13,7 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' diff --git a/packages/meteor-lib/src/triggers/triggersContext.ts b/packages/meteor-lib/src/triggers/triggersContext.ts index 74e19856104..fd2f1e1a83b 100644 --- a/packages/meteor-lib/src/triggers/triggersContext.ts +++ b/packages/meteor-lib/src/triggers/triggersContext.ts @@ -8,7 +8,7 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { LoggerInstanceFixed } from '@sofie-automation/corelib/dist/logging' diff --git a/packages/meteor-lib/tsconfig.build.json b/packages/meteor-lib/tsconfig.build.json index 27eaa1ca650..f0170e5e9a7 100755 --- a/packages/meteor-lib/tsconfig.build.json +++ b/packages/meteor-lib/tsconfig.build.json @@ -3,7 +3,7 @@ "include": ["src/**/*.ts"], "exclude": ["node_modules/**", "**/*spec.ts", "**/__tests__/*", "**/__mocks__/*"], "compilerOptions": { - "target": "es2019", + "target": "es2024", "outDir": "./dist", "rootDir": "./src", "baseUrl": "./", @@ -14,7 +14,8 @@ "resolveJsonModule": true, "types": ["node"], "esModuleInterop": true, - "composite": true + "composite": true, + "module": "node20" }, "references": [ // diff --git a/packages/mos-gateway/jest.config.js b/packages/mos-gateway/jest.config.js index 04b8ea8dd1c..e9a18c183b8 100644 --- a/packages/mos-gateway/jest.config.js +++ b/packages/mos-gateway/jest.config.js @@ -9,12 +9,17 @@ module.exports = { diagnostics: { ignoreCodes: [ 151002, // hybrid module kind (Node16/18/Next) + 2823, // Import attributes not supported in CJS mode (ts-jest forces CJS, emits require() anyway) ], }, }, ], }, moduleNameMapper: { + // Jest is not happy with esm modules, we need to point it to the source files instead + '^@sofie-automation/shared-lib/dist/(.+)\\.js$': '/../shared-lib/src/$1', + '^@sofie-automation/shared-lib/dist/(.+)$': '/../shared-lib/src/$1', + '^@sofie-automation/server-core-integration$': '/../server-core-integration/src/index.ts', '(.+)\\.js$': '$1', }, testMatch: ['**/__tests__/**/*.(spec|test).(ts|js)'], diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index a4a4b198d34..22d37992b73 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -65,9 +65,9 @@ "@sofie-automation/shared-lib": "26.3.0-2", "tslib": "^2.8.1", "type-fest": "^4.41.0", - "underscore": "^1.13.7", + "underscore": "^1.13.8", "winston": "^3.19.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/packages/mos-gateway/src/CoreMosDeviceHandler.ts b/packages/mos-gateway/src/CoreMosDeviceHandler.ts index faff9a50a79..879ed977343 100644 --- a/packages/mos-gateway/src/CoreMosDeviceHandler.ts +++ b/packages/mos-gateway/src/CoreMosDeviceHandler.ts @@ -5,6 +5,8 @@ import { Observer, PeripheralDevicePubSub, stringifyError, + CoreConnectionChild, + Queue, } from '@sofie-automation/server-core-integration' import { IMOSConnectionStatus, @@ -33,8 +35,12 @@ import _ from 'underscore' import { MosHandler } from './mosHandler.js' import { PartialDeep } from 'type-fest' import type { CoreHandler } from './coreHandler.js' -import { CoreConnectionChild } from '@sofie-automation/server-core-integration/dist/lib/CoreConnectionChild' -import { Queue } from '@sofie-automation/server-core-integration/dist/lib/queue' +import { + mosDeviceConnectedGauge, + mosMessagesFailedCounter, + mosMessagesReceivedCounter, + mosQueueDepthGauge, +} from './mosMetrics.js' function deepMatch(object: any, attrs: any, deep: boolean): boolean { const keys = Object.keys(attrs) @@ -146,7 +152,7 @@ export class CoreMosDeviceHandler { } onMosConnectionChanged(connectionStatus: IMOSConnectionStatus): void { let statusCode: StatusCode - const messages: Array = [] + const statusDetails: Array<{ message: string }> = [] if (this._options.openMediaHotStandby) { // OpenMedia treats secondary server as hot-standby @@ -157,12 +163,12 @@ export class CoreMosDeviceHandler { // Primary not connected is only bad if there is no secondary: if (connectionStatus.SecondaryConnected) { statusCode = StatusCode.GOOD - messages.push(connectionStatus.SecondaryStatus || 'Running NRCS on hot standby') + statusDetails.push({ message: connectionStatus.SecondaryStatus || 'Running NRCS on hot standby' }) } else { statusCode = StatusCode.BAD // Send messages for both connections - messages.push(connectionStatus.PrimaryStatus || 'Primary and hot standby are not connected') - messages.push(connectionStatus.SecondaryStatus || 'Primary and hot standby are not connected') + statusDetails.push({ message: connectionStatus.PrimaryStatus || 'Primary and hot standby are not connected' }) + statusDetails.push({ message: connectionStatus.SecondaryStatus || 'Primary and hot standby are not connected' }) } } } else { @@ -184,19 +190,31 @@ export class CoreMosDeviceHandler { } if (!connectionStatus.PrimaryConnected) { - messages.push(connectionStatus.PrimaryStatus || 'Primary not connected') + statusDetails.push({ message: connectionStatus.PrimaryStatus || 'Primary not connected' }) } if (this._mosDevice.idSecondary && !connectionStatus.SecondaryConnected) { - messages.push(connectionStatus.SecondaryStatus || 'Fallback not connected') + statusDetails.push({ message: connectionStatus.SecondaryStatus || 'Fallback not connected' }) } } this.core .setStatus({ statusCode: statusCode, - messages: messages, + statusDetails, }) .catch((e) => this._coreParentHandler.logger.warn('Error when setting status:' + e)) + + const deviceId = this._mosDevice.idPrimary + mosDeviceConnectedGauge.set( + { device_id: deviceId, connection: 'primary' }, + connectionStatus.PrimaryConnected ? 1 : 0 + ) + if (this._mosDevice.idSecondary) { + mosDeviceConnectedGauge.set( + { device_id: deviceId, connection: 'secondary' }, + connectionStatus.SecondaryConnected ? 1 : 0 + ) + } } async getMachineInfo(): Promise { const info: IMOSListMachInfo = { @@ -432,7 +450,7 @@ export class CoreMosDeviceHandler { await this.core.setStatus({ statusCode: StatusCode.BAD, - messages: ['Uninitialized'], + statusDetails: [{ message: 'Uninitialized' }], }) if (subdevice === 'removeSubDevice') await this.core.unInitialize() @@ -456,8 +474,15 @@ export class CoreMosDeviceHandler { return this.fixMosData(attr) }) as any + const deviceId = this._mosDevice.idPrimary + const commandName = methodName as string + mosMessagesReceivedCounter.inc({ device_id: deviceId, command: commandName }) + mosQueueDepthGauge.inc({ device_id: deviceId }) + // Make the commands be sent sequantially: return this._messageQueue.putOnQueue(async () => { + mosQueueDepthGauge.dec({ device_id: deviceId }) + // Log info about the sent command: let msg = 'Command: ' + methodName const attr0 = attrs[0] as any | undefined @@ -476,6 +501,7 @@ export class CoreMosDeviceHandler { const res = (this.core.coreMethods[methodName] as any)(...attrs) return res.catch((e: any) => { this._coreParentHandler.logger.info('MOS command rejected: ' + ((e && JSON.stringify(e)) || e)) + mosMessagesFailedCounter.inc({ device_id: deviceId, command: commandName }) throw e }) }) diff --git a/packages/mos-gateway/src/connector.ts b/packages/mos-gateway/src/connector.ts index f7fa4d7dde1..ec0553b424d 100644 --- a/packages/mos-gateway/src/connector.ts +++ b/packages/mos-gateway/src/connector.ts @@ -3,7 +3,7 @@ import { CoreHandler, CoreConfig } from './coreHandler.js' import * as Winston from 'winston' import { PeripheralDeviceId, - loadCertificatesFromDisk, + loadDDPTLSOptions, CertificatesConfig, stringifyError, HealthConfig, @@ -40,14 +40,14 @@ export class Connector implements IConnector { try { this._logger.info('Initializing Process...') - const certificates = loadCertificatesFromDisk(this._logger, config.certificates) + const tlsOptions = loadDDPTLSOptions(this._logger, config.certificates) this._logger.info('Process initialized') this._logger.info('Initializing Core...') this.coreHandler = await CoreHandler.create( this._logger, this._config.core, - certificates, + tlsOptions, this._config.device ) diff --git a/packages/mos-gateway/src/coreHandler.ts b/packages/mos-gateway/src/coreHandler.ts index 2fbbf479d00..2157b7fb68b 100644 --- a/packages/mos-gateway/src/coreHandler.ts +++ b/packages/mos-gateway/src/coreHandler.ts @@ -2,6 +2,7 @@ import { CoreConnection, CoreOptions, DDPConnectorOptions, + DDPTLSOptions, Observer, PeripheralDeviceAPI, PeripheralDeviceCommand, @@ -34,7 +35,6 @@ export class CoreHandler implements ICoreHandler { core: CoreConnection | undefined logger: Winston.Logger public _observers: Array> = [] - public connectedToCore = false private _deviceOptions: DeviceConfig private _coreMosHandlers: Array = [] private _onConnected?: () => any @@ -42,16 +42,19 @@ export class CoreHandler implements ICoreHandler { private _isDestroyed = false private _executedFunctions = new Set() private _coreConfig?: CoreConfig - private _certificates?: Buffer[] + + public get connectedToCore(): boolean { + return !!this.core && this.core.connected + } public static async create( logger: Winston.Logger, config: CoreConfig, - certificates: Buffer[], + tlsOptions: DDPTLSOptions, deviceOptions: DeviceConfig ): Promise { const handler = new CoreHandler(logger, deviceOptions) - await handler.init(config, certificates) + await handler.init(config, tlsOptions) return handler } @@ -60,20 +63,17 @@ export class CoreHandler implements ICoreHandler { this._deviceOptions = deviceOptions } - private async init(config: CoreConfig, certificates: Buffer[]): Promise { + private async init(config: CoreConfig, tlsOptions: DDPTLSOptions): Promise { // this.logger.info('========') this._coreConfig = config - this._certificates = certificates this.core = new CoreConnection(this.getCoreConnectionOptions()) this.core.onConnected(() => { this.logger.info('Core Connected!') - this.connectedToCore = true if (this._isInitialized) this.onConnectionRestored() }) this.core.onDisconnected(() => { this.logger.info('Core Disconnected!') - this.connectedToCore = false }) this.core.onError((err) => { this.logger.error('Core Error: ' + (typeof err === 'string' ? err : err.message || err.toString())) @@ -82,11 +82,7 @@ export class CoreHandler implements ICoreHandler { const ddpConfig: DDPConnectorOptions = { host: config.host, port: config.port, - } - if (this._certificates?.length) { - ddpConfig.tlsOpts = { - ca: this._certificates, - } + tlsOpts: tlsOptions, } await this.core.init(ddpConfig) @@ -96,24 +92,21 @@ export class CoreHandler implements ICoreHandler { await this.updateCoreStatus() } - getCoreStatus(): { - statusCode: StatusCode - messages: string[] - } { + getCoreStatus(): PeripheralDeviceAPI.PeripheralDeviceStatusObject { let statusCode = StatusCode.GOOD - const messages: string[] = [] + const statusDetails: Array<{ message: string }> = [] if (!this._isInitialized) { statusCode = StatusCode.BAD - messages.push('Starting up...') + statusDetails.push({ message: 'Starting up...' }) } if (this._isDestroyed) { statusCode = StatusCode.FATAL - messages.push('Shut down') + statusDetails.push({ message: 'Shut down' }) } return { statusCode, - messages, + statusDetails, } } async updateCoreStatus(): Promise { diff --git a/packages/mos-gateway/src/mosHandler.ts b/packages/mos-gateway/src/mosHandler.ts index c6337a072b2..8055afd3421 100644 --- a/packages/mos-gateway/src/mosHandler.ts +++ b/packages/mos-gateway/src/mosHandler.ts @@ -40,6 +40,7 @@ import { PeripheralDeviceForDevice } from '@sofie-automation/server-core-integra import _ from 'underscore' import { MosStatusHandler } from './mosStatus/handler.js' import { isPromise } from 'util/types' +import { mosDevicesTotalGauge } from './mosMetrics.js' export interface MosConfig { self: IConnectionConfig @@ -80,9 +81,9 @@ export class MosHandler { private _logger: Winston.Logger private _disposed = false private _settings?: MosGatewayConfig - private _coreHandler: CoreHandler | undefined + private _coreHandler!: CoreHandler private _observers: Array> = [] - private _triggerupdateDevicesTimeout: any = null + private _triggerUpdateDevicesTimeout: any = null private mosTypes: MosTypes public static async create( @@ -118,9 +119,6 @@ export class MosHandler { } } */ - if (!coreHandler) { - throw Error('coreHandler is undefined!') - } if (!coreHandler.core) { throw Error('coreHandler.core is undefined!') @@ -132,15 +130,16 @@ export class MosHandler { this.mosTypes = getMosTypes(this.strict) - await this._updateDevices() + await this._initMosConnection() - if (!this._coreHandler) throw Error('_coreHandler is undefined!') - this._coreHandler.onConnected(() => { + coreHandler.onConnected(() => { // This is called whenever a connection to Core has been (re-)established this.setupObservers() this.sendStatusOfAllMosDevices() }) this.setupObservers() + + this.triggerUpdateDevices() } async dispose(): Promise { this._disposed = true @@ -208,10 +207,13 @@ export class MosHandler { this._logger.debug('test log debug') } } - if (this._triggerupdateDevicesTimeout) { - clearTimeout(this._triggerupdateDevicesTimeout) + this.triggerUpdateDevices() + } + private triggerUpdateDevices() { + if (this._triggerUpdateDevicesTimeout) { + clearTimeout(this._triggerUpdateDevicesTimeout) } - this._triggerupdateDevicesTimeout = setTimeout(() => { + this._triggerUpdateDevicesTimeout = setTimeout(() => { this._updateDevices().catch((e) => { this._logger.error(stringifyError(e)) }) @@ -539,6 +541,7 @@ export class MosHandler { mosDevice: mosDevice, deviceOptions, }) + mosDevicesTotalGauge.set(this._allMosDevices.size) await this.setupMosDevice(mosDevice) @@ -592,6 +595,7 @@ export class MosHandler { private async _removeDevice(deviceId: string): Promise { const deviceEntry = this._allMosDevices.get(deviceId) this._allMosDevices.delete(deviceId) + mosDevicesTotalGauge.set(this._allMosDevices.size) if (deviceEntry) { const mosDevice = deviceEntry.mosDevice diff --git a/packages/mos-gateway/src/mosMetrics.ts b/packages/mos-gateway/src/mosMetrics.ts new file mode 100644 index 00000000000..0f6387851db --- /dev/null +++ b/packages/mos-gateway/src/mosMetrics.ts @@ -0,0 +1,48 @@ +import { MetricsCounter, MetricsGauge } from '@sofie-automation/server-core-integration' + +export const mosDevicesTotalGauge = new MetricsGauge({ + name: 'sofie_mos_gateway_devices_total', + help: 'Number of configured MOS sub-devices', +}) + +export const mosDeviceConnectedGauge = new MetricsGauge({ + name: 'sofie_mos_gateway_device_connected', + help: 'Connection status of a MOS device (1 = connected, 0 = disconnected)', + labelNames: ['device_id', 'connection'] as const, +}) + +export const mosMessagesReceivedCounter = new MetricsCounter({ + name: 'sofie_mos_gateway_messages_received_total', + help: 'Total number of MOS commands received from the NRCS', + labelNames: ['device_id', 'command'] as const, +}) + +export const mosMessagesFailedCounter = new MetricsCounter({ + name: 'sofie_mos_gateway_messages_failed_total', + help: 'Total number of MOS commands that failed when forwarding to Core', + labelNames: ['device_id', 'command'] as const, +}) + +export const mosQueueDepthGauge = new MetricsGauge({ + name: 'sofie_mos_gateway_queue_depth', + help: 'Number of MOS commands currently waiting in the Core-forwarding queue', + labelNames: ['device_id'] as const, +}) + +export const mosStatusSentCounter = new MetricsCounter({ + name: 'sofie_mos_gateway_status_sent_total', + help: 'Total number of story/item status messages sent back to the NRCS', + labelNames: ['device_id', 'status_type', 'mos_status'] as const, +}) + +export const mosStatusSkippedCounter = new MetricsCounter({ + name: 'sofie_mos_gateway_status_skipped_total', + help: 'Total number of story/item status updates that were skipped', + labelNames: ['device_id', 'reason'] as const, +}) + +export const mosStatusQueueDepthGauge = new MetricsGauge({ + name: 'sofie_mos_gateway_status_queue_depth', + help: 'Number of status write-back operations currently waiting in the queue', + labelNames: ['device_id'] as const, +}) diff --git a/packages/mos-gateway/src/mosStatus/handler.ts b/packages/mos-gateway/src/mosStatus/handler.ts index 38b89616d93..f8d740b2aa8 100644 --- a/packages/mos-gateway/src/mosStatus/handler.ts +++ b/packages/mos-gateway/src/mosStatus/handler.ts @@ -15,12 +15,13 @@ import { PeripheralDevicePubSubCollectionsNames, stringifyError, SubscriptionId, + Queue, } from '@sofie-automation/server-core-integration' import type { IngestRundownStatus } from '@sofie-automation/shared-lib/dist/ingest/rundownStatus' import type { RundownId } from '@sofie-automation/shared-lib/dist/core/model/Ids' import type * as winston from 'winston' -import { Queue } from '@sofie-automation/server-core-integration/dist/lib/queue' import { diffStatuses } from './diff.js' +import { mosStatusQueueDepthGauge, mosStatusSentCounter, mosStatusSkippedCounter } from '../mosMetrics.js' export class MosStatusHandler { readonly #logger: winston.Logger @@ -98,10 +99,15 @@ export class MosStatusHandler { // New implementation 2022 only sends PLAY, never stop, after getting advice from AP // Reason 1: NRK ENPS "sendt tid" (elapsed time) stopped working in ENPS 8/9 when doing STOP prior to PLAY // Reason 2: there's a delay between the STOP (yellow line disappears) and PLAY (yellow line re-appears), which annoys the users - if (this.#config.onlySendPlay && status.mosStatus !== IMOSObjectStatus.PLAY) continue + if (this.#config.onlySendPlay && status.mosStatus !== IMOSObjectStatus.PLAY) { + mosStatusSkippedCounter.inc({ device_id: this.#mosDevice.idPrimary, reason: 'only_send_play' }) + continue + } + mosStatusQueueDepthGauge.inc({ device_id: this.#mosDevice.idPrimary }) this.#messageQueue .putOnQueue(async () => { + mosStatusQueueDepthGauge.dec({ device_id: this.#mosDevice.idPrimary }) if (this.#isDeviceConnected()) { if (status.type === 'item') { const newStatus: IMOSItemStatus = { @@ -115,6 +121,11 @@ export class MosStatusHandler { // Send status await this.#mosDevice.sendItemStatus(newStatus) + mosStatusSentCounter.inc({ + device_id: this.#mosDevice.idPrimary, + status_type: 'item', + mos_status: String(status.mosStatus), + }) } else if (status.type === 'story') { const newStatus: IMOSStoryStatus = { RunningOrderId: this.#mosTypes.mosString128.create(status.rundownExternalId), @@ -126,6 +137,11 @@ export class MosStatusHandler { // Send status await this.#mosDevice.sendStoryStatus(newStatus) + mosStatusSentCounter.inc({ + device_id: this.#mosDevice.idPrimary, + status_type: 'story', + mos_status: String(status.mosStatus), + }) } else { this.#logger.debug(`Discarding unknown queued status: ${JSON.stringify(status)}`) assertNever(status) @@ -133,8 +149,10 @@ export class MosStatusHandler { } else if (this.#config.onlySendPlay) { // No need to do anything. this.#logger.info(`Not connected, skipping play status: ${JSON.stringify(status)}`) + mosStatusSkippedCounter.inc({ device_id: this.#mosDevice.idPrimary, reason: 'not_connected' }) } else { this.#logger.info(`Not connected, discarding status: ${JSON.stringify(status)}`) + mosStatusSkippedCounter.inc({ device_id: this.#mosDevice.idPrimary, reason: 'not_connected' }) } }) .catch((e) => { diff --git a/packages/mos-gateway/tsconfig.build.json b/packages/mos-gateway/tsconfig.build.json index e415e6edf0d..4f247279b54 100644 --- a/packages/mos-gateway/tsconfig.build.json +++ b/packages/mos-gateway/tsconfig.build.json @@ -15,7 +15,8 @@ "skipLibCheck": true, "esModuleInterop": true, "declaration": true, - "composite": true + "composite": true, + "module": "node20" }, "references": [ // diff --git a/packages/openapi/api/actions.yaml b/packages/openapi/api/actions.yaml index 3cafa46b329..aeac0b7184c 100644 --- a/packages/openapi/api/actions.yaml +++ b/packages/openapi/api/actions.yaml @@ -73,6 +73,24 @@ paths: $ref: 'definitions/playlists.yaml#/resources/sourceLayer' /playlists/{playlistId}/sourceLayer/{sourceLayerId}/sticky: $ref: 'definitions/playlists.yaml#/resources/sourceLayer/sticky' + /playlists/{playlistId}/t-timers/{timerIndex}/countdown: + $ref: 'definitions/playlists.yaml#/resources/tTimerCountdown' + /playlists/{playlistId}/t-timers/{timerIndex}/free-run: + $ref: 'definitions/playlists.yaml#/resources/tTimerFreeRun' + /playlists/{playlistId}/t-timers/{timerIndex}/pause: + $ref: 'definitions/playlists.yaml#/resources/tTimerPause' + /playlists/{playlistId}/t-timers/{timerIndex}/resume: + $ref: 'definitions/playlists.yaml#/resources/tTimerResume' + /playlists/{playlistId}/t-timers/{timerIndex}/restart: + $ref: 'definitions/playlists.yaml#/resources/tTimerRestart' + /playlists/{playlistId}/t-timers/{timerIndex}/projected/clear: + $ref: 'definitions/playlists.yaml#/resources/tTimerProjectedClear' + /playlists/{playlistId}/t-timers/{timerIndex}/projected/anchor-part: + $ref: 'definitions/playlists.yaml#/resources/tTimerProjectedAnchorPart' + /playlists/{playlistId}/t-timers/{timerIndex}/projected/time: + $ref: 'definitions/playlists.yaml#/resources/tTimerProjectedTime' + /playlists/{playlistId}/t-timers/{timerIndex}/projected/duration: + $ref: 'definitions/playlists.yaml#/resources/tTimerProjectedDuration' # studio operations /studios: $ref: 'definitions/studios.yaml#/resources/studios' diff --git a/packages/openapi/api/definitions/buckets.yaml b/packages/openapi/api/definitions/buckets.yaml index 67fa9c20160..d565c82a684 100644 --- a/packages/openapi/api/definitions/buckets.yaml +++ b/packages/openapi/api/definitions/buckets.yaml @@ -95,7 +95,7 @@ resources: type: string responses: 200: - description: Bucket successfuly removed. + description: Bucket successfully removed. content: application/json: schema: @@ -154,7 +154,7 @@ resources: type: string responses: 200: - description: Bucket Adlibs successfuly removed. + description: Bucket Adlibs successfully removed. content: application/json: schema: @@ -188,7 +188,7 @@ resources: type: string responses: 200: - description: Bucket Adlib successfuly removed. + description: Bucket Adlib successfully removed. content: application/json: schema: diff --git a/packages/openapi/api/definitions/playlists.yaml b/packages/openapi/api/definitions/playlists.yaml index cdb34f65a26..08ea8c35b5c 100644 --- a/packages/openapi/api/definitions/playlists.yaml +++ b/packages/openapi/api/definitions/playlists.yaml @@ -789,9 +789,297 @@ resources: example: Rundown must be active! 500: $ref: '#/components/responses/internalServerError' + tTimerCountdown: + post: + operationId: tTimerCountdown + tags: + - playlists + summary: Start a countdown timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + $ref: '#/components/schemas/tTimerIndex' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + duration: + type: number + description: Duration in seconds. + stopAtZero: + type: boolean + description: Whether to stop the timer at zero. + startPaused: + type: boolean + description: Whether to start the timer in a paused state. + required: + - duration + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + + tTimerFreeRun: + post: + operationId: tTimerFreeRun + tags: + - playlists + summary: Start a free-running timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + $ref: '#/components/schemas/tTimerIndex' + requestBody: + content: + application/json: + schema: + type: object + properties: + startPaused: + type: boolean + description: Whether to start the timer in a paused state. + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + tTimerPause: + post: + operationId: tTimerPause + tags: + - playlists + summary: Pause a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + $ref: '#/components/schemas/tTimerIndex' + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + tTimerResume: + post: + operationId: tTimerResume + tags: + - playlists + summary: Resume a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + $ref: '#/components/schemas/tTimerIndex' + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + tTimerRestart: + post: + operationId: tTimerRestart + tags: + - playlists + summary: Restart a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + $ref: '#/components/schemas/tTimerIndex' + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + + tTimerProjectedClear: + post: + operationId: tTimerProjectedClear + tags: + - playlists + summary: Clear the projection state of a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + $ref: '#/components/schemas/tTimerIndex' + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + + tTimerProjectedAnchorPart: + post: + operationId: tTimerProjectedAnchorPart + tags: + - playlists + summary: Set the anchor part for automatic projection calculation. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + $ref: '#/components/schemas/tTimerIndex' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/tTimerProjectedAnchorPartRequest' + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + + tTimerProjectedTime: + post: + operationId: tTimerProjectedTime + tags: + - playlists + summary: Set the timer projection as an absolute unix timestamp. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + $ref: '#/components/schemas/tTimerIndex' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/tTimerProjectedTimeRequest' + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + + tTimerProjectedDuration: + post: + operationId: tTimerProjectedDuration + tags: + - playlists + summary: Set the timer projection as a duration from now. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + $ref: '#/components/schemas/tTimerIndex' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/tTimerProjectedDurationRequest' + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' components: schemas: + tTimerIndex: + type: integer + description: Index of the timer (1, 2, or 3). + enum: [1, 2, 3] playlistItem: type: object properties: @@ -803,6 +1091,52 @@ components: - id - externalId additionalProperties: false + + tTimerProjectedAnchorPartRequest: + type: object + additionalProperties: false + description: | + Set the anchor part for automatic projection calculation.\n + Provide either `partId` (internal PartId) or `externalId` (ingest part externalId).\n + If `partId` is provided and it does not match an internal PartId, it may be treated as an externalId. + properties: + partId: + type: string + description: Internal PartId (or part externalId). + externalId: + type: string + description: Part externalId. + anyOf: + - required: [partId] + - required: [externalId] + example: + externalId: myPartExternalId + + tTimerProjectedTimeRequest: + type: object + additionalProperties: false + properties: + time: + type: number + description: Unix timestamp (milliseconds) for the projection target. + example: 1707024000000 + paused: + type: boolean + description: When true, the projection is treated as paused (does not change as time passes). + required: [time] + + tTimerProjectedDurationRequest: + type: object + additionalProperties: false + properties: + duration: + type: number + description: Duration in milliseconds from now for the projection target. + example: 60000 + paused: + type: boolean + description: When true, the projection is treated as paused (does not change as time passes). + required: [duration] responses: putSuccess: description: Action successfully sent diff --git a/packages/openapi/package.json b/packages/openapi/package.json index f91fc7aee31..07d84527090 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -14,12 +14,13 @@ "cov": "run unit && open-cli coverage/lcov-report/index.html", "cov-open": "open-cli coverage/lcov-report/index.html", "lint": "run -T lint openapi", + "bundle": "node ./scripts/bundle-openapi.mjs", "unit": "run genserver && node --experimental-fetch run_server_tests.mjs", - "genclient:ts": "run -T rimraf client/ts && openapi-generator-cli generate -i ./api/actions.yaml -o client/ts -g typescript-fetch -p supportsES6=true", - "genclient:rs": "run -T rimraf client/rs && openapi-generator-cli generate -i ./api/actions.yaml -o client/rs -g rust", - "genclient:cs": "run -T rimraf client/cs && openapi-generator-cli generate -i ./api/actions.yaml -o client/cs -g csharp", - "gendocs": "run -T rimraf docs && node install_swagger.mjs && java -jar ./jars/swagger-codegen-cli.jar generate -i ./api/actions.yaml -l html2 -o ./docs", - "genserver": "run -T rimraf server && node install_swagger.mjs && java -jar ./jars/swagger-codegen-cli.jar generate -i ./api/actions.yaml -l nodejs-server -o server && cd server && npm install && cd ../", + "genclient:ts": "run bundle && run -T rimraf client/ts && openapi-generator-cli generate -i ./api/actions.yaml -o client/ts -g typescript-fetch -p supportsES6=true", + "genclient:rs": "run bundle && run -T rimraf client/rs && openapi-generator-cli generate -i ./api/actions.yaml -o client/rs -g rust", + "genclient:cs": "run bundle && run -T rimraf client/cs && openapi-generator-cli generate -i ./api/actions.yaml -o client/cs -g csharp", + "gendocs": "run bundle && run -T rimraf docs && node install_swagger.mjs && java -jar ./jars/swagger-codegen-cli.jar generate -i ./api/actions.yaml -l html2 -o ./docs", + "genserver": "run bundle && run -T rimraf server && node install_swagger.mjs && java -jar ./jars/swagger-codegen-cli.jar generate -i ./api/actions.yaml -l nodejs-server -o server && cd server && npm install && cd ../", "runserver": "run genserver && cd server && node index.js", "test": "run lint && run genclient:ts && run unit", "unit:no-server": "node --experimental-fetch ../node_modules/jest/bin/jest.js --detectOpenHandles --forceExit" @@ -37,13 +38,14 @@ "tslib": "^2.8.1" }, "devDependencies": { - "@openapitools/openapi-generator-cli": "^2.28.0", - "eslint": "^9.39.2", + "@apidevtools/json-schema-ref-parser": "^15.3.5", + "@openapitools/openapi-generator-cli": "^2.31.1", + "eslint": "^9.39.4", "js-yaml": "^4.1.1", "wget-improved": "^3.4.0" }, "publishConfig": { "access": "public" }, - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/packages/openapi/scripts/bundle-openapi.mjs b/packages/openapi/scripts/bundle-openapi.mjs new file mode 100644 index 00000000000..886c6e99e81 --- /dev/null +++ b/packages/openapi/scripts/bundle-openapi.mjs @@ -0,0 +1,35 @@ +import fs from 'fs' +import path from 'path' +import yaml from 'js-yaml' +import $RefParser from '@apidevtools/json-schema-ref-parser' + +const ROOT_FILE = './api/actions.yaml' +const OUTPUT_FILE = './src/generated/openapi.yaml' + +const BANNER = + '# This file was automatically generated using @apidevtools/json-schema-ref-parser\n' + + '# DO NOT MODIFY IT BY HAND. Instead, modify the source OpenAPI schema files,\n' + + '# and run "yarn bundle" (in packages/openapi) to regenerate this file.\n' + +async function main() { + // Dereference all $refs so tools like Postman/Insomnia can import a single file. + const resolved = await $RefParser.dereference(ROOT_FILE, { + dereference: { + // Some schemas are self-referential; most importers handle this fine, + // but the dereferencer can choke if we don't ignore cycles. + circular: 'ignore', + }, + }) + + fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true }) + + const yamlContent = BANNER + yaml.dump(resolved, { noRefs: true, lineWidth: 120 }) + fs.writeFileSync(OUTPUT_FILE, yamlContent, 'utf-8') + + console.log(`Fully resolved OpenAPI schema written to: ${OUTPUT_FILE}`) +} + +main().catch((err) => { + console.error('Failed to generate resolved OpenAPI schema:', err) + process.exitCode = 1 +}) diff --git a/packages/openapi/src/generated/openapi.yaml b/packages/openapi/src/generated/openapi.yaml new file mode 100644 index 00000000000..aaf2806a2e6 --- /dev/null +++ b/packages/openapi/src/generated/openapi.yaml @@ -0,0 +1,7789 @@ +# This file was automatically generated using @apidevtools/json-schema-ref-parser +# DO NOT MODIFY IT BY HAND. Instead, modify the source OpenAPI schema files, +# and run "yarn bundle" (in packages/openapi) to regenerate this file. +openapi: 3.0.3 +info: + title: Sofie User Actions API + description: The Sofie User Actions API provides paths to allow a device to control a Sofie and query health information + version: 1.0.0 + license: + name: MIT License + url: http://opensource.org/licenses/MIT + x-logo: + url: https://sofie-automation.github.io/sofie-core/img/sofie-logo.svg + backgroundColor: '#FFFFFF' + altText: Sofie TV Automation +servers: + - url: http://localhost:3000/api/v1.0 + description: Initial release of UserActions API - http - for use during development only! +tags: + - name: userActions + description: Provides the ability to control the Sofie application +paths: + /: + get: + operationId: index + tags: + - sofie + summary: Returns the current version of Sofie. + responses: + '200': + description: Command successfully handled - returns a string with the version number + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + version: + type: string + example: 1.44.0 + /health: + get: + operationId: getHealth + tags: + - sofie + summary: Gets the current health status of Sofie and all its components + responses: + '200': + description: Command successfully handled - returns an object with detailed health status + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Name of the running system + example: Sofie Automation system + updated: + type: string + format: date-time + description: Time when the status of Sofie was updated + example: '2023-11-29T16:50:06.057Z' + status: + type: string + enum: + - OK + - FAIL + - WARNING + - UNDEFINED + description: Sofie status string + example: OK + version: + type: string + description: Sofie core software version + example: 1.50.10 + blueprintsVersion: + type: string + description: Sofie blueprints version + example: 1.0.60 + components: + type: array + description: Array of components that are part of the Sofie software + items: + type: object + properties: + name: + type: string + description: Name of the component + example: Playout Gateway + updated: + type: string + format: date-time + description: Time when the component status was updated + status: + type: string + enum: + - OK + - FAIL + - WARNING + - UNDEFINED + description: Component status string + example: OK + version: + type: string + description: Component software version + example: 0.1.13 + components: + type: array + description: >- + Array of components that are children of this component. Can recurse - components with no + child will have no components member + items: + type: object + description: Components conforming to the same definition as the parent object + example: + - name: atem + updated: '2023-11-28T15:17:21.712Z' + status: OK + statusMessage: + type: string + description: Status messages for this component + example: Disconnected + required: + - name + - updated + - status + additionalProperties: false + statusMessage: + type: string + description: Concatenation of Sofie status and all component statuses, separated by semicolons + example: 'Playout gateway: Disconnected' + required: + - name + - updated + - status + - version + - blueprintsVersion + - components + - statusMessage + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + /system/blueprint: + put: + operationId: assignSystemBlueprint + tags: + - sofie + summary: Assigns a system Blueprint to Sofie core. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + blueprintId: + type: string + description: System blueprint to assign. + required: + - blueprintId + responses: + '200': + description: PUT success. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Blueprint Id does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + delete: + operationId: unassignSystemBlueprint + tags: + - sofie + summary: Unassigns the assigned system Blueprint, if any Blueprint is assigned. + responses: + '200': + description: PUT success. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Blueprint Id does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + /system/migrations: + get: + operationId: getPendingMigrations + tags: + - sofie + summary: Gets a set of pending system-level migrations + responses: + '200': + description: Command successfully handled - returns an array of migration steps + content: + application/json: + schema: + type: object + additionalProperties: false + required: + - status + - result + properties: + status: + type: number + example: 200 + result: + type: object + additionalProperties: false + required: + - inputs + properties: + inputs: + type: array + items: + type: object + properties: + stepId: + type: string + attributeId: + type: string + additionalProperties: false + required: + - stepId + - attributeId + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + post: + operationId: applyPendingMigrations + tags: + - sofie + summary: Apply a set of migrations with a given set of values + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + oneOf: + - type: object + properties: + stepId: + type: string + attributeId: + type: string + migrationValue: + type: string + required: + - stepId + - attributeId + - migrationValue + additionalProperties: false + - type: object + properties: + stepId: + type: string + attributeId: + type: string + migrationValue: + type: number + required: + - stepId + - attributeId + - migrationValue + additionalProperties: false + - type: object + properties: + stepId: + type: string + attributeId: + type: string + migrationValue: + type: boolean + required: + - stepId + - attributeId + - migrationValue + additionalProperties: false + responses: + '200': + description: POST success. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '412': + description: Failed to apply migration due to migrations already having been applied + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + /devices: + get: + operationId: devices + tags: + - devices + summary: Gets all peripheral devices attached to Sofie. + responses: + '200': + description: Command successfully handled - returns an array of peripheral device Ids + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: array + items: + type: object + properties: + id: + type: string + required: + - id + additionalProperties: false + example: + - id: '47' + - id: '27' + /devices/{deviceId}: + get: + operationId: device + tags: + - devices + summary: Gets a specified peripheral device. + parameters: + - name: deviceId + in: path + description: Requested device. + required: true + schema: + type: string + responses: + '200': + description: Command successfully handled - returns the peripheral device + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + id: + type: string + description: Device Id. + example: 47 + name: + type: string + description: Device Name. + example: PlayoutGateway0 + status: + type: string + enum: + - unknown + - good + - warning_major + - warning_minor + - bad + - fatal + description: Device status. + example: warning_major + messages: + type: array + items: + type: string + example: Device failed to start + description: Service messages from the device. + deviceType: + type: string + enum: + - unknown + - mos + - spreadsheet + - inews + - playout + - package_manager + - live_status + - input + description: Device type. + example: playout + connected: + type: boolean + description: Whether device is currently connected. + example: true + required: + - id + - name + - status + - messages + - deviceType + - connected + '404': + description: The requested device does not exist. + /devices/{deviceId}/action: + post: + operationId: deviceAction + tags: + - devices + summary: Sends a command to a specified peripheral device + parameters: + - name: deviceId + in: path + description: Target device. + required: true + schema: + type: string + requestBody: + description: Contains the action to perform. + required: true + content: + application/json: + schema: + oneOf: + - type: object + properties: + action: + type: string + const: restart + required: + - action + additionalProperties: false + example: + action: restart + responses: + '202': + description: The action is valid for the requested device and will be dispatched. It may not have been dispatched yet. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 202 + '404': + description: The specified Device does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Device was not found + /blueprints: + get: + operationId: blueprints + tags: + - blueprints + summary: Returns all blueprints available in Sofie. + responses: + '200': + description: Command successfully handled - returns an array of blueprint Ids. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: array + items: + type: object + properties: + id: + type: string + required: + - id + additionalProperties: false + example: + - id: studio + - id: showstyle + - id: system + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + /blueprints/{blueprintId}: + get: + operationId: blueprint + tags: + - blueprints + summary: Returns some information about the specified blueprint + parameters: + - name: blueprintId + in: path + description: Requested blueprint. + required: true + schema: + type: string + responses: + '200': + description: Command successfully handled - returns an array of blueprint Ids. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + id: + type: string + description: Blueprint Id. + name: + type: string + description: Blueprint Name. + blueprintType: + type: string + enum: + - system + - studio + - showstyle + description: Blueprint type. + blueprintVersion: + type: string + description: Version reported by blueprint bundle. + required: + - id + - name + - blueprintType + - blueprintVersion + '404': + description: The specified Blueprint does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + /playlists: + get: + operationId: playlists + tags: + - playlists + summary: Returns all playlists available in Sofie. + responses: + '200': + description: Command successfully handled - returns an array of playlist Ids. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: array + items: + type: object + properties: + id: + type: string + externalId: + type: string + required: + - id + - externalId + additionalProperties: false + example: + - id: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/activate: + put: + operationId: activate + tags: + - playlists + summary: Activates a Playlist. + parameters: + - name: playlistId + in: path + description: Playlist to activate. + required: true + schema: + type: string + requestBody: + description: Whether to activate into rehearsal mode. + required: true + content: + application/json: + schema: + type: object + properties: + rehearsal: + type: boolean + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: There is already an active Playlist for the studio that the Playlist belongs to. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: Rundown Playlist is active, please deactivate before preparing it for broadcast + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/deactivate: + put: + operationId: deactivate + tags: + - playlists + summary: Deactivates a Playlist. + parameters: + - name: playlistId + in: path + description: Playlist to deactivate. + required: true + schema: + type: string + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/rundowns/{rundownId}/activate-adlib-testing: + put: + operationId: activateAdlibTesting + tags: + - playlists + summary: Activates AdLib testing mode. + parameters: + - name: playlistId + in: path + description: Playlist to activate testing mode for. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown to activate testing mode for. + required: true + schema: + type: string + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/execute-adlib: + post: + operationId: executeAdLib + tags: + - playlists + summary: >- + Executes the requested AdLib/AdLib Action. This is a "planned" AdLib (Action) that has been produced by the + blueprints during the ingest process. + parameters: + - name: playlistId + in: path + description: Playlist to execute adLib in. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + adLibId: + type: string + description: AdLib to execute + actionType: + type: string + description: >- + An actionType string to specify a particular variation for the AdLibAction, valid strings are to be + read from the status API + adLibOptions: + type: object + description: AdLibAction options object defined according to the optionsSchema provided in the adLib status API + required: + - adLibId + example: + adLibId: adlib_action_camera + actionType: pvw + responses: + '200': + description: >- + Command successfully handled - returns an object that informs whether a part was queued by the action and/or + if the next part was automatically taken + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + queuedPartInstanceId: + type: string + example: YjGd_1dWjta_E1ZuDaOczP1lsgk_ + taken: + type: boolean + example: false + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: >- + Specified Playlist is not active, there is not an on-air Part instance or an adLib for the provided + `adLibId` cannot be found. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: AdLib could not be found! + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/execute-bucket-adlib: + post: + operationId: executeBucketAdLib + tags: + - playlists + summary: >- + Executes the requested Bucket AdLib/AdLib Action. This is a Bucket AdLib (Action) that has been previously + inserted into a Bucket. + parameters: + - name: playlistId + in: path + description: Playlist to execute the Bucket AdLib in. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + actionType: + type: string + description: >- + An actionType string to specify a particular variation for the AdLibAction, valid strings are to be + read from the status API + bucketId: + type: string + description: Bucket to execute the adlib from + externalId: + type: string + description: External Id of the Bucket AdLib to execute + required: + - bucketId + - externalId + example: + bucketId: 6jZ6NvpoikxuXqcm4 + externalId: my_lower_third + actionType: pvw + responses: + '200': + description: >- + Command successfully handled - returns an object that informs whether a part was queued by the action and/or + if the next part was automatically taken + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + queuedPartInstanceId: + type: string + example: YjGd_1dWjta_E1ZuDaOczP1lsgk_ + taken: + type: boolean + example: false + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: >- + Specified Playlist is not active, there is not an on-air Part instance or an adLib for the provided + `externalId` cannot be found. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: AdLib could not be found! + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/move-next-part: + post: + operationId: moveNextPart + tags: + - playlists + summary: Moves the next point by `delta` places. Negative values are allowed to move "backwards" in the script. + parameters: + - name: playlistId + in: path + description: Playlist to target. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + delta: + type: number + description: >- + Amount to move next point by (+/-). If delta results in an index that is greater than the number of + Parts available, no action will be taken. + required: + - delta + responses: + '200': + description: Command successfully handled - returns a string with the new PartID + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: string + example: 3Y9at66pZipxE8Kkn850LLV9Cz0_ + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: Specified Playlist is not active or there is both no current or next Part. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: The selected part does not exist + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/move-next-segment: + post: + operationId: moveNextSegment + tags: + - playlists + summary: Moves the next Segment point by `delta` places. Negative values are allowed to move "backwards" in the script. + parameters: + - name: playlistId + in: path + description: Playlist to target. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + delta: + type: number + description: >- + Amount to move next Segment point by (+/-). If delta results in an index that is greater than the + number of Segments available, no action will be taken. + ignoreQuickLoop: + type: boolean + description: >- + When true, the operation will ignore any boundaries set by the QuickLoop feature when moving to the + next part + required: + - delta + responses: + '200': + description: Command successfully handled - returns a string with the new PartID + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: string + example: YjGd_1dWjta_E1ZuDaOczP1lsgk_ + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: Specified Playlist is not active or there is both no current or next Part. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: The selected part does not exist + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/reload-playlist: + put: + operationId: reloadPlaylist + tags: + - playlists + summary: Reloads a Playlist from its ingest source (e.g. MOS/Spreadsheet etc.) + parameters: + - name: playlistId + in: path + description: Playlist to reload. + required: true + schema: + type: string + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/reset-playlist: + put: + operationId: resetPlaylist + tags: + - playlists + summary: Resets a Playlist back to its pre-played state. + parameters: + - name: playlistId + in: path + description: Playlist to reset. + required: true + schema: + type: string + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: The target Playlist is currently active (reset while on-air can be enabled in core settings). + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: >- + RundownPlaylist is active but not in rehearsal, please deactivate it or set in in rehearsal to be + able to reset it. + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/set-next-part: + put: + operationId: setNextPart + tags: + - playlists + summary: Sets the next Part to a given PartId. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + partId: + type: string + description: Part to set as next. + required: + - partId + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: >- + Specified Playlist is not active, the specified Part does not exist, the specified Part is not playable, + currently in hold, or the specified part is not playable. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: The selected part does not exist + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/set-next-segment: + post: + operationId: setNextSegment + tags: + - playlists + summary: Sets the next part to the first playable Part of the Segment with given segmentId. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + segmentId: + type: string + description: Segment to set as next. + example: n1mOVd5_K5tt4sfk6HYfTuwumGQ_ + required: + - segmentId + responses: + '200': + description: Command successfully handled - returns Part ID if the first part of a segment was set as next. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: string + example: 3Y9at66pZipxE8Kkn850LLV9Cz0_ + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: >- + Specified Playlist is not active, the specified Segment does not exist, the specified Segment does not + contain any playable parts, or currently in hold. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: The selected part does not exist + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/queue-next-segment: + post: + operationId: queueNextSegment + tags: + - playlists + summary: >- + Queue Segment with a given segmentId, so that the Next point will jump to that Segment when reaching the end of + the currently playing Segment. If the part currently set as next is outside of the current segment, it will set + the first part of the given segment as next. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + segmentId: + type: string + description: Segment to queue. + example: n1mOVd5_K5tt4sfk6HYfTuwumGQ_ + required: + - segmentId + responses: + '200': + description: Command successfully handled - returns ID of the queued Segment, or ID of the Part set as next. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + oneOf: + - properties: + queuedSegmentId: + type: string + nullable: true + description: >- + Segment ID, when a segment was queued, or 'null' when previously queued segment is + cleared. + example: n1mOVd5_K5tt4sfk6HYfTuwumGQ_ + - properties: + nextPartId: + type: string + description: Part ID, when a part was set as next. + example: 3Y9at66pZipxE8Kkn850LLV9Cz0_ + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: >- + Specified Playlist is not active, the specified Segment does not exist, the specified Segment does not + contain any playable parts, or currently in hold. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: The selected part does not exist + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/clear-sourcelayers: + put: + operationId: clearSourceLayers + tags: + - playlists + summary: Clears the target SourceLayers. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + sourceLayerIds: + description: Target SourceLayers. + type: array + items: + type: string + required: + - sourceLayerIds + example: + sourceLayerIds: + - mySourceLayerId + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: Playlist is not active. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: Rundown must be active! + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/take: + post: + operationId: take + tags: + - playlists + summary: Performs a take in the given Playlist. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + fromPartInstanceId: + type: string + description: >- + May be specified to ensure that multiple take requests from the same Part do not result in multiple + takes. + responses: + '200': + description: Take was successful - returns the next allowed take time. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + nextTakeTime: + type: number + description: Unix timestamp (ms) of when the next take will be allowed. + example: 1707024000000 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: Specified Playlist is not active or specified Playlist does not have a next Part. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: No Next point found, please set a part as Next before doing a TAKE. + '425': + description: Take is blocked due to a transition or adlib action. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 425 + message: + type: string + example: Cannot take during a transition + additionalInfo: + type: object + description: Additional error details, e.g. includes nextAllowedTakeTime (Unix timestamp ms) for blocked takes. + additionalProperties: true + '429': + description: Take rate limit exceeded - takes are happening too quickly. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 429 + message: + type: string + example: Ignoring TAKES that are too quick after eachother (1000 ms) + additionalInfo: + type: object + description: Additional error details, e.g. includes nextAllowedTakeTime (Unix timestamp ms) for blocked takes. + additionalProperties: true + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/sourceLayer/{sourceLayerId}: + delete: + deprecated: true + operationId: clearSourceLayer + tags: + - sourceLayers + summary: | + Clears the target SourceLayer. + This endpoint is deprecated, use the `clear-sourcelayers` endpoint instead. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: sourceLayerId + in: path + description: Target SourceLayer. + required: true + schema: + type: string + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: Playlist is not active. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: Rundown must be active! + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + sticky: + post: + operationId: recallSticky + tags: + - sourceLayers + summary: Recalls the last sticky Piece on the specified SourceLayer, if there is any. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: sourceLayerId + in: path + description: Target sourcelayer. + required: true + schema: + type: string + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: Playlist is not active, SourceLayer is not sticky, or there is no sticky piece for this SourceLayer. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: Rundown must be active! + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/sourceLayer/{sourceLayerId}/sticky: + post: + operationId: recallSticky + tags: + - sourceLayers + summary: Recalls the last sticky Piece on the specified SourceLayer, if there is any. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: sourceLayerId + in: path + description: Target sourcelayer. + required: true + schema: + type: string + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '412': + description: Playlist is not active, SourceLayer is not sticky, or there is no sticky piece for this SourceLayer. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + example: Rundown must be active! + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/t-timers/{timerIndex}/countdown: + post: + operationId: tTimerCountdown + tags: + - playlists + summary: Start a countdown timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + description: Index of the timer (1, 2, or 3). + enum: + - 1 + - 2 + - 3 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + duration: + type: number + description: Duration in seconds. + stopAtZero: + type: boolean + description: Whether to stop the timer at zero. + startPaused: + type: boolean + description: Whether to start the timer in a paused state. + required: + - duration + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/t-timers/{timerIndex}/free-run: + post: + operationId: tTimerFreeRun + tags: + - playlists + summary: Start a free-running timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + description: Index of the timer (1, 2, or 3). + enum: + - 1 + - 2 + - 3 + requestBody: + content: + application/json: + schema: + type: object + properties: + startPaused: + type: boolean + description: Whether to start the timer in a paused state. + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/t-timers/{timerIndex}/pause: + post: + operationId: tTimerPause + tags: + - playlists + summary: Pause a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + description: Index of the timer (1, 2, or 3). + enum: + - 1 + - 2 + - 3 + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/t-timers/{timerIndex}/resume: + post: + operationId: tTimerResume + tags: + - playlists + summary: Resume a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + description: Index of the timer (1, 2, or 3). + enum: + - 1 + - 2 + - 3 + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/t-timers/{timerIndex}/restart: + post: + operationId: tTimerRestart + tags: + - playlists + summary: Restart a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + description: Index of the timer (1, 2, or 3). + enum: + - 1 + - 2 + - 3 + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/t-timers/{timerIndex}/projected/clear: + post: + operationId: tTimerProjectedClear + tags: + - playlists + summary: Clear the projection state of a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + description: Index of the timer (1, 2, or 3). + enum: + - 1 + - 2 + - 3 + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/t-timers/{timerIndex}/projected/anchor-part: + post: + operationId: tTimerProjectedAnchorPart + tags: + - playlists + summary: Set the anchor part for automatic projection calculation. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + description: Index of the timer (1, 2, or 3). + enum: + - 1 + - 2 + - 3 + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + description: | + Set the anchor part for automatic projection calculation.\n + Provide either `partId` (internal PartId) or `externalId` (ingest part externalId).\n + If `partId` is provided and it does not match an internal PartId, it may be treated as an externalId. + properties: + partId: + type: string + description: Internal PartId (or part externalId). + externalId: + type: string + description: Part externalId. + anyOf: + - required: + - partId + - required: + - externalId + example: + externalId: myPartExternalId + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/t-timers/{timerIndex}/projected/time: + post: + operationId: tTimerProjectedTime + tags: + - playlists + summary: Set the timer projection as an absolute unix timestamp. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + description: Index of the timer (1, 2, or 3). + enum: + - 1 + - 2 + - 3 + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + time: + type: number + description: Unix timestamp (milliseconds) for the projection target. + example: 1707024000000 + paused: + type: boolean + description: When true, the projection is treated as paused (does not change as time passes). + required: + - time + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /playlists/{playlistId}/t-timers/{timerIndex}/projected/duration: + post: + operationId: tTimerProjectedDuration + tags: + - playlists + summary: Set the timer projection as a duration from now. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + description: Index of the timer (1, 2, or 3). + enum: + - 1 + - 2 + - 3 + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + duration: + type: number + description: Duration in milliseconds from now for the projection target. + example: 60000 + paused: + type: boolean + description: When true, the projection is treated as paused (does not change as time passes). + required: + - duration + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + example: The specified Playlist was not found + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + example: Internal Server Error + /studios: + get: + operationId: getStudios + tags: + - studios + summary: Gets all Studios. + responses: + '200': + description: Contains all Studio Ids. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: array + items: + type: object + properties: + id: + type: string + required: + - id + additionalProperties: false + example: + - id: studio0 + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + post: + operationId: addStudio + tags: + - studios + summary: Adds a new Studio. + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Studio display name. + example: Default Studio + blueprintId: + type: string + description: Id of the Blueprint to use for the Studio. + example: studio0 + blueprintConfigPresetId: + type: string + description: Id of the Blueprint config preset to use for the Studio. + example: fcr + supportedShowStyleBase: + type: array + items: + type: string + description: ShowStyleBases that this Studio wants to support. + example: + - showstyle0 + - showstyle1 + config: + type: object + description: Blueprint configuration. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + settings: + type: object + properties: + frameRate: + type: number + exclusiveMinimum: 0 + example: 25 + description: >- + The framerate (frames per second) used to convert internal timing information (in milliseconds) + into timecodes and timecode-like strings and interpret timecode user input. + mediaPreviewsUrl: + type: string + example: http://127.0.0.1:8080 + description: URL to endpoint where media preview are exposed + slackEvaluationUrls: + type: array + items: + type: string + example: http://127.0.0.1:8080 + description: URLs for slack webhook to send evaluations. + supportedMediaFormats: + type: array + items: + type: string + example: 1080i5000 + description: Media Resolutions supported by the studio for media playback. + supportedAudioStreams: + type: array + items: + type: string + example: stereo + description: Audio Stream Formats supported by the studio for media playback. + enablePlayFromAnywhere: + type: boolean + description: Should the play from anywhere feature be enabled in this studio. + forceMultiGatewayMode: + type: boolean + description: >- + If set, forces the multi-playout-gateway mode (aka set "now"-time right away) for single + playout-gateways setups. + multiGatewayNowSafeLatency: + type: number + exclusiveMinimum: 0 + description: >- + How much extra delay to add to the Now-time (used for the "multi-playout-gateway" feature). A + higher value adds delays in playout, but reduces the risk of missed frames. + preserveUnsyncedPlayingSegmentContents: + type: boolean + deprecated: true + description: This no longer has any effect + allowRundownResetOnAir: + type: boolean + description: Allow resets while a rundown is on-air + preserveOrphanedSegmentPositionInRundown: + type: boolean + description: Preserve unsynced segments psoition in the rundown, relative to the other segments + enableQuickLoop: + type: boolean + description: Should QuickLoop context menu options be available to the users + forceQuickLoopAutoNext: + type: string + enum: + - disabled + - enabled_when_valid_duration + - enabled_forcing_min_duration + description: If and how to force auto-nexting in a looping Playlist + fallbackPartDuration: + type: number + description: >- + The duration to apply on too short Parts Within QuickLoop when forceQuickLoopAutoNext is set to + `enabled_forcing_min_duration` + allowAdlibTestingSegment: + type: boolean + description: Whether to allow adlib testing mode, before a Part is playing in a Playlist + allowTestingAdlibsToPersist: + type: boolean + description: Whether to allow infinite adlib from adlib testing mode to persist in the rundown + allowHold: + type: boolean + description: Whether to allow hold operations for Rundowns in this Studio + allowPieceDirectPlay: + type: boolean + description: Whether to allow direct playing of a piece in the rundown + rundownGlobalPiecesPrepareTime: + type: number + description: How long before their start time a rundown owned piece be added to the timeline + required: + - frameRate + - mediaPreviewsUrl + required: + - name + - config + - settings + additionalProperties: false + responses: + '200': + description: Create successful, Id of new Studio is returned. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: string + description: Id of the newly created Studio. + '400': + description: Invalid Studio. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 400 + message: + type: string + required: + - status + - message + additionalProperties: false + '409': + description: The specified Studio configuration is not valid. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 409 + message: + type: string + details: + type: array + items: + type: string + example: Invalid Union + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /studios/{studioId}: + get: + operationId: getStudio + tags: + - studios + summary: Gets a Studio. + parameters: + - name: studioId + in: path + description: Id of Studio to retrieve. + required: true + schema: + type: string + responses: + '200': + description: Studio found. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + name: + type: string + description: Studio display name. + example: Default Studio + blueprintId: + type: string + description: Id of the Blueprint to use for the Studio. + example: studio0 + blueprintConfigPresetId: + type: string + description: Id of the Blueprint config preset to use for the Studio. + example: fcr + supportedShowStyleBase: + type: array + items: + type: string + description: ShowStyleBases that this Studio wants to support. + example: + - showstyle0 + - showstyle1 + config: + type: object + description: Blueprint configuration. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + settings: + type: object + properties: + frameRate: + type: number + exclusiveMinimum: 0 + example: 25 + description: >- + The framerate (frames per second) used to convert internal timing information (in + milliseconds) into timecodes and timecode-like strings and interpret timecode user input. + mediaPreviewsUrl: + type: string + example: http://127.0.0.1:8080 + description: URL to endpoint where media preview are exposed + slackEvaluationUrls: + type: array + items: + type: string + example: http://127.0.0.1:8080 + description: URLs for slack webhook to send evaluations. + supportedMediaFormats: + type: array + items: + type: string + example: 1080i5000 + description: Media Resolutions supported by the studio for media playback. + supportedAudioStreams: + type: array + items: + type: string + example: stereo + description: Audio Stream Formats supported by the studio for media playback. + enablePlayFromAnywhere: + type: boolean + description: Should the play from anywhere feature be enabled in this studio. + forceMultiGatewayMode: + type: boolean + description: >- + If set, forces the multi-playout-gateway mode (aka set "now"-time right away) for single + playout-gateways setups. + multiGatewayNowSafeLatency: + type: number + exclusiveMinimum: 0 + description: >- + How much extra delay to add to the Now-time (used for the "multi-playout-gateway" + feature). A higher value adds delays in playout, but reduces the risk of missed frames. + preserveUnsyncedPlayingSegmentContents: + type: boolean + deprecated: true + description: This no longer has any effect + allowRundownResetOnAir: + type: boolean + description: Allow resets while a rundown is on-air + preserveOrphanedSegmentPositionInRundown: + type: boolean + description: Preserve unsynced segments psoition in the rundown, relative to the other segments + enableQuickLoop: + type: boolean + description: Should QuickLoop context menu options be available to the users + forceQuickLoopAutoNext: + type: string + enum: + - disabled + - enabled_when_valid_duration + - enabled_forcing_min_duration + description: If and how to force auto-nexting in a looping Playlist + fallbackPartDuration: + type: number + description: >- + The duration to apply on too short Parts Within QuickLoop when forceQuickLoopAutoNext is + set to `enabled_forcing_min_duration` + allowAdlibTestingSegment: + type: boolean + description: Whether to allow adlib testing mode, before a Part is playing in a Playlist + allowTestingAdlibsToPersist: + type: boolean + description: Whether to allow infinite adlib from adlib testing mode to persist in the rundown + allowHold: + type: boolean + description: Whether to allow hold operations for Rundowns in this Studio + allowPieceDirectPlay: + type: boolean + description: Whether to allow direct playing of a piece in the rundown + rundownGlobalPiecesPrepareTime: + type: number + description: How long before their start time a rundown owned piece be added to the timeline + required: + - frameRate + - mediaPreviewsUrl + required: + - name + - config + - settings + additionalProperties: false + '404': + description: The specified Studio does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + put: + operationId: addOrUpdateStudio + tags: + - studios + summary: Updates an existing Studio or creates a new one. + parameters: + - name: studioId + in: path + description: Id of Studio to update/create. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Studio display name. + example: Default Studio + blueprintId: + type: string + description: Id of the Blueprint to use for the Studio. + example: studio0 + blueprintConfigPresetId: + type: string + description: Id of the Blueprint config preset to use for the Studio. + example: fcr + supportedShowStyleBase: + type: array + items: + type: string + description: ShowStyleBases that this Studio wants to support. + example: + - showstyle0 + - showstyle1 + config: + type: object + description: Blueprint configuration. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + settings: + type: object + properties: + frameRate: + type: number + exclusiveMinimum: 0 + example: 25 + description: >- + The framerate (frames per second) used to convert internal timing information (in milliseconds) + into timecodes and timecode-like strings and interpret timecode user input. + mediaPreviewsUrl: + type: string + example: http://127.0.0.1:8080 + description: URL to endpoint where media preview are exposed + slackEvaluationUrls: + type: array + items: + type: string + example: http://127.0.0.1:8080 + description: URLs for slack webhook to send evaluations. + supportedMediaFormats: + type: array + items: + type: string + example: 1080i5000 + description: Media Resolutions supported by the studio for media playback. + supportedAudioStreams: + type: array + items: + type: string + example: stereo + description: Audio Stream Formats supported by the studio for media playback. + enablePlayFromAnywhere: + type: boolean + description: Should the play from anywhere feature be enabled in this studio. + forceMultiGatewayMode: + type: boolean + description: >- + If set, forces the multi-playout-gateway mode (aka set "now"-time right away) for single + playout-gateways setups. + multiGatewayNowSafeLatency: + type: number + exclusiveMinimum: 0 + description: >- + How much extra delay to add to the Now-time (used for the "multi-playout-gateway" feature). A + higher value adds delays in playout, but reduces the risk of missed frames. + preserveUnsyncedPlayingSegmentContents: + type: boolean + deprecated: true + description: This no longer has any effect + allowRundownResetOnAir: + type: boolean + description: Allow resets while a rundown is on-air + preserveOrphanedSegmentPositionInRundown: + type: boolean + description: Preserve unsynced segments psoition in the rundown, relative to the other segments + enableQuickLoop: + type: boolean + description: Should QuickLoop context menu options be available to the users + forceQuickLoopAutoNext: + type: string + enum: + - disabled + - enabled_when_valid_duration + - enabled_forcing_min_duration + description: If and how to force auto-nexting in a looping Playlist + fallbackPartDuration: + type: number + description: >- + The duration to apply on too short Parts Within QuickLoop when forceQuickLoopAutoNext is set to + `enabled_forcing_min_duration` + allowAdlibTestingSegment: + type: boolean + description: Whether to allow adlib testing mode, before a Part is playing in a Playlist + allowTestingAdlibsToPersist: + type: boolean + description: Whether to allow infinite adlib from adlib testing mode to persist in the rundown + allowHold: + type: boolean + description: Whether to allow hold operations for Rundowns in this Studio + allowPieceDirectPlay: + type: boolean + description: Whether to allow direct playing of a piece in the rundown + rundownGlobalPiecesPrepareTime: + type: number + description: How long before their start time a rundown owned piece be added to the timeline + required: + - frameRate + - mediaPreviewsUrl + required: + - name + - config + - settings + additionalProperties: false + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + required: + - status + additionalProperties: false + '404': + description: The specified Studio does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '409': + description: The specified Studio configuration is not valid. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 409 + message: + type: string + details: + type: array + items: + type: string + example: Invalid Union + required: + - status + - message + additionalProperties: false + '412': + description: The specified Studio is in use in an on-air Rundown. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + delete: + operationId: deleteStudio + tags: + - studios + summary: Deletes a specified Studio, cleaning up any resources in use (e.g. Playlists). + parameters: + - name: studioId + in: path + description: Id of the Studio to remvoe. + required: true + schema: + type: string + responses: + '200': + description: Operation successful. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Studio does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '412': + description: The specified Studio is in use in an on-air Rundown. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /studios/{studioId}/config: + get: + operationId: getStudioConfig + tags: + - studios + summary: Gets a Studio blueprint configuration. + parameters: + - name: studioId + in: path + description: Id of Studio config to retrieve. + required: true + schema: + type: string + responses: + '200': + description: Configuration found. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + description: Blueprint configuration. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + '404': + description: The specified Studio does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + put: + operationId: updateStudioConfig + tags: + - studios + summary: Updates an existing Studio blueprint configuration. + parameters: + - name: studioId + in: path + description: Id of Studio to update/create. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + description: Blueprint configuration. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + required: + - status + additionalProperties: false + '404': + description: The specified Studio does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '409': + description: The specified Studio configuration is not valid. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 409 + message: + type: string + details: + type: array + items: + type: string + example: Invalid Union + required: + - status + - message + additionalProperties: false + '412': + description: The specified Studio is in use in an on-air Rundown. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /studios/{studioId}/switch-route-set: + put: + operationId: switchRouteSet + tags: + - studios + summary: Activates / Deactivates a route set. + parameters: + - name: studioId + in: path + description: Studio the route set belongs to. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + routeSetId: + type: string + description: Route set to switch + active: + type: boolean + description: Whether the route set should be active + required: + - routeSetId + - active + responses: + '200': + description: Action successfully sent + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + required: + - status + additionalProperties: false + '404': + description: The specified Studio does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /studios/{studioId}/devices: + get: + operationId: devices + tags: + - studios + summary: Returns a list of all devices for a given studio. + parameters: + - name: studioId + in: path + description: Studio the route set belongs to. + required: true + schema: + type: string + responses: + '200': + description: Command successfully handled - returns an array of peripheral device Ids + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: array + items: + type: object + properties: + id: + type: string + required: + - id + additionalProperties: false + '404': + description: The specified Studio does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + put: + operationId: attachDevice + tags: + - studios + summary: Attaches a device to a studio. + parameters: + - name: studioId + in: path + description: Studio to attach the device to. + required: true + schema: + type: string + requestBody: + description: Contains the device Id to attach. + required: true + content: + application/json: + schema: + type: object + properties: + deviceId: + type: string + configId: + type: string + description: Id of the studio owned configuration to assign to the device. If not specified, one will be created. + required: + - deviceId + responses: + '200': + description: Device successfully attached to the studio. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: Specified Studio or device does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + '412': + description: The specified device is already attached to a different studio. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /studios/{studioId}/devices/{deviceId}: + delete: + operationId: detachDevice + tags: + - studios + summary: Detaches a device from a studio. + parameters: + - name: studioId + in: path + description: Studio the device belongs to. + required: true + schema: + type: string + - name: deviceId + in: path + description: Device to remove. + required: true + schema: + type: string + responses: + '200': + description: Device detached from studio. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Studio does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /studios/{studioId}/action: + post: + operationId: studioAction + tags: + - studios + summary: Performs an action on a studio + parameters: + - name: studioId + in: path + description: Target studio. + required: true + schema: + type: string + requestBody: + description: Contains the action to perform. + required: true + content: + application/json: + schema: + oneOf: + - type: object + description: Runs blueprint upgrades. + properties: + action: + type: string + const: blueprintUpgrade + required: + - action + additionalProperties: false + example: + action: blueprintUpgrade + responses: + '202': + description: The action is valid for the requested studio and will be dispatched. It may not have been dispatched yet. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 202 + '400': + description: Request is invalid + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 400 + message: + type: string + required: + - status + - message + additionalProperties: false + '404': + description: The specified Studio does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /showstyles: + get: + operationId: getShowStyleBases + tags: + - showstyles + summary: Returns the Ids of all ShowStyleBases. + responses: + '200': + description: Get successful + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: array + items: + type: object + properties: + id: + type: string + required: + - id + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + post: + operationId: addShowStyleBase + tags: + - showstyles + summary: Adds a ShowStyleBase. + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: ShowStyle Name. + blueprintId: + type: string + description: Id of the blueprint used by this ShowStyleBase. + blueprintConfigPresetId: + type: string + description: Id of the Blueprint config preset to use for the ShowStyle. + outputLayers: + type: array + items: + type: object + properties: + id: + type: string + description: Output layer Id. + name: + type: string + description: Output layer name. + rank: + type: number + description: Display rank. + isPgm: + type: boolean + description: PGM treatment of this output should be in effect. + required: + - id + - name + - rank + - isPgm + additionalProperties: false + sourceLayers: + type: array + items: + type: object + properties: + id: + type: string + description: Source layer Id. + name: + type: string + description: Source layer name. + abbreviation: + type: string + description: Abbreviated display name. + rank: + type: number + description: Display rank. + layerType: + type: string + enum: + - unknown + - camera + - vt + - remote + - script + - graphics + - splits + - audio + - lower-third + - live-speak + - transition + - local + description: Source layer content type. + exclusiveGroup: + type: string + description: Exclusivity group the layer belongs to. + required: + - id + - name + - rank + - layerType + additionalProperties: false + config: + type: object + description: Blueprint config. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + required: + - name + - blueprintId + - outputLayers + - sourceLayers + - config + additionalProperties: false + responses: + '200': + description: Create successful, Id of new ShowStyle is returned. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: string + description: Id of the newly created ShowStyle. + required: + - status + - result + additionalProperties: false + '400': + description: Invalid ShowStyleBase, blueprint Id is not a ShowStyle blueprint. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 400 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /showstyles/{showStyleBaseId}: + get: + operationId: showStyleBase + tags: + - showstyles + summary: Returns the requested ShowStyleBase + parameters: + - name: showStyleBaseId + in: path + description: Id of ShowStyleBase to retrieve + required: true + schema: + type: string + responses: + '200': + description: ShowStyleBase found. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + name: + type: string + description: ShowStyle Name. + blueprintId: + type: string + description: Id of the blueprint used by this ShowStyleBase. + blueprintConfigPresetId: + type: string + description: Id of the Blueprint config preset to use for the ShowStyle. + outputLayers: + type: array + items: + type: object + properties: + id: + type: string + description: Output layer Id. + name: + type: string + description: Output layer name. + rank: + type: number + description: Display rank. + isPgm: + type: boolean + description: PGM treatment of this output should be in effect. + required: + - id + - name + - rank + - isPgm + additionalProperties: false + sourceLayers: + type: array + items: + type: object + properties: + id: + type: string + description: Source layer Id. + name: + type: string + description: Source layer name. + abbreviation: + type: string + description: Abbreviated display name. + rank: + type: number + description: Display rank. + layerType: + type: string + enum: + - unknown + - camera + - vt + - remote + - script + - graphics + - splits + - audio + - lower-third + - live-speak + - transition + - local + description: Source layer content type. + exclusiveGroup: + type: string + description: Exclusivity group the layer belongs to. + required: + - id + - name + - rank + - layerType + additionalProperties: false + config: + type: object + description: Blueprint config. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + required: + - name + - blueprintId + - outputLayers + - sourceLayers + - config + additionalProperties: false + '404': + description: The specified ShowStyleBase does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + put: + operationId: addOrUpdateShowStyleBase + tags: + - showstyles + summary: Updates an existing ShowStyleBase or creates a new one. + parameters: + - name: showStyleBaseId + in: path + description: Id of resource to update/create. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: ShowStyle Name. + blueprintId: + type: string + description: Id of the blueprint used by this ShowStyleBase. + blueprintConfigPresetId: + type: string + description: Id of the Blueprint config preset to use for the ShowStyle. + outputLayers: + type: array + items: + type: object + properties: + id: + type: string + description: Output layer Id. + name: + type: string + description: Output layer name. + rank: + type: number + description: Display rank. + isPgm: + type: boolean + description: PGM treatment of this output should be in effect. + required: + - id + - name + - rank + - isPgm + additionalProperties: false + sourceLayers: + type: array + items: + type: object + properties: + id: + type: string + description: Source layer Id. + name: + type: string + description: Source layer name. + abbreviation: + type: string + description: Abbreviated display name. + rank: + type: number + description: Display rank. + layerType: + type: string + enum: + - unknown + - camera + - vt + - remote + - script + - graphics + - splits + - audio + - lower-third + - live-speak + - transition + - local + description: Source layer content type. + exclusiveGroup: + type: string + description: Exclusivity group the layer belongs to. + required: + - id + - name + - rank + - layerType + additionalProperties: false + config: + type: object + description: Blueprint config. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + required: + - name + - blueprintId + - outputLayers + - sourceLayers + - config + additionalProperties: false + responses: + '200': + description: Operation successful. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '400': + description: Invalid ShowStyleBase, blueprint Id is not a ShowStyle blueprint. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 400 + message: + type: string + required: + - status + - message + additionalProperties: false + '409': + description: The specified ShowStyleBase is not valid. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 409 + message: + type: string + details: + type: array + items: + type: string + example: Invalid Union + required: + - status + - message + additionalProperties: false + '412': + description: The specified ShowStyleBase is in use in an on-air Rundown. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + delete: + operationId: deleteShowStyleBase + tags: + - showstyles + summary: Deletes a specified ShowStyleBase. + parameters: + - name: showStyleBaseId + in: path + description: Id of the ShowStyleBase to remvoe. + required: true + schema: + type: string + responses: + '200': + description: Operation successful. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '412': + description: The specified ShowStyleBase is in use in an on-air Rundown. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /showstyles/{showStyleBaseId}/config: + get: + operationId: getShowStyleConfig + tags: + - showstyles + summary: Returns the requested ShowStyle config + parameters: + - name: showStyleBaseId + in: path + description: Id of ShowStyle to retrieve the config from + required: true + schema: + type: string + responses: + '200': + description: ShowStyle config found. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + description: Blueprint config. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + '404': + description: The specified ShowStyleBase does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + put: + operationId: updateShowStyleConfig + tags: + - showstyles + summary: Updates an existing ShowStyle config. + parameters: + - name: showStyleBaseId + in: path + description: Id of ShowStyle to update the config for. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + description: Blueprint config. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + responses: + '200': + description: Operation successful. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified ShowStyleBase does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '409': + description: The specified ShowStyle config has failed validation. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 409 + message: + type: string + details: + type: array + items: + type: string + example: Invalid Union + required: + - status + - message + additionalProperties: false + '412': + description: The specified ShowStyleBase is in use in an on-air Rundown. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /showstyles/{showStyleBaseId}/variants: + get: + operationId: getShowStyleVariants + tags: + - showstyles + summary: Returns the Ids of all ShowStyleVariants. + parameters: + - name: showStyleBaseId + in: path + description: Id of the ShowStyleBase to fetch variants for. + required: true + schema: + type: string + responses: + '200': + description: Get successful + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: array + items: + type: object + properties: + id: + type: string + required: + - id + additionalProperties: false + '404': + description: The specified ShowStyleBase does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + post: + operationId: addShowStyleVariant + tags: + - showstyles + summary: Adds a ShowStyleVariant. + parameters: + - name: showStyleBaseId + in: path + description: Id of the ShowStyleBase to add a variant to. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: ShowStyle variant Name. + rank: + type: number + description: Rank for ordering variants for display. + showStyleBaseId: + type: string + description: Id of the ShowStyleBase this ShowStyleVariant is based on. + config: + type: object + description: Blueprint config. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + required: + - name + - rank + - showStyleBaseId + - config + additionalProperties: false + responses: + '200': + description: Create successful, Id of new ShowStyle is returned. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: string + description: Id of the newly created ShowStyle Variant. + '404': + description: The specified ShowStyleBase does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /showstyles/{showStyleBaseId}/variants/{showStyleVariantId}: + get: + operationId: showStyleVariant + tags: + - showstyles + summary: Returns the requested ShowStyleVariant + parameters: + - name: showStyleBaseId + in: path + description: Id of ShowStyleBase to the requested variant belongs to. + required: true + schema: + type: string + - name: showStyleVariantId + in: path + description: Id of the ShowStyleVariant to retrieve. + required: true + schema: + type: string + responses: + '200': + description: ShowStyle Variant found. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + name: + type: string + description: ShowStyle variant Name. + rank: + type: number + description: Rank for ordering variants for display. + showStyleBaseId: + type: string + description: Id of the ShowStyleBase this ShowStyleVariant is based on. + config: + type: object + description: Blueprint config. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + required: + - name + - rank + - showStyleBaseId + - config + additionalProperties: false + '404': + description: The specified ShowStyleBase does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + put: + operationId: addOrUpdateShowStyleVariant + tags: + - showstyles + summary: Updates an existing ShowStyleVariant or creates a new one. + parameters: + - name: showStyleBaseId + in: path + description: Id of ShowStyleBase the ShowStyleVariant belongs to. + required: true + schema: + type: string + - name: showStyleVariantId + in: path + description: Id of ShowStyleVariant to insert/update. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: ShowStyle variant Name. + rank: + type: number + description: Rank for ordering variants for display. + showStyleBaseId: + type: string + description: Id of the ShowStyleBase this ShowStyleVariant is based on. + config: + type: object + description: Blueprint config. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + required: + - name + - rank + - showStyleBaseId + - config + additionalProperties: false + responses: + '200': + description: Operation successful. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '400': + description: Invalid ShowStyleVariant + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 400 + message: + type: string + required: + - status + - message + additionalProperties: false + '404': + description: The specified ShowStyleBase does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '412': + description: The specified ShowStyleVariant is in use in an on-air Rundown. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + delete: + operationId: deleteShowStyleVariant + tags: + - showstyles + summary: Deletes a specified ShowStyleVariant. + parameters: + - name: showStyleBaseId + in: path + description: Id of the ShowStyleBase the variant belongs to. + required: true + schema: + type: string + - name: showStyleVariantId + in: path + description: Id of ShowStyleVariant to remove. + required: true + schema: + type: string + responses: + '200': + description: Operation successful. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + required: + - status + additionalProperties: false + '404': + description: The specified ShowStyleBase does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '412': + description: The specified ShowStyleVariant is in use in an on-air Rundown. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /showstyles/{showStyleBaseId}/action: + put: + operationId: showStyleBaseAction + tags: + - showstyles + summary: Performs an action on a showstyle base. + parameters: + - name: showStyleBaseId + in: path + description: Target showstyle base. + required: true + schema: + type: string + requestBody: + description: Contains the action to perform. + required: true + content: + application/json: + schema: + oneOf: + - type: object + description: Runs blueprint upgrades. + properties: + action: + type: string + const: blueprintUpgrade + required: + - action + additionalProperties: false + example: + action: blueprintUpgrade + responses: + '202': + description: >- + The action is valid for the requested showstyle base and will be dispatched. It may not have been dispatched + yet. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 202 + '400': + description: Request is invalid + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 400 + message: + type: string + '404': + description: The specified ShowStyleBase does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /buckets: + get: + operationId: buckets + tags: + - buckets + summary: Returns all buckets available in Sofie. + responses: + '200': + description: Command successfully handled - returns an array of buckets. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: array + items: + type: object + properties: + id: + type: string + description: Bucket Id. + name: + type: string + description: Bucket Name. + studioId: + type: string + description: Id of the studio this bucket belongs to. + additionalProperties: false + example: + id: 6jZ6NvpoikxuXqcm4 + name: My Bucket + studioId: studio0 + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + post: + operationId: addBucket + tags: + - buckets + summary: Adds a Bucket. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Bucket Name. + studioId: + type: string + description: Id of the studio this bucket belongs to. + additionalProperties: false + example: + name: My Bucket + studioId: studio0 + responses: + '200': + description: Command successfully handled - returns a bucket id. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: string + description: Bucket Id. + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + /buckets/{bucketId}: + get: + operationId: bucket + tags: + - buckets + summary: Returns some information about the specified bucket + parameters: + - name: bucketId + in: path + description: Requested bucket. + required: true + schema: + type: string + responses: + '200': + description: Command successfully handled - returns a bucket. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + id: + type: string + description: Bucket Id. + name: + type: string + description: Bucket Name. + studioId: + type: string + description: Id of the studio this bucket belongs to. + additionalProperties: false + example: + id: 6jZ6NvpoikxuXqcm4 + name: My Bucket + studioId: studio0 + '404': + description: The specified Bucket does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + delete: + operationId: deleteBucket + tags: + - buckets + summary: Deletes a bucket + parameters: + - name: bucketId + in: path + description: Bucket to remove. + required: true + schema: + type: string + responses: + '200': + description: Bucket successfully removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Bucket does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + /buckets/{bucketId}/adlibs: + put: + operationId: importBucketAdlib + tags: + - buckets + summary: Imports a Bucket Adlib. + parameters: + - name: bucketId + in: path + description: Bucket to import the adlib to. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + externalId: + type: string + description: >- + Id of the adlib recognizable by the external source. Unique within a bucket. If an adlib with the + same `externalId` already exists in the bucket, it will be replaced. + showStyleBaseId: + type: string + description: Id of the ShowStyle to use when importing the adlib. + name: + type: string + description: Adlib Name. + payloadType: + type: string + description: Hint for the blueprints on how to process the payload. + payload: + description: Data that the blueprints can use to create the Adlib. + required: + - externalId + - showStyleBaseId + - name + - payloadType + additionalProperties: false + example: + externalId: my_lower_third + showStyleBaseId: showstyle0 + name: My Lower Third + payloadType: JSON + payload: + name: Joe + occupation: developer + responses: + '200': + description: Bucket adlib successfully imported. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + delete: + operationId: deleteBucketAdlibs + tags: + - buckets + summary: Deletes all adlibs in a bucket + parameters: + - name: bucketId + in: path + description: Bucket to remove adlibs from. + required: true + schema: + type: string + responses: + '200': + description: Bucket Adlibs successfully removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Bucket does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + /buckets/{bucketId}/adlibs/{externalId}: + delete: + operationId: deleteBucketAdlib + tags: + - buckets + summary: Deletes a bucket adlib + parameters: + - name: bucketId + in: path + description: Bucket to remove the adlib from. + required: true + schema: + type: string + - name: externalId + in: path + description: External id of the bucket adlib to remove. + required: true + schema: + type: string + responses: + '200': + description: Bucket Adlib successfully removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + '404': + description: The specified Bucket does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 404 + message: + type: string + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + /snapshots: + post: + operationId: storeSnapshot + tags: + - snapshots + summary: Stores a Snapshot. + parameters: + - in: header + name: Idempotency-Key + description: >- + Unique ID generated by the client, helping to prevent unintended side effects or duplicate operations caused + by retransmissions or retries of the same request. Expiry period: 5 minutes. + schema: + type: string + format: uuid + example: faf1ba52-a1b5-4958-9baa-b91727ace097 + required: true + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - allOf: + - type: object + required: + - snapshotType + - reason + properties: + snapshotType: + type: string + reason: + type: string + description: Reason this snapshot is stored. + example: Bug report from a user + withDeviceSnapshots: + type: boolean + description: If Peripheral Device snapshots should be included. + discriminator: + propertyName: type + - type: object + properties: + studioId: + type: string + description: When provided, only data related to that Studio will be stored. + example: studio0 + - allOf: + - type: object + required: + - snapshotType + - reason + properties: + snapshotType: + type: string + reason: + type: string + description: Reason this snapshot is stored. + example: Bug report from a user + withDeviceSnapshots: + type: boolean + description: If Peripheral Device snapshots should be included. + discriminator: + propertyName: type + - type: object + required: + - rundownPlaylistId + properties: + rundownPlaylistId: + type: string + description: Id of the Playlist to take a Snapshot of. + example: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ + withTimeline: + type: boolean + description: If the Timeline should be included. + withArchivedDocuments: + type: boolean + description: If played out but already reset instances of Parts and Pieces should be included. + discriminator: + propertyName: snapshotType + mapping: + system: '#/components/schemas/storeSystemSnapshotRequestBody' + playlist: '#/components/schemas/storePlaylistSnapshotRequestBody' + responses: + '200': + description: Command successfully handled - returns a snapshot id. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: string + description: Snapshot Id. + '400': + description: Provided `snapshotType` is invalid or `Idempotency-Key` is missing + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 400 + message: + type: string + required: + - status + - message + additionalProperties: false + '422': + description: Idempotency-Key is already used + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 422 + message: + type: string + required: + - status + - message + additionalProperties: false + '429': + description: Rate limit exceeded + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 429 + message: + type: string + required: + - status + - message + additionalProperties: false + '500': + description: An error unlikely to be the fault of the caller has occurred. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 500 + message: + type: string + required: + - status + - message + additionalProperties: false + /ingest/{studioId}/playlists: + get: + operationId: getPlaylists + summary: Gets all Playlists. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + responses: + '200': + description: Command successfully handled - returns an array of Playlists. + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + externalId: + type: string + example: playlist1 + rundownIds: + type: array + items: + type: string + example: + - rundown1 + - rundown2 + - rundown3 + studioId: + type: string + example: studio0 + required: + - id + - externalId + - rundownIds + - studioId + additionalProperties: false + delete: + operationId: deletePlaylists + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + summary: Deletes all Playlists. Resources under the Playlists (e.g. Rundowns) will also be removed. + responses: + '202': + description: Request for deleting accepted. + /ingest/{studioId}/playlists/{playlistId}: + get: + operationId: getPlaylist + summary: Gets the specified Playlist. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist to return. + required: true + schema: + type: string + responses: + '200': + description: Playlist is returned. + content: + application/json: + schema: + type: object + properties: + id: + type: string + externalId: + type: string + example: playlist1 + rundownIds: + type: array + items: + type: string + example: + - rundown1 + - rundown2 + - rundown3 + studioId: + type: string + example: studio0 + required: + - id + - externalId + - rundownIds + - studioId + additionalProperties: false + '404': + description: Invalid playlistId + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + notFound: + type: string + const: playlist + example: playlist + message: + type: string + example: The specified Playlist was not found. + required: + - status + - notFound + - message + additionalProperties: false + delete: + operationId: deletePlaylist + summary: Deletes a specified Playlist. Resources under the Playlist (e.g. Rundowns) will also be removed. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist to delete. + required: true + schema: + type: string + responses: + '202': + description: Request for deleting accepted. + '404': + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + notFound: + type: string + const: playlist + example: playlist + message: + type: string + example: The specified Playlist was not found. + required: + - status + - notFound + - message + additionalProperties: false + /ingest/{studioId}/rundowns: + post: + operationId: postRundownInStudio + summary: >- + Creates a Rundown in a specified Studio. For all other rundown operations use the + /ingest/{studioId}/playlists/{playlistId}/rundowns routes. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + requestBody: + description: Rundown data to ingest. May include playlistExternalId for grouping if supported by the blueprints. + required: true + content: + application/json: + schema: + type: object + properties: + externalId: + type: string + example: rundown1 + name: + type: string + example: Rundown 1 + type: + type: string + example: external + description: Value that defines the structure of the payload, must be known by Sofie. + resyncUrl: + type: string + example: http://nrcs-url/resync/rundownId + description: URL on which the Sofie will send the POST request to request re-syncing of the Rundown. + segments: + type: array + items: + type: object + properties: + externalId: + type: string + example: segment1 + name: + type: string + example: Segment 1 + rank: + type: number + description: The position of the Segment in the parent Rundown. + inclusiveMinimum: 0 + example: 1 + parts: + type: array + items: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + - parts + additionalProperties: false + payload: + type: object + additionalProperties: true + playlistExternalId: + type: string + example: playlist1 + required: + - externalId + - name + - type + - resyncUrl + - segments + additionalProperties: false + responses: + '202': + description: Request has been accepted. + '400': + description: Bad request. + /ingest/{studioId}/playlists/{playlistId}/rundowns: + get: + operationId: getRundowns + summary: Gets all Rundowns belonging to a specified Playlist. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundowns belong to. + required: true + schema: + type: string + responses: + '200': + description: Command successfully handled - returns an array of Rundowns. + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + externalId: + type: string + example: rundown1 + studioId: + type: string + example: studio0 + playlistId: + type: string + example: playlist1 + playlistExternalId: + type: string + example: playlistExternal1 + name: + type: string + example: Rundown 1 + type: + type: string + timing: + type: object + properties: + type: + type: string + enum: + - none + - forward-time + - back-time + expectedStart: + type: number + description: Epoch timestamp in milliseconds. + expectedDuration: + type: number + description: Epoch interval in milliseconds. + expectedEnd: + type: number + description: Epoch timestamp in milliseconds. + required: + - type + additionalProperties: false + required: + - id + - externalId + - studioId + - playlistId + - name + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + post: + operationId: postRundown + summary: Creates a Rundown in a specified Playlist. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the new Rundown belongs to. + required: true + schema: + type: string + requestBody: + description: Rundown data to ingest. + required: true + content: + application/json: + schema: + type: object + properties: + externalId: + type: string + example: rundown1 + name: + type: string + example: Rundown 1 + type: + type: string + example: external + description: Value that defines the structure of the payload, must be known by Sofie. + resyncUrl: + type: string + example: http://nrcs-url/resync/rundownId + description: URL on which the Sofie will send the POST request to request re-syncing of the Rundown. + segments: + type: array + items: + type: object + properties: + externalId: + type: string + example: segment1 + name: + type: string + example: Segment 1 + rank: + type: number + description: The position of the Segment in the parent Rundown. + inclusiveMinimum: 0 + example: 1 + parts: + type: array + items: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + - parts + additionalProperties: false + payload: + type: object + additionalProperties: true + playlistExternalId: + type: string + example: playlist1 + required: + - externalId + - name + - type + - resyncUrl + - segments + additionalProperties: false + responses: + '202': + description: Request has been accepted. + '400': + description: Bad request. + put: + operationId: putRundowns + summary: Updates Rundowns belonging to a specified Playlist. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundowns to update belong to. + required: true + schema: + type: string + requestBody: + description: Contains the Rundown data. + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + externalId: + type: string + example: rundown1 + name: + type: string + example: Rundown 1 + type: + type: string + example: external + description: Value that defines the structure of the payload, must be known by Sofie. + resyncUrl: + type: string + example: http://nrcs-url/resync/rundownId + description: URL on which the Sofie will send the POST request to request re-syncing of the Rundown. + segments: + type: array + items: + type: object + properties: + externalId: + type: string + example: segment1 + name: + type: string + example: Segment 1 + rank: + type: number + description: The position of the Segment in the parent Rundown. + inclusiveMinimum: 0 + example: 1 + parts: + type: array + items: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + - parts + additionalProperties: false + payload: + type: object + additionalProperties: true + playlistExternalId: + type: string + example: playlist1 + required: + - externalId + - name + - type + - resyncUrl + - segments + additionalProperties: false + responses: + '202': + description: Request has been accepted. + '400': + description: Bad request. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + delete: + operationId: deleteRundowns + tags: + - ingest + summary: >- + Deletes all Rundowns belonging to specified Playlist. Resources under the Rundowns (e.g. Segments) will also be + removed. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundowns to delete belong to. + required: true + schema: + type: string + responses: + '202': + description: Request accepted. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}: + get: + operationId: getRundown + summary: Gets the specified Rundown. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundown belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown to return. + required: true + schema: + type: string + responses: + '200': + description: Rundown is returned. + content: + application/json: + schema: + type: object + properties: + id: + type: string + externalId: + type: string + example: rundown1 + studioId: + type: string + example: studio0 + playlistId: + type: string + example: playlist1 + playlistExternalId: + type: string + example: playlistExternal1 + name: + type: string + example: Rundown 1 + type: + type: string + timing: + type: object + properties: + type: + type: string + enum: + - none + - forward-time + - back-time + expectedStart: + type: number + description: Epoch timestamp in milliseconds. + expectedDuration: + type: number + description: Epoch interval in milliseconds. + expectedEnd: + type: number + description: Epoch timestamp in milliseconds. + required: + - type + additionalProperties: false + required: + - id + - externalId + - studioId + - playlistId + - name + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + put: + operationId: putRundown + summary: Updates an existing specified Rundown. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundown to update belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown to update. + required: true + schema: + type: string + requestBody: + description: Contains the Rundown data. + required: true + content: + application/json: + schema: + type: object + properties: + externalId: + type: string + example: rundown1 + name: + type: string + example: Rundown 1 + type: + type: string + example: external + description: Value that defines the structure of the payload, must be known by Sofie. + resyncUrl: + type: string + example: http://nrcs-url/resync/rundownId + description: URL on which the Sofie will send the POST request to request re-syncing of the Rundown. + segments: + type: array + items: + type: object + properties: + externalId: + type: string + example: segment1 + name: + type: string + example: Segment 1 + rank: + type: number + description: The position of the Segment in the parent Rundown. + inclusiveMinimum: 0 + example: 1 + parts: + type: array + items: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + - parts + additionalProperties: false + payload: + type: object + additionalProperties: true + playlistExternalId: + type: string + example: playlist1 + required: + - externalId + - name + - type + - resyncUrl + - segments + additionalProperties: false + responses: + '202': + description: Request has been accepted. + '400': + description: Bad request. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + delete: + operationId: deleteRundown + summary: Deletes a specified Rundown. Resources under the Rundown (e.g. Segments) will also be removed. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundown belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown to delete. + required: true + schema: + type: string + responses: + '202': + description: Request for deleting accepted. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments: + get: + operationId: getSegments + tags: + - ingest + summary: Gets all Segments belonging to a specified Rundown. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segments belong to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segments belong to. + required: true + schema: + type: string + responses: + '200': + description: Command successfully handled - returns an array of Segments. + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + example: segment1 + externalId: + type: string + example: segmentExternal1 + rundownId: + type: string + example: rundown11 + name: + type: string + example: Segment 1 + rank: + type: number + example: 1 + isHidden: + type: boolean + timing: + type: object + properties: + expectedStart: + type: number + description: Epoch timestamp in milliseconds. + expectedEnd: + type: number + description: Epoch timestamp in milliseconds. + additionalProperties: false + required: + - id + - externalId + - rundownId + - name + - rank + additionalProperties: false + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + post: + operationId: postSegment + tags: + - ingest + summary: Creates a Segment in a specified Rundown. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the new Segment belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the new Segment belongs to. + required: true + schema: + type: string + requestBody: + description: Contains the Segment data. + required: true + content: + application/json: + schema: + type: object + properties: + externalId: + type: string + example: segment1 + name: + type: string + example: Segment 1 + rank: + type: number + description: The position of the Segment in the parent Rundown. + inclusiveMinimum: 0 + example: 1 + parts: + type: array + items: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + - parts + additionalProperties: false + responses: + '202': + description: Request accepted. + '400': + description: Bad request. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + put: + operationId: putSegments + tags: + - ingest + summary: Updates Segments belonging to a specified Rundown. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segments to update belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segments to update belong to. + required: true + schema: + type: string + requestBody: + description: Contains the Segment data. + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + externalId: + type: string + example: segment1 + name: + type: string + example: Segment 1 + rank: + type: number + description: The position of the Segment in the parent Rundown. + inclusiveMinimum: 0 + example: 1 + parts: + type: array + items: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + - parts + additionalProperties: false + responses: + '202': + description: Request accepted. + '400': + description: Bad request. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + delete: + operationId: deleteSegments + tags: + - ingest + summary: >- + Deletes all Segments belonging to specified Rundown. Resources under the Segments (e.g. Parts) will also be + removed. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segments belong to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segments to delete belong to. + required: true + schema: + type: string + responses: + '202': + description: Request accepted. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}: + get: + operationId: getSegment + tags: + - ingest + summary: Gets the specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segment belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segment belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment to return. + required: true + schema: + type: string + responses: + '200': + description: Segment is returned. + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: segment1 + externalId: + type: string + example: segmentExternal1 + rundownId: + type: string + example: rundown11 + name: + type: string + example: Segment 1 + rank: + type: number + example: 1 + isHidden: + type: boolean + timing: + type: object + properties: + expectedStart: + type: number + description: Epoch timestamp in milliseconds. + expectedEnd: + type: number + description: Epoch timestamp in milliseconds. + additionalProperties: false + required: + - id + - externalId + - rundownId + - name + - rank + additionalProperties: false + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + put: + operationId: putSegment + tags: + - ingest + summary: Updates an existing specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segment to update belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segment to update belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment to update. + required: true + schema: + type: string + requestBody: + description: Contains the Segment data. + required: true + content: + application/json: + schema: + type: object + properties: + externalId: + type: string + example: segment1 + name: + type: string + example: Segment 1 + rank: + type: number + description: The position of the Segment in the parent Rundown. + inclusiveMinimum: 0 + example: 1 + parts: + type: array + items: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + - parts + additionalProperties: false + responses: + '202': + description: Request accepted. + '400': + description: Bad request. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + delete: + operationId: deleteSegment + tags: + - ingest + summary: Deletes a specified Segment. Resources under the Segment (e.g. Parts) will also be removed. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segment belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segment belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment to delete. + required: true + schema: + type: string + responses: + '202': + description: Request accepted. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts: + get: + operationId: getParts + tags: + - ingest + summary: Gets all Parts belonging to a specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Parts belong to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Parts belong to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Parts belong to. + required: true + schema: + type: string + responses: + '200': + description: Command successfully handled - returns an array of Parts. + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + example: part1 + externalId: + type: string + example: partExternal1 + rundownId: + type: string + example: rundown1 + segmentId: + type: string + example: segment1 + name: + type: string + example: Part 1 + rank: + type: number + example: 0 + expectedDuration: + type: number + description: Calculated based on pieces. + example: 10000 + autoNext: + type: boolean + example: false + required: + - id + - externalId + - rundownId + - segmentId + - name + - rank + additionalProperties: false + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + post: + operationId: postPart + tags: + - ingest + summary: Creates a Part in a specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the new Part belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the new Part belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the new Part belongs to. + required: true + schema: + type: string + requestBody: + description: Contains the Parts data. + required: true + content: + application/json: + schema: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + responses: + '202': + description: Request accepted. + '400': + description: Bad request. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + put: + operationId: putParts + tags: + - ingest + summary: Updates Parts belonging to a specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Parts to update belong to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Parts to update belong to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Parts to update belong to. + required: true + schema: + type: string + requestBody: + description: Contains the Parts data. + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + responses: + '202': + description: Request accepted. + '400': + description: Bad request. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + delete: + operationId: deleteParts + tags: + - ingest + summary: Deletes all Parts belonging to specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Parts belong to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Parts belong to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Parts to delete belong to. + required: true + schema: + type: string + responses: + '202': + description: Request for deleting accepted. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts/{partId}: + get: + operationId: getPart + tags: + - ingest + summary: Gets the specified Part. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Part belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Part belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Part belongs to. + required: true + schema: + type: string + - name: partId + in: path + description: Internal or external ID of the Part to return. + required: true + schema: + type: string + responses: + '200': + description: Part is returned. + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: part1 + externalId: + type: string + example: partExternal1 + rundownId: + type: string + example: rundown1 + segmentId: + type: string + example: segment1 + name: + type: string + example: Part 1 + rank: + type: number + example: 0 + expectedDuration: + type: number + description: Calculated based on pieces. + example: 10000 + autoNext: + type: boolean + example: false + required: + - id + - externalId + - rundownId + - segmentId + - name + - rank + additionalProperties: false + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + put: + operationId: putPart + tags: + - ingest + summary: Updates an existing specified Part. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Part to update belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Part to update belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Part to update belongs to. + schema: + type: string + - name: partId + in: path + description: Internal or external ID of the Part to update. + schema: + type: string + requestBody: + description: Contains the Rundown data. + required: true + content: + application/json: + schema: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + responses: + '202': + description: Request has been accepted. + '400': + description: Bad request. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + delete: + operationId: deletePart + tags: + - ingest + summary: Deletes a specified Part. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Part belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Part belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Part belongs to. + required: true + schema: + type: string + - name: partId + in: path + description: Internal or external ID of the Part to delete. + required: true + schema: + type: string + responses: + '202': + description: Request has been accepted. + '404': + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false diff --git a/packages/openapi/tsconfig.build.json b/packages/openapi/tsconfig.build.json index 288791ae90a..20131ea33f3 100644 --- a/packages/openapi/tsconfig.build.json +++ b/packages/openapi/tsconfig.build.json @@ -14,6 +14,7 @@ "noImplicitAny": false, "noUnusedLocals": false, "noUnusedParameters": false, - "strictNullChecks": false + "strictNullChecks": false, + "module": "node20" } } diff --git a/packages/package.json b/packages/package.json index 15749304d03..6a96d06e2d1 100644 --- a/packages/package.json +++ b/packages/package.json @@ -43,37 +43,36 @@ "devDependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@sofie-automation/code-standard-preset": "^3.2.1", + "@sofie-automation/code-standard-preset": "^3.2.2", "@types/amqplib": "0.10.6", - "@types/debug": "^4.1.12", + "@types/debug": "^4.1.13", "@types/ejson": "^2.2.2", "@types/jest": "^30.0.0", - "@types/node": "^22.19.8", + "@types/node": "^22.19.17", "@types/object-path": "^0.11.4", "@types/underscore": "^1.13.0", - "babel-jest": "^30.2.0", - "copyfiles": "^2.4.1", - "eslint": "^9.39.2", + "babel-jest": "^30.3.0", + "eslint": "^9.39.4", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-yml": "^3.1.2", - "jest": "^30.2.0", - "jest-environment-jsdom": "^30.2.0", + "eslint-plugin-yml": "^3.3.1", + "jest": "^30.3.0", + "jest-environment-jsdom": "^30.3.0", "jest-mock-extended": "^4.0.0", "json-schema-to-typescript": "^15.0.4", "lerna": "^9.0.7", "nodemon": "^3.1.14", - "open-cli": "^8.0.0", + "open-cli": "^9.0.0", "pinst": "^3.0.0", - "prettier": "^3.8.1", - "rimraf": "^6.1.2", - "semver": "^7.7.3", - "ts-jest": "^29.4.6", + "prettier": "^3.8.3", + "rimraf": "^6.1.3", + "semver": "^7.7.4", + "ts-jest": "^29.4.9", "ts-node": "^10.9.2", "typedoc": "^0.27.9", - "typescript": "~5.7.3" + "typescript": "~5.9.3" }, "name": "packages", - "packageManager": "yarn@4.12.0", + "packageManager": "yarn@4.14.1", "resolutions": { "timecode@0.0.4": "patch:timecode@npm%3A0.0.4#./.yarn/patches/timecode-npm-0.0.4-82bde9e6fe.patch", "@hyperjump/json-schema-core@^0.28.0": "patch:@hyperjump/json-schema-core@npm%3A0.28.5#./.yarn/patches/@hyperjump-json-schema-core-npm-0.28.5-e8b590eb0d.patch", diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index b73a05c2875..252ba34c67c 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -55,10 +55,10 @@ "@sofie-automation/shared-lib": "26.3.0-2", "debug": "^4.4.3", "influx": "^5.12.0", - "timeline-state-resolver": "10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0", + "timeline-state-resolver": "10.0.0-nightly-main-20260602-133931-c0882da4d.0", "tslib": "^2.8.1", - "underscore": "^1.13.7", + "underscore": "^1.13.8", "winston": "^3.19.0" }, - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/packages/playout-gateway/src/connector.ts b/packages/playout-gateway/src/connector.ts index 71dbaf6eed2..0f51e88db13 100644 --- a/packages/playout-gateway/src/connector.ts +++ b/packages/playout-gateway/src/connector.ts @@ -5,7 +5,7 @@ import { InfluxConfig } from './influxdb.js' import { CertificatesConfig, PeripheralDeviceId, - loadCertificatesFromDisk, + loadDDPTLSOptions, stringifyError, HealthConfig, HealthEndpoints, @@ -32,7 +32,6 @@ export class Connector implements IConnector { private tsrHandler: TSRHandler | undefined private coreHandler: CoreHandler | undefined private _logger: Logger - private _certificates: Buffer[] | undefined constructor(logger: Logger) { this._logger = logger @@ -41,14 +40,14 @@ export class Connector implements IConnector { public async init(config: Config): Promise { try { this._logger.info('Initializing Certificates...') - this._certificates = loadCertificatesFromDisk(this._logger, config.certificates) + const tlsOptions = loadDDPTLSOptions(this._logger, config.certificates) this._logger.info('Certificates initialized') this._logger.info('Initializing Core...') this.coreHandler = new CoreHandler(this._logger, config.device) new HealthEndpoints(this, this.coreHandler, config.health) - await this.coreHandler.init(config.core, this._certificates) + await this.coreHandler.init(config.core, tlsOptions) this._logger.info('Core initialized') this._logger.info('Initializing TSR...') diff --git a/packages/playout-gateway/src/coreHandler.ts b/packages/playout-gateway/src/coreHandler.ts index fe6cbdecf24..541d1fa136f 100644 --- a/packages/playout-gateway/src/coreHandler.ts +++ b/packages/playout-gateway/src/coreHandler.ts @@ -2,6 +2,7 @@ import { CoreConnection, CoreOptions, DDPConnectorOptions, + DDPTLSOptions, PeripheralDeviceAPI, PeripheralDeviceCommand, PeripheralDeviceId, @@ -12,8 +13,9 @@ import { PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames, ICoreHandler, + CoreConnectionChild, } from '@sofie-automation/server-core-integration' -import { MediaObject, DeviceOptionsAny, ActionExecutionResult } from 'timeline-state-resolver' +import { MediaObject, DeviceOptionsAny, ActionExecutionResult, DeviceStatus } from 'timeline-state-resolver' import _ from 'underscore' import { DeviceConfig } from './connector.js' import { TSRHandler } from './tsrHandler.js' @@ -23,7 +25,6 @@ import { MemUsageReport as ThreadMemUsageReport } from 'threadedclass' import { compilePlayoutGatewayConfigManifest } from './configManifest.js' import { BaseRemoteDeviceIntegration } from 'timeline-state-resolver/dist/service/remoteDeviceInstance' import { getVersions } from './versions.js' -import { CoreConnectionChild } from '@sofie-automation/server-core-integration/dist/lib/CoreConnectionChild' import { PlayoutGatewayConfig } from '@sofie-automation/shared-lib/dist/generated/PlayoutGatewayConfigTypes' import { PeripheralDeviceCommandId } from '@sofie-automation/shared-lib/dist/core/model/Ids' @@ -55,34 +56,32 @@ export class CoreHandler implements ICoreHandler { private _executedFunctions = new Set() private _tsrHandler?: TSRHandler private _coreConfig?: CoreConfig - private _certificates?: Buffer[] private _statusInitialized = false private _statusDestroyed = false - public connectedToCore = false + public get connectedToCore(): boolean { + return this.core && this.core.connected + } constructor(logger: Logger, deviceOptions: DeviceConfig) { this.logger = logger this._deviceOptions = deviceOptions } - async init(config: CoreConfig, certificates: Buffer[]): Promise { + async init(config: CoreConfig, tlsOptions: DDPTLSOptions): Promise { this._statusInitialized = false this._coreConfig = config - this._certificates = certificates this.core = new CoreConnection(this.getCoreConnectionOptions()) this.core.onConnected(() => { this.logger.info('Core Connected!') - this.connectedToCore = true if (this._onConnected) this._onConnected() }) this.core.onDisconnected(() => { this.logger.warn('Core Disconnected!') - this.connectedToCore = false }) this.core.onError((err: any) => { this.logger.error('Core Error: ' + (typeof err === 'string' ? err : err.message || err.toString() || err)) @@ -91,11 +90,7 @@ export class CoreHandler implements ICoreHandler { const ddpConfig: DDPConnectorOptions = { host: config.host, port: config.port, - } - if (this._certificates.length) { - ddpConfig.tlsOpts = { - ca: this._certificates, - } + tlsOpts: tlsOptions, } await this.core.init(ddpConfig) @@ -121,6 +116,11 @@ export class CoreHandler implements ICoreHandler { this.core.autoSubscribe(PeripheralDevicePubSub.peripheralDeviceCommands, this.core.deviceId), this.core.autoSubscribe(PeripheralDevicePubSub.rundownsForDevice, this.core.deviceId), this.core.autoSubscribe(PeripheralDevicePubSub.expectedPlayoutItemsForDevice, this.core.deviceId), + this.core.autoSubscribe( + PeripheralDevicePubSub.externalEventSubscriptionsForDevice, + 'tsr', + this.core.deviceId + ), ]) this.logger.info('Core: Subscriptions are set up!') @@ -380,24 +380,21 @@ export class CoreHandler implements ICoreHandler { return Object.fromEntries(this._tsrHandler.getDebugStates().entries()) } - getCoreStatus(): { - statusCode: StatusCode - messages: string[] - } { + getCoreStatus(): PeripheralDeviceAPI.PeripheralDeviceStatusObject { let statusCode = StatusCode.GOOD - const messages: string[] = [] + const statusDetails: Array<{ message: string }> = [] if (!this._statusInitialized) { statusCode = StatusCode.BAD - messages.push('Starting up...') + statusDetails.push({ message: 'Starting up...' }) } if (this._statusDestroyed) { statusCode = StatusCode.BAD - messages.push('Shut down') + statusDetails.push({ message: 'Shut down' }) } return { statusCode, - messages, + statusDetails, } } async updateCoreStatus(): Promise { @@ -415,7 +412,7 @@ export class CoreTSRDeviceHandler { private _hasGottenStatusChange = false private _deviceStatus: PeripheralDeviceAPI.PeripheralDeviceStatusObject = { statusCode: StatusCode.BAD, - messages: ['Starting up...'], + statusDetails: [{ message: 'Starting up...' }], } private disposed = false @@ -446,7 +443,15 @@ export class CoreTSRDeviceHandler { console.log('has got status? ' + this._hasGottenStatusChange) if (!this._hasGottenStatusChange) { - this._deviceStatus = await this._device.device.getStatus() + const rawStatus = await this._device.device.getStatus() + if ('statusDetails' in rawStatus) { + this._deviceStatus = rawStatus + } else { + this._deviceStatus = { + statusCode: rawStatus.statusCode, + statusDetails: (rawStatus.messages ?? []).map((m) => ({ message: m })), + } + } } this.sendStatus() if (this.disposed) throw new Error('CoreTSRDeviceHandler cant init, is disposed') @@ -480,13 +485,14 @@ export class CoreTSRDeviceHandler { // setup observers this._coreParentHandler.setupObserverForPeripheralDeviceCommands(this) } - statusChanged(deviceStatus: Partial, fromDevice = true): void { + statusChanged(deviceStatus: DeviceStatus, fromDevice = true): void { console.log('device ' + this._deviceId + ' status set to ' + deviceStatus.statusCode) if (fromDevice) this._hasGottenStatusChange = true this._deviceStatus = { ...this._deviceStatus, - ...deviceStatus, + statusCode: deviceStatus.statusCode, + statusDetails: deviceStatus.statusDetails ?? (deviceStatus.messages ?? []).map((m) => ({ message: m })), } this.sendStatus() } @@ -540,7 +546,7 @@ export class CoreTSRDeviceHandler { await this.core.setStatus({ statusCode: StatusCode.BAD, - messages: ['Uninitialized'], + statusDetails: [{ message: 'Uninitialized' }], }) if (subdevice === 'removeSubDevice') await this.core.unInitialize() diff --git a/packages/playout-gateway/src/index.ts b/packages/playout-gateway/src/index.ts index 248fa89f445..efce29277fb 100644 --- a/packages/playout-gateway/src/index.ts +++ b/packages/playout-gateway/src/index.ts @@ -44,6 +44,7 @@ if (logPath) { }) logger = Winston.createLogger({ + exitOnError: false, transports: [transportConsole, transportFile], }) logger.info('Logging to', logPath) @@ -67,6 +68,7 @@ if (logPath) { }) logger = Winston.createLogger({ + exitOnError: false, transports: [transportConsole], }) logger.info('Logging to Console') diff --git a/packages/playout-gateway/src/playoutMetrics.ts b/packages/playout-gateway/src/playoutMetrics.ts new file mode 100644 index 00000000000..ea2df745416 --- /dev/null +++ b/packages/playout-gateway/src/playoutMetrics.ts @@ -0,0 +1,53 @@ +import { MetricsCounter, MetricsGauge, MetricsHistogram } from '@sofie-automation/server-core-integration' + +export const playoutDevicesTotalGauge = new MetricsGauge({ + name: 'sofie_playout_gateway_devices_total', + help: 'Total number of TSR devices under management', +}) + +export const playoutDeviceConnectedGauge = new MetricsGauge({ + name: 'sofie_playout_gateway_device_connected', + help: 'Whether a TSR device is connected (1) or not (0)', + labelNames: ['device_id', 'device_type'] as const, +}) + +export const playoutResolveDurationHistogram = new MetricsHistogram({ + name: 'sofie_playout_gateway_resolve_duration_seconds', + help: 'Time spent resolving the timeline, in seconds', + buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10], +}) + +export const playoutTimelineAgeGauge = new MetricsGauge({ + name: 'sofie_playout_gateway_timeline_age_seconds', + help: 'Age of the timeline at the moment it finished resolving, measured from when Sofie Core generated it', +}) + +export const playoutSlowSentCommandsCounter = new MetricsCounter({ + name: 'sofie_playout_gateway_slow_sent_commands_total', + help: 'Number of commands that were slow to be sent', + labelNames: ['device_id', 'device_type'] as const, +}) + +export const playoutSlowFulfilledCommandsCounter = new MetricsCounter({ + name: 'sofie_playout_gateway_slow_fulfilled_commands_total', + help: 'Number of commands that were slow to be fulfilled by the device', + labelNames: ['device_id', 'device_type'] as const, +}) + +export const playoutCommandErrorsCounter = new MetricsCounter({ + name: 'sofie_playout_gateway_command_errors_total', + help: 'Number of commands that resulted in an error', + labelNames: ['device_id', 'device_type'] as const, +}) + +export const playoutCommandsSentCounter = new MetricsCounter({ + name: 'sofie_playout_gateway_commands_sent_total', + help: 'Number of commands sent to devices (only increments when reportAllCommands is enabled per device)', + labelNames: ['device_id', 'device_type'] as const, +}) + +export const playoutPlaybackCallbacksCounter = new MetricsCounter({ + name: 'sofie_playout_gateway_playback_callbacks_total', + help: 'Number of playback timeline callbacks received from TSR', + labelNames: ['type'] as const, +}) diff --git a/packages/playout-gateway/src/tsrHandler.ts b/packages/playout-gateway/src/tsrHandler.ts index 617f3c96b66..6502ea86dee 100644 --- a/packages/playout-gateway/src/tsrHandler.ts +++ b/packages/playout-gateway/src/tsrHandler.ts @@ -48,6 +48,17 @@ import { } from '@sofie-automation/server-core-integration' import { BaseRemoteDeviceIntegration } from 'timeline-state-resolver/dist/service/remoteDeviceInstance' import { TSRDeviceRegistry } from './tsrDeviceRegistry.js' +import { + playoutDevicesTotalGauge, + playoutDeviceConnectedGauge, + playoutResolveDurationHistogram, + playoutTimelineAgeGauge, + playoutSlowSentCommandsCounter, + playoutSlowFulfilledCommandsCounter, + playoutCommandErrorsCounter, + playoutCommandsSentCounter, + playoutPlaybackCallbacksCounter, +} from './playoutMetrics.js' const debug = Debug('playout-gateway') @@ -71,6 +82,7 @@ export class TSRHandler { // private _config: TSRConfig private _coreHandler!: CoreHandler private _triggerupdateExpectedPlayoutItemsTimeout: NodeJS.Timeout | null = null + private _triggerUpdateEventSubscriptionsTimeout: NodeJS.Timeout | null = null private _coreTsrHandlers: { [deviceId: string]: CoreTSRDeviceHandler } = {} private _observers: Array> = [] private _cachedStudioId: StudioId | null = null @@ -85,6 +97,7 @@ export class TSRHandler { private _triggerUpdateDevicesTimeout: NodeJS.Timeout | undefined private _debugStates: Map = new Map() + private _pendingTimelineGeneratedAt: Map = new Map() constructor(logger: Logger) { this.logger = logger @@ -165,6 +178,13 @@ export class TSRHandler { this.handleTSRTimelineCallback(time, objId, callbackName, data) }) this.tsr.on('resolveDone', (timelineHash: string, resolveDuration: number) => { + playoutResolveDurationHistogram.observe(resolveDuration / 1000) + const generatedAt = this._pendingTimelineGeneratedAt.get(timelineHash) + if (generatedAt !== undefined) { + playoutTimelineAgeGauge.set((Date.now() - generatedAt) / 1000) + this._pendingTimelineGeneratedAt.delete(timelineHash) + } + // Make sure we only report back once, per update timeline if (this._lastReportedObjHashes.includes(timelineHash)) return @@ -216,17 +236,21 @@ export class TSRHandler { this.tsr.connectionManager.on('connectionAdded', (id, container) => { const coreTsrHandler = new CoreTSRDeviceHandler(this._coreHandler, Promise.resolve(container), id) this._coreTsrHandlers[id] = coreTsrHandler + playoutDevicesTotalGauge.set(Object.keys(this._coreTsrHandlers).length) // set the status to uninitialized for now: coreTsrHandler.statusChanged( { statusCode: StatusCode.BAD, messages: ['Device initialising...'], + statusDetails: [{ message: 'Device initialising...' }], + active: false, }, false ) this._triggerupdateExpectedPlayoutItems() // So that any recently created devices will get all the ExpectedPlayoutItems + this._triggerUpdateEventSubscriptions() // Ensure new device gets current external event subscriptions }) this.tsr.connectionManager.on('connectionInitialised', (id) => { @@ -248,10 +272,16 @@ export class TSRHandler { return } + const removedDeviceType = coreTsrHandler._device?.deviceType + const removedDeviceTypeName = + removedDeviceType !== undefined ? (DeviceType[removedDeviceType] ?? 'unknown') : 'unknown' + coreTsrHandler.dispose('removeSubDevice').catch((e) => { this.logger.error('Failed to dispose of coreTsrHandler for ' + id + ': ' + e) }) delete this._coreTsrHandlers[id] + playoutDevicesTotalGauge.set(Object.keys(this._coreTsrHandlers).length) + playoutDeviceConnectedGauge.set({ device_id: id, device_type: removedDeviceTypeName }, 0) }) const fixLog = (id: string, e: string): string => { @@ -259,20 +289,20 @@ export class TSRHandler { return `Device "${device?.deviceName ?? id}" (${device?.instanceId ?? 'instance unknown'}): ` + e } - const fixError = (id: string, e: Error): any => { + const fixError = (id: string, e: any): any => { const device = this._coreTsrHandlers[id]?._device const name = `Device "${device?.deviceName ?? id}" (${device?.instanceId ?? 'instance unknown'})` - if (!e || !('message' in e)) { + if (!e || typeof e !== 'object' || !('message' in e)) { return { - message: name + ': ' + 'Unknown error: ' + JSON.stringify(e), + message: name + ': ' + 'Unknown error: ' + stringifyError(e, true), } } return { - message: e.message && name + ': ' + e.message, - name: e.name && name + ': ' + e.name, - stack: e.stack && e.stack + '\nAt device' + name, + message: e.message !== undefined ? name + ': ' + e.message : undefined, + name: e.name !== undefined ? name + ': ' + e.name : undefined, + stack: e.stack !== undefined ? e.stack + '\nAt device' + name : undefined, } } const fixContext = (...context: any[]): any => { @@ -286,6 +316,13 @@ export class TSRHandler { if (!coreTsrHandler) return if (!coreTsrHandler._device) return // Not initialized yet + const changedDeviceType = coreTsrHandler._device.deviceType + const changedDeviceTypeName = DeviceType[changedDeviceType] ?? 'unknown' + playoutDeviceConnectedGauge.set( + { device_id: id, device_type: changedDeviceTypeName }, + status.statusCode <= StatusCode.WARNING_MAJOR ? 1 : 0 + ) + coreTsrHandler.statusChanged(status) if (!coreTsrHandler._device) return @@ -315,6 +352,12 @@ export class TSRHandler { } }) this.tsr.connectionManager.on('connectionEvent:slowSentCommand', (id, info) => { + const deviceType0 = this._coreTsrHandlers[id]?._device?.deviceType + playoutSlowSentCommandsCounter.inc({ + device_id: id, + device_type: deviceType0 !== undefined ? (DeviceType[deviceType0] ?? 'unknown') : 'unknown', + }) + // If the internalDelay is too large, it should be logged as an error, // since something took too long internally. @@ -331,6 +374,12 @@ export class TSRHandler { } }) this.tsr.connectionManager.on('connectionEvent:slowFulfilledCommand', (id, info) => { + const deviceType1 = this._coreTsrHandlers[id]?._device?.deviceType + playoutSlowFulfilledCommandsCounter.inc({ + device_id: id, + device_type: deviceType1 !== undefined ? (DeviceType[deviceType1] ?? 'unknown') : 'unknown', + }) + // Note: we don't emit slow fulfilled commands as error, since // the fulfillment of them lies on the device being controlled, not on us. @@ -340,10 +389,22 @@ export class TSRHandler { }) }) this.tsr.connectionManager.on('connectionEvent:commandError', (id, error, context) => { + const deviceType2 = this._coreTsrHandlers[id]?._device?.deviceType + playoutCommandErrorsCounter.inc({ + device_id: id, + device_type: deviceType2 !== undefined ? (DeviceType[deviceType2] ?? 'unknown') : 'unknown', + }) + // todo: handle this better this.logger.error(fixError(id, error), { context }) }) - this.tsr.connectionManager.on('connectionEvent:commandReport', (_id, commandReport) => { + this.tsr.connectionManager.on('connectionEvent:commandReport', (id, commandReport) => { + const deviceType3 = this._coreTsrHandlers[id]?._device?.deviceType + playoutCommandsSentCounter.inc({ + device_id: id, + device_type: deviceType3 !== undefined ? (DeviceType[deviceType3] ?? 'unknown') : 'unknown', + }) + if (this._reportAllCommands) { // Todo: send these to Core this.logger.info('commandReport', { @@ -397,6 +458,14 @@ export class TSRHandler { this.tsr.connectionManager.on('connectionEvent:timeTrace', (_id, trace) => { sendTrace(trace) }) + this.tsr.connectionManager.on('connectionEvent:stateEvent', (_id, events) => { + this.logger.debug( + `connectionEvent:stateEvent: received ${events.length} event(s) from device "${_id}": ${events.map((e) => e.event).join(', ')}` + ) + this._coreHandler.core.coreMethods + .reportExternalEvents(events.map((e) => ({ ...e, type: 'tsr' as const }))) + .catch((e: unknown) => this.logger.error('Error when reporting external events to core', e)) + }) } private setupObservers(): void { @@ -480,6 +549,20 @@ export class TSRHandler { this._triggerUpdateDatastore() } this._observers.push(timelineDatastoreObserver) + + const externalEventSubscriptionsObserver = this._coreHandler.core.observe( + PeripheralDevicePubSubCollectionsNames.externalEventSubscriptions + ) + externalEventSubscriptionsObserver.added = () => { + this._triggerUpdateEventSubscriptions() + } + externalEventSubscriptionsObserver.changed = () => { + this._triggerUpdateEventSubscriptions() + } + externalEventSubscriptionsObserver.removed = () => { + this._triggerUpdateEventSubscriptions() + } + this._observers.push(externalEventSubscriptionsObserver) } private resendStatuses(): void { _.each(this._coreTsrHandlers, (tsrHandler) => { @@ -589,6 +672,7 @@ export class TSRHandler { } const transformedTimeline = this._transformTimeline(deserializeTimelineBlob(timeline.timelineBlob)) + this._pendingTimelineGeneratedAt.set(unprotectString(timeline.timelineHash), timeline.generated) this.tsr.timelineHash = unprotectString(timeline.timelineHash) this.tsr.setTimelineAndMappings(transformedTimeline, unprotectObject(mappingsObject.mappings)) } @@ -677,6 +761,7 @@ export class TSRHandler { } this.tsr.connectionManager.setConnections(connections) + this._triggerUpdateEventSubscriptions() // Re-apply subscriptions after connection set changes } } @@ -778,6 +863,55 @@ export class TSRHandler { if (!this._initialized) return this._updateDatastore().catch((e) => this.logger.error('Error in _updateDatastore', e)) } + private _triggerUpdateEventSubscriptions() { + if (!this._initialized) return + if (this._triggerUpdateEventSubscriptionsTimeout) { + clearTimeout(this._triggerUpdateEventSubscriptionsTimeout) + } + this._triggerUpdateEventSubscriptionsTimeout = setTimeout(() => { + this._updateEventSubscriptions().catch((e) => { + this.logger.error('Error in _updateEventSubscriptions', e) + }) + }, 200) + } + private async _updateEventSubscriptions() { + const subscriptionDocs = this._coreHandler.core + .getCollection(PeripheralDevicePubSubCollectionsNames.externalEventSubscriptions) + .find({}) + + this.logger.debug(`_updateEventSubscriptions: ${subscriptionDocs.length} subscription doc(s) in collection`) + + // Aggregate subscriptions, group by deviceId + const subscriptionsByDeviceId = new Map>() + for (const sub of subscriptionDocs) { + if (sub.type !== 'tsr') continue + + let events = subscriptionsByDeviceId.get(sub.deviceId) + if (!events) { + events = new Set() + subscriptionsByDeviceId.set(sub.deviceId, events) + } + events.add(sub.event) + } + + if (subscriptionsByDeviceId.size === 0) { + this.logger.debug('_updateEventSubscriptions: no subscriptions — clearing all devices') + } + + await Promise.allSettled( + _.map(this.tsr.connectionManager.getConnections(), async (container) => { + const events = subscriptionsByDeviceId.get(container.deviceId) ?? new Set() + this.logger.debug( + `_updateEventSubscriptions: setting ${events.size} event subscription(s) on device "${container.deviceId}": [${Array.from(events).join(', ')}]` + ) + await container.device.setEventSubscriptions(Array.from(events)).catch((e) => { + this.logger.error( + `Error setting event subscriptions for device "${container.deviceId}": ${stringifyError(e)}` + ) + }) + }) + ) + } private async _updateDatastore() { const datastoreCollection = this._coreHandler.core.getCollection( PeripheralDevicePubSubCollectionsNames.timelineDatastore @@ -882,6 +1016,7 @@ export class TSRHandler { return } const callbackName = callbackName0 as PeripheralDeviceAPI.PlayoutChangedType + playoutPlaybackCallbacksCounter.inc({ type: callbackName }) // debounce if (this.changedResults && this.changedResults.rundownPlaylistId !== data.rundownPlaylistId) { // The playlistId changed. Send what we have right away and reset: diff --git a/packages/playout-gateway/tsconfig.build.json b/packages/playout-gateway/tsconfig.build.json index 20e0ac52be7..c277d12980a 100644 --- a/packages/playout-gateway/tsconfig.build.json +++ b/packages/playout-gateway/tsconfig.build.json @@ -14,7 +14,8 @@ "skipLibCheck": true, "resolveJsonModule": true, "declaration": true, - "composite": true + "composite": true, + "module": "node20" }, "references": [ // diff --git a/packages/server-core-integration/examples/client.ts b/packages/server-core-integration/examples/client.ts index 27eb4f60478..8e199ebcb90 100644 --- a/packages/server-core-integration/examples/client.ts +++ b/packages/server-core-integration/examples/client.ts @@ -69,7 +69,7 @@ const setup = async () => { await core.setStatus({ statusCode: StatusCode.GOOD, - messages: [''], + statusDetails: [], }) setupObserver() @@ -80,7 +80,7 @@ const setup = async () => { console.log('updating status') core.setStatus({ statusCode: StatusCode.GOOD, - messages: ['a'], + statusDetails: [{ message: 'a' }], }).catch((e) => { console.error(`Failed to set status`, e) }) @@ -95,7 +95,7 @@ const setup = async () => { console.log('updating status') core.setStatus({ statusCode: StatusCode.GOOD, - messages: ['b'], + statusDetails: [{ message: 'b' }], }).catch((e) => { console.error(`Failed to set status`, e) }) diff --git a/packages/server-core-integration/jest.config.js b/packages/server-core-integration/jest.config.cjs similarity index 65% rename from packages/server-core-integration/jest.config.js rename to packages/server-core-integration/jest.config.cjs index 660eb87a241..da2b235b52e 100644 --- a/packages/server-core-integration/jest.config.js +++ b/packages/server-core-integration/jest.config.cjs @@ -9,12 +9,17 @@ module.exports = { diagnostics: { ignoreCodes: [ 151002, // hybrid module kind (Node16/18/Next) + 2823, // Import attributes not supported in CJS mode (ts-jest forces CJS, emits require() anyway) + 7006, // Parameter implicitly has an 'any' type + 7016, // Some import errors with underscore ], }, }, ], }, moduleNameMapper: { + '^@sofie-automation/shared-lib/dist/(.+)\\.js$': '/../shared-lib/src/$1', + '^@sofie-automation/shared-lib/dist/(.+)$': '/../shared-lib/src/$1', '(.+)\\.js$': '$1', }, testMatch: ['**/__tests__/**/*.spec.(ts|js)'], diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index ff8e3f3ed96..a07ffb4d251 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -4,8 +4,15 @@ "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", - "module": "dist/module/index.js", - "browser": "dist/browser/index.js", + "type": "module", + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./package.json": "./package.json" + }, "license": "MIT", "repository": { "type": "git", @@ -37,7 +44,6 @@ "test": "run lint && run unit", "test:integration": "run lint && run -T jest --config=jest-integration.config.js", "watch": "run -T jest --watch", - "build:prepare": "copyfiles -u 1 src/types/* dist", "cov": "run -T jest --coverage; open-cli coverage/lcov-report/index.html", "cov-open": "open-cli coverage/lcov-report/index.html", "validate:dependencies": "yarn npm audit --environment production && run license-validate", @@ -67,21 +73,21 @@ "production" ], "devDependencies": { - "@types/koa": "^3.0.1", - "@types/koa__router": "^12.0.5" + "@types/koa": "^3.0.2", + "@types/ws": "^8.18.1" }, "dependencies": { - "@koa/router": "^15.3.0", + "@koa/router": "^15.4.0", "@sofie-automation/shared-lib": "26.3.0-2", "ejson": "^2.2.3", - "faye-websocket": "^0.11.4", - "got": "^11.8.6", - "koa": "^3.1.1", + "koa": "^3.2.0", + "prom-client": "^15.1.3", "tslib": "^2.8.1", - "underscore": "^1.13.7" + "underscore": "^1.13.8", + "ws": "^8.20.0" }, "publishConfig": { "access": "public" }, - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/packages/server-core-integration/src/__mocks__/faye-websocket.ts b/packages/server-core-integration/src/__mocks__/ws.ts similarity index 68% rename from packages/server-core-integration/src/__mocks__/faye-websocket.ts rename to packages/server-core-integration/src/__mocks__/ws.ts index 520e455710c..5493a8e07c4 100644 --- a/packages/server-core-integration/src/__mocks__/faye-websocket.ts +++ b/packages/server-core-integration/src/__mocks__/ws.ts @@ -1,19 +1,25 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { EventEmitter } from 'events' -import { AnyMessage } from '../lib/ddpClient.js' +import type { AnyMessage } from '../lib/ddpClient.js' import * as EJSON from 'ejson' // import * as util from 'util' const literal = (t: T) => t -export class Client extends EventEmitter { +class MockWebSocket extends EventEmitter { private cachedId = '' private initialized = true - constructor(_url: string, _protcols?: Array | null, _options?: { [name: string]: unknown }) { + constructor(url: string, _options?: { [name: string]: unknown }) { super() + const isValidHost = url.includes('127.0.0.1') setTimeout(() => { - this.emit('open') + if (isValidHost) { + this.emit('open') + } else { + this.emit('error', new Error('Network error')) + this.emit('close', 1006, Buffer.from('Network error')) + } }, 1) } @@ -21,46 +27,50 @@ export class Client extends EventEmitter { const message = EJSON.parse(data) as AnyMessage // console.log(util.inspect(message, { depth: 10 })) if (message.msg === 'connect') { - this.emit('message', { - data: EJSON.stringify( + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'connected', session: 'wibble', }) - ), - }) + ) + ) return } if (message.msg === 'method') { if (message.method === 'peripheralDevice.initialize') { this.initialized = true - this.emit('message', { - data: EJSON.stringify( + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'result', id: message.id, result: message.params![0], }) - ), - }) + ) + ) return } if (message.method === 'systemTime.getTimeDiff') { - this.emit('message', { - data: EJSON.stringify( + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'result', id: message.id, result: { currentTime: Date.now() }, }) - ), - }) + ) + ) return } if (message.method === 'peripheralDevice.status') { if (this.initialized) { - this.emit('message', { - data: EJSON.stringify( + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'result', id: message.id, @@ -68,22 +78,24 @@ export class Client extends EventEmitter { statusCode: (message.params![2] as any).statusCode, }, }) - ), - }) - if ((message.params![2] as any).messages[0].indexOf('Jest ') >= 0) { - this.emit('message', { - data: EJSON.stringify( + ) + ) + if ((message.params![2] as any).statusDetails?.[0]?.message?.indexOf('Jest ') >= 0) { + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'changed', collection: 'peripheralDeviceForDevice', id: 'JestTest', }) - ), - }) + ) + ) } } else { - this.emit('message', { - data: EJSON.stringify( + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'result', id: message.id, @@ -92,14 +104,15 @@ export class Client extends EventEmitter { errorType: 'Meteor.Error', }, }) - ), - }) + ) + ) } return } if (message.method === 'peripheralDevice.testMethod') { - this.emit('message', { - data: EJSON.stringify( + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'result', id: message.id, @@ -112,25 +125,27 @@ export class Client extends EventEmitter { } : undefined, }) - ), - }) + ) + ) return } if (message.method === 'peripheralDevice.unInitialize') { this.initialized = false - this.emit('message', { - data: EJSON.stringify( + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'result', id: message.id, result: message.params![0], }) - ), - }) + ) + ) return } - this.emit('message', { - data: EJSON.stringify( + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'result', id: message.id, @@ -140,60 +155,62 @@ export class Client extends EventEmitter { errorType: 'Meteor.Error', }, }) - ), - }) + ) + ) return } if (message.msg === 'sub') { this.cachedId = message.params![0] as any setTimeout(() => { - this.emit('message', { - data: EJSON.stringify( + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'added', collection: message.name, id: this.cachedId, }) - ), - }) + ) + ) }, 1) setTimeout(() => { - this.emit('message', { - data: EJSON.stringify( + this.emit( + 'message', + EJSON.stringify( literal({ msg: 'ready', subs: [message.id], }) - ), - }) + ) + ) }, 100) return } if (message.msg === 'unsub') { - this.emit('message', { - data: JSON.stringify( + this.emit( + 'message', + JSON.stringify( literal({ msg: 'removed', collection: 'peripheralDeviceForDevice', id: this.cachedId, }) - ), - }) - this.emit('message', { - data: JSON.stringify( + ) + ) + this.emit( + 'message', + JSON.stringify( literal({ msg: 'nosub', id: message.id, }) - ), - }) + ) + ) } } close(): void { - this.emit('close', { - code: 200, - reason: 'I had a great time!', - wasClean: true, - }) + this.emit('close', 1200, Buffer.from('I had a great time!')) } } + +export default MockWebSocket diff --git a/packages/server-core-integration/src/__tests__/index.spec.ts b/packages/server-core-integration/src/__tests__/index.spec.ts index 0ee7092bc12..176d0189732 100644 --- a/packages/server-core-integration/src/__tests__/index.spec.ts +++ b/packages/server-core-integration/src/__tests__/index.spec.ts @@ -1,14 +1,13 @@ -import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' +import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString.js' +import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status.js' import { PeripheralDeviceCategory, PeripheralDeviceType, PERIPHERAL_SUBTYPE_PROCESS, -} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' +} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI.js' import { CoreConnection, PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames } from '../index.js' -import { DDPConnectorOptions } from '../lib/ddpClient.js' -jest.mock('faye-websocket') -jest.mock('got') +import type { DDPConnectorOptions } from '../lib/ddpClient.js' +jest.mock('ws') process.on('unhandledRejection', (reason) => { console.log('Unhandled Promise rejection!', reason) @@ -94,7 +93,7 @@ describe('coreConnection', () => { let statusResponse = await core.setStatus({ statusCode: StatusCode.WARNING_MAJOR, - messages: ['testing testing'], + statusDetails: [{ message: 'testing testing' }], }) expect(statusResponse).toMatchObject({ @@ -103,6 +102,7 @@ describe('coreConnection', () => { statusResponse = await core.setStatus({ statusCode: StatusCode.GOOD, + statusDetails: [], }) expect(statusResponse).toMatchObject({ @@ -154,8 +154,7 @@ describe('coreConnection', () => { // Set the status now (should cause an error) await expect( core.setStatus({ - statusCode: StatusCode.GOOD, - }) + statusCode: StatusCode.GOOD, statusDetails: [], }) ).rejects.toMatchObject({ error: 404, }) @@ -305,7 +304,7 @@ describe('coreConnection', () => { await core.setStatus({ statusCode: StatusCode.GOOD, - messages: ['Jest A ' + Date.now()], + statusDetails: [{ message: 'Jest A ' + Date.now() }], }) await wait(300) expect(observerChanged).toHaveBeenCalledTimes(1) @@ -323,7 +322,7 @@ describe('coreConnection', () => { observerChanged.mockClear() await core.setStatus({ statusCode: StatusCode.GOOD, - messages: ['Jest B' + Date.now()], + statusDetails: [{ message: 'Jest B' + Date.now() }], }) await wait(300) expect(observerChanged).toHaveBeenCalledTimes(1) @@ -425,7 +424,7 @@ describe('coreConnection', () => { // Set some statuses: let statusResponse = await coreChild.setStatus({ statusCode: StatusCode.WARNING_MAJOR, - messages: ['testing testing'], + statusDetails: [{ message: 'testing testing' }], }) expect(statusResponse).toMatchObject({ @@ -434,6 +433,7 @@ describe('coreConnection', () => { statusResponse = await coreChild.setStatus({ statusCode: StatusCode.GOOD, + statusDetails: [], }) expect(statusResponse).toMatchObject({ @@ -449,8 +449,7 @@ describe('coreConnection', () => { // Set the status now (should cause an error) await expect( coreChild.setStatus({ - statusCode: StatusCode.GOOD, - }) + statusCode: StatusCode.GOOD, statusDetails: [], }) ).rejects.toMatchObject({ error: 404, }) diff --git a/packages/server-core-integration/src/index.ts b/packages/server-core-integration/src/index.ts index 4318915d86d..38a13c62945 100644 --- a/packages/server-core-integration/src/index.ts +++ b/packages/server-core-integration/src/index.ts @@ -1,23 +1,26 @@ export * from './lib/coreConnection.js' +export * from './lib/CoreConnectionChild.js' export * from './lib/configManifest.js' export * from './lib/ddpClient.js' export * from './lib/gateway-types.js' export * from './lib/health.js' export * from './lib/methods.js' export * from './lib/process.js' -export { SubscriptionId } from './lib/subscriptions.js' +export * from './lib/prometheus.js' +export * from './lib/queue.js' +export type { SubscriptionId, ParametersOfFunctionOrNever } from './lib/subscriptions.js' // Re-export some util from shared-lib -export * from '@sofie-automation/shared-lib/dist/lib/lib' -export * from '@sofie-automation/shared-lib/dist/lib/stringifyError' -export * from '@sofie-automation/shared-lib/dist/lib/protectedString' -export * from '@sofie-automation/shared-lib/dist/lib/JSONBlob' -export * from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' -export * from '@sofie-automation/shared-lib/dist/lib/JSONSchemaUtil' -export { PeripheralDeviceAPIMethods } from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI' -export { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/core/model/peripheralDevice' -export { PeripheralDeviceCommand } from '@sofie-automation/shared-lib/dist/core/model/PeripheralDeviceCommand' -export { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' -export * as PeripheralDeviceAPI from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' -export { PeripheralDeviceId, StudioId } from '@sofie-automation/shared-lib/dist/core/model/Ids' -export * from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' +export * from '@sofie-automation/shared-lib/dist/lib/lib.js' +export * from '@sofie-automation/shared-lib/dist/lib/stringifyError.js' +export * from '@sofie-automation/shared-lib/dist/lib/protectedString.js' +export * from '@sofie-automation/shared-lib/dist/lib/JSONBlob.js' +export * from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes.js' +export * from '@sofie-automation/shared-lib/dist/lib/JSONSchemaUtil.js' +export { PeripheralDeviceAPIMethods } from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI.js' +export type { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/core/model/peripheralDevice.js' +export type { PeripheralDeviceCommand } from '@sofie-automation/shared-lib/dist/core/model/PeripheralDeviceCommand.js' +export { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status.js' +export * as PeripheralDeviceAPI from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI.js' +export type { PeripheralDeviceId, StudioId } from '@sofie-automation/shared-lib/dist/core/model/Ids.js' +export * from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice.js' diff --git a/packages/server-core-integration/src/integrationTests/index.spec.ts b/packages/server-core-integration/src/integrationTests/index.spec.ts index ceaccdb4d49..6ffe673b3f0 100644 --- a/packages/server-core-integration/src/integrationTests/index.spec.ts +++ b/packages/server-core-integration/src/integrationTests/index.spec.ts @@ -1,11 +1,11 @@ jest.dontMock('ddp') -import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' +import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString.js' +import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status.js' import { PERIPHERAL_SUBTYPE_PROCESS, PeripheralDeviceCategory, PeripheralDeviceType, -} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' +} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI.js' import { CoreConnection, PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames } from '../index.js' process.on('unhandledRejection', (reason) => { @@ -68,7 +68,7 @@ test('Integration: Test connection and basic Core functionality', async () => { let statusResponse = await core.setStatus({ statusCode: StatusCode.WARNING_MAJOR, - messages: ['testing testing'], + statusDetails: [{ message: 'testing testing' }], }) expect(statusResponse).toMatchObject({ @@ -77,6 +77,7 @@ test('Integration: Test connection and basic Core functionality', async () => { statusResponse = await core.setStatus({ statusCode: StatusCode.GOOD, + statusDetails: [], }) expect(statusResponse).toMatchObject({ @@ -130,6 +131,7 @@ test('Integration: Test connection and basic Core functionality', async () => { await expect( core.setStatus({ statusCode: StatusCode.GOOD, + statusDetails: [], }) ).rejects.toMatchObject({ error: 404, @@ -285,7 +287,7 @@ test('Integration: autoSubscription', async () => { await core.setStatus({ statusCode: StatusCode.GOOD, - messages: ['Jest A ' + Date.now()], + statusDetails: [{ message: 'Jest A ' + Date.now() }], }) await wait(300) expect(observerChanged).toHaveBeenCalledTimes(1) @@ -303,7 +305,7 @@ test('Integration: autoSubscription', async () => { observerChanged.mockClear() await core.setStatus({ statusCode: StatusCode.GOOD, - messages: ['Jest B' + Date.now()], + statusDetails: [{ message: 'Jest B' + Date.now() }], }) await wait(300) expect(observerChanged).toHaveBeenCalledTimes(1) @@ -404,7 +406,7 @@ test('Integration: Parent connections', async () => { // Set some statuses: let statusResponse = await coreChild.setStatus({ statusCode: StatusCode.WARNING_MAJOR, - messages: ['testing testing'], + statusDetails: [{ message: 'testing testing' }], }) expect(statusResponse).toMatchObject({ @@ -413,6 +415,7 @@ test('Integration: Parent connections', async () => { statusResponse = await coreChild.setStatus({ statusCode: StatusCode.GOOD, + statusDetails: [], }) expect(statusResponse).toMatchObject({ @@ -428,8 +431,7 @@ test('Integration: Parent connections', async () => { // Set the status now (should cause an error) await expect( coreChild.setStatus({ - statusCode: StatusCode.GOOD, - }) + statusCode: StatusCode.GOOD, statusDetails: [], }) ).rejects.toMatchObject({ error: 404, }) diff --git a/packages/server-core-integration/src/lib/CoreConnectionChild.ts b/packages/server-core-integration/src/lib/CoreConnectionChild.ts index 074d5c5f834..9be027a5dcf 100644 --- a/packages/server-core-integration/src/lib/CoreConnectionChild.ts +++ b/packages/server-core-integration/src/lib/CoreConnectionChild.ts @@ -1,22 +1,22 @@ import { EventEmitter } from 'events' -import { +import type { PeripheralDeviceStatusObject, PeripheralDeviceInitOptions, PeripheralDeviceSubType, -} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' -import { PeripheralDeviceAPIMethods } from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI' -import { DDPConnector } from './ddpConnector.js' -import { Observer } from './ddpClient.js' -import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids' -import { ConnectionMethodsQueue, ExternalPeripheralDeviceAPI, makeMethods, makeMethodsLowPrio } from './methods.js' -import { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/core/model/peripheralDevice' -import { CoreConnection, Collection, CoreOptions, CollectionDocCheck } from './coreConnection.js' +} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI.js' +import { PeripheralDeviceAPIMethods } from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI.js' +import type { DDPConnector } from './ddpConnector.js' +import type { Observer } from './ddpClient.js' +import type { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids.js' +import { ConnectionMethodsQueue, type ExternalPeripheralDeviceAPI, makeMethods, makeMethodsLowPrio } from './methods.js' +import type { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/core/model/peripheralDevice.js' +import type { CoreConnection, Collection, CoreOptions, CollectionDocCheck } from './coreConnection.js' import { CorePinger } from './ping.js' -import { ParametersOfFunctionOrNever, SubscriptionId, SubscriptionsHelper } from './subscriptions.js' -import { +import { type ParametersOfFunctionOrNever, type SubscriptionId, SubscriptionsHelper } from './subscriptions.js' +import type { PeripheralDevicePubSubCollections, PeripheralDevicePubSubTypes, -} from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' +} from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice.js' export interface ChildCoreOptions { deviceId: PeripheralDeviceId diff --git a/packages/server-core-integration/src/lib/__tests__/ddpClient.spec.ts b/packages/server-core-integration/src/lib/__tests__/ddpClient.spec.ts index 17e3248e401..c678d74f7f6 100644 --- a/packages/server-core-integration/src/lib/__tests__/ddpClient.spec.ts +++ b/packages/server-core-integration/src/lib/__tests__/ddpClient.spec.ts @@ -1,5 +1,5 @@ -import { DDPClient, DDPConnectorOptions } from '../../index.js' -jest.mock('faye-websocket') +import { DDPClient, type DDPConnectorOptions } from '../../index.js' +jest.mock('ws') const wait = async (t: number): Promise => new Promise((resolve) => { diff --git a/packages/server-core-integration/src/lib/configManifest.ts b/packages/server-core-integration/src/lib/configManifest.ts index 0933a55b071..f5f0f7c4b2d 100644 --- a/packages/server-core-integration/src/lib/configManifest.ts +++ b/packages/server-core-integration/src/lib/configManifest.ts @@ -1 +1 @@ -export * from '@sofie-automation/shared-lib/dist/core/deviceConfigManifest' +export * from '@sofie-automation/shared-lib/dist/core/deviceConfigManifest.js' diff --git a/packages/server-core-integration/src/lib/coreConnection.ts b/packages/server-core-integration/src/lib/coreConnection.ts index c5f59ab703a..41107abee95 100644 --- a/packages/server-core-integration/src/lib/coreConnection.ts +++ b/packages/server-core-integration/src/lib/coreConnection.ts @@ -1,33 +1,32 @@ import { EventEmitter } from 'events' import _ from 'underscore' import { - PeripheralDeviceCategory, + type PeripheralDeviceCategory, PERIPHERAL_SUBTYPE_PROCESS, - PeripheralDeviceStatusObject, - PeripheralDeviceInitOptions, - PeripheralDeviceType, -} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' -import { PeripheralDeviceAPIMethods } from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI' + type PeripheralDeviceStatusObject, + type PeripheralDeviceInitOptions, + type PeripheralDeviceType, +} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI.js' +import { PeripheralDeviceAPIMethods } from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI.js' import { DDPConnector } from './ddpConnector.js' -import { DDPConnectorOptions, Observer } from './ddpClient.js' +import type { DDPConnectorOptions, Observer } from './ddpClient.js' import { TimeSync } from './timeSync.js' import { WatchDog } from './watchDog.js' -import { DeviceConfigManifest } from './configManifest.js' -import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids' -import { ConnectionMethodsQueue, ExternalPeripheralDeviceAPI, makeMethods, makeMethodsLowPrio } from './methods.js' -import { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/core/model/peripheralDevice' -import { ProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { ChildCoreOptions, CoreConnectionChild } from './CoreConnectionChild.js' +import type { DeviceConfigManifest } from './configManifest.js' +import type { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids.js' +import { ConnectionMethodsQueue, type ExternalPeripheralDeviceAPI, makeMethods, makeMethodsLowPrio } from './methods.js' +import type { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/core/model/peripheralDevice.js' +import type { ProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString.js' +import { type ChildCoreOptions, CoreConnectionChild } from './CoreConnectionChild.js' import { CorePinger } from './ping.js' -import { ParametersOfFunctionOrNever, SubscriptionId, SubscriptionsHelper } from './subscriptions.js' -import { +import { type ParametersOfFunctionOrNever, type SubscriptionId, SubscriptionsHelper } from './subscriptions.js' +import type { PeripheralDevicePubSubCollections, PeripheralDevicePubSubTypes, -} from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' - -const PkgInfo = require('../../package.json') +} from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice.js' +import PkgInfo from '../../package.json' with { type: 'json' } export interface CoreCredentials { deviceId: PeripheralDeviceId @@ -148,11 +147,11 @@ export class CoreConnection< }) this._ddp.on('connected', () => { // this.emit('connected') - if (this._watchDog) this._watchDog.addCheck(async () => this._watchDogCheck()) + if (this._watchDog) this._watchDog.addCheck(this._watchDogCheck) }) this._ddp.on('disconnected', () => { // this.emit('disconnected') - if (this._watchDog) this._watchDog.removeCheck(async () => this._watchDogCheck()) + if (this._watchDog) this._watchDog.removeCheck(this._watchDogCheck) }) this._ddp.on('message', () => { if (this._watchDog) this._watchDog.receivedData() @@ -434,7 +433,7 @@ export class CoreConnection< return this.coreMethods.initialize(options) } - private async _watchDogCheck() { + private _watchDogCheck = async () => { /* Randomize a message and send it to Core. Core should then reply with triggering executeFunction with the "pingResponse" method. diff --git a/packages/server-core-integration/src/lib/ddpClient.ts b/packages/server-core-integration/src/lib/ddpClient.ts index d5700478a50..ca76e5025fe 100644 --- a/packages/server-core-integration/src/lib/ddpClient.ts +++ b/packages/server-core-integration/src/lib/ddpClient.ts @@ -6,15 +6,12 @@ * * Brought into this project for maintenance reasons, including conversion to Typescript. */ -/// - -import * as WebSocket from 'faye-websocket' -import * as EJSON from 'ejson' +import WebSocket from 'ws' +import EJSON from 'ejson' import { EventEmitter } from 'events' -import got from 'got' -import { ProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import type { ProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString.js' -export interface TLSOpts { +export interface DDPTLSOptions { // Described in https://nodejs.org/api/tls.html#tls_tls_connect_options_callback /* Necessary only if the server uses a self-signed certificate.*/ @@ -26,6 +23,9 @@ export interface TLSOpts { /* Necessary only if the server's cert isn't for "localhost". */ checkServerIdentity?: (hostname: string, cert: object) => Error | undefined // () => { }, // Returns object, populating it with reason, host, and cert on failure. On success, returns . + + /* If true, the server certificate is automatically rejected if it is not valid. The default value is true. */ + rejectUnauthorized?: boolean } /** @@ -40,8 +40,7 @@ export interface DDPConnectorOptions { debug?: boolean autoReconnect?: boolean // default: true autoReconnectTimer?: number - tlsOpts?: TLSOpts - useSockJs?: boolean + tlsOpts?: DDPTLSOptions url?: string maintainCollections?: boolean ddpVersion?: '1' | 'pre2' | 'pre1' @@ -315,7 +314,7 @@ export type DDPClientEvents = { failed: [error: Error] 'socket-error': [error: Error] 'socket-close': [code: number, reason: string] - message: [data: any] + message: [data: string] connected: [] } @@ -333,7 +332,7 @@ export class DDPClient extends EventEmitter { } } = {} - public socket: WebSocket.Client | undefined + public socket: WebSocket | undefined public session: string | undefined private hostInt!: string @@ -356,10 +355,6 @@ export class DDPClient extends EventEmitter { public get ssl(): boolean { return this.sslInt } - private useSockJSInt!: boolean - public get useSockJS(): boolean { - return this.useSockJSInt - } private autoReconnectInt!: boolean public get autoReconnect(): boolean { return this.autoReconnectInt @@ -390,7 +385,7 @@ export class DDPClient extends EventEmitter { } public static readonly supportedDdpVersions = ['1', 'pre2', 'pre1'] - private tlsOpts!: TLSOpts + private tlsOpts!: DDPTLSOptions private isConnecting = false private isReconnecting = false private isClosing = false @@ -419,7 +414,6 @@ export class DDPClient extends EventEmitter { this.pathInt = opts.path this.sslInt = opts.ssl || this.port === 443 this.tlsOpts = opts.tlsOpts || {} - this.useSockJSInt = opts.useSockJs || false this.autoReconnectInt = opts.autoReconnect === false ? false : true this.autoReconnectTimerInt = opts.autoReconnectTimer || 500 this.maintainCollectionsInt = opts.maintainCollections || true @@ -692,14 +686,7 @@ export class DDPClient extends EventEmitter { }) } - if (this.useSockJS) { - this.makeSockJSConnection().catch((e) => { - this.emit('failed', e) - }) - } else { - const url = this.buildWsUrl() - this.makeWebSocketConnection(url) - } + this.makeWebSocketConnection(this.buildWsUrl()) } private endPendingMethodCalls(): void { @@ -726,57 +713,19 @@ export class DDPClient extends EventEmitter { } } - private async makeSockJSConnection(): Promise { - const protocol = this.ssl ? 'https://' : 'http://' - if (this.path && !this.path?.endsWith('/')) { - this.pathInt = this.path + '/' - } - const url = `${protocol}${this.host}:${this.port}/${this.path || ''}sockjs/info` - - try { - const response = await got(url, { - headers: this.getHeadersWithDefaults(), - https: { - certificateAuthority: this.tlsOpts.ca, - key: this.tlsOpts.key, - certificate: this.tlsOpts.cert, - checkServerIdentity: this.tlsOpts.checkServerIdentity, - }, - responseType: 'json', - }) - // Info object defined here(?): https://github.com/sockjs/sockjs-node/blob/master/lib/info.js - const info = response.body as { base_url: string } - if (!info || !info.base_url) { - const url = this.buildWsUrl() - this.makeWebSocketConnection(url) - } else if (info.base_url.indexOf('http') === 0) { - const url = (info.base_url + '/websocket').replace(/^http/, 'ws') - this.makeWebSocketConnection(url) - } else { - const path = info.base_url + '/websocket' - const url = this.buildWsUrl(path) - this.makeWebSocketConnection(url) - } - } catch (err) { - this.recoverNetworkError(err) - } - } - - private buildWsUrl(path?: string): string { - let url: string - path = path || this.path || 'websocket' - const protocol = this.ssl ? 'wss://' : 'ws://' - if (this.url && !this.useSockJS) { - url = this.url + private buildWsUrl(): string { + if (this.url) { + return this.url } else { - url = `${protocol}${this.host}:${this.port}${path.indexOf('/') === 0 ? path : '/' + path}` + const path = this.path || 'websocket' + const protocol = this.ssl ? 'wss://' : 'ws://' + return `${protocol}${this.host}:${this.port}${path.indexOf('/') === 0 ? path : '/' + path}` } - return url } private makeWebSocketConnection(url: string): void { // console.log('About to create WebSocket client') - this.socket = new WebSocket.Client(url, null, { tls: this.tlsOpts, headers: this.getHeadersWithDefaults() }) + this.socket = new WebSocket(url, { ...this.tlsOpts, headers: this.getHeadersWithDefaults() }) this.socket.on('open', () => { // just go ahead and open the connection on connect @@ -796,15 +745,21 @@ export class DDPClient extends EventEmitter { this.emit('socket-error', error) }) - this.socket.on('close', (event) => { - this.emit('socket-close', event.code, event.reason) + this.socket.on('close', (code, reason) => { + this.emit('socket-close', code, reason.toString()) this.endPendingMethodCalls() this.recoverNetworkError() }) - this.socket.on('message', (event) => { - this.message(event.data) - this.emit('message', event.data) + this.socket.on('message', (data) => { + const str = + typeof data === 'string' + ? data + : Array.isArray(data) + ? Buffer.concat(data).toString('utf-8') + : Buffer.from(data as ArrayBuffer).toString('utf-8') + this.message(str) + this.emit('message', str) }) } diff --git a/packages/server-core-integration/src/lib/ddpConnector.ts b/packages/server-core-integration/src/lib/ddpConnector.ts index d62dca74f4b..7ac704a696b 100644 --- a/packages/server-core-integration/src/lib/ddpConnector.ts +++ b/packages/server-core-integration/src/lib/ddpConnector.ts @@ -1,10 +1,10 @@ import { EventEmitter } from 'events' -import { DDPClient, DDPConnectorOptions } from './ddpClient.js' +import { DDPClient, type DDPConnectorOptions } from './ddpClient.js' export type DDPConnectorEvents = { error: [e: any] failed: [error: Error] - message: [message: any] + message: [message: string] connectionChanged: [connected: boolean] connected: [] @@ -20,7 +20,7 @@ export class DDPConnector extends EventEmitter { private _connectionId: string | undefined = undefined private ddpIsOpen = false - private _monitorDDPConnectionInterval: any = null + private _monitorDDPConnectionInterval: NodeJS.Timeout | null = null constructor(options: DDPConnectorOptions) { super() @@ -35,7 +35,6 @@ export class DDPConnector extends EventEmitter { path: this._options.path || '', ssl: this._options.ssl || false, tlsOpts: this._options.tlsOpts || {}, - useSockJs: true, autoReconnect: false, // we'll handle reconnections ourselves autoReconnectTimer: 1000, maintainCollections: true, @@ -48,8 +47,8 @@ export class DDPConnector extends EventEmitter { this.ddpClient.on('socket-close', () => { this._onclientConnectionChange(false) }) - this.ddpClient.on('message', (message: any) => this._onClientMessage(message)) - this.ddpClient.on('socket-error', (error: any) => this._onClientError(error)) + this.ddpClient.on('message', (message) => this._onClientMessage(message)) + this.ddpClient.on('socket-error', (error) => this._onClientError(error)) } else { if (this.ddpClient.socket) { this.ddpClient.close() @@ -162,7 +161,7 @@ export class DDPConnector extends EventEmitter { } this._monitorDDPConnection() } - private _onClientMessage(message: any) { + private _onClientMessage(message: string) { // message this.emit('message', message) } diff --git a/packages/server-core-integration/src/lib/gateway-types.ts b/packages/server-core-integration/src/lib/gateway-types.ts index e2745a6aa86..b847ffe63cf 100644 --- a/packages/server-core-integration/src/lib/gateway-types.ts +++ b/packages/server-core-integration/src/lib/gateway-types.ts @@ -1,4 +1,4 @@ -import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' +import type { PeripheralDeviceStatusObject } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' export interface IConnector { initialized: boolean @@ -6,6 +6,6 @@ export interface IConnector { } export interface ICoreHandler { - getCoreStatus: () => { statusCode: StatusCode; messages: string[] } + getCoreStatus: () => PeripheralDeviceStatusObject connectedToCore: boolean } diff --git a/packages/server-core-integration/src/lib/health.ts b/packages/server-core-integration/src/lib/health.ts index 05c2e6c9c92..09d69bd8c6f 100644 --- a/packages/server-core-integration/src/lib/health.ts +++ b/packages/server-core-integration/src/lib/health.ts @@ -1,8 +1,9 @@ import Koa from 'koa' import Router from '@koa/router' -import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' -import { assertNever } from '@sofie-automation/shared-lib/dist/lib/lib' -import { IConnector, ICoreHandler } from './gateway-types.js' +import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status.js' +import { assertNever } from '@sofie-automation/shared-lib/dist/lib/lib.js' +import type { IConnector, ICoreHandler } from './gateway-types.js' +import { getPrometheusMetricsString, PrometheusHTTPContentType, setupPrometheusMetrics } from './prometheus.js' export interface HealthConfig { /** If set, exposes health HTTP endpoints on the given port */ @@ -18,13 +19,17 @@ export class HealthEndpoints { constructor( private connector: IConnector, private coreHandler: ICoreHandler, - private config: HealthConfig + private config: HealthConfig, + private customMetrics?: () => Promise ) { if (!config.port) return // disabled + // Setup default prometheus metrics when endpoints are enabled + setupPrometheusMetrics() + const router = new Router() - router.get('/healthz', async (ctx) => { + router.get('/healthz', async (ctx: Koa.Context) => { if (this.connector.initializedError !== undefined) { ctx.status = 503 ctx.body = `Error during initialization: ${this.connector.initializedError}` @@ -47,13 +52,14 @@ export class HealthEndpoints { else assertNever(coreStatus.statusCode) if (ctx.status !== 200) { - ctx.body = `Status: ${StatusCode[coreStatus.statusCode]}, messages: ${coreStatus.messages.join(', ')}` + const messages = coreStatus.statusDetails.map((d) => d.message).join(', ') + ctx.body = `Status: ${StatusCode[coreStatus.statusCode]}, messages: ${messages}` } else { ctx.body = 'OK' } }) - router.get('/readyz', async (ctx) => { + router.get('/readyz', async (ctx: Koa.Context) => { if (!this.coreHandler.connectedToCore) { ctx.status = 503 ctx.body = 'Not connected to Core' @@ -64,6 +70,22 @@ export class HealthEndpoints { ctx.body = 'READY' }) + router.get('/metrics', async (ctx: Koa.Context) => { + try { + ctx.response.type = PrometheusHTTPContentType + + const [meteorMetrics, workerMetrics] = await Promise.all([ + getPrometheusMetricsString(), + this.customMetrics?.(), + ]) + + ctx.body = [meteorMetrics, ...(workerMetrics || [])].join('\n\n') + } catch (ex) { + ctx.response.status = 500 + ctx.body = ex + '' + } + }) + this.app.use(router.routes()).use(router.allowedMethods()) this.app.listen(this.config.port) } diff --git a/packages/server-core-integration/src/lib/methods.ts b/packages/server-core-integration/src/lib/methods.ts index 290fb65c40d..479b051847e 100644 --- a/packages/server-core-integration/src/lib/methods.ts +++ b/packages/server-core-integration/src/lib/methods.ts @@ -1,12 +1,12 @@ -import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids' -import { +import type { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids.js' +import type { NewPeripheralDeviceAPI, PeripheralDeviceAPIMethods, -} from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI' +} from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI.js' import _ from 'underscore' -import { CoreConnection, CoreCredentials } from './coreConnection.js' -import { DDPError } from './ddpClient.js' -import { DDPConnector } from './ddpConnector.js' +import type { CoreConnection, CoreCredentials } from './coreConnection.js' +import type { DDPError } from './ddpClient.js' +import type { DDPConnector } from './ddpConnector.js' export function makeMethods(connection: Pick, methods: object): any { const o: any = {} diff --git a/packages/server-core-integration/src/lib/process.ts b/packages/server-core-integration/src/lib/process.ts index d594a56e57a..aef1bb906d2 100644 --- a/packages/server-core-integration/src/lib/process.ts +++ b/packages/server-core-integration/src/lib/process.ts @@ -1,23 +1,28 @@ import * as fs from 'fs' +import type { DDPTLSOptions } from './ddpClient.js' export interface CertificatesConfig { - /** Will cause the Node applocation to blindly accept all certificates. Not recommenced unless in local, controlled networks. */ + /** Will cause the Node application to blindly accept all certificates. Not recommended unless in local, controlled networks. */ unsafeSSL: boolean /** Paths to certificates to load, for SSL-connections */ certificates: string[] } -export function loadCertificatesFromDisk(logger: SomeLogger, certConfig: CertificatesConfig): Buffer[] { +/** The subset of TLSOpts that can be derived from a CertificatesConfig */ + +export function loadDDPTLSOptions(logger: SomeLogger, certConfig: CertificatesConfig): DDPTLSOptions { + const result: DDPTLSOptions = {} + if (certConfig.unsafeSSL) { - logger.info('Disabling NODE_TLS_REJECT_UNAUTHORIZED, be sure to ONLY DO THIS ON A LOCAL NETWORK!') - process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' - } else { - // var rootCas = SSLRootCAs.create() + logger.info( + 'Disabling certificate validation (rejectUnauthorized: false), be sure to ONLY DO THIS ON A LOCAL NETWORK!' + ) + result.rejectUnauthorized = false } - const certificates: Buffer[] = [] if (certConfig.certificates.length) { logger.info(`Loading certificates...`) + const certificates: Buffer[] = [] for (const certificate of certConfig.certificates) { try { certificates.push(fs.readFileSync(certificate)) @@ -26,9 +31,10 @@ export function loadCertificatesFromDisk(logger: SomeLogger, certConfig: Certifi logger.error(`Error loading certificate "${certificate}"`, error) } } + result.ca = certificates } - return certificates + return result } interface SomeLogger { diff --git a/packages/server-core-integration/src/lib/prometheus.ts b/packages/server-core-integration/src/lib/prometheus.ts new file mode 100644 index 00000000000..4d20a535c51 --- /dev/null +++ b/packages/server-core-integration/src/lib/prometheus.ts @@ -0,0 +1,34 @@ +import { register, collectDefaultMetrics } from 'prom-client' + +// Re-export types, to ensure the correct 'instance' of 'prom-client' is used +export { + Gauge as MetricsGauge, + Counter as MetricsCounter, + Histogram as MetricsHistogram, + Summary as MetricsSummary, +} from 'prom-client' + +/** + * HTTP Content-type header for the metrics + */ +export const PrometheusHTTPContentType = register.contentType + +/** + * Stringified metrics for serving over HTTP + */ +export async function getPrometheusMetricsString(): Promise { + return register.metrics() +} + +/** + * Setup metric reporting for this app + */ +export function setupPrometheusMetrics(): void { + // Label all metrics with the source 'thread' + // register.setDefaultLabels({ + // threadName: threadName, + // }) + + // Collect the default metrics nodejs metrics + collectDefaultMetrics() +} diff --git a/packages/server-core-integration/src/lib/subscriptions.ts b/packages/server-core-integration/src/lib/subscriptions.ts index 53fa7278b79..93e5ff80e00 100644 --- a/packages/server-core-integration/src/lib/subscriptions.ts +++ b/packages/server-core-integration/src/lib/subscriptions.ts @@ -1,4 +1,8 @@ -import { ProtectedString, protectString, unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { + type ProtectedString, + protectString, + unprotectString, +} from '@sofie-automation/shared-lib/dist/lib/protectedString.js' import type { DDPConnector } from './ddpConnector.js' export type SubscriptionId = ProtectedString<'SubscriptionId'> diff --git a/packages/server-core-integration/src/types/faye-websocket.d.ts b/packages/server-core-integration/src/types/faye-websocket.d.ts deleted file mode 100644 index 04ec54ae433..00000000000 --- a/packages/server-core-integration/src/types/faye-websocket.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -declare module 'faye-websocket' { - export interface MessageEvent { - data: any - } - - export interface CloseEvent { - code: number - reason: string - wasClean: boolean - } - - export class Client { - constructor(url: string, protcols?: Array | null, options?: { [name: string]: unknown }) - send(data: string): void - close(code?: number, reason?: string): void - - on(event: 'open', cb: () => void): void - on(event: 'message', cb: (msg: MessageEvent) => void): void - on(event: 'close', cb: (event: CloseEvent) => void): void - on(event: 'error', cb: (error: Error) => void): void - } -} diff --git a/packages/server-core-integration/tsconfig.build.json b/packages/server-core-integration/tsconfig.build.json index fe4684c51f8..d6325e295d2 100755 --- a/packages/server-core-integration/tsconfig.build.json +++ b/packages/server-core-integration/tsconfig.build.json @@ -1,20 +1,18 @@ { "extends": "@sofie-automation/code-standard-preset/ts/tsconfig.lib", - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "package.json"], "exclude": ["node_modules/**", "**/*spec.ts", "**/__tests__/*", "**/__mocks__/*"], "compilerOptions": { "outDir": "./dist", "rootDir": "./src", "baseUrl": "./", - "paths": { - "*": ["./node_modules/*"], - "@sofie-automation/server-core-integration": ["./src/index.ts"] - }, "resolveJsonModule": true, "types": ["node"], "skipLibCheck": true, "esModuleInterop": true, - "composite": true + "composite": true, + "module": "node20", + "verbatimModuleSyntax": true }, "references": [ { diff --git a/packages/server-core-integration/tsconfig.json b/packages/server-core-integration/tsconfig.json index e2561ed6e1e..1bf941dc126 100755 --- a/packages/server-core-integration/tsconfig.json +++ b/packages/server-core-integration/tsconfig.json @@ -1,9 +1,10 @@ { "extends": "./tsconfig.build.json", "exclude": ["node_modules/**"], - "include": ["src/**/*.ts", "examples/*.ts"], + "include": ["src/**/*.ts", "examples/*.ts", "package.json"], "compilerOptions": { "rootDir": "./", - "types": ["jest", "node"] + "types": ["jest", "node"], + "verbatimModuleSyntax": false } } diff --git a/packages/shared-lib/jest.config.js b/packages/shared-lib/jest.config.cjs similarity index 91% rename from packages/shared-lib/jest.config.js rename to packages/shared-lib/jest.config.cjs index 04b8ea8dd1c..08e7d190d9b 100644 --- a/packages/shared-lib/jest.config.js +++ b/packages/shared-lib/jest.config.cjs @@ -18,7 +18,7 @@ module.exports = { '(.+)\\.js$': '$1', }, testMatch: ['**/__tests__/**/*.(spec|test).(ts|js)'], - testPathIgnorePatterns: ['integrationTests'], + testPathIgnorePatterns: ['integrationTests', '/dist/'], testEnvironment: 'node', // coverageThreshold: { // global: { diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 32730339cba..9dc70111700 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -5,6 +5,20 @@ "main": "dist/index.js", "typings": "dist/index.d.ts", "license": "MIT", + "type": "module", + "exports": { + "./dist/*.js": { + "types": "./dist/*.d.ts", + "require": "./dist/*.js", + "import": "./dist/*.js" + }, + "./dist/*": { + "types": "./dist/*.d.ts", + "require": "./dist/*.js", + "import": "./dist/*.js" + }, + "./package.json": "./package.json" + }, "repository": { "type": "git", "url": "git+https://github.com/nrkno/sofie-core.git", @@ -36,8 +50,8 @@ ], "dependencies": { "@mos-connection/model": "^5.0.0-alpha.0", - "kairos-lib": "^0.2.3", - "timeline-state-resolver-types": "10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0", + "kairos-lib": "^1.0.0", + "timeline-state-resolver-types": "10.0.0-nightly-main-20260602-133931-c0882da4d.0", "tslib": "^2.8.1", "type-fest": "^4.41.0" }, @@ -45,5 +59,5 @@ "publishConfig": { "access": "public" }, - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.14.1" } diff --git a/packages/shared-lib/src/core/deviceConfigManifest.ts b/packages/shared-lib/src/core/deviceConfigManifest.ts index 6b385cac8f9..033e64fbe1e 100644 --- a/packages/shared-lib/src/core/deviceConfigManifest.ts +++ b/packages/shared-lib/src/core/deviceConfigManifest.ts @@ -11,10 +11,10 @@ * describe some properties to be rendered inside this table */ -import { JSONBlob } from '../lib/JSONBlob.js' -import { TSRActionSchema } from 'timeline-state-resolver-types' -import { TranslationsBundle } from '../lib/translations.js' -import { JSONSchema } from '../lib/JSONSchemaTypes.js' +import type { JSONBlob } from '../lib/JSONBlob.js' +import type { TSRActionSchema } from 'timeline-state-resolver-types' +import type { TranslationsBundle } from '../lib/translations.js' +import type { JSONSchema } from '../lib/JSONSchemaTypes.js' export interface DeviceConfigManifest { /** diff --git a/packages/shared-lib/src/core/model/Ids.ts b/packages/shared-lib/src/core/model/Ids.ts index f9c989af902..6d864a6235c 100644 --- a/packages/shared-lib/src/core/model/Ids.ts +++ b/packages/shared-lib/src/core/model/Ids.ts @@ -1,4 +1,4 @@ -import { ProtectedString } from '../../lib/protectedString.js' +import type { ProtectedString } from '../../lib/protectedString.js' /** A string, identifying a Studio */ export type StudioId = ProtectedString<'StudioId'> diff --git a/packages/shared-lib/src/core/model/MediaObjects.ts b/packages/shared-lib/src/core/model/MediaObjects.ts index 22d8dec854e..904994f38c9 100644 --- a/packages/shared-lib/src/core/model/MediaObjects.ts +++ b/packages/shared-lib/src/core/model/MediaObjects.ts @@ -1,5 +1,5 @@ -import { PackageInfo } from '../../package-manager/packageInfo.js' -import { MediaObjId, StudioId } from './Ids.js' +import type { PackageInfo } from '../../package-manager/packageInfo.js' +import type { MediaObjId, StudioId } from './Ids.js' export interface MediaObject0 { _id: MediaObjId diff --git a/packages/shared-lib/src/core/model/PackageContainer.ts b/packages/shared-lib/src/core/model/PackageContainer.ts index 27ce8d86a34..10c95616c20 100644 --- a/packages/shared-lib/src/core/model/PackageContainer.ts +++ b/packages/shared-lib/src/core/model/PackageContainer.ts @@ -1,4 +1,4 @@ -import { PackageContainer } from '../../package-manager/package.js' +import type { PackageContainer } from '../../package-manager/package.js' export interface StudioPackageContainer { /** List of which peripheraldevices uses this packageContainer */ diff --git a/packages/shared-lib/src/core/model/PeripheralDeviceCommand.ts b/packages/shared-lib/src/core/model/PeripheralDeviceCommand.ts index 58d9330f0ea..6d74fca9cf0 100644 --- a/packages/shared-lib/src/core/model/PeripheralDeviceCommand.ts +++ b/packages/shared-lib/src/core/model/PeripheralDeviceCommand.ts @@ -1,5 +1,5 @@ -import { Time } from '../../lib/lib.js' -import { PeripheralDeviceCommandId, PeripheralDeviceId } from './Ids.js' +import type { Time } from '../../lib/lib.js' +import type { PeripheralDeviceCommandId, PeripheralDeviceId } from './Ids.js' export interface PeripheralDeviceCommand { _id: PeripheralDeviceCommandId diff --git a/packages/shared-lib/src/core/model/StudioRouteSet.ts b/packages/shared-lib/src/core/model/StudioRouteSet.ts index 386f56cd8cf..27ca542979e 100644 --- a/packages/shared-lib/src/core/model/StudioRouteSet.ts +++ b/packages/shared-lib/src/core/model/StudioRouteSet.ts @@ -1,5 +1,5 @@ -import { BlueprintMapping } from './Timeline.js' -import { TSR } from '../../tsr.js' +import type { BlueprintMapping } from './Timeline.js' +import type { TSR } from '../../tsr.js' export type AbPlayerId = number | string diff --git a/packages/shared-lib/src/core/model/StudioSettings.ts b/packages/shared-lib/src/core/model/StudioSettings.ts index 1a117f18388..574dbc84f5d 100644 --- a/packages/shared-lib/src/core/model/StudioSettings.ts +++ b/packages/shared-lib/src/core/model/StudioSettings.ts @@ -7,6 +7,12 @@ export enum ForceQuickLoopAutoNext { ENABLED_FORCING_MIN_DURATION = 'enabled_forcing_min_duration', } +export enum ShelfButtonSize { + INHERIT = 'inherit', + COMPACT = 'compact', + LARGE = 'large', +} + export interface IStudioSettings { /** The framerate (frames per second) used to convert internal timing information (in milliseconds) * into timecodes and timecode-like strings and interpret timecode user input @@ -90,6 +96,11 @@ export interface IStudioSettings { */ enableEvaluationForm: boolean + /** + * Default size of AdLib buttons in shelf-like UIs (eg mini shelf). + */ + shelfAdlibButtonSize: Exclude + /** * Doubleclick changes behaviour as selector for userediting */ diff --git a/packages/shared-lib/src/core/model/Timeline.ts b/packages/shared-lib/src/core/model/Timeline.ts index 1a6231cc189..addf75c61be 100644 --- a/packages/shared-lib/src/core/model/Timeline.ts +++ b/packages/shared-lib/src/core/model/Timeline.ts @@ -1,6 +1,6 @@ import { unprotectString, protectString } from '../../lib/protectedString.js' -import { TSR } from '../../tsr.js' -import { MappingsHash, PeripheralDeviceId, StudioId, TimelineBlob, TimelineHash } from './Ids.js' +import type { TSR } from '../../tsr.js' +import type { MappingsHash, PeripheralDeviceId, StudioId, TimelineBlob, TimelineHash } from './Ids.js' /** * This defines a session, indicating that this TimelineObject uses an AB player diff --git a/packages/shared-lib/src/core/model/TimelineDatastore.ts b/packages/shared-lib/src/core/model/TimelineDatastore.ts index e4cef258722..b9d2e8d06cf 100644 --- a/packages/shared-lib/src/core/model/TimelineDatastore.ts +++ b/packages/shared-lib/src/core/model/TimelineDatastore.ts @@ -1,5 +1,5 @@ -import { Time } from '../../lib/lib.js' -import { StudioId, TimelineDatastoreEntryId } from './Ids.js' +import type { Time } from '../../lib/lib.js' +import type { StudioId, TimelineDatastoreEntryId } from './Ids.js' export enum DatastorePersistenceMode { Temporary = 'temporary', diff --git a/packages/shared-lib/src/core/model/peripheralDevice.ts b/packages/shared-lib/src/core/model/peripheralDevice.ts index 2f896ede8ea..275a9b6b160 100644 --- a/packages/shared-lib/src/core/model/peripheralDevice.ts +++ b/packages/shared-lib/src/core/model/peripheralDevice.ts @@ -1,5 +1,5 @@ -import { TSR } from '../../tsr.js' -import { PeripheralDeviceId, StudioId } from './Ids.js' +import type { TSR } from '../../tsr.js' +import type { PeripheralDeviceId, StudioId } from './Ids.js' export interface IngestDeviceSecretSettingsStatus { /** OAuth: Set to true when secret value exists */ diff --git a/packages/shared-lib/src/expectedPlayoutItem.ts b/packages/shared-lib/src/expectedPlayoutItem.ts index a788ebde6dc..9a1516fe6df 100644 --- a/packages/shared-lib/src/expectedPlayoutItem.ts +++ b/packages/shared-lib/src/expectedPlayoutItem.ts @@ -1,6 +1,6 @@ -import { RundownId } from './core/model/Ids.js' -import { ProtectedString } from './lib/protectedString.js' -import { TSR } from './tsr.js' +import type { RundownId } from './core/model/Ids.js' +import type { ProtectedString } from './lib/protectedString.js' +import type { TSR } from './tsr.js' /** @deprecated */ export interface ExpectedPlayoutItemGeneric { @@ -20,5 +20,4 @@ export interface ExpectedPlayoutItemPeripheralDevice extends ExpectedPlayoutItem baseline?: 'rundown' | 'studio' } -type ExpectedPlayoutItemContent = TSR.ExpectedPlayoutItemContent -export { ExpectedPlayoutItemContent } +export type ExpectedPlayoutItemContent = TSR.ExpectedPlayoutItemContent diff --git a/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts b/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts index dadd7ef0252..30df3ba026d 100644 --- a/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts +++ b/packages/shared-lib/src/input-gateway/deviceTriggerPreviews.ts @@ -1,8 +1,8 @@ -import { ITranslatableMessage } from '../lib/translations.js' -import { PartId, ShowStyleBaseId, StudioId, TriggeredActionId } from '../core/model/Ids.js' -import { ProtectedString } from '../lib/protectedString.js' -import { ISourceLayer, IOutputLayer, SourceLayerType, SomeActionIdentifier } from '../core/model/ShowStyle.js' -import { PieceLifespan } from '../core/model/Rundown.js' +import type { ITranslatableMessage } from '../lib/translations.js' +import type { PartId, ShowStyleBaseId, StudioId, TriggeredActionId } from '../core/model/Ids.js' +import type { ProtectedString } from '../lib/protectedString.js' +import type { ISourceLayer, IOutputLayer, SourceLayerType, SomeActionIdentifier } from '../core/model/ShowStyle.js' +import type { PieceLifespan } from '../core/model/Rundown.js' export type DeviceTriggerMountedActionId = ProtectedString<'deviceTriggerMountedActionId'> diff --git a/packages/shared-lib/src/lib/JSONSchemaUtil.ts b/packages/shared-lib/src/lib/JSONSchemaUtil.ts index f1f1f85a6b8..7142670a4fd 100644 --- a/packages/shared-lib/src/lib/JSONSchemaUtil.ts +++ b/packages/shared-lib/src/lib/JSONSchemaUtil.ts @@ -1,4 +1,4 @@ -import { JSONSchema, TypeName } from './JSONSchemaTypes.js' +import { type JSONSchema, TypeName } from './JSONSchemaTypes.js' /** * The custom JSONSchema properties we can use for building the UI @@ -12,6 +12,10 @@ export enum SchemaFormUIField { * Title of the property */ Title = 'ui:title', + /** + * Icon to use for the property, for widgets that support them: `oneOfButtons` in `oneOf` array members + */ + Icon = 'ui:icon', /** * Description/hint for the property */ @@ -24,15 +28,27 @@ export enum SchemaFormUIField { * If an integer property, whether to treat it as zero-based */ ZeroBased = 'ui:zeroBased', + /** + * Whether the property is read-only. This will disable the input and hide any buttons for modifying the value. + */ + ReadOnly = 'ui:readOnly', /** * Override the presentation with a special mode. * Currently only valid for: - * - object properties. Valid values are 'json'. - * - string properties. Valid values are 'base64-image'. - * - boolean properties. Valid values are 'switch'. - * - array properties with items.type string. Valid values are 'bread-crumbs'. + * - object properties. Valid values are `json`, `oneOfButtons`. + * - `oneOfButtons` uses a `oneOf` list of possible variants of the object, with a `ui:oneOf:discriminant` field + * to determine which variant is selected. + * - string properties. Valid values are `base64-image`. + * - boolean properties. Valid values are `switch`. + * - number properties. Valid values are `timeMs`. + * - array properties with items.type string. Valid values are `bread-crumbs`. */ DisplayType = 'ui:displayType', + /** + * When using `oneOf` for an object, the discriminant field is used to determine which variant is selected. + * The value of this field must be a property that has a unique const value for each variant in the oneOf + */ + OneOfDiscriminant = 'ui:oneOf:discriminant', /** * Name of the enum values as generated for the typescript enum. * Future: a new field should probably be added for the UI to use. diff --git a/packages/shared-lib/src/lib/__tests__/protectedString.spec.ts b/packages/shared-lib/src/lib/__tests__/protectedString.spec.ts index f15823db860..d4e4f1caf80 100644 --- a/packages/shared-lib/src/lib/__tests__/protectedString.spec.ts +++ b/packages/shared-lib/src/lib/__tests__/protectedString.spec.ts @@ -1,5 +1,5 @@ import { - ProtectedString, + type ProtectedString, protectString, protectStringArray, unprotectObject, diff --git a/packages/shared-lib/src/lib/lib.ts b/packages/shared-lib/src/lib/lib.ts index 077cea537d0..12668221c2f 100644 --- a/packages/shared-lib/src/lib/lib.ts +++ b/packages/shared-lib/src/lib/lib.ts @@ -1,4 +1,4 @@ -import { ProtectedString } from './protectedString.js' +import type { ProtectedString } from './protectedString.js' export type Time = number export type TimeDuration = number diff --git a/packages/shared-lib/src/lib/protectedString.ts b/packages/shared-lib/src/lib/protectedString.ts index 3c9d6c9e1ee..3204de60270 100644 --- a/packages/shared-lib/src/lib/protectedString.ts +++ b/packages/shared-lib/src/lib/protectedString.ts @@ -1,4 +1,4 @@ -import { PartialDeep, ReadonlyDeep } from 'type-fest' +import type { PartialDeep, ReadonlyDeep } from 'type-fest' /** Runtime-wise, this is a string. * In compile-time, this is used to make sure that the "right" string is provided, typings-wise, diff --git a/packages/shared-lib/src/lib/translations.ts b/packages/shared-lib/src/lib/translations.ts index ad3924f591d..85a9009a4b6 100644 --- a/packages/shared-lib/src/lib/translations.ts +++ b/packages/shared-lib/src/lib/translations.ts @@ -1,4 +1,4 @@ -export { TranslationsBundle, TranslationsBundleType, I18NextData, ITranslatableMessage } +export { type TranslationsBundle, TranslationsBundleType, type I18NextData, type ITranslatableMessage } enum TranslationsBundleType { /** i18next JSON data */ diff --git a/packages/shared-lib/src/package-manager/__tests__/splitBoxMedia.spec.ts b/packages/shared-lib/src/package-manager/__tests__/splitBoxMedia.spec.ts new file mode 100644 index 00000000000..cc3471cef0c --- /dev/null +++ b/packages/shared-lib/src/package-manager/__tests__/splitBoxMedia.spec.ts @@ -0,0 +1,61 @@ +import { SourceLayerType } from '../../core/model/ShowStyle.js' +import { ExpectedPackage } from '../package.js' +import { + buildPublishedBoxPreviews, + findExpectedPackageForMediaId, + getMediaIdFromSplitBox, + normalizeSplitBoxMediaId, +} from '../splitBoxMedia.js' + +describe('splitBoxMedia', () => { + test('normalizeSplitBoxMediaId', () => { + expect(normalizeSplitBoxMediaId('clips/head3_Snow.mp4')).toEqual('CLIPS/HEAD3_SNOW.MP4') + }) + + test('getMediaIdFromSplitBox', () => { + expect( + getMediaIdFromSplitBox({ + type: SourceLayerType.VT, + fileName: 'clips/foo.mp4', + }) + ).toEqual('CLIPS/FOO.MP4') + expect( + getMediaIdFromSplitBox({ + type: SourceLayerType.CAMERA, + fileName: 'ignored', + }) + ).toBeUndefined() + }) + + test('findExpectedPackageForMediaId case insensitive', () => { + const pkg: ExpectedPackage.ExpectedPackageMediaFile = { + _id: 'p1', + type: ExpectedPackage.PackageType.MEDIA_FILE, + layers: [], + content: { filePath: 'CLIPS/FOO.MP4' }, + version: {}, + contentVersionHash: 'hash_p1', + sources: [], + sideEffect: {}, + } + expect(findExpectedPackageForMediaId([pkg], 'clips/foo.mp4')).toBe(pkg) + }) + + test('buildPublishedBoxPreviews index aligned', () => { + const previews = buildPublishedBoxPreviews( + [ + { type: SourceLayerType.CAMERA }, + { type: SourceLayerType.VT, fileName: 'a.mp4' }, + { type: SourceLayerType.VT, fileName: 'b.mp4' }, + ], + new Map([ + ['A.MP4', { thumbnailUrl: '/thumb-a' }], + ['B.MP4', { thumbnailUrl: '/thumb-b' }], + ]) + ) + expect(previews).toHaveLength(3) + expect(previews[0]).toEqual({}) + expect(previews[1]).toEqual({ thumbnailUrl: '/thumb-a' }) + expect(previews[2]).toEqual({ thumbnailUrl: '/thumb-b' }) + }) +}) diff --git a/packages/shared-lib/src/package-manager/helpers.ts b/packages/shared-lib/src/package-manager/helpers.ts index b99524183cb..4df960476c1 100644 --- a/packages/shared-lib/src/package-manager/helpers.ts +++ b/packages/shared-lib/src/package-manager/helpers.ts @@ -1,4 +1,4 @@ -import { ExpectedPackage } from './package.js' +import type { ExpectedPackage } from './package.js' // Note: These functions are copied from Package Manager diff --git a/packages/shared-lib/src/package-manager/package.ts b/packages/shared-lib/src/package-manager/package.ts index 0b70f6460b3..73f7500954b 100644 --- a/packages/shared-lib/src/package-manager/package.ts +++ b/packages/shared-lib/src/package-manager/package.ts @@ -5,8 +5,8 @@ * will fetch from a MAM and copy to the media-folder of CasparCG. */ -import { StatusCode } from '../lib/status.js' -import { MediaRamRecRef, MediaStillRef } from 'kairos-lib' +import type { StatusCode } from '../lib/status.js' +import type { MediaRamRecRef, MediaStillRef } from 'kairos-lib' type AccessorId = string type ExpectedPackageId = string diff --git a/packages/shared-lib/src/package-manager/publications.ts b/packages/shared-lib/src/package-manager/publications.ts index 99a097af6be..e391e5352d4 100644 --- a/packages/shared-lib/src/package-manager/publications.ts +++ b/packages/shared-lib/src/package-manager/publications.ts @@ -1,7 +1,7 @@ -import { ExpectedPackage, PackageContainer, PackageContainerOnPackage } from './package.js' -import { ExpectedPackageId, PeripheralDeviceId, RundownId, RundownPlaylistId } from '../core/model/Ids.js' -import { ProtectedString } from '../lib/protectedString.js' -import { ReadonlyDeep } from 'type-fest' +import type { ExpectedPackage, PackageContainer, PackageContainerOnPackage } from './package.js' +import type { ExpectedPackageId, PeripheralDeviceId, RundownId, RundownPlaylistId } from '../core/model/Ids.js' +import type { ProtectedString } from '../lib/protectedString.js' +import type { ReadonlyDeep } from 'type-fest' export interface PackageManagerPlayoutContext { _id: PeripheralDeviceId @@ -30,6 +30,9 @@ export type PackageManagerExpectedPackageId = ProtectedString<'PackageManagerExp export type PackageManagerExpectedPackageBase = ReadonlyDeep> & { _id: ExpectedPackageId + + /** The ID of the rundown this package is associated with, if any */ + rundownId: RundownId | undefined } export interface PackageManagerExpectedPackage { diff --git a/packages/shared-lib/src/package-manager/splitBoxMedia.ts b/packages/shared-lib/src/package-manager/splitBoxMedia.ts new file mode 100644 index 00000000000..6a26f9179c5 --- /dev/null +++ b/packages/shared-lib/src/package-manager/splitBoxMedia.ts @@ -0,0 +1,75 @@ +import { SourceLayerType } from '../core/model/ShowStyle.js' +import { ExpectedPackage } from './package.js' + +export interface SplitBoxPreviewUrlsLike { + thumbnailUrl?: string + previewUrl?: string +} + +/** Box entry with optional media file name (VT / graphics / live speak). */ +export interface SplitBoxWithOptionalFileName { + type: SourceLayerType + fileName?: string +} + +export interface SplitsContentLike { + boxSourceConfiguration: SplitBoxWithOptionalFileName[] +} + +/** Same normalization as MediaObject `mediaId` and VT `getMediaObjectMediaId`. */ +export function normalizeSplitBoxMediaId(filePath: string): string { + return filePath.toUpperCase() +} + +export function getExpectedPackageMediaId(expectedPackage: ExpectedPackage.Any): string | undefined { + if (expectedPackage.type === ExpectedPackage.PackageType.MEDIA_FILE) { + return expectedPackage.content.filePath + } + return undefined +} + +export function getMediaIdFromSplitBox(box: SplitBoxWithOptionalFileName): string | undefined { + const fileName = box.fileName + if (!fileName) return undefined + + switch (box.type) { + case SourceLayerType.VT: + case SourceLayerType.LIVE_SPEAK: + case SourceLayerType.GRAPHICS: + case SourceLayerType.TRANSITION: + return normalizeSplitBoxMediaId(fileName) + default: + return undefined + } +} + +export function getMediaIdsFromSplitsContent(content: SplitsContentLike): string[] { + const ids = new Set() + for (const box of content.boxSourceConfiguration) { + const mediaId = getMediaIdFromSplitBox(box) + if (mediaId) ids.add(mediaId) + } + return [...ids] +} + +export function findExpectedPackageForMediaId( + packages: ReadonlyArray, + mediaId: string +): ExpectedPackage.Any | undefined { + const normalized = normalizeSplitBoxMediaId(mediaId) + return packages.find((pkg) => { + const pkgMediaId = getExpectedPackageMediaId(pkg) + return pkgMediaId !== undefined && normalizeSplitBoxMediaId(pkgMediaId) === normalized + }) +} + +export function buildPublishedBoxPreviews( + boxes: ReadonlyArray, + previewByMediaId: ReadonlyMap +): SplitBoxPreviewUrlsLike[] { + return boxes.map((box) => { + const mediaId = getMediaIdFromSplitBox(box) + if (!mediaId) return {} + return previewByMediaId.get(mediaId) ?? {} + }) +} diff --git a/packages/shared-lib/src/peripheralDevice/externalEvents.ts b/packages/shared-lib/src/peripheralDevice/externalEvents.ts new file mode 100644 index 00000000000..331bbe4b571 --- /dev/null +++ b/packages/shared-lib/src/peripheralDevice/externalEvents.ts @@ -0,0 +1,44 @@ +/** + * A subscription to a single named event on a TSR playout device. + * + * Typed loosely (plain `string` fields) so that shared-lib does not depend on blueprints-integration or TSR. + */ +export interface PeripheralDeviceExternalTSREventSubscription { + type: 'tsr' + /** The id of the playout device, e.g. `'atem0'` */ + deviceId: string + /** + * The type of the playout device, e.g. `'ATEM'`. + * Typed as `string` rather than `TSR.DeviceType` to accommodate custom plugin device types. + */ + deviceType: string + /** The event key, e.g. `'me.0.programInput'` */ + event: string +} + +/** + * A subscription to an external device event, as declared by a blueprint. + * + * This is the shared-lib mirror of `BlueprintExternalEventSubscription`. + */ +export type PeripheralDeviceExternalEventSubscription = PeripheralDeviceExternalTSREventSubscription + +/** + * A TSR device state event as reported over the wire from a gateway. + * + * Extends the subscription type by adding the event payload. + * `deviceType` is a plain `string` rather than `TSR.DeviceType` because TSR plugins can define + * custom device types, and shared-lib deliberately avoids a hard dependency on TSR types. + */ +export interface PeripheralDeviceExternalTSREvent extends PeripheralDeviceExternalTSREventSubscription { + /** The event payload. Opaque on the wire; cast to the appropriate type in the job-worker. */ + payload: unknown +} + +/** + * An external event received from a gateway over the DDP wire. + * + * This is a discriminated union so that additional event sources can be added in future + * without breaking existing consumers. + */ +export type PeripheralDeviceExternalEvent = PeripheralDeviceExternalTSREvent diff --git a/packages/shared-lib/src/peripheralDevice/methodsAPI.ts b/packages/shared-lib/src/peripheralDevice/methodsAPI.ts index 0bf9314ce2c..5b9b09aae3d 100644 --- a/packages/shared-lib/src/peripheralDevice/methodsAPI.ts +++ b/packages/shared-lib/src/peripheralDevice/methodsAPI.ts @@ -1,14 +1,14 @@ -import { +import type { ExpectedPackageId, ExpectedPackageWorkStatusId, PeripheralDeviceCommandId, PeripheralDeviceId, TimelineHash, } from '../core/model/Ids.js' -import { PeripheralDeviceForDevice } from '../core/model/peripheralDevice.js' -import { IngestPlaylist, IngestRundown, IngestPart, IngestSegment } from './ingest.js' -import { MediaObjectRevision } from './mediaManager.js' -import { +import type { PeripheralDeviceForDevice } from '../core/model/peripheralDevice.js' +import type { IngestPlaylist, IngestRundown, IngestPart, IngestSegment } from './ingest.js' +import type { MediaObjectRevision } from './mediaManager.js' +import type { IMOSRunningOrder, IMOSRunningOrderBase, IMOSRunningOrderStatus, @@ -21,10 +21,10 @@ import { IMOSROAction, IMOSROReadyToAir, IMOSROFullStory, + IMOSString128, } from '@mos-connection/model' -import { IMOSString128 } from '@mos-connection/model' -import { ExpectedPackageStatusAPI } from '../package-manager/package.js' -import { +import type { ExpectedPackageStatusAPI } from '../package-manager/package.js' +import type { PeripheralDeviceInitOptions, PeripheralDeviceStatusObject, TimelineTriggerTimeResult, @@ -32,7 +32,8 @@ import { TimeDiff, PlayoutChangedResults, } from './peripheralDeviceAPI.js' -import { MediaObject } from '../core/model/MediaObjects.js' +import type { PeripheralDeviceExternalEvent } from './externalEvents.js' +import type { MediaObject } from '../core/model/MediaObjects.js' export type UpdateExpectedPackageWorkStatusesChanges = | { @@ -96,6 +97,11 @@ export interface NewPeripheralDeviceAPI { ping(deviceId: PeripheralDeviceId, deviceToken: string): Promise getPeripheralDevice(deviceId: PeripheralDeviceId, deviceToken: string): Promise playoutPlaybackChanged(deviceId: PeripheralDeviceId, deviceToken: string, r: PlayoutChangedResults): Promise + reportExternalEvents( + deviceId: PeripheralDeviceId, + deviceToken: string, + events: PeripheralDeviceExternalEvent[] + ): Promise pingWithCommand( deviceId: PeripheralDeviceId, deviceToken: string, @@ -367,6 +373,8 @@ export enum PeripheralDeviceAPIMethods { 'playoutPlaybackChanged' = 'peripheralDevice.playout.playbackChanged', + 'reportExternalEvents' = 'peripheralDevice.playout.reportExternalEvents', + 'getDebugStates' = 'peripheralDevice.playout.getDebugStates', // 'reportCommandError' = 'peripheralDevice.playout.reportCommandError', diff --git a/packages/shared-lib/src/peripheralDevice/peripheralDeviceAPI.ts b/packages/shared-lib/src/peripheralDevice/peripheralDeviceAPI.ts index 92f4bae3ad3..8d20bb31889 100644 --- a/packages/shared-lib/src/peripheralDevice/peripheralDeviceAPI.ts +++ b/packages/shared-lib/src/peripheralDevice/peripheralDeviceAPI.ts @@ -1,6 +1,10 @@ -import { DeviceConfigManifest } from '../core/deviceConfigManifest.js' -import { PeripheralDeviceId, RundownPlaylistId, PartInstanceId, PieceInstanceId } from '../core/model/Ids.js' -import { StatusCode } from '../lib/status.js' +import type { DeviceConfigManifest } from '../core/deviceConfigManifest.js' +import type { PeripheralDeviceId, RundownPlaylistId, PartInstanceId, PieceInstanceId } from '../core/model/Ids.js' +import type { StatusCode } from '../lib/status.js' +import type { DeviceStatusDetail } from 'timeline-state-resolver-types' + +// Re-export for use in UI components +export type { DeviceStatusDetail } from 'timeline-state-resolver-types' export interface PartPlaybackCallbackData { rundownPlaylistId: RundownPlaylistId @@ -76,6 +80,12 @@ export type PlayoutChangedResult = { export interface PeripheralDeviceStatusObject { statusCode: StatusCode messages?: Array + /** + * Structured status details for blueprint customization and UI display. + * Blueprints can provide custom translations for status codes when present. + * The messages array is derived from these details for backward compatibility. + */ + statusDetails: Array } // Note The actual type of a device is determined by the Category, Type and SubType export enum PeripheralDeviceCategory { diff --git a/packages/shared-lib/src/pubsub/peripheralDevice.ts b/packages/shared-lib/src/pubsub/peripheralDevice.ts index 6bb610846bb..0af14f24721 100644 --- a/packages/shared-lib/src/pubsub/peripheralDevice.ts +++ b/packages/shared-lib/src/pubsub/peripheralDevice.ts @@ -1,16 +1,21 @@ -import { PeripheralDeviceForDevice } from '../core/model/peripheralDevice.js' -import { RoutedMappings, RoutedTimeline } from '../core/model/Timeline.js' -import { DBTimelineDatastoreEntry } from '../core/model/TimelineDatastore.js' -import { +import type { PeripheralDeviceForDevice } from '../core/model/peripheralDevice.js' +import type { RoutedMappings, RoutedTimeline } from '../core/model/Timeline.js' +import type { DBTimelineDatastoreEntry } from '../core/model/TimelineDatastore.js' +import type { PackageManagerPlayoutContext, PackageManagerPackageContainers, PackageManagerExpectedPackage, } from '../package-manager/publications.js' -import { PeripheralDeviceId, RundownId, RundownPlaylistId } from '../core/model/Ids.js' -import { PeripheralDeviceCommand } from '../core/model/PeripheralDeviceCommand.js' -import { ExpectedPlayoutItemPeripheralDevice } from '../expectedPlayoutItem.js' -import { DeviceTriggerMountedAction, PreviewWrappedAdLib } from '../input-gateway/deviceTriggerPreviews.js' -import { IngestRundownStatus } from '../ingest/rundownStatus.js' +import type { PeripheralDeviceId, RundownId, RundownPlaylistId } from '../core/model/Ids.js' +import type { PeripheralDeviceCommand } from '../core/model/PeripheralDeviceCommand.js' +import type { ExpectedPlayoutItemPeripheralDevice } from '../expectedPlayoutItem.js' +import type { DeviceTriggerMountedAction, PreviewWrappedAdLib } from '../input-gateway/deviceTriggerPreviews.js' +import type { IngestRundownStatus } from '../ingest/rundownStatus.js' +import type { + PeripheralDeviceExternalEvent, + PeripheralDeviceExternalEventSubscription, +} from '../peripheralDevice/externalEvents.js' +import type { ProtectedString } from '../lib/protectedString.js' /** * Ids of possible DDP subscriptions for any PeripheralDevice. @@ -59,6 +64,11 @@ export enum PeripheralDevicePubSub { * Ingest status of rundowns for a PeripheralDevice */ ingestDeviceRundownStatus = 'ingestDeviceRundownStatus', + + // Playout gateway (external event subscriptions): + + /** External event subscriptions from blueprints for the Studio */ + externalEventSubscriptionsForDevice = 'externalEventSubscriptionsForDevice', } /** @@ -127,6 +137,17 @@ export interface PeripheralDevicePubSubTypes { deviceId: PeripheralDeviceId, token?: string ) => PeripheralDevicePubSubCollectionsNames.ingestRundownStatus + [PeripheralDevicePubSub.externalEventSubscriptionsForDevice]: ( + type: PeripheralDeviceExternalEvent['type'], + deviceId: PeripheralDeviceId, + token?: string + ) => PeripheralDevicePubSubCollectionsNames.externalEventSubscriptions +} + +/** An individual external device event subscription, as published by the server for the playout gateway */ +export type ExternalEventSubscriptionId = ProtectedString<'ExternalEventSubscriptionId'> +export type ExternalEventSubscriptionDocument = PeripheralDeviceExternalEventSubscription & { + _id: ExternalEventSubscriptionId } export enum PeripheralDevicePubSubCollectionsNames { @@ -149,6 +170,7 @@ export enum PeripheralDevicePubSubCollectionsNames { packageManagerExpectedPackages = 'packageManagerExpectedPackages', ingestRundownStatus = 'ingestRundownStatus', + externalEventSubscriptions = 'externalEventSubscriptions', } export type PeripheralDevicePubSubCollections = { @@ -171,4 +193,5 @@ export type PeripheralDevicePubSubCollections = { [PeripheralDevicePubSubCollectionsNames.packageManagerExpectedPackages]: PackageManagerExpectedPackage [PeripheralDevicePubSubCollectionsNames.ingestRundownStatus]: IngestRundownStatus + [PeripheralDevicePubSubCollectionsNames.externalEventSubscriptions]: ExternalEventSubscriptionDocument } diff --git a/packages/shared-lib/src/systemErrorMessages.ts b/packages/shared-lib/src/systemErrorMessages.ts new file mode 100644 index 00000000000..63d63aa7656 --- /dev/null +++ b/packages/shared-lib/src/systemErrorMessages.ts @@ -0,0 +1,40 @@ +/** + * System-level error codes for customizable error messages. + * + * These are for core Sofie system errors. + * Each error code documents the variables available in the translation context. + */ +export enum SystemErrorCode { + /** + * Database connection lost + * Variables: database + */ + DATABASE_CONNECTION_LOST = 'SYSTEM_DB_CONNECTION_LOST', + + /** + * System resources running low + * Variables: resource, available, required + */ + INSUFFICIENT_RESOURCES = 'SYSTEM_INSUFFICIENT_RESOURCES', + + /** + * Service unavailable + * Variables: service, reason + */ + SERVICE_UNAVAILABLE = 'SYSTEM_SERVICE_UNAVAILABLE', +} + +export interface SystemErrorContexts { + [SystemErrorCode.DATABASE_CONNECTION_LOST]: { + database: string + } + [SystemErrorCode.INSUFFICIENT_RESOURCES]: { + resource: string + available: unknown + required: unknown + } + [SystemErrorCode.SERVICE_UNAVAILABLE]: { + service: string + reason: string + } +} diff --git a/packages/shared-lib/src/tsr.ts b/packages/shared-lib/src/tsr.ts index 7941d189c35..480ed2a01b5 100644 --- a/packages/shared-lib/src/tsr.ts +++ b/packages/shared-lib/src/tsr.ts @@ -1,5 +1,5 @@ import * as TSR from 'timeline-state-resolver-types' export { TSR } -import * as tsrPkgInfo from 'timeline-state-resolver-types/package.json' +import tsrPkgInfo from 'timeline-state-resolver-types/package.json' with { type: 'json' } export const TSR_VERSION: string = tsrPkgInfo.version diff --git a/packages/shared-lib/tsconfig.build.json b/packages/shared-lib/tsconfig.build.json index 83c8cb4ebc3..ee87ba4814f 100644 --- a/packages/shared-lib/tsconfig.build.json +++ b/packages/shared-lib/tsconfig.build.json @@ -3,16 +3,16 @@ "include": ["src/**/*.ts"], "exclude": ["node_modules/**", "**/*spec.ts", "**/__tests__/*", "**/__mocks__/*"], "compilerOptions": { - "target": "es2019", + "target": "es2024", "outDir": "./dist", "rootDir": "./src", "baseUrl": "./", - "paths": { - "*": ["./node_modules/*"], - "@sofie-automation/shared-lib": ["./src/index.ts"] - }, "resolveJsonModule": true, "types": ["node"], - "composite": true + "composite": true, + "module": "node20", + "skipLibCheck": true, + "esModuleInterop": true, + "verbatimModuleSyntax": true } } diff --git a/packages/shared-lib/tsconfig.json b/packages/shared-lib/tsconfig.json index 39cf9672dcd..daf361df73b 100644 --- a/packages/shared-lib/tsconfig.json +++ b/packages/shared-lib/tsconfig.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.build.json", "exclude": ["node_modules/**"], "compilerOptions": { - "types": ["jest", "node"] + "types": ["jest", "node"], + "verbatimModuleSyntax": false } } diff --git a/packages/tsconfig.build.json b/packages/tsconfig.build.json index 2b155543fa0..751b59c3263 100644 --- a/packages/tsconfig.build.json +++ b/packages/tsconfig.build.json @@ -9,6 +9,7 @@ { "path": "./job-worker/tsconfig.build.json" }, { "path": "./corelib/tsconfig.build.json" }, { "path": "./shared-lib/tsconfig.build.json" }, + { "path": "./live-status-gateway-api/tsconfig.build.json" }, // { "path": "./openapi/tsconfig.build.json" }, { "path": "./live-status-gateway/tsconfig.build.json" }, { "path": "./meteor-lib/tsconfig.build.json" }, diff --git a/packages/tsconfig.test.json b/packages/tsconfig.test.json index 09ae33dd197..d4f465f6dc3 100644 --- a/packages/tsconfig.test.json +++ b/packages/tsconfig.test.json @@ -9,7 +9,8 @@ "composite": true, "noEmit": true, "skipLibCheck": true, - "importHelpers": false // To mitigate tslib errors which are meaningless + "importHelpers": false, // To mitigate tslib errors which are meaningless + "module": "node20" }, "references": [ { "path": "./blueprints-integration/tsconfig.build.json" }, diff --git a/packages/webui/jest.config.cjs b/packages/webui/jest.config.cjs index c9d553b42a6..ec5f87a2393 100644 --- a/packages/webui/jest.config.cjs +++ b/packages/webui/jest.config.cjs @@ -7,6 +7,8 @@ module.exports = { globals: {}, moduleFileExtensions: ['js', 'ts', 'tsx'], moduleNameMapper: { + '^@sofie-automation/shared-lib/dist/(.+)\\.js$': '/../shared-lib/src/$1', + '^@sofie-automation/shared-lib/dist/(.+)$': '/../shared-lib/src/$1', 'sha.js': 'sha.js', 'meteor/(.*)': '/src/meteor/$1', '(.+)\\.js$': '$1', @@ -19,6 +21,7 @@ module.exports = { diagnostics: { ignoreCodes: [ 151002, // hybrid module kind (Node16/18/Next) + 2823, // Import attributes not supported in CJS mode (ts-jest forces CJS, emits require() anyway) ], }, }, diff --git a/packages/webui/package.json b/packages/webui/package.json index 575bf57f441..6367fd2b6ae 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -30,10 +30,10 @@ "license-validate": "run -T sofie-licensecheck" }, "dependencies": { - "@fortawesome/fontawesome-free": "^7.1.0", - "@fortawesome/fontawesome-svg-core": "^7.1.0", - "@fortawesome/free-solid-svg-icons": "^7.1.0", - "@fortawesome/react-fontawesome": "^3.1.1", + "@fortawesome/fontawesome-free": "^7.2.0", + "@fortawesome/fontawesome-svg-core": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-fontawesome": "^3.3.1", "@jstarpl/react-contextmenu": "^2.15.3", "@popperjs/core": "^2.11.8", "@sofie-automation/blueprints-integration": "26.3.0-2", @@ -47,14 +47,13 @@ "cubic-spline": "^3.0.3", "deep-extend": "0.6.0", "ejson": "^2.2.3", - "i18next": "^21.10.0", - "i18next-browser-languagedetector": "^8.2.0", - "i18next-http-backend": "^3.0.2", + "i18next": "^26.0.8", + "i18next-browser-languagedetector": "^8.2.1", + "i18next-http-backend": "^3.0.6", "immutability-helper": "^3.1.1", "lottie-react": "^2.4.1", "moment": "^2.30.1", - "motion": "^12.31.0", - "promise.allsettled": "^1.0.7", + "motion": "^12.38.0", "query-string": "^6.14.1", "rc-tooltip": "^6.4.0", "react": "^18.3.1", @@ -65,43 +64,43 @@ "react-dom": "^18.3.1", "react-focus-bounder": "^1.1.6", "react-hotkeys": "^2.0.0", - "react-i18next": "^11.18.6", + "react-i18next": "^17.0.2", "react-intersection-observer": "^9.16.0", - "react-moment": "^1.2.1", + "react-moment": "^1.2.2", "react-popper": "^2.3.0", "react-router-bootstrap": "^0.25.0", "react-router-dom": "^5.3.4", - "semver": "^7.7.3", + "semver": "^7.7.4", "sha.js": "^2.4.12", "shuttle-webhid": "^0.1.3", "type-fest": "^4.41.0", - "underscore": "^1.13.7", + "underscore": "^1.13.8", "webmidi": "^2.5.3", "xmlbuilder": "^15.1.1" }, "devDependencies": { - "@babel/preset-env": "^7.29.0", + "@babel/preset-env": "^7.29.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/bootstrap": "^5.2.10", "@types/classnames": "^2.3.4", "@types/deep-extend": "^0.6.2", - "@types/react": "^18.3.27", + "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", "@types/react-router": "^5.1.20", "@types/react-router-bootstrap": "^0.26.8", "@types/react-router-dom": "^5.3.3", "@types/sha.js": "^2.4.4", "@types/xml2js": "^0.4.14", - "@vitejs/plugin-react": "^5.1.3", + "@vitejs/plugin-react": "^5.2.0", "@welldone-software/why-did-you-render": "^4.3.2", - "@xmldom/xmldom": "^0.8.11", - "babel-jest": "^30.2.0", - "globals": "^17.3.0", - "sass-embedded": "^1.97.3", - "typescript": "~5.7.3", - "vite": "^7.3.1", + "@xmldom/xmldom": "^0.9.10", + "babel-jest": "^30.3.0", + "globals": "^17.5.0", + "sass-embedded": "^1.99.0", + "typescript": "~5.9.3", + "vite": "^7.3.2", "vite-plugin-node-polyfills": "^0.25.0", "vite-tsconfig-paths": "^5.1.4", "xml2js": "^0.6.2" diff --git a/packages/webui/public/locales/nb/translations.json b/packages/webui/public/locales/nb/translations.json index 596b186988c..0bfad5b367e 100644 --- a/packages/webui/public/locales/nb/translations.json +++ b/packages/webui/public/locales/nb/translations.json @@ -1,974 +1,802 @@ { - "Account Page": "Brukerkontoside", - "Name:": "Navn:", - "Email:": "E-post:", - "Old Password": "Gammelt passord", - "New Password": "Nytt passord", - "Save Changes": "Lagre endringer", - "Edit Account": "Endre brukerkonto", - "Organization": "Organisasjon", - "User roles in organization": "Brukerroller i organisasjon", - "Studio": "Studio", - "Configurator": "Configurator", - "Developer": "Developer", - "Admin": "Admin", - "Remove Self": "Fjern denne brukeren", - "Email Address": "E-postadresse", - "Password": "Passord", - "Sign in": "Logg inn", - "Create New Account": "Opprett ny brukerkonto", - "Lost password?": "Glemt passord?", - "Send reset email": "Send e-post for å nullstille", - "Go back": "Tilbake", - "Password must be atleast 5 characters long": "Passord må være minst 5 tegn langt", - "Enter your new password": "Skriv inn ditt nye passord", - "Set new password": "Lagre nytt passord", - "Your Account": "Din brukerkonto", - "About Your Organization": "Om din oranisasjon", - "We are mainly": "Vi er hovedsaklig", - "Areas": "Områder", - "Invite User": "Inviter brukar", - "New User's Email": "Ny brukers e-post", - "New User's Name": "Ny brukers navn", - "Create New User & Send Enrollment Email": "Opprett ny bruker og send e-post for innmelding", - "Users in organization": "Brukere i organisasjonen", - "Return to list": "Gå tilbake til listen", - "There is no rundown active in this studio.": "Fant ingen aktive kjøreplaner for dette studioet.", - "This studio doesn't exist.": "Dette studioet finnes ikke.", - "There are no active rundowns.": "Fant ingen aktive kjøreplaner.", - "Evaluation": "Evaluering", - "Please take a minute to fill in this form.": "Vennligst fyll ut dette skjemaet.", - "Be aware that while filling out the form keyboard and streamdeck commands will not be executed!": "OBS! Du kan ikke utføre Sofie-kommandoer mens du skriver evaluering!", - "Did you have any problems with the broadcast?": "Hadde du noen problemer med sendingen?", - "Please explain the problems you experienced (what happened and when, what should have happened, what could have triggered the problems, etcetera...)": "Vennligst forklar problemene du opplevde (hva skjedde og når skjedde det, hva skulle skjedd, hva kan ha utløst problemene, o.s.v.)", - "Your name": "Ditt navn", - "Save message": "Lagre melding", - "Save message and Deactivate Rundown": "Send evalueringen og deaktiver kjøreplanen", - "No problems": "Ingen problemer", - "Something went wrong, but it didn't affect the output": "Noe gikk galt, men det påvirket ikke sendingen", - "Something went wrong, and it affected the output": "Noe gikk galt, og det påvirket sendingen", - "Are you sure?": "Er du sikker?", - "Trimming this clip has timed out. It's possible that the story is currently locked for writing in {{nrcsName}} and will eventually be updated. Make sure that the story is not being edited by other users.": "Endring av inn-/utpunkt for dette klippet tar lang tid. Det er mulig manuset i er låst i {{nrcsName}} og at inn-/utpunkt endres om litt. Forsikre deg om at manuset ikke blir redigert av andre brukere.", - "Trimming this clip has failed due to an error: {{error}}.": "Endring av inn-/utpunkt for dette klippet feilet: {{error}}.", - "Trimmed succesfully.": "Endring av inn-/utpunkt var vellykket.", - "Trimming this clip is taking longer than expected. It's possible that the story is locked for writing in {{nrcsName}}.": "Endring av inn-/utpunkt for dette klippet tek meir tid enn forventa. Det er mogleg manuset er låst for redigering i {{nrcsName}}.", - "Trim \"{{name}}\"": "Trim \"{{name}}\"", - "OK": "OK", - "Cancel": "Avbryt", - "Remove in-trimming": "Nullstill innpunkt", - "In": "Inn", - "Remove all trimming": "Nullstill inn- og utpunkt", - "Duration": "Varighet", - "Remove out-trimming": "Nullstill utpunkt", - "Out": "Ut", - "Next": "Neste", - "Test test": "Test test", - "Until next take": "Til neste Take", - "Until next segment": "Til neste segment", - "Until end of segment": "Til slutten av segment", - "Until next rundown": "Til neste kjøreplan", - "Until end of showstyle": "Til slutten av showstyle", - "Script is empty": "Manuset er tomt", - "Clip:": "Klipp:", - "Home": "Hjem", - "Rundowns": "Kjøreplaner", - "Test Tools": "Testverktøy", - "Status": "Status", - "Settings": "Innstillinger", - "Account": "Konto", - "Logout": "Logg ut", - "My name is {{name}}": "Mitt navn er {{name}}", - "Operating Mode": "Styringsmodus", - "Switching operating mode to {{mode}}": "Bytt til {{mode}}", - "Prompter": "Prompter", - "End of script": "Slutt på manus", - "Could not get system status. Please consult system administrator.": "Kan ikke innhente status for systemet. Kontakt systemadministrator.", - "There are no rundowns ingested into Sofie.": "Det er ikke sendt kjøreplaner til Sofie.", - "Click on a rundown to control your studio": "Klikk på en kjøreplan for å kontrollere studioet ditt", - "Rundown": "Kjøreplan", - "Problems": "Problemer", - "Show Style": "Showstyle", - "On Air Start Time": "Sendestart", - "Expected End Time": "Forventet sendeslutt", - "Last updated": "Sist oppdatert", - "View Layout": "Vis layout", - "Today": "I dag", - "Yesterday": "I går", - "Tomorrow": "I morgen", - "Last": "Forrige", - "Getting Started": "Kom i gang", - "Start with giving this browser configuration permissions by adding this to the URL: ": "Først må du gå i konfigurasjonsmodus ved å legge dette til url-en: ", - "Start Here!": "Start her!", - "Then, run the migrations script:": "Kjør så migreringsprosedyren:", - "Run Migrations to get set up": "Kjør migreringsprosedyrer for å sette opp", - "Migrations": "Migrering", - "Documentation is available at": "Dokumentasjon er tilgjengelig på", - "Use {{nrcsName}} order": "Bruk rekkefølge fra {{nrcsName}}", - "Reset Sort Order": "Tilbakestill rekkefølge", - "Enable configuration mode by adding ?configure=1 to the address bar.": "Aktiver konfigurasjonsmodus ved å legge til ?configure=1 på slutten av nettadressen.", - "You need to run migrations to set the system up for operation.": "Du må kjøre migrering for å klargjøre systemet for bruk.", - "Drop Rundown here to move it out of its current Playlist": "Slipp kjøreplanen her for å flytte den ut av spillelisten", - "Sofie Automation": "Sofie", - "version": "versjon", - "System Status": "Systemstatus", - "System has issues which need to be resolved": "Systemet har problemer som må fikses", - "Status Messages:": "Statusmeldinger:", - "{{showStyleVariant}} – {{showStyleBase}}": "{{showStyleVariant}} – {{showStyleBase}}", - "Drag to reorder or move out of playlist": "Dra for å endre rekkefølge eller flytte ut av spillelisten", - "This rundown is currently active": "Denne kjøreplanen er allerede aktiv", - "Not set": "Ikke angitt", - "This rundown will loop indefinitely": "Denne kjøreplanen vil gå i en uendelig loop", + "({{time}} ago)": "(for {{time}} siden)", "({{timecode}})": "({{timecode}})", - "Re-sync rundown data with {{nrcsName}}": "Ikke synkronisert med MOS/{{nrcsName}}", - "Delete": "Slett", - "Standalone Shelf": "Frittstående skuff", - "Rundown & Shelf": "Kjøreplan & skuff", - "Default": "Standard", - "Delete rundown?": "Slette kjøreplanen?", - "Are you sure you want to delete the \"{{name}}\" rundown?": "Er du sikker på at du vil slette kjøreplanen \"{{name}}\"?", - "Please note: This action is irreversible!": "Merk: Denne handlingen kan ikke angres!", - "Re-Sync rundown?": "Synkroniser kjøreplanen med ENPS?", - "Re-Sync": "Synkroniser", - "Are you sure you want to re-sync the \"{{name}}\" rundown?": "Er du sikker på at du vil synkronisere kjøreplanen \"{{rundownSlug}}\" med ENPS?", - "Start time is close": "Oppgitt sendestart er hvert øyeblikk", - "Yes": "Ja", - "No": "Nei", - "You are in rehearsal mode, the broadcast starts in less than 1 minute. Do you want to reset the rundown and go into On-Air mode?": "Du er i testmodus og sendingen starter om mindre enn ett minutt. Vil du laste inn kjøreplanen på nytt og gjøre klar til sending?", - "Hold": "Hold", - "Could not find a Piece that can be disabled.": "Kunne ikke finne et element som kan skippes.", - "Failed to execute take": "Kunne ikke gjennomføre Take", - "The rundown you are trying to execute a take on is inactive, would you like to activate this rundown?": "Du prøve å gjøre en Take i en inaktiv kjøreplan. Vil du aktivere denne kjøreplanen?", - "Activate (Rehearsal)": "Aktiver (testmodus)", + "(in: {{time}})": "(om: {{time}})", + "(Optional) A name/identifier of the local network where the share is located": "(Valgfri) Et navn/en identifikator for det lokale nettverket hvor den delte mappen er lokalisert", + "(Optional) A name/identifier of the local network where the share is located, leave empty if globally accessible": "(Valgfri) Et navn/en identifikator for det lokale nettverket hvor den delte mappen er lokalisert, la være tom dersom den er globalt tilgjengelig", + "(Optional) This could be the name of the computer on which the local folder is on": "(Valgfri) Dette kan være navnet til datamaskinen som den lokale mappen er på", + "(Unknown playlist)": "(Ukjent kjøreplanliste)", + "(Unknown rundown)": "(Ukjent kjøreplan)", + "{{currentRundownName}} - {{rundownPlaylistName}}": "{{currentRundownName}} - {{rundownPlaylistName}}", + "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)": "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)", + "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago": "for {{days}} dager, {{hours}} t {{minutes}} min {{seconds}} s siden", + "{{hours}} h {{minutes}} min {{seconds}} s ago": "for {{hours}} t {{minutes}} min {{seconds}} s siden", + "{{indexCount}} indexes was removed.": "{{indexCount}} indexer ble fjernet.", + "{{minutes}} min {{seconds}} s ago": "for {{minutes}} min {{seconds}} s siden", + "{{nrcsName}} Connection": "{{nrcsName}}-tilkobling", + "{{rundownPlaylistName}} (Looping)": "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)", + "{{seconds}} s ago": "for {{seconds}} s siden", + "{{showStyleVariant}} – {{showStyleBase}}": "{{showStyleVariant}} – {{showStyleBase}}", + "{{sourceLayer}} doesn't have both audio & video": "{{sourceLayer}} har ikke lyd og/eller bilde", + "{{sourceLayer}} has {{audioStreams}} audio streams": "{{sourceLayer}} har {{audioStreams}} lydstrømmer", + "{{sourceLayer}} has the wrong format: {{format}}": "{{sourceLayer}}-formatet er ikke støttet: {{format}}", + "{{sourceLayer}} is being ingested": "{{sourceLayer}} blir prosessert", + "{{sourceLayer}} is missing a file path": "{{sourceLayer}} kan ikke spilles av fordi filnavnet mangler", + "{{sourceLayer}} is not yet ready on the playout system": "{{sourceLayer}} er ennå ikke klar til å spilles ut fra avviklingsserver", + "{{sourceLayer}} is transferring to the playout system": "{{sourceLayer}} overføres til avviklingsserver", + "A Full System Snapshot contains all system settings (studios, showstyles, blueprints, devices, etc.)": "Et systemsnapshot inneholder alle systeminnstillinger (studio, showstyles, blueprints, enheter o.s.v.)", + "A snapshot of the current Running Order has been created for troubleshooting.": "Et snapshot av den gjeldende kjøreplanen har blitt opprettet for feilsøking.", + "A Studio Snapshot contains all system settings related to that studio": "Et studiosnapshot inneholder alle systeminnstillinger tilknyttet et studio", + "Abort": "Avbryt", + "Accessor ID": "Aksessor-id", + "Accessor Type": "Aksessortype", + "Accessors": "Aksessorer", + "Action": "Handling", + "Action Buttons": "Handlingsknapper", + "Action Triggers": "Handlingsutløsere", "Activate (On-Air)": "Aktiver (gå ON AIR)", - "Failed to activate": "Kunne ikke aktivere", - "Something went wrong, please contact the system administrator if the problem persists.": "Noe gikk galt, kontakt systemadministrator hvis problemet fortsetter.", + "Activate (Rehearsal)": "Aktiver (testmodus)", + "Activate Rundown": "Aktiver kjøreplan", + "Active": "Aktiv", + "Ad-Lib": "Adlib", + "Ad-Lib Action": "Adlib-handling", + "Add": "Legg til", + "Add {{filtersTitle}}": "Legg til {{filtersTitle}}", + "Add a playout device to the studio in order to configure the route sets": "For å kunne redigere omkoblingsgrupper, må du legge til en playout-enhet til studio", + "Add a playout device to the studio in order to edit the layer mappings": "For å kunne redigere lagmappinger, må du legge til en playout-enhet til studio", + "Add button": "Legg til knapp", + "Add filter": "Legg til filter", + "Add some source layers (e.g. Graphics) for your data to appear in rundowns": "Legg til kildelag (for eksempel Grafikk) for å vise dine data i kjøreplaner", + "AdLib": "Adlib", + "Adlib Rank": "Adlib-rang", + "AdLibs on this layer can be queued": "Adliber på dette laget kan cues", + "All connections working correctly": "Alle tilkoblinger er OK", + "All devices working correctly": "Alle enheter fungerer som de skal", + "All is well, go get a": "Alt er greit, gå og hent deg en", + "All steps": "Alle trinn", + "Allow disabling of Pieces": "Tillat deaktivering av elementer", + "Allow Read access": "Tillat lesing", + "Allow Rundowns to be reset while on-air": "Tillat tilbakestilling av kjøreplaner som er on-air", + "Allow Write access": "Tillat skriving/lagring", "Another Rundown is Already Active!": "En annen kjøreplan er allerede aktiv!", - "The rundown \"{{rundownName}}\" will need to be deactivated in order to activate this one.\n\nAre you sure you want to activate this one anyway?": "Kjøreplanen \"{{rundownName}}\" må deaktiveres for å aktivere denne kjøreplanen.\n\nEr du sikker på at du ønsker å aktivere?", - "Activate Anyway (Rehearsal)": "Aktiver uansett (testmodus)", - "Activate Anyway (On-Air)": "Aktiver uansett (gå ON AIR)", - "Do you want to activate this Rundown?": "Vil du aktivere denne kjøreplanen?", - "The planned end time has passed, are you sure you want to activate this Rundown?": "Det planlagte sluttidspunktet er passert, er du sikker på at du vil aktivere denne kjøreplan?", + "Answers": "Svar", + "APM Enabled": "AMP aktivert", + "APM Transaction Sample Rate": "Prøvefrekvens for AMP-transaksjoner", + "Append": "Legg til", + "Append or Replace": "Legg til eller erstatt", + "Application credentials": "Brukernavn/passord (Application Credentials)", + "Application Performance Monitoring": "Overvåkning av applikasjonsytelse (AMP)", "Are you sure you want to activate Rehearsal Mode?": "Er du sikker på at du vil gå i testmodus?", - "Are you sure you want to deactivate this Rundown?\n(This will clear the outputs)": "Er du sikker på at du vil deaktivere denne kjøreplanen?\n(Dette vil nullstille alle utganger.)", - "The rundown can not be reset while it is active": "En aktivert kjøreplan kan ikke tilbakestilles", - "A snapshot of the current Running Order has been created for troubleshooting.": "Et snapshot av den gjeldende kjøreplanen har blitt opprettet for feilsøking.", - "Prepare Studio and Activate (Rehearsal)": "Forbered studio og aktiver testmodus", - "Deactivate": "Deaktiver", - "Take": "Take", - "Reset Rundown": "Tilbakestill kjøreplanen", - "Reload {{nrcsName}} Data": "Last inn {{nrcsName}}-data på nytt", - "Store Snapshot": "Lagre snapshot", - "No actions available": "Ingen kjøreplanvalg tilgjengelige i påsynsmodus", - "Add ?studio=1 to the URL to enter studio mode": "Legg til ?admin=1 på slutten av nettadressen for å starte studiomodus", - "Exit": "Lukk", - "Error": "Feil", - "This rundown is now active. Are you sure you want to exit this screen?": "Denne kjøreplanen er aktiv. Er du sikker på at du vil avslutte?", - "Invalid AdLib": "Ugyldig adlib", - "Cannot play this AdLib because it is marked as Invalid": "Kan ikke spille av adlib fordi den er markert som ugyldig", + "Are you sure you want to deactivate this Rundown\n(This will clear the outputs)": [ + "Er du sikker på at du vil deaktivere denne kjøreplanen?", + "(Dette vil nullstille alle utganger.)" + ], + "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?": "Er du sikker på at du vil slette kildelaget \"{{sourceLayerId}}\"?", + "Are you sure you want to delete the \"{{name}}\" rundown?": "Er du sikker på at du vil slette kjøreplanen \"{{name}}\"?", + "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?": "Er du sikker på at du vil slette blueprintet \"{{blueprintId}}\"?", + "Are you sure you want to delete the shelf layout \"{{name}}\"?": "Er du sikker på at du vil slette layouten \"{{name}}\"?", + "Are you sure you want to delete the show style \"{{showStyleId}}\"?": "Er du sikker på at du vil slette showstylen \"{{showStyleId}}\"?", + "Are you sure you want to delete the studio \"{{studioId}}\"?": "Er du sikker på at du vil slette studioet \"{{studioId}}\"?", + "Are you sure you want to delete this AdLib?": "Er du sikker på at du vil slette denne adliben?", + "Are you sure you want to delete this Bucket?": "Er du sikker på at du vil slette denne bøtten?", + "Are you sure you want to delete this device: \"{{deviceId}}\"?": "Er du sikker på at du vil fjerne enheten \"{{deviceId}}\"?", + "Are you sure you want to empty (remove all adlibs inside) this Bucket?": "Er du sikker på at du vil tømme denne bøtten (fjerner alle adliber)?", + "Are you sure you want to force the migration? This will bypass the migration checks, so be sure to verify that the values in the settings are correct!": "Er du sikker på at du vil tvinge migreringen? Dette gjør at du hopper over migreringskontrollene, så vær sikker på at verdiene oppgitt i innstillinger er korrekte!", + "Are you sure you want to re-sync the \"{{name}}\" rundown?": "Er du sikker på at du vil synkronisere kjøreplanen \"{{rundownSlug}}\" med ENPS?", + "Are you sure you want to re-sync the Rundown?\n(If the currently playing Part has been changed, this can affect the output)": [ + "Er du sikker på at du vil synkronisere denne kjøreplanen?", + "(Dette kan påvirke gjennomføring av en pågående sending)" + ], + "Are you sure you want to remove {{type}} \"{{deviceId}}\"?": "Er du sikker på at du vil fjerne enheten {{type}} \"{{deviceId}}\"?", + "Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\nRoute Sets assigned to this group will be reset to no group.": [ + "Er du sikker på at du vil fjerne eksklusivitetsgruppen \"{{eGroupName}}\"?", + "Omkoblinger satt til denne gruppen vil bli resatt til ingen gruppe." + ], + "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?": "Er du sikker på at du fil fjerne mappingen for laget \"{{mappingId}}\"?", + "Are you sure you want to remove the device \"{{deviceName}}\" and all of it's sub-devices?": "Er du sikker på at du vil fjerne enheten \"{{deviceName}}\" og alle dens underenheter?", + "Are you sure you want to remove the Package Container \"{{containerId}}\"?": "Er du sikker på at du vil fjerne pakkekontaineren \"{{containerId}}\"?", + "Are you sure you want to remove the Package Container Accessor \"{{accessorId}}\"?": "Er du sikker på at du vil fjerne pakkekontainer-aksessoren \"{{accessorId}}\"?", + "Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to \"{{newLayerId}}\"?": "Er du sikker på at du vil fjerne omkoblingen fra \"{{sourceLayerId}}\" til \"{{newLayerId}}\"?", + "Are you sure you want to remove the Route Set \"{{routeId}}\"?": "Er du sikker på at du vil fjerne omkoblingsgruppen \"{{routeId}}\"?", + "Are you sure you want to replace the blueprints with the file \"{{fileName}}\"?": "Er du sikker på at du vil erstatte blueprints fra filen \"{{fileName}}\"?", + "Are you sure you want to reset the database version?\nOnly do this if you plan on running the migration right after.": [ + "Er du sikker på at du vil nullstille databaseversjonen?", + "Bare gjør dette dersom du har tenkt å kjøre en migrering umiddelbart." + ], + "Are you sure you want to restart this device?": "Er du sikker på at du vil starte denne enheten på nytt?", + "Are you sure you want to restart this Sofie Automation Server Core: {{name}}?": "Er du sikker på at du vil starte Sofie Core: {{name}} på nytt?", + "Are you sure you want to restore the system from the snapshot file \"{{fileName}}\"?": "Er du sikker på at du vil gjenopprettet systemet fra snapshotfilen \"{{fileName}}\"?", + "Are you sure you want to update the blueprints from the file \"{{fileName}}\"?": "Er du sikker på at du vil oppdatere blueprints fra filen \"{{fileName}}\"?", + "Are you sure you want to upload the shelf layout from the file \"{{fileName}}\"?": "Er du sikker på at du vil laste opp layout for skuff fra filen \"{{fileName}}\"?", + "Are you sure?": "Er du sikker?", + "Around 10 minutes ago": "Cirka 10 minutter siden", + "Assign": "Tilordne", + "Attached Subdevices": "Tilkoblede underenheter", + "Audio Mixing": "Lydmiksing", + "Auto": "Auto", + "Bad": "Feil", + "Base URL": "Base-url", + "Base url to the resource (example: http://myserver/folder)": "Base-url for ressursen (eksempel: http://minserver/mappe)", + "Baseline needs reload, this studio may not work until reloaded": "Baseline må lastes på nytt, dette studioet vil kanskje ikke fungere før baseline er lastet på nytt", + "Behavior": "Oppførsel", + "Blueprint": "Blueprint", + "Blueprint Configuration": "Blueprintkonfigurasjon", + "Blueprint ID": "Blueprint-id", + "Blueprint Name": "Blueprintnavn", + "Blueprint not set": "Blueprint ikke valgt", + "Blueprint Type": "Blueprinttype", + "Blueprint Version": "Blueprintversjon", + "Blueprints": "Blueprints", + "Blueprints updated successfully.": "Blueprints ble oppdatert.", + "BREAK": "PAUSE", + "Break In": "Pause om", + "Button": "Knapp", + "Button height scale factor": "Høydeskala for knapp", + "Button width scale factor": "Breddeskala for knapp", + "Camera": "Kamera", + "Cancel": "Avbryt", + "Cancel currently pressed hotkey": "Avbryt den trykte tasten", "Cannot play this AdLib because it is marked as Floated": "Kan ikke spille av adlib fordi den er markert som på vent (float)", - "Not queueable": "Kan ikke settes i kø", + "Cannot play this AdLib because it is marked as Invalid": "Kan ikke spille av adlib fordi den er markert som ugyldig", "Cannot play this adlib because source layer is not queueable": "Kan ikke spille av adlib fordi den ikke kan settes i kø på kildelaget", - "There are no Playout Gateways connected and attached to this studio. Please contact the system administrator to start the Playout Gateway.": "Dette studioet har ingen tilkoblede playout-gatewayer. Kontakt systemadministrator for å starte den.", - "Playout Gateway \"{{playoutDeviceName}}\" is now restarting.": "Playout-gateway \"{{playoutDeviceName}}\" starter på nytt...", - "Could not restart Playout Gateway \"{{playoutDeviceName}}\".": "Playout-gateway \"{{playoutDeviceName}}\" kunne ikke startes på nytt.", - "Restart Playout": "Start Playout-gateway på nytt", - "Restart CasparCG Server": "Restart CasparCG", - "Do you want to restart CasparCG Server \"{{device}}\"?": "Er du sikker på at du vil restarte CasparCG Server \"{{device}}\"?", "CasparCG on device \"{{deviceName}}\" restarting...": "CasparCG på \"{{deviceName}}\" starter på nytt...", - "Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}": "Omstart av CasparCG på \"{{deviceName}}\" feilet: {{errorMessage}}", - "Cancel currently pressed hotkey": "Avbryt den trykte tasten", "Change to fullscreen mode": "Fullskjermmodus", - "Show Hotkeys": "Vise hurtigtaster", - "Take a Snapshot": "Lagre et snapshot", - "Restart {{device}}": "Start {{device}} på nytt", - "Rundown not found": "Kjøreplan ikke funnet", - "Close": "Lukk", - "Rundown for piece \"{{pieceLabel}}\" could not be found.": "Finner ikke kjøreplan for \"{{pieceLabel}}\".", - "This rundown has been unpublished from Sofie.": "Denne kjøreplanen er ikke lenger tilgjengelig i Sofie.", - "Error: The studio of this Rundown was not found.": "Feil: Kan ikke finne studioet for denne kjøreplanen.", - "This playlist is empty": "Denne spillelisten er tom", - "Error: The ShowStyle of this Rundown was not found.": "Feil: Kan ikke finne showstyle for denne kjøreplanen.", - "Unknown error": "Ukjent feil", - "Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced or remove the rundown from Sofie. What do you want to do?": "Kjøreplan {{rundownName}} i listen {{playlistName}} mangler i data fra {{nrcsName}}. Du kan enten markere den som usynkronisert og beholde den i Sofie, eller fjerne kjøreplanen fra Sofie. Hva vil du gjøre?", - "(Unknown rundown)": "(Ukjent kjøreplan)", - "(Unknown playlist)": "(Ukjent kjøreplanliste)", - "Leave Unsynced": "Behold ikke-synkronisert kjøreplan", - "Remove": "Fjern", - "Remove rundown": "Fjern kjøreplan", - "Do you really want to remove just the rundown \"{{rundownName}}\" in the playlist {{playlistName}} from Sofie? This cannot be undone!": "Er du sikker på at du vil slette kjøreplanen {{rundownName}} i lista {{playlistName}} fra Sofie? Dette kan ikke angres!", - "Loop Start": "Start for loop", - "Loop End": "Slutt for loop", - "(in: {{time}})": "(om: {{time}})", - "({{time}} ago)": "(for {{time}} siden)", - "Planned Start": "Planlagt start", - "Planned Duration": "Planlagt varighet", - "Planned End": "Planlagt slutt", - "The rundown \"{{rundownName}}\" is not published or activated in {{nrcsName}}! No data updates will currently come through.": "Kjøreplanen \"{{rundownName}}\" er ikke synkronisert med MOS/{{nrcsName}}! Kontroller at den er satt MOS Active i ENPS.", - "Re-sync": "Synkroniser med MOS", - "Re-sync Rundown": "Synkroniser kjøreplanen med ENPS på nytt", - "Are you sure you want to re-sync the Rundown?\n(If the currently playing Part has been changed, this can affect the output)": "Er du sikker på at du vil synkronisere denne kjøreplanen?\n(Dette kan påvirke gjennomføring av en pågående sending)", - "Restart": "Restart", - "Fixing this problem requires a restart to the host device. Are you sure you want to restart {{device}}?\n(This might affect output)": "Feilretting krever en omstart av {{device}}. Er du sikker på at du ønsker å starte enheten på nytt?(Dette kan påvirke gjennomføring av en pågående sending)", + "Channel Name": "Kanalnavn", + "Check the console for troubleshooting data from device \"{{deviceName}}\"!": "Sjekk konsollen for feilsøkingsdata fra enheten \"{{deviceName}}\"!", + "Cleanup": "Opprydding", + "Cleanup old data": "Rydd opp i gamle data", + "Cleanup old database indexes": "Rydd opp i gamle databaseindexer", + "Clear {{layerName}}": "Tøm {{layerName}}", + "Clear queued segment": "Fjern cuet tittel", + "Clear Source Layer": "Tøm kildelag", + "Click on a rundown to control your studio": "Klikk på en kjøreplan for å kontrollere studioet ditt", + "Click to show available Package Containers": "Klikk for å vise tilgjengelige pakkekontainere", + "Click to show available Show Styles": "Klikk for å vise tilgjengelige showstyles", + "Client IP": "Klient-ip", + "Clips": "Klipp", + "Close": "Lukk", + "Connect some devices to the playout gateway": "Koble til en eller flere enheter til playout gatewayen", + "Connected": "Tilkoblet", + "Connected App Containers": "Tilkoblede app-kontainere", + "Connected Workers": "Tilkoblede arbeidere", + "Controls for exposed Route Sets will be displayed to the producer within the Rundown View in the Switchboard.": "Kontroller for eksponerte omkoblingsgrupper vil vises til producer i kjøreplansvisningen i omkoblingspanelet.", + "Core System settings": "Systeminstillinger for Core", + "Could not get system status. Please consult system administrator.": "Kan ikke innhente status for systemet. Kontakt systemadministrator.", + "Could not restart Playout Gateway \"{{playoutDeviceName}}\".": "Playout-gateway \"{{playoutDeviceName}}\" kunne ikke startes på nytt.", + "Create new Bucket": "Opprett ny bøtte", + "Created": "Opprettet", + "Cron jobs": "Cron-jobber", + "Current Part": "Nåværende del", + "Current Segment": "Nåværende tittel", + "Custom Classes": "Tilpassede klasser", + "Custom Hotkey Labels": "Egendefinerte etiketter for hurtigtaster", + "Deactivate": "Deaktiver", + "Deactivate Rundown": "Deaktiver kjøreplan", + "Default": "Standard", + "Default Layout": "Standardlayout", + "Default shelf height": "Standard høyde for skuff", + "Default State": "Standardtilstand", + "Delete": "Slett", + "Delete layout?": "Slett layout?", + "Delete rundown?": "Slette kjøreplanen?", + "Delete this AdLib": "Slett denne adliben", + "Delete this Blueprint?": "Slett dette blueprintet?", + "Delete this Bucket": "Slett denne bøtten", + "Delete this item?": "Slett dette elementet?", + "Delete this output?": "Slett denne utgangen?", + "Delete this Show Style?": "Slett denne showstylen?", + "Delete this Studio?": "Slett dette studioet?", "Device \"{{deviceName}}\" restarting...": "\"{{deviceName}}\" starter på nytt...", - "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}": "Kunne ikke starte \"{{deviceName}}\" på nytt: {{errorMessage}}", - "There is an unknown problem with the part.": "Det er et ukjent problem med denne delen.", - "Show issue": "Vis problem", - "There is an unspecified problem with the source.": "Det er et ikke-spesifisert problem med kilden.", - "External message queue has unsent messages.": "Ekstern meldingskø har meldinger som ikke er sendt.", - "The system configuration has been changed since importing this rundown. It might not run correctly": "Systemoppsettet har blitt endret etter at denne kjøreplanen ble importert. Kjøreplanen kan spilles av med feil", - "Unable to check the system configuration for changes": "Kan ikke kontrollere endringer i systemoppsettet", - "The Studio configuration is missing some required fields:": "Studiooppsettet mangler obligatoriske felter:", - "The Show Style configuration \"{{name}}\" could not be validated": "Showstyleoppsettet \"{{name}}\" kunne ikke valideres", - "The ShowStyle \"{{name}}\" configuration is missing some required fields:": "Showstyleoppsettet \"{{name}}\" mangler obligatoriske felter:", - "Unable to validate the system configuration": "Systemoppsettet kunne ikke valideres", "Device {{deviceName}} is disconnected": "{{deviceName}} er koblet fra", - "Warnings": "Advarsler", + "Device ID": "Enhets-id", + "Device Name": "Enhetsnavn", + "Device Type": "Enhetstype", + "Devices": "Enheter", + "Devices with issues": "Enheter med problemer", + "Did you have any problems with the broadcast?": "Hadde du noen problemer med sendingen?", + "Diff": "Forskjell", + "Disable Context Menu": "Skru av kontekstmeny", + "Disable hints by adding this to the URL:": "Deaktiver hint ved å legge dette til på url-en:", + "Disable next Piece": "Skip neste element", + "Disable the next element": "Skip neste super", + "Disable version check": "Deaktiver versjonsjekk", + "Disabled": "Deaktivert", + "Disconnected": "Frakoblet", + "Display name of the Package Container": "Pakkekontainerens navn som vises i oversikten", + "Display Rank": "Rangering for visning", + "Display Style": "Visningsstil", + "Display Take buttons": "Vis Take-knapp", + "Do you want to activate this Rundown?": "Vil du aktivere denne kjøreplanen?", + "Do you want to append these to existing Action Triggers, or do you want to replace them?": "Vil du legge disse til de nåværende handlingsutløserne, eller vil du erstatte dem?", + "Do you want to restart CasparCG Server \"{{device}}\"?": "Er du sikker på at du vil restarte CasparCG Server \"{{device}}\"?", + "Documentation is available at": "Dokumentasjon er tilgjengelig på", + "Documents to be removed:": "Dokumenter som fjernes:", + "Don't treat the end of the last rundown in a playlist as a break": "Ikke behandle slutten av den siste kjøreplanen i en spilleliste som en pause", + "Done": "Utført", + "Download Action Triggers": "Last ned handlingsutløsere", + "Drag to reorder or move out of playlist": "Dra for å endre rekkefølge eller flytte ut av spillelisten", + "Drop Rundown here to move it out of its current Playlist": "Slipp kjøreplanen her for å flytte den ut av spillelisten", + "Duration": "Varighet", + "Edit in Nora": "Rediger i Nora", + "Edit Support Panel": "Rediger supportpanel", + "Empty": "Tom", + "Empty this Bucket": "Tøm denne bøtten", + "Enable": "Aktiver", + "Enable \"Play from Anywhere\"": "Slå på \"Play from Anywhere\"", + "Enable CasparCG restart job": "Aktiver CasparCG restartjobber", + "Enable configuration mode by adding ?configure=1 to the address bar.": "Aktiver konfigurasjonsmodus ved å legge til ?configure=1 på slutten av nettadressen.", + "Enable hints by adding this to the URL:": "Aktiver hint ved å legge dette til på url-en:", + "Enable search toolbar": "Aktiver søkeverktøy", + "Enabled": "Aktivert", + "End of script": "Slutt på manus", + "End Words": "Stikkord", + "Error": "Feil", + "Error: The ShowStyle of this Rundown was not found.": "Feil: Kan ikke finne showstyle for denne kjøreplanen.", + "Error: The studio of this Rundown was not found.": "Feil: Kan ikke finne studioet for denne kjøreplanen.", + "Evaluations": "Evalueringer", + "Exclusivity group": "Ekslusivitetsgruppe", + "Exclusivity Group ID": "Eksklusivitetsgruppe-id", + "Exclusivity Group Name": "Eksklusivitetsgruppenavn", + "Exclusivity Groups": "Eksklusivitetsgrupper", + "Execute": "Utfør", + "Executes within the currently open Rundown, requires a Client-side trigger.": "Utføers innenfor den valgte kjøreplanen, men trenger en utløser fra klienten.", + "Execution times": "Kjøretider", + "Exit": "Lukk", + "Expected End": "Forventet slutt", + "Expected End text": "Tekst for forventet slutt", + "Expected End Time": "Forventet sendeslutt", + "Expected Start": "Forventet slutt", + "Export": "Eksporter", + "Expose as user selectable layout": "Gjør tilgjengelig som brukervalgt layout", + "Expose layout as a standalone page": "Gjør layout tilgjengelig som en selvstendig side", + "External message queue has unsent messages.": "Ekstern meldingskø har meldinger som ikke er sendt.", + "Failed to activate": "Kunne ikke aktivere", + "Failed to execute take": "Kunne ikke gjennomføre Take", + "Failed to reset OAuth credentials: {{errorMessage}}": "Nullstiling av OAuth credentials feilet: {{errorMessage}}", + "Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}": "Omstart av CasparCG på \"{{deviceName}}\" feilet: {{errorMessage}}", + "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}": "Kunne ikke starte \"{{deviceName}}\" på nytt: {{errorMessage}}", + "Failed to update blueprints: {{errorMessage}}": "Oppdatering av blueprints feilet: {{errorMessage}}", + "Failed to update config: {{errorMessage}}": "Oppdatering av konfigurasjon feilet: {{errorMessage}}", + "Failed to upload OAuth credentials: {{errorMessage}}": "Opplasting av OAuth credentials feilet: {{errorMessage}}", + "Failed to upload shelf layout: {{errorMessage}}": "Opplasting av layout feilet: {{errorMessage}}", + "Fatal": "Kritisk", + "File path to the folder of the local folder": "Sti til lokale mappe", + "Filter disabled": "Filter deaktivert", + "Filter Disabled": "Filter deaktivert", + "Filters": "Filtre", + "Find Trigger...": "Finn utløser...", + "Fixed duration in Segment header": "Låst varighet i tittelheader", + "Fixing this problem requires a restart to the host device. Are you sure you want to restart {{device}}?\n(This might affect output)": "Feilretting krever en omstart av {{device}}. Er du sikker på at du ønsker å starte enheten på nytt?(Dette kan påvirke gjennomføring av en pågående sending)", + "Floated AdLib": "Adlib satt på vent", + "Folder path": "Mappesti", + "Folder path to shared folder": "Sti til delt mappe", + "Force": "Tving", + "Force (deactivate others)": "Tving (deaktiver andre)", + "Force Migration": "Tving migrering", + "Force Migration (unsafe)": "Tving migrering (utrygt)", + "Force the Multi-gateway-mode": "Tving multigateway-modus", + "Frame Rate": "Framerate", + "Full System Snapshot": "Fullt systemsnapshot", + "Generic Properties": "Generelle egenskaper", + "Generic Script": "Generisk manus", + "Getting Started": "Kom i gang", + "Global AdLib": "Globale adliber", + "Global AdLibs": "Globale adliber", + "Go to On Air line": "Gå til OnAir-posisjon", + "Good": "Bra", + "Graphics": "Grafikk", + "GUI": "Brukergrensesnitt", + "Height": "Høyde", + "Help & Support": "Hjelp og brukerstøtte", + "Hide Countdown": "Skjul nedtelling", + "Hide Diff": "Skjul forskjell", + "Hide Diff Label": "Skjul etikett for forskjell", + "Hide duplicated AdLibs": "Skjul dupliserte adliber", + "Hide End Time": "Skjul sendeslutt", + "Hide Expected End timing when a break is next": "Gjem nedtelling til forventet slutt når neste punkt er en pause", + "Hide for dynamically inserted parts": "Skjul for dynamisk innsatte deler", + "Hide Label": "Skjul etikett", + "Hide Panel from view": "Ikke vis dette panelet", + "Hide Planned End Label": "Skjul etikett for planlagt slutt", + "Hide Planned Start": "Skjul planlagt start", + "Hide Rundown Divider": "Skjul kjøreplanskille", + "Hide rundown divider between rundowns in a playlist": "Skjul skille mellom kjøreplaner i en spilleliste", + "Hold": "Hold", + "Hotkey": "Hurtigtast", + "How many of the transactions to monitor. Set to -1 to log nothing (max performance), 0.5 to log 50% of the transactions, 1 to log all transactions": "Antall transaksjoner som overvåkes. Sett verdien til -1 for å ikke logge noe (maks ytelse), til 0.5 for å logge halvparten av transaksjonene eller til 1 for å logge alle transaksjonene", + "HTML that will be shown in the Support Panel": "HTML-kode som vil bli vist i supportpanelet", + "Human-readable name of the layer": "Leservennlig lagnavn", + "Icon": "Ikon", + "Icon color": "Ikonfarge", + "Id": "Id", + "ID of the device (corresponds to the device ID in the peripheralDevice settings)": "Enhets-id (korresponderer med enhets-id under enhetsinnstillinger)", + "ID of the timeline-layer to map to some output": "Lag-id for tidslinjelaget som skal mappes til en utgang", + "If set, only one Route Set will be active per exclusivity group": "Bare en omkoblingsgruppe være aktiv per eksklusivitetsgruppe når dette er krysset av for", + "Import": "Importer", + "In": "Inn", + "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s": "om {{days}} dager, {{hours}} t {{minutes}} min {{seconds}} s", + "in {{hours}} h {{minutes}} min {{seconds}} s": "om {{hours}} t {{minutes}} min {{seconds}} s", + "in {{minutes}} min {{seconds}} s": "om {{minutes}} min {{seconds}} s", + "in {{seconds}} s": "om {{seconds}} s", + "Include Clear Source Layer in Ad-Libs": "Ta med \"Tøm kildelag\" i adliber", + "Include Global AdLibs": "Inkluder globale adliber", + "Installation name": "Installasjonsnavn", + "Internal ID": "Intern-id", + "Invalid AdLib": "Ugyldig adlib", + "Is a Guest Input": "Er en gjesteinngang", + "Is a Live Remote Input": "Er en RM", + "Is collapsed by default": "Er minimert som standard", + "Is flattened": "Er slått sammen", + "Is hidden": "Er skjult", + "Is PGM Output": "Er programutgang", + "ISA URLs": "ISA-adresse (url)", "Just now": "Nå", + "Key": "Key", + "Kill (debug)": "Kill (debug)", + "Label": "Etikett", + "Label contains": "Etikett inneholder", + "Last": "Forrige", + "Last {{layerName}}": "Siste {{layerName}}", + "Last modified": "Sist endret", + "Last rundown is not break": "Siste kjøreplan er ingen pause", + "Last seen": "Sist sett", + "Last update": "Nyeste oppdatering", + "Last updated": "Sist oppdatert", + "Layer ID": "Lag-id", + "Layer Mappings": "Lagmappinger", + "Layer Name": "Lagnavn", + "Leave Unsynced": "Behold ikke-synkronisert kjøreplan", "Less than a minute ago": "Under ett minutt siden", "Less than five minutes ago": "Under fem minutter siden", - "Around 10 minutes ago": "Cirka 10 minutter siden", + "Limit": "Grense", + "Live Speak": "STK", + "Local": "Lokal", + "Local Time": "Lokal tid", + "Logging level": "Loggenivå", + "Lookahead Mode": "Lookahead-modus", + "Loop End": "Slutt for loop", + "Loop Start": "Start for loop", + "Lower Third": "Super", + "Manage Snapshots": "Behandle snapshots", + "Mappings": "Lagmappinger", + "Media": "Media", + "Media Preview URL": "Forhåndsvisnings-URL", + "Message": "Melding", + "Message Queue": "Meldingskø", + "Messages": "Meldinger", + "Method": "Metode", + "Migrate database": "Migrer database", + "Migrations": "Migrering", + "Mini Shelf Layout": "Layouter for miniskuff", + "Minor Warning": "Mindre advarsel (avvik)", + "More documentation available at:": "Mer dokumentasjon er tilgjengelig på:", "More than 10 minutes ago": "Over 10 minutter siden", - "More than 30 minutes ago": "Over 30 minutter siden", "More than 2 hours ago": "Over 2 timer siden", + "More than 30 minutes ago": "Over 30 minutter siden", "More than 5 hours ago": "Over 5 timer siden", "More than a day ago": "Over en dag siden", - "{{nrcsName}} Connection": "{{nrcsName}}-tilkobling", - "Last update": "Nyeste oppdatering", - "Off-line devices": "Frakoblede enheter", - "Devices with issues": "Enheter med problemer", - "All connections working correctly": "Alle tilkoblinger er OK", - "Play-out": "Avspilling", - "All devices working correctly": "Alle enheter fungerer som de skal", - "Auto": "Auto", - "Expected End": "Forventet slutt", + "Move Next": "Skip neste", + "Move Next backwards": "Unskip neste", + "Move Next forwards": "Skip neste", + "Move Next to the following segment": "Skip til neste segment", + "Move Next to the previous segment": "Unskip neste segment", + "Move Parts": "Skip del", + "Move Segments": "Skip segmenter", + "Multi-gateway-mode delay time": "Delaytid for multigateway-modus", + "Multilingual description, editing will overwrite": "Endring vil overskrive flerspråklig beskrivelse", + "My name is {{name}}": "Mitt navn er {{name}}", + "Name": "Navn", + "Network Id": "Nettverk-id", + "New Bucket": "Ny bøtte", + "New Filter": "Nytt filter", + "New Layer": "Nytt lag", + "New Layout": "Ny layout", + "New Output": "Ny utgang", + "New Source": "Ny kilde", + "Next": "Neste", + "Next Break text": "Tekst for neste pause", "Next Loop at": "Neste loop starter", - "Diff": "Forskjell", - "Started": "Startet", - "Expected Start": "Forventet slutt", - "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)": "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)", - "{{currentRundownName}} - {{rundownPlaylistName}}": "{{currentRundownName}} - {{rundownPlaylistName}}", - "{{rundownPlaylistName}} (Looping)": "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)", - "Floated AdLib": "Adlib satt på vent", - "Switchboard": "Sentralbord", - "This is not in it's normal setting": "Endret fra standardoppsett", + "Next Part": "Neste del", + "Next scheduled show": "Neste planlagte sending", + "Next Segment": "Neste tittel", + "No": "Nei", + "No Action Triggers set up.": "Ingen handlingsutløsere er satt opp.", + "No actions available": "Ingen kjøreplanvalg tilgjengelige i påsynsmodus", + "No Ad-Lib matches in the current state of Rundown: \"{{rundownPlaylistName}}\"": "Ingen treff på adliber i nåværende tilstand for kjøreplanen: \"{{rundownPlaylistName}}\"", + "No matching Action Trigger.": "Fikk ikke treff blant handlingsutløsere.", + "No matching Rundowns available to be used for preview": "Ingen passende kjøreplaner tilgjengelige for forhåndsvisning", + "No name set": "Navn ikke definert", + "No output channels set": "Ingen utgangskanal definert", + "No PGM output": "Ingen programutgang", + "No problems": "Ingen problemer", + "No source layers set": "Ingen kildelag definert", + "No status loaded": "Ingen status lastet", + "None": "Ingen", + "Not Active": "Inaktiv", + "Not Connected": "Ikke tilkoblet", + "Not defined": "Ikke definert", + "Not Global": "Ikke globale", + "Not queueable": "Kan ikke settes i kø", + "Not set": "Ikke angitt", + "Note: Core needs to be restarted to apply these settings": "Merknad: Core må startes på nytt for å ta i bruk disse innstillingene", "Off": "Av", + "Off-line devices": "Frakoblede enheter", + "OK": "OK", + "On": "På", + "On Air": "On Air", "On Air At": "On Air klokken", "On Air In": "On Air om", - "Unsynced": "Ikke synkronisert med MOS", - "On Air": "On Air", - "Loops to top": "Looper til toppen", - "Show End": "Sendeslutt", - "BREAK": "PAUSE", - "Break In": "Pause om", - "part": "punkt", - "Set segment as Next": "Sett tittel som Neste: Starter på neste Take", - "Queue segment": "Cue tittel: Starter når aktiv tittel er ferdig", - "Clear queued segment": "Fjern cuet tittel", - "Set this part as Next": "Sett dette punktet som neste: Starter på neste Take", - "Set Next Here": "Sett Neste her", - "Play from Here": "Spill av herfra", - "Switch to Storyboard mode": "Bytt til storyboard-visning", - "Zoom Out": "Zoom Ut", - "Show All": "Vis alle", - "Zoom In": "Zoom inn", - "Parts Duration": "Varighet for del", - "Unknown": "Ukjent", - "Good": "Bra", - "Minor Warning": "Mindre advarsel (avvik)", - "Warning": "Advarsel", - "Bad": "Feil", - "Fatal": "Kritisk", - "Connected": "Tilkoblet", - "Disconnected": "Frakoblet", - "MOS Gateway": "MOS-gateway", - "Spreadsheet Gateway": "Spreadsheet-gateway", - "Play-out Gateway": "Playout-gateway", - "Media Manager": "Media Manager", - "Unknown Device": "Ukjent enhet", - "Delete this Studio?": "Slett dette studioet?", - "Are you sure you want to delete the studio \"{{studioId}}\"?": "Er du sikker på at du vil slette studioet \"{{studioId}}\"?", - "Delete this Show Style?": "Slett denne showstylen?", - "Are you sure you want to delete the show style \"{{showStyleId}}\"?": "Er du sikker på at du vil slette showstylen \"{{showStyleId}}\"?", - "Delete this Blueprint?": "Slett dette blueprintet?", - "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?": "Er du sikker på at du vil slette blueprintet \"{{blueprintId}}\"?", - "Remove this Device?": "Fjern denne enheten?", - "Are you sure you want to remove the device \"{{deviceName}}\" and all of it's sub-devices?": "Er du sikker på at du vil fjerne enheten \"{{deviceName}}\" og alle dens underenheter?", - "Studios": "Studio", - "Unnamed Studio": "Studio uten navn", - "Show Styles": "Showstyle", - "Unnamed Show Style": "Showstyle uten navn", - "Source Layers": "Kildelag", - "Output Channels": "Utgangskanal", - "Blueprints": "Blueprints", - "Unnamed blueprint": "Blueprint uten navn", - "Type": "Type", - "Version": "Versjon", - "Devices": "Enheter", - "Tools": "Verktøy", - "Core System settings": "Systeminstillinger for Core", - "Upgrade Database": "Oppgrader databasen", - "Manage Snapshots": "Behandle snapshots", - "System Settings": "Systeminstillinger", - "Update Blueprints?": "Oppdater blueprints?", - "Update": "Oppdater", - "Are you sure you want to update the blueprints from the file \"{{fileName}}\"?": "Er du sikker på at du vil oppdatere blueprints fra filen \"{{fileName}}\"?", - "Blueprints updated successfully.": "Blueprints ble oppdatert.", - "Replace Blueprints?": "Erstatte blueprints?", - "Replace": "Erstatt", - "Are you sure you want to replace the blueprints with the file \"{{fileName}}\"?": "Er du sikker på at du vil erstatte blueprints fra filen \"{{fileName}}\"?", - "Failed to update blueprints: {{errorMessage}}": "Oppdatering av blueprints feilet: {{errorMessage}}", - "Assigned Show Styles:": "Tilordnede showstyles:", - "This Blueprint is not being used by any Show Style": "Dette blueprintet er ikke i bruk av noen showstyles", - "Assigned Studios:": "Tilordnede studio:", - "This Blueprint is not compatible with any Studio": "Dette blueprintet er ikke kompatibel med noe studio", - "Unassign": "Fjern tilordning", - "Assign": "Tilordne", - "Blueprint ID": "Blueprint-id", - "Blueprint Name": "Blueprintnavn", - "No name set": "Navn ikke definert", - "Blueprint Type": "Blueprinttype", - "Upload a new blueprint": "Last opp et nytt blueprint", - "Last modified": "Sist endret", - "Blueprint Id": "Blueprint-id", - "Blueprint Version": "Blueprintversjon", - "Disable version check": "Deaktiver versjonsjekk", - "Upload Blueprints": "Last opp blueprints", - "OAuth credentials succesfully uploaded.": "Opplasting av OAuth credentials var vellykket.", - "Failed to upload OAuth credentials: {{errorMessage}}": "Opplasting av OAuth credentials feilet: {{errorMessage}}", - "OAuth credentials successfuly reset": "OAuth credentials nullstilt", - "Failed to reset OAuth credentials: {{errorMessage}}": "Nullstiling av OAuth credentials feilet: {{errorMessage}}", - "Reset Authentication": "Nullstill autentisering", - "Application credentials": "Brukernavn/passord (Application Credentials)", - "Access token": "Tilgangskode (Access Token)", - "Click on the link below and accept the permissions request.": "Klikk på linken under og godta permissions-forespørselen", - "Waiting for gateway to generate URL...": "Venter på at gateway genererer URL...", - "Only Match Global AdLibs": "Vis kun globale adliber", - "Name": "Navn", - "Display Style": "Visningsstil", - "Show thumbnails next to list items": "Vis miniatyrbilder ved siden av listeelementer", - "Button width scale factor": "Breddeskala for knapp", - "Button height scale factor": "Høydeskala for knapp", + "On Air Start Time": "Sendestart", + "On release": "På slipp (\"Key up\")", + "OnAir": "OnAir", + "One of these source layers must have a piece for the countdown to segment on-air to be show": "Et av disse kildelagene må ha et element for at nedtelling til tittelen er OnAir vises", "Only Display AdLibs from Current Segment": "Vis kun adliber fra gjeldende tittel", - "Include Global AdLibs": "Inkluder globale adliber", - "Filter Disabled": "Filter deaktivert", - "Include Clear Source Layer in Ad-Libs": "Ta med \"Tøm kildelag\" i adliber", - "Source Layer Types": "Kildelagstyper", - "Filter disabled": "Filter deaktivert", - "Label contains": "Etikett inneholder", - "Tags must contain": "Tagger må inneholde", - "Hide Panel from view": "Ikke vis dette panelet", - "Show panel as a timeline": "Vis panel som en tidslinje", - "Enable search toolbar": "Aktiver søkeverktøy", + "Only Global": "Bare globale", + "Only Match Global AdLibs": "Vis kun globale adliber", + "Only Pieces present in rundown are sticky": "Kun elementer tilstede i kjøreplanen er sticky", + "Open": "Åpne", + "Open shelf by default": "Åpne skuff som standard", + "Operating Mode": "Styringsmodus", + "Optional description of the action": "Valgfri beskrivelse av handlingen", + "Original Layer": "Opprinnelig lag", + "Out": "Ut", + "Output channels": "Utgangskanal", + "Output Channels": "Utgangskanal", + "Output channels are required for your studio to work": "Utgangskanaler er nødvendige for at studioet ditt skal fungere", + "Output Layer": "Utgangslag", + "Over/Under": "Over/Under", "Overflow horizontally": "Horisontal overflyt", - "Display Take buttons": "Vis Take-knapp", - "Queue all adlibs": "Cue alle adliber", - "Toggle AdLibs on single mouse click": "Veksle mellom adliber med enkelt museklikk", - "Hide duplicated AdLibs": "Skjul dupliserte adliber", + "Package Container ID": "Pakkekontainer-id", + "Package Containers": "Pakkekontainere", + "Package Containers to use for previews": "Pakkekontainere som skal benyttes til forhåndsvisninger", + "Package Containers to use for thumbnails": "Pakkekontainere som skal benyttes til miniatyrbilder", + "Package Manager": "Pakkebehandler", + "Package Manager status": "Status fo pakkebehandler", + "Package Status": "Pakkestatus", + "Packages": "Pakker", + "Parameters": "Parametre", + "part": "punkt", + "Part": "Del", + "Part Count Down": "Nedtelling for del", + "Part Count Up": "Opptelling for del", + "Parts Duration": "Varighet for del", + "Parts: {{delta}}": "Deler: {{delta}}", + "Password": "Passord", + "Password for authentication": "Passord for autentisering", + "Peripheral Device is outdated": "Tilkoblet enhet er utdatert", + "Pick": "Plukk", + "Pick last": "Plukk siste", "Picks the first instance of an adLib per rundown, identified by uniqueness Id": "Velger den første forekomsten av en adlib i hver kjøreplan, identifisert av unik id", - "URL": "Adresse (url)", - "Display Rank": "Rangering for visning", - "Role": "Rolle", - "Adlib Rank": "Adlib-rang", + "Pieces on this layer are sticky": "Elementer i dette laget er sticky", + "Pieces on this layer can be cleared": "Elementer på dette laget kan tømmes", "Place label below panel": "Plasser etikett under panel", - "Disabled": "Deaktivert", - "Show segment name": "Vis tittelens navn", - "Show part title": "Vis delens tittel", - "Hide for dynamically inserted parts": "Skjul for dynamisk innsatte deler", - "Planned Start Text": "Tekst for planlagt start", - "Text to show above show start time": "Tekst som vises over klokkeslett for sendestart", - "Hide Diff": "Skjul forskjell", - "Hide Planned Start": "Skjul planlagt start", + "Planned Duration": "Planlagt varighet", + "Planned End": "Planlagt slutt", "Planned End text": "Tekst for planlagt slutt", - "Text to show above show end time": "Tekst som vises over klokkeslett for sendeslutt", - "Hide Planned End Label": "Skjul etikett for planlagt slutt", - "Hide Diff Label": "Skjul etikett for forskjell", - "Hide Countdown": "Skjul nedtelling", - "Hide End Time": "Skjul sendeslutt", - "Hide Label": "Skjul etikett", - "Text": "Tekst", - "Show Rundown Name": "Vis kjøreplannavn", - "Segment": "Tittel", - "Part": "Del", - "X": "X", - "Y": "Y", - "Width": "Bredde", - "Height": "Høyde", - "Scale": "Skala", - "Custom Classes": "Tilpassede klasser", - "Device ID": "Enhets-id", - "Device Type": "Enhetstype", - "Remove this item?": "Fjern dette elementet?", - "Are you sure you want to remove {{type}} \"{{deviceId}}\"?": "Er du sikker på at du vil fjerne enheten {{type}} \"{{deviceId}}\"?", - "Attached Subdevices": "Tilkoblede underenheter", - "Expected End text": "Tekst for forventet slutt", - "Text to show above countdown to end of show": "Tekst som vises over nedtelling til forventet slutt", - "Hide Expected End timing when a break is next": "Gjem nedtelling til forventet slutt når neste punkt er en pause", - "While there are still breaks coming up in the show, hide the Expected End timers": "Gjem nedtelling til forventet slutt mens det fremdeles er pauser igjen i sendingen", - "Show next break timing": "Vis tid for neste pause", - "Whether to show countdown to next break": "Om nedtelling til neste pause skal vises", - "Last rundown is not break": "Siste kjøreplan er ingen pause", - "Don't treat the end of the last rundown in a playlist as a break": "Ikke behandle slutten av den siste kjøreplanen i en spilleliste som en pause", - "Next Break text": "Tekst for neste pause", - "Text to show above countdown to next break": "Tekst som vises over nedtelling til neste pause", - "Expose as user selectable layout": "Gjør tilgjengelig som brukervalgt layout", - "Shelf Layout": "Layouter for skuffen", - "Mini Shelf Layout": "Layouter for miniskuff", - "Rundown Header Layout": "Layout for kjøreplanens topptekst", - "Hide Rundown Divider": "Skjul kjøreplanskille", - "Hide rundown divider between rundowns in a playlist": "Skjul skille mellom kjøreplaner i en spilleliste", - "Show Breaks as Segments": "Vis pauser som titler", - "Segment countdown requires source layer": "Nedtelling for tittel krever kildelag", - "One of these source layers must have a piece for the countdown to segment on-air to be show": "Et av disse kildelagene må ha et element for at nedtelling til tittelen er OnAir vises", - "Fixed duration in Segment header": "Låst varighet i tittelheader", - "The segment duration in the segment header always displays the planned duration instead of acting as a counter": "Tittelens varighet i tittelheaderen vil alltid vise den planlagte varigheten i stedet for å telle ned", - "Select visible Source Layers": "Velg synlige kildelag", - "Select visible Output Groups": "Velg synlig utgangsgruppe", - "Expose layout as a standalone page": "Gjør layout tilgjengelig som en selvstendig side", - "Open shelf by default": "Åpne skuff som standard", - "Default shelf height": "Standard høyde for skuff", - "Show Buckets": "Vis bøtter", - "Disable Context Menu": "Skru av kontekstmeny", - "This action has an invalid combination of filters": "Denne handlingen har en ugyldig kombinasjon av filtre", - "Force": "Tving", - "Rehearsal": "Testmodus", - "Undo": "Angre", - "Segments: {{delta}}": "Segmenter: {{delta}}", - "Parts: {{delta}}": "Deler: {{delta}}", - "Open": "Åpne", - "Toggle": "Veklse", - "On": "På", - "Activate Rundown": "Aktiver kjøreplan", - "Ad-Lib": "Adlib", - "Deactivate Rundown": "Deaktiver kjøreplan", - "Disable next Piece": "Skip neste element", - "Move Next": "Skip neste", + "Planned Start": "Planlagt start", + "Planned Start Text": "Tekst for planlagt start", + "Play-out": "Avspilling", + "Playout devices which uses this package container": "Playout-enheter som benytter denne pakkekontaineren", + "Playout Gateway \"{{playoutDeviceName}}\" is now restarting.": "Playout-gateway \"{{playoutDeviceName}}\" starter på nytt...", + "Please check the database related to the warnings above. If neccessary, you can": "Vennligst sjekk databasen tilknyttet advarslene over. Hvis nødvendig kan du", + "Please note: This action is irreversible!": "Merk: Denne handlingen kan ikke angres!", + "Prepare Studio and Activate (Rehearsal)": "Forbered studio og aktiver testmodus", + "Previous work status reasons": "Tidligere årsaker for jobbsatus", + "Priority": "Prioritet", + "Problems": "Problemer", + "Profile name to be used by FileFlow when exporting the clips": "Profilnavn som benyttes av FileFlow når klippene eksporteres", + "Prompter": "Prompter", + "Quantel FileFlow Profile name": "Quantel FileFlow profilnavn", + "Quantel FileFlow URL": "Quantel FileFlow-adresse (url)", + "Quantel gateway URL": "Quantel Gateway-adresse (url)", + "Quantel transformer URL": "Quantel Transformer-adresse (url)", + "Queue all adlibs": "Cue alle adliber", + "Queue segment": "Cue tittel: Starter når aktiv tittel er ferdig", + "Queue this AdLib": "Cue denne adliben", + "Queued Messages": "Meldinger i kø", + "Re-check": "Sjekk på nytt", + "Re-sync": "Synkroniser med MOS", + "Re-Sync": "Synkroniser", + "Re-sync Rundown": "Synkroniser kjøreplanen med ENPS på nytt", + "Re-sync rundown data with {{nrcsName}}": "Ikke synkronisert med MOS/{{nrcsName}}", + "Re-Sync rundown?": "Synkroniser kjøreplanen med ENPS?", + "Ready": "Klar", + "Rehearsal": "Testmodus", + "Reload {{nrcsName}} Data": "Last inn {{nrcsName}}-data på nytt", + "Reload Baseline": "Last inn baseline på nytt", "Reload NRCS Data": "Last inn MOS-data på nytt", + "Reload statuses": "Last inn status på nytt", + "Remote Source": "RM", + "Remove": "Fjern", + "Remove all trimming": "Nullstill inn- og utpunkt", + "Remove in-trimming": "Nullstill innpunkt", + "Remove indexes": "Fjern indexer", + "Remove old data": "Fjern gamle data", + "Remove old data from database": "Fjern gamle data fra databasen", + "Remove out-trimming": "Nullstill utpunkt", + "Remove rundown": "Fjern kjøreplan", + "Remove this device?": "Fjern denne enheten?", + "Remove this Device?": "Fjern denne enheten?", + "Remove this Exclusivity Group?": "Fjern fra denne eksklusivitetsgruppen?", + "Remove this item?": "Fjern dette elementet?", + "Remove this mapping?": "Fjern denne mappingen?", + "Remove this Package Container Accessor?": "Fjern denne pakkekontainer-aksessoren?", + "Remove this Package Container?": "Fjern denne pakkekontaineren?", + "Remove this Route from this Route Set?": "Fjern denne omkoblingen fra omkoblingsgruppen?", + "Remove this Route Set?": "Fjern denne omkoblingsgruppen?", + "Rename this AdLib": "Gi denne adliben nytt navn", + "Rename this Bucket": "Gi bøtten nytt navn", + "Replace": "Erstatt", + "Replace Blueprints?": "Erstatte blueprints?", + "Reset All Versions": "Nullstill alle versjoner", + "Reset Database Version": "Nullstill databaseversjon", + "Reset Rundown": "Tilbakestill kjøreplanen", + "Reset Sort Order": "Tilbakestill rekkefølge", + "Resource Id": "Ressurs-id", + "Restart": "Restart", + "Restart {{device}}": "Start {{device}} på nytt", + "Restart CasparCG Server": "Restart CasparCG", + "Restart Device": "Start enheten på nytt", + "Restart Playout": "Start Playout-gateway på nytt", + "Restart this Device?": "Start denne enheten på nytt?", + "Restart this system?": "Starte dette Sofie-systemet på nytt?", + "Restore": "Gjenopprett", + "Restore from Snapshot File": "Gjenopprett fra snapshotfil", + "Restore from Stored Snapshots": "Gjenopprett fra lagrede snapshots", + "Restore from this Snapshot file?": "Gjenopprette fra denne snapshotfilen?", "Resync with NRCS": "Synkroniser med ENPS", - "Shelf": "Skuff", + "Retry": "Prøv igjen", + "Return to list": "Gå tilbake til listen", + "Reveal in Shelf": "Vis i skuff", + "Rewind segments to start": "Sett segmentene tilbake til start", "Rewind Segments to start": "Sett alle segmenter tilbake til start", - "Go to On Air line": "Gå til OnAir-posisjon", + "Role": "Rolle", + "Route Set ID": "Omkoblingsgruppe-id", + "Route Set Name": "Omkoblingsgruppens navn", + "Route Sets": "Omkoblingsgrupper", + "Routes": "Omkoblinger", + "Run automatic migration procedure": "Kjør automatisk migreringsprosedyre", + "Run Migrations to get set up": "Kjør migreringsprosedyrer for å sette opp", + "Rundown": "Kjøreplan", + "Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced or remove the rundown from Sofie. What do you want to do?": "Kjøreplan {{rundownName}} i listen {{playlistName}} mangler i data fra {{nrcsName}}. Du kan enten markere den som usynkronisert og beholde den i Sofie, eller fjerne kjøreplanen fra Sofie. Hva vil du gjøre?", + "Rundown & Shelf": "Kjøreplan & skuff", + "Rundown for piece \"{{pieceLabel}}\" could not be found.": "Finner ikke kjøreplan for \"{{pieceLabel}}\".", + "Rundown Header Layout": "Layout for kjøreplanens topptekst", + "Rundown not found": "Kjøreplan ikke funnet", + "Rundowns": "Kjøreplaner", + "Save Changes": "Lagre endringer", + "Save to Bucket": "Lagre til bøtte", + "Scale": "Skala", + "Script is empty": "Manuset er tomt", + "Search...": "Søk...", + "Segment": "Tittel", + "Segment Count Down": "Nedtelling for tittel", + "Segment Count Up": "Opptelling for tittel", + "Segment countdown requires source layer": "Nedtelling for tittel krever kildelag", + "Segment no longer exists in {{nrcs}}": "Segmenet eksisterer ikke lenger i {{nrcs}}", + "Segment was hidden in {{nrcs}}": "Tittelen eksisterer ikke lenger i {{nrcs}}", + "Segments: {{delta}}": "Segmenter: {{delta}}", + "Select Action": "Velg handling", + "Select Compatible Show Styles": "Velg kompatibel showstyles", + "Select visible Output Groups": "Velg synlig utgangsgruppe", + "Select visible Source Layers": "Velg synlige kildelag", + "Select which playout devices are using this package container": "Velg hvilke playout-enheter som skal benytte denne pakkekontaineren", + "Sent Messages": "Sendte meldinger", + "Server ID": "Server-id", + "Server ID. For sources, this should generally be omitted (or set to 0) so clip-searches are zone-wide. If set, clip-searches are limited to that server.": "Server-ID. For kilder skal denne droppes (eller bli satt til 0) siden klippsøk skjer i heile sonen. Hvis denne er satt skjer klippsøk bare på den serveren.", + "Set segment as Next": "Sett tittel som Neste: Starter på neste Take", + "Settings": "Innstillinger", + "Shelf": "Skuff", + "Shelf Layout": "Layouter for skuffen", + "Shelf layout uploaded successfully.": "Opplastingen av layout for skuff var vellykket.", + "Shortcuts": "Hurtigtaster", + "Show \"Remove snapshots\"-buttons": "Vis \"Fjern snapshots\"-knappene", + "Show All": "Vis alle", + "Show Breaks as Segments": "Vis pauser som titler", + "Show End": "Sendeslutt", "Show entire On Air Segment": "Vis hele tittelen som er OnAir", - "Force (deactivate others)": "Tving (deaktiver andre)", - "Move Segments": "Skip segmenter", - "Move Parts": "Skip del", - "State": "Tilstand", - "Action": "Handling", - "Ad-Lib Action": "Adlib-handling", - "Clear Source Layer": "Tøm kildelag", - "Sticky Piece": "Element er sticky", - "Global AdLibs": "Globale adliber", - "Label": "Etikett", - "Limit": "Grense", - "Output Layer": "Utgangslag", - "Pick": "Plukk", - "Pick last": "Plukk siste", + "Show Hotkeys": "Vise hurtigtaster", + "Show issue": "Vis problem", + "Show next break timing": "Vis tid for neste pause", + "Show panel as a timeline": "Vis panel som en tidslinje", + "Show part title": "Vis delens tittel", + "Show Rundown Name": "Vis kjøreplannavn", + "Show segment name": "Vis tittelens navn", + "Show Style": "Showstyle", + "Show Style Base Name": "Showstylenavn", + "Show style not set": "Showstyle ikke satt", + "Show Style Variant": "Showstylevariant", + "Show Styles": "Showstyle", + "Show thumbnails next to list items": "Vis miniatyrbilder ved siden av listeelementer", + "Slack Webhook URLs": "Slack Webhook-adresser (url)", + "Snapshot restore failed: {{errorMessage}}": "Gjenoppretting fra snapshot feilet: {{errorMessage}}", + "Sofie Automation": "Sofie", + "Sofie Automation Server Core will restart in {{time}}s...": "Sofie Core restartes om {{time}}s...", + "Sofie Automation Server Core: {{name}}": "Sofie:", + "Something went wrong, and it affected the output": "Noe gikk galt, og det påvirket sendingen", + "Something went wrong, but it didn't affect the output": "Noe gikk galt, men det påvirket ikke sendingen", + "Something went wrong, please contact the system administrator if the problem persists.": "Noe gikk galt, kontakt systemadministrator hvis problemet fortsetter.", + "Source Abbreviation": "Kildeforkortelse", "Source Layer": "Kildelag", "Source Layer Type": "Kildelagstyper", - "Tag": "Tag", - "Not Global": "Ikke globale", - "Only Global": "Bare globale", - "OnAir": "OnAir", - "Now active rundown": "Aktiv kjøreplan akkurat nå", - "View": "Visning", - "Executes within the currently open Rundown, requires a Client-side trigger.": "Utføers innenfor den valgte kjøreplanen, men trenger en utløser fra klienten.", - "Select Action": "Velg handling", - "No Ad-Lib matches in the current state of Rundown: \"{{rundownPlaylistName}}\"": "Ingen treff på adliber i nåværende tilstand for kjøreplanen: \"{{rundownPlaylistName}}\"", - "No matching Rundowns available to be used for preview": "Ingen passende kjøreplaner tilgjengelige for forhåndsvisning", - "Multilingual description, editing will overwrite": "Endring vil overskrive flerspråklig beskrivelse", - "Optional description of the action": "Valgfri beskrivelse av handlingen", - "Triggered Actions uploaded successfully.": "Opplasting av handlingsutløsere var vellykket.", - "Triggered Actions failed to upload: {{errorMessage}}": "Opplasting av handlingsutløsere feilet: {{errorMessage}}", - "Append or Replace": "Legg til eller erstatt", - "Do you want to append these to existing Action Triggers, or do you want to replace them?": "Vil du legge disse til de nåværende handlingsutløserne, eller vil du erstatte dem?", - "Append": "Legg til", - "Action Triggers": "Handlingsutløsere", - "Find Trigger...": "Finn utløser...", - "No matching Action Trigger.": "Fikk ikke treff blant handlingsutløsere.", - "No Action Triggers set up.": "Ingen handlingsutløsere er satt opp.", - "System-wide": "Systemvid", - "Upload stored Action Triggers": "Last opp lagrede handlingsutløsere", - "Download Action Triggers": "Last ned handlingsutløsere", - "On release": "På slipp (\"Key up\")", - "Empty": "Tom", - "Hotkey": "Hurtigtast", - "Trigger Type": "Type utløser", - "Failed to update config: {{errorMessage}}": "Oppdatering av konfigurasjon feilet: {{errorMessage}}", - "Export": "Eksporter", - "Import": "Importer", - "true": "true", - "false": "false", - "{{count}} rows": "{{count}} rader", - "Value": "Verdi", - "Create": "Opprett", - "Add config item": "Legg til konfigurasjonselement", - "Add": "Legg til", - "Item": "Element", - "Delete this item?": "Slett dette elementet?", - "Are you sure you want to delete this config item \"{{configId}}\"?": "Er du sikker på at du vil slette konfigurasjonselementet \"{{configId}}\"?", - "Blueprint Configuration": "Blueprintkonfigurasjon", - "More settings specific to this studio can be found here": "Mer spesifikke innstillinger for dette studioet kan du finne her", - "There was an error: {{error}}": "Det skjedde en feil: {{error}}", - "Package Manager status": "Status fo pakkebehandler", - "Reload statuses": "Last inn status på nytt", - "Updated": "Oppdatert", - "Package Manager": "Pakkebehandler", + "Source Layer Types": "Kildelagstyper", + "Source Layers": "Kildelag", + "Source Name": "Kildenavn", + "Source Type": "Kildetype", + "Split Screen": "Splitt", + "Standalone Shelf": "Frittstående skuff", + "Start Here!": "Start her!", + "Start this AdLib": "Slett denne adliben", + "Start time is close": "Oppgitt sendestart er hvert øyeblikk", + "Start with giving this browser configuration permissions by adding this to the URL: ": "Først må du gå i konfigurasjonsmodus ved å legge dette til url-en: ", + "Started": "Startet", + "State": "Tilstand", "Statistics": "Statistikk", - "Times": "Tider", - "Connected Workers": "Tilkoblede arbeidere", - "Work-in-progress": "Pågående jobber", - "WorkForce": "Arbeiderstyrke", - "Kill (debug)": "Kill (debug)", - "Connected App Containers": "Tilkoblede app-kontainere", - "No status loaded": "Ingen status lastet", - "Peripheral Device is outdated": "Tilkoblet enhet er utdatert", + "Status": "Status", + "Status Messages:": "Statusmeldinger:", + "Sticky Piece": "Element er sticky", + "Store Snapshot": "Lagre snapshot", + "Studio": "Studio", + "Studio Baseline needs update: ": "Studio baseline må oppdateres: ", + "Studio Name": "Studionavn", + "Studio Settings": "Studioinnstillinger", + "Studio Snapshot": "Studiosnapshot", + "Studios": "Studio", + "Successfully restored snapshot": "Gjenoppretting fra snapshot var vellykket", + "Successfully stored snapshot": "Gjenoppretting fra snapshot var vellykket", + "Supported Audio Formats": "Støttede lydformater", + "Supported Media Formats": "Støttede medieformater", + "Switchboard": "Sentralbord", + "Switching operating mode to {{mode}}": "Bytt til {{mode}}", + "System": "System", + "System has issues which need to be resolved": "Systemet har problemer som må fikses", + "System Status": "Systemstatus", + "System-wide": "Systemvid", + "System-wide Notification Message": "Lokal systemmelding", + "Tag": "Tag", + "Tags must contain": "Tagger må inneholde", + "Take": "Take", + "Take a Full System Snapshot": "Lagre et fullt systemsnapshot", + "Take a Snapshot": "Lagre et snapshot", + "Take a Snapshot for studio \"{{studioName}}\" only": "Lagre et studiosnapshot utelukkende for \"{{studioName}}\"", + "Technical reason: {{reason}}": "Teknisk årsak: {{reason}}", + "Test test": "Test test", + "Test Tools": "Testverktøy", + "Text": "Tekst", + "Text to show above countdown to end of show": "Tekst som vises over nedtelling til forventet slutt", + "Text to show above countdown to next break": "Tekst som vises over nedtelling til neste pause", + "Text to show above show end time": "Tekst som vises over klokkeslett for sendeslutt", + "Text to show above show start time": "Tekst som vises over klokkeslett for sendestart", "The config UI is now driven by manifests fed by the device. This device needs updating to provide the configManifest to be configurable": "Brukergrensesnittet for konfigurasjon drives nå av manifester matet fra enhetene. Denne enheten må oppdateres for å gjøre configManifest konfigurerbart", - "Are you sure you want to restart this device?": "Er du sikker på at du vil starte denne enheten på nytt?", - "Restart this Device?": "Start denne enheten på nytt?", - "Check the console for troubleshooting data from device \"{{deviceName}}\"!": "Sjekk konsollen for feilsøkingsdata fra enheten \"{{deviceName}}\"!", - "There was an error when troubleshooting the device: \"{{deviceName}}\": {{errorMessage}}": "Det skjedde en feil under feilsøking av enhenten \"{{deviceName}}\": {{errorMessage}}", - "Generic Properties": "Generelle egenskaper", - "Device Name": "Enhetsnavn", - "Restart Device": "Start enheten på nytt", - "Troubleshoot": "Feilsøk", - "Reset Database Version": "Nullstill databaseversjon", - "Are you sure you want to reset the database version?\nOnly do this if you plan on running the migration right after.": "Er du sikker på at du vil nullstille databaseversjonen?\nBare gjør dette dersom du har tenkt å kjøre en migrering umiddelbart.", - "Version for {{name}}: From {{fromVersion}} to {{toVersion}}": "Versjon for {{name}}: Fra {{fromVersion}} til {{toVersion}}", - "Re-check": "Sjekk på nytt", - "Reset Version to": "Nullstill versjon til", - "Reset All Versions": "Nullstill alle versjoner", - "Migrate database": "Migrer database", - "All steps": "Alle trinn", - "The migration consists of several phases, you will get more options after you've this migration": "Migreringen består av flere faser, du vil få flere valg etter at du har kjørt denne migreringen", + "The following parts no longer exist in {{nrcs}}: {{partNames}}": "De følgende delene eksisterer ikke lenger i {{nrcs}}: {{partNames}}", "The migration can be completed automatically.": "Migreringen kan gjennomføres automatisk.", - "Run automatic migration procedure": "Kjør automatisk migreringsprosedyre", - "The migration procedure needs some help from you in order to complete, see below:": "Migreringsprosedyren trenger litt hjelp fra deg for å kunne fullføre. Se under:", - "Double-check Values": "Dobbeltsjekk verdier", - "Are you sure the values you have entered are correct?": "Er du sikker på at verdiene du har oppgitt er korrekte?", - "Run Migration Procedure": "Kjør migreringsprosedyre", - "Warnings During Migration": "Advarsler under migrering", - "Please check the database related to the warnings above. If neccessary, you can": "Vennligst sjekk databasen tilknyttet advarslene over. Hvis nødvendig kan du", - "Force Migration": "Tving migrering", - "Are you sure you want to force the migration? This will bypass the migration checks, so be sure to verify that the values in the settings are correct!": "Er du sikker på at du vil tvinge migreringen? Dette gjør at du hopper over migreringskontrollene, så vær sikker på at verdiene oppgitt i innstillinger er korrekte!", - "Force Migration (unsafe)": "Tving migrering (utrygt)", + "The migration consists of several phases, you will get more options after you've this migration": "Migreringen består av flere faser, du vil få flere valg etter at du har kjørt denne migreringen", "The migration was completed successfully!": "Migreringen var vellykket!", - "All is well, go get a": "Alt er greit, gå og hent deg en", - "New Layout": "Ny layout", - "Button": "Knapp", - "New Filter": "Nytt filter", - "Delete layout?": "Slett layout?", - "Are you sure you want to delete the shelf layout \"{{name}}\"?": "Er du sikker på at du vil slette layouten \"{{name}}\"?", - "Action Buttons": "Handlingsknapper", - "Icon": "Ikon", - "Icon color": "Ikonfarge", - "Filters": "Filtre", + "The old data was removed.": "Gamle data ble fjernet.", + "The planned end time has passed, are you sure you want to activate this Rundown?": "Det planlagte sluttidspunktet er passert, er du sikker på at du vil aktivere denne kjøreplan?", + "The progress of all steps": "Fremdrift for alle steg", + "The progress of steps required for playout": "Fremdrift for steg som er nødvendige for avspilling", + "The rundown \"{{rundownName}}\" is not published or activated in {{nrcsName}}! No data updates will currently come through.": "Kjøreplanen \"{{rundownName}}\" er ikke synkronisert med MOS/{{nrcsName}}! Kontroller at den er satt MOS Active i ENPS.", + "The rundown can not be reset while it is active": "En aktivert kjøreplan kan ikke tilbakestilles", + "The rundown you are trying to execute a take on is inactive, would you like to activate this rundown?": "Du prøve å gjøre en Take i en inaktiv kjøreplan. Vil du aktivere denne kjøreplanen?", + "The segment duration in the segment header always displays the planned duration instead of acting as a counter": "Tittelens varighet i tittelheaderen vil alltid vise den planlagte varigheten i stedet for å telle ned", + "The system configuration has been changed since importing this rundown. It might not run correctly": "Systemoppsettet har blitt endret etter at denne kjøreplanen ble importert. Kjøreplanen kan spilles av med feil", + "The type of device to use for the output": "Enhetstype som skal brukes for utgangen", + "The way this Route Set should behave towards the user": "Måten denne omkoblingsgruppen skal oppføre seg på overfor brukeren", + "Then, run the migrations script:": "Kjør så migreringsprosedyren:", + "There are no Accessors set up.": "Ingen aksessorer er satt opp.", + "There are no active rundowns.": "Fant ingen aktive kjøreplaner.", + "There are no exclusivity groups set up.": "Ingen eksklusivitetsgrupper er satt opp.", "There are no filters set up yet": "Det er ikke satt opp noen filtre ennå", - "Default Layout": "Standardlayout", - "Add {{filtersTitle}}": "Legg til {{filtersTitle}}", - "Add filter": "Legg til filter", - "Add button": "Legg til knapp", - "Upload Layout?": "Last opp layout?", - "Upload": "Last opp", - "Are you sure you want to upload the shelf layout from the file \"{{fileName}}\"?": "Er du sikker på at du vil laste opp layout for skuff fra filen \"{{fileName}}\"?", - "Shelf layout uploaded successfully.": "Opplastingen av layout for skuff var vellykket.", - "Failed to upload shelf layout: {{errorMessage}}": "Opplasting av layout feilet: {{errorMessage}}", - "Show Style Base Name": "Showstylenavn", - "Blueprint": "Blueprint", - "Blueprint not set": "Blueprint ikke valgt", - "Compatible Studios:": "Kompatible studio:", + "There are no Playout Gateways connected and attached to this studio. Please contact the system administrator to start the Playout Gateway.": "Dette studioet har ingen tilkoblede playout-gatewayer. Kontakt systemadministrator for å starte den.", + "There are no Route Sets set up.": "Det er ikke satt opp omkoblinger ennå.", + "There are no routes set up yet": "Det er ikke satt opp omkoblinger ennå", + "There are no rundowns ingested into Sofie.": "Det er ikke sendt kjøreplaner til Sofie.", + "There is an unknown problem with the part.": "Det er et ukjent problem med denne delen.", + "There is an unspecified problem with the source.": "Det er et ikke-spesifisert problem med kilden.", + "There is no rundown active in this studio.": "Fant ingen aktive kjøreplaner for dette studioet.", + "There was an error when troubleshooting the device: \"{{deviceName}}\": {{errorMessage}}": "Det skjedde en feil under feilsøking av enhenten \"{{deviceName}}\": {{errorMessage}}", + "There was an error: {{error}}": "Det skjedde en feil: {{error}}", + "This action has an invalid combination of filters": "Denne handlingen har en ugyldig kombinasjon av filtre", + "This affects how much is logged to the console on the server": "Dette påvirker hvor mye som blir logget til serverkonsollen", + "This Blueprint is not being used by any Show Style": "Dette blueprintet er ikke i bruk av noen showstyles", + "This Blueprint is not compatible with any Studio": "Dette blueprintet er ikke kompatibel med noe studio", + "This is not in it's normal setting": "Endret fra standardoppsett", + "This name will be shown in the title bar of the window": "Dette navnet vil vises i tittellinjen for vinduet", + "This playlist is empty": "Denne spillelisten er tom", + "This rundown has been unpublished from Sofie.": "Denne kjøreplanen er ikke lenger tilgjengelig i Sofie.", + "This rundown is currently active": "Denne kjøreplanen er allerede aktiv", + "This rundown is now active. Are you sure you want to exit this screen?": "Denne kjøreplanen er aktiv. Er du sikker på at du vil avslutte?", + "This rundown will loop indefinitely": "Denne kjøreplanen vil gå i en uendelig loop", "This Show Style is not compatible with any Studio": "Denne showstylen er ikke kompatibelt med noe studio", - "Camera": "Kamera", - "Graphics": "Grafikk", - "Live Speak": "STK", - "Lower Third": "Super", - "Studio Microphone": "Studiomikrofon", - "Remote Source": "RM", - "Generic Script": "Generisk manus", - "Split Screen": "Splitt", - "Clips": "Klipp", - "Metadata": "Metadata", - "Camera Movement": "Kamerabevegelse", - "Unknown Layer": "Ukjent lag", - "Audio Mixing": "Lydmiksing", + "This step is required for playout": "Dette steget er nødvendig for avspilling", + "This studio doesn't exist.": "Dette studioet finnes ikke.", + "This will remove {{indexCount}} old indexes, do you want to continue?": "Dette vil fjerne {{indexCount}} gamle indexer. Vil du fortsette?", + "Time since planned end": "Tid siden planlagt slutt", + "Time to planned end": "Tid til planlagt slutt", + "Timeline": "Tidslinje", + "Times": "Tider", + "Timestamp": "Tidsstempel", + "Today": "I dag", + "Toggle": "Veklse", + "Toggle AdLibs on single mouse click": "Veksle mellom adliber med enkelt museklikk", + "Toggle Shelf": "Skuff", + "Tomorrow": "I morgen", + "Tools": "Verktøy", "Transition": "Effekt", - "Lights": "Lys", - "Local": "Lokal", - "New Source": "Ny kilde", - "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?": "Er du sikker på at du vil slette kildelaget \"{{sourceLayerId}}\"?", - "Source Name": "Kildenavn", - "Source Abbreviation": "Kildeforkortelse", - "Internal ID": "Intern-id", - "Source Type": "Kildetype", - "Is a Live Remote Input": "Er en RM", - "Is a Guest Input": "Er en gjesteinngang", - "Is hidden": "Er skjult", - "Pieces on this layer can be cleared": "Elementer på dette laget kan tømmes", - "Pieces on this layer are sticky": "Elementer i dette laget er sticky", - "Only Pieces present in rundown are sticky": "Kun elementer tilstede i kjøreplanen er sticky", - "Allow disabling of Pieces": "Tillat deaktivering av elementer", - "AdLibs on this layer can be queued": "Adliber på dette laget kan cues", - "Exclusivity group": "Ekslusivitetsgruppe", - "Add some source layers (e.g. Graphics) for your data to appear in rundowns": "Legg til kildelag (for eksempel Grafikk) for å vise dine data i kjøreplaner", - "No source layers set": "Ingen kildelag definert", - "Delete this output?": "Slett denne utgangen?", - "Are you sure you want to delete source layer \"{{outputId}}\"?": "Er du sikker på at du vil slette kildelaget \"{{outputId}}\"?", - "New Output": "Ny utgang", - "Channel Name": "Kanalnavn", - "Is PGM Output": "Er programutgang", - "Is collapsed by default": "Er minimert som standard", - "Is flattened": "Er slått sammen", - "Output channels are required for your studio to work": "Utgangskanaler er nødvendige for at studioet ditt skal fungere", - "Output channels": "Utgangskanal", - "No output channels set": "Ingen utgangskanal definert", - "No PGM output": "Ingen programutgang", - "Key": "Key", - "Custom Hotkey Labels": "Egendefinerte etiketter for hurtigtaster", - "Remove this Variant?": "Fjern denne varianten?", - "Are you sure you want to remove the variant \"{{showStyleVariantId}}\"?": "Er du sikker på at du vil fjerne denne showstylevarianten \"{{showStyleVariantId}}\"?", + "Triggered Actions failed to upload: {{errorMessage}}": "Opplasting av handlingsutløsere feilet: {{errorMessage}}", + "Triggered Actions uploaded successfully.": "Opplasting av handlingsutløsere var vellykket.", + "Trim \"{{name}}\"": "Trim \"{{name}}\"", + "Trimming this clip has failed due to an error: {{error}}.": "Endring av inn-/utpunkt for dette klippet feilet: {{error}}.", + "Trimming this clip has timed out. It's possible that the story is currently locked for writing in {{nrcsName}} and will eventually be updated. Make sure that the story is not being edited by other users.": "Endring av inn-/utpunkt for dette klippet tar lang tid. Det er mulig manuset i er låst i {{nrcsName}} og at inn-/utpunkt endres om litt. Forsikre deg om at manuset ikke blir redigert av andre brukere.", + "Trimming this clip is taking longer than expected. It's possible that the story is locked for writing in {{nrcsName}}.": "Endring av inn-/utpunkt for dette klippet tek meir tid enn forventa. Det er mogleg manuset er låst for redigering i {{nrcsName}}.", + "Troubleshoot": "Feilsøk", + "Type": "Type", + "Unable to check the system configuration for changes": "Kan ikke kontrollere endringer i systemoppsettet", + "Unassign": "Fjern tilordning", + "Undo": "Angre", + "Undo Disable the next element": "Unskip neste super", + "Undo Hold": "Angre hold", + "Unknown": "Ukjent", + "Unknown error": "Ukjent feil", + "Unknown Layer": "Ukjent lag", + "Unknown Package \"{{packageId}}\"": "Ukjent pakke \"{{packageId}}\"", + "Unnamed blueprint": "Blueprint uten navn", + "Unnamed Show Style": "Showstyle uten navn", + "Unnamed Studio": "Studio uten navn", "Unnamed variant": "Variant uten navn", - "Variant Name": "Variantnavn", - "Variants": "Varianter", - "Restore from this Snapshot file?": "Gjenopprette fra denne snapshotfilen?", - "Are you sure you want to restore the system from the snapshot file \"{{fileName}}\"?": "Er du sikker på at du vil gjenopprettet systemet fra snapshotfilen \"{{fileName}}\"?", - "Successfully restored snapshot": "Gjenoppretting fra snapshot var vellykket", - "Snapshot restore failed: {{errorMessage}}": "Gjenoppretting fra snapshot feilet: {{errorMessage}}", - "Full System Snapshot": "Fullt systemsnapshot", - "A Full System Snapshot contains all system settings (studios, showstyles, blueprints, devices, etc.)": "Et systemsnapshot inneholder alle systeminnstillinger (studio, showstyles, blueprints, enheter o.s.v.)", - "Take a Full System Snapshot": "Lagre et fullt systemsnapshot", - "Studio Snapshot": "Studiosnapshot", - "A Studio Snapshot contains all system settings related to that studio": "Et studiosnapshot inneholder alle systeminnstillinger tilknyttet et studio", - "Take a Snapshot for studio \"{{studioName}}\" only": "Lagre et studiosnapshot utelukkende for \"{{studioName}}\"", - "Restore from Snapshot File": "Gjenopprett fra snapshotfil", + "Until end of rundown": "Til slutten av kjøreplanen", + "Until end of segment": "Til slutten av segment", + "Until end of showstyle": "Til slutten av showstyle", + "Until next rundown": "Til neste kjøreplan", + "Until next segment": "Til neste segment", + "Until next take": "Til neste Take", + "Update": "Oppdater", + "Update Blueprints?": "Oppdater blueprints?", + "Updated": "Oppdatert", + "Upgrade Database": "Oppgrader databasen", + "Upload": "Last opp", + "Upload a new blueprint": "Last opp et nytt blueprint", + "Upload Blueprints": "Last opp blueprints", + "Upload Layout?": "Last opp layout?", "Upload Snapshot": "Last opp snapshot", - "Restore from Stored Snapshots": "Gjenopprett fra lagrede snapshots", - "Restore": "Gjenopprett", - "Show \"Remove snapshots\"-buttons": "Vis \"Fjern snapshots\"-knappene", - "Remove this device?": "Fjern denne enheten?", - "Are you sure you want to remove device \"{{deviceId}}\"?": "Er du sikker på at du vil fjerne enheten \"{{deviceId}}\"?", - "Devices are needed to control your studio hardware": "Enheter er nødvendige for å kontrollere utstyret i studioet ditt", - "Attached Devices": "Tilkoblede enheter", - "No devices connected": "Ingen enheter tilkoblet", - "Playout gateway not connected": "Playout-gateway er ikke tilkoblet", - "Remove this mapping?": "Fjern denne mappingen?", - "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?": "Er du sikker på at du fil fjerne mappingen for laget \"{{mappingId}}\"?", - "This layer is now rerouted by an active Route Set: {{routeSets}}": "Dette laget blir omkoblet av en aktiv omkoplingsgruppe: {{routeSets}}", - "Layer ID": "Lag-id", - "ID of the timeline-layer to map to some output": "Lag-id for tidslinjelaget som skal mappes til en utgang", - "Layer Name": "Lagnavn", - "Human-readable name of the layer": "Leservennlig lagnavn", - "The type of device to use for the output": "Enhetstype som skal brukes for utgangen", - "ID of the device (corresponds to the device ID in the peripheralDevice settings)": "Enhets-id (korresponderer med enhets-id under enhetsinnstillinger)", - "Lookahead Mode": "Lookahead-modus", - "Lookahead Target Objects (Default = 1)": "Lookahead målobjekter (standard = 1)", - "Lookahead Maximum Search Distance (Default = {{limit}})": "Lookahead maksimum søkelengde (standard = {{limit}})", - "Layer Mappings": "Lagmappinger", - "Add a playout device to the studio in order to edit the layer mappings": "For å kunne redigere lagmappinger, må du legge til en playout-enhet til studio", - "Remove this Exclusivity Group?": "Fjern fra denne eksklusivitetsgruppen?", - "Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\nRoute Sets assigned to this group will be reset to no group.": "Er du sikker på at du vil fjerne eksklusivitetsgruppen \"{{eGroupName}}\"?\nOmkoblinger satt til denne gruppen vil bli resatt til ingen gruppe.", - "Remove this Route from this Route Set?": "Fjern denne omkoblingen fra omkoblingsgruppen?", - "Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to \"{{newLayerId}}\"?": "Er du sikker på at du vil fjerne omkoblingen fra \"{{sourceLayerId}}\" til \"{{newLayerId}}\"?", - "Remove this Route Set?": "Fjern denne omkoblingsgruppen?", - "Are you sure you want to remove the Route Set \"{{routeId}}\"?": "Er du sikker på at du vil fjerne omkoblingsgruppen \"{{routeId}}\"?", - "Routes": "Omkoblinger", - "There are no routes set up yet": "Det er ikke satt opp omkoblinger ennå", - "Original Layer": "Opprinnelig lag", - "None": "Ingen", - "New Layer": "Nytt lag", - "Source Layer not found": "Kildelag ikke funnet", - "There are no exclusivity groups set up.": "Ingen eksklusivitetsgrupper er satt opp.", - "Exclusivity Group ID": "Eksklusivitetsgruppe-id", - "Exclusivity Group Name": "Eksklusivitetsgruppenavn", - "Display name of the Exclusivity Group": "Eksklusivitetsgruppens navn som vises i oversikten", - "Active": "Aktiv", - "Not Active": "Inaktiv", - "Not defined": "Ikke definert", - "There are no Route Sets set up.": "Det er ikke satt opp omkoblinger ennå.", - "Route Set ID": "Omkoblingsgruppe-id", - "Is this Route Set currently active": "Er denne omkoblingsgruppen aktiv nå", - "Default State": "Standardtilstand", - "The default state of this Route Set": "Standardtilstand for denne omkoblingsgruppen", - "Route Set Name": "Omkoblingsgruppens navn", - "Display name of the Route Set": "Omkoblingsgruppens navn som vises i oversikten", - "If set, only one Route Set will be active per exclusivity group": "Bare en omkoblingsgruppe være aktiv per eksklusivitetsgruppe når dette er krysset av for", - "Behavior": "Oppførsel", - "The way this Route Set should behave towards the user": "Måten denne omkoblingsgruppen skal oppføre seg på overfor brukeren", - "Route Sets": "Omkoblingsgrupper", - "Add a playout device to the studio in order to configure the route sets": "For å kunne redigere omkoblingsgrupper, må du legge til en playout-enhet til studio", - "Controls for exposed Route Sets will be displayed to the producer within the Rundown View in the Switchboard.": "Kontroller for eksponerte omkoblingsgrupper vil vises til producer i kjøreplansvisningen i omkoblingspanelet.", - "Exclusivity Groups": "Eksklusivitetsgrupper", - "Remove this Package Container?": "Fjern denne pakkekontaineren?", - "Are you sure you want to remove the Package Container \"{{containerId}}\"?": "Er du sikker på at du vil fjerne pakkekontaineren \"{{containerId}}\"?", - "There are no Package Containers set up.": "Det er ikke satt opp pakkekontainere ennå.", - "Package Container ID": "Pakkekontainer-id", - "Display name/label of the Package Container": "Vis navn/merkelapp for pakkekontaineren", - "Playout devices which uses this package container": "Playout-enheter som benytter denne pakkekontaineren", - "Select playout devices": "Velg playout-enhet", - "Select which playout devices are using this package container": "Velg hvilke playout-enheter som skal benytte denne pakkekontaineren", - "Accessors": "Aksessorer", - "Remove this Package Container Accessor?": "Fjern denne pakkekontainer-aksessoren?", - "Are you sure you want to remove the Package Container Accessor \"{{accessorId}}\"?": "Er du sikker på at du vil fjerne pakkekontainer-aksessoren \"{{accessorId}}\"?", - "There are no Accessors set up.": "Ingen aksessorer er satt opp.", - "Accessor ID": "Aksessor-id", - "Display name of the Package Container": "Pakkekontainerens navn som vises i oversikten", - "Accessor Type": "Aksessortype", - "Folder path": "Mappesti", - "File path to the folder of the local folder": "Sti til lokale mappe", - "Resource Id": "Ressurs-id", - "(Optional) This could be the name of the computer on which the local folder is on": "(Valgfri) Dette kan være navnet til datamaskinen som den lokale mappen er på", - "Base URL": "Base-url", - "Base url to the resource (example: http://myserver/folder)": "Base-url for ressursen (eksempel: http://minserver/mappe)", - "Network Id": "Nettverk-id", - "(Optional) A name/identifier of the local network where the share is located, leave empty if globally accessible": "(Valgfri) Et navn/en identifikator for det lokale nettverket hvor den delte mappen er lokalisert, la være tom dersom den er globalt tilgjengelig", - "Folder path to shared folder": "Sti til delt mappe", - "UserName": "Brukernavn", - "Username for athuentication": "Brukernavn for autentisering", - "Password for authentication": "Passord for autentisering", - "(Optional) A name/identifier of the local network where the share is located": "(Valgfri) Et navn/en identifikator for det lokale nettverket hvor den delte mappen er lokalisert", - "Quantel gateway URL": "Quantel Gateway-adresse (url)", + "Upload stored Action Triggers": "Last opp lagrede handlingsutløsere", + "URL": "Adresse (url)", + "URL to the Quantel FileFlow Manager": "Adresse til Quantel FileFlow Manager", "URL to the Quantel Gateway": "Start Quantel Gateway på nytt", - "ISA URLs": "ISA-adresse (url)", - "URLs to the ISAs, in order of importance (comma separated)": "Adresser (url-er) for ISA-ene (kommaseparert i prioritert rekkefølge)", - "Zone ID": "Sone-id", - "Zone ID (default value: \"default\")": "Sone-id (standardverdi: \"default\")", - "Server ID": "Server-id", - "Server ID. For sources, this should generally be omitted (or set to 0) so clip-searches are zone-wide. If set, clip-searches are limited to that server.": "Server-ID. For kilder skal denne droppes (eller bli satt til 0) siden klippsøk skjer i heile sonen. Hvis denne er satt skjer klippsøk bare på den serveren.", - "Quantel transformer URL": "Quantel Transformer-adresse (url)", "URL to the Quantel HTTP transformer": "Adresse til Quantel HTTP transformer", - "Quantel FileFlow URL": "Quantel FileFlow-adresse (url)", - "URL to the Quantel FileFlow Manager": "Adresse til Quantel FileFlow Manager", - "Quantel FileFlow Profile name": "Quantel FileFlow profilnavn", - "Profile name to be used by FileFlow when exporting the clips": "Profilnavn som benyttes av FileFlow når klippene eksporteres", - "Allow Read access": "Tillat lesing", - "Allow Write access": "Tillat skriving/lagring", - "Studio Settings": "Studioinnstillinger", - "Package Containers to use for previews": "Pakkekontainere som skal benyttes til forhåndsvisninger", - "Click to show available Package Containers": "Klikk for å vise tilgjengelige pakkekontainere", - "Package Containers to use for thumbnails": "Pakkekontainere som skal benyttes til miniatyrbilder", - "Package Containers": "Pakkekontainere", - "Studio Baseline needs update: ": "Studio baseline må oppdateres: ", - "Baseline needs reload, this studio may not work until reloaded": "Baseline må lastes på nytt, dette studioet vil kanskje ikke fungere før baseline er lastet på nytt", - "Reload Baseline": "Last inn baseline på nytt", - "Studio Name": "Studionavn", - "Select Compatible Show Styles": "Velg kompatibel showstyles", - "Show style not set": "Showstyle ikke satt", - "Click to show available Show Styles": "Klikk for å vise tilgjengelige showstyles", - "Frame Rate": "Framerate", - "Enable \"Play from Anywhere\"": "Slå på \"Play from Anywhere\"", - "Media Preview URL": "Forhåndsvisnings-URL", - "Sofie Host URL": "Sofie vertadresse (url)", - "Slack Webhook URLs": "Slack Webhook-adresser (url)", - "Supported Media Formats": "Støttede medieformater", - "Supported Audio Formats": "Støttede lydformater", - "Force the Multi-gateway-mode": "Tving multigateway-modus", - "Multi-gateway-mode delay time": "Delaytid for multigateway-modus", - "Allow Rundowns to be reset while on-air": "Tillat tilbakestilling av kjøreplaner som er on-air", - "Remove indexes": "Fjern indexer", - "This will remove {{indexCount}} old indexes, do you want to continue?": "Dette vil fjerne {{indexCount}} gamle indexer. Vil du fortsette?", - "{{indexCount}} indexes was removed.": "{{indexCount}} indexer ble fjernet.", - "Installation name": "Installasjonsnavn", - "This name will be shown in the title bar of the window": "Dette navnet vil vises i tittellinjen for vinduet", - "Logging level": "Loggenivå", - "This affects how much is logged to the console on the server": "Dette påvirker hvor mye som blir logget til serverkonsollen", - "System-wide Notification Message": "Lokal systemmelding", - "Message": "Melding", - "Enabled": "Aktivert", - "Edit Support Panel": "Rediger supportpanel", - "HTML that will be shown in the Support Panel": "HTML-kode som vil bli vist i supportpanelet", - "Application Performance Monitoring": "Overvåkning av applikasjonsytelse (AMP)", - "APM Enabled": "AMP aktivert", - "APM Transaction Sample Rate": "Prøvefrekvens for AMP-transaksjoner", - "How many of the transactions to monitor. Set to -1 to log nothing (max performance), 0.5 to log 50% of the transactions, 1 to log all transactions": "Antall transaksjoner som overvåkes. Sett verdien til -1 for å ikke logge noe (maks ytelse), til 0.5 for å logge halvparten av transaksjonene eller til 1 for å logge alle transaksjonene", - "Note: Core needs to be restarted to apply these settings": "Merknad: Core må startes på nytt for å ta i bruk disse innstillingene", - "Enable": "Aktiver", - "Cron jobs": "Cron-jobber", - "Enable CasparCG restart job": "Aktiver CasparCG restartjobber", - "Cleanup": "Opprydding", - "Cleanup old database indexes": "Rydd opp i gamle databaseindexer", - "Cleanup old data": "Rydd opp i gamle data", - "Disable CasparCG restart job": "Deaktiver CasparCG restartjobber", - "Remove old data from database": "Fjern gamle data fra databasen", - "There are {{count}} documents that can be removed, do you want to continue?": "Det er {{count}} dokumenter som kan fjernes. Vil du fortsette?", - "Documents to be removed:": "Dokumenter som fjernes:", - "Retry": "Prøv igjen", - "Remove old data": "Fjern gamle data", - "The old data was removed.": "Gamle data ble fjernet.", - "Last {{layerName}}": "Siste {{layerName}}", - "Clear {{layerName}}": "Tøm {{layerName}}", - "Search...": "Søk...", - "Are you sure you want to deactivate this Rundown\n(This will clear the outputs)": "Er du sikker på at du vil deaktivere denne kjøreplanen?\n(Dette vil nullstille alle utganger.)", - "Successfully stored snapshot": "Gjenoppretting fra snapshot var vellykket", - "End Words": "Stikkord", - "Global AdLib": "Globale adliber", - "AdLib does not provide any options": "Adlib har ingen valg", - "Execute": "Utfør", - "Save to Bucket": "Lagre til bøtte", - "Reveal in Shelf": "Vis i skuff", - "Edit in Nora": "Rediger i Nora", - "Current Part": "Nåværende del", - "Next Part": "Neste del", - "Part Count Down": "Nedtelling for del", - "Part Count Up": "Opptelling for del", - "Until end of rundown": "Til slutten av kjøreplanen", - "New Bucket": "Ny bøtte", - "Are you sure you want to delete this AdLib?": "Er du sikker på at du vil slette denne adliben?", - "Are you sure you want to delete this Bucket?": "Er du sikker på at du vil slette denne bøtten?", - "Are you sure you want to empty (remove all adlibs inside) this Bucket?": "Er du sikker på at du vil tømme denne bøtten (fjerner alle adliber)?", - "Current Segment": "Nåværende tittel", - "Next Segment": "Neste tittel", - "Segment Count Down": "Nedtelling for tittel", - "Segment Count Up": "Opptelling for tittel", - "Start this AdLib": "Slett denne adliben", - "Queue this AdLib": "Cue denne adliben", - "Inspect this AdLib": "Inspiser denne adliben", - "Rename this AdLib": "Gi denne adliben nytt navn", - "Delete this AdLib": "Slett denne adliben", - "Empty this Bucket": "Tøm denne bøtten", - "Rename this Bucket": "Gi bøtten nytt navn", - "Delete this Bucket": "Slett denne bøtten", - "Create new Bucket": "Opprett ny bøtte", - "AdLib": "Adlib", - "Shortcuts": "Hurtigtaster", - "Show Style Variant": "Showstylevariant", - "Local Time": "Lokal tid", - "System": "System", - "Media": "Media", - "Packages": "Pakker", - "Messages": "Meldinger", + "URLs to the ISAs, in order of importance (comma separated)": "Adresser (url-er) for ISA-ene (kommaseparert i prioritert rekkefølge)", + "Use {{nrcsName}} order": "Bruk rekkefølge fra {{nrcsName}}", + "User Activity Log": "Aktivitetslogg", + "User ID": "Bruker-id", "User Log": "Brukerlogg", - "Evaluations": "Evalueringer", - "Timestamp": "Tidsstempel", "User Name": "Brukernavn", - "Answers": "Svar", - "Message Queue": "Meldingskø", - "Queued Messages": "Meldinger i kø", - "Sent Messages": "Sendte meldinger", - "File Copy": "Kopier fil", - "File Delete": "Slett fil", - "Check file size": "Sjekk filstørrelse", - "Scan File": "Scan fil", - "Generate Thumbnail": "Generer miniatyrbilder", - "Generate Preview": "Generer forhåndsvisning", - "Unknown action: {{action}}": "Ukjent handling: {{action}}", - "Done": "Utført", - "Failed": "Mislykket", - "Working, Media Available": "Jobber, media er tilgjengelig", - "Working": "Jobber", - "Pending": "Venter", - "Blocked": "Blokkert", - "Canceled": "Avbrutt", - "Idle": "Inaktiv", - "Skipped": "Hoppet over", - "Step progress: {{progress}}": "Fremdrift: {{progress}}", - "Processing": "Prosesserer", - "Unknown: {{status}}": "Ukjent: {{status}}", - "Collapse": "Minimer", - "Details": "Detaljer", - "Abort": "Avbryt", - "Prioritize": "Prioriter", - "Media Transfer Status": "Status for medieoverføringer", - "Abort All": "Avbryt alle", - "Restart All": "Start alle på nytt", - "Unknown Package \"{{packageId}}\"": "Ukjent pakke \"{{packageId}}\"", - "Package Status": "Pakkestatus", - "Package container status": "Status for pakkekontainer", - "Id": "Id", - "Work status": "Jobbstatus", - "Restart All jobs": "Start alle jobber på nytt", - "Created": "Opprettet", - "Ready": "Klar", - "The progress of steps required for playout": "Fremdrift for steg som er nødvendige for avspilling", - "The progress of all steps": "Fremdrift for alle steg", - "This step is required for playout": "Dette steget er nødvendig for avspilling", + "Value": "Verdi", + "Variants": "Varianter", + "version": "versjon", + "Version": "Versjon", + "Version for {{name}}: From {{fromVersion}} to {{toVersion}}": "Versjon for {{name}}: Fra {{fromVersion}} til {{toVersion}}", + "View": "Visning", + "View Layout": "Vis layout", + "Waiting for gateway to generate URL...": "Venter på at gateway genererer URL...", + "Warning": "Advarsel", + "Warnings": "Advarsler", + "Warnings During Migration": "Advarsler under migrering", + "Whether to show countdown to next break": "Om nedtelling til neste pause skal vises", + "While there are still breaks coming up in the show, hide the Expected End timers": "Gjem nedtelling til forventet slutt mens det fremdeles er pauser igjen i sendingen", + "Width": "Bredde", "Work description": "Jobbeskrivlese", + "Work status": "Jobbstatus", "Work status reason": "Årsak for jobbstatus", - "Technical reason: {{reason}}": "Teknisk årsak: {{reason}}", - "Previous work status reasons": "Tidligere årsaker for jobbsatus", - "Priority": "Prioritet", - "Not Connected": "Ikke tilkoblet", - "Do you want to restart CasparCG Server?": "Er du sikker på at du vil restarte CasparCG?", - "Restart Quantel Gateway": "Start Quantel Gateway på nytt", - "Do you want to restart Quantel Gateway?": "Vil du starte Quantel Gateway på nytt?", - "Quantel Gateway restarting...": "Quantel Gateway starter på nytt...", - "Failed to restart Quantel Gateway: {{errorMessage}}": "Klarte ikke å restarte Quantel Gateway: {{errorMessage}}", - "Format HyperDeck disks": "Formater HyperDeck-disker", - "Do you want to format the HyperDeck disks? This is a destructive action and cannot be undone.": "Er du sikker på at du vil formatere HyperDeck-diskene? Dette kan ikke angres.", - "Formatting HyperDeck disks on device \"{{deviceName}}\"...": "Formaterer HyperDeck-disker på \"{{deviceName}}\"...", - "Failed to format HyperDecks on device: \"{{deviceName}}\": {{errorMessage}}": "Formatering av HyperDeck-disker på \"{{deviceName}}\" feilet: {{errorMessage}}", - "Last seen": "Sist sett", - "Connect some devices to the playout gateway": "Koble til en eller flere enheter til playout gatewayen", - "Format disks": "Formater disker", - "Are you sure you want to delete this device: \"{{deviceId}}\"?": "Er du sikker på at du vil fjerne enheten \"{{deviceId}}\"?", - "Sofie Automation Server Core: {{name}}": "Sofie:", - "Restart this system?": "Starte dette Sofie-systemet på nytt?", - "Are you sure you want to restart this Sofie Automation Server Core: {{name}}?": "Er du sikker på at du vil starte Sofie Core: {{name}} på nytt?", - "Could not generate restart token!": "Kunne ikke generere Restart Token!", - "Could not generate restart core: {{err}}": "Kunne ikke generere Restart Core: {{err}}", - "Sofie Automation Server Core will restart in {{time}}s...": "Sofie Core restartes om {{time}}s...", - "Execution times": "Kjøretider", - "User ID": "Bruker-id", - "Client IP": "Klient-ip", - "Method": "Metode", - "Parameters": "Parametre", - "GUI": "Brukergrensesnitt", - "User Activity Log": "Aktivitetslogg", - "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s": "om {{days}} dager, {{hours}} t {{minutes}} min {{seconds}} s", - "in {{hours}} h {{minutes}} min {{seconds}} s": "om {{hours}} t {{minutes}} min {{seconds}} s", - "in {{minutes}} min {{seconds}} s": "om {{minutes}} min {{seconds}} s", - "in {{seconds}} s": "om {{seconds}} s", - "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago": "for {{days}} dager, {{hours}} t {{minutes}} min {{seconds}} s siden", - "{{hours}} h {{minutes}} min {{seconds}} s ago": "for {{hours}} t {{minutes}} min {{seconds}} s siden", - "{{minutes}} min {{seconds}} s ago": "for {{minutes}} min {{seconds}} s siden", - "{{seconds}} s ago": "for {{seconds}} s siden", - "Next scheduled show": "Neste planlagte sending", - "Help & Support": "Hjelp og brukerstøtte", - "Disable hints by adding this to the URL:": "Deaktiver hint ved å legge dette til på url-en:", - "Enable hints by adding this to the URL:": "Aktiver hint ved å legge dette til på url-en:", - "More documentation available at:": "Mer dokumentasjon er tilgjengelig på:", - "Timeline": "Tidslinje", - "Mappings": "Lagmappinger", - "User Log Player": "Brukerloggspiller", - "Play from here": "Spill av herfra", - "Exectute Single": "Utfør enslig handling", - "Next Action": "Neste handling", - "Run in": "Kjør i", - "Stop": "Stopp", - "Clip \"{{fileName}}\" can't be played because it doesn't exist on the playout system": "Klippet \"{{fileName}}\" kan ikke spilles av fordi det ikke finnes på utspillingssystemet", - "{{sourceLayer}} is not yet ready on the playout system": "{{sourceLayer}} er ennå ikke klar til å spilles ut fra avviklingsserver", - "{{sourceLayer}} is transferring to the playout system": "{{sourceLayer}} overføres til avviklingsserver", - "{{sourceLayer}} is transferring to the playout system and cannot be played yet": "{{sourceLayer}} overføres til avviklingsserver og kan ikke spilles av ennå", - "{{sourceLayer}} doesn't have both audio & video": "{{sourceLayer}} har ikke lyd og/eller bilde", - "{{sourceLayer}} has the wrong format: {{format}}": "{{sourceLayer}}-formatet er ikke støttet: {{format}}", - "{{sourceLayer}} has {{audioStreams}} audio streams": "{{sourceLayer}} har {{audioStreams}} lydstrømmer", - "Clip starts with {{frames}} {{type}} frames": "Klippet starter med {{frames}} {{type}} ruter", - "This clip ends with {{type}} frames after {{count}} seconds": "Klippet slutter med {{frames}} {{type}} frame", - "{{frames}} {{type}} frames detected within the clip": "{{frames}} {{type}} frame oppdaget inne i klippet", - "{{frames}} {{type}} frames detected in the clip": "{{frames}} {{type}} frame oppdaget inne i klippet", - "black": "svart(e)", - "freeze": "fryst(e)", - "{{sourceLayer}} is missing a file path": "{{sourceLayer}} kan ikke spilles av fordi filnavnet mangler", - "Clip doesn't have audio & video": "Klippet har ikke lyd og/eller bilde", - "Clip starts with {{frames}} {{type}} frame": "Klippet starter med {{frames}} {{type}} frame", - "This clip ends with {{type}} frames after {{count}} second": "Klippet slutter med {{frames}} {{type}} frame", - "{{frames}} {{type}} frame detected within the clip": "{{frames}} {{type}} frame oppdaget inne i klippet", - "{{frames}} {{type}} frame detected in clip": "{{frames}} {{type}} frame oppdaget i klippet", - "{{sourceLayer}} is being ingested": "{{sourceLayer}} blir prosessert", - "Source is missing": "Kilde mangler", - "Segment no longer exists in {{nrcs}}": "Segmenet eksisterer ikke lenger i {{nrcs}}", - "Segment was hidden in {{nrcs}}": "Tittelen eksisterer ikke lenger i {{nrcs}}", - "The following parts no longer exist in {{nrcs}}: {{partNames}}": "De følgende delene eksisterer ikke lenger i {{nrcs}}: {{partNames}}", - "Toggle Shelf": "Skuff", - "Undo Hold": "Angre hold", - "Disable the next element": "Skip neste super", - "Undo Disable the next element": "Unskip neste super", - "Move Next forwards": "Skip neste", - "Move Next to the following segment": "Skip til neste segment", - "Move Next backwards": "Unskip neste", - "Move Next to the previous segment": "Unskip neste segment", - "Rewind segments to start": "Sett segmentene tilbake til start", - "{{count}} rows°°°°°°_°°°°°°plural": "{{count}} rader°°°°°°", - "This layer is now rerouted by an active Route Set: {{routeSets}}°°°°°°_°°°°°°plural": "Dette laget blir omkoblet av flere aktive omkoblingsgrupper: {{routeSets}}°°°°°°", - "There are {{count}} documents that can be removed, do you want to continue?°°°°°°_°°°°°°plural": "Det er {{count}} dokumenter i {{collections}} som kan fjernes. Vil du fortsette?°°°°°°", - "Clip starts with {{frames}} {{type}} frame°°°°°°_°°°°°°plural": "Klippet starter med {{frames}} {{type}} frame°°°°°°", - "This clip ends with {{type}} frames after {{count}} second°°°°°°_°°°°°°plural": "Klippet slutter med {{frames}} {{type}} frame°°°°°°", - "{{frames}} {{type}} frame detected within the clip°°°°°°_°°°°°°plural": "{{frames}} {{type}} frames oppdaget inne i klippet°°°°°°", - "{{frames}} {{type}} frame detected in clip°°°°°°_°°°°°°plural": "{{frames}} {{type}} frames oppdaget i klippet°°°°°°" + "Work-in-progress": "Pågående jobber", + "WorkForce": "Arbeiderstyrke", + "X": "X", + "Y": "Y", + "Yes": "Ja", + "Yesterday": "I går", + "You need to run migrations to set the system up for operation.": "Du må kjøre migrering for å klargjøre systemet for bruk.", + "Your name": "Ditt navn", + "Zone ID": "Sone-id", + "Zoom In": "Zoom inn", + "Zoom Out": "Zoom Ut" } diff --git a/packages/webui/public/locales/nn/translations.json b/packages/webui/public/locales/nn/translations.json index fee4ac1e7eb..15d2df15174 100644 --- a/packages/webui/public/locales/nn/translations.json +++ b/packages/webui/public/locales/nn/translations.json @@ -1,973 +1,802 @@ { - "Account Page": "Brukarkontoside", - "Name:": "Namn:", - "Email:": "E-post:", - "Old Password": "Gammalt passord", - "New Password": "Nytt passord", - "Save Changes": "Lagre endringer", - "Edit Account": "Endre brukarkonto", - "Organization": "Organisasjon", - "User roles in organization": "Brukarroller i organisasjon", - "Studio": "Studio", - "Configurator": "Configurator", - "Developer": "Developer", - "Admin": "Admin", - "Remove Self": "Fjern denne brukarkontoen", - "Email Address": "E-postadresse", - "Password": "Passord", - "Sign in": "Logg inn", - "Create New Account": "Opprett ny brukarkonto", - "Lost password?": "Gløymd passord?", - "Send reset email": "Send e-post for å nullstille", - "Go back": "Tilbake", - "Password must be atleast 5 characters long": "Passord må vere minst 5 tegn langt", - "Enter your new password": "Skriv inn ditt nye passord", - "Set new password": "Lagre nytt passord", - "Your Account": "Din brukarkonto", - "About Your Organization": "Om din oranisasjon", - "We are mainly": "Vi er hovudsakleg", - "Areas": "Område", - "Invite User": "Inviter brukar", - "New User's Email": "Ny brukar sin e-post", - "New User's Name": "Ny brukar sitt namn", - "Create New User & Send Enrollment Email": "Opprett ny brukar og send e-post for innmelding", - "Users in organization": "Brukarar i organisasjonen", - "Return to list": "Gå tilbake til lista", - "There is no rundown active in this studio.": "Fann ingen aktive køyreplanar for dette studioet.", - "This studio doesn't exist.": "Dette studioet eksisterer ikkje.", - "There are no active rundowns.": "Fann ingen aktive køyreplanar.", - "Evaluation": "Evaluering", - "Please take a minute to fill in this form.": "Ver venleg og fyll ut dette skjemaet.", - "Be aware that while filling out the form keyboard and streamdeck commands will not be executed!": "OBS! Du kan ikkje utføra Sofie-kommandoar medan du skriv evalueringa!", - "Did you have any problems with the broadcast?": "Hadde du nokre problem under sendinga?", - "Please explain the problems you experienced (what happened and when, what should have happened, what could have triggered the problems, etcetera...)": "Ver venleg og forklar kva problem du hadde (kva hende og når hende det, kva skulle skjedd, kva kan ha utløyst problema o.s.b.)", - "Your name": "Namnet ditt", - "Save message": "Lagre melding", - "Save message and Deactivate Rundown": "Send evalueringa og deaktiver køyreplanen", - "No problems": "Ingen problem", - "Something went wrong, but it didn't affect the output": "Noko gjekk gale, men det virka ikkje inn på sendinga", - "Something went wrong, and it affected the output": "Noko gjekk gale, og det virka inn på sendinga", - "Are you sure?": "Er du sikker?", - "Trimming this clip has timed out. It's possible that the story is currently locked for writing in {{nrcsName}} and will eventually be updated. Make sure that the story is not being edited by other users.": "Endring av inn-/utpunkt for dette klippet tek lang tid. Det er mogleg manuset i er låst i {{nrcsName}} og at inn-/utpunkt endrast om litt. Forsikre deg om at manuset ikkje vert redigert av andre brukarar.", - "Trimming this clip has failed due to an error: {{error}}.": "Endring av inn-/utpunkt for dette klippet feila: {{error}}.", - "Trimmed succesfully.": "Endring av inn-/utpunkt var vellukka.", - "Trimming this clip is taking longer than expected. It's possible that the story is locked for writing in {{nrcsName}}.": "Endring av inn-/utpunkt for dette klippet tek meir tid enn forventa. Det er mogleg manuset er låst for redigering i {{nrcsName}}.", - "Trim \"{{name}}\"": "Trim \"{{name}}\"", - "OK": "OK", - "Cancel": "Avbryt", - "Remove in-trimming": "Nullstill innpunkt", - "In": "Inn", - "Remove all trimming": "Nullstill inn- og utpunkt", - "Duration": "Lengde", - "Remove out-trimming": "Nullstill utpunkt", - "Out": "Ut", - "Next": "Neste", - "Test test": "Test test", - "Until next take": "Til neste Take", - "Until next segment": "Til neste segment", - "Until end of segment": "Til slutten av segment", - "Until next rundown": "Til neste køyreplan", - "Until end of showstyle": "Til slutten av showstyle", - "Script is empty": "Manuset er tomt", - "Clip:": "Klipp:", - "Home": "Heim", - "Rundowns": "Køyreplanar", - "Test Tools": "Testverktøy", - "Status": "Status", - "Settings": "Innstillingar", - "Account": "Konto", - "Logout": "Logg ut", - "My name is {{name}}": "Mitt namn er {{name}}", - "Operating Mode": "Styringsmodus", - "Switching operating mode to {{mode}}": "Endrer styringsmodus til {{mode}}", - "Prompter": "Prompter", - "End of script": "Slutt på manus", - "Could not get system status. Please consult system administrator.": "Kan ikkje innhente status for systemet. Kontakt systemadministrator.", - "There are no rundowns ingested into Sofie.": "Det er ikkje send køyreplanar til Sofie.", - "Click on a rundown to control your studio": "Klikk på ein køyreplan for å kontrollere studioet ditt", - "Rundown": "Køyreplan", - "Problems": "Problem", - "Show Style": "Showstyle", - "On Air Start Time": "Sendestart", - "Expected End Time": "Venta sendeslutt", - "Last updated": "Sist oppdatert", - "View Layout": "Vis layout", - "Today": "I dag", - "Yesterday": "I går", - "Tomorrow": "I morgon", - "Last": "Førre", - "Getting Started": "Kom i gong", - "Start with giving this browser configuration permissions by adding this to the URL: ": "Først må du gå i konfigurasjonsmodus ved å leggje dette til i url-en: ", - "Start Here!": "Start her!", - "Then, run the migrations script:": "Køyr deretter migreringsprosedyra:", - "Run Migrations to get set up": "Køyr migreringsprosedyrar for å setje opp", - "Migrations": "Migrering", - "Documentation is available at": "Dokumentasjon er tilgjengelig på", - "Use {{nrcsName}} order": "Nytt rekkefølgje frå {{nrcsName}}", - "Reset Sort Order": "Tilbakestill rekkefølgje", - "Enable configuration mode by adding ?configure=1 to the address bar.": "Aktiver konfigurasjonsmodus ved å legge til ?configure=1 på slutten av nettadressa.", - "You need to run migrations to set the system up for operation.": "Du må køyre migrering for å klargjere systemet for bruk.", - "Sofie Automation": "Sofie", - "version": "versjon", - "System Status": "Systemstatus", - "System has issues which need to be resolved": "Systemet har problemer som må løysast", - "Status Messages:": "Statusmeldingar:", - "{{showStyleVariant}} – {{showStyleBase}}": "{{showStyleVariant}} – {{showStyleBase}}", - "Drag to reorder or move out of playlist": "Dra for å endre rekkefølgje eller flytta ut av speleliste", - "This rundown is currently active": "Denne køyreplanen er allereie aktiv", - "Not set": "Ikkje angjeve", - "This rundown will loop indefinitely": "Denne køyreplanen vil gå i ein uendeleg loop", + "({{time}} ago)": "(for {{time}} sidan)", "({{timecode}})": "({{timecode}})", - "Re-sync rundown data with {{nrcsName}}": "Ikkje synkronisert med MOS/{{nrcsName}}", - "Delete": "Slett", - "Standalone Shelf": "Frittståande skuff", - "Rundown & Shelf": "Køyreplan & skuff", - "Default": "Standard", - "Delete rundown?": "Slette køyreplanen?", - "Are you sure you want to delete the \"{{name}}\" rundown?": "Er du viss på at du vil slette køyreplanen \"{{name}}\"?", - "Please note: This action is irreversible!": "Merk: Denne handlinga kan du ikkje angre!", - "Re-Sync rundown?": "Synkroniser køyreplanen med ENPS på ny?", - "Re-Sync": "Synkroniser", - "Are you sure you want to re-sync the \"{{name}}\" rundown?": "Er du viss på at du vil synkronisere køyreplanen \"{{name}}\" med ENPS?", - "Start time is close": "Oppgitt sendestart er kvart augeblink", - "Yes": "Ja", - "No": "Nei", - "You are in rehearsal mode, the broadcast starts in less than 1 minute. Do you want to reset the rundown and go into On-Air mode?": "Du er i testmodus og sendinga startar om mindre enn eitt minutt. Vil du laste inn køyreplanen på nytt og gjere klar til sending?", - "Hold": "Hold", - "Could not find a Piece that can be disabled.": "Kunne ikkje finne eit element som kan skippes.", - "Failed to execute take": "Kunne ikkje gjennomføre Take", - "The rundown you are trying to execute a take on is inactive, would you like to activate this rundown?": "Du prøve å gjere ein Take i ein inaktiv køyreplan. Vil du aktivere denne køyreplanen?", - "Activate (Rehearsal)": "Aktiver (testmodus)", + "(in: {{time}})": "(om: {{time}})", + "(Optional) A name/identifier of the local network where the share is located": "(Valfri) Eit namn/ein identifikator for det lokale nettverket der den delte mappa er lokalisert", + "(Optional) A name/identifier of the local network where the share is located, leave empty if globally accessible": "(Valfri) Eit namn/ein identifikator for det lokale nettverket der den delte mappa er lokalisert, la vere tom om den er globalt tilgjengeleg", + "(Optional) This could be the name of the computer on which the local folder is on": "(Valfri) Dette kan vere namnet til datamaskinen som den lokale mappa er på", + "(Unknown playlist)": "(Ukjend køyreplanliste)", + "(Unknown rundown)": "(Ukjend køyreplan)", + "{{currentRundownName}} - {{rundownPlaylistName}}": "{{currentRundownName}} - {{rundownPlaylistName}}", + "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)": "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)", + "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago": "for {{days}} dagar, {{hours}} t {{minutes}} min {{seconds}} s sidan", + "{{hours}} h {{minutes}} min {{seconds}} s ago": "for {{hours}} t {{minutes}} min {{seconds}} s sidan", + "{{indexCount}} indexes was removed.": "{{indexCount}} indexer vart fjerna.", + "{{minutes}} min {{seconds}} s ago": "for {{minutes}} min {{seconds}} s sidan", + "{{nrcsName}} Connection": "{{nrcsName}}-tilkopling", + "{{rundownPlaylistName}} (Looping)": "{{rundownPlaylistName}} (Looper)", + "{{seconds}} s ago": "for {{seconds}} s sidan", + "{{showStyleVariant}} – {{showStyleBase}}": "{{showStyleVariant}} – {{showStyleBase}}", + "{{sourceLayer}} doesn't have both audio & video": "{{sourceLayer}} har ikkje lyd og/eller bilete", + "{{sourceLayer}} has {{audioStreams}} audio streams": "{{sourceLayer}} har {{audioStreams}} lydstraumar", + "{{sourceLayer}} has the wrong format: {{format}}": "{{sourceLayer}}-formatet er ikkje støtta: {{format}}", + "{{sourceLayer}} is being ingested": "{{sourceLayer}} vert prosessert", + "{{sourceLayer}} is missing a file path": "{{sourceLayer}} kan ikkje spelast av fordi filnamnet manglar", + "{{sourceLayer}} is not yet ready on the playout system": "{{sourceLayer}} er enno ikkje klar til å spelast ut fra avviklingsserver", + "{{sourceLayer}} is transferring to the playout system": "{{sourceLayer}} overførast til avviklingsserver", + "A Full System Snapshot contains all system settings (studios, showstyles, blueprints, devices, etc.)": "Eit fullt systemsnapshot inneheld alle systeminnstillingar (studio, showstyles, blueprints, einingar o.s.b.)", + "A snapshot of the current Running Order has been created for troubleshooting.": "Eit snapshot av den gjeldande køyreplanen har verte oppretta.", + "A Studio Snapshot contains all system settings related to that studio": "Eit studiosnapshot inneheld alle systeminnstillingar knytt til eit studio", + "Abort": "Avbryt", + "Accessor ID": "Aksessor-id", + "Accessor Type": "Aksessortype", + "Accessors": "Aksessorer", + "Action": "Handling", + "Action Buttons": "Handlingsknappar", + "Action Triggers": "Handlingsutløysarar", "Activate (On-Air)": "Aktiver (gå ON AIR)", - "Failed to activate": "Kunne ikkje aktivere", - "Something went wrong, please contact the system administrator if the problem persists.": "Noko gikk gale, kontakt systemadministrator om problemet held fram.", + "Activate (Rehearsal)": "Aktiver (testmodus)", + "Activate Rundown": "Aktiver køyreplan", + "Active": "Aktiv", + "Ad-Lib": "Adlib", + "Ad-Lib Action": "Adlib-handling", + "Add": "Legg til", + "Add {{filtersTitle}}": "Legg til {{filtersTitle}}", + "Add a playout device to the studio in order to configure the route sets": "For å kunne redigere omkoplingsgrupper, må du leggje til ein playout-eining til studio", + "Add a playout device to the studio in order to edit the layer mappings": "For å kunne redigere lagmappingar, må du leggje til ein playout-eining til studio", + "Add button": "Legg til knapp", + "Add filter": "Legg til filter", + "Add some source layers (e.g. Graphics) for your data to appear in rundowns": "Legg til kjeldelag (til dømes Grafikk) for å vise dine data i køyreplanar", + "AdLib": "Adlib", + "Adlib Rank": "Adlib-rang", + "AdLibs on this layer can be queued": "Adliber på dette laget kan cues", + "All connections working correctly": "Alle tilkoplingar er OK", + "All devices working correctly": "Alle eininger fungerer som dei skal", + "All is well, go get a": "Alt er greitt, gå og finn deg ein", + "All steps": "Alle steg", + "Allow disabling of Pieces": "Tillat deaktivering av element", + "Allow Read access": "Tillat lesing", + "Allow Write access": "Tillat skriving/lagring", "Another Rundown is Already Active!": "Ein annan køyreplan er allereie aktiv!", - "The rundown \"{{rundownName}}\" will need to be deactivated in order to activate this one.\n\nAre you sure you want to activate this one anyway?": "Køyreplanen \"{{rundownName}}\" må deaktiveres for å aktivere denne køyreplanen.\n\nEr du sikker på at du ønsker å aktivere?", - "Activate Anyway (Rehearsal)": "Aktiver uansett (testmodus)", - "Activate Anyway (On-Air)": "Aktiver uansett (gå ON AIR)", - "Do you want to activate this Rundown?": "Vil du aktivere denne køyreplanen?", - "The planned end time has passed, are you sure you want to activate this Rundown?": "Det planlagte sluttidspunktet er passert, er du sikker på at du vil aktivere denne køyreplanen?", + "Answers": "Svar", + "APM Enabled": "AMP aktivert", + "APM Transaction Sample Rate": "Prøvefrekvens for AMP-transaksjonar", + "Append": "Legg til", + "Append or Replace": "Legg til eller erstatt", + "Application credentials": "Brukarnamn/passord (Application Credentials)", + "Application Performance Monitoring": "Overvaking av yting for applikasjonar (AMP)", "Are you sure you want to activate Rehearsal Mode?": "Er du sikker på at du vil gå i testmodus?", - "Are you sure you want to deactivate this Rundown?\n(This will clear the outputs)": "Er du sikker på at du vil deaktivere denne køyreplanen?\n(Dette vil nullstille alle utgangar.)", - "The rundown can not be reset while it is active": "Ein aktivert køyreplan kan ikkje tilbakestillast", - "A snapshot of the current Running Order has been created for troubleshooting.": "Eit snapshot av den gjeldande køyreplanen har verte oppretta.", - "Prepare Studio and Activate (Rehearsal)": "Førebu studio og aktiver testmodus", - "Deactivate": "Deaktiver", - "Take": "Take", - "Reset Rundown": "Tilbakestill køyreplanen", - "Reload {{nrcsName}} Data": "Last inn {{nrcsName}}-data på nytt", - "Store Snapshot": "Lagre snapshot", - "No actions available": "Ingen køyreplanval tilgjengelege i påsynmodus", - "Add ?studio=1 to the URL to enter studio mode": "Leggje til ?admin=1 på slutten av nettadressa for å starte studiomodus", - "Exit": "Lukk", - "Error": "Feil", - "This rundown is now active. Are you sure you want to exit this screen?": "Denne køyreplanen er aktiv. Er du sikker på at du vil avslutte?", - "Invalid AdLib": "Ugyldig adlib", - "Cannot play this AdLib because it is marked as Invalid": "Kan ikkje spele av adlib fordi den er markert som ugyldig", + "Are you sure you want to deactivate this Rundown\n(This will clear the outputs)": [ + "Er du sikker på at du vil deaktivere denne køyreplanen?", + "(Dette vil nullstille alle utgangar.)" + ], + "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?": "Er du sikker på at du vil slette kjeldelaget \"{{sourceLayerId}}\"?", + "Are you sure you want to delete the \"{{name}}\" rundown?": "Er du viss på at du vil slette køyreplanen \"{{name}}\"?", + "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?": "Er du sikker på at du vil slette blueprintet \"{{blueprintId}}\"?", + "Are you sure you want to delete the shelf layout \"{{name}}\"?": "Er du sikker på at du vil slette layouten \"{{name}}\"?", + "Are you sure you want to delete the show style \"{{showStyleId}}\"?": "Er du sikker på at du vil slette showstylen \"{{showStyleId}}\"?", + "Are you sure you want to delete the studio \"{{studioId}}\"?": "Er du sikker på at du vil slette studioet \"{{studioId}}\"?", + "Are you sure you want to delete this AdLib?": "Er du sikker på at du vil slette denne adliben?", + "Are you sure you want to delete this Bucket?": "Er du sikker på at du vil slette denne bøtta?", + "Are you sure you want to delete this device: \"{{deviceId}}\"?": "Er du sikker på at du vil fjerne eininga \"{{deviceId}}\"?", + "Are you sure you want to empty (remove all adlibs inside) this Bucket?": "Er du sikker på at du vil tømme denne bøtta (fjerner alle adliber)?", + "Are you sure you want to force the migration? This will bypass the migration checks, so be sure to verify that the values in the settings are correct!": "Er du sikker på at du vil tvinge migreringa? Dette gjer at du hoppar over migreringskontrollane, så ver sikker på at verdiane oppgitt i innstillingar er korrekte!", + "Are you sure you want to re-sync the \"{{name}}\" rundown?": "Er du viss på at du vil synkronisere køyreplanen \"{{name}}\" med ENPS?", + "Are you sure you want to re-sync the Rundown?\n(If the currently playing Part has been changed, this can affect the output)": [ + "Er du sikker på at du vil gjenopprette synkronisering mot ENPS for denne køyreplanen?", + "(Dette kan virke inn på pågåande sending)" + ], + "Are you sure you want to remove {{type}} \"{{deviceId}}\"?": "Er du sikker på at du vil fjerne eininga {{type}} \"{{deviceId}}\"?", + "Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\nRoute Sets assigned to this group will be reset to no group.": [ + "Er du sikker på at du vil fjerne eksklusivitetsgruppa \"{{eGroupName}}\"?", + "Omkoplingar satt til denne gruppa vil bli resatt til inga gruppe." + ], + "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?": "Er du sikker på at du fil fjerne mappinga for laget \"{{mappingId}}\"?", + "Are you sure you want to remove the device \"{{deviceName}}\" and all of it's sub-devices?": "Er du sikker på at du vil fjerne eninga \"{{deviceName}}\" og alle undereiningane?", + "Are you sure you want to remove the Package Container \"{{containerId}}\"?": "Er du sikker på at du vil fjerne pakkecontaineren \"{{containerId}}\"?", + "Are you sure you want to remove the Package Container Accessor \"{{accessorId}}\"?": "Er du sikker på at du vil fjerne pakkekontainer-aksessoren \"{{accessorId}}\"?", + "Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to \"{{newLayerId}}\"?": "Er du sikker på at du vil fjerne omkoplinga frå \"{{sourceLayerId}}\" til \"{{newLayerId}}\"?", + "Are you sure you want to remove the Route Set \"{{routeId}}\"?": "Er du sikker på at du vil fjerne omkoplingsgruppa \"{{routeId}}\"?", + "Are you sure you want to replace the blueprints with the file \"{{fileName}}\"?": "Er du sikker på at du vil erstatte blueprints frå fila \"{{fileName}}\"?", + "Are you sure you want to reset the database version?\nOnly do this if you plan on running the migration right after.": [ + "Er du sikker på at du vil nullstille databaseversjonen?", + "Berre gjer dette om du har tenkt å køyre ei migrering med ein gong." + ], + "Are you sure you want to restart this device?": "Er du sikker på at du vil starte denne eininga på nytt?", + "Are you sure you want to restart this Sofie Automation Server Core: {{name}}?": "Er du sikker på at du vil starte Sofie Core: {{name}} om att?", + "Are you sure you want to restore the system from the snapshot file \"{{fileName}}\"?": "Er du sikker på at du vil tilbakestille systemet frå denne snapshotfila \"{{fileName}}\"?", + "Are you sure you want to update the blueprints from the file \"{{fileName}}\"?": "Er du sikker på at du vil oppdatere blueprints frå fila \"{{fileName}}\"?", + "Are you sure you want to upload the shelf layout from the file \"{{fileName}}\"?": "Er du sikker på at du vil laste opp layout for skuff frå fila \"{{fileName}}\"?", + "Are you sure?": "Er du sikker?", + "Around 10 minutes ago": "Cirka 10 minutt sidan", + "Assign": "Tilordne", + "Attached Subdevices": "Tilkopla undereiningar", + "Audio Mixing": "Lydmiksing", + "Auto": "Auto", + "Bad": "Feil", + "Base URL": "Base-url", + "Base url to the resource (example: http://myserver/folder)": "Base-url for ressursen (døme: http://minserver/mappe)", + "Baseline needs reload, this studio may not work until reloaded": "Baseline må lastast om att, dette studioet vil kanskje ikkje fungere før baseline er lasta om att", + "Behavior": "Oppførsel", + "Blueprint": "Blueprint", + "Blueprint Configuration": "Blueprintkonfigurasjon", + "Blueprint ID": "Blueprint-id", + "Blueprint Name": "Blueprintnamn", + "Blueprint not set": "Blueprint ikkje valt", + "Blueprint Type": "Blueprinttype", + "Blueprint Version": "Blueprintversjon", + "Blueprints": "Blueprint", + "Blueprints updated successfully.": "Blueprints blei oppdatert.", + "BREAK": "PAUSE", + "Break In": "Pause om", + "Button": "Knapp", + "Button height scale factor": "Høgdeskala for knapp", + "Button width scale factor": "Breiddeskala for knapp", + "Camera": "Kamera", + "Cancel": "Avbryt", + "Cancel currently pressed hotkey": "Avbryt den trykte tasten", "Cannot play this AdLib because it is marked as Floated": "Kan ikkje spele av adlib fordi den er markert som på vent (float)", - "Not queueable": "Kan ikkje setjast i kø", + "Cannot play this AdLib because it is marked as Invalid": "Kan ikkje spele av adlib fordi den er markert som ugyldig", "Cannot play this adlib because source layer is not queueable": "Kan ikkje spele av adlib fordi den ikkje kan setjast i kø på kjeldelaget", - "There are no Playout Gateways connected and attached to this studio. Please contact the system administrator to start the Playout Gateway.": "Dette studioet har ingen tilkopla playout-gatewayar. Kontakt systemadministrator for å starte den.", - "Playout Gateway \"{{playoutDeviceName}}\" is now restarting.": "Playout-gateway \"{{playoutDeviceName}}\" startar om att.", - "Could not restart Playout Gateway \"{{playoutDeviceName}}\".": "Playout-gateway \"{{playoutDeviceName}}\" kunne ikkje startas om att.", - "Restart Playout": "Start Playout-gateway på ny", - "Restart CasparCG Server": "Start CasparCG på nytt", - "Do you want to restart CasparCG Server \"{{device}}\"?": "Er du sikker på at du vil starta CasparCG Server \"{{device}}\" på nytt?", "CasparCG on device \"{{deviceName}}\" restarting...": "CasparCG på \"{{deviceName}}\" startar på nytt...", - "Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}": "Omstart av CasparCG på \"{{deviceName}}\" feila: {{errorMessage}}", - "Cancel currently pressed hotkey": "Avbryt den trykte tasten", "Change to fullscreen mode": "Fullskjermmodus", - "Show Hotkeys": "Vis hurtigtastar", - "Take a Snapshot": "Lagre eit snapshot", - "Restart {{device}}": "Start {{device}} på ny", - "Rundown not found": "Køyreplan ikkje funnen", + "Channel Name": "Kanalnavn", + "Check the console for troubleshooting data from device \"{{deviceName}}\"!": "Sjekk konsollen for feilsøkingsdata frå eninga \"{{deviceName}}\"!", + "Cleanup": "Opprydding", + "Cleanup old data": "Rydd opp i gamle data", + "Cleanup old database indexes": "Rydd opp i gamle databaseindexer", + "Clear {{layerName}}": "Tøm {{layerName}}", + "Clear queued segment": "Fjern cuet tittel", + "Clear Source Layer": "Tøm kjeldelag", + "Click on a rundown to control your studio": "Klikk på ein køyreplan for å kontrollere studioet ditt", + "Click to show available Package Containers": "Klikk for å vise tilgjengelege pakkekntainere", + "Click to show available Show Styles": "Klikk for å vise tilgjengelege showstyles", + "Client IP": "Klient-ip", + "Clips": "Klipp", "Close": "Lukk", - "Rundown for piece \"{{pieceLabel}}\" could not be found.": "Kan ikkje finne øyreplan for \"{{pieceLabel}}\".", - "This rundown has been unpublished from Sofie.": "Denne køyreplanen er ikkje lenger tilgjengeleg i Sofie.", - "Error: The studio of this Rundown was not found.": "Feil: Kan ikkje finne studioet for denne køyreplanen.", - "This playlist is empty": "Denne spelelista er tom", - "Error: The ShowStyle of this Rundown was not found.": "Feil: Kan ikkje finne showstyle for denne køyreplanen.", - "Unknown error": "Ukjend feil", - "Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced or remove the rundown from Sofie. What do you want to do?": "Køyreplanen {{rundownName}} i lista {{playlistName}} manglar i data frå {{nrcsName}}. Du kan anten markere den som ikkje synkronisert og behalde den i Sofie, eller du kan fjerne køyreplanen ifrå Sofie. Kva vil du gjere?", - "(Unknown rundown)": "(Ukjend køyreplan)", - "(Unknown playlist)": "(Ukjend køyreplanliste)", - "Leave Unsynced": "Behald ikkje-synkronisert køyreplan", - "Remove": "Fjern", - "Remove rundown": "Fjern køyreplan", - "Do you really want to remove just the rundown \"{{rundownName}}\" in the playlist {{playlistName}} from Sofie? This cannot be undone!": "Er du sikker på at du vil slette køyreplanen {{rundownName}} i lista {{playlistName}} frå Sofie? Denne handlinga kan du ikkje angre!", - "Loop Start": "Start for loop", - "Loop End": "Slutt for loop", - "(in: {{time}})": "(om: {{time}})", - "({{time}} ago)": "(for {{time}} sidan)", - "Planned Start": "Planlagt start", - "Planned Duration": "Planlagt varigheit", - "Planned End": "Planlagt slutt", - "The rundown \"{{rundownName}}\" is not published or activated in {{nrcsName}}! No data updates will currently come through.": "Køyreplanen \"{{rundownName}}\" er ikkje synkronisert med MOS/{{nrcsName}}! Kontroller at den er satt til MOS Active i ENPS.", - "Re-sync": "Synkroniser med MOS", - "Re-sync Rundown": "Synkroniser køyreplanen med ENPS på nytt", - "Are you sure you want to re-sync the Rundown?\n(If the currently playing Part has been changed, this can affect the output)": "Er du sikker på at du vil gjenopprette synkronisering mot ENPS for denne køyreplanen?\n(Dette kan virke inn på pågåande sending)", - "Restart": "Restart", - "Fixing this problem requires a restart to the host device. Are you sure you want to restart {{device}}?\n(This might affect output)": "Feilretting krever ein omstart av {{device}}. Er du sikker på at du ønsker å starta einingen på nytt?(Dette kan ha innverknad på gjennomføringa av ein igangverande sending)", + "Connect some devices to the playout gateway": "Kople til ein eller fleire einingar til playout-gatewayen", + "Connected": "Tilkopla", + "Connected App Containers": "Tilkopla app-kontainere", + "Connected Workers": "Tilkopla arbeidarar", + "Controls for exposed Route Sets will be displayed to the producer within the Rundown View in the Switchboard.": "Kontroller for eksponerte omkoplingsgrupper vil verte synt for producer i køyreplansvisninga i omkoplingspanelet.", + "Core System settings": "Systeminnstillingar for Core", + "Could not get system status. Please consult system administrator.": "Kan ikkje innhente status for systemet. Kontakt systemadministrator.", + "Could not restart Playout Gateway \"{{playoutDeviceName}}\".": "Playout-gateway \"{{playoutDeviceName}}\" kunne ikkje startas om att.", + "Create new Bucket": "Opprett ny bøtte", + "Created": "Oppretta", + "Cron jobs": "Cron-jobbar", + "Current Part": "Noverande del", + "Current Segment": "Noverande tittel", + "Custom Classes": "Tilpassa klasser", + "Custom Hotkey Labels": "Eigendefinerte etikettar for hurtigtastar", + "Deactivate": "Deaktiver", + "Deactivate Rundown": "Deaktiver køyreplan", + "Default": "Standard", + "Default Layout": "Standardlayout", + "Default shelf height": "Standard høyde for skuff", + "Default State": "Standardtilstand", + "Delete": "Slett", + "Delete layout?": "Slett layout?", + "Delete rundown?": "Slette køyreplanen?", + "Delete this AdLib": "Slett denne adliben", + "Delete this Blueprint?": "Slett dette blueprintet?", + "Delete this Bucket": "Slett denne bøtta", + "Delete this item?": "Slett dette elementet?", + "Delete this output?": "Slett denne utgangen?", + "Delete this Show Style?": "Slett denne showstylen?", + "Delete this Studio?": "Slett dette studioet?", "Device \"{{deviceName}}\" restarting...": "\"{{deviceName}}\" starter på ny...", - "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}": "Kunne ikkje starta \"{{deviceName}}\" om att: {{errorMessage}}", - "There is an unknown problem with the part.": "Det er eit ukjend problem med denne delen.", - "Show issue": "Vis problem", - "There is an unspecified problem with the source.": "Det er eit ikkje-spesifisert problem med kjelden.", - "External message queue has unsent messages.": "Ekstern meldingskø har meldingar som ikkje er sendt.", - "The system configuration has been changed since importing this rundown. It might not run correctly": "Systemoppsettet har verte endra etter at denne køyreplanen vart importert. Køyreplanen kan verte spelt av med feil", - "Unable to check the system configuration for changes": "Kan ikkje kontrollere endringar i systemoppsettet", - "The Studio configuration is missing some required fields:": "Studiooppsettet manglar obligatoriske felt:", - "The Show Style configuration \"{{name}}\" could not be validated": "Showstyleoppsettet \"{{name}}\" kunne ikkje validerast", - "The ShowStyle \"{{name}}\" configuration is missing some required fields:": "Showstyleoppsettet \"{{name}}\" manglar obligatoriske felt:", - "Unable to validate the system configuration": "Systemoppsettet kunne ikkje validerast", "Device {{deviceName}} is disconnected": "{{deviceName}} er fråkopla", - "Warnings": "Åtvaringar", + "Device ID": "Eining-id", + "Device Name": "Einingsnamn", + "Device Type": "Type eining", + "Devices": "Einingar", + "Devices with issues": "Einingar med problem", + "Did you have any problems with the broadcast?": "Hadde du nokre problem under sendinga?", + "Diff": "Skilnad", + "Disable Context Menu": "Skruv av kontekstmeny", + "Disable hints by adding this to the URL:": "Deaktiver hint ved å legge dette til på url-en:", + "Disable next Piece": "Skip neste element", + "Disable the next element": "Skip neste super", + "Disable version check": "Deaktiver versjonsjekk", + "Disabled": "Deaktivert", + "Disconnected": "Fråkopla", + "Display name of the Package Container": "Pakkekontaineren sitt namn som visast i oversikten", + "Display Rank": "Rangering for visning", + "Display Style": "Stil for vising", + "Display Take buttons": "Vis Take-knapp", + "Do you want to activate this Rundown?": "Vil du aktivere denne køyreplanen?", + "Do you want to append these to existing Action Triggers, or do you want to replace them?": "Vil du legge desse til dei noverande handlingsutløysarane, eller vil du erstatta dei?", + "Do you want to restart CasparCG Server \"{{device}}\"?": "Er du sikker på at du vil starta CasparCG Server \"{{device}}\" på nytt?", + "Documentation is available at": "Dokumentasjon er tilgjengelig på", + "Documents to be removed:": "Dokument som vert fjerna:", + "Don't treat the end of the last rundown in a playlist as a break": "Ikkje behandle slutten av den siste køyreplanen i ei speleliste som ei pause", + "Done": "Utført", + "Download Action Triggers": "Last ned handlingsutløysarar", + "Drag to reorder or move out of playlist": "Dra for å endre rekkefølgje eller flytta ut av speleliste", + "Duration": "Lengde", + "Edit in Nora": "Rediger i Nora", + "Edit Support Panel": "Rediger supportpanel", + "Empty": "Tom", + "Empty this Bucket": "Tøm denne bøtta", + "Enable": "Aktivert", + "Enable \"Play from Anywhere\"": "Slå på \"Play from Anywhere\"", + "Enable CasparCG restart job": "Aktiver CasparCG restartjobbar", + "Enable configuration mode by adding ?configure=1 to the address bar.": "Aktiver konfigurasjonsmodus ved å legge til ?configure=1 på slutten av nettadressa.", + "Enable hints by adding this to the URL:": "Aktiver hint ved å legge dette til på url-en:", + "Enable search toolbar": "Aktiver søkeverktøy", + "Enabled": "Aktivert", + "End of script": "Slutt på manus", + "End Words": "Stikkord", + "Error": "Feil", + "Error: The ShowStyle of this Rundown was not found.": "Feil: Kan ikkje finne showstyle for denne køyreplanen.", + "Error: The studio of this Rundown was not found.": "Feil: Kan ikkje finne studioet for denne køyreplanen.", + "Evaluations": "Evalueringar", + "Exclusivity group": "Ekslusivitetgruppe", + "Exclusivity Group ID": "Eksklusivitetgruppe-id", + "Exclusivity Group Name": "Eksklusivitetgruppenamn", + "Exclusivity Groups": "Ekslusivitetgrupper", + "Execute": "Utfør", + "Executes within the currently open Rundown, requires a Client-side trigger.": "Blir utførte innanfor den valde køyreplanen, men treng ein utløysar frå klienten.", + "Execution times": "Køyretider", + "Exit": "Lukk", + "Expected End": "Venta slutt", + "Expected End text": "Tekst for venta slutt", + "Expected End Time": "Venta sendeslutt", + "Expected Start": "Venta slutt", + "Export": "Eksporter", + "Expose as user selectable layout": "Gjer tilgjengeleg som brukarvalgt layout", + "Expose layout as a standalone page": "Gjer layout tilgjengeleg som ei sjølvstendig side", + "External message queue has unsent messages.": "Ekstern meldingskø har meldingar som ikkje er sendt.", + "Failed to activate": "Kunne ikkje aktivere", + "Failed to execute take": "Kunne ikkje gjennomføre Take", + "Failed to reset OAuth credentials: {{errorMessage}}": "Nullstilling av OAuth credentials feila: {{errorMessage}}", + "Failed to restart CasparCG on device: \"{{deviceName}}\": {{errorMessage}}": "Omstart av CasparCG på \"{{deviceName}}\" feila: {{errorMessage}}", + "Failed to restart device: \"{{deviceName}}\": {{errorMessage}}": "Kunne ikkje starta \"{{deviceName}}\" om att: {{errorMessage}}", + "Failed to update blueprints: {{errorMessage}}": "Oppdatering av blueprints feila: {{errorMessage}}", + "Failed to update config: {{errorMessage}}": "Oppdatering av konfigurasjon feila: {{errorMessage}}", + "Failed to upload OAuth credentials: {{errorMessage}}": "Opplasting av OAuth credentials feila: {{errorMessage}}", + "Failed to upload shelf layout: {{errorMessage}}": "Opplasting av layout feila: {{errorMessage}}", + "Fatal": "Kritisk", + "File path to the folder of the local folder": "Sti til lokal mappe", + "Filter disabled": "Filter deaktivert", + "Filter Disabled": "Filter deaktivert", + "Filters": "Filtre", + "Find Trigger...": "Finn utløysar...", + "Fixed duration in Segment header": "Låst lengde i tittelheader", + "Fixing this problem requires a restart to the host device. Are you sure you want to restart {{device}}?\n(This might affect output)": "Feilretting krever ein omstart av {{device}}. Er du sikker på at du ønsker å starta einingen på nytt?(Dette kan ha innverknad på gjennomføringa av ein igangverande sending)", + "Floated AdLib": "Adlib satt på vent", + "Folder path": "Mappesti", + "Folder path to shared folder": "Sti til delt mappe", + "Force": "Tving", + "Force (deactivate others)": "Tving (deaktiver andre)", + "Force Migration": "Tving migrering", + "Force Migration (unsafe)": "Tving migrering (utrygt)", + "Force the Multi-gateway-mode": "Tving multigateway-modus", + "Frame Rate": "Framerate", + "Full System Snapshot": "Fullt systemsnapshot", + "Generic Properties": "Generelle eigenskapar", + "Generic Script": "Generisk manus", + "Getting Started": "Kom i gong", + "Global AdLib": "Globale adliber", + "Global AdLibs": "Globale adliber", + "Go to On Air line": "Gå til OnAir-posisjon", + "Good": "Bra", + "Graphics": "Grafikk", + "GUI": "Brukergrensesnitt", + "Height": "Høgde", + "Help & Support": "Hjelp og brukarstøtte", + "Hide Countdown": "Skjul nedteljing", + "Hide Diff": "Skjul skilnad", + "Hide Diff Label": "Skjul etikett for skilnad", + "Hide duplicated AdLibs": "Skjul dupliserte adliber", + "Hide End Time": "Skjul sendeslutt", + "Hide Expected End timing when a break is next": "Gøym nedteljing til venta slutt når neste punkt er ei pause", + "Hide for dynamically inserted parts": "Skjul for dynamisk innsatte delar", + "Hide Label": "Skjul etikett", + "Hide Panel from view": "Ikkje vis dette panelet", + "Hide Planned End Label": "Skjul etikett for planlagt slutt", + "Hide Planned Start": "Skjul planlagt start", + "Hide Rundown Divider": "Skjul køyreplanskilje", + "Hide rundown divider between rundowns in a playlist": "Skjul skilje mellom køyreplanar i ei speleliste", + "Hold": "Hold", + "Hotkey": "Hurtigtast", + "How many of the transactions to monitor. Set to -1 to log nothing (max performance), 0.5 to log 50% of the transactions, 1 to log all transactions": "Tal på transaksjonar som overvakast. Set verdien til -1 for å ikkje logge noko (maks yting), til 0.5 for å logge halvparten av transaksjonane eller til 1 for å logge alle transaksjonane", + "HTML that will be shown in the Support Panel": "HTML-kode som vert vist i supportpanelet", + "Human-readable name of the layer": "Lesarvenleg lagnamn", + "Icon": "Ikon", + "Icon color": "Ikonfarge", + "Id": "Id", + "ID of the device (corresponds to the device ID in the peripheralDevice settings)": "Eining-id (korresponderer med enhets-id under enhetsinnstillinger)", + "ID of the timeline-layer to map to some output": "Lag-id for tidslinjelaget som skal mappast til ein utgang", + "If set, only one Route Set will be active per exclusivity group": "Berre ei omkoplingsgruppe vere aktiv per eksklusivitetsgruppe når dette er kryssa av for", + "Import": "Import", + "In": "Inn", + "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s": "om {{days}} dagar, {{hours}} h {{minutes}} min {{seconds}} s", + "in {{hours}} h {{minutes}} min {{seconds}} s": "om {{hours}} t {{minutes}} min {{seconds}} s", + "in {{minutes}} min {{seconds}} s": "om {{minutes}} min {{seconds}} s", + "in {{seconds}} s": "om {{seconds}} s", + "Include Clear Source Layer in Ad-Libs": "Ta med \"Tøm kjeldelag\" i adliber", + "Include Global AdLibs": "Inkluder globale adliber", + "Installation name": "Installasjonsnamn", + "Internal ID": "Intern-id", + "Invalid AdLib": "Ugyldig adlib", + "Is a Guest Input": "Er ein gjesteinngang", + "Is a Live Remote Input": "Er ein RM", + "Is collapsed by default": "Er minimert som standard", + "Is flattened": "Er slått saman", + "Is hidden": "Er skjult", + "Is PGM Output": "Er programutgang", + "ISA URLs": "ISA-adresse (url)", "Just now": "No", + "Key": "Key", + "Kill (debug)": "Kill (debug)", + "Label": "Etikett", + "Label contains": "Etikett inneheld", + "Last": "Førre", + "Last {{layerName}}": "Siste {{layerName}}", + "Last modified": "Sist endra", + "Last rundown is not break": "Siste køyreplan er inga pause", + "Last seen": "Sist sett", + "Last update": "Nyeste oppdatering", + "Last updated": "Sist oppdatert", + "Layer ID": "Lag-id", + "Layer Mappings": "Lagmapping", + "Layer Name": "Lagnamn", + "Leave Unsynced": "Behald ikkje-synkronisert køyreplan", "Less than a minute ago": "Under eitt minutt sidan", "Less than five minutes ago": "Under fem minutt sidan", - "Around 10 minutes ago": "Cirka 10 minutt sidan", + "Limit": "Grense", + "Live Speak": "STK", + "Local": "Lokal", + "Local Time": "Lokal tid", + "Logging level": "Loggenivå", + "Lookahead Mode": "Lookahead-modus", + "Loop End": "Slutt for loop", + "Loop Start": "Start for loop", + "Lower Third": "Super", + "Manage Snapshots": "Behandle snapshots", + "Mappings": "Lagmapping", + "Media": "Media", + "Media Preview URL": "Førehandsvisningsadresse (url)", + "Message": "Melding", + "Message Queue": "Kø for meldingar", + "Messages": "Meldingar", + "Method": "Metode", + "Migrate database": "Migrer database", + "Migrations": "Migrering", + "Mini Shelf Layout": "Layouter for miniskuff", + "Minor Warning": "Mindre åtvaring (avvik)", + "More documentation available at:": "Meir dokumentasjon er tilgjengeleg på:", "More than 10 minutes ago": "Over 10 minutt sidan", - "More than 30 minutes ago": "Over 30 minutt sidan", "More than 2 hours ago": "Over 2 timer sidan", + "More than 30 minutes ago": "Over 30 minutt sidan", "More than 5 hours ago": "Over 5 timar sidan", "More than a day ago": "Over ein dag sidan", - "{{nrcsName}} Connection": "{{nrcsName}}-tilkopling", - "Last update": "Nyeste oppdatering", - "Off-line devices": "Fråkoplete einingar", - "Devices with issues": "Einingar med problem", - "All connections working correctly": "Alle tilkoplingar er OK", - "Play-out": "Avspelning", - "All devices working correctly": "Alle eininger fungerer som dei skal", - "Auto": "Auto", - "Expected End": "Venta slutt", - "Next Loop at": "Neste loop starter", - "Diff": "Skilnad", - "Started": "Starta", - "Expected Start": "Venta slutt", - "{{currentRundownName}} - {{rundownPlaylistName}} (Looping)": "{{currentRundownName}} - {{rundownPlaylistName}} (Looper)", - "{{currentRundownName}} - {{rundownPlaylistName}}": "{{currentRundownName}} - {{rundownPlaylistName}}", - "{{rundownPlaylistName}} (Looping)": "{{rundownPlaylistName}} (Looper)", - "Floated AdLib": "Adlib satt på vent", - "Switchboard": "Omkoplingssentral", - "This is not in it's normal setting": "Denne innstillinga er vorte endra frå standardverdien", + "Move Next": "Skip neste", + "Move Next backwards": "Unskip neste", + "Move Next forwards": "Skip neste", + "Move Next to the following segment": "Skip til neste segment", + "Move Next to the previous segment": "Unskip neste segment", + "Move Parts": "Skip del", + "Move Segments": "Skip segment", + "Multi-gateway-mode delay time": "Delaytid for multigateway-modus", + "Multilingual description, editing will overwrite": "Endring vil overskrive fleirspråkleg skildring", + "My name is {{name}}": "Mitt namn er {{name}}", + "Name": "Namn", + "Network Id": "Nettverk-id", + "New Bucket": "Ny bøtte", + "New Filter": "Nytt filter", + "New Layer": "Nytt lag", + "New Layout": "Ny layout", + "New Output": "Ny utgang", + "New Source": "Ny kjelde", + "Next": "Neste", + "Next Break text": "Tekst for neste pause", + "Next Loop at": "Neste loop starter", + "Next Part": "Neste del", + "Next scheduled show": "Neste planlagde sending", + "Next Segment": "Neste tittel", + "No": "Nei", + "No Action Triggers set up.": "Ingen handlingsutløysarar er satt opp.", + "No actions available": "Ingen køyreplanval tilgjengelege i påsynmodus", + "No Ad-Lib matches in the current state of Rundown: \"{{rundownPlaylistName}}\"": "Ingen treff på adliber i noverande tilstand for køyeplanen: \"{{rundownPlaylistName}}\"", + "No matching Action Trigger.": "Fekk ikkje treff blant handlingsutløysar.", + "No matching Rundowns available to be used for preview": "Ingen passande køyreplanar tilgjengelege for førehandvisning", + "No name set": "Namn ikkje definert", + "No output channels set": "Ingen utgangskanal definert", + "No PGM output": "Ingen programutgang", + "No problems": "Ingen problem", + "No source layers set": "Ingen kjeldelag definert", + "No status loaded": "Ingen status lasta", + "None": "Ingen", + "Not Active": "Inaktiv", + "Not Connected": "Ikkje tilkopla", + "Not defined": "Ikkje definert", + "Not Global": "Ikkje globale", + "Not queueable": "Kan ikkje setjast i kø", + "Not set": "Ikkje angjeve", + "Note: Core needs to be restarted to apply these settings": "Merknad: Core må startast om att for å ta i bruk desse innstillingane", "Off": "Av", + "Off-line devices": "Fråkoplete einingar", + "OK": "OK", + "On": "På", + "On Air": "On Air", "On Air At": "On Air klokka", "On Air In": "On Air om", - "Unsynced": "Ikkje synkronisert med MOS", - "Sources": "Kjelder", - "On Air": "On Air", - "Loops to top": "Looper til toppen", - "Show End": "Sendeslutt", - "BREAK": "PAUSE", - "Break In": "Pause om", - "part": "punkt", - "Set segment as Next": "Set tittel som neste: Startar på neste Take", - "Queue segment": "Cue tittel: Startar når aktiv tittel er ferdig", - "Clear queued segment": "Fjern cuet tittel", - "Set this part as Next": "Set dette punktet som neste: Startar på neste Take", - "Set Next Here": "Sett Neste her", - "Play from Here": "Spel av frå her", - "Zoom Out": "Zoom ut", - "Show All": "Vis Alle", - "Zoom In": "Zoom In", - "Parts Duration": "Varigheit for del", - "Unknown": "Ukjend", - "Good": "Bra", - "Minor Warning": "Mindre åtvaring (avvik)", - "Warning": "Åtvaring", - "Bad": "Feil", - "Fatal": "Kritisk", - "Connected": "Tilkopla", - "Disconnected": "Fråkopla", - "MOS Gateway": "MOS-gateway", - "Spreadsheet Gateway": "Spreadsheet-gateway", - "Play-out Gateway": "Playout-gateway", - "Media Manager": "Media Manager", - "Unknown Device": "Ukjend eining", - "Delete this Studio?": "Slett dette studioet?", - "Are you sure you want to delete the studio \"{{studioId}}\"?": "Er du sikker på at du vil slette studioet \"{{studioId}}\"?", - "Delete this Show Style?": "Slett denne showstylen?", - "Are you sure you want to delete the show style \"{{showStyleId}}\"?": "Er du sikker på at du vil slette showstylen \"{{showStyleId}}\"?", - "Delete this Blueprint?": "Slett dette blueprintet?", - "Are you sure you want to delete the blueprint \"{{blueprintId}}\"?": "Er du sikker på at du vil slette blueprintet \"{{blueprintId}}\"?", - "Remove this Device?": "Fjern denne eininga?", - "Are you sure you want to remove the device \"{{deviceName}}\" and all of it's sub-devices?": "Er du sikker på at du vil fjerne eninga \"{{deviceName}}\" og alle undereiningane?", - "Studios": "Studio", - "Unnamed Studio": "Studio utan namn", - "Show Styles": "Showstyle", - "Unnamed Show Style": "Showstyle utan namn", - "Source Layers": "Kjeldelag", - "Output Channels": "Utgangskanalar", - "Blueprints": "Blueprint", - "Unnamed blueprint": "Blueprint utan namn", - "Type": "Type", - "Version": "Versjon", - "Devices": "Einingar", - "Tools": "Verktøy", - "Core System settings": "Systeminnstillingar for Core", - "Upgrade Database": "Oppgrader databasen", - "Manage Snapshots": "Behandle snapshots", - "System Settings": "Systeminstillinger", - "Update Blueprints?": "Oppdater blueprints?", - "Update": "Oppdater", - "Are you sure you want to update the blueprints from the file \"{{fileName}}\"?": "Er du sikker på at du vil oppdatere blueprints frå fila \"{{fileName}}\"?", - "Blueprints updated successfully.": "Blueprints blei oppdatert.", - "Replace Blueprints?": "Erstatte blueprints?", - "Replace": "Erstatt", - "Are you sure you want to replace the blueprints with the file \"{{fileName}}\"?": "Er du sikker på at du vil erstatte blueprints frå fila \"{{fileName}}\"?", - "Failed to update blueprints: {{errorMessage}}": "Oppdatering av blueprints feila: {{errorMessage}}", - "Assigned Show Styles:": "Tilordna showstyles:", - "This Blueprint is not being used by any Show Style": "Dette blueprintet er ikkje i bruk av nokon showstyles", - "Assigned Studios:": "Tilordna studio:", - "This Blueprint is not compatible with any Studio": "Dette blueprintet er ikkje kompatibel med noko studio", - "Unassign": "Fjern tilordning", - "Assign": "Tilordne", - "Blueprint ID": "Blueprint-id", - "Blueprint Name": "Blueprintnamn", - "No name set": "Namn ikkje definert", - "Blueprint Type": "Blueprinttype", - "Upload a new blueprint": "Last opp eit nytt blueprint", - "Last modified": "Sist endra", - "Blueprint Id": "Blueprint-id", - "Blueprint Version": "Blueprintversjon", - "Disable version check": "Deaktiver versjonsjekk", - "Upload Blueprints": "Last opp blueprints", - "OAuth credentials succesfully uploaded.": "Opplasting av OAuth credentials var vellukka.", - "Failed to upload OAuth credentials: {{errorMessage}}": "Opplasting av OAuth credentials feila: {{errorMessage}}", - "OAuth credentials successfuly reset": "OAuth credentials nullstilt.", - "Failed to reset OAuth credentials: {{errorMessage}}": "Nullstilling av OAuth credentials feila: {{errorMessage}}", - "Reset Authentication": "Passord for autentisering", - "Application credentials": "Brukarnamn/passord (Application Credentials)", - "Access token": "Tilgongskode (Access Token)", - "Click on the link below and accept the permissions request.": "Klikk på lenka under og godta permissions-førespurnaden", - "Waiting for gateway to generate URL...": "Ventar på at gateway genererar URL...", - "Only Match Global AdLibs": "Vis kun globale adliber", - "Name": "Namn", - "Display Style": "Stil for vising", - "Show thumbnails next to list items": "Vis miniatyrbilete ved sida av listeelement", - "Button width scale factor": "Breiddeskala for knapp", - "Button height scale factor": "Høgdeskala for knapp", + "On Air Start Time": "Sendestart", + "On release": "På slipp (\"Key up\")", + "OnAir": "OnAir", + "One of these source layers must have a piece for the countdown to segment on-air to be show": "Eit av desse kjeldelaga må ha eit element for at nedteljinga til tittelen er OnAir visas", "Only Display AdLibs from Current Segment": "Vis berre adliber frå gjeldande tittel", - "Include Global AdLibs": "Inkluder globale adliber", - "Filter Disabled": "Filter deaktivert", - "Include Clear Source Layer in Ad-Libs": "Ta med \"Tøm kjeldelag\" i adliber", - "Source Layer Types": "Kjeldelagstypar", - "Filter disabled": "Filter deaktivert", - "Label contains": "Etikett inneheld", - "Tags must contain": "Tagger må innehalde", - "Hide Panel from view": "Ikkje vis dette panelet", - "Show panel as a timeline": "Vis panel som ei tidslinje", - "Enable search toolbar": "Aktiver søkeverktøy", + "Only Global": "Berre globale", + "Only Match Global AdLibs": "Vis kun globale adliber", + "Only Pieces present in rundown are sticky": "Kun element til stades i køyreplanen er sticky", + "Open": "Opne", + "Open shelf by default": "Åpne skuff som standard", + "Operating Mode": "Styringsmodus", + "Optional description of the action": "Valfri skildring av handlinga", + "Original Layer": "Opprinneleg lag", + "Out": "Ut", + "Output channels": "Utgangskanalar", + "Output Channels": "Utgangskanalar", + "Output channels are required for your studio to work": "Utgangskanalar er naudsynte for at studioet ditt skal fungere", + "Output Layer": "Utgangslag", + "Over/Under": "Over/Under", "Overflow horizontally": "Horisontal overflyt", - "Display Take buttons": "Vis Take-knapp", - "Queue all adlibs": "Cue alle adliber", - "Toggle AdLibs on single mouse click": "Veksle mellom adliber med enkelt museklikk", - "Hide duplicated AdLibs": "Skjul dupliserte adliber", + "Package Container ID": "Pakkekontainer-id", + "Package Containers": "Pakkekontainere", + "Package Containers to use for previews": "Pakkekontainere som skal nyttast til førehandsvisingar", + "Package Containers to use for thumbnails": "Pakkekontainere som skal nyttast til miniatyrbilete", + "Package Manager": "Pakkebehandlar", + "Package Manager status": "Status for pakkebehandlar", + "Package Status": "Pakkestatus", + "Packages": "Pakker", + "Parameters": "Parametrar", + "part": "punkt", + "Part": "Del", + "Part Count Down": "Nedteljing for del", + "Part Count Up": "Opptelling for del", + "Parts Duration": "Varigheit for del", + "Parts: {{delta}}": "Delar: {{delta}}", + "Password": "Passord", + "Password for authentication": "Passord for autentisering", + "Peripheral Device is outdated": "Tilkopla eining er utdatert", + "Pick": "Plukk", + "Pick last": "Plukk siste", "Picks the first instance of an adLib per rundown, identified by uniqueness Id": "Velger den første førekomsten av ein adlib i kvar køyreplan, identifisert av unik id", - "URL": "Adresse (url)", - "Display Rank": "Rangering for visning", - "Role": "Rolle", - "Adlib Rank": "Adlib-rang", + "Pieces on this layer are sticky": "Element på dette laget er sticky", + "Pieces on this layer can be cleared": "Element på dette laget kan tømmas", "Place label below panel": "Plasser etikett under panel", - "Disabled": "Deaktivert", - "Show segment name": "Vis tittelen sitt namn", - "Show part title": "Vis delen sin tittel", - "Hide for dynamically inserted parts": "Skjul for dynamisk innsatte delar", - "Planned Start Text": "Tekst for planlagt start", - "Text to show above show start time": "Tekst som blir vist over klokkeslett for sendestart", - "Hide Diff": "Skjul skilnad", - "Hide Planned Start": "Skjul planlagt start", + "Planned Duration": "Planlagt varigheit", + "Planned End": "Planlagt slutt", "Planned End text": "Tekst for planlagt slutt", - "Text to show above show end time": "Tekst som blir vist over klokkeslett for sendeslutt", - "Hide Planned End Label": "Skjul etikett for planlagt slutt", - "Hide Diff Label": "Skjul etikett for skilnad", - "Hide Countdown": "Skjul nedteljing", - "Hide End Time": "Skjul sendeslutt", - "Hide Label": "Skjul etikett", - "Text": "Tekst", - "Show Rundown Name": "Vis køyreplannamn", - "Segment": "Tittel", - "Part": "Del", - "X": "X", - "Y": "Y", - "Width": "Breidde", - "Height": "Høgde", - "Scale": "Skala", - "Custom Classes": "Tilpassa klasser", - "Device ID": "Eining-id", - "Device Type": "Type eining", - "Remove this item?": "Fjern dette elementet?", - "Are you sure you want to remove {{type}} \"{{deviceId}}\"?": "Er du sikker på at du vil fjerne eininga {{type}} \"{{deviceId}}\"?", - "Attached Subdevices": "Tilkopla undereiningar", - "Expected End text": "Tekst for venta slutt", - "Text to show above countdown to end of show": "Tekst som blir vist over nedteljing til venta slutt", - "Hide Expected End timing when a break is next": "Gøym nedteljing til venta slutt når neste punkt er ei pause", - "While there are still breaks coming up in the show, hide the Expected End timers": "Gøym nedteljing til venta slutt medan det framleis er pauser att i sendinga", - "Show next break timing": "Vis tid for neste pause", - "Whether to show countdown to next break": "Om nedteljing til neste pause skal visast", - "Last rundown is not break": "Siste køyreplan er inga pause", - "Don't treat the end of the last rundown in a playlist as a break": "Ikkje behandle slutten av den siste køyreplanen i ei speleliste som ei pause", - "Next Break text": "Tekst for neste pause", - "Text to show above countdown to next break": "Tekst som blir vist over nedteljing til neste pause", - "Expose as user selectable layout": "Gjer tilgjengeleg som brukarvalgt layout", - "Shelf Layout": "Layouter for skuffen", - "Mini Shelf Layout": "Layouter for miniskuff", - "Rundown Header Layout": "Layout for køyreplanen sin topptekst", - "Hide Rundown Divider": "Skjul køyreplanskilje", - "Hide rundown divider between rundowns in a playlist": "Skjul skilje mellom køyreplanar i ei speleliste", - "Show Breaks as Segments": "Vis pauser som titlar", - "Segment countdown requires source layer": "Nedteljing for tittel krev kjeldelag", - "One of these source layers must have a piece for the countdown to segment on-air to be show": "Eit av desse kjeldelaga må ha eit element for at nedteljinga til tittelen er OnAir visas", - "Fixed duration in Segment header": "Låst lengde i tittelheader", - "The segment duration in the segment header always displays the planned duration instead of acting as a counter": "Tittelen si lengde i tittelheaderen vil alltid vise den planlagde lengda i staden for å telje ned", - "Select visible Source Layers": "Vel synlege kjeldelag", - "Select visible Output Groups": "Vel synleg gruppe for utgang", - "Expose layout as a standalone page": "Gjer layout tilgjengeleg som ei sjølvstendig side", - "Open shelf by default": "Åpne skuff som standard", - "Default shelf height": "Standard høyde for skuff", - "Show Buckets": "Vis bøtter", - "Disable Context Menu": "Skruv av kontekstmeny", - "This action has an invalid combination of filters": "Denne handlinga har ein ugydlig kombinasjon av filtre", - "Use Trigger Mode": "Type utløysar", - "Force": "Tving", - "Rehearsal": "Testmodus", - "Undo": "Angre", - "Segments: {{delta}}": "Segment: {{delta}}", - "Parts: {{delta}}": "Delar: {{delta}}", - "Open": "Opne", - "Toggle": "Veklse", - "On": "På", - "Activate Rundown": "Aktiver køyreplan", - "Ad-Lib": "Adlib", - "Deactivate Rundown": "Deaktiver køyreplan", - "Disable next Piece": "Skip neste element", - "Move Next": "Skip neste", + "Planned Start": "Planlagt start", + "Planned Start Text": "Tekst for planlagt start", + "Play-out": "Avspelning", + "Playout devices which uses this package container": "Playout-einingar som nyttar denne pakkekontaineren", + "Playout Gateway \"{{playoutDeviceName}}\" is now restarting.": "Playout-gateway \"{{playoutDeviceName}}\" startar om att.", + "Please check the database related to the warnings above. If neccessary, you can": "Ver vennleg og sjekk databasen tilknytta åtvaringane over. Om det er naudsynt kan du", + "Please note: This action is irreversible!": "Merk: Denne handlinga kan du ikkje angre!", + "Prepare Studio and Activate (Rehearsal)": "Førebu studio og aktiver testmodus", + "Previous work status reasons": "Tidlegare årsakar for jobbsatus", + "Priority": "Prioritet", + "Problems": "Problem", + "Profile name to be used by FileFlow when exporting the clips": "Profilnamn som blir nytta av FileFlow når klippa vert eksportert", + "Prompter": "Prompter", + "Quantel FileFlow Profile name": "Quantel FileFlow profilnavn", + "Quantel FileFlow URL": "Quantel GatewayFileFlow-adresse (url)", + "Quantel gateway URL": "Quantel Gateway-adresse (url)", + "Quantel transformer URL": "Quantel Transformer-adresse (url)", + "Queue all adlibs": "Cue alle adliber", + "Queue segment": "Cue tittel: Startar når aktiv tittel er ferdig", + "Queue this AdLib": "Cue denne adliben", + "Queued Messages": "Meldingar i kø", + "Re-check": "Sjekk om att", + "Re-sync": "Synkroniser med MOS", + "Re-Sync": "Synkroniser", + "Re-sync Rundown": "Synkroniser køyreplanen med ENPS på nytt", + "Re-sync rundown data with {{nrcsName}}": "Ikkje synkronisert med MOS/{{nrcsName}}", + "Re-Sync rundown?": "Synkroniser køyreplanen med ENPS på ny?", + "Ready": "Klar", + "Rehearsal": "Testmodus", + "Reload {{nrcsName}} Data": "Last inn {{nrcsName}}-data på nytt", + "Reload Baseline": "Last inn baseline om att", "Reload NRCS Data": "Last inn MOS-data på nytt", + "Reload statuses": "Last inn status om att", + "Remote Source": "RM", + "Remove": "Fjern", + "Remove all trimming": "Nullstill inn- og utpunkt", + "Remove in-trimming": "Nullstill innpunkt", + "Remove indexes": "Fjern indexer", + "Remove old data": "Fjern gamle data", + "Remove old data from database": "Fjern gamle data frå databasen", + "Remove out-trimming": "Nullstill utpunkt", + "Remove rundown": "Fjern køyreplan", + "Remove this device?": "Fjern denne eininga?", + "Remove this Device?": "Fjern denne eininga?", + "Remove this Exclusivity Group?": "Fjern frå denne eksklusivitetgruppa?", + "Remove this item?": "Fjern dette elementet?", + "Remove this mapping?": "Fjern denne mappinga?", + "Remove this Package Container Accessor?": "Fjern denne pakkekontainer-aksessoren?", + "Remove this Package Container?": "Fjern denne pakkecontaineren?", + "Remove this Route from this Route Set?": "Fjern denne omkoplinga frå denne omkoplingsgruppa?", + "Remove this Route Set?": "Fjern denne omkoplingsgruppa?", + "Rename this AdLib": "Gi denne adliben nytt namn", + "Rename this Bucket": "Gi bøtta nytt namn", + "Replace": "Erstatt", + "Replace Blueprints?": "Erstatte blueprints?", + "Reset All Versions": "Nullstill alle versjonar", + "Reset Database Version": "Nullstill databaseversjon", + "Reset Rundown": "Tilbakestill køyreplanen", + "Reset Sort Order": "Tilbakestill rekkefølgje", + "Resource Id": "Ressurs-id", + "Restart": "Restart", + "Restart {{device}}": "Start {{device}} på ny", + "Restart CasparCG Server": "Start CasparCG på nytt", + "Restart Device": "Start eining på nytt", + "Restart Playout": "Start Playout-gateway på ny", + "Restart this Device?": "Start denne eininga på nytt?", + "Restart this system?": "Start dette Sofie-systemet om att?", + "Restore": "Tilbakestill", + "Restore from Snapshot File": "Tilbakestill frå snapshotfil", + "Restore from Stored Snapshots": "Tilbakestill frå lagra snapshots", + "Restore from this Snapshot file?": "Tilbakestill frå denne snapshotfila?", "Resync with NRCS": "Synkroniser med ENPS", - "Shelf": "Skuff", + "Retry": "Prøv igjen", + "Return to list": "Gå tilbake til lista", + "Reveal in Shelf": "Vis i skuff", + "Rewind segments to start": "Sett segmenta tilbake til start", "Rewind Segments to start": "Sett alle segment attende til start", - "Go to On Air line": "Gå til OnAir-posisjon", + "Role": "Rolle", + "Route Set ID": "Omkoplingsgruppe-id", + "Route Set Name": "Omkoplingsgruppa sitt namn", + "Route Sets": "Omkoplingsgrupper", + "Routes": "Omkoplingar", + "Run automatic migration procedure": "Køyr automatisk migreringsprosedyre", + "Run Migrations to get set up": "Køyr migreringsprosedyrar for å setje opp", + "Rundown": "Køyreplan", + "Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced or remove the rundown from Sofie. What do you want to do?": "Køyreplanen {{rundownName}} i lista {{playlistName}} manglar i data frå {{nrcsName}}. Du kan anten markere den som ikkje synkronisert og behalde den i Sofie, eller du kan fjerne køyreplanen ifrå Sofie. Kva vil du gjere?", + "Rundown & Shelf": "Køyreplan & skuff", + "Rundown for piece \"{{pieceLabel}}\" could not be found.": "Kan ikkje finne øyreplan for \"{{pieceLabel}}\".", + "Rundown Header Layout": "Layout for køyreplanen sin topptekst", + "Rundown not found": "Køyreplan ikkje funnen", + "Rundowns": "Køyreplanar", + "Save Changes": "Lagre endringer", + "Save to Bucket": "Lagre til bøtte", + "Scale": "Skala", + "Script is empty": "Manuset er tomt", + "Search...": "Søk...", + "Segment": "Tittel", + "Segment Count Down": "Nedteljing for tittel", + "Segment Count Up": "Oppteljing for tittel", + "Segment countdown requires source layer": "Nedteljing for tittel krev kjeldelag", + "Segment no longer exists in {{nrcs}}": "Segmentet eksisterer ikkje lenger i {{nrcs}}", + "Segment was hidden in {{nrcs}}": "Tittelen eksisterer ikkje lenger i {{nrcs}}", + "Segments: {{delta}}": "Segment: {{delta}}", + "Select Action": "Vel handling", + "Select Compatible Show Styles": "Vel kompatible showstyles", + "Select visible Output Groups": "Vel synleg gruppe for utgang", + "Select visible Source Layers": "Vel synlege kjeldelag", + "Select which playout devices are using this package container": "Vel kva for nokre playout-einingar som skal nytte denne pakkekontaineren", + "Sent Messages": "Sendte meldingar", + "Server ID": "Server-id", + "Server ID. For sources, this should generally be omitted (or set to 0) so clip-searches are zone-wide. If set, clip-searches are limited to that server.": "Server-id (Må droppast for kjelder, sidan klippsøk skjer i heile sona.)", + "Set segment as Next": "Set tittel som neste: Startar på neste Take", + "Settings": "Innstillingar", + "Shelf": "Skuff", + "Shelf Layout": "Layouter for skuffen", + "Shelf layout uploaded successfully.": "Opplasting av layout for skuff var vellukka.", + "Shortcuts": "Hurtigtastar", + "Show \"Remove snapshots\"-buttons": "Vis \"Fjern snapshots\"-knappar", + "Show All": "Vis Alle", + "Show Breaks as Segments": "Vis pauser som titlar", + "Show End": "Sendeslutt", "Show entire On Air Segment": "Vis heile tittelen som er OnAir", - "Force (deactivate others)": "Tving (deaktiver andre)", - "Move Segments": "Skip segment", - "Move Parts": "Skip del", - "State": "Tilstand", - "Action": "Handling", - "Ad-Lib Action": "Adlib-handling", - "Clear Source Layer": "Tøm kjeldelag", - "Sticky Piece": "Element er sticky", - "Global AdLibs": "Globale adliber", - "Label": "Etikett", - "Limit": "Grense", - "Output Layer": "Utgangslag", - "Pick": "Plukk", - "Pick last": "Plukk siste", + "Show Hotkeys": "Vis hurtigtastar", + "Show issue": "Vis problem", + "Show next break timing": "Vis tid for neste pause", + "Show panel as a timeline": "Vis panel som ei tidslinje", + "Show part title": "Vis delen sin tittel", + "Show Rundown Name": "Vis køyreplannamn", + "Show segment name": "Vis tittelen sitt namn", + "Show Style": "Showstyle", + "Show Style Base Name": "Showstylenamn", + "Show style not set": "Showstyle ikkje satt", + "Show Style Variant": "Showstylevariant", + "Show Styles": "Showstyle", + "Show thumbnails next to list items": "Vis miniatyrbilete ved sida av listeelement", + "Slack Webhook URLs": "Slack Webhook-adresser (url)", + "Snapshot restore failed: {{errorMessage}}": "Tilbakestilling frå snapshot feila: {{errorMessage}}", + "Sofie Automation": "Sofie", + "Sofie Automation Server Core will restart in {{time}}s...": "Sofie Core starter om att om {{time}}s...", + "Sofie Automation Server Core: {{name}}": "Sofie:", + "Something went wrong, and it affected the output": "Noko gjekk gale, og det virka inn på sendinga", + "Something went wrong, but it didn't affect the output": "Noko gjekk gale, men det virka ikkje inn på sendinga", + "Something went wrong, please contact the system administrator if the problem persists.": "Noko gikk gale, kontakt systemadministrator om problemet held fram.", + "Source Abbreviation": "Kjeldeforkorting", "Source Layer": "Kjeldelag", "Source Layer Type": "Kjeldelagstypar", - "Tag": "Tag", - "Not Global": "Ikkje globale", - "Only Global": "Berre globale", - "OnAir": "OnAir", - "Now active rundown": "Aktiv køyreplan nett no", - "View": "Visning", - "Executes within the currently open Rundown, requires a Client-side trigger.": "Blir utførte innanfor den valde køyreplanen, men treng ein utløysar frå klienten.", - "Select Action": "Vel handling", - "No Ad-Lib matches in the current state of Rundown: \"{{rundownPlaylistName}}\"": "Ingen treff på adliber i noverande tilstand for køyeplanen: \"{{rundownPlaylistName}}\"", - "No matching Rundowns available to be used for preview": "Ingen passande køyreplanar tilgjengelege for førehandvisning", - "Multilingual description, editing will overwrite": "Endring vil overskrive fleirspråkleg skildring", - "Optional description of the action": "Valfri skildring av handlinga", - "Triggered Actions uploaded successfully.": "Opplasting av handlingsutløysarar var vellukka.", - "Triggered Actions failed to upload: {{errorMessage}}": "Opplasting av handlingsutløysarar feila: {{errorMessage}}", - "Append or Replace": "Legg til eller erstatt", - "Do you want to append these to existing Action Triggers, or do you want to replace them?": "Vil du legge desse til dei noverande handlingsutløysarane, eller vil du erstatta dei?", - "Append": "Legg til", - "Action Triggers": "Handlingsutløysarar", - "Find Trigger...": "Finn utløysar...", - "No matching Action Trigger.": "Fekk ikkje treff blant handlingsutløysar.", - "No Action Triggers set up.": "Ingen handlingsutløysarar er satt opp.", - "System-wide": "Systemvid", - "Upload stored Action Triggers": "Last opp lagra handlingsutløysarar", - "Download Action Triggers": "Last ned handlingsutløysarar", - "On release": "På slipp (\"Key up\")", - "Empty": "Tom", - "Hotkey": "Hurtigtast", - "Trigger Type": "Type utløysar", - "Failed to update config: {{errorMessage}}": "Oppdatering av konfigurasjon feila: {{errorMessage}}", - "Export": "Eksporter", - "Import": "Import", - "true": "true", - "false": "false", - "{{count}} rows": "{{count}} rader", - "Value": "Verdi", - "Create": "Opprett", - "Add config item": "Legg til konfigurasjonselement", - "Add": "Legg til", - "Item": "Element", - "Delete this item?": "Slett dette elementet?", - "Are you sure you want to delete this config item \"{{configId}}\"?": "Er du sikker på at du vil slette dette konfigurasjonselementet \"{{configId}}\"?", - "Blueprint Configuration": "Blueprintkonfigurasjon", - "More settings specific to this studio can be found here": "Meir spesifikke innstillingar for dette studioet finn du her", - "There was an error: {{error}}": "Det skjedde ein feil: {{error}}", - "Package Manager status": "Status for pakkebehandlar", - "Reload statuses": "Last inn status om att", - "Updated": "Oppdatert", - "Package Manager": "Pakkebehandlar", + "Source Layer Types": "Kjeldelagstypar", + "Source Layers": "Kjeldelag", + "Source Name": "Kjeldenamn", + "Source Type": "Kjeldetype", + "Sources": "Kjelder", + "Split Screen": "Splitt", + "Standalone Shelf": "Frittståande skuff", + "Start Here!": "Start her!", + "Start this AdLib": "Start denne adliben", + "Start time is close": "Oppgitt sendestart er kvart augeblink", + "Start with giving this browser configuration permissions by adding this to the URL: ": "Først må du gå i konfigurasjonsmodus ved å leggje dette til i url-en: ", + "Started": "Starta", + "State": "Tilstand", "Statistics": "Statistikk", - "Times": "Tider", - "Connected Workers": "Tilkopla arbeidarar", - "Work-in-progress": "Pågåande jobbar", - "WorkForce": "Arbeidarstyrke", - "Kill (debug)": "Kill (debug)", - "Connected App Containers": "Tilkopla app-kontainere", - "No status loaded": "Ingen status lasta", - "Peripheral Device is outdated": "Tilkopla eining er utdatert", + "Status": "Status", + "Status Messages:": "Statusmeldingar:", + "Sticky Piece": "Element er sticky", + "Store Snapshot": "Lagre snapshot", + "Studio": "Studio", + "Studio Baseline needs update: ": "Studio baseline treng oppdatering: ", + "Studio Name": "Studionamn", + "Studio Settings": "Studioinnstillingar", + "Studio Snapshot": "Studiosnapshot", + "Studios": "Studio", + "Successfully restored snapshot": "Tilbakestilling frå snapshot var vellukka", + "Successfully stored snapshot": "Tilbakestilling frå snapshot var vellukka", + "Supported Audio Formats": "Støtta lydformat", + "Supported Media Formats": "Støtta medieformat", + "Switchboard": "Omkoplingssentral", + "Switching operating mode to {{mode}}": "Endrer styringsmodus til {{mode}}", + "System": "System", + "System has issues which need to be resolved": "Systemet har problemer som må løysast", + "System Status": "Systemstatus", + "System-wide": "Systemvid", + "System-wide Notification Message": "Lokal systemmelding", + "Tag": "Tag", + "Tags must contain": "Tagger må innehalde", + "Take": "Take", + "Take a Full System Snapshot": "Lagre eit fullt systemsnapshot", + "Take a Snapshot": "Lagre eit snapshot", + "Take a Snapshot for studio \"{{studioName}}\" only": "Lagre eit studiosnapshot utelukkande for \"{{studioName}}\"", + "Technical reason: {{reason}}": "Teknisk årsak: {{reason}}", + "Test test": "Test test", + "Test Tools": "Testverktøy", + "Text": "Tekst", + "Text to show above countdown to end of show": "Tekst som blir vist over nedteljing til venta slutt", + "Text to show above countdown to next break": "Tekst som blir vist over nedteljing til neste pause", + "Text to show above show end time": "Tekst som blir vist over klokkeslett for sendeslutt", + "Text to show above show start time": "Tekst som blir vist over klokkeslett for sendestart", "The config UI is now driven by manifests fed by the device. This device needs updating to provide the configManifest to be configurable": "Brukergrensesnitt for konfigurasjon drivast no av manifest mata frå einingane. Denne einga må oppdaterast for å gjere configManifest konfigurerbart", - "Are you sure you want to restart this device?": "Er du sikker på at du vil starte denne eininga på nytt?", - "Restart this Device?": "Start denne eininga på nytt?", - "Check the console for troubleshooting data from device \"{{deviceName}}\"!": "Sjekk konsollen for feilsøkingsdata frå eninga \"{{deviceName}}\"!", - "There was an error when troubleshooting the device: \"{{deviceName}}\": {{errorMessage}}": "Det hende ein feil under feilsøking av eininga \"{{deviceName}}\": {{errorMessage}}", - "Generic Properties": "Generelle eigenskapar", - "Device Name": "Einingsnamn", - "Restart Device": "Start eining på nytt", - "Troubleshoot": "Feilsøk", - "Reset Database Version": "Nullstill databaseversjon", - "Are you sure you want to reset the database version?\nOnly do this if you plan on running the migration right after.": "Er du sikker på at du vil nullstille databaseversjonen?\nBerre gjer dette om du har tenkt å køyre ei migrering med ein gong.", - "Version for {{name}}: From {{fromVersion}} to {{toVersion}}": "Versjon for {{name}}: Frå {{fromVersion}} til {{toVersion}}", - "Re-check": "Sjekk om att", - "Reset Version to": "Nullstill versjon til", - "Reset All Versions": "Nullstill alle versjonar", - "Migrate database": "Migrer database", - "All steps": "Alle steg", - "The migration consists of several phases, you will get more options after you've this migration": "Migreringa har fleire fasar, du vil få fleire val etter at du har køyrd denne migreringa", + "The following parts no longer exist in {{nrcs}}: {{partNames}}": "Dei følgande delane eksisterer ikkje lenger i {{nrcs}}: {{partNames}}", "The migration can be completed automatically.": "Migreringa kan gjerast ferdig automatisk.", - "Run automatic migration procedure": "Køyr automatisk migreringsprosedyre", - "The migration procedure needs some help from you in order to complete, see below:": "Migreringsprosedyra treng litt hjelp frå deg for å gjere seg ferdig. Sjå under:", - "Double-check Values": "Dobbeltsjekk verdiar", - "Are you sure the values you have entered are correct?": "Er du sikker på at verdiane du har oppgitt er korrekte?", - "Run Migration Procedure": "Køyr migreringsprosedyre", - "Warnings During Migration": "Åtvaringar under migrering", - "Please check the database related to the warnings above. If neccessary, you can": "Ver vennleg og sjekk databasen tilknytta åtvaringane over. Om det er naudsynt kan du", - "Force Migration": "Tving migrering", - "Are you sure you want to force the migration? This will bypass the migration checks, so be sure to verify that the values in the settings are correct!": "Er du sikker på at du vil tvinge migreringa? Dette gjer at du hoppar over migreringskontrollane, så ver sikker på at verdiane oppgitt i innstillingar er korrekte!", - "Force Migration (unsafe)": "Tving migrering (utrygt)", + "The migration consists of several phases, you will get more options after you've this migration": "Migreringa har fleire fasar, du vil få fleire val etter at du har køyrd denne migreringa", "The migration was completed successfully!": "Migreringa var vellukka!", - "All is well, go get a": "Alt er greitt, gå og finn deg ein", - "New Layout": "Ny layout", - "Button": "Knapp", - "New Filter": "Nytt filter", - "Delete layout?": "Slett layout?", - "Are you sure you want to delete the shelf layout \"{{name}}\"?": "Er du sikker på at du vil slette layouten \"{{name}}\"?", - "Action Buttons": "Handlingsknappar", - "Icon": "Ikon", - "Icon color": "Ikonfarge", - "Filters": "Filtre", + "The old data was removed.": "Gamle data vart fjerna.", + "The planned end time has passed, are you sure you want to activate this Rundown?": "Det planlagte sluttidspunktet er passert, er du sikker på at du vil aktivere denne køyreplanen?", + "The progress of all steps": "Framdrift for alle steg", + "The progress of steps required for playout": "Framdrift for steg som er naudsynte for avspeling", + "The rundown \"{{rundownName}}\" is not published or activated in {{nrcsName}}! No data updates will currently come through.": "Køyreplanen \"{{rundownName}}\" er ikkje synkronisert med MOS/{{nrcsName}}! Kontroller at den er satt til MOS Active i ENPS.", + "The rundown can not be reset while it is active": "Ein aktivert køyreplan kan ikkje tilbakestillast", + "The rundown you are trying to execute a take on is inactive, would you like to activate this rundown?": "Du prøve å gjere ein Take i ein inaktiv køyreplan. Vil du aktivere denne køyreplanen?", + "The segment duration in the segment header always displays the planned duration instead of acting as a counter": "Tittelen si lengde i tittelheaderen vil alltid vise den planlagde lengda i staden for å telje ned", + "The system configuration has been changed since importing this rundown. It might not run correctly": "Systemoppsettet har verte endra etter at denne køyreplanen vart importert. Køyreplanen kan verte spelt av med feil", + "The type of device to use for the output": "Einingtype som skal nyttast for utgangen", + "The way this Route Set should behave towards the user": "Måten denne omkoplingsgruppa skal oppføre seg overfor brukaren", + "Then, run the migrations script:": "Køyr deretter migreringsprosedyra:", + "There are no Accessors set up.": "Ingen aksessorer er satt opp.", + "There are no active rundowns.": "Fann ingen aktive køyreplanar.", + "There are no exclusivity groups set up.": "Ingen eksklusivitetsgrupper er satt opp.", "There are no filters set up yet": "Det er ikkje satt opp noko filter enno", - "Default Layout": "Standardlayout", - "Add {{filtersTitle}}": "Legg til {{filtersTitle}}", - "Add filter": "Legg til filter", - "Add button": "Legg til knapp", - "Upload Layout?": "Last opp layout?", - "Upload": "Last opp", - "Are you sure you want to upload the shelf layout from the file \"{{fileName}}\"?": "Er du sikker på at du vil laste opp layout for skuff frå fila \"{{fileName}}\"?", - "Shelf layout uploaded successfully.": "Opplasting av layout for skuff var vellukka.", - "Failed to upload shelf layout: {{errorMessage}}": "Opplasting av layout feila: {{errorMessage}}", - "Show Style Base Name": "Showstylenamn", - "Blueprint": "Blueprint", - "Blueprint not set": "Blueprint ikkje valt", - "Compatible Studios:": "Kompatible studio:", + "There are no Playout Gateways connected and attached to this studio. Please contact the system administrator to start the Playout Gateway.": "Dette studioet har ingen tilkopla playout-gatewayar. Kontakt systemadministrator for å starte den.", + "There are no Route Sets set up.": "Det er ikkje satt opp omkoplingar enno.", + "There are no routes set up yet": "Det er ikkje satt opp omkoplingar enno", + "There are no rundowns ingested into Sofie.": "Det er ikkje send køyreplanar til Sofie.", + "There is an unknown problem with the part.": "Det er eit ukjend problem med denne delen.", + "There is an unspecified problem with the source.": "Det er eit ikkje-spesifisert problem med kjelden.", + "There is no rundown active in this studio.": "Fann ingen aktive køyreplanar for dette studioet.", + "There was an error when troubleshooting the device: \"{{deviceName}}\": {{errorMessage}}": "Det hende ein feil under feilsøking av eininga \"{{deviceName}}\": {{errorMessage}}", + "There was an error: {{error}}": "Det skjedde ein feil: {{error}}", + "This action has an invalid combination of filters": "Denne handlinga har ein ugydlig kombinasjon av filtre", + "This affects how much is logged to the console on the server": "Dette påverkar kor mykje som blir logga til serverkonsollen", + "This Blueprint is not being used by any Show Style": "Dette blueprintet er ikkje i bruk av nokon showstyles", + "This Blueprint is not compatible with any Studio": "Dette blueprintet er ikkje kompatibel med noko studio", + "This is not in it's normal setting": "Denne innstillinga er vorte endra frå standardverdien", + "This name will be shown in the title bar of the window": "Dette namnet vert vist i tittellinja for vindauget", + "This playlist is empty": "Denne spelelista er tom", + "This rundown has been unpublished from Sofie.": "Denne køyreplanen er ikkje lenger tilgjengeleg i Sofie.", + "This rundown is currently active": "Denne køyreplanen er allereie aktiv", + "This rundown is now active. Are you sure you want to exit this screen?": "Denne køyreplanen er aktiv. Er du sikker på at du vil avslutte?", + "This rundown will loop indefinitely": "Denne køyreplanen vil gå i ein uendeleg loop", "This Show Style is not compatible with any Studio": "Denne showstylen er ikkje kompatibelt med noko studio", - "Camera": "Kamera", - "Graphics": "Grafikk", - "Live Speak": "STK", - "Lower Third": "Super", - "Studio Microphone": "Studiomikrofon", - "Remote Source": "RM", - "Generic Script": "Generisk manus", - "Split Screen": "Splitt", - "Clips": "Klipp", - "Metadata": "Metadata", - "Camera Movement": "Kamerarørsle", - "Unknown Layer": "Ukjend lag", - "Audio Mixing": "Lydmiksing", + "This step is required for playout": "Dette steget er naudsynt for avspeling", + "This studio doesn't exist.": "Dette studioet eksisterer ikkje.", + "This will remove {{indexCount}} old indexes, do you want to continue?": "Dette vil fjerne {{indexCount}} gamle indexer. Vil du fortsette?", + "Time since planned end": "Tid sidan planlagt slutt", + "Time to planned end": "Tid til planlagt slutt", + "Timeline": "Tidslinje", + "Times": "Tider", + "Timestamp": "Tidsstempel", + "Today": "I dag", + "Toggle": "Veklse", + "Toggle AdLibs on single mouse click": "Veksle mellom adliber med enkelt museklikk", + "Toggle Shelf": "Skuff", + "Tomorrow": "I morgon", + "Tools": "Verktøy", "Transition": "Effekt", - "Lights": "Lys", - "Local": "Lokal", - "New Source": "Ny kjelde", - "Are you sure you want to delete source layer \"{{sourceLayerId}}\"?": "Er du sikker på at du vil slette kjeldelaget \"{{sourceLayerId}}\"?", - "Source Name": "Kjeldenamn", - "Source Abbreviation": "Kjeldeforkorting", - "Internal ID": "Intern-id", - "Source Type": "Kjeldetype", - "Is a Live Remote Input": "Er ein RM", - "Is a Guest Input": "Er ein gjesteinngang", - "Is hidden": "Er skjult", - "Pieces on this layer can be cleared": "Element på dette laget kan tømmas", - "Pieces on this layer are sticky": "Element på dette laget er sticky", - "Only Pieces present in rundown are sticky": "Kun element til stades i køyreplanen er sticky", - "Allow disabling of Pieces": "Tillat deaktivering av element", - "AdLibs on this layer can be queued": "Adliber på dette laget kan cues", - "Exclusivity group": "Ekslusivitetgruppe", - "Add some source layers (e.g. Graphics) for your data to appear in rundowns": "Legg til kjeldelag (til dømes Grafikk) for å vise dine data i køyreplanar", - "No source layers set": "Ingen kjeldelag definert", - "Delete this output?": "Slett denne utgangen?", - "Are you sure you want to delete source layer \"{{outputId}}\"?": "Er du sikker på at du vil slette kjeldelaget \"{{outputId}}\"?", - "New Output": "Ny utgang", - "Channel Name": "Kanalnavn", - "Is PGM Output": "Er programutgang", - "Is collapsed by default": "Er minimert som standard", - "Is flattened": "Er slått saman", - "Output channels are required for your studio to work": "Utgangskanalar er naudsynte for at studioet ditt skal fungere", - "Output channels": "Utgangskanalar", - "No output channels set": "Ingen utgangskanal definert", - "No PGM output": "Ingen programutgang", - "Key": "Key", - "Custom Hotkey Labels": "Eigendefinerte etikettar for hurtigtastar", - "Remove this Variant?": "Fjern denne varianten?", - "Are you sure you want to remove the variant \"{{showStyleVariantId}}\"?": "Er du sikker på at du vil fjerne denne showstylevarianten \"{{showStyleVariantId}}\"?", + "Triggered Actions failed to upload: {{errorMessage}}": "Opplasting av handlingsutløysarar feila: {{errorMessage}}", + "Triggered Actions uploaded successfully.": "Opplasting av handlingsutløysarar var vellukka.", + "Trim \"{{name}}\"": "Trim \"{{name}}\"", + "Trimming this clip has failed due to an error: {{error}}.": "Endring av inn-/utpunkt for dette klippet feila: {{error}}.", + "Trimming this clip has timed out. It's possible that the story is currently locked for writing in {{nrcsName}} and will eventually be updated. Make sure that the story is not being edited by other users.": "Endring av inn-/utpunkt for dette klippet tek lang tid. Det er mogleg manuset i er låst i {{nrcsName}} og at inn-/utpunkt endrast om litt. Forsikre deg om at manuset ikkje vert redigert av andre brukarar.", + "Trimming this clip is taking longer than expected. It's possible that the story is locked for writing in {{nrcsName}}.": "Endring av inn-/utpunkt for dette klippet tek meir tid enn forventa. Det er mogleg manuset er låst for redigering i {{nrcsName}}.", + "Troubleshoot": "Feilsøk", + "Type": "Type", + "Unable to check the system configuration for changes": "Kan ikkje kontrollere endringar i systemoppsettet", + "Unassign": "Fjern tilordning", + "Undo": "Angre", + "Undo Disable the next element": "Unskip neste super", + "Undo Hold": "Angre hold", + "Unknown": "Ukjend", + "Unknown error": "Ukjend feil", + "Unknown Layer": "Ukjend lag", + "Unknown Package \"{{packageId}}\"": "Ukjend pakke \"{{packageId}}\"", + "Unnamed blueprint": "Blueprint utan namn", + "Unnamed Show Style": "Showstyle utan namn", + "Unnamed Studio": "Studio utan namn", "Unnamed variant": "Variant utan namn", - "Variant Name": "Variantnamn", - "Variants": "Variantar", - "Restore from this Snapshot file?": "Tilbakestill frå denne snapshotfila?", - "Are you sure you want to restore the system from the snapshot file \"{{fileName}}\"?": "Er du sikker på at du vil tilbakestille systemet frå denne snapshotfila \"{{fileName}}\"?", - "Successfully restored snapshot": "Tilbakestilling frå snapshot var vellukka", - "Snapshot restore failed: {{errorMessage}}": "Tilbakestilling frå snapshot feila: {{errorMessage}}", - "Full System Snapshot": "Fullt systemsnapshot", - "A Full System Snapshot contains all system settings (studios, showstyles, blueprints, devices, etc.)": "Eit fullt systemsnapshot inneheld alle systeminnstillingar (studio, showstyles, blueprints, einingar o.s.b.)", - "Take a Full System Snapshot": "Lagre eit fullt systemsnapshot", - "Studio Snapshot": "Studiosnapshot", - "A Studio Snapshot contains all system settings related to that studio": "Eit studiosnapshot inneheld alle systeminnstillingar knytt til eit studio", - "Take a Snapshot for studio \"{{studioName}}\" only": "Lagre eit studiosnapshot utelukkande for \"{{studioName}}\"", - "Restore from Snapshot File": "Tilbakestill frå snapshotfil", + "Until end of rundown": "Til slutten av køyreplanen", + "Until end of segment": "Til slutten av segment", + "Until end of showstyle": "Til slutten av showstyle", + "Until next rundown": "Til neste køyreplan", + "Until next segment": "Til neste segment", + "Until next take": "Til neste Take", + "Update": "Oppdater", + "Update Blueprints?": "Oppdater blueprints?", + "Updated": "Oppdatert", + "Upgrade Database": "Oppgrader databasen", + "Upload": "Last opp", + "Upload a new blueprint": "Last opp eit nytt blueprint", + "Upload Blueprints": "Last opp blueprints", + "Upload Layout?": "Last opp layout?", "Upload Snapshot": "Last opp snapshot", - "Restore from Stored Snapshots": "Tilbakestill frå lagra snapshots", - "Restore": "Tilbakestill", - "Show \"Remove snapshots\"-buttons": "Vis \"Fjern snapshots\"-knappar", - "Remove this device?": "Fjern denne eininga?", - "Are you sure you want to remove device \"{{deviceId}}\"?": "Er du sikker på at du vil fjerne eininga \"{{deviceId}}\"?", - "Devices are needed to control your studio hardware": "Einingar er naudsynte for å kontrollere utstyr i studioet ditt", - "Attached Devices": "Tilkopla eingingar", - "No devices connected": "Ingen einingar tilkopla", - "Playout gateway not connected": "Playout-gateway ikkje tilkopla", - "Remove this mapping?": "Fjern denne mappinga?", - "Are you sure you want to remove mapping for layer \"{{mappingId}}\"?": "Er du sikker på at du fil fjerne mappinga for laget \"{{mappingId}}\"?", - "This layer is now rerouted by an active Route Set: {{routeSets}}": "Dette laget vert omkopla av ei aktiv omkoplingsgruppe: {{routeSets}}", - "Layer ID": "Lag-id", - "ID of the timeline-layer to map to some output": "Lag-id for tidslinjelaget som skal mappast til ein utgang", - "Layer Name": "Lagnamn", - "Human-readable name of the layer": "Lesarvenleg lagnamn", - "The type of device to use for the output": "Einingtype som skal nyttast for utgangen", - "ID of the device (corresponds to the device ID in the peripheralDevice settings)": "Eining-id (korresponderer med enhets-id under enhetsinnstillinger)", - "Lookahead Mode": "Lookahead-modus", - "Lookahead Target Objects (Default = 1)": "Lookahead målobjekter (standard = 1)", - "Lookahead Maximum Search Distance (Default = {{limit}})": "Lookahead maksimum søkelengde (standard = {{limit}})", - "Layer Mappings": "Lagmapping", - "Add a playout device to the studio in order to edit the layer mappings": "For å kunne redigere lagmappingar, må du leggje til ein playout-eining til studio", - "Remove this Exclusivity Group?": "Fjern frå denne eksklusivitetgruppa?", - "Are you sure you want to remove exclusivity group \"{{eGroupName}}\"?\nRoute Sets assigned to this group will be reset to no group.": "Er du sikker på at du vil fjerne eksklusivitetsgruppa \"{{eGroupName}}\"?\nOmkoplingar satt til denne gruppa vil bli resatt til inga gruppe.", - "Remove this Route from this Route Set?": "Fjern denne omkoplinga frå denne omkoplingsgruppa?", - "Are you sure you want to remove the Route from \"{{sourceLayerId}}\" to \"{{newLayerId}}\"?": "Er du sikker på at du vil fjerne omkoplinga frå \"{{sourceLayerId}}\" til \"{{newLayerId}}\"?", - "Remove this Route Set?": "Fjern denne omkoplingsgruppa?", - "Are you sure you want to remove the Route Set \"{{routeId}}\"?": "Er du sikker på at du vil fjerne omkoplingsgruppa \"{{routeId}}\"?", - "Routes": "Omkoplingar", - "There are no routes set up yet": "Det er ikkje satt opp omkoplingar enno", - "Original Layer": "Opprinneleg lag", - "None": "Ingen", - "New Layer": "Nytt lag", - "Source Layer not found": "Kjeldelag ikkje funnen", - "There are no exclusivity groups set up.": "Ingen eksklusivitetsgrupper er satt opp.", - "Exclusivity Group ID": "Eksklusivitetgruppe-id", - "Exclusivity Group Name": "Eksklusivitetgruppenamn", - "Display name of the Exclusivity Group": "Eksklusivitetsgruppa sitt namn som visast i oversikten", - "Active": "Aktiv", - "Not Active": "Inaktiv", - "Not defined": "Ikkje definert", - "There are no Route Sets set up.": "Det er ikkje satt opp omkoplingar enno.", - "Route Set ID": "Omkoplingsgruppe-id", - "Is this Route Set currently active": "Er denne omkoplingsgruppa aktiv no", - "Default State": "Standardtilstand", - "The default state of this Route Set": "Standardtilstand for denne omkoplingsgruppa", - "Route Set Name": "Omkoplingsgruppa sitt namn", - "Display name of the Route Set": "Omkoplingsgruppa sitt namn som visast i oversikten", - "If set, only one Route Set will be active per exclusivity group": "Berre ei omkoplingsgruppe vere aktiv per eksklusivitetsgruppe når dette er kryssa av for", - "Behavior": "Oppførsel", - "The way this Route Set should behave towards the user": "Måten denne omkoplingsgruppa skal oppføre seg overfor brukaren", - "Route Sets": "Omkoplingsgrupper", - "Add a playout device to the studio in order to configure the route sets": "For å kunne redigere omkoplingsgrupper, må du leggje til ein playout-eining til studio", - "Controls for exposed Route Sets will be displayed to the producer within the Rundown View in the Switchboard.": "Kontroller for eksponerte omkoplingsgrupper vil verte synt for producer i køyreplansvisninga i omkoplingspanelet.", - "Exclusivity Groups": "Ekslusivitetgrupper", - "Remove this Package Container?": "Fjern denne pakkecontaineren?", - "Are you sure you want to remove the Package Container \"{{containerId}}\"?": "Er du sikker på at du vil fjerne pakkecontaineren \"{{containerId}}\"?", - "There are no Package Containers set up.": "Det er ikkje satt opp pakkekontainere enno.", - "Package Container ID": "Pakkekontainer-id", - "Display name/label of the Package Container": "Vis namn/merkelapp for pakkekontaineren", - "Playout devices which uses this package container": "Playout-einingar som nyttar denne pakkekontaineren", - "Select playout devices": "Vel playout-eining", - "Select which playout devices are using this package container": "Vel kva for nokre playout-einingar som skal nytte denne pakkekontaineren", - "Accessors": "Aksessorer", - "Remove this Package Container Accessor?": "Fjern denne pakkekontainer-aksessoren?", - "Are you sure you want to remove the Package Container Accessor \"{{accessorId}}\"?": "Er du sikker på at du vil fjerne pakkekontainer-aksessoren \"{{accessorId}}\"?", - "There are no Accessors set up.": "Ingen aksessorer er satt opp.", - "Accessor ID": "Aksessor-id", - "Display name of the Package Container": "Pakkekontaineren sitt namn som visast i oversikten", - "Accessor Type": "Aksessortype", - "Folder path": "Mappesti", - "File path to the folder of the local folder": "Sti til lokal mappe", - "Resource Id": "Ressurs-id", - "(Optional) This could be the name of the computer on which the local folder is on": "(Valfri) Dette kan vere namnet til datamaskinen som den lokale mappa er på", - "Base URL": "Base-url", - "Base url to the resource (example: http://myserver/folder)": "Base-url for ressursen (døme: http://minserver/mappe)", - "Network Id": "Nettverk-id", - "(Optional) A name/identifier of the local network where the share is located, leave empty if globally accessible": "(Valfri) Eit namn/ein identifikator for det lokale nettverket der den delte mappa er lokalisert, la vere tom om den er globalt tilgjengeleg", - "Folder path to shared folder": "Sti til delt mappe", - "UserName": "Brukernamn", - "Username for athuentication": "Brukarnamn for autentisering", - "Password for authentication": "Passord for autentisering", - "(Optional) A name/identifier of the local network where the share is located": "(Valfri) Eit namn/ein identifikator for det lokale nettverket der den delte mappa er lokalisert", - "Quantel gateway URL": "Quantel Gateway-adresse (url)", + "Upload stored Action Triggers": "Last opp lagra handlingsutløysarar", + "URL": "Adresse (url)", + "URL to the Quantel FileFlow Manager": "Adresse til Quantel FileFlow Manager", "URL to the Quantel Gateway": "Start Quantel-gateway om att", - "ISA URLs": "ISA-adresse (url)", - "URLs to the ISAs, in order of importance (comma separated)": "Adresser (url-er) for ISA-ene (kommaseparert i prioritert rekkefølgje)", - "Zone ID": "Sone-id", - "Zone ID (default value: \"default\")": "Sone-id (standardverdi: \"default\")", - "Server ID": "Server-id", - "Server ID. For sources, this should generally be omitted (or set to 0) so clip-searches are zone-wide. If set, clip-searches are limited to that server.": "Server-id (Må droppast for kjelder, sidan klippsøk skjer i heile sona.)", - "Quantel transformer URL": "Quantel Transformer-adresse (url)", "URL to the Quantel HTTP transformer": "Adresse til Quantel HTTP transformer", - "Quantel FileFlow URL": "Quantel GatewayFileFlow-adresse (url)", - "URL to the Quantel FileFlow Manager": "Adresse til Quantel FileFlow Manager", - "Quantel FileFlow Profile name": "Quantel FileFlow profilnavn", - "Profile name to be used by FileFlow when exporting the clips": "Profilnamn som blir nytta av FileFlow når klippa vert eksportert", - "Allow Read access": "Tillat lesing", - "Allow Write access": "Tillat skriving/lagring", - "Studio Settings": "Studioinnstillingar", - "Package Containers to use for previews": "Pakkekontainere som skal nyttast til førehandsvisingar", - "Click to show available Package Containers": "Klikk for å vise tilgjengelege pakkekntainere", - "Package Containers to use for thumbnails": "Pakkekontainere som skal nyttast til miniatyrbilete", - "Package Containers": "Pakkekontainere", - "Studio Baseline needs update: ": "Studio baseline treng oppdatering: ", - "Baseline needs reload, this studio may not work until reloaded": "Baseline må lastast om att, dette studioet vil kanskje ikkje fungere før baseline er lasta om att", - "Reload Baseline": "Last inn baseline om att", - "Studio Name": "Studionamn", - "Select Compatible Show Styles": "Vel kompatible showstyles", - "Show style not set": "Showstyle ikkje satt", - "Click to show available Show Styles": "Klikk for å vise tilgjengelege showstyles", - "Frame Rate": "Framerate", - "Enable \"Play from Anywhere\"": "Slå på \"Play from Anywhere\"", - "Media Preview URL": "Førehandsvisningsadresse (url)", - "Sofie Host URL": "Sofie vertadresse (url)", - "Slack Webhook URLs": "Slack Webhook-adresser (url)", - "Supported Media Formats": "Støtta medieformat", - "Supported Audio Formats": "Støtta lydformat", - "Force the Multi-gateway-mode": "Tving multigateway-modus", - "Multi-gateway-mode delay time": "Delaytid for multigateway-modus", - "Remove indexes": "Fjern indexer", - "This will remove {{indexCount}} old indexes, do you want to continue?": "Dette vil fjerne {{indexCount}} gamle indexer. Vil du fortsette?", - "{{indexCount}} indexes was removed.": "{{indexCount}} indexer vart fjerna.", - "Installation name": "Installasjonsnamn", - "This name will be shown in the title bar of the window": "Dette namnet vert vist i tittellinja for vindauget", - "Logging level": "Loggenivå", - "This affects how much is logged to the console on the server": "Dette påverkar kor mykje som blir logga til serverkonsollen", - "System-wide Notification Message": "Lokal systemmelding", - "Message": "Melding", - "Enabled": "Aktivert", - "Edit Support Panel": "Rediger supportpanel", - "HTML that will be shown in the Support Panel": "HTML-kode som vert vist i supportpanelet", - "Application Performance Monitoring": "Overvaking av yting for applikasjonar (AMP)", - "APM Enabled": "AMP aktivert", - "APM Transaction Sample Rate": "Prøvefrekvens for AMP-transaksjonar", - "How many of the transactions to monitor. Set to -1 to log nothing (max performance), 0.5 to log 50% of the transactions, 1 to log all transactions": "Tal på transaksjonar som overvakast. Set verdien til -1 for å ikkje logge noko (maks yting), til 0.5 for å logge halvparten av transaksjonane eller til 1 for å logge alle transaksjonane", - "Note: Core needs to be restarted to apply these settings": "Merknad: Core må startast om att for å ta i bruk desse innstillingane", - "Enable": "Aktivert", - "Cron jobs": "Cron-jobbar", - "Enable CasparCG restart job": "Aktiver CasparCG restartjobbar", - "Cleanup": "Opprydding", - "Cleanup old database indexes": "Rydd opp i gamle databaseindexer", - "Cleanup old data": "Rydd opp i gamle data", - "Disable CasparCG restart job": "Deaktiver CasparCG restartjobbar", - "Remove old data from database": "Fjern gamle data frå databasen", - "There are {{count}} documents that can be removed, do you want to continue?": "Det er {{count}} dokument som kan fjernast. Vil du fortsette?", - "Documents to be removed:": "Dokument som vert fjerna:", - "Retry": "Prøv igjen", - "Remove old data": "Fjern gamle data", - "The old data was removed.": "Gamle data vart fjerna.", - "Last {{layerName}}": "Siste {{layerName}}", - "Clear {{layerName}}": "Tøm {{layerName}}", - "Search...": "Søk...", - "Are you sure you want to deactivate this Rundown\n(This will clear the outputs)": "Er du sikker på at du vil deaktivere denne køyreplanen?\n(Dette vil nullstille alle utgangar.)", - "Successfully stored snapshot": "Tilbakestilling frå snapshot var vellukka", - "End Words": "Stikkord", - "Global AdLib": "Globale adliber", - "AdLib does not provide any options": "Adlib har ingen val", - "Execute": "Utfør", - "Save to Bucket": "Lagre til bøtte", - "Reveal in Shelf": "Vis i skuff", - "Edit in Nora": "Rediger i Nora", - "Current Part": "Noverande del", - "Next Part": "Neste del", - "Part Count Down": "Nedteljing for del", - "Part Count Up": "Opptelling for del", - "Until end of rundown": "Til slutten av køyreplanen", - "New Bucket": "Ny bøtte", - "Are you sure you want to delete this AdLib?": "Er du sikker på at du vil slette denne adliben?", - "Are you sure you want to delete this Bucket?": "Er du sikker på at du vil slette denne bøtta?", - "Are you sure you want to empty (remove all adlibs inside) this Bucket?": "Er du sikker på at du vil tømme denne bøtta (fjerner alle adliber)?", - "Current Segment": "Noverande tittel", - "Next Segment": "Neste tittel", - "Segment Count Down": "Nedteljing for tittel", - "Segment Count Up": "Oppteljing for tittel", - "Start this AdLib": "Start denne adliben", - "Queue this AdLib": "Cue denne adliben", - "Inspect this AdLib": "Inspiser denne adliben", - "Rename this AdLib": "Gi denne adliben nytt namn", - "Delete this AdLib": "Slett denne adliben", - "Empty this Bucket": "Tøm denne bøtta", - "Rename this Bucket": "Gi bøtta nytt namn", - "Delete this Bucket": "Slett denne bøtta", - "Create new Bucket": "Opprett ny bøtte", - "AdLib": "Adlib", - "Shortcuts": "Hurtigtastar", - "Show Style Variant": "Showstylevariant", - "Local Time": "Lokal tid", - "System": "System", - "Media": "Media", - "Packages": "Pakker", - "Messages": "Meldingar", + "URLs to the ISAs, in order of importance (comma separated)": "Adresser (url-er) for ISA-ene (kommaseparert i prioritert rekkefølgje)", + "Use {{nrcsName}} order": "Nytt rekkefølgje frå {{nrcsName}}", + "Use Trigger Mode": "Type utløysar", + "User Activity Log": "Aktivitetslogg", + "User ID": "Brukar-id", "User Log": "Brukarlogg", - "Evaluations": "Evalueringar", - "Timestamp": "Tidsstempel", "User Name": "Brukernamn", - "Answers": "Svar", - "Message Queue": "Kø for meldingar", - "Queued Messages": "Meldingar i kø", - "Sent Messages": "Sendte meldingar", - "File Copy": "Kopier fil", - "File Delete": "Slett fil", - "Check file size": "Sjekk filstorleik", - "Scan File": "Scan fil", - "Generate Thumbnail": "Generer miniatyrbilete", - "Generate Preview": "Generer førehandsvisning", - "Unknown action: {{action}}": "Ukjent handling", - "Done": "Utført", - "Failed": "Mislukka", - "Working, Media Available": "Arbeider, media er tilgjengeleg", - "Working": "Arbeider", - "Pending": "Venter", - "Blocked": "Blokkert", - "Canceled": "Avbrote", - "Idle": "Inaktiv", - "Skipped": "Hoppa over", - "Step progress: {{progress}}": "Framdrift: {{progress}}", - "Processing": "Prosesserer", - "Unknown: {{status}}": "Ukjend: {{status}}", - "Collapse": "Minimer", - "Details": "Detaljar", - "Abort": "Avbryt", - "Prioritize": "Prioriter", - "Media Transfer Status": "Status for medieoverføringar", - "Abort All": "Avbryt alle", - "Restart All": "Start alle på ny", - "Unknown Package \"{{packageId}}\"": "Ukjend pakke \"{{packageId}}\"", - "Package Status": "Pakkestatus", - "Package container status": "Status for pakkekontainer", - "Id": "Id", - "Work status": "Jobbstatus", - "Restart All jobs": "Start alle jobbar om att", - "Created": "Oppretta", - "Ready": "Klar", - "The progress of steps required for playout": "Framdrift for steg som er naudsynte for avspeling", - "The progress of all steps": "Framdrift for alle steg", - "This step is required for playout": "Dette steget er naudsynt for avspeling", + "Value": "Verdi", + "Variants": "Variantar", + "version": "versjon", + "Version": "Versjon", + "Version for {{name}}: From {{fromVersion}} to {{toVersion}}": "Versjon for {{name}}: Frå {{fromVersion}} til {{toVersion}}", + "View": "Visning", + "View Layout": "Vis layout", + "Waiting for gateway to generate URL...": "Ventar på at gateway genererar URL...", + "Warning": "Åtvaring", + "Warnings": "Åtvaringar", + "Warnings During Migration": "Åtvaringar under migrering", + "Whether to show countdown to next break": "Om nedteljing til neste pause skal visast", + "While there are still breaks coming up in the show, hide the Expected End timers": "Gøym nedteljing til venta slutt medan det framleis er pauser att i sendinga", + "Width": "Breidde", "Work description": "Jobbskildring", + "Work status": "Jobbstatus", "Work status reason": "Årsak for jobbstatus", - "Technical reason: {{reason}}": "Teknisk årsak: {{reason}}", - "Previous work status reasons": "Tidlegare årsakar for jobbsatus", - "Priority": "Prioritet", - "Not Connected": "Ikkje tilkopla", - "Do you want to restart CasparCG Server?": "Er du sikker på at du vil starte CasparCG om att?", - "Restart Quantel Gateway": "Start Quantel-gateway om att", - "Do you want to restart Quantel Gateway?": "Er du sikker på at du vil starte Quantel-gateway om att?", - "Quantel Gateway restarting...": "Quantel-gateway startar om att...", - "Failed to restart Quantel Gateway: {{errorMessage}}": "Kunne ikkje starta Quantel-gateway om att: {{errorMessage}}", - "Format HyperDeck disks": "Formater Hyperdeck-diskar", - "Do you want to format the HyperDeck disks? This is a destructive action and cannot be undone.": "Ynskjer du å formatere Hyperdeck-diskar? Dette kan ikkje gjerast om.", - "Formatting HyperDeck disks on device \"{{deviceName}}\"...": "Formaterer Hyperdeck-diskar på eining \"{{deviceName}}\"...", - "Failed to format HyperDecks on device: \"{{deviceName}}\": {{errorMessage}}": "Kunne ikkje formatere Hyperdecks på eining: \"{{deviceName}}\": {{errorMessage}}", - "Last seen": "Sist sett", - "Connect some devices to the playout gateway": "Kople til ein eller fleire einingar til playout-gatewayen", - "Format disks": "Formater diskar", - "Are you sure you want to delete this device: \"{{deviceId}}\"?": "Er du sikker på at du vil fjerne eininga \"{{deviceId}}\"?", - "Sofie Automation Server Core: {{name}}": "Sofie:", - "Restart this system?": "Start dette Sofie-systemet om att?", - "Are you sure you want to restart this Sofie Automation Server Core: {{name}}?": "Er du sikker på at du vil starte Sofie Core: {{name}} om att?", - "Could not generate restart token!": "Kunne ikkje generere Restart Token!", - "Could not generate restart core: {{err}}": "Kunne ikkje generere Restart Core: {{err}}", - "Sofie Automation Server Core will restart in {{time}}s...": "Sofie Core starter om att om {{time}}s...", - "Execution times": "Køyretider", - "User ID": "Brukar-id", - "Client IP": "Klient-ip", - "Method": "Metode", - "Parameters": "Parametrar", - "GUI": "Brukergrensesnitt", - "User Activity Log": "Aktivitetslogg", - "in {{days}} days, {{hours}} h {{minutes}} min {{seconds}} s": "om {{days}} dagar, {{hours}} h {{minutes}} min {{seconds}} s", - "in {{hours}} h {{minutes}} min {{seconds}} s": "om {{hours}} t {{minutes}} min {{seconds}} s", - "in {{minutes}} min {{seconds}} s": "om {{minutes}} min {{seconds}} s", - "in {{seconds}} s": "om {{seconds}} s", - "{{days}} days, {{hours}} h {{minutes}} min {{seconds}} s ago": "for {{days}} dagar, {{hours}} t {{minutes}} min {{seconds}} s sidan", - "{{hours}} h {{minutes}} min {{seconds}} s ago": "for {{hours}} t {{minutes}} min {{seconds}} s sidan", - "{{minutes}} min {{seconds}} s ago": "for {{minutes}} min {{seconds}} s sidan", - "{{seconds}} s ago": "for {{seconds}} s sidan", - "Next scheduled show": "Neste planlagde sending", - "Help & Support": "Hjelp og brukarstøtte", - "Disable hints by adding this to the URL:": "Deaktiver hint ved å legge dette til på url-en:", - "Enable hints by adding this to the URL:": "Aktiver hint ved å legge dette til på url-en:", - "More documentation available at:": "Meir dokumentasjon er tilgjengeleg på:", - "Timeline": "Tidslinje", - "Mappings": "Lagmapping", - "User Log Player": "Brukarloggspelar", - "Play from here": "Spel av frå her", - "Exectute Single": "Utfør einsleg handling", - "Next Action": "Neste handling", - "Run in": "Køyr i", - "Stop": "Stopp", - "Clip \"{{fileName}}\" can't be played because it doesn't exist on the playout system": "Klippet \"{{fileName}}\" kan ikkje spelast av fordi det ikkje finnast på utspelingssystemet", - "{{sourceLayer}} is not yet ready on the playout system": "{{sourceLayer}} er enno ikkje klar til å spelast ut fra avviklingsserver", - "{{sourceLayer}} is transferring to the playout system": "{{sourceLayer}} overførast til avviklingsserver", - "{{sourceLayer}} is transferring to the playout system and cannot be played yet": "{{sourceLayer}} overførast til avviklingsserver og kan ikkje spelast av enno", - "{{sourceLayer}} doesn't have both audio & video": "{{sourceLayer}} har ikkje lyd og/eller bilete", - "{{sourceLayer}} has the wrong format: {{format}}": "{{sourceLayer}}-formatet er ikkje støtta: {{format}}", - "{{sourceLayer}} has {{audioStreams}} audio streams": "{{sourceLayer}} har {{audioStreams}} lydstraumar", - "Clip starts with {{frames}} {{type}} frames": "Klippet startar med {{frames}} {{type}} ruter", - "This clip ends with {{type}} frames after {{count}} seconds": "Klippet sluttar med {{type}} ruter etter {{count}} sekund", - "{{frames}} {{type}} frames detected within the clip": "{{frames}} {{type}} rute oppdaga inne i klippet", - "{{frames}} {{type}} frames detected in the clip": "{{frames}} {{type}} rute oppdaga inne i klippet", - "black": "svart(e)", - "freeze": "fryst(e)", - "{{sourceLayer}} is missing a file path": "{{sourceLayer}} kan ikkje spelast av fordi filnamnet manglar", - "Clip doesn't have audio & video": "Klippet har ikkje lyd og/eller bilete", - "Clip starts with {{frames}} {{type}} frame": "Klippet startar med {{frames}} {{type}} frame", - "This clip ends with {{type}} frames after {{count}} second": "Klippet sluttar med {{frames}} {{type}} frame", - "{{frames}} {{type}} frame detected within the clip": "{{frames}} {{type}} frame oppdaga inne i klippet", - "{{frames}} {{type}} frame detected in clip": "{{frames}} {{type}} frame oppdaga i klippet", - "{{sourceLayer}} is being ingested": "{{sourceLayer}} vert prosessert", - "Source is missing": "Kjelde manglar", - "Segment no longer exists in {{nrcs}}": "Segmentet eksisterer ikkje lenger i {{nrcs}}", - "Segment was hidden in {{nrcs}}": "Tittelen eksisterer ikkje lenger i {{nrcs}}", - "The following parts no longer exist in {{nrcs}}: {{partNames}}": "Dei følgande delane eksisterer ikkje lenger i {{nrcs}}: {{partNames}}", - "Toggle Shelf": "Skuff", - "Undo Hold": "Angre hold", - "Disable the next element": "Skip neste super", - "Undo Disable the next element": "Unskip neste super", - "Move Next forwards": "Skip neste", - "Move Next to the following segment": "Skip til neste segment", - "Move Next backwards": "Unskip neste", - "Move Next to the previous segment": "Unskip neste segment", - "Rewind segments to start": "Sett segmenta tilbake til start", - "{{count}} rows°°°°°°_°°°°°°plural": "{{count}} rader°°°°°°", - "This layer is now rerouted by an active Route Set: {{routeSets}}°°°°°°_°°°°°°plural": "Dette laget vert omkopla av fleire aktive omkoplingsgrupper: {{routeSets}}°°°°°°", - "There are {{count}} documents that can be removed, do you want to continue?°°°°°°_°°°°°°plural": "Det er {{count}} dokument i {{collections}} som kan fjernast. Vil du fortsette?°°°°°°", - "Clip starts with {{frames}} {{type}} frame°°°°°°_°°°°°°plural": "Klipp startar med {{frames}} {{type}} frame°°°°°°", - "This clip ends with {{type}} frames after {{count}} second°°°°°°_°°°°°°plural": "Klipp sluttar {{frames}} {{type}} frame°°°°°°", - "{{frames}} {{type}} frame detected within the clip°°°°°°_°°°°°°plural": "{{frames}} {{type}} frame oppdaga inne i klippet°°°°°°", - "{{frames}} {{type}} frame detected in clip°°°°°°_°°°°°°plural": "{{frames}} {{type}} frame oppdaga i klippet°°°°°°" + "Work-in-progress": "Pågåande jobbar", + "WorkForce": "Arbeidarstyrke", + "X": "X", + "Y": "Y", + "Yes": "Ja", + "Yesterday": "I går", + "You need to run migrations to set the system up for operation.": "Du må køyre migrering for å klargjere systemet for bruk.", + "Your name": "Namnet ditt", + "Zone ID": "Sone-id", + "Zoom In": "Zoom In", + "Zoom Out": "Zoom ut" } diff --git a/packages/webui/public/locales/sv/translations.json b/packages/webui/public/locales/sv/translations.json index 8f511594103..0967ef424bc 100644 --- a/packages/webui/public/locales/sv/translations.json +++ b/packages/webui/public/locales/sv/translations.json @@ -1,23 +1 @@ -{ - "Return to list": "Återgå till listan", - "Home": "Hem", - "Rundowns": "Körscheman", - "Nyman's Playground": "Tester", - "Status": "Status", - "ID": "ID", - "Created": "Skapad", - "Air Status": "Sändningsstatus", - "On Air": "On Air", - "Unknown": "Okänd", - "Good": "Bra", - "Minor Warning": "Liten varning", - "Warning": "Varning", - "Bad": "Dålig", - "Connected": "Ansluten", - "Disconnected": "Oansluten", - "Unknown Device": "Okänd enhet", - "Type": "Typ", - "Last seen": "Senast sedd", - "System Status": "Systemstatus", - "Name": "Namn" -} +{} diff --git a/packages/webui/src/__mocks__/defaultCollectionObjects.ts b/packages/webui/src/__mocks__/defaultCollectionObjects.ts index 3cbd2d4e910..f41f0368cfd 100644 --- a/packages/webui/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/webui/src/__mocks__/defaultCollectionObjects.ts @@ -1,16 +1,17 @@ -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import type { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { clone } from '@sofie-automation/corelib/dist/lib' import { unprotectString, protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import type { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { IBlueprintPieceType, PieceLifespan } from '@sofie-automation/blueprints-integration' -import { Piece, EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' +import { ShelfButtonSize } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' +import { type Piece, EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { PartId, PartInstanceId, PeripheralDeviceId, @@ -26,7 +27,7 @@ import { StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance' +import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance' export function defaultRundownPlaylist(_id: RundownPlaylistId, studioId: StudioId): DBRundownPlaylist { return { @@ -111,6 +112,7 @@ export function defaultStudio(_id: StudioId): DBStudio { allowPieceDirectPlay: true, enableBuckets: true, enableEvaluationForm: true, + shelfAdlibButtonSize: ShelfButtonSize.LARGE, }), _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), diff --git a/packages/webui/src/__mocks__/helpers/database.ts b/packages/webui/src/__mocks__/helpers/database.ts index 5159f3ec8f2..d220aa8d370 100644 --- a/packages/webui/src/__mocks__/helpers/database.ts +++ b/packages/webui/src/__mocks__/helpers/database.ts @@ -1,24 +1,24 @@ import _ from 'underscore' -import { DBStudio, UIStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import type { DBStudio, UIStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { PieceLifespan, - IOutputLayer, - ISourceLayer, + type IOutputLayer, + type ISourceLayer, SourceLayerType, IBlueprintPieceType, } from '@sofie-automation/blueprints-integration' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { ICoreSystem, SYSTEM_ID } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' -import { literal, getRandomId, Complete, normalizeArray } from '@sofie-automation/corelib/dist/lib' +import type { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import type { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' +import { type ICoreSystem, SYSTEM_ID } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' +import { literal, getRandomId, type Complete, normalizeArray } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { EmptyPieceTimelineObjectsBlob, Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' +import type { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { EmptyPieceTimelineObjectsBlob, type Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import type { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' +import type { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { restartRandomId } from '../random.js' import { MongoMock } from '../mongo.js' import { defaultRundownPlaylist, defaultStudio } from '../defaultCollectionObjects.js' @@ -26,7 +26,7 @@ import { applyAndValidateOverrides, wrapDefaultObject, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { +import type { BlueprintId, RundownId, RundownPlaylistId, @@ -36,7 +36,6 @@ import { import { AdLibPieces, CoreSystem, - Parts, Pieces, RundownBaselineAdLibPieces, RundownPlaylists, @@ -46,6 +45,7 @@ import { ShowStyleVariants, Studios, } from '../../client/collections/index.js' +import { UIParts } from '../../client/ui/Collections.js' export enum LAYER_IDS { SOURCE_CAM0 = 'cam0', @@ -369,7 +369,7 @@ export async function setupDefaultRundown( title: 'Part 0 0', expectedDurationWithTransition: undefined, } - MongoMock.getInnerMockCollection(Parts).insert(part00) + MongoMock.getInnerMockCollection(UIParts).insert(part00) const piece000: Piece = { _id: protectString(rundownId + '_piece000'), @@ -437,7 +437,7 @@ export async function setupDefaultRundown( title: 'Part 0 1', expectedDurationWithTransition: undefined, } - MongoMock.getInnerMockCollection(Parts).insert(part01) + MongoMock.getInnerMockCollection(UIParts).insert(part01) const piece010: Piece = { _id: protectString(rundownId + '_piece010'), @@ -477,7 +477,7 @@ export async function setupDefaultRundown( title: 'Part 1 0', expectedDurationWithTransition: undefined, } - MongoMock.getInnerMockCollection(Parts).insert(part10) + MongoMock.getInnerMockCollection(UIParts).insert(part10) const part11: DBPart = { _id: protectString(rundownId + '_part1_1'), @@ -488,7 +488,7 @@ export async function setupDefaultRundown( title: 'Part 1 1', expectedDurationWithTransition: undefined, } - MongoMock.getInnerMockCollection(Parts).insert(part11) + MongoMock.getInnerMockCollection(UIParts).insert(part11) const part12: DBPart = { _id: protectString(rundownId + '_part1_2'), @@ -499,7 +499,7 @@ export async function setupDefaultRundown( title: 'Part 1 2', expectedDurationWithTransition: undefined, } - MongoMock.getInnerMockCollection(Parts).insert(part12) + MongoMock.getInnerMockCollection(UIParts).insert(part12) const segment2: DBSegment = { _id: protectString(rundownId + '_segment2'), diff --git a/packages/webui/src/__mocks__/mongo.ts b/packages/webui/src/__mocks__/mongo.ts index ef9267a532b..8d8e67919fa 100644 --- a/packages/webui/src/__mocks__/mongo.ts +++ b/packages/webui/src/__mocks__/mongo.ts @@ -1,12 +1,16 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import _ from 'underscore' import { literal, getRandomString } from '@sofie-automation/corelib/dist/lib' -import { ProtectedString, unprotectString, protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { + type ProtectedString, + unprotectString, + protectString, +} from '@sofie-automation/shared-lib/dist/lib/protectedString' import { RandomMock } from './random.js' import { MeteorMock } from './meteor.js' import { Meteor } from 'meteor/meteor' import type { AnyBulkWriteOperation } from 'mongodb' -import { +import type { FindOneOptions, FindOptions, MongoReadOnlyCollection, @@ -20,10 +24,10 @@ import { mongoWhere, mongoFindOptions, mongoModify, - MongoQuery, - MongoModifier, + type MongoQuery, + type MongoModifier, } from '@sofie-automation/corelib/dist/mongo' -import { Mongo } from 'meteor/mongo' +import type { Mongo } from 'meteor/mongo' import { sleep } from '@sofie-automation/shared-lib/dist/lib/lib' import clone from 'fast-clone' diff --git a/packages/webui/src/client/collections/index.ts b/packages/webui/src/client/collections/index.ts index 9ca0a700d6b..ea417b74b83 100644 --- a/packages/webui/src/client/collections/index.ts +++ b/packages/webui/src/client/collections/index.ts @@ -7,39 +7,37 @@ * and will be stronger typed in the future. */ -import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' -import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' -import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' +import type { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' +import type { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' +import type { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' -import { ExpectedPackageWorkStatus } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackageWorkStatuses' -import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' -import { PackageContainerStatusDB } from '@sofie-automation/corelib/dist/dataModel/PackageContainerStatus' -import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' -import { ICoreSystem, SYSTEM_ID } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' -import { Evaluation } from '@sofie-automation/meteor-lib/dist/collections/Evaluations' -import { ExpectedPackageDB } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import type { ExpectedPackageWorkStatus } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackageWorkStatuses' +import type { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' +import type { PackageContainerStatusDB } from '@sofie-automation/corelib/dist/dataModel/PackageContainerStatus' +import type { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' +import { type ICoreSystem, SYSTEM_ID } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' +import type { Evaluation } from '@sofie-automation/meteor-lib/dist/collections/Evaluations' +import type { ExpectedPackageDB } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { createSyncMongoCollection, createSyncReadOnlyMongoCollection } from './lib.js' -import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { RundownLayoutBase } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { SnapshotItem } from '@sofie-automation/meteor-lib/dist/collections/Snapshots' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { TranslationsBundle } from '@sofie-automation/meteor-lib/dist/collections/TranslationsBundles' -import { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' -import { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog' -import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' -import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBNotificationObj } from '@sofie-automation/corelib/dist/dataModel/Notifications' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import type { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' +import type { RundownLayoutBase } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' +import type { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import type { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' +import type { SnapshotItem } from '@sofie-automation/meteor-lib/dist/collections/Snapshots' +import type { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import type { TranslationsBundle } from '@sofie-automation/meteor-lib/dist/collections/TranslationsBundles' +import type { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' +import type { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog' +import type { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' +import type { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' +import type { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' +import type { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { DBNotificationObj } from '@sofie-automation/corelib/dist/dataModel/Notifications' export const AdLibActions = createSyncReadOnlyMongoCollection(CollectionName.AdLibActions) @@ -75,10 +73,6 @@ export const PackageContainerStatuses = createSyncReadOnlyMongoCollection(CollectionName.PartInstances) - -export const Parts = createSyncReadOnlyMongoCollection(CollectionName.Parts) - export const PeripheralDevices = createSyncMongoCollection(CollectionName.PeripheralDevices) export const PieceInstances = createSyncReadOnlyMongoCollection(CollectionName.PieceInstances) diff --git a/packages/webui/src/client/collections/lib.ts b/packages/webui/src/client/collections/lib.ts index 0b0aa7d8ef6..72b6da6fc53 100644 --- a/packages/webui/src/client/collections/lib.ts +++ b/packages/webui/src/client/collections/lib.ts @@ -1,16 +1,19 @@ import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' -import { ProtectedString, protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { type ProtectedString, protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import type { Collection as RawCollection, Db as RawDb } from 'mongodb' -import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' -import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { CustomCollectionName, MeteorPubSubCustomCollections } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { +import type { + CollectionName, + CustomCollectionName as CustomCorelibCollectionName, +} from '@sofie-automation/corelib/dist/dataModel/Collections' +import type { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import type { CustomCollectionName, MeteorPubSubCustomCollections } from '@sofie-automation/meteor-lib/dist/api/pubsub' +import type { PeripheralDevicePubSubCollections, PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' -import { +import type { MongoCollection, MongoReadOnlyCollection, MongoCursor, @@ -19,10 +22,22 @@ import { UpdateOptions, UpsertOptions, } from '@sofie-automation/meteor-lib/dist/collections/lib' -import { CustomCollectionName as CustomCorelibCollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' -import { CorelibPubSubCustomCollections } from '@sofie-automation/corelib/dist/pubsub' +import type { CorelibPubSubCustomCollections } from '@sofie-automation/corelib/dist/pubsub' -export * from '@sofie-automation/meteor-lib/dist/collections/lib' +export type { + FieldNames, + FindOneOptions, + FindOptions, + IndexSpecifier, + MongoCollection, + MongoCursor, + MongoLiveQueryHandle, + MongoReadOnlyCollection, + ObserveCallbacks, + ObserveChangesCallbacks, + UpdateOptions, + UpsertOptions, +} from '@sofie-automation/meteor-lib/dist/collections/lib' export const ClientCollections = new Map | WrappedMongoReadOnlyCollection>() function registerClientCollection( diff --git a/packages/webui/src/client/collections/rundownPlaylistUtil.ts b/packages/webui/src/client/collections/rundownPlaylistUtil.ts index 2618f3b5b01..670c50f0153 100644 --- a/packages/webui/src/client/collections/rundownPlaylistUtil.ts +++ b/packages/webui/src/client/collections/rundownPlaylistUtil.ts @@ -1,12 +1,12 @@ -import { RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Rundown, DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { Rundown, DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' import { sortRundownIDsInPlaylist } from '@sofie-automation/corelib/dist/playout/playlist' import _ from 'underscore' import { Rundowns } from './index.js' -import { FindOptions } from './lib.js' -import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import type { FindOptions } from './lib.js' +import type { MongoQuery } from '@sofie-automation/corelib/dist/mongo' /** * Direct database accessors for the RundownPlaylist diff --git a/packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx b/packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx index 6d18d80b098..909420a11d9 100644 --- a/packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx +++ b/packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx @@ -6,7 +6,7 @@ const GLOBAL_BLUEPRINT_ASSET_CACHE: Record = {} export function BlueprintAssetIcon({ src, className }: { src: string; className?: string }): JSX.Element | null { const url = useMemo(() => { if (src.startsWith('data:')) return new URL(src) - return new URL(createPrivateApiPath('/blueprints/assets/' + src), location.href) + return new URL(createPrivateApiPath('blueprints/assets/' + src), location.href) }, [src]) const [svgAsset, setSvgAsset] = useState(GLOBAL_BLUEPRINT_ASSET_CACHE[url.href] ?? null) diff --git a/packages/webui/src/client/lib/Components/Button.tsx b/packages/webui/src/client/lib/Components/Button.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/webui/src/client/lib/Components/Checkbox.tsx b/packages/webui/src/client/lib/Components/Checkbox.tsx index 29e9aba98d1..b7e13ccc093 100644 --- a/packages/webui/src/client/lib/Components/Checkbox.tsx +++ b/packages/webui/src/client/lib/Components/Checkbox.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react' +import { useCallback } from 'react' import Form from 'react-bootstrap/Form' interface ICheckboxControlProps { diff --git a/packages/webui/src/client/lib/Components/CounterComponents.tsx b/packages/webui/src/client/lib/Components/CounterComponents.tsx index 504969259d2..9ea0d4ff8ea 100644 --- a/packages/webui/src/client/lib/Components/CounterComponents.tsx +++ b/packages/webui/src/client/lib/Components/CounterComponents.tsx @@ -1,6 +1,8 @@ import Moment from 'react-moment' import { RundownUtils } from '../rundown.js' +import { Countdown } from '../../ui/RundownView/RundownHeader/Countdown.js' + interface OverUnderProps { value: number } @@ -23,18 +25,24 @@ export const PlannedEndComponent = (props: OverUnderProps): JSX.Element => { ) } -export const TimeToPlannedEndComponent = (props: OverUnderProps): JSX.Element => { +export const TimeToFromPlannedEndComponent = (props: OverUnderProps): JSX.Element => { + const isOver = props.value > -100 return ( - - {RundownUtils.formatDiffToTimecode(props.value, true, false, true, true, true, undefined, true, true)} - - ) -} - -export const TimeSincePlannedEndComponent = (props: OverUnderProps): JSX.Element => { - return ( - - {RundownUtils.formatDiffToTimecode(props.value, true, false, true, true, true, undefined, true, true)} - + + {`${isOver ? '+' : ''}${RundownUtils.formatDiffToTimecode( + props.value, // restored to match Rem. Dur math (negative value triggers Math.ceil) + false, + false, // disable hardcoded showHours + true, + true, + true, // useSmartHours automatically shows hours if > 0 + '', + true, + true + )}`} + ) } diff --git a/packages/webui/src/client/lib/Components/DropdownInput.tsx b/packages/webui/src/client/lib/Components/DropdownInput.tsx index 36dbfdccd30..94c5bccce21 100644 --- a/packages/webui/src/client/lib/Components/DropdownInput.tsx +++ b/packages/webui/src/client/lib/Components/DropdownInput.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react' +import { useCallback, useMemo } from 'react' import ClassNames from 'classnames' import Form from 'react-bootstrap/esm/Form' diff --git a/packages/webui/src/client/lib/Components/FloatInput.tsx b/packages/webui/src/client/lib/Components/FloatInput.tsx index dc9c8f31c0d..d2be6ef2481 100644 --- a/packages/webui/src/client/lib/Components/FloatInput.tsx +++ b/packages/webui/src/client/lib/Components/FloatInput.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react' +import { useCallback, useState } from 'react' import ClassNames from 'classnames' import Form from 'react-bootstrap/Form' @@ -6,6 +6,7 @@ interface IFloatInputControlProps { classNames?: string modifiedClassName?: string disabled?: boolean + readOnly?: boolean placeholder?: string /** Call handleUpdate on every change, before focus is lost */ @@ -24,6 +25,7 @@ export function FloatInputControl({ modifiedClassName, value, disabled, + readOnly, placeholder, handleUpdate, updateOnKey, @@ -36,6 +38,8 @@ export function FloatInputControl({ const handleChange = useCallback( (event: React.ChangeEvent) => { + if (readOnly) return + const number = parseFloat(event.target.value.replace(',', '.')) setEditingValue(number) @@ -43,10 +47,15 @@ export function FloatInputControl({ handleUpdate(zeroBased ? number - 1 : number) } }, - [handleUpdate, updateOnKey, zeroBased] + [handleUpdate, updateOnKey, zeroBased, readOnly] ) const handleBlur = useCallback( (event: React.FocusEvent) => { + if (readOnly) { + setEditingValue(null) + return + } + const number = parseFloat(event.currentTarget.value.replace(',', '.')) if (!isNaN(number)) { handleUpdate(zeroBased ? number - 1 : number) @@ -54,13 +63,19 @@ export function FloatInputControl({ setEditingValue(null) }, - [handleUpdate, zeroBased] + [handleUpdate, zeroBased, readOnly] + ) + const handleFocus = useCallback( + (event: React.FocusEvent) => { + if (readOnly) return + setEditingValue(parseFloat(event.currentTarget.value.replace(',', '.'))) + }, + [readOnly] ) - const handleFocus = useCallback((event: React.FocusEvent) => { - setEditingValue(parseFloat(event.currentTarget.value.replace(',', '.'))) - }, []) const handleKeyUp = useCallback( (event: React.KeyboardEvent) => { + if (readOnly) return + if (event.key === 'Escape') { setEditingValue(null) } else if (event.key === 'Enter') { @@ -70,7 +85,7 @@ export function FloatInputControl({ } } }, - [handleUpdate, zeroBased] + [handleUpdate, zeroBased, readOnly] ) let showValue: string | number | undefined = editingValue ?? undefined @@ -93,6 +108,7 @@ export function FloatInputControl({ onFocus={handleFocus} onKeyUp={handleKeyUp} disabled={disabled} + readOnly={readOnly} /> ) } diff --git a/packages/webui/src/client/lib/Components/IntInput.tsx b/packages/webui/src/client/lib/Components/IntInput.tsx index c46d4547859..b37ec42f5a8 100644 --- a/packages/webui/src/client/lib/Components/IntInput.tsx +++ b/packages/webui/src/client/lib/Components/IntInput.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react' +import { useCallback, useState } from 'react' import ClassNames from 'classnames' import Form from 'react-bootstrap/Form' @@ -6,6 +6,7 @@ interface IIntInputControlProps { classNames?: string modifiedClassName?: string disabled?: boolean + readOnly?: boolean placeholder?: string /** Call handleUpdate on every change, before focus is lost */ @@ -24,6 +25,7 @@ export function IntInputControl({ modifiedClassName, value, disabled, + readOnly, placeholder, handleUpdate, updateOnKey, @@ -36,6 +38,8 @@ export function IntInputControl({ const handleChange = useCallback( (event: React.ChangeEvent) => { + if (readOnly) return + const number = parseInt(event.target.value, 10) setEditingValue(number) @@ -43,10 +47,12 @@ export function IntInputControl({ handleUpdate(zeroBased ? number - 1 : number) } }, - [handleUpdate, updateOnKey, zeroBased] + [handleUpdate, updateOnKey, zeroBased, readOnly] ) const handleBlur = useCallback( (event: React.FocusEvent) => { + if (readOnly) return + const number = parseInt(event.currentTarget.value, 10) if (!isNaN(number)) { handleUpdate(zeroBased ? number - 1 : number) @@ -54,13 +60,15 @@ export function IntInputControl({ setEditingValue(null) }, - [handleUpdate, zeroBased] + [handleUpdate, zeroBased, readOnly] ) const handleFocus = useCallback((event: React.FocusEvent) => { setEditingValue(parseInt(event.currentTarget.value, 10)) }, []) const handleKeyUp = useCallback( (event: React.KeyboardEvent) => { + if (readOnly) return + if (event.key === 'Escape') { setEditingValue(null) } else if (event.key === 'Enter') { @@ -70,7 +78,7 @@ export function IntInputControl({ } } }, - [handleUpdate, zeroBased] + [handleUpdate, zeroBased, readOnly] ) let showValue: string | number | undefined = editingValue ?? undefined @@ -93,6 +101,7 @@ export function IntInputControl({ onFocus={handleFocus} onKeyUp={handleKeyUp} disabled={disabled} + readOnly={readOnly} /> ) } diff --git a/packages/webui/src/client/lib/Components/JsonTextInput.tsx b/packages/webui/src/client/lib/Components/JsonTextInput.tsx index cc3a0dd1d77..703d1ddf541 100644 --- a/packages/webui/src/client/lib/Components/JsonTextInput.tsx +++ b/packages/webui/src/client/lib/Components/JsonTextInput.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react' +import { useCallback, useState } from 'react' import ClassNames from 'classnames' export function tryParseJson(str: string | undefined): { parsed: object } | undefined { diff --git a/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx b/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx index 59d4c794253..5798964c6a8 100644 --- a/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx +++ b/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx @@ -1,14 +1,14 @@ import { faQuestionCircle, faSync } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { objectPathGet } from '@sofie-automation/corelib/dist/lib' -import React, { useCallback } from 'react' +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { ReadonlyDeep } from 'type-fest' -import { +import type { ReadonlyDeep } from 'type-fest' +import type { OverrideOpHelperForItemContents, WrappedOverridableItemNormal, } from '../../ui/Settings/util/OverrideOpHelper.js' -import { DropdownInputOption, findOptionByValue } from './DropdownInput.js' +import { type DropdownInputOption, findOptionByValue } from './DropdownInput.js' import { hasOpWithPath } from './util.js' import Button from 'react-bootstrap/Button' import classNames from 'classnames' diff --git a/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx b/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx index c2fef23781c..39302c8fbb8 100644 --- a/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx +++ b/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import ClassNames from 'classnames' export function splitValueIntoLines(v: string | undefined): string[] { @@ -20,6 +20,7 @@ interface IMultiLineTextInputControlProps { classNames?: string modifiedClassName?: string disabled?: boolean + readOnly?: boolean placeholder?: string /** Call handleUpdate on every change, before focus is lost */ @@ -33,6 +34,7 @@ export function MultiLineTextInputControl({ modifiedClassName, value, disabled, + readOnly, placeholder, handleUpdate, updateOnKey, @@ -41,20 +43,24 @@ export function MultiLineTextInputControl({ const handleChange = useCallback( (event: React.ChangeEvent) => { + if (readOnly) return + setEditingValue(event.target.value) if (updateOnKey) { handleUpdate(splitValueIntoLines(event.target.value)) } }, - [handleUpdate, updateOnKey] + [handleUpdate, updateOnKey, readOnly] ) const handleBlur = useCallback( (event: React.FocusEvent) => { + if (readOnly) return + handleUpdate(splitValueIntoLines(event.target.value)) setEditingValue(null) }, - [handleUpdate] + [handleUpdate, readOnly] ) const handleFocus = useCallback((event: React.FocusEvent) => { setEditingValue(event.currentTarget.value) @@ -82,6 +88,7 @@ export function MultiLineTextInputControl({ onKeyUp={handleKeyUp} onKeyPress={handleKeyPress} disabled={disabled} + readOnly={readOnly} /> ) } diff --git a/packages/webui/src/client/lib/Components/MultiSelectInput.tsx b/packages/webui/src/client/lib/Components/MultiSelectInput.tsx index fe8cb9a7e22..598d5162283 100644 --- a/packages/webui/src/client/lib/Components/MultiSelectInput.tsx +++ b/packages/webui/src/client/lib/Components/MultiSelectInput.tsx @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react' -import { MultiSelect, MultiSelectEvent, MultiSelectOptions } from '../multiSelect.js' -import { DropdownInputOption } from './DropdownInput.js' +import { MultiSelect, type MultiSelectEvent, type MultiSelectOptions } from '../multiSelect.js' +import type { DropdownInputOption } from './DropdownInput.js' import ClassNames from 'classnames' interface IMultiSelectInputControlProps { diff --git a/packages/webui/src/client/lib/Components/OverUnderChip.scss b/packages/webui/src/client/lib/Components/OverUnderChip.scss new file mode 100644 index 00000000000..33302864636 --- /dev/null +++ b/packages/webui/src/client/lib/Components/OverUnderChip.scss @@ -0,0 +1,49 @@ +@import '../../styles/colorScheme'; + +.over-under-chip { + font-family: Roboto Flex; + display: inline-block; + border-radius: 999px; + white-space: nowrap; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; + color: #000; + + padding: var(--overUnderChipPaddingY, 0.05em) var(--overUnderChipPaddingX, 0.25em); + margin-left: var(--overUnderChipMarginLeft, 0em); + margin-top: var(--overUnderChipMarginTop, 0em); + line-height: var(--overUnderChipLineHeight, 1); + + font-variation-settings: + 'wdth' var(--overUnderChipWdth, 25), + 'wght' var(--overUnderChipWght, 600), + 'slnt' var(--overUnderChipSlnt, 0), + 'GRAD' var(--overUnderChipGrad, 0), + 'opsz' var(--overUnderChipOpsz, 14), + 'XOPQ' var(--overUnderChipXopq, 96), + 'XTRA' var(--overUnderChipXtra, 468), + 'YOPQ' var(--overUnderChipYopq, 79), + 'YTAS' var(--overUnderChipYtas, 750), + 'YTFI' var(--overUnderChipYtfi, 738), + 'YTLC' var(--overUnderChipYtlc, 548), + 'YTDE' var(--overUnderChipYtde, -203), + 'YTUC' var(--overUnderChipYtuc, 712); + + &.over-under-chip--over { + --overUnderChipWght: 700; + background-color: $general-late-color; + } + + &.over-under-chip--under { + --overUnderChipWght: 500; + background-color: #ff0; // Should probably be changed to $general-fast-color; + } +} + +// Optional preset for when the chip is used as a large screen overlay. +.over-under-chip--overlay { + --overUnderChipWght: 700; + --overUnderChipOpsz: 20; + --overUnderChipYopq: 92; + --overUnderChipYtlc: 514; +} diff --git a/packages/webui/src/client/lib/Components/OverUnderChip.tsx b/packages/webui/src/client/lib/Components/OverUnderChip.tsx new file mode 100644 index 00000000000..0f6703b735e --- /dev/null +++ b/packages/webui/src/client/lib/Components/OverUnderChip.tsx @@ -0,0 +1,72 @@ +import type { CSSProperties } from 'react' +import classNames from 'classnames' +import { RundownUtils } from '../rundown.js' +import './OverUnderChip.scss' +import { useTiming } from '../../ui/RundownView/RundownTiming/withTiming.js' +import { getPlaylistTimingDiff } from '../rundownTiming.js' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/src/dataModel/RundownPlaylist/RundownPlaylist.js' + +export type OverUnderChipFormat = 'playlistDiff' | 'timerPostfix' + +type OverUnderChipBaseProps = { + className?: string + style?: CSSProperties + format?: OverUnderChipFormat +} + +type OverUnderChipValueProps = + | { + valueMs: number | undefined + rundownPlaylist?: never + } + | { + valueMs?: never + rundownPlaylist: DBRundownPlaylist + } + +type OverUnderChipInnerProps = OverUnderChipBaseProps & { valueMs: number | undefined } + +/** + * Over/under "chip" display. + * Can either take a direct `valueMs` or a `rundownPlaylist` (requires RundownTiming context). + */ +export function OverUnderChip(props: Readonly): JSX.Element | null { + if ('valueMs' in props) { + return + } else { + return + } +} + +function OverUnderChipFromPlaylist( + props: Readonly +): JSX.Element | null { + const timingDurations = useTiming() + const valueMs = getPlaylistTimingDiff(props.rundownPlaylist, timingDurations) + return +} + +function OverUnderChipInner({ valueMs, format = 'playlistDiff', className, style }: Readonly) { + if (valueMs === undefined) return null + + const isUnder = valueMs <= 0 + const timeStr = (() => { + switch (format) { + case 'timerPostfix': + return RundownUtils.formatDiffToTimecode(Math.abs(valueMs), false, false, true, false, true) + case 'playlistDiff': + default: + return RundownUtils.formatDiffToTimecode(Math.abs(valueMs), false, false, true, true, true) + } + })() + + return ( + + {isUnder ? '−' : '+'} + {timeStr} + + ) +} diff --git a/packages/webui/src/client/lib/Components/PromiseButton.tsx b/packages/webui/src/client/lib/Components/PromiseButton.tsx index 3dee66ad8e3..4b1feda16f9 100644 --- a/packages/webui/src/client/lib/Components/PromiseButton.tsx +++ b/packages/webui/src/client/lib/Components/PromiseButton.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import ClassNames from 'classnames' import { logger } from '../logging.js' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' diff --git a/packages/webui/src/client/lib/Components/TextInput.tsx b/packages/webui/src/client/lib/Components/TextInput.tsx index 67fd00be506..bffcb19a9a7 100644 --- a/packages/webui/src/client/lib/Components/TextInput.tsx +++ b/packages/webui/src/client/lib/Components/TextInput.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import ClassNames from 'classnames' -import { DropdownInputOption } from './DropdownInput.js' +import type { DropdownInputOption } from './DropdownInput.js' import { getRandomString } from '@sofie-automation/corelib/dist/lib' import Form from 'react-bootstrap/Form' @@ -13,6 +13,7 @@ interface ITextInputControlProps { classNames?: string modifiedClassName?: string disabled?: boolean + readOnly?: boolean placeholder?: string spellCheck?: boolean @@ -29,6 +30,7 @@ export function TextInputControl({ modifiedClassName, value, disabled, + readOnly, placeholder, spellCheck, suggestions, @@ -39,16 +41,20 @@ export function TextInputControl({ const handleChange = useCallback( (event: React.ChangeEvent) => { + if (readOnly) return + setEditingValue(event.target.value) if (updateOnKey) { handleUpdate(event.target.value) } }, - [handleUpdate, updateOnKey] + [handleUpdate, updateOnKey, readOnly] ) const handleBlur = useCallback( (event: React.FocusEvent) => { + if (readOnly) return + let value: string = event.target.value if (value) { value = value.trim() @@ -57,20 +63,22 @@ export function TextInputControl({ setEditingValue(null) }, - [handleUpdate] + [handleUpdate, readOnly] ) const handleFocus = useCallback((event: React.FocusEvent) => { setEditingValue(event.currentTarget.value) }, []) const handleKeyUp = useCallback( (event: React.KeyboardEvent) => { + if (readOnly) return + if (event.key === 'Escape') { setEditingValue(null) } else if (event.key === 'Enter') { handleUpdate(event.currentTarget.value) } }, - [handleUpdate] + [handleUpdate, readOnly] ) const fieldId = useMemo(() => getRandomString(), []) @@ -85,6 +93,7 @@ export function TextInputControl({ onBlur={handleBlur} onFocus={handleFocus} onKeyUp={handleKeyUp} + readOnly={readOnly} disabled={disabled} spellCheck={spellCheck} list={suggestions ? fieldId : undefined} diff --git a/packages/webui/src/client/lib/Components/TimeMsInput.tsx b/packages/webui/src/client/lib/Components/TimeMsInput.tsx new file mode 100644 index 00000000000..8ccb3876246 --- /dev/null +++ b/packages/webui/src/client/lib/Components/TimeMsInput.tsx @@ -0,0 +1,187 @@ +import { useCallback, useState } from 'react' +import ClassNames from 'classnames' +import Form from 'react-bootstrap/Form' + +interface ITimeMsInputControlProps { + classNames?: string + modifiedClassName?: string + disabled?: boolean + readOnly?: boolean + placeholder?: string + + /** Call handleUpdate on every change, before focus is lost */ + updateOnKey?: boolean + + value: number | undefined + handleUpdate: (value: number) => void + + min?: number + max?: number + multipleOf?: number +} + +const ALLOWED_KEYS = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '.', + ':', + ',', + 'Backspace', + 'Tab', + 'Enter', + 'Escape', + 'ArrowLeft', + 'ArrowRight', + 'Home', + 'End', +] + +function formatTime(time: number, opts?: { showZeroHours?: boolean; showZeroFrames?: boolean }): string { + const frames = time % 1000 + const ms = String(frames).padStart(3, '0') + const ss = String(Math.floor(time / 1000) % 60).padStart(2, '0') + const mm = String(Math.floor(time / 60000) % 60).padStart(2, '0') + const hours = Math.floor(time / 3600000) + + let result = `${mm}:${ss}` + if (frames > 0 || opts?.showZeroFrames) { + result += `.${ms}` + } + if (hours > 0 || opts?.showZeroHours) { + const hh = String(hours).padStart(2, '0') + result = `${hh}:${result}` + } + + return result +} + +function parseTime(time: string): number { + const parts = time.split(':').map((part) => part.trim()) + const partsCount = parts.length + if (partsCount > 3) return Number.NaN + + let ms = 0 + for (let i = 0; i < partsCount; i++) { + const part = parts[partsCount - 1 - i] + const number = parseInt(part, 10) + if (i === 0 && part.includes('.')) { + const number = parseFloat(part) + if (isNaN(number) || number < 0) return Number.NaN + ms += number * 1000 + } else if (isNaN(number) || number < 0) return Number.NaN + else if (i === 0 && partsCount) ms += number * 1000 + else if (i === 1) ms += number * 60000 + else if (i === 2) ms += number * 3600000 + } + + return ms +} + +export function TimeMsInputControl({ + classNames, + modifiedClassName, + value, + disabled, + readOnly, + placeholder, + handleUpdate, + updateOnKey, + min, + max, + multipleOf, +}: Readonly): JSX.Element { + const [editingValue, setEditingValue] = useState(null) + + const isValidValue = useCallback( + (value: number): boolean => { + if (isNaN(value) || value < 0) return false + if (min !== undefined && value < min) return false + if (max !== undefined && value > max) return false + if (multipleOf !== undefined && value % multipleOf !== 0) return false + return true + }, + [min, max, multipleOf] + ) + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + if (readOnly) return + + const number = parseTime(event.target.value) + setEditingValue(event.target.value) + + if (updateOnKey && !isNaN(number) && isValidValue(number)) { + handleUpdate(number) + } + }, + [handleUpdate, updateOnKey, isValidValue, readOnly] + ) + const handleBlur = useCallback( + (event: React.FocusEvent) => { + if (readOnly) return + const number = parseTime(event.currentTarget.value) + if (!isNaN(number) && isValidValue(number)) { + handleUpdate(number) + } + + setEditingValue(null) + }, + [handleUpdate, isValidValue, readOnly] + ) + const handleFocus = useCallback((event: React.FocusEvent) => { + setEditingValue(event.currentTarget.value) + event.currentTarget.selectionStart = 0 + event.currentTarget.selectionEnd = event.currentTarget.value.length + }, []) + const handleKeyUp = useCallback( + (event: React.KeyboardEvent) => { + if (readOnly) return + + if (event.key === 'Escape') { + setEditingValue(null) + } else if (event.key === 'Enter') { + const number = parseTime(event.currentTarget.value) + if (!isNaN(number) && isValidValue(number)) { + handleUpdate(number) + } + } + }, + [handleUpdate, isValidValue, readOnly] + ) + const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + // allow ctrl/cmd + any key, to allow for shortcuts like ctrl+a, ctrl+c, ctrl+v, etc. + if (!ALLOWED_KEYS.includes(event.key) && event.ctrlKey === false && event.metaKey === false) { + event.preventDefault() + } + }, []) + + let showValue: string | number | undefined = editingValue ?? undefined + if (showValue === undefined && value !== undefined) { + showValue = formatTime(value) + } + if (showValue === undefined) showValue = '' + + return ( + + ) +} diff --git a/packages/webui/src/client/lib/Components/ToggleSwitch.tsx b/packages/webui/src/client/lib/Components/ToggleSwitch.tsx index c247f2ddb4c..47ad94bf54a 100644 --- a/packages/webui/src/client/lib/Components/ToggleSwitch.tsx +++ b/packages/webui/src/client/lib/Components/ToggleSwitch.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useCallback, useRef } from 'react' +import { type ChangeEvent, useCallback, useRef } from 'react' import Form from 'react-bootstrap/esm/Form' interface IToggleSwitchControlProps { diff --git a/packages/webui/src/client/lib/Components/util.tsx b/packages/webui/src/client/lib/Components/util.tsx index 9478d9df9d0..12b93884d78 100644 --- a/packages/webui/src/client/lib/Components/util.tsx +++ b/packages/webui/src/client/lib/Components/util.tsx @@ -1,5 +1,5 @@ -import { SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { ReadonlyDeep } from 'type-fest' +import type { SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import type { ReadonlyDeep } from 'type-fest' export function hasOpWithPath(allOps: ReadonlyDeep, id: string, subpath: string): boolean { const path = `${id}.${subpath}` diff --git a/packages/webui/src/client/lib/ConnectionStatusNotification.tsx b/packages/webui/src/client/lib/ConnectionStatusNotification.tsx index 91b58b7501d..f7a3a407b96 100644 --- a/packages/webui/src/client/lib/ConnectionStatusNotification.tsx +++ b/packages/webui/src/client/lib/ConnectionStatusNotification.tsx @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' -import { DDP } from 'meteor/ddp' -import * as React from 'react' +import type { DDP } from 'meteor/ddp' +import type * as React from 'react' import _ from 'underscore' import { MomentFromNow } from './Moment.js' import { @@ -8,14 +8,18 @@ import { NoticeLevel, Notification, NotificationList, - NotifierHandle, + type NotifierHandle, } from './notifications/notifications.js' import { WithManagedTracker } from './reactiveData/reactiveDataHelper.js' import { useTranslation } from 'react-i18next' import { NotificationCenterPopUps } from './notifications/NotificationCenterPanel.js' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { ICoreSystem, ServiceMessage, Criticality } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' -import { TFunction } from 'react-i18next' +import { + type ICoreSystem, + type ServiceMessage, + Criticality, +} from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' +import type { TFunction } from 'i18next' import { getRandomId } from '@sofie-automation/corelib/dist/lib' import { CoreSystem } from '../collections/index.js' import { useEffect } from 'react' diff --git a/packages/webui/src/client/lib/DocumentTitleProvider.tsx b/packages/webui/src/client/lib/DocumentTitleProvider.tsx index f4e017e7d9f..608e535c596 100644 --- a/packages/webui/src/client/lib/DocumentTitleProvider.tsx +++ b/packages/webui/src/client/lib/DocumentTitleProvider.tsx @@ -1,7 +1,7 @@ import * as React from 'react' -import { translateWithTracker, Translated } from './ReactMeteorData/ReactMeteorData.js' -import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' +import { translateWithTracker, type Translated } from './ReactMeteorData/ReactMeteorData.js' +import type { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { ReactiveVar } from 'meteor/reactive-var' import { isRunningInPWA } from './lib.js' diff --git a/packages/webui/src/client/lib/EditAttribute.tsx b/packages/webui/src/client/lib/EditAttribute.tsx index 65dd049c9e2..db450c6fb42 100644 --- a/packages/webui/src/client/lib/EditAttribute.tsx +++ b/packages/webui/src/client/lib/EditAttribute.tsx @@ -1,12 +1,12 @@ import * as React from 'react' import _ from 'underscore' import { useTracker } from './ReactMeteorData/react-meteor-data.js' -import { MultiSelect, MultiSelectEvent, MultiSelectOptions } from './multiSelect.js' +import { MultiSelect, type MultiSelectEvent, type MultiSelectOptions } from './multiSelect.js' import ClassNames from 'classnames' -import { ColorPickerEvent, ColorPicker } from './colorPicker.js' -import { IconPicker, IconPickerEvent } from './iconPicker.js' +import { type ColorPickerEvent, ColorPicker } from './colorPicker.js' +import { IconPicker, type IconPickerEvent } from './iconPicker.js' import { assertNever } from '@sofie-automation/corelib/dist/lib' -import { MongoCollection } from '../collections/lib.js' +import type { MongoCollection } from '../collections/lib.js' import { CheckboxControl } from './Components/Checkbox.js' import { TextInputControl } from './Components/TextInput.js' import { IntInputControl } from './Components/IntInput.js' diff --git a/packages/webui/src/client/lib/Escape.tsx b/packages/webui/src/client/lib/Escape.tsx index 2e723e4b686..37e6494e23d 100644 --- a/packages/webui/src/client/lib/Escape.tsx +++ b/packages/webui/src/client/lib/Escape.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, PropsWithChildren } from 'react' +import { useRef, useEffect, type PropsWithChildren } from 'react' import { createPortal } from 'react-dom' /** @@ -40,6 +40,7 @@ function usePortal(id: string, style?: Partial(`#${id}`) // Parent is either a new root or the existing dom element const parentElem = existingParent || createRootElement(id) + const parentCreatedByThisHook = !existingParent // If there is no existing DOM element, add a new one. if (!existingParent) { @@ -53,7 +54,7 @@ function usePortal(id: string, style?: Partial = + withTranslation()(ModalDialogGlobalContainer0) let modalDialogGlobalContainerSingleton: ModalDialogGlobalContainer0 /** * Display a ModalDialog, callback on user input diff --git a/packages/webui/src/client/lib/Moment.tsx b/packages/webui/src/client/lib/Moment.tsx index d5064d4dc9f..633cd6159c3 100644 --- a/packages/webui/src/client/lib/Moment.tsx +++ b/packages/webui/src/client/lib/Moment.tsx @@ -1,4 +1,4 @@ -import Moment, { MomentProps } from 'react-moment' +import Moment, { type MomentProps } from 'react-moment' import moment from 'moment' import { useCurrentTime } from './lib' diff --git a/packages/webui/src/client/lib/ReactMeteorData/ReactMeteorData.tsx b/packages/webui/src/client/lib/ReactMeteorData/ReactMeteorData.tsx index b2216909c7a..b3c7c59a14a 100644 --- a/packages/webui/src/client/lib/ReactMeteorData/ReactMeteorData.tsx +++ b/packages/webui/src/client/lib/ReactMeteorData/ReactMeteorData.tsx @@ -2,8 +2,8 @@ import React, { useState, useEffect, useRef, useCallback } from 'react' import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' import { Tracker } from 'meteor/tracker' -import { withTranslation, WithTranslation } from 'react-i18next' -import { AllPubSubTypes } from '@sofie-automation/meteor-lib/dist/api/pubsub' +import { withTranslation, type WithTranslation } from 'react-i18next' +import type { AllPubSubTypes } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { meteorSubscribe } from '../meteorApi.js' import { stringifyObjects } from '@sofie-automation/corelib/dist/lib' import _ from 'underscore' diff --git a/packages/webui/src/client/lib/Settings.ts b/packages/webui/src/client/lib/Settings.ts index b654e5d6392..f0eeb827357 100644 --- a/packages/webui/src/client/lib/Settings.ts +++ b/packages/webui/src/client/lib/Settings.ts @@ -1,5 +1,5 @@ import _ from 'underscore' -import { ISettings, DEFAULT_SETTINGS } from '@sofie-automation/meteor-lib/dist/Settings' +import { type ISettings, DEFAULT_SETTINGS } from '@sofie-automation/meteor-lib/dist/Settings' /** * This is an object specifying installation-wide, User Interface settings. diff --git a/packages/webui/src/client/lib/SettingsNavigation.tsx b/packages/webui/src/client/lib/SettingsNavigation.tsx index 96e14facca1..9794c344c6b 100644 --- a/packages/webui/src/client/lib/SettingsNavigation.tsx +++ b/packages/webui/src/client/lib/SettingsNavigation.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' import { useHistory } from 'react-router-dom' -import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' import Button from 'react-bootstrap/Button' export function RedirectToBlueprintButton(props: Readonly<{ id: BlueprintId | undefined }>): JSX.Element { diff --git a/packages/webui/src/client/lib/SorensenContext.tsx b/packages/webui/src/client/lib/SorensenContext.tsx index 5f8808666bc..9bf14ee9158 100644 --- a/packages/webui/src/client/lib/SorensenContext.tsx +++ b/packages/webui/src/client/lib/SorensenContext.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, useEffect, useState } from 'react' +import React, { type PropsWithChildren, useEffect, useState } from 'react' import Sorensen from '@sofie-automation/sorensen' import { catchError } from './lib.js' diff --git a/packages/webui/src/client/lib/Spinner.tsx b/packages/webui/src/client/lib/Spinner.tsx index 19b5d5d3885..59435f24761 100644 --- a/packages/webui/src/client/lib/Spinner.tsx +++ b/packages/webui/src/client/lib/Spinner.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import type * as React from 'react' interface SpinnerProps { size?: 'large' | 'medium' | 'small' diff --git a/packages/webui/src/client/lib/SplitDropdown.tsx b/packages/webui/src/client/lib/SplitDropdown.tsx index c629bdc3050..69400aa5658 100644 --- a/packages/webui/src/client/lib/SplitDropdown.tsx +++ b/packages/webui/src/client/lib/SplitDropdown.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react' +import React, { type ReactNode } from 'react' import ClassNames from 'classnames' import { Manager, Popper, Reference } from 'react-popper' diff --git a/packages/webui/src/client/lib/SplitPreviewBox.tsx b/packages/webui/src/client/lib/SplitPreviewBox.tsx index bf3cbd4f627..36a9ccfc061 100644 --- a/packages/webui/src/client/lib/SplitPreviewBox.tsx +++ b/packages/webui/src/client/lib/SplitPreviewBox.tsx @@ -1,53 +1,59 @@ import classNames from 'classnames' import React from 'react' -import { SplitRole, SplitSubItem } from './ui/splitPreview.js' +import { SplitRole, type SplitSubItem } from './ui/splitPreview.js' import { RundownUtils } from './rundown.js' export const RenderSplitPreview = React.memo(function RenderSplitPreview({ subItems, showLabels, + /** When true, boxes are rendered without an outer wrapper (parent must provide layout context). */ + flatLayout = false, }: { subItems: ReadonlyArray> showLabels: boolean + flatLayout?: boolean }) { const reversedSubItems = subItems.slice() reversedSubItems.reverse() - return ( -
      - {reversedSubItems.map((item, index, array) => { - return ( -
      1 && index > 0 && item.type === array[index - 1].type, - }, - { upper: index >= array.length / 2 }, - { lower: index < array.length / 2 } - )} - key={item._id + '-preview'} - style={{ - left: ((item.content?.x ?? 0) * 100).toString() + '%', - top: ((item.content?.y ?? 0) * 100).toString() + '%', - width: ((item.content?.scale ?? 1) * 100).toString() + '%', - height: ((item.content?.scale ?? 1) * 100).toString() + '%', - clipPath: item.content?.crop - ? `inset(${item.content.crop.top * 100}% ${item.content.crop.right * 100}% ${ - item.content.crop.bottom * 100 - }% ${item.content.crop.left * 100}%)` - : undefined, - }} - > - {showLabels && item.role === SplitRole.BOX &&
      {item.label}
      } -
      - ) - })} -
      - ) + const boxes = reversedSubItems.map((item, index, array) => { + return ( +
      1 && index > 0 && item.type === array[index - 1].type, + }, + { upper: index >= array.length / 2 }, + { lower: index < array.length / 2 } + )} + key={item._id + '-preview'} + style={{ + left: ((item.content?.x ?? 0) * 100).toString() + '%', + top: ((item.content?.y ?? 0) * 100).toString() + '%', + width: ((item.content?.scale ?? 1) * 100).toString() + '%', + height: ((item.content?.scale ?? 1) * 100).toString() + '%', + clipPath: item.content?.crop + ? `inset(${item.content.crop.top * 100}% ${item.content.crop.right * 100}% ${ + item.content.crop.bottom * 100 + }% ${item.content.crop.left * 100}%)` + : undefined, + }} + > + {item.thumbnailUrl && } + {showLabels && item.role === SplitRole.BOX &&
      {item.label}
      } +
      + ) + }) + + if (flatLayout) { + return <>{boxes} + } + + return
      {boxes}
      }) diff --git a/packages/webui/src/client/lib/StyledTimecode.tsx b/packages/webui/src/client/lib/StyledTimecode.tsx index af33ea7cc8f..23be5343c6a 100644 --- a/packages/webui/src/client/lib/StyledTimecode.tsx +++ b/packages/webui/src/client/lib/StyledTimecode.tsx @@ -1,8 +1,7 @@ -import React from 'react' import ClassNames from 'classnames' import { formatDurationAsTimecode } from '@sofie-automation/corelib/dist/lib' import type { Time } from '@sofie-automation/shared-lib/dist/lib/lib' -import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' +import type { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' interface IProps { studioSettings: Pick | undefined diff --git a/packages/webui/src/client/lib/VideoPreviewPlayer.tsx b/packages/webui/src/client/lib/VideoPreviewPlayer.tsx index 473dc8321ff..d31af289859 100644 --- a/packages/webui/src/client/lib/VideoPreviewPlayer.tsx +++ b/packages/webui/src/client/lib/VideoPreviewPlayer.tsx @@ -1,4 +1,4 @@ -import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' +import type { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import classNames from 'classnames' import { useEffect, useRef } from 'react' import { StyledTimecode } from './StyledTimecode.js' diff --git a/packages/webui/src/client/lib/VirtualElement.tsx b/packages/webui/src/client/lib/VirtualElement.tsx index af5c6581255..c065f1035ac 100644 --- a/packages/webui/src/client/lib/VirtualElement.tsx +++ b/packages/webui/src/client/lib/VirtualElement.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react' +import { useCallback, useEffect, useMemo, useState, useRef } from 'react' import { InView } from 'react-intersection-observer' import { getViewPortScrollingState } from './viewPort' @@ -76,26 +76,38 @@ export function VirtualElement({ const isTransitioning = useRef(false) const isCurrentlyObserving = useRef(false) + const observedElementRef = useRef(null) + const isMountedRef = useRef(true) + + useEffect(() => { + return () => { + isMountedRef.current = false + } + }, []) + + const placeholderHeightPx = measurements?.clientHeight ?? placeholderHeight ?? ref?.clientHeight ?? 0 const styleObj = useMemo( () => ({ width: width ?? 'auto', - height: ((placeholderHeight || ref?.clientHeight) ?? '0') + 'px', + height: `${placeholderHeightPx}px`, // These properties are used to ensure that if a prior element is changed from - // placeHolder to element, the position of visible elements are not affected. + // placeholder to element, the position of visible elements are not affected. contentVisibility: 'auto', - containIntrinsicSize: `0 ${(placeholderHeight || ref?.clientHeight) ?? '0'}px`, + containIntrinsicSize: `0 ${placeholderHeightPx}px`, contain: 'size layout', }), - [width, placeholderHeight] + [width, placeholderHeightPx] ) const handleResize = useCallback(() => { + if (!isMountedRef.current) return if (ref) { // Show children during measurement setIsShowingChildren(true) requestAnimationFrame(() => { + if (!isMountedRef.current) return const measurements = measureElement(ref, placeholderHeight) if (measurements) { setMeasurements(measurements) @@ -111,6 +123,18 @@ export function VirtualElement({ } }, [ref, inView, placeholderHeight]) + const unobserveElement = useCallback( + (element: HTMLDivElement | null) => { + if (!element) return + resizeObserverManager.unobserve(element) + if (observedElementRef.current === element) { + observedElementRef.current = null + } + isCurrentlyObserving.current = false + }, + [resizeObserverManager] + ) + // failsafe to ensure visible elements if resizing happens while scrolling useEffect(() => { if (!isShowingChildren) { @@ -165,6 +189,10 @@ export function VirtualElement({ }, [inView, isShowingChildren]) useEffect(() => { + if (observedElementRef.current && observedElementRef.current !== ref) { + unobserveElement(observedElementRef.current) + } + if (inView) { setIsShowingChildren(true) } @@ -195,14 +223,12 @@ export function VirtualElement({ if (ref) { if (!isCurrentlyObserving.current) { resizeObserverManager.observe(ref, handleResize) + observedElementRef.current = ref isCurrentlyObserving.current = true } } } else { - if (ref && isCurrentlyObserving.current) { - resizeObserverManager.unobserve(ref) - isCurrentlyObserving.current = false - } + if (ref) unobserveElement(ref) setIsShowingChildren(false) } } catch (error) { @@ -212,7 +238,7 @@ export function VirtualElement({ inViewChangeTimerRef.current = undefined } }, 100) - }, [inView, ref, handleResize, resizeObserverManager]) + }, [inView, ref, handleResize, resizeObserverManager, unobserveElement]) const onVisibleChanged = useCallback( (visible: boolean) => { @@ -225,12 +251,13 @@ export function VirtualElement({ ) const isScrolling = (): boolean => { + const { isProgrammaticScrollInProgress, lastProgrammaticScrollTime } = getViewPortScrollingState() // Don't do updates while scrolling: - if (getViewPortScrollingState().isProgrammaticScrollInProgress) { + if (isProgrammaticScrollInProgress) { return true } // And wait if a programmatic scroll was done recently: - const timeSinceLastProgrammaticScroll = Date.now() - getViewPortScrollingState().lastProgrammaticScrollTime + const timeSinceLastProgrammaticScroll = Date.now() - lastProgrammaticScrollTime if (timeSinceLastProgrammaticScroll < 100) { return true } @@ -241,22 +268,23 @@ export function VirtualElement({ // Setup initial observer if element is in view if (ref && inView && !isCurrentlyObserving.current) { resizeObserverManager.observe(ref, handleResize) + observedElementRef.current = ref isCurrentlyObserving.current = true } // Cleanup function return () => { // Clean up resize observer - if (ref && isCurrentlyObserving.current) { - resizeObserverManager.unobserve(ref) - isCurrentlyObserving.current = false + if (ref) unobserveElement(ref) + if (observedElementRef.current && observedElementRef.current !== ref) { + unobserveElement(observedElementRef.current) } if (inViewChangeTimerRef.current) { clearTimeout(inViewChangeTimerRef.current) } } - }, [ref, inView, handleResize]) + }, [ref, inView, handleResize, unobserveElement]) useEffect(() => { if (inView === true) { @@ -296,6 +324,7 @@ export function VirtualElement({ } idleCallback = window.requestIdleCallback( () => { + if (!isMountedRef.current) return // Measure the entire wrapper element instead of just the childRef if (ref) { const measurements = measureElement(ref, placeholderHeight) @@ -413,6 +442,23 @@ export class ElementObserverManager { private resizeObserver: ResizeObserver private mutationObserver: MutationObserver private observedElements: Map void> + private isMutationObserverActive = false + + private hasConnectedObservedElements(): boolean { + for (const observedElement of this.observedElements.keys()) { + if (document.contains(observedElement)) return true + } + return false + } + + private pruneDetachedObservedElements(): void { + for (const observedElement of Array.from(this.observedElements.keys())) { + if (!document.contains(observedElement)) { + this.observedElements.delete(observedElement) + this.resizeObserver.unobserve(observedElement) + } + } + } private constructor() { this.observedElements = new Map() @@ -421,39 +467,85 @@ export class ElementObserverManager { this.resizeObserver = new ResizeObserver((entries) => { entries.forEach((entry) => { const element = entry.target as HTMLElement + if (!document.contains(element)) { + this.observedElements.delete(element) + this.resizeObserver.unobserve(element) + return + } const callback = this.observedElements.get(element) if (callback) { callback() } }) + + // Ensure detached entries are aggressively cleaned even without follow-up DOM mutations. + this.pruneDetachedObservedElements() + if (this.observedElements.size === 0) { + this.disconnectMutationObserver() + } }) - // Configure MutationObserver + // Configure MutationObserver once and only connect/disconnect based on active observed elements. this.mutationObserver = new MutationObserver((mutations) => { + if (this.observedElements.size === 0) return + + this.pruneDetachedObservedElements() + if (this.observedElements.size === 0) { + this.disconnectMutationObserver() + return + } const targets = new Set() mutations.forEach((mutation) => { - const target = mutation.target as HTMLElement - // Find the closest observed element - let element = target + let element: HTMLElement | null = null + if (mutation.target instanceof HTMLElement) { + element = mutation.target + } else { + element = mutation.target.parentElement + } + + if (!element || !document.contains(element)) return + while (element) { if (this.observedElements.has(element)) { targets.add(element) break } - if (!element.parentElement) break element = element.parentElement } }) - // Call callbacks for affected elements targets.forEach((element) => { + if (!document.contains(element)) { + this.observedElements.delete(element) + this.resizeObserver.unobserve(element) + return + } const callback = this.observedElements.get(element) if (callback) callback() }) }) } + private ensureMutationObserverConnected(): void { + if (this.isMutationObserverActive) return + if (this.observedElements.size === 0) return + if (!this.hasConnectedObservedElements()) return + if (!document.body) return + + this.mutationObserver.observe(document.body, { + childList: true, + subtree: true, + }) + this.isMutationObserverActive = true + } + + private disconnectMutationObserver(): void { + if (!this.isMutationObserverActive) return + this.mutationObserver.disconnect() + this.isMutationObserverActive = false + } + public static getInstance(): ElementObserverManager { if (!ElementObserverManager.instance) { ElementObserverManager.instance = new ElementObserverManager() @@ -463,31 +555,31 @@ export class ElementObserverManager { public observe(element: HTMLElement, callback: () => void): void { if (!element) return + if (!document.contains(element)) return + this.pruneDetachedObservedElements() this.observedElements.set(element, callback) this.resizeObserver.observe(element) - this.mutationObserver.observe(element, { - childList: true, - subtree: true, - attributes: true, - characterData: true, - }) + this.ensureMutationObserverConnected() } public unobserve(element: HTMLElement): void { if (!element) return this.observedElements.delete(element) this.resizeObserver.unobserve(element) + this.pruneDetachedObservedElements() - // Disconnect and reconnect mutation observer to refresh the list of observed elements - this.mutationObserver.disconnect() - this.observedElements.forEach((_, el) => { - this.mutationObserver.observe(el, { - childList: true, - subtree: true, - attributes: true, - characterData: true, - }) - }) + if (this.observedElements.size === 0) { + this.resizeObserver.disconnect() + this.disconnectMutationObserver() + return + } + + if (!this.hasConnectedObservedElements()) { + this.disconnectMutationObserver() + return + } + + this.ensureMutationObserverConnected() } } diff --git a/packages/webui/src/client/lib/__tests__/rundown.test.ts b/packages/webui/src/client/lib/__tests__/rundown.test.ts index e84023fbb4b..f97333842c3 100644 --- a/packages/webui/src/client/lib/__tests__/rundown.test.ts +++ b/packages/webui/src/client/lib/__tests__/rundown.test.ts @@ -1,40 +1,30 @@ import { setupDefaultStudioEnvironment, - DefaultEnvironment, + type DefaultEnvironment, setupDefaultRundownPlaylist, convertToUIStudio, } from '../../../__mocks__/helpers/database.js' import { RundownUtils } from '../rundown.js' -import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { defaultPartInstance, defaultPiece, defaultPieceInstance } from '../../../__mocks__/defaultCollectionObjects.js' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { PieceLifespan } from '@sofie-automation/blueprints-integration' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PartInstances, PieceInstances, Pieces, RundownPlaylists } from '../../collections/index.js' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PieceInstances, Pieces, RundownPlaylists } from '../../collections/index.js' import { MongoMock } from '../../../__mocks__/mongo.js' import { RundownPlaylistCollectionUtil } from '../../collections/rundownPlaylistUtil.js' import { RundownPlaylistClientUtil } from '../rundownPlaylistUtil.js' -import { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import type { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' +import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' import { convertToUIShowStyleBase } from '@sofie-automation/corelib/src/playout/stateCacheResolver.js' +import { UIPartInstances } from '../../ui/Collections.js' const mockRundownPlaylistsCollection = MongoMock.getInnerMockCollection(RundownPlaylists) -const mockPartInstancesCollection = MongoMock.getInnerMockCollection(PartInstances) +const mockPartInstancesCollection = MongoMock.getInnerMockCollection(UIPartInstances) const mockPieceInstancesCollection = MongoMock.getInnerMockCollection(PieceInstances) const mockPiecesCollection = MongoMock.getInnerMockCollection(Pieces) -// This is a hack, the tests should be rewritten to not use methods unrelated to the testee -jest.mock('../../ui/Collections', () => { - const mockClientCollections = jest.requireActual('../../ui/Collections') - const mockLibCollections = jest.requireActual('../../collections/index') - return { - ...mockClientCollections, - UIParts: mockLibCollections.Parts, // for most purposes they're equivalent - UIPartInstances: mockLibCollections.PartInstances, // for most purposes they're equivalent - } -}) - describe('client/lib/rundown', () => { let env: DefaultEnvironment let playlistId: RundownPlaylistId diff --git a/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts b/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts index 307e3bfe3b0..3dc640af210 100644 --- a/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts +++ b/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts @@ -1,14 +1,17 @@ -import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + type DBRundownPlaylist, + QuickLoopMarkerType, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { literal } from '@sofie-automation/corelib/dist/lib' import { unprotectString, protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { RundownTimingCalculator, RundownTimingContext, findPartInstancesInQuickLoop } from '../rundownTiming.js' -import { PlaylistTimingType, SegmentTimingInfo } from '@sofie-automation/blueprints-integration' -import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import { RundownTimingCalculator, type RundownTimingContext, findPartInstancesInQuickLoop } from '../rundownTiming.js' +import { PlaylistTimingType, type SegmentTimingInfo } from '@sofie-automation/blueprints-integration' +import type { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' import { wrapPartToTemporaryInstance } from '@sofie-automation/corelib/src/playout/stateCacheResolver' const DEFAULT_DURATION = 0 diff --git a/packages/webui/src/client/lib/clientAPI.ts b/packages/webui/src/client/lib/clientAPI.ts index 30cdb13e540..8eb9b330250 100644 --- a/packages/webui/src/client/lib/clientAPI.ts +++ b/packages/webui/src/client/lib/clientAPI.ts @@ -1,8 +1,8 @@ -import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' +import type { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { MeteorCall } from '../lib/meteorApi.js' -import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { eventContextForLog } from './clientUserAction.js' -import { TSR } from '@sofie-automation/blueprints-integration' +import type { TSR } from '@sofie-automation/blueprints-integration' export async function callPeripheralDeviceFunction( e: Event | React.SyntheticEvent, diff --git a/packages/webui/src/client/lib/clientUserAction.ts b/packages/webui/src/client/lib/clientUserAction.ts index 5d856f5a54f..af4e2c46d24 100644 --- a/packages/webui/src/client/lib/clientUserAction.ts +++ b/packages/webui/src/client/lib/clientUserAction.ts @@ -1,4 +1,4 @@ -import * as i18next from 'i18next' +import type * as i18next from 'i18next' import _ from 'underscore' import { NotificationCenter, Notification, NoticeLevel } from './notifications/notifications.js' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' diff --git a/packages/webui/src/client/lib/collapseJSON.tsx b/packages/webui/src/client/lib/collapseJSON.tsx index 53e7d323da0..782663dcc39 100644 --- a/packages/webui/src/client/lib/collapseJSON.tsx +++ b/packages/webui/src/client/lib/collapseJSON.tsx @@ -59,7 +59,7 @@ export function CollapseJSON({ json }: { json: string }): JSX.Element { + + + ) +} + +function LabelAndOverridesForOneOfButtons({ + children, + label, + hint, + item, + itemKey, + overrideHelper, + showClearButton, + formatDefaultValue, +}: Readonly>): JSX.Element { + const { t } = useTranslation() + + const clearOverride = useCallback(() => { + overrideHelper().clearItemOverrides(item.id, String(itemKey)).commit() + }, [overrideHelper, item.id, itemKey]) + const setValue = useCallback( + (newValue: any) => { + overrideHelper().setItemValue(item.id, String(itemKey), newValue).commit() + }, + [overrideHelper, item.id, itemKey] + ) + + const isOverridden = hasOpWithPath(item.overrideOps, item.id, String(itemKey)) + + let displayValue: JSX.Element | string | null = '""' + if (item.defaults) { + const defaultValue: any = objectPathGet(item.defaults, String(itemKey)) + // Special cases for formatting of the default + if (formatDefaultValue) { + displayValue = formatDefaultValue(defaultValue) + } else if (defaultValue === false) { + displayValue = 'false' + } else if (defaultValue === true) { + displayValue = 'true' + } else if (!defaultValue) { + displayValue = '' + } else if (Array.isArray(defaultValue) || typeof defaultValue === 'object') { + displayValue = JSON.stringify(defaultValue) || '' + } else { + // Display it as a string + displayValue = `${defaultValue}` + } + } + + const value = objectPathGet(item.computed, String(itemKey)) + + return ( +
      + + +
      + {showClearButton && ( + + )} + + {children(value, setValue)} +
      + + {item.defaults && ( + <> + + {displayValue === null ? ( + + ) : typeof displayValue === 'object' ? ( + displayValue + ) : ( + + )} + + + + )} + + {hint && {hint}} +
      + ) +} diff --git a/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTable.tsx b/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTable.tsx index 3b93b98f068..ec1301ac6c6 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTable.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTable.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { clone, objectPathGet } from '@sofie-automation/corelib/dist/lib' import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { +import type { WrappedOverridableItemNormal, OverrideOpHelperForItemContents, } from '../../../ui/Settings/util/OverrideOpHelper.js' @@ -11,10 +11,10 @@ import { useToggleExpandHelper } from '../../../ui/util/useToggleExpandHelper.js import { doModalDialog } from '../../ModalDialog.js' import { getSchemaSummaryFieldsForObject, - SchemaFormSofieEnumDefinition, + type SchemaFormSofieEnumDefinition, translateStringIfHasNamespaces, } from '../schemaFormUtil.js' -import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' +import type { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import { getSchemaDefaultValues, getSchemaUIField, diff --git a/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx b/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx index 03f181c3230..f4969060f5a 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTableOpHelper.tsx @@ -1,5 +1,5 @@ import { clone, objectPathSet } from '@sofie-automation/corelib/dist/lib' -import { OverrideOpHelperForItemContentsBatcher } from '../../../ui/Settings/util/OverrideOpHelper.js' +import type { OverrideOpHelperForItemContentsBatcher } from '../../../ui/Settings/util/OverrideOpHelper.js' /** * The OverrideOp system does not support Arrays currently. diff --git a/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx b/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx index 0d2159b7ae5..b90f163586e 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormTable/ArrayTableRow.tsx @@ -1,12 +1,12 @@ import React, { useMemo } from 'react' -import { WrappedOverridableItemNormal } from '../../../ui/Settings/util/OverrideOpHelper.js' +import type { WrappedOverridableItemNormal } from '../../../ui/Settings/util/OverrideOpHelper.js' import { SchemaFormTableEditRow } from './TableEditRow.js' import { SchemaTableSummaryRow } from '../SchemaTableSummaryRow.js' import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' -import { JSONSchema } from '@sofie-automation/blueprints-integration' -import { SchemaSummaryField, SchemaFormSofieEnumDefinition } from '../schemaFormUtil.js' -import { OverrideOpHelperArrayTable } from './ArrayTableOpHelper.js' -import { ReadonlyDeep } from 'type-fest' +import type { JSONSchema } from '@sofie-automation/blueprints-integration' +import type { SchemaSummaryField, SchemaFormSofieEnumDefinition } from '../schemaFormUtil.js' +import type { OverrideOpHelperArrayTable } from './ArrayTableOpHelper.js' +import type { ReadonlyDeep } from 'type-fest' interface ArrayTableRowProps { columns: Record diff --git a/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTable.tsx b/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTable.tsx index 69ba7ce15e7..0c355c49714 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTable.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTable.tsx @@ -4,25 +4,25 @@ import { getRandomString, joinObjectPathFragments, literal, objectPathGet } from import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { - WrappedOverridableItemNormal, - OverrideOpHelperForItemContents, + type WrappedOverridableItemNormal, + type OverrideOpHelperForItemContents, getAllCurrentAndDeletedItemsFromOverrides, - WrappedOverridableItem, + type WrappedOverridableItem, } from '../../../ui/Settings/util/OverrideOpHelper.js' import { useToggleExpandHelper } from '../../../ui/util/useToggleExpandHelper.js' import { doModalDialog } from '../../ModalDialog.js' import { getSchemaSummaryFieldsForObject, - SchemaFormSofieEnumDefinition, + type SchemaFormSofieEnumDefinition, translateStringIfHasNamespaces, } from '../schemaFormUtil.js' -import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' +import type { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import { getSchemaDefaultValues, getSchemaUIField, SchemaFormUIField, } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaUtil' -import { SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import type { SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { SchemaFormTableEditRow } from './TableEditRow.js' import { SchemaTableSummaryRow } from '../SchemaTableSummaryRow.js' import { OverrideOpHelperObjectTable } from './ObjectTableOpHelper.js' diff --git a/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTableDeletedRow.tsx b/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTableDeletedRow.tsx index ba1b2ca7ffb..3975cd43608 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTableDeletedRow.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTableDeletedRow.tsx @@ -2,7 +2,7 @@ import { faSync } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { objectPathGet } from '@sofie-automation/corelib/dist/lib' import { useCallback } from 'react' -import { SchemaSummaryField } from '../schemaFormUtil.js' +import type { SchemaSummaryField } from '../schemaFormUtil.js' interface ObjectTableDeletedRowProps { summaryFields: SchemaSummaryField[] diff --git a/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx b/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx index 37e98189595..2d24f9e15d6 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormTable/ObjectTableOpHelper.tsx @@ -1,5 +1,5 @@ import { clone, joinObjectPathFragments, objectPathSet } from '@sofie-automation/corelib/dist/lib' -import { +import type { OverrideOpHelperForItemContentsBatcher, WrappedOverridableItem, } from '../../../ui/Settings/util/OverrideOpHelper.js' diff --git a/packages/webui/src/client/lib/forms/SchemaFormTable/TableEditRow.tsx b/packages/webui/src/client/lib/forms/SchemaFormTable/TableEditRow.tsx index 973d1fec04f..76a072cdac4 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormTable/TableEditRow.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormTable/TableEditRow.tsx @@ -2,11 +2,11 @@ import { faCheck } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import classNames from 'classnames' import { useCallback } from 'react' -import { OverrideOpHelperForItemContents } from '../../../ui/Settings/util/OverrideOpHelper.js' -import { SchemaFormSofieEnumDefinition } from '../schemaFormUtil.js' +import type { OverrideOpHelperForItemContents } from '../../../ui/Settings/util/OverrideOpHelper.js' +import type { SchemaFormSofieEnumDefinition } from '../schemaFormUtil.js' import { SchemaFormWithOverrides } from '../SchemaFormWithOverrides.js' -import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' -import { ReadonlyDeep } from 'type-fest' +import type { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' +import type { ReadonlyDeep } from 'type-fest' interface SchemaFormTableEditRowProps { sofieEnumDefinitons: Record | undefined diff --git a/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx b/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx index 0f9984e8708..1d2e300fe54 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx @@ -1,12 +1,12 @@ import { joinObjectPathFragments, literal } from '@sofie-automation/corelib/dist/lib' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { +import type { WrappedOverridableItemNormal, OverrideOpHelperForItemContents, } from '../../ui/Settings/util/OverrideOpHelper.js' import { CheckboxControl } from '../Components/Checkbox.js' -import { DropdownInputOption, DropdownInputControl } from '../Components/DropdownInput.js' +import { type DropdownInputOption, DropdownInputControl } from '../Components/DropdownInput.js' import { FloatInputControl } from '../Components/FloatInput.js' import { IntInputControl } from '../Components/IntInput.js' import { JsonTextInputControl } from '../Components/JsonTextInput.js' @@ -19,11 +19,11 @@ import { } from '../Components/LabelAndOverrides.js' import { MultiLineTextInputControl } from '../Components/MultiLineTextInput.js' import { TextInputControl } from '../Components/TextInput.js' -import { JSONSchema, TypeName } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' +import { type JSONSchema, TypeName } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import { SchemaFormArrayTable } from './SchemaFormTable/ArrayTable.js' import { - SchemaFormCommonProps, - SchemaFormSofieEnumDefinition, + type SchemaFormCommonProps, + type SchemaFormSofieEnumDefinition, translateStringIfHasNamespaces, } from './schemaFormUtil.js' import { MultiSelectInputControl } from '../Components/MultiSelectInput.js' @@ -34,6 +34,8 @@ import { Base64ImageInputControl } from '../Components/Base64ImageInput.js' import { MultiLineIntInputControl } from '../Components/MultiLineIntInput.js' import { ToggleSwitchControl } from '../Components/ToggleSwitch.js' import { BreadCrumbTextInput } from '../Components/BreadCrumbTextInput.js' +import { OneOfButtonsWithOverrides } from './SchemaFormOneOfButtons/OneOfButtons.js' +import { TimeMsInputControl } from '../Components/TimeMsInput.js' interface SchemaFormWithOverridesProps extends SchemaFormCommonProps { /** Base path of the schema within the document */ @@ -58,6 +60,7 @@ interface FormComponentProps { /** Whether a clear button should be showed for fields not marked as "required" */ showClearButton: boolean + readOnly: boolean } /** Whether this field has been marked as "required" */ @@ -80,6 +83,7 @@ function useChildPropsForFormComponent(props: Readonly): JSX.Element { +export function SchemaFormWithOverrides(props: Readonly): JSX.Element | null { const { t } = useTranslation() const childProps = useChildPropsForFormComponent(props) + if (props.schema.const) { + return null + } + switch (props.schema.type) { case TypeName.Array: if ( @@ -112,6 +120,11 @@ export function SchemaFormWithOverrides(props: Readonly + } else if ( + getSchemaUIField(props.schema, SchemaFormUIField.DisplayType) === 'oneOfButtons' && + props.schema.oneOf + ) { + return } else if (props.schema.patternProperties) { if (props.allowTables) { return @@ -128,7 +141,11 @@ export function SchemaFormWithOverrides(props: Readonly } case TypeName.Number: - return + if (getSchemaUIField(props.schema, SchemaFormUIField.DisplayType) === 'timeMs') { + return + } else { + return + } case TypeName.Boolean: if (getSchemaUIField(props.schema, SchemaFormUIField.DisplayType) === 'switch') { return @@ -330,12 +347,31 @@ const IntegerFormWithOverrides = ({ schema, commonAttrs }: Readonly )} ) } +const TimeMsFormWithOverrides = ({ schema, commonAttrs }: Readonly) => { + return ( + + {(value, handleUpdate) => ( + + )} + + ) +} + const NumberFormWithOverrides = ({ schema, commonAttrs }: Readonly) => { return ( @@ -346,6 +382,7 @@ const NumberFormWithOverrides = ({ schema, commonAttrs }: Readonly )} @@ -377,7 +414,12 @@ const StringFormWithOverrides = ({ schema, commonAttrs }: Readonly {(value, handleUpdate) => ( - + )} ) @@ -391,6 +433,7 @@ const StringArrayFormWithOverrides = ({ schema, commonAttrs }: Readonly )} diff --git a/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx b/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx index a652089c41a..e2550905e66 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormWithState.tsx @@ -1,12 +1,12 @@ import { useCallback, useMemo } from 'react' -import { +import type { OverrideOpHelperForItemContentsBatcher, WrappedOverridableItemNormal, } from '../../ui/Settings/util/OverrideOpHelper.js' -import { SchemaFormCommonProps } from './schemaFormUtil.js' +import type { SchemaFormCommonProps } from './schemaFormUtil.js' import { SchemaFormWithOverrides } from './SchemaFormWithOverrides.js' import { literal, objectPathSet } from '@sofie-automation/corelib/dist/lib' -import { AnyARecord } from 'dns' +import type { AnyARecord } from 'dns' interface SchemaFormWithStateProps extends Omit { object: any diff --git a/packages/webui/src/client/lib/forms/SchemaTableSummaryRow.tsx b/packages/webui/src/client/lib/forms/SchemaTableSummaryRow.tsx index a75f9dc5057..94db7ac2d86 100644 --- a/packages/webui/src/client/lib/forms/SchemaTableSummaryRow.tsx +++ b/packages/webui/src/client/lib/forms/SchemaTableSummaryRow.tsx @@ -3,8 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { objectPathGet } from '@sofie-automation/corelib/dist/lib' import classNames from 'classnames' import { useCallback } from 'react' -import { SchemaSummaryField } from './schemaFormUtil.js' -import { WrappedOverridableItemNormal } from '../../ui/Settings/util/OverrideOpHelper.js' +import type { SchemaSummaryField } from './schemaFormUtil.js' +import type { WrappedOverridableItemNormal } from '../../ui/Settings/util/OverrideOpHelper.js' import { useTranslation } from 'react-i18next' interface SchemaTableSummaryRowProps { diff --git a/packages/webui/src/client/lib/forms/schemaFormUtil.tsx b/packages/webui/src/client/lib/forms/schemaFormUtil.tsx index b639b59fa75..06e72e1977b 100644 --- a/packages/webui/src/client/lib/forms/schemaFormUtil.tsx +++ b/packages/webui/src/client/lib/forms/schemaFormUtil.tsx @@ -1,6 +1,6 @@ import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { i18nTranslator } from '../../ui/i18n.js' -import { JSONSchema, TypeName } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' +import { type JSONSchema, TypeName } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import { getSchemaUIField, SchemaFormUIField } from '@sofie-automation/blueprints-integration' import { joinObjectPathFragments } from '@sofie-automation/corelib/dist/lib' diff --git a/packages/webui/src/client/lib/iconPicker.tsx b/packages/webui/src/client/lib/iconPicker.tsx index 3a550e6977a..a8917fa2e9f 100644 --- a/packages/webui/src/client/lib/iconPicker.tsx +++ b/packages/webui/src/client/lib/iconPicker.tsx @@ -3,10 +3,10 @@ import _ from 'underscore' import ClassNames from 'classnames' import { library } from '@fortawesome/fontawesome-svg-core' -import { fas, IconName, IconPack, IconDefinition } from '@fortawesome/free-solid-svg-icons' +import { fas, type IconName, type IconPack, type IconDefinition } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { withTranslation } from 'react-i18next' -import { Translated } from './ReactMeteorData/ReactMeteorData.js' +import type { Translated } from './ReactMeteorData/ReactMeteorData.js' import { Manager, Popper, Reference } from 'react-popper' import Form from 'react-bootstrap/esm/Form' @@ -32,7 +32,7 @@ interface IState { searchText: string } -export const IconPicker = withTranslation()( +export const IconPicker: React.ComponentType = withTranslation()( class IconPicker extends React.Component, IState> { private _popperRef: HTMLElement | null = null private _popperUpdate: (() => Promise) | undefined diff --git a/packages/webui/src/client/lib/language.ts b/packages/webui/src/client/lib/language.ts index ff0b6c2e6e8..08c295f1f3e 100644 --- a/packages/webui/src/client/lib/language.ts +++ b/packages/webui/src/client/lib/language.ts @@ -1,4 +1,4 @@ -import { TFunction } from 'i18next' +import type { TFunction } from 'i18next' /** For phrases like "a, b, c, d or e" */ export function languageOr(t: TFunction, statements: string[]): string { diff --git a/packages/webui/src/client/lib/lib.tsx b/packages/webui/src/client/lib/lib.tsx index 20c0f2a1db9..a1e21a5502a 100644 --- a/packages/webui/src/client/lib/lib.tsx +++ b/packages/webui/src/client/lib/lib.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { Meteor } from 'meteor/meteor' import _ from 'underscore' import type { Time } from '@sofie-automation/shared-lib/dist/lib/lib' @@ -7,8 +7,8 @@ import { logger } from './logging.js' import shajs from 'sha.js' import { SINGLE_USE_TOKEN_SALT } from '@sofie-automation/meteor-lib/dist/api/userActions' import RundownViewEventBus, { - RundownViewEventBusEvents, - RundownViewEvents, + type RundownViewEventBusEvents, + type RundownViewEvents, } from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' export { isEventInInputField } diff --git a/packages/webui/src/client/lib/logging.ts b/packages/webui/src/client/lib/logging.ts index 4c2945fb553..a1b2b3dd377 100644 --- a/packages/webui/src/client/lib/logging.ts +++ b/packages/webui/src/client/lib/logging.ts @@ -1,6 +1,6 @@ import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { MeteorCall } from './meteorApi.js' -import { LoggerInstanceFixed } from '@sofie-automation/corelib/dist/logging' +import type { LoggerInstanceFixed } from '@sofie-automation/corelib/dist/logging' const getLogMethod = (type: string) => { return (...args: any[]) => { diff --git a/packages/webui/src/client/lib/meteorApi.ts b/packages/webui/src/client/lib/meteorApi.ts index 0522063d013..1e28d5879a0 100644 --- a/packages/webui/src/client/lib/meteorApi.ts +++ b/packages/webui/src/client/lib/meteorApi.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { logger } from './logging.js' -import { AllPubSubTypes } from '@sofie-automation/meteor-lib/dist/api/pubsub' +import type { AllPubSubTypes } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { MakeMeteorCall } from '@sofie-automation/meteor-lib/dist/api/methods' import { MeteorApply } from './MeteorApply.js' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' diff --git a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx index 82d8e8bce15..6fc493df667 100644 --- a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx +++ b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx @@ -1,18 +1,17 @@ import * as React from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import ClassNames from 'classnames' -import { motion, AnimatePresence, HTMLMotionProps } from 'motion/react' -import { translateWithTracker, Translated, useTracker } from '../ReactMeteorData/ReactMeteorData.js' -import { NotificationCenter, Notification, NoticeLevel, NotificationAction } from './notifications.js' +import { motion, AnimatePresence, type HTMLMotionProps } from 'motion/react' +import { translateWithTracker, type Translated, useTracker } from '../ReactMeteorData/ReactMeteorData.js' +import { NotificationCenter, Notification, NoticeLevel, type NotificationAction } from './notifications.js' import { ContextMenuTrigger, ContextMenu, MenuItem } from '@jstarpl/react-contextmenu' import { translateMessage, isTranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { CriticalIcon, WarningIcon, CollapseChevrons, InformationIcon } from '../ui/icons/notifications.js' import update from 'immutability-helper' import { i18nTranslator } from '../../ui/i18n.js' -import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { useTranslation } from 'react-i18next' import { PopUpPanel } from '../../ui/RundownView/PopUpPanel.js' -import classNames from 'classnames' interface IPopUpProps { id?: string @@ -471,7 +470,7 @@ export const NotificationCenterPanel = (props: { hideRundownHeader?: boolean }): JSX.Element => ( diff --git a/packages/webui/src/client/lib/notifications/ReactNotification.tsx b/packages/webui/src/client/lib/notifications/ReactNotification.tsx index 5a8db9b7606..eee89b95f70 100644 --- a/packages/webui/src/client/lib/notifications/ReactNotification.tsx +++ b/packages/webui/src/client/lib/notifications/ReactNotification.tsx @@ -1,5 +1,5 @@ -import React, { useEffect } from 'react' -import { NoticeLevel, NotificationCenter, Notification, NotificationAction } from './notifications.js' +import { useEffect } from 'react' +import { NoticeLevel, NotificationCenter, Notification, type NotificationAction } from './notifications.js' import { getRandomString } from '@sofie-automation/corelib/dist/lib' import { getCurrentTime } from '../systemTime.js' @@ -8,10 +8,9 @@ export interface IProps { source?: string actions?: NotificationAction[] rank?: number - children?: React.ReactElement } -export function ReactNotification(props: IProps): JSX.Element | null { +export function ReactNotification(props: React.PropsWithChildren): JSX.Element | null { useEffect(() => { const notificationId = getRandomString() const notification = new Notification( diff --git a/packages/webui/src/client/lib/notifications/notifications.ts b/packages/webui/src/client/lib/notifications/notifications.ts index 09352e7202f..9d9ec6667e3 100644 --- a/packages/webui/src/client/lib/notifications/notifications.ts +++ b/packages/webui/src/client/lib/notifications/notifications.ts @@ -6,15 +6,15 @@ import { EventEmitter } from 'events' import { assertNever, getRandomString } from '@sofie-automation/corelib/dist/lib' import type { Time } from '@sofie-automation/shared-lib/dist/lib/lib' import { - ProtectedString, + type ProtectedString, unprotectString, isProtectedString, protectString, } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { isTranslatableMessage, ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { isTranslatableMessage, type ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { MeteorCall } from '../../lib/meteorApi.js' -import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { LocalStorageProperty } from '../localStorage.js' let reportNotificationsId: string | null = null @@ -436,7 +436,7 @@ export const NotificationCenter = new NotificationCenter0() export class Notification extends EventEmitter { id: string | undefined status: NoticeLevel - message: string | React.ReactElement | ITranslatableMessage | null + message: React.ReactNode | ITranslatableMessage | null source: NotificationsSource persistent?: boolean timeout?: number @@ -448,7 +448,7 @@ export class Notification extends EventEmitter { constructor( id: string | ProtectedString | undefined, status: NoticeLevel, - message: string | React.ReactElement | ITranslatableMessage | null, + message: React.ReactNode | ITranslatableMessage | null, source: NotificationsSource, created?: Time, persistent?: boolean, diff --git a/packages/webui/src/client/lib/notifications/warningIcon.tsx b/packages/webui/src/client/lib/notifications/warningIcon.tsx index 085f3bedd61..404288a116a 100644 --- a/packages/webui/src/client/lib/notifications/warningIcon.tsx +++ b/packages/webui/src/client/lib/notifications/warningIcon.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import type * as React from 'react' export const sofieWarningIcon: React.FunctionComponent<{}> = () => ( diff --git a/packages/webui/src/client/lib/partInstanceUtil.ts b/packages/webui/src/client/lib/partInstanceUtil.ts new file mode 100644 index 00000000000..64d459a1a80 --- /dev/null +++ b/packages/webui/src/client/lib/partInstanceUtil.ts @@ -0,0 +1,67 @@ +import type { DBPart, PartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part' + +/** + * Minimal interface for a PartInstance containing the properties needed for invalidReason checks. + */ +export interface PartInstanceLike { + part: Pick + invalidReason?: PartInvalidReason +} + +export interface PartInvalidReasonExt extends PartInvalidReason { + /** + * If this invalid reason came from the instance (playout) rather than the part (ingest) + */ + isInstanceInvalid: boolean +} + +/** + * Get the effective invalidReason for a PartInstance. + * + * If the Part has a planned invalidReason (from ingest), it takes precedence. + * Otherwise, returns the runtime invalidReason from the PartInstance (from playout). + * + * This distinction matters because: + * - Part.invalidReason is planned/static (set during ingest, shouldn't create real PartInstance) + * - PartInstance.invalidReason is runtime/dynamic (set during playout, can be fixed) + * + * @param partInstance The PartInstance object + * @returns The effective invalidReason to display, or undefined if none + */ +export function getEffectiveInvalidReason(partInstance: PartInstanceLike): PartInvalidReasonExt | undefined { + // Planned invalidReason (from Part/ingest) takes precedence + // It shouldn't be possible to create a real PartInstance of an invalid Part + if (partInstance.part.invalidReason) { + return { + ...partInstance.part.invalidReason, + isInstanceInvalid: false, + } + } + + // Runtime invalidReason (from PartInstance/playout) + if (partInstance.invalidReason) { + return { + ...partInstance.invalidReason, + isInstanceInvalid: true, + } + } + + return undefined +} + +/** + * Check if the effective state is "invalid" for a PartInstance. + * + * A PartInstance is considered invalid if either: + * - The Part has `invalid: true` (planned invalid, may not have an invalidReason) + * - The PartInstance has a runtime invalidReason (runtime invalid) + * + * Note: This is separate from getEffectiveInvalidReason because part.invalid can be true + * without an invalidReason being set (legacy behavior). + * + * @param partInstance The PartInstance object + * @returns true if the part should be shown as invalid + */ +export function isPartInstanceInvalid(partInstance: PartInstanceLike): boolean { + return !!partInstance.part.invalid || !!partInstance.invalidReason +} diff --git a/packages/webui/src/client/lib/polyfill/polyfills.ts b/packages/webui/src/client/lib/polyfill/polyfills.ts index a83003fcb29..a04781839bb 100644 --- a/packages/webui/src/client/lib/polyfill/polyfills.ts +++ b/packages/webui/src/client/lib/polyfill/polyfills.ts @@ -1,3 +1,2 @@ import './requestIdleCallback.js' import './vibrate.js' -import './promise.allSettled.js' diff --git a/packages/webui/src/client/lib/polyfill/promise.allSettled.ts b/packages/webui/src/client/lib/polyfill/promise.allSettled.ts deleted file mode 100644 index 0828766d02c..00000000000 --- a/packages/webui/src/client/lib/polyfill/promise.allSettled.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * CasparCG HTML renderer in 2.1.12 uses an old version of Chromium that doesn't support Promise.allSettled and as a - * result causes views rendered using it to crash. Promise.allSettled is used as a part of the i18next integration. - * This polyfill can be removed once CasparCG HTML renderer is updated to Chromium >= 76. - * -- 2021/02/22, Jan Starzak - */ -// @ts-expect-error No types available -import allSettled from 'promise.allsettled' - -// will be a no-op if not needed -allSettled.shim() diff --git a/packages/webui/src/client/lib/popperUtils.ts b/packages/webui/src/client/lib/popperUtils.ts index c2aa6c33f2a..cacd559755f 100644 --- a/packages/webui/src/client/lib/popperUtils.ts +++ b/packages/webui/src/client/lib/popperUtils.ts @@ -1,4 +1,4 @@ -import { ModifierPhases } from '@popperjs/core' +import type { ModifierPhases } from '@popperjs/core' export const sameWidth = { name: 'sameWidth', diff --git a/packages/webui/src/client/lib/reactiveData/reactiveData.ts b/packages/webui/src/client/lib/reactiveData/reactiveData.ts index 96a94e7cfc2..658513692ec 100644 --- a/packages/webui/src/client/lib/reactiveData/reactiveData.ts +++ b/packages/webui/src/client/lib/reactiveData/reactiveData.ts @@ -1,12 +1,12 @@ import { Tracker } from 'meteor/tracker' import { ReactiveVar } from 'meteor/reactive-var' -import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' +import type { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { getCurrentTime } from '../systemTime.js' -import { FindOptions } from '../../collections/lib.js' -import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { FindOptions } from '../../collections/lib.js' +import type { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ExternalMessageQueue, PeripheralDevices, Pieces, Rundowns } from '../../collections/index.js' -import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' export namespace reactiveData { // export function getRRundownId (rundownId: RundownId): ReactiveVar { diff --git a/packages/webui/src/client/lib/reactiveData/reactiveDataHelper.ts b/packages/webui/src/client/lib/reactiveData/reactiveDataHelper.ts index e8879dc9035..9396583b051 100644 --- a/packages/webui/src/client/lib/reactiveData/reactiveDataHelper.ts +++ b/packages/webui/src/client/lib/reactiveData/reactiveDataHelper.ts @@ -1,5 +1,5 @@ import { Tracker } from 'meteor/tracker' -import { AllPubSubTypes } from '@sofie-automation/meteor-lib/dist/api/pubsub' +import type { AllPubSubTypes } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { meteorSubscribe } from '../meteorApi.js' import { Meteor } from 'meteor/meteor' @@ -59,11 +59,29 @@ export function slowDownReactivity any>(fnc: T, dela export abstract class WithManagedTracker { private _autoruns: Tracker.Computation[] = [] private _subs: Meteor.SubscriptionHandle[] = [] + private _pendingSubsToStop: Meteor.SubscriptionHandle[] = [] + private _stopSubsTimeout: ReturnType | null = null stop(): void { this._autoruns.forEach((comp) => comp.stop()) - setTimeout(() => { - this._subs.forEach((comp) => comp.stop()) + this._autoruns = [] + // If a previous stop() is already pending, cancel its timer but carry its + // subscriptions forward so they still get torn down. This avoids stacking + // timers across repeated stop()/recreate cycles without losing pending subs. + if (this._stopSubsTimeout !== null) { + clearTimeout(this._stopSubsTimeout) + this._stopSubsTimeout = null + } + this._pendingSubsToStop.push(...this._subs) + this._subs = [] + if (this._pendingSubsToStop.length === 0) { + return + } + this._stopSubsTimeout = setTimeout(() => { + this._stopSubsTimeout = null + const toStop = this._pendingSubsToStop + this._pendingSubsToStop = [] + toStop.forEach((comp) => comp.stop()) }, 2000) // wait for a couple of seconds, before unsubscribing } diff --git a/packages/webui/src/client/lib/rundown.ts b/packages/webui/src/client/lib/rundown.ts index fcbd5940d1d..0e99d5d45aa 100644 --- a/packages/webui/src/client/lib/rundown.ts +++ b/packages/webui/src/client/lib/rundown.ts @@ -1,7 +1,7 @@ -import { PartUi } from '../ui/SegmentTimeline/SegmentTimelineContainer.js' +import type { PartUi } from '../ui/SegmentTimeline/SegmentTimelineContainer.js' import { Timecode } from '@sofie-automation/corelib/dist/index' import { Settings } from '../lib/Settings.js' -import { TFunction } from 'react-i18next' +import type { TFunction } from 'i18next' import { getResolvedSegment as getResolvedSegmentBase, getSegmentsWithPartInstances as getSegmentsWithPartInstancesBase, @@ -9,30 +9,30 @@ import { } from '@sofie-automation/corelib/dist/playout/stateCacheResolver' import { SourceLayerType, - IBlueprintActionManifestDisplay, - IBlueprintActionManifestDisplayContent, + type IBlueprintActionManifestDisplay, + type IBlueprintActionManifestDisplayContent, } from '@sofie-automation/blueprints-integration' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { getCurrentTime } from './systemTime.js' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { IAdLibListItem } from '../ui/Shelf/AdLibListItem.js' -import { BucketAdLibItem, BucketAdLibUi } from '../ui/Shelf/RundownViewBuckets.js' -import { FindOptions } from '../collections/lib.js' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { IAdLibListItem } from '../ui/Shelf/AdLibListItem.js' +import type { BucketAdLibItem, BucketAdLibUi } from '../ui/Shelf/RundownViewBuckets.js' +import type { FindOptions } from '../collections/lib.js' import { getShowHiddenSourceLayers } from './localStorage.js' -import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' +import type { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { calculatePartInstanceExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' -import { AdLibPieceUi } from './shelf.js' -import { PieceId, PieceInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { AdLibPieceUi } from './shelf.js' +import type { PieceId, PieceInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PieceInstances, Pieces, Segments } from '../collections/index.js' -import { Piece, PieceStatusCode, PieceUi } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { type Piece, PieceStatusCode, type PieceUi } from '@sofie-automation/corelib/dist/dataModel/Piece' import { assertNever } from '@sofie-automation/shared-lib/dist/lib/lib' -import { FindOneOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { FindOneOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownPlaylistClientUtil } from './rundownPlaylistUtil.js' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' import { invalidateAfter } from './invalidatingTime' -import { +import type { PieceInstancesForPartInstanceContext, PieceInstancesForPartInstanceWrapperOptions, ResolvedSegmentContext, diff --git a/packages/webui/src/client/lib/rundownLayouts.ts b/packages/webui/src/client/lib/rundownLayouts.ts index 810793849c9..590c2c21c17 100644 --- a/packages/webui/src/client/lib/rundownLayouts.ts +++ b/packages/webui/src/client/lib/rundownLayouts.ts @@ -1,4 +1,4 @@ -import { +import type { PartInstanceId, RundownLayoutId, RundownPlaylistActivationId, @@ -8,56 +8,52 @@ import { createPartCurrentTimes, processAndPrunePieceInstanceTimings, } from '@sofie-automation/corelib/dist/playout/processAndPrune' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { CustomizableRegions, - DashboardLayout, - DashboardLayoutFilter, + type DashboardLayout, + type DashboardLayoutFilter, PieceDisplayStyle, - RequiresActiveLayers, - RundownLayout, - RundownLayoutAdLibRegion, - RundownLayoutBase, - RundownLayoutColoredBox, - RundownLayoutElementBase, + type RequiresActiveLayers, + type RundownLayout, + type RundownLayoutAdLibRegion, + type RundownLayoutBase, + type RundownLayoutColoredBox, + type RundownLayoutElementBase, RundownLayoutElementType, - RundownLayoutEndWords, - RundownLayoutExternalFrame, - RundownLayoutFilterBase, - RundownLayoutMiniRundown, - RundownLayoutNextBreakTiming, - RundownLayoutNextInfo, - RundownLayoutPartName, - RundownLayoutPartTiming, - RundownLayoutPieceCountdown, - RundownLayoutPlaylistEndTimer, - RundownLayoutPlaylistName, - RundownLayoutPlaylistStartTimer, - RundownLayoutPresenterView, - RundownLayoutRundownHeader, - RundownLayoutSegmentName, - RundownLayoutSegmentTiming, - RundownLayoutShelfBase, - RundownLayoutShowStyleDisplay, - RundownLayoutStudioName, - RundownLayoutSytemStatus, - RundownLayoutTextLabel, - RundownLayoutTimeOfDay, + type RundownLayoutExternalFrame, + type RundownLayoutFilterBase, + type RundownLayoutMiniRundown, + type RundownLayoutNextBreakTiming, + type RundownLayoutNextInfo, + type RundownLayoutPartName, + type RundownLayoutPartTiming, + type RundownLayoutPieceCountdown, + type RundownLayoutPlaylistEndTimer, + type RundownLayoutPlaylistName, + type RundownLayoutPlaylistStartTimer, + type RundownLayoutPresenterView, + type RundownLayoutSegmentName, + type RundownLayoutSegmentTiming, + type RundownLayoutShelfBase, + type RundownLayoutStudioName, + type RundownLayoutTextLabel, + type RundownLayoutTimeOfDay, RundownLayoutType, - RundownLayoutWithFilters, - RundownViewLayout, + type RundownLayoutWithFilters, + type RundownViewLayout, } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { literal } from '@sofie-automation/corelib/dist/lib' import { getCurrentTime } from './systemTime.js' import { invalidateAt } from './invalidatingTime.js' import { memoizedIsolatedAutorun } from './memoizedIsolatedAutorun.js' import { PieceInstances } from '../collections/index.js' -import { ReadonlyDeep } from 'type-fest' -import { TFunction } from 'i18next' +import type { ReadonlyDeep } from 'type-fest' +import type { TFunction } from 'i18next' import _ from 'underscore' import { UIPartInstances } from '../ui/Collections.js' -import { UIShowStyleBase } from '@sofie-automation/corelib/src/dataModel/ShowStyleBase.js' +import type { UIShowStyleBase } from '@sofie-automation/corelib/src/dataModel/ShowStyleBase.js' export interface LayoutDescriptor { supportedFilters: RundownLayoutElementType[] @@ -200,7 +196,6 @@ class RundownLayoutsRegistry { private shelfLayouts: Map = new Map() private rundownViewLayouts: Map = new Map() private miniShelfLayouts: Map = new Map() - private rundownHeaderLayouts: Map = new Map() private presenterViewLayouts: Map = new Map() public registerShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) { @@ -215,10 +210,6 @@ class RundownLayoutsRegistry { this.miniShelfLayouts.set(id, description) } - public registerRundownHeaderLayouts(id: RundownLayoutType, description: LayoutDescriptor) { - this.rundownHeaderLayouts.set(id, description) - } - public registerPresenterViewLayout(id: RundownLayoutType, description: LayoutDescriptor) { this.presenterViewLayouts.set(id, description) } @@ -235,10 +226,6 @@ class RundownLayoutsRegistry { return regionId === CustomizableRegions.MiniShelf } - public isRundownHeaderLayout(regionId: CustomizableRegions) { - return regionId === CustomizableRegions.RundownHeader - } - public isPresenterViewLayout(regionId: CustomizableRegions) { return regionId === CustomizableRegions.PresenterView } @@ -277,12 +264,6 @@ class RundownLayoutsRegistry { layouts: this.wrapToCustomizableRegionLayout(this.miniShelfLayouts, t), navigationLink: (studioId, layoutId) => `/activeRundown/${studioId}?miniShelfLayout=${layoutId}`, }, - { - _id: CustomizableRegions.RundownHeader, - title: t('Rundown Header Layouts'), - layouts: this.wrapToCustomizableRegionLayout(this.rundownHeaderLayouts, t), - navigationLink: (studioId, layoutId) => `/activeRundown/${studioId}?rundownHeaderLayout=${layoutId}`, - }, { _id: CustomizableRegions.PresenterView, title: t('Presenter View Layouts'), @@ -328,27 +309,6 @@ export namespace RundownLayoutsAPI { registry.registerRundownViewLayout(RundownLayoutType.RUNDOWN_VIEW_LAYOUT, { supportedFilters: [], }) - registry.registerRundownHeaderLayouts(RundownLayoutType.RUNDOWN_HEADER_LAYOUT, { - supportedFilters: [], - }) - registry.registerRundownHeaderLayouts(RundownLayoutType.DASHBOARD_LAYOUT, { - filtersTitle: 'Layout Elements', - supportedFilters: [ - RundownLayoutElementType.PIECE_COUNTDOWN, - RundownLayoutElementType.PLAYLIST_START_TIMER, - RundownLayoutElementType.PLAYLIST_END_TIMER, - RundownLayoutElementType.NEXT_BREAK_TIMING, - RundownLayoutElementType.END_WORDS, - RundownLayoutElementType.SEGMENT_TIMING, - RundownLayoutElementType.PART_TIMING, - RundownLayoutElementType.TEXT_LABEL, - RundownLayoutElementType.PLAYLIST_NAME, - RundownLayoutElementType.TIME_OF_DAY, - RundownLayoutElementType.SHOWSTYLE_DISPLAY, - RundownLayoutElementType.SYSTEM_STATUS, - RundownLayoutElementType.COLORED_BOX, - ], - }) registry.registerPresenterViewLayout(RundownLayoutType.CLOCK_PRESENTER_VIEW_LAYOUT, { supportedFilters: [], }) @@ -393,10 +353,6 @@ export namespace RundownLayoutsAPI { return registry.isMiniShelfLayout(layout.regionId) } - export function isLayoutForRundownHeader(layout: RundownLayoutBase): layout is RundownLayoutRundownHeader { - return registry.isRundownHeaderLayout(layout.regionId) - } - export function isRundownViewLayout(layout: RundownLayoutBase): layout is RundownViewLayout { return layout.type === RundownLayoutType.RUNDOWN_VIEW_LAYOUT } @@ -411,10 +367,6 @@ export namespace RundownLayoutsAPI { return layout.type === RundownLayoutType.DASHBOARD_LAYOUT && (layout as DashboardLayout).filters !== undefined } - export function isRundownHeaderLayout(layout: RundownLayoutBase): layout is RundownLayoutRundownHeader { - return layout.type === RundownLayoutType.RUNDOWN_HEADER_LAYOUT - } - export function isDefaultLayout(layout: RundownLayoutBase): boolean { return layout.isDefaultLayout } @@ -461,10 +413,6 @@ export namespace RundownLayoutsAPI { return element.type === RundownLayoutElementType.NEXT_BREAK_TIMING } - export function isEndWords(element: RundownLayoutElementBase): element is RundownLayoutEndWords { - return element.type === RundownLayoutElementType.END_WORDS - } - export function isSegmentTiming(element: RundownLayoutElementBase): element is RundownLayoutSegmentTiming { return element.type === RundownLayoutElementType.SEGMENT_TIMING } @@ -489,14 +437,6 @@ export namespace RundownLayoutsAPI { return element.type === RundownLayoutElementType.TIME_OF_DAY } - export function isSystemStatus(element: RundownLayoutElementBase): element is RundownLayoutSytemStatus { - return element.type === RundownLayoutElementType.SYSTEM_STATUS - } - - export function isShowStyleDisplay(element: RundownLayoutElementBase): element is RundownLayoutShowStyleDisplay { - return element.type === RundownLayoutElementType.SHOWSTYLE_DISPLAY - } - export function isSegmentName(element: RundownLayoutElementBase): element is RundownLayoutSegmentName { return element.type === RundownLayoutElementType.SEGMENT_NAME } diff --git a/packages/webui/src/client/lib/rundownPlaylistUtil.ts b/packages/webui/src/client/lib/rundownPlaylistUtil.ts index 64a039c423d..5a91dce5d43 100644 --- a/packages/webui/src/client/lib/rundownPlaylistUtil.ts +++ b/packages/webui/src/client/lib/rundownPlaylistUtil.ts @@ -1,13 +1,13 @@ -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { FindOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import type { FindOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { RundownPlaylistCollectionUtil } from '../collections/rundownPlaylistUtil.js' import { UIPartInstances, UIParts } from '../ui/Collections.js' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { Pieces, Segments } from '../collections/index.js' -import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { RundownId, PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { RundownId, PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { normalizeArrayFunc, groupByToMap } from '@sofie-automation/corelib/dist/lib' import { sortSegmentsInRundowns, @@ -16,7 +16,7 @@ import { } from '@sofie-automation/corelib/dist/playout/playlist' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import _ from 'underscore' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' export class RundownPlaylistClientUtil { /** Returns all segments joined with their rundowns in their correct order for this RundownPlaylist */ @@ -96,7 +96,6 @@ export class RundownPlaylistClientUtil { currentPartInstance: PartInstance | undefined nextPartInstance: PartInstance | undefined previousPartInstance: PartInstance | undefined - partInstanceToCountTimeFrom: PartInstance | undefined } { let unorderedRundownIds = rundownIds0 if (!unorderedRundownIds) { @@ -117,26 +116,10 @@ export class RundownPlaylistClientUtil { }).fetch() : [] - const areAllPartsTimed = !!UIPartInstances.findOne({ - rundownId: { $in: unorderedRundownIds }, - ['part.untimed']: { $ne: true }, - }) - - const partInstanceToCountTimeFrom = UIPartInstances.findOne( - { - rundownId: { $in: unorderedRundownIds }, - reset: { $ne: true }, - takeCount: { $exists: true }, - ['part.untimed']: { $ne: areAllPartsTimed }, - }, - { sort: { takeCount: 1 } } - ) - return { currentPartInstance: instances.find((inst) => inst._id === playlist.currentPartInfo?.partInstanceId), nextPartInstance: instances.find((inst) => inst._id === playlist.nextPartInfo?.partInstanceId), previousPartInstance: instances.find((inst) => inst._id === playlist.previousPartInfo?.partInstanceId), - partInstanceToCountTimeFrom, } } diff --git a/packages/webui/src/client/lib/rundownTiming.ts b/packages/webui/src/client/lib/rundownTiming.ts index c7b3586b57f..e8a5be4fd8f 100644 --- a/packages/webui/src/client/lib/rundownTiming.ts +++ b/packages/webui/src/client/lib/rundownTiming.ts @@ -11,21 +11,24 @@ * without knowing what particular case you are trying to solve. */ -import { PartId, PartInstanceId, SegmentId, SegmentPlayoutId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { PartId, PartInstanceId, SegmentId, SegmentPlayoutId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { literal } from '@sofie-automation/corelib/dist/lib' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' import { calculatePartInstanceExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { DBPart, PartExtended } from '@sofie-automation/corelib/dist/dataModel/Part' -import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBPart, PartExtended } from '@sofie-automation/corelib/dist/dataModel/Part' +import { + type DBRundownPlaylist, + QuickLoopMarkerType, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { objectFromEntries } from '@sofie-automation/shared-lib/dist/lib/lib' import { getCurrentTime } from './systemTime.js' import { Settings } from '../lib/Settings.js' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { CountdownType } from '@sofie-automation/blueprints-integration' import { RundownUtils } from './rundown.js' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance' import { isLoopRunning, isLoopDefined, @@ -854,10 +857,18 @@ export function getPlaylistTimingDiff( backAnchor = timingContext.nextRundownAnchor ?? timing.expectedEnd } - let diff = PlaylistTiming.isPlaylistTimingNone(timing) - ? (timingContext.asPlayedPlaylistDuration || 0) - + let diff = 0 + if (PlaylistTiming.isPlaylistTimingNone(timing)) { + diff = + (timingContext.asPlayedPlaylistDuration || 0) - + (timing.expectedDuration ?? timingContext.totalPlaylistDuration ?? 0) + } else if (PlaylistTiming.isPlaylistDurationTimed(timing)) { + diff = + (timingContext.asPlayedPlaylistDuration || 0) - (timing.expectedDuration ?? timingContext.totalPlaylistDuration ?? 0) - : frontAnchor + (timingContext.remainingPlaylistDuration || 0) - backAnchor + } else { + diff = frontAnchor + (timingContext.remainingPlaylistDuration || 0) - backAnchor + } // handle special cases @@ -877,9 +888,11 @@ export function getPlaylistTimingDiff( diff = (timingContext.asPlayedPlaylistDuration || 0) - (timing.expectedDuration ?? timingContext.totalPlaylistDuration ?? 0) - } else if (PlaylistTiming.isPlaylistTimingBackTime(timing)) { - // we want to see how late we've ended compared to the expectedEnd - diff = startedPlayback + (timingContext.asPlayedPlaylistDuration || 0) - timing.expectedEnd + } else if (PlaylistTiming.isPlaylistDurationTimed(timing)) { + // we want to know how heavy/light we were compared to the original plan + diff = + (timingContext.asPlayedPlaylistDuration || 0) - + (timing.expectedDuration ?? timingContext.totalPlaylistDuration ?? 0) } } diff --git a/packages/webui/src/client/lib/shelf.ts b/packages/webui/src/client/lib/shelf.ts index ab0068c8128..29819ce6b24 100644 --- a/packages/webui/src/client/lib/shelf.ts +++ b/packages/webui/src/client/lib/shelf.ts @@ -1,19 +1,19 @@ import _ from 'underscore' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { createPartCurrentTimes, processAndPrunePieceInstanceTimings, } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { getUnfinishedPieceInstancesReactive } from './rundownLayouts.js' -import { PieceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { PieceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PieceInstances } from '../collections/index.js' -import { ReadonlyDeep } from 'type-fest' -import { AdLibPieceUi } from '@sofie-automation/meteor-lib/dist/uiTypes/Adlib' +import type { ReadonlyDeep } from 'type-fest' +import type { AdLibPieceUi } from '@sofie-automation/meteor-lib/dist/uiTypes/Adlib' import { getCurrentTimeReactive } from './currentTimeReactive' -import { UIShowStyleBase } from '@sofie-automation/corelib/src/dataModel/ShowStyleBase.js' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import type { UIShowStyleBase } from '@sofie-automation/corelib/src/dataModel/ShowStyleBase.js' +import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' export type { AdLibPieceUi } from '@sofie-automation/meteor-lib/dist/uiTypes/Adlib' diff --git a/packages/webui/src/client/lib/systemTime.ts b/packages/webui/src/client/lib/systemTime.ts index b0cbd87a15a..ded0db135e8 100644 --- a/packages/webui/src/client/lib/systemTime.ts +++ b/packages/webui/src/client/lib/systemTime.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor' import { logger } from './logging.js' import { MeteorCall } from './meteorApi.js' -import { Time } from '@sofie-automation/shared-lib/dist/lib/lib' +import type { Time } from '@sofie-automation/shared-lib/dist/lib/lib' export const systemTime = { hasBeenSet: false, diff --git a/packages/webui/src/client/lib/tTimerUtils.ts b/packages/webui/src/client/lib/tTimerUtils.ts index 637c8d75e54..dd81678dbec 100644 --- a/packages/webui/src/client/lib/tTimerUtils.ts +++ b/packages/webui/src/client/lib/tTimerUtils.ts @@ -1,4 +1,7 @@ -import { RundownTTimer, timerStateToDuration } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + type RundownTTimer, + timerStateToDuration, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' /** * Calculate the display diff for a T-Timer. diff --git a/packages/webui/src/client/lib/triggers/ActionAdLibHotkeyPreview.tsx b/packages/webui/src/client/lib/triggers/ActionAdLibHotkeyPreview.tsx index c2550ea6d8b..93af6405fbc 100644 --- a/packages/webui/src/client/lib/triggers/ActionAdLibHotkeyPreview.tsx +++ b/packages/webui/src/client/lib/triggers/ActionAdLibHotkeyPreview.tsx @@ -1,17 +1,17 @@ -import { ISourceLayer } from '@sofie-automation/blueprints-integration' -import React, { useContext, useState, useEffect } from 'react' +import type { ISourceLayer } from '@sofie-automation/blueprints-integration' +import { useContext, useState, useEffect } from 'react' import { assertNever } from '@sofie-automation/corelib/dist/lib' import { useTracker } from '../ReactMeteorData/ReactMeteorData.js' import { SorensenContext } from '../SorensenContext.js' import { MountedAdLibTriggers } from './TriggersHandler.js' import { codesToKeyLabels } from './codesToKeyLabels.js' -import { AdLibActionId, PieceId, RundownBaselineAdLibActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { AdLibActionId, PieceId, RundownBaselineAdLibActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { - MountedAdLibTrigger, + type MountedAdLibTrigger, MountedAdLibTriggerType, - MountedHotkeyMixin, + type MountedHotkeyMixin, } from '@sofie-automation/meteor-lib/dist/api/MountedTriggers' -import { FindOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import type { FindOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' type IProps = | { diff --git a/packages/webui/src/client/lib/triggers/TriggersHandler.tsx b/packages/webui/src/client/lib/triggers/TriggersHandler.tsx index 14676f51cd8..652ee15182a 100644 --- a/packages/webui/src/client/lib/triggers/TriggersHandler.tsx +++ b/packages/webui/src/client/lib/triggers/TriggersHandler.tsx @@ -1,30 +1,38 @@ -import * as React from 'react' +import type * as React from 'react' import { useEffect, useRef, useState } from 'react' -import { TFunction } from 'i18next' +import type { TFunction } from 'i18next' import { useTranslation } from 'react-i18next' import Sorensen from '@sofie-automation/sorensen' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { useSubscription, useTracker } from '../ReactMeteorData/ReactMeteorData.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { PlayoutActions, SomeAction, SomeBlueprintTrigger, TriggerType } from '@sofie-automation/blueprints-integration' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { + PlayoutActions, + type SomeAction, + type SomeBlueprintTrigger, + TriggerType, +} from '@sofie-automation/blueprints-integration' import { isPreviewableAction, - ReactivePlaylistActionContext, + type ReactivePlaylistActionContext, createAction as libCreateAction, } from '@sofie-automation/meteor-lib/dist/triggers/actionFactory' import { flatten } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { IWrappedAdLib } from '@sofie-automation/meteor-lib/dist/triggers/actionFilterChainCompilers' +import type { IWrappedAdLib } from '@sofie-automation/meteor-lib/dist/triggers/actionFilterChainCompilers' import { ReactiveVar } from 'meteor/reactive-var' import { preventDefault } from '../SorensenContext.js' import { getFinalKey } from './codesToKeyLabels.js' -import { RundownViewEvents, TriggerActionEvent } from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' +import { + RundownViewEvents, + type TriggerActionEvent, +} from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' import { Tracker } from 'meteor/tracker' import { Settings } from '../../lib/Settings.js' import { createInMemorySyncMongoCollection } from '../../collections/lib.js' import { RundownPlaylists } from '../../collections/index.js' import { UIShowStyleBases, UITriggeredActions } from '../../ui/Collections.js' -import { +import type { PartId, RundownId, RundownPlaylistId, @@ -32,7 +40,7 @@ import { StudioId, TriggeredActionId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { +import type { MountedAdLibTrigger, MountedGenericTrigger, MountedHotkeyMixin, @@ -43,7 +51,7 @@ import { catchError, useRundownViewEventBusListener } from '../lib.js' import { logger } from '../logging.js' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { toTriggersComputation, toTriggersReactiveVar, UiTriggersContext } from './triggersContext.js' -import { UIShowStyleBase } from '@sofie-automation/corelib/src/dataModel/ShowStyleBase.js' +import type { UIShowStyleBase } from '@sofie-automation/corelib/src/dataModel/ShowStyleBase.js' type HotkeyTriggerListener = (e: KeyboardEvent) => void diff --git a/packages/webui/src/client/lib/triggers/triggersContext.ts b/packages/webui/src/client/lib/triggers/triggersContext.ts index 4499a7a8db0..81057a216d6 100644 --- a/packages/webui/src/client/lib/triggers/triggersContext.ts +++ b/packages/webui/src/client/lib/triggers/triggersContext.ts @@ -1,17 +1,16 @@ -import { +import type { TriggersAsyncCollection, TriggersContext, TriggerTrackerComputation, } from '@sofie-automation/meteor-lib/dist/triggers/triggersContext' import { hashSingleUseToken } from '../lib.js' import { MeteorCall } from '../meteorApi.js' -import { IBaseFilterLink } from '@sofie-automation/blueprints-integration' +import type { IBaseFilterLink } from '@sofie-automation/blueprints-integration' import { doUserAction } from '../clientUserAction.js' import { Tracker } from 'meteor/tracker' import { AdLibActions, AdLibPieces, - Parts, RundownBaselineAdLibActions, RundownBaselineAdLibPieces, RundownPlaylists, @@ -19,14 +18,15 @@ import { Segments, } from '../../collections/index.js' import { logger } from '../logging.js' -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ReactivePlaylistActionContext } from '@sofie-automation/meteor-lib/dist/triggers/actionFactory' -import { FindOneOptions, MongoReadOnlyCollection } from '../../collections/lib.js' -import { ProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import { ReactiveVar as MeteorReactiveVar } from 'meteor/reactive-var' -import { TriggerReactiveVar } from '@sofie-automation/meteor-lib/dist/triggers/reactive-var' -import { FindOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import type { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { ReactivePlaylistActionContext } from '@sofie-automation/meteor-lib/dist/triggers/actionFactory' +import type { FindOneOptions, MongoReadOnlyCollection } from '../../collections/lib.js' +import type { ProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import type { ReactiveVar as MeteorReactiveVar } from 'meteor/reactive-var' +import type { TriggerReactiveVar } from '@sofie-automation/meteor-lib/dist/triggers/reactive-var' +import type { FindOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { memoizedIsolatedAutorunAsync } from '../memoizedIsolatedAutorun.js' +import { UIParts } from '../../ui/Collections.js' class UiTriggersCollectionWrapper< DBInterface extends { _id: ProtectedString }, @@ -67,7 +67,7 @@ export const UiTriggersContext: TriggersContext = { AdLibActions: new UiTriggersCollectionWrapper(AdLibActions), AdLibPieces: new UiTriggersCollectionWrapper(AdLibPieces), - Parts: new UiTriggersCollectionWrapper(Parts), + Parts: new UiTriggersCollectionWrapper(UIParts), RundownBaselineAdLibActions: new UiTriggersCollectionWrapper(RundownBaselineAdLibActions), RundownBaselineAdLibPieces: new UiTriggersCollectionWrapper(RundownBaselineAdLibPieces), RundownPlaylists: new UiTriggersCollectionWrapper(RundownPlaylists), diff --git a/packages/webui/src/client/lib/ui/icons/freezeFrame.tsx b/packages/webui/src/client/lib/ui/icons/freezeFrame.tsx index 9e68fc1a0cf..11eeede5ef0 100644 --- a/packages/webui/src/client/lib/ui/icons/freezeFrame.tsx +++ b/packages/webui/src/client/lib/ui/icons/freezeFrame.tsx @@ -1,5 +1,3 @@ -import React from 'react' - export function FreezeFrameIcon(props: Readonly>): JSX.Element { return ( diff --git a/packages/webui/src/client/lib/ui/icons/listView.tsx b/packages/webui/src/client/lib/ui/icons/listView.tsx index c2f4572aa36..3e7eaf07f33 100644 --- a/packages/webui/src/client/lib/ui/icons/listView.tsx +++ b/packages/webui/src/client/lib/ui/icons/listView.tsx @@ -1,5 +1,3 @@ -import React from 'react' - export function SegmentViewMode(props: Readonly>): JSX.Element { return ( diff --git a/packages/webui/src/client/lib/ui/icons/looping.tsx b/packages/webui/src/client/lib/ui/icons/looping.tsx index 54b5023a84b..2301543035c 100644 --- a/packages/webui/src/client/lib/ui/icons/looping.tsx +++ b/packages/webui/src/client/lib/ui/icons/looping.tsx @@ -1,6 +1,6 @@ -import React, { JSX, useEffect, useRef } from 'react' +import { useEffect, useRef, type JSX } from 'react' import loopAnimation from './icon-loop.json' -import Lottie, { LottieComponentProps, LottieRefCurrentProps } from 'lottie-react' +import Lottie, { type LottieComponentProps, type LottieRefCurrentProps } from 'lottie-react' export function LoopingIcon(props?: Readonly>): JSX.Element { return ( diff --git a/packages/webui/src/client/lib/ui/icons/mediaStatus.tsx b/packages/webui/src/client/lib/ui/icons/mediaStatus.tsx index d1894fe639c..77e80e6d7dc 100644 --- a/packages/webui/src/client/lib/ui/icons/mediaStatus.tsx +++ b/packages/webui/src/client/lib/ui/icons/mediaStatus.tsx @@ -1,4 +1,4 @@ -import { JSX } from 'react' +import type { JSX } from 'react' export function MediaStatusIcon(): JSX.Element { return ( diff --git a/packages/webui/src/client/lib/ui/icons/notifications.tsx b/packages/webui/src/client/lib/ui/icons/notifications.tsx index dfda6cc49c4..7446819e1a1 100644 --- a/packages/webui/src/client/lib/ui/icons/notifications.tsx +++ b/packages/webui/src/client/lib/ui/icons/notifications.tsx @@ -1,4 +1,4 @@ -import { JSX } from 'react' +import type { JSX } from 'react' import { relativeToSiteRootUrl } from '../../../url.js' export function CriticalIcon(): JSX.Element { diff --git a/packages/webui/src/client/lib/ui/icons/segment.tsx b/packages/webui/src/client/lib/ui/icons/segment.tsx index 376f8c3a244..4ceb43887be 100644 --- a/packages/webui/src/client/lib/ui/icons/segment.tsx +++ b/packages/webui/src/client/lib/ui/icons/segment.tsx @@ -1,5 +1,3 @@ -import React from 'react' - export const RightArrow = (props: Readonly>): JSX.Element => ( diff --git a/packages/webui/src/client/lib/ui/icons/shelf.tsx b/packages/webui/src/client/lib/ui/icons/shelf.tsx index 32a52dd71f8..68586e163fc 100644 --- a/packages/webui/src/client/lib/ui/icons/shelf.tsx +++ b/packages/webui/src/client/lib/ui/icons/shelf.tsx @@ -1,5 +1,6 @@ import { faPencil, faTrashCan } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import classNames from 'classnames' export function EmptyBucket({ className }: { className?: string }): JSX.Element { return ( @@ -71,3 +72,41 @@ export function BucketHandle({ className }: { className?: string }): JSX.Element ) } + +export function CompactViewIcon(props: React.SVGProps): JSX.Element { + return ( + + + + + ) +} + +export function LargeViewIcon(props: React.SVGProps): JSX.Element { + return ( + + + + + + + + + ) +} diff --git a/packages/webui/src/client/lib/ui/icons/sorting.tsx b/packages/webui/src/client/lib/ui/icons/sorting.tsx index a8bd8b274c5..c9d852273e6 100644 --- a/packages/webui/src/client/lib/ui/icons/sorting.tsx +++ b/packages/webui/src/client/lib/ui/icons/sorting.tsx @@ -1,4 +1,4 @@ -import { JSX } from 'react' +import type { JSX } from 'react' export function SortDescending(): JSX.Element { return ( diff --git a/packages/webui/src/client/lib/ui/icons/switchboard.tsx b/packages/webui/src/client/lib/ui/icons/switchboard.tsx index 94c57e5f2c8..00e855e4611 100644 --- a/packages/webui/src/client/lib/ui/icons/switchboard.tsx +++ b/packages/webui/src/client/lib/ui/icons/switchboard.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import type * as React from 'react' export function SwitchboardIcon(props: Readonly>): JSX.Element { return ( diff --git a/packages/webui/src/client/lib/ui/icons/useredits.tsx b/packages/webui/src/client/lib/ui/icons/useredits.tsx index 79d913d1c9e..2946631d7d8 100644 --- a/packages/webui/src/client/lib/ui/icons/useredits.tsx +++ b/packages/webui/src/client/lib/ui/icons/useredits.tsx @@ -1,4 +1,4 @@ -import { JSX } from 'react' +import type { JSX } from 'react' export function UserEditsIcon(): JSX.Element { return ( diff --git a/packages/webui/src/client/lib/ui/pieceUiClassNames.ts b/packages/webui/src/client/lib/ui/pieceUiClassNames.ts index 2e172de9d3d..de1e0565679 100644 --- a/packages/webui/src/client/lib/ui/pieceUiClassNames.ts +++ b/packages/webui/src/client/lib/ui/pieceUiClassNames.ts @@ -1,9 +1,9 @@ -import { PieceLifespan, SourceLayerType } from '@sofie-automation/blueprints-integration' -import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PieceStatusCode, PieceUi } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { PieceLifespan, type SourceLayerType } from '@sofie-automation/blueprints-integration' +import type { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PieceStatusCode, type PieceUi } from '@sofie-automation/corelib/dist/dataModel/Piece' import classNames from 'classnames' -import { ReadonlyDeep } from 'type-fest' -import { PieceContentStatusObj } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' +import type { ReadonlyDeep } from 'type-fest' +import type { PieceContentStatusObj } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' import { RundownUtils } from '../rundown.js' export function pieceUiClassNames( diff --git a/packages/webui/src/client/lib/ui/splitPreview.ts b/packages/webui/src/client/lib/ui/splitPreview.ts index 85f7433cb73..12bce3abafe 100644 --- a/packages/webui/src/client/lib/ui/splitPreview.ts +++ b/packages/webui/src/client/lib/ui/splitPreview.ts @@ -1,10 +1,12 @@ import { SourceLayerType, - SplitsContentBoxContent, - SplitsContentBoxProperties, + type SplitsContentBoxContent, + type SplitsContentBoxProperties, + type VTContent, } from '@sofie-automation/blueprints-integration' +import type { SplitBoxPreviewUrls } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' import { literal } from '@sofie-automation/corelib/dist/lib' -import { ReadonlyDeep } from 'type-fest' +import type { ReadonlyDeep } from 'type-fest' const DEFAULT_POSITIONS = [ { @@ -28,23 +30,36 @@ export interface SplitSubItem { _id: string type: SourceLayerType label: string - // TODO: To be replaced with the structure used by the Core role: SplitRole content?: SplitsContentBoxProperties['geometry'] + thumbnailUrl?: string + previewUrl?: string + /** VT/LIVE_SPEAK editorial seek (ms), used for in-box hover scrub */ + seek?: number } export function getSplitPreview( boxSourceConfiguration: | (SplitsContentBoxContent & SplitsContentBoxProperties)[] - | ReadonlyDeep<(SplitsContentBoxContent & SplitsContentBoxProperties)[]> + | ReadonlyDeep<(SplitsContentBoxContent & SplitsContentBoxProperties)[]>, + boxPreviews?: ReadonlyDeep ): ReadonlyArray> { return boxSourceConfiguration.map((item, index) => { + const boxPreview = boxPreviews?.[index] + const seek = + item.type === SourceLayerType.VT || item.type === SourceLayerType.LIVE_SPEAK + ? (item as VTContent).seek + : undefined + return literal>({ _id: item.studioLabel + '_' + index, type: item.type, label: item.studioLabel, role: SplitRole.BOX, content: item.geometry || DEFAULT_POSITIONS[index], + thumbnailUrl: boxPreview?.thumbnailUrl, + previewUrl: boxPreview?.previewUrl, + seek, }) }) } diff --git a/packages/webui/src/client/lib/ui/splitsPreviewVideo.ts b/packages/webui/src/client/lib/ui/splitsPreviewVideo.ts new file mode 100644 index 00000000000..932b082abe0 --- /dev/null +++ b/packages/webui/src/client/lib/ui/splitsPreviewVideo.ts @@ -0,0 +1,47 @@ +import { SourceLayerType, type SplitsContent } from '@sofie-automation/blueprints-integration' +import type { PieceContentStatusObj } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' +import type { ReadonlyDeep } from 'type-fest' + +export type PreviewVideoContentUI = { + type: 'video' + src: string + itemDuration?: number + seek?: number + loop?: boolean +} + +export interface SplitsBoxLayoutScrubSettings { + itemDuration: number + loop?: boolean +} + +/** True when at least one VT/LIVE_SPEAK box has a preview URL to scrub in the box layout. */ +export function getSplitsBoxLayoutScrubSettings( + content: ReadonlyDeep, + contentStatus: ReadonlyDeep | undefined +): SplitsBoxLayoutScrubSettings | undefined { + const boxes = content.boxSourceConfiguration ?? [] + + for (let i = 0; i < boxes.length; i++) { + const box = boxes[i] + if (box.type !== SourceLayerType.VT && box.type !== SourceLayerType.LIVE_SPEAK) { + continue + } + + if (contentStatus?.boxPreviews?.[i]?.previewUrl) { + return { + itemDuration: content.sourceDuration ?? contentStatus?.contentDuration ?? 0, + loop: content.loop, + } + } + } + + return undefined +} + +export function getPieceScrubDurationMs( + content: ReadonlyDeep<{ sourceDuration?: number }> | undefined, + contentStatus: ReadonlyDeep | undefined +): number { + return content?.sourceDuration ?? contentStatus?.contentDuration ?? 0 +} diff --git a/packages/webui/src/client/lib/ui/videoPreviewScrub.ts b/packages/webui/src/client/lib/ui/videoPreviewScrub.ts new file mode 100644 index 00000000000..dbae78d7aa4 --- /dev/null +++ b/packages/webui/src/client/lib/ui/videoPreviewScrub.ts @@ -0,0 +1,16 @@ +export function setVideoElementPosition( + vEl: HTMLVideoElement, + timePosition: number, + itemDuration: number, + seek: number, + loop: boolean +): void { + let targetTime = timePosition + seek + if (loop && vEl.duration > 0) { + const loopWindowMs = itemDuration > 0 ? Math.min(vEl.duration * 1000, itemDuration) : vEl.duration * 1000 + targetTime = ((targetTime % loopWindowMs) + loopWindowMs) % loopWindowMs + } else if (itemDuration > 0) { + targetTime = Math.max(0, Math.min(targetTime, itemDuration)) + } + vEl.currentTime = targetTime / 1000 +} diff --git a/packages/webui/src/client/lib/uncaughtErrorHandler.ts b/packages/webui/src/client/lib/uncaughtErrorHandler.ts index 62a5873c684..ef5db16cf36 100644 --- a/packages/webui/src/client/lib/uncaughtErrorHandler.ts +++ b/packages/webui/src/client/lib/uncaughtErrorHandler.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { Tracker } from 'meteor/tracker' -import { Time } from '@sofie-automation/blueprints-integration' +import type { Time } from '@sofie-automation/blueprints-integration' import { getCurrentTime } from './systemTime.js' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { MeteorCall } from '../lib/meteorApi.js' diff --git a/packages/webui/src/client/lib/utilComponents.tsx b/packages/webui/src/client/lib/utilComponents.tsx index 9780fd0d7ba..14cb9d219c8 100644 --- a/packages/webui/src/client/lib/utilComponents.tsx +++ b/packages/webui/src/client/lib/utilComponents.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import type * as React from 'react' import _ from 'underscore' // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types diff --git a/packages/webui/src/client/lib/viewPort.ts b/packages/webui/src/client/lib/viewPort.ts index 7c84d6db19f..9810b0a7fda 100644 --- a/packages/webui/src/client/lib/viewPort.ts +++ b/packages/webui/src/client/lib/viewPort.ts @@ -2,7 +2,7 @@ import { SEGMENT_TIMELINE_ELEMENT_ID } from '../ui/SegmentTimeline/SegmentTimeli import { isProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import RundownViewEventBus, { RundownViewEvents } from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' import { Settings } from '../lib/Settings.js' -import { PartId, PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { PartId, PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { UIPartInstances, UIParts } from '../ui/Collections.js' import { logger } from './logging.js' import { parse as queryStringParse } from 'query-string' @@ -22,6 +22,20 @@ const viewPortScrollingState = { lastProgrammaticScrollTime: 0, } +function clearPendingScrollState(): void { + if (pendingFirstStageTimeout) { + clearTimeout(pendingFirstStageTimeout) + pendingFirstStageTimeout = undefined + } + + if (pendingSecondStageScroll) { + window.cancelIdleCallback(pendingSecondStageScroll) + pendingSecondStageScroll = undefined + } + + currentScrollingElement = undefined +} + export function getViewPortScrollingState(): { isProgrammaticScrollInProgress: boolean lastProgrammaticScrollTime: number @@ -48,6 +62,7 @@ export function maintainFocusOnPartInstance( // Handle error if needed } finally { focusState.isScrolling = false + viewPortScrollingState.lastProgrammaticScrollTime = Date.now() } } else if (Date.now() - focusState.startTime >= timeWindow) { quitFocusOnPart() @@ -91,6 +106,24 @@ function quitFocusOnPart() { clearInterval(focusState.interval) focusState.interval = undefined } + + if (!focusState.isScrolling) { + viewPortScrollingState.isProgrammaticScrollInProgress = false + viewPortScrollingState.lastProgrammaticScrollTime = Date.now() + } +} + +export function resetViewportScrollState(): void { + quitFocusOnPart() + clearPendingScrollState() + viewPortScrollingState.isProgrammaticScrollInProgress = false +} + +export function clearViewportLifecycleState(): void { + resetViewportScrollState() + viewPortScrollingState.lastProgrammaticScrollTime = 0 + focusState.isScrolling = false + focusState.startTime = 0 } export async function scrollToPartInstance( @@ -156,6 +189,8 @@ export async function scrollToSegment( forceScroll?: boolean, noAnimation?: boolean ): Promise { + clearPendingScrollState() + const elementToScrollTo: HTMLElement | null = getElementToScrollTo(elementToScrollToOrSegmentId, false) const historyTarget: HTMLElement | null = getElementToScrollTo(elementToScrollToOrSegmentId, true) @@ -257,6 +292,7 @@ async function innerScrollToSegment( // If not in place atempt to scroll again innerScrollToSegment(elementToScrollTo, forceScroll, true, true).then(resolve, reject) } else { + currentScrollingElement = undefined resolve(true) } }, 1000) // When UI is getting optimized further we could lower this value @@ -268,11 +304,13 @@ async function innerScrollToSegment( }, (error) => { if (!error.toString().match(/another scroll/)) logger.error(error) + currentScrollingElement = undefined return false } ) } + currentScrollingElement = undefined return Promise.resolve(false) } @@ -315,7 +353,9 @@ export async function scrollToPosition(scrollPosition: number, noAnimation?: boo behavior: 'smooth', }) await new Promise((resolve) => setTimeout(resolve, 300)) + viewPortScrollingState.isProgrammaticScrollInProgress = false + viewPortScrollingState.lastProgrammaticScrollTime = Date.now() } } diff --git a/packages/webui/src/client/styles/_checkerboard.scss b/packages/webui/src/client/styles/_checkerboard.scss new file mode 100644 index 00000000000..05963fb9154 --- /dev/null +++ b/packages/webui/src/client/styles/_checkerboard.scss @@ -0,0 +1,25 @@ +// Transparency indicator (matches origo-ui `.checkerboard-bg`). +@mixin checkerboard-background { + background-color: #000; + background-image: + linear-gradient(45deg, #191919 25%, transparent 25%), linear-gradient(-45deg, #191919 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #191919 75%), linear-gradient(-45deg, transparent 75%, #191919 75%); + background-size: 10px 10px; + background-position: + 0 0, + 0 5px, + 5px -5px, + -5px 0; +} + +.checkerboard-bg { + @include checkerboard-background; +} + +.checkerboard-bg .image-container { + background-position: center; + background-repeat: no-repeat; + background-size: contain; + width: 100%; + height: 100%; +} diff --git a/packages/webui/src/client/styles/_colorScheme.scss b/packages/webui/src/client/styles/_colorScheme.scss index 1967769b3aa..a0283d7a3b7 100644 --- a/packages/webui/src/client/styles/_colorScheme.scss +++ b/packages/webui/src/client/styles/_colorScheme.scss @@ -19,6 +19,7 @@ $general-live-remote-color: var(--general-live-remote-color); $general-live-guest-color: var(--general-live-guest-color); $general-late-color: var(--general-late-color); +$over-under-over-color: var(--over-under-over-color); $general-fast-color: var(--general-fast-color); $general-fast-color--shadow: var(--general-fast-color--shadow); $general-countdown-to-next-color: var(--general-countdown-to-next-color); diff --git a/packages/webui/src/client/styles/_cssVariables.ts b/packages/webui/src/client/styles/_cssVariables.ts index 6ec8fc92836..d634b1f49c8 100644 --- a/packages/webui/src/client/styles/_cssVariables.ts +++ b/packages/webui/src/client/styles/_cssVariables.ts @@ -1,4 +1,4 @@ -import * as React from 'react' +import type * as React from 'react' /** Add typings for custom css-variables */ export interface CSSProperties extends React.CSSProperties { '--invalid-reason-color-opaque': string diff --git a/packages/webui/src/client/styles/countdown/director.scss b/packages/webui/src/client/styles/countdown/director.scss index 0c034624ee1..1d1592a73cf 100644 --- a/packages/webui/src/client/styles/countdown/director.scss +++ b/packages/webui/src/client/styles/countdown/director.scss @@ -17,6 +17,13 @@ $hold-status-color: $liveline-timecode-color; font-family: Roboto Flex; font-style: normal; + // Over/under timer overlay (reuses `screen-timing-clock` from prompter view) + // Ensure it layers above the fixed top bar. + .screen-timing-clock { + z-index: 2000; + font-size: 9vmin; + } + .director-screen__top { position: fixed; top: 0; @@ -35,24 +42,24 @@ $hold-status-color: $liveline-timecode-color; padding: 0 0.2em; text-transform: uppercase; + .director-screen__top__spacer { + flex: 1 1 0; + } + .director-screen__top__planned-end { + flex: 1 1 0; text-align: left; + transform: translateY(0.35em); } - .director-screen__top__time-to { - text-align: center; + .director-screen__top__planned-container { + flex: 1 1 0; } + .director-screen__top__center, .director-screen__top__planned-to { text-align: center; } - .director-screen__top__planned-since { - margin-left: -50px; - } - - .director-screen__top__over-under { - margin-left: 5vw; - } } .director-screen__body { @@ -208,14 +215,14 @@ $hold-status-color: $liveline-timecode-color; text-align: center; width: 100vw; margin-left: -13vw; - + .director-screen__body__part__timeto-name { color: #888; font-size: 9.63em; text-transform: uppercase; margin-top: -2vh; } - + .director-screen__body__part__timeto-countdown { margin-top: 4vh; grid-row: inherit; @@ -480,75 +487,13 @@ $hold-status-color: $liveline-timecode-color; .clocks-counter-heavy { font-weight: 600; } - - .director-screen__body__t-timer { - position: absolute; - bottom: 0; - right: 0; - text-align: right; - font-size: 5vh; - z-index: 10; - line-height: 1; - - .t-timer-display { - display: flex; - align-items: stretch; - justify-content: flex-end; - font-weight: 500; - background: #333; - border-radius: 0; - overflow: hidden; - - &__label { - display: flex; - align-items: center; - color: #fff; - padding-left: 0.4em; - padding-right: 0.2em; - font-size: 1em; - text-transform: uppercase; - letter-spacing: 0.05em; - font-stretch: condensed; - } - - &__value { - display: flex; - align-items: center; - color: #fff; - font-variant-numeric: tabular-nums; - padding: 0 0.2em; - font-size: 1em; - - .t-timer-display__part { - &--dimmed { - color: #aaa; - } - } - } - - &__over-under { - display: flex; - align-items: center; - justify-content: center; - margin: 0 0 0 0.2em; - font-size: 1em; - font-variant-numeric: tabular-nums; - padding: 0 0.4em; - line-height: 1.1; - min-width: 3.5em; - border-radius: 1em; - - &--over { - background-color: $general-late-color; - color: #fff; - } - - &--under { - background-color: #ffe900; - color: #000; - } - } - } - } + } + .director-screen__bottom-bar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 20; + font-size: 4.58vh; } } diff --git a/packages/webui/src/client/styles/countdown/presenter.scss b/packages/webui/src/client/styles/countdown/presenter.scss index df9a20d66a0..498d58d4701 100644 --- a/packages/webui/src/client/styles/countdown/presenter.scss +++ b/packages/webui/src/client/styles/countdown/presenter.scss @@ -15,10 +15,7 @@ $hold-status-color: $liveline-timecode-color; .presenter-screen { position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; + inset: 0; font-size: 1vh; @@ -28,6 +25,16 @@ $hold-status-color: $liveline-timecode-color; overflow: hidden; white-space: nowrap; + // Over/under timer (reuses `screen-timing-clock` class from prompter view), + // but presenter view has a tiny base font-size (1vh), so we need explicit sizing + layering. + .screen-timing-clock { + position: fixed; + left: auto; + font-size: 9vmin; + z-index: 2000; + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5); + } + .presenter-screen__part { display: grid; grid-template: @@ -47,9 +54,17 @@ $hold-status-color: $liveline-timecode-color; font-weight: bold; &.live { - background: $general-live-color; + // Layer the director-style gradient over the live color + background: + linear-gradient( + 90deg, + rgba(223, 0, 0, 0) 0%, + rgba(223, 0, 0, 0) 60%, + rgba(116, 0, 0, 0.808) 70%, + rgba(0, 0, 0, 0.8) 75% + ), + $general-live-color; color: #fff; - border-top: 0.1em solid #fff; -webkit-text-stroke: black; -webkit-text-stroke-width: 0.025em; text-shadow: 0px 0px 20px #00000044; @@ -58,7 +73,6 @@ $hold-status-color: $liveline-timecode-color; &.next { background: $general-next-color; color: #000; - border-top: 0.1em solid #fff; } } @@ -161,102 +175,6 @@ $hold-status-color: $liveline-timecode-color; } } - .presenter-screen__rundown-status-bar { - display: grid; - grid-template-columns: auto fit-content(20em) fit-content(5em); - grid-template-rows: fit-content(1em); - font-size: 6em; - color: #888; - padding: 0 0.2em; - - .presenter-screen__rundown-status-bar__rundown-name { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - line-height: 1.44em; - } - - .presenter-screen__rundown-status-bar__t-timer { - margin-right: 1em; - font-size: 0.8em; - align-self: center; - justify-self: end; - - .t-timer-display { - display: flex; - align-items: stretch; - justify-content: flex-end; - font-weight: 500; - background: #333; - border-radius: 0; - overflow: hidden; - - &__label { - display: flex; - align-items: center; - color: #fff; - padding-left: 0.4em; - padding-right: 0.2em; - font-size: 1em; - text-transform: uppercase; - letter-spacing: 0.05em; - font-stretch: condensed; - } - - &__value { - display: flex; - align-items: center; - color: #fff; - font-variant-numeric: tabular-nums; - padding: 0 0.2em; - font-size: 1em; - - .t-timer-display__part { - &--dimmed { - color: #aaa; - } - } - } - - &__over-under { - display: flex; - align-items: center; - justify-content: center; - margin: 0 0 0 0.2em; - font-size: 1em; - font-variant-numeric: tabular-nums; - padding: 0 0.4em; - line-height: 1.1; - min-width: 3.5em; - border-radius: 1em; - - &--over { - background-color: $general-late-color; - color: #fff; - } - - &--under { - background-color: #ffe900; - color: #000; - } - } - } - } - - .presenter-screen__rundown-status-bar__countdown { - white-space: nowrap; - - color: $general-countdown-to-next-color; - - font-weight: 600; - font-size: 1.2em; - - &.over { - color: $general-late-color; - } - } - } - .presenter-screen__part + .presenter-screen__part { border-top: solid 0.8em #454545; } @@ -411,4 +329,7 @@ $hold-status-color: $liveline-timecode-color; } } } + .rundown-status-bar { + font-size: 6.95vmin; + } } diff --git a/packages/webui/src/client/styles/counterComponents.scss b/packages/webui/src/client/styles/counterComponents.scss index 36b206f604b..5adbcc5146c 100644 --- a/packages/webui/src/client/styles/counterComponents.scss +++ b/packages/webui/src/client/styles/counterComponents.scss @@ -62,7 +62,7 @@ display: inline-block; } .over { - background-color: #ff5218; + background-color: $over-under-over-color; border-radius: 139px; align-self: stretch; gap: 10px; @@ -92,20 +92,35 @@ .counter-component__planned-end { color: #ffffff; - letter-spacing: 0%; text-align: right; + line-height: 1; - letter-spacing: 0.01em; + letter-spacing: 0.02em; font-variation-settings: - 'slnt' -7, - 'wdth' 70, - 'wght' 500; + 'wdth' 85, + 'wght' 400, + 'slnt' -5, + 'GRAD' 0, + 'opsz' 40, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; } .counter-component__time-to-planned-end { - color: $general-countdown-to-next-color; + color: #ffffff; letter-spacing: 0%; + &.countdown { + justify-content: center; + transition: none; + } + letter-spacing: 0.01em; font-variation-settings: 'GRAD' 0, @@ -121,12 +136,21 @@ 'slnt' 0, 'wdth' 70, 'wght' 500; + + .countdown__counter { + color: inherit; + margin-left: 0; + } } .counter-component__time-since-planned-end { - color: #ff5218; - text-align: right; - margin-left: 1.2vw; + color: $over-under-over-color; + text-align: center; + + &.countdown { + justify-content: center; + transition: none; + } letter-spacing: 0.01em; font-variation-settings: @@ -143,4 +167,9 @@ 'slnt' 0, 'wdth' 70, 'wght' 600; + + .countdown__counter { + color: inherit; + margin-left: 0; + } } diff --git a/packages/webui/src/client/styles/defaultColors.scss b/packages/webui/src/client/styles/defaultColors.scss index 51ceac9472b..17cb732d08b 100644 --- a/packages/webui/src/client/styles/defaultColors.scss +++ b/packages/webui/src/client/styles/defaultColors.scss @@ -22,6 +22,7 @@ --general-live-guest-color: #008a92; --general-late-color: #ff0000; + --over-under-over-color: #ff5218; --general-fast-color: #ff0000; --general-countdown-to-next-color: #ffff00; --general-freeze-color: #00bfff; diff --git a/packages/webui/src/client/styles/main.scss b/packages/webui/src/client/styles/main.scss index 254f12efbec..07146102034 100644 --- a/packages/webui/src/client/styles/main.scss +++ b/packages/webui/src/client/styles/main.scss @@ -19,6 +19,7 @@ input { @import 'colorScheme'; @import '_variables'; @import '_colorScheme'; +@import 'checkerboard'; @import '_header'; @@ -38,6 +39,8 @@ input { @import 'overflowingContainer'; @import 'pieceStatusIcon'; @import 'prompter'; +@import 'tTimerDisplay'; +@import 'rundownStatusBar'; @import 'rundownList'; @import 'rundownSystemStatus'; @import 'settings'; @@ -65,7 +68,6 @@ input { @import 'shelf/dashboard-streamdeck'; @import 'shelf/dashboard'; @import 'shelf/endTimerPanel'; -@import 'shelf/endWordsPanel'; @import 'shelf/externalFramePanel'; @import 'shelf/inspector'; @import 'shelf/miniRundownPanel'; @@ -77,44 +79,43 @@ input { @import 'shelf/segmentNamePanel'; @import 'shelf/segmentTimingPanel'; @import 'shelf/shelf'; -@import 'shelf/showStylePanel'; @import 'shelf/startTimerPanel'; @import 'shelf/studioNamePanel'; -@import 'shelf/systemStatusPanel'; @import 'shelf/textLabelPanel'; @import 'shelf/timeOfDayPanel'; -@import '../lib/forms/SchemaFormTable/ObjectTable.scss'; -@import '../lib/Components/PromiseButton.scss'; -@import '../lib/VideoPreviewPlayer.scss'; -@import '../ui/ClipTrimPanel/ClipTrimPanel.scss'; -@import '../ui/ClipTrimPanel/TimecodeEncoder.scss'; -@import '../ui/ClipTrimPanel/VideoEditMonitor.scss'; -@import '../ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail.scss'; -@import '../ui/SegmentStoryboard/SegmentStoryboard.scss'; -@import '../ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardPartSecondaryPieces.scss'; -@import '../ui/Settings/components/triggeredActions/TriggeredActionsEditor.scss'; -@import '../ui/Settings/Forms.scss'; -@import '../ui/SegmentTimeline/SegmentTimeline.scss'; -@import '../ui/Status/media-status/MediaStatusListItem.scss'; -@import '../ui/Status/media-status/MediaStatusList.scss'; -@import '../ui/Status/media-status/MediaStatusListHeader.scss'; -@import '../ui/Status/package-status/package-status.scss'; -@import '../ui/SegmentList/LinePartPieceIndicator/LinePartPieceIndicator.scss'; -@import '../ui/SegmentList/LinePartPieceIndicator/PieceIndicatorMenu.scss'; -@import '../ui/SegmentList/SegmentList.scss'; -@import '../ui/SegmentList/LinePartMainPiece/LinePartMainPiece.scss'; -@import '../ui/SegmentList/LinePartTransitionPiece/LinePartTransitionPiece.scss'; -@import '../ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece.scss'; -@import '../ui/PieceIcons/IconColors.scss'; +@import '../lib/forms/SchemaFormTable/ObjectTable'; +@import '../lib/forms/SchemaFormOneOfButtons/OneOfButtons'; +@import '../lib/Components/PromiseButton'; +@import '../lib/VideoPreviewPlayer'; +@import '../ui/ClipTrimPanel/ClipTrimPanel'; +@import '../ui/ClipTrimPanel/TimecodeEncoder'; +@import '../ui/ClipTrimPanel/VideoEditMonitor'; +@import '../ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnail'; +@import '../ui/SegmentStoryboard/SegmentStoryboard'; +@import '../ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardPartSecondaryPieces'; +@import '../ui/Settings/components/triggeredActions/TriggeredActionsEditor'; +@import '../ui/Settings/Forms'; +@import '../ui/SegmentTimeline/SegmentTimeline'; +@import '../ui/Status/media-status/MediaStatusListItem'; +@import '../ui/Status/media-status/MediaStatusList'; +@import '../ui/Status/media-status/MediaStatusListHeader'; +@import '../ui/Status/package-status/package-status'; +@import '../ui/SegmentList/LinePartPieceIndicator/LinePartPieceIndicator'; +@import '../ui/SegmentList/LinePartPieceIndicator/PieceIndicatorMenu'; +@import '../ui/SegmentList/SegmentList'; +@import '../ui/SegmentList/LinePartMainPiece/LinePartMainPiece'; +@import '../ui/SegmentList/LinePartTransitionPiece/LinePartTransitionPiece'; +@import '../ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece'; +@import '../ui/PieceIcons/IconColors'; @import '../ui/PieceIcons/PieceIcons'; @import '../ui/ClockView/ClockViewPieceIcons/ClockViewPieceIcons'; -@import '../ui/ClockView/CameraScreen/CameraScreen.scss'; -@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem.scss'; -@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUp.scss'; -@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader.scss'; -@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule.scss'; -@import '../ui/SegmentAdlibTesting/SegmentAdlibTesting.scss'; +@import '../ui/ClockView/CameraScreen/CameraScreen'; +@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpItem'; +@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUp'; +@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpHeader'; +@import '../ui/RundownView/MediaStatusPopUp/MediaStatusPopUpSegmentRule'; +@import '../ui/SegmentAdlibTesting/SegmentAdlibTesting'; @import 'rundownView'; diff --git a/packages/webui/src/client/styles/notifications.scss b/packages/webui/src/client/styles/notifications.scss index c3bd5a439ab..a8525b6fd4e 100644 --- a/packages/webui/src/client/styles/notifications.scss +++ b/packages/webui/src/client/styles/notifications.scss @@ -490,9 +490,6 @@ .rundown-view { &.notification-center-open { padding-right: 25vw !important; - > .rundown-header_OLD .rundown-overview { - padding-right: calc(25vw + 1.5em) !important; - } } } diff --git a/packages/webui/src/client/styles/prompter.scss b/packages/webui/src/client/styles/prompter.scss index c3ca335957f..fcf67b7ff22 100644 --- a/packages/webui/src/client/styles/prompter.scss +++ b/packages/webui/src/client/styles/prompter.scss @@ -90,7 +90,8 @@ body.prompter-scrollbar { .prompter-part { text-align: center; letter-spacing: 0.1em; - font-size: 50%; + font-size: 63%; + height: 12.2vh; } .overlay-fix { pointer-events: none; @@ -129,6 +130,7 @@ body.prompter-scrollbar { &.live { //border-bottom: 0.2em solid #f55; + // Layer the director-style gradient over the live color background: $general-live-color; color: #fff; -webkit-text-stroke: black; @@ -277,36 +279,25 @@ body.prompter-scrollbar { } } -.prompter-timing-clock { +.screen-timing-clock { position: fixed; display: block; top: 0; right: 0; - left: auto; - font-size: 75%; - padding: 0.05em 0.3em; - border-radius: 1em; - box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5); - - &.heavy-light { - font-weight: 600; + font-size: 9vmin; + box-shadow: 0px 0px 3px 3px rgba(0, 0, 0, 0.5); +} - &.light { - // color: $general-late-color; - background-color: #ffe900; - color: #000; - } +.prompter-rundown-status-bar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 1100; +} - &.heavy { - background-color: $general-fast-color; - color: #fff; - text-shadow: - 1px 1px 0px #000, - 1px -1px 0px #000, - -1px -1px 0px #000, - -1px 1px 0px #000; - } - } +.prompter-rundown-status-bar.rundown-status-bar { + font-size: 6.95vmin; } .script-text-formatted { diff --git a/packages/webui/src/client/styles/propertiesPanel.scss b/packages/webui/src/client/styles/propertiesPanel.scss index d56a62f6edb..bdde984086b 100644 --- a/packages/webui/src/client/styles/propertiesPanel.scss +++ b/packages/webui/src/client/styles/propertiesPanel.scss @@ -68,7 +68,12 @@ letter-spacing: 0.5px; > .svg { + width: 1em; flex-shrink: 0; + + > svg { + display: block; + } } > .title { flex-grow: 1; diff --git a/packages/webui/src/client/styles/rundownStatusBar.scss b/packages/webui/src/client/styles/rundownStatusBar.scss new file mode 100644 index 00000000000..d3445d4dd95 --- /dev/null +++ b/packages/webui/src/client/styles/rundownStatusBar.scss @@ -0,0 +1,42 @@ +/** + * Styling for `ui/ClockView/RundownStatusBar.tsx`. + * + * View-level styles (Presenter/Prompter) should only handle placement and scaling. + */ +@import 'colorScheme'; + +.rundown-status-bar { + display: grid; + grid-template-columns: auto fit-content(1em); + grid-template-rows: fit-content(1em); + font-size: 6em; + color: #888; + padding: 0 0 0 0.2em; + background: rgba(0, 0, 0, 0.75); + + .rundown-status-bar__rundown-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + line-height: 1.44em; + } + + .rundown-status-bar__right { + display: flex; + align-items: center; + justify-content: flex-end; + justify-self: end; + // Match the exact background of `.t-timer-display` + background: #333; + } + + .rundown-status-bar__t-timer { + display: flex; + align-items: center; + white-space: nowrap; + } + + &.rundown-status-bar--no-title { + background: transparent; + } +} diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index 79f32bbff4a..b4259c5c298 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -1,6 +1,7 @@ @use 'sass:color'; @import 'utils'; +@import 'checkerboard'; $output-layer-group-line: 1px solid #5c5c5c; $output-layer-group-collapse-animation-duration: 0.3s; @@ -142,32 +143,14 @@ $break-width: 35rem; } } - .rundown-header_OLD .notification-pop-ups { - top: 65px; - } - - > .rundown-header_OLD .rundown-overview { - transition: 0s padding-right 0.5s; - } - &.notification-center-open { padding-right: $notification-center-width; transition: 0s padding-right 1s; - - > .rundown-header_OLD .rundown-overview { - padding-right: calc(#{$notification-center-width} + 1.5em); - transition: 0s padding-right 1s; - } } &.properties-panel-open { padding-right: calc(#{$properties-panel-width} - 3.5em); transition: 0s padding-right 1s; - - > .rundown-header_OLD .rundown-overview { - padding-right: calc(#{$properties-panel-width} + 1.5em); - transition: 0s padding-right 1s; - } } .dark { @@ -208,16 +191,20 @@ body.no-overflow { left: 0; bottom: 0; right: 0; - background: linear-gradient( - 45deg, - $color-status-fatal 33%, - transparent 33%, - transparent 66%, - $color-status-fatal 66% - ), // Top border - linear-gradient(45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), // Bottom border - linear-gradient(45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), // Left border - linear-gradient(45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%); // Right border + background: + linear-gradient(45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), + // Top border + linear-gradient(45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), + // Bottom border + linear-gradient(45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), + // Left border + linear-gradient( + 45deg, + $color-status-fatal 33%, + transparent 33%, + transparent 66%, + $color-status-fatal 66% + ); // Right border background-repeat: repeat-x, repeat-x, repeat-y, repeat-y; background-size: 30px 8px, @@ -244,263 +231,10 @@ body.no-overflow { } } -.rundown-header_OLD { - padding: 0; - - .header-row { - width: 100%; - - &.margin-right { - padding-right: $statusbar-width; - } - } - - .flex-col.col-timing { - flex: 1 1; - } - - .timing { - margin: 0 0; - min-width: auto; - width: 100%; - text-align: center; - - display: grid; - grid-template-columns: 1fr auto 1fr; - - .timing__header__left { - text-align: left; - display: flex; - } - - .timing__header__center { - position: relative; - display: flex; - justify-content: center; - align-items: center; - } - - .timing__header__right { - display: grid; - grid-template-columns: auto auto; - text-align: right; - vertical-align: middle; - - .timing__header__right__right { - align-content: end; - } - } - - .timing-clock { - display: inline; - position: relative; - margin-right: 1em; - font-family: - 'Roboto', - Helvetica Neue, - Arial, - sans-serif; - font-weight: 100; - color: $general-clock; - font-size: 1.5em; - margin-top: 0.8em; - word-break: keep-all; - white-space: nowrap; - - &.visual-last-child { - margin-right: 0; - } - - &.countdown { - font-weight: 400; - } - - &.playback-started { - display: inline-block; - width: 25%; - } - - &.left { - text-align: left; - } - - &.time-now { - margin-top: 0.05em; - margin-right: 0; - font-size: 2.3em; - font-weight: 100; - text-align: center; - } - - &.current-remaining { - position: absolute; - left: calc(50% + 3.5em); - text-align: left; - color: $liveline-timecode-color; - font-weight: 500; - - .overtime { - color: $general-fast-color; - text-shadow: 0px 0px 6px $general-fast-color--shadow; - } - } - - .timing-clock-label { - position: absolute; - display: inline; - top: -1em; - color: #b8b8b8; - text-transform: uppercase; - white-space: nowrap; - font-weight: 300; - font-size: 0.5em; - - &.left { - left: 0; - right: auto; - text-align: left; - } - - &.right { - right: 0; - left: auto; - text-align: right; - } - - &.hide-overflow { - overflow: hidden; - text-overflow: ellipsis; - width: 100%; - } - - &.rundown-name { - width: auto; - max-width: calc(40vw - 138px); - min-width: 100%; - margin: 0; - - > strong { - margin-right: 0.4em; - } - - > svg.icon.looping { - width: 1.4em; - height: 1.4em; - } - } - } - - &.heavy-light { - font-weight: 600; - - &.heavy { - // color: $general-late-color; - color: #ffe900; - background: none; - } - - &.light { - color: $general-fast-color; - text-shadow: 0px 0px 6px $general-fast-color--shadow; - background: none; - } - } - } - - .rundown__header-status { - position: absolute; - font-size: 0.7rem; - text-transform: uppercase; - background: #fff; - border-radius: 1rem; - line-height: 1em; - font-weight: 700; - color: #000; - top: 2.4em; - left: 0; - padding: 2px 5px 1px; - - &.rundown__header-status--hold { - background: $hold-status-color; - } - } - - .timing-clock-header-label { - font-weight: 100px; - } - } - - .close { - margin: 17px 20px 14px; - } - - /* RWD: iPad */ - @media screen and (max-width: 1024px) { - .timing { - .timing-clock.plan-end:not(.countdown) { - display: none; - } - - .timing-clock.countdown.plan-end { - margin-right: 0; - } - } - } - - /* RWD: Anything below an iPad */ - @media screen and (max-width: 1023px) { - .timing { - .timing-clock.plan-end:not(.countdown), - .timing-clock.plan-start:not(.countdown) { - display: none; - } - } - } - - @media screen and (max-width: 800px) { - .timing { - .timing-clock.heavy-light { - margin-right: 0; - } - - .timing-clock.plan-end { - display: none; - } - } - } - - @media screen and (max-width: 600px) { - .timing { - .timing-clock.plan-start.countdown { - display: none; - } - - .timing-clock.time-now { - left: 2em; - transform: none; - } - } - } -} - .first-row { cursor: default; } -.rundown-header_OLD.not-active .first-row { - background-color: rgb(38, 137, 186); -} -.rundown-header_OLD.not-active .first-row .timing-clock, -.rundown-header_OLD.not-active .first-row .timing-clock-label { - color: #fff !important; -} -// .rundown-header_OLD.active .first-row { -// background-color: #600 -// } -.rundown-header_OLD.active.rehearsal .first-row { - background-color: #660; -} - svg.icon { margin-top: -0.2em; position: relative; @@ -511,6 +245,8 @@ svg.icon { display: grid; grid-column-gap: 0; grid-row-gap: 0; + position: relative; + z-index: 3; grid-template-columns: [segment-name] calc($segment-header-width / 2) [duration-onAirIn-split] calc($segment-header-width / 2) @@ -1169,19 +905,38 @@ svg.icon { } .segment-timeline__part { .segment-timeline__part__invalid-cover { + mix-blend-mode: color-burn; + background-image: + repeating-linear-gradient( + 45deg, + var(--invalid-reason-color-transparent) 0%, + var(--invalid-reason-color-transparent) 5px, + var(--invalid-reason-color-opaque) 5px, + var(--invalid-reason-color-opaque) 8px + ), + repeating-linear-gradient( + -45deg, + var(--invalid-reason-color-transparent) 0%, + var(--invalid-reason-color-transparent) 5px, + var(--invalid-reason-color-opaque) 5px, + var(--invalid-reason-color-opaque) 8px + ) !important; + } + .segment-timeline__part__invalid-part-instance-cover { + mix-blend-mode: color-burn; background-image: repeating-linear-gradient( 45deg, var(--invalid-reason-color-transparent) 0%, - var(--invalid-reason-color-transparent) 4px, - var(--invalid-reason-color-opaque) 4px, + var(--invalid-reason-color-transparent) 6px, + var(--invalid-reason-color-opaque) 6px, var(--invalid-reason-color-opaque) 8px ), repeating-linear-gradient( -45deg, var(--invalid-reason-color-transparent) 0%, - var(--invalid-reason-color-transparent) 4px, - var(--invalid-reason-color-opaque) 4px, + var(--invalid-reason-color-transparent) 6px, + var(--invalid-reason-color-opaque) 6px, var(--invalid-reason-color-opaque) 8px ) !important; } @@ -1635,20 +1390,47 @@ svg.icon { right: 1px; z-index: 10; pointer-events: all; + mix-blend-mode: color-burn; background-image: repeating-linear-gradient( 45deg, var(--invalid-reason-color-transparent) 0%, var(--invalid-reason-color-transparent) 5px, var(--invalid-reason-color-opaque) 5px, - var(--invalid-reason-color-opaque) 10px + var(--invalid-reason-color-opaque) 8px ), repeating-linear-gradient( -45deg, var(--invalid-reason-color-transparent) 0%, var(--invalid-reason-color-transparent) 5px, var(--invalid-reason-color-opaque) 5px, - var(--invalid-reason-color-opaque) 10px + var(--invalid-reason-color-opaque) 8px + ); + } + + .segment-timeline__part__invalid-part-instance-cover { + position: absolute; + top: 0; + left: 1px; + bottom: 0; + right: 1px; + z-index: 10; + pointer-events: all; + mix-blend-mode: color-burn; + background-image: + repeating-linear-gradient( + 45deg, + var(--invalid-reason-color-transparent) 0%, + var(--invalid-reason-color-transparent) 6px, + var(--invalid-reason-color-opaque) 6px, + var(--invalid-reason-color-opaque) 8px + ), + repeating-linear-gradient( + -45deg, + var(--invalid-reason-color-transparent) 0%, + var(--invalid-reason-color-transparent) 6px, + var(--invalid-reason-color-opaque) 6px, + var(--invalid-reason-color-opaque) 8px ); } @@ -2076,7 +1858,7 @@ svg.icon { min-height: 0; transform-style: preserve-3d; - @include item-type-colors(); + @include item-type-colors; > .segment-timeline__piece__label.right-side { margin-right: 0 !important; @@ -2390,6 +2172,21 @@ svg.icon { > .label-icon.label-loop-icon { margin: 0 0 0 0; } + + > .label-icon.label-custom-icon { + margin-top: -3px; + display: flex; + flex-direction: row-reverse; + + > div { + flex: 0 0; + + > svg { + height: 1em; + width: auto; + } + } + } } &.last-words { @@ -2889,6 +2686,23 @@ svg.icon { } } } +.segment-timeline-wrapper--shelf { + div .segment-timeline { + margin-bottom: 0 !important; + } + + &:has(.rundown-view-shelf) { + margin-bottom: -0.8em !important; + } + + + .segment-timeline-wrapper--hidden { + + .segment-timeline-wrapper--shelf:has(.rundown-view-shelf) { + .segment-timeline { + margin-top: 0 !important; + } + } + } +} .playlist-looping-header { color: $rundown-divider-color; @@ -3359,7 +3173,7 @@ svg.icon { display: block; width: 414px; height: 233px; - background: #000; + @include checkerboard-background; } > .segment-timeline__mini-inspector__warnings { @@ -3422,8 +3236,18 @@ svg.icon { transform: translate(-50%, -50%); } + .video-preview__image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 1; + } + & > * { - @include item-type-colors(); + @include item-type-colors; } .video-preview__label { @@ -3665,81 +3489,3 @@ svg.icon { } @import 'rundownOverview'; - -.rundown-header_OLD .timing__header_t-timers { - position: absolute; - right: 100%; - top: 50%; - transform: translateY(-38%); - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-end; - margin-right: 1em; - - .timing__header_t-timers__timer { - display: flex; - gap: 0.5em; - justify-content: space-between; - align-items: baseline; - white-space: nowrap; - line-height: 1.3; - - .timing__header_t-timers__timer__label { - font-size: 0.7em; - color: #b8b8b8; - text-transform: uppercase; - white-space: nowrap; - } - - .timing__header_t-timers__timer__value { - font-family: - 'Roboto', - Helvetica Neue, - Arial, - sans-serif; - font-variant-numeric: tabular-nums; - font-weight: 500; - color: #fff; - font-size: 1.1em; - } - - .timing__header_t-timers__timer__sign { - display: inline-block; - width: 0.6em; - text-align: center; - font-weight: 500; - font-size: 0.9em; - color: #fff; - margin-right: 0.3em; - } - - .timing__header_t-timers__timer__part { - color: #fff; - &.timing__header_t-timers__timer__part--dimmed { - color: #888; - font-weight: 400; - } - } - .timing__header_t-timers__timer__separator { - margin: 0 0.05em; - color: #888; - } - - .timing__header_t-timers__timer__over-under { - font-size: 0.75em; - font-weight: 400; - font-variant-numeric: tabular-nums; - margin-left: 0.5em; - white-space: nowrap; - - &.timing__header_t-timers__timer__over-under--over { - color: $general-late-color; - } - - &.timing__header_t-timers__timer__over-under--under { - color: #0f0; - } - } - } -} diff --git a/packages/webui/src/client/styles/shelf/dashboard-rundownView.scss b/packages/webui/src/client/styles/shelf/dashboard-rundownView.scss index 6e0e1eff210..13b5667707c 100644 --- a/packages/webui/src/client/styles/shelf/dashboard-rundownView.scss +++ b/packages/webui/src/client/styles/shelf/dashboard-rundownView.scss @@ -1,144 +1,24 @@ -.rundown-view-shelf.dashboard-panel { - width: auto; - margin: -0.8em 1.5em 0px 0px; - padding: 0; - position: relative; - background: none; - border: none; - - .rundown-view-shelf__identifier { - display: none; - } - - .dashboard-panel__panel__button { - margin-top: 10px; - height: 110px; - max-width: 170px !important; - > .dashboard-panel__panel__button__content { - display: grid; - grid-template-columns: 1fr min-content; - grid-template-rows: 1fr; - grid-template-areas: '. .'; - - > .dashboard-panel__panel__button__label-container { - padding: 3px 1px 3px 6px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - grid-row: 2; - - > .dashboard-panel__panel__button__label { - top: unset; - } - } - - > .dashboard-panel__panel__button__thumbnail { - position: relative; - height: 85px; - z-index: 1; - overflow: hidden; - grid-column: auto / span 2; - - img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; - } - } - - > .dashboard-panel__panel__button__sub-label { - display: block; - margin: 1px 3px 0 3px; - padding: 0 2px 0 2px; - color: #ffff00; - font-size: 1.17em; - font-family: 'Roboto Condensed', sans-serif; - text-shadow: - 0px 0px 2px rgba(0, 0, 0, 1), - 0px 0px 1px rgba(0, 0, 0, 1); - z-index: 2; - grid-column: 2; - grid-row: 2; - white-space: nowrap; - } - - > .dashboard-panel__panel__button__hotkey { - display: block; - position: absolute; - left: 4px; - top: 4px; - border-radius: 2px; - width: 18px; - height: 18px; - color: black; - z-index: 1; - text-align: center; - font-weight: 600; - line-height: 19px; - font-size: 1.3em; - - &::before { - content: ' '; - display: block; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - background: rgb(255, 255, 255); - background: linear-gradient(180deg, rgba(255, 255, 255, 1) 0%, rgba(118, 118, 118, 1) 100%); - border-radius: 3px; - z-index: -1; - } +@import './rundownViewShelfAdlibSizeToggle'; - &::after { - content: ' '; - display: block; - position: absolute; - top: 1px; - left: 1px; - bottom: 1px; - right: 1px; - background: rgb(255, 255, 255); - background: radial-gradient(circle, rgba(255, 255, 255, 1) 0%, rgba(198, 198, 198, 1) 100%); - border-radius: 3px; - z-index: -1; - } - } - } - - &::before { - display: block; - position: absolute; - border-radius: 4px; - content: ''; - top: 0; - left: 0; - bottom: 0; - right: 0; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5); - z-index: 4; - } - - &.vt { - .dashboard-panel__panel__button__label-container { - top: unset; - } - } +.segment-timeline + .rundown-view-shelf { + padding: 12px 0 0 0; +} - &.graphics { - .dashboard-panel__panel__button__label-container { - /* - grid-row: 1; - white-space: normal; - padding-left: 26px; - */ - } +.rundown-view-shelf.dashboard-panel { + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(0, 0, 0, 0.1); + margin: -11px 0.8em 0 178px; + border-radius: 0 0 11px 11px; + z-index: 0; + box-shadow: 7px 6px 15px 0px rgba(0, 0, 0, 0.2); + display: flex; + .dashboard-panel__panel { + flex-wrap: wrap; + margin: -11px 0 0 0; + padding: 11px 2px 2px 0; + overflow: unset; + .dashboard-panel__panel__button { + z-index: 3; } } } - -.segment-timeline + .rundown-view-shelf { - padding: 2px 5px 5px 13.3em; -} diff --git a/packages/webui/src/client/styles/shelf/dashboard-streamdeck.scss b/packages/webui/src/client/styles/shelf/dashboard-streamdeck.scss index 4cbea95ed7f..bf7f503e2cf 100644 --- a/packages/webui/src/client/styles/shelf/dashboard-streamdeck.scss +++ b/packages/webui/src/client/styles/shelf/dashboard-streamdeck.scss @@ -113,8 +113,8 @@ max-height: 100%; overflow: hidden; - > .dashboard-panel__panel__button, - > span > .dashboard-panel__panel__button { + > .dashboard-panel__panel__button-wrapper > .dashboard-panel__panel__button, + > span > .dashboard-panel__panel__button-wrapper > .dashboard-panel__panel__button { > .dashboard-panel__panel__button__label { top: 4px; } @@ -125,11 +125,6 @@ } } } - - > .buckets { - > .dashboard-panel__panel--bucket { - } - } } #render-target .container-fluid-custom .notification-pop-ups { @@ -155,13 +150,24 @@ --dashboard-panel-margin-width: 0em; --dashboard-panel-margin-height: 0em; - .dashboard-panel > .dashboard-panel__panel > .dashboard-panel__panel__button, - .dashboard-panel > .dashboard-panel__panel > span > .dashboard-panel__panel__button { + .dashboard-panel + > .dashboard-panel__panel + > .dashboard-panel__panel__button-wrapper + > .dashboard-panel__panel__button, + .dashboard-panel + > .dashboard-panel__panel + > span + > .dashboard-panel__panel__button-wrapper + > .dashboard-panel__panel__button { width: 71px; height: 71px; - margin: 0px 1px 1px 0px; font-size: 13px; } + + .dashboard-panel > .dashboard-panel__panel > .dashboard-panel__panel__button-wrapper, + .dashboard-panel > .dashboard-panel__panel > span > .dashboard-panel__panel__button-wrapper { + margin: 0px 1px 1px 0px; + } } } } @@ -180,13 +186,24 @@ --dashboard-panel-margin-width: 0em; --dashboard-panel-margin-height: 0em; - .dashboard-panel > .dashboard-panel__panel > .dashboard-panel__panel__button, - .dashboard-panel > .dashboard-panel__panel > span > .dashboard-panel__panel__button { + .dashboard-panel + > .dashboard-panel__panel + > .dashboard-panel__panel__button-wrapper + > .dashboard-panel__panel__button, + .dashboard-panel + > .dashboard-panel__panel + > span + > .dashboard-panel__panel__button-wrapper + > .dashboard-panel__panel__button { width: 95px; height: 95px; - margin: 0px 1px 1px 0px; font-size: 15px; } + + .dashboard-panel > .dashboard-panel__panel > .dashboard-panel__panel__button-wrapper, + .dashboard-panel > .dashboard-panel__panel > span > .dashboard-panel__panel__button-wrapper { + margin: 0px 1px 1px 0px; + } } } } @@ -205,13 +222,24 @@ --dashboard-panel-margin-width: 0em; --dashboard-panel-margin-height: 0em; - .dashboard-panel > .dashboard-panel__panel > .dashboard-panel__panel__button, - .dashboard-panel > .dashboard-panel__panel > span > .dashboard-panel__panel__button { + .dashboard-panel + > .dashboard-panel__panel + > .dashboard-panel__panel__button-wrapper + > .dashboard-panel__panel__button, + .dashboard-panel + > .dashboard-panel__panel + > span + > .dashboard-panel__panel__button-wrapper + > .dashboard-panel__panel__button { width: 79px; height: 79px; - margin: 0px 1px 1px 0px; font-size: 13px; } + + .dashboard-panel > .dashboard-panel__panel > .dashboard-panel__panel__button-wrapper, + .dashboard-panel > .dashboard-panel__panel > span > .dashboard-panel__panel__button-wrapper { + margin: 0px 1px 1px 0px; + } } } } diff --git a/packages/webui/src/client/styles/shelf/dashboard.scss b/packages/webui/src/client/styles/shelf/dashboard.scss index dd457288a45..c246b29a355 100644 --- a/packages/webui/src/client/styles/shelf/dashboard.scss +++ b/packages/webui/src/client/styles/shelf/dashboard.scss @@ -1,4 +1,5 @@ @import '../colorScheme'; +@import '../checkerboard'; $dashboard-button-width: 6.40625em; $dashboard-button-height: 3.625em; @@ -31,35 +32,33 @@ $dashboard-button-height: 3.625em; margin: 0.625rem; user-select: none; - &.dashboard-panel__panel--bucket { - &.dashboard-panel__panel--bucket-active { - &::after { - content: ' '; - display: block; - background-color: rgba(0, 183, 255, 0.5); - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - border-radius: 10px; - } + &.dashboard-panel__panel--bucket-active { + &::after { + content: ' '; + display: block; + background-color: rgba(0, 183, 255, 0.5); + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + border-radius: 10px; } + } - &.dashboard-panel__panel--sort-dragging { - opacity: 0.5; + &.dashboard-panel__panel--sort-dragging { + opacity: 0.5; - &::after { - content: ' '; - display: block; - background-color: rgba(0, 183, 255, 0.5); - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - border-radius: 10px; - } + &::after { + content: ' '; + display: block; + background-color: rgba(0, 183, 255, 0.5); + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + border-radius: 10px; } } @@ -205,6 +204,8 @@ $dashboard-button-height: 3.625em; margin: -4px; overflow: auto; max-height: calc(100% - 12px); + display: flex; + flex-wrap: wrap; &.dashboard-panel__panel--horizontal { white-space: nowrap; overflow: auto; @@ -288,18 +289,87 @@ $dashboard-button-height: 3.625em; text-overflow: ellipsis; white-space: normal; line-break: loose; - word-break: break-all; width: $dashboard-button-width; height: $dashboard-button-height; - margin: 4px; + margin: 0; vertical-align: top; cursor: pointer; - @include item-type-colors(); + @include item-type-colors; @include invalid-overlay(); @include floated-overlay(); @include missing-overlay(); + &.source-broken, + &.source-missing, + &.source-unknown-state, + &.source-not-ready { + &::before { + content: none; + } + + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__thumbnail + .dashboard-panel__panel__button__thumbnail__aspect { + &::before { + content: ' '; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + mix-blend-mode: normal; + animation-name: animated-zebra; + animation-fill-mode: forwards; + animation-duration: calc(0.25s * var(--missing-overlay-animated)); + animation-iteration-count: infinite; + opacity: 0.4; + pointer-events: none; + z-index: 1; + + background-image: repeating-linear-gradient( + 45deg, + transparent, + transparent 7px, + black 8px, + black 12px, + transparent 13px, + transparent 16px + ); + } + } + } + + &.vt.source-broken, + &.vt.source-missing, + &.vt.source-unknown-state, + &.vt.source-not-ready { + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__thumbnail + .dashboard-panel__panel__button__thumbnail__aspect::before { + opacity: 0.6; + } + } + + &.source-broken { + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__thumbnail + .dashboard-panel__panel__button__thumbnail__aspect::before { + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.54) 3px, + rgba(0, 0, 0, 0.54) 4px, + transparent 5px + ); + } + } + $dashboard-panel__button__border-width: 2px; &.selected { @@ -360,7 +430,6 @@ $dashboard-button-height: 3.625em; left: 0; bottom: 0; right: 0; - background: rgba(255, 255, 255, 0.3); z-index: 10; // animation: 2s button-flash normal infinite; } @@ -401,74 +470,267 @@ $dashboard-button-height: 3.625em; } } + &.dashboard-panel__panel__button--has-hotkey { + .dashboard-panel__panel__button__label__content { + max-height: 5.85em; + } + } + + &.dashboard-panel__panel__button--no-thumbnail { + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__tag-container--inner + > .dashboard-panel__panel__button__label-container { + border-radius: 0 4px 4px 0; + } + + // Large + no-thumbnail: keep hotkey visible in bottom-right corner + // (Large + thumbnail keeps the default top-left placement) + &:not(.dashboard-panel__panel__button--compact):not(.list) { + .dashboard-panel__panel__button__hotkey { + left: auto; + top: auto; + right: 5px; + bottom: 5px; + } + } + } + + // Large buttons: clamp label text to 2 lines by default (when thumbnail exists), + // and expand to 6 lines only when the button has no thumbnail. + &:not(.dashboard-panel__panel__button--compact):not(.list):not(.dashboard-panel__panel__button--no-thumbnail) + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__tag-container--inner + > .dashboard-panel__panel__button__label-container + .dashboard-panel__panel__button__label + > .dashboard-panel__panel__button__label__content:not(.dashboard-panel__panel__button__label__content--editable) { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + } + + &.dashboard-panel__panel__button--no-thumbnail:not(.dashboard-panel__panel__button--compact):not(.list) + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__tag-container--inner + > .dashboard-panel__panel__button__label-container + .dashboard-panel__panel__button__label + > .dashboard-panel__panel__button__label__content:not(.dashboard-panel__panel__button__label__content--editable) { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 6; + overflow: hidden; + text-overflow: ellipsis; + } + + &:not(.dashboard-panel__panel__button--compact):not(.list):not(.dashboard-panel__panel__button--no-thumbnail) { + > .dashboard-panel__panel__button__content > .dashboard-panel__panel__button__tag-container { + border-bottom-right-radius: 4px; + } + + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__tag-container--inner + > .dashboard-panel__panel__button__label-container { + border-bottom-right-radius: 4px; + max-height: 34.5667px; + } + } + > .dashboard-panel__panel__button__content { - display: block; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; + display: flex; + flex-direction: column; + position: static; + flex: 1 1 auto; + min-height: 0; + justify-content: end; - > .dashboard-panel__panel__button__label-container { - position: relative; - left: 0; - top: 0; - right: 0; - bottom: 0; + > .dashboard-panel__panel__button__tag-container .dashboard-panel__panel__button__label-container { padding: 3px 4px; z-index: 2; + display: flex; + align-items: stretch; + gap: 0; + flex: 0 0 auto; .dashboard-panel__panel__button__label { - text-align: left; - hyphens: auto; - - vertical-align: top; - // -webkit-text-stroke-color: #000; - // -webkit-text-stroke-width: 0.02em; - line-height: 1.15em; - top: 3px; - font-family: 'Roboto Condensed', sans-serif; - font-size: 1.17em; - font-weight: 400; - text-shadow: - -1px -1px 0px rgba(0, 0, 0, 0.5), - 1px 1px 0px rgba(0, 0, 0, 0.5), - 1px -1px 0px rgba(0, 0, 0, 0.5), - 1px -1px 0px rgba(0, 0, 0, 0.5), - 0.5px 0.5px 2px rgba(0, 0, 0, 1); - z-index: 2; - word-break: break-word; - overflow-wrap: break-word; - white-space: normal; + display: flex; + flex-direction: row; + height: 100%; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + + > .dashboard-panel__panel__button__label__content { + text-align: left; + hyphens: auto; + min-width: 0; + min-height: 0; + // -webkit-text-stroke-color: #000; + // -webkit-text-stroke-width: 0.02em; + line-height: 1.15em; + font-family: 'Roboto Condensed', sans-serif; + font-size: 1.17em; + font-weight: 400; + text-shadow: + -1px -1px 0px rgba(0, 0, 0, 0.5), + 1px 1px 0px rgba(0, 0, 0, 0.5), + 1px -1px 0px rgba(0, 0, 0, 0.5), + 1px -1px 0px rgba(0, 0, 0, 0.5), + 0.5px 0.5px 2px rgba(0, 0, 0, 1); + z-index: 2; + overflow-wrap: break-word; + white-space: normal; + overflow: hidden; + box-sizing: border-box; + } &.dashboard-panel__panel__button__label--editable { - text-shadow: none; - color: #000; - background: #fff; + > .dashboard-panel__panel__button__label__content { + text-shadow: none; + color: #000; + background: #fff; + border: none; + outline: none; + resize: none; + width: 100%; + } } } } - > .dashboard-panel__panel__button__thumbnail { - position: absolute; - left: 0; - right: 0; - height: auto; + > .dashboard-panel__panel__button__tag-container > .dashboard-panel__panel__button__thumbnail { z-index: 1; - bottom: 0; - } - - > .dashboard-panel__panel__button__sub-label { + flex: 1 1 auto; + min-height: 0; + display: flex; + align-items: flex-start; + justify-content: center; + width: 100%; + background: #4b4b4b; position: relative; - float: right; - margin: 3px 3px 0 3px; - padding: 0 0 0 2px; - font-size: 8px; - text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); - z-index: 2; + + .dashboard-panel__panel__button__thumbnail__aspect { + width: 100%; + aspect-ratio: 16 / 9; + position: relative; + background: rgba(0, 0, 0, 0.15); + overflow: hidden; + flex: 1 1 auto; + + @supports not (aspect-ratio: 1 / 1) { + height: 0; + padding-top: 56.25%; + } + + &--splits { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + aspect-ratio: 16 / 9; + flex: none; + @include checkerboard-background; + + @supports not (aspect-ratio: 1 / 1) { + height: 100%; + padding-top: 0; + } + + > .box, + > .background { + position: absolute; + transform: translate(-50%, -50%); + overflow: hidden; + } + + > .video-preview { + position: absolute; + transform: translate(-50%, -50%); + overflow: hidden; + + .video-preview__image { + position: absolute; + inset: 0; + display: block; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + z-index: 1; + } + } + + & > * { + @include item-type-colors; + } + } + } + + .dashboard-panel__panel__button__thumbnail__overlay { + position: absolute; + right: 0; + top: 0; + padding: 0 1.25px 0 0; + margin: -1px; + z-index: 2; + + .dashboard-panel__panel__button__sub-label { + font-family: Roboto Flex; + font-weight: 400; + font-size: 13px; + line-height: 17px; + letter-spacing: 0px; + text-align: right; + vertical-align: middle; + + padding: 0 1px; + margin: 0 0 0; + -webkit-text-stroke: 1px #000000; + paint-order: stroke fill; + // Fallback for non-stroke browsers + text-shadow: + -1px -1px 0 #000000, + 0 -1px 0 #000000, + 1px -1px 0 #000000, + -1px 0 0 #000000, + 1px 0 0 #000000, + -1px 1px 0 #000000, + 0 1px 0 #000000, + 1px 1px 0 #000000; + font-variation-settings: + 'GRAD' 150, + 'XOPQ' 85, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 570, + 'YTUC' 712, + 'slnt' 0, + 'wdth' 25; + background: rgba(0, 0, 0, 0.3); + } + } + + .dashboard-panel__panel__button__thumbnail__aspect:not( + .dashboard-panel__panel__button__thumbnail__aspect--splits + ) + > img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } } - > svg { + > .dashboard-panel__panel__button__tag-container > svg { position: absolute; top: 0; left: 0; @@ -477,12 +739,60 @@ $dashboard-button-height: 3.625em; } > .dashboard-panel__panel__button__hotkey { - display: none; + display: block; + position: absolute; + left: 5px; + top: 5px; + border-radius: 2px; + width: 18px; + height: 18px; + color: black; + z-index: 5; + text-align: center; + font-weight: 600; + line-height: 19px; + font-size: 1.3em; + + &::before { + content: ' '; + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: rgb(255, 255, 255); + background: linear-gradient(180deg, rgba(255, 255, 255, 1) 0%, rgba(118, 118, 118, 1) 100%); + border-radius: 3px; + z-index: -1; + } + + &::after { + content: ' '; + display: block; + position: absolute; + top: 1px; + left: 1px; + bottom: 1px; + right: 1px; + background: rgb(255, 255, 255); + background: radial-gradient(circle, rgba(255, 255, 255, 1) 0%, rgba(198, 198, 198, 1) 100%); + border-radius: 3px; + z-index: -1; + } } } .dashboard-panel__panel__button__hotkey { - display: none; + display: block; + } + + &.dashboard-panel__panel__button--no-media:not(.dashboard-panel__panel__button--compact):not( + .dashboard-panel__panel__button--no-thumbnail + ) { + .dashboard-panel__panel__button__hotkey { + left: 10px; + } } &.list { @@ -493,6 +803,8 @@ $dashboard-button-height: 3.625em; background-position: center; > .dashboard-panel__panel__button__content { + flex-direction: row; + align-items: center; > .dashboard-panel__panel__button__sub-label { font-size: 20px; position: relative; @@ -501,49 +813,23 @@ $dashboard-button-height: 3.625em; margin: auto 4px; top: 50%; transform: translateY(-50%); - text-shadow: - -1px -1px 0px rgba(0, 0, 0, 0.5), - 1px 1px 0px rgba(0, 0, 0, 0.5), - 1px -1px 0px rgba(0, 0, 0, 0.5), - 1px -1px 0px rgba(0, 0, 0, 0.5), - 0.5px 0.5px 2px rgba(0, 0, 0, 1); } - > .dashboard-panel__panel__button__label-container { - padding: 3px 4px 0 4px; - top: 50%; - transform: translateY(-50%); - - > .dashboard-panel__panel__button__label { - position: static; - line-height: 0.9em; - line-clamp: 2; - -webkit-line-clamp: 2; - - span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: block; - height: 1em; - line-height: 0.9em; - } - } - } - - > .dashboard-panel__panel__button__thumbnail { + > .dashboard-panel__panel__button__tag-container > .dashboard-panel__panel__button__thumbnail { height: 100%; right: auto; + flex: 0 0 auto; + order: 1; img { height: 100%; } } - > .video-preview { + > .dashboard-panel__panel__button__tag-container > .video-preview { position: absolute; overflow: hidden; - background: #000; + @include checkerboard-background; height: 100%; width: 67px; margin-left: 4px; @@ -561,8 +847,18 @@ $dashboard-button-height: 3.625em; transform: translate(-50%, -50%); } + .video-preview__image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 1; + } + & > * { - @include item-type-colors(); + @include item-type-colors; } } } @@ -576,37 +872,173 @@ $dashboard-button-height: 3.625em; &.transition, &.graphics, &.splits { - > .dashboard-panel__panel__button__content > .dashboard-panel__panel__button__label-container { + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + .dashboard-panel__panel__button__label-container { margin-left: 75px; - word-break: break-word; + overflow-wrap: break-word; } } } + + &.dashboard-panel__panel__button--compact { + height: 2.85em; + width: calc(($dashboard-button-width * 2) + 28px); + + > .dashboard-panel__panel__button__content { + flex-direction: row; + align-items: stretch; + } + + > .dashboard-panel__panel__button__content > .dashboard-panel__panel__button__tag-container { + display: flex; + flex-direction: row; + align-items: stretch; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + } + + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__tag-container--inner { + order: 1; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: row; + align-items: stretch; + } + + .dashboard-panel__panel__button__hotkey { + top: auto; + left: auto; + right: 1px; + bottom: 1px; + } + + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__thumbnail { + order: 3; + flex: 0 0 auto; + height: 100%; + min-height: 0; + width: auto; + align-self: stretch; + } + + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__thumbnail + .dashboard-panel__panel__button__thumbnail__aspect { + height: 100%; + width: auto; + aspect-ratio: 16 / 9; + } + + // Split preview must stay in-flow (like VT); absolute inset collapses width in the row layout. + > .dashboard-panel__panel__button__content + > .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__thumbnail + .dashboard-panel__panel__button__thumbnail__aspect--splits { + position: relative; + inset: unset; + flex: 0 0 auto; + height: 100%; + width: auto; + max-width: none; + } + } } - &.dashboard-panel--take { - display: flex; - flex-direction: column; + .dashboard-panel__panel__button-wrapper { + display: inline-flex; + margin: 4px; + position: relative; + border-radius: 5px; + overflow: hidden; + box-sizing: border-box; - > .dashboard-panel__panel { - flex: 1; + transition: box-shadow 0.2s ease; + + &::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 5px; + border: 1px solid rgba(255, 255, 255, 0.2); + background-color: rgba(0, 0, 0, 0); + pointer-events: none; + box-sizing: border-box; + z-index: 20; + + transition: + box-shadow 0.2s ease, + border-color 0.3s ease, + background-color 0.4s ease-out; } + &:hover { + box-shadow: 0px 0px 8px 0px rgba(255, 255, 255, 0.1); - > .dashboard-panel__buttons { - display: flex; - margin-top: 0.625rem; + &::after { + border: 1px solid rgba(255, 255, 255, 0.5); + } + } + &:hover:not(.live) { + .dashboard-panel__panel__button + .dashboard-panel__panel__button__content + .dashboard-panel__panel__button__tag-container + .dashboard-panel__panel__button__tag-container--inner + .dashboard-panel__panel__button__label-container { + transition: background-color 0.2s ease; + + background-color: rgba(0, 0, 0, 0.55); + } + } + &:active { + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.7); - > .dashboard-panel__panel__button { - flex: 1; - background: var(--general-live-color); - height: 3em; - align-items: center; + &::after { + border: 1px solid rgba(0, 0, 0, 0.7); + background-color: rgba(0, 0, 0, 0.2); + } + .dashboard-panel__panel__button + .dashboard-panel__panel__button__content + .dashboard-panel__panel__button__tag-container + .dashboard-panel__panel__button__tag-container--inner + .dashboard-panel__panel__button__label-container { + transition: background-color 0.2s ease; + + background-color: rgba(0, 0, 0, 0.7); + } + } + &.live { + box-shadow: 0px 0px 8px 0px var(--general-live-color); - > .dashboard-panel__panel__button__label { - width: 100%; - text-align: center; - font-size: 1.4em; - } + &::after { + border: 2px solid var(--general-live-color); + } + .dashboard-panel__panel__button__content + .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__tag-container--inner + > .dashboard-panel__panel__button__label-container { + background-color: rgb(106, 0, 0); + } + } + &.selected { + box-shadow: 0px 0px 8px 0px var(--adlib-item-selected-color); + + &::after { + border: 2px solid var(--adlib-item-selected-color); + } + } + &.next { + box-shadow: 0px 0px 8px 0px var(--general-next-color); + + &::after { + border: 2px solid var(--general-next-color); } } } @@ -646,92 +1078,122 @@ $dashboard-button-height: 3.625em; } } - &.dashboard-panel__panel--bucket { - $dashboard-button-width: 10.625em; - $dashboard-button-height: 8.90625em; - $dashboard-button-label-height: 2.7em; - $dashboard-button-thumbnail-height: $dashboard-button-height - $dashboard-button-label-height; + $dashboard-button-width-bucket: 10.625em; + $dashboard-button-height-bucket: 8.69em; + $dashboard-button-label-height-bucket: 2.7em; + $dashboard-button-thumbnail-height-bucket: $dashboard-button-height-bucket - $dashboard-button-label-height-bucket; - .dashboard-panel__panel__group { - .dashboard-panel__panel__group__liveline { - transform: translate(calc(-1 * #{$dashboard-button-width}), 0); - &::after { - transform: translate(calc(#{$dashboard-button-width} + -1px), 0); - } + .dashboard-panel__panel__group { + .dashboard-panel__panel__group__liveline { + transform: translate(calc(-1 * #{$dashboard-button-width-bucket}), 0); + &::after { + transform: translate(calc(#{$dashboard-button-width-bucket} + -1px), 0); + } - &::before { - transform: translate(calc(#{$dashboard-button-width} + -1px), 0); - } + &::before { + transform: translate(calc(#{$dashboard-button-width-bucket} + -1px), 0); } } + } - .dashboard-panel__panel__button { - border-radius: 0px; - width: $dashboard-button-width; - height: $dashboard-button-height; + .dashboard-panel__panel__button { + border-radius: 0px; + width: $dashboard-button-width-bucket; + height: $dashboard-button-height-bucket; + + &.dashboard-panel__panel__button--no-thumbnail:not(.dashboard-panel__panel__button--compact):not(.list) + > .dashboard-panel__panel__button__content + .dashboard-panel__panel__button__tag-container + > .dashboard-panel__panel__button__tag-container--inner + > .dashboard-panel__panel__button__label-container + > .dashboard-panel__panel__button__label + > .dashboard-panel__panel__button__label__content:not(.dashboard-panel__panel__button__label__content--editable) { + -webkit-line-clamp: 6; + } - &.source-missing, - &.source-not-ready, - &.unknown-state, - &.source-unknown-state { - &::before { - z-index: 2; - } + &.source-missing, + &.source-not-ready, + &.unknown-state, + &.source-unknown-state { + &::before { + z-index: 2; } + } + > .dashboard-panel__panel__button__content { + background: linear-gradient(90deg, rgb(84, 84, 84) 0%, rgb(61, 61, 61) 100%); - > .dashboard-panel__panel__button__content { - background: linear-gradient(90deg, rgb(84, 84, 84) 0%, rgb(61, 61, 61) 100%); - - > .dashboard-panel__panel__button__label-container { - background-color: #000; - height: $dashboard-button-label-height; + .dashboard-panel__panel__button__tag-container { + @include item-type-colors; + > .dashboard-panel__panel__button__tag-container--inner > .dashboard-panel__panel__button__label-container { + background-color: rgba(0, 0, 0, 0.8); display: flex; - position: absolute; - top: auto; - bottom: 0; padding: unset; + min-height: $dashboard-button-label-height-bucket; + height: 100%; + flex: 1 1 $dashboard-button-label-height-bucket; + transition: background-color 0.4s ease-out; - .dashboard-panel__panel__button__tag-container { - flex: 0 0 0.46875em; - z-index: 2; - background: var(--segment-layer-background-unknown); - - @include item-type-colors(); - } - - .dashboard-panel__panel__button__label { - text-align: unset; - padding: 0.1em 0.5px 0em 3px; - vertical-align: unset; - font-size: 1.125em; - font-weight: unset; - text-shadow: unset; - z-index: 2; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - top: unset; + > .dashboard-panel__panel__button__label { + position: static; + display: flex; + align-items: center; + + > .dashboard-panel__panel__button__label__content { + font-family: + Roboto Flex, + Roboto; + font-size: 15px; + line-height: 15.5px; + letter-spacing: 0.4px; + font-variation-settings: + 'wdth' 25, + 'wght' 400, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 14, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + vertical-align: middle; + text-align: unset; + padding: 0.1em 0.5px 0em 3px; + z-index: 2; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + top: unset; + } + > .dashboard-panel__panel__button__label__content span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + height: 1em; + line-height: 0.9em; + } } } + } - > .dashboard-panel__panel__button__thumbnail { - height: $dashboard-button-thumbnail-height; - top: unset; - right: unset; - bottom: unset; - left: unset; - z-index: unset; - } + > .dashboard-panel__panel__button__thumbnail { + height: $dashboard-button-thumbnail-height-bucket; + flex: 0 0 $dashboard-button-thumbnail-height-bucket; + } - > .dashboard-panel__panel__button__sub-label { - padding: 2px; - font-size: 0.9375em; - margin: unset; - background: rgba(0, 0, 0, 0.5); - font-family: 'Roboto Condensed', sans-serif; - text-shadow: unset; - } + > .dashboard-panel__panel__button__sub-label { + padding: 2px; + font-size: 0.9375em; + margin: unset; + background: rgba(0, 0, 0, 0.5); + font-family: 'Roboto Condensed', sans-serif; + text-shadow: unset; } } } diff --git a/packages/webui/src/client/styles/shelf/endWordsPanel.scss b/packages/webui/src/client/styles/shelf/endWordsPanel.scss deleted file mode 100644 index ee46fe28e2b..00000000000 --- a/packages/webui/src/client/styles/shelf/endWordsPanel.scss +++ /dev/null @@ -1,18 +0,0 @@ -.end-words-panel { - overflow: hidden; - position: absolute; - - .timing-clock { - width: 100%; - } - - .text { - text-overflow: ellipsis; - text-align: left; - direction: rtl; - overflow: hidden; - white-space: nowrap; - display: inline-block; - width: 100%; - } -} diff --git a/packages/webui/src/client/styles/shelf/rundownViewShelfAdlibSizeToggle.scss b/packages/webui/src/client/styles/shelf/rundownViewShelfAdlibSizeToggle.scss new file mode 100644 index 00000000000..ebe5aead36f --- /dev/null +++ b/packages/webui/src/client/styles/shelf/rundownViewShelfAdlibSizeToggle.scss @@ -0,0 +1,48 @@ +.rundown-view-shelf.dashboard-panel { + position: relative; + + .rundown-view-shelf__size-toggle-holder { + width: 33px; + z-index: 4; + pointer-events: auto; + flex-shrink: 0; + } + + .rundown-view-shelf__size-toggle { + display: flex; + flex-direction: column; + > button { + border: none; + background: transparent; + padding: 0.15em 0.5em; + line-height: 1; + cursor: pointer; + + > svg { + color: inherit; + > rect { + fill: rgba(255, 255, 255, 0.18); + transition: fill 0.2s ease; + } + } + + &:hover { + svg > rect { + fill: rgba(255, 255, 255, 1); + } + + filter: drop-shadow(0px 0px 2px #ffffff); + } + + &.active:hover { + filter: drop-shadow(0px 0px 2px #ffffff00); + } + + &.active { + svg > rect { + fill: rgba(255, 255, 255, 0.49); + } + } + } + } +} diff --git a/packages/webui/src/client/styles/shelf/showStylePanel.scss b/packages/webui/src/client/styles/shelf/showStylePanel.scss deleted file mode 100644 index 59f27740100..00000000000 --- a/packages/webui/src/client/styles/shelf/showStylePanel.scss +++ /dev/null @@ -1,34 +0,0 @@ -.show-style-panel { - position: absolute; - display: flex; - justify-content: flex-start; - - .show-style-subpanel { - flex: 1; - overflow: hidden; - margin-right: 1em; - font-size: 1.5em; - font-family: - 'Roboto', - Helvetica Neue, - Arial, - sans-serif; - font-weight: 100; - - &__label { - margin-top: 6px; - color: #b8b8b8; - text-transform: uppercase; - white-space: nowrap; - font-weight: 300; - font-size: 0.5em; - } - - &__name { - overflow: hidden; - font-weight: 400; - white-space: nowrap; - text-overflow: ellipsis; - } - } -} diff --git a/packages/webui/src/client/styles/shelf/systemStatusPanel.scss b/packages/webui/src/client/styles/shelf/systemStatusPanel.scss deleted file mode 100644 index 8dd9eaf432d..00000000000 --- a/packages/webui/src/client/styles/shelf/systemStatusPanel.scss +++ /dev/null @@ -1,14 +0,0 @@ -.system-status-panel { - position: absolute; - isolation: isolate; - - .rundown-system-status { - top: 0.2em !important; - transform: unset !important; - left: unset !important; - - .rundown-system-status__indicators { - justify-content: unset !important; - } - } -} diff --git a/packages/webui/src/client/styles/tTimerDisplay.scss b/packages/webui/src/client/styles/tTimerDisplay.scss new file mode 100644 index 00000000000..05c71cb1806 --- /dev/null +++ b/packages/webui/src/client/styles/tTimerDisplay.scss @@ -0,0 +1,80 @@ +@import 'colorScheme'; + +/** + * Default styling for `ui/ClockView/TTimerDisplay.tsx`. + * + * Layout/positioning/font-size should be applied by the embedding view + * (eg. status bar, director screen), but the core look of the timer itself + * is defined here. + */ +.t-timer-display { + display: flex; + align-items: stretch; + justify-content: flex-end; + + background: #333; + border-radius: 0; + overflow: hidden; + + font-family: Roboto Flex; + font-size: 1em; + line-height: 1; + + height: 1.46em; + + &__countdown { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0; + + .countdown__label { + font-variation-settings: + 'wdth' 25, + 'wght' 400, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 30, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + letter-spacing: 0.005em; + text-transform: none; + font-size: 1.3em; + padding-left: 0.125em; + color: #fff; + } + + .countdown__counter { + display: flex; + align-items: center; + font-variation-settings: + 'wdth' 25, + 'wght' 500, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 20, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + padding: 0 0.1em; + .over-under-chip { + position: relative; + left: 0.1em; + --overUnderChipOpsz: 20; + --overUnderChipYtlc: 514; + --overUnderChipMarginLeft: 0.15em; + } + } + } +} diff --git a/packages/webui/src/client/ui/ActiveRundownView.tsx b/packages/webui/src/client/ui/ActiveRundownView.tsx index 189d2258902..5130978115f 100644 --- a/packages/webui/src/client/ui/ActiveRundownView.tsx +++ b/packages/webui/src/client/ui/ActiveRundownView.tsx @@ -1,11 +1,13 @@ -import { NavLink, Route, Switch, useRouteMatch } from 'react-router-dom' +import { NavLink, Route, Switch, useLocation, useRouteMatch } from 'react-router-dom' +import { parse as queryStringParse } from 'query-string' import { useSubscription, useTracker } from '../lib/ReactMeteorData/ReactMeteorData.js' import { Spinner } from '../lib/Spinner.js' import { RundownView } from './RundownView.js' +import { StudioScreenSaver } from './StudioScreenSaver/StudioScreenSaver.js' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { UIStudios } from './Collections.js' -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylists } from '../collections/index.js' import { useTranslation } from 'react-i18next' import { useSetDocumentClass, useSetDocumentDarkTheme } from './util/useSetDocumentClass.js' @@ -14,6 +16,8 @@ export function ActiveRundownView({ studioId }: Readonly<{ studioId: StudioId }> const { t } = useTranslation() const { path } = useRouteMatch() + const { search } = useLocation() + const lockView = queryStringParse(search)['lockView'] === '1' const studioReady = useSubscription(MeteorPubSub.uiStudio, studioId) const playlistReady = useSubscription(MeteorPubSub.rundownPlaylistForStudio, studioId, true) @@ -48,6 +52,9 @@ export function ActiveRundownView({ studioId }: Readonly<{ studioId: StudioId }> ) } else if (studio) { + if (lockView) { + return + } return } else if (studioId) { return diff --git a/packages/webui/src/client/ui/AfterBroadcastForm.tsx b/packages/webui/src/client/ui/AfterBroadcastForm.tsx index 7f128c10c70..39da4b0a00c 100644 --- a/packages/webui/src/client/ui/AfterBroadcastForm.tsx +++ b/packages/webui/src/client/ui/AfterBroadcastForm.tsx @@ -1,14 +1,19 @@ import React, { useMemo, useState } from 'react' import { Meteor } from 'meteor/meteor' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { TFunction, useTranslation } from 'react-i18next' -import { EvaluationBase } from '@sofie-automation/meteor-lib/dist/collections/Evaluations' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { useTranslation } from 'react-i18next' +import type { TFunction } from 'i18next' +import type { EvaluationBase } from '@sofie-automation/meteor-lib/dist/collections/Evaluations' import { doUserAction, UserAction } from '../lib/clientUserAction.js' import { MeteorCall } from '../lib/meteorApi.js' -import { SnapshotId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { SnapshotId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { hashSingleUseToken } from '../lib/lib.js' -import { DropdownInputControl, DropdownInputOption, getDropdownInputOptions } from '../lib/Components/DropdownInput.js' +import { + DropdownInputControl, + type DropdownInputOption, + getDropdownInputOptions, +} from '../lib/Components/DropdownInput.js' import { MultiLineTextInputControl } from '../lib/Components/MultiLineTextInput.js' import { TextInputControl } from '../lib/Components/TextInput.js' import { Spinner } from '../lib/Spinner.js' diff --git a/packages/webui/src/client/ui/App.tsx b/packages/webui/src/client/ui/App.tsx index 279c3b8dab0..3651e927bd1 100644 --- a/packages/webui/src/client/ui/App.tsx +++ b/packages/webui/src/client/ui/App.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import 'moment/min/locales' import { parse as queryStringParse } from 'query-string' @@ -83,33 +83,30 @@ export const App: React.FC = function App() { }, [lastStart]) const mountPWAFullScreenTrigger = useCallback(() => { - document.addEventListener( - 'mousedown', - (event) => { - event.preventDefault() + const onMouseDown = (event: MouseEvent) => { + event.preventDefault() - document.documentElement - .requestFullscreen({ - navigationUI: 'auto', + document.documentElement + .requestFullscreen({ + navigationUI: 'auto', + }) + .then(() => { + document.addEventListener('fullscreenchange', mountPWAFullScreenTrigger, { + once: true, }) - .then(() => { - document.addEventListener('fullscreenchange', mountPWAFullScreenTrigger, { - once: true, - }) - }) - .catch(catchError('documentElement.requestFullscreen')) + }) + .catch(catchError('documentElement.requestFullscreen')) - // Use Keyboard API to lock the keyboard and disable all browser shortcuts - if (!('keyboard' in navigator)) - return // but we check for its availability, so it should be fine. - // Keyboard Lock: https://wicg.github.io/keyboard-lock/ - ;(navigator.keyboard as any).lock().catch(catchError('keyboard.lock')) - }, - { - once: true, - passive: false, - } - ) + // Use Keyboard API to lock the keyboard and disable all browser shortcuts + if (!('keyboard' in navigator)) + return // but we check for its availability, so it should be fine. + // Keyboard Lock: https://wicg.github.io/keyboard-lock/ + ;(navigator.keyboard as any).lock().catch(catchError('keyboard.lock')) + } + document.addEventListener('mousedown', onMouseDown, { + once: true, + passive: false, + }) }, []) useEffect(() => { diff --git a/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimDialog.tsx b/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimDialog.tsx index 717c1f27bdc..dd5f40411a3 100644 --- a/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimDialog.tsx +++ b/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimDialog.tsx @@ -1,19 +1,19 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { ClipTrimPanel } from './ClipTrimPanel.js' -import { VTContent } from '@sofie-automation/blueprints-integration' -import { ModalDialog, SomeEvent } from '../../lib/ModalDialog.js' +import type { VTContent } from '@sofie-automation/blueprints-integration' +import { ModalDialog, type SomeEvent } from '../../lib/ModalDialog.js' import { doUserAction, UserAction } from '../../lib/clientUserAction.js' import { MeteorCall } from '../../lib/meteorApi.js' import { NotificationCenter, Notification, NoticeLevel } from '../../lib/notifications/notifications.js' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' -import { Rundown, getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ReadonlyDeep } from 'type-fest' -import { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' +import { type Rundown, getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { ReadonlyDeep } from 'type-fest' +import type { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' export interface IProps { playlistId: RundownPlaylistId @@ -116,7 +116,7 @@ export function ClipTrimDialog({ NoticeLevel.NOTIFICATION, <> {selectedPiece.name}:  - {t('Trimmed succesfully.')} + {t('Trimmed successfully.')} , protectString('ClipTrimDialog') ) diff --git a/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimPanel.tsx b/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimPanel.tsx index 85f8f10a65f..bfd670235d8 100644 --- a/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimPanel.tsx +++ b/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimPanel.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react' -import { VTContent } from '@sofie-automation/blueprints-integration' +import type { VTContent } from '@sofie-automation/blueprints-integration' import { VideoEditMonitor } from './VideoEditMonitor.js' import { TimecodeEncoder } from './TimecodeEncoder.js' import { faUndo } from '@fortawesome/free-solid-svg-icons' @@ -8,10 +8,10 @@ import Tooltip from 'rc-tooltip' import { useTranslation } from 'react-i18next' import { useContentStatusForPiece } from '../SegmentTimeline/withMediaObjectStatus.js' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' -import { PieceId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { PieceId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Pieces } from '../../collections/index.js' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' -import { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' +import type { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' export interface IProps { studio: UIStudio diff --git a/packages/webui/src/client/ui/ClipTrimPanel/VideoEditMonitor.tsx b/packages/webui/src/client/ui/ClipTrimPanel/VideoEditMonitor.tsx index 6955d9afae3..fb08dc3bd5b 100644 --- a/packages/webui/src/client/ui/ClipTrimPanel/VideoEditMonitor.tsx +++ b/packages/webui/src/client/ui/ClipTrimPanel/VideoEditMonitor.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { withTranslation } from 'react-i18next' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData.js' +import type { Translated } from '../../lib/ReactMeteorData/ReactMeteorData.js' import ClassNames from 'classnames' import { catchError } from '../../lib/lib.js' @@ -16,7 +16,7 @@ interface IState { isMouseDown: boolean } -export const VideoEditMonitor = withTranslation()( +export const VideoEditMonitor: React.ComponentType = withTranslation()( class VideoEditMonitor extends React.Component, IState> { private videoEl: HTMLVideoElement | null = null private retryCount = 0 diff --git a/packages/webui/src/client/ui/ClockView/CameraConfigForm.tsx b/packages/webui/src/client/ui/ClockView/CameraConfigForm.tsx index 78007abc61b..b5b2b9862fb 100644 --- a/packages/webui/src/client/ui/ClockView/CameraConfigForm.tsx +++ b/packages/webui/src/client/ui/ClockView/CameraConfigForm.tsx @@ -1,8 +1,8 @@ import { useState, useCallback, useMemo } from 'react' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ISourceLayer, SourceLayerType } from '@sofie-automation/blueprints-integration' +import type { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { type ISourceLayer, SourceLayerType } from '@sofie-automation/blueprints-integration' import Form from 'react-bootstrap/esm/Form' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' diff --git a/packages/webui/src/client/ui/ClockView/CameraScreen/OrderedPartsProvider.tsx b/packages/webui/src/client/ui/ClockView/CameraScreen/OrderedPartsProvider.tsx index b0f381ff7bf..287f539a17e 100644 --- a/packages/webui/src/client/ui/ClockView/CameraScreen/OrderedPartsProvider.tsx +++ b/packages/webui/src/client/ui/ClockView/CameraScreen/OrderedPartsProvider.tsx @@ -1,5 +1,5 @@ -import React, { PropsWithChildren, useMemo } from 'react' -import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import React, { type PropsWithChildren, useMemo } from 'react' +import type { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { TimingDataResolution, TimingTickResolution, useTiming } from '../../RundownView/RundownTiming/withTiming.js' import { protectStringArray } from '@sofie-automation/corelib/dist/protectedString' diff --git a/packages/webui/src/client/ui/ClockView/CameraScreen/Part.tsx b/packages/webui/src/client/ui/ClockView/CameraScreen/Part.tsx index 49ef23fe776..bf0b035804b 100644 --- a/packages/webui/src/client/ui/ClockView/CameraScreen/Part.tsx +++ b/packages/webui/src/client/ui/ClockView/CameraScreen/Part.tsx @@ -2,7 +2,7 @@ import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import classNames from 'classnames' import { useContext } from 'react' import { AreaZoom } from './index.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { getAllowSpeaking, getAllowVibrating } from '../../../lib/localStorage.js' import { getPartInstanceTimingValue } from '../../../lib/rundownTiming.js' import { AutoNextStatus } from '../../RundownView/RundownTiming/AutoNextStatus.js' @@ -10,9 +10,9 @@ import { CurrentPartOrSegmentRemaining } from '../../RundownView/RundownHeader/C import { PartCountdown } from '../../RundownView/RundownTiming/PartCountdown.js' import { PartDisplayDuration } from '../../RundownView/RundownTiming/PartDuration.js' import { TimingDataResolution, TimingTickResolution, useTiming } from '../../RundownView/RundownTiming/withTiming.js' -import { PartUi } from '../../SegmentContainer/withResolvedSegment.js' +import type { PartUi } from '../../SegmentContainer/withResolvedSegment.js' import { Piece } from './Piece.js' -import { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js' +import type { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js' interface IProps { part: PartUi diff --git a/packages/webui/src/client/ui/ClockView/CameraScreen/Piece.tsx b/packages/webui/src/client/ui/ClockView/CameraScreen/Piece.tsx index ee0b2a63cf2..6951efc04d3 100644 --- a/packages/webui/src/client/ui/ClockView/CameraScreen/Piece.tsx +++ b/packages/webui/src/client/ui/ClockView/CameraScreen/Piece.tsx @@ -1,10 +1,11 @@ import React, { useContext, useMemo } from 'react' -import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' import classNames from 'classnames' import { CanvasSizeContext } from './index.js' import { PieceElement } from '../../SegmentContainer/PieceElement.js' import { getSplitItems } from '../../SegmentContainer/getSplitItems.js' -import { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js' +import { useContentStatusForPieceInstance } from '../../SegmentTimeline/withMediaObjectStatus.js' +import type { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js' const PIECE_TYPE_INDICATOR_BORDER_RADIUS = 11 @@ -24,6 +25,7 @@ export const Piece = React.memo(function Piece({ isLive: boolean }): JSX.Element | null { const canvasWidth = useContext(CanvasSizeContext) + const contentStatus = useContentStatusForPieceInstance(piece.instance) const indicatorPadding = -1 * PIECE_TYPE_INDICATOR_BORDER_RADIUS let pixelLeft = Math.max(indicatorPadding, left * zoom) + PIECE_TYPE_INDICATOR_BORDER_RADIUS @@ -58,7 +60,7 @@ export const Piece = React.memo(function Piece({ partId={partId} style={style} > - {getSplitItems(piece, 'camera-screen__piece-sub-background')} + {getSplitItems(piece, 'camera-screen__piece-sub-background', contentStatus?.boxPreviews)} ) : null} - {getSplitItems(piece, 'camera-screen__piece-type-indicator-sub-background')} + {getSplitItems(piece, 'camera-screen__piece-type-indicator-sub-background', contentStatus?.boxPreviews)}
      {piece.instance.piece.name}
      diff --git a/packages/webui/src/client/ui/ClockView/CameraScreen/Rundown.tsx b/packages/webui/src/client/ui/ClockView/CameraScreen/Rundown.tsx index e42c10e2ca9..933b8a8b08b 100644 --- a/packages/webui/src/client/ui/ClockView/CameraScreen/Rundown.tsx +++ b/packages/webui/src/client/ui/ClockView/CameraScreen/Rundown.tsx @@ -1,15 +1,15 @@ import { useContext } from 'react' import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData.js' -import { Rundown as RundownObj } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { Rundown as RundownObj } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { Segments } from '../../../collections/index.js' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { Segment as SegmentComponent } from './Segment.js' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { UIShowStyleBases } from '../../Collections.js' import { RundownToShowStyleContext, StudioContext } from './index.js' -import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' interface IProps { playlist: DBRundownPlaylist diff --git a/packages/webui/src/client/ui/ClockView/CameraScreen/Segment.tsx b/packages/webui/src/client/ui/ClockView/CameraScreen/Segment.tsx index 3d19fbfa429..ef3c8b7648b 100644 --- a/packages/webui/src/client/ui/ClockView/CameraScreen/Segment.tsx +++ b/packages/webui/src/client/ui/ClockView/CameraScreen/Segment.tsx @@ -4,8 +4,8 @@ import { useContext, useMemo } from 'react' import { ActivePartInstancesContext, PieceFilter } from './index.js' import { withResolvedSegment, - IResolvedSegmentProps as IWithResolvedSegmentProps, - ITrackedResolvedSegmentProps as IWithResolvedSegmentInjectedProps, + type IResolvedSegmentProps as IWithResolvedSegmentProps, + type ITrackedResolvedSegmentProps as IWithResolvedSegmentInjectedProps, } from '../../SegmentContainer/withResolvedSegment.js' import { OrderedPartsContext } from './OrderedPartsProvider.js' import { Part } from './Part.js' diff --git a/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx b/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx index 368ecb0aad7..775dab8096e 100644 --- a/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx +++ b/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx @@ -1,16 +1,16 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { - CameraContent, - RemoteContent, - RemoteSpeakContent, + type CameraContent, + type RemoteContent, + type RemoteSpeakContent, SourceLayerType, - SplitsContent, + type SplitsContent, } from '@sofie-automation/blueprints-integration' -import { RundownId, ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { RundownId, ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { Rundowns } from '../../../collections/index.js' import { useSubscription, useSubscriptionIfEnabled, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData.js' import { UIPartInstances, UIStudios } from '../../Collections.js' @@ -25,10 +25,14 @@ import { useBlackBrowserTheme } from '../../../lib/useBlackBrowserTheme.js' import { useWakeLock } from './useWakeLock.js' import { useDebounce } from '../../../lib/lib.js' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' -import { useSetDocumentClass, useSetDocumentDarkTheme } from '../../util/useSetDocumentClass.js' -import { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' -import { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js' +import { + useSetDocumentClass, + useSetDocumentDarkTheme, + useOwnedElementClassToggle, +} from '../../util/useSetDocumentClass.js' +import type { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' +import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import type { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js' interface IProps { playlist: DBRundownPlaylist | undefined @@ -145,15 +149,7 @@ export function CameraScreen({ playlist, studioId }: Readonly): JSX.Elem useSetDocumentClass('dark', 'xdark', 'vertical-overflow-only') useSetDocumentDarkTheme() - - useEffect(() => { - const containerEl = document.querySelector('#render-target > .container-fluid.header-clear') - if (containerEl) containerEl.classList.remove('header-clear') - - return () => { - if (containerEl) containerEl.classList.add('header-clear') - } - }, []) + useOwnedElementClassToggle('#render-target > .container-fluid', 'header-clear') const studio = useTracker(() => UIStudios.findOne(studioId), [studioId], undefined) diff --git a/packages/webui/src/client/ui/ClockView/ClockView.tsx b/packages/webui/src/client/ui/ClockView/ClockView.tsx index 8674369ad95..be2ac34d6da 100644 --- a/packages/webui/src/client/ui/ClockView/ClockView.tsx +++ b/packages/webui/src/client/ui/ClockView/ClockView.tsx @@ -9,7 +9,7 @@ import { DirectorScreen } from './DirectorScreen/DirectorScreen' import { OverlayScreen } from './OverlayScreen.js' import { OverlayScreenSaver } from './OverlayScreenSaver.js' import { RundownPlaylists } from '../../collections/index.js' -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CameraScreen } from './CameraScreen/index.js' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { useTranslation } from 'react-i18next' diff --git a/packages/webui/src/client/ui/ClockView/ClockViewIndex.tsx b/packages/webui/src/client/ui/ClockView/ClockViewIndex.tsx index b48283a81d9..498042c1fee 100644 --- a/packages/webui/src/client/ui/ClockView/ClockViewIndex.tsx +++ b/packages/webui/src/client/ui/ClockView/ClockViewIndex.tsx @@ -1,7 +1,7 @@ import { useState, useCallback } from 'react' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import Container from 'react-bootstrap/esm/Container' import Accordion from 'react-bootstrap/esm/Accordion' import { PresenterConfigForm } from './PresenterConfigForm' diff --git a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewFreezeCount.tsx b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewFreezeCount.tsx index a1f0b0e1661..569bb9ee860 100644 --- a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewFreezeCount.tsx +++ b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewFreezeCount.tsx @@ -1,10 +1,10 @@ import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData.js' -import { SourceLayerType, VTContent } from '@sofie-automation/blueprints-integration' +import { SourceLayerType, type VTContent } from '@sofie-automation/blueprints-integration' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { findPieceInstanceToShow } from './utils.js' import { Timediff } from '../Timediff.js' import { getCurrentTime } from '../../../lib/systemTime.js' -import { +import type { PartInstanceId, RundownId, RundownPlaylistActivationId, diff --git a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewPieceIcon.tsx b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewPieceIcon.tsx index 29bb7e6302c..3f60c9ae468 100644 --- a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewPieceIcon.tsx +++ b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewPieceIcon.tsx @@ -1,10 +1,10 @@ import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData.js' import { SourceLayerType, - ISourceLayer, - CameraContent, - RemoteContent, - EvsContent, + type ISourceLayer, + type CameraContent, + type RemoteContent, + type EvsContent, } from '@sofie-automation/blueprints-integration' import { CamInputIcon } from './ClockViewRenderers/CamInputIcon.js' import { VTInputIcon } from './ClockViewRenderers/VTInputIcon.js' @@ -15,16 +15,16 @@ import { RemoteSpeakInputIcon } from './ClockViewRenderers/RemoteSpeakInputIcon. import { GraphicsInputIcon } from './ClockViewRenderers/GraphicsInputIcon.js' import { UnknownInputIcon } from './ClockViewRenderers/UnknownInputIcon.js' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { findPieceInstanceToShow, findPieceInstanceToShowFromInstances } from './utils.js' import LocalInputIcon from './ClockViewRenderers/LocalInputIcon.js' -import { +import type { PartInstanceId, RundownId, RundownPlaylistActivationId, ShowStyleBaseId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ReadonlyDeep } from 'type-fest' +import type { ReadonlyDeep } from 'type-fest' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export interface IPropsHeader { diff --git a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewPieceName.tsx b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewPieceName.tsx index 129cb47df74..b93debe097a 100644 --- a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewPieceName.tsx +++ b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewPieceName.tsx @@ -1,14 +1,14 @@ import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData.js' -import { EvsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' +import { type EvsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { IPropsHeader } from './ClockViewPieceIcon.js' +import type { IPropsHeader } from './ClockViewPieceIcon.js' import { findPieceInstanceToShow } from './utils.js' -import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ReadonlyDeep } from 'type-fest' +import type { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { ReadonlyDeep } from 'type-fest' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' -import { AdjustLabelFit, AdjustLabelFitProps } from '../../util/AdjustLabelFit.js' +import { AdjustLabelFit, type AdjustLabelFitProps } from '../../util/AdjustLabelFit.js' interface INamePropsHeader extends IPropsHeader { partName: string diff --git a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewRenderers/RemoteInputIcon.tsx b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewRenderers/RemoteInputIcon.tsx index 1261866a69c..47e9fb37afe 100644 --- a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewRenderers/RemoteInputIcon.tsx +++ b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewRenderers/RemoteInputIcon.tsx @@ -1,5 +1,3 @@ -import React from 'react' - export function BaseRemoteInputIcon(props: Readonly>): JSX.Element { return
      {props.children}
      } diff --git a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewRenderers/SplitInputIcon.tsx b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewRenderers/SplitInputIcon.tsx index 54450742a04..06125d8c716 100644 --- a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewRenderers/SplitInputIcon.tsx +++ b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/ClockViewRenderers/SplitInputIcon.tsx @@ -1,7 +1,7 @@ import * as React from 'react' -import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { SplitsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' -import { ReadonlyDeep } from 'type-fest' +import type { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { type SplitsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' +import type { ReadonlyDeep } from 'type-fest' import { RundownUtils } from '../../../../lib/rundown.js' type SplitIconPieceType = ReadonlyDeep> diff --git a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/utils.ts b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/utils.ts index d8e79b7595e..24622603f9c 100644 --- a/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/utils.ts +++ b/packages/webui/src/client/ui/ClockView/ClockViewPieceIcons/utils.ts @@ -1,11 +1,11 @@ -import { SourceLayerType, ISourceLayer } from '@sofie-automation/blueprints-integration' -import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { IPropsHeader } from './ClockViewPieceIcon.js' +import type { SourceLayerType, ISourceLayer } from '@sofie-automation/blueprints-integration' +import type { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { IPropsHeader } from './ClockViewPieceIcon.js' import { UIShowStyleBases } from '../../Collections.js' import { PieceInstances } from '../../../collections/index.js' -import { ReadonlyDeep } from 'type-fest' -import { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js' +import type { ReadonlyDeep } from 'type-fest' +import type { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js' export interface IFoundPieceInstance { sourceLayer: ISourceLayer | undefined diff --git a/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx index ffe2824ef35..8ac2d1bf618 100644 --- a/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx @@ -1,8 +1,11 @@ import ClassNames from 'classnames' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { PartUi } from '../../SegmentTimeline/SegmentTimelineContainer.js' -import { DBRundownPlaylist, ABSessionAssignment } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { PartUi } from '../../SegmentTimeline/SegmentTimelineContainer.js' +import type { + DBRundownPlaylist, + ABSessionAssignment, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import type { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { useSubscription, useSubscriptions, @@ -16,17 +19,17 @@ import { PieceNameContainer } from '../ClockViewPieceIcons/ClockViewPieceName.js import { Timediff } from '../Timediff.js' import { RundownUtils } from '../../../lib/rundown.js' import { PieceLifespan, SourceLayerType } from '@sofie-automation/blueprints-integration' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PieceFreezeContainer } from '../ClockViewPieceIcons/ClockViewFreezeCount.js' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -import { +import type { RundownId, RundownPlaylistId, ShowStyleBaseId, ShowStyleVariantId, StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' +import type { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { calculatePartInstanceExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { UIShowStyleBases, UIStudios } from '../../Collections.js' import { PieceInstances, RundownPlaylists, Rundowns, ShowStyleVariants } from '../../../collections/index.js' @@ -40,14 +43,13 @@ import { CurrentPartOrSegmentRemaining } from '../../RundownView/RundownHeader/C import { AdjustLabelFit } from '../../util/AdjustLabelFit.js' import { AutoNextStatus } from '../../RundownView/RundownTiming/AutoNextStatus.js' import { useTranslation } from 'react-i18next' -import { DBShowStyleBase, UIShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { TTimerDisplay } from '../TTimerDisplay.js' -import { getDefaultTTimer } from '../../../lib/tTimerUtils.js' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance.js' +import type { DBShowStyleBase, UIShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance.js' import { DirectorScreenTop } from './DirectorScreenTop.js' import { useTiming } from '../../RundownView/RundownTiming/withTiming.js' -import { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import type { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' +import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import { RundownStatusBar } from '../RundownStatusBar.js' interface SegmentUi extends DBSegment { items: Array @@ -139,7 +141,6 @@ export interface DirectorScreenTrackedProps { nextShowStyleBaseId: ShowStyleBaseId | undefined showStyleBaseIds: ShowStyleBaseId[] rundownIds: RundownId[] - partInstanceToCountTimeFrom: PartInstance | undefined } function getShowStyleBaseIdSegmentPartUi( @@ -268,7 +269,6 @@ const getDirectorScreenReactive = (props: DirectorScreenProps): DirectorScreenTr let nextSegment: SegmentUi | undefined = undefined let nextPartInstanceUi: PartUi | undefined = undefined let nextShowStyleBaseId: ShowStyleBaseId | undefined = undefined - let partInstanceToCountTimeFromUi: PartInstance | undefined = undefined if (playlist) { rundowns = RundownPlaylistCollectionUtil.getRundownsOrdered(playlist) @@ -281,10 +281,7 @@ const getDirectorScreenReactive = (props: DirectorScreenProps): DirectorScreenTr } showStyleBaseIds = rundowns.map((rundown) => rundown.showStyleBaseId) - const { currentPartInstance, nextPartInstance, partInstanceToCountTimeFrom } = - RundownPlaylistClientUtil.getSelectedPartInstances(playlist) - - partInstanceToCountTimeFromUi = partInstanceToCountTimeFrom + const { currentPartInstance, nextPartInstance } = RundownPlaylistClientUtil.getSelectedPartInstances(playlist) const partInstance = currentPartInstance ?? nextPartInstance if (partInstance) { @@ -354,7 +351,6 @@ const getDirectorScreenReactive = (props: DirectorScreenProps): DirectorScreenTr nextSegment, nextPartInstance: nextPartInstanceUi, nextShowStyleBaseId, - partInstanceToCountTimeFrom: partInstanceToCountTimeFromUi, } } @@ -378,6 +374,7 @@ function useDirectorScreenSubscriptions(props: DirectorScreenProps): void { useSubscription(CorelibPubSub.segments, rundownIds, {}) useSubscription(CorelibPubSub.parts, rundownIds, null) + useSubscription(MeteorPubSub.uiParts, playlist?._id ?? null) useSubscription(MeteorPubSub.uiPartInstances, playlist?.activationId ?? null) useSubscriptions( MeteorPubSub.uiShowStyleBase, @@ -386,11 +383,7 @@ function useDirectorScreenSubscriptions(props: DirectorScreenProps): void { useSubscription(CorelibPubSub.showStyleVariants, null, showStyleVariantIds) useSubscription(MeteorPubSub.rundownLayouts, showStyleBaseIds) - const { - currentPartInstance, - nextPartInstance, - partInstanceToCountTimeFrom: firstTakenPartInstance, - } = useTracker( + const { currentPartInstance, nextPartInstance } = useTracker( () => { const playlist = RundownPlaylists.findOne(props.playlistId, { fields: { @@ -408,7 +401,6 @@ function useDirectorScreenSubscriptions(props: DirectorScreenProps): void { currentPartInstance: undefined, nextPartInstance: undefined, previousPartInstance: undefined, - partInstanceToCountTimeFrom: undefined, } } }, @@ -417,14 +409,12 @@ function useDirectorScreenSubscriptions(props: DirectorScreenProps): void { currentPartInstance: undefined, nextPartInstance: undefined, previousPartInstance: undefined, - partInstanceToCountTimeFrom: undefined, } ) useSubscriptions(CorelibPubSub.pieceInstances, [ currentPartInstance && [[currentPartInstance.rundownId], [currentPartInstance._id], {}], nextPartInstance && [[nextPartInstance.rundownId], [nextPartInstance._id], {}], - firstTakenPartInstance && [[firstTakenPartInstance.rundownId], [firstTakenPartInstance._id], {}], ]) } @@ -446,7 +436,6 @@ function DirectorScreenRender({ nextPartInstance, nextSegment, rundownIds, - partInstanceToCountTimeFrom, }: Readonly) { useSetDocumentClass('dark', 'xdark') const { t } = useTranslation() @@ -571,11 +560,9 @@ function DirectorScreenRender({ } } - const activeTTimer = getDefaultTTimer(playlist.tTimers) - return (
      - +
      { // Current Part: @@ -758,12 +745,8 @@ function DirectorScreenRender({ ) : null}
      - {!!activeTTimer && ( -
      - -
      - )}
      + ) } diff --git a/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreenTop.tsx b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreenTop.tsx index 686f20a30cb..390df4f0018 100644 --- a/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreenTop.tsx +++ b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreenTop.tsx @@ -1,86 +1,69 @@ -import { - OverUnderClockComponent, - PlannedEndComponent, - TimeSincePlannedEndComponent, - TimeToPlannedEndComponent, -} from '../../../lib/Components/CounterComponents' -import { useTiming } from '../../RundownView/RundownTiming/withTiming' -import { getPlaylistTimingDiff } from '../../../lib/rundownTiming' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { getCurrentTime } from '../../../lib/systemTime' -import { useTranslation } from 'react-i18next' +import { PlannedEndComponent, TimeToFromPlannedEndComponent } from '../../../lib/Components/CounterComponents' +import { useTiming } from '../../RundownView/RundownTiming/withTiming.js' +import { getPlaylistTimingDiff } from '../../../lib/rundownTiming.js' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { getCurrentTime } from '../../../lib/systemTime.js' +import { useTranslation } from 'react-i18next' +import { OverUnderChip } from '../../../lib/Components/OverUnderChip.js' export interface DirectorScreenTopProps { playlist: DBRundownPlaylist - partInstanceToCountTimeFrom: PartInstance | undefined } -export function DirectorScreenTop({ - playlist, - partInstanceToCountTimeFrom, -}: Readonly): JSX.Element { +export function DirectorScreenTop({ playlist }: Readonly): JSX.Element { const timingDurations = useTiming() - - const rehearsalInProgress = Boolean(playlist.rehearsal && partInstanceToCountTimeFrom?.timings?.take) - - const expectedStart = rehearsalInProgress - ? partInstanceToCountTimeFrom?.timings?.take || 0 - : PlaylistTiming.getExpectedStart(playlist.timing) || 0 - const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) || 0 - - const expectedEnd = rehearsalInProgress - ? (partInstanceToCountTimeFrom?.timings?.take || 0) + expectedDuration - : PlaylistTiming.getExpectedEnd(playlist.timing) + const { t } = useTranslation() const now = timingDurations.currentTime ?? getCurrentTime() - const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0 + const rehearsalInProgress = Boolean(playlist.rehearsal && playlist.startedPlayback) - const { t } = useTranslation() + const startedPlayback = playlist.activationId ? playlist.startedPlayback : undefined + + const estimatedEnd = PlaylistTiming.getEstimatedEnd( + playlist.timing, + now, + timingDurations.remainingPlaylistDuration, + startedPlayback + ) + + const remainingDuration = PlaylistTiming.getRemainingDuration( + playlist.timing, + now, + timingDurations.remainingPlaylistDuration, + startedPlayback + ) return ( -
      - {expectedEnd ? ( -
      -
      - -
      - {t('Planned End')} -
      - ) : null} - {expectedEnd && expectedEnd > now ? ( -
      -
      - + <> +
      + {estimatedEnd !== undefined ? ( +
      +
      + +
      + {rehearsalInProgress ? t('Rehearsal end') : t('Estimated end')}
      - - {rehearsalInProgress ? t('Time to rehearsal end') : t('Time to planned end')} - -
      - ) : ( -
      -
      - - - {rehearsalInProgress ? t('Time since rehearsal end') : t('Time since planned end')} + ) : null} + + {remainingDuration !== undefined ? ( +
      +
      + +
      + + {rehearsalInProgress + ? remainingDuration >= 0 + ? t('Time to rehearsal end') + : t('Time since rehearsal end') + : t('Remaining duration')}
      -
      - )} -
      -
      - -
      - {t('Over/Under')} + ) : null} +
      -
      + + ) } diff --git a/packages/webui/src/client/ui/ClockView/FullscreenLink.tsx b/packages/webui/src/client/ui/ClockView/FullscreenLink.tsx index 9e184f7a100..6c282536ddb 100644 --- a/packages/webui/src/client/ui/ClockView/FullscreenLink.tsx +++ b/packages/webui/src/client/ui/ClockView/FullscreenLink.tsx @@ -1,4 +1,4 @@ -import { useCallback, MouseEvent } from 'react' +import { useCallback, type MouseEvent } from 'react' import { Link, useHistory } from 'react-router-dom' import { catchError } from '../../lib/lib.js' diff --git a/packages/webui/src/client/ui/ClockView/MultiviewScreen.tsx b/packages/webui/src/client/ui/ClockView/MultiviewScreen.tsx index 477f93c15c1..b1b6c7daa8e 100644 --- a/packages/webui/src/client/ui/ClockView/MultiviewScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/MultiviewScreen.tsx @@ -1,4 +1,4 @@ -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import './MultiviewScreen.scss' interface MultiviewScreenProps { diff --git a/packages/webui/src/client/ui/ClockView/OverlayScreen.tsx b/packages/webui/src/client/ui/ClockView/OverlayScreen.tsx index d4db7c92f13..7fe08c5eb55 100644 --- a/packages/webui/src/client/ui/ClockView/OverlayScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/OverlayScreen.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import { useEffect } from 'react' import Moment from 'react-moment' import { useTranslation } from 'react-i18next' import { useTiming } from '../RundownView/RundownTiming/withTiming.js' @@ -7,7 +7,7 @@ import { PieceIconContainer } from '../PieceIcons/PieceIcon.js' import { PieceNameContainer } from '../PieceIcons/PieceName.js' import { Timediff } from './Timediff.js' import { getPresenterScreenReactive, usePresenterScreenSubscriptions } from './PresenterScreen.js' -import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' interface TimeMap { [key: string]: number diff --git a/packages/webui/src/client/ui/ClockView/OverlayScreenSaver.tsx b/packages/webui/src/client/ui/ClockView/OverlayScreenSaver.tsx index 1982caf5462..63566778590 100644 --- a/packages/webui/src/client/ui/ClockView/OverlayScreenSaver.tsx +++ b/packages/webui/src/client/ui/ClockView/OverlayScreenSaver.tsx @@ -3,9 +3,9 @@ import { Clock } from '../StudioScreenSaver/Clock.js' import { useTracker, useSubscription } from '../../lib/ReactMeteorData/ReactMeteorData.js' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { findNextPlaylist } from '../StudioScreenSaver/StudioScreenSaver.js' -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { useSetDocumentClass } from '../util/useSetDocumentClass.js' -import { AnimationPlaybackControls, animate as motionAnimate } from 'motion' +import { type AnimationPlaybackControls, animate as motionAnimate } from 'motion' export function OverlayScreenSaver({ studioId }: Readonly<{ studioId: StudioId }>): JSX.Element { const studioNameRef = useRef(null) diff --git a/packages/webui/src/client/ui/ClockView/PresenterConfigForm.tsx b/packages/webui/src/client/ui/ClockView/PresenterConfigForm.tsx index d087f59ae60..b2040cda6cc 100644 --- a/packages/webui/src/client/ui/ClockView/PresenterConfigForm.tsx +++ b/packages/webui/src/client/ui/ClockView/PresenterConfigForm.tsx @@ -1,7 +1,7 @@ import { useState, useCallback, useMemo } from 'react' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import Form from 'react-bootstrap/esm/Form' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' diff --git a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx index b80206229f9..daa0975f3ea 100644 --- a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx @@ -1,8 +1,8 @@ import ClassNames from 'classnames' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { PartUi } from '../SegmentTimeline/SegmentTimelineContainer.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import type { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import type { PartUi } from '../SegmentTimeline/SegmentTimelineContainer.js' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import type { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { useTiming } from '../RundownView/RundownTiming/withTiming.js' import { useSubscription, @@ -18,11 +18,11 @@ import { PieceNameContainer } from '../PieceIcons/PieceName.js' import { Timediff } from './Timediff.js' import { RundownUtils } from '../../lib/rundown.js' import { CountdownType, PieceLifespan } from '@sofie-automation/blueprints-integration' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PieceCountdownContainer } from '../PieceIcons/PieceCountdown.js' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -import { DashboardLayout, RundownLayoutBase } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { +import type { DashboardLayout, RundownLayoutBase } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' +import type { RundownId, RundownLayoutId, RundownPlaylistId, @@ -30,12 +30,12 @@ import { ShowStyleVariantId, StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' +import type { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { RundownLayoutsAPI } from '../../lib/rundownLayouts.js' import { ShelfDashboardLayout } from '../Shelf/ShelfDashboardLayout.js' import { parse as queryStringParse } from 'query-string' import { calculatePartInstanceExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' -import { getPlaylistTimingDiff, RundownTimingContext } from '../../lib/rundownTiming.js' +import type { RundownTimingContext } from '../../lib/rundownTiming.js' import { UIShowStyleBases, UIStudios } from '../Collections.js' import { PieceInstances, @@ -50,11 +50,11 @@ import { useSetDocumentClass, useSetDocumentDarkTheme } from '../util/useSetDocu import { useRundownAndShowStyleIdsForPlaylist } from '../util/useRundownAndShowStyleIdsForPlaylist.js' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil.js' import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js' -import { TTimerDisplay } from './TTimerDisplay.js' -import { getDefaultTTimer } from '../../lib/tTimerUtils.js' -import { UIShowStyleBase } from '@sofie-automation/corelib/src/dataModel/ShowStyleBase.js' -import { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' -import { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import { RundownStatusBar } from './RundownStatusBar.js' +import type { UIShowStyleBase } from '@sofie-automation/corelib/src/dataModel/ShowStyleBase.js' +import type { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js' +import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js' +import { OverUnderChip } from '../../lib/Components/OverUnderChip.js' // TODO: We have another definition of this in the Director screen, and there is also another SegmentUI type. We should look into clearing this up. interface SegmentUi extends DBSegment { @@ -88,6 +88,8 @@ export interface PresenterScreenTrackedProps { rundownIds: RundownId[] rundownLayouts?: Array presenterLayoutId: RundownLayoutId | undefined + margin: number | undefined + fontSize: number | undefined } function getShowStyleBaseIdSegmentPartUi( @@ -218,6 +220,18 @@ export const getPresenterScreenReactive = ( const params = queryStringParse(location.search) const presenterLayoutId = protectString((params['presenterLayout'] as string) || '') + const margin = (() => { + // Support both `margin` (PrompterView) and `margins` / `m` (legacy/typos in URLs) + const raw = (params['margin'] ?? params['margins'] ?? params['m']) as string + const val = Number.parseInt(raw, 10) + return Number.isNaN(val) ? undefined : val + })() + const fontSize = (() => { + // Support both `fontsize` (PrompterView) and `fontSize` (camelCase URLs) + const raw = (params['fontsize'] ?? params['fontSize']) as string + const val = Number.parseInt(raw, 10) + return Number.isNaN(val) ? undefined : val + })() if (playlist) { rundowns = RundownPlaylistCollectionUtil.getRundownsOrdered(playlist) @@ -301,6 +315,8 @@ export const getPresenterScreenReactive = ( rundownLayouts: rundowns.length > 0 ? RundownLayouts.find({ showStyleBaseId: rundowns[0].showStyleBaseId }).fetch() : undefined, presenterLayoutId, + margin, + fontSize, } } @@ -379,6 +395,8 @@ export function PresenterScreen({ playlistId, studioId }: PresenterScreenProps): rundowns={presenterScreenProps?.rundowns ?? []} segments={presenterScreenProps?.segments ?? []} showStyleBaseIds={presenterScreenProps?.showStyleBaseIds ?? []} + margin={presenterScreenProps?.margin} + fontSize={presenterScreenProps?.fontSize} studio={presenterScreenProps?.studio} studioId={studioId} timingDurations={timing} @@ -496,12 +514,11 @@ function PresenterScreenContentDefaultLayout({ timingDurations.remainingBudgetOnCurrentSegment ?? timingDurations.remainingTimeOnCurrentPart ?? 0 const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) - const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0 - const activeTTimer = getDefaultTTimer(playlist.tTimers) return (
      +
      ) : null}
      -
      -
      - {playlist ? playlist.name : 'UNKNOWN'} -
      -
      - {!!activeTTimer && } -
      -
      = 0, - })} - > - {RundownUtils.formatDiffToTimecode(overUnderClock, true, false, true, true, true, undefined, true, true)} -
      -
      +
      ) } diff --git a/packages/webui/src/client/ui/ClockView/PrompterConfigForm.tsx b/packages/webui/src/client/ui/ClockView/PrompterConfigForm.tsx index f9c86b18ad0..019bdcb9b7d 100644 --- a/packages/webui/src/client/ui/ClockView/PrompterConfigForm.tsx +++ b/packages/webui/src/client/ui/ClockView/PrompterConfigForm.tsx @@ -1,7 +1,7 @@ import { useState, useCallback, useMemo } from 'react' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import Form from 'react-bootstrap/esm/Form' import Collapse from 'react-bootstrap/esm/Collapse' import { FullscreenLink } from './FullscreenLink.js' diff --git a/packages/webui/src/client/ui/ClockView/RundownStatusBar.tsx b/packages/webui/src/client/ui/ClockView/RundownStatusBar.tsx new file mode 100644 index 00000000000..830f8de2fa5 --- /dev/null +++ b/packages/webui/src/client/ui/ClockView/RundownStatusBar.tsx @@ -0,0 +1,35 @@ +import ClassNames from 'classnames' +import { getDefaultTTimer } from '../../lib/tTimerUtils.js' +import { TTimerDisplay } from './TTimerDisplay.js' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist.js' +import { useTranslation } from 'react-i18next' + +interface RundownStatusBarProps { + playlist?: DBRundownPlaylist + className?: string + showPlaylistName?: boolean +} + +export function RundownStatusBar({ + playlist, + className, + showPlaylistName = true, +}: Readonly): JSX.Element { + const activeTTimer = playlist ? getDefaultTTimer(playlist.tTimers) : undefined + const { t } = useTranslation() + + const playlistName = playlist?.name ?? t('Unknown') + + return ( +
      + {showPlaylistName &&
      {playlistName}
      } +
      +
      {!!activeTTimer && }
      +
      +
      + ) +} diff --git a/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx b/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx index c26a084a521..df3244ad74a 100644 --- a/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx +++ b/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx @@ -1,55 +1,37 @@ -import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import { RundownUtils } from '../../lib/rundown.js' -import { calculateTTimerDiff, calculateTTimerOverUnder } from '../../lib/tTimerUtils' -import { useTiming } from '../RundownView/RundownTiming/withTiming' -import classNames from 'classnames' +import { calculateTTimerDiff, calculateTTimerOverUnder } from '../../lib/tTimerUtils.js' +import { useTiming } from '../RundownView/RundownTiming/withTiming.js' +import { OverUnderChip } from '../../lib/Components/OverUnderChip.js' +import { Countdown } from '../RundownView/RundownHeader/Countdown.js' +import { getCurrentTime } from '../../lib/systemTime.js' interface TTimerDisplayProps { timer: RundownTTimer } export function TTimerDisplay({ timer }: Readonly): JSX.Element | null { - useTiming() + const timing = useTiming() if (!timer.mode) return null - const now = Date.now() + const now = timing.currentTime ?? getCurrentTime() const diff = calculateTTimerDiff(timer, now) const overUnder = calculateTTimerOverUnder(timer, now) - - const timerStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true) - const timerParts = timerStr.split(':') + const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true) const timerSign = diff >= 0 ? '' : '-' return (
      - {timer.label} - - {timerSign} - {timerParts.map((p, i) => ( - - {p} - {i < timerParts.length - 1 && :} - - ))} - - {overUnder !== undefined && ( - 0, - 't-timer-display__over-under--under': overUnder <= 0, - })} - > - {overUnder > 0 ? '+' : '\u2013'} - {RundownUtils.formatDiffToTimecode(Math.abs(overUnder), false, true, true, false, true)} - - )} + } + > + {`${timerSign}${timeStr}`} +
      ) } diff --git a/packages/webui/src/client/ui/FloatingInspector.tsx b/packages/webui/src/client/ui/FloatingInspector.tsx index fb57fa2fd4a..8d3387bf66d 100644 --- a/packages/webui/src/client/ui/FloatingInspector.tsx +++ b/packages/webui/src/client/ui/FloatingInspector.tsx @@ -1,4 +1,3 @@ -import React from 'react' import Escape from './../lib/Escape.js' interface IProps { diff --git a/packages/webui/src/client/ui/MediaStatus/MediaStatus.tsx b/packages/webui/src/client/ui/MediaStatus/MediaStatus.tsx index b3133044425..cfa2b4d88aa 100644 --- a/packages/webui/src/client/ui/MediaStatus/MediaStatus.tsx +++ b/packages/webui/src/client/ui/MediaStatus/MediaStatus.tsx @@ -1,7 +1,7 @@ -import { useMemo, JSX } from 'react' +import { useMemo, type JSX } from 'react' import { useSubscription, useSubscriptions, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { +import type { AdLibActionId, PartId, PartInstanceId, @@ -24,13 +24,13 @@ import { RundownPlaylists, Rundowns, } from '../../collections/index.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { ExpectedPackage } from '@sofie-automation/shared-lib/dist/package-manager/package' -import { PartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part' -import { IBlueprintActionManifestDisplayContent, SourceLayerType } from '@sofie-automation/blueprints-integration' -import { PieceContentStatusObj } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' -import { Piece, PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' +import { type ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import type { ExpectedPackage } from '@sofie-automation/shared-lib/dist/package-manager/package' +import type { PartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { IBlueprintActionManifestDisplayContent, SourceLayerType } from '@sofie-automation/blueprints-integration' +import type { PieceContentStatusObj } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' +import { type Piece, PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' import { UIPieceContentStatuses, UIShowStyleBases } from '../Collections.js' import { isTranslatableMessage, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' @@ -122,7 +122,7 @@ function useRundownPlaylists(playlistIds: RundownPlaylistId[]) { privateData: 0, notes: 0, segmentTiming: 0, - showShelf: 0, + displayMinishelf: 0, }, }, partInstances: { diff --git a/packages/webui/src/client/ui/MediaStatus/MediaStatusIndicator.tsx b/packages/webui/src/client/ui/MediaStatus/MediaStatusIndicator.tsx index 2fb8ffe1da1..b3f848939cd 100644 --- a/packages/webui/src/client/ui/MediaStatus/MediaStatusIndicator.tsx +++ b/packages/webui/src/client/ui/MediaStatus/MediaStatusIndicator.tsx @@ -1,4 +1,4 @@ -import React, { JSX } from 'react' +import React, { type JSX } from 'react' import Tooltip from 'rc-tooltip' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { assertNever } from '@sofie-automation/corelib/dist/lib' diff --git a/packages/webui/src/client/ui/MediaStatus/SortOrderButton.tsx b/packages/webui/src/client/ui/MediaStatus/SortOrderButton.tsx index c337977c007..1ff43de159f 100644 --- a/packages/webui/src/client/ui/MediaStatus/SortOrderButton.tsx +++ b/packages/webui/src/client/ui/MediaStatus/SortOrderButton.tsx @@ -1,4 +1,4 @@ -import { JSX } from 'react' +import type { JSX } from 'react' import { assertNever } from '@sofie-automation/corelib/dist/lib' import { SortAscending, SortDescending, SortDisabled } from '../../lib/ui/icons/sorting.js' diff --git a/packages/webui/src/client/ui/PieceIcons/PieceCountdown.tsx b/packages/webui/src/client/ui/PieceIcons/PieceCountdown.tsx index 1ff82fea984..b33a114b291 100644 --- a/packages/webui/src/client/ui/PieceIcons/PieceCountdown.tsx +++ b/packages/webui/src/client/ui/PieceIcons/PieceCountdown.tsx @@ -1,10 +1,10 @@ import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' -import { SourceLayerType, VTContent } from '@sofie-automation/blueprints-integration' +import { SourceLayerType, type VTContent } from '@sofie-automation/blueprints-integration' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { findPieceInstanceToShow } from './utils.js' import { Timediff } from '../ClockView/Timediff.js' import { getCurrentTime } from '../../lib/systemTime.js' -import { +import type { PartInstanceId, RundownId, RundownPlaylistActivationId, diff --git a/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx b/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx index 120bbe4d5ad..058afd12355 100644 --- a/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx @@ -1,10 +1,10 @@ import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' import { SourceLayerType, - ISourceLayer, - CameraContent, - RemoteContent, - EvsContent, + type ISourceLayer, + type CameraContent, + type RemoteContent, + type EvsContent, } from '@sofie-automation/blueprints-integration' import { CamInputIcon } from './Renderers/CamInputIcon.js' import { VTInputIcon } from './Renderers/VTInputIcon.js' @@ -15,16 +15,16 @@ import { RemoteSpeakInputIcon } from './Renderers/RemoteSpeakInputIcon.js' import { GraphicsInputIcon } from './Renderers/GraphicsInputIcon.js' import { UnknownInputIcon } from './Renderers/UnknownInputIcon.js' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { findPieceInstanceToShow, findPieceInstanceToShowFromInstances } from './utils.js' import LocalInputIcon from './Renderers/LocalInputIcon.js' -import { +import type { PartInstanceId, RundownId, RundownPlaylistActivationId, ShowStyleBaseId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ReadonlyDeep } from 'type-fest' +import type { ReadonlyDeep } from 'type-fest' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' export interface IPropsHeader { diff --git a/packages/webui/src/client/ui/PieceIcons/PieceName.tsx b/packages/webui/src/client/ui/PieceIcons/PieceName.tsx index cffa42665db..578af0fd7e5 100644 --- a/packages/webui/src/client/ui/PieceIcons/PieceName.tsx +++ b/packages/webui/src/client/ui/PieceIcons/PieceName.tsx @@ -1,12 +1,12 @@ import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' -import { EvsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' +import { type EvsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { IPropsHeader } from './PieceIcon.js' +import type { IPropsHeader } from './PieceIcon.js' import { findPieceInstanceToShow } from './utils.js' -import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ReadonlyDeep } from 'type-fest' +import type { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' +import type { RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import type { ReadonlyDeep } from 'type-fest' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' interface INamePropsHeader extends IPropsHeader { diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx index 81e5ba2c72e..2aeb846194e 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx @@ -1,5 +1,3 @@ -import React from 'react' - export function BaseRemoteInputIcon(props: Readonly>): JSX.Element { return ( diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/SplitInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/SplitInputIcon.tsx index e7a318e3294..ab95636f8eb 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/SplitInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/SplitInputIcon.tsx @@ -1,8 +1,8 @@ import * as React from 'react' -import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { SplitsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' +import type { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { type SplitsContent, SourceLayerType } from '@sofie-automation/blueprints-integration' import classNames from 'classnames' -import { ReadonlyDeep } from 'type-fest' +import type { ReadonlyDeep } from 'type-fest' import { RundownUtils } from '../../../lib/rundown.js' type SplitIconPieceType = ReadonlyDeep> diff --git a/packages/webui/src/client/ui/PieceIcons/utils.ts b/packages/webui/src/client/ui/PieceIcons/utils.ts index 8a10abda93a..9d0c7f33008 100644 --- a/packages/webui/src/client/ui/PieceIcons/utils.ts +++ b/packages/webui/src/client/ui/PieceIcons/utils.ts @@ -1,11 +1,11 @@ -import { SourceLayerType, ISourceLayer } from '@sofie-automation/blueprints-integration' -import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { IPropsHeader } from './PieceIcon.js' +import type { SourceLayerType, ISourceLayer } from '@sofie-automation/blueprints-integration' +import type { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import type { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { IPropsHeader } from './PieceIcon.js' import { UIShowStyleBases } from '../Collections.js' import { PieceInstances } from '../../collections/index.js' -import { ReadonlyDeep } from 'type-fest' -import { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js' +import type { ReadonlyDeep } from 'type-fest' +import type { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js' export interface IFoundPieceInstance { sourceLayer: ISourceLayer | undefined diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss index 99c409ad219..293e11a19d2 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss @@ -1,4 +1,5 @@ @use '../../styles/itemTypeColors'; +@import '../../styles/checkerboard'; .preview-popUp { border: 1px solid var(--sofie-segment-layer-hover-popup-border); @@ -380,7 +381,7 @@ position: relative; overflow: hidden; - background-color: #000; + @include checkerboard-background; > .background { position: absolute; @@ -388,11 +389,34 @@ bottom: 0; left: 0; right: 0; + overflow: hidden; + + > img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + } } - > .box { + > .box, + > .video-preview { position: absolute; transform: translate(-50%, -50%); + overflow: hidden; + + > .video-preview__image, + .video-preview__image { + position: absolute; + inset: 0; + display: block; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + } } & > * { @@ -405,6 +429,7 @@ left: 0; right: 0; bottom: 0; + z-index: 2; font-weight: 400; text-shadow: 0 0 2px rgba(0, 0, 0, 0.75); text-align: center; diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx index 328a11ac83e..aae98d36342 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx @@ -1,10 +1,14 @@ import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import classNames from 'classnames' import { usePopper } from 'react-popper' -import { Padding, Placement, VirtualElement } from '@popperjs/core' +import type { Padding, Placement, VirtualElement } from '@popperjs/core' import './PreviewPopUp.scss' +function isDetachedHTMLElementAnchor(anchor: HTMLElement | VirtualElement | null): anchor is HTMLElement { + return anchor instanceof HTMLElement && !anchor.isConnected +} + export const PreviewPopUp = React.forwardRef< PreviewPopUpHandle, React.PropsWithChildren<{ @@ -21,11 +25,20 @@ export const PreviewPopUp = React.forwardRef< ref ): React.JSX.Element { const [popperEl, setPopperEl] = useState(null) + const popperWidthPx = size === 'large' ? 482 : 322 + const popperOptions = useMemo( () => ({ placement: placement ?? 'top', strategy: 'fixed' as const, modifiers: [ + { + name: 'computeStyles', + options: { + // Do not shrink the popup to the (zero-width) virtual mouse anchor when trackMouse is on. + adaptive: false, + }, + }, { name: 'flip', options: { @@ -56,32 +69,70 @@ export const PreviewPopUp = React.forwardRef< }), [padding] ) + const initialVirtualX = + (trackMouse && typeof initialOffsetX === 'number') + ? initialOffsetX + : anchor?.getBoundingClientRect().x ?? 0 + const virtualPositionRef = useRef({ + x: initialVirtualX, + y: anchor?.getBoundingClientRect().y ?? 0, + }) const virtualElement = useRef({ - getBoundingClientRect: generateGetBoundingClientRect( - initialOffsetX ?? anchor?.getBoundingClientRect().x ?? 0, - anchor?.getBoundingClientRect().y ?? 0 - ), + getBoundingClientRect: () => + generateVirtualBoundingClientRect(virtualPositionRef.current.x, virtualPositionRef.current.y), }) + const anchorRef = useRef(anchor) + const anchorYRef = useRef(anchor?.getBoundingClientRect().y ?? 0) const { styles, attributes, update } = usePopper( trackMouse ? virtualElement.current : anchor, popperEl, popperOptions ) + const popperStyle = useMemo( + () => ({ + ...styles.popper, + width: popperWidthPx, + }), + [styles.popper, popperWidthPx] + ) + const updateRef = useRef(update) useEffect(() => { updateRef.current = update }, [update]) + // Re-sync virtual anchor when a new preview session starts (trackMouse + cursor X). + useEffect(() => { + if (!trackMouse) return + const anchorX = anchor?.getBoundingClientRect().x ?? 0 + const anchorY = anchor?.getBoundingClientRect().y ?? 0 + anchorYRef.current = anchorY + virtualPositionRef.current = { + x: typeof initialOffsetX === 'number' ? initialOffsetX : anchorX, + y: anchorY, + } + updateRef.current?.().catch(console.error) + }, [trackMouse, initialOffsetX, anchor]) + + useEffect(() => { + anchorRef.current = anchor + const anchorX = anchor?.getBoundingClientRect().x ?? 0 + const anchorY = anchor?.getBoundingClientRect().y ?? 0 + anchorYRef.current = anchorY + virtualPositionRef.current = { + x: trackMouse && typeof initialOffsetX === 'number' ? initialOffsetX : anchorX, + y: anchorY, + } + }, [anchor, initialOffsetX, trackMouse]) + useEffect(() => { if (trackMouse) { const listener = ({ clientX: x }: MouseEvent) => { - virtualElement.current.getBoundingClientRect = generateGetBoundingClientRect( - x, - anchor?.getBoundingClientRect().y ?? 0 - ) - // If update is available, call it to reposition the popper: + if (isDetachedHTMLElementAnchor(anchorRef.current)) return + anchorYRef.current = anchorRef.current?.getBoundingClientRect().y ?? anchorYRef.current + virtualPositionRef.current = { x, y: anchorYRef.current } if (updateRef.current) { updateRef.current().catch((e) => console.error(e)) } @@ -92,11 +143,21 @@ export const PreviewPopUp = React.forwardRef< document.removeEventListener('mousemove', listener) } } - }, [trackMouse, anchor]) + }, [trackMouse]) + + useEffect(() => { + return () => { + anchorRef.current = null + anchorYRef.current = 0 + virtualPositionRef.current = { x: 0, y: 0 } + updateRef.current = null + } + }, []) useImperativeHandle(ref, () => { return { update: () => { + if (isDetachedHTMLElementAnchor(anchorRef.current)) return if (!updateRef.current) return updateRef.current().catch(console.error) }, @@ -110,7 +171,7 @@ export const PreviewPopUp = React.forwardRef< 'preview-popUp--large': size === 'large', 'preview-popUp--small': size === 'small', })} - style={styles.popper} + style={popperStyle} {...attributes.popper} > {children &&
      {children}
      } @@ -122,8 +183,8 @@ export type PreviewPopUpHandle = { readonly update: () => void } -function generateGetBoundingClientRect(x = 0, y = 0) { - return () => ({ +function generateVirtualBoundingClientRect(x = 0, y = 0) { + return { width: 0, height: 0, x: x, @@ -133,5 +194,5 @@ function generateGetBoundingClientRect(x = 0, y = 0) { bottom: y, left: x, toJSON: () => '', - }) + } } diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContent.tsx b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContent.tsx index 5c1c3a35d04..d6481a6dfa3 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContent.tsx +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContent.tsx @@ -1,17 +1,17 @@ -import React from 'react' import { WarningIconSmall } from '../../lib/ui/icons/notifications.js' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { TFunction, useTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' +import type { TFunction } from 'i18next' import { VTPreviewElement } from './Previews/VTPreview.js' import { IFramePreview } from './Previews/IFramePreview.js' import { BoxLayoutPreview } from './Previews/BoxLayoutPreview.js' import { ScriptPreview } from './Previews/ScriptPreview.js' import { RundownUtils } from '../../lib/rundown.js' -import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' +import type { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' import { PieceLifespan } from '@sofie-automation/blueprints-integration' import { LayerInfoPreview } from './Previews/LayerInfoPreview.js' -import { PreviewContentUI } from './PreviewPopUpContext.js' +import type { PreviewContentUI } from './PreviewPopUpContext.js' interface PreviewPopUpContentProps { content: PreviewContentUI @@ -64,7 +64,7 @@ export function PreviewPopUpContent({ content, time }: PreviewPopUpContentProps) case 'separationLine': return
      case 'boxLayout': - return + return case 'warning': return (
      diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx index 0e43ca735e0..9815e09190b 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx @@ -1,28 +1,35 @@ -import React, { useRef, useState } from 'react' -import { PreviewPopUp, PreviewPopUpHandle } from './PreviewPopUp.js' -import { Padding, Placement } from '@popperjs/core' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { PreviewPopUp, type PreviewPopUpHandle } from './PreviewPopUp.js' +import Escape from '../../lib/Escape.js' +import type { Padding, Placement } from '@popperjs/core' import { PreviewPopUpContent } from './PreviewPopUpContent.js' import { JSONBlobParse, - NoraPayload, - PieceLifespan, - PreviewContent, + type NoraPayload, + type PieceLifespan, + type PreviewContent, PreviewType, - ScriptContent, + type ScriptContent, SourceLayerType, - SplitsContent, - SplitsContentBoxContent, - SplitsContentBoxProperties, - TransitionContent, - VTContent, + type SplitsContent, + type SplitsContentBoxContent, + type SplitsContentBoxProperties, + type TransitionContent, + type VTContent, } from '@sofie-automation/blueprints-integration' -import { ReadonlyDeep, ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' -import { PieceContentStatusObj } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' -import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' +import type { ReadonlyDeep, ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' +import type { PieceContentStatusObj } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' +import type { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import _ from 'underscore' -import { IAdLibListItem } from '../Shelf/AdLibListItem.js' -import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import type { IAdLibListItem } from '../Shelf/AdLibListItem.js' +import type { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { createPrivateApiPath } from '../../url.js' +import { + getPieceScrubDurationMs, + getSplitsBoxLayoutScrubSettings, + type PreviewVideoContentUI, + type SplitsBoxLayoutScrubSettings, +} from '../../lib/ui/splitsPreviewVideo.js' type VirtualElement = { getBoundingClientRect: () => DOMRect @@ -50,7 +57,7 @@ export function convertSourceLayerItemToPreview( case PreviewType.BlueprintImage: contents.push({ type: 'image', - src: createPrivateApiPath('/blueprints/assets/' + popupPreview.preview.image), + src: createPrivateApiPath('blueprints/assets/' + popupPreview.preview.image), }) break case PreviewType.HTML: @@ -78,13 +85,20 @@ export function convertSourceLayerItemToPreview( lastModified: popupPreview.preview.lastModified, }) break - case PreviewType.Split: + case PreviewType.Split: { + const splitScrub = getSplitsBoxLayoutScrubSettings( + { boxSourceConfiguration: popupPreview.preview.boxes }, + contentStatus + ) contents.push({ type: 'boxLayout', boxSourceConfiguration: popupPreview.preview.boxes, - backgroundArtSrc: createPrivateApiPath('/blueprints/assets/' + popupPreview.preview.background), + boxPreviews: contentStatus?.boxPreviews, + scrub: splitScrub, + backgroundArtSrc: createPrivateApiPath('blueprints/assets/' + popupPreview.preview.background), }) break + } case PreviewType.Table: contents.push({ type: 'data', @@ -159,6 +173,9 @@ export function convertSourceLayerItemToPreview( ? { type: 'video', src: contentStatus.previewUrl, + itemDuration: getPieceScrubDurationMs(content, contentStatus), + seek: content.seek, + loop: content.loop, } : contentStatus?.thumbnailUrl ? { @@ -283,12 +300,23 @@ export function convertSourceLayerItemToPreview( } } else if (sourceLayerType === SourceLayerType.SPLITS) { const content = item.content as SplitsContent - return { contents: [{ type: 'boxLayout', boxSourceConfiguration: content.boxSourceConfiguration }], options: {} } + const splitScrub = getSplitsBoxLayoutScrubSettings(content, contentStatus) + return { + contents: [ + { + type: 'boxLayout', + boxSourceConfiguration: content.boxSourceConfiguration, + boxPreviews: contentStatus?.boxPreviews, + scrub: splitScrub, + }, + ], + options: {}, + } } else if (sourceLayerType === SourceLayerType.TRANSITION) { const content = item.content as TransitionContent if (content.preview) return { - contents: [{ type: 'image', src: createPrivateApiPath('/blueprints/assets/' + content.preview) }], + contents: [{ type: 'image', src: createPrivateApiPath('blueprints/assets/' + content.preview) }], options: {}, } } @@ -298,11 +326,16 @@ export function convertSourceLayerItemToPreview( /* PreviewContentUI is an extension of PreviewContent with some additional types used in the UI * These additional types are added to support some extra UI features that are not relevant for blueprints */ +export type { PreviewVideoContentUI } from '../../lib/ui/splitsPreviewVideo.js' + export type PreviewContentUI = - | PreviewContent + | Exclude + | PreviewVideoContentUI | { type: 'boxLayout' boxSourceConfiguration: ReadonlyDeep<(SplitsContentBoxContent & SplitsContentBoxProperties)[]> + boxPreviews?: ReadonlyDeep + scrub?: SplitsBoxLayoutScrubSettings showLabels?: boolean backgroundArtSrc?: string } @@ -378,6 +411,7 @@ export const PreviewPopUpContext = React.createContext({ }) interface PreviewSession { + token: number anchor: HTMLElement | VirtualElement padding: Padding placement: Placement @@ -389,19 +423,93 @@ interface PreviewSession { export function PreviewPopUpContextProvider({ children }: React.PropsWithChildren<{}>): React.ReactNode { const currentHandle = useRef() const previewRef = useRef(null) + const closeSessionRef = useRef<() => void>(() => undefined) + const sessionTokenRef = useRef(0) const [previewSession, setPreviewSession] = useState(null) const [previewContent, setPreviewContent] = useState(null) const [t, setTime] = useState(null) + const [previewSessionKey, setPreviewSessionKey] = useState(0) + + const isDetachedHTMLElement = (anchor: HTMLElement | VirtualElement): boolean => { + return anchor instanceof HTMLElement && !anchor.isConnected + } + + const closeSession = useCallback(() => { + sessionTokenRef.current += 1 + + const previousHandle = currentHandle.current + if (previousHandle) { + currentHandle.current = undefined + previousHandle.onClosed?.() + } + + setPreviewSession(null) + setPreviewContent(null) + setTime(null) + }, []) + + useEffect(() => { + closeSessionRef.current = closeSession + }, [closeSession]) + + useEffect(() => { + return () => { + closeSession() + } + }, [closeSession]) + + useEffect(() => { + if (!previewSession) return + const token = previewSession.token + const anchor = previewSession.anchor + if (!(anchor instanceof HTMLElement)) return + + let rafHandle: number | undefined + let disposed = false + const checkAnchorConnection = () => { + if (disposed) return + if (sessionTokenRef.current !== token) return + if (!anchor.isConnected) { + closeSessionRef.current() + return + } + rafHandle = window.requestAnimationFrame(checkAnchorConnection) + } + + rafHandle = window.requestAnimationFrame(checkAnchorConnection) + + return () => { + disposed = true + if (rafHandle !== undefined) { + window.cancelAnimationFrame(rafHandle) + } + } + }, [previewSession]) const context: IPreviewPopUpContext = { requestPreview: (anchor, content, opts) => { - if (opts?.time) { + if (isDetachedHTMLElement(anchor)) { + closeSession() + const closedHandle: IPreviewPopUpSession = { + close: () => undefined, + update: () => undefined, + setPointerTime: () => undefined, + } + return closedHandle + } + + closeSession() + setPreviewSessionKey((prev) => prev + 1) + const token = ++sessionTokenRef.current + + if (typeof opts?.time === 'number') { setTime(opts.time) } else { setTime(null) } setPreviewSession({ + token, anchor, padding: opts?.padding ?? 0, placement: opts?.placement ?? 'top', @@ -413,15 +521,26 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre const handle: IPreviewPopUpSession = { close: () => { - setPreviewSession(null) + if (currentHandle.current !== handle) return + closeSession() }, update: (contents) => { + if (currentHandle.current !== handle) return + if (isDetachedHTMLElement(anchor)) { + closeSession() + return + } if (contents) { setPreviewContent(contents) } previewRef.current?.update() }, setPointerTime: (t) => { + if (currentHandle.current !== handle) return + if (isDetachedHTMLElement(anchor)) { + closeSession() + return + } setTime(t) }, } @@ -435,18 +554,21 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre {children} {previewSession && ( - - {previewContent && - previewContent.map((content, i) => )} - + + + {previewContent && + previewContent.map((content, i) => )} + + )} ) diff --git a/packages/webui/src/client/ui/PreviewPopUp/Previews/BoxLayoutPreview.tsx b/packages/webui/src/client/ui/PreviewPopUp/Previews/BoxLayoutPreview.tsx index c009841610a..a1c1ad23f00 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/Previews/BoxLayoutPreview.tsx +++ b/packages/webui/src/client/ui/PreviewPopUp/Previews/BoxLayoutPreview.tsx @@ -1,27 +1,74 @@ -import { SplitsContentBoxContent, SplitsContentBoxProperties } from '@sofie-automation/blueprints-integration' +import type { SplitsContentBoxContent, SplitsContentBoxProperties } from '@sofie-automation/blueprints-integration' +import type { SplitBoxPreviewUrls } from '@sofie-automation/corelib/dist/dataModel/PieceContentStatus' +import type { SplitsBoxLayoutScrubSettings } from '../../../lib/ui/splitsPreviewVideo.js' +import { setVideoElementPosition } from '../../../lib/ui/videoPreviewScrub.js' import classNames from 'classnames' -import { useMemo } from 'react' -import { getSplitPreview, SplitRole } from '../../../lib/ui/splitPreview.js' -import { ReadonlyDeep } from 'type-fest' +import { useEffect, useMemo, useRef } from 'react' +import { getSplitPreview, SplitRole, type SplitSubItem } from '../../../lib/ui/splitPreview.js' +import type { ReadonlyDeep } from 'type-fest' import { RundownUtils } from '../../../lib/rundown.js' interface BoxLayoutPreviewProps { content: { type: 'boxLayout' boxSourceConfiguration: ReadonlyDeep<(SplitsContentBoxContent & SplitsContentBoxProperties)[]> + boxPreviews?: ReadonlyDeep + scrub?: SplitsBoxLayoutScrubSettings showLabels?: boolean backgroundArtSrc?: string } + time: number | null } -export function BoxLayoutPreview({ content }: BoxLayoutPreviewProps): React.ReactElement { +function SplitBoxMedia({ + item, + time, + scrub, +}: { + item: Readonly + time: number | null + scrub: SplitsBoxLayoutScrubSettings | undefined +}): React.ReactElement | null { + const videoRef = useRef(null) + + useEffect(() => { + const el = videoRef.current + if (!el || !item.previewUrl) return + + setVideoElementPosition(el, time ?? 0, scrub?.itemDuration ?? 0, item.seek ?? 0, scrub?.loop ?? false) + }, [time, scrub?.itemDuration, scrub?.loop, item.previewUrl, item.seek]) + + if (item.previewUrl) { + return ( +