build: expand typecheck coverage beyond src

This commit is contained in:
2026-03-06 01:03:51 -08:00
parent 8c2c950564
commit ebe9515486
13 changed files with 156 additions and 44 deletions

View File

@@ -166,8 +166,10 @@ test('buildSubminerScriptOpts includes aniskip payload fields', () => {
assert.match(opts, /subminer-aniskip_intro_end=62/);
assert.match(opts, /subminer-aniskip_lookup_status=ready/);
assert.ok(payloadMatch !== null);
assert.equal(payloadMatch[1].includes('%'), false);
const payloadJson = Buffer.from(payloadMatch[1], 'base64url').toString('utf-8');
const encodedPayload = payloadMatch[1];
assert.ok(encodedPayload !== undefined);
assert.equal(encodedPayload.includes('%'), false);
const payloadJson = Buffer.from(encodedPayload, 'base64url').toString('utf-8');
const payload = JSON.parse(payloadJson);
assert.equal(payload.found, true);
const first = payload.results?.[0];

View File

@@ -53,6 +53,13 @@ interface AniSkipPayloadResponse {
results?: unknown;
}
const ROMAN_SEASON_ALIASES: Record<number, readonly string[]> = {
2: [' ii ', ' second season ', ' 2nd season '],
3: [' iii ', ' third season ', ' 3rd season '],
4: [' iv ', ' fourth season ', ' 4th season '],
5: [' v ', ' fifth season ', ' 5th season '],
};
const MAL_PREFIX_API = 'https://myanimelist.net/search/prefix.json?type=anime&keyword=';
const ANISKIP_PAYLOAD_API = 'https://api.aniskip.com/v1/skip-times/';
const MAL_USER_AGENT = 'SubMiner-launcher/ani-skip';
@@ -188,14 +195,7 @@ function seasonSignalScore(requestedSeason: number | null, candidateTitle: strin
return 40;
}
const romanAliases = {
2: [' ii ', ' second season ', ' 2nd season '],
3: [' iii ', ' third season ', ' 3rd season '],
4: [' iv ', ' fourth season ', ' 4th season '],
5: [' v ', ' fifth season ', ' 5th season '],
} as const;
const aliases = romanAliases[season] ?? [];
const aliases = ROMAN_SEASON_ALIASES[season] ?? [];
return aliases.some((alias) => normalized.includes(alias))
? 40
: hasAnySequelMarker(candidateTitle)

View File

@@ -284,8 +284,10 @@ export function parseEpisodePathFromDisplay(
const normalized = display.trim().replace(/\s+/g, ' ');
const match = normalized.match(/^(.*?)\s+S(\d{1,2})E\d{1,3}\b/i);
if (!match) return null;
const seriesName = match[1].trim();
const seasonNumber = Number.parseInt(match[2], 10);
const seriesName = match[1]?.trim();
const seasonText = match[2];
if (!seriesName || !seasonText) return null;
const seasonNumber = Number.parseInt(seasonText, 10);
if (!seriesName || !Number.isFinite(seasonNumber) || seasonNumber < 0) return null;
return { seriesName, seasonNumber };
}

View File

@@ -59,6 +59,7 @@ function getRetryAfter(headers: http.IncomingHttpHeaders): number | undefined {
const value = headers['x-ratelimit-reset-after'];
if (!value) return undefined;
const raw = Array.isArray(value) ? value[0] : value;
if (!raw) return undefined;
const parsed = Number.parseFloat(raw);
if (!Number.isFinite(parsed)) return undefined;
return parsed;
@@ -72,9 +73,14 @@ export function matchEpisodeFromName(name: string): {
} {
const seasonEpisode = name.match(/S(\d{1,2})E(\d{1,3})/i);
if (seasonEpisode && seasonEpisode.index !== undefined) {
const seasonText = seasonEpisode[1];
const episodeText = seasonEpisode[2];
if (!seasonText || !episodeText) {
return { season: null, episode: null, index: null, confidence: 'low' };
}
return {
season: Number.parseInt(seasonEpisode[1], 10),
episode: Number.parseInt(seasonEpisode[2], 10),
season: Number.parseInt(seasonText, 10),
episode: Number.parseInt(episodeText, 10),
index: seasonEpisode.index,
confidence: 'high',
};
@@ -82,9 +88,14 @@ export function matchEpisodeFromName(name: string): {
const alt = name.match(/(\d{1,2})x(\d{1,3})/i);
if (alt && alt.index !== undefined) {
const seasonText = alt[1];
const episodeText = alt[2];
if (!seasonText || !episodeText) {
return { season: null, episode: null, index: null, confidence: 'low' };
}
return {
season: Number.parseInt(alt[1], 10),
episode: Number.parseInt(alt[2], 10),
season: Number.parseInt(seasonText, 10),
episode: Number.parseInt(episodeText, 10),
index: alt.index,
confidence: 'high',
};
@@ -92,9 +103,13 @@ export function matchEpisodeFromName(name: string): {
const epOnly = name.match(/(?:^|[\s._-])E(?:P)?(\d{1,3})(?:\b|[\s._-])/i);
if (epOnly && epOnly.index !== undefined) {
const episodeText = epOnly[1];
if (!episodeText) {
return { season: null, episode: null, index: null, confidence: 'low' };
}
return {
season: null,
episode: Number.parseInt(epOnly[1], 10),
episode: Number.parseInt(episodeText, 10),
index: epOnly.index,
confidence: 'medium',
};
@@ -102,9 +117,13 @@ export function matchEpisodeFromName(name: string): {
const numeric = name.match(/(?:^|[-–—]\s*)(\d{1,3})\s*[-–—]/);
if (numeric && numeric.index !== undefined) {
const episodeText = numeric[1];
if (!episodeText) {
return { season: null, episode: null, index: null, confidence: 'low' };
}
return {
season: null,
episode: Number.parseInt(numeric[1], 10),
episode: Number.parseInt(episodeText, 10),
index: numeric.index,
confidence: 'medium',
};
@@ -117,7 +136,9 @@ function detectSeasonFromDir(mediaPath: string): number | null {
const parent = path.basename(path.dirname(mediaPath));
const match = parent.match(/(?:Season|S)\s*(\d{1,2})/i);
if (!match) return null;
const parsed = Number.parseInt(match[1], 10);
const seasonText = match[1];
if (!seasonText) return null;
const parsed = Number.parseInt(seasonText, 10);
return Number.isFinite(parsed) ? parsed : null;
}

View File

@@ -427,7 +427,7 @@ export async function startMpv(
appPath: string,
preloadedSubtitles?: { primaryPath?: string; secondaryPath?: string },
options?: { startPaused?: boolean },
): void {
): Promise<void> {
if (targetKind === 'file' && (!fs.existsSync(target) || !fs.statSync(target).isFile())) {
fail(`Video file not found: ${target}`);
}

View File

@@ -207,7 +207,8 @@ export function pickLibrary(
iconPath: ensureIcon(session, lib.id) || undefined,
}));
const idx = showRofiIconMenu(entries, 'Jellyfin Library', initialQuery, themePath);
return idx >= 0 ? visibleLibraries[idx].id : '';
const selected = idx >= 0 ? visibleLibraries[idx] : undefined;
return selected?.id ?? '';
}
const lines = visibleLibraries.map((lib) => `${lib.id}\t${lib.name} [${lib.kind}]`);
@@ -244,7 +245,8 @@ export function pickItem(
iconPath: ensureIcon(session, item.id) || undefined,
}));
const idx = showRofiIconMenu(entries, 'Jellyfin Item', initialQuery, themePath);
return idx >= 0 ? visibleItems[idx].id : '';
const selected = idx >= 0 ? visibleItems[idx] : undefined;
return selected?.id ?? '';
}
const lines = visibleItems.map((item) => `${item.id}\t${item.display}`);
@@ -281,7 +283,8 @@ export function pickGroup(
iconPath: ensureIcon(session, group.id) || undefined,
}));
const idx = showRofiIconMenu(entries, 'Jellyfin Anime/Folder', initialQuery, themePath);
return idx >= 0 ? visibleGroups[idx].id : '';
const selected = idx >= 0 ? visibleGroups[idx] : undefined;
return selected?.id ?? '';
}
const lines = visibleGroups.map((group) => `${group.id}\t${group.display}`);

View File

@@ -58,7 +58,7 @@ function pickBestCandidate(candidates: SubtitleCandidate[]): SubtitleCandidate |
if (srtA !== srtB) return srtB - srtA;
return b.size - a.size;
});
return scored[0];
return scored[0] ?? null;
}
function scanSubtitleCandidates(
@@ -120,7 +120,7 @@ function findAudioFile(tempDir: string, preferredExt: string): string | null {
const preferred = audioFiles.find((entry) => entry.ext === `.${preferredExt.toLowerCase()}`);
if (preferred) return preferred.path;
audioFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
return audioFiles[0].path;
return audioFiles[0]?.path ?? null;
}
async function runWhisper(