mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-26 12:11:26 -07:00
Fix Windows YouTube playback flow and overlay pointer tracking
This commit is contained in:
@@ -246,6 +246,23 @@ test('handleCliCommand defaults youtube mode to download when omitted', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('handleCliCommand reuses initialized overlay runtime for second-instance youtube playback', () => {
|
||||
const { deps, calls } = createDeps({
|
||||
isOverlayRuntimeInitialized: () => true,
|
||||
runYoutubePlaybackFlow: async (request) => {
|
||||
calls.push(`youtube:${request.url}:${request.mode}:${request.source}`);
|
||||
},
|
||||
});
|
||||
|
||||
handleCliCommand(
|
||||
makeArgs({ youtubePlay: 'https://youtube.com/watch?v=abc', youtubeMode: 'download' }),
|
||||
'second-instance',
|
||||
deps,
|
||||
);
|
||||
|
||||
assert.deepEqual(calls, ['youtube:https://youtube.com/watch?v=abc:download:second-instance']);
|
||||
});
|
||||
|
||||
test('handleCliCommand reports youtube playback flow failures to logs and OSD', async () => {
|
||||
const { deps, calls, osd } = createDeps({
|
||||
runYoutubePlaybackFlow: async () => {
|
||||
|
||||
67
src/core/services/youtube/playback-resolve.test.ts
Normal file
67
src/core/services/youtube/playback-resolve.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { resolveYoutubePlaybackUrl } from './playback-resolve';
|
||||
|
||||
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-youtube-playback-resolve-'));
|
||||
try {
|
||||
return await fn(dir);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function makeFakeYtDlpScript(dir: string, payload: string): void {
|
||||
const scriptPath = path.join(dir, 'yt-dlp');
|
||||
const script = `#!/usr/bin/env node
|
||||
process.stdout.write(${JSON.stringify(payload)});
|
||||
`;
|
||||
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: string, fn: () => Promise<T>): Promise<T> {
|
||||
return await withTempDir(async (root) => {
|
||||
const binDir = path.join(root, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
makeFakeYtDlpScript(binDir, payload);
|
||||
const fakeCommandPath =
|
||||
process.platform === 'win32' ? path.join(binDir, 'yt-dlp.cmd') : path.join(binDir, 'yt-dlp');
|
||||
const originalCommand = process.env.SUBMINER_YTDLP_BIN;
|
||||
process.env.SUBMINER_YTDLP_BIN = fakeCommandPath;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
if (originalCommand === undefined) {
|
||||
delete process.env.SUBMINER_YTDLP_BIN;
|
||||
} else {
|
||||
process.env.SUBMINER_YTDLP_BIN = originalCommand;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('resolveYoutubePlaybackUrl returns the first playable URL line', async () => {
|
||||
await withFakeYtDlp(
|
||||
'\nhttps://manifest.googlevideo.com/api/manifest/hls_playlist/test\nhttps://ignored.example/video\n',
|
||||
async () => {
|
||||
const result = await resolveYoutubePlaybackUrl('https://www.youtube.com/watch?v=abc123');
|
||||
assert.equal(result, 'https://manifest.googlevideo.com/api/manifest/hls_playlist/test');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveYoutubePlaybackUrl rejects when yt-dlp returns no URL', async () => {
|
||||
await withFakeYtDlp('\n', async () => {
|
||||
await assert.rejects(
|
||||
resolveYoutubePlaybackUrl('https://www.youtube.com/watch?v=abc123'),
|
||||
/returned empty output/,
|
||||
);
|
||||
});
|
||||
});
|
||||
63
src/core/services/youtube/playback-resolve.ts
Normal file
63
src/core/services/youtube/playback-resolve.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const YOUTUBE_PLAYBACK_RESOLVE_TIMEOUT_MS = 15_000;
|
||||
const DEFAULT_PLAYBACK_FORMAT = 'b';
|
||||
|
||||
function runCapture(
|
||||
command: string,
|
||||
args: string[],
|
||||
timeoutMs = YOUTUBE_PLAYBACK_RESOLVE_TIMEOUT_MS,
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill();
|
||||
reject(new Error(`yt-dlp timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
proc.stdout.setEncoding('utf8');
|
||||
proc.stderr.setEncoding('utf8');
|
||||
proc.stdout.on('data', (chunk) => {
|
||||
stdout += String(chunk);
|
||||
});
|
||||
proc.stderr.on('data', (chunk) => {
|
||||
stderr += String(chunk);
|
||||
});
|
||||
proc.once('error', (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
proc.once('close', (code) => {
|
||||
clearTimeout(timer);
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
return;
|
||||
}
|
||||
reject(new Error(stderr.trim() || `yt-dlp exited with status ${code ?? 'unknown'}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveYoutubePlaybackUrl(
|
||||
targetUrl: string,
|
||||
format = DEFAULT_PLAYBACK_FORMAT,
|
||||
): Promise<string> {
|
||||
const ytDlpCommand = process.env.SUBMINER_YTDLP_BIN?.trim() || 'yt-dlp';
|
||||
const { stdout } = await runCapture(ytDlpCommand, [
|
||||
'--get-url',
|
||||
'--no-warnings',
|
||||
'-f',
|
||||
format,
|
||||
targetUrl,
|
||||
]);
|
||||
const playbackUrl =
|
||||
stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.length > 0) ?? '';
|
||||
if (!playbackUrl) {
|
||||
throw new Error('yt-dlp returned empty output while resolving YouTube playback URL');
|
||||
}
|
||||
return playbackUrl;
|
||||
}
|
||||
Reference in New Issue
Block a user