mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-27 18:12:05 -07:00
refactor: migrate shared type imports
This commit is contained in:
@@ -78,8 +78,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||
refreshDiscordPresence: () => deps.refreshDiscordPresence(),
|
||||
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
|
||||
hasInitialPlaybackQuitOnDisconnectArg: () =>
|
||||
deps.hasInitialPlaybackQuitOnDisconnectArg(),
|
||||
hasInitialPlaybackQuitOnDisconnectArg: () => deps.hasInitialPlaybackQuitOnDisconnectArg(),
|
||||
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
||||
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized: () =>
|
||||
deps.shouldQuitOnDisconnectWhenOverlayRuntimeInitialized(),
|
||||
@@ -88,7 +87,11 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
isMpvConnected: () => deps.isMpvConnected(),
|
||||
quitApp: () => deps.quitApp(),
|
||||
});
|
||||
const handleMpvConnectionChangeWithSidebarReset = ({ connected }: { connected: boolean }): void => {
|
||||
const handleMpvConnectionChangeWithSidebarReset = ({
|
||||
connected,
|
||||
}: {
|
||||
connected: boolean;
|
||||
}): void => {
|
||||
if (connected) {
|
||||
deps.resetSubtitleSidebarEmbeddedLayout();
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
appState: {
|
||||
initialArgs?: { jellyfinPlay?: unknown; youtubePlay?: unknown } | null;
|
||||
overlayRuntimeInitialized: boolean;
|
||||
mpvClient:
|
||||
| {
|
||||
connected?: boolean;
|
||||
currentSecondarySubText?: string;
|
||||
currentTimePos?: number;
|
||||
requestProperty?: (name: string) => Promise<unknown>;
|
||||
}
|
||||
| null;
|
||||
mpvClient: {
|
||||
connected?: boolean;
|
||||
currentSecondarySubText?: string;
|
||||
currentTimePos?: number;
|
||||
requestProperty?: (name: string) => Promise<unknown>;
|
||||
} | null;
|
||||
immersionTracker: {
|
||||
recordSubtitleLine?: (
|
||||
text: string,
|
||||
|
||||
@@ -14,9 +14,7 @@ test('autoplay release keeps the short retry budget for normal playback signals'
|
||||
test('autoplay release uses the full startup timeout window while paused', () => {
|
||||
assert.equal(
|
||||
resolveAutoplayReadyMaxReleaseAttempts({ forceWhilePaused: true }),
|
||||
Math.ceil(
|
||||
STARTUP_AUTOPLAY_RELEASE_TIMEOUT_MS / DEFAULT_AUTOPLAY_RELEASE_RETRY_DELAY_MS,
|
||||
),
|
||||
Math.ceil(STARTUP_AUTOPLAY_RELEASE_TIMEOUT_MS / DEFAULT_AUTOPLAY_RELEASE_RETRY_DELAY_MS),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -22,7 +22,4 @@ export function resolveAutoplayReadyMaxReleaseAttempts(options?: {
|
||||
return Math.max(3, Math.ceil(startupTimeoutMs / retryDelayMs));
|
||||
}
|
||||
|
||||
export {
|
||||
DEFAULT_AUTOPLAY_RELEASE_RETRY_DELAY_MS,
|
||||
STARTUP_AUTOPLAY_RELEASE_TIMEOUT_MS,
|
||||
};
|
||||
export { DEFAULT_AUTOPLAY_RELEASE_RETRY_DELAY_MS, STARTUP_AUTOPLAY_RELEASE_TIMEOUT_MS };
|
||||
|
||||
@@ -33,10 +33,7 @@ export function resolveWindowsMpvPath(deps: WindowsMpvLaunchDeps): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
export function buildWindowsMpvLaunchArgs(
|
||||
targets: string[],
|
||||
extraArgs: string[] = [],
|
||||
): string[] {
|
||||
export function buildWindowsMpvLaunchArgs(targets: string[], extraArgs: string[] = []): string[] {
|
||||
return ['--player-operation-mode=pseudo-gui', '--profile=subminer', ...extraArgs, ...targets];
|
||||
}
|
||||
|
||||
|
||||
@@ -141,9 +141,7 @@ test('youtube flow can open a manual picker session and load the selected subtit
|
||||
assert.ok(
|
||||
commands.some(
|
||||
(command) =>
|
||||
command[0] === 'set_property' &&
|
||||
command[1] === 'sub-visibility' &&
|
||||
command[2] === 'yes',
|
||||
command[0] === 'set_property' && command[1] === 'sub-visibility' && command[2] === 'yes',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
@@ -263,9 +261,7 @@ test('youtube flow retries secondary after partial batch subtitle failure', asyn
|
||||
assert.ok(
|
||||
commands.some(
|
||||
(command) =>
|
||||
command[0] === 'sub-add' &&
|
||||
command[1] === '/tmp/manual:en.vtt' &&
|
||||
command[2] === 'cached',
|
||||
command[0] === 'sub-add' && command[1] === '/tmp/manual:en.vtt' && command[2] === 'cached',
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -708,12 +704,54 @@ test('youtube flow leaves non-authoritative youtube subtitle tracks untouched af
|
||||
return selectedSecondarySid;
|
||||
}
|
||||
return [
|
||||
{ type: 'sub', id: 1, lang: 'en', title: 'English', external: true, 'external-filename': null },
|
||||
{ type: 'sub', id: 2, lang: 'ja', title: 'Japanese', external: true, 'external-filename': null },
|
||||
{ type: 'sub', id: 3, lang: 'ja-en', title: 'Japanese from English', external: true, 'external-filename': null },
|
||||
{ type: 'sub', id: 4, lang: 'ja-ja', title: 'Japanese from Japanese', external: true, 'external-filename': null },
|
||||
{ type: 'sub', id: 5, lang: 'ja-orig', title: 'auto-ja-orig.vtt', external: true, 'external-filename': '/tmp/auto-ja-orig.vtt' },
|
||||
{ type: 'sub', id: 6, lang: 'en', title: 'manual-en.en.srt', external: true, 'external-filename': '/tmp/manual-en.en.srt' },
|
||||
{
|
||||
type: 'sub',
|
||||
id: 1,
|
||||
lang: 'en',
|
||||
title: 'English',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 2,
|
||||
lang: 'ja',
|
||||
title: 'Japanese',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 3,
|
||||
lang: 'ja-en',
|
||||
title: 'Japanese from English',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 4,
|
||||
lang: 'ja-ja',
|
||||
title: 'Japanese from Japanese',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 5,
|
||||
lang: 'ja-orig',
|
||||
title: 'auto-ja-orig.vtt',
|
||||
external: true,
|
||||
'external-filename': '/tmp/auto-ja-orig.vtt',
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 6,
|
||||
lang: 'en',
|
||||
title: 'manual-en.en.srt',
|
||||
external: true,
|
||||
'external-filename': '/tmp/manual-en.en.srt',
|
||||
},
|
||||
];
|
||||
},
|
||||
refreshCurrentSubtitle: () => {},
|
||||
@@ -737,7 +775,10 @@ test('youtube flow leaves non-authoritative youtube subtitle tracks untouched af
|
||||
|
||||
await runtime.openManualPicker({ url: 'https://example.com' });
|
||||
|
||||
assert.equal(commands.some((command) => command[0] === 'sub-remove'), false);
|
||||
assert.equal(
|
||||
commands.some((command) => command[0] === 'sub-remove'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('youtube flow reuses existing manual youtube subtitle tracks when both requested languages already exist', async () => {
|
||||
@@ -751,8 +792,20 @@ test('youtube flow reuses existing manual youtube subtitle tracks when both requ
|
||||
videoId: 'video123',
|
||||
title: 'Video 123',
|
||||
tracks: [
|
||||
{ ...primaryTrack, id: 'manual:ja', sourceLanguage: 'ja', kind: 'manual', title: 'Japanese' },
|
||||
{ ...secondaryTrack, id: 'manual:en', sourceLanguage: 'en', kind: 'manual', title: 'English' },
|
||||
{
|
||||
...primaryTrack,
|
||||
id: 'manual:ja',
|
||||
sourceLanguage: 'ja',
|
||||
kind: 'manual',
|
||||
title: 'Japanese',
|
||||
},
|
||||
{
|
||||
...secondaryTrack,
|
||||
id: 'manual:en',
|
||||
sourceLanguage: 'en',
|
||||
kind: 'manual',
|
||||
title: 'English',
|
||||
},
|
||||
],
|
||||
}),
|
||||
acquireYoutubeSubtitleTracks: async () => {
|
||||
@@ -801,10 +854,38 @@ test('youtube flow reuses existing manual youtube subtitle tracks when both requ
|
||||
return selectedSecondarySid;
|
||||
}
|
||||
return [
|
||||
{ type: 'sub', id: 1, lang: 'en', title: 'English', external: true, 'external-filename': null },
|
||||
{ type: 'sub', id: 2, lang: 'ja', title: 'Japanese', external: true, 'external-filename': null },
|
||||
{ type: 'sub', id: 3, lang: 'ja-en', title: 'Japanese from English', external: true, 'external-filename': null },
|
||||
{ type: 'sub', id: 4, lang: 'ja-ja', title: 'Japanese from Japanese', external: true, 'external-filename': null },
|
||||
{
|
||||
type: 'sub',
|
||||
id: 1,
|
||||
lang: 'en',
|
||||
title: 'English',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 2,
|
||||
lang: 'ja',
|
||||
title: 'Japanese',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 3,
|
||||
lang: 'ja-en',
|
||||
title: 'Japanese from English',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 4,
|
||||
lang: 'ja-ja',
|
||||
title: 'Japanese from Japanese',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
];
|
||||
},
|
||||
refreshCurrentSubtitle: () => {},
|
||||
@@ -833,9 +914,15 @@ test('youtube flow reuses existing manual youtube subtitle tracks when both requ
|
||||
|
||||
assert.equal(selectedPrimarySid, 2);
|
||||
assert.equal(selectedSecondarySid, 1);
|
||||
assert.equal(commands.some((command) => command[0] === 'sub-add'), false);
|
||||
assert.equal(
|
||||
commands.some((command) => command[0] === 'sub-add'),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(refreshedSidebarSources, ['/tmp/manual-ja.ja.srt']);
|
||||
assert.equal(commands.some((command) => command[0] === 'sub-remove'), false);
|
||||
assert.equal(
|
||||
commands.some((command) => command[0] === 'sub-remove'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('youtube flow waits for manual youtube tracks to appear before falling back to injected copies', async () => {
|
||||
@@ -849,8 +936,20 @@ test('youtube flow waits for manual youtube tracks to appear before falling back
|
||||
videoId: 'video123',
|
||||
title: 'Video 123',
|
||||
tracks: [
|
||||
{ ...primaryTrack, id: 'manual:ja', sourceLanguage: 'ja', kind: 'manual', title: 'Japanese' },
|
||||
{ ...secondaryTrack, id: 'manual:en', sourceLanguage: 'en', kind: 'manual', title: 'English' },
|
||||
{
|
||||
...primaryTrack,
|
||||
id: 'manual:ja',
|
||||
sourceLanguage: 'ja',
|
||||
kind: 'manual',
|
||||
title: 'Japanese',
|
||||
},
|
||||
{
|
||||
...secondaryTrack,
|
||||
id: 'manual:en',
|
||||
sourceLanguage: 'en',
|
||||
kind: 'manual',
|
||||
title: 'English',
|
||||
},
|
||||
],
|
||||
}),
|
||||
acquireYoutubeSubtitleTracks: async () => {
|
||||
@@ -903,10 +1002,38 @@ test('youtube flow waits for manual youtube tracks to appear before falling back
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{ type: 'sub', id: 1, lang: 'en', title: 'English', external: true, 'external-filename': null },
|
||||
{ type: 'sub', id: 2, lang: 'ja', title: 'Japanese', external: true, 'external-filename': null },
|
||||
{ type: 'sub', id: 3, lang: 'ja-en', title: 'Japanese from English', external: true, 'external-filename': null },
|
||||
{ type: 'sub', id: 4, lang: 'ja-ja', title: 'Japanese from Japanese', external: true, 'external-filename': null },
|
||||
{
|
||||
type: 'sub',
|
||||
id: 1,
|
||||
lang: 'en',
|
||||
title: 'English',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 2,
|
||||
lang: 'ja',
|
||||
title: 'Japanese',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 3,
|
||||
lang: 'ja-en',
|
||||
title: 'Japanese from English',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
{
|
||||
type: 'sub',
|
||||
id: 4,
|
||||
lang: 'ja-ja',
|
||||
title: 'Japanese from Japanese',
|
||||
external: true,
|
||||
'external-filename': null,
|
||||
},
|
||||
];
|
||||
},
|
||||
refreshCurrentSubtitle: () => {},
|
||||
@@ -932,7 +1059,10 @@ test('youtube flow waits for manual youtube tracks to appear before falling back
|
||||
|
||||
assert.equal(selectedPrimarySid, 2);
|
||||
assert.equal(selectedSecondarySid, 1);
|
||||
assert.equal(commands.some((command) => command[0] === 'sub-add'), false);
|
||||
assert.equal(
|
||||
commands.some((command) => command[0] === 'sub-add'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('youtube flow reuses manual youtube tracks even when mpv exposes external filenames', async () => {
|
||||
@@ -970,7 +1100,9 @@ test('youtube flow reuses manual youtube tracks even when mpv exposes external f
|
||||
if (track.id === 'manual:ja') {
|
||||
return { path: '/tmp/manual-ja.ja.srt' };
|
||||
}
|
||||
throw new Error('should not download secondary track when existing manual english track is reusable');
|
||||
throw new Error(
|
||||
'should not download secondary track when existing manual english track is reusable',
|
||||
);
|
||||
},
|
||||
openPicker: async () => false,
|
||||
pauseMpv: () => {},
|
||||
@@ -1051,7 +1183,10 @@ test('youtube flow reuses manual youtube tracks even when mpv exposes external f
|
||||
|
||||
assert.equal(selectedPrimarySid, 2);
|
||||
assert.equal(selectedSecondarySid, 1);
|
||||
assert.equal(commands.some((command) => command[0] === 'sub-add'), false);
|
||||
assert.equal(
|
||||
commands.some((command) => command[0] === 'sub-add'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('youtube flow falls back to existing auto secondary track when auto secondary download fails', async () => {
|
||||
|
||||
@@ -384,7 +384,9 @@ async function injectDownloadedSubtitles(
|
||||
} else {
|
||||
deps.warn(
|
||||
`Unable to bind downloaded primary subtitle track in mpv: ${
|
||||
primarySelection.injectedPath ? path.basename(primarySelection.injectedPath) : primarySelection.track.label
|
||||
primarySelection.injectedPath
|
||||
? path.basename(primarySelection.injectedPath)
|
||||
: primarySelection.track.label
|
||||
}`,
|
||||
);
|
||||
}
|
||||
@@ -415,9 +417,7 @@ async function injectDownloadedSubtitles(
|
||||
deps.refreshCurrentSubtitle(currentSubText);
|
||||
}
|
||||
|
||||
deps.showMpvOsd(
|
||||
secondaryTrack ? 'Primary and secondary subtitles loaded.' : 'Subtitles loaded.',
|
||||
);
|
||||
deps.showMpvOsd(secondaryTrack ? 'Primary and secondary subtitles loaded.' : 'Subtitles loaded.');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -587,7 +587,8 @@ export function createYoutubeFlowRuntime(deps: YoutubeFlowDeps) {
|
||||
existingPrimaryTrackId,
|
||||
)
|
||||
: null;
|
||||
const primaryReady = input.primaryTrack.kind !== 'manual' || existingPrimaryTrackId !== null;
|
||||
const primaryReady =
|
||||
input.primaryTrack.kind !== 'manual' || existingPrimaryTrackId !== null;
|
||||
const secondaryReady =
|
||||
!input.secondaryTrack ||
|
||||
input.secondaryTrack.kind !== 'manual' ||
|
||||
@@ -631,7 +632,11 @@ export function createYoutubeFlowRuntime(deps: YoutubeFlowDeps) {
|
||||
secondaryInjectedPath = acquired.secondaryPath;
|
||||
}
|
||||
|
||||
if (input.secondaryTrack && existingSecondaryTrackId === null && secondaryInjectedPath === null) {
|
||||
if (
|
||||
input.secondaryTrack &&
|
||||
existingSecondaryTrackId === null &&
|
||||
secondaryInjectedPath === null
|
||||
) {
|
||||
try {
|
||||
secondaryInjectedPath = (
|
||||
await deps.acquireYoutubeSubtitleTrack({
|
||||
|
||||
@@ -183,7 +183,13 @@ test('prepare youtube playback accepts a non-youtube resolved path once playable
|
||||
'/videos/episode01.mkv',
|
||||
'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc',
|
||||
];
|
||||
const observedTrackLists = [[], [{ type: 'video', id: 1 }, { type: 'audio', id: 2 }]];
|
||||
const observedTrackLists = [
|
||||
[],
|
||||
[
|
||||
{ type: 'video', id: 1 },
|
||||
{ type: 'audio', id: 2 },
|
||||
],
|
||||
];
|
||||
let requestCount = 0;
|
||||
const prepare = createPrepareYoutubePlaybackInMpvHandler({
|
||||
requestPath: async () => {
|
||||
@@ -256,11 +262,14 @@ test('prepare youtube playback does not accept a different youtube video after p
|
||||
|
||||
test('prepare youtube playback accepts a fresh-start path change when the direct target matches exactly', async () => {
|
||||
const commands: Array<Array<string>> = [];
|
||||
const observedPaths = [
|
||||
'',
|
||||
'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc',
|
||||
const observedPaths = ['', 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc'];
|
||||
const observedTrackLists = [
|
||||
[],
|
||||
[
|
||||
{ type: 'video', id: 1 },
|
||||
{ type: 'audio', id: 2 },
|
||||
],
|
||||
];
|
||||
const observedTrackLists = [[], [{ type: 'video', id: 1 }, { type: 'audio', id: 2 }]];
|
||||
let requestCount = 0;
|
||||
const prepare = createPrepareYoutubePlaybackInMpvHandler({
|
||||
requestPath: async () => {
|
||||
|
||||
@@ -74,7 +74,9 @@ function hasPlayableMediaTracks(trackListRaw: unknown): boolean {
|
||||
if (!Array.isArray(trackListRaw)) return false;
|
||||
return trackListRaw.some((track) => {
|
||||
if (!track || typeof track !== 'object') return false;
|
||||
const type = String((track as Record<string, unknown>).type || '').trim().toLowerCase();
|
||||
const type = String((track as Record<string, unknown>).type || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return type === 'video' || type === 'audio';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { isYoutubeMediaPath } from './youtube-playback';
|
||||
import { normalizeYoutubeLangCode } from '../../core/services/youtube/labels';
|
||||
|
||||
export type YoutubePrimarySubtitleNotificationTimer = ReturnType<typeof setTimeout> | { id: number };
|
||||
export type YoutubePrimarySubtitleNotificationTimer =
|
||||
| ReturnType<typeof setTimeout>
|
||||
| { id: number };
|
||||
|
||||
type SubtitleTrackEntry = {
|
||||
id: number | null;
|
||||
@@ -82,7 +84,9 @@ function hasSelectedPrimarySubtitle(
|
||||
|
||||
const tracks = trackList.map(normalizeTrack);
|
||||
const activeTrack =
|
||||
(sid === null ? null : tracks.find((track) => track?.type === 'sub' && track.id === sid) ?? null) ??
|
||||
(sid === null
|
||||
? null
|
||||
: (tracks.find((track) => track?.type === 'sub' && track.id === sid) ?? null)) ??
|
||||
tracks.find((track) => track?.type === 'sub' && track.selected) ??
|
||||
null;
|
||||
if (!activeTrack) {
|
||||
@@ -130,7 +134,9 @@ export function createYoutubePrimarySubtitleNotificationRuntime(deps: {
|
||||
return;
|
||||
}
|
||||
lastReportedMediaPath = mediaPath;
|
||||
deps.notifyFailure('Primary subtitle failed to download or load. Try again from the subtitle modal.');
|
||||
deps.notifyFailure(
|
||||
'Primary subtitle failed to download or load. Try again from the subtitle modal.',
|
||||
);
|
||||
};
|
||||
|
||||
const schedulePendingCheck = (): void => {
|
||||
@@ -150,7 +156,8 @@ export function createYoutubePrimarySubtitleNotificationRuntime(deps: {
|
||||
|
||||
return {
|
||||
handleMediaPathChange: (path: string | null): void => {
|
||||
const normalizedPath = typeof path === 'string' && path.trim().length > 0 ? path.trim() : null;
|
||||
const normalizedPath =
|
||||
typeof path === 'string' && path.trim().length > 0 ? path.trim() : null;
|
||||
if (currentMediaPath !== normalizedPath) {
|
||||
lastReportedMediaPath = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user