ADD thumbnail to database (#12)
* ADD thumbnail to database * ADD thumbnail generation and retrieval functionality * ADD thumbnail availability check in VideoCard component * ADD ClipDTO to reduce exposed internals * REFactor move APIResponse and VideoMetadata to dto package * REMOVE unused props from VideoCard * ADD isThumbnailAvailable function
This commit is contained in:
@@ -1,14 +1,13 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { formatTime, stringToDate, dateToTimeAgo } from "../../utils/utils.ts";
|
import { formatTime, stringToDate, dateToTimeAgo } from "../../utils/utils.ts";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
|
import { isThumbnailAvailable } from "../../utils/endpoints.ts";
|
||||||
|
|
||||||
type VideoCardProps = {
|
type VideoCardProps = {
|
||||||
id: number,
|
id: number,
|
||||||
title: string,
|
title: string,
|
||||||
duration: number,
|
duration: number,
|
||||||
thumbnailPath: string | null,
|
|
||||||
videoPath: string,
|
|
||||||
createdAt: string,
|
createdAt: string,
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
@@ -19,31 +18,35 @@ const VideoCard = ({
|
|||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
duration,
|
duration,
|
||||||
thumbnailPath,
|
|
||||||
createdAt,
|
createdAt,
|
||||||
className
|
className
|
||||||
}: VideoCardProps) => {
|
}: VideoCardProps) => {
|
||||||
|
|
||||||
const initialSrc = thumbnailPath && thumbnailPath.trim() !== "" ? thumbnailPath : fallbackThumbnail;
|
|
||||||
const [imgSrc, setImgSrc] = useState(initialSrc);
|
|
||||||
const [timeAgo, setTimeAgo] = useState(dateToTimeAgo(stringToDate(createdAt)));
|
const [timeAgo, setTimeAgo] = useState(dateToTimeAgo(stringToDate(createdAt)));
|
||||||
|
const [thumbnailAvailable, setThumbnailAvailable] = useState(true);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setTimeAgo(dateToTimeAgo(stringToDate(createdAt)))
|
setTimeAgo(dateToTimeAgo(stringToDate(createdAt)))
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isThumbnailAvailable(id)
|
||||||
|
.then((available) => {
|
||||||
|
setThumbnailAvailable(available);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setThumbnailAvailable(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={"/video/" + id}>
|
<Link to={"/video/" + id}>
|
||||||
<div className={clsx("flex flex-col", className)}>
|
<div className={clsx("flex flex-col", className)}>
|
||||||
<div className={"relative inline-block"}>
|
<div className={"relative inline-block"}>
|
||||||
<img
|
<img
|
||||||
src={imgSrc}
|
src={thumbnailAvailable ? `/api/v1/download/thumbnail/${id}` : fallbackThumbnail}
|
||||||
alt="Video Thumbnail"
|
alt="Video Thumbnail"
|
||||||
onError={() => {
|
|
||||||
if (imgSrc !== fallbackThumbnail) {
|
|
||||||
setImgSrc(fallbackThumbnail);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="
|
<p className="
|
||||||
|
|||||||
@@ -18,11 +18,8 @@ const MyClips = () => {
|
|||||||
{clips.map((clip) => (
|
{clips.map((clip) => (
|
||||||
<VideoCard
|
<VideoCard
|
||||||
id={clip.id}
|
id={clip.id}
|
||||||
key={clip.videoPath}
|
|
||||||
title={clip.title}
|
title={clip.title}
|
||||||
duration={clip.duration}
|
duration={clip.duration}
|
||||||
thumbnailPath={clip.thumbnailPath}
|
|
||||||
videoPath={clip.videoPath}
|
|
||||||
createdAt={clip.createdAt}
|
createdAt={clip.createdAt}
|
||||||
className={"w-40 m-5"}
|
className={"w-40 m-5"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -176,6 +176,15 @@ const getClipById = async (id: string): Promise<Clip | null> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isThumbnailAvailable = async (id: number): Promise<boolean> => {
|
||||||
|
const response = await fetch(`/api/v1/download/thumbnail/${id}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
uploadFile,
|
uploadFile,
|
||||||
editFile,
|
editFile,
|
||||||
@@ -184,5 +193,6 @@ export {
|
|||||||
getMetadata,
|
getMetadata,
|
||||||
getUser,
|
getUser,
|
||||||
getClips,
|
getClips,
|
||||||
getClipById
|
getClipById,
|
||||||
|
isThumbnailAvailable
|
||||||
};
|
};
|
||||||
@@ -23,14 +23,10 @@ type User = {
|
|||||||
|
|
||||||
type Clip = {
|
type Clip = {
|
||||||
id: number,
|
id: number,
|
||||||
|
userId: number,
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
duration: number,
|
duration: number,
|
||||||
thumbnailPath: string,
|
|
||||||
videoPath: string,
|
|
||||||
fps: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
createdAt: string,
|
createdAt: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
pom.xml
5
pom.xml
@@ -66,6 +66,11 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct</artifactId>
|
||||||
|
<version>1.5.5.Final</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.ddf.vodsystem.controllers;
|
package com.ddf.vodsystem.controllers;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.APIResponse;
|
import com.ddf.vodsystem.dto.APIResponse;
|
||||||
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ddf.vodsystem.controllers;
|
package com.ddf.vodsystem.controllers;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.APIResponse;
|
import com.ddf.vodsystem.dto.ClipDTO;
|
||||||
|
import com.ddf.vodsystem.dto.APIResponse;
|
||||||
import com.ddf.vodsystem.entities.Clip;
|
import com.ddf.vodsystem.entities.Clip;
|
||||||
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
||||||
import com.ddf.vodsystem.services.ClipService;
|
import com.ddf.vodsystem.services.ClipService;
|
||||||
@@ -24,19 +25,23 @@ public class ClipController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public ResponseEntity<APIResponse<List<Clip>>> getClips(@AuthenticationPrincipal OAuth2User principal) {
|
public ResponseEntity<APIResponse<List<ClipDTO>>> getClips(@AuthenticationPrincipal OAuth2User principal) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
throw new NotAuthenticated("User is not authenticated");
|
throw new NotAuthenticated("User is not authenticated");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Clip> clips = clipService.getClipsByUser();
|
List<Clip> clips = clipService.getClipsByUser();
|
||||||
|
List<ClipDTO> clipDTOs = clips.stream()
|
||||||
|
.map(this::convertToDTO)
|
||||||
|
.toList();
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
new APIResponse<>("success", "Clips retrieved successfully", clips)
|
new APIResponse<>("success", "Clips retrieved successfully", clipDTOs)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<APIResponse<Clip>> getClipById(@AuthenticationPrincipal OAuth2User principal, @PathVariable Long id) {
|
public ResponseEntity<APIResponse<ClipDTO>> getClipById(@AuthenticationPrincipal OAuth2User principal, @PathVariable Long id) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
throw new NotAuthenticated("User is not authenticated");
|
throw new NotAuthenticated("User is not authenticated");
|
||||||
}
|
}
|
||||||
@@ -46,8 +51,21 @@ public class ClipController {
|
|||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClipDTO clipDTO = convertToDTO(clip);
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
new APIResponse<>("success", "Clip retrieved successfully", clip)
|
new APIResponse<>("success", "Clip retrieved successfully", clipDTO)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ClipDTO convertToDTO(Clip clip) {
|
||||||
|
ClipDTO dto = new ClipDTO();
|
||||||
|
dto.setId(clip.getId());
|
||||||
|
dto.setUserId(clip.getUser().getId());
|
||||||
|
dto.setTitle(clip.getTitle());
|
||||||
|
dto.setDescription(clip.getDescription());
|
||||||
|
dto.setDuration(clip.getDuration());
|
||||||
|
dto.setCreatedAt(clip.getCreatedAt());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,4 +65,18 @@ public class DownloadController {
|
|||||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||||
.body(resource);
|
.body(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/thumbnail/{id}")
|
||||||
|
public ResponseEntity<Resource> downloadThumbnail(@AuthenticationPrincipal OAuth2User principal, @PathVariable Long id) {
|
||||||
|
Resource resource = downloadService.downloadThumbnail(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package com.ddf.vodsystem.controllers;
|
package com.ddf.vodsystem.controllers;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.VideoMetadata;
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import com.ddf.vodsystem.services.EditService;
|
import com.ddf.vodsystem.services.EditService;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import com.ddf.vodsystem.entities.APIResponse;
|
import com.ddf.vodsystem.dto.APIResponse;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.ddf.vodsystem.controllers;
|
package com.ddf.vodsystem.controllers;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.APIResponse;
|
import com.ddf.vodsystem.dto.APIResponse;
|
||||||
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
||||||
import com.ddf.vodsystem.exceptions.JobNotFinished;
|
import com.ddf.vodsystem.exceptions.JobNotFinished;
|
||||||
import com.ddf.vodsystem.exceptions.JobNotFound;
|
import com.ddf.vodsystem.exceptions.JobNotFound;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.ddf.vodsystem.controllers;
|
package com.ddf.vodsystem.controllers;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.VideoMetadata;
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import com.ddf.vodsystem.entities.APIResponse;
|
import com.ddf.vodsystem.dto.APIResponse;
|
||||||
import com.ddf.vodsystem.services.JobService;
|
import com.ddf.vodsystem.services.JobService;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.ddf.vodsystem.controllers;
|
package com.ddf.vodsystem.controllers;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.APIResponse;
|
import com.ddf.vodsystem.dto.APIResponse;
|
||||||
import com.ddf.vodsystem.services.UploadService;
|
import com.ddf.vodsystem.services.UploadService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.ddf.vodsystem.entities;
|
package com.ddf.vodsystem.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
15
src/main/java/com/ddf/vodsystem/dto/ClipDTO.java
Normal file
15
src/main/java/com/ddf/vodsystem/dto/ClipDTO.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ddf.vodsystem.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ClipDTO {
|
||||||
|
private Long id;
|
||||||
|
private Long userId;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private Float duration;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.ddf.vodsystem.entities;
|
package com.ddf.vodsystem.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -47,4 +47,7 @@ public class Clip {
|
|||||||
|
|
||||||
@Column(name = "video_path", nullable = false, length = 255)
|
@Column(name = "video_path", nullable = false, length = 255)
|
||||||
private String videoPath;
|
private String videoPath;
|
||||||
|
|
||||||
|
@Column(name = "thumbnail_path", nullable = false, length = 255)
|
||||||
|
private String thumbnailPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ddf.vodsystem.entities;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import com.ddf.vodsystem.entities.*;
|
import com.ddf.vodsystem.entities.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -83,8 +84,20 @@ public class ClipService {
|
|||||||
private void persistClip(VideoMetadata videoMetadata, User user, Job job) {
|
private void persistClip(VideoMetadata videoMetadata, User user, Job job) {
|
||||||
// Move clip from temp to output directory
|
// Move clip from temp to output directory
|
||||||
String fileExtension = directoryService.getFileExtension(job.getOutputFile().getAbsolutePath());
|
String fileExtension = directoryService.getFileExtension(job.getOutputFile().getAbsolutePath());
|
||||||
File outputFile = directoryService.getOutputFile(job.getUuid(), fileExtension);
|
|
||||||
directoryService.copyFile(job.getOutputFile(), outputFile);
|
File clipOutputDir = directoryService.getUserClipsDir(user.getId());
|
||||||
|
File clipOutputFile = new File(clipOutputDir, job.getUuid() + "." + fileExtension);
|
||||||
|
directoryService.copyFile(job.getOutputFile(), clipOutputFile);
|
||||||
|
|
||||||
|
File thumbnailOutputDir = directoryService.getUserThumbnailsDir(user.getId());
|
||||||
|
File thumbnailOutputFile = new File(thumbnailOutputDir, job.getUuid() + ".png");
|
||||||
|
|
||||||
|
try {
|
||||||
|
ffmpegService.generateThumbnail(clipOutputFile, thumbnailOutputFile, 0.0f);
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
logger.error("Error generating thumbnail for clip: {}", e.getMessage());
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
// Save clip to database
|
// Save clip to database
|
||||||
Clip clip = new Clip();
|
Clip clip = new Clip();
|
||||||
@@ -97,7 +110,8 @@ public class ClipService {
|
|||||||
clip.setFps(videoMetadata.getFps());
|
clip.setFps(videoMetadata.getFps());
|
||||||
clip.setDuration(videoMetadata.getEndPoint() - videoMetadata.getStartPoint());
|
clip.setDuration(videoMetadata.getEndPoint() - videoMetadata.getStartPoint());
|
||||||
clip.setFileSize(videoMetadata.getFileSize());
|
clip.setFileSize(videoMetadata.getFileSize());
|
||||||
clip.setVideoPath(outputFile.getPath());
|
clip.setVideoPath(clipOutputFile.getPath());
|
||||||
|
clip.setThumbnailPath(thumbnailOutputFile.getPath());
|
||||||
clipRepository.save(clip);
|
clipRepository.save(clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,32 @@ public class DirectoryService {
|
|||||||
return new File(dir);
|
return new File(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public File getUserClipsDir(Long userId) {
|
||||||
|
if (userId == null) {
|
||||||
|
throw new IllegalArgumentException("User ID cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
String dir = outputDir + File.separator + userId + File.separator + "clips";
|
||||||
|
return new File(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getUserThumbnailsDir(Long userId) {
|
||||||
|
if (userId == null) {
|
||||||
|
throw new IllegalArgumentException("User ID cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
String dir = outputDir + File.separator + userId + File.separator + "thumbnails";
|
||||||
|
File thumbnailDir = new File(dir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
createDirectory(thumbnailDir.getAbsolutePath());
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error creating thumbnails directory: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnailDir;
|
||||||
|
}
|
||||||
|
|
||||||
public void saveAtDir(File file, MultipartFile multipartFile) {
|
public void saveAtDir(File file, MultipartFile multipartFile) {
|
||||||
try {
|
try {
|
||||||
createDirectory(file.getAbsolutePath());
|
createDirectory(file.getAbsolutePath());
|
||||||
@@ -118,7 +144,6 @@ public class DirectoryService {
|
|||||||
Files.delete(f.toPath());
|
Files.delete(f.toPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
|
|||||||
@@ -65,4 +65,19 @@ public class DownloadService {
|
|||||||
Clip clip = clipRepository.findById(id).orElseThrow(() -> new JobNotFound("Clip not found with id: " + id));
|
Clip clip = clipRepository.findById(id).orElseThrow(() -> new JobNotFound("Clip not found with id: " + id));
|
||||||
return downloadClip(clip);
|
return downloadClip(clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Resource downloadThumbnail(Clip clip) {
|
||||||
|
String path = clip.getThumbnailPath();
|
||||||
|
File file = new File(path);
|
||||||
|
if (!file.exists()) {
|
||||||
|
throw new JobNotFound("Thumbnail file not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FileSystemResource(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Resource downloadThumbnail(Long id) {
|
||||||
|
Clip clip = clipRepository.findById(id).orElseThrow(() -> new JobNotFound("Clip not found with id: " + id));
|
||||||
|
return downloadThumbnail(clip);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import com.ddf.vodsystem.entities.*;
|
import com.ddf.vodsystem.entities.*;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.VideoMetadata;
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -48,6 +48,35 @@ public class FfmpegService {
|
|||||||
runWithProgress(inputFile, outputFile, videoMetadata, new AtomicReference<>(0f));
|
runWithProgress(inputFile, outputFile, videoMetadata, new AtomicReference<>(0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void generateThumbnail(File inputFile, File outputFile, Float time) throws IOException, InterruptedException {
|
||||||
|
logger.info("Generating thumbnail at {} seconds", time);
|
||||||
|
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add("ffmpeg");
|
||||||
|
command.add("-ss");
|
||||||
|
command.add(time.toString());
|
||||||
|
command.add("-i");
|
||||||
|
command.add(inputFile.getAbsolutePath());
|
||||||
|
command.add("-frames:v");
|
||||||
|
command.add("1");
|
||||||
|
command.add(outputFile.getAbsolutePath());
|
||||||
|
|
||||||
|
String strCommand = String.join(" ", command);
|
||||||
|
logger.info("FFMPEG thumbnail command: {}", strCommand);
|
||||||
|
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||||
|
processBuilder.redirectErrorStream(true);
|
||||||
|
|
||||||
|
Process process = processBuilder.start();
|
||||||
|
|
||||||
|
if (process.waitFor() != 0) {
|
||||||
|
logger.error("FFMPEG process failed to generate thumbnail");
|
||||||
|
throw new IOException("FFMPEG process failed to generate thumbnail");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Thumbnail generated successfully at {}", outputFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
private void updateJobProgress(Process process, AtomicReference<Float> progress, Float length) throws IOException {
|
private void updateJobProgress(Process process, AtomicReference<Float> progress, Float length) throws IOException {
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.VideoMetadata;
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.Job;
|
import com.ddf.vodsystem.entities.Job;
|
||||||
import com.ddf.vodsystem.entities.VideoMetadata;
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ spring.servlet.multipart.max-file-size=2GB
|
|||||||
spring.servlet.multipart.max-request-size=2GB
|
spring.servlet.multipart.max-request-size=2GB
|
||||||
storage.temp.inputs=videos/inputs/
|
storage.temp.inputs=videos/inputs/
|
||||||
storage.temp.outputs=videos/outputs/
|
storage.temp.outputs=videos/outputs/
|
||||||
storage.outputs=videos/clips/
|
storage.outputs=outputs/
|
||||||
|
|
||||||
## Server Configuration
|
## Server Configuration
|
||||||
server.servlet.session.timeout=30m
|
server.servlet.session.timeout=30m
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ CREATE TABLE IF NOT EXISTS clips (
|
|||||||
duration FLOAT NOT NULL,
|
duration FLOAT NOT NULL,
|
||||||
file_size FLOAT NOT NULL,
|
file_size FLOAT NOT NULL,
|
||||||
video_path VARCHAR(255) NOT NULL,
|
video_path VARCHAR(255) NOT NULL,
|
||||||
|
thumbnail_path VARCHAR(255) NOT NULL,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
Reference in New Issue
Block a user