app/vidispine/item/VidispineItem.ts (223 lines of code) (raw):

import VidispineItemTI from "./VidispineItem-ti"; import VidispineShapeTI from "../shape/VidispineShape-ti"; import CustomDataTI from "../field-group/CustomData-ti"; import VidispineFileTI from "../shape/VidispineFile-ti"; import { createCheckers } from "ts-interface-checker"; import { VidispineShapeIF, VidispineShape, VidispineShapeChecker, } from "../shape/VidispineShape"; interface URIList { uri: string[]; } interface MetadataValue { value: string; uuid?: string; user?: string; timestamp?: string; change?: string; } interface MetadataField { name: string; value: MetadataValue[]; uuid?: string; user?: string; timestamp?: string; change?: string; } interface MetadataGroup { name: string; field: MetadataField[]; } interface MetadataTimespan { field: MetadataField[]; group: MetadataGroup[]; start: string; end: string; } interface ItemMetadata { revision: string; timespan: MetadataTimespan[]; } interface ItemIF { metadata?: ItemMetadata; shape?: VidispineShapeIF[]; files?: URIList; id: string; } interface ItemResponse { item: ItemIF[]; } const { MetadataValue, MetadataField, MetadataGroup, MetadataTimespan, ItemMetadata, ItemIF, ItemResponse, } = createCheckers( VidispineItemTI, VidispineShapeTI, CustomDataTI, VidispineFileTI ); /** * Main vidispine item class. This defines a group of useful methods for retrieving metadata values from the overall * document structure */ class VidispineItem implements ItemIF { metadata?: ItemMetadata; shape?: VidispineShape[]; id: string; files?: URIList; /** * constructs the class from a raw object * @param sourceObject, if this does not validate then VError is thrown */ constructor(sourceObject: any) { let bareObject = { id: sourceObject.hasOwnProperty("id") ? sourceObject.id : undefined, }; ItemIF.check(bareObject); this.metadata = sourceObject.metadata; this.shape = sourceObject.hasOwnProperty("shape") ? sourceObject.shape .map((s: VidispineShapeIF) => { try { return new VidispineShape(s, true); } catch (e) { const loggedId = s.id ?? "unknown-id"; console.warn(`Shape ${loggedId} did not validate: `, e); return null; } }) .filter((maybeShape: VidispineShapeIF | null) => maybeShape != null) : undefined; this.files = sourceObject.files; this.id = sourceObject.id; } /** * perform a validation as a Shape object for each item in the given list. * only validated ones are returned * @param sourceShapes */ validateSourceShapes(sourceShapes: any[]): VidispineShapeIF[] { return <VidispineShapeIF[]>sourceShapes .map((maybeShape) => { try { VidispineShapeChecker.check(maybeShape); return <VidispineShapeIF>maybeShape; } catch (e) { const presentableId = maybeShape.hasOwnProperty("id") ? maybeShape.id : "unknown-id"; console.warn(`Source shape ${presentableId} did not validate: `, e); return null; } }) .filter((maybeShape) => maybeShape !== null); } /** * convenience method that calls getMetadataValuesInGroup with no group specified, i.e. finds the metadata values * for a field in the root group * @param forKey field name that you want to get metadata for * @return same as for getMetadataValuesInGroup */ getMetadataValues(forKey: string): Array<string> | undefined { return this.getMetadataValuesInGroup(forKey, undefined); } /** * convenience method that calls getMetadataInGroup and strips out all excess information, simply returning * the values as an array of strings. * @param forKey the field name that you want to get metadata for * @param inGroup inGroup optionally, the name of a group to locate the field in * @return either an array of metadata values for the field (which can be empty) or undefined if the field or group * do not exist within the metadata response */ getMetadataValuesInGroup( forKey: string, inGroup: string | undefined ): string[] | undefined { const metavalues = this.getMetadataInGroup(forKey, inGroup); return metavalues ? metavalues.map((entry) => entry.value) : undefined; } /** * convenience method that will coalesce all values into a string joined by a comma separator * @param forKey */ getMetadataString(forKey: string): string | undefined { const possibleValues = this.getMetadataValuesInGroup(forKey, undefined); if (possibleValues) { return possibleValues.join(", "); } else { return undefined; } } /** * searches the default timespan for metadata entries metching the given key, optionally within the given group. * @param forKey the field name that you want to get metadata for * @param inGroup optionally, the name of a group to locate the field in * @return either an array of matching MetadataValue entries (which can be an empty array if the field exists with * no values) or undefined if the field or key do not exist within the metadata response */ getMetadataInGroup( forKey: string, inGroup: string | undefined ): MetadataValue[] | undefined { const timespan = this.getDefaultTimespan(); if (!timespan) return undefined; let fieldset: MetadataField[]; if (inGroup) { const groupref = this.getGroup(inGroup, timespan); if (!groupref) return undefined; fieldset = groupref.field; } else { fieldset = timespan.field; } const values = fieldset .filter((f) => f.name === forKey) .map((f) => f.value); if (values.length === 0) return undefined; return values.reduce((acc, elem) => acc.concat(...elem)); } /** * gets the group with the given name in the given timespan. * if multiple groups match, the first is returned and an error emitted to console * @param groupName name of the group to search for * @param timespan timespan to search in */ getGroup( groupName: string, timespan: MetadataTimespan ): MetadataGroup | undefined { const potentialGroups = timespan.group.filter((g) => g.name === groupName); if (potentialGroups.length == 0) { return undefined; } else if (potentialGroups.length > 1) { console.warn( `VidispineItem::getGroup - ${potentialGroups.length} groups were found for ${groupName}` ); } return potentialGroups[0]; } /** * returns a simple boolean indicating whether the item has a given group on it * @param groupName group to search for * @param timespan optional timspan to search in, if not set the default timespan is used */ hasGroup(groupName: string, timespan?: MetadataTimespan): boolean { const timespanForLookup = timespan ?? this.getDefaultTimespan(); if (timespanForLookup && timespanForLookup.group) { return ( timespanForLookup.group.filter((group) => group.name === groupName) .length > 0 ); } else { return false; } } /** * returns the "default" timespan: * - if there is only one timespan, that one * - if there are multiple timespans, the one that has a start and end at "-INF" and "+INF" respectively * - if there are multiple timespans running from "-INF" to "+INF" then a RangeError is thrown */ getDefaultTimespan(): MetadataTimespan | undefined { if (this.metadata) { if (this.metadata.timespan.length === 0) { return undefined; } else if (this.metadata.timespan.length === 1) { return this.metadata.timespan[0]; } else { const potentialTimespans = this.metadata.timespan.filter( (timespan) => timespan.start === "-INF" && timespan.end === "+INF" ); if (potentialTimespans.length === 0) { return undefined; } else if (potentialTimespans.length === 1) { return potentialTimespans[0]; } else { const err = new RangeError(); err.message = "Multiple default timespans existed? This is incorrect."; throw err; } } } else { return undefined; } } /** * returns an array of strings representing the names of the groups mentioned in the document. * This is an empty list if either the timespan does not exist or there are no group entries. */ getGroupNames(): string[] { const ts = this.getDefaultTimespan(); if (ts == undefined) return []; return ts.group.map((g) => g.name); } /** * finds an attached shape matching the given shape tag * @param shapeTagName shape tag name to search for * @returns undefined if there is no shape data in this VidispineItem. An array of VidispineShape otherwise, which can * be empty if there are no matching shapes present. */ findShape(shapeTagName: string): VidispineShape[] | undefined { if (this.shape) { return this.shape.filter((shape) => shape.tag.includes(shapeTagName)); } else { return undefined; } } } /** * takes an untyped object from JSON.parse, validates it and returns an array of VidispineItems. * if it fails to validate as an ItemResponse, then an exception is thrown describing the syntax issue * @param content decoded json object * @returns an erray of VidispineItems (which might be empty) or throws an error */ function GetItems(content: object): Array<VidispineItem> { const itemresponse = <ItemResponse>content; return itemresponse.item.map((rawitem) => new VidispineItem(rawitem)); } export type { ItemResponse, ItemIF, MetadataValue, MetadataField, MetadataGroup, MetadataTimespan, ItemMetadata, }; export { GetItems, VidispineItem };