import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { EventEmitter } from 'node:events'; import test from 'node:test'; import type { spawn as spawnFn } from 'node:child_process'; import { SOURCE_TYPE_LOCAL } from './types'; import { getLocalVideoMetadata, runFfprobe } from './metadata'; type Spawn = typeof spawnFn; function createSpawnStub(options: { stdout?: string; stderr?: string; emitError?: boolean; }): Spawn { return (() => { const child = new EventEmitter() as EventEmitter & { stdout: EventEmitter; stderr: EventEmitter; }; child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); queueMicrotask(() => { if (options.emitError) { child.emit('error', new Error('ffprobe failed')); return; } if (options.stderr) { child.stderr.emit('data', Buffer.from(options.stderr)); } if (options.stdout !== undefined) { child.stdout.emit('data', Buffer.from(options.stdout)); } child.emit('close', 0); }); return child as unknown as ReturnType; }) as Spawn; } test('runFfprobe parses valid JSON from stream and format sections', async () => { const metadata = await runFfprobe('/tmp/video.mp4', { spawn: createSpawnStub({ stdout: JSON.stringify({ format: { duration: '12.34', bit_rate: '3456000' }, streams: [ { codec_type: 'video', codec_tag_string: 'avc1', width: 1920, height: 1080, avg_frame_rate: '24000/1001', }, { codec_type: 'audio', codec_tag_string: 'mp4a', }, ], }), }), }); assert.equal(metadata.durationMs, 12340); assert.equal(metadata.bitrateKbps, 3456); assert.equal(metadata.widthPx, 1920); assert.equal(metadata.heightPx, 1080); assert.equal(metadata.fpsX100, 2398); assert.equal(metadata.containerId, 0); assert.ok(Number(metadata.codecId) > 0); assert.ok(Number(metadata.audioCodecId) > 0); }); test('runFfprobe returns empty metadata for invalid JSON and process errors', async () => { const invalidJsonMetadata = await runFfprobe('/tmp/broken.mp4', { spawn: createSpawnStub({ stdout: '{invalid' }), }); assert.deepEqual(invalidJsonMetadata, { durationMs: null, codecId: null, containerId: null, widthPx: null, heightPx: null, fpsX100: null, bitrateKbps: null, audioCodecId: null, }); const errorMetadata = await runFfprobe('/tmp/error.mp4', { spawn: createSpawnStub({ emitError: true }), }); assert.deepEqual(errorMetadata, { durationMs: null, codecId: null, containerId: null, widthPx: null, heightPx: null, fpsX100: null, bitrateKbps: null, audioCodecId: null, }); }); test('getLocalVideoMetadata derives title and falls back to null hash on read errors', async () => { const successMetadata = await getLocalVideoMetadata('/tmp/Episode 01.mkv', { spawn: createSpawnStub({ stdout: JSON.stringify({ format: { duration: '0' }, streams: [] }) }), fs: { createReadStream: () => { const stream = new EventEmitter(); queueMicrotask(() => { stream.emit('data', Buffer.from('hello world')); stream.emit('end'); }); return stream as unknown as ReturnType; }, promises: { stat: (async () => ({ size: 1234 }) as unknown) as typeof import('node:fs').promises.stat, }, } as never, }); assert.equal(successMetadata.sourceType, SOURCE_TYPE_LOCAL); assert.equal(successMetadata.canonicalTitle, 'Episode 01'); assert.equal(successMetadata.fileSizeBytes, 1234); assert.equal( successMetadata.hashSha256, createHash('sha256').update('hello world').digest('hex'), ); const hashFallbackMetadata = await getLocalVideoMetadata('/tmp/Episode 02.mkv', { spawn: createSpawnStub({ stdout: JSON.stringify({ format: {}, streams: [] }) }), fs: { createReadStream: () => { const stream = new EventEmitter(); queueMicrotask(() => { stream.emit('error', new Error('read failed')); }); return stream as unknown as ReturnType; }, promises: { stat: (async () => ({ size: 5678 }) as unknown) as typeof import('node:fs').promises.stat, }, } as never, }); assert.equal(hashFallbackMetadata.canonicalTitle, 'Episode 02'); assert.equal(hashFallbackMetadata.hashSha256, null); });