From 338eb605fd1a357edf17359beeb620d4e57ef164 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite <98231127+ThisBirchWood@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:01:21 +0200 Subject: [PATCH] 20 modularize the code to split up large classes (#21) * MODULARIZE ClipService by introducing CompressionService, MetadataService, and ThumbnailService * ADD deleteClip functionality to ClipController --- .../vodsystem/controllers/ClipController.java | 18 +++ .../ddf/vodsystem/services/ClipService.java | 39 ++--- .../ddf/vodsystem/services/UploadService.java | 9 +- .../services/{ => media}/CommandRunner.java | 2 +- .../CompressionService.java} | 108 +------------ .../services/media/MetadataService.java | 146 ++++++++++++++++++ .../services/media/ThumbnailService.java | 29 ++++ 7 files changed, 222 insertions(+), 129 deletions(-) rename src/main/java/com/ddf/vodsystem/services/{ => media}/CommandRunner.java (97%) rename src/main/java/com/ddf/vodsystem/services/{MediaService.java => media/CompressionService.java} (52%) create mode 100644 src/main/java/com/ddf/vodsystem/services/media/MetadataService.java create mode 100644 src/main/java/com/ddf/vodsystem/services/media/ThumbnailService.java diff --git a/src/main/java/com/ddf/vodsystem/controllers/ClipController.java b/src/main/java/com/ddf/vodsystem/controllers/ClipController.java index f484bef..03cf32a 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/ClipController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/ClipController.java @@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -58,6 +59,23 @@ public class ClipController { ); } + @DeleteMapping("/{id}") + public ResponseEntity> deleteClip(@AuthenticationPrincipal OAuth2User principal, @PathVariable Long id) { + if (principal == null) { + throw new NotAuthenticated("User is not authenticated"); + } + + boolean deleted = clipService.deleteClip(id); + + if (!deleted) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok( + new APIResponse<>("success", "Clip deleted successfully", "Clip with ID " + id + " has been deleted") + ); + } + private ClipDTO convertToDTO(Clip clip) { ClipDTO dto = new ClipDTO(); dto.setId(clip.getId()); diff --git a/src/main/java/com/ddf/vodsystem/services/ClipService.java b/src/main/java/com/ddf/vodsystem/services/ClipService.java index 617ca49..5012d9a 100644 --- a/src/main/java/com/ddf/vodsystem/services/ClipService.java +++ b/src/main/java/com/ddf/vodsystem/services/ClipService.java @@ -12,6 +12,9 @@ import java.util.Optional; import com.ddf.vodsystem.exceptions.NotAuthenticated; import com.ddf.vodsystem.repositories.ClipRepository; +import com.ddf.vodsystem.services.media.CompressionService; +import com.ddf.vodsystem.services.media.MetadataService; +import com.ddf.vodsystem.services.media.ThumbnailService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -22,16 +25,22 @@ public class ClipService { private final ClipRepository clipRepository; private final DirectoryService directoryService; - private final MediaService mediaService; + private final CompressionService compressionService; + private final MetadataService metadataService; + private final ThumbnailService thumbnailService; private final UserService userService; public ClipService(ClipRepository clipRepository, DirectoryService directoryService, - MediaService mediaService, + CompressionService compressionService, + MetadataService metadataService, + ThumbnailService thumbnailService, UserService userService) { this.clipRepository = clipRepository; this.directoryService = directoryService; - this.mediaService = mediaService; + this.compressionService = compressionService; + this.metadataService = metadataService; + this.thumbnailService = thumbnailService; this.userService = userService; } @@ -55,10 +64,10 @@ public class ClipService { File inputFile, File outputFile, ProgressTracker progress) throws IOException, InterruptedException { - normalizeVideoMetadata(inputMetadata, outputMetadata); - mediaService.compress(inputFile, outputFile, outputMetadata, progress); + metadataService.normalizeVideoMetadata(inputMetadata, outputMetadata); + compressionService.compress(inputFile, outputFile, outputMetadata, progress); - Float fileSize = mediaService.getVideoMetadata(outputFile).getFileSize(); + Float fileSize = metadataService.getVideoMetadata(outputFile).getFileSize(); outputMetadata.setFileSize(fileSize); User user = userService.getUser(); @@ -97,11 +106,11 @@ public class ClipService { return clip; } - public void deleteClip(Long id) { + public boolean deleteClip(Long id) { Clip clip = getClipById(id); if (clip == null) { logger.warn("Clip with ID {} not found for deletion", id); - return; + return false; } if (!isAuthenticatedForClip(clip)) { @@ -115,6 +124,8 @@ public class ClipService { directoryService.deleteFile(thumbnailFile); clipRepository.delete(clip); + logger.info("Clip with ID {} deleted successfully", id); + return true; } public boolean isAuthenticatedForClip(Clip clip) { @@ -136,7 +147,7 @@ public class ClipService { try { - mediaService.createThumbnail(clipFile, thumbnailFile, 0.0f); + thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f); } catch (IOException | InterruptedException e) { logger.error("Error generating thumbnail for clip: {}", e.getMessage()); Thread.currentThread().interrupt(); @@ -157,14 +168,4 @@ public class ClipService { clip.setThumbnailPath(thumbnailFile.getPath()); return clipRepository.save(clip); } - - public void normalizeVideoMetadata(VideoMetadata inputFileMetadata, VideoMetadata outputFileMetadata) { - if (outputFileMetadata.getStartPoint() == null) { - outputFileMetadata.setStartPoint(0f); - } - - if (outputFileMetadata.getEndPoint() == null) { - outputFileMetadata.setEndPoint(inputFileMetadata.getEndPoint()); - } - } } diff --git a/src/main/java/com/ddf/vodsystem/services/UploadService.java b/src/main/java/com/ddf/vodsystem/services/UploadService.java index 3c81cdf..6a24c30 100644 --- a/src/main/java/com/ddf/vodsystem/services/UploadService.java +++ b/src/main/java/com/ddf/vodsystem/services/UploadService.java @@ -2,6 +2,7 @@ package com.ddf.vodsystem.services; import com.ddf.vodsystem.entities.Job; import com.ddf.vodsystem.dto.VideoMetadata; +import com.ddf.vodsystem.services.media.MetadataService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -19,15 +20,15 @@ public class UploadService { private static final Logger logger = LoggerFactory.getLogger(UploadService.class); private final JobService jobService; - private final MediaService mediaService; + private final MetadataService metadataService; private final DirectoryService directoryService; @Autowired public UploadService(JobService jobService, - MediaService mediaService, + MetadataService metadataService, DirectoryService directoryService) { this.jobService = jobService; - this.mediaService = mediaService; + this.metadataService = metadataService; this.directoryService = directoryService; } @@ -42,7 +43,7 @@ public class UploadService { // add job logger.info("Uploaded file and creating job with UUID: {}", uuid); - VideoMetadata videoMetadata = mediaService.getVideoMetadata(inputFile); + VideoMetadata videoMetadata = metadataService.getVideoMetadata(inputFile); Job job = new Job(uuid, inputFile, outputFile, videoMetadata); jobService.add(job); diff --git a/src/main/java/com/ddf/vodsystem/services/CommandRunner.java b/src/main/java/com/ddf/vodsystem/services/media/CommandRunner.java similarity index 97% rename from src/main/java/com/ddf/vodsystem/services/CommandRunner.java rename to src/main/java/com/ddf/vodsystem/services/media/CommandRunner.java index 2b96247..25b3a3e 100644 --- a/src/main/java/com/ddf/vodsystem/services/CommandRunner.java +++ b/src/main/java/com/ddf/vodsystem/services/media/CommandRunner.java @@ -1,4 +1,4 @@ -package com.ddf.vodsystem.services; +package com.ddf.vodsystem.services.media; import com.ddf.vodsystem.dto.CommandOutput; diff --git a/src/main/java/com/ddf/vodsystem/services/MediaService.java b/src/main/java/com/ddf/vodsystem/services/media/CompressionService.java similarity index 52% rename from src/main/java/com/ddf/vodsystem/services/MediaService.java rename to src/main/java/com/ddf/vodsystem/services/media/CompressionService.java index 540c6aa..9c4d26b 100644 --- a/src/main/java/com/ddf/vodsystem/services/MediaService.java +++ b/src/main/java/com/ddf/vodsystem/services/media/CompressionService.java @@ -1,11 +1,7 @@ -package com.ddf.vodsystem.services; +package com.ddf.vodsystem.services.media; -import com.ddf.vodsystem.dto.CommandOutput; import com.ddf.vodsystem.dto.ProgressTracker; import com.ddf.vodsystem.dto.VideoMetadata; -import com.ddf.vodsystem.exceptions.FFMPEGException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -18,8 +14,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; @Service -public class MediaService { - private static final Logger logger = LoggerFactory.getLogger(MediaService.class); +public class CompressionService { + private static final Logger logger = LoggerFactory.getLogger(CompressionService.class); private static final float AUDIO_RATIO = 0.15f; private static final float MAX_AUDIO_BITRATE = 128f; @@ -34,104 +30,6 @@ public class MediaService { CommandRunner.run(command, line -> setProgress(line, progress, length)); } - public void createThumbnail(File inputFile, File outputFile, Float timeInVideo) throws IOException, InterruptedException { - logger.info("Creating thumbnail at {} seconds", timeInVideo); - - List command = List.of( - "ffmpeg", - "-ss", timeInVideo.toString(), - "-i", inputFile.getAbsolutePath(), - "-frames:v", "1", - outputFile.getAbsolutePath() - ); - - CommandRunner.run(command); - } - - public VideoMetadata getVideoMetadata(File file) { - logger.info("Getting metadata for file {}", file.getAbsolutePath()); - - List command = List.of( - "ffprobe", - "-v", "quiet", - "-print_format", "json", - "-show_format", "-select_streams", - "v:0", "-show_entries", "stream=duration,width,height,r_frame_rate:format=size,duration", - "-i", file.getAbsolutePath() - ); - - ObjectMapper mapper = new ObjectMapper(); - StringBuilder outputBuilder = new StringBuilder(); - - try { - CommandOutput output = CommandRunner.run(command); - - for (String line : output.getOutput()) { - outputBuilder.append(line); - } - - JsonNode node = mapper.readTree(outputBuilder.toString()); - return parseVideoMetadata(node); - } catch (IOException | InterruptedException e) { - Thread.currentThread().interrupt(); - throw new FFMPEGException("Error while getting video metadata: " + e); - } - } - - private VideoMetadata parseVideoMetadata(JsonNode node) { - VideoMetadata metadata = new VideoMetadata(); - metadata.setStartPoint(0f); - - JsonNode streamNode = node.path("streams").get(0); - - // if stream doesn't exist - if (streamNode == null || streamNode.isMissingNode()) { - throw new FFMPEGException("ffprobe streams missing"); - } - - if (streamNode.has("duration")) { - metadata.setEndPoint(Float.valueOf(streamNode.get("duration").asText())); - } - - if (streamNode.has("width")) { - metadata.setWidth(streamNode.get("width").asInt()); - } - - if (streamNode.has("height")) { - metadata.setHeight(streamNode.get("height").asInt()); - } - - if (streamNode.has("r_frame_rate")) { - String fpsFraction = streamNode.get("r_frame_rate").asText(); - - if (fpsFraction.contains("/")) { - String[] parts = fpsFraction.split("/"); - double numerator = Float.parseFloat(parts[0]); - double denominator = Float.parseFloat(parts[1]); - if (denominator != 0) { - metadata.setFps((float) (numerator / denominator)); - } - } else { - metadata.setFps(Float.valueOf(fpsFraction)); // Handle cases like "25" directly - } - } - - // Extract from the 'format' section - JsonNode formatNode = node.path("format"); - if (formatNode != null && !formatNode.isMissingNode()) { - if (formatNode.has("size")) { - metadata.setFileSize(Float.parseFloat(formatNode.get("size").asText())); - } - - // Use format duration as a fallback or primary source if stream duration is absent/zero - if (formatNode.has("duration") && metadata.getEndPoint() == null) { - metadata.setEndPoint(Float.parseFloat(formatNode.get("duration").asText())); - } - } - - return metadata; - } - private void setProgress(String line, ProgressTracker progress, float length) { Matcher matcher = timePattern.matcher(line); if (matcher.find()) { diff --git a/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java b/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java new file mode 100644 index 0000000..c71245f --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java @@ -0,0 +1,146 @@ +package com.ddf.vodsystem.services.media; + +import com.ddf.vodsystem.dto.CommandOutput; +import com.ddf.vodsystem.dto.VideoMetadata; +import com.ddf.vodsystem.exceptions.FFMPEGException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +@Service +public class MetadataService { + private static final Logger logger = LoggerFactory.getLogger(MetadataService.class); + + public VideoMetadata getVideoMetadata(File file) { + logger.info("Getting metadata for file {}", file.getAbsolutePath()); + + List command = List.of( + "ffprobe", + "-v", "quiet", + "-print_format", "json", + "-show_format", "-select_streams", + "v:0", "-show_entries", "stream=duration,width,height,r_frame_rate:format=size,duration", + "-i", file.getAbsolutePath() + ); + + ObjectMapper mapper = new ObjectMapper(); + StringBuilder outputBuilder = new StringBuilder(); + + try { + CommandOutput output = CommandRunner.run(command); + + for (String line : output.getOutput()) { + outputBuilder.append(line); + } + + JsonNode node = mapper.readTree(outputBuilder.toString()); + return parseVideoMetadata(node); + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); + throw new FFMPEGException("Error while getting video metadata: " + e); + } + } + + public void normalizeVideoMetadata(VideoMetadata inputFileMetadata, VideoMetadata outputFileMetadata) { + if (outputFileMetadata.getStartPoint() == null) { + outputFileMetadata.setStartPoint(0f); + } + + if (outputFileMetadata.getEndPoint() == null) { + outputFileMetadata.setEndPoint(inputFileMetadata.getEndPoint()); + } + } + + private VideoMetadata parseVideoMetadata(JsonNode node) { + VideoMetadata metadata = new VideoMetadata(); + metadata.setStartPoint(0f); + + JsonNode streamNode = extractStreamNode(node); + + metadata.setEndPoint(extractDuration(streamNode)); + metadata.setWidth(getWidth(streamNode)); + metadata.setHeight(getHeight(streamNode)); + metadata.setFps(extractFps(streamNode)); + + JsonNode formatNode = extractFormatNode(node); + metadata.setFileSize(extractFileSize(formatNode)); + extractEndPointFromFormat(metadata, formatNode); + + return metadata; + } + + private JsonNode extractStreamNode(JsonNode node) { + JsonNode streamNode = node.path("streams").get(0); + if (streamNode == null || streamNode.isMissingNode()) { + throw new FFMPEGException("ffprobe streams missing"); + } + return streamNode; + } + + private Float extractDuration(JsonNode streamNode) { + if (streamNode.has("duration")) { + return Float.valueOf(streamNode.get("duration").asText()); + } + + throw new FFMPEGException("ffprobe duration missing"); + } + + private Integer getWidth(JsonNode streamNode) { + if (streamNode.has("width")) { + return streamNode.get("width").asInt(); + } + + throw new FFMPEGException("ffprobe width missing"); + } + + private Integer getHeight(JsonNode streamNode) { + if (streamNode.has("height")) { + return streamNode.get("height").asInt(); + } + + throw new FFMPEGException("ffprobe height missing"); + } + + private JsonNode extractFormatNode(JsonNode node) { + JsonNode formatNode = node.path("format"); + return (formatNode != null && !formatNode.isMissingNode()) ? formatNode : null; + } + + private Float extractFileSize(JsonNode formatNode) { + if (formatNode != null && formatNode.has("size")) { + return Float.parseFloat(formatNode.get("size").asText()); + } + + throw new FFMPEGException("ffprobe file size missing"); + } + + private void extractEndPointFromFormat(VideoMetadata metadata, JsonNode formatNode) { + if (formatNode != null && formatNode.has("duration") && metadata.getEndPoint() == null) { + metadata.setEndPoint(Float.parseFloat(formatNode.get("duration").asText())); + } + } + + private Float extractFps(JsonNode streamNode) { + if (streamNode.has("r_frame_rate")) { + String fpsFraction = streamNode.get("r_frame_rate").asText(); + + if (fpsFraction.contains("/")) { + String[] parts = fpsFraction.split("/"); + double numerator = Float.parseFloat(parts[0]); + double denominator = Float.parseFloat(parts[1]); + if (denominator != 0) { + return (float) (numerator / denominator); + } + } else { + return Float.valueOf(fpsFraction); // Handle cases like "25" directly + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/ddf/vodsystem/services/media/ThumbnailService.java b/src/main/java/com/ddf/vodsystem/services/media/ThumbnailService.java new file mode 100644 index 0000000..37c977d --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/services/media/ThumbnailService.java @@ -0,0 +1,29 @@ +package com.ddf.vodsystem.services.media; + +import com.ddf.vodsystem.dto.CommandOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +@Service +public class ThumbnailService { + private static final Logger logger = LoggerFactory.getLogger(ThumbnailService.class); + + public CommandOutput createThumbnail(File inputFile, File outputFile, Float timeInVideo) throws IOException, InterruptedException { + logger.info("Creating thumbnail at {} seconds", timeInVideo); + + List command = List.of( + "ffmpeg", + "-ss", timeInVideo.toString(), + "-i", inputFile.getAbsolutePath(), + "-frames:v", "1", + outputFile.getAbsolutePath() + ); + + return CommandRunner.run(command); + } +}