mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-25 00:11:26 -07:00
fix: restore app-owned youtube subtitle flow
This commit is contained in:
168
src/main/runtime/youtube-primary-subtitle-notification.ts
Normal file
168
src/main/runtime/youtube-primary-subtitle-notification.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { isYoutubeMediaPath } from './youtube-playback';
|
||||
import { normalizeYoutubeLangCode } from '../../core/services/youtube/labels';
|
||||
|
||||
export type YoutubePrimarySubtitleNotificationTimer = ReturnType<typeof setTimeout> | { id: number };
|
||||
|
||||
type SubtitleTrackEntry = {
|
||||
id: number | null;
|
||||
type: string;
|
||||
lang: string;
|
||||
external: boolean;
|
||||
};
|
||||
|
||||
function parseTrackId(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value.trim());
|
||||
return Number.isInteger(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeTrack(entry: unknown): SubtitleTrackEntry | null {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const track = entry as Record<string, unknown>;
|
||||
return {
|
||||
id: parseTrackId(track.id),
|
||||
type: String(track.type || '').trim(),
|
||||
lang: String(track.lang || '').trim(),
|
||||
external: track.external === true,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearYoutubePrimarySubtitleNotificationTimer(
|
||||
timer: YoutubePrimarySubtitleNotificationTimer | null,
|
||||
): void {
|
||||
if (!timer) {
|
||||
return;
|
||||
}
|
||||
if (typeof timer === 'object' && timer !== null && 'id' in timer) {
|
||||
clearTimeout((timer as { id: number }).id);
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
function buildPreferredLanguageSet(values: string[]): Set<string> {
|
||||
const normalized = values
|
||||
.map((value) => normalizeYoutubeLangCode(value))
|
||||
.filter((value) => value.length > 0);
|
||||
return new Set(normalized);
|
||||
}
|
||||
|
||||
function matchesPreferredLanguage(language: string, preferred: Set<string>): boolean {
|
||||
if (preferred.size === 0) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeYoutubeLangCode(language);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (preferred.has(normalized)) {
|
||||
return true;
|
||||
}
|
||||
const base = normalized.split('-')[0] || normalized;
|
||||
return preferred.has(base);
|
||||
}
|
||||
|
||||
function hasSelectedPrimarySubtitle(
|
||||
sid: number | null,
|
||||
trackList: unknown[] | null,
|
||||
preferredLanguages: Set<string>,
|
||||
): boolean {
|
||||
if (sid === null || !Array.isArray(trackList)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeTrack =
|
||||
trackList.map(normalizeTrack).find((track) => track?.type === 'sub' && track.id === sid) ?? null;
|
||||
if (!activeTrack) {
|
||||
return false;
|
||||
}
|
||||
if (activeTrack.external) {
|
||||
return true;
|
||||
}
|
||||
return matchesPreferredLanguage(activeTrack.lang, preferredLanguages);
|
||||
}
|
||||
|
||||
export function createYoutubePrimarySubtitleNotificationRuntime(deps: {
|
||||
getPrimarySubtitleLanguages: () => string[];
|
||||
notifyFailure: (message: string) => void;
|
||||
schedule: (fn: () => void, delayMs: number) => YoutubePrimarySubtitleNotificationTimer;
|
||||
clearSchedule: (timer: YoutubePrimarySubtitleNotificationTimer | null) => void;
|
||||
delayMs?: number;
|
||||
}) {
|
||||
const delayMs = deps.delayMs ?? 5000;
|
||||
let currentMediaPath: string | null = null;
|
||||
let currentSid: number | null = null;
|
||||
let currentTrackList: unknown[] | null = null;
|
||||
let pendingTimer: YoutubePrimarySubtitleNotificationTimer | null = null;
|
||||
let lastReportedMediaPath: string | null = null;
|
||||
|
||||
const clearPendingTimer = (): void => {
|
||||
deps.clearSchedule(pendingTimer);
|
||||
pendingTimer = null;
|
||||
};
|
||||
|
||||
const maybeReportFailure = (): void => {
|
||||
const mediaPath = currentMediaPath?.trim() || '';
|
||||
if (!mediaPath || !isYoutubeMediaPath(mediaPath)) {
|
||||
return;
|
||||
}
|
||||
if (lastReportedMediaPath === mediaPath) {
|
||||
return;
|
||||
}
|
||||
const preferredLanguages = buildPreferredLanguageSet(deps.getPrimarySubtitleLanguages());
|
||||
if (preferredLanguages.size === 0) {
|
||||
return;
|
||||
}
|
||||
if (hasSelectedPrimarySubtitle(currentSid, currentTrackList, preferredLanguages)) {
|
||||
return;
|
||||
}
|
||||
lastReportedMediaPath = mediaPath;
|
||||
deps.notifyFailure('Primary subtitle failed to download or load. Try again from the subtitle modal.');
|
||||
};
|
||||
|
||||
const schedulePendingCheck = (): void => {
|
||||
clearPendingTimer();
|
||||
const mediaPath = currentMediaPath?.trim() || '';
|
||||
if (!mediaPath || !isYoutubeMediaPath(mediaPath)) {
|
||||
return;
|
||||
}
|
||||
pendingTimer = deps.schedule(() => {
|
||||
pendingTimer = null;
|
||||
maybeReportFailure();
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
return {
|
||||
handleMediaPathChange: (path: string | null): void => {
|
||||
const normalizedPath = typeof path === 'string' && path.trim().length > 0 ? path.trim() : null;
|
||||
if (currentMediaPath !== normalizedPath) {
|
||||
lastReportedMediaPath = null;
|
||||
}
|
||||
currentMediaPath = normalizedPath;
|
||||
currentSid = null;
|
||||
currentTrackList = null;
|
||||
schedulePendingCheck();
|
||||
},
|
||||
handleSubtitleTrackChange: (sid: number | null): void => {
|
||||
currentSid = sid;
|
||||
const preferredLanguages = buildPreferredLanguageSet(deps.getPrimarySubtitleLanguages());
|
||||
if (hasSelectedPrimarySubtitle(currentSid, currentTrackList, preferredLanguages)) {
|
||||
clearPendingTimer();
|
||||
}
|
||||
},
|
||||
handleSubtitleTrackListChange: (trackList: unknown[] | null): void => {
|
||||
currentTrackList = trackList;
|
||||
const preferredLanguages = buildPreferredLanguageSet(deps.getPrimarySubtitleLanguages());
|
||||
if (hasSelectedPrimarySubtitle(currentSid, currentTrackList, preferredLanguages)) {
|
||||
clearPendingTimer();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user