diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 45c810c..3b611f5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import ClipEdit from './pages/ClipEdit'; import Home from './pages/Home'; import {useEffect} from "react"; import MyClips from './pages/MyClips'; +import VideoPlayer from "./pages/VideoPlayer.tsx"; function App() { @@ -21,6 +22,7 @@ function App() { } /> } /> } /> + } /> diff --git a/frontend/src/components/video/VideoCard.tsx b/frontend/src/components/video/VideoCard.tsx index f288887..25c2767 100644 --- a/frontend/src/components/video/VideoCard.tsx +++ b/frontend/src/components/video/VideoCard.tsx @@ -4,6 +4,7 @@ import { Link } from "react-router-dom"; import { useState } from "react"; type VideoCardProps = { + id: number, title: string, duration: number, thumbnailPath: string | null, @@ -15,19 +16,24 @@ type VideoCardProps = { const fallbackThumbnail = "../../../public/default_thumbnail.png"; const VideoCard = ({ + id, title, duration, thumbnailPath, - videoPath, createdAt, className }: VideoCardProps) => { const initialSrc = thumbnailPath && thumbnailPath.trim() !== "" ? thumbnailPath : fallbackThumbnail; const [imgSrc, setImgSrc] = useState(initialSrc); + const [timeAgo, setTimeAgo] = useState(dateToTimeAgo(stringToDate(createdAt))); + + setTimeout(() => { + setTimeAgo(dateToTimeAgo(stringToDate(createdAt))) + }, 1000); return ( - +
{formatTime(duration)}

- -

- {dateToTimeAgo(stringToDate(createdAt))} -

-
+

{title == "" ? "(No Title)" : title}

+

+ {timeAgo} +

diff --git a/frontend/src/pages/MyClips.tsx b/frontend/src/pages/MyClips.tsx index cae90a1..3f7ffba 100644 --- a/frontend/src/pages/MyClips.tsx +++ b/frontend/src/pages/MyClips.tsx @@ -15,6 +15,7 @@ const MyClips = () => {
{clips.map((clip) => ( { + const { id } = useParams(); + const [videoUrl, setVideoUrl] = useState(""); + const [error, setError] = useState(null); + + useEffect(() => { + // Fetch the video URL from the server + fetch(`/api/v1/download/clip/${id}`) + .then(response => { + if (!response.ok) { + throw new Error("Failed to load video"); + } + return response.blob(); + }) + .then(blob => { + const url = URL.createObjectURL(blob); + setVideoUrl(url); + }) + .catch(err => { + console.error("Error fetching video:", err); + setError("Failed to load video. Please try again later."); + }); + }, [id]); + + return ( +
+ + + {error &&
{error}
} + {!videoUrl && !error &&
Loading video...
} +
+ ); +}; + +export default VideoPlayer; \ No newline at end of file diff --git a/frontend/src/utils/types.ts b/frontend/src/utils/types.ts index b8feb56..4036a90 100644 --- a/frontend/src/utils/types.ts +++ b/frontend/src/utils/types.ts @@ -22,6 +22,7 @@ type User = { } type Clip = { + id: number, title: string, description: string, duration: number, diff --git a/src/main/java/com/ddf/vodsystem/controllers/DownloadController.java b/src/main/java/com/ddf/vodsystem/controllers/DownloadController.java index 608b048..5e633d5 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/DownloadController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/DownloadController.java @@ -49,4 +49,18 @@ public class DownloadController { .contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM)) .body(resource); } + + @GetMapping("/clip/{id}") + public ResponseEntity downloadClip(@PathVariable Long id) { + Resource resource = downloadService.downloadClip(id); + + if (resource == null || !resource.exists()) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"") + .contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM)) + .body(resource); + } } diff --git a/src/main/java/com/ddf/vodsystem/repositories/ClipRepository.java b/src/main/java/com/ddf/vodsystem/repositories/ClipRepository.java index 10e9885..fd1bc8e 100644 --- a/src/main/java/com/ddf/vodsystem/repositories/ClipRepository.java +++ b/src/main/java/com/ddf/vodsystem/repositories/ClipRepository.java @@ -13,3 +13,5 @@ public interface ClipRepository extends JpaRepository { @Query("SELECT c FROM Clip c WHERE c.user = ?1") List findByUser(User user); } + + diff --git a/src/main/java/com/ddf/vodsystem/services/DownloadService.java b/src/main/java/com/ddf/vodsystem/services/DownloadService.java index 8d35875..504cc44 100644 --- a/src/main/java/com/ddf/vodsystem/services/DownloadService.java +++ b/src/main/java/com/ddf/vodsystem/services/DownloadService.java @@ -1,9 +1,11 @@ package com.ddf.vodsystem.services; +import com.ddf.vodsystem.entities.Clip; import com.ddf.vodsystem.entities.JobStatus; import com.ddf.vodsystem.exceptions.JobNotFinished; import com.ddf.vodsystem.exceptions.JobNotFound; import com.ddf.vodsystem.entities.Job; +import com.ddf.vodsystem.repositories.ClipRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; @@ -15,10 +17,12 @@ import java.io.File; public class DownloadService { private final JobService jobService; + private final ClipRepository clipRepository; @Autowired - public DownloadService(JobService jobService) { + public DownloadService(JobService jobService, ClipRepository clipRepository) { this.jobService = jobService; + this.clipRepository = clipRepository; } public Resource downloadInput(String uuid) { @@ -46,4 +50,19 @@ public class DownloadService { File file = job.getOutputFile(); return new FileSystemResource(file); } + + public Resource downloadClip(Clip clip) { + String path = clip.getVideoPath(); + File file = new File(path); + if (!file.exists()) { + throw new JobNotFound("Clip file not found"); + } + + return new FileSystemResource(file); + } + + public Resource downloadClip(Long id) { + Clip clip = clipRepository.findById(id).orElseThrow(() -> new JobNotFound("Clip not found with id: " + id)); + return downloadClip(clip); + } }