/** * Server-side MinIO presign helper. * Calls the scraper API to get presigned URLs, then optionally rewrites * the MinIO host to the public-facing URL for browser use. * * Never import this from client-side code. */ import { env } from '$env/dynamic/private'; import { env as pubEnv } from '$env/dynamic/public'; const SCRAPER_URL = env.SCRAPER_API_URL ?? 'http://localhost:8080'; // Public MinIO URL — used to rewrite presigned URLs so the browser can reach MinIO directly. // In docker-compose this would differ from the internal endpoint. const MINIO_PUBLIC_URL = pubEnv.PUBLIC_MINIO_PUBLIC_URL ?? 'http://localhost:9000'; /** * Rewrites the MinIO host in a presigned URL to the public-facing URL. * The presigned URL is signed against the internal endpoint (e.g. minio:9000), * but the browser needs the public URL (e.g. localhost:9000 in dev, or a CDN in prod). * Rewriting the host preserves all query params (signature, expiry, etc). */ function rewriteHost(presignedUrl: string): string { try { const u = new URL(presignedUrl); const pub = new URL(MINIO_PUBLIC_URL); u.protocol = pub.protocol; u.hostname = pub.hostname; u.port = pub.port; return u.toString(); } catch { return presignedUrl; } } /** * Returns a presigned URL for a chapter markdown file. * URL is valid for ~15 minutes (set by the scraper). * The returned URL points to the public MinIO endpoint and can be used * server-side (in a +page.server.ts load function) to fetch the markdown content. */ export async function presignChapter(slug: string, n: number): Promise { const res = await fetch(`${SCRAPER_URL}/api/presign/chapter/${slug}/${n}`); if (!res.ok) throw new Error(`presign chapter ${slug}/${n}: ${res.status}`); const data = (await res.json()) as { url: string }; return rewriteHost(data.url); } /** * Returns a presigned URL for an audio file. * URL is valid for ~1 hour. The URL is returned to the browser for direct streaming. */ export async function presignAudio( slug: string, n: number, voice?: string, speed?: number ): Promise { const params = new URLSearchParams(); if (voice) params.set('voice', voice); if (speed) params.set('speed', String(speed)); const qs = params.toString() ? `?${params.toString()}` : ''; const res = await fetch(`${SCRAPER_URL}/api/presign/audio/${slug}/${n}${qs}`); if (!res.ok) throw new Error(`presign audio ${slug}/${n}: ${res.status}`); const data = (await res.json()) as { url: string }; return rewriteHost(data.url); }