import { test, expect } from 'bun:test'; import { readFileSync } from 'node:fs'; import { parseSearch, lengthToSeconds, search, searchDeep, parseContinuation } from './innertube.js'; const fixture = JSON.parse(readFileSync(new URL('./fixtures/innertube-search.json', import.meta.url))); test('parses video renderers into slim cards', () => { const r = parseSearch(fixture); expect(r.length).toBe(4); expect(r[0]).toEqual({ id: 'nQWFzMvCfLE', title: 'What A Beautiful Name - Hillsong Worship', channel: 'Hillsong Worship', channelId: 'UC4q12NoPNySbVqwpw4iO5Vg', channelUrl: 'https://www.youtube.com/channel/UC4q12NoPNySbVqwpw4iO5Vg', duration: 343, thumbnail: 'https://i.ytimg.com/vi/nQWFzMvCfLE/mqdefault.jpg', }); expect(r[1].duration).toBe(7187); expect(r[3].duration).toBe(0); // live, no lengthText }); test('lengthToSeconds', () => { expect(lengthToSeconds('5:43')).toBe(343); expect(lengthToSeconds('1:59:47')).toBe(7187); expect(lengthToSeconds('LIVE')).toBe(0); expect(lengthToSeconds(undefined)).toBe(0); }); test('unexpected shape throws (caller falls back to yt-dlp)', () => { expect(() => parseSearch({})).toThrow(); }); test('search() throws on HTTP error', async () => { const fetchImpl = async () => new Response('no', { status: 429 }); await expect(search('x', { fetchImpl })).rejects.toThrow('429'); }); const card = (id) => ({ videoRenderer: { videoId: id, title: { runs: [{ text: 't' + id }] }, ownerText: { runs: [{ text: 'c' }] } } }); test('searchDeep follows continuation tokens, de-duplicates and stops at the limit', async () => { const calls = []; const fetchImpl = async (_u, init) => { const body = JSON.parse(init.body); calls.push(body.continuation || 'first'); const mk = (ids, token) => [{ itemSectionRenderer: { contents: ids.map(card) } }, ...(token ? [{ continuationItemRenderer: { continuationEndpoint: { continuationCommand: { token } } } }] : [])]; if (!body.continuation) return Response.json({ contents: { twoColumnSearchResultsRenderer: { primaryContents: { sectionListRenderer: { contents: mk(['a', 'b', 'c'], 'T1') } } } } }); if (body.continuation === 'T1') return Response.json({ onResponseReceivedCommands: [{ appendContinuationItemsAction: { continuationItems: mk(['c', 'd', 'e'], 'T2') } }] }); return Response.json({ onResponseReceivedCommands: [{ appendContinuationItemsAction: { continuationItems: mk(['f', 'g'], '') } }] }); }; const all = await searchDeep('x', { fetchImpl, limit: 200 }); expect(all.map((c) => c.id)).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g']); expect(calls).toEqual(['first', 'T1', 'T2']); expect((await searchDeep('x', { fetchImpl, limit: 4 })).length).toBe(4); }); test('parseContinuation of an empty response is empty', () => { expect(parseContinuation({})).toEqual({ cards: [], next: '' }); });