mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-01 06:12:07 -07:00
refactor: split main.ts into domain runtimes
This commit is contained in:
207
src/main/subtitle-runtime.test.ts
Normal file
207
src/main/subtitle-runtime.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
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 { createSubtitleRuntime } from './subtitle-runtime';
|
||||
|
||||
function createResolvedConfig() {
|
||||
return {
|
||||
subtitleStyle: {
|
||||
frequencyDictionary: {
|
||||
enabled: true,
|
||||
topX: 5,
|
||||
mode: 'top',
|
||||
},
|
||||
},
|
||||
subtitleSidebar: {
|
||||
autoScroll: true,
|
||||
pauseVideoOnHover: false,
|
||||
maxWidth: 420,
|
||||
opacity: 0.92,
|
||||
backgroundColor: '#111111',
|
||||
textColor: '#ffffff',
|
||||
fontFamily: 'sans-serif',
|
||||
fontSize: 24,
|
||||
timestampColor: '#cccccc',
|
||||
activeLineColor: '#ffffff',
|
||||
activeLineBackgroundColor: '#222222',
|
||||
hoverLineBackgroundColor: '#333333',
|
||||
},
|
||||
subtitlePosition: {
|
||||
yPercent: 84,
|
||||
},
|
||||
subsync: {
|
||||
defaultMode: 'auto' as const,
|
||||
alass_path: 'alass',
|
||||
ffmpeg_path: 'ffmpeg',
|
||||
ffsubsync_path: 'ffsubsync',
|
||||
replace: false,
|
||||
},
|
||||
} as never;
|
||||
}
|
||||
|
||||
function createMpvClient(properties: Record<string, unknown>) {
|
||||
return {
|
||||
connected: true,
|
||||
currentSubStart: 1.25,
|
||||
currentSubEnd: 2.5,
|
||||
currentTimePos: 12.5,
|
||||
requestProperty: async (name: string) => properties[name],
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntime(overrides: Partial<Parameters<typeof createSubtitleRuntime>[0]> = {}) {
|
||||
const calls: string[] = [];
|
||||
const config = createResolvedConfig();
|
||||
let subtitlePosition: unknown = null;
|
||||
let pendingSubtitlePosition: unknown = null;
|
||||
const runtime = createSubtitleRuntime({
|
||||
getResolvedConfig: () => config,
|
||||
getCurrentMediaPath: () => '/media/episode.mkv',
|
||||
getCurrentMediaTitle: () => 'Episode',
|
||||
getCurrentSubText: () => 'current subtitle',
|
||||
getCurrentSubAssText: () => '[Events]',
|
||||
getMpvClient: () =>
|
||||
createMpvClient({
|
||||
'current-tracks/sub/external-filename': '/tmp/episode.ass',
|
||||
'current-tracks/sub': {
|
||||
type: 'sub',
|
||||
id: 3,
|
||||
external: true,
|
||||
'external-filename': '/tmp/episode.ass',
|
||||
},
|
||||
'track-list': [
|
||||
{
|
||||
type: 'sub',
|
||||
id: 3,
|
||||
external: true,
|
||||
'external-filename': '/tmp/episode.ass',
|
||||
},
|
||||
],
|
||||
sid: 3,
|
||||
path: '/media/episode.mkv',
|
||||
}),
|
||||
broadcastToOverlayWindows: (channel, payload) => {
|
||||
calls.push(`${channel}:${JSON.stringify(payload)}`);
|
||||
},
|
||||
subtitleWsService: {
|
||||
broadcast: () => calls.push('subtitle-ws'),
|
||||
},
|
||||
annotationSubtitleWsService: {
|
||||
broadcast: () => calls.push('annotation-ws'),
|
||||
},
|
||||
subtitlePositionsDir: fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-subtitle-runtime-')),
|
||||
setSubtitlePosition: (position) => {
|
||||
subtitlePosition = position;
|
||||
},
|
||||
setPendingSubtitlePosition: (position) => {
|
||||
pendingSubtitlePosition = position;
|
||||
},
|
||||
clearPendingSubtitlePosition: () => {
|
||||
pendingSubtitlePosition = null;
|
||||
},
|
||||
parseSubtitleCues: (content) => [
|
||||
{
|
||||
startTime: 0,
|
||||
endTime: 1,
|
||||
text: content.trim(),
|
||||
},
|
||||
],
|
||||
createSubtitlePrefetchService: ({ cues }) => ({
|
||||
start: () => calls.push(`start:${cues.length}`),
|
||||
stop: () => calls.push('stop'),
|
||||
onSeek: (time) => calls.push(`seek:${time}`),
|
||||
pause: () => calls.push('pause'),
|
||||
resume: () => calls.push('resume'),
|
||||
}),
|
||||
logDebug: (message) => calls.push(`debug:${message}`),
|
||||
logInfo: (message) => calls.push(`info:${message}`),
|
||||
logWarn: (message) => calls.push(`warn:${message}`),
|
||||
schedule: (fn, delayMs) => setTimeout(fn, delayMs),
|
||||
clearSchedule: (timer) => clearTimeout(timer),
|
||||
...overrides,
|
||||
});
|
||||
return { runtime, calls, subtitlePosition, pendingSubtitlePosition };
|
||||
}
|
||||
|
||||
test('subtitle runtime schedules and cancels subtitle prefetch refreshes', async () => {
|
||||
const calls: string[] = [];
|
||||
const { runtime } = createRuntime({
|
||||
refreshSubtitlePrefetchFromActiveTrack: async () => {
|
||||
calls.push('refresh');
|
||||
},
|
||||
});
|
||||
|
||||
runtime.scheduleSubtitlePrefetchRefresh(5);
|
||||
runtime.clearScheduledSubtitlePrefetchRefresh();
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('subtitle runtime times out remote subtitle source fetches', async () => {
|
||||
const { runtime } = createRuntime({
|
||||
fetchImpl: async (_url, init) => {
|
||||
await new Promise<void>((_resolve, reject) => {
|
||||
init?.signal?.addEventListener('abort', () => reject(new Error('aborted')), { once: true });
|
||||
});
|
||||
return new Response('');
|
||||
},
|
||||
subtitleSourceFetchTimeoutMs: 10,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
async () => await runtime.loadSubtitleSourceText('https://example.com/subtitles.srt'),
|
||||
/aborted/,
|
||||
);
|
||||
});
|
||||
|
||||
test('subtitle runtime reuses cached sidebar cues for the same source key', async () => {
|
||||
const subtitlePath = path.join(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-subtitle-cache-')),
|
||||
'episode.ass',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
subtitlePath,
|
||||
`1
|
||||
00:00:01,000 --> 00:00:02,000
|
||||
Hello`,
|
||||
);
|
||||
|
||||
let loadCount = 0;
|
||||
const { runtime } = createRuntime({
|
||||
getMpvClient: () =>
|
||||
createMpvClient({
|
||||
'current-tracks/sub/external-filename': subtitlePath,
|
||||
'current-tracks/sub': {
|
||||
type: 'sub',
|
||||
id: 3,
|
||||
external: true,
|
||||
'external-filename': subtitlePath,
|
||||
},
|
||||
'track-list': [
|
||||
{
|
||||
type: 'sub',
|
||||
id: 3,
|
||||
external: true,
|
||||
'external-filename': subtitlePath,
|
||||
},
|
||||
],
|
||||
sid: 3,
|
||||
path: '/media/episode.mkv',
|
||||
}),
|
||||
loadSubtitleSourceText: async () => {
|
||||
loadCount += 1;
|
||||
return fs.readFileSync(subtitlePath, 'utf8');
|
||||
},
|
||||
});
|
||||
|
||||
const first = await runtime.getSubtitleSidebarSnapshot();
|
||||
const second = await runtime.getSubtitleSidebarSnapshot();
|
||||
|
||||
assert.equal(loadCount, 1);
|
||||
assert.deepEqual(second.cues, first.cues);
|
||||
assert.equal(second.currentSubtitle.text, 'current subtitle');
|
||||
});
|
||||
Reference in New Issue
Block a user