mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-09 16:19:25 -07:00
- Use #!/bin/sh instead of #!/usr/bin/env sh (avoids PATH dependency) - Set minimal PATH inside script for base64/cat commands - Fix base64 content encoding: use stdoutBody directly instead of JSON.stringify(stdoutBody) which double-encoded the output - Use unique heredoc delimiter to avoid conflicts with content
169 lines
5.4 KiB
TypeScript
169 lines
5.4 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { probeYoutubeTracks } from './track-probe';
|
|
|
|
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-youtube-track-probe-'));
|
|
try {
|
|
return await fn(dir);
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function makeFakeYtDlpScript(dir: string, payload: unknown, rawScript = false): void {
|
|
const scriptPath = path.join(dir, 'yt-dlp');
|
|
const stdoutBody = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
const script =
|
|
process.platform === 'win32'
|
|
? rawScript
|
|
? stdoutBody
|
|
: `#!/usr/bin/env bun
|
|
process.stdout.write(${JSON.stringify(stdoutBody)});
|
|
`
|
|
: `#!/bin/sh
|
|
PATH=/usr/bin:/bin:/usr/local/bin
|
|
cat <<'SUBMINER_EOF' | base64 -d
|
|
${Buffer.from(stdoutBody).toString('base64')}
|
|
SUBMINER_EOF
|
|
`;
|
|
fs.writeFileSync(scriptPath, script, 'utf8');
|
|
if (process.platform !== 'win32') {
|
|
fs.chmodSync(scriptPath, 0o755);
|
|
}
|
|
fs.writeFileSync(scriptPath + '.cmd', `@echo off\r\nnode "${scriptPath}"\r\n`, 'utf8');
|
|
}
|
|
|
|
async function withFakeYtDlp<T>(
|
|
payload: unknown,
|
|
fn: () => Promise<T>,
|
|
options: { rawScript?: boolean } = {},
|
|
): Promise<T> {
|
|
return await withTempDir(async (root) => {
|
|
const binDir = path.join(root, 'bin');
|
|
fs.mkdirSync(binDir, { recursive: true });
|
|
makeFakeYtDlpScript(binDir, payload, options.rawScript === true);
|
|
const originalPath = process.env.PATH ?? '';
|
|
const originalCommand = process.env.SUBMINER_YTDLP_BIN;
|
|
process.env.PATH = `${binDir}${path.delimiter}${originalPath}`;
|
|
process.env.SUBMINER_YTDLP_BIN =
|
|
process.platform === 'win32' ? path.join(binDir, 'yt-dlp.cmd') : path.join(binDir, 'yt-dlp');
|
|
try {
|
|
return await fn();
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
if (originalCommand === undefined) {
|
|
delete process.env.SUBMINER_YTDLP_BIN;
|
|
} else {
|
|
process.env.SUBMINER_YTDLP_BIN = originalCommand;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async function withFakeYtDlpCommand<T>(
|
|
payload: unknown,
|
|
fn: () => Promise<T>,
|
|
options: { rawScript?: boolean } = {},
|
|
): Promise<T> {
|
|
return await withTempDir(async (root) => {
|
|
const binDir = path.join(root, 'bin');
|
|
fs.mkdirSync(binDir, { recursive: true });
|
|
makeFakeYtDlpScript(binDir, payload, options.rawScript === true);
|
|
const originalPath = process.env.PATH;
|
|
const originalCommand = process.env.SUBMINER_YTDLP_BIN;
|
|
process.env.PATH = '';
|
|
process.env.SUBMINER_YTDLP_BIN =
|
|
process.platform === 'win32' ? path.join(binDir, 'yt-dlp.cmd') : path.join(binDir, 'yt-dlp');
|
|
try {
|
|
return await fn();
|
|
} finally {
|
|
if (originalPath === undefined) {
|
|
delete process.env.PATH;
|
|
} else {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
if (originalCommand === undefined) {
|
|
delete process.env.SUBMINER_YTDLP_BIN;
|
|
} else {
|
|
process.env.SUBMINER_YTDLP_BIN = originalCommand;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
test('probeYoutubeTracks prefers srv3 over vtt for automatic captions', async () => {
|
|
await withFakeYtDlp(
|
|
{
|
|
id: 'abc123',
|
|
title: 'Example',
|
|
automatic_captions: {
|
|
'ja-orig': [
|
|
{ ext: 'vtt', url: 'https://example.com/ja.vtt', name: 'Japanese auto' },
|
|
{ ext: 'srv3', url: 'https://example.com/ja.srv3', name: 'Japanese auto' },
|
|
],
|
|
},
|
|
},
|
|
async () => {
|
|
const result = await probeYoutubeTracks('https://www.youtube.com/watch?v=abc123');
|
|
assert.equal(result.videoId, 'abc123');
|
|
assert.equal(result.tracks[0]?.downloadUrl, 'https://example.com/ja.srv3');
|
|
assert.equal(result.tracks[0]?.fileExtension, 'srv3');
|
|
},
|
|
);
|
|
});
|
|
|
|
test('probeYoutubeTracks honors SUBMINER_YTDLP_BIN when yt-dlp is not on PATH', async () => {
|
|
if (process.platform === 'win32') {
|
|
return;
|
|
}
|
|
|
|
await withFakeYtDlpCommand(
|
|
{
|
|
id: 'abc123',
|
|
title: 'Example',
|
|
subtitles: {
|
|
ja: [{ ext: 'vtt', url: 'https://example.com/ja.vtt', name: 'Japanese manual' }],
|
|
},
|
|
},
|
|
async () => {
|
|
const result = await probeYoutubeTracks('https://www.youtube.com/watch?v=abc123');
|
|
assert.equal(result.videoId, 'abc123');
|
|
assert.equal(result.tracks[0]?.downloadUrl, 'https://example.com/ja.vtt');
|
|
assert.equal(result.tracks[0]?.fileExtension, 'vtt');
|
|
},
|
|
);
|
|
});
|
|
|
|
test('probeYoutubeTracks keeps preferring srt for manual captions', async () => {
|
|
await withFakeYtDlp(
|
|
{
|
|
id: 'abc123',
|
|
title: 'Example',
|
|
subtitles: {
|
|
ja: [
|
|
{ ext: 'srv3', url: 'https://example.com/ja.srv3', name: 'Japanese manual' },
|
|
{ ext: 'srt', url: 'https://example.com/ja.srt', name: 'Japanese manual' },
|
|
],
|
|
},
|
|
},
|
|
async () => {
|
|
const result = await probeYoutubeTracks('https://www.youtube.com/watch?v=abc123');
|
|
assert.equal(result.tracks[0]?.downloadUrl, 'https://example.com/ja.srt');
|
|
assert.equal(result.tracks[0]?.fileExtension, 'srt');
|
|
},
|
|
);
|
|
});
|
|
|
|
test('probeYoutubeTracks reports malformed yt-dlp JSON with context', async () => {
|
|
await withFakeYtDlp('not-json', async () => {
|
|
await assert.rejects(
|
|
async () => await probeYoutubeTracks('https://www.youtube.com/watch?v=abc123'),
|
|
/Failed to parse yt-dlp output as JSON/,
|
|
);
|
|
});
|
|
});
|