From 43c3af4e35dcf7f1d61d73e15f26002332d83449 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 14 Sep 2025 14:42:12 +0700 Subject: [PATCH] feat: proxy video response --- .../src/api/routes/public.controller.ts | 58 ++++++++++++++++++- apps/backend/src/mcp/main.mcp.ts | 2 +- .../helpers/media.settings.component.tsx | 6 +- .../src/upload/cloudflare.storage.ts | 49 +++++++++------- .../src/upload/local.storage.ts | 51 ++++++++-------- 5 files changed, 117 insertions(+), 49 deletions(-) diff --git a/apps/backend/src/api/routes/public.controller.ts b/apps/backend/src/api/routes/public.controller.ts index 66238060..3137fc2e 100644 --- a/apps/backend/src/api/routes/public.controller.ts +++ b/apps/backend/src/api/routes/public.controller.ts @@ -1,4 +1,14 @@ -import { Body, Controller, Get, Param, Post, Req, Res } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + Query, + Req, + Res, + StreamableFile, +} from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service'; import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; @@ -11,6 +21,10 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management'; import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service'; import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments'; +import { Readable, pipeline } from 'stream'; +import { promisify } from 'util'; + +const pump = promisify(pipeline); @ApiTags('Public') @Controller('/public') @@ -136,4 +150,46 @@ export class PublicController { console.log('cryptoPost', body, path); return this._nowpayments.processPayment(path, body); } + + @Get('/stream') + async streamFile( + @Query('url') url: string, + @Res() res: Response, + @Req() req: Request + ) { + if (!url.endsWith('mp4')) { + return res.status(400).send('Invalid video URL'); + } + + const ac = new AbortController(); + const onClose = () => ac.abort(); + req.on('aborted', onClose); + res.on('close', onClose); + + const r = await fetch(url, { signal: ac.signal }); + + if (!r.ok && r.status !== 206) { + res.status(r.status); + throw new Error(`Upstream error: ${r.statusText}`); + } + + const type = r.headers.get('content-type') ?? 'application/octet-stream'; + res.setHeader('Content-Type', type); + + const contentRange = r.headers.get('content-range'); + if (contentRange) res.setHeader('Content-Range', contentRange); + + const len = r.headers.get('content-length'); + if (len) res.setHeader('Content-Length', len); + + const acceptRanges = r.headers.get('accept-ranges') ?? 'bytes'; + res.setHeader('Accept-Ranges', acceptRanges); + + if (r.status === 206) res.status(206); // Partial Content for range responses + + try { + await pump(Readable.fromWeb(r.body as any), res); + } catch (err) { + } + } } diff --git a/apps/backend/src/mcp/main.mcp.ts b/apps/backend/src/mcp/main.mcp.ts index 483ae851..83a05ade 100644 --- a/apps/backend/src/mcp/main.mcp.ts +++ b/apps/backend/src/mcp/main.mcp.ts @@ -50,7 +50,7 @@ export class MainMcp { @McpTool({ toolName: 'POSTIZ_SCHEDULE_POST', zod: { - type: eenum(['draft', 'scheduled']), + type: eenum(['draft', 'schedule']), configId: string(), generatePictures: boolean(), date: string().describe('UTC TIME'), diff --git a/apps/frontend/src/components/launches/helpers/media.settings.component.tsx b/apps/frontend/src/components/launches/helpers/media.settings.component.tsx index 3cf774d1..2a0531e8 100644 --- a/apps/frontend/src/components/launches/helpers/media.settings.component.tsx +++ b/apps/frontend/src/components/launches/helpers/media.settings.component.tsx @@ -5,6 +5,7 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; +import { useVariables } from '@gitroom/react/helpers/variable.context'; const postUrlEmitter = new EventEmitter(); export const MediaSettingsLayout = () => { @@ -97,7 +98,8 @@ export const CreateThumbnail: FC<{ altText?: string; onAltTextChange?: (altText: string) => void; }> = (props) => { - const { onSelect, media, altText, onAltTextChange } = props; + const { onSelect, media } = props; + const { backendUrl } = useVariables(); const videoRef = useRef(null); const canvasRef = useRef(null); const [currentTime, setCurrentTime] = useState(0); @@ -211,7 +213,7 @@ export const CreateThumbnail: FC<{