Files
SubMiner/src/anki-integration/media-source.test.ts
T
sudacode 028636c76d feat(youtube): queue media for background cache and fill fields when rea
- Add `youtube.mediaCache.maxHeight` config option (default 720p)
- Background mode creates text-only cards while cache downloads, queues media updates, fills audio/image fields once cached file is ready
- Announce cache download start and readiness via overlay/OSD notifications
- Skip mpv stream indexes when generating audio from a YouTube cached file
2026-06-23 20:45:28 -07:00

299 lines
10 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import * as mediaSource from './media-source';
const { resolveMediaGenerationInputPath } = mediaSource;
type StructuredMediaInput = {
path: string;
source: string;
singleResolvedStream: boolean;
inputOptions?: {
reconnect?: boolean;
userAgent?: string;
headers?: Record<string, string>;
};
};
type StructuredMediaResolver = (
mpvClient: Parameters<typeof resolveMediaGenerationInputPath>[0],
kind?: Parameters<typeof resolveMediaGenerationInputPath>[1],
options?: {
getCachedMediaPath?: (
currentVideoPath: string,
kind: Parameters<typeof resolveMediaGenerationInputPath>[1],
) => Promise<string | null>;
remoteCacheMode?: 'optional' | 'required';
logDebug?: (message: string) => void;
},
) => Promise<StructuredMediaInput | null>;
test('resolveMediaGenerationInputPath keeps local file paths', async () => {
const result = await resolveMediaGenerationInputPath({
currentVideoPath: '/tmp/video.mkv',
});
assert.equal(result, '/tmp/video.mkv');
});
test('resolveMediaGenerationInputPath prefers stream-open-filename for remote media', async () => {
const requests: string[] = [];
const result = await resolveMediaGenerationInputPath({
currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
requestProperty: async (name: string) => {
requests.push(name);
return 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123';
},
});
assert.equal(result, 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123');
assert.deepEqual(requests, ['stream-open-filename']);
});
test('resolveMediaGenerationInputPath unwraps mpv edl source for audio and video', async () => {
const edlSource = [
'edl://!new_stream;!no_clip;!no_chapters;%70%https://audio.example/videoplayback?mime=audio%2Fwebm',
'!new_stream;!no_clip;!no_chapters;%69%https://video.example/videoplayback?mime=video%2Fmp4',
'!global_tags,title=test',
].join(';');
const audioResult = await resolveMediaGenerationInputPath(
{
currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
requestProperty: async () => edlSource,
},
'audio',
);
const videoResult = await resolveMediaGenerationInputPath(
{
currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
requestProperty: async () => edlSource,
},
'video',
);
assert.equal(audioResult, 'https://audio.example/videoplayback?mime=audio%2Fwebm');
assert.equal(videoResult, 'https://video.example/videoplayback?mime=video%2Fmp4');
});
test('resolveMediaGenerationInputPath falls back to currentVideoPath when stream-open-filename fails', async () => {
const result = await resolveMediaGenerationInputPath({
currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
requestProperty: async () => {
throw new Error('property unavailable');
},
});
assert.equal(result, 'https://www.youtube.com/watch?v=abc123');
});
test('resolveMediaGenerationInput returns single-stream metadata for mpv EDL URLs', async () => {
const resolver = (
mediaSource as typeof mediaSource & {
resolveMediaGenerationInput?: StructuredMediaResolver;
}
).resolveMediaGenerationInput;
assert.equal(typeof resolver, 'function');
const edlSource = [
'edl://!new_stream;!no_clip;!no_chapters;%70%https://audio.example/videoplayback?mime=audio%2Fwebm',
'!new_stream;!no_clip;!no_chapters;%69%https://video.example/videoplayback?mime=video%2Fmp4',
].join(';');
const result = await resolver!(
{
currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
requestProperty: async (name: string) => {
if (name === 'stream-open-filename') return edlSource;
if (name === 'user-agent') return 'Mozilla/5.0';
if (name === 'http-header-fields') {
return ['Cookie: SID=secret', 'Referer: https://www.youtube.com/', 'X-Test: ok'];
}
return null;
},
},
'audio',
);
assert.equal(result?.path, 'https://audio.example/videoplayback?mime=audio%2Fwebm');
assert.equal(result?.singleResolvedStream, true);
assert.equal(result?.inputOptions?.reconnect, true);
assert.equal(result?.inputOptions?.userAgent, 'Mozilla/5.0');
assert.deepEqual(result?.inputOptions?.headers, {
Referer: 'https://www.youtube.com/',
'X-Test': 'ok',
});
});
test('resolveMediaGenerationInput reads file-local mpv request options', async () => {
const resolver = (
mediaSource as typeof mediaSource & {
resolveMediaGenerationInput?: StructuredMediaResolver;
}
).resolveMediaGenerationInput;
assert.equal(typeof resolver, 'function');
const result = await resolver!(
{
currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
requestProperty: async (name: string) => {
if (name === 'stream-open-filename') {
return 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123';
}
if (name === 'file-local-options/user-agent') return 'SubMiner Test Agent';
if (name === 'options/http-header-fields') return ['X-Shared: ok'];
if (name === 'file-local-options/http-header-fields') {
return ['Cookie: SID=secret', 'Referer: https://m.youtube.com/', 'X-Local: yes'];
}
return null;
},
},
'video',
);
assert.equal(result?.path, 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123');
assert.equal(result?.singleResolvedStream, true);
assert.equal(result?.inputOptions?.userAgent, 'SubMiner Test Agent');
assert.deepEqual(result?.inputOptions?.headers, {
'X-Shared': 'ok',
Referer: 'https://m.youtube.com/',
'X-Local': 'yes',
Origin: 'https://www.youtube.com',
});
});
test('resolveMediaGenerationInput prefers a ready cached media file for YouTube extraction', async () => {
const resolver = (
mediaSource as typeof mediaSource & {
resolveMediaGenerationInput?: StructuredMediaResolver;
}
).resolveMediaGenerationInput;
assert.equal(typeof resolver, 'function');
const result = await resolver!(
{
currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
requestProperty: async () => 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123',
},
'video',
{
getCachedMediaPath: async () => '/tmp/subminer-youtube-media-cache/abc123/media.mkv',
},
);
assert.equal(result?.path, '/tmp/subminer-youtube-media-cache/abc123/media.mkv');
assert.equal(result?.source, 'youtube-cache');
assert.equal(result?.singleResolvedStream, false);
assert.equal(result?.inputOptions, undefined);
});
test('resolveMediaGenerationInput debug-logs sanitized YouTube cache hits', async () => {
const resolver = (
mediaSource as typeof mediaSource & {
resolveMediaGenerationInput?: StructuredMediaResolver;
}
).resolveMediaGenerationInput;
assert.equal(typeof resolver, 'function');
const logs: string[] = [];
const result = await resolver!(
{
currentVideoPath: 'https://www.youtube.com/watch?v=abc123&signature=secret',
requestProperty: async () => 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123',
},
'video',
{
getCachedMediaPath: async () => '/tmp/subminer-youtube-media-cache/abc123/media.mkv',
logDebug: (message) => logs.push(message),
},
);
assert.equal(result?.source, 'youtube-cache');
assert.match(logs.join('\n'), /kind=video source=youtube-cache/);
assert.match(
logs.join('\n'),
/input=local:\/tmp\/subminer-youtube-media-cache\/abc123\/media\.mkv/,
);
assert.match(logs.join('\n'), /current=remote:www\.youtube\.com/);
assert.doesNotMatch(logs.join('\n'), /signature=secret|videoplayback/);
});
test('resolveMediaGenerationInput does not fall back to direct remote streams when cache is required', async () => {
const resolver = (
mediaSource as typeof mediaSource & {
resolveMediaGenerationInput?: StructuredMediaResolver;
}
).resolveMediaGenerationInput;
assert.equal(typeof resolver, 'function');
const result = await resolver!(
{
currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
requestProperty: async () => 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123',
},
'video',
{
getCachedMediaPath: async () => null,
remoteCacheMode: 'required',
},
);
assert.equal(result, null);
});
test('resolveMediaGenerationInput falls back when optional cache lookup fails', async () => {
const resolver = (
mediaSource as typeof mediaSource & {
resolveMediaGenerationInput?: StructuredMediaResolver;
}
).resolveMediaGenerationInput;
assert.equal(typeof resolver, 'function');
const result = await resolver!(
{
currentVideoPath: 'https://www.youtube.com/watch?v=abc123',
requestProperty: async () => 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123',
},
'video',
{
getCachedMediaPath: async () => {
throw new Error('cache unavailable');
},
},
);
assert.equal(result?.path, 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123');
assert.equal(result?.source, 'stream-open-filename');
});
test('resolveMediaGenerationInput debug-logs sanitized required-cache misses', async () => {
const resolver = (
mediaSource as typeof mediaSource & {
resolveMediaGenerationInput?: StructuredMediaResolver;
}
).resolveMediaGenerationInput;
assert.equal(typeof resolver, 'function');
const logs: string[] = [];
const result = await resolver!(
{
currentVideoPath: 'https://www.youtube.com/watch?v=abc123&signature=secret',
requestProperty: async () => 'https://rr1---sn.example.googlevideo.com/videoplayback?id=123',
},
'video',
{
getCachedMediaPath: async () => null,
remoteCacheMode: 'required',
logDebug: (message) => logs.push(message),
},
);
assert.equal(result, null);
assert.match(logs.join('\n'), /kind=video source=cache-miss/);
assert.match(logs.join('\n'), /mode=required/);
assert.match(logs.join('\n'), /current=remote:www\.youtube\.com/);
assert.doesNotMatch(logs.join('\n'), /signature=secret|videoplayback|googlevideo/);
});