mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
build(ts): enable noUncheckedIndexedAccess and isolatedModules
This commit is contained in:
@@ -168,7 +168,7 @@ function pickBestSearchResult(
|
||||
return titles.includes(normalizedTarget);
|
||||
});
|
||||
|
||||
const selected = exact ?? candidates[0];
|
||||
const selected = exact ?? candidates[0]!;
|
||||
const selectedTitle =
|
||||
selected.title?.english || selected.title?.romaji || selected.title?.native || title;
|
||||
return { id: selected.id, title: selectedTitle };
|
||||
|
||||
@@ -129,7 +129,7 @@ test('refreshKnownWords throws when integration is unavailable', async () => {
|
||||
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await registered.refreshKnownWords();
|
||||
await registered.refreshKnownWords!();
|
||||
},
|
||||
{ message: 'AnkiConnect integration not enabled' },
|
||||
);
|
||||
@@ -144,7 +144,7 @@ test('refreshKnownWords delegates to integration', async () => {
|
||||
},
|
||||
};
|
||||
|
||||
await registered.refreshKnownWords();
|
||||
await registered.refreshKnownWords!();
|
||||
|
||||
assert.equal(refreshed, 1);
|
||||
});
|
||||
@@ -158,7 +158,7 @@ test('setAnkiConnectEnabled disables active integration and broadcasts changes',
|
||||
},
|
||||
};
|
||||
|
||||
registered.setAnkiConnectEnabled(false);
|
||||
registered.setAnkiConnectEnabled!(false);
|
||||
|
||||
assert.deepEqual(state.patches, [false]);
|
||||
assert.equal(destroyed, 1);
|
||||
@@ -188,8 +188,8 @@ test('clearAnkiHistory and respondFieldGrouping execute runtime callbacks', () =
|
||||
deleteDuplicate: true,
|
||||
cancelled: false,
|
||||
};
|
||||
registered.clearAnkiHistory();
|
||||
registered.respondFieldGrouping(choice);
|
||||
registered.clearAnkiHistory!();
|
||||
registered.respondFieldGrouping!(choice);
|
||||
|
||||
options.getSubtitleTimingTracker = originalGetTracker;
|
||||
|
||||
@@ -201,7 +201,7 @@ test('clearAnkiHistory and respondFieldGrouping execute runtime callbacks', () =
|
||||
test('buildKikuMergePreview returns guard error when integration is missing', async () => {
|
||||
const { registered } = createHarness();
|
||||
|
||||
const result = await registered.buildKikuMergePreview({
|
||||
const result = await registered.buildKikuMergePreview!({
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
deleteDuplicate: false,
|
||||
@@ -227,7 +227,7 @@ test('buildKikuMergePreview delegates to integration when available', async () =
|
||||
},
|
||||
};
|
||||
|
||||
const result = await registered.buildKikuMergePreview({
|
||||
const result = await registered.buildKikuMergePreview!({
|
||||
keepNoteId: 3,
|
||||
deleteNoteId: 4,
|
||||
deleteDuplicate: true,
|
||||
@@ -240,7 +240,7 @@ test('buildKikuMergePreview delegates to integration when available', async () =
|
||||
test('searchJimakuEntries caps results and onDownloadedSubtitle sends sub-add to mpv', async () => {
|
||||
const { registered, state } = createHarness();
|
||||
|
||||
const searchResult = await registered.searchJimakuEntries({ query: 'test' });
|
||||
const searchResult = await registered.searchJimakuEntries!({ query: 'test' });
|
||||
assert.deepEqual(state.fetchCalls, [
|
||||
{
|
||||
endpoint: '/api/entries/search',
|
||||
@@ -250,6 +250,6 @@ test('searchJimakuEntries caps results and onDownloadedSubtitle sends sub-add to
|
||||
assert.equal((searchResult as { ok: boolean }).ok, true);
|
||||
assert.equal((searchResult as { data: unknown[] }).data.length, 2);
|
||||
|
||||
registered.onDownloadedSubtitle('/tmp/subtitle.ass');
|
||||
registered.onDownloadedSubtitle!('/tmp/subtitle.ass');
|
||||
assert.deepEqual(state.sentCommands, [{ command: ['sub-add', '/tmp/subtitle.ass', 'select'] }]);
|
||||
});
|
||||
|
||||
@@ -209,30 +209,31 @@ test('runAppReadyRuntime aggregates multiple critical anki mapping errors', asyn
|
||||
|
||||
await runAppReadyRuntime(deps);
|
||||
|
||||
const firstErrorSet = capturedErrors[0]!;
|
||||
assert.equal(capturedErrors.length, 1);
|
||||
assert.equal(capturedErrors[0].length, 5);
|
||||
assert.equal(firstErrorSet.length, 5);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
firstErrorSet.includes(
|
||||
'ankiConnect.fields.audio must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
firstErrorSet.includes(
|
||||
'ankiConnect.fields.image must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
firstErrorSet.includes(
|
||||
'ankiConnect.fields.sentence must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
firstErrorSet.includes(
|
||||
'ankiConnect.fields.miscInfo must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
firstErrorSet.includes(
|
||||
'ankiConnect.fields.translation must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -124,8 +124,8 @@ testIfSqlite('persists and retrieves minimum immersion tracking fields', async (
|
||||
|
||||
const summaries = await tracker.getSessionSummaries(10);
|
||||
assert.ok(summaries.length >= 1);
|
||||
assert.ok(summaries[0].linesSeen >= 1);
|
||||
assert.ok(summaries[0].cardsMined >= 2);
|
||||
assert.ok(summaries[0]!.linesSeen >= 1);
|
||||
assert.ok(summaries[0]!.cardsMined >= 2);
|
||||
|
||||
tracker.destroy();
|
||||
|
||||
@@ -376,8 +376,8 @@ testIfSqlite('monthly rollups are grouped by calendar month', async () => {
|
||||
const videoRows = rows.filter((row) => row.videoId === 1);
|
||||
|
||||
assert.equal(videoRows.length, 2);
|
||||
assert.equal(videoRows[0].rollupDayOrMonth, 202602);
|
||||
assert.equal(videoRows[1].rollupDayOrMonth, 202601);
|
||||
assert.equal(videoRows[0]!.rollupDayOrMonth, 202602);
|
||||
assert.equal(videoRows[1]!.rollupDayOrMonth, 202601);
|
||||
} finally {
|
||||
tracker?.destroy();
|
||||
cleanupDbPath(dbPath);
|
||||
|
||||
@@ -1443,7 +1443,7 @@ export class ImmersionTrackerService {
|
||||
const parsed = new URL(mediaPath);
|
||||
const parts = parsed.pathname.split('/').filter(Boolean);
|
||||
if (parts.length > 0) {
|
||||
const leaf = decodeURIComponent(parts[parts.length - 1]);
|
||||
const leaf = decodeURIComponent(parts[parts.length - 1]!);
|
||||
return this.normalizeText(leaf.replace(/\.[^/.]+$/, ''));
|
||||
}
|
||||
return this.normalizeText(parsed.hostname) || 'unknown';
|
||||
|
||||
@@ -35,7 +35,6 @@ export { createFrequencyDictionaryLookup } from './frequency-dictionary';
|
||||
export { createJlptVocabularyLookup } from './jlpt-vocab';
|
||||
export {
|
||||
getIgnoredPos1Entries,
|
||||
JlptIgnoredPos1Entry,
|
||||
JLPT_EXCLUDED_TERMS,
|
||||
JLPT_IGNORED_MECAB_POS1,
|
||||
JLPT_IGNORED_MECAB_POS1_ENTRIES,
|
||||
@@ -43,6 +42,7 @@ export {
|
||||
shouldIgnoreJlptByTerm,
|
||||
shouldIgnoreJlptForMecabPos1,
|
||||
} from './jlpt-token-filter';
|
||||
export type { JlptIgnoredPos1Entry } from './jlpt-token-filter';
|
||||
export { loadYomitanExtension } from './yomitan-extension-loader';
|
||||
export {
|
||||
getJimakuLanguagePreference,
|
||||
@@ -72,8 +72,6 @@ export {
|
||||
export {
|
||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||
MpvIpcClient,
|
||||
MpvRuntimeClientLike,
|
||||
MpvTrackProperty,
|
||||
playNextSubtitleRuntime,
|
||||
replayCurrentSubtitleRuntime,
|
||||
resolveCurrentAudioStreamIndex,
|
||||
@@ -81,6 +79,7 @@ export {
|
||||
setMpvSubVisibilityRuntime,
|
||||
showMpvOsdRuntime,
|
||||
} from './mpv';
|
||||
export type { MpvRuntimeClientLike, MpvTrackProperty } from './mpv';
|
||||
export {
|
||||
applyMpvSubtitleRenderMetricsPatch,
|
||||
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||
|
||||
@@ -70,11 +70,11 @@ test('start posts capabilities on socket connect', async () => {
|
||||
});
|
||||
|
||||
service.start();
|
||||
sockets[0].emit('open');
|
||||
sockets[0]!.emit('open');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(fetchCalls[0].input, 'http://jellyfin.local:8096/Sessions/Capabilities/Full');
|
||||
assert.equal(fetchCalls[0]!.input, 'http://jellyfin.local:8096/Sessions/Capabilities/Full');
|
||||
assert.equal(service.isConnected(), true);
|
||||
});
|
||||
|
||||
@@ -97,9 +97,9 @@ test('socket headers include jellyfin authorization metadata', () => {
|
||||
|
||||
service.start();
|
||||
assert.equal(seenHeaders.length, 1);
|
||||
assert.ok(seenHeaders[0].Authorization.includes('Client="SubMiner"'));
|
||||
assert.ok(seenHeaders[0].Authorization.includes('DeviceId="device-auth"'));
|
||||
assert.ok(seenHeaders[0]['X-Emby-Authorization']);
|
||||
assert.ok(seenHeaders[0]!['Authorization']!.includes('Client="SubMiner"'));
|
||||
assert.ok(seenHeaders[0]!['Authorization']!.includes('DeviceId="device-auth"'));
|
||||
assert.ok(seenHeaders[0]!['X-Emby-Authorization']);
|
||||
});
|
||||
|
||||
test('dispatches inbound Play, Playstate, and GeneralCommand messages', () => {
|
||||
@@ -124,7 +124,7 @@ test('dispatches inbound Play, Playstate, and GeneralCommand messages', () => {
|
||||
});
|
||||
|
||||
service.start();
|
||||
const socket = sockets[0];
|
||||
const socket = sockets[0]!;
|
||||
socket.emit('message', JSON.stringify({ MessageType: 'Play', Data: { ItemId: 'movie-1' } }));
|
||||
socket.emit(
|
||||
'message',
|
||||
@@ -174,13 +174,13 @@ test('schedules reconnect with bounded exponential backoff', () => {
|
||||
});
|
||||
|
||||
service.start();
|
||||
sockets[0].emit('close');
|
||||
sockets[0]!.emit('close');
|
||||
pendingTimers.shift()?.();
|
||||
sockets[1].emit('close');
|
||||
sockets[1]!.emit('close');
|
||||
pendingTimers.shift()?.();
|
||||
sockets[2].emit('close');
|
||||
sockets[2]!.emit('close');
|
||||
pendingTimers.shift()?.();
|
||||
sockets[3].emit('close');
|
||||
sockets[3]!.emit('close');
|
||||
|
||||
assert.deepEqual(delays, [100, 200, 400, 400]);
|
||||
assert.equal(sockets.length, 4);
|
||||
@@ -216,7 +216,7 @@ test('Jellyfin remote stop prevents further reconnect/network activity', () => {
|
||||
|
||||
service.start();
|
||||
assert.equal(sockets.length, 1);
|
||||
sockets[0].emit('close');
|
||||
sockets[0]!.emit('close');
|
||||
assert.equal(pendingTimers.length, 1);
|
||||
|
||||
service.stop();
|
||||
@@ -252,7 +252,7 @@ test('reportProgress posts timeline payload and treats failure as non-fatal', as
|
||||
});
|
||||
|
||||
service.start();
|
||||
sockets[0].emit('open');
|
||||
sockets[0]!.emit('open');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const expectedPayload = buildJellyfinTimelinePayload({
|
||||
@@ -310,7 +310,7 @@ test('advertiseNow validates server registration using Sessions endpoint', async
|
||||
});
|
||||
|
||||
service.start();
|
||||
sockets[0].emit('open');
|
||||
sockets[0]!.emit('open');
|
||||
const ok = await service.advertiseNow();
|
||||
assert.equal(ok, true);
|
||||
assert.ok(calls.some((url) => url.endsWith('/Sessions')));
|
||||
|
||||
@@ -119,7 +119,7 @@ test('listItems supports search and formats title', async () => {
|
||||
limit: 25,
|
||||
},
|
||||
);
|
||||
assert.equal(items[0].title, 'Space Show S01E02 Pilot');
|
||||
assert.equal(items[0]!.title, 'Space Show S01E02 Pilot');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
@@ -338,14 +338,14 @@ test('listSubtitleTracks returns all subtitle streams with delivery urls', async
|
||||
[2, 3, 4],
|
||||
);
|
||||
assert.equal(
|
||||
tracks[0].deliveryUrl,
|
||||
tracks[0]!.deliveryUrl,
|
||||
'http://jellyfin.local/Videos/movie-1/ms-1/Subtitles/2/Stream.srt?api_key=token',
|
||||
);
|
||||
assert.equal(
|
||||
tracks[1].deliveryUrl,
|
||||
tracks[1]!.deliveryUrl,
|
||||
'http://jellyfin.local/Videos/movie-1/ms-1/Subtitles/3/Stream.srt?api_key=token',
|
||||
);
|
||||
assert.equal(tracks[2].deliveryUrl, 'https://cdn.example.com/subs.srt');
|
||||
assert.equal(tracks[2]!.deliveryUrl, 'https://cdn.example.com/subs.srt');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
@@ -556,9 +556,9 @@ test('listSubtitleTracks falls back from PlaybackInfo to item media sources', as
|
||||
);
|
||||
assert.equal(requestCount, 2);
|
||||
assert.equal(tracks.length, 1);
|
||||
assert.equal(tracks[0].index, 11);
|
||||
assert.equal(tracks[0]!.index, 11);
|
||||
assert.equal(
|
||||
tracks[0].deliveryUrl,
|
||||
tracks[0]!.deliveryUrl,
|
||||
'http://jellyfin.local/Videos/movie-fallback/ms-fallback/Subtitles/11/Stream.srt?api_key=token',
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -177,8 +177,8 @@ test('splitMpvMessagesFromBuffer parses complete lines and preserves partial buf
|
||||
|
||||
assert.equal(parsed.messages.length, 2);
|
||||
assert.equal(parsed.nextBuffer, '{"partial"');
|
||||
assert.equal(parsed.messages[0].event, 'shutdown');
|
||||
assert.equal(parsed.messages[1].name, 'media-title');
|
||||
assert.equal(parsed.messages[0]!.event, 'shutdown');
|
||||
assert.equal(parsed.messages[1]!.name, 'media-title');
|
||||
});
|
||||
|
||||
test('splitMpvMessagesFromBuffer reports invalid JSON lines', () => {
|
||||
@@ -189,7 +189,7 @@ test('splitMpvMessagesFromBuffer reports invalid JSON lines', () => {
|
||||
});
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.equal(errors[0].line, '{invalid}');
|
||||
assert.equal(errors[0]!.line, '{invalid}');
|
||||
});
|
||||
|
||||
test('visibility and boolean parsers handle text values', () => {
|
||||
|
||||
@@ -84,11 +84,11 @@ test('scheduleMpvReconnect clears existing timer and increments attempt', () =>
|
||||
|
||||
assert.equal(nextAttempt, 4);
|
||||
assert.equal(cleared.length, 1);
|
||||
assert.equal(cleared[0], existing);
|
||||
assert.equal(cleared[0]!, existing);
|
||||
assert.equal(setTimers.length, 1);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].attempt, 4);
|
||||
assert.equal(calls[0].delay, getMpvReconnectDelay(3, true));
|
||||
assert.equal(calls[0]!.attempt, 4);
|
||||
assert.equal(calls[0]!.delay, getMpvReconnectDelay(3, true));
|
||||
assert.equal(connected, 1);
|
||||
});
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@ test('MpvIpcClient handles sub-text property change and broadcasts tokenized sub
|
||||
});
|
||||
|
||||
assert.equal(events.length, 1);
|
||||
assert.equal(events[0].text, '字幕');
|
||||
assert.equal(events[0].isOverlayVisible, false);
|
||||
assert.equal(events[0]!.text, '字幕');
|
||||
assert.equal(events[0]!.isOverlayVisible, false);
|
||||
});
|
||||
|
||||
test('MpvIpcClient parses JSON line protocol in processBuffer', () => {
|
||||
@@ -70,8 +70,8 @@ test('MpvIpcClient parses JSON line protocol in processBuffer', () => {
|
||||
(client as any).processBuffer();
|
||||
|
||||
assert.equal(seen.length, 2);
|
||||
assert.equal(seen[0].name, 'path');
|
||||
assert.equal(seen[1].request_id, 1);
|
||||
assert.equal(seen[0]!.name, 'path');
|
||||
assert.equal(seen[1]!.request_id, 1);
|
||||
assert.equal((client as any).buffer, '{"partial":');
|
||||
});
|
||||
|
||||
|
||||
@@ -83,5 +83,5 @@ test('overlay measurement store rate-limits invalid payload warnings', () => {
|
||||
now = 11_000;
|
||||
store.report({ layer: 'visible' });
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.match(warnings[0], /Dropped 3 invalid measurement payload/);
|
||||
assert.match(warnings[0]!, /Dropped 3 invalid measurement payload/);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ const VIDEO_EXTENSIONS = new Set([
|
||||
]);
|
||||
|
||||
function getPathExtension(pathValue: string): string {
|
||||
const normalized = pathValue.split(/[?#]/, 1)[0];
|
||||
const normalized = pathValue.split(/[?#]/, 1)[0] ?? '';
|
||||
const dot = normalized.lastIndexOf('.');
|
||||
return dot >= 0 ? normalized.slice(dot).toLowerCase() : '';
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function shortcutMatchesInputForLocalFallback(
|
||||
const parts = normalized.split('+').filter(Boolean);
|
||||
if (parts.length === 0) return false;
|
||||
|
||||
const keyToken = parts[parts.length - 1];
|
||||
const keyToken = parts[parts.length - 1]!;
|
||||
const modifierTokens = new Set(parts.slice(0, -1));
|
||||
const allowedModifiers = new Set(['shift', 'alt', 'meta', 'control', 'commandorcontrol']);
|
||||
for (const token of modifierTokens) {
|
||||
|
||||
@@ -339,6 +339,6 @@ test('runSubsyncManual resolves string sid values from mpv stream properties', a
|
||||
assert.equal(syncOutputIndex >= 0, true);
|
||||
const outputPath = ffArgs[syncOutputIndex + 1];
|
||||
assert.equal(typeof outputPath, 'string');
|
||||
assert.ok(outputPath.length > 0);
|
||||
assert.equal(fs.readFileSync(outputPath, 'utf8'), '');
|
||||
assert.ok(outputPath!.length > 0);
|
||||
assert.equal(fs.readFileSync(outputPath!, 'utf8'), '');
|
||||
});
|
||||
|
||||
@@ -28,7 +28,8 @@ export function cycleSecondarySubMode(deps: CycleSecondarySubModeDeps): void {
|
||||
|
||||
const currentMode = deps.getSecondarySubMode();
|
||||
const currentIndex = SECONDARY_SUB_CYCLE.indexOf(currentMode);
|
||||
const nextMode = SECONDARY_SUB_CYCLE[(currentIndex + 1) % SECONDARY_SUB_CYCLE.length];
|
||||
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
|
||||
const nextMode = SECONDARY_SUB_CYCLE[(safeIndex + 1) % SECONDARY_SUB_CYCLE.length]!;
|
||||
deps.setSecondarySubMode(nextMode);
|
||||
deps.broadcastSecondarySubMode(nextMode);
|
||||
deps.showMpvOsd(`Secondary subtitle: ${nextMode}`);
|
||||
|
||||
@@ -82,8 +82,9 @@ export function serializeSubtitleMarkup(
|
||||
const klass = computeWordClass(token, options);
|
||||
const parts = token.surface.split('\n');
|
||||
for (let index = 0; index < parts.length; index += 1) {
|
||||
if (parts[index]) {
|
||||
chunks.push(`<span class="${klass}">${escapeHtml(parts[index])}</span>`);
|
||||
const part = parts[index];
|
||||
if (part) {
|
||||
chunks.push(`<span class="${klass}">${escapeHtml(part)}</span>`);
|
||||
}
|
||||
if (index < parts.length - 1) {
|
||||
chunks.push('<br>');
|
||||
|
||||
@@ -20,7 +20,7 @@ export class Texthooker {
|
||||
}
|
||||
|
||||
this.server = http.createServer((req, res) => {
|
||||
const urlPath = (req.url || '/').split('?')[0];
|
||||
const urlPath = (req.url || '/').split('?')[0] ?? '/';
|
||||
const filePath = path.join(texthookerPath, urlPath === '/' ? 'index.html' : urlPath);
|
||||
|
||||
const ext = path.extname(filePath);
|
||||
|
||||
@@ -377,7 +377,7 @@ function isRepeatedKanaSfx(text: string): boolean {
|
||||
let hasAdjacentRepeat = false;
|
||||
|
||||
for (let i = 0; i < chars.length; i += 1) {
|
||||
const char = chars[i];
|
||||
const char = chars[i]!;
|
||||
counts.set(char, (counts.get(char) ?? 0) + 1);
|
||||
if (i > 0 && chars[i] === chars[i - 1]) {
|
||||
hasAdjacentRepeat = true;
|
||||
|
||||
@@ -73,7 +73,8 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti
|
||||
|
||||
setTimeout(() => {
|
||||
if (!settingsWindow.isDestroyed()) {
|
||||
settingsWindow.setSize(settingsWindow.getSize()[0], settingsWindow.getSize()[1]);
|
||||
const [width = 0, height = 0] = settingsWindow.getSize();
|
||||
settingsWindow.setSize(width, height);
|
||||
settingsWindow.webContents.invalidate();
|
||||
settingsWindow.show();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user