refactor(media): consolidate upload/edit/download into MediaService
Merge UploadService, EditService, JobOrchestrationService, and
DownloadService into a single MediaService with a unified Job lifecycle
(READY → PROCESSING → SUCCEEDED/FAILED via JobState enum).
- Add MediaController (/api/v1/media) exposing compress, save, and clip
- Add JobController (/api/v1/job) for status polling and file download
- Move clip/thumbnail download into ClipController (/{id}/media, /{id}/thumbnail)
- Move stream saveSection/clip from StreamService into MediaService
- Simplify Job DTO: remove dual ClipOptions/files, add ProgressTracker and download file
- Refactor JobRegistryService.add() → generateJob() to co-locate creation and registration
- Wire FFmpeg out_time_ms progress parsing into StreamActionsService via CommandRunner
- Rename GET /api/v1/auth/user → GET /api/v1/user/me
- Delete DownloadController, MetadataController, UploadController
This commit is contained in:
@@ -5,6 +5,10 @@ import com.ddf.vodsystem.dto.APIResponse;
|
||||
import com.ddf.vodsystem.controllers.dto.ClipUpdateRequest;
|
||||
import com.ddf.vodsystem.entities.Clip;
|
||||
import com.ddf.vodsystem.services.ClipService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -16,6 +20,7 @@ import java.util.Optional;
|
||||
public class ClipController {
|
||||
private final ClipService clipService;
|
||||
private static final String SUCCESS = "success";
|
||||
private static final String FILENAME_HEADER = "inline; filename=\"%s\"";
|
||||
|
||||
public ClipController(ClipService clipService) {
|
||||
this.clipService = clipService;
|
||||
@@ -79,6 +84,34 @@ public class ClipController {
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/media")
|
||||
public ResponseEntity<Resource> downloadClip(@PathVariable Long id) {
|
||||
Resource resource = clipService.downloadClip(id);
|
||||
|
||||
if (resource == null || !resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/thumbnail")
|
||||
public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) {
|
||||
Resource resource = clipService.downloadThumbnail(id);
|
||||
|
||||
if (resource == null || !resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
private ClipResponse convertToDTO(Clip clip) {
|
||||
return new ClipResponse(
|
||||
clip.getId(),
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package com.ddf.vodsystem.controllers;
|
||||
|
||||
import com.ddf.vodsystem.services.DownloadService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/v1/download")
|
||||
public class DownloadController {
|
||||
private final DownloadService downloadService;
|
||||
private static final String FILENAME_HEADER = "inline; filename=\"%s\"";
|
||||
|
||||
@Autowired
|
||||
public DownloadController(DownloadService downloadService) {
|
||||
this.downloadService = downloadService;
|
||||
}
|
||||
|
||||
@GetMapping("/output/{uuid}")
|
||||
public ResponseEntity<Resource> downloadFile(@PathVariable String uuid) {
|
||||
Resource resource = downloadService.downloadOutput(uuid);
|
||||
|
||||
if (resource == null || !resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/input/{uuid}")
|
||||
public ResponseEntity<Resource> downloadInput(@PathVariable String uuid) {
|
||||
Resource resource = downloadService.downloadInput(uuid);
|
||||
|
||||
if (resource == null || !resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/clip/{id}")
|
||||
public ResponseEntity<Resource> 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, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/thumbnail/{id}")
|
||||
public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) {
|
||||
Resource resource = downloadService.downloadThumbnail(id);
|
||||
|
||||
if (resource == null || !resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(resource);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.ddf.vodsystem.controllers;
|
||||
|
||||
import com.ddf.vodsystem.dto.JobStatus;
|
||||
import com.ddf.vodsystem.dto.ClipOptions;
|
||||
import com.ddf.vodsystem.services.EditService;
|
||||
import com.ddf.vodsystem.dto.APIResponse;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/v1")
|
||||
public class EditController {
|
||||
private final EditService editService;
|
||||
private static final String SUCCESS = "success";
|
||||
|
||||
public EditController(EditService editService) {
|
||||
this.editService = editService;
|
||||
}
|
||||
|
||||
@PostMapping("edit/{uuid}")
|
||||
public ResponseEntity<APIResponse<Void>> edit(@PathVariable("uuid") String uuid, @ModelAttribute ClipOptions clipOptions) {
|
||||
editService.edit(uuid, clipOptions);
|
||||
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Editing started for UUID: " + uuid, null));
|
||||
}
|
||||
|
||||
@GetMapping("/process/{uuid}")
|
||||
public ResponseEntity<APIResponse<Void>> process(@PathVariable("uuid") String uuid) {
|
||||
editService.process(uuid);
|
||||
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Processing started for UUID: " + uuid, null));
|
||||
}
|
||||
|
||||
@GetMapping("/convert/{uuid}")
|
||||
public ResponseEntity<APIResponse<Void>> convert(@PathVariable("uuid") String uuid) {
|
||||
editService.convert(uuid);
|
||||
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Conversion started for UUID: " + uuid, null));
|
||||
}
|
||||
|
||||
@GetMapping("/progress/{uuid}")
|
||||
public ResponseEntity<APIResponse<JobStatus>> getStatus(@PathVariable("uuid") String uuid) {
|
||||
JobStatus status = editService.getStatus(uuid);
|
||||
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Status for UUID: " + uuid, status));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.ddf.vodsystem.controllers;
|
||||
|
||||
import com.ddf.vodsystem.controllers.dto.JobResponse;
|
||||
import com.ddf.vodsystem.dto.APIResponse;
|
||||
import com.ddf.vodsystem.dto.Job;
|
||||
import com.ddf.vodsystem.exceptions.NotReadyException;
|
||||
import com.ddf.vodsystem.services.JobRegistryService;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/v1/job")
|
||||
public class JobController {
|
||||
private final JobRegistryService jobRegistryService;
|
||||
private static final String SUCCESS = "success";
|
||||
private static final String FILENAME_HEADER = "inline; filename=\"%s\"";
|
||||
|
||||
public JobController(JobRegistryService jobRegistryService){
|
||||
this.jobRegistryService = jobRegistryService;
|
||||
}
|
||||
|
||||
@GetMapping("/{uuid}")
|
||||
public ResponseEntity<APIResponse<JobResponse>> getJob(@PathVariable String uuid) {
|
||||
Job job = jobRegistryService.getJob(uuid);
|
||||
JobResponse jobResponse = new JobResponse(
|
||||
job.getUuid(),
|
||||
job.getProgressTracker().getProgress(),
|
||||
job.getProgressTracker().isComplete(),
|
||||
job.getState(),
|
||||
job.getErrorOutput(),
|
||||
job.getCreatedAt()
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(new APIResponse<>(
|
||||
SUCCESS,
|
||||
"Job successfully retreived",
|
||||
jobResponse
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/{uuid}/download")
|
||||
public ResponseEntity<Resource> downloadJobFile(@PathVariable String uuid) {
|
||||
Job job = jobRegistryService.getJob(uuid);
|
||||
|
||||
if (job.getDownload() == null) {
|
||||
throw new NotReadyException("Job " + uuid + " not ready for download.");
|
||||
}
|
||||
|
||||
Resource resource = new FileSystemResource(job.getDownload());
|
||||
|
||||
if (!resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.ddf.vodsystem.controllers;
|
||||
|
||||
import com.ddf.vodsystem.controllers.dto.UUIDResponse;
|
||||
import com.ddf.vodsystem.dto.Job;
|
||||
import com.ddf.vodsystem.dto.ClipOptions;
|
||||
import com.ddf.vodsystem.dto.APIResponse;
|
||||
import com.ddf.vodsystem.services.MediaService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/media")
|
||||
public class MediaController {
|
||||
private final MediaService mediaService;
|
||||
private static final String SUCCESS = "success";
|
||||
|
||||
public MediaController(MediaService mediaService) {
|
||||
this.mediaService = mediaService;
|
||||
}
|
||||
|
||||
@PostMapping("/compress")
|
||||
public ResponseEntity<APIResponse<UUIDResponse>> compress(
|
||||
@RequestParam("file")MultipartFile file,
|
||||
@RequestParam("options") ClipOptions options
|
||||
) throws IOException, InterruptedException {
|
||||
Job job = mediaService.compress(file, options);
|
||||
return ResponseEntity.ok(new APIResponse<>(
|
||||
SUCCESS,
|
||||
"Compression successfully started",
|
||||
new UUIDResponse(job.getUuid())
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/save")
|
||||
public ResponseEntity<APIResponse<UUIDResponse>> save(
|
||||
@RequestParam("start") Instant startTime,
|
||||
@RequestParam("end") Instant endTime) throws IOException {
|
||||
Job job = mediaService.saveSection(startTime, endTime);
|
||||
return ResponseEntity.ok(new APIResponse<>(
|
||||
SUCCESS,
|
||||
"Section saving successfully started",
|
||||
new UUIDResponse(job.getUuid())
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/clip")
|
||||
public ResponseEntity<APIResponse<UUIDResponse>> clip(
|
||||
@RequestParam("duration") float duration) throws IOException {
|
||||
Job job = mediaService.clip(duration);
|
||||
return ResponseEntity.ok(new APIResponse<>(
|
||||
SUCCESS,
|
||||
"Clipping successfully started",
|
||||
new UUIDResponse(job.getUuid())
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.ddf.vodsystem.controllers;
|
||||
|
||||
import com.ddf.vodsystem.dto.ClipOptions;
|
||||
import com.ddf.vodsystem.dto.APIResponse;
|
||||
import com.ddf.vodsystem.services.JobRegistryService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/metadata")
|
||||
public class MetadataController {
|
||||
private final JobRegistryService jobRegistryService;
|
||||
|
||||
public MetadataController(
|
||||
JobRegistryService jobRegistryService) {
|
||||
this.jobRegistryService = jobRegistryService;
|
||||
}
|
||||
|
||||
@GetMapping("/original/{uuid}")
|
||||
public ResponseEntity<APIResponse<ClipOptions>> getMetadata(@PathVariable String uuid) {
|
||||
ClipOptions originalMetadata = jobRegistryService.getJob(uuid).getInputClipOptions();
|
||||
|
||||
if (originalMetadata == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(new APIResponse<>("error", "Original metadata not found", null));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.body(new APIResponse<>("success", "Original metadata retrieved", originalMetadata));
|
||||
}
|
||||
|
||||
@GetMapping("/converted/{uuid}")
|
||||
public ResponseEntity<APIResponse<ClipOptions>> getConvertedMetadata(@PathVariable String uuid) {
|
||||
ClipOptions convertedMetadata = jobRegistryService.getJob(uuid).getOutputClipOptions();
|
||||
|
||||
if (convertedMetadata == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(new APIResponse<>("error", "Converted metadata not found", null));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.body(new APIResponse<>("success", "Converted metadata retrieved", convertedMetadata));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.ddf.vodsystem.controllers;
|
||||
|
||||
import com.ddf.vodsystem.controllers.dto.ClipSectionRequest;
|
||||
import com.ddf.vodsystem.controllers.dto.StreamResponse;
|
||||
import com.ddf.vodsystem.dto.APIResponse;
|
||||
import com.ddf.vodsystem.controllers.dto.SaveSectionRequest;
|
||||
import com.ddf.vodsystem.entities.Stream;
|
||||
import com.ddf.vodsystem.services.StreamService;
|
||||
import org.slf4j.Logger;
|
||||
@@ -11,7 +9,6 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -49,20 +46,6 @@ public class StreamController {
|
||||
return ResponseEntity.ok(new APIResponse<>("success", "Heartbeat recorded", null));
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
public ResponseEntity<APIResponse<Void>> saveSection(
|
||||
@RequestBody SaveSectionRequest request) throws IOException, InterruptedException {
|
||||
streamService.saveSection(request.startTime(), request.endTime());
|
||||
return ResponseEntity.accepted().body(new APIResponse<>("success", "Section saved", null));
|
||||
}
|
||||
|
||||
@PostMapping("/clip")
|
||||
public ResponseEntity<APIResponse<Void>> saveClip(
|
||||
@RequestBody ClipSectionRequest request) throws IOException, InterruptedException {
|
||||
streamService.clip(request.duration());
|
||||
return ResponseEntity.accepted().body(new APIResponse<>("success", "Clip saved", null));
|
||||
}
|
||||
|
||||
@GetMapping("/history/{userId}")
|
||||
public ResponseEntity<APIResponse<List<Stream>>> getStreamHistory(@PathVariable Long userId) {
|
||||
List<Stream> history = streamService.getStreamHistory(userId);
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.ddf.vodsystem.controllers;
|
||||
|
||||
import com.ddf.vodsystem.dto.APIResponse;
|
||||
import com.ddf.vodsystem.services.UploadService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/v1/upload")
|
||||
public class UploadController {
|
||||
|
||||
private final UploadService uploadService;
|
||||
|
||||
@Autowired
|
||||
public UploadController(UploadService uploadService) {
|
||||
this.uploadService = uploadService;
|
||||
}
|
||||
|
||||
@PostMapping()
|
||||
public ResponseEntity<APIResponse<Object>> uploadVideo(@RequestParam("file") MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid file");
|
||||
}
|
||||
|
||||
String uuid = uploadService.upload(file);
|
||||
APIResponse<Object> response = new APIResponse<>("success", "File uploaded successfully", java.util.Collections.singletonMap("uuid", uuid));
|
||||
return new ResponseEntity<>(response, HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth/")
|
||||
@RequestMapping("/api/v1/user/")
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
|
||||
@@ -25,7 +25,7 @@ public class UserController {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/user")
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<APIResponse<User>> user() {
|
||||
Optional<User> user = userService.getLoggedInUser();
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ddf.vodsystem.controllers.dto;
|
||||
|
||||
import com.ddf.vodsystem.dto.JobState;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record JobResponse (
|
||||
String uuid,
|
||||
float progress,
|
||||
boolean isComplete,
|
||||
JobState state,
|
||||
String errorOutput,
|
||||
Instant createdAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.ddf.vodsystem.controllers.dto;
|
||||
|
||||
public record UUIDResponse(String uuid) {}
|
||||
Reference in New Issue
Block a user