From b8338d8643169172a834beb59b30392e762ed02b Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Fri, 30 Jan 2026 14:36:35 +0200 Subject: [PATCH] feat: add public podcast endpoints and player support --- .../app/routes/public_chat_routes.py | 27 +++++++- .../components/tool-ui/generate-podcast.tsx | 62 ++++++++++++++----- 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/surfsense_backend/app/routes/public_chat_routes.py b/surfsense_backend/app/routes/public_chat_routes.py index d79c4dea5..b5f523429 100644 --- a/surfsense_backend/app/routes/public_chat_routes.py +++ b/surfsense_backend/app/routes/public_chat_routes.py @@ -49,12 +49,37 @@ async def clone_public_chat( """ Clone a public chat snapshot to the user's account. - Single-phase clone: creates thread and copies messages in one request. + Creates thread and copies messages. Requires authentication. """ return await clone_from_snapshot(session, share_token, user) +@router.get("/{share_token}/podcasts/{podcast_id}") +async def get_public_podcast( + share_token: str, + podcast_id: int, + session: AsyncSession = Depends(get_async_session), +): + """ + Get podcast details from a public chat snapshot. + + No authentication required - the share_token provides access. + Returns podcast info including transcript. + """ + podcast_info = await get_snapshot_podcast(session, share_token, podcast_id) + + if not podcast_info: + raise HTTPException(status_code=404, detail="Podcast not found") + + return { + "id": podcast_info.get("original_id"), + "title": podcast_info.get("title"), + "status": "ready", + "podcast_transcript": podcast_info.get("transcript"), + } + + @router.get("/{share_token}/podcasts/{podcast_id}/stream") async def stream_public_podcast( share_token: str, diff --git a/surfsense_web/components/tool-ui/generate-podcast.tsx b/surfsense_web/components/tool-ui/generate-podcast.tsx index 3ae0755ef..64892ebab 100644 --- a/surfsense_web/components/tool-ui/generate-podcast.tsx +++ b/surfsense_web/components/tool-ui/generate-podcast.tsx @@ -173,7 +173,18 @@ function AudioLoadingState({ title }: { title: string }) { } /** - * Podcast Player Component - Fetches audio and transcript with authentication + * Get public share token from URL if in public view. + * Returns null if not in a public view. + */ +function getPublicShareToken(): string | null { + if (typeof window === "undefined") return null; + const match = window.location.pathname.match(/^\/public\/([^/]+)/); + return match ? match[1] : null; +} + +/** + * Podcast Player Component - Fetches audio and transcript + * Automatically uses public endpoint when viewing a public chat snapshot. */ function PodcastPlayer({ podcastId, @@ -217,30 +228,49 @@ function PodcastPlayer({ const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s timeout try { - // Fetch audio blob and podcast details in parallel - const [audioResponse, rawPodcastDetails] = await Promise.all([ - authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcastId}/audio`, - { method: "GET", signal: controller.signal } - ), - baseApiService.get(`/api/v1/podcasts/${podcastId}`), - ]); + // Check if we're in a public view + const shareToken = getPublicShareToken(); - if (!audioResponse.ok) { - throw new Error(`Failed to load audio: ${audioResponse.status}`); + let audioBlob: Blob; + let rawPodcastDetails: unknown = null; + + if (shareToken) { + // Public view - use public endpoints (baseApiService handles no-auth for /api/v1/public/) + const [blob, details] = await Promise.all([ + baseApiService.getBlob(`/api/v1/public/${shareToken}/podcasts/${podcastId}/stream`), + baseApiService.get(`/api/v1/public/${shareToken}/podcasts/${podcastId}`), + ]); + audioBlob = blob; + rawPodcastDetails = details; + } else { + // Authenticated view - fetch audio and details in parallel + const [audioResponse, details] = await Promise.all([ + authenticatedFetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcastId}/audio`, + { method: "GET", signal: controller.signal } + ), + baseApiService.get(`/api/v1/podcasts/${podcastId}`), + ]); + + if (!audioResponse.ok) { + throw new Error(`Failed to load audio: ${audioResponse.status}`); + } + + audioBlob = await audioResponse.blob(); + rawPodcastDetails = details; } - const audioBlob = await audioResponse.blob(); - // Create object URL from blob const objectUrl = URL.createObjectURL(audioBlob); objectUrlRef.current = objectUrl; setAudioSrc(objectUrl); // Parse and validate podcast details, then set transcript - const podcastDetails = parsePodcastDetails(rawPodcastDetails); - if (podcastDetails.podcast_transcript) { - setTranscript(podcastDetails.podcast_transcript); + if (rawPodcastDetails) { + const podcastDetails = parsePodcastDetails(rawPodcastDetails); + if (podcastDetails.podcast_transcript) { + setTranscript(podcastDetails.podcast_transcript); + } } } finally { clearTimeout(timeoutId);