diff --git a/ui/src/lib/components/AudioPlayer.svelte b/ui/src/lib/components/AudioPlayer.svelte new file mode 100644 index 0000000..187318c --- /dev/null +++ b/ui/src/lib/components/AudioPlayer.svelte @@ -0,0 +1,187 @@ + + +
+
+ + + + Audio Narration +
+ + {#if status === 'idle'} +
+ + +
+ + {:else if status === 'loading' || status === 'generating'} +
+ + + + + {status === 'generating' ? 'Generating audio (this may take a few minutes)…' : 'Loading…'} +
+ + {:else if status === 'error'} +
+

{errorMsg || 'Failed to load audio.'}

+ +
+ + {:else if status === 'ready' && audioUrl} + + + + +
+ +
+ + + + {formatTime(currentTime)} / {formatTime(duration || 0)} + + + + +
+
+ {/if} +
diff --git a/ui/src/routes/api/presign/audio/+server.ts b/ui/src/routes/api/presign/audio/+server.ts new file mode 100644 index 0000000..5ba05f5 --- /dev/null +++ b/ui/src/routes/api/presign/audio/+server.ts @@ -0,0 +1,26 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { presignAudio } from '$lib/server/minio'; + +/** + * GET /api/presign/audio?slug=...&n=...&voice=...&speed=... + * Returns a presigned MinIO URL for the audio file so the browser + * can stream it directly without going through the server. + */ +export const GET: RequestHandler = async ({ url }) => { + const slug = url.searchParams.get('slug'); + const n = parseInt(url.searchParams.get('n') ?? '', 10); + const voice = url.searchParams.get('voice') ?? undefined; + const speed = parseFloat(url.searchParams.get('speed') ?? '1') || 1; + + if (!slug || !n || n < 1) { + error(400, 'Missing slug or n'); + } + + try { + const presignedUrl = await presignAudio(slug, n, voice, speed); + return json({ url: presignedUrl }); + } catch (e) { + error(500, `Could not get presigned URL: ${e}`); + } +}; diff --git a/ui/src/routes/api/progress/+server.ts b/ui/src/routes/api/progress/+server.ts new file mode 100644 index 0000000..d499620 --- /dev/null +++ b/ui/src/routes/api/progress/+server.ts @@ -0,0 +1,19 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { setProgress } from '$lib/server/pocketbase'; + +/** + * POST /api/progress + * Body: { slug: string, chapter: number } + * Records the user's reading position for the current session. + */ +export const POST: RequestHandler = async ({ request, locals }) => { + const body = await request.json().catch(() => null); + + if (!body || typeof body.slug !== 'string' || typeof body.chapter !== 'number') { + error(400, 'Invalid body — expected { slug, chapter }'); + } + + await setProgress(locals.sessionId, body.slug, body.chapter); + return json({ ok: true }); +}; diff --git a/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte b/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte index e4706fb..9089b84 100644 --- a/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte +++ b/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte @@ -1,7 +1,22 @@ @@ -41,7 +56,7 @@ -
+

Chapter {data.chapter.number}

{data.chapter.title || `Chapter ${data.chapter.number}`} @@ -51,13 +66,16 @@ {/if}

+ + + {#if !data.html}

Chapter content not available.

{:else} -
+
{@html data.html}
{/if}