import test from 'node:test'; import assert from 'node:assert/strict'; import { createPrepareYoutubePlaybackInMpvHandler } from './youtube-playback-launch'; function createWaitStub() { return async (_ms: number): Promise => {}; } test('prepare youtube playback skips load when current path already matches exact URL', async () => { const commands: Array> = []; const prepare = createPrepareYoutubePlaybackInMpvHandler({ requestPath: async () => 'https://www.youtube.com/watch?v=abc123', requestProperty: async () => [{ type: 'video', id: 1 }], sendMpvCommand: (command) => commands.push(command), wait: createWaitStub(), }); const ok = await prepare({ url: 'https://www.youtube.com/watch?v=abc123' }); assert.equal(ok, true); assert.deepEqual(commands, [ ['set_property', 'pause', 'yes'], ['set_property', 'sub-auto', 'no'], ['set_property', 'sid', 'no'], ['set_property', 'secondary-sid', 'no'], ]); }); test('prepare youtube playback treats matching video IDs as already loaded', async () => { const commands: Array> = []; const prepare = createPrepareYoutubePlaybackInMpvHandler({ requestPath: async () => 'https://youtu.be/abc123?t=5', requestProperty: async () => [{ type: 'video', id: 1 }], sendMpvCommand: (command) => commands.push(command), wait: createWaitStub(), }); const ok = await prepare({ url: 'https://www.youtube.com/watch?v=abc123' }); assert.equal(ok, true); assert.deepEqual(commands, [ ['set_property', 'pause', 'yes'], ['set_property', 'sub-auto', 'no'], ['set_property', 'sid', 'no'], ['set_property', 'secondary-sid', 'no'], ]); }); test('prepare youtube playback does not mark matching target ready until tracks exist', async () => { const commands: Array> = []; let requestCount = 0; const prepare = createPrepareYoutubePlaybackInMpvHandler({ requestPath: async () => { requestCount += 1; return 'https://www.youtube.com/watch?v=abc123'; }, requestProperty: async (name) => { if (name !== 'track-list') return null; return requestCount >= 3 ? [{ type: 'video', id: 1 }] : []; }, sendMpvCommand: (command) => commands.push(command), wait: createWaitStub(), }); const ok = await prepare({ url: 'https://www.youtube.com/watch?v=abc123', timeoutMs: 1500, pollIntervalMs: 1, }); assert.equal(ok, true); assert.deepEqual(commands, [ ['set_property', 'pause', 'yes'], ['set_property', 'sub-auto', 'no'], ['set_property', 'sid', 'no'], ['set_property', 'secondary-sid', 'no'], ]); }); test('prepare youtube playback replaces media and waits for path switch', async () => { const commands: Array> = []; const observedPaths = [ '/videos/episode01.mkv', '/videos/episode01.mkv', 'https://www.youtube.com/watch?v=newvid', ]; const observedTrackLists = [null, [], [{ type: 'video', id: 1 }]]; let requestCount = 0; const prepare = createPrepareYoutubePlaybackInMpvHandler({ requestPath: async () => { const value = observedPaths[Math.min(requestCount, observedPaths.length - 1)] ?? null; requestCount += 1; return value; }, requestProperty: async (name) => { if (name !== 'track-list') return null; return observedTrackLists[Math.min(requestCount - 1, observedTrackLists.length - 1)] ?? []; }, sendMpvCommand: (command) => commands.push(command), wait: createWaitStub(), }); const ok = await prepare({ url: 'https://www.youtube.com/watch?v=newvid', timeoutMs: 1500, pollIntervalMs: 1, }); assert.equal(ok, true); assert.deepEqual(commands, [ ['set_property', 'pause', 'yes'], ['set_property', 'sub-auto', 'no'], ['set_property', 'sid', 'no'], ['set_property', 'secondary-sid', 'no'], ['loadfile', 'https://www.youtube.com/watch?v=newvid', 'replace'], ]); }); test('prepare youtube playback returns false after timeout when path never updates', async () => { const commands: Array> = []; let nowTick = 0; const prepare = createPrepareYoutubePlaybackInMpvHandler({ requestPath: async () => '/videos/episode01.mkv', requestProperty: async () => [], sendMpvCommand: (command) => commands.push(command), wait: createWaitStub(), now: () => { nowTick += 100; return nowTick; }, }); const ok = await prepare({ url: 'https://www.youtube.com/watch?v=never-switches', timeoutMs: 350, pollIntervalMs: 1, }); assert.equal(ok, false); assert.deepEqual(commands[4], [ 'loadfile', 'https://www.youtube.com/watch?v=never-switches', 'replace', ]); }); test('prepare youtube playback waits for playable media tracks after youtube path matches', async () => { const commands: Array> = []; const observedPaths = [ '/videos/episode01.mkv', 'https://www.youtube.com/watch?v=newvid', 'https://www.youtube.com/watch?v=newvid', ]; const observedTrackLists = [[], [], [{ type: 'audio', id: 1 }]]; let requestCount = 0; const prepare = createPrepareYoutubePlaybackInMpvHandler({ requestPath: async () => { const value = observedPaths[Math.min(requestCount, observedPaths.length - 1)] ?? null; requestCount += 1; return value; }, requestProperty: async (name) => { if (name !== 'track-list') return null; return observedTrackLists[Math.min(requestCount - 1, observedTrackLists.length - 1)] ?? []; }, sendMpvCommand: (command) => commands.push(command), wait: createWaitStub(), }); const ok = await prepare({ url: 'https://www.youtube.com/watch?v=newvid', timeoutMs: 1500, pollIntervalMs: 1, }); assert.equal(ok, true); assert.deepEqual(commands[4], ['loadfile', 'https://www.youtube.com/watch?v=newvid', 'replace']); }); test('prepare youtube playback accepts a non-youtube resolved path once playable tracks exist', async () => { const commands: Array> = []; const observedPaths = [ '/videos/episode01.mkv', 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc', ]; const observedTrackLists = [ [], [ { type: 'video', id: 1 }, { type: 'audio', id: 2 }, ], ]; let requestCount = 0; const prepare = createPrepareYoutubePlaybackInMpvHandler({ requestPath: async () => { const value = observedPaths[Math.min(requestCount, observedPaths.length - 1)] ?? null; requestCount += 1; return value; }, requestProperty: async (name) => { if (name !== 'track-list') return null; return observedTrackLists[Math.min(requestCount - 1, observedTrackLists.length - 1)] ?? []; }, sendMpvCommand: (command) => commands.push(command), wait: createWaitStub(), }); const ok = await prepare({ url: 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc', timeoutMs: 1500, pollIntervalMs: 1, }); assert.equal(ok, true); assert.deepEqual(commands[4], [ 'loadfile', 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc', 'replace', ]); }); test('prepare youtube playback does not accept a different youtube video after path change', async () => { const commands: Array> = []; let nowTick = 0; const observedPaths = [ '/videos/episode01.mkv', 'https://www.youtube.com/watch?v=wrongvid', 'https://www.youtube.com/watch?v=wrongvid', ]; let requestCount = 0; const prepare = createPrepareYoutubePlaybackInMpvHandler({ requestPath: async () => { const value = observedPaths[Math.min(requestCount, observedPaths.length - 1)] ?? null; requestCount += 1; return value; }, requestProperty: async (name) => { if (name !== 'track-list') return null; return [{ type: 'video', id: 1 }]; }, sendMpvCommand: (command) => commands.push(command), wait: createWaitStub(), now: () => { nowTick += 100; return nowTick; }, }); const ok = await prepare({ url: 'https://www.youtube.com/watch?v=targetvid', timeoutMs: 350, pollIntervalMs: 1, }); assert.equal(ok, false); assert.deepEqual(commands[4], [ 'loadfile', 'https://www.youtube.com/watch?v=targetvid', 'replace', ]); }); test('prepare youtube playback accepts a fresh-start path change when the direct target matches exactly', async () => { const commands: Array> = []; const observedPaths = ['', 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc']; const observedTrackLists = [ [], [ { type: 'video', id: 1 }, { type: 'audio', id: 2 }, ], ]; let requestCount = 0; const prepare = createPrepareYoutubePlaybackInMpvHandler({ requestPath: async () => { const value = observedPaths[Math.min(requestCount, observedPaths.length - 1)] ?? null; requestCount += 1; return value; }, requestProperty: async (name) => { if (name !== 'track-list') return null; return observedTrackLists[Math.min(requestCount - 1, observedTrackLists.length - 1)] ?? []; }, sendMpvCommand: (command) => commands.push(command), wait: createWaitStub(), }); const ok = await prepare({ url: 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc', timeoutMs: 1500, pollIntervalMs: 1, }); assert.equal(ok, true); assert.deepEqual(commands[4], [ 'loadfile', 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc', 'replace', ]); });