import type { AssetRef } from '@tts/shared'; /** Guess a MIME type from a URL's file extension. */ export function guessMimeType(url: string): string { const ext = url.split('?')[0]!.split('.').pop()!.toLowerCase(); switch (ext) { case 'png': return 'image/png'; case 'jpg': case 'jpeg': return 'image/jpeg'; case 'gif': return 'image/gif'; case 'webp': return 'image/webp'; case 'pdf': return 'application/pdf'; default: return 'application/octet-stream'; } } /** Download a single asset as a `Blob`. */ export async function downloadAsset(url: string): Promise { const res = await fetch(url); if (!res.ok) { throw new Error(`Failed to download asset (${res.status}): ${url}`); } return res.blob(); } export interface DownloadProgress { done: number; total: number; url: string; } export interface DownloadOptions { concurrency?: number; onProgress?: (p: DownloadProgress) => void; } export interface DownloadedAsset { ref: AssetRef; blob: Blob; } /** * Download multiple assets with limited concurrency and a progress callback. */ export async function downloadAll( refs: AssetRef[], options: DownloadOptions = {}, ): Promise { const concurrency = options.concurrency ?? 4; const results: DownloadedAsset[] = []; let done = 0; const worker = async (ref: AssetRef): Promise => { const blob = await downloadAsset(ref.url); results.push({ ref, blob }); done += 1; options.onProgress?.({ done, total: refs.length, url: ref.url }); }; const queue = [...refs]; const workers: Promise[] = []; for (let i = 0; i < Math.min(concurrency, queue.length); i++) { workers.push( (async () => { while (queue.length > 0) { const ref = queue.shift()!; await worker(ref); } })(), ); } await Promise.all(workers); return results; }