v-connection
Version:
Sofie TV Automation Vizrt Media Sequencer Engine connection library
605 lines • 29.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Rundown = void 0;
const msehttp_1 = require("./msehttp");
const peptalk_1 = require("./peptalk");
const util_1 = require("./util");
const xml_1 = require("./xml");
const ALTERNATIVE_CONCEPT = 'alternative_concept';
class Rundown {
constructor(mseRep, profile, playlist, description) {
this.elementMap = {};
this.initialElementMapPromise = null;
this.buildingElementMap = false;
this.mse = mseRep;
this.profile = profile.startsWith('/config/profiles/') ? profile.slice(17) : profile;
this.playlist = playlist;
if (this.playlist.startsWith('{')) {
this.playlist = this.playlist.slice(1);
}
if (this.playlist.endsWith('}')) {
this.playlist = this.playlist.slice(0, -1);
}
this.description = description;
this.msehttp = (0, msehttp_1.createHTTPContext)(this.profile, this.mse.resthost ? this.mse.resthost : this.mse.hostname, this.mse.restPort);
// Initialize element map when the rundown is created
// This will run asynchronously in the background
this.initialElementMapPromise = this.buildElementMap()
.then((result) => {
console.log('Initial element map built with size:', Object.keys(this.elementMap).length);
return result;
})
.catch((err) => {
console.error('Failed to build initial element map:', (0, peptalk_1.getPepErrorMessage)(err));
return false;
});
}
get pep() {
return this.mse.getPep();
}
static makeKey(elementId) {
return `${elementId.vcpid}_${elementId.channel ?? ''}`;
}
static makeKeySet(elementIds) {
return new Set(elementIds.map((e) => {
return Rundown.makeKey(e);
}));
}
async buildElementMap(elementId) {
console.log('buildElementMap called', elementId);
// If we're looking for a specific element and it's already in the map, return immediately
if (elementId && (0, util_1.has)(this.elementMap, Rundown.makeKey(elementId))) {
console.log('Element already in map, returning immediately');
return true;
}
// If we're already building the map, wait for that to complete
if (this.buildingElementMap) {
console.log('Element map build already in progress, waiting...');
// Wait a bit and check if the element is in the map
await new Promise((resolve) => setTimeout(resolve, 500));
if (elementId && (0, util_1.has)(this.elementMap, Rundown.makeKey(elementId))) {
console.log('Element appeared in map while waiting');
return true;
}
// Wait a bit longer for the build to complete
await new Promise((resolve) => setTimeout(resolve, 1500));
if (elementId && (0, util_1.has)(this.elementMap, Rundown.makeKey(elementId))) {
console.log('Element appeared in map after waiting longer');
return true;
}
}
// Set the building flag
this.buildingElementMap = true;
try {
await this.mse.checkConnection();
// Only handle external elements
if (!elementId) {
// If we're looking for a specific element, just process that one
// Otherwise, try to get all external elements with retries if needed
let externalElements;
if (elementId) {
externalElements = [elementId];
}
else {
// Add a small delay before the first attempt to ensure the system is ready
console.log('Waiting before fetching external elements...');
await new Promise((resolve) => setTimeout(resolve, 1000));
// Try up to 3 times to get a complete list of external elements
let attempts = 0;
const maxAttempts = 3;
externalElements = [];
while (attempts < maxAttempts) {
attempts++;
externalElements = await this.listExternalElements();
// If we got more than one element, break
if (externalElements.length > 1) {
break;
}
// If we've reached max attempts, break
if (attempts >= maxAttempts) {
break;
}
// Wait a bit before retrying, with increasing delay
console.log('Got only one element, retrying after delay...');
await new Promise((resolve) => setTimeout(resolve, 1000 * attempts));
}
}
// If we didn't get any elements and we're not looking for a specific one,
// there might be an issue with the connection or data
if (externalElements.length === 0 && !elementId) {
console.log('Warning: No external elements found');
}
// Get the playlist elements to extract refPath
const playlistElementsList = await this.pep.getJS(`/storage/playlists/${(0, util_1.wrapInBracesIfNeeded)(this.playlist)}/elements`, 4);
const flatPlaylistElements = await (0, xml_1.flattenEntry)(playlistElementsList.js);
// Helper function to find refPath for an element
const findRefPath = (obj, vcpid) => {
// Base case: not an object or null
if (!obj || typeof obj !== 'object') {
return undefined;
}
// Check if this is a direct reference with the matching vcpid
if (obj.value && typeof obj.value === 'string' && obj.value.includes(`/elements/${vcpid}`)) {
return obj.value;
}
// Check if this is a reference with the matching path in _
if (obj._ && typeof obj._ === 'string' && obj._.includes(`/elements/${vcpid}`)) {
return obj._;
}
// Recursively search all properties
for (const key in obj) {
// Skip certain properties that we know aren't relevant
if (key === 'name' || key === 'id' || key === 'status')
continue;
// Handle arrays
if (Array.isArray(obj[key])) {
for (const item of obj[key]) {
const result = findRefPath(item, vcpid);
if (result)
return result;
}
}
// Handle nested objects
else if (typeof obj[key] === 'object') {
const result = findRefPath(obj[key], vcpid);
if (result)
return result;
}
}
return undefined;
};
// Process each external element and add it to the elementMap
for (const e of externalElements) {
if (typeof e !== 'string') {
try {
const refPath = findRefPath(flatPlaylistElements, e.vcpid);
// Try to get the element details, but don't fail if we can't
let element;
try {
element = await this.getElement(e);
}
catch (err) {
console.log(`Could not get element details for ${e.vcpid}: ${(0, peptalk_1.getPepErrorMessage)(err)}`);
}
// Always use 'ref' as the reference name
this.elementMap[Rundown.makeKey(e)] = {
vcpid: e.vcpid,
channel: element?.channel || e.channel,
refName: 'ref',
refPath,
};
}
catch (err) {
// If we can't get the element, create a default entry
const refPath = findRefPath(flatPlaylistElements, e.vcpid);
this.elementMap[Rundown.makeKey(e)] = {
vcpid: e.vcpid,
channel: e.channel,
refName: 'ref',
refPath,
};
console.log(`Created default entry for ${e.vcpid}: ${(0, peptalk_1.getPepErrorMessage)(err)}`);
}
}
}
}
console.log('elementMap size:', Object.keys(this.elementMap).length);
if (Object.keys(this.elementMap).length > 0) {
console.log('elementMap sample:', Object.keys(this.elementMap)
.slice(0, 5)
.map((key) => {
const info = this.elementMap[key];
return 'vcpid' in info ? `${info.vcpid}:${info.refPath?.slice(-20) || 'no-path'}` : key;
}));
}
return elementId ? (0, util_1.has)(this.elementMap, Rundown.makeKey(elementId)) : true;
}
finally {
// Always reset the building flag when done
this.buildingElementMap = false;
}
}
async listTemplates(showId) {
await this.mse.checkConnection();
const templateList = await this.pep.getJS(`/storage/shows/{${showId}}/mastertemplates`, 1);
const flatTemplates = await (0, xml_1.flattenEntry)(templateList.js);
return Object.keys(flatTemplates).filter((x) => x !== 'name');
}
async getTemplate(templateName, showId) {
await this.mse.checkConnection();
const template = await this.pep.getJS(`/storage/shows/${(0, util_1.wrapInBracesIfNeeded)(showId)}/mastertemplates/${templateName}`);
let flatTemplate = await (0, xml_1.flattenEntry)(template.js);
if (Object.keys(flatTemplate).length === 1) {
flatTemplate = flatTemplate[Object.keys(flatTemplate)[0]];
}
return flatTemplate;
}
async listExternalElements() {
await this.mse.checkConnection();
// First, ensure we have a fresh connection
//console.log('Fetching external elements from playlist:', this.playlist)
const playlistElementsList = await this.pep.getJS(`/storage/playlists/${(0, util_1.wrapInBracesIfNeeded)(this.playlist)}/elements`, 4);
const flatPlaylistElements = await (0, xml_1.flattenEntry)(playlistElementsList.js);
const elementsRefs = [];
// Helper function to process entries recursively
const processEntry = (entry, depth = 0) => {
// Base case: not an object or null
if (!entry || typeof entry !== 'object') {
return;
}
// Handle direct references
if (entry.key === 'ref' && entry.value) {
const ref = entry.value;
if (ref.includes('/elements/')) {
const lastSlash = ref.lastIndexOf('/');
elementsRefs.push({
vcpid: +ref.slice(lastSlash + 1),
channel: entry.viz_program,
});
}
}
// Handle groups
else if (entry.key === 'group') {
// Process refs directly in this group
if (entry.ref) {
const refs = Array.isArray(entry.ref) ? entry.ref : [entry.ref];
for (const groupRef of refs) {
if (groupRef.value) {
const ref = groupRef.value;
if (ref.includes('/elements/')) {
const lastSlash = ref.lastIndexOf('/');
elementsRefs.push({
vcpid: +ref.slice(lastSlash + 1),
channel: groupRef.viz_program,
});
}
}
else if (groupRef._ && typeof groupRef._ === 'string' && groupRef._.includes('/elements/')) {
const ref = groupRef._;
const lastSlash = ref.lastIndexOf('/');
elementsRefs.push({
vcpid: +ref.slice(lastSlash + 1),
channel: (groupRef.$ && groupRef.$.viz_program),
});
}
}
}
// Process nested entries
for (const key in entry) {
if (typeof entry[key] === 'object' && entry[key] !== null && key !== 'ref') {
processEntry(entry[key], depth + 1);
}
}
}
// Process all other properties recursively
else {
for (const key in entry) {
// Skip certain properties that we know aren't relevant
if (key === 'name' || key === 'id' || key === 'status')
continue;
// Handle arrays
if (Array.isArray(entry[key])) {
for (const item of entry[key]) {
processEntry(item, depth + 1);
}
}
// Handle nested objects
else if (typeof entry[key] === 'object' && entry[key] !== null) {
processEntry(entry[key], depth + 1);
}
// Check if this is a string that contains a reference to an element
else if (typeof entry[key] === 'string' && entry[key].includes('/elements/')) {
const ref = entry[key];
const lastSlash = ref.lastIndexOf('/');
const vcpid = +ref.slice(lastSlash + 1);
if (!isNaN(vcpid) && vcpid > 0) {
elementsRefs.push({
vcpid,
channel: entry.viz_program,
});
}
}
}
}
};
// Process all elements
if (flatPlaylistElements.elements) {
processEntry(flatPlaylistElements.elements);
}
else {
// If no elements property, try to process the whole object
processEntry(flatPlaylistElements);
}
// Remove duplicates by vcpid
const uniqueElements = Array.from(new Map(elementsRefs.map((item) => [item.vcpid, item])).values());
console.log(`Found ${uniqueElements.length} unique external elements`);
return uniqueElements;
}
async listPilotDBExternalElements() {
await this.mse.checkConnection();
const playlistElementsList = await this.pep.getJS(`/storage/playlists/${(0, util_1.wrapInBracesIfNeeded)(this.playlist)}/elements`, 3);
// Parse the groups and their elements
const groups = [];
const groupList = playlistElementsList.js?.elements?.group || [];
for (const group of groupList) {
const groupElements = Array.isArray(group.ref) ? group.ref : [group.ref];
//console.log('groupElements', groupElements)
groups.push({
name: group.$.name,
description: group.$.description,
elements: (await Promise.all(groupElements.map(async (ref) => {
const refPath = ref._;
const lastSlash = refPath.lastIndexOf('/');
const vcpid = +refPath.slice(lastSlash + 1);
if (!vcpid)
return null;
const elementInfo = await this.getExternalPilotDbElement({ vcpid });
return {
name: elementInfo[vcpid]?.description || ref.$.name,
vcpid,
ref: ref._,
};
}))).filter((element) => element !== null && typeof element.vcpid === 'number'),
});
}
return groups;
}
async initializeShow(showId) {
return this.msehttp.initializeShow(showId);
}
async cleanupShow(showId) {
return this.msehttp.cleanupShow(showId);
}
async activate(twice, initPlaylist = true) {
let result = {
// Returned when initShow = false and initPlaylist = false
path: '/',
status: 200,
response: 'No commands to run.',
};
if (twice && initPlaylist) {
result = await this.msehttp.initializePlaylist(this.playlist);
}
if (initPlaylist) {
result = await this.msehttp.initializePlaylist(this.playlist);
}
return result;
}
async deactivate() {
return this.msehttp.cleanupPlaylist(this.playlist);
}
async deleteElement(elementId) {
// Note: For some reason, in contrast to the other commands, the delete command only works with the path being unescaped:
const path = this.getExternalElementPath(elementId, true);
if (await this.buildElementMap(elementId)) {
return this.pep.delete(path);
}
else {
console.log('Not found', path);
throw new peptalk_1.InexistentError(-1, path);
}
}
async cue(elementId) {
const path = this.getExternalElementPath(elementId);
if (await this.buildElementMap(elementId)) {
return this.msehttp.cue(path);
}
else {
throw new msehttp_1.HTTPRequestError(`Cannot cue external element as ID '${elementId.vcpid}' is not known in this rundown.`, this.msehttp.baseURL, path);
}
}
async take(elementId) {
console.log('take', elementId);
try {
await this.buildElementMap(elementId);
const path = this.elementMap[Rundown.makeKey(elementId)]?.refPath || '';
return this.msehttp.take(path);
}
catch (e) {
throw new msehttp_1.HTTPRequestError(`Cannot take external element as ID '${elementId.vcpid}' is not known in this rundown.`, this.msehttp.baseURL, this.elementMap[Rundown.makeKey(elementId)]?.refPath || '');
}
}
async continue(elementId) {
const path = this.getExternalElementPath(elementId);
if (await this.buildElementMap(elementId)) {
return this.msehttp.continue(path);
}
else {
throw new msehttp_1.HTTPRequestError(`Cannot continue external element as ID '${elementId.vcpid}' is not known in this rundown.`, this.msehttp.baseURL, path);
}
}
async continueReverse(elementId) {
const path = this.getExternalElementPath(elementId);
if (await this.buildElementMap(elementId)) {
return this.msehttp.continueReverse(path);
}
else {
throw new msehttp_1.HTTPRequestError(`Cannot continue reverse external element as ID '${elementId.vcpid}' is not known in this rundown.`, this.msehttp.baseURL, path);
}
}
async out(elementId) {
const path = this.getExternalElementPath(elementId);
if (await this.buildElementMap(elementId)) {
return this.msehttp.out(path);
}
else {
throw new msehttp_1.HTTPRequestError(`Cannot take out external element as ID '${elementId.vcpid}' is not known in this rundown.`, this.msehttp.baseURL, path);
}
}
async initialize(elementId) {
const path = this.getExternalElementPath(elementId);
if (await this.buildElementMap(elementId)) {
return this.msehttp.initialize(path);
}
else {
throw new msehttp_1.HTTPRequestError(`Cannot initialize external element as ID '${elementId.vcpid}' is not known in this rundown.`, this.msehttp.baseURL, path);
}
}
async purgeExternalElements(elementsToKeep = []) {
await this.buildElementMap();
const elementsToKeepSet = Rundown.makeKeySet(elementsToKeep);
const deletePromises = Object.keys(this.elementMap)
.filter((key) => {
const info = this.elementMap[key];
return 'vcpid' in info; // Only process external elements
})
.map(async (key) => {
if (elementsToKeepSet.has(key))
return;
try {
const info = this.elementMap[key];
await this.deleteElement({ vcpid: info.vcpid, channel: info.channel });
}
catch (e) {
if (!(e instanceof peptalk_1.InexistentError)) {
throw e;
}
}
});
await Promise.allSettled(deletePromises); // Wait for all Promises
await Promise.all(deletePromises); // throw if there are any rejected Promises
return { id: '*', status: 'ok' };
}
async getElement(elementId) {
await this.mse.checkConnection();
const playlistsList = await this.pep.getJS(`/storage/playlists/${(0, util_1.wrapInBracesIfNeeded)(this.playlist)}/elements`, 4);
const flatPlaylistElements = await (0, xml_1.flattenEntry)(playlistsList.js);
// Helper function to find element recursively
const findElementRecursively = (obj) => {
// Base case: not an object or null
if (!obj || typeof obj !== 'object') {
return null;
}
// Check if this is a direct reference with the matching vcpid
if (obj.value && typeof obj.value === 'string' && obj.value.includes(`/elements/${elementId.vcpid}`)) {
return {
vcpid: elementId.vcpid.toString(),
channel: obj.viz_program,
name: obj.key || 'ref',
refPath: obj.value,
};
}
// Check if this is a reference with the matching path in _
if (obj._ && typeof obj._ === 'string' && obj._.includes(`/elements/${elementId.vcpid}`)) {
return {
vcpid: elementId.vcpid.toString(),
channel: obj.viz_program || (obj.$ && obj.$.viz_program),
name: (obj.$ && obj.$.name) || 'ref',
refPath: obj._,
};
}
// Check if this is a reference with a name matching the vcpid pattern
if (obj.$ && obj.$.name && obj.$.name.startsWith(elementId.vcpid + '_')) {
// Look for the path in this object
if (obj._ && typeof obj._ === 'string') {
return {
vcpid: elementId.vcpid.toString(),
channel: obj.viz_program || (obj.$ && obj.$.viz_program),
name: obj.$.name,
refPath: obj._,
};
}
}
// Recursively search all properties
for (const key in obj) {
// Skip certain properties that we know aren't relevant
if (key === 'name' || key === 'id' || key === 'status')
continue;
// Handle arrays
if (Array.isArray(obj[key])) {
for (const item of obj[key]) {
const result = findElementRecursively(item);
if (result)
return result;
}
}
// Handle nested objects
else if (typeof obj[key] === 'object') {
const result = findElementRecursively(obj[key]);
if (result)
return result;
}
}
return null;
};
// Start the search from the root
const element = findElementRecursively(flatPlaylistElements);
if (!element) {
throw new peptalk_1.InexistentError(typeof playlistsList.id === 'number' ? playlistsList.id : 0, `/storage/playlists/${(0, util_1.wrapInBracesIfNeeded)(this.playlist)}/elements/${elementId.vcpid}`);
}
else {
// Update the element map with the correct reference path
this.elementMap[Rundown.makeKey(elementId)] = {
vcpid: elementId.vcpid,
channel: element.channel,
refName: 'ref',
};
return element;
}
}
async getExternalPilotDbElement(elementId) {
const element = await this.pep.getJS(`/external/pilotdb/elements/${elementId.vcpid}`);
const flatPlaylistElements = await (0, xml_1.flattenEntry)(element.js);
return flatPlaylistElements;
}
async isActive() {
const playlist = await this.mse.getPlaylist(this.playlist);
return playlist.active_profile && typeof playlist.active_profile.value !== 'undefined';
}
getExternalElementPath(elementId, unescaped = false) {
// Ensure the element map is built
this.checkElementMapWasBuilt().catch((err) => {
console.error('Failed to check element map:', (0, peptalk_1.getPepErrorMessage)(err));
});
// Get the element info from the map
const key = Rundown.makeKey(elementId);
const info = this.elementMap[key];
if (!info) {
console.warn(`Element ${key} not found in element map, using default path`);
return `/storage/elements/${elementId.vcpid}`;
}
// Check if this is an external element with a refPath
if ('refPath' in info && info.refPath) {
return unescaped ? info.refPath : encodeURIComponent(info.refPath);
}
else {
return `/storage/elements/${elementId.vcpid}`;
}
}
async setAlternativeConcept(value) {
const environmentPath = `/storage/playlists/${(0, util_1.wrapInBracesIfNeeded)(this.playlist)}/environment`;
const alternativeConceptEntry = `<entry name="${ALTERNATIVE_CONCEPT}">${value}</entry>`;
// Environment entry must exists!
await this.pep.ensurePath(environmentPath);
await this.pep.replace(`${environmentPath}/${ALTERNATIVE_CONCEPT}`, alternativeConceptEntry);
}
async checkElementMapWasBuilt() {
// If we have elements in the map, we're good
if (Object.keys(this.elementMap).length > 0) {
return;
}
// Wait for the initial build if it's in progress
if (this.initialElementMapPromise) {
try {
await this.initialElementMapPromise;
console.log('Used initial element map build');
// If the map is still empty after the initial build, rebuild it
if (Object.keys(this.elementMap).length === 0) {
console.log('Element map is empty after initial build, rebuilding');
await this.buildElementMap();
}
}
catch (err) {
console.warn('Initial element map build failed, rebuilding:', (0, peptalk_1.getPepErrorMessage)(err));
await this.buildElementMap();
}
}
else {
// No initial build, so build the map now
console.log('No initial element map build, building now');
await this.buildElementMap();
}
}
}
exports.Rundown = Rundown;
//# sourceMappingURL=rundown.js.map