Files
SubMiner/src/main/runtime/youtube-playback.ts
T

93 lines
2.5 KiB
TypeScript

function trimToNull(value: string | null | undefined): string | null {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function matchesYoutubeHost(hostname: string, expectedHost: string): boolean {
return hostname === expectedHost || hostname.endsWith(`.${expectedHost}`);
}
function extractYoutubeVideoId(mediaPath: string | null | undefined): string | null {
const normalized = trimToNull(mediaPath);
if (!normalized) {
return null;
}
let parsed: URL;
try {
parsed = new URL(normalized);
} catch {
return null;
}
const host = parsed.hostname.toLowerCase();
if (matchesYoutubeHost(host, 'youtu.be')) {
return parsed.pathname.replace(/^\/+/, '').split('/')[0]?.trim() || null;
}
if (
!matchesYoutubeHost(host, 'youtube.com') &&
!matchesYoutubeHost(host, 'youtube-nocookie.com')
) {
return null;
}
if (parsed.pathname === '/watch') {
return parsed.searchParams.get('v')?.trim() || null;
}
const pathSegments = parsed.pathname.replace(/^\/+/, '').split('/');
if (pathSegments[0] === 'shorts' || pathSegments[0] === 'embed') {
return pathSegments[1]?.trim() || null;
}
return null;
}
export function isYoutubeMediaPath(mediaPath: string | null | undefined): boolean {
const normalized = trimToNull(mediaPath);
if (!normalized) {
return false;
}
let parsed: URL;
try {
parsed = new URL(normalized);
} catch {
return false;
}
const host = parsed.hostname.toLowerCase();
return (
matchesYoutubeHost(host, 'youtu.be') ||
matchesYoutubeHost(host, 'youtube.com') ||
matchesYoutubeHost(host, 'youtube-nocookie.com')
);
}
export function isSameYoutubeMediaPath(
left: string | null | undefined,
right: string | null | undefined,
): boolean {
const leftId = extractYoutubeVideoId(left);
const rightId = extractYoutubeVideoId(right);
return Boolean(leftId && rightId && leftId === rightId);
}
export function shouldUseCachedYoutubeParsedCues(input: {
videoPath: string | null | undefined;
cachedMediaPath: string | null | undefined;
cachedCueCount: number;
}): boolean {
return (
input.cachedCueCount > 0 &&
isSameYoutubeMediaPath(input.videoPath, input.cachedMediaPath)
);
}
export function isYoutubePlaybackActive(
currentMediaPath: string | null | undefined,
currentVideoPath: string | null | undefined,
): boolean {
return isYoutubeMediaPath(currentMediaPath) || isYoutubeMediaPath(currentVideoPath);
}