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);
+ }
}