Skip to content

Commit

Permalink
feat(types): proper type conversion from JSON result
Browse files Browse the repository at this point in the history
MediaInfoLib returns strings for all track fields. The XSD has a list of
datatypes for each field (string, int, float). So we use them to convert
the fields, so the result object matches the generated typings.
  • Loading branch information
buzz committed Jul 28, 2023
1 parent 9ebae1f commit 9b87930
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 20 deletions.
22 changes: 22 additions & 0 deletions gulp/generate-types/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,25 @@ export function createInterface(name: string, members: readonly ts.TypeElement[]
members
)
}

export function createArrayAsConst(name: string, elements: string[]) {
return ts.factory.createVariableStatement(
[exportModifier],
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
ts.factory.createIdentifier(name),
undefined,
undefined,
ts.factory.createAsExpression(
ts.factory.createArrayLiteralExpression(
elements.map((e) => ts.factory.createStringLiteral(e))
),
ts.factory.createTypeReferenceNode('const')
)
),
],
ts.NodeFlags.Const
)
)
}
32 changes: 26 additions & 6 deletions gulp/generate-types/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ import ts from 'typescript'
import { BUILD_DIR, SRC_DIR } from '../constants'
import { format } from '../utils'

import { createInterface, createProperty, exportModifier, readonlyModifier } from './factories'
import {
createArrayAsConst,
createInterface,
createProperty,
exportModifier,
readonlyModifier,
} from './factories'
import parseXsd from './parseXsd'

const topComment = '// DO NOT EDIT! File generated using `generate-types` script.'
const filename = 'MediaInfoType.d.ts'
const filename = 'MediaInfoType.ts'
const outFilename = join(SRC_DIR, filename)

async function generate() {
// Parse XSD
const { nodes, intFields, floatFields } = await parseXsd()

// CreationType
const ICreationType = createInterface('CreationType', [
createProperty('version', 'string', { required: true }),
Expand All @@ -34,6 +43,10 @@ async function generate() {
])
)

// Field types
const intFieldsArr = createArrayAsConst('INT_FIELDS', intFields)
const floatFieldsArr = createArrayAsConst('FLOAT_FIELDS', floatFields)

// TrackType
const ITrackType = createInterface('TrackType', [
ts.addSyntheticLeadingComment(
Expand All @@ -53,12 +66,12 @@ async function generate() {
true
),
ts.addSyntheticLeadingComment(
createProperty("'@typeorder'", 'string', { required: true }),
createProperty("'@typeorder'", 'string', { required: false }),
ts.SyntaxKind.MultiLineCommentTrivia,
'* If there is more than one track of the same type (i.e. four audio tracks) this attribute will number them according to storage order within the bitstream. ',
true
),
...(await parseXsd()), // Take long attribute list from MediaInfo XSD
...nodes, // Take long attribute list from MediaInfo XSD
])

// MediaType
Expand All @@ -72,11 +85,18 @@ async function generate() {
createProperty('creatingApplication', 'CreationType'),
createProperty('creatingLibrary', 'CreationType'),
createProperty('media', 'MediaType'),
createProperty('track', 'TrackType'),
])

// Generate source
const allNodes = [ICreationType, ExtraType, ITrackType, IMediaType, IMediaInfo]
const allNodes = [
intFieldsArr,
floatFieldsArr,
ICreationType,
ExtraType,
ITrackType,
IMediaType,
IMediaInfo,
]
const file = ts.createSourceFile('DUMMY.ts', '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS)
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })
const tsSrc = [
Expand Down
27 changes: 18 additions & 9 deletions gulp/generate-types/parseXsd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,34 @@ async function parseXsd() {
const xmlDoc = parseXml(xmlDocData)
const attrs = xmlDoc.find('//xmlns:complexType[@name="trackType"]/xmlns:all/*', namespace)

return attrs.filter(isElement).map((element) => {
let name = element.attr('name')?.value()
// Collect int/float types
const intFields: string[] = []
const floatFields: string[] = []

const nodes = attrs.filter(isElement).map((element) => {
const name = element.attr('name')?.value()
const minOccurs = element.attr('minOccurs')?.value()
const maxOccurs = element.attr('maxOccurs')?.value()
const xsdType = element.attr('type')?.value()
if (!name || !minOccurs || !maxOccurs || !xsdType) {
throw new Error('Element missing attribute')
}

name = name.includes('-') ? `'${name}'` : name

// all attributes should be required
// all attributes should be optional
if (minOccurs !== '0' || maxOccurs !== '1')
throw new Error(`minOccurs=${minOccurs} maxOccurs=${maxOccurs}`)

// extract type
let type: string
if (xsdType === 'extraType') type = 'ExtraType'
else if (xsdType === 'xsd:string') type = 'string'
else if (xsdType === 'xsd:integer') type = 'number'
else if (xsdType === 'xsd:float') type = 'number'
else throw new Error(`Unknown type: ${xsdType}`)
else if (xsdType === 'xsd:integer') {
type = 'number'
intFields.push(name)
} else if (xsdType === 'xsd:float') {
type = 'number'
floatFields.push(name)
} else throw new Error(`Unknown type: ${xsdType}`)

// extract docstring
let docString: string | undefined
Expand All @@ -64,7 +70,8 @@ async function parseXsd() {
}

// create property
const prop = createProperty(name, type, { required: false })
const quotedName = name.includes('-') ? `'${name}'` : name
const prop = createProperty(quotedName, type, { required: false })
return docString
? ts.addSyntheticLeadingComment(
prop,
Expand All @@ -74,6 +81,8 @@ async function parseXsd() {
)
: prop
})

return { nodes, intFields, floatFields }
}

export default parseXsd
52 changes: 49 additions & 3 deletions src/MediaInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
MediaInfoWasmInterface,
WasmConstructableFormatType,
} from './MediaInfoModule'
import type { MediaInfoType } from './MediaInfoType'
import { FLOAT_FIELDS, INT_FIELDS, type MediaInfoType, type TrackType } from './MediaInfoType'

/** Format of the result type */
type FormatType = 'object' | WasmConstructableFormatType
Expand Down Expand Up @@ -153,8 +153,11 @@ class MediaInfo<TFormat extends FormatType = typeof DEFAULT_OPTIONS.format> {
const finalize = () => {
this.openBufferFinalize()
const result = this.inform()
if (this.options.format === 'object') callback(JSON.parse(result) as ResultMap[TFormat])
else callback(result)
if (this.options.format === 'object') {
callback(this.parseResultJson(result))
} else {
callback(result)
}
}

this.openBufferInit(fileSize, offset)
Expand Down Expand Up @@ -245,6 +248,49 @@ class MediaInfo<TFormat extends FormatType = typeof DEFAULT_OPTIONS.format> {
openBufferInit(size: number, offset: number): void {
this.mediainfoModuleInstance.open_buffer_init(size, offset)
}

/**
* Parse result JSON. Convert integer/float fields.
*
* @param result Serialized JSON from MediaInfo
* @returns Parsed JSON object
*/
private parseResultJson(resultString: string): ResultMap[TFormat] {
type Writable<T> = { -readonly [P in keyof T]: T[P] }

const intFields = INT_FIELDS as ReadonlyArray<string>
const floatFields = FLOAT_FIELDS as ReadonlyArray<string>

// Parse JSON
const result = JSON.parse(resultString) as MediaInfoType

if (result.media) {
const newMedia = { ...result.media, track: [] as Writable<TrackType>[] }

if (result.media.track && Array.isArray(result.media.track)) {
for (const track of result.media.track) {
let newTrack: Writable<TrackType> = { '@type': track['@type'] }
for (const [key, val] of Object.entries(track) as [string, unknown][]) {
if (key === '@type') {
continue
}
if (typeof val === 'string' && intFields.includes(key)) {
newTrack = { ...newTrack, [key]: parseInt(val, 10) }
} else if (typeof val === 'string' && floatFields.includes(key)) {
newTrack = { ...newTrack, [key]: parseFloat(val) }
} else {
newTrack = { ...newTrack, [key]: val }
}
}
newMedia.track.push(newTrack)
}
}

return { ...result, media: newMedia } as ResultMap[TFormat]
}

return result as ResultMap[TFormat]
}
}

export type { FormatType, GetSizeFunc, ReadChunkFunc, ResultMap }
Expand Down
126 changes: 124 additions & 2 deletions src/MediaInfoType.d.ts → src/MediaInfoType.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,124 @@
// DO NOT EDIT! File generated using `generate-types` script.

export const INT_FIELDS = [
'AudioCount',
'Audio_Channels_Total',
'BitDepth_Detected',
'BitDepth',
'BitDepth_Stored',
'Channels',
'Channels_Original',
'Chapters_Pos_Begin',
'Chapters_Pos_End',
'Comic_Position_Total',
'Count',
'DataSize',
'ElementCount',
'EPG_Positions_Begin',
'EPG_Positions_End',
'FirstPacketOrder',
'FooterSize',
'Format_Settings_GMC',
'Format_Settings_RefFrames',
'FrameCount',
'FrameRate_Den',
'FrameRate_Num',
'GeneralCount',
'HeaderSize',
'Height_CleanAperture',
'Height',
'Height_Offset',
'Height_Original',
'ImageCount',
'Matrix_Channels',
'MenuCount',
'OtherCount',
'Part_Position',
'Part_Position_Total',
'Played_Count',
'Reel_Position',
'Reel_Position_Total',
'Resolution',
'Sampled_Height',
'Sampled_Width',
'SamplingCount',
'Season_Position',
'Season_Position_Total',
'Source_FrameCount',
'Source_SamplingCount',
'Source_StreamSize_Encoded',
'Source_StreamSize',
'Status',
'Stored_Height',
'Stored_Width',
'StreamCount',
'StreamKindID',
'StreamKindPos',
'StreamOrder',
'StreamSize_Demuxed',
'StreamSize_Encoded',
'StreamSize',
'TextCount',
'Track_Position',
'Track_Position_Total',
'Video0_Delay',
'VideoCount',
'Width_CleanAperture',
'Width',
'Width_Offset',
'Width_Original',
] as const

export const FLOAT_FIELDS = [
'BitRate_Encoded',
'BitRate_Maximum',
'BitRate_Minimum',
'BitRate',
'BitRate_Nominal',
'Bits-Pixel_Frame',
'BitsPixel_Frame',
'Compression_Ratio',
'Delay',
'Delay_Original',
'DisplayAspectRatio_CleanAperture',
'DisplayAspectRatio',
'DisplayAspectRatio_Original',
'Duration_End_Command',
'Duration_End',
'Duration_FirstFrame',
'Duration_LastFrame',
'Duration',
'Duration_Start2End',
'Duration_Start_Command',
'Duration_Start',
'Events_MinDuration',
'FrameRate_Maximum',
'FrameRate_Minimum',
'FrameRate',
'FrameRate_Nominal',
'FrameRate_Original_Den',
'FrameRate_Original',
'FrameRate_Original_Num',
'FrameRate_Real',
'Interleave_Duration',
'Interleave_Preload',
'Interleave_VideoFrames',
'OverallBitRate_Maximum',
'OverallBitRate_Minimum',
'OverallBitRate',
'OverallBitRate_Nominal',
'PixelAspectRatio_CleanAperture',
'PixelAspectRatio',
'PixelAspectRatio_Original',
'SamplesPerFrame',
'SamplingRate',
'Source_Duration_FirstFrame',
'Source_Duration_LastFrame',
'Source_Duration',
'TimeStamp_FirstFrame',
'Video_Delay',
] as const

export interface CreationType {
readonly version: string
readonly url?: string
Expand All @@ -14,7 +133,7 @@ export interface TrackType {
/** Documents the type of encoded media with the track, ie: General, Video, Audio, Text, Image, etc. */
readonly '@type': 'General' | 'Video' | 'Audio' | 'Text' | 'Image' | 'Chapters' | 'Menu'
/** If there is more than one track of the same type (i.e. four audio tracks) this attribute will number them according to storage order within the bitstream. */
readonly '@typeorder': string
readonly '@typeorder'?: string
readonly Accompaniment?: string
/** This element describes Active Format Description (AFD) codes as described in the DVB standard TS 101 154. */
readonly ActiveFormatDescription?: string
Expand Down Expand Up @@ -44,6 +163,7 @@ export interface TrackType {
readonly AssistantDirector?: string
readonly Audio_Codec_List?: string
readonly AudioCount?: number
readonly Audio_Channels_Total?: number
readonly Audio_Format_List?: string
readonly Audio_Format_WithHint_List?: string
readonly Audio_Language_List?: string
Expand Down Expand Up @@ -572,6 +692,9 @@ export interface TrackType {
readonly Rating?: string
readonly Recorded_Date?: string
readonly Recorded_Location?: string
readonly Reel?: string
readonly Reel_Position?: number
readonly Reel_Position_Total?: number
readonly Released_Date?: string
readonly RemixedBy?: string
readonly ReplayGain_Gain?: string
Expand Down Expand Up @@ -780,5 +903,4 @@ export interface MediaInfoType {
readonly creatingApplication?: CreationType
readonly creatingLibrary?: CreationType
readonly media?: MediaType
readonly track?: TrackType
}

0 comments on commit 9b87930

Please sign in to comment.