import * as fs from 'fs'; import * as childProcess from 'child_process'; import { DEFAULT_CONFIG } from '../config'; import { SubsyncConfig, SubsyncMode } from '../types'; export interface MpvTrack { id?: number; type?: string; selected?: boolean; external?: boolean; lang?: string; title?: string; codec?: string; 'ff-index'?: number; 'external-filename'?: string; } export interface SubsyncResolvedConfig { defaultMode: SubsyncMode; alassPath: string; ffsubsyncPath: string; ffmpegPath: string; } const DEFAULT_SUBSYNC_EXECUTABLE_PATHS = { alass: '/usr/bin/alass', ffsubsync: '/usr/bin/ffsubsync', ffmpeg: '/usr/bin/ffmpeg', } as const; export interface SubsyncContext { videoPath: string; primaryTrack: MpvTrack; secondaryTrack: MpvTrack | null; sourceTracks: MpvTrack[]; audioStreamIndex: number | null; } export interface CommandResult { ok: boolean; code: number | null; stderr: string; stdout: string; error?: string; } export function getSubsyncConfig(config: SubsyncConfig | undefined): SubsyncResolvedConfig { const resolvePath = (value: string | undefined, fallback: string): string => { const trimmed = value?.trim(); return trimmed && trimmed.length > 0 ? trimmed : fallback; }; return { defaultMode: config?.defaultMode ?? DEFAULT_CONFIG.subsync.defaultMode, alassPath: resolvePath(config?.alass_path, DEFAULT_SUBSYNC_EXECUTABLE_PATHS.alass), ffsubsyncPath: resolvePath(config?.ffsubsync_path, DEFAULT_SUBSYNC_EXECUTABLE_PATHS.ffsubsync), ffmpegPath: resolvePath(config?.ffmpeg_path, DEFAULT_SUBSYNC_EXECUTABLE_PATHS.ffmpeg), }; } export function hasPathSeparators(value: string): boolean { return value.includes('/') || value.includes('\\'); } export function fileExists(pathOrEmpty: string): boolean { if (!pathOrEmpty) return false; try { return fs.existsSync(pathOrEmpty); } catch { return false; } } export function formatTrackLabel(track: MpvTrack): string { const trackId = typeof track.id === 'number' ? track.id : -1; const source = track.external ? 'External' : 'Internal'; const lang = track.lang || track.title || 'unknown'; const active = track.selected ? ' (active)' : ''; return `${source} #${trackId} - ${lang}${active}`; } export function getTrackById(tracks: MpvTrack[], trackId: number | null): MpvTrack | null { if (trackId === null) return null; return tracks.find((track) => track.id === trackId) ?? null; } export function codecToExtension(codec: string | undefined): string | null { if (!codec) return null; const normalized = codec.toLowerCase(); if ( normalized === 'subrip' || normalized === 'srt' || normalized === 'text' || normalized === 'mov_text' ) return 'srt'; if (normalized === 'ass' || normalized === 'ssa') return 'ass'; if (normalized === 'webvtt' || normalized === 'vtt') return 'vtt'; if (normalized === 'ttml') return 'ttml'; return null; } export function runCommand( executable: string, args: string[], timeoutMs = 120000, ): Promise { return new Promise((resolve) => { const child = childProcess.spawn(executable, args, { stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; const timeout = setTimeout(() => { child.kill('SIGKILL'); }, timeoutMs); child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); }); child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); }); child.on('error', (error: Error) => { clearTimeout(timeout); resolve({ ok: false, code: null, stderr, stdout, error: error.message, }); }); child.on('close', (code: number | null) => { clearTimeout(timeout); resolve({ ok: code === 0, code, stderr, stdout, }); }); }); }