chore(prod): sync with main #4

Merged
dylan merged 67 commits from master into prod 2026-06-25 23:28:52 +01:00
6 changed files with 123 additions and 63 deletions
Showing only changes of commit a6850d3d65 - Show all commits

View File

@@ -2,7 +2,8 @@ package com.ddf.vodsystem.controllers;
import com.ddf.vodsystem.dto.ClipOptions; import com.ddf.vodsystem.dto.ClipOptions;
import com.ddf.vodsystem.dto.APIResponse; import com.ddf.vodsystem.dto.APIResponse;
import com.ddf.vodsystem.services.JobService; import com.ddf.vodsystem.services.JobOrchestrationService;
import com.ddf.vodsystem.services.JobRegistryService;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -13,15 +14,16 @@ import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@RequestMapping("/api/v1/metadata") @RequestMapping("/api/v1/metadata")
public class MetadataController { public class MetadataController {
private final JobService jobService; private final JobRegistryService jobRegistryService;
public MetadataController(JobService jobService) { public MetadataController(
this.jobService = jobService; JobRegistryService jobRegistryService) {
this.jobRegistryService = jobRegistryService;
} }
@GetMapping("/original/{uuid}") @GetMapping("/original/{uuid}")
public ResponseEntity<APIResponse<ClipOptions>> getMetadata(@PathVariable String uuid) { public ResponseEntity<APIResponse<ClipOptions>> getMetadata(@PathVariable String uuid) {
ClipOptions originalMetadata = jobService.getJob(uuid).getInputClipOptions(); ClipOptions originalMetadata = jobRegistryService.getJob(uuid).getInputClipOptions();
if (originalMetadata == null) { if (originalMetadata == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND) return ResponseEntity.status(HttpStatus.NOT_FOUND)
@@ -34,7 +36,7 @@ public class MetadataController {
@GetMapping("/converted/{uuid}") @GetMapping("/converted/{uuid}")
public ResponseEntity<APIResponse<ClipOptions>> getConvertedMetadata(@PathVariable String uuid) { public ResponseEntity<APIResponse<ClipOptions>> getConvertedMetadata(@PathVariable String uuid) {
ClipOptions convertedMetadata = jobService.getJob(uuid).getOutputClipOptions(); ClipOptions convertedMetadata = jobRegistryService.getJob(uuid).getOutputClipOptions();
if (convertedMetadata == null) { if (convertedMetadata == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND) return ResponseEntity.status(HttpStatus.NOT_FOUND)

View File

@@ -15,27 +15,27 @@ import java.io.File;
@Service @Service
public class DownloadService { public class DownloadService {
private final JobService jobService; private final JobRegistryService jobRegistryService;
private final ClipRepository clipRepository; private final ClipRepository clipRepository;
private final ClipService clipService; private final ClipService clipService;
public DownloadService(JobService jobService, public DownloadService(JobRegistryService jobRegistryService,
ClipRepository clipRepository, ClipRepository clipRepository,
ClipService clipService) { ClipService clipService) {
this.jobService = jobService; this.jobRegistryService = jobRegistryService;
this.clipRepository = clipRepository; this.clipRepository = clipRepository;
this.clipService = clipService; this.clipService = clipService;
} }
public Resource downloadInput(String uuid) { public Resource downloadInput(String uuid) {
Job job = jobService.getJob(uuid); Job job = jobRegistryService.getJob(uuid);
File file = job.getInputFile(); File file = job.getInputFile();
return new FileSystemResource(file); return new FileSystemResource(file);
} }
public Resource downloadOutput(String uuid) { public Resource downloadOutput(String uuid) {
Job job = jobService.getJob(uuid); Job job = jobRegistryService.getJob(uuid);
if (!job.getStatus().getProcess().isComplete()) { if (!job.getStatus().getProcess().isComplete()) {
throw new JobNotFinished("Job is not finished"); throw new JobNotFinished("Job is not finished");

View File

@@ -8,17 +8,22 @@ import org.springframework.stereotype.Service;
@Service @Service
public class EditService { public class EditService {
private final JobService jobService;
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(EditService.class); private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(EditService.class);
private final MetadataService metadataService; private final MetadataService metadataService;
private final JobRegistryService jobRegistryService;
private final JobOrchestrationService jobOrchestrationService;
public EditService(JobService jobService, MetadataService metadataService) { public EditService(JobOrchestrationService jobOrchestrationService,
this.jobService = jobService; MetadataService metadataService,
JobRegistryService jobRegistryService) {
this.metadataService = metadataService; this.metadataService = metadataService;
this.jobRegistryService = jobRegistryService;
this.jobOrchestrationService = jobOrchestrationService;
} }
public void edit(String uuid, ClipOptions clipOptions) { public void edit(String uuid, ClipOptions clipOptions) {
Job job = jobService.getJob(uuid); Job job = jobRegistryService.getJob(uuid);
metadataService.validateMetadata(job.getInputClipOptions(), clipOptions); metadataService.validateMetadata(job.getInputClipOptions(), clipOptions);
job.setOutputClipOptions(clipOptions); job.setOutputClipOptions(clipOptions);
@@ -26,21 +31,21 @@ public class EditService {
} }
public void process(String uuid) { public void process(String uuid) {
Job job = jobService.getJob(uuid); Job job = jobRegistryService.getJob(uuid);
jobService.processJob(job); jobOrchestrationService.processJob(job);
logger.info("Job {} - Started processing", uuid); logger.info("Job {} - Started processing", uuid);
} }
public void convert(String uuid) { public void convert(String uuid) {
Job job = jobService.getJob(uuid); Job job = jobRegistryService.getJob(uuid);
jobService.convertJob(job); jobOrchestrationService.convertJob(job);
logger.info("Job {} - Started converting", uuid); logger.info("Job {} - Started converting", uuid);
} }
public JobStatus getStatus(String uuid) { public JobStatus getStatus(String uuid) {
Job job = jobService.getJob(uuid); Job job = jobRegistryService.getJob(uuid);
return job.getStatus(); return job.getStatus();
} }
} }

View File

@@ -17,16 +17,20 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ddf.vodsystem.exceptions.JobNotFound;
/** /**
* Service for managing and processing jobs in a background thread. * Orchestrates the processing pipeline for a {@link Job}: converting (remuxing)
* Uses a blocking queue to avoid busy waiting and ensures jobs are processed sequentially. * the input file and, separately, compressing/persisting the output clip.
* <p>
* Job storage and lookup are handled by {@link JobRegistryService}; this class
* is only responsible for acting on a {@link Job} instance once it's been retrieved.
* Long-running steps (remux, compress) are kicked off asynchronously via
* {@link java.util.concurrent.CompletableFuture}-returning service calls rather
* than blocked on, so callers should not assume the job is finished when these
* methods return.
*/ */
@Service @Service
public class JobService { public class JobOrchestrationService {
private static final Logger logger = LoggerFactory.getLogger(JobService.class); private static final Logger logger = LoggerFactory.getLogger(JobOrchestrationService.class);
private final ConcurrentHashMap<String, Job> jobs = new ConcurrentHashMap<>();
private final ClipService clipService; private final ClipService clipService;
private final RemuxService remuxService; private final RemuxService remuxService;
private final DirectoryService directoryService; private final DirectoryService directoryService;
@@ -34,14 +38,19 @@ public class JobService {
private final UserService userService; private final UserService userService;
/** /**
* Constructs a JobService with the given CompressionService. * Constructs the orchestration service with its collaborating services.
* @param clipService the compression service to use for processing jobs *
* @param clipService used to persist a completed clip's metadata once processing succeeds
* @param remuxService performs the remux step during conversion
* @param compressionService performs the compression step during processing
* @param directoryService used for filesystem operations (e.g. copying the input file to a temp location)
* @param userService used to resolve the currently logged-in user when persisting a clip
*/ */
public JobService(ClipService clipService, public JobOrchestrationService(ClipService clipService,
RemuxService remuxService, RemuxService remuxService,
CompressionService compressionService, CompressionService compressionService,
DirectoryService directoryService, DirectoryService directoryService,
UserService userService) { UserService userService) {
this.clipService = clipService; this.clipService = clipService;
this.remuxService = remuxService; this.remuxService = remuxService;
this.directoryService = directoryService; this.directoryService = directoryService;
@@ -50,30 +59,19 @@ public class JobService {
} }
/** /**
* Adds a new job to the job map. * Converts a job's input file in place by remuxing it.
* @param job the job to add * <p>
* The original input file is first copied to a temporary {@code .temp} file so the
* remux can read from a stable source; on successful completion the temp file is
* deleted. The remux itself runs asynchronously — this method returns once the
* remux has been kicked off, not once it has finished. Progress/completion is
* tracked via {@code job.getStatus().getConversion()}, which is reset at the
* start of this call and marked complete in the remux's completion callback.
*
* @param job the job whose input file should be converted
* @throws StorageException if the temporary file cannot be created, or if it
* cannot be deleted after a successful remux
*/ */
public void add(Job job) {
logger.info("Added job: {}", job.getUuid());
jobs.put(job.getUuid(), job);
}
/**
* Retrieves a job by its UUID.
* @param uuid the UUID of the job
* @return the Job object
* @throws JobNotFound if the job does not exist
*/
public Job getJob(String uuid) {
Job job = jobs.get(uuid);
if (job == null) {
throw new JobNotFound("Job not found");
}
return job;
}
public void convertJob(Job job) { public void convertJob(Job job) {
logger.info("Converting job: {}", job.getUuid()); logger.info("Converting job: {}", job.getUuid());
File tempFile = new File(job.getInputFile().getAbsolutePath() + ".temp"); File tempFile = new File(job.getInputFile().getAbsolutePath() + ".temp");
@@ -115,8 +113,16 @@ public class JobService {
} }
/** /**
* Marks a job as ready and adds it to the processing queue. * Compresses a job's output file according to its output clip options and, on
* @param job The job to process * success, persists the resulting clip against the currently logged-in user (if any).
* <p>
* Compression runs asynchronously — this method returns once compression has been
* kicked off, not once it has finished. Progress is tracked via
* {@code job.getStatus().getProcess()}, which is reset at the start of this call.
* If compression or persistence fails, the job's status is marked failed with the
* triggering exception's message.
*
* @param job the job to process
*/ */
public void processJob(Job job) { public void processJob(Job job) {
logger.info("Job ready: {}", job.getUuid()); logger.info("Job ready: {}", job.getUuid());

View File

@@ -0,0 +1,47 @@
package com.ddf.vodsystem.services;
import com.ddf.vodsystem.dto.Job;
import com.ddf.vodsystem.exceptions.JobNotFound;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
/**
* In-memory store for {@link Job} instances, keyed by job UUID.
* Provides add/lookup only — actual job processing is handled by
* {@link JobOrchestrationService}.
*/
@Service
public class JobRegistryService {
private static final Logger logger = LoggerFactory.getLogger(JobRegistryService.class);
private final ConcurrentHashMap<String, Job> jobs = new ConcurrentHashMap<>();
/**
* Adds a new job to the job map.
*
* @param job the job to add
*/
public void add(Job job) {
logger.info("Added job: {}", job.getUuid());
jobs.put(job.getUuid(), job);
}
/**
* Retrieves a job by its UUID.
*
* @param uuid the UUID of the job
* @return the Job object
* @throws JobNotFound if the job does not exist
*/
public Job getJob(String uuid) {
Job job = jobs.get(uuid);
if (job == null) {
throw new JobNotFound("Job not found");
}
return job;
}
}

View File

@@ -24,14 +24,14 @@ import org.slf4j.LoggerFactory;
public class UploadService { public class UploadService {
private static final Logger logger = LoggerFactory.getLogger(UploadService.class); private static final Logger logger = LoggerFactory.getLogger(UploadService.class);
private final JobService jobService; private final JobRegistryService jobRegistryService;
private final MetadataService metadataService; private final MetadataService metadataService;
private final DirectoryService directoryService; private final DirectoryService directoryService;
public UploadService(JobService jobService, public UploadService(JobRegistryService jobRegistryService,
MetadataService metadataService, MetadataService metadataService,
DirectoryService directoryService) { DirectoryService directoryService) {
this.jobService = jobService; this.jobRegistryService = jobRegistryService;
this.metadataService = metadataService; this.metadataService = metadataService;
this.directoryService = directoryService; this.directoryService = directoryService;
} }
@@ -54,7 +54,7 @@ public class UploadService {
ClipOptions clipOptions = getMetadataWithTimeout(inputFile); ClipOptions clipOptions = getMetadataWithTimeout(inputFile);
Job job = new Job(uuid, inputFile, outputFile, clipOptions); Job job = new Job(uuid, inputFile, outputFile, clipOptions);
jobService.add(job); jobRegistryService.add(job);
return uuid; return uuid;
} }