From 87173992f3248b388ba25de9995ffb0c30f152d0 Mon Sep 17 00:00:00 2001 From: Hanif Dwy Putra S Date: Sat, 30 Aug 2025 08:35:55 +0800 Subject: [PATCH] feat: added queryparams support on download endpoint, and minor changes at download page Signed-off-by: Hanif Dwy Putra S --- apps/web/src/app/api/download/route.ts | 21 ++- apps/web/src/app/page.tsx | 179 +++++++++++++++++++------ 2 files changed, 156 insertions(+), 44 deletions(-) diff --git a/apps/web/src/app/api/download/route.ts b/apps/web/src/app/api/download/route.ts index 1fea237..9ad981d 100644 --- a/apps/web/src/app/api/download/route.ts +++ b/apps/web/src/app/api/download/route.ts @@ -3,9 +3,8 @@ import { rotateProvider } from "@/services/rotator"; import { NextRequest } from "next/server"; import { getProvider } from "tiktok-dl-core"; -export async function POST(request: NextRequest) { +const handleRequest = async (json: T) => { try { - const json = await request.json(); const safeData = await downloadValidator.safeParseAsync(json); if (safeData.error || !safeData.success) { @@ -50,8 +49,18 @@ export async function POST(request: NextRequest) { } } -export async function GET() { - return Response.json({ - message: 'Currently we moved to POST method only.', - }); +export async function GET(request: NextRequest) { + const allParams = Object.fromEntries(request.nextUrl.searchParams.entries()); + return handleRequest(allParams); +} + +export async function POST(request: NextRequest) { + try { + const json = await request.json(); + return handleRequest(json); + } catch (e) { + return Response.json({ + message: (e as Error).message, + }, { status: 500 }); + } } \ No newline at end of file diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index d02d14c..391d8f2 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input"; import { getTikTokURL } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, useCallback } from "react"; import { Download, ExternalLink, Loader2 } from "lucide-react"; import z from "zod"; @@ -28,25 +28,111 @@ const VideoPlayer = ({ isActive?: boolean; }) => { const videoRef = useRef(null); + const [isVideoReady, setIsVideoReady] = useState(false); + const [lastSyncTime, setLastSyncTime] = useState(0); + const syncTimeoutRef = useRef(null); + const playPromiseRef = useRef | null>(null); - useEffect(() => { + const syncVideoTime = useCallback((targetTime: number) => { + if (syncTimeoutRef.current) { + clearTimeout(syncTimeoutRef.current); + } + + syncTimeoutRef.current = setTimeout(() => { + const video = videoRef.current; + if (video && isActive && isVideoReady) { + const timeDiff = Math.abs(video.currentTime - targetTime); + if (timeDiff > 0.5) { + video.currentTime = targetTime; + setLastSyncTime(targetTime); + } + } + }, 100); + }, [isActive, isVideoReady]); + + const handlePlayPause = useCallback(async (shouldPlay: boolean) => { const video = videoRef.current; - if (video && isActive) { + if (!video || !isActive || !isVideoReady) return; + + try { + if (playPromiseRef.current) { + await playPromiseRef.current.catch(() => {}); + } + + if (shouldPlay && video.paused) { + playPromiseRef.current = video.play(); + await playPromiseRef.current; + } else if (!shouldPlay && !video.paused) { + video.pause(); + playPromiseRef.current = null; + } + } catch (error) { + console.warn('Playback error:', error); + onPlayStateChange(false); + } + }, [isActive, isVideoReady]); + + const handleLoadedMetadata = useCallback(() => { + setIsVideoReady(true); + const video = videoRef.current; + if (video && currentTime > 0) { video.currentTime = currentTime; - if (isPlaying) { - video.play().catch(() => { - onPlayStateChange(false); - }); + } + }, [currentTime]); + + const handleTimeUpdate = useCallback(() => { + const video = videoRef.current; + if (video && isActive && isVideoReady) { + const currentVideoTime = video.currentTime; + if (Math.abs(currentVideoTime - lastSyncTime) > 0.1) { + onTimeUpdate(currentVideoTime); + setLastSyncTime(currentVideoTime); } } - }, [currentTime, isActive, isPlaying]); + }, [isActive, isVideoReady, lastSyncTime]); + + const handlePlay = useCallback(() => { + if (isActive) { + onPlayStateChange(true); + } + }, [isActive]); + + const handlePause = useCallback(() => { + if (isActive) { + onPlayStateChange(false); + } + }, [isActive]); + + const handleEnded = useCallback(() => { + onPlayStateChange(false); + onTimeUpdate(0); + }, []); + + const handleError = useCallback((e: React.SyntheticEvent) => { + console.error('Video error:', e); + onPlayStateChange(false); + setIsVideoReady(false); + }, []); + + const handleWaiting = useCallback(() => { + // Buffering video... + }, []); + + const handleCanPlay = useCallback(() => { + setIsVideoReady(true); + }, []); useEffect(() => { - const video = videoRef.current; - if (video && isActive && Math.abs(video.currentTime - currentTime) > 1) { - video.currentTime = currentTime; + if (isVideoReady && isActive) { + handlePlayPause(isPlaying); } - }, [currentTime, isActive]); + }, [isPlaying, isVideoReady, isActive]); + + useEffect(() => { + if (isVideoReady && isActive) { + syncVideoTime(currentTime); + } + }, [currentTime, isVideoReady, isActive]); useEffect(() => { const video = videoRef.current; @@ -55,34 +141,37 @@ const VideoPlayer = ({ if (!video.paused) { video.pause(); } - } else { - if (isPlaying && video.paused) { + } else if (isVideoReady && isPlaying) { + if (Math.abs(video.currentTime - currentTime) > 0.5) { video.currentTime = currentTime; - video.play().catch(() => onPlayStateChange(false)); - } else if (!isPlaying && !video.paused) { - video.pause(); + } + if (video.paused) { + handlePlayPause(true); } } } - }, [isPlaying, isActive, currentTime]); + }, [isActive, isVideoReady]); - const handleTimeUpdate = () => { - if (videoRef.current && isActive) { - onTimeUpdate(videoRef.current.currentTime); + useEffect(() => { + setIsVideoReady(false); + setLastSyncTime(0); + if (playPromiseRef.current) { + playPromiseRef.current.catch(() => {}); + playPromiseRef.current = null; } - }; + }, [videoData.video?.urls[0]]); - const handlePlay = () => { - if (isActive) { - onPlayStateChange(true); - } - }; - - const handlePause = () => { - if (isActive) { - onPlayStateChange(false); - } - }; + // Cleanup + useEffect(() => { + return () => { + if (syncTimeoutRef.current) { + clearTimeout(syncTimeoutRef.current); + } + if (playPromiseRef.current) { + playPromiseRef.current.catch(() => {}); + } + }; + }, []); return (
@@ -95,17 +184,30 @@ const VideoPlayer = ({ src={videoData.video?.urls[0]} poster={videoData.video?.thumb} preload="metadata" + playsInline + onLoadedMetadata={handleLoadedMetadata} onTimeUpdate={handleTimeUpdate} onPlay={handlePlay} onPause={handlePause} - onLoadedMetadata={handleTimeUpdate} + onEnded={handleEnded} + onError={handleError} + onWaiting={handleWaiting} + onCanPlay={handleCanPlay} > Your browser does not support the video tag. + + {/* Loading indicator */} + {!isVideoReady && ( +
+ +
+ )}

Provider: {videoData.provider}

Available formats: {videoData.video?.urls.length}

+

Status: {isVideoReady ? 'Ready' : 'Loading...'}

); @@ -218,6 +320,7 @@ export default function Home() { } setVideoData(response); + // Reset video state untuk video baru setVideoCurrentTime(0); setVideoIsPlaying(false); } catch (error) { @@ -247,13 +350,13 @@ export default function Home() { form.reset(); }; - const handleTimeUpdate = (time: number) => { + const handleTimeUpdate = useCallback((time: number) => { setVideoCurrentTime(time); - }; + }, []); - const handlePlayStateChange = (playing: boolean) => { + const handlePlayStateChange = useCallback((playing: boolean) => { setVideoIsPlaying(playing); - }; + }, []); return (