mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-30 18:12:08 -07:00
refactor: migrate shared type imports
This commit is contained in:
@@ -185,11 +185,7 @@ test('runAppReadyRuntime uses minimal startup for texthooker-only mode', async (
|
||||
|
||||
await runAppReadyRuntime(deps);
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'ensureDefaultConfigBootstrap',
|
||||
'reloadConfig',
|
||||
'handleInitialArgs',
|
||||
]);
|
||||
assert.deepEqual(calls, ['ensureDefaultConfigBootstrap', 'reloadConfig', 'handleInitialArgs']);
|
||||
});
|
||||
|
||||
test('runAppReadyRuntime skips Jellyfin remote startup when dependency is not wired', async () => {
|
||||
|
||||
@@ -58,7 +58,12 @@ function classifyDiff(prev: ResolvedConfig, next: ResolvedConfig): ConfigHotRelo
|
||||
]);
|
||||
|
||||
for (const key of keys) {
|
||||
if (key === 'subtitleStyle' || key === 'keybindings' || key === 'shortcuts' || key === 'subtitleSidebar') {
|
||||
if (
|
||||
key === 'subtitleStyle' ||
|
||||
key === 'keybindings' ||
|
||||
key === 'shortcuts' ||
|
||||
key === 'subtitleSidebar'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,10 +79,7 @@ export {
|
||||
handleOverlayWindowBeforeInputEvent,
|
||||
isTabInputForMpvForwarding,
|
||||
} from './overlay-window-input';
|
||||
export {
|
||||
initializeOverlayAnkiIntegration,
|
||||
initializeOverlayRuntime,
|
||||
} from './overlay-runtime-init';
|
||||
export { initializeOverlayAnkiIntegration, initializeOverlayRuntime } from './overlay-runtime-init';
|
||||
export { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility';
|
||||
export {
|
||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||
|
||||
@@ -70,7 +70,11 @@ function createControllerConfigFixture() {
|
||||
nextAudio: { kind: 'button' as const, buttonIndex: 5 },
|
||||
playCurrentAudio: { kind: 'button' as const, buttonIndex: 7 },
|
||||
toggleMpvPause: { kind: 'button' as const, buttonIndex: 6 },
|
||||
leftStickHorizontal: { kind: 'axis' as const, axisIndex: 0, dpadFallback: 'horizontal' as const },
|
||||
leftStickHorizontal: {
|
||||
kind: 'axis' as const,
|
||||
axisIndex: 0,
|
||||
dpadFallback: 'horizontal' as const,
|
||||
},
|
||||
leftStickVertical: { kind: 'axis' as const, axisIndex: 1, dpadFallback: 'vertical' as const },
|
||||
rightStickHorizontal: { kind: 'axis' as const, axisIndex: 3, dpadFallback: 'none' as const },
|
||||
rightStickVertical: { kind: 'axis' as const, axisIndex: 4, dpadFallback: 'none' as const },
|
||||
|
||||
@@ -64,7 +64,9 @@ export interface IpcServiceDeps {
|
||||
getCurrentSecondarySub: () => string;
|
||||
focusMainWindow: () => void;
|
||||
runSubsyncManual: (request: SubsyncManualRunRequest) => Promise<SubsyncResult>;
|
||||
onYoutubePickerResolve: (request: YoutubePickerResolveRequest) => Promise<YoutubePickerResolveResult>;
|
||||
onYoutubePickerResolve: (
|
||||
request: YoutubePickerResolveRequest,
|
||||
) => Promise<YoutubePickerResolveResult>;
|
||||
getAnkiConnectStatus: () => boolean;
|
||||
getRuntimeOptions: () => unknown;
|
||||
setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown;
|
||||
@@ -167,7 +169,9 @@ export interface IpcDepsRuntimeOptions {
|
||||
getMpvClient: () => MpvClientLike | null;
|
||||
focusMainWindow: () => void;
|
||||
runSubsyncManual: (request: SubsyncManualRunRequest) => Promise<SubsyncResult>;
|
||||
onYoutubePickerResolve: (request: YoutubePickerResolveRequest) => Promise<YoutubePickerResolveResult>;
|
||||
onYoutubePickerResolve: (
|
||||
request: YoutubePickerResolveRequest,
|
||||
) => Promise<YoutubePickerResolveResult>;
|
||||
getAnkiConnectStatus: () => boolean;
|
||||
getRuntimeOptions: () => unknown;
|
||||
setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown;
|
||||
@@ -291,13 +295,16 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
|
||||
deps.onOverlayModalOpened(parsedModal);
|
||||
});
|
||||
|
||||
ipc.handle(IPC_CHANNELS.request.youtubePickerResolve, async (_event: unknown, request: unknown) => {
|
||||
const parsedRequest = parseYoutubePickerResolveRequest(request);
|
||||
if (!parsedRequest) {
|
||||
return { ok: false, message: 'Invalid YouTube picker resolve payload' };
|
||||
}
|
||||
return await deps.onYoutubePickerResolve(parsedRequest);
|
||||
});
|
||||
ipc.handle(
|
||||
IPC_CHANNELS.request.youtubePickerResolve,
|
||||
async (_event: unknown, request: unknown) => {
|
||||
const parsedRequest = parseYoutubePickerResolveRequest(request);
|
||||
if (!parsedRequest) {
|
||||
return { ok: false, message: 'Invalid YouTube picker resolve payload' };
|
||||
}
|
||||
return await deps.onYoutubePickerResolve(parsedRequest);
|
||||
},
|
||||
);
|
||||
|
||||
ipc.on(IPC_CHANNELS.command.openYomitanSettings, () => {
|
||||
deps.openYomitanSettings();
|
||||
@@ -375,13 +382,16 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
|
||||
},
|
||||
);
|
||||
|
||||
ipc.handle(IPC_CHANNELS.command.saveControllerConfig, async (_event: unknown, update: unknown) => {
|
||||
const parsedUpdate = parseControllerConfigUpdate(update);
|
||||
if (!parsedUpdate) {
|
||||
throw new Error('Invalid controller config payload');
|
||||
}
|
||||
await deps.saveControllerConfig(parsedUpdate);
|
||||
});
|
||||
ipc.handle(
|
||||
IPC_CHANNELS.command.saveControllerConfig,
|
||||
async (_event: unknown, update: unknown) => {
|
||||
const parsedUpdate = parseControllerConfigUpdate(update);
|
||||
if (!parsedUpdate) {
|
||||
throw new Error('Invalid controller config payload');
|
||||
}
|
||||
await deps.saveControllerConfig(parsedUpdate);
|
||||
},
|
||||
);
|
||||
|
||||
ipc.handle(IPC_CHANNELS.request.getMecabStatus, () => {
|
||||
return deps.getMecabStatus();
|
||||
|
||||
@@ -228,7 +228,11 @@ test('consumeCachedSubtitle returns prefetched payload and prevents reprocessing
|
||||
controller.onSubtitleChange('猫\nです');
|
||||
await flushMicrotasks();
|
||||
|
||||
assert.equal(tokenizeCalls, 0, 'same cached subtitle should not reprocess after immediate consume');
|
||||
assert.equal(
|
||||
tokenizeCalls,
|
||||
0,
|
||||
'same cached subtitle should not reprocess after immediate consume',
|
||||
);
|
||||
assert.deepEqual(emitted, []);
|
||||
});
|
||||
|
||||
|
||||
@@ -3428,40 +3428,43 @@ test('tokenizeSubtitle keeps standalone grammar-only tokens hoverable while clea
|
||||
test('tokenizeSubtitle keeps trailing quote-particle merged tokens hoverable while clearing only their annotation metadata', async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
'どうしてもって',
|
||||
makeDepsFromYomitanTokens([{ surface: 'どうしてもって', reading: 'どうしてもって', headword: 'どうしても' }], {
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
getFrequencyRank: (text) => (text === 'どうしても' ? 123 : null),
|
||||
getJlptLevel: (text) => (text === 'どうしても' ? 'N3' : null),
|
||||
tokenizeWithMecab: async () => [
|
||||
{
|
||||
headword: 'どうしても',
|
||||
surface: 'どうしても',
|
||||
reading: 'ドウシテモ',
|
||||
startPos: 0,
|
||||
endPos: 5,
|
||||
partOfSpeech: PartOfSpeech.other,
|
||||
pos1: '副詞',
|
||||
pos2: '一般',
|
||||
isMerged: false,
|
||||
isKnown: false,
|
||||
isNPlusOneTarget: false,
|
||||
},
|
||||
{
|
||||
headword: 'って',
|
||||
surface: 'って',
|
||||
reading: 'ッテ',
|
||||
startPos: 5,
|
||||
endPos: 7,
|
||||
partOfSpeech: PartOfSpeech.particle,
|
||||
pos1: '助詞',
|
||||
pos2: '格助詞',
|
||||
isMerged: false,
|
||||
isKnown: false,
|
||||
isNPlusOneTarget: false,
|
||||
},
|
||||
],
|
||||
getMinSentenceWordsForNPlusOne: () => 1,
|
||||
}),
|
||||
makeDepsFromYomitanTokens(
|
||||
[{ surface: 'どうしてもって', reading: 'どうしてもって', headword: 'どうしても' }],
|
||||
{
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
getFrequencyRank: (text) => (text === 'どうしても' ? 123 : null),
|
||||
getJlptLevel: (text) => (text === 'どうしても' ? 'N3' : null),
|
||||
tokenizeWithMecab: async () => [
|
||||
{
|
||||
headword: 'どうしても',
|
||||
surface: 'どうしても',
|
||||
reading: 'ドウシテモ',
|
||||
startPos: 0,
|
||||
endPos: 5,
|
||||
partOfSpeech: PartOfSpeech.other,
|
||||
pos1: '副詞',
|
||||
pos2: '一般',
|
||||
isMerged: false,
|
||||
isKnown: false,
|
||||
isNPlusOneTarget: false,
|
||||
},
|
||||
{
|
||||
headword: 'って',
|
||||
surface: 'って',
|
||||
reading: 'ッテ',
|
||||
startPos: 5,
|
||||
endPos: 7,
|
||||
partOfSpeech: PartOfSpeech.particle,
|
||||
pos1: '助詞',
|
||||
pos2: '格助詞',
|
||||
isMerged: false,
|
||||
isKnown: false,
|
||||
isNPlusOneTarget: false,
|
||||
},
|
||||
],
|
||||
getMinSentenceWordsForNPlusOne: () => 1,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
assert.equal(result.text, 'どうしてもって');
|
||||
@@ -3812,7 +3815,14 @@ test('tokenizeSubtitle clears all annotations for explanatory pondering endings'
|
||||
jlptLevel: token.jlptLevel,
|
||||
})),
|
||||
[
|
||||
{ surface: '俺', headword: '俺', isKnown: true, isNPlusOneTarget: false, frequencyRank: 19, jlptLevel: 'N5' },
|
||||
{
|
||||
surface: '俺',
|
||||
headword: '俺',
|
||||
isKnown: true,
|
||||
isNPlusOneTarget: false,
|
||||
frequencyRank: 19,
|
||||
jlptLevel: 'N5',
|
||||
},
|
||||
{
|
||||
surface: 'どうかしちゃった',
|
||||
headword: 'どうかしちゃう',
|
||||
|
||||
@@ -140,7 +140,11 @@ function isExcludedFromSubtitleAnnotationsByPos1(normalizedPos1: string): boolea
|
||||
function isExcludedTrailingParticleMergedToken(token: MergedToken): boolean {
|
||||
const normalizedSurface = normalizeJlptTextForExclusion(token.surface);
|
||||
const normalizedHeadword = normalizeJlptTextForExclusion(token.headword);
|
||||
if (!normalizedSurface || !normalizedHeadword || !normalizedSurface.startsWith(normalizedHeadword)) {
|
||||
if (
|
||||
!normalizedSurface ||
|
||||
!normalizedHeadword ||
|
||||
!normalizedSurface.startsWith(normalizedHeadword)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -164,7 +168,10 @@ function isExcludedTrailingParticleMergedToken(token: MergedToken): boolean {
|
||||
|
||||
function isAuxiliaryStemGrammarTailToken(token: MergedToken): boolean {
|
||||
const pos1Parts = splitNormalizedTagParts(normalizePos1Tag(token.pos1));
|
||||
if (pos1Parts.length === 0 || !pos1Parts.every((part) => AUXILIARY_STEM_GRAMMAR_TAIL_POS1.has(part))) {
|
||||
if (
|
||||
pos1Parts.length === 0 ||
|
||||
!pos1Parts.every((part) => AUXILIARY_STEM_GRAMMAR_TAIL_POS1.has(part))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,11 @@ const SUBTITLE_ANNOTATION_EXCLUDED_EXPLANATORY_ENDING_TRAILING_PARTICLES = [
|
||||
'かな',
|
||||
'かね',
|
||||
] as const;
|
||||
const SUBTITLE_ANNOTATION_EXCLUDED_EXPLANATORY_ENDING_THOUGHT_SUFFIXES = ['か', 'かな', 'かね'] as const;
|
||||
const SUBTITLE_ANNOTATION_EXCLUDED_EXPLANATORY_ENDING_THOUGHT_SUFFIXES = [
|
||||
'か',
|
||||
'かな',
|
||||
'かね',
|
||||
] as const;
|
||||
const SUBTITLE_ANNOTATION_EXCLUDED_EXPLANATORY_ENDINGS = new Set(
|
||||
SUBTITLE_ANNOTATION_EXCLUDED_EXPLANATORY_ENDING_PREFIXES.flatMap((prefix) =>
|
||||
SUBTITLE_ANNOTATION_EXCLUDED_EXPLANATORY_ENDING_CORES.flatMap((core) =>
|
||||
@@ -96,9 +100,7 @@ function isExcludedByTagSet(normalizedTag: string, exclusions: ReadonlySet<strin
|
||||
return parts.every((part) => exclusions.has(part));
|
||||
}
|
||||
|
||||
function resolvePos1Exclusions(
|
||||
options: SubtitleAnnotationFilterOptions = {},
|
||||
): ReadonlySet<string> {
|
||||
function resolvePos1Exclusions(options: SubtitleAnnotationFilterOptions = {}): ReadonlySet<string> {
|
||||
if (options.pos1Exclusions) {
|
||||
return options.pos1Exclusions;
|
||||
}
|
||||
@@ -106,9 +108,7 @@ function resolvePos1Exclusions(
|
||||
return resolveAnnotationPos1ExclusionSet(DEFAULT_ANNOTATION_POS1_EXCLUSION_CONFIG);
|
||||
}
|
||||
|
||||
function resolvePos2Exclusions(
|
||||
options: SubtitleAnnotationFilterOptions = {},
|
||||
): ReadonlySet<string> {
|
||||
function resolvePos2Exclusions(options: SubtitleAnnotationFilterOptions = {}): ReadonlySet<string> {
|
||||
if (options.pos2Exclusions) {
|
||||
return options.pos2Exclusions;
|
||||
}
|
||||
@@ -212,7 +212,11 @@ function isReduplicatedKanaSfxWithOptionalTrailingTo(text: string): boolean {
|
||||
function isExcludedTrailingParticleMergedToken(token: MergedToken): boolean {
|
||||
const normalizedSurface = normalizeKana(token.surface);
|
||||
const normalizedHeadword = normalizeKana(token.headword);
|
||||
if (!normalizedSurface || !normalizedHeadword || !normalizedSurface.startsWith(normalizedHeadword)) {
|
||||
if (
|
||||
!normalizedSurface ||
|
||||
!normalizedHeadword ||
|
||||
!normalizedSurface.startsWith(normalizedHeadword)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -236,7 +240,10 @@ function isExcludedTrailingParticleMergedToken(token: MergedToken): boolean {
|
||||
|
||||
function isAuxiliaryStemGrammarTailToken(token: MergedToken): boolean {
|
||||
const pos1Parts = splitNormalizedTagParts(normalizePosTag(token.pos1));
|
||||
if (pos1Parts.length === 0 || !pos1Parts.every((part) => AUXILIARY_STEM_GRAMMAR_TAIL_POS1.has(part))) {
|
||||
if (
|
||||
pos1Parts.length === 0 ||
|
||||
!pos1Parts.every((part) => AUXILIARY_STEM_GRAMMAR_TAIL_POS1.has(part))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@ import type { YoutubeTrackKind } from './kinds';
|
||||
export type { YoutubeTrackKind };
|
||||
|
||||
export function normalizeYoutubeLangCode(value: string): string {
|
||||
return value.trim().toLowerCase().replace(/_/g, '-').replace(/[^a-z0-9-]+/g, '');
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/_/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '');
|
||||
}
|
||||
|
||||
export function isJapaneseYoutubeLang(value: string): boolean {
|
||||
|
||||
@@ -75,15 +75,11 @@ test('probeYoutubeVideoMetadata returns null on malformed yt-dlp JSON', async ()
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'probeYoutubeVideoMetadata times out when yt-dlp hangs',
|
||||
{ timeout: 20_000 },
|
||||
async () => {
|
||||
await withHangingFakeYtDlp(async () => {
|
||||
await assert.rejects(
|
||||
probeYoutubeVideoMetadata('https://www.youtube.com/watch?v=abc123'),
|
||||
/timed out after 15000ms/,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
test('probeYoutubeVideoMetadata times out when yt-dlp hangs', { timeout: 20_000 }, async () => {
|
||||
await withHangingFakeYtDlp(async () => {
|
||||
await assert.rejects(
|
||||
probeYoutubeVideoMetadata('https://www.youtube.com/watch?v=abc123'),
|
||||
/timed out after 15000ms/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,9 +25,7 @@ function decodeHtmlEntities(value: string): string {
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&#(\d+);/g, (match, codePoint) =>
|
||||
decodeNumericEntity(match, Number(codePoint)),
|
||||
)
|
||||
.replace(/&#(\d+);/g, (match, codePoint) => decodeNumericEntity(match, Number(codePoint)))
|
||||
.replace(/&#x([0-9a-f]+);/gi, (match, codePoint) =>
|
||||
decodeNumericEntity(match, Number.parseInt(codePoint, 16)),
|
||||
);
|
||||
@@ -52,9 +50,7 @@ function extractYoutubeTimedTextRows(xml: string): YoutubeTimedTextRow[] {
|
||||
continue;
|
||||
}
|
||||
|
||||
const inner = (match[2] ?? '')
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<[^>]+>/g, '');
|
||||
const inner = (match[2] ?? '').replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '');
|
||||
const text = decodeHtmlEntities(inner).trim();
|
||||
if (!text) {
|
||||
continue;
|
||||
@@ -110,7 +106,9 @@ export function convertYoutubeTimedTextToVtt(xml: string): string {
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
blocks.push(`${formatVttTimestamp(row.startMs)} --> ${formatVttTimestamp(clampedEnd)}\n${text}`);
|
||||
blocks.push(
|
||||
`${formatVttTimestamp(row.startMs)} --> ${formatVttTimestamp(clampedEnd)}\n${text}`,
|
||||
);
|
||||
}
|
||||
|
||||
return `WEBVTT\n\n${blocks.join('\n\n')}\n`;
|
||||
|
||||
@@ -16,7 +16,7 @@ async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
||||
|
||||
function makeFakeYtDlpScript(dir: string): string {
|
||||
const scriptPath = path.join(dir, 'yt-dlp');
|
||||
const script = `#!/usr/bin/env node
|
||||
const script = `#!/usr/bin/env node
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
@@ -115,7 +115,9 @@ async function withFakeYtDlp<T>(
|
||||
}
|
||||
|
||||
async function withFakeYtDlpExpectations<T>(
|
||||
expectations: Partial<Record<'YTDLP_EXPECT_AUTO_SUBS' | 'YTDLP_EXPECT_MANUAL_SUBS' | 'YTDLP_EXPECT_SUB_LANG', string>>,
|
||||
expectations: Partial<
|
||||
Record<'YTDLP_EXPECT_AUTO_SUBS' | 'YTDLP_EXPECT_MANUAL_SUBS' | 'YTDLP_EXPECT_SUB_LANG', string>
|
||||
>,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const previous = {
|
||||
@@ -144,11 +146,7 @@ async function withStubFetch<T>(
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input: string | URL | Request) => {
|
||||
const url =
|
||||
typeof input === 'string'
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.toString()
|
||||
: input.url;
|
||||
typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
||||
return await handler(url);
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,10 @@ const YOUTUBE_BATCH_PREFIX = 'youtube-batch';
|
||||
const YOUTUBE_DOWNLOAD_TIMEOUT_MS = 15_000;
|
||||
|
||||
function sanitizeFilenameSegment(value: string): string {
|
||||
const sanitized = value.trim().replace(/[^a-z0-9_-]+/gi, '-').replace(/-+/g, '-');
|
||||
const sanitized = value
|
||||
.trim()
|
||||
.replace(/[^a-z0-9_-]+/gi, '-')
|
||||
.replace(/-+/g, '-');
|
||||
return sanitized.replace(/^-+|-+$/g, '') || 'unknown';
|
||||
}
|
||||
|
||||
@@ -163,10 +166,7 @@ async function downloadSubtitleFromUrl(input: {
|
||||
? ext
|
||||
: 'vtt';
|
||||
const safeSourceLanguage = sanitizeFilenameSegment(input.track.sourceLanguage);
|
||||
const targetPath = path.join(
|
||||
input.outputDir,
|
||||
`${input.prefix}.${safeSourceLanguage}.${safeExt}`,
|
||||
);
|
||||
const targetPath = path.join(input.outputDir, `${input.prefix}.${safeSourceLanguage}.${safeExt}`);
|
||||
const response = await fetch(input.track.downloadUrl, {
|
||||
signal: createFetchTimeoutSignal(YOUTUBE_DOWNLOAD_TIMEOUT_MS),
|
||||
});
|
||||
|
||||
@@ -127,7 +127,10 @@ export async function probeYoutubeTracks(targetUrl: string): Promise<YoutubeTrac
|
||||
}${snippet ? `; stdout=${snippet}` : ''}`,
|
||||
);
|
||||
}
|
||||
const tracks = [...toTracks(info.subtitles, 'manual'), ...toTracks(info.automatic_captions, 'auto')];
|
||||
const tracks = [
|
||||
...toTracks(info.subtitles, 'manual'),
|
||||
...toTracks(info.automatic_captions, 'auto'),
|
||||
];
|
||||
return {
|
||||
videoId: info.id || '',
|
||||
title: info.title || '',
|
||||
|
||||
@@ -10,9 +10,10 @@ function pickTrack(
|
||||
return matching[0] ?? null;
|
||||
}
|
||||
|
||||
export function chooseDefaultYoutubeTrackIds(
|
||||
tracks: YoutubeTrackOption[],
|
||||
): { primaryTrackId: string | null; secondaryTrackId: string | null } {
|
||||
export function chooseDefaultYoutubeTrackIds(tracks: YoutubeTrackOption[]): {
|
||||
primaryTrackId: string | null;
|
||||
secondaryTrackId: string | null;
|
||||
} {
|
||||
const primary =
|
||||
pickTrack(
|
||||
tracks.filter((track) => track.kind === 'manual'),
|
||||
@@ -52,7 +53,11 @@ export function normalizeYoutubeTrackSelection(input: {
|
||||
primaryTrackId: string | null;
|
||||
secondaryTrackId: string | null;
|
||||
} {
|
||||
if (input.primaryTrackId && input.secondaryTrackId && input.primaryTrackId === input.secondaryTrackId) {
|
||||
if (
|
||||
input.primaryTrackId &&
|
||||
input.secondaryTrackId &&
|
||||
input.primaryTrackId === input.secondaryTrackId
|
||||
) {
|
||||
return {
|
||||
primaryTrackId: input.primaryTrackId,
|
||||
secondaryTrackId: null,
|
||||
@@ -60,4 +65,3 @@ export function normalizeYoutubeTrackSelection(input: {
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user