mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-28 04:49:49 -07:00
fix: honor YouTube media cache height changes
This commit is contained in:
@@ -2,6 +2,6 @@ type: fixed
|
|||||||
area: youtube
|
area: youtube
|
||||||
|
|
||||||
- Improved YouTube card media generation by sending safer ffmpeg request options for resolved streams and skipping stale stream maps, including cached YouTube files.
|
- Improved YouTube card media generation by sending safer ffmpeg request options for resolved streams and skipping stale stream maps, including cached YouTube files.
|
||||||
- Added `youtube.mediaCache.mode` with `direct` and `background` modes so YouTube card audio/image extraction can optionally use a background yt-dlp media cache when direct stream extraction is unreliable; background mode now announces cache download start/readiness through queued overlay/OSD notifications, creates text-only cards while the cache downloads, queues media updates for the mined note IDs, fills audio/image fields once the cached file is ready, and caps background downloads at `youtube.mediaCache.maxHeight` 720p by default.
|
- Added `youtube.mediaCache.mode` with `direct` and `background` modes so YouTube card audio/image extraction can optionally use a background yt-dlp media cache when direct stream extraction is unreliable; background mode now announces cache download start/readiness through queued overlay/OSD notifications, creates text-only cards while the cache downloads, queues media updates for the mined note IDs, fills audio/image fields once the cached file is ready, and caps background downloads at `youtube.mediaCache.maxHeight` 720p by default while honoring height changes between downloads.
|
||||||
- Cleaned stale YouTube background media cache files on startup and before new background downloads so only the active cached media file remains.
|
- Cleaned stale YouTube background media cache files on startup and before new background downloads so only the active cached media file remains.
|
||||||
- Hardened YouTube media cache downloads with IPv4/extractor retry flags and made failed background downloads notify users and clear queued media updates instead of leaving them pending silently.
|
- Hardened YouTube media cache downloads with IPv4/extractor retry flags and made failed background downloads notify users and clear queued media updates instead of leaving them pending silently.
|
||||||
|
|||||||
@@ -225,6 +225,80 @@ test('YouTube media cache applies the configured download height cap', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('YouTube media cache restarts ready sessions when the height cap changes', async () => {
|
||||||
|
const cacheRoot = makeTempCacheRoot();
|
||||||
|
const spawnedProcesses: FakeYtDlpProcess[] = [];
|
||||||
|
const spawnCalls: SpawnCall[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cache = createYoutubeMediaCacheService({
|
||||||
|
cacheRoot,
|
||||||
|
getYtDlpCommand: () => 'yt-dlp',
|
||||||
|
spawn: (command, args, options) => {
|
||||||
|
spawnCalls.push({ command, args, options });
|
||||||
|
const proc = new FakeYtDlpProcess();
|
||||||
|
spawnedProcesses.push(proc);
|
||||||
|
return proc;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.start('https://youtu.be/demo', { mode: 'background', maxHeight: 720 });
|
||||||
|
const firstOutputTemplate = spawnCalls[0]?.args[spawnCalls[0].args.indexOf('-o') + 1];
|
||||||
|
assert.equal(typeof firstOutputTemplate, 'string');
|
||||||
|
const firstOutputDir = path.dirname(firstOutputTemplate!);
|
||||||
|
const firstOutputPath = path.join(firstOutputDir, 'media.mkv');
|
||||||
|
fs.mkdirSync(firstOutputDir, { recursive: true });
|
||||||
|
fs.writeFileSync(firstOutputPath, 'cached media');
|
||||||
|
spawnedProcesses[0]?.emit('close', 0);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
assert.equal(await cache.getCachedMediaPath('https://youtu.be/demo'), firstOutputPath);
|
||||||
|
|
||||||
|
cache.start('https://youtu.be/demo', { mode: 'background', maxHeight: 480 });
|
||||||
|
|
||||||
|
assert.equal(spawnCalls.length, 2);
|
||||||
|
assert.equal(fs.existsSync(firstOutputPath), false);
|
||||||
|
assert.equal(await cache.getCachedMediaPath('https://youtu.be/demo'), null);
|
||||||
|
assert.equal(
|
||||||
|
spawnCalls[1]?.args[spawnCalls[1].args.indexOf('-f') + 1],
|
||||||
|
'bestvideo*[height<=480]+bestaudio/best[height<=480]',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(cacheRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('YouTube media cache restarts running sessions when the height cap changes', () => {
|
||||||
|
const cacheRoot = makeTempCacheRoot();
|
||||||
|
const spawnedProcesses: FakeYtDlpProcess[] = [];
|
||||||
|
const spawnCalls: SpawnCall[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cache = createYoutubeMediaCacheService({
|
||||||
|
cacheRoot,
|
||||||
|
getYtDlpCommand: () => 'yt-dlp',
|
||||||
|
spawn: (command, args, options) => {
|
||||||
|
spawnCalls.push({ command, args, options });
|
||||||
|
const proc = new FakeYtDlpProcess();
|
||||||
|
spawnedProcesses.push(proc);
|
||||||
|
return proc;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.start('https://youtu.be/demo', { mode: 'background', maxHeight: 720 });
|
||||||
|
cache.start('https://youtu.be/demo', { mode: 'background', maxHeight: 480 });
|
||||||
|
|
||||||
|
assert.equal(spawnedProcesses[0]?.killed, true);
|
||||||
|
assert.equal(spawnCalls.length, 2);
|
||||||
|
assert.equal(
|
||||||
|
spawnCalls[1]?.args[spawnCalls[1].args.indexOf('-f') + 1],
|
||||||
|
'bestvideo*[height<=480]+bestaudio/best[height<=480]',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(cacheRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('YouTube media cache removes stale files from previous runs on startup', () => {
|
test('YouTube media cache removes stale files from previous runs on startup', () => {
|
||||||
const cacheRoot = makeTempCacheRoot();
|
const cacheRoot = makeTempCacheRoot();
|
||||||
const staleDir = path.join(cacheRoot, 'stale-session');
|
const staleDir = path.join(cacheRoot, 'stale-session');
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type SpawnProcess = (
|
|||||||
interface MediaCacheSession {
|
interface MediaCacheSession {
|
||||||
url: string;
|
url: string;
|
||||||
dir: string;
|
dir: string;
|
||||||
|
maxHeight: number;
|
||||||
process: SpawnedProcess | null;
|
process: SpawnedProcess | null;
|
||||||
readyPath: string | null;
|
readyPath: string | null;
|
||||||
state: MediaCacheSessionState;
|
state: MediaCacheSessionState;
|
||||||
@@ -175,6 +176,7 @@ export function createYoutubeMediaCacheService(deps: YoutubeMediaCacheServiceDep
|
|||||||
sessions.set(key, {
|
sessions.set(key, {
|
||||||
url,
|
url,
|
||||||
dir: path.dirname(readyPath),
|
dir: path.dirname(readyPath),
|
||||||
|
maxHeight: session?.maxHeight ?? DEFAULT_MAX_HEIGHT,
|
||||||
process: null,
|
process: null,
|
||||||
readyPath,
|
readyPath,
|
||||||
state: 'ready',
|
state: 'ready',
|
||||||
@@ -199,16 +201,19 @@ export function createYoutubeMediaCacheService(deps: YoutubeMediaCacheServiceDep
|
|||||||
}
|
}
|
||||||
|
|
||||||
const key = cacheKeyForUrl(url);
|
const key = cacheKeyForUrl(url);
|
||||||
|
const maxHeight = normalizeMaxHeight(options.maxHeight);
|
||||||
activeKey = key;
|
activeKey = key;
|
||||||
const dir = getSessionDir(url);
|
const dir = getSessionDir(url);
|
||||||
const existingSession = sessions.get(key);
|
const existingSession = sessions.get(key);
|
||||||
if (existingSession?.state === 'running') {
|
const canReuseExistingSession = existingSession?.maxHeight === maxHeight;
|
||||||
|
if (existingSession?.state === 'running' && canReuseExistingSession) {
|
||||||
removeInactiveSessions(key);
|
removeInactiveSessions(key);
|
||||||
removeCacheRootEntriesExcept([existingSession.dir]);
|
removeCacheRootEntriesExcept([existingSession.dir]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (existingSession) {
|
if (existingSession) {
|
||||||
if (
|
if (
|
||||||
|
canReuseExistingSession &&
|
||||||
existingSession.state === 'ready' &&
|
existingSession.state === 'ready' &&
|
||||||
((existingSession.readyPath && fs.existsSync(existingSession.readyPath)) ||
|
((existingSession.readyPath && fs.existsSync(existingSession.readyPath)) ||
|
||||||
findReadyMediaPath(existingSession.dir))
|
findReadyMediaPath(existingSession.dir))
|
||||||
@@ -225,11 +230,12 @@ export function createYoutubeMediaCacheService(deps: YoutubeMediaCacheServiceDep
|
|||||||
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
const outputTemplate = path.join(dir, 'media.%(ext)s');
|
const outputTemplate = path.join(dir, 'media.%(ext)s');
|
||||||
const args = createYtDlpArgs(url, outputTemplate, options.maxHeight);
|
const args = createYtDlpArgs(url, outputTemplate, maxHeight);
|
||||||
const child = spawn(getYtDlpCommand(), args, { stdio: ['ignore', 'ignore', 'ignore'] });
|
const child = spawn(getYtDlpCommand(), args, { stdio: ['ignore', 'ignore', 'ignore'] });
|
||||||
const session: MediaCacheSession = {
|
const session: MediaCacheSession = {
|
||||||
url,
|
url,
|
||||||
dir,
|
dir,
|
||||||
|
maxHeight,
|
||||||
process: child,
|
process: child,
|
||||||
readyPath: null,
|
readyPath: null,
|
||||||
state: 'running',
|
state: 'running',
|
||||||
|
|||||||
Reference in New Issue
Block a user