60 lines
3.6 KiB
JavaScript
60 lines
3.6 KiB
JavaScript
import { test, expect } from 'bun:test';
|
|
import { createResilientYtdlp, canTryAnotherClient } from './ytdlp-resilience.js';
|
|
const quiet = { warn() {} };
|
|
const unavailable = () => new Error('ERROR: [youtube] wZzRoXymOUU: Video unavailable');
|
|
|
|
test('metadata and save calls retry unavailable videos with alternate clients', async () => {
|
|
for (const args of [['-J', '--no-warnings', 'https://www.youtube.com/watch?v=wZzRoXymOUU'], ['https://www.youtube.com/watch?v=wZzRoXymOUU', '-f', 'bv*+ba/b', '--merge-output-format', 'mp4', '-o', '/tmp/save.mp4']]) {
|
|
const calls = [];
|
|
const signal = new AbortController().signal;
|
|
const run = createResilientYtdlp({ clients: ['web_embedded', 'web_safari'], log: quiet,
|
|
withCookies: argv => ['--cookies', '/fixture/cookies.txt', ...argv],
|
|
run: async (argv, opts) => { calls.push([argv, opts]); if (calls.length < 3) throw calls.length === 1 ? unavailable() : new Error('Requested format is not available'); return 'success'; },
|
|
});
|
|
expect(await run(args, { signal })).toBe('success');
|
|
expect(calls).toHaveLength(3);
|
|
expect(calls[2][0]).toEqual(['--cookies', '/fixture/cookies.txt', '--extractor-args', 'youtube:player_client=web_safari', ...args]);
|
|
expect(calls.every(([, opts]) => opts.signal === signal)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('an initial missing-format error also gets a client fallback', async () => {
|
|
let calls = 0;
|
|
const run = createResilientYtdlp({ log: quiet, clients: ['web_safari'], run: async () => { if (++calls === 1) throw new Error('Requested format is not available'); return 'ok'; } });
|
|
expect(await run(['-f', 'b'])).toBe('ok');
|
|
});
|
|
|
|
test('exhaustion preserves the original diagnosis and the attempt trail', async () => {
|
|
let calls = 0;
|
|
const run = createResilientYtdlp({ clients: ['web_embedded', 'web_embedded', 'mweb'], log: quiet, run: async () => { if (++calls === 1) throw unavailable(); throw new Error('Requested format is not available'); } });
|
|
const err = await run(['-J']).catch(e => e);
|
|
expect(calls).toBe(3);
|
|
expect(err.message).toContain('wZzRoXymOUU: Video unavailable');
|
|
expect(err.message).toContain('default, web_embedded, mweb');
|
|
expect(err.attempts).toHaveLength(3);
|
|
});
|
|
|
|
test('explicit client selection and unrelated failures are not retried', async () => {
|
|
for (const [args, message] of [[['--extractor-args', 'youtube:player_client=mweb'], 'Video unavailable'], [['--extractor-args', 'youtube:player-client=mweb'], 'Video unavailable'], [[], 'No space left on device'], [[], 'yt-dlp not found'], [[], 'HTTP Error 429: Too Many Requests']]) {
|
|
let calls = 0;
|
|
const run = createResilientYtdlp({ log: quiet, run: async () => { calls++; throw new Error(message); } });
|
|
await expect(run(args)).rejects.toThrow(message);
|
|
expect(calls).toBe(1);
|
|
}
|
|
});
|
|
|
|
test('explicit access restrictions stop retries, even with an unavailable prefix', () => {
|
|
for (const detail of ['Private video', 'Video has been removed', 'not available in your country', 'members-only', 'Confirm your age', 'copyright claim']) expect(canTryAnotherClient(new Error('Video unavailable. ' + detail))).toBe(false);
|
|
expect(canTryAnotherClient(new Error('Sign in to confirm you are not a bot'))).toBe(true);
|
|
});
|
|
|
|
test('cancellation stops the fallback chain', async () => {
|
|
const controller = new AbortController();
|
|
let calls = 0;
|
|
const run = createResilientYtdlp({ log: quiet, run: async () => { calls++; controller.abort(); throw unavailable(); } });
|
|
await expect(run(['-J'], { signal: controller.signal })).rejects.toThrow('Video unavailable');
|
|
expect(calls).toBe(1);
|
|
await expect(run(['-J'], { signal: controller.signal })).rejects.toThrow();
|
|
expect(calls).toBe(1);
|
|
});
|