(line)
| 312 | activeProcs.set(id, proc); |
| 313 | |
| 314 | const handleLine = (line) => { |
| 315 | const trimmed = line.trim(); |
| 316 | if (!trimmed) return; |
| 317 | const idx = downloads.findIndex((d) => d.id === id); |
| 318 | if (idx === -1) return; |
| 319 | |
| 320 | const update = {}; |
| 321 | |
| 322 | // (frag N/total), source of truth for HLS progress |
| 323 | const fragMatch = trimmed.match(/\(frag\s+(\d+)\/(\d+)\)/); |
| 324 | if (fragMatch) { |
| 325 | const currentFrag = parseInt(fragMatch[1]); |
| 326 | const total = parseInt(fragMatch[2]); |
| 327 | update.completedFragments = currentFrag; |
| 328 | update.totalFragments = total; |
| 329 | update.progress = Math.min( |
| 330 | 99, |
| 331 | Math.round((currentFrag / total) * 100), |
| 332 | ); |
| 333 | update.lastMessage = `Fragment ${currentFrag} / ${total}`; |
| 334 | } |
| 335 | |
| 336 | // [download] X% of Y (direct mp4, no fragments) |
| 337 | if (!fragMatch && !downloads[idx].totalFragments) { |
| 338 | const dlPctMatch = trimmed.match( |
| 339 | /^\[download\]\s+([\d.]+)%\s+of\s+~?\s*([\d.]+\s*(?:[KMGT]i?B|B))/i, |
| 340 | ); |
| 341 | if (dlPctMatch) { |
| 342 | const pct = parseFloat(dlPctMatch[1]); |
| 343 | update.progress = Math.min(99, Math.round(pct)); |
| 344 | update.size = dlPctMatch[2].trim(); |
| 345 | const spMatch = trimmed.match( |
| 346 | /\bat\s+([\d.]+\s*(?:[KMGT]i?B|B)\/s)/i, |
| 347 | ); |
| 348 | if (spMatch) update.speed = spMatch[1].trim(); |
| 349 | update.lastMessage = `${Math.round(pct)}% of ${update.size}`; |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | // ffmpeg Duration line |
| 354 | const durationMatch = trimmed.match( |
| 355 | /Duration:\s*(\d+):(\d+):([\d.]+)/, |
| 356 | ); |
| 357 | if (durationMatch) { |
| 358 | const totalSecs = |
| 359 | parseInt(durationMatch[1]) * 3600 + |
| 360 | parseInt(durationMatch[2]) * 60 + |
| 361 | parseFloat(durationMatch[3]); |
| 362 | if (totalSecs > 0) downloads[idx]._ffmpegTotalSecs = totalSecs; |
| 363 | return; |
| 364 | } |
| 365 | |
| 366 | // ffmpeg progress: size=… time=… |
| 367 | const ffmpegMatch = trimmed.match( |
| 368 | /size=\s*([\d.]+\s*\w+)\s+time=(\d+):(\d+):([\d.]+)/i, |
| 369 | ); |
| 370 | if (ffmpegMatch) { |
| 371 | const elapsedSecs = |
no test coverage detected